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
« 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)
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)
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}
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
75class CleanHelperImpl(CleanHelper):
77 def __init__(self) -> None:
78 self.files_to_remove: Set[str] = set()
79 self.dirs_to_remove: Set[str] = set()
81 def schedule_removal_of_files(self, *args: str) -> None:
82 self.files_to_remove.update(args)
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)
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)
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)
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
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)
136 if had_errors:
137 _error("Aborting during to the above errors")
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.")
207 dh_autoreconf_used = os.path.lexists("debian/autoreconf.before")
208 debhelper_used = False
210 if dh_autoreconf_used or _debhelper_left_overs():
211 debhelper_used = True
213 _scan_for_standard_removals(clean_helper)
214 _apply_remove_during_clean_rules(clean_helper, manifest)
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)
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)
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")
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")
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)}")