diff -Nru ubuntu-image-1.4+18.04ubuntu1/debian/changelog ubuntu-image-1.4+18.04ubuntu2/debian/changelog --- ubuntu-image-1.4+18.04ubuntu1/debian/changelog 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/debian/changelog 2018-08-21 13:25:59.000000000 +0000 @@ -1,3 +1,14 @@ +ubuntu-image (1.4+18.04ubuntu2) bionic; urgency=medium + + * Demote the qemu-user-static dependency to Suggests, modify the code to + gracefully handle the lack of qemu binaries for cross-compilation. + (LP:1788177) + * Add missing information about UBUNTU_IMAGE_QEMU_USER_STATIC_PATH to the + manpage. + * SRU tracking number LP: #1786229 + + -- Ɓukasz 'sil2100' Zemczak Tue, 21 Aug 2018 15:25:59 +0200 + ubuntu-image (1.4+18.04ubuntu1) bionic; urgency=medium * Add support for classic cross-compilation, add qemu-user-static as a diff -Nru ubuntu-image-1.4+18.04ubuntu1/debian/control ubuntu-image-1.4+18.04ubuntu2/debian/control --- ubuntu-image-1.4+18.04ubuntu1/debian/control 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/debian/control 2018-08-21 13:25:59.000000000 +0000 @@ -52,9 +52,9 @@ python3-voluptuous, python3-yaml, snapd, - qemu-user-static, ${misc:Depends}, ${python3:Depends}, +Suggests: qemu-user-static, Description: toolkit for building Ubuntu images . This package contains the ubuntu-image library for Python 3. diff -Nru ubuntu-image-1.4+18.04ubuntu1/snapcraft.yaml ubuntu-image-1.4+18.04ubuntu2/snapcraft.yaml --- ubuntu-image-1.4+18.04ubuntu1/snapcraft.yaml 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/snapcraft.yaml 2018-08-21 13:25:59.000000000 +0000 @@ -2,7 +2,7 @@ summary: Create Ubuntu images description: | Use this tool to create Ubuntu images. -version: 1.4+snap1 +version: 1.4+snap4 grade: stable confinement: classic @@ -31,7 +31,6 @@ - dosfstools - e2fsprogs - fakeroot - - libc6 - mtools - python3-debian - python3-git diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/classic_builder.py ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/classic_builder.py --- ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/classic_builder.py 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/classic_builder.py 2018-08-21 13:25:59.000000000 +0000 @@ -9,7 +9,7 @@ from tempfile import gettempdir from ubuntu_image.common_builder import AbstractImageBuilderState from ubuntu_image.helpers import ( - check_root_privilege, live_build, run) + check_root_privilege, get_host_arch, live_build, run) from ubuntu_image.parser import ( BootLoader, FileSystemType, StructureRole) @@ -67,6 +67,9 @@ env['EXTRA_PPAS'] = self.args.extra_ppas # Only genereate a single rootfs tree for classic image creation. env['IMAGEFORMAT'] = 'none' + # ensure ARCH is set + if self.args.arch is None: + env['ARCH'] = get_host_arch() live_build(self.unpackdir, env) except CalledProcessError: if self.args.debug: diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/helpers.py ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/helpers.py --- ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/helpers.py 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/helpers.py 2018-08-21 13:25:59.000000000 +0000 @@ -175,6 +175,11 @@ # Try to guess the qemu-user-static binary name and find it. static = get_qemu_static_for_arch(arch) qemu_path = find_executable(static) + if qemu_path is None: + raise DependencyError( + static, + 'Use UBUNTU_IMAGE_QEMU_USER_STATIC_PATH in case of ' + 'non-standard archs or custom paths.') config_cmd.extend(['--bootstrap-qemu-arch', arch, '--bootstrap-qemu-static', qemu_path, '--architectures', arch]) @@ -273,3 +278,11 @@ def __init__(self, user_name): self.user_name = user_name + + +class DependencyError(ExpectedError): + """An optional dependency is missing.""" + + def __init__(self, name, additional_info=''): + self.name = name + self.additional_info = additional_info diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/__main__.py ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/__main__.py --- ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/__main__.py 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/__main__.py 2018-08-21 13:25:59.000000000 +0000 @@ -11,7 +11,7 @@ from ubuntu_image.assertion_builder import ModelAssertionBuilder from ubuntu_image.classic_builder import ClassicBuilder from ubuntu_image.helpers import ( - DoesNotFit, PrivilegeError, as_size, get_host_arch, + DependencyError, DoesNotFit, PrivilegeError, as_size, get_host_distro) from ubuntu_image.hooks import HookError from ubuntu_image.i18n import _ @@ -254,7 +254,7 @@ help=_("""Distribution name to be specified to livecd-rootfs.""")) classic_cmd.add_argument( '-a', '--arch', - default=get_host_arch(), metavar='CPU-ARCHITECTURE', + default=None, metavar='CPU-ARCHITECTURE', help=_("""CPU architecture to be specified to livecd-rootfs. default value is builder arch.""")) classic_cmd.add_argument( @@ -361,6 +361,10 @@ ' classic image. Please run ubuntu-image as root.' .format(error.user_name)) return 1 + except DependencyError as error: + _logger.error('Required dependency {} seems to be missing. {}'.format( + error.name, error.additional_info)) + return 1 except: # noqa: E722 _logger.exception('Crash in state machine') return 1 diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/testing/helpers.py ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/testing/helpers.py --- ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/testing/helpers.py 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/testing/helpers.py 2018-08-21 13:25:59.000000000 +0000 @@ -91,6 +91,19 @@ pass +class CallLBLeaveATraceClassicBuilder(XXXClassicBuilder): + def prepare_gadget_tree(self): + # We're skipping the gadget tree build as we're leaving early and will + # not use it for the tests. + self._next.append(self.prepare_image) + + def load_gadget_yaml(self): + # This time we want to call prepare_image for the live-build call but + # then finish after leaving a trace + with open(os.path.join(self.workdir, 'success'), 'w'): + pass + + class LogCapture: def __init__(self): self.logs = [] diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/tests/test_helpers.py ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/tests/test_helpers.py --- ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/tests/test_helpers.py 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/tests/test_helpers.py 2018-08-21 13:25:59.000000000 +0000 @@ -13,7 +13,7 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory from types import SimpleNamespace from ubuntu_image.helpers import ( - GiB, MiB, PrivilegeError, as_bool, as_size, + DependencyError, GiB, MiB, PrivilegeError, as_bool, as_size, check_root_privilege, get_host_arch, get_host_distro, get_qemu_static_for_arch, live_build, mkfs_ext4, run, snap, sparse_copy) @@ -433,6 +433,30 @@ 'PROJECT=ubuntu-server', 'SUITE=xenial', 'ARCH=armhf', 'lb', 'build']) + def test_live_build_cross_build_no_static(self): + with ExitStack() as resources: + tmpdir = resources.enter_context(TemporaryDirectory()) + root_dir = os.path.join(tmpdir, 'root_dir') + mock = LiveBuildMocker(root_dir) + resources.enter_context(LogCapture()) + resources.enter_context( + patch('ubuntu_image.helpers.run', mock.run)) + resources.enter_context( + patch('ubuntu_image.helpers.get_host_arch', + return_value='amd64')) + resources.enter_context( + patch('ubuntu_image.helpers.find_executable', + return_value=None)) + env = OrderedDict() + env['PROJECT'] = 'ubuntu-server' + env['SUITE'] = 'xenial' + env['ARCH'] = 'armhf' + with self.assertRaises(DependencyError) as cm: + live_build(root_dir, env) + self.assertEqual(len(mock.call_args_list), 1) + self.assertEqual( + cm.exception.name, 'qemu-arm-static') + def test_live_build_no_cross_build(self): with ExitStack() as resources: tmpdir = resources.enter_context(TemporaryDirectory()) diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/tests/test_main.py ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/tests/test_main.py --- ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/tests/test_main.py 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/tests/test_main.py 2018-08-21 13:25:59.000000000 +0000 @@ -16,10 +16,10 @@ from ubuntu_image.helpers import GiB, MiB from ubuntu_image.hooks import supported_hooks from ubuntu_image.testing.helpers import ( - CrashingModelAssertionBuilder, DoNothingBuilder, - EarlyExitLeaveATraceAssertionBuilder, EarlyExitLeaveATraceClassicBuilder, - EarlyExitModelAssertionBuilder, LogCapture, XXXModelAssertionBuilder, - envar) + CallLBLeaveATraceClassicBuilder, CrashingModelAssertionBuilder, + DoNothingBuilder, EarlyExitLeaveATraceAssertionBuilder, + EarlyExitLeaveATraceClassicBuilder, EarlyExitModelAssertionBuilder, + LogCapture, XXXModelAssertionBuilder, envar) from ubuntu_image.testing.nose import NosePlugin from unittest import TestCase, skipIf from unittest.mock import call, patch @@ -615,6 +615,45 @@ call('Current user(test) does not have root privilege to build ' 'classic image. Please run ubuntu-image as root.')) + def test_classic_cross_build_no_static(self): + # We need to check that a DependencyError is raised when + # find_executable does not find the qemu--static binary in + # PATH (and no path env is set) + workdir = self._resources.enter_context(TemporaryDirectory()) + livecd_rootfs = self._resources.enter_context(TemporaryDirectory()) + auto = os.path.join(livecd_rootfs, 'auto') + os.mkdir(auto) + self._resources.enter_context(patch( + 'ubuntu_image.__main__.ClassicBuilder', + CallLBLeaveATraceClassicBuilder)) + self._resources.enter_context( + envar('UBUNTU_IMAGE_LIVECD_ROOTFS_AUTO_PATH', auto)) + self._resources.enter_context( + patch('ubuntu_image.helpers.run', return_value=None)) + self._resources.enter_context( + patch('ubuntu_image.helpers.find_executable', return_value=None)) + self._resources.enter_context( + patch('ubuntu_image.helpers.get_host_arch', + return_value='amd64')) + self._resources.enter_context( + patch('ubuntu_image.__main__.get_host_distro', + return_value='bionic')) + self._resources.enter_context( + patch('ubuntu_image.classic_builder.check_root_privilege', + return_value=None)) + mock = self._resources.enter_context(patch( + 'ubuntu_image.__main__._logger.error')) + code = main(('classic', '--workdir', workdir, + '--project', 'ubuntu-cpc', '--arch', 'armhf', + self.classic_gadget_tree)) + self.assertEqual(code, 1) + self.assertFalse(os.path.exists(os.path.join(workdir, 'success'))) + self.assertEqual( + mock.call_args_list[-1], + call('Required dependency qemu-arm-static seems to be missing. ' + 'Use UBUNTU_IMAGE_QEMU_USER_STATIC_PATH in case of ' + 'non-standard archs or custom paths.')) + def test_hook_fired(self): # For the purpose of testing, we will be using the post-populate-rootfs # hook as we made sure it's still executed as part of of the diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/version.txt ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/version.txt --- ubuntu-image-1.4+18.04ubuntu1/ubuntu_image/version.txt 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu_image/version.txt 2018-08-21 13:25:59.000000000 +0000 @@ -1 +1 @@ -1.4+18.04ubuntu1 +1.4+18.04ubuntu2 diff -Nru ubuntu-image-1.4+18.04ubuntu1/ubuntu-image.rst ubuntu-image-1.4+18.04ubuntu2/ubuntu-image.rst --- ubuntu-image-1.4+18.04ubuntu1/ubuntu-image.rst 2018-07-20 14:43:51.000000000 +0000 +++ ubuntu-image-1.4+18.04ubuntu2/ubuntu-image.rst 2018-08-21 13:25:59.000000000 +0000 @@ -266,6 +266,13 @@ Otherwise it will attempt to localize ``livecd-rootfs`` through a call to ``dpkg``. +``UBUNTU_IMAGE_QEMU_USER_STATIC_PATH`` + In case of classic image cross-compilation for a different architecture, + ``ubuntu-image`` will attempt to use the qemu-user-static emulator with + ``live-build``. If set, ``ubuntu-image`` will use the selected path for + the cross-compilation. Otherwise it will attempt to find a matching + emulator binary in the current ``$PATH``. + There are a few other environment variables used for building and testing only.