Coverage for src/debputy/package_build/assemble_deb.py: 14%
101 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
1import json
2import os
3import subprocess
4from typing import Optional, List, Tuple
5from collections.abc import Sequence
7from debputy import DEBPUTY_ROOT_DIR
8from debputy.commands.debputy_cmd.context import CommandContext
9from debputy.deb_packaging_support import setup_control_files
10from debputy.filesystem_scan import FSRootDir
11from debputy.highlevel_manifest import HighLevelManifest
12from debputy.intermediate_manifest import IntermediateManifest
13from debputy.plugin.api.impl_types import PackageDataTable
14from debputy.util import (
15 escape_shell,
16 _error,
17 compute_output_filename,
18 scratch_dir,
19 ensure_dir,
20 assume_not_none,
21 _info,
22)
24_RRR_DEB_ASSEMBLY_KEYWORD = "debputy/deb-assembly"
25_NOTIFIED_ABOUT_FALLBACK_ASSEMBLY = False
28def _serialize_intermediate_manifest(members: IntermediateManifest) -> str:
29 serial_format = [m.to_manifest() for m in members]
30 return json.dumps(serial_format)
33def determine_assembly_method(
34 package: str,
35 intermediate_manifest: IntermediateManifest,
36) -> tuple[bool, bool, list[str]]:
37 paths_needing_root = (
38 tm for tm in intermediate_manifest if tm.owner != "root" or tm.group != "root"
39 )
40 matched_path = next(paths_needing_root, None)
41 if matched_path is None:
42 return False, False, []
43 rrr = os.environ.get("DEB_RULES_REQUIRES_ROOT")
44 if rrr and _RRR_DEB_ASSEMBLY_KEYWORD in rrr:
45 gain_root_cmd = os.environ.get("DEB_GAIN_ROOT_CMD")
46 if not gain_root_cmd:
47 _error(
48 "DEB_RULES_REQUIRES_ROOT contains a debputy keyword but DEB_GAIN_ROOT_CMD does not contain a "
49 '"gain root" command'
50 )
51 return True, False, gain_root_cmd.split()
52 if rrr == "no":
53 global _NOTIFIED_ABOUT_FALLBACK_ASSEMBLY
54 if not _NOTIFIED_ABOUT_FALLBACK_ASSEMBLY:
55 _info(
56 'Using internal assembly method due to "Rules-Requires-Root" being "no" and dpkg-deb assembly would'
57 " require (fake)root for binary packages that needs it."
58 )
59 _NOTIFIED_ABOUT_FALLBACK_ASSEMBLY = True
60 return True, True, []
62 _error(
63 f'Due to the path "{matched_path.member_path}" in {package}, the package assembly will require (fake)root.'
64 " However, this command is not run as root nor was debputy requested to use a root command via"
65 f' "Rules-Requires-Root". Please consider adding "{_RRR_DEB_ASSEMBLY_KEYWORD}" to "Rules-Requires-Root"'
66 " in debian/control. Though, due to #1036865, you may have to revert to"
67 ' "Rules-Requires-Root: binary-targets" depending on which version of dpkg you need to support.'
68 ' Alternatively, you can set "Rules-Requires-Root: no" in debian/control and debputy will assemble'
69 " the package anyway. In this case, dpkg-deb will not be used, but the output should be bit-for-bit"
70 " compatible with what debputy would have produced with dpkg-deb (and root/fakeroot)."
71 )
74def assemble_debs(
75 context: CommandContext,
76 manifest: HighLevelManifest,
77 package_data_table: PackageDataTable,
78 is_dh_rrr_only_mode: bool,
79 *,
80 debug_materialization: bool = False,
81) -> None:
82 parsed_args = context.parsed_args
83 output_path = parsed_args.output
84 upstream_args = parsed_args.upstream_args
85 deb_materialize = str(DEBPUTY_ROOT_DIR / "deb_materialization.py")
86 mtime = context.mtime
88 for dctrl_bin in manifest.active_packages:
89 package = dctrl_bin.name
90 dbgsym_package_name = f"{package}-dbgsym"
91 dctrl_data = package_data_table[package]
92 fs_root = dctrl_data.fs_root
93 control_output_fs_path = dctrl_data.control_output_dir.fs_path
94 package_metadata_context = dctrl_data.package_metadata_context
95 if (
96 dbgsym_package_name in package_data_table
97 or "noautodbgsym" in manifest.deb_options_and_profiles.deb_build_options
98 or "noddebs" in manifest.deb_options_and_profiles.deb_build_options
99 ):
100 # Discard the dbgsym part if it conflicts with a real package, or
101 # we were asked not to build it.
102 dctrl_data.dbgsym_info.dbgsym_fs_root = FSRootDir()
103 dctrl_data.dbgsym_info.dbgsym_ids.clear()
104 dbgsym_fs_root = dctrl_data.dbgsym_info.dbgsym_fs_root
105 dbgsym_ids = dctrl_data.dbgsym_info.dbgsym_ids
106 intermediate_manifest = manifest.finalize_data_tar_contents(
107 package, fs_root, mtime
108 )
110 setup_control_files(
111 dctrl_data,
112 manifest,
113 dbgsym_fs_root,
114 dbgsym_ids,
115 package_metadata_context,
116 allow_ctrl_file_management=not is_dh_rrr_only_mode,
117 )
119 needs_root, use_fallback_assembly, gain_root_cmd = determine_assembly_method(
120 package, intermediate_manifest
121 )
123 if not dctrl_bin.is_udeb and any(
124 f for f in dbgsym_fs_root.all_paths() if f.is_file
125 ):
126 # We never built udebs due to #797391. We currently do not generate a control
127 # file for it either for the same reason.
128 dbgsym_root = dctrl_data.dbgsym_info.dbgsym_root_dir
129 if not os.path.isdir(output_path):
130 _error(
131 "Cannot produce a dbgsym package when output path is not a directory."
132 )
133 dbgsym_intermediate_manifest = manifest.finalize_data_tar_contents(
134 dbgsym_package_name,
135 dbgsym_fs_root,
136 mtime,
137 )
138 _assemble_deb(
139 dbgsym_package_name,
140 deb_materialize,
141 dbgsym_intermediate_manifest,
142 mtime,
143 os.path.join(dbgsym_root, "DEBIAN"),
144 output_path,
145 upstream_args,
146 is_udeb=dctrl_bin.is_udeb, # Review this if we ever do dbgsyms for udebs
147 use_fallback_assembly=False,
148 needs_root=False,
149 debug_materialization=debug_materialization,
150 )
152 _assemble_deb(
153 package,
154 deb_materialize,
155 intermediate_manifest,
156 mtime,
157 control_output_fs_path,
158 output_path,
159 upstream_args,
160 is_udeb=dctrl_bin.is_udeb,
161 use_fallback_assembly=use_fallback_assembly,
162 needs_root=needs_root,
163 gain_root_cmd=gain_root_cmd,
164 debug_materialization=debug_materialization,
165 )
168def _assemble_deb(
169 package: str,
170 deb_materialize_cmd: str,
171 intermediate_manifest: IntermediateManifest,
172 mtime: int,
173 control_output_fs_path: str,
174 output_path: str,
175 upstream_args: list[str] | None,
176 is_udeb: bool = False,
177 use_fallback_assembly: bool = False,
178 needs_root: bool = False,
179 gain_root_cmd: Sequence[str] | None = None,
180 *,
181 debug_materialization: bool = False,
182) -> None:
183 scratch_root_dir = scratch_dir()
184 materialization_dir = os.path.join(
185 scratch_root_dir, "materialization-dirs", package
186 )
187 ensure_dir(os.path.dirname(materialization_dir))
188 materialize_cmd: list[str] = []
189 assert not use_fallback_assembly or not gain_root_cmd
190 if needs_root and gain_root_cmd:
191 # Only use the gain_root_cmd if we absolutely need it.
192 # Note that gain_root_cmd will be empty unless R³ is set to the relevant keyword
193 # that would make us use targeted promotion. Therefore, we do not need to check other
194 # conditions than the package needing root. (R³: binary-targets implies `needs_root=True`
195 # without a gain_root_cmd)
196 materialize_cmd.extend(gain_root_cmd)
197 materialize_cmd.append(deb_materialize_cmd)
198 if debug_materialization:
199 materialize_cmd.append("--verbose")
200 materialize_cmd.extend(
201 [
202 "materialize-deb",
203 "--intermediate-package-manifest",
204 "-",
205 "--may-move-control-files",
206 "--may-move-data-files",
207 "--source-date-epoch",
208 str(mtime),
209 "--discard-existing-output",
210 control_output_fs_path,
211 materialization_dir,
212 ]
213 )
214 output = output_path
215 if is_udeb:
216 materialize_cmd.append("--udeb")
217 output = os.path.join(
218 output_path, compute_output_filename(control_output_fs_path, True)
219 )
221 assembly_method = "debputy" if needs_root and use_fallback_assembly else "dpkg-deb"
222 combined_materialization_and_assembly = not needs_root
223 if combined_materialization_and_assembly:
224 materialize_cmd.extend(
225 ["--build-method", assembly_method, "--assembled-deb-output", output]
226 )
228 if upstream_args:
229 materialize_cmd.append("--")
230 materialize_cmd.extend(upstream_args)
232 if combined_materialization_and_assembly:
233 _info(
234 f"Materializing and assembling {package} via: {escape_shell(*materialize_cmd)}"
235 )
236 else:
237 _info(f"Materializing {package} via: {escape_shell(*materialize_cmd)}")
238 proc = subprocess.Popen(materialize_cmd, stdin=subprocess.PIPE)
239 proc.communicate(
240 _serialize_intermediate_manifest(intermediate_manifest).encode("utf-8")
241 )
242 if proc.returncode != 0:
243 _error(f"{escape_shell(deb_materialize_cmd)} exited with a non-zero exit code!")
245 if not combined_materialization_and_assembly:
246 build_materialization = [
247 deb_materialize_cmd,
248 "build-materialized-deb",
249 materialization_dir,
250 assembly_method,
251 "--output",
252 output,
253 ]
254 _info(f"Assembling {package} via: {escape_shell(*build_materialization)}")
255 try:
256 subprocess.check_call(build_materialization)
257 except subprocess.CalledProcessError as e:
258 exit_code = f" with exit code {e.returncode}" if e.returncode else ""
259 _error(
260 f"Assembly command for {package} failed{exit_code}. Please review the output of the command"
261 f" for more details on the problem."
262 )