summaryrefslogtreecommitdiffstats
path: root/src/debputy/package_build/assemble_deb.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/package_build/assemble_deb.py')
-rw-r--r--src/debputy/package_build/assemble_deb.py255
1 files changed, 255 insertions, 0 deletions
diff --git a/src/debputy/package_build/assemble_deb.py b/src/debputy/package_build/assemble_deb.py
new file mode 100644
index 0000000..bed60e6
--- /dev/null
+++ b/src/debputy/package_build/assemble_deb.py
@@ -0,0 +1,255 @@
+import json
+import os
+import subprocess
+from typing import Optional, Sequence, List, Tuple
+
+from debputy import DEBPUTY_ROOT_DIR
+from debputy.commands.debputy_cmd.context import CommandContext
+from debputy.deb_packaging_support import setup_control_files
+from debputy.debhelper_emulation import dhe_dbgsym_root_dir
+from debputy.filesystem_scan import FSRootDir
+from debputy.highlevel_manifest import HighLevelManifest
+from debputy.intermediate_manifest import IntermediateManifest
+from debputy.plugin.api.impl_types import PackageDataTable
+from debputy.util import (
+ escape_shell,
+ _error,
+ compute_output_filename,
+ scratch_dir,
+ ensure_dir,
+ _warn,
+ assume_not_none,
+)
+
+
+_RRR_DEB_ASSEMBLY_KEYWORD = "debputy/deb-assembly"
+_WARNED_ABOUT_FALLBACK_ASSEMBLY = False
+
+
+def _serialize_intermediate_manifest(members: IntermediateManifest) -> str:
+ serial_format = [m.to_manifest() for m in members]
+ return json.dumps(serial_format)
+
+
+def determine_assembly_method(
+ package: str,
+ intermediate_manifest: IntermediateManifest,
+) -> Tuple[bool, bool, List[str]]:
+ paths_needing_root = (
+ tm for tm in intermediate_manifest if tm.owner != "root" or tm.group != "root"
+ )
+ matched_path = next(paths_needing_root, None)
+ if matched_path is None:
+ return False, False, []
+ rrr = os.environ.get("DEB_RULES_REQUIRES_ROOT")
+ if rrr and _RRR_DEB_ASSEMBLY_KEYWORD in rrr:
+ gain_root_cmd = os.environ.get("DEB_GAIN_ROOT_CMD")
+ if not gain_root_cmd:
+ _error(
+ "DEB_RULES_REQUIRES_ROOT contains a debputy keyword but DEB_GAIN_ROOT_CMD does not contain a "
+ '"gain root" command'
+ )
+ return True, False, gain_root_cmd.split()
+ if rrr == "no":
+ global _WARNED_ABOUT_FALLBACK_ASSEMBLY
+ if not _WARNED_ABOUT_FALLBACK_ASSEMBLY:
+ _warn(
+ 'Using internal assembly method due to "Rules-Requires-Root" being "no" and dpkg-deb assembly would'
+ " require (fake)root for binary packages that needs it."
+ )
+ _WARNED_ABOUT_FALLBACK_ASSEMBLY = True
+ return True, True, []
+
+ _error(
+ f'Due to the path "{matched_path.member_path}" in {package}, the package assembly will require (fake)root.'
+ " However, this command is not run as root nor was debputy requested to use a root command via"
+ f' "Rules-Requires-Root". Please consider adding "{_RRR_DEB_ASSEMBLY_KEYWORD}" to "Rules-Requires-Root"'
+ " in debian/control. Though, due to #1036865, you may have to revert to"
+ ' "Rules-Requires-Root: binary-targets" depending on which version of dpkg you need to support.'
+ ' Alternatively, you can set "Rules-Requires-Root: no" in debian/control and debputy will assemble'
+ " the package anyway. In this case, dpkg-deb will not be used, but the output should be bit-for-bit"
+ " compatible with what debputy would have produced with dpkg-deb (and root/fakeroot)."
+ )
+
+
+def assemble_debs(
+ context: CommandContext,
+ manifest: HighLevelManifest,
+ package_data_table: PackageDataTable,
+ is_dh_rrr_only_mode: bool,
+) -> None:
+ parsed_args = context.parsed_args
+ output_path = parsed_args.output
+ upstream_args = parsed_args.upstream_args
+ deb_materialize = str(DEBPUTY_ROOT_DIR / "deb_materialization.py")
+ mtime = context.mtime
+
+ for dctrl_bin in manifest.active_packages:
+ package = dctrl_bin.name
+ dbgsym_package_name = f"{package}-dbgsym"
+ dctrl_data = package_data_table[package]
+ fs_root = dctrl_data.fs_root
+ control_output_dir = assume_not_none(dctrl_data.control_output_dir)
+ package_metadata_context = dctrl_data.package_metadata_context
+ if (
+ dbgsym_package_name in package_data_table
+ or "noautodbgsym" in manifest.build_env.deb_build_options
+ or "noddebs" in manifest.build_env.deb_build_options
+ ):
+ # Discard the dbgsym part if it conflicts with a real package, or
+ # we were asked not to build it.
+ dctrl_data.dbgsym_info.dbgsym_fs_root = FSRootDir()
+ dctrl_data.dbgsym_info.dbgsym_ids.clear()
+ dbgsym_fs_root = dctrl_data.dbgsym_info.dbgsym_fs_root
+ dbgsym_ids = dctrl_data.dbgsym_info.dbgsym_ids
+ intermediate_manifest = manifest.finalize_data_tar_contents(
+ package, fs_root, mtime
+ )
+
+ setup_control_files(
+ dctrl_data,
+ manifest,
+ dbgsym_fs_root,
+ dbgsym_ids,
+ package_metadata_context,
+ allow_ctrl_file_management=not is_dh_rrr_only_mode,
+ )
+
+ needs_root, use_fallback_assembly, gain_root_cmd = determine_assembly_method(
+ package, intermediate_manifest
+ )
+
+ if not dctrl_bin.is_udeb and any(
+ f for f in dbgsym_fs_root.all_paths() if f.is_file
+ ):
+ # We never built udebs due to #797391. We currently do not generate a control
+ # file for it either for the same reason.
+ dbgsym_root = dhe_dbgsym_root_dir(dctrl_bin)
+ if not os.path.isdir(output_path):
+ _error(
+ "Cannot produce a dbgsym package when output path is not a directory."
+ )
+ dbgsym_intermediate_manifest = manifest.finalize_data_tar_contents(
+ dbgsym_package_name,
+ dbgsym_fs_root,
+ mtime,
+ )
+ _assemble_deb(
+ dbgsym_package_name,
+ deb_materialize,
+ dbgsym_intermediate_manifest,
+ mtime,
+ os.path.join(dbgsym_root, "DEBIAN"),
+ output_path,
+ upstream_args,
+ is_udeb=dctrl_bin.is_udeb, # Review this if we ever do dbgsyms for udebs
+ use_fallback_assembly=False,
+ needs_root=False,
+ )
+
+ _assemble_deb(
+ package,
+ deb_materialize,
+ intermediate_manifest,
+ mtime,
+ control_output_dir,
+ output_path,
+ upstream_args,
+ is_udeb=dctrl_bin.is_udeb,
+ use_fallback_assembly=use_fallback_assembly,
+ needs_root=needs_root,
+ gain_root_cmd=gain_root_cmd,
+ )
+
+
+def _assemble_deb(
+ package: str,
+ deb_materialize_cmd: str,
+ intermediate_manifest: IntermediateManifest,
+ mtime: int,
+ control_output_dir: str,
+ output_path: str,
+ upstream_args: Optional[List[str]],
+ is_udeb: bool = False,
+ use_fallback_assembly: bool = False,
+ needs_root: bool = False,
+ gain_root_cmd: Optional[Sequence[str]] = None,
+) -> None:
+ scratch_root_dir = scratch_dir()
+ materialization_dir = os.path.join(
+ scratch_root_dir, "materialization-dirs", package
+ )
+ ensure_dir(os.path.dirname(materialization_dir))
+ materialize_cmd: List[str] = []
+ assert not use_fallback_assembly or not gain_root_cmd
+ if needs_root and gain_root_cmd:
+ # Only use the gain_root_cmd if we absolutely need it.
+ # Note that gain_root_cmd will be empty unless R³ is set to the relevant keyword
+ # that would make us use targeted promotion. Therefore, we do not need to check other
+ # conditions than the package needing root. (R³: binary-targets implies `needs_root=True`
+ # without a gain_root_cmd)
+ materialize_cmd.extend(gain_root_cmd)
+ materialize_cmd.extend(
+ [
+ deb_materialize_cmd,
+ "materialize-deb",
+ "--intermediate-package-manifest",
+ "-",
+ "--may-move-control-files",
+ "--may-move-data-files",
+ "--source-date-epoch",
+ str(mtime),
+ "--discard-existing-output",
+ control_output_dir,
+ materialization_dir,
+ ]
+ )
+ output = output_path
+ if is_udeb:
+ materialize_cmd.append("--udeb")
+ output = os.path.join(
+ output_path, compute_output_filename(control_output_dir, True)
+ )
+
+ assembly_method = "debputy" if needs_root and use_fallback_assembly else "dpkg-deb"
+ combined_materialization_and_assembly = not needs_root
+ if combined_materialization_and_assembly:
+ materialize_cmd.extend(
+ ["--build-method", assembly_method, "--assembled-deb-output", output]
+ )
+
+ if upstream_args:
+ materialize_cmd.append("--")
+ materialize_cmd.extend(upstream_args)
+
+ if combined_materialization_and_assembly:
+ print(
+ f"Materializing and assembling {package} via: {escape_shell(*materialize_cmd)}"
+ )
+ else:
+ print(f"Materializing {package} via: {escape_shell(*materialize_cmd)}")
+ proc = subprocess.Popen(materialize_cmd, stdin=subprocess.PIPE)
+ proc.communicate(
+ _serialize_intermediate_manifest(intermediate_manifest).encode("utf-8")
+ )
+ if proc.returncode != 0:
+ _error(f"{escape_shell(deb_materialize_cmd)} exited with a non-zero exit code!")
+
+ if not combined_materialization_and_assembly:
+ build_materialization = [
+ deb_materialize_cmd,
+ "build-materialized-deb",
+ materialization_dir,
+ assembly_method,
+ "--output",
+ output,
+ ]
+ print(f"Assembling {package} via: {escape_shell(*build_materialization)}")
+ try:
+ subprocess.check_call(build_materialization)
+ except subprocess.CalledProcessError as e:
+ exit_code = f" with exit code {e.returncode}" if e.returncode else ""
+ _error(
+ f"Assembly command for {package} failed{exit_code}. Please review the output of the command"
+ f" for more details on the problem."
+ )