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

1import json 

2import os 

3import subprocess 

4from collections.abc import Sequence 

5 

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 

26 

27_RRR_DEB_ASSEMBLY_KEYWORD = "debputy/deb-assembly" 

28_NOTIFIED_ABOUT_FALLBACK_ASSEMBLY = False 

29 

30 

31def _serialize_intermediate_manifest(members: IntermediateManifest) -> str: 

32 serial_format = [m.to_manifest() for m in members] 

33 return json.dumps(serial_format) 

34 

35 

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, [] 

73 

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 ) 

84 

85 

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 

100 

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 ) 

122 

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 ) 

131 

132 needs_root, use_fallback_assembly, gain_root_cmd = determine_assembly_method( 

133 integration_mode, 

134 package, 

135 intermediate_manifest, 

136 ) 

137 

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 ) 

166 

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 ) 

181 

182 

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 ) 

235 

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 ) 

242 

243 if upstream_args: 

244 materialize_cmd.append("--") 

245 materialize_cmd.extend(upstream_args) 

246 

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!") 

259 

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 )