summaryrefslogtreecommitdiffstats
path: root/src/debputy/dh_migration/migrators_impl.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/dh_migration/migrators_impl.py')
-rw-r--r--src/debputy/dh_migration/migrators_impl.py1706
1 files changed, 1706 insertions, 0 deletions
diff --git a/src/debputy/dh_migration/migrators_impl.py b/src/debputy/dh_migration/migrators_impl.py
new file mode 100644
index 0000000..6613c25
--- /dev/null
+++ b/src/debputy/dh_migration/migrators_impl.py
@@ -0,0 +1,1706 @@
+import collections
+import dataclasses
+import json
+import os
+import re
+import subprocess
+from typing import (
+ Iterable,
+ Optional,
+ Tuple,
+ List,
+ Set,
+ Mapping,
+ Any,
+ Union,
+ Callable,
+ TypeVar,
+ Dict,
+)
+
+from debian.deb822 import Deb822
+
+from debputy.architecture_support import dpkg_architecture_table
+from debputy.deb_packaging_support import dpkg_field_list_pkg_dep
+from debputy.debhelper_emulation import (
+ dhe_filedoublearray,
+ DHConfigFileLine,
+ dhe_pkgfile,
+ parse_drules_for_addons,
+ extract_dh_addons_from_control,
+)
+from debputy.dh_migration.models import (
+ ConflictingChange,
+ FeatureMigration,
+ UnsupportedFeature,
+ AcceptableMigrationIssues,
+ DHMigrationSubstitution,
+)
+from debputy.highlevel_manifest import (
+ MutableYAMLSymlink,
+ HighLevelManifest,
+ MutableYAMLConffileManagementItem,
+ AbstractMutableYAMLInstallRule,
+)
+from debputy.installations import MAN_GUESS_FROM_BASENAME, MAN_GUESS_LANG_FROM_PATH
+from debputy.packages import BinaryPackage
+from debputy.plugin.api import VirtualPath
+from debputy.util import (
+ _error,
+ PKGVERSION_REGEX,
+ PKGNAME_REGEX,
+ _normalize_path,
+ assume_not_none,
+ has_glob_magic,
+)
+
+MIGRATION_TARGET_DH_DEBPUTY_RRR = "dh-sequence-zz-debputy-rrr"
+MIGRATION_TARGET_DH_DEBPUTY = "dh-sequence-zz-debputy"
+
+
+# Align with debputy.py
+DH_COMMANDS_REPLACED = {
+ MIGRATION_TARGET_DH_DEBPUTY_RRR: frozenset(
+ {
+ "dh_fixperms",
+ "dh_gencontrol",
+ "dh_md5sums",
+ "dh_builddeb",
+ }
+ ),
+ MIGRATION_TARGET_DH_DEBPUTY: frozenset(
+ {
+ "dh_install",
+ "dh_installdocs",
+ "dh_installchangelogs",
+ "dh_installexamples",
+ "dh_installman",
+ "dh_installcatalogs",
+ "dh_installcron",
+ "dh_installdebconf",
+ "dh_installemacsen",
+ "dh_installifupdown",
+ "dh_installinfo",
+ "dh_installinit",
+ "dh_installsysusers",
+ "dh_installtmpfiles",
+ "dh_installsystemd",
+ "dh_installsystemduser",
+ "dh_installmenu",
+ "dh_installmime",
+ "dh_installmodules",
+ "dh_installlogcheck",
+ "dh_installlogrotate",
+ "dh_installpam",
+ "dh_installppp",
+ "dh_installudev",
+ "dh_installgsettings",
+ "dh_installinitramfs",
+ "dh_installalternatives",
+ "dh_bugfiles",
+ "dh_ucf",
+ "dh_lintian",
+ "dh_icons",
+ "dh_usrlocal",
+ "dh_perl",
+ "dh_link",
+ "dh_installwm",
+ "dh_installxfonts",
+ "dh_strip_nondeterminism",
+ "dh_compress",
+ "dh_fixperms",
+ "dh_dwz",
+ "dh_strip",
+ "dh_makeshlibs",
+ "dh_shlibdeps",
+ "dh_missing",
+ "dh_installdeb",
+ "dh_gencontrol",
+ "dh_md5sums",
+ "dh_builddeb",
+ }
+ ),
+}
+
+
+@dataclasses.dataclass(frozen=True, slots=True)
+class UnsupportedDHConfig:
+ dh_config_basename: str
+ dh_tool: str
+ bug_950723_prefix_matching: bool = False
+ is_missing_migration: bool = False
+
+
+@dataclasses.dataclass(frozen=True, slots=True)
+class DHSequenceMigration:
+ debputy_plugin: str
+ remove_dh_sequence: bool = True
+ must_use_zz_debputy: bool = False
+
+
+UNSUPPORTED_DH_CONFIGS_AND_TOOLS_FOR_ZZ_DEBPUTY = [
+ UnsupportedDHConfig("config", "dh_installdebconf"),
+ UnsupportedDHConfig("templates", "dh_installdebconf"),
+ UnsupportedDHConfig("emacsen-compat", "dh_installemacsen"),
+ UnsupportedDHConfig("emacsen-install", "dh_installemacsen"),
+ UnsupportedDHConfig("emacsen-remove", "dh_installemacsen"),
+ UnsupportedDHConfig("emacsen-startup", "dh_installemacsen"),
+ # The `upstart` file should be long dead, but we might as well detect it.
+ UnsupportedDHConfig("upstart", "dh_installinit"),
+ # dh_installsystemduser
+ UnsupportedDHConfig(
+ "user.path", "dh_installsystemduser", bug_950723_prefix_matching=False
+ ),
+ UnsupportedDHConfig(
+ "user.path", "dh_installsystemduser", bug_950723_prefix_matching=True
+ ),
+ UnsupportedDHConfig(
+ "user.service", "dh_installsystemduser", bug_950723_prefix_matching=False
+ ),
+ UnsupportedDHConfig(
+ "user.service", "dh_installsystemduser", bug_950723_prefix_matching=True
+ ),
+ UnsupportedDHConfig(
+ "user.socket", "dh_installsystemduser", bug_950723_prefix_matching=False
+ ),
+ UnsupportedDHConfig(
+ "user.socket", "dh_installsystemduser", bug_950723_prefix_matching=True
+ ),
+ UnsupportedDHConfig(
+ "user.target", "dh_installsystemduser", bug_950723_prefix_matching=False
+ ),
+ UnsupportedDHConfig(
+ "user.target", "dh_installsystemduser", bug_950723_prefix_matching=True
+ ),
+ UnsupportedDHConfig(
+ "user.timer", "dh_installsystemduser", bug_950723_prefix_matching=False
+ ),
+ UnsupportedDHConfig(
+ "user.timer", "dh_installsystemduser", bug_950723_prefix_matching=True
+ ),
+ UnsupportedDHConfig("udev", "dh_installudev"),
+ UnsupportedDHConfig("menu", "dh_installmenu"),
+ UnsupportedDHConfig("menu-method", "dh_installmenu"),
+ UnsupportedDHConfig("ucf", "dh_ucf"),
+ UnsupportedDHConfig("wm", "dh_installwm"),
+ UnsupportedDHConfig("triggers", "dh_installdeb"),
+ UnsupportedDHConfig("postinst", "dh_installdeb"),
+ UnsupportedDHConfig("postrm", "dh_installdeb"),
+ UnsupportedDHConfig("preinst", "dh_installdeb"),
+ UnsupportedDHConfig("prerm", "dh_installdeb"),
+ UnsupportedDHConfig("menutest", "dh_installdeb"),
+ UnsupportedDHConfig("isinstallable", "dh_installdeb"),
+]
+SUPPORTED_DH_ADDONS = frozenset(
+ {
+ # debputy's own
+ "debputy",
+ "zz-debputy",
+ # debhelper provided sequences that should work.
+ "single-binary",
+ }
+)
+DH_ADDONS_TO_REMOVE = frozenset(
+ [
+ # Sequences debputy directly replaces
+ "dwz",
+ "elf-tools",
+ "installinitramfs",
+ "installsysusers",
+ "doxygen",
+ # Sequences that are embedded fully into debputy
+ "bash-completion",
+ "sodeps",
+ ]
+)
+DH_ADDONS_TO_PLUGINS = {
+ "gnome": DHSequenceMigration(
+ "gnome",
+ # The sequence still provides a command for the clean sequence
+ remove_dh_sequence=False,
+ must_use_zz_debputy=True,
+ ),
+ "numpy3": DHSequenceMigration(
+ "numpy3",
+ # The sequence provides (build-time) dependencies that we cannot provide
+ remove_dh_sequence=False,
+ must_use_zz_debputy=True,
+ ),
+ "perl-openssl": DHSequenceMigration(
+ "perl-openssl",
+ # The sequence provides (build-time) dependencies that we cannot provide
+ remove_dh_sequence=False,
+ must_use_zz_debputy=True,
+ ),
+}
+
+
+def _dh_config_file(
+ debian_dir: VirtualPath,
+ dctrl_bin: BinaryPackage,
+ basename: str,
+ helper_name: str,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ manifest: HighLevelManifest,
+ support_executable_files: bool = False,
+ allow_dh_exec_rename: bool = False,
+ pkgfile_lookup: bool = True,
+ remove_on_migration: bool = True,
+) -> Union[Tuple[None, None], Tuple[VirtualPath, Iterable[DHConfigFileLine]]]:
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ dh_config_file = (
+ dhe_pkgfile(debian_dir, dctrl_bin, basename)
+ if pkgfile_lookup
+ else debian_dir.get(basename)
+ )
+ if dh_config_file is None or dh_config_file.is_dir:
+ return None, None
+ if dh_config_file.is_executable and not support_executable_files:
+ primary_key = f"executable-{helper_name}-config"
+ if (
+ primary_key in acceptable_migration_issues
+ or "any-executable-dh-configs" in acceptable_migration_issues
+ ):
+ feature_migration.warn(
+ f'TODO: MANUAL MIGRATION of executable dh config "{dh_config_file}" is required.'
+ )
+ return None, None
+ raise UnsupportedFeature(
+ f"Executable configuration files not supported (found: {dh_config_file}).",
+ [primary_key, "any-executable-dh-configs"],
+ )
+
+ if remove_on_migration:
+ feature_migration.remove_on_success(dh_config_file.fs_path)
+ substitution = DHMigrationSubstitution(
+ dpkg_architecture_table(),
+ acceptable_migration_issues,
+ feature_migration,
+ mutable_manifest,
+ )
+ content = dhe_filedoublearray(
+ dh_config_file,
+ substitution,
+ allow_dh_exec_rename=allow_dh_exec_rename,
+ )
+ return dh_config_file, content
+
+
+def _validate_rm_mv_conffile(
+ package: str,
+ config_line: DHConfigFileLine,
+) -> Tuple[str, str, Optional[str], Optional[str], Optional[str]]:
+ cmd, *args = config_line.tokens
+ if "--" in config_line.tokens:
+ raise ValueError(
+ f'The maintscripts file "{config_line.config_file.path}" for {package} includes a "--" in line'
+ f" {config_line.line_no}. The offending line is: {config_line.original_line}"
+ )
+ if cmd == "rm_conffile":
+ min_args = 1
+ max_args = 3
+ else:
+ min_args = 2
+ max_args = 4
+ if len(args) > max_args or len(args) < min_args:
+ raise ValueError(
+ f'The "{cmd}" command takes at least {min_args} and at most {max_args} arguments. However,'
+ f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), there'
+ f" are {len(args)} arguments. The offending line is: {config_line.original_line}"
+ )
+
+ obsolete_conffile = args[0]
+ new_conffile = args[1] if cmd == "mv_conffile" else None
+ prior_version = args[min_args] if len(args) > min_args else None
+ owning_package = args[min_args + 1] if len(args) > min_args + 1 else None
+ if not obsolete_conffile.startswith("/"):
+ raise ValueError(
+ f'The (old-)conffile parameter for {cmd} must be absolute (i.e., start with "/"). However,'
+ f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it was specified'
+ f' as "{obsolete_conffile}". The offending line is: {config_line.original_line}'
+ )
+ if new_conffile is not None and not new_conffile.startswith("/"):
+ raise ValueError(
+ f'The new-conffile parameter for {cmd} must be absolute (i.e., start with "/"). However,'
+ f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it was specified'
+ f' as "{new_conffile}". The offending line is: {config_line.original_line}'
+ )
+ if prior_version is not None and not PKGVERSION_REGEX.fullmatch(prior_version):
+ raise ValueError(
+ f"The prior-version parameter for {cmd} must be a valid package version (i.e., match"
+ f' {PKGVERSION_REGEX}). However, in "{config_line.config_file.path}" line {config_line.line_no}'
+ f' (for {package}), it was specified as "{prior_version}". The offending line is:'
+ f" {config_line.original_line}"
+ )
+ if owning_package is not None and not PKGNAME_REGEX.fullmatch(owning_package):
+ raise ValueError(
+ f"The package parameter for {cmd} must be a valid package name (i.e., match {PKGNAME_REGEX})."
+ f' However, in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it'
+ f' was specified as "{owning_package}". The offending line is: {config_line.original_line}'
+ )
+ return cmd, obsolete_conffile, new_conffile, prior_version, owning_package
+
+
+_BASH_COMPLETION_RE = re.compile(
+ r"""
+ (^|[|&;])\s*complete.*-[A-Za-z].*
+ | \$\(.*\)
+ | \s*compgen.*-[A-Za-z].*
+ | \s*if.*;.*then/
+""",
+ re.VERBOSE,
+)
+
+
+def migrate_bash_completion(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_bash-completion files"
+ is_single_binary = sum(1 for _ in manifest.all_packages) == 1
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ installations = mutable_manifest.installations(create_if_absent=False)
+
+ for dctrl_bin in manifest.all_packages:
+ dh_file = dhe_pkgfile(debian_dir, dctrl_bin, "bash-completion")
+ if dh_file is None:
+ continue
+ is_bash_completion_file = False
+ with dh_file.open() as fd:
+ for line in fd:
+ line = line.strip()
+ if not line or line[0] == "#":
+ continue
+ if _BASH_COMPLETION_RE.search(line):
+ is_bash_completion_file = True
+ break
+ if not is_bash_completion_file:
+ _, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "bash-completion",
+ "dh_bash-completion",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=True,
+ )
+ else:
+ content = None
+
+ if content:
+ install_dest_sources: List[str] = []
+ install_as_rules: List[Tuple[str, str]] = []
+ for dhe_line in content:
+ if len(dhe_line.tokens) > 2:
+ raise UnsupportedFeature(
+ f"The dh_bash-completion file {dh_file.path} more than two words on"
+ f' line {dhe_line.line_no} (line: "{dhe_line.original_line}").'
+ )
+ source = dhe_line.tokens[0]
+ dest_basename = (
+ dhe_line.tokens[1]
+ if len(dhe_line.tokens) > 1
+ else os.path.basename(source)
+ )
+ if source.startswith("debian/") and not has_glob_magic(source):
+ if dctrl_bin.name != dest_basename:
+ dest_path = (
+ f"debian/{dctrl_bin.name}.{dest_basename}.bash-completion"
+ )
+ else:
+ dest_path = f"debian/{dest_basename}.bash-completion"
+ feature_migration.rename_on_success(source, dest_path)
+ elif len(dhe_line.tokens) == 1:
+ install_dest_sources.append(source)
+ else:
+ install_as_rules.append((source, dest_basename))
+
+ if install_dest_sources:
+ sources = (
+ install_dest_sources
+ if len(install_dest_sources) > 1
+ else install_dest_sources[0]
+ )
+ installations.append(
+ AbstractMutableYAMLInstallRule.install_dest(
+ sources=sources,
+ dest_dir="{{path:BASH_COMPLETION_DIR}}",
+ into=dctrl_bin.name if not is_single_binary else None,
+ )
+ )
+
+ for source, dest_basename in install_as_rules:
+ installations.append(
+ AbstractMutableYAMLInstallRule.install_as(
+ source=source,
+ install_as="{{path:BASH_COMPLETION_DIR}}/" + dest_basename,
+ into=dctrl_bin.name if not is_single_binary else None,
+ )
+ )
+
+
+def migrate_dh_installsystemd_files(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ _acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_installsystemd files"
+ for dctrl_bin in manifest.all_packages:
+ for stem in [
+ "path",
+ "service",
+ "socket",
+ "target",
+ "timer",
+ ]:
+ pkgfile = dhe_pkgfile(
+ debian_dir, dctrl_bin, stem, bug_950723_prefix_matching=True
+ )
+ if not pkgfile:
+ continue
+ if not pkgfile.name.endswith(f".{stem}") or "@." not in pkgfile.name:
+ raise UnsupportedFeature(
+ f'Unable to determine the correct name for {pkgfile.fs_path}. It should be a ".@{stem}"'
+ f" file now (foo@.service => foo.@service)"
+ )
+ newname = pkgfile.name.replace("@.", ".")
+ newname = newname[: -len(stem)] + f"@{stem}"
+ feature_migration.rename_on_success(
+ pkgfile.fs_path, os.path.join(debian_dir.fs_path, newname)
+ )
+
+
+def migrate_maintscript(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_installdeb files"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ for dctrl_bin in manifest.all_packages:
+ mainscript_file, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "maintscript",
+ "dh_installdeb",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ )
+
+ if mainscript_file is None:
+ continue
+ assert content is not None
+
+ package_definition = mutable_manifest.package(dctrl_bin.name)
+ conffiles = {
+ it.obsolete_conffile: it
+ for it in package_definition.conffile_management_items()
+ }
+ seen_conffiles = set()
+
+ for dhe_line in content:
+ cmd = dhe_line.tokens[0]
+ if cmd not in {"rm_conffile", "mv_conffile"}:
+ raise UnsupportedFeature(
+ f"The dh_installdeb file {mainscript_file.path} contains the (currently)"
+ f' unsupported command "{cmd}" on line {dhe_line.line_no}'
+ f' (line: "{dhe_line.original_line}")'
+ )
+
+ try:
+ (
+ _,
+ obsolete_conffile,
+ new_conffile,
+ prior_to_version,
+ owning_package,
+ ) = _validate_rm_mv_conffile(dctrl_bin.name, dhe_line)
+ except ValueError as e:
+ _error(
+ f"Validation error in {mainscript_file} on line {dhe_line.line_no}. The error was: {e.args[0]}."
+ )
+
+ if obsolete_conffile in seen_conffiles:
+ raise ConflictingChange(
+ f'The {mainscript_file} file defines actions for "{obsolete_conffile}" twice!'
+ f" Please ensure that it is defined at most once in that file."
+ )
+ seen_conffiles.add(obsolete_conffile)
+
+ if cmd == "rm_conffile":
+ item = MutableYAMLConffileManagementItem.rm_conffile(
+ obsolete_conffile,
+ prior_to_version,
+ owning_package,
+ )
+ else:
+ assert cmd == "mv_conffile"
+ item = MutableYAMLConffileManagementItem.mv_conffile(
+ obsolete_conffile,
+ assume_not_none(new_conffile),
+ prior_to_version,
+ owning_package,
+ )
+
+ existing_def = conffiles.get(item.obsolete_conffile)
+ if existing_def is not None:
+ if not (
+ item.command == existing_def.command
+ and item.new_conffile == existing_def.new_conffile
+ and item.prior_to_version == existing_def.prior_to_version
+ and item.owning_package == existing_def.owning_package
+ ):
+ raise ConflictingChange(
+ f"The maintscript defines the action {item.command} for"
+ f' "{obsolete_conffile}" in {mainscript_file}, but there is another'
+ f" conffile management definition for same path defined already (in the"
+ f" existing manifest or an migration e.g., inside {mainscript_file})"
+ )
+ feature_migration.already_present += 1
+ continue
+
+ package_definition.add_conffile_management(item)
+ feature_migration.successful_manifest_changes += 1
+
+
+@dataclasses.dataclass(slots=True)
+class SourcesAndConditional:
+ dest_dir: Optional[str] = None
+ sources: List[str] = dataclasses.field(default_factory=list)
+ conditional: Optional[Union[str, Mapping[str, Any]]] = None
+
+
+def _strip_d_tmp(p: str) -> str:
+ if p.startswith("debian/tmp/") and len(p) > 11:
+ return p[11:]
+ return p
+
+
+def migrate_install_file(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_install config files"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ installations = mutable_manifest.installations(create_if_absent=False)
+ priority_lines = []
+ remaining_install_lines = []
+ warn_about_fixmes_in_dest_dir = False
+
+ is_single_binary = sum(1 for _ in manifest.all_packages) == 1
+
+ for dctrl_bin in manifest.all_packages:
+ install_file, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "install",
+ "dh_install",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=True,
+ allow_dh_exec_rename=True,
+ )
+ if not install_file or not content:
+ continue
+ current_sources = []
+ sources_by_destdir: Dict[Tuple[str, Tuple[str, ...]], SourcesAndConditional] = (
+ {}
+ )
+ install_as_rules = []
+ multi_dest = collections.defaultdict(list)
+ seen_sources = set()
+ multi_dest_sources: Set[str] = set()
+
+ for dhe_line in content:
+ special_rule = None
+ if "=>" in dhe_line.tokens:
+ if dhe_line.tokens[0] == "=>" and len(dhe_line.tokens) == 2:
+ # This rule must be as early as possible to retain the semantics
+ path = _strip_d_tmp(
+ _normalize_path(dhe_line.tokens[1], with_prefix=False)
+ )
+ special_rule = AbstractMutableYAMLInstallRule.install_dest(
+ path,
+ dctrl_bin.name if not is_single_binary else None,
+ dest_dir=None,
+ when=dhe_line.conditional(),
+ )
+ elif len(dhe_line.tokens) != 3:
+ _error(
+ f"Validation error in {install_file.path} on line {dhe_line.line_no}. Cannot migrate dh-exec"
+ ' renames that is not exactly "SOURCE => TARGET" or "=> TARGET".'
+ )
+ else:
+ install_rule = AbstractMutableYAMLInstallRule.install_as(
+ _strip_d_tmp(
+ _normalize_path(dhe_line.tokens[0], with_prefix=False)
+ ),
+ _normalize_path(dhe_line.tokens[2], with_prefix=False),
+ dctrl_bin.name if not is_single_binary else None,
+ when=dhe_line.conditional(),
+ )
+ install_as_rules.append(install_rule)
+ else:
+ if len(dhe_line.tokens) > 1:
+ sources = list(
+ _strip_d_tmp(_normalize_path(w, with_prefix=False))
+ for w in dhe_line.tokens[:-1]
+ )
+ dest_dir = _normalize_path(dhe_line.tokens[-1], with_prefix=False)
+ else:
+ sources = list(
+ _strip_d_tmp(_normalize_path(w, with_prefix=False))
+ for w in dhe_line.tokens
+ )
+ dest_dir = None
+
+ multi_dest_sources.update(s for s in sources if s in seen_sources)
+ seen_sources.update(sources)
+
+ if dest_dir is None and dhe_line.conditional() is None:
+ current_sources.extend(sources)
+ continue
+ key = (dest_dir, dhe_line.conditional_key())
+ md = _fetch_or_create(
+ sources_by_destdir,
+ key,
+ # Use named parameters to avoid warnings about the values possible changing
+ # in the next iteration. We always resolve the lambda in this iteration, so
+ # the bug is non-existent. However, that is harder for a linter to prove.
+ lambda *, dest=dest_dir, dhe=dhe_line: SourcesAndConditional(
+ dest_dir=dest,
+ conditional=dhe.conditional(),
+ ),
+ )
+ md.sources.extend(sources)
+
+ if special_rule:
+ priority_lines.append(special_rule)
+
+ remaining_install_lines.extend(install_as_rules)
+
+ for md in sources_by_destdir.values():
+ if multi_dest_sources:
+ sources = [s for s in md.sources if s not in multi_dest_sources]
+ already_installed = (s for s in md.sources if s in multi_dest_sources)
+ for s in already_installed:
+ # The sources are ignored, so we can reuse the object as-is
+ multi_dest[s].append(md)
+ if not sources:
+ continue
+ else:
+ sources = md.sources
+ install_rule = AbstractMutableYAMLInstallRule.install_dest(
+ sources[0] if len(sources) == 1 else sources,
+ dctrl_bin.name if not is_single_binary else None,
+ dest_dir=md.dest_dir,
+ when=md.conditional,
+ )
+ remaining_install_lines.append(install_rule)
+
+ if current_sources:
+ if multi_dest_sources:
+ sources = [s for s in current_sources if s not in multi_dest_sources]
+ already_installed = (
+ s for s in current_sources if s in multi_dest_sources
+ )
+ for s in already_installed:
+ # The sources are ignored, so we can reuse the object as-is
+ dest_dir = os.path.dirname(s)
+ if has_glob_magic(dest_dir):
+ warn_about_fixmes_in_dest_dir = True
+ dest_dir = f"FIXME: {dest_dir} (could not reliably compute the dest dir)"
+ multi_dest[s].append(
+ SourcesAndConditional(
+ dest_dir=dest_dir,
+ conditional=None,
+ )
+ )
+ else:
+ sources = current_sources
+
+ if sources:
+ install_rule = AbstractMutableYAMLInstallRule.install_dest(
+ sources[0] if len(sources) == 1 else sources,
+ dctrl_bin.name if not is_single_binary else None,
+ dest_dir=None,
+ )
+ remaining_install_lines.append(install_rule)
+
+ if multi_dest:
+ for source, dest_and_conditionals in multi_dest.items():
+ dest_dirs = [dac.dest_dir for dac in dest_and_conditionals]
+ # We assume the conditional is the same.
+ conditional = next(
+ iter(
+ dac.conditional
+ for dac in dest_and_conditionals
+ if dac.conditional is not None
+ ),
+ None,
+ )
+ remaining_install_lines.append(
+ AbstractMutableYAMLInstallRule.multi_dest_install(
+ source,
+ dest_dirs,
+ dctrl_bin.name if not is_single_binary else None,
+ when=conditional,
+ )
+ )
+
+ if priority_lines:
+ installations.extend(priority_lines)
+
+ if remaining_install_lines:
+ installations.extend(remaining_install_lines)
+
+ feature_migration.successful_manifest_changes += len(priority_lines) + len(
+ remaining_install_lines
+ )
+ if warn_about_fixmes_in_dest_dir:
+ feature_migration.warn(
+ "TODO: FIXME left in dest-dir(s) of some installation rules."
+ " Please review these and remove the FIXME (plus correct as necessary)"
+ )
+
+
+def migrate_installdocs_file(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_installdocs config files"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ installations = mutable_manifest.installations(create_if_absent=False)
+
+ is_single_binary = sum(1 for _ in manifest.all_packages) == 1
+
+ for dctrl_bin in manifest.all_packages:
+ install_file, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "docs",
+ "dh_installdocs",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=True,
+ )
+ if not install_file:
+ continue
+ assert content is not None
+ docs: List[str] = []
+ for dhe_line in content:
+ if dhe_line.arch_filter or dhe_line.build_profile_filter:
+ _error(
+ f"Unable to migrate line {dhe_line.line_no} of {install_file.path}."
+ " Missing support for conditions."
+ )
+ docs.extend(_normalize_path(w, with_prefix=False) for w in dhe_line.tokens)
+
+ if not docs:
+ continue
+ feature_migration.successful_manifest_changes += 1
+ install_rule = AbstractMutableYAMLInstallRule.install_docs(
+ docs if len(docs) > 1 else docs[0],
+ dctrl_bin.name if not is_single_binary else None,
+ )
+ installations.create_definition_if_missing()
+ installations.append(install_rule)
+
+
+def migrate_installexamples_file(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_installexamples config files"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ installations = mutable_manifest.installations(create_if_absent=False)
+ is_single_binary = sum(1 for _ in manifest.all_packages) == 1
+
+ for dctrl_bin in manifest.all_packages:
+ install_file, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "examples",
+ "dh_installexamples",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=True,
+ )
+ if not install_file:
+ continue
+ assert content is not None
+ examples: List[str] = []
+ for dhe_line in content:
+ if dhe_line.arch_filter or dhe_line.build_profile_filter:
+ _error(
+ f"Unable to migrate line {dhe_line.line_no} of {install_file.path}."
+ " Missing support for conditions."
+ )
+ examples.extend(
+ _normalize_path(w, with_prefix=False) for w in dhe_line.tokens
+ )
+
+ if not examples:
+ continue
+ feature_migration.successful_manifest_changes += 1
+ install_rule = AbstractMutableYAMLInstallRule.install_examples(
+ examples if len(examples) > 1 else examples[0],
+ dctrl_bin.name if not is_single_binary else None,
+ )
+ installations.create_definition_if_missing()
+ installations.append(install_rule)
+
+
+@dataclasses.dataclass(slots=True)
+class InfoFilesDefinition:
+ sources: List[str] = dataclasses.field(default_factory=list)
+ conditional: Optional[Union[str, Mapping[str, Any]]] = None
+
+
+def migrate_installinfo_file(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_installinfo config files"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ installations = mutable_manifest.installations(create_if_absent=False)
+ is_single_binary = sum(1 for _ in manifest.all_packages) == 1
+
+ for dctrl_bin in manifest.all_packages:
+ info_file, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "info",
+ "dh_installinfo",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=True,
+ )
+ if not info_file:
+ continue
+ assert content is not None
+ info_files_by_condition: Dict[Tuple[str, ...], InfoFilesDefinition] = {}
+ for dhe_line in content:
+ key = dhe_line.conditional_key()
+ info_def = _fetch_or_create(
+ info_files_by_condition,
+ key,
+ lambda: InfoFilesDefinition(conditional=dhe_line.conditional()),
+ )
+ info_def.sources.extend(
+ _normalize_path(w, with_prefix=False) for w in dhe_line.tokens
+ )
+
+ if not info_files_by_condition:
+ continue
+ feature_migration.successful_manifest_changes += 1
+ installations.create_definition_if_missing()
+ for info_def in info_files_by_condition.values():
+ info_files = info_def.sources
+ install_rule = AbstractMutableYAMLInstallRule.install_docs(
+ info_files if len(info_files) > 1 else info_files[0],
+ dctrl_bin.name if not is_single_binary else None,
+ dest_dir="{{path:GNU_INFO_DIR}}",
+ when=info_def.conditional,
+ )
+ installations.append(install_rule)
+
+
+@dataclasses.dataclass(slots=True)
+class ManpageDefinition:
+ sources: List[str] = dataclasses.field(default_factory=list)
+ language: Optional[str] = None
+ conditional: Optional[Union[str, Mapping[str, Any]]] = None
+
+
+DK = TypeVar("DK")
+DV = TypeVar("DV")
+
+
+def _fetch_or_create(d: Dict[DK, DV], key: DK, factory: Callable[[], DV]) -> DV:
+ v = d.get(key)
+ if v is None:
+ v = factory()
+ d[key] = v
+ return v
+
+
+def migrate_installman_file(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_installman config files"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ installations = mutable_manifest.installations(create_if_absent=False)
+ is_single_binary = sum(1 for _ in manifest.all_packages) == 1
+ warn_about_basename = False
+
+ for dctrl_bin in manifest.all_packages:
+ manpages_file, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "manpages",
+ "dh_installman",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=True,
+ allow_dh_exec_rename=True,
+ )
+ if not manpages_file:
+ continue
+ assert content is not None
+
+ vanilla_definitions = []
+ install_as_rules = []
+ complex_definitions: Dict[
+ Tuple[Optional[str], Tuple[str, ...]], ManpageDefinition
+ ] = {}
+ install_rule: AbstractMutableYAMLInstallRule
+ for dhe_line in content:
+ if "=>" in dhe_line.tokens:
+ # dh-exec allows renaming features. For `debputy`, we degenerate it into an `install` (w. `as`) feature
+ # without any of the `install-man` features.
+ if dhe_line.tokens[0] == "=>" and len(dhe_line.tokens) == 2:
+ _error(
+ f'Unsupported "=> DEST" rule for error in {manpages_file.path} on line {dhe_line.line_no}."'
+ f' Cannot migrate dh-exec renames that is not exactly "SOURCE => TARGET" for d/manpages files.'
+ )
+ elif len(dhe_line.tokens) != 3:
+ _error(
+ f"Validation error in {manpages_file.path} on line {dhe_line.line_no}. Cannot migrate dh-exec"
+ ' renames that is not exactly "SOURCE => TARGET" or "=> TARGET".'
+ )
+ else:
+ install_rule = AbstractMutableYAMLInstallRule.install_doc_as(
+ _normalize_path(dhe_line.tokens[0], with_prefix=False),
+ _normalize_path(dhe_line.tokens[2], with_prefix=False),
+ dctrl_bin.name if not is_single_binary else None,
+ when=dhe_line.conditional(),
+ )
+ install_as_rules.append(install_rule)
+ continue
+
+ sources = [_normalize_path(w, with_prefix=False) for w in dhe_line.tokens]
+ needs_basename = any(
+ MAN_GUESS_FROM_BASENAME.search(x)
+ and not MAN_GUESS_LANG_FROM_PATH.search(x)
+ for x in sources
+ )
+ if needs_basename or dhe_line.conditional() is not None:
+ if needs_basename:
+ warn_about_basename = True
+ language = "derive-from-basename"
+ else:
+ language = None
+ key = (language, dhe_line.conditional_key())
+ manpage_def = _fetch_or_create(
+ complex_definitions,
+ key,
+ lambda: ManpageDefinition(
+ language=language, conditional=dhe_line.conditional()
+ ),
+ )
+ manpage_def.sources.extend(sources)
+ else:
+ vanilla_definitions.extend(sources)
+
+ if not install_as_rules and not vanilla_definitions and not complex_definitions:
+ continue
+ feature_migration.successful_manifest_changes += 1
+ installations.create_definition_if_missing()
+ installations.extend(install_as_rules)
+ if vanilla_definitions:
+ man_source = (
+ vanilla_definitions
+ if len(vanilla_definitions) > 1
+ else vanilla_definitions[0]
+ )
+ install_rule = AbstractMutableYAMLInstallRule.install_man(
+ man_source,
+ dctrl_bin.name if not is_single_binary else None,
+ None,
+ )
+ installations.append(install_rule)
+ for manpage_def in complex_definitions.values():
+ sources = manpage_def.sources
+ install_rule = AbstractMutableYAMLInstallRule.install_man(
+ sources if len(sources) > 1 else sources[0],
+ dctrl_bin.name if not is_single_binary else None,
+ manpage_def.language,
+ when=manpage_def.conditional,
+ )
+ installations.append(install_rule)
+
+ if warn_about_basename:
+ feature_migration.warn(
+ 'Detected manpages that might rely on "derive-from-basename" logic. Please double check'
+ " that the generated `install-man` rules are correct"
+ )
+
+
+def migrate_not_installed_file(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_missing's not-installed config file"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ installations = mutable_manifest.installations(create_if_absent=False)
+ main_binary = [p for p in manifest.all_packages if p.is_main_package][0]
+
+ missing_file, content = _dh_config_file(
+ debian_dir,
+ main_binary,
+ "not-installed",
+ "dh_missing",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=False,
+ pkgfile_lookup=False,
+ )
+ discard_rules: List[str] = []
+ if missing_file:
+ assert content is not None
+ for dhe_line in content:
+ discard_rules.extend(
+ _normalize_path(w, with_prefix=False) for w in dhe_line.tokens
+ )
+
+ if discard_rules:
+ feature_migration.successful_manifest_changes += 1
+ install_rule = AbstractMutableYAMLInstallRule.discard(
+ discard_rules if len(discard_rules) > 1 else discard_rules[0],
+ )
+ installations.create_definition_if_missing()
+ installations.append(install_rule)
+
+
+def detect_pam_files(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ _acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "detect dh_installpam files (min dh compat)"
+ for dctrl_bin in manifest.all_packages:
+ dh_config_file = dhe_pkgfile(debian_dir, dctrl_bin, "pam")
+ if dh_config_file is not None:
+ feature_migration.assumed_compat = 14
+ break
+
+
+def migrate_tmpfile(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ _acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_installtmpfiles config files"
+ for dctrl_bin in manifest.all_packages:
+ dh_config_file = dhe_pkgfile(debian_dir, dctrl_bin, "tmpfile")
+ if dh_config_file is not None:
+ target = (
+ dh_config_file.name.replace(".tmpfile", ".tmpfiles")
+ if "." in dh_config_file.name
+ else "tmpfiles"
+ )
+ _rename_file_if_exists(
+ debian_dir,
+ dh_config_file.name,
+ target,
+ feature_migration,
+ )
+
+
+def migrate_lintian_overrides_files(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_lintian config files"
+ for dctrl_bin in manifest.all_packages:
+ # We do not support executable lintian-overrides and `_dh_config_file` handles all of that.
+ # Therefore, the return value is irrelevant to us.
+ _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "lintian-overrides",
+ "dh_lintian",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=False,
+ remove_on_migration=False,
+ )
+
+
+def migrate_links_files(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh_link files"
+ mutable_manifest = assume_not_none(manifest.mutable_manifest)
+ for dctrl_bin in manifest.all_packages:
+ links_file, content = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "links",
+ "dh_link",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=True,
+ )
+
+ if links_file is None:
+ continue
+ assert content is not None
+
+ package_definition = mutable_manifest.package(dctrl_bin.name)
+ defined_symlink = {
+ symlink.symlink_path: symlink.symlink_target
+ for symlink in package_definition.symlinks()
+ }
+
+ seen_symlinks: Set[str] = set()
+
+ for dhe_line in content:
+ if len(dhe_line.tokens) != 2:
+ raise UnsupportedFeature(
+ f"The dh_link file {links_file.fs_path} did not have exactly two paths on line"
+ f' {dhe_line.line_no} (line: "{dhe_line.original_line}"'
+ )
+ target, source = dhe_line.tokens
+ if source in seen_symlinks:
+ # According to #934499, this has happened in the wild already
+ raise ConflictingChange(
+ f"The {links_file.fs_path} file defines the link path {source} twice! Please ensure"
+ " that it is defined at most once in that file"
+ )
+ seen_symlinks.add(source)
+ # Symlinks in .links are always considered absolute, but you were not required to have a leading slash.
+ # However, in the debputy manifest, you can have relative links, so we should ensure it is explicitly
+ # absolute.
+ if not target.startswith("/"):
+ target = "/" + target
+ existing_target = defined_symlink.get(source)
+ if existing_target is not None:
+ if existing_target != target:
+ raise ConflictingChange(
+ f'The symlink "{source}" points to "{target}" in {links_file}, but there is'
+ f' another symlink with same path pointing to "{existing_target}" defined'
+ " already (in the existing manifest or an migration e.g., inside"
+ f" {links_file.fs_path})"
+ )
+ feature_migration.already_present += 1
+ continue
+ condition = dhe_line.conditional()
+ package_definition.add_symlink(
+ MutableYAMLSymlink.new_symlink(
+ source,
+ target,
+ condition,
+ )
+ )
+ feature_migration.successful_manifest_changes += 1
+
+
+def migrate_misspelled_readme_debian_files(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "misspelled README.Debian files"
+ for dctrl_bin in manifest.all_packages:
+ readme, _ = _dh_config_file(
+ debian_dir,
+ dctrl_bin,
+ "README.debian",
+ "dh_installdocs",
+ acceptable_migration_issues,
+ feature_migration,
+ manifest,
+ support_executable_files=False,
+ remove_on_migration=False,
+ )
+ if readme is None:
+ continue
+ new_name = readme.name.replace("README.debian", "README.Debian")
+ assert readme.name != new_name
+ _rename_file_if_exists(
+ debian_dir,
+ readme.name,
+ new_name,
+ feature_migration,
+ )
+
+
+def migrate_doc_base_files(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ _: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "doc-base files"
+ # ignore the dh_make ".EX" file if one should still be present. The dh_installdocs tool ignores it too.
+ possible_effected_doc_base_files = [
+ f
+ for f in debian_dir.iterdir
+ if (
+ (".doc-base." in f.name or f.name.startswith("doc-base."))
+ and not f.name.endswith("doc-base.EX")
+ )
+ ]
+ known_packages = {d.name: d for d in manifest.all_packages}
+ main_package = [d for d in manifest.all_packages if d.is_main_package][0]
+ for doc_base_file in possible_effected_doc_base_files:
+ parts = doc_base_file.name.split(".")
+ owning_package = known_packages.get(parts[0])
+ if owning_package is None:
+ owning_package = main_package
+ package_part = None
+ else:
+ package_part = parts[0]
+ parts = parts[1:]
+
+ if not parts or parts[0] != "doc-base":
+ # Not a doc-base file after all
+ continue
+
+ if len(parts) > 1:
+ name_part = ".".join(parts[1:])
+ if package_part is None:
+ # Named files must have a package prefix
+ package_part = owning_package.name
+ else:
+ # No rename needed
+ continue
+
+ new_basename = ".".join(filter(None, (package_part, name_part, "doc-base")))
+ _rename_file_if_exists(
+ debian_dir,
+ doc_base_file.name,
+ new_basename,
+ feature_migration,
+ )
+
+
+def migrate_dh_hook_targets(
+ debian_dir: VirtualPath,
+ _: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ migration_target: str,
+) -> None:
+ feature_migration.tagline = "dh hook targets"
+ source_root = os.path.dirname(debian_dir.fs_path)
+ if source_root == "":
+ source_root = "."
+ detected_hook_targets = json.loads(
+ subprocess.check_output(
+ ["dh_assistant", "detect-hook-targets"],
+ cwd=source_root,
+ ).decode("utf-8")
+ )
+ sample_hook_target: Optional[str] = None
+ replaced_commands = DH_COMMANDS_REPLACED[migration_target]
+
+ for hook_target_def in detected_hook_targets["hook-targets"]:
+ if hook_target_def["is-empty"]:
+ continue
+ command = hook_target_def["command"]
+ if command not in replaced_commands:
+ continue
+ hook_target = hook_target_def["target-name"]
+ if sample_hook_target is None:
+ sample_hook_target = hook_target
+ feature_migration.warn(
+ f"TODO: MANUAL MIGRATION required for hook target {hook_target}"
+ )
+ if (
+ feature_migration.warnings
+ and "dh-hook-targets" not in acceptable_migration_issues
+ ):
+ assert sample_hook_target
+ raise UnsupportedFeature(
+ f"The debian/rules file contains one or more non empty dh hook targets that will not"
+ f" be run with the requested debputy dh sequence. One of these would be"
+ f" {sample_hook_target}.",
+ ["dh-hook-targets"],
+ )
+
+
+def detect_unsupported_zz_debputy_features(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "Known unsupported features"
+
+ for unsupported_config in UNSUPPORTED_DH_CONFIGS_AND_TOOLS_FOR_ZZ_DEBPUTY:
+ _unsupported_debhelper_config_file(
+ debian_dir,
+ manifest,
+ unsupported_config,
+ acceptable_migration_issues,
+ feature_migration,
+ )
+
+
+def detect_obsolete_substvars(
+ debian_dir: VirtualPath,
+ _manifest: HighLevelManifest,
+ _acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = (
+ "Check for obsolete ${foo:var} variables in debian/control"
+ )
+ ctrl_file = debian_dir.get("control")
+ if not ctrl_file:
+ feature_migration.warn(
+ "Cannot find debian/control. Detection of obsolete substvars could not be performed."
+ )
+ return
+ with ctrl_file.open() as fd:
+ ctrl = list(Deb822.iter_paragraphs(fd))
+
+ relationship_fields = dpkg_field_list_pkg_dep()
+ relationship_fields_lc = frozenset(x.lower() for x in relationship_fields)
+
+ for p in ctrl[1:]:
+ seen_obsolete_relationship_substvars = set()
+ obsolete_fields = set()
+ is_essential = p.get("Essential") == "yes"
+ for df in relationship_fields:
+ field: Optional[str] = p.get(df)
+ if field is None:
+ continue
+ df_lc = df.lower()
+ number_of_relations = 0
+ obsolete_substvars_in_field = set()
+ for d in (d.strip() for d in field.strip().split(",")):
+ if not d:
+ continue
+ number_of_relations += 1
+ if not d.startswith("${"):
+ continue
+ try:
+ end_idx = d.index("}")
+ except ValueError:
+ continue
+ substvar_name = d[2:end_idx]
+ if ":" not in substvar_name:
+ continue
+ _, field = substvar_name.rsplit(":", 1)
+ field_lc = field.lower()
+ if field_lc not in relationship_fields_lc:
+ continue
+ is_obsolete = field_lc == df_lc
+ if (
+ not is_obsolete
+ and is_essential
+ and substvar_name.lower() == "shlibs:depends"
+ and df_lc == "pre-depends"
+ ):
+ is_obsolete = True
+
+ if is_obsolete:
+ obsolete_substvars_in_field.add(d)
+
+ if number_of_relations == len(obsolete_substvars_in_field):
+ obsolete_fields.add(df)
+ else:
+ seen_obsolete_relationship_substvars.update(obsolete_substvars_in_field)
+
+ package = p.get("Package", "(Missing package name!?)")
+ if obsolete_fields:
+ fields = ", ".join(obsolete_fields)
+ feature_migration.warn(
+ f"The following relationship fields can be removed from {package}: {fields}."
+ f" (The content in them would be applied automatically.)"
+ )
+ if seen_obsolete_relationship_substvars:
+ v = ", ".join(sorted(seen_obsolete_relationship_substvars))
+ feature_migration.warn(
+ f"The following relationship substitution variables can be removed from {package}: {v}"
+ )
+
+
+def read_dh_addon_sequences(
+ debian_dir: VirtualPath,
+) -> Optional[Tuple[Set[str], Set[str]]]:
+ ctrl_file = debian_dir.get("control")
+ if ctrl_file:
+ dr_sequences: Set[str] = set()
+ bd_sequences = set()
+
+ drules = debian_dir.get("rules")
+ if drules and drules.is_file:
+ parse_drules_for_addons(drules, dr_sequences)
+
+ with ctrl_file.open() as fd:
+ ctrl = list(Deb822.iter_paragraphs(fd))
+ source_paragraph = ctrl[0] if ctrl else {}
+
+ extract_dh_addons_from_control(source_paragraph, bd_sequences)
+ return bd_sequences, dr_sequences
+ return None
+
+
+def detect_dh_addons_zz_debputy_rrr(
+ debian_dir: VirtualPath,
+ _manifest: HighLevelManifest,
+ _acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "Check for dh-sequence-addons"
+ r = read_dh_addon_sequences(debian_dir)
+ if r is None:
+ feature_migration.warn(
+ "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon"
+ " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy-rrr."
+ )
+ return
+
+ bd_sequences, dr_sequences = r
+
+ remaining_sequences = bd_sequences | dr_sequences
+ saw_dh_debputy = "zz-debputy-rrr" in remaining_sequences
+
+ if not saw_dh_debputy:
+ feature_migration.warn("Missing Build-Depends on dh-sequence-zz-debputy-rrr")
+
+
+def detect_dh_addons(
+ debian_dir: VirtualPath,
+ _manifest: HighLevelManifest,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+ _migration_target: str,
+) -> None:
+ feature_migration.tagline = "Check for dh-sequence-addons"
+ r = read_dh_addon_sequences(debian_dir)
+ if r is None:
+ feature_migration.warn(
+ "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon"
+ " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy"
+ " and not rely on any other debhelper sequence addons except those debputy explicitly supports."
+ )
+ return
+
+ bd_sequences, dr_sequences = r
+
+ remaining_sequences = bd_sequences | dr_sequences
+ saw_dh_debputy = (
+ "debputy" in remaining_sequences or "zz-debputy" in remaining_sequences
+ )
+ saw_zz_debputy = "zz-debputy" in remaining_sequences
+ must_use_zz_debputy = False
+ remaining_sequences -= SUPPORTED_DH_ADDONS
+ for sequence in remaining_sequences & DH_ADDONS_TO_PLUGINS.keys():
+ migration = DH_ADDONS_TO_PLUGINS[sequence]
+ feature_migration.require_plugin(migration.debputy_plugin)
+ if migration.remove_dh_sequence:
+ if migration.must_use_zz_debputy:
+ must_use_zz_debputy = True
+ if sequence in bd_sequences:
+ feature_migration.warn(
+ f"TODO: MANUAL MIGRATION - Remove build-dependency on dh-sequence-{sequence}"
+ f" (replaced by debputy-plugin-{migration.debputy_plugin})"
+ )
+ else:
+ feature_migration.warn(
+ f"TODO: MANUAL MIGRATION - Remove --with {sequence} from dh in d/rules"
+ f" (replaced by debputy-plugin-{migration.debputy_plugin})"
+ )
+
+ remaining_sequences -= DH_ADDONS_TO_PLUGINS.keys()
+
+ alt_key = "unsupported-dh-sequences"
+ for sequence in remaining_sequences & DH_ADDONS_TO_REMOVE:
+ if sequence in bd_sequences:
+ feature_migration.warn(
+ f"TODO: MANUAL MIGRATION - Remove build dependency on dh-sequence-{sequence}"
+ )
+ else:
+ feature_migration.warn(
+ f"TODO: MANUAL MIGRATION - Remove --with {sequence} from dh in d/rules"
+ )
+
+ remaining_sequences -= DH_ADDONS_TO_REMOVE
+
+ for sequence in remaining_sequences:
+ key = f"unsupported-dh-sequence-{sequence}"
+ msg = f'The dh addon "{sequence}" is not known to work with dh-debputy and might malfunction'
+ if (
+ key not in acceptable_migration_issues
+ and alt_key not in acceptable_migration_issues
+ ):
+ raise UnsupportedFeature(msg, [key, alt_key])
+ feature_migration.warn(msg)
+
+ if not saw_dh_debputy:
+ feature_migration.warn("Missing Build-Depends on dh-sequence-zz-debputy")
+ elif must_use_zz_debputy and not saw_zz_debputy:
+ feature_migration.warn(
+ "Please use the zz-debputy sequence rather than the debputy (needed due to dh add-on load order)"
+ )
+
+
+def _rename_file_if_exists(
+ debian_dir: VirtualPath,
+ source: str,
+ dest: str,
+ feature_migration: FeatureMigration,
+) -> None:
+ source_path = debian_dir.get(source)
+ dest_path = debian_dir.get(dest)
+ spath = (
+ source_path.path
+ if source_path is not None
+ else os.path.join(debian_dir.path, source)
+ )
+ dpath = (
+ dest_path.path if dest_path is not None else os.path.join(debian_dir.path, dest)
+ )
+ if source_path is not None and source_path.is_file:
+ if dest_path is not None:
+ if not dest_path.is_file:
+ feature_migration.warnings.append(
+ f'TODO: MANUAL MIGRATION - there is a "{spath}" (file) and "{dpath}" (not a file).'
+ f' The migration wanted to replace "{spath}" with "{dpath}", but since "{dpath}" is not'
+ " a file, this step is left as a manual migration."
+ )
+ return
+ if (
+ subprocess.call(["cmp", "-s", source_path.fs_path, dest_path.fs_path])
+ != 0
+ ):
+ feature_migration.warnings.append(
+ f'TODO: MANUAL MIGRATION - there is a "{source_path.path}" and "{dest_path.path}"'
+ f" file. Normally these files are for the same package and there would only be one of"
+ f" them. In this case, they both exist but their content differs. Be advised that"
+ f' debputy tool will use the "{dest_path.path}".'
+ )
+ else:
+ feature_migration.remove_on_success(dest_path.fs_path)
+ else:
+ feature_migration.rename_on_success(
+ source_path.fs_path,
+ os.path.join(debian_dir.fs_path, dest),
+ )
+ elif source_path is not None:
+ feature_migration.warnings.append(
+ f'TODO: MANUAL MIGRATION - The migration would normally have renamed "{spath}" to "{dpath}".'
+ f' However, the migration assumed "{spath}" would be a file and it is not. Therefore, this step'
+ " as a manual migration."
+ )
+
+
+def _find_dh_config_file_for_any_pkg(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ unsupported_config: UnsupportedDHConfig,
+) -> Iterable[VirtualPath]:
+ for dctrl_bin in manifest.all_packages:
+ dh_config_file = dhe_pkgfile(
+ debian_dir,
+ dctrl_bin,
+ unsupported_config.dh_config_basename,
+ bug_950723_prefix_matching=unsupported_config.bug_950723_prefix_matching,
+ )
+ if dh_config_file is not None:
+ yield dh_config_file
+
+
+def _unsupported_debhelper_config_file(
+ debian_dir: VirtualPath,
+ manifest: HighLevelManifest,
+ unsupported_config: UnsupportedDHConfig,
+ acceptable_migration_issues: AcceptableMigrationIssues,
+ feature_migration: FeatureMigration,
+) -> None:
+ dh_config_files = list(
+ _find_dh_config_file_for_any_pkg(debian_dir, manifest, unsupported_config)
+ )
+ if not dh_config_files:
+ return
+ dh_tool = unsupported_config.dh_tool
+ basename = unsupported_config.dh_config_basename
+ file_stem = (
+ f"@{basename}" if unsupported_config.bug_950723_prefix_matching else basename
+ )
+ dh_config_file = dh_config_files[0]
+ if unsupported_config.is_missing_migration:
+ feature_migration.warn(
+ f'Missing migration support for the "{dh_config_file.path}" debhelper config file'
+ f" (used by {dh_tool}). Manual migration may be feasible depending on the exact features"
+ " required."
+ )
+ return
+ primary_key = f"unsupported-dh-config-file-{file_stem}"
+ secondary_key = "any-unsupported-dh-config-file"
+ if (
+ primary_key not in acceptable_migration_issues
+ and secondary_key not in acceptable_migration_issues
+ ):
+ msg = (
+ f'The "{dh_config_file.path}" debhelper config file (used by {dh_tool} is currently not'
+ " supported by debputy."
+ )
+ raise UnsupportedFeature(
+ msg,
+ [primary_key, secondary_key],
+ )
+ for dh_config_file in dh_config_files:
+ feature_migration.warn(
+ f'TODO: MANUAL MIGRATION - Use of unsupported "{dh_config_file.path}" file (used by {dh_tool})'
+ )