From feb574a913df677bb5b0a43bfdeedbeb4eaa6e3d Mon Sep 17 00:00:00 2001 From: David Cassany Date: Fri, 28 Jun 2019 11:26:37 -0400 Subject: [PATCH] Adding support for Pacman package manager This commit adds support for pacman package manager and, in consequence, support for Arch Linux images. It also adds the package definition for Arch Linux. --- Makefile | 4 + kiwi/defaults.py | 3 + kiwi/package_manager/__init__.py | 3 + kiwi/package_manager/pacman.py | 244 ++++++++++++++++++++++ kiwi/repository/__init__.py | 3 + kiwi/repository/pacman.py | 247 +++++++++++++++++++++++ kiwi/schema/kiwi.rnc | 2 +- kiwi/schema/kiwi.rng | 1 + kiwi/system/setup.py | 16 ++ package/python-kiwi-pkgbuild-template | 69 +++++++ test/unit/iso_tools/base_test.py | 36 ++++ test/unit/iso_tools/xorriso_test.py | 1 + test/unit/package_manager/init_test.py | 6 + test/unit/package_manager/pacman_test.py | 137 +++++++++++++ test/unit/repository/init_test.py | 6 + test/unit/repository/pacman_test.py | 191 ++++++++++++++++++ test/unit/system/setup_test.py | 18 ++ 17 files changed, 986 insertions(+), 1 deletion(-) create mode 100644 kiwi/package_manager/pacman.py create mode 100644 kiwi/repository/pacman.py create mode 100644 package/python-kiwi-pkgbuild-template create mode 100644 test/unit/package_manager/pacman_test.py create mode 100644 test/unit/repository/pacman_test.py diff --git a/Makefile b/Makefile index 823c1e6c..ef224bad 100644 --- a/Makefile +++ b/Makefile @@ -132,6 +132,10 @@ build: clean tox # update package version in spec file cat package/python-kiwi-spec-template | sed -e s'@%%VERSION@${version}@' \ > dist/python-kiwi.spec + # update package version in PKGBUILD file + md5sums=$$(md5sum dist/python-kiwi.tar.gz | cut -d" " -f1); \ + cat package/python-kiwi-pkgbuild-template | sed -e s'@%%VERSION@${version}@' \ + -e s"@%%MD5SUM@$${md5sums}@" > dist/PKGBUILD # provide rpm rpmlintrc cp package/python-kiwi-rpmlintrc dist diff --git a/kiwi/defaults.py b/kiwi/defaults.py index d597e3bc..616a5b87 100644 --- a/kiwi/defaults.py +++ b/kiwi/defaults.py @@ -672,6 +672,7 @@ class Defaults: """ return [ '/usr/share/syslinux', + '/usr/lib/syslinux/bios', '/usr/lib/syslinux/modules/bios', '/usr/lib/ISOLINUX' ] @@ -1470,6 +1471,8 @@ class Defaults: return 'rpm' elif package_manager in deb_based: return 'dpkg' + elif package_manager == 'pacman': + return 'pacman' def get(self, key): """ diff --git a/kiwi/package_manager/__init__.py b/kiwi/package_manager/__init__.py index 2a1576de..4604ff88 100644 --- a/kiwi/package_manager/__init__.py +++ b/kiwi/package_manager/__init__.py @@ -21,6 +21,7 @@ import logging from kiwi.package_manager.zypper import PackageManagerZypper from kiwi.package_manager.apt import PackageManagerApt from kiwi.package_manager.dnf import PackageManagerDnf +from kiwi.package_manager.pacman import PackageManagerPacman from kiwi.exceptions import ( KiwiPackageManagerSetupError @@ -50,6 +51,8 @@ class PackageManager: manager = PackageManagerDnf(repository, custom_args) elif package_manager == 'apt-get': manager = PackageManagerApt(repository, custom_args) + elif package_manager == 'pacman': + manager = PackageManagerPacman(repository, custom_args) else: raise KiwiPackageManagerSetupError( 'Support for package manager %s not implemented' % diff --git a/kiwi/package_manager/pacman.py b/kiwi/package_manager/pacman.py new file mode 100644 index 00000000..3fc82a2f --- /dev/null +++ b/kiwi/package_manager/pacman.py @@ -0,0 +1,244 @@ +# Copyright (c) 2019 SUSE Linux GmbH. All rights reserved. +# +# This file is part of kiwi. +# +# kiwi is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kiwi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kiwi. If not, see +# +import re +import logging + +# project +from kiwi.command import Command +from kiwi.package_manager.base import PackageManagerBase +from kiwi.exceptions import KiwiRequestError +from kiwi.path import Path + +log = logging.getLogger('kiwi') + + +class PackageManagerPacman(PackageManagerBase): + """ + **Implements base class for installation/deletion of + packages and collections using pacman** + + :param list pacman_args: pacman arguments from repository runtime + configuration + :param dict command_env: pacman command environment from repository + runtime configuration + """ + def post_init(self, custom_args=None): + """ + Post initialization method + + Store custom pacman arguments + + :param list custom_args: custom pacman arguments + """ + self.custom_args = custom_args + if not custom_args: + self.custom_args = [] + + runtime_config = self.repository.runtime_config() + self.pacman_args = runtime_config['pacman_args'] + self.command_env = runtime_config['command_env'] + + def request_package(self, name): + """ + Queue a package request + + :param str name: package name + """ + self.package_requests.append(name) + + def request_collection(self, name): + """ + Queue a collection request + + Pacman does not distinguish the installation of pacakges + and collections. Because of that collections are listed together + with package requests and a warning message is shown. + :param str name: pacman group name + """ + log.warning(( + 'Pacman treats collection installations as regular packages.' + 'It is preferred to list them as regular packages' + )) + self.package_requests.append(name) + + def request_product(self, name): + """ + Queue a product request + + There is no product definition in the Arch Linux repo data + + :param str name: unused + """ + pass + + def request_package_exclusion(self, name): + """ + Queue a package exclusion(skip) request + + :param str name: package name + """ + self.exclude_requests.append(name) + + def process_install_requests_bootstrap(self): + """ + Process package install requests for bootstrap phase (no chroot) + + :return: process results in command type + + :rtype: namedtuple + """ + Command.run( + ['pacman'] + self.pacman_args + [ + '--root', self.root_dir, '-Sy' + ] + ) + bash_command = [ + 'pacman' + ] + self.pacman_args + [ + '--root', self.root_dir + ] + self.custom_args + [ + '-S', '--needed', + '--overwrite', '{}/var/run'.format(self.root_dir) + ] + self.package_requests + self.cleanup_requests() + return Command.call( + ['bash', '-c', ' '.join(bash_command)], self.command_env + ) + + def process_install_requests(self): + """ + Process package install requests for image phase (chroot) + + :return: process results in command type + + :rtype: namedtuple + """ + chroot_pacman_args = Path.move_to_root(self.root_dir, self.pacman_args) + bash_command = [ + 'chroot', self.root_dir, 'pacman' + ] + chroot_pacman_args + self.custom_args + [ + '-S', '--needed' + ] + self.package_requests + self.cleanup_requests() + return Command.call( + ['bash', '-c', ' '.join(bash_command)], self.command_env + ) + + def process_delete_requests(self, force=False): + """ + Process package delete requests (chroot) + + :param bool force: force deletion: true|false + + :raises KiwiRequestError: if none of the packages to delete is + installed + :return: process results in command type + + :rtype: namedtuple + """ + delete_items = [] + for delete_item in self.package_requests: + try: + Command.run(['chroot', self.root_dir, 'pacman', '-Qi', delete_item]) + delete_items.append(delete_item) + except Exception: + # ignore packages which are not installed + pass + if not delete_items: + raise KiwiRequestError( + 'None of the requested packages to delete are installed' + ) + self.cleanup_requests() + chroot_pacman_args = Path.move_to_root(self.root_dir, self.pacman_args) + return Command.call( + [ + 'chroot', self.root_dir, 'pacman' + ] + chroot_pacman_args + self.custom_args + [ + '-Rdd' if force else '-Rs' + ] + delete_items, + self.command_env + ) + + def update(self): + """ + Process package update requests (chroot) + + :return: process results in command type + + :rtype: namedtuple + """ + chroot_pacman_args = Path.move_to_root(self.root_dir, self.pacman_args) + return Command.call( + [ + 'chroot', self.root_dir, 'pacman' + ] + chroot_pacman_args + self.custom_args + [ + '-Su' + ], + self.command_env + ) + + def process_only_required(self): + """ + Setup package processing only for required packages. + + This is the only option in pacman, nothing to do here. + """ + pass + + def process_plus_recommended(self): + """ + Setup package processing to also include recommended dependencies. + + There is no such concept of recommended dependencies in pacman. + """ + pass + + def match_package_installed(self, package_name, pacman_output): + """ + Match expression to indicate a package has been installed + + This match for the package to be installed in the output + of the pacman command is not 100% accurate. There might + be false positives due to sub package names starting with + the same base package name + + :param list package_list: list of all packages + :param str log_line: dnf status line + + :returns: match or None if there isn't any match + + :rtype: match object, None + """ + return re.match( + '.* installing ' + re.escape(package_name) + '.*', pacman_output + ) + + def match_package_deleted(self, package_name, pacman_output): + """ + Match expression to indicate a package has been deleted + + :param list package_list: list of all packages + :param str log_line: pacman status line + + :returns: match or None if there isn't any match + + :rtype: match object, None + """ + return re.match( + '.* removing ' + re.escape(package_name) + '.*', pacman_output + ) diff --git a/kiwi/repository/__init__.py b/kiwi/repository/__init__.py index decf9493..30b41ddc 100644 --- a/kiwi/repository/__init__.py +++ b/kiwi/repository/__init__.py @@ -19,6 +19,7 @@ from kiwi.repository.zypper import RepositoryZypper from kiwi.repository.apt import RepositoryApt from kiwi.repository.dnf import RepositoryDnf +from kiwi.repository.pacman import RepositoryPacman from kiwi.exceptions import ( KiwiRepositorySetupError @@ -43,6 +44,8 @@ class Repository: return RepositoryDnf(root_bind, custom_args) elif package_manager == 'apt-get': return RepositoryApt(root_bind, custom_args) + elif package_manager == 'pacman': + return RepositoryPacman(root_bind, custom_args) else: raise KiwiRepositorySetupError( 'Support for %s repository manager not implemented' % diff --git a/kiwi/repository/pacman.py b/kiwi/repository/pacman.py new file mode 100644 index 00000000..0cfe3d3d --- /dev/null +++ b/kiwi/repository/pacman.py @@ -0,0 +1,247 @@ +# Copyright (c) 2019 SUSE Linux GmbH. All rights reserved. +# +# This file is part of kiwi. +# +# kiwi is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kiwi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kiwi. If not, see +# +import os +from configparser import ConfigParser +from tempfile import NamedTemporaryFile + +# project +from kiwi.repository.base import RepositoryBase +from kiwi.path import Path +from kiwi.command import Command + + +class RepositoryPacman(RepositoryBase): + """ + **Implements repo handling for pacman package manager** + + :param str shared_pacman_dir: shared directory between image root + and build system root + :param str runtime_pacman_config_file: pacman runtime config file name + :param list pacman_args: zypper caller args plus additional custom args + :param dict command_env: customized os.environ for zypper + """ + def post_init(self, custom_args=None): + """ + Post initialization method + + Store custom pacman arguments and create runtime configuration + and environment + + :param list custom_args: pacman arguments + """ + self.custom_args = custom_args + self.check_signatures = False + self.repo_names = [] + if not custom_args: + self.custom_args = [] + + self.runtime_pacman_config_file = NamedTemporaryFile( + dir=self.root_dir + ) + + if 'check_signatures' in self.custom_args: + self.custom_args.remove('check_signatures') + self.check_signatures = True + + manager_base = self.shared_location + '/pacman' + + self.shared_pacman_dir = { + 'cache-dir': manager_base + '/cache', + 'repos-dir': manager_base + '/repos' + } + Path.create(self.shared_pacman_dir['repos-dir']) + + self.pacman_args = [ + '--config', self.runtime_pacman_config_file.name, + '--noconfirm' + ] + + self._write_runtime_config() + + def use_default_location(self): + """ + Setup pacman repository operations to store all data + in the default places + """ + self.shared_pacman_dir['repos-dir'] = \ + self.root_dir + '/etc/pacman.d' + + def runtime_config(self): + """ + pacman runtime configuration and environment + """ + return { + 'pacman_args': self.pacman_args, + 'command_env': os.environ + } + + def _add_repo_section( + self, config_parser, name, uri, repo_gpgcheck=None, pkg_gpgcheck=None + ): + config_parser.add_section(name) + if uri.startswith(os.sep) and os.path.exists(uri): + uri = 'file://{}'.format(uri) + config_parser.set(name, 'Server', uri) + config_parser.set( + name, 'SigLevel', '{0} {1}'.format( + 'Required' if pkg_gpgcheck else 'Never', + 'DatabaseRequired' if repo_gpgcheck else 'DatabaseNever' + ) + ) + + def add_repo( + self, name, uri, repo_type=None, + prio=None, dist=None, components=None, + user=None, secret=None, credentials_file=None, + repo_gpgcheck=None, pkg_gpgcheck=None, + sourcetype=None + ): + """ + Add pacman repository + + :param str name: repository base file name + :param str uri: repository URI + :param repo_type: unused + :param int prio: + :param dist: unused + :param components: components to be used from this repository + :param user: unused + :param secret: unused + :param credentials_file: unused + :param bool repo_gpgcheck: enable database signature validation + :param bool pkg_gpgcheck: enable package signature validation + :param str sourcetype: unused + """ + repo_file = '{0}/{1}.repo'.format( + self.shared_pacman_dir['repos-dir'], name + ) + self.repo_names.append(name + '.repo') + repo_config = ConfigParser() + repo_config.optionxform = str + if not components: + self._add_repo_section( + repo_config, name, uri, repo_gpgcheck, + pkg_gpgcheck + ) + else: + for component in components.split(): + self._add_repo_section( + repo_config, component, uri, repo_gpgcheck, + pkg_gpgcheck + ) + + with open(repo_file, 'w') as config: + repo_config.write(config) + + def import_trusted_keys(self, signing_keys): + """ + zypper runtime configuration and environment + """ + for signing_key in signing_keys: + if os.path.exists(signing_key): + Command.run( + ['pacman-key', '--recv-keys', signing_key] + ) + else: + Command.run( + ['pacman-key', '--add', signing_key] + ) + + def delete_repo(self, name): + """ + Delete pacman repository + + :param str name: repository name + """ + Path.wipe( + '{0}/{1}.repo'.format(self.shared_pacman_dir['repos-dir'], name) + ) + + def delete_all_repos(self): + """ + Delete all pacman repositories + """ + Path.wipe(self.shared_pacman_dir['repos-dir']) + Path.create(self.shared_pacman_dir['repos-dir']) + + def delete_repo_cache(self, name): + """ + Delete pacman repository cache + + The method deletes these directories to cleanup the + cache information + + :param str name: repository name + """ + Path.wipe(self.shared_pacman_dir['cache-dir']) + + def setup_package_database_configuration(self): + """ + Creates the folder that will contain the package manager database + """ + Path.create(os.sep.join([self.root_dir, 'var/lib/pacman'])) + + def cleanup_unused_repos(self): + """ + Delete unused pacman repositories + + Repository configurations which are not used for this build + must be removed otherwise they are taken into account for + the package installations + """ + repos_dir = self.shared_pacman_dir['repos-dir'] + repo_files = list(os.walk(repos_dir))[0][2] + for repo_file in repo_files: + if repo_file not in self.repo_names: + Path.wipe(repos_dir + '/' + repo_file) + + def _write_runtime_config(self): + runtime_pacman_config = ConfigParser() + runtime_pacman_config.optionxform = str + runtime_pacman_config.add_section('options') + + runtime_pacman_config.set( + 'options', 'Architecture', 'auto' + ) + runtime_pacman_config.set( + 'options', 'CacheDir', self.shared_pacman_dir['cache-dir'] + ) + + if self.check_signatures: + runtime_pacman_config.set( + 'options', 'SigLevel', 'Required DatabaseRequired' + ) + runtime_pacman_config.set( + 'options', 'LocalFileSigLevel', 'Required DatabaseRequired' + ) + else: + runtime_pacman_config.set( + 'options', 'SigLevel', 'Never DatabaseNever' + ) + runtime_pacman_config.set( + 'options', 'LocalFileSigLevel', 'Never DatabaseNever' + ) + + runtime_pacman_config.set( + 'options', 'Include', '{0}/*.repo'.format( + self.shared_pacman_dir['repos-dir'] + ) + ) + + with open(self.runtime_pacman_config_file.name, 'w') as config: + runtime_pacman_config.write(config) diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc index df57ccd9..de37825f 100644 --- a/kiwi/schema/kiwi.rnc +++ b/kiwi/schema/kiwi.rnc @@ -793,7 +793,7 @@ div { # div { k.packagemanager.content = - "apt-get" | "zypper" | "yum" | "dnf" + "apt-get" | "zypper" | "yum" | "dnf" | "pacman" k.packagemanager.attlist = empty k.packagemanager = ## Name of the Package Manager diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng index e4db2d7a..39c9adfd 100644 --- a/kiwi/schema/kiwi.rng +++ b/kiwi/schema/kiwi.rng @@ -1228,6 +1228,7 @@ the device is looked up in /dev/disk/by-* and /dev/mapper/* zypper yum dnf + pacman diff --git a/kiwi/system/setup.py b/kiwi/system/setup.py index f3b0d3ac..50c5a4da 100644 --- a/kiwi/system/setup.py +++ b/kiwi/system/setup.py @@ -499,6 +499,9 @@ class SystemSetup: elif packager == 'dpkg': self._export_deb_package_list(filename) return filename + elif packager == 'pacman': + self._export_pacman_package_list(filename) + return filename def export_package_verification(self, target_dir): """ @@ -1016,6 +1019,19 @@ class SystemSetup: ) packages.write(os.linesep) + def _export_pacman_package_list(self, filename): + log.info('Export pacman packages metadata') + query_call = Command.run(['pacman', '-Qe']) + with open(filename, 'w') as packages: + for line in query_call.output.splitlines(): + package, _, version_release = line.partition(' ') + version, _, release = version_release.partition('-') + packages.writelines([ + '{0}|None|{1}|{2}|None|None|None{3}'.format( + package, version, release, os.linesep + ) + ]) + def _export_rpm_package_verification(self, filename): log.info('Export rpm verification metadata') dbpath_option = [ diff --git a/package/python-kiwi-pkgbuild-template b/package/python-kiwi-pkgbuild-template new file mode 100644 index 00000000..a5176755 --- /dev/null +++ b/package/python-kiwi-pkgbuild-template @@ -0,0 +1,69 @@ +# Maintainer: Marcus Schaefer +# Maintainer: David Cassany + +pkgname=('python-kiwi' 'kiwi-man-pages' 'dracut-kiwi-lib' 'dracut-kiwi-oem-repart' 'dracut-kiwi-oem-dump' 'dracut-kiwi-live' 'dracut-kiwi-overlay') +arch=(x86_64) +pkgver=%%VERSION +pkgrel=0 +pkgdesc="KIWI - Appliance Builder Next Generation" +url="https://github.com/SUSE/kiwi/tarball/master" +license=('GPL3') +makedepends=(python-setuptools gcc shadow grep) +provides=(kiwi-ng kiwi) +source=("${pkgname}.tar.gz") +changelog="${pkgname}.changes" +md5sums=('%%MD5SUM') + + +build() { + cd kiwi-${pkgver} + python setup.py build +} + +package_python-kiwi(){ + depends=(python-docopt python-future python-lxml python-requests python-setuptools python-six python-pyxattr python-yaml grub qemu squashfs-tools gptfdisk pacman e2fsprogs xfsprogs btrfs-progs libisoburn lvm2 mtools parted multipath-tools rsync tar shadow kiwi-man-pages) + cd kiwi-${pkgver} + python setup.py install --root="${pkgdir}/" --optimize=1 --skip-build + ln -sr "${pkgdir}/usr/bin/kiwi-ng" "${pkgdir}/usr/bin/kiwi" + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} + +package_kiwi-man-pages(){ + cd kiwi-${pkgver} + make buildroot="${pkgdir}/" docdir="/usr/share/doc/${pkgname}" install_package_docs +} + +package_dracut-kiwi-lib(){ + depends=(cryptsetup btrfs-progs gptfdisk coreutils e2fsprogs grep lvm2 mdadm parted util-linux xfsprogs dialog curl xz device-mapper dracut) + cd kiwi-${pkgver} + install -d -m 755 ${pkgdir}/usr/lib/dracut/modules.d/99kiwi-lib + cp -a dracut/modules.d/99kiwi-lib ${pkgdir}/usr/lib/dracut/modules.d/ +} + +package_dracut-kiwi-oem-repart(){ + depends=(dracut-kiwi-lib) + cd kiwi-${pkgver} + install -d -m 755 ${pkgdir}/usr/lib/dracut/modules.d/90kiwi-repart + cp -a dracut/modules.d/90kiwi-repart ${pkgdir}/usr/lib/dracut/modules.d/ +} + +package_dracut-kiwi-oem-dump(){ + depends=(dracut-kiwi-lib multipath-tools) + cd kiwi-${pkgver} + install -d -m 755 ${pkgdir}/usr/lib/dracut/modules.d/90kiwi-dump + cp -a dracut/modules.d/90kiwi-dump ${pkgdir}/usr/lib/dracut/modules.d/ +} + +package_dracut-kiwi-live(){ + depends=(dracut dialog xfsprogs e2fsprogs util-linux device-mapper libisoburn parted) + cd kiwi-${pkgver} + install -d -m 755 ${pkgdir}/usr/lib/dracut/modules.d/90kiwi-live + cp -a dracut/modules.d/90kiwi-live ${pkgdir}/usr/lib/dracut/modules.d/ +} + +package_dracut-kiwi-overlay(){ + depends=(dracut util-linux) + cd kiwi-${pkgver} + install -d -m 755 ${pkgdir}/usr/lib/dracut/modules.d/90kiwi-overlay + cp -a dracut/modules.d/90kiwi-overlay ${pkgdir}/usr/lib/dracut/modules.d/ +} diff --git a/test/unit/iso_tools/base_test.py b/test/unit/iso_tools/base_test.py index dabe0283..d70057a8 100644 --- a/test/unit/iso_tools/base_test.py +++ b/test/unit/iso_tools/base_test.py @@ -63,6 +63,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/isolinux.bin', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/isolinux.bin', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/isolinux.bin', 'root_dir/image/loader/' @@ -75,6 +79,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/ldlinux.c32', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/ldlinux.c32', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/ldlinux.c32', 'root_dir/image/loader/' @@ -87,6 +95,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/libcom32.c32', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/libcom32.c32', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/libcom32.c32', 'root_dir/image/loader/' @@ -99,6 +111,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/libutil.c32', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/libutil.c32', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/libutil.c32', 'root_dir/image/loader/' @@ -111,6 +127,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/gfxboot.c32', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/gfxboot.c32', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/gfxboot.c32', 'root_dir/image/loader/' @@ -123,6 +143,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/gfxboot.com', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/gfxboot.com', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/gfxboot.com', 'root_dir/image/loader/' @@ -135,6 +159,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/menu.c32', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/menu.c32', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/menu.c32', 'root_dir/image/loader/' @@ -147,6 +175,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/chain.c32', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/chain.c32', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/chain.c32', 'root_dir/image/loader/' @@ -159,6 +191,10 @@ class TestIsoToolsBase: 'root_dir/usr/share/syslinux/mboot.c32', 'root_dir/image/loader/' ), + call( + 'root_dir/usr/lib/syslinux/bios/mboot.c32', + 'root_dir/image/loader/' + ), call( 'root_dir/usr/lib/syslinux/modules/bios/mboot.c32', 'root_dir/image/loader/' diff --git a/test/unit/iso_tools/xorriso_test.py b/test/unit/iso_tools/xorriso_test.py index a2df0762..95035165 100644 --- a/test/unit/iso_tools/xorriso_test.py +++ b/test/unit/iso_tools/xorriso_test.py @@ -37,6 +37,7 @@ class TestIsoToolsXorrIso: mock_which.assert_called_once_with( 'isohdpfx.bin', [ '/usr/share/syslinux', + '/usr/lib/syslinux/bios', '/usr/lib/syslinux/modules/bios', '/usr/lib/ISOLINUX' ] diff --git a/test/unit/package_manager/init_test.py b/test/unit/package_manager/init_test.py index 5f3ebbc7..f3d852f0 100644 --- a/test/unit/package_manager/init_test.py +++ b/test/unit/package_manager/init_test.py @@ -36,3 +36,9 @@ class TestPackageManager: repository = Mock() PackageManager(repository, 'apt-get') mock_manager.assert_called_once_with(repository, None) + + @patch('kiwi.package_manager.PackageManagerPacman') + def test_manager_pacman(self, mock_manager): + repository = Mock() + PackageManager(repository, 'pacman') + mock_manager.assert_called_once_with(repository, None) diff --git a/test/unit/package_manager/pacman_test.py b/test/unit/package_manager/pacman_test.py new file mode 100644 index 00000000..9aceff41 --- /dev/null +++ b/test/unit/package_manager/pacman_test.py @@ -0,0 +1,137 @@ +from mock import patch, call +from pytest import raises +import mock + +from kiwi.package_manager.pacman import PackageManagerPacman + +from kiwi.exceptions import KiwiRequestError + + +class TestPackageManagerPacman: + def setup(self): + repository = mock.Mock() + repository.root_dir = '/root-dir' + + repository.runtime_config = mock.Mock( + return_value={ + 'pacman_args': [ + '--config', '/root-dir/pacman.conf', '-y', '--noconfirm' + ], + 'command_env': ['env'] + } + ) + self.manager = PackageManagerPacman(repository) + + def test_request_package(self): + self.manager.request_package('name') + assert self.manager.package_requests == ['name'] + + def test_request_collection(self): + self.manager.request_collection('name') + assert self.manager.collection_requests == [] + assert self.manager.package_requests == ['name'] + + def test_request_product(self): + self.manager.request_product('name') + assert self.manager.product_requests == [] + + def test_request_package_exclusion(self): + self.manager.request_package_exclusion('name') + assert self.manager.exclude_requests == ['name'] + + @patch('kiwi.command.Command.call') + @patch('kiwi.command.Command.run') + def test_process_install_requests_bootstrap(self, mock_run, mock_call): + self.manager.request_package('vim') + self.manager.request_collection('collection') + self.manager.process_install_requests_bootstrap() + mock_run.call_args_list == [ + call(['rm', '-r', '-f', '/root-dir/var/run']), + call([ + 'pacman', '--config', '/root-dir/pacman.conf', '-y', + '--noconfirm', '--needed', '--root', '/root-dir', '-Sy' + ]) + ] + mock_call.assert_called_once_with( + [ + 'bash', '-c', + 'pacman --config /root-dir/pacman.conf -y --noconfirm ' + '--root /root-dir -S --needed --overwrite /root-dir/var/run ' + 'vim collection' + ], ['env'] + ) + + @patch('kiwi.command.Command.call') + def test_process_install_requests(self, mock_call): + self.manager.request_package('vim') + self.manager.request_collection('collection') + self.manager.request_package_exclusion('skipme') + self.manager.process_install_requests() + mock_call.assert_called_once_with( + [ + 'bash', '-c', + 'chroot /root-dir pacman --config /pacman.conf -y ' + '--noconfirm -S --needed vim collection' + ], ['env'] + ) + + @patch('kiwi.command.Command.call') + @patch('kiwi.command.Command.run') + def test_process_delete_requests_force(self, mock_run, mock_call): + self.manager.request_package('vim') + self.manager.process_delete_requests(True) + mock_call.assert_called_once_with( + [ + 'chroot', '/root-dir', 'pacman', '--config', '/pacman.conf', + '-y', '--noconfirm', '-Rdd', 'vim' + ], ['env'] + ) + + @patch('kiwi.command.Command.call') + @patch('kiwi.command.Command.run') + def test_process_delete_requests_no_force(self, mock_run, mock_call): + self.manager.request_package('vim') + self.manager.process_delete_requests() + mock_call.assert_called_once_with( + [ + 'chroot', '/root-dir', 'pacman', '--config', '/pacman.conf', + '-y', '--noconfirm', '-Rs', 'vim' + ], ['env'] + ) + + @patch('kiwi.command.Command.run') + @patch('kiwi.command.Command.call') + def test_process_delete_requests_package_missing( + self, mock_call, mock_run + ): + mock_run.side_effect = Exception + self.manager.request_package('vim') + with raises(KiwiRequestError): + self.manager.process_delete_requests() + mock_run.assert_called_once_with( + ['chroot', '/root-dir', 'pacman', '-Qi', 'vim'] + ) + + def test_process_only_required(self): + self.manager.process_only_required() + assert self.manager.custom_args == [] + + def test_process_plus_recommended(self): + self.manager.process_plus_recommended() + assert self.manager.custom_args == [] + + @patch('kiwi.command.Command.call') + def test_update(self, mock_call): + self.manager.update() + mock_call.assert_called_once_with( + [ + 'chroot', '/root-dir', 'pacman', '--config', '/pacman.conf', + '-y', '--noconfirm', '-Su' + ], ['env'] + ) + + def test_match_package_installed(self): + assert self.manager.match_package_installed('foo', ' installing foo') + + def test_match_package_deleted(self): + assert self.manager.match_package_deleted('foo', ' removing foo') diff --git a/test/unit/repository/init_test.py b/test/unit/repository/init_test.py index 343b404d..8299fa01 100644 --- a/test/unit/repository/init_test.py +++ b/test/unit/repository/init_test.py @@ -34,3 +34,9 @@ class TestRepository: root_bind = mock.Mock() Repository(root_bind, 'apt-get') mock_manager.assert_called_once_with(root_bind, None) + + @patch('kiwi.repository.RepositoryPacman') + def test_repository_pacman(self, mock_manager): + root_bind = mock.Mock() + Repository(root_bind, 'pacman') + mock_manager.assert_called_once_with(root_bind, None) diff --git a/test/unit/repository/pacman_test.py b/test/unit/repository/pacman_test.py new file mode 100644 index 00000000..83a1d3fe --- /dev/null +++ b/test/unit/repository/pacman_test.py @@ -0,0 +1,191 @@ + +from mock import ( + patch, Mock, call, mock_open +) + +import os + +from kiwi.repository.pacman import RepositoryPacman + + +class TestRepositorPacman(object): + @patch('kiwi.repository.pacman.NamedTemporaryFile') + @patch('kiwi.repository.pacman.ConfigParser') + @patch('kiwi.repository.pacman.Path.create') + def setup(self, mock_path, mock_config, mock_temp): + runtime_pacman_config = Mock() + mock_config.return_value = runtime_pacman_config + tmpfile = Mock() + tmpfile.name = 'tmpfile' + mock_temp.return_value = tmpfile + root_bind = Mock() + root_bind.move_to_root = Mock( + return_value=['root-moved-arguments'] + ) + root_bind.root_dir = '../data' + root_bind.shared_location = '/shared-dir' + with patch('builtins.open', create=True): + self.repo = RepositoryPacman(root_bind) + + assert runtime_pacman_config.set.call_args_list == [ + call('options', 'Architecture', 'auto'), + call('options', 'CacheDir', '/shared-dir/pacman/cache'), + call('options', 'SigLevel', 'Never DatabaseNever'), + call('options', 'LocalFileSigLevel', 'Never DatabaseNever'), + call('options', 'Include', '/shared-dir/pacman/repos/*.repo') + ] + + @patch('kiwi.repository.pacman.NamedTemporaryFile') + @patch('kiwi.repository.pacman.Path.create') + def test_post_init_with_custom_args(self, mock_path, mock_temp): + self.repo.post_init(['check_signatures']) + assert self.repo.custom_args == [] + assert self.repo.check_signatures + + def test_runtime_config(self): + assert self.repo.runtime_config()['pacman_args'] == \ + self.repo.pacman_args + assert self.repo.runtime_config()['command_env'] == \ + os.environ + + @patch('kiwi.repository.pacman.ConfigParser') + @patch('os.path.exists') + def test_add_local_repo_with_components( + self, mock_exists, mock_config + ): + repo_config = Mock() + mock_config.return_value = repo_config + mock_exists.return_value = True + + m_open = mock_open() + with patch('builtins.open', m_open, create=True): + self.repo.add_repo( + 'foo', '/some_uri', components='core extra', + repo_gpgcheck=False, pkg_gpgcheck=True + ) + m_open.assert_called_once_with( + '/shared-dir/pacman/repos/foo.repo', 'w' + ) + + assert repo_config.add_section.call_args_list == [ + call('core'), call('extra') + ] + assert repo_config.set.call_args_list == [ + call('core', 'Server', 'file:///some_uri'), + call('core', 'SigLevel', 'Required DatabaseNever'), + call('extra', 'Server', 'file:///some_uri'), + call('extra', 'SigLevel', 'Required DatabaseNever'), + ] + + @patch('kiwi.repository.pacman.ConfigParser') + @patch('os.path.exists') + def test_add_repo_with_components( + self, mock_exists, mock_config + ): + repo_config = Mock() + mock_config.return_value = repo_config + mock_exists.return_value = True + + m_open = mock_open() + with patch('builtins.open', m_open, create=True): + self.repo.add_repo( + 'foo', 'some_uri', components='core extra', + repo_gpgcheck=False, pkg_gpgcheck=True + ) + m_open.assert_called_once_with( + '/shared-dir/pacman/repos/foo.repo', 'w' + ) + + assert repo_config.add_section.call_args_list == [ + call('core'), call('extra') + ] + assert repo_config.set.call_args_list == [ + call('core', 'Server', 'some_uri'), + call('core', 'SigLevel', 'Required DatabaseNever'), + call('extra', 'Server', 'some_uri'), + call('extra', 'SigLevel', 'Required DatabaseNever'), + ] + + @patch('kiwi.repository.pacman.ConfigParser') + @patch('os.path.exists') + def test_add_repo_without_components( + self, mock_exists, mock_config + ): + repo_config = Mock() + mock_config.return_value = repo_config + mock_exists.return_value = True + + m_open = mock_open() + with patch('builtins.open', m_open, create=True): + self.repo.add_repo('foo', 'some_uri', pkg_gpgcheck=False) + + m_open.assert_called_once_with( + '/shared-dir/pacman/repos/foo.repo', 'w' + ) + + assert repo_config.add_section.call_args_list == [ + call('foo') + ] + assert repo_config.set.call_args_list == [ + call('foo', 'Server', 'some_uri'), + call('foo', 'SigLevel', 'Never DatabaseNever'), + ] + + @patch('kiwi.repository.pacman.Path.create') + def test_setup_package_database_configuration(self, mock_path_create): + self.repo.setup_package_database_configuration() + mock_path_create.assert_called_once_with( + '../data/var/lib/pacman' + ) + + @patch('kiwi.repository.pacman.os.path.exists') + @patch('kiwi.repository.pacman.Command.run') + def test_import_trusted_keys(self, mock_cmd, mock_exists): + exists = [True, False] + mock_exists.side_effect = lambda x: exists.pop() + signing_keys = ['key-file-a.asc', 'key-file-b_ID'] + self.repo.import_trusted_keys(signing_keys) + assert mock_cmd.call_args_list == [ + call(['pacman-key', '--add', 'key-file-a.asc']), + call(['pacman-key', '--recv-keys', 'key-file-b_ID']) + ] + + @patch('kiwi.repository.pacman.Path') + def test_delete_repo(self, mock_path): + self.repo.delete_repo('foo') + mock_path.wipe.assert_called_once_with( + '/shared-dir/pacman/repos/foo.repo' + ) + + @patch('kiwi.repository.pacman.Path') + def test_delete_all_repos(self, mock_path): + self.repo.delete_all_repos() + mock_path.wipe.assert_called_once_with('/shared-dir/pacman/repos') + mock_path.create.assert_called_once_with('/shared-dir/pacman/repos') + + @patch('kiwi.repository.pacman.Path.wipe') + @patch('kiwi.repository.pacman.os.walk') + def test_cleanup_unused_repos(self, mock_os_walk, mock_wipe): + mock_os_walk.return_value = [( + '/shared-dir/pacman/repos', + [], + ['foo.repo', 'bar.repo'] + )] + self.repo.cleanup_unused_repos() + mock_os_walk.assert_called_once_with('/shared-dir/pacman/repos') + assert mock_wipe.call_args_list == [ + call('/shared-dir/pacman/repos/foo.repo'), + call('/shared-dir/pacman/repos/bar.repo') + ] + + def test_use_default_location(self): + self.repo.use_default_location() + assert self.repo.shared_pacman_dir['cache-dir'] == \ + '/shared-dir/pacman/cache' + assert self.repo.shared_pacman_dir['repos-dir'] == \ + '../data/etc/pacman.d' + + @patch('kiwi.repository.pacman.Path') + def test_delete_repo_cache(self, mock_path): + self.repo.delete_repo_cache('foo') + mock_path.wipe.assert_called_once_with('/shared-dir/pacman/cache') diff --git a/test/unit/system/setup_test.py b/test/unit/system/setup_test.py index 5fb94ffd..ab5f4b02 100644 --- a/test/unit/system/setup_test.py +++ b/test/unit/system/setup_test.py @@ -924,6 +924,24 @@ class TestSystemSetup: '-f', '${Package}|None|${Version}|None|${Architecture}|None|None\\n' ]) + @patch('kiwi.system.setup.Command.run') + def test_export_package_list_pacman(self, mock_command): + command = Mock() + command.output = 'packages_data' + mock_command.return_value = command + self.xml_state.get_package_manager = Mock( + return_value='pacman' + ) + + with patch('builtins.open') as m_open: + result = self.setup.export_package_list('target_dir') + m_open.assert_called_once_with( + 'target_dir/some-image.x86_64-1.2.3.packages', 'w' + ) + + assert result == 'target_dir/some-image.x86_64-1.2.3.packages' + mock_command.assert_called_once_with(['pacman', '-Qe']) + @patch('kiwi.system.setup.Command.run') @patch('kiwi.system.setup.RpmDataBase') @patch('kiwi.system.setup.MountManager')