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

1import json 

2import os 

3import subprocess 

4from typing import Optional, List, Tuple 

5from collections.abc import Sequence 

6 

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) 

23 

24_RRR_DEB_ASSEMBLY_KEYWORD = "debputy/deb-assembly" 

25_NOTIFIED_ABOUT_FALLBACK_ASSEMBLY = False 

26 

27 

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

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

30 return json.dumps(serial_format) 

31 

32 

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

61 

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 ) 

72 

73 

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 

87 

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 ) 

109 

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 ) 

118 

119 needs_root, use_fallback_assembly, gain_root_cmd = determine_assembly_method( 

120 package, intermediate_manifest 

121 ) 

122 

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 ) 

151 

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 ) 

166 

167 

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 ) 

220 

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 ) 

227 

228 if upstream_args: 

229 materialize_cmd.append("--") 

230 materialize_cmd.extend(upstream_args) 

231 

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

244 

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 )