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