Coverage for src/debputy/build_support/build_logic.py: 11%

141 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +0000

1import collections 

2import contextlib 

3import os 

4import tempfile 

5from typing import ( 

6 List, 

7 Dict, 

8 Optional, 

9 TYPE_CHECKING, 

10 Tuple, 

11 FrozenSet, 

12) 

13from collections.abc import Iterator, Mapping 

14 

15from debputy.build_support.build_context import BuildContext 

16from debputy.build_support.buildsystem_detection import ( 

17 auto_detect_buildsystem, 

18) 

19from debputy.commands.debputy_cmd.context import CommandContext 

20from debputy.manifest_parser.base_types import BuildEnvironmentDefinition 

21from debputy.packages import BinaryPackage 

22from debputy.plugins.debputy.to_be_api_types import BuildRule 

23from debputy.util import ( 

24 _error, 

25 _info, 

26 _non_verbose_info, 

27 generated_content_dir, 

28) 

29 

30if TYPE_CHECKING: 

31 from debputy.highlevel_manifest import HighLevelManifest 

32 

33 

34@contextlib.contextmanager 

35def in_build_env( 

36 build_env: BuildEnvironmentDefinition, 

37 *, 

38 env_is_for_clean: bool = False, 

39) -> Iterator[None]: 

40 # Should possibly be per build 

41 with _setup_build_env(build_env, env_is_for_clean=env_is_for_clean): 

42 yield 

43 

44 

45def _set_stem_if_absent(stems: list[str | None], idx: int, stem: str) -> None: 

46 if stems[idx] is None: 

47 stems[idx] = stem 

48 

49 

50def assign_stems( 

51 build_rules: list[BuildRule], 

52 manifest: "HighLevelManifest", 

53) -> None: 

54 if not build_rules: 

55 return 

56 if len(build_rules) == 1: 

57 build_rules[0].auto_generated_stem = "" 

58 return 

59 

60 debs = {p.name for p in manifest.all_packages if p.package_type == "deb"} 

61 udebs = {p.name for p in manifest.all_packages if p.package_type == "udeb"} 

62 deb_only_builds: list[int] = [] 

63 udeb_only_builds: list[int] = [] 

64 by_name_only_builds: dict[str, list[int]] = collections.defaultdict(list) 

65 stems = [rule.name for rule in build_rules] 

66 reserved_stems = {n for n in stems if n is not None} 

67 

68 for idx, rule in enumerate(build_rules): 

69 stem = stems[idx] 

70 if stem is not None: 

71 continue 

72 pkg_names = {p.name for p in rule.for_packages} 

73 if pkg_names == debs: 

74 deb_only_builds.append(idx) 

75 elif pkg_names == udebs: 

76 udeb_only_builds.append(idx) 

77 

78 if len(pkg_names) == 1: 

79 pkg_name = next(iter(pkg_names)) 

80 by_name_only_builds[pkg_name].append(idx) 

81 

82 if "deb" not in reserved_stems and len(deb_only_builds) == 1: 

83 _set_stem_if_absent(stems, deb_only_builds[0], "deb") 

84 

85 if "udeb" not in reserved_stems and len(udeb_only_builds) == 1: 

86 _set_stem_if_absent(stems, udeb_only_builds[0], "udeb") 

87 

88 for pkg, idxs in by_name_only_builds.items(): 

89 if len(idxs) != 1 or pkg in reserved_stems: 

90 continue 

91 _set_stem_if_absent(stems, idxs[0], pkg) 

92 

93 for idx, rule in enumerate(build_rules): 

94 stem = stems[idx] 

95 if stem is None: 

96 stem = f"bno_{idx}" 

97 rule.auto_generated_stem = stem 

98 _info(f"Assigned {rule.auto_generated_stem} [{stem}] to step {idx}") 

99 

100 

101def perform_builds( 

102 context: CommandContext, 

103 manifest: "HighLevelManifest", 

104 build_system_install_dirs: list[tuple[str, frozenset[BinaryPackage]]], 

105) -> None: 

106 prune_unnecessary_env() 

107 build_rules = manifest.build_rules 

108 if build_rules is not None: 

109 if not build_rules: 

110 # Defined but empty disables the auto-detected build system 

111 return 

112 active_packages = frozenset(manifest.active_packages) 

113 condition_context = manifest.source_condition_context 

114 build_context = BuildContext.from_command_context(context) 

115 assign_stems(build_rules, manifest) 

116 for step_no, build_rule in enumerate(build_rules): 

117 step_ref = ( 

118 f"step {step_no} [{build_rule.auto_generated_stem}]" 

119 if build_rule.name is None 

120 else f"step {step_no} [{build_rule.name}]" 

121 ) 

122 if build_rule.for_packages.isdisjoint(active_packages): 

123 _info( 

124 f"Skipping build for {step_ref}: None of the relevant packages are being built" 

125 ) 

126 continue 

127 manifest_condition = build_rule.manifest_condition 

128 if manifest_condition is not None and not manifest_condition.evaluate( 

129 condition_context 

130 ): 

131 _info( 

132 f"Skipping build for {step_ref}: The condition clause evaluated to false" 

133 ) 

134 continue 

135 _info(f"Starting build for {step_ref}.") 

136 with in_build_env(build_rule.environment): 

137 try: 

138 build_rule.run_build(build_context, manifest) 

139 except (RuntimeError, AttributeError) as e: 

140 if context.parsed_args.debug_mode: 

141 raise e 

142 _error( 

143 f"An error occurred during build/install at {step_ref} (defined at {build_rule.attribute_path.path}): {str(e)}" 

144 ) 

145 dest_dir = build_rule.install_dest_dir() 

146 if dest_dir is not None: 

147 build_system_install_dirs.append((dest_dir, build_rule.for_packages)) 

148 

149 _info(f"Completed build for {step_ref}.") 

150 

151 else: 

152 build_system = auto_detect_buildsystem(manifest) 

153 if build_system: 

154 _info(f"Auto-detected build system: {build_system.__class__.__name__}") 

155 build_context = BuildContext.from_command_context(context) 

156 with in_build_env(build_system.environment): 

157 build_system.run_build( 

158 build_context, 

159 manifest, 

160 ) 

161 dest_dir = build_system.install_dest_dir() 

162 if dest_dir is not None: 

163 build_system_install_dirs.append( 

164 (dest_dir, build_system.for_packages) 

165 ) 

166 

167 _non_verbose_info("Upstream builds completed successfully") 

168 else: 

169 _info("No build system was detected from the current plugin set.") 

170 

171 

172def prune_unnecessary_env() -> None: 

173 vs = [ 

174 "XDG_CACHE_HOME", 

175 "XDG_CONFIG_DIRS", 

176 "XDG_CONFIG_HOME", 

177 "XDG_DATA_HOME", 

178 "XDG_DATA_DIRS", 

179 "XDG_RUNTIME_DIR", 

180 ] 

181 for v in vs: 

182 if v in os.environ: 

183 del os.environ[v] 

184 os.environ["HOME"] = "/non-existent" 

185 

186 

187@contextlib.contextmanager 

188def _setup_build_env( 

189 build_env: BuildEnvironmentDefinition, 

190 *, 

191 env_is_for_clean: bool = False, 

192) -> Iterator[None]: 

193 env_backup = dict(os.environ) 

194 env = dict(env_backup) 

195 had_delta = False 

196 build_env.update_env(env) 

197 if env != env_backup: 

198 _set_env(env) 

199 had_delta = True 

200 _info("Updated environment to match build") 

201 if env_is_for_clean: 

202 yield 

203 else: 

204 orig_home = env_backup["HOME"] 

205 env["HOME"] = generated_content_dir(subdir_key="dpty_buildtime_home") 

206 with tempfile.TemporaryDirectory() as xdg_runtime_dir: 

207 env["XDG_RUNTIME_DIR"] = xdg_runtime_dir 

208 yield 

209 try: 

210 del env["XDG_RUNTIME_DIR"] 

211 except KeyError: 

212 pass 

213 env["HOME"] = orig_home 

214 if had_delta or env != env_backup: 

215 _set_env(env_backup) 

216 

217 

218def _set_env(desired_env: Mapping[str, str]) -> None: 

219 os_env = os.environ 

220 for key in os_env.keys() | desired_env.keys(): 

221 desired_value = desired_env.get(key) 

222 if desired_value is None: 

223 try: 

224 del os_env[key] 

225 except KeyError: 

226 pass 

227 else: 

228 os_env[key] = desired_value