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