import pathlib import subprocess import tempfile from isobuilder.apt_state import AptStateManager, PackageInfo generate_template = """ Dir::ArchiveDir "{root}"; Dir::CacheDir "{scratch}/apt-ftparchive-db"; TreeDefault::Contents " "; Tree "dists/{series}" {{ FileList "{scratch}/filelist_$(SECTION)"; Sections "{components}"; Architectures "{arches}"; }} """ class PoolBuilder: def __init__( self, logger, series: str, apt_state: AptStateManager, rootdir: pathlib.Path ): self.logger = logger self.series = series self.apt_state = apt_state self.rootdir = rootdir self.arches: set[str] = set() self._present_components: set[str] = set() def add_packages(self, pkglist: list[PackageInfo]): for pkg_info in pkglist: if pkg_info.architecture != "all": self.arches.add(pkg_info.architecture) self.apt_state.download(self.rootdir, pkg_info) def make_packages(self) -> None: with self.logger.logged("making Packages files"): with tempfile.TemporaryDirectory() as tmpdir: scratchdir = pathlib.Path(tmpdir) with self.logger.logged("scanning for packages"): for component in ["main", "restricted", "universe", "multiverse"]: if not self.rootdir.joinpath("pool", component).is_dir(): continue self._present_components.add(component) for arch in self.arches: self.rootdir.joinpath( "dists", self.series, component, f"binary-{arch}" ).mkdir(parents=True) proc = self.logger.run( ["find", f"pool/{component}"], stdout=subprocess.PIPE, cwd=self.rootdir, encoding="utf-8", check=True, ) scratchdir.joinpath(f"filelist_{component}").write_text( "\n".join(sorted(proc.stdout.splitlines())) ) with self.logger.logged("writing apt-ftparchive config"): scratchdir.joinpath("apt-ftparchive-db").mkdir() generate_path = scratchdir.joinpath("generate-binary") generate_path.write_text( generate_template.format( arches=" ".join(self.arches), series=self.series, root=self.rootdir.resolve(), scratch=scratchdir.resolve(), components=" ".join(self._present_components), ) ) with self.logger.logged("running apt-ftparchive generate"): self.logger.run( [ "apt-ftparchive", "--no-contents", "--no-md5", "--no-sha1", "--no-sha512", "generate", generate_path, ], check=True, ) def make_release(self) -> pathlib.Path: # Build the Release file by merging metadata from the mirror with # checksums for our pool. We can't just use apt-ftparchive's Release # output directly because: # 1. apt-ftparchive doesn't know about Origin, Label, Suite, Version, # Codename, etc. - these come from the mirror and maintain package # provenance # 2. We keep the mirror's Date (when packages were released) rather than # apt-ftparchive's Date (when we ran the command) # 3. We need to override Architectures/Components to match our pool # # There may be a cleaner way (apt-get indextargets?) but this works. with self.logger.logged("making Release file"): in_release = self.apt_state.in_release_path() cp_mirror_release = self.logger.run( ["gpg", "--verify", "--output", "-", in_release], stdout=subprocess.PIPE, encoding="utf-8", check=False, ) if cp_mirror_release.returncode not in (0, 2): # gpg returns code 2 when the public key the InRelease is # signed with is not available, which is most of the time. raise Exception("gpg failed") mirror_release_lines = cp_mirror_release.stdout.splitlines() release_dir = self.rootdir.joinpath("dists", self.series) af_release_lines = self.logger.run( [ "apt-ftparchive", "--no-contents", "--no-md5", "--no-sha1", "--no-sha512", "release", ".", ], stdout=subprocess.PIPE, encoding="utf-8", cwd=release_dir, check=True, ).stdout.splitlines() # Build the final Release file by merging mirror metadata with pool # checksums. # Strategy: # 1. Take metadata fields (Suite, Origin, etc.) from the mirror's InRelease # 2. Override Architectures and Components to match what's actually in our # pool # 3. Skip the mirror's checksum sections (MD5Sum, SHA256, etc.) because they # don't apply to our pool # 4. Skip Acquire-By-Hash since we don't use it # 5. Append checksums from apt-ftparchive (but not the Date field) release_lines = [] skipping = False for line in mirror_release_lines: if line.startswith("Architectures:"): line = "Architectures: " + " ".join(sorted(self.arches)) elif line.startswith("Components:"): line = "Components: " + " ".join(sorted(self._present_components)) elif line.startswith("MD5") or line.startswith("SHA"): # Start of a checksum section - skip this and indented lines below # it skipping = True elif not line.startswith(" "): # Non-indented line means we've left the checksum section if we were # in one. skipping = False if line.startswith("Acquire-By-Hash"): continue if not skipping: release_lines.append(line) # Append checksums from apt-ftparchive, but skip its Date field # (we want to keep the Date from the mirror release) for line in af_release_lines: if not line.startswith("Date"): release_lines.append(line) release_path = release_dir.joinpath("Release") release_path.write_text("\n".join(release_lines)) return release_path