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

145 statements  

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

1import os.path 

2from typing import ( 

3 Set, 

4 cast, 

5) 

6 

7from debputy.build_support.build_context import BuildContext 

8from debputy.build_support.build_logic import ( 

9 in_build_env, 

10 assign_stems, 

11 prune_unnecessary_env, 

12) 

13from debputy.build_support.buildsystem_detection import auto_detect_buildsystem 

14from debputy.commands.debputy_cmd.context import CommandContext 

15from debputy.filesystem_scan import FSROOverlay 

16from debputy.highlevel_manifest import HighLevelManifest 

17from debputy.plugins.debputy.to_be_api_types import BuildSystemRule, CleanHelper 

18from debputy.util import ( 

19 _info, 

20 print_command, 

21 _error, 

22 _debug_log, 

23 _warn, 

24 PRINT_BUILD_SYSTEM_COMMAND, 

25) 

26from debputy.util import ( 

27 run_build_system_command, 

28) 

29 

30_REMOVE_DIRS = frozenset( 

31 [ 

32 "__pycache__", 

33 "autom4te.cache", 

34 ] 

35) 

36_IGNORE_DIRS = frozenset( 

37 [ 

38 ".git", 

39 ".svn", 

40 ".bzr", 

41 ".hg", 

42 "CVS", 

43 ".pc", 

44 "_darcs", 

45 ] 

46) 

47DELETE_FILE_EXT = ( 

48 "~", 

49 ".orig", 

50 ".rej", 

51 ".bak", 

52) 

53DELETE_FILE_BASENAMES = { 

54 "DEADJOE", 

55 ".SUMS", 

56 "TAGS", 

57} 

58 

59 

60def _debhelper_left_overs() -> bool: 

61 if os.path.lexists("debian/.debhelper") or os.path.lexists( 

62 "debian/debhelper-build-stamp" 

63 ): 

64 return True 

65 with os.scandir(".") as root_dir: 

66 for child in root_dir: 

67 if child.is_file(follow_symlinks=False) and ( 

68 child.name.endswith(".debhelper.log") 

69 or child.name.endswith(".debhelper") 

70 ): 

71 return True 

72 return False 

73 

74 

75class CleanHelperImpl(CleanHelper): 

76 

77 def __init__(self) -> None: 

78 self.files_to_remove: Set[str] = set() 

79 self.dirs_to_remove: Set[str] = set() 

80 

81 def schedule_removal_of_files(self, *args: str) -> None: 

82 self.files_to_remove.update(args) 

83 

84 def schedule_removal_of_directories(self, *args: str) -> None: 

85 if any(p == "/" for p in args): 

86 raise ValueError("Refusing to delete '/'") 

87 self.dirs_to_remove.update(args) 

88 

89 

90def _scan_for_standard_removals(clean_helper: CleanHelperImpl) -> None: 

91 remove_files = clean_helper.files_to_remove 

92 remove_dirs = clean_helper.dirs_to_remove 

93 with os.scandir(".") as root_dir: 

94 for child in root_dir: 

95 if child.is_file(follow_symlinks=False) and child.name.endswith("-stamp"): 

96 remove_files.add(child.path) 

97 for current_dir, subdirs, files in os.walk("."): 

98 for remove_dir in [d for d in subdirs if d in _REMOVE_DIRS]: 

99 path = os.path.join(current_dir, remove_dir) 

100 remove_dirs.add(path) 

101 subdirs.remove(remove_dir) 

102 for skip_dir in [d for d in subdirs if d in _IGNORE_DIRS]: 

103 subdirs.remove(skip_dir) 

104 

105 for basename in files: 

106 if ( 

107 basename.endswith(DELETE_FILE_EXT) 

108 or basename in DELETE_FILE_BASENAMES 

109 or (basename.startswith("#") and basename.endswith("#")) 

110 ): 

111 path = os.path.join(current_dir, basename) 

112 remove_files.add(path) 

113 

114 

115def _apply_remove_during_clean_rules( 

116 clean_helper: CleanHelper, 

117 manifest: HighLevelManifest, 

118) -> None: 

119 source_root = FSROOverlay.create_root_dir(".", ".") 

120 had_errors = False 

121 for rule in manifest.remove_during_clean_rules: 

122 allow_dir_matches = rule.raw_match_rule.endswith("/") 

123 for match in rule.match_rule.finditer(source_root): 

124 if match.is_dir and not allow_dir_matches: 

125 _warn( 

126 f' * The path {match.path} is a directory and the remove-during-clean rule matching it does not explicitly end with a "/". Please add a slash to {rule.raw_match_rule} (at {rule.attribute_path.path}) if it is intended to remove directories' 

127 ) 

128 had_errors = True 

129 continue 

130 

131 if match.is_dir: 

132 clean_helper.schedule_removal_of_directories(match.path) 

133 else: 

134 clean_helper.schedule_removal_of_files(match.path) 

135 

136 if had_errors: 

137 _error("Aborting during to the above errors") 

138 

139 

140def perform_clean( 

141 context: CommandContext, 

142 manifest: HighLevelManifest, 

143) -> None: 

144 clean_helper = CleanHelperImpl() 

145 prune_unnecessary_env() 

146 build_rules = manifest.build_rules 

147 if build_rules is not None: 

148 if not build_rules: 

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

150 return 

151 active_packages = frozenset(manifest.active_packages) 

152 condition_context = manifest.source_condition_context 

153 build_context = BuildContext.from_command_context(context) 

154 assign_stems(build_rules, manifest) 

155 for step_no, build_rule in enumerate(build_rules): 

156 step_ref = ( 

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

158 if build_rule.name is None 

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

160 ) 

161 if not build_rule.is_buildsystem: 

162 _debug_log(f"Skipping clean for {step_ref}: Not a build system") 

163 continue 

164 build_system_rule: BuildSystemRule = cast("BuildSystemRule", build_rule) 

165 if build_system_rule.for_packages.isdisjoint(active_packages): 

166 _info( 

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

168 ) 

169 continue 

170 manifest_condition = build_system_rule.manifest_condition 

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

172 condition_context 

173 ): 

174 _info( 

175 f"Skipping clean for {step_ref}: The condition clause evaluated to false" 

176 ) 

177 continue 

178 _info(f"Starting clean for {step_ref}.") 

179 with in_build_env(build_rule.environment, env_is_for_clean=True): 

180 try: 

181 build_system_rule.run_clean( 

182 build_context, 

183 manifest, 

184 clean_helper, 

185 ) 

186 except (RuntimeError, AttributeError) as e: 

187 if context.parsed_args.debug_mode: 

188 raise e 

189 _error( 

190 f"An error occurred during clean at {step_ref} (defined at {build_rule.attribute_path.path}): {str(e)}" 

191 ) 

192 _info(f"Completed clean for {step_ref}.") 

193 else: 

194 build_system = auto_detect_buildsystem(manifest) 

195 if build_system: 

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

197 build_context = BuildContext.from_command_context(context) 

198 with in_build_env(build_system.environment, env_is_for_clean=True): 

199 build_system.run_clean( 

200 build_context, 

201 manifest, 

202 clean_helper, 

203 ) 

204 else: 

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

206 

207 dh_autoreconf_used = os.path.lexists("debian/autoreconf.before") 

208 debhelper_used = False 

209 

210 if dh_autoreconf_used or _debhelper_left_overs(): 

211 debhelper_used = True 

212 

213 _scan_for_standard_removals(clean_helper) 

214 _apply_remove_during_clean_rules(clean_helper, manifest) 

215 

216 for package in manifest.all_packages: 

217 package_staging_dir = os.path.join("debian", package.name) 

218 if os.path.lexists(package_staging_dir): 

219 clean_helper.schedule_removal_of_directories(package_staging_dir) 

220 

221 remove_files = sorted(clean_helper.files_to_remove) 

222 remove_dirs = sorted(clean_helper.dirs_to_remove) 

223 if remove_files: 

224 print_command( 

225 "rm", "-f", *remove_files, print_at_log_level=PRINT_BUILD_SYSTEM_COMMAND 

226 ) 

227 _remove_files_if_exists(*remove_files) 

228 if remove_dirs: 

229 run_build_system_command("rm", "-fr", *remove_dirs) 

230 

231 if debhelper_used: 

232 _info( 

233 "Noted traces of debhelper commands being used; invoking dh_clean to clean up after them" 

234 ) 

235 if dh_autoreconf_used: 

236 run_build_system_command("dh_autoreconf_clean") 

237 run_build_system_command("dh_clean") 

238 

239 try: 

240 run_build_system_command( 

241 "dpkg-buildtree", 

242 "clean", 

243 raise_file_not_found_on_missing_command=True, 

244 ) 

245 except FileNotFoundError: 

246 _warn("The dpkg-buildtree command is not present. Emulating it") 

247 # This is from the manpage of dpkg-buildtree for 1.22.11. 

248 _remove_files_if_exists( 

249 "debian/files", 

250 "debian/files.new", 

251 "debian/substvars", 

252 "debian/substvars.new", 

253 ) 

254 run_build_system_command("rm", "-fr", "debian/tmp") 

255 # Remove debian/.debputy as a separate step. While `rm -fr` should process things in order, 

256 # it will continue on error, which could cause our manifests of things to delete to be deleted 

257 # while leaving things half-removed unless we do this extra step. 

258 run_build_system_command("rm", "-fr", "debian/.debputy") 

259 

260 

261def _remove_files_if_exists(*args: str) -> None: 

262 for path in args: 

263 try: 

264 os.unlink(path) 

265 except FileNotFoundError: 

266 continue 

267 except OSError as e: 

268 if os.path.isdir(path): 

269 _error( 

270 f"Failed to remove {path}: It is a directory, but it should have been a non-directory." 

271 " Please verify everything is as expected and, if it is, remove it manually." 

272 ) 

273 _error(f"Failed to remove {path}: {str(e)}")