summaryrefslogtreecommitdiffstats
path: root/src/debputy/deb_packaging_support.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/deb_packaging_support.py')
-rw-r--r--src/debputy/deb_packaging_support.py1489
1 files changed, 1489 insertions, 0 deletions
diff --git a/src/debputy/deb_packaging_support.py b/src/debputy/deb_packaging_support.py
new file mode 100644
index 0000000..4cb4e8f
--- /dev/null
+++ b/src/debputy/deb_packaging_support.py
@@ -0,0 +1,1489 @@
+import collections
+import contextlib
+import dataclasses
+import datetime
+import functools
+import hashlib
+import itertools
+import operator
+import os
+import re
+import subprocess
+import tempfile
+import textwrap
+from contextlib import ExitStack
+from tempfile import mkstemp
+from typing import (
+ Iterable,
+ List,
+ Optional,
+ Set,
+ Dict,
+ Sequence,
+ Tuple,
+ Iterator,
+ Literal,
+ TypeVar,
+ FrozenSet,
+ cast,
+)
+
+import debian.deb822
+from debian.changelog import Changelog
+from debian.deb822 import Deb822
+
+from debputy._deb_options_profiles import DebBuildOptionsAndProfiles
+from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable
+from debputy.debhelper_emulation import (
+ dhe_install_pkg_file_as_ctrl_file_if_present,
+ dhe_dbgsym_root_dir,
+)
+from debputy.elf_util import find_all_elf_files, ELF_MAGIC
+from debputy.exceptions import DebputyDpkgGensymbolsError
+from debputy.filesystem_scan import FSPath, FSROOverlay
+from debputy.highlevel_manifest import (
+ HighLevelManifest,
+ PackageTransformationDefinition,
+ BinaryPackageData,
+)
+from debputy.maintscript_snippet import (
+ ALL_CONTROL_SCRIPTS,
+ MaintscriptSnippetContainer,
+ STD_CONTROL_SCRIPTS,
+)
+from debputy.packages import BinaryPackage, SourcePackage
+from debputy.packaging.alternatives import process_alternatives
+from debputy.packaging.debconf_templates import process_debconf_templates
+from debputy.packaging.makeshlibs import (
+ compute_shlibs,
+ ShlibsContent,
+ generate_shlib_dirs,
+)
+from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
+from debputy.plugin.api.impl import ServiceRegistryImpl
+from debputy.plugin.api.impl_types import (
+ MetadataOrMaintscriptDetector,
+ PackageDataTable,
+)
+from debputy.plugin.api.spec import (
+ FlushableSubstvars,
+ VirtualPath,
+ PackageProcessingContext,
+)
+from debputy.util import (
+ _error,
+ ensure_dir,
+ assume_not_none,
+ perl_module_dirs,
+ perlxs_api_dependency,
+ detect_fakeroot,
+ grouper,
+ _info,
+ xargs,
+ escape_shell,
+ generated_content_dir,
+ print_command,
+ _warn,
+)
+
+VP = TypeVar("VP", bound=VirtualPath, covariant=True)
+
+_T64_REGEX = re.compile("^lib.*t64(?:-nss)?$")
+_T64_PROVIDES = "t64:Provides"
+
+
+def generate_md5sums_file(control_output_dir: str, fs_root: VirtualPath) -> None:
+ conffiles = os.path.join(control_output_dir, "conffiles")
+ md5sums = os.path.join(control_output_dir, "md5sums")
+ exclude = set()
+ if os.path.isfile(conffiles):
+ with open(conffiles, "rt") as fd:
+ for line in fd:
+ if not line.startswith("/"):
+ continue
+ exclude.add("." + line.rstrip("\n"))
+ had_content = False
+ files = sorted(
+ (
+ path
+ for path in fs_root.all_paths()
+ if path.is_file and path.path not in exclude
+ ),
+ # Sort in the same order as dh_md5sums, which is not quite the same as dpkg/`all_paths()`
+ # Compare `.../doc/...` vs `.../doc-base/...` if you want to see the difference between
+ # the two approaches.
+ key=lambda p: p.path,
+ )
+ with open(md5sums, "wt") as md5fd:
+ for member in files:
+ path = member.path
+ assert path.startswith("./")
+ path = path[2:]
+ with member.open(byte_io=True) as f:
+ file_hash = hashlib.md5()
+ while chunk := f.read(8192):
+ file_hash.update(chunk)
+ had_content = True
+ md5fd.write(f"{file_hash.hexdigest()} {path}\n")
+ if not had_content:
+ os.unlink(md5sums)
+
+
+def install_or_generate_conffiles(
+ binary_package: BinaryPackage,
+ root_dir: str,
+ fs_root: VirtualPath,
+ debian_dir: VirtualPath,
+) -> None:
+ conffiles_dest = os.path.join(root_dir, "conffiles")
+ dhe_install_pkg_file_as_ctrl_file_if_present(
+ debian_dir,
+ binary_package,
+ "conffiles",
+ root_dir,
+ 0o0644,
+ )
+ etc_dir = fs_root.lookup("etc")
+ if etc_dir:
+ _add_conffiles(conffiles_dest, (p for p in etc_dir.all_paths() if p.is_file))
+ if os.path.isfile(conffiles_dest):
+ os.chmod(conffiles_dest, 0o0644)
+
+
+PERL_DEP_PROGRAM = 1
+PERL_DEP_INDEP_PM_MODULE = 2
+PERL_DEP_XS_MODULE = 4
+PERL_DEP_ARCH_PM_MODULE = 8
+PERL_DEP_MA_ANY_INCOMPATIBLE_TYPES = ~(PERL_DEP_PROGRAM | PERL_DEP_INDEP_PM_MODULE)
+
+
+@functools.lru_cache(2) # In practice, param will be "perl" or "perl-base"
+def _dpkg_perl_version(package: str) -> str:
+ dpkg_version = None
+ lines = (
+ subprocess.check_output(["dpkg", "-s", package])
+ .decode("utf-8")
+ .splitlines(keepends=False)
+ )
+ for line in lines:
+ if line.startswith("Version: "):
+ dpkg_version = line[8:].strip()
+ break
+ assert dpkg_version is not None
+ return dpkg_version
+
+
+def handle_perl_code(
+ dctrl_bin: BinaryPackage,
+ dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
+ fs_root: FSPath,
+ substvars: FlushableSubstvars,
+) -> None:
+ known_perl_inc_dirs = perl_module_dirs(dpkg_architecture_variables, dctrl_bin)
+ detected_dep_requirements = 0
+
+ # MakeMaker always makes lib and share dirs, but typically only one directory is actually used.
+ for perl_inc_dir in known_perl_inc_dirs:
+ p = fs_root.lookup(perl_inc_dir)
+ if p and p.is_dir:
+ p.prune_if_empty_dir()
+
+ # FIXME: 80% of this belongs in a metadata detector, but that requires us to expose .walk() in the public API,
+ # which will not be today.
+ for d, pm_mode in [
+ (known_perl_inc_dirs.vendorlib, PERL_DEP_INDEP_PM_MODULE),
+ (known_perl_inc_dirs.vendorarch, PERL_DEP_ARCH_PM_MODULE),
+ ]:
+ inc_dir = fs_root.lookup(d)
+ if not inc_dir:
+ continue
+ for path in inc_dir.all_paths():
+ if not path.is_file:
+ continue
+ if path.name.endswith(".so"):
+ detected_dep_requirements |= PERL_DEP_XS_MODULE
+ elif path.name.endswith(".pm"):
+ detected_dep_requirements |= pm_mode
+
+ for path, children in fs_root.walk():
+ if path.path == "./usr/share/doc":
+ children.clear()
+ continue
+ if (
+ not path.is_file
+ or not path.has_fs_path
+ or not (path.is_executable or path.name.endswith(".pl"))
+ ):
+ continue
+
+ interpreter = path.interpreter()
+ if interpreter is not None and interpreter.command_full_basename == "perl":
+ detected_dep_requirements |= PERL_DEP_PROGRAM
+
+ if not detected_dep_requirements:
+ return
+ dpackage = "perl"
+ # FIXME: Currently, dh_perl supports perl-base via manual toggle.
+
+ dependency = dpackage
+ if not (detected_dep_requirements & PERL_DEP_MA_ANY_INCOMPATIBLE_TYPES):
+ dependency += ":any"
+
+ if detected_dep_requirements & PERL_DEP_XS_MODULE:
+ dpkg_version = _dpkg_perl_version(dpackage)
+ dependency += f" (>= {dpkg_version})"
+ substvars.add_dependency("perl:Depends", dependency)
+
+ if detected_dep_requirements & (PERL_DEP_XS_MODULE | PERL_DEP_ARCH_PM_MODULE):
+ substvars.add_dependency("perl:Depends", perlxs_api_dependency())
+
+
+def usr_local_transformation(dctrl: BinaryPackage, fs_root: VirtualPath) -> None:
+ path = fs_root.lookup("./usr/local")
+ if path and any(path.iterdir):
+ # There are two key issues:
+ # 1) Getting the generated maintscript carried on to the final maintscript
+ # 2) Making sure that manifest created directories do not trigger the "unused error".
+ _error(
+ f"Replacement of /usr/local paths is currently not supported in debputy (triggered by: {dctrl.name})."
+ )
+
+
+def _find_and_analyze_systemd_service_files(
+ fs_root: VirtualPath,
+ systemd_service_dir: Literal["system", "user"],
+) -> Iterable[VirtualPath]:
+ service_dirs = [
+ f"./usr/lib/systemd/{systemd_service_dir}",
+ f"./lib/systemd/{systemd_service_dir}",
+ ]
+ aliases: Dict[str, List[str]] = collections.defaultdict(list)
+ seen = set()
+ all_files = []
+
+ for d in service_dirs:
+ system_dir = fs_root.lookup(d)
+ if not system_dir:
+ continue
+ for child in system_dir.iterdir:
+ if child.is_symlink:
+ dest = os.path.basename(child.readlink())
+ aliases[dest].append(child.name)
+ elif child.is_file and child.name not in seen:
+ seen.add(child.name)
+ all_files.append(child)
+
+ return all_files
+
+
+def detect_systemd_user_service_files(
+ dctrl: BinaryPackage,
+ fs_root: VirtualPath,
+) -> None:
+ for service_file in _find_and_analyze_systemd_service_files(fs_root, "user"):
+ _error(
+ f'Sorry, systemd user services files are not supported at the moment (saw "{service_file.path}"'
+ f" in {dctrl.name})"
+ )
+
+
+# Generally, this should match the release date of oldstable or oldoldstable
+_DCH_PRUNE_CUT_OFF_DATE = datetime.date(2019, 7, 6)
+_DCH_MIN_NUM_OF_ENTRIES = 4
+
+
+def _prune_dch_file(
+ package: BinaryPackage,
+ path: VirtualPath,
+ is_changelog: bool,
+ keep_versions: Optional[Set[str]],
+ *,
+ trim: bool = True,
+) -> Tuple[bool, Optional[Set[str]]]:
+ # TODO: Process `d/changelog` once
+ # Note we cannot assume that changelog_file is always `d/changelog` as you can have
+ # per-package changelogs.
+ with path.open() as fd:
+ dch = Changelog(fd)
+ shortened = False
+ important_entries = 0
+ binnmu_entries = []
+ if is_changelog:
+ kept_entries = []
+ for block in dch:
+ if block.other_pairs.get("binary-only", "no") == "yes":
+ # Always keep binNMU entries (they are always in the top) and they do not count
+ # towards our kept_entries limit
+ binnmu_entries.append(block)
+ continue
+ block_date = block.date
+ if block_date is None:
+ _error(f"The Debian changelog was missing date in sign off line")
+ entry_date = datetime.datetime.strptime(
+ block_date, "%a, %d %b %Y %H:%M:%S %z"
+ ).date()
+ if (
+ trim
+ and entry_date < _DCH_PRUNE_CUT_OFF_DATE
+ and important_entries >= _DCH_MIN_NUM_OF_ENTRIES
+ ):
+ shortened = True
+ break
+ # Match debhelper in incrementing after the check.
+ important_entries += 1
+ kept_entries.append(block)
+ else:
+ assert keep_versions is not None
+ # The NEWS files should match the version for the dch to avoid lintian warnings.
+ # If that means we remove all entries in the NEWS file, then we delete the NEWS
+ # file (see #1021607)
+ kept_entries = [b for b in dch if b.version in keep_versions]
+ shortened = len(dch) > len(kept_entries)
+ if shortened and not kept_entries:
+ path.unlink()
+ return True, None
+
+ if not shortened and not binnmu_entries:
+ return False, None
+
+ parent_dir = assume_not_none(path.parent_dir)
+
+ with path.replace_fs_path_content() as fs_path, open(
+ fs_path, "wt", encoding="utf-8"
+ ) as fd:
+ for entry in kept_entries:
+ fd.write(str(entry))
+
+ if is_changelog and shortened:
+ # For changelog (rather than NEWS) files, add a note about how to
+ # get the full version.
+ msg = textwrap.dedent(
+ f"""\
+ # Older entries have been removed from this changelog.
+ # To read the complete changelog use `apt changelog {package.name}`.
+ """
+ )
+ fd.write(msg)
+
+ if binnmu_entries:
+ if package.is_arch_all:
+ _error(
+ f"The package {package.name} is architecture all, but it is built during a binNMU. A binNMU build"
+ " must not include architecture all packages"
+ )
+
+ with parent_dir.add_file(
+ f"{path.name}.{package.resolved_architecture}"
+ ) as binnmu_changelog, open(
+ binnmu_changelog.fs_path,
+ "wt",
+ encoding="utf-8",
+ ) as binnmu_fd:
+ for entry in binnmu_entries:
+ binnmu_fd.write(str(entry))
+
+ if not shortened:
+ return False, None
+ return True, {b.version for b in kept_entries}
+
+
+def fixup_debian_changelog_and_news_file(
+ dctrl: BinaryPackage,
+ fs_root: VirtualPath,
+ is_native: bool,
+ build_env: DebBuildOptionsAndProfiles,
+) -> None:
+ doc_dir = fs_root.lookup(f"./usr/share/doc/{dctrl.name}")
+ if not doc_dir:
+ return
+ changelog = doc_dir.get("changelog.Debian")
+ if changelog and is_native:
+ changelog.name = "changelog"
+ elif is_native:
+ changelog = doc_dir.get("changelog")
+
+ trim = False if "notrimdch" in build_env.deb_build_options else True
+
+ kept_entries = None
+ pruned_changelog = False
+ if changelog and changelog.has_fs_path:
+ pruned_changelog, kept_entries = _prune_dch_file(
+ dctrl, changelog, True, None, trim=trim
+ )
+
+ if not trim:
+ return
+
+ news_file = doc_dir.get("NEWS.Debian")
+ if news_file and news_file.has_fs_path and pruned_changelog:
+ _prune_dch_file(dctrl, news_file, False, kept_entries)
+
+
+_UPSTREAM_CHANGELOG_SOURCE_DIRS = [
+ ".",
+ "doc",
+ "docs",
+]
+_UPSTREAM_CHANGELOG_NAMES = {
+ # The value is a priority to match the debhelper order.
+ # - The suffix weights heavier than the basename (because that is what debhelper did)
+ #
+ # We list the name/suffix in order of priority in the code. That makes it easier to
+ # see the priority directly, but it gives the "lowest" value to the most important items
+ f"{n}{s}": (sw, nw)
+ for (nw, n), (sw, s) in itertools.product(
+ enumerate(["changelog", "changes", "history"], start=1),
+ enumerate(["", ".txt", ".md", ".rst"], start=1),
+ )
+}
+_NONE_TUPLE = (None, (0, 0))
+
+
+def _detect_upstream_changelog(names: Iterable[str]) -> Optional[str]:
+ matches = []
+ for name in names:
+ match_priority = _UPSTREAM_CHANGELOG_NAMES.get(name.lower())
+ if match_priority is not None:
+ matches.append((name, match_priority))
+ return min(matches, default=_NONE_TUPLE, key=operator.itemgetter(1))[0]
+
+
+def install_upstream_changelog(
+ dctrl_bin: BinaryPackage,
+ fs_root: FSPath,
+ source_fs_root: VirtualPath,
+) -> None:
+ doc_dir = f"./usr/share/doc/{dctrl_bin.name}"
+ bdir = fs_root.lookup(doc_dir)
+ if bdir and not bdir.is_dir:
+ # "/usr/share/doc/foo -> bar" symlink. Avoid croaking on those per:
+ # https://salsa.debian.org/debian/debputy/-/issues/49
+ return
+
+ if bdir:
+ if bdir.get("changelog") or bdir.get("changelog.gz"):
+ # Upstream's build system already provided the changelog with the correct name.
+ # Accept that as the canonical one.
+ return
+ upstream_changelog = _detect_upstream_changelog(
+ p.name for p in bdir.iterdir if p.is_file and p.has_fs_path and p.size > 0
+ )
+ if upstream_changelog:
+ p = bdir.lookup(upstream_changelog)
+ assert p is not None # Mostly as a typing hint
+ p.name = "changelog"
+ return
+ for dirname in _UPSTREAM_CHANGELOG_SOURCE_DIRS:
+ dir_path = source_fs_root.lookup(dirname)
+ if not dir_path or not dir_path.is_dir:
+ continue
+ changelog_name = _detect_upstream_changelog(
+ p.name
+ for p in dir_path.iterdir
+ if p.is_file and p.has_fs_path and p.size > 0
+ )
+ if changelog_name:
+ if bdir is None:
+ bdir = fs_root.mkdirs(doc_dir)
+ bdir.insert_file_from_fs_path(
+ "changelog",
+ dir_path[changelog_name].fs_path,
+ )
+ break
+
+
+@dataclasses.dataclass(slots=True)
+class _ElfInfo:
+ path: VirtualPath
+ fs_path: str
+ is_stripped: Optional[bool] = None
+ build_id: Optional[str] = None
+ dbgsym: Optional[FSPath] = None
+
+
+def _elf_static_lib_walk_filter(
+ fs_path: VirtualPath,
+ children: List[VP],
+) -> bool:
+ if (
+ fs_path.name == ".build-id"
+ and assume_not_none(fs_path.parent_dir).name == "debug"
+ ):
+ children.clear()
+ return False
+ # Deal with some special cases, where certain files are not supposed to be stripped in a given directory
+ if "debug/" in fs_path.path or fs_path.name.endswith("debug/"):
+ # FIXME: We need a way to opt out of this per #468333/#1016122
+ for so_file in (f for f in list(children) if f.name.endswith(".so")):
+ children.remove(so_file)
+ if "/guile/" in fs_path.path or fs_path.name == "guile":
+ for go_file in (f for f in list(children) if f.name.endswith(".go")):
+ children.remove(go_file)
+ return True
+
+
+@contextlib.contextmanager
+def _all_elf_files(fs_root: VirtualPath) -> Iterator[Dict[str, _ElfInfo]]:
+ all_elf_files = find_all_elf_files(
+ fs_root,
+ walk_filter=_elf_static_lib_walk_filter,
+ )
+ if not all_elf_files:
+ yield {}
+ return
+ with ExitStack() as cm_stack:
+ resolved = (
+ (p, cm_stack.enter_context(p.replace_fs_path_content()))
+ for p in all_elf_files
+ )
+ elf_info = {
+ fs_path: _ElfInfo(
+ path=assume_not_none(fs_root.lookup(detached_path.path)),
+ fs_path=fs_path,
+ )
+ for detached_path, fs_path in resolved
+ }
+ _resolve_build_ids(elf_info)
+ yield elf_info
+
+
+def _find_all_static_libs(
+ fs_root: FSPath,
+) -> Iterator[FSPath]:
+ for path, children in fs_root.walk():
+ # Matching the logic of dh_strip for now.
+ if not _elf_static_lib_walk_filter(path, children):
+ continue
+ if not path.is_file:
+ continue
+ if path.name.startswith("lib") and path.name.endswith("_g.a"):
+ # _g.a are historically ignored. I do not remember why, but guessing the "_g" is
+ # an encoding of gcc's -g parameter into the filename (with -g meaning "I want debug
+ # symbols")
+ continue
+ if not path.has_fs_path:
+ continue
+ with path.open(byte_io=True) as fd:
+ magic = fd.read(8)
+ if magic not in (b"!<arch>\n", b"!<thin>\n"):
+ continue
+ # Maybe we should see if the first file looks like an index file.
+ # Three random .a samples suggests the index file is named "/"
+ # Not sure if we should skip past it and then do the ELF check or just assume
+ # that "index => static lib".
+ data = fd.read(1024 * 1024)
+ if b"\0" not in data and ELF_MAGIC not in data:
+ continue
+ yield path
+
+
+@contextlib.contextmanager
+def _all_static_libs(fs_root: FSPath) -> Iterator[List[str]]:
+ all_static_libs = list(_find_all_static_libs(fs_root))
+ if not all_static_libs:
+ yield []
+ return
+ with ExitStack() as cm_stack:
+ resolved: List[str] = [
+ cm_stack.enter_context(p.replace_fs_path_content()) for p in all_static_libs
+ ]
+ yield resolved
+
+
+_FILE_BUILD_ID_RE = re.compile(rb"BuildID(?:\[\S+\])?=([A-Fa-f0-9]+)")
+
+
+def _resolve_build_ids(elf_info: Dict[str, _ElfInfo]) -> None:
+ static_cmd = ["file", "-00", "-N"]
+ if detect_fakeroot():
+ static_cmd.append("--no-sandbox")
+
+ for cmd in xargs(static_cmd, (i.fs_path for i in elf_info.values())):
+ _info(f"Looking up build-ids via: {escape_shell(*cmd)}")
+ output = subprocess.check_output(cmd)
+
+ # Trailing "\0" gives an empty element in the end when splitting, so strip it out
+ lines = output.rstrip(b"\0").split(b"\0")
+
+ for fs_path_b, verdict in grouper(lines, 2, incomplete="strict"):
+ fs_path = fs_path_b.decode("utf-8")
+ info = elf_info[fs_path]
+ info.is_stripped = b"not stripped" not in verdict
+ m = _FILE_BUILD_ID_RE.search(verdict)
+ if m:
+ info.build_id = m.group(1).decode("utf-8")
+
+
+def _make_debug_file(
+ objcopy: str, fs_path: str, build_id: str, dbgsym_fs_root: FSPath
+) -> FSPath:
+ dbgsym_dirname = f"./usr/lib/debug/.build-id/{build_id[0:2]}/"
+ dbgsym_basename = f"{build_id[2:]}.debug"
+ dbgsym_dir = dbgsym_fs_root.mkdirs(dbgsym_dirname)
+ if dbgsym_basename in dbgsym_dir:
+ return dbgsym_dir[dbgsym_basename]
+ # objcopy is a pain and includes the basename verbatim when you do `--add-gnu-debuglink` without having an option
+ # to overwrite the physical basename. So we have to ensure that the physical basename matches the installed
+ # basename.
+ with dbgsym_dir.add_file(
+ dbgsym_basename,
+ unlink_if_exists=False,
+ fs_basename_matters=True,
+ subdir_key="dbgsym-build-ids",
+ ) as dbgsym:
+ try:
+ subprocess.check_call(
+ [
+ objcopy,
+ "--only-keep-debug",
+ "--compress-debug-sections",
+ fs_path,
+ dbgsym.fs_path,
+ ]
+ )
+ except subprocess.CalledProcessError:
+ full_command = (
+ f"{objcopy} --only-keep-debug --compress-debug-sections"
+ f" {escape_shell(fs_path, dbgsym.fs_path)}"
+ )
+ _error(
+ f"Attempting to create a .debug file failed. Please review the error message from {objcopy} to"
+ f" understand what went wrong. Full command was: {full_command}"
+ )
+ return dbgsym
+
+
+def _strip_binary(strip: str, options: List[str], paths: Iterable[str]) -> None:
+ # We assume the paths are obtained via `p.replace_fs_path_content()`,
+ # which is the case at the time of written and should remain so forever.
+ it = iter(paths)
+ first = next(it, None)
+ if first is None:
+ return
+ static_cmd = [strip]
+ static_cmd.extend(options)
+
+ for cmd in xargs(static_cmd, itertools.chain((first,), (f for f in it))):
+ _info(f"Removing unnecessary ELF debug info via: {escape_shell(*cmd)}")
+ try:
+ subprocess.check_call(
+ cmd,
+ stdin=subprocess.DEVNULL,
+ restore_signals=True,
+ )
+ except subprocess.CalledProcessError:
+ _error(
+ f"Attempting to remove ELF debug info failed. Please review the error from {strip} above"
+ f" understand what went wrong."
+ )
+
+
+def _attach_debug(objcopy: str, elf_binary: VirtualPath, dbgsym: FSPath) -> None:
+ dbgsym_fs_path: str
+ with dbgsym.replace_fs_path_content() as dbgsym_fs_path:
+ cmd = [objcopy, "--add-gnu-debuglink", dbgsym_fs_path, elf_binary.fs_path]
+ print_command(*cmd)
+ try:
+ subprocess.check_call(cmd)
+ except subprocess.CalledProcessError:
+ _error(
+ f"Attempting to attach ELF debug link to ELF binary failed. Please review the error from {objcopy}"
+ f" above understand what went wrong."
+ )
+
+
+def _run_dwz(
+ dctrl: BinaryPackage,
+ dbgsym_fs_root: FSPath,
+ unstripped_elf_info: List[_ElfInfo],
+) -> None:
+ if not unstripped_elf_info or dctrl.is_udeb:
+ return
+ dwz_cmd = ["dwz"]
+ dwz_ma_dir_name = f"usr/lib/debug/.dwz/{dctrl.deb_multiarch}"
+ dwz_ma_basename = f"{dctrl.name}.debug"
+ multifile = f"{dwz_ma_dir_name}/{dwz_ma_basename}"
+ build_time_multifile = None
+ if len(unstripped_elf_info) > 1:
+ fs_content_dir = generated_content_dir()
+ fd, build_time_multifile = mkstemp(suffix=dwz_ma_basename, dir=fs_content_dir)
+ os.close(fd)
+ dwz_cmd.append(f"-m{build_time_multifile}")
+ dwz_cmd.append(f"-M/{multifile}")
+
+ # TODO: configuration for disabling multi-file and tweaking memory limits
+
+ dwz_cmd.extend(e.fs_path for e in unstripped_elf_info)
+
+ _info(f"Deduplicating ELF debug info via: {escape_shell(*dwz_cmd)}")
+ try:
+ subprocess.check_call(dwz_cmd)
+ except subprocess.CalledProcessError:
+ _error(
+ "Attempting to deduplicate ELF info via dwz failed. Please review the output from dwz above"
+ " to understand what went wrong."
+ )
+ if build_time_multifile is not None and os.stat(build_time_multifile).st_size > 0:
+ dwz_dir = dbgsym_fs_root.mkdirs(dwz_ma_dir_name)
+ dwz_dir.insert_file_from_fs_path(
+ dwz_ma_basename,
+ build_time_multifile,
+ mode=0o644,
+ require_copy_on_write=False,
+ follow_symlinks=False,
+ )
+
+
+def relocate_dwarves_into_dbgsym_packages(
+ dctrl: BinaryPackage,
+ package_fs_root: FSPath,
+ dbgsym_fs_root: VirtualPath,
+) -> List[str]:
+ # FIXME: hardlinks
+ with _all_static_libs(package_fs_root) as all_static_files:
+ if all_static_files:
+ strip = dctrl.cross_command("strip")
+ _strip_binary(
+ strip,
+ [
+ "--strip-debug",
+ "--remove-section=.comment",
+ "--remove-section=.note",
+ "--enable-deterministic-archives",
+ "-R",
+ ".gnu.lto_*",
+ "-R",
+ ".gnu.debuglto_*",
+ "-N",
+ "__gnu_lto_slim",
+ "-N",
+ "__gnu_lto_v1",
+ ],
+ all_static_files,
+ )
+
+ with _all_elf_files(package_fs_root) as all_elf_files:
+ if not all_elf_files:
+ return []
+ objcopy = dctrl.cross_command("objcopy")
+ strip = dctrl.cross_command("strip")
+ unstripped_elf_info = list(
+ e for e in all_elf_files.values() if not e.is_stripped
+ )
+
+ _run_dwz(dctrl, dbgsym_fs_root, unstripped_elf_info)
+
+ for elf_info in unstripped_elf_info:
+ elf_info.dbgsym = _make_debug_file(
+ objcopy,
+ elf_info.fs_path,
+ assume_not_none(elf_info.build_id),
+ dbgsym_fs_root,
+ )
+
+ # Note: When run strip, we do so also on already stripped ELF binaries because that is what debhelper does!
+ # Executables (defined by mode)
+ _strip_binary(
+ strip,
+ ["--remove-section=.comment", "--remove-section=.note"],
+ (i.fs_path for i in all_elf_files.values() if i.path.is_executable),
+ )
+
+ # Libraries (defined by mode)
+ _strip_binary(
+ strip,
+ ["--remove-section=.comment", "--remove-section=.note", "--strip-unneeded"],
+ (i.fs_path for i in all_elf_files.values() if not i.path.is_executable),
+ )
+
+ for elf_info in unstripped_elf_info:
+ _attach_debug(
+ objcopy,
+ assume_not_none(elf_info.path),
+ assume_not_none(elf_info.dbgsym),
+ )
+
+ # Set for uniqueness
+ all_debug_info = sorted(
+ {assume_not_none(i.build_id) for i in unstripped_elf_info}
+ )
+
+ dbgsym_doc_dir = dbgsym_fs_root.mkdirs("./usr/share/doc/")
+ dbgsym_doc_dir.add_symlink(f"{dctrl.name}-dbgsym", dctrl.name)
+ return all_debug_info
+
+
+def run_package_processors(
+ manifest: HighLevelManifest,
+ package_metadata_context: PackageProcessingContext,
+ fs_root: VirtualPath,
+) -> None:
+ pppps = manifest.plugin_provided_feature_set.package_processors_in_order()
+ binary_package = package_metadata_context.binary_package
+ for pppp in pppps:
+ if not pppp.applies_to(binary_package):
+ continue
+ pppp.run_package_processor(fs_root, None, package_metadata_context)
+
+
+def cross_package_control_files(
+ package_data_table: PackageDataTable,
+ manifest: HighLevelManifest,
+) -> None:
+ errors = []
+ combined_shlibs = ShlibsContent()
+ shlibs_dir = None
+ shlib_dirs: List[str] = []
+ shlibs_local = manifest.debian_dir.get("shlibs.local")
+ if shlibs_local and shlibs_local.is_file:
+ with shlibs_local.open() as fd:
+ combined_shlibs.add_entries_from_shlibs_file(fd)
+
+ debputy_plugin_metadata = manifest.plugin_provided_feature_set.plugin_data[
+ "debputy"
+ ]
+
+ for binary_package_data in package_data_table:
+ binary_package = binary_package_data.binary_package
+ if binary_package.is_arch_all or not binary_package.should_be_acted_on:
+ continue
+ control_output_dir = assume_not_none(binary_package_data.control_output_dir)
+ fs_root = binary_package_data.fs_root
+ package_state = manifest.package_state_for(binary_package.name)
+ related_udeb_package = (
+ binary_package_data.package_metadata_context.related_udeb_package
+ )
+
+ udeb_package_name = related_udeb_package.name if related_udeb_package else None
+ ctrl = binary_package_data.ctrl_creator.for_plugin(
+ debputy_plugin_metadata,
+ "compute_shlibs",
+ )
+ try:
+ soname_info_list = compute_shlibs(
+ binary_package,
+ control_output_dir,
+ fs_root,
+ manifest,
+ udeb_package_name,
+ ctrl,
+ package_state.reserved_packager_provided_files,
+ combined_shlibs,
+ )
+ except DebputyDpkgGensymbolsError as e:
+ errors.append(e.message)
+ else:
+ if soname_info_list:
+ if shlibs_dir is None:
+ shlibs_dir = generated_content_dir(
+ subdir_key="_shlibs_materialization_dir"
+ )
+ generate_shlib_dirs(
+ binary_package,
+ shlibs_dir,
+ soname_info_list,
+ shlib_dirs,
+ )
+ if errors:
+ for error in errors:
+ _warn(error)
+ _error("Stopping due to the errors above")
+
+ generated_shlibs_local = None
+ if combined_shlibs:
+ if shlibs_dir is None:
+ shlibs_dir = generated_content_dir(subdir_key="_shlibs_materialization_dir")
+ generated_shlibs_local = os.path.join(shlibs_dir, "shlibs.local")
+ with open(generated_shlibs_local, "wt", encoding="utf-8") as fd:
+ combined_shlibs.write_to(fd)
+ _info(f"Generated {generated_shlibs_local} for dpkg-shlibdeps")
+
+ for binary_package_data in package_data_table:
+ binary_package = binary_package_data.binary_package
+ if binary_package.is_arch_all or not binary_package.should_be_acted_on:
+ continue
+ binary_package_data.ctrl_creator.shlibs_details = (
+ generated_shlibs_local,
+ shlib_dirs,
+ )
+
+
+def setup_control_files(
+ binary_package_data: BinaryPackageData,
+ manifest: HighLevelManifest,
+ dbgsym_fs_root: VirtualPath,
+ dbgsym_ids: List[str],
+ package_metadata_context: PackageProcessingContext,
+ *,
+ allow_ctrl_file_management: bool = True,
+) -> None:
+ binary_package = package_metadata_context.binary_package
+ control_output_dir = assume_not_none(binary_package_data.control_output_dir)
+ fs_root = binary_package_data.fs_root
+ package_state = manifest.package_state_for(binary_package.name)
+
+ feature_set: PluginProvidedFeatureSet = manifest.plugin_provided_feature_set
+ metadata_maintscript_detectors = feature_set.metadata_maintscript_detectors
+ substvars = binary_package_data.substvars
+
+ snippets = STD_CONTROL_SCRIPTS
+ if binary_package.is_udeb:
+ # FIXME: Add missing udeb scripts
+ snippets = ["postinst"]
+
+ if allow_ctrl_file_management:
+ process_alternatives(
+ binary_package,
+ fs_root,
+ package_state.reserved_packager_provided_files,
+ package_state.maintscript_snippets,
+ )
+ process_debconf_templates(
+ binary_package,
+ package_state.reserved_packager_provided_files,
+ package_state.maintscript_snippets,
+ substvars,
+ control_output_dir,
+ )
+
+ for service_manager_details in feature_set.service_managers.values():
+ service_registry = ServiceRegistryImpl(service_manager_details)
+ service_manager_details.service_detector(
+ fs_root,
+ service_registry,
+ package_metadata_context,
+ )
+
+ ctrl = binary_package_data.ctrl_creator.for_plugin(
+ service_manager_details.plugin_metadata,
+ service_manager_details.service_manager,
+ )
+ service_definitions = service_registry.detected_services
+ if not service_definitions:
+ continue
+ service_manager_details.service_integrator(
+ service_definitions,
+ ctrl,
+ package_metadata_context,
+ )
+
+ plugin_detector_definition: MetadataOrMaintscriptDetector
+ for plugin_detector_definition in itertools.chain.from_iterable(
+ metadata_maintscript_detectors.values()
+ ):
+ if not plugin_detector_definition.applies_to(binary_package):
+ continue
+ ctrl = binary_package_data.ctrl_creator.for_plugin(
+ plugin_detector_definition.plugin_metadata,
+ plugin_detector_definition.detector_id,
+ )
+ plugin_detector_definition.run_detector(
+ fs_root, ctrl, package_metadata_context
+ )
+
+ for script in snippets:
+ _generate_snippet(
+ control_output_dir,
+ script,
+ package_state.maintscript_snippets,
+ )
+
+ else:
+ if package_state.maintscript_snippets:
+ for script, snippet_container in package_state.maintscript_snippets.items():
+ for snippet in snippet_container.all_snippets():
+ source = snippet.definition_source
+ _error(
+ f"This integration mode cannot use maintscript snippets"
+ f' (since dh_installdeb has already been called). However, "{source}" triggered'
+ f" a snippet for {script}. Please remove the offending definition if it is from"
+ f" the manifest or file a bug if it is caused by a built-in rule."
+ )
+
+ dh_staging_dir = os.path.join("debian", binary_package.name, "DEBIAN")
+ try:
+ with os.scandir(dh_staging_dir) as it:
+ existing_control_files = [
+ f.path
+ for f in it
+ if f.is_file(follow_symlinks=False)
+ and f.name not in ("control", "md5sums")
+ ]
+ except FileNotFoundError:
+ existing_control_files = []
+
+ if existing_control_files:
+ cmd = ["cp", "-a"]
+ cmd.extend(existing_control_files)
+ cmd.append(control_output_dir)
+ print_command(*cmd)
+ subprocess.check_call(cmd)
+
+ if binary_package.is_udeb:
+ _generate_control_files(
+ binary_package_data.source_package,
+ binary_package,
+ package_state,
+ control_output_dir,
+ fs_root,
+ substvars,
+ # We never built udebs due to #797391, so skip over this information,
+ # when creating the udeb
+ None,
+ None,
+ )
+ return
+
+ generated_triggers = list(binary_package_data.ctrl_creator.generated_triggers())
+ if generated_triggers:
+ if not allow_ctrl_file_management:
+ for trigger in generated_triggers:
+ source = f"{trigger.provider.plugin_name}:{trigger.provider_source_id}"
+ _error(
+ f"This integration mode must not generate triggers"
+ f' (since dh_installdeb has already been called). However, "{source}" created'
+ f" a trigger. Please remove the offending definition if it is from"
+ f" the manifest or file a bug if it is caused by a built-in rule."
+ )
+
+ if generated_triggers:
+ dest_file = os.path.join(control_output_dir, "triggers")
+ with open(dest_file, "at", encoding="utf-8") as fd:
+ fd.writelines(
+ textwrap.dedent(
+ f"""\
+ # Added by {t.provider_source_id} from {t.provider.plugin_name}
+ {t.dpkg_trigger_type} {t.dpkg_trigger_target}
+ """
+ )
+ for t in generated_triggers
+ )
+ os.chmod(fd.fileno(), 0o644)
+ install_or_generate_conffiles(
+ binary_package,
+ control_output_dir,
+ fs_root,
+ manifest.debian_dir,
+ )
+ _generate_control_files(
+ binary_package_data.source_package,
+ binary_package,
+ package_state,
+ control_output_dir,
+ fs_root,
+ substvars,
+ dbgsym_fs_root,
+ dbgsym_ids,
+ )
+
+
+def _generate_snippet(
+ control_output_dir: str,
+ script: str,
+ maintscript_snippets: Dict[str, MaintscriptSnippetContainer],
+) -> None:
+ debputy_snippets = maintscript_snippets.get(script)
+ if debputy_snippets is None:
+ return
+ reverse = script in ("prerm", "postrm")
+ snippets = [
+ debputy_snippets.generate_snippet(reverse=reverse),
+ debputy_snippets.generate_snippet(snippet_order="service", reverse=reverse),
+ ]
+ if reverse:
+ snippets = reversed(snippets)
+ full_content = "".join(f"{s}\n" for s in filter(None, snippets))
+ if not full_content:
+ return
+ filename = os.path.join(control_output_dir, script)
+ with open(filename, "wt") as fd:
+ fd.write("#!/bin/sh\nset -e\n\n")
+ fd.write(full_content)
+ os.chmod(fd.fileno(), 0o755)
+
+
+def _add_conffiles(
+ conffiles_dest: str,
+ conffile_matches: Iterable[VirtualPath],
+) -> None:
+ with open(conffiles_dest, "at") as fd:
+ for conffile_match in conffile_matches:
+ conffile = conffile_match.absolute
+ assert conffile_match.is_file
+ fd.write(f"{conffile}\n")
+ if os.stat(conffiles_dest).st_size == 0:
+ os.unlink(conffiles_dest)
+
+
+def _ensure_base_substvars_defined(substvars: FlushableSubstvars) -> None:
+ for substvar in ("misc:Depends", "misc:Pre-Depends"):
+ if substvar not in substvars:
+ substvars[substvar] = ""
+
+
+def _compute_installed_size(fs_root: VirtualPath) -> int:
+ """Emulate dpkg-gencontrol's code for computing the default Installed-Size"""
+ size_in_kb = 0
+ hard_links = set()
+ for path in fs_root.all_paths():
+ if not path.is_dir and path.has_fs_path:
+ st = path.stat()
+ if st.st_nlink > 1:
+ hl_key = (st.st_dev, st.st_ino)
+ if hl_key in hard_links:
+ continue
+ hard_links.add(hl_key)
+ path_size = (st.st_size + 1023) // 1024
+ elif path.is_symlink:
+ path_size = (len(path.readlink()) + 1023) // 1024
+ else:
+ path_size = 1
+ size_in_kb += path_size
+ return size_in_kb
+
+
+def _generate_dbgsym_control_file_if_relevant(
+ binary_package: BinaryPackage,
+ dbgsym_fs_root: VirtualPath,
+ dbgsym_root_dir: str,
+ dbgsym_ids: str,
+ multi_arch: Optional[str],
+ extra_common_params: Sequence[str],
+) -> None:
+ section = binary_package.archive_section
+ component = ""
+ extra_params = []
+ if section is not None and "/" in section and not section.startswith("main/"):
+ component = section.split("/", 1)[1] + "/"
+ if multi_arch != "same":
+ extra_params.append("-UMulti-Arch")
+ extra_params.append("-UReplaces")
+ extra_params.append("-UBreaks")
+ dbgsym_control_dir = os.path.join(dbgsym_root_dir, "DEBIAN")
+ ensure_dir(dbgsym_control_dir)
+ # Pass it via cmd-line to make it more visible that we are providing the
+ # value. It also prevents the dbgsym package from picking up this value.
+ ctrl_fs_root = FSROOverlay.create_root_dir("DEBIAN", dbgsym_control_dir)
+ total_size = _compute_installed_size(dbgsym_fs_root) + _compute_installed_size(
+ ctrl_fs_root
+ )
+ extra_params.append(f"-VInstalled-Size={total_size}")
+ extra_params.extend(extra_common_params)
+
+ package = binary_package.name
+ dpkg_cmd = [
+ "dpkg-gencontrol",
+ f"-p{package}",
+ # FIXME: Support d/<pkg>.changelog at some point.
+ "-ldebian/changelog",
+ "-T/dev/null",
+ f"-P{dbgsym_root_dir}",
+ f"-DPackage={package}-dbgsym",
+ "-DDepends=" + package + " (= ${binary:Version})",
+ f"-DDescription=debug symbols for {package}",
+ f"-DSection={component}debug",
+ f"-DBuild-Ids={dbgsym_ids}",
+ "-UPre-Depends",
+ "-URecommends",
+ "-USuggests",
+ "-UEnhances",
+ "-UProvides",
+ "-UEssential",
+ "-UConflicts",
+ "-DPriority=optional",
+ "-UHomepage",
+ "-UImportant",
+ "-UBuilt-Using",
+ "-UStatic-Built-Using",
+ "-DAuto-Built-Package=debug-symbols",
+ "-UProtected",
+ *extra_params,
+ ]
+ print_command(*dpkg_cmd)
+ try:
+ subprocess.check_call(dpkg_cmd)
+ except subprocess.CalledProcessError:
+ _error(
+ f"Attempting to generate DEBIAN/control file for {package}-dbgsym failed. Please review the output from "
+ " dpkg-gencontrol above to understand what went wrong."
+ )
+ os.chmod(os.path.join(dbgsym_root_dir, "DEBIAN", "control"), 0o644)
+
+
+def _all_parent_directories_of(directories: Iterable[str]) -> Set[str]:
+ result = {"."}
+ for path in directories:
+ current = os.path.dirname(path)
+ while current and current not in result:
+ result.add(current)
+ current = os.path.dirname(current)
+ return result
+
+
+def _auto_compute_multi_arch(
+ binary_package: BinaryPackage,
+ control_output_dir: str,
+ fs_root: FSPath,
+) -> Optional[str]:
+ resolved_arch = binary_package.resolved_architecture
+ if resolved_arch == "all":
+ return None
+ if any(
+ script
+ for script in ALL_CONTROL_SCRIPTS
+ if os.path.isfile(os.path.join(control_output_dir, script))
+ ):
+ return None
+
+ resolved_multiarch = binary_package.deb_multiarch
+ assert resolved_arch != "all"
+ acceptable_no_descend_paths = {
+ f"./usr/lib/{resolved_multiarch}",
+ f"./usr/include/{resolved_multiarch}",
+ }
+ acceptable_files = {
+ f"./usr/share/doc/{binary_package.name}/{basename}"
+ for basename in (
+ "copyright",
+ "changelog.gz",
+ "changelog.Debian.gz",
+ f"changelog.Debian.{resolved_arch}.gz",
+ "NEWS.Debian",
+ "NEWS.Debian.gz",
+ "README.Debian",
+ "README.Debian.gz",
+ )
+ }
+ acceptable_intermediate_dirs = _all_parent_directories_of(
+ itertools.chain(acceptable_no_descend_paths, acceptable_files)
+ )
+
+ for fs_path, children in fs_root.walk():
+ path = fs_path.path
+ if path in acceptable_no_descend_paths:
+ children.clear()
+ continue
+ if path in acceptable_intermediate_dirs or path in acceptable_files:
+ continue
+ return None
+
+ return "same"
+
+
+@functools.lru_cache()
+def _has_t64_enabled() -> bool:
+ try:
+ output = subprocess.check_output(
+ ["dpkg-buildflags", "--query-features", "abi"]
+ ).decode()
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return False
+
+ for stanza in Deb822.iter_paragraphs(output):
+ if stanza.get("Feature") == "time64" and stanza.get("Enabled") == "yes":
+ return True
+ return False
+
+
+def _t64_migration_substvar(
+ binary_package: BinaryPackage,
+ control_output_dir: str,
+ substvars: FlushableSubstvars,
+) -> None:
+ name = binary_package.name
+ compat_name = binary_package.fields.get("X-Time64-Compat")
+ if compat_name is None and not _T64_REGEX.match(name):
+ return
+
+ if not any(
+ os.path.isfile(os.path.join(control_output_dir, n))
+ for n in ["symbols", "shlibs"]
+ ):
+ return
+
+ if compat_name is None:
+ compat_name = name.replace("t64", "", 1)
+ if compat_name == name:
+ raise AssertionError(
+ f"Failed to derive a t64 compat name for {name}. Please file a bug against debputy."
+ " As a work around, you can explicitly provide a X-Time64-Compat header in debian/control"
+ " where you specify the desired compat name."
+ )
+
+ arch_bits = binary_package.package_deb_architecture_variable("ARCH_BITS")
+
+ if arch_bits != "32" or not _has_t64_enabled():
+ substvars.add_dependency(
+ _T64_PROVIDES,
+ f"{compat_name} (= ${{binary:Version}})",
+ )
+ elif _T64_PROVIDES not in substvars:
+ substvars[_T64_PROVIDES] = ""
+
+
+@functools.lru_cache()
+def dpkg_field_list_pkg_dep() -> Sequence[str]:
+ try:
+ output = subprocess.check_output(
+ [
+ "perl",
+ "-MDpkg::Control::Fields",
+ "-e",
+ r'print "$_\n" for field_list_pkg_dep',
+ ]
+ )
+ except (FileNotFoundError, subprocess.CalledProcessError):
+ _error("Could not run perl -MDpkg::Control::Fields to get a list of fields")
+ return output.decode("utf-8").splitlines(keepends=False)
+
+
+def _handle_relationship_substvars(
+ source: SourcePackage,
+ dctrl: BinaryPackage,
+ substvars: FlushableSubstvars,
+) -> Optional[str]:
+ relationship_fields = dpkg_field_list_pkg_dep()
+ relationship_fields_lc = frozenset(x.lower() for x in relationship_fields)
+ substvar_fields = collections.defaultdict(list)
+ for substvar_name, substvar in substvars.as_substvar.items():
+ if substvar.assignment_operator == "$=" or ":" not in substvar_name:
+ # Automatically handled; no need for manual merging.
+ continue
+ _, field = substvar_name.rsplit(":", 1)
+ field_lc = field.lower()
+ if field_lc not in relationship_fields_lc:
+ continue
+ substvar_fields[field_lc].append("${" + substvar_name + "}")
+ if not substvar_fields:
+ return None
+
+ replacement_stanza = debian.deb822.Deb822(dctrl.fields)
+
+ for field_name in relationship_fields:
+ field_name_lc = field_name.lower()
+ addendum = substvar_fields.get(field_name_lc)
+ if addendum is None:
+ # No merging required
+ continue
+ substvars_part = ", ".join(addendum)
+ existing_value = replacement_stanza.get(field_name)
+
+ if existing_value is None or existing_value.isspace():
+ final_value = substvars_part
+ else:
+ existing_value = existing_value.rstrip().rstrip(",")
+ final_value = f"{existing_value}, {substvars_part}"
+ replacement_stanza[field_name] = final_value
+
+ tmpdir = generated_content_dir(package=dctrl)
+ with tempfile.NamedTemporaryFile(
+ mode="wb",
+ dir=tmpdir,
+ suffix="__DEBIAN_control",
+ delete=False,
+ ) as fd:
+ try:
+ cast("Any", source.fields).dump(fd)
+ except AttributeError:
+ debian.deb822.Deb822(source.fields).dump(fd)
+ fd.write(b"\n")
+ replacement_stanza.dump(fd)
+ return fd.name
+
+
+def _generate_control_files(
+ source_package: SourcePackage,
+ binary_package: BinaryPackage,
+ package_state: PackageTransformationDefinition,
+ control_output_dir: str,
+ fs_root: FSPath,
+ substvars: FlushableSubstvars,
+ dbgsym_root_fs: Optional[VirtualPath],
+ dbgsym_build_ids: Optional[List[str]],
+) -> None:
+ package = binary_package.name
+ extra_common_params = []
+ extra_params_specific = []
+ _ensure_base_substvars_defined(substvars)
+ if "Installed-Size" not in substvars:
+ # Pass it via cmd-line to make it more visible that we are providing the
+ # value. It also prevents the dbgsym package from picking up this value.
+ ctrl_fs_root = FSROOverlay.create_root_dir("DEBIAN", control_output_dir)
+ total_size = _compute_installed_size(fs_root) + _compute_installed_size(
+ ctrl_fs_root
+ )
+ extra_params_specific.append(f"-VInstalled-Size={total_size}")
+
+ ma_value = binary_package.fields.get("Multi-Arch")
+ if not binary_package.is_udeb and ma_value is None:
+ ma_value = _auto_compute_multi_arch(binary_package, control_output_dir, fs_root)
+ if ma_value is not None:
+ _info(
+ f'The package "{binary_package.name}" looks like it should be "Multi-Arch: {ma_value}" based'
+ ' on the contents and there is no explicit "Multi-Arch" field. Setting the Multi-Arch field'
+ ' accordingly in the binary. If this auto-correction is wrong, please add "Multi-Arch: no" to the'
+ ' relevant part of "debian/control" to disable this feature.'
+ )
+ extra_params_specific.append(f"-DMulti-Arch={ma_value}")
+ elif ma_value == "no":
+ extra_params_specific.append("-UMulti-Arch")
+
+ dbgsym_root_dir = dhe_dbgsym_root_dir(binary_package)
+ dbgsym_ids = " ".join(dbgsym_build_ids) if dbgsym_build_ids else ""
+ if package_state.binary_version is not None:
+ extra_common_params.append(f"-v{package_state.binary_version}")
+
+ _t64_migration_substvar(binary_package, control_output_dir, substvars)
+
+ with substvars.flush() as flushed_substvars:
+ if dbgsym_root_fs is not None and any(
+ f for f in dbgsym_root_fs.all_paths() if f.is_file
+ ):
+ _generate_dbgsym_control_file_if_relevant(
+ binary_package,
+ dbgsym_root_fs,
+ dbgsym_root_dir,
+ dbgsym_ids,
+ ma_value,
+ extra_common_params,
+ )
+ generate_md5sums_file(
+ os.path.join(dbgsym_root_dir, "DEBIAN"),
+ dbgsym_root_fs,
+ )
+ elif dbgsym_ids:
+ extra_common_params.append(f"-DBuild-Ids={dbgsym_ids}")
+
+ dctrl = _handle_relationship_substvars(
+ source_package,
+ binary_package,
+ substvars,
+ )
+ if dctrl is None:
+ dctrl = "debian/control"
+
+ ctrl_file = os.path.join(control_output_dir, "control")
+ dpkg_cmd = [
+ "dpkg-gencontrol",
+ f"-p{package}",
+ # FIXME: Support d/<pkg>.changelog at some point.
+ "-ldebian/changelog",
+ f"-c{dctrl}",
+ f"-T{flushed_substvars}",
+ f"-O{ctrl_file}",
+ f"-P{control_output_dir}",
+ *extra_common_params,
+ *extra_params_specific,
+ ]
+ print_command(*dpkg_cmd)
+ try:
+ subprocess.check_call(dpkg_cmd)
+ except subprocess.CalledProcessError:
+ _error(
+ f"Attempting to generate DEBIAN/control file for {package} failed. Please review the output from "
+ " dpkg-gencontrol above to understand what went wrong."
+ )
+ os.chmod(ctrl_file, 0o644)
+
+ if not binary_package.is_udeb:
+ generate_md5sums_file(control_output_dir, fs_root)