from textwrap import dedent
from typing import IO, Any, Dict, List, Optional, cast
-from .backend import CommandLineArguments, ManifestFormat, PackageType, run
+from .backend import (
+ CommandLineArguments,
+ Distribution,
+ ManifestFormat,
+ PackageType,
+ run,
+ run_workspace_command,
+)
@dataclasses.dataclass
def record_packages(self, root: Path) -> None:
if cast(Any, self.args.distribution).package_type == PackageType.rpm:
self.record_rpm_packages(root)
+ if cast(Any, self.args.distribution).package_type == PackageType.deb:
+ self.record_deb_packages(root)
# TODO: add implementations for other package managers
def record_rpm_packages(self, root: Path) -> None:
source.add(package)
+ def record_deb_packages(self, root: Path) -> None:
+ c = run(
+ ["dpkg-query", f"--admindir={root}/var/lib/dpkg", "--show", "--showformat",
+ r'${Package}\t${source:Package}\t${Version}_${Architecture}\t${Installed-Size}\t${db-fsys:Last-Modified}\n'],
+ stdout=PIPE,
+ stderr=DEVNULL,
+ text=True,
+ )
+
+ packages = sorted(c.stdout.splitlines())
+
+ for package in packages:
+ name, source, version, size, installtime = package.split("\t")
+
+ # dpkg records the size in KBs
+ size = int(size) * 1024
+ installtime = datetime.fromtimestamp(int(installtime))
+
+ # If we are creating a layer based on a BaseImage=, e.g. a sysext, filter by
+ # packages that were installed in this execution of mkosi. We assume that the
+ # upper layer is put together in one go, which currently is always true.
+ if self.args.base_image and installtime < self._init_timestamp:
+ continue
+
+ package = PackageManifest("deb", name, version, size)
+ self.packages.append(package)
+
+ if not self.need_source_info():
+ continue
+
+ source_package = self.source_packages.get(source)
+ if source_package is None:
+ # Yes, --quiet is specified twice, to avoid output about download stats.
+ # Note that the argument of the 'changelog' verb is the binary package name,
+ # not the source package name.
+ cmd = ["apt-get", "--quiet", "--quiet", "changelog", name]
+
+ # If we are building with docs then it's easy, as the changelogs are saved
+ # in the image, just fetch them. Otherwise they will be downloaded from the network.
+ if self.args.with_docs:
+ # By default apt drops privileges and runs as the 'apt' user, but that means it
+ # loses access to the build directory, which is 700.
+ cmd += ["--option", "Acquire::Changelogs::AlwaysOnline=false",
+ "--option", "Debug::NoDropPrivs=true"]
+ else:
+ # Override the URL to avoid HTTPS, so that we don't need to install
+ # ca-certificates to make it work.
+ if self.args.distribution == Distribution.ubuntu:
+ cmd += ["--option", "Acquire::Changelogs::URI::Override::Origin::Ubuntu=http://changelogs.ubuntu.com/changelogs/pool/@CHANGEPATH@/changelog"]
+ else:
+ cmd += ["--option", "Acquire::Changelogs::URI::Override::Origin::Debian=http://metadata.ftp-master.debian.org/changelogs/@CHANGEPATH@_changelog"]
+
+ # We have to run from the root, because if we use the RootDir option to make
+ # apt from the host look at the repositories in the image, it will also pick
+ # the 'methods' executables from there, but the ABI might not be compatible.
+ changelog = run_workspace_command(self.args, root, cmd, network=not self.args.with_docs, capture_stdout=True)
+ source_package = SourcePackageManifest(source, changelog)
+ self.source_packages[source] = source_package
+
+ source_package.add(package)
+
def has_data(self) -> bool:
# We might add more data in the future
return len(self.packages) > 0