kiwi-el8/kiwi/package_manager/apt.py
Marcus Schäfer 9b6c121957
reinstall bootstrap packs in image phase for apt
Due to the special bootstrap process, the packages unpacked
during bootstrap are not properly listed in the apt index.
Therefore the bootstrap packages are added to the install
phase which causes an install of this packages again to
fix the apt index and provide a consistent system from
an apt perspective. This Fixes #2768
2025-04-08 20:04:58 +02:00

586 lines
20 KiB
Python

# Copyright (c) 2015 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 <http://www.gnu.org/licenses/>
#
import re
import os
import glob
import logging
import pathlib
from typing import (
List, Dict
)
# project
from kiwi.command import CommandCallT
from kiwi.command import Command
from kiwi.path import Path
from kiwi.package_manager.base import PackageManagerBase
from kiwi.system.root_bind import RootBind
from kiwi.repository.apt import RepositoryApt
from kiwi.utils.temporary import Temporary
import kiwi.defaults as defaults
from kiwi.exceptions import (
KiwiDebianBootstrapError,
KiwiRequestError,
KiwiFileNotFound
)
log = logging.getLogger('kiwi')
class PackageManagerApt(PackageManagerBase):
"""
**Implements Installation/Deletion of packages/collections with apt-get**
:param list apt_get_args:
apt-get arguments from repository runtime configuration
:param dict command_env:
apt-get command environment from repository
runtime configuration
"""
def post_init(self, custom_args: List = []) -> None:
"""
Post initialization method
Store custom apt-get arguments
:param list custom_args: custom apt-get arguments
"""
self.repository: RepositoryApt = self.repository
self.custom_args = custom_args
self.deboostrap_minbase: bool = True
runtime_config = self.repository.runtime_config()
self.apt_get_args = runtime_config['apt_get_args']
self.command_env = runtime_config['command_env']
self.distribution = runtime_config['distribution']
self.distribution_path = runtime_config['distribution_path']
self.bootstrap_packages: List[str] = []
def request_package(self, name: str) -> None:
"""
Queue a package request
:param str name: package name
"""
self.package_requests.append(name)
def request_collection(self, name: str) -> None:
"""
Queue a collection request
There is no collection definition in the deb repo data
:param str name: unused
"""
log.warning(
'Collection(%s) handling not supported for apt-get', name
)
def request_product(self, name: str) -> None:
"""
Queue a product request
There is no product definition in the deb repo data
:param str name: unused
"""
log.warning(
'Product(%s) handling not supported for apt-get', name
)
def request_package_exclusion(self, name: str) -> None:
"""
Queue a package exclusion(skip) request
Package exclusion for apt package manager not yet implemented
:param str name: unused
"""
log.warning(
'Package exclusion for (%s) not supported for apt-get', name
)
def setup_repository_modules(
self, collection_modules: Dict[str, List[str]]
) -> None:
"""
Repository modules not supported for apt-get
The method does nothing in this scope
:param dict collection_modules: unused
"""
pass
def process_install_requests_bootstrap(
self, root_bind: RootBind = None, bootstrap_package: str = None
) -> CommandCallT:
"""
Process package install requests for bootstrap phase (no chroot)
Either a manual unpacking strategy or a prebuilt bootstrap
package can be used to bootstrap a new system.
:param object root_bind:
unused
:param str bootstrap_package:
package name of a bootstrap package
:return: process results as command instance
:rtype: command_call_type
"""
if bootstrap_package:
return self._process_install_requests_bootstrap_prebuild_root(
bootstrap_package
)
else:
return self._process_install_requests_bootstrap()
def _process_install_requests_bootstrap_prebuild_root(
self, bootstrap_package: str
) -> CommandCallT:
"""
Process bootstrap phase (no chroot) using a prebuilt bootstrap
package. The package has to provide a tarball below the
directory /var/cache/kiwi/bootstrap/PACKAGE_NAME.ARCH.tar.xz
and will be unpacked to serve as the bootstrap root.
The optionally listed packages in the kiwi bootstrap section
will be installed as part of a chroot install and returned
as command instance
:param str bootstrap_package:
package name of the bootstrap package containing the
bootstrap root as a tarball
:return: process results as command instance
:rtype: command_call_type
"""
# Install prebuilt bootstrap package on build host
update_command = [
'apt-get'
] + self.apt_get_args + self.custom_args + [
'update'
]
install_command = [
'apt-get'
] + self.apt_get_args + self.custom_args + [
'install', bootstrap_package
]
Command.run(
update_command, self.command_env
)
Command.run(
install_command, self.command_env
)
# Unpack prebuilt bootstrap root tarball as new root
bootstrap_root_tarball = '/var/lib/bootstrap/{0}.{1}.tar.xz'.format(
bootstrap_package, defaults.PLATFORM_MACHINE
)
if not os.path.isfile(bootstrap_root_tarball):
raise KiwiFileNotFound(
f'bootstrap tarball: {bootstrap_root_tarball!r} not found'
)
Command.run(
['tar', '-C', self.root_dir, '-xf', bootstrap_root_tarball]
)
# Install eventual bootstrap packages as standard system install
return self.process_install_requests()
def _process_install_requests_bootstrap(self) -> CommandCallT:
"""
Process package install requests for bootstrap phase (no chroot).
:raises KiwiDebianBootstrapError
:return: process results in command type
:rtype: CommandCallT
"""
# we invoke apt-get install to download all the essential packages.
# With DPkg::Pre-Install-Pkgs, we specify a shell command that will
# receive the list of packages that will be installed on stdin.
# By configuring Debug::pkgDpkgPm=1, apt-get install will not
# actually execute any dpkg commands, so all it does is download
# the essential debs and tell us their full in the apt cache without
# actually installing them.
try:
# TODO: Drop once apt 2.5.4 is widely available.
pathlib.Path(f'{self.root_dir}/var/lib/dpkg').mkdir(
parents=True, exist_ok=True
)
pathlib.Path(f'{self.root_dir}/var/lib/dpkg/status.kiwi').touch()
pathlib.Path(f'{self.root_dir}/var/lib/dpkg/available').touch()
if 'apt' not in self.package_requests:
self.package_requests.append('apt')
update_cache = [
'apt-get'
] + self.apt_get_args + self.custom_args + [
'update'
]
result = Command.run(
update_cache, self.command_env
)
log.debug(
'Apt update: {0} {1}'.format(result.output, result.error)
)
package_names = Temporary(prefix='kiwi_debs_').new_file()
self.bootstrap_packages = [
'?essential'
] + self.package_requests
download_bootstrap = [
'apt-get'
] + self.apt_get_args + self.custom_args + [
'install',
'-oDebug::pkgDPkgPm=1',
f'-oDPkg::Pre-Install-Pkgs::=cat >{package_names.name}'
] + self.bootstrap_packages
# Download solved bootstrap packages
result = Command.run(
download_bootstrap, self.command_env
)
log.debug(
'Apt download: {0} {1}'.format(result.output, result.error)
)
# Extract bootstrap packages
with open(package_names.name) as packages:
solved_debootstrap_packages = [p.rstrip() for p in packages]
self.command_env['PATH'] = '$PATH:/usr/bin:/bin:/usr/sbin:/sbin'
for package in solved_debootstrap_packages:
Command.run(
[
'bash', '-c',
'dpkg-deb --fsys-tarfile {0} | tar -C {1} -x'.format(
package, self.root_dir
)
], self.command_env
)
# Run package scripts. Unfortuantely Debian based systems
# requires special sauce for bootstrap. See the exceptions
# we have to apply below:
#
# * manual order is required to make sure users(root) exists
# * the usr-merge strategy only works after bootstrap
#
# 1. Pass: Run package scripts, manual order
self._run_bootstrap_scripts(
solved_debootstrap_packages,
only_for=['base-passwd'], skip=['usrmerge']
)
# 2. Pass: Run package scripts in apt order
self._run_bootstrap_scripts(
solved_debootstrap_packages,
skip=['usrmerge']
)
self.cleanup_requests()
return Command.call(
update_cache, self.command_env
)
except Exception as e:
raise KiwiDebianBootstrapError(
'%s: %s' % (type(e).__name__, format(e))
)
def process_install_requests(self) -> CommandCallT:
"""
Process package install requests for image phase (chroot)
:return: process results in command type
:rtype: namedtuple
"""
update_command = ['chroot', self.root_dir, 'apt-get']
update_command.extend(
Path.move_to_root(self.root_dir, self.apt_get_args)
)
update_command.extend(self.custom_args)
update_command.append('update')
Command.run(update_command, self.command_env)
apt_get_command = ['chroot', self.root_dir, 'apt-get']
apt_get_command.extend(
Path.move_to_root(self.root_dir, self.apt_get_args)
)
apt_get_command.extend(self.custom_args)
apt_get_command.append('install')
# Due to the special bootstrap process, the packages unpacked
# during bootstrap are not properly listed in the apt index.
# Therefore the bootstrap packages are added to the install
# phase which causes an install of this packages again to
# fix the apt index and provide a consistent system from
# an apt perspective.
apt_get_command.extend(self.bootstrap_packages)
apt_get_command.extend(self._package_requests())
return Command.call(
apt_get_command, self.command_env
)
def process_delete_requests(self, force: bool = False) -> CommandCallT:
"""
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, 'dpkg', '-l', 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'
)
# deleting debs for some reason can also trigger package
# scripts from the libc-bin (glibc) package. This often
# results in calls like ldconfig which failed unless the
# system would be really running. Since the deletion
# happens via chroot and with the system in offline mode
# many attempts to uninstall a package, even cracefully
# failed for this reason. So far I only saw the workaround
# to make ldconfig a noop during the process of uninstall.
# This is not a nice solution and I dislike it. However,
# the only one I could come up with to turn the package
# uninstall in a useful feature in kiwi.
Command.run(
[
'cp',
f'{self.root_dir}/usr/sbin/ldconfig',
f'{self.root_dir}/usr/sbin/ldconfig.orig'
]
)
Command.run(
[
'cp',
f'{self.root_dir}/usr/bin/true',
f'{self.root_dir}/usr/sbin/ldconfig'
]
)
if force:
# force deleting debs only worked well for me when ignoring
# the pre/post-inst and pre/post-remove codings. No idea why it
# ends in so many conflicts but if you want to force get rid
# of stuff this was the only way I could come up with. There
# are still cases when it does not work depending on the many
# code that runs on deleting
for package in delete_items:
pre_delete_pattern = \
f'{self.root_dir}/var/lib/dpkg/info/{package}*.pre*'
post_delete_pattern = \
f'{self.root_dir}/var/lib/dpkg/info/{package}*.post*'
for delete_file in glob.iglob(pre_delete_pattern):
Path.wipe(delete_file)
for delete_file in glob.iglob(post_delete_pattern):
Path.wipe(delete_file)
apt_get_command = ['chroot', self.root_dir, 'dpkg']
apt_get_command.extend(
[
'--remove',
'--force-remove-reinstreq',
'--force-remove-essential',
'--force-depends'
]
)
apt_get_command.extend(delete_items)
else:
apt_get_command = ['chroot', self.root_dir, 'apt-get']
apt_get_command.extend(
Path.move_to_root(self.root_dir, self.apt_get_args)
)
apt_get_command.extend(self.custom_args)
apt_get_command.extend(['--auto-remove', 'remove'])
apt_get_command.extend(delete_items)
return Command.call(
apt_get_command, self.command_env
)
def post_process_delete_requests(
self, root_bind: RootBind = None
) -> None:
"""
Revert system changes done prior deleting packages
:param object root_bind: unused
"""
Command.run(
[
'mv',
f'{self.root_dir}/usr/sbin/ldconfig.orig',
f'{self.root_dir}/usr/sbin/ldconfig'
]
)
def update(self) -> CommandCallT:
"""
Process package update requests (chroot)
:return: process results in command type
:rtype: namedtuple
"""
apt_get_command = ['chroot', self.root_dir, 'apt-get']
apt_get_command.extend(
Path.move_to_root(self.root_dir, self.apt_get_args)
)
apt_get_command.extend(self.custom_args)
apt_get_command.append('upgrade')
return Command.call(
apt_get_command, self.command_env
)
def process_only_required(self) -> None:
"""
Setup package processing only for required packages
"""
if '--no-install-recommends' not in self.custom_args:
self.custom_args.append('--no-install-recommends')
self.deboostrap_minbase = True
def process_plus_recommended(self) -> None:
"""
Setup package processing to also include recommended dependencies.
"""
if '--no-install-recommends' in self.custom_args:
self.custom_args.remove('--no-install-recommends')
self.deboostrap_minbase = False
def match_package_installed(
self, package_name: str, package_manager_output: str
) -> bool:
"""
Match expression to indicate a package has been installed
This match for the package to be installed in the output
of the apt-get command is not 100% accurate. There might
be false positives due to sub package names starting with
the same base package name
:param str package_name: package_name
:param str package_manager_output: apt-get status line
:returns: True|False
:rtype: bool
"""
return bool(
re.match(
'.*Unpacking {0}.*'.format(re.escape(package_name)),
package_manager_output
)
)
def match_package_deleted(
self, package_name: str, package_manager_output: str
) -> bool:
"""
Match expression to indicate a package has been deleted
:param str package_name: package_name
:param str package_manager_output: apt-get status line
:returns: True|False
:rtype: bool
"""
return bool(
re.match(
'.*Removing {0}.*'.format(re.escape(package_name)),
package_manager_output
)
)
def _package_requests(self) -> List:
items = self.package_requests[:]
self.cleanup_requests()
return items
def _run_bootstrap_scripts(
self, solved_debootstrap_packages: List[str],
only_for: List[str] = [], skip: List[str] = []
):
# TODO: this should not be needed but without setting
# the following environment variables no package pre/post
# script completes its task. I leave it up to the Debian
# experts to provide a fix if needed.
self.command_env['DPKG_MAINTSCRIPT_NAME'] = 'true'
self.command_env['DPKG_MAINTSCRIPT_PACKAGE'] = 'libc6'
post_script_dir = Temporary(
prefix='kiwi_debpost.', path=self.root_dir
).new_dir()
for package in solved_debootstrap_packages:
package_base_name = os.path.basename(package)
go_ahead = False if only_for else True
for name in only_for:
if name in package_base_name:
go_ahead = True
break
for name in skip:
if name in package_base_name:
go_ahead = False
break
if not go_ahead:
continue
log.debug(
f'Running pre/post scripts for: {package_base_name}'
)
package_metadata_dir = \
f'{post_script_dir.name}/{os.path.basename(package)}'
Command.run(
['dpkg', '-e', package, package_metadata_dir]
)
script_pre = f'{package_metadata_dir}/preinst'
script_post = f'{package_metadata_dir}/postinst'
# 1. preinst
if os.path.exists(script_pre):
Command.run(['chmod', '755', script_pre])
Command.run(
[
'chroot', self.root_dir,
f'{script_pre.replace(self.root_dir, "")}', 'install'
], self.command_env
)
# 2. postinst
if os.path.exists(script_post):
Command.run(['chmod', '755', script_post])
Command.run(
[
'chroot', self.root_dir,
f'{script_post.replace(self.root_dir, "")}', 'configure'
], self.command_env
)