Coverage for src/debputy/dh_migration/migration.py: 7%
192 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
1import json
2import os
3import re
4import subprocess
5from itertools import chain
6from typing import Optional, List, Callable, Set
8from debian.deb822 import Deb822
10from debputy.debhelper_emulation import CannotEmulateExecutableDHConfigFile
11from debputy.dh_migration.migrators import MIGRATORS
12from debputy.dh_migration.migrators_impl import (
13 read_dh_addon_sequences,
14 MIGRATION_TARGET_DH_DEBPUTY,
15 MIGRATION_TARGET_DH_DEBPUTY_RRR,
16)
17from debputy.dh_migration.models import (
18 FeatureMigration,
19 AcceptableMigrationIssues,
20 UnsupportedFeature,
21 ConflictingChange,
22)
23from debputy.highlevel_manifest import HighLevelManifest
24from debputy.manifest_parser.exceptions import ManifestParseException
25from debputy.plugin.api import VirtualPath
26from debputy.util import _error, _warn, _info, escape_shell, assume_not_none
29def _print_migration_summary(
30 migrations: List[FeatureMigration],
31 compat: int,
32 min_compat_level: int,
33 required_plugins: Set[str],
34 requested_plugins: Optional[Set[str]],
35) -> None:
36 warning_count = 0
38 for migration in migrations:
39 if not migration.anything_to_do:
40 continue
41 underline = "-" * len(migration.tagline)
42 if migration.warnings:
43 _warn(f"Summary for migration: {migration.tagline}")
44 _warn(f"-----------------------{underline}")
45 _warn(" /!\\ ATTENTION /!\\")
46 warning_count += len(migration.warnings)
47 for warning in migration.warnings:
48 _warn(f" * {warning}")
50 if compat < min_compat_level:
51 if warning_count:
52 _warn("")
53 _warn("Supported debhelper compat check")
54 _warn("--------------------------------")
55 warning_count += 1
56 _warn(
57 f"The migration tool assumes debhelper compat {min_compat_level}+ semantics, but this package"
58 f" is using compat {compat}. Consider upgrading the package to compat {min_compat_level}"
59 " first."
60 )
62 if required_plugins:
63 if requested_plugins is None:
64 warning_count += 1
65 needed_plugins = ", ".join(f"debputy-plugin-{n}" for n in required_plugins)
66 if warning_count:
67 _warn("")
68 _warn("Missing debputy plugin check")
69 _warn("----------------------------")
70 _warn(
71 f"The migration tool could not read d/control and therefore cannot tell if all the required"
72 f" plugins have been requested. Please ensure that the package Build-Depends on: {needed_plugins}"
73 )
74 else:
75 missing_plugins = required_plugins - requested_plugins
76 if missing_plugins:
77 warning_count += 1
78 needed_plugins = ", ".join(
79 f"debputy-plugin-{n}" for n in missing_plugins
80 )
81 if warning_count:
82 _warn("")
83 _warn("Missing debputy plugin check")
84 _warn("----------------------------")
85 _warn(
86 f"The migration tool asserted that the following `debputy` plugins would be required, which"
87 f" are not explicitly requested. Please add the following to Build-Depends: {needed_plugins}"
88 )
90 if warning_count:
91 _warn("")
92 _warn(
93 f"/!\\ Total number of warnings or manual migrations required: {warning_count}"
94 )
97def _dh_compat_level() -> Optional[int]:
98 try:
99 res = subprocess.check_output(
100 ["dh_assistant", "active-compat-level"], stderr=subprocess.DEVNULL
101 )
102 except subprocess.CalledProcessError:
103 compat = None
104 else:
105 try:
106 compat = json.loads(res)["declared-compat-level"]
107 except RuntimeError:
108 compat = None
109 else:
110 if not isinstance(compat, int):
111 compat = None
112 return compat
115def _requested_debputy_plugins(debian_dir: VirtualPath) -> Optional[Set[str]]:
116 ctrl_file = debian_dir.get("control")
117 if not ctrl_file:
118 return None
120 dep_regex = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII)
121 plugins = set()
123 with ctrl_file.open() as fd:
124 ctrl = list(Deb822.iter_paragraphs(fd))
125 source_paragraph = ctrl[0] if ctrl else {}
127 for f in ("Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"):
128 field = source_paragraph.get(f)
129 if not field:
130 continue
132 for dep_clause in (d.strip() for d in field.split(",")):
133 match = dep_regex.match(dep_clause.strip())
134 if not match:
135 continue
136 dep = match.group(1)
137 if not dep.startswith("debputy-plugin-"):
138 continue
139 plugins.add(dep[15:])
140 return plugins
143def _check_migration_target(
144 debian_dir: VirtualPath,
145 migration_target: Optional[str],
146) -> str:
147 r = read_dh_addon_sequences(debian_dir)
148 if r is None and migration_target is None:
149 _error("debian/control is missing and no migration target was provided")
150 bd_sequences, dr_sequences = r
151 all_sequences = bd_sequences | dr_sequences
153 has_zz_debputy = "zz-debputy" in all_sequences or "debputy" in all_sequences
154 has_zz_debputy_rrr = "zz-debputy-rrr" in all_sequences
155 has_any_existing = has_zz_debputy or has_zz_debputy_rrr
157 if migration_target == "dh-sequence-zz-debputy-rrr" and has_zz_debputy:
158 _error("Cannot migrate from (zz-)debputy to zz-debputy-rrr")
160 if has_zz_debputy_rrr and not has_zz_debputy:
161 resolved_migration_target = MIGRATION_TARGET_DH_DEBPUTY_RRR
162 else:
163 resolved_migration_target = MIGRATION_TARGET_DH_DEBPUTY
165 if migration_target is not None:
166 resolved_migration_target = migration_target
168 if has_any_existing:
169 _info(
170 f'Using "{resolved_migration_target}" as migration target based on the packaging'
171 )
172 else:
173 _info(
174 f'Using "{resolved_migration_target}" as default migration target. Use --migration-target to choose!'
175 )
177 return resolved_migration_target
180def migrate_from_dh(
181 manifest: HighLevelManifest,
182 acceptable_migration_issues: AcceptableMigrationIssues,
183 permit_destructive_changes: Optional[bool],
184 migration_target: Optional[str],
185 manifest_parser_factory: Callable[[str], HighLevelManifest],
186) -> None:
187 migrations = []
188 compat = _dh_compat_level()
189 if compat is None:
190 _error(
191 'Cannot detect declared compat level (try running "dh_assistant active-compat-level")'
192 )
194 debian_dir = manifest.debian_dir
195 mutable_manifest = assume_not_none(manifest.mutable_manifest)
197 resolved_migration_target = _check_migration_target(debian_dir, migration_target)
199 try:
200 for migrator in MIGRATORS[resolved_migration_target]:
201 feature_migration = FeatureMigration(migrator.__name__)
202 migrator(
203 debian_dir,
204 manifest,
205 acceptable_migration_issues,
206 feature_migration,
207 resolved_migration_target,
208 )
209 migrations.append(feature_migration)
210 except CannotEmulateExecutableDHConfigFile as e:
211 _error(
212 f"Unable to process the executable dh config file {e.config_file().fs_path}: {e.message()}"
213 )
214 except UnsupportedFeature as e:
215 msg = (
216 f"Unable to migrate automatically due to missing features in debputy. The feature is:"
217 f"\n\n * {e.message}"
218 )
219 keys = e.issue_keys
220 if keys:
221 primary_key = keys[0]
222 alt_keys = ""
223 if len(keys) > 1:
224 alt_keys = (
225 f' Alternatively you can also use one of: {", ".join(keys[1:])}. Please note that some'
226 " of these may cover more cases."
227 )
228 msg += (
229 f"\n\nUse --acceptable-migration-issues={primary_key} to convert this into a warning and try again."
230 " However, you should only do that if you believe you can replace the functionality manually"
231 f" or the usage is obsolete / can be removed. {alt_keys}"
232 )
233 _error(msg)
234 except ConflictingChange as e:
235 _error(
236 "The migration tool detected a conflict data being migrated and data already migrated / in the existing"
237 "manifest."
238 f"\n\n * {e.message}"
239 "\n\nPlease review the situation and resolve the conflict manually."
240 )
242 # We start on compat 12 for arch:any due to the new dh_makeshlibs and dh_installinit default
243 min_compat = 12
244 min_compat = max(
245 (m.assumed_compat for m in migrations if m.assumed_compat is not None),
246 default=min_compat,
247 )
249 if compat < min_compat and "min-compat-level" not in acceptable_migration_issues:
250 # The migration summary special-cases the compat mismatch and warns for us.
251 _error(
252 f"The migration tool assumes debhelper compat {min_compat} or later but the package is only on"
253 f" compat {compat}. This may lead to incorrect result."
254 f"\n\nUse --acceptable-migration-issues=min-compat-level to convert this into a warning and"
255 f" try again, if you want to continue regardless."
256 )
258 requested_plugins = _requested_debputy_plugins(debian_dir)
259 required_plugins: Set[str] = set()
260 required_plugins.update(
261 chain.from_iterable(
262 m.required_plugins for m in migrations if m.required_plugins
263 )
264 )
266 _print_migration_summary(
267 migrations, compat, min_compat, required_plugins, requested_plugins
268 )
269 migration_count = sum((m.performed_changes for m in migrations), 0)
271 if not migration_count:
272 _info(
273 "debputy was not able to find any (supported) migrations that it could perform for you."
274 )
275 return
277 if any(m.successful_manifest_changes for m in migrations):
278 new_manifest_path = manifest.manifest_path + ".new"
280 with open(new_manifest_path, "w") as fd:
281 mutable_manifest.write_to(fd)
283 try:
284 _info("Verifying the generating manifest")
285 manifest_parser_factory(new_manifest_path)
286 except ManifestParseException as e:
287 raise AssertionError(
288 "Could not parse the manifest generated from the migrator"
289 ) from e
291 if permit_destructive_changes:
292 if os.path.isfile(manifest.manifest_path):
293 os.rename(manifest.manifest_path, manifest.manifest_path + ".orig")
294 os.rename(new_manifest_path, manifest.manifest_path)
295 _info(f"Updated manifest {manifest.manifest_path}")
296 else:
297 _info(
298 f'Created draft manifest "{new_manifest_path}" (rename to "{manifest.manifest_path}"'
299 " to activate it)"
300 )
301 else:
302 _info("No manifest changes detected; skipping update of manifest.")
304 removals: int = sum((len(m.remove_paths_on_success) for m in migrations), 0)
305 renames: int = sum((len(m.rename_paths_on_success) for m in migrations), 0)
307 if renames:
308 if permit_destructive_changes:
309 _info("Paths being renamed:")
310 else:
311 _info("Migration *would* rename the following paths:")
312 for previous_path, new_path in (
313 p for m in migrations for p in m.rename_paths_on_success
314 ):
315 _info(f" mv {escape_shell(previous_path, new_path)}")
317 if removals:
318 if permit_destructive_changes:
319 _info("Removals:")
320 else:
321 _info("Migration *would* remove the following files:")
322 for path in (p for m in migrations for p in m.remove_paths_on_success):
323 _info(f" rm -f {escape_shell(path)}")
325 if permit_destructive_changes is None:
326 print()
327 _info(
328 "If you would like to perform the migration, please re-run with --apply-changes."
329 )
330 elif permit_destructive_changes:
331 for previous_path, new_path in (
332 p for m in migrations for p in m.rename_paths_on_success
333 ):
334 os.rename(previous_path, new_path)
335 for path in (p for m in migrations for p in m.remove_paths_on_success):
336 os.unlink(path)
338 print()
339 _info("Migrations performed successfully")
340 print()
341 _info(
342 "Remember to validate the resulting binary packages after rebuilding with debputy"
343 )
344 else:
345 print()
346 _info("No migrations performed as requested")