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

140 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +0000

1import collections 

2import contextlib 

3import os 

4import tempfile 

5from typing import ( 

6 Iterator, 

7 Mapping, 

8 List, 

9 Dict, 

10 Optional, 

11 TYPE_CHECKING, 

12 Tuple, 

13 FrozenSet, 

14) 

15 

16from debputy.build_support.build_context import BuildContext 

17from debputy.build_support.buildsystem_detection import ( 

18 auto_detect_buildsystem, 

19) 

20from debputy.commands.debputy_cmd.context import CommandContext 

21from debputy.manifest_parser.base_types import BuildEnvironmentDefinition 

22from debputy.packages import BinaryPackage 

23from debputy.plugins.debputy.to_be_api_types import BuildRule 

24from debputy.util import ( 

25 _error, 

26 _info, 

27 _non_verbose_info, 

28 generated_content_dir, 

29) 

30 

31if TYPE_CHECKING: 

32 from debputy.highlevel_manifest import HighLevelManifest 

33 

34 

35@contextlib.contextmanager 

36def in_build_env( 

37 build_env: BuildEnvironmentDefinition, 

38 *, 

39 env_is_for_clean: bool = False, 

40) -> Iterator[None]: 

41 # Should possibly be per build 

42 with _setup_build_env(build_env, env_is_for_clean=env_is_for_clean): 

43 yield 

44 

45 

46def _set_stem_if_absent(stems: List[Optional[str]], idx: int, stem: str) -> None: 

47 if stems[idx] is None: 

48 stems[idx] = stem 

49 

50 

51def assign_stems( 

52 build_rules: List[BuildRule], 

53 manifest: "HighLevelManifest", 

54) -> None: 

55 if not build_rules: 

56 return 

57 if len(build_rules) == 1: 

58 build_rules[0].auto_generated_stem = "" 

59 return 

60 

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

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

63 deb_only_builds: List[int] = [] 

64 udeb_only_builds: List[int] = [] 

65 by_name_only_builds: Dict[str, List[int]] = collections.defaultdict(list) 

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

67 reserved_stems = set(n for n in stems if n is not None) 

68 

69 for idx, rule in enumerate(build_rules): 

70 stem = stems[idx] 

71 if stem is not None: 

72 continue 

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

74 if pkg_names == debs: 

75 deb_only_builds.append(idx) 

76 elif pkg_names == udebs: 

77 udeb_only_builds.append(idx) 

78 

79 if len(pkg_names) == 1: 

80 pkg_name = next(iter(pkg_names)) 

81 by_name_only_builds[pkg_name].append(idx) 

82 

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

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

85 

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

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

88 

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

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

91 continue 

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

93 

94 for idx, rule in enumerate(build_rules): 

95 stem = stems[idx] 

96 if stem is None: 

97 stem = f"bno_{idx}" 

98 rule.auto_generated_stem = stem 

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

100 

101 

102def perform_builds( 

103 context: CommandContext, 

104 manifest: "HighLevelManifest", 

105 build_system_install_dirs: List[Tuple[str, FrozenSet[BinaryPackage]]], 

106) -> None: 

107 prune_unnecessary_env() 

108 build_rules = manifest.build_rules 

109 if build_rules is not None: 

110 if not build_rules: 

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

112 return 

113 active_packages = frozenset(manifest.active_packages) 

114 condition_context = manifest.source_condition_context 

115 build_context = BuildContext.from_command_context(context) 

116 assign_stems(build_rules, manifest) 

117 for step_no, build_rule in enumerate(build_rules): 

118 step_ref = ( 

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

120 if build_rule.name is None 

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

122 ) 

123 if build_rule.for_packages.isdisjoint(active_packages): 

124 _info( 

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

126 ) 

127 continue 

128 manifest_condition = build_rule.manifest_condition 

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

130 condition_context 

131 ): 

132 _info( 

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

134 ) 

135 continue 

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

137 with in_build_env(build_rule.environment): 

138 try: 

139 build_rule.run_build(build_context, manifest) 

140 except (RuntimeError, AttributeError) as e: 

141 if context.parsed_args.debug_mode: 

142 raise e 

143 _error( 

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

145 ) 

146 dest_dir = build_rule.install_dest_dir() 

147 if dest_dir is not None: 

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

149 

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

151 

152 else: 

153 build_system = auto_detect_buildsystem(manifest) 

154 if build_system: 

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

156 build_context = BuildContext.from_command_context(context) 

157 with in_build_env(build_system.environment): 

158 build_system.run_build( 

159 build_context, 

160 manifest, 

161 ) 

162 dest_dir = build_system.install_dest_dir() 

163 if dest_dir is not None: 

164 build_system_install_dirs.append( 

165 (dest_dir, build_system.for_packages) 

166 ) 

167 

168 _non_verbose_info("Upstream builds completed successfully") 

169 else: 

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

171 

172 

173def prune_unnecessary_env() -> None: 

174 vs = [ 

175 "XDG_CACHE_HOME", 

176 "XDG_CONFIG_DIRS", 

177 "XDG_CONFIG_HOME", 

178 "XDG_DATA_HOME", 

179 "XDG_DATA_DIRS", 

180 "XDG_RUNTIME_DIR", 

181 ] 

182 for v in vs: 

183 if v in os.environ: 

184 del os.environ[v] 

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

186 

187 

188@contextlib.contextmanager 

189def _setup_build_env( 

190 build_env: BuildEnvironmentDefinition, 

191 *, 

192 env_is_for_clean: bool = False, 

193) -> Iterator[None]: 

194 env_backup = dict(os.environ) 

195 env = dict(env_backup) 

196 had_delta = False 

197 build_env.update_env(env) 

198 if env != env_backup: 

199 _set_env(env) 

200 had_delta = True 

201 _info("Updated environment to match build") 

202 if env_is_for_clean: 

203 yield 

204 else: 

205 orig_home = env_backup["HOME"] 

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

207 with tempfile.TemporaryDirectory() as xdg_runtime_dir: 

208 env["XDG_RUNTIME_DIR"] = xdg_runtime_dir 

209 yield 

210 try: 

211 del env["XDG_RUNTIME_DIR"] 

212 except KeyError: 

213 pass 

214 env["HOME"] = orig_home 

215 if had_delta or env != env_backup: 

216 _set_env(env_backup) 

217 

218 

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

220 os_env = os.environ 

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

222 desired_value = desired_env.get(key) 

223 if desired_value is None: 

224 try: 

225 del os_env[key] 

226 except KeyError: 

227 pass 

228 else: 

229 os_env[key] = desired_value