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

127 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1import os.path 

2from typing import ( 

3 Set, 

4 cast, 

5 List, 

6) 

7 

8from debputy.build_support.build_context import BuildContext 

9from debputy.build_support.build_logic import ( 

10 in_build_env, 

11 assign_stems, 

12) 

13from debputy.build_support.buildsystem_detection import auto_detect_buildsystem 

14from debputy.commands.debputy_cmd.context import CommandContext 

15from debputy.highlevel_manifest import HighLevelManifest 

16from debputy.plugin.debputy.to_be_api_types import BuildSystemRule, CleanHelper 

17from debputy.util import _info, print_command, _error, _debug_log, _warn 

18from debputy.util import ( 

19 run_build_system_command, 

20) 

21 

22_REMOVE_DIRS = frozenset( 

23 [ 

24 "__pycache__", 

25 "autom4te.cache", 

26 ] 

27) 

28_IGNORE_DIRS = frozenset( 

29 [ 

30 ".git", 

31 ".svn", 

32 ".bzr", 

33 ".hg", 

34 "CVS", 

35 ".pc", 

36 "_darcs", 

37 ] 

38) 

39DELETE_FILE_EXT = ( 

40 "~", 

41 ".orig", 

42 ".rej", 

43 ".bak", 

44) 

45DELETE_FILE_BASENAMES = { 

46 "DEADJOE", 

47 ".SUMS", 

48 "TAGS", 

49} 

50 

51 

52def _debhelper_left_overs() -> bool: 

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

54 "debian/debhelper-build-stamp" 

55 ): 

56 return True 

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

58 for child in root_dir: 

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

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

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

62 ): 

63 return True 

64 return False 

65 

66 

67class CleanHelperImpl(CleanHelper): 

68 

69 def __init__(self) -> None: 

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

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

72 

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

74 self.files_to_remove.update(args) 

75 

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

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

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

79 self.dirs_to_remove.update(args) 

80 

81 

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

83 remove_files = clean_helper.files_to_remove 

84 remove_dirs = clean_helper.dirs_to_remove 

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

86 for child in root_dir: 

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

88 remove_files.add(child.path) 

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

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

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

92 remove_dirs.add(path) 

93 subdirs.remove(remove_dir) 

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

95 subdirs.remove(skip_dir) 

96 

97 for basename in files: 

98 if ( 

99 basename.endswith(DELETE_FILE_EXT) 

100 or basename in DELETE_FILE_BASENAMES 

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

102 ): 

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

104 remove_files.add(path) 

105 

106 

107def perform_clean( 

108 context: CommandContext, 

109 manifest: HighLevelManifest, 

110) -> None: 

111 clean_helper = CleanHelperImpl() 

112 

113 build_rules = manifest.build_rules 

114 if build_rules is not None: 

115 if not build_rules: 

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

117 return 

118 active_packages = frozenset(manifest.active_packages) 

119 condition_context = manifest.source_condition_context 

120 build_context = BuildContext.from_command_context(context) 

121 assign_stems(build_rules, manifest) 

122 for step_no, build_rule in enumerate(build_rules): 

123 step_ref = ( 

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

125 if build_rule.name is None 

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

127 ) 

128 if not build_rule.is_buildsystem: 

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

130 continue 

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

132 if build_system_rule.for_packages.isdisjoint(active_packages): 

133 _info( 

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

135 ) 

136 continue 

137 manifest_condition = build_system_rule.manifest_condition 

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

139 condition_context 

140 ): 

141 _info( 

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

143 ) 

144 continue 

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

146 with in_build_env(build_rule.environment): 

147 try: 

148 build_system_rule.run_clean( 

149 build_context, 

150 manifest, 

151 clean_helper, 

152 ) 

153 except (RuntimeError, AttributeError) as e: 

154 if context.parsed_args.debug_mode: 

155 raise e 

156 _error( 

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

158 ) 

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

160 else: 

161 build_system = auto_detect_buildsystem(manifest) 

162 if build_system: 

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

164 build_context = BuildContext.from_command_context(context) 

165 with in_build_env(build_system.environment): 

166 build_system.run_clean( 

167 build_context, 

168 manifest, 

169 clean_helper, 

170 ) 

171 else: 

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

173 

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

175 debhelper_used = False 

176 

177 if dh_autoreconf_used or _debhelper_left_overs(): 

178 debhelper_used = True 

179 

180 _scan_for_standard_removals(clean_helper) 

181 

182 for package in manifest.all_packages: 

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

184 if os.path.lexists(package_staging_dir): 

185 clean_helper.schedule_removal_of_directories(package_staging_dir) 

186 

187 remove_files = clean_helper.files_to_remove 

188 remove_dirs = clean_helper.dirs_to_remove 

189 if remove_files: 

190 print_command("rm", "-f", *remove_files) 

191 _remove_files_if_exists(*remove_files) 

192 if remove_dirs: 

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

194 

195 if debhelper_used: 

196 _info( 

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

198 ) 

199 if dh_autoreconf_used: 

200 run_build_system_command("dh_autoreconf_clean") 

201 run_build_system_command("dh_clean") 

202 

203 try: 

204 run_build_system_command( 

205 "dpkg-buildtree", 

206 "clean", 

207 raise_file_not_found_on_missing_command=True, 

208 ) 

209 except FileNotFoundError: 

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

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

212 _remove_files_if_exists( 

213 "debian/files", 

214 "debian/files.new", 

215 "debian/substvars", 

216 "debian/substvars.new", 

217 ) 

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

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

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

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

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

223 

224 

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

226 for path in args: 

227 try: 

228 os.unlink(path) 

229 except FileNotFoundError: 

230 continue 

231 except OSError as e: 

232 if os.path.isdir(path): 

233 _error( 

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

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

236 ) 

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