summaryrefslogtreecommitdiffstats
path: root/src/debputy/dh_migration/migration.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/dh_migration/migration.py')
-rw-r--r--src/debputy/dh_migration/migration.py344
1 files changed, 344 insertions, 0 deletions
diff --git a/src/debputy/dh_migration/migration.py b/src/debputy/dh_migration/migration.py
new file mode 100644
index 0000000..1366f22
--- /dev/null
+++ b/src/debputy/dh_migration/migration.py
@@ -0,0 +1,344 @@
+import json
+import os
+import re
+import subprocess
+from itertools import chain
+from typing import Optional, List, Callable, Set
+
+from debian.deb822 import Deb822
+
+from debputy.debhelper_emulation import CannotEmulateExecutableDHConfigFile
+from debputy.dh_migration.migrators import MIGRATORS
+from debputy.dh_migration.migrators_impl import (
+ read_dh_addon_sequences,
+ MIGRATION_TARGET_DH_DEBPUTY,
+ MIGRATION_TARGET_DH_DEBPUTY_RRR,
+)
+from debputy.dh_migration.models import (
+ FeatureMigration,
+ AcceptableMigrationIssues,
+ UnsupportedFeature,
+ ConflictingChange,
+)
+from debputy.highlevel_manifest import HighLevelManifest
+from debputy.manifest_parser.exceptions import ManifestParseException
+from debputy.plugin.api import VirtualPath
+from debputy.util import _error, _warn, _info, escape_shell, assume_not_none
+
+
+def _print_migration_summary(
+ migrations: List[FeatureMigration],
+ compat: int,
+ min_compat_level: int,
+ required_plugins: Set[str],
+ requested_plugins: Optional[Set[str]],
+) -> None:
+ warning_count = 0
+
+ for migration in migrations:
+ if not migration.anything_to_do:
+ continue
+ underline = "-" * len(migration.tagline)
+ if migration.warnings:
+ _warn(f"Summary for migration: {migration.tagline}")
+ _warn(f"-----------------------{underline}")
+ _warn(" /!\\ ATTENTION /!\\")
+ warning_count += len(migration.warnings)
+ for warning in migration.warnings:
+ _warn(f" * {warning}")
+
+ if compat < min_compat_level:
+ if warning_count:
+ _warn("")
+ _warn("Supported debhelper compat check")
+ _warn("--------------------------------")
+ warning_count += 1
+ _warn(
+ f"The migration tool assumes debhelper compat {min_compat_level}+ semantics, but this package"
+ f" is using compat {compat}. Consider upgrading the package to compat {min_compat_level}"
+ " first."
+ )
+
+ if required_plugins:
+ if requested_plugins is None:
+ warning_count += 1
+ needed_plugins = ", ".join(f"debputy-plugin-{n}" for n in required_plugins)
+ if warning_count:
+ _warn("")
+ _warn("Missing debputy plugin check")
+ _warn("----------------------------")
+ _warn(
+ f"The migration tool could not read d/control and therefore cannot tell if all the required"
+ f" plugins have been requested. Please ensure that the package Build-Depends on: {needed_plugins}"
+ )
+ else:
+ missing_plugins = required_plugins - requested_plugins
+ if missing_plugins:
+ warning_count += 1
+ needed_plugins = ", ".join(
+ f"debputy-plugin-{n}" for n in missing_plugins
+ )
+ if warning_count:
+ _warn("")
+ _warn("Missing debputy plugin check")
+ _warn("----------------------------")
+ _warn(
+ f"The migration tool asserted that the following `debputy` plugins would be required, which"
+ f" are not explicitly requested. Please add the following to Build-Depends: {needed_plugins}"
+ )
+
+ if warning_count:
+ _warn("")
+ _warn(
+ f"/!\\ Total number of warnings or manual migrations required: {warning_count}"
+ )
+
+
+def _dh_compat_level() -> Optional[int]:
+ try:
+ res = subprocess.check_output(
+ ["dh_assistant", "active-compat-level"], stderr=subprocess.DEVNULL
+ )
+ except subprocess.CalledProcessError:
+ compat = None
+ else:
+ try:
+ compat = json.loads(res)["declared-compat-level"]
+ except RuntimeError:
+ compat = None
+ else:
+ if not isinstance(compat, int):
+ compat = None
+ return compat
+
+
+def _requested_debputy_plugins(debian_dir: VirtualPath) -> Optional[Set[str]]:
+ ctrl_file = debian_dir.get("control")
+ if not ctrl_file:
+ return None
+
+ dep_regex = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII)
+ plugins = set()
+
+ with ctrl_file.open() as fd:
+ ctrl = list(Deb822.iter_paragraphs(fd))
+ source_paragraph = ctrl[0] if ctrl else {}
+
+ for f in ("Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"):
+ field = source_paragraph.get(f)
+ if not field:
+ continue
+
+ for dep_clause in (d.strip() for d in field.split(",")):
+ match = dep_regex.match(dep_clause.strip())
+ if not match:
+ continue
+ dep = match.group(1)
+ if not dep.startswith("debputy-plugin-"):
+ continue
+ plugins.add(dep[15:])
+ return plugins
+
+
+def _check_migration_target(
+ debian_dir: VirtualPath,
+ migration_target: Optional[str],
+) -> str:
+ r = read_dh_addon_sequences(debian_dir)
+ if r is None and migration_target is None:
+ _error("debian/control is missing and no migration target was provided")
+ bd_sequences, dr_sequences = r
+ all_sequences = bd_sequences | dr_sequences
+
+ has_zz_debputy = "zz-debputy" in all_sequences or "debputy" in all_sequences
+ has_zz_debputy_rrr = "zz-debputy-rrr" in all_sequences
+ has_any_existing = has_zz_debputy or has_zz_debputy_rrr
+
+ if migration_target == "dh-sequence-zz-debputy-rrr" and has_zz_debputy:
+ _error("Cannot migrate from (zz-)debputy to zz-debputy-rrr")
+
+ if has_zz_debputy_rrr and not has_zz_debputy:
+ resolved_migration_target = MIGRATION_TARGET_DH_DEBPUTY_RRR
+ else:
+ resolved_migration_target = MIGRATION_TARGET_DH_DEBPUTY
+
+ if migration_target is not None:
+ resolved_migration_target = migration_target
+
+ if has_any_existing:
+ _info(
+ f'Using "{resolved_migration_target}" as migration target based on the packaging'
+ )
+ else:
+ _info(f'Using "{resolved_migration_target}" as default migration target.')
+
+ return resolved_migration_target
+
+
+def migrate_from_dh(
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ permit_destructive_changes: Optional[bool],
+ migration_target: Optional[str],
+ manifest_parser_factory: Callable[[str], HighLevelManifest],
+) -> None:
+ migrations = []
+ compat = _dh_compat_level()
+ if compat is None:
+ _error(
+ 'Cannot detect declared compat level (try running "dh_assistant active-compat-level")'
+ )
+
+ debian_dir = manifest.debian_dir
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+
+ resolved_migration_target = _check_migration_target(debian_dir, migration_target)
+
+ try:
+ for migrator in MIGRATORS[resolved_migration_target]:
+ feature_migration = FeatureMigration(migrator.__name__)
+ migrator(
+ debian_dir,
+ manifest,
+ acceptable_migration_issues,
+ feature_migration,
+ resolved_migration_target,
+ )
+ migrations.append(feature_migration)
+ except CannotEmulateExecutableDHConfigFile as e:
+ _error(
+ f"Unable to process the executable dh config file {e.config_file().fs_path}: {e.message()}"
+ )
+ except UnsupportedFeature as e:
+ msg = (
+ f"Unable to migrate automatically due to missing features in debputy. The feature is:"
+ f"\n\n * {e.message}"
+ )
+ keys = e.issue_keys
+ if keys:
+ primary_key = keys[0]
+ alt_keys = ""
+ if len(keys) > 1:
+ alt_keys = (
+ f' Alternatively you can also use one of: {", ".join(keys[1:])}. Please note that some'
+ " of these may cover more cases."
+ )
+ msg += (
+ f"\n\nUse --acceptable-migration-issues={primary_key} to convert this into a warning and try again."
+ " However, you should only do that if you believe you can replace the functionality manually"
+ f" or the usage is obsolete / can be removed. {alt_keys}"
+ )
+ _error(msg)
+ except ConflictingChange as e:
+ _error(
+ "The migration tool detected a conflict data being migrated and data already migrated / in the existing"
+ "manifest."
+ f"\n\n * {e.message}"
+ "\n\nPlease review the situation and resolve the conflict manually."
+ )
+
+ # We start on compat 12 for arch:any due to the new dh_makeshlibs and dh_installinit default
+ min_compat = 12
+ min_compat = max(
+ (m.assumed_compat for m in migrations if m.assumed_compat is not None),
+ default=min_compat,
+ )
+
+ if compat < min_compat and "min-compat-level" not in acceptable_migration_issues:
+ # The migration summary special-cases the compat mismatch and warns for us.
+ _error(
+ f"The migration tool assumes debhelper compat {min_compat} or later but the package is only on"
+ f" compat {compat}. This may lead to incorrect result."
+ f"\n\nUse --acceptable-migration-issues=min-compat-level to convert this into a warning and"
+ f" try again, if you want to continue regardless."
+ )
+
+ requested_plugins = _requested_debputy_plugins(debian_dir)
+ required_plugins: Set[str] = set()
+ required_plugins.update(
+ chain.from_iterable(
+ m.required_plugins for m in migrations if m.required_plugins
+ )
+ )
+
+ _print_migration_summary(
+ migrations, compat, min_compat, required_plugins, requested_plugins
+ )
+ migration_count = sum((m.performed_changes for m in migrations), 0)
+
+ if not migration_count:
+ _info(
+ "debputy was not able to find any (supported) migrations that it could perform for you."
+ )
+ return
+
+ if any(m.successful_manifest_changes for m in migrations):
+ new_manifest_path = manifest.manifest_path + ".new"
+
+ with open(new_manifest_path, "w") as fd:
+ mutable_manifest.write_to(fd)
+
+ try:
+ _info("Verifying the generating manifest")
+ manifest_parser_factory(new_manifest_path)
+ except ManifestParseException as e:
+ raise AssertionError(
+ "Could not parse the manifest generated from the migrator"
+ ) from e
+
+ if permit_destructive_changes:
+ if os.path.isfile(manifest.manifest_path):
+ os.rename(manifest.manifest_path, manifest.manifest_path + ".orig")
+ os.rename(new_manifest_path, manifest.manifest_path)
+ _info(f"Updated manifest {manifest.manifest_path}")
+ else:
+ _info(
+ f'Created draft manifest "{new_manifest_path}" (rename to "{manifest.manifest_path}"'
+ " to activate it)"
+ )
+ else:
+ _info("No manifest changes detected; skipping update of manifest.")
+
+ removals: int = sum((len(m.remove_paths_on_success) for m in migrations), 0)
+ renames: int = sum((len(m.rename_paths_on_success) for m in migrations), 0)
+
+ if renames:
+ if permit_destructive_changes:
+ _info("Paths being renamed:")
+ else:
+ _info("Migration *would* rename the following paths:")
+ for previous_path, new_path in (
+ p for m in migrations for p in m.rename_paths_on_success
+ ):
+ _info(f" mv {escape_shell(previous_path, new_path)}")
+
+ if removals:
+ if permit_destructive_changes:
+ _info("Removals:")
+ else:
+ _info("Migration *would* remove the following files:")
+ for path in (p for m in migrations for p in m.remove_paths_on_success):
+ _info(f" rm -f {escape_shell(path)}")
+
+ if permit_destructive_changes is None:
+ print()
+ _info(
+ "If you would like to perform the migration, please re-run with --apply-changes."
+ )
+ elif permit_destructive_changes:
+ for previous_path, new_path in (
+ p for m in migrations for p in m.rename_paths_on_success
+ ):
+ os.rename(previous_path, new_path)
+ for path in (p for m in migrations for p in m.remove_paths_on_success):
+ os.unlink(path)
+
+ print()
+ _info("Migrations performed successfully")
+ print()
+ _info(
+ "Remember to validate the resulting binary packages after rebuilding with debputy"
+ )
+ else:
+ print()
+ _info("No migrations performed as requested")