Coverage for src/debputy/dh_migration/migrators_impl.py: 80%
775 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-09-06 13:40 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-09-06 13:40 +0000
1import collections
2import dataclasses
3import functools
4import json
5import os
6import re
7import subprocess
8from collections.abc import Iterable, Mapping, Callable, Container
9from itertools import product, chain
10from typing import (
11 Any,
12 TypeVar,
13)
15from debian.deb822 import Deb822
17from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable
18from debputy.deb_packaging_support import dpkg_field_list_pkg_dep
19from debputy.dh.debhelper_emulation import (
20 dhe_filedoublearray,
21 DHConfigFileLine,
22 dhe_pkgfile,
23)
24from debputy.dh.dh_assistant import (
25 read_dh_addon_sequences,
26)
27from debputy.dh_migration.models import (
28 ConflictingChange,
29 FeatureMigration,
30 UnsupportedFeature,
31 AcceptableMigrationIssues,
32 DHMigrationSubstitution,
33 MigrationRequest,
34)
35from debputy.highlevel_manifest import (
36 MutableYAMLSymlink,
37 HighLevelManifest,
38 MutableYAMLConffileManagementItem,
39 AbstractMutableYAMLInstallRule,
40)
41from debputy.installations import MAN_GUESS_FROM_BASENAME, MAN_GUESS_LANG_FROM_PATH
42from debputy.maintscript_snippet import SUPPORTED_UDEB_SCRIPTS, DPKG_DEB_CONTROL_SCRIPTS
43from debputy.packages import BinaryPackage
44from debputy.plugin.api import VirtualPath
45from debputy.plugin.api.spec import (
46 INTEGRATION_MODE_DH_DEBPUTY_RRR,
47 INTEGRATION_MODE_DH_DEBPUTY,
48 DebputyIntegrationMode,
49 INTEGRATION_MODE_FULL,
50)
51from debputy.util import (
52 _error,
53 PKGVERSION_REGEX,
54 PKGNAME_REGEX,
55 _normalize_path,
56 assume_not_none,
57 has_glob_magic,
58)
59from debputy.version import debputy_doc_root_dir
62class ContainsEverything:
64 def __contains__(self, item: str) -> bool:
65 return True
68# Align with debputy.py
69DH_COMMANDS_REPLACED: Mapping[DebputyIntegrationMode, Container[str]] = {
70 INTEGRATION_MODE_DH_DEBPUTY_RRR: frozenset(
71 {
72 "dh_fixperms",
73 "dh_shlibdeps",
74 "dh_gencontrol",
75 "dh_md5sums",
76 "dh_builddeb",
77 }
78 ),
79 INTEGRATION_MODE_DH_DEBPUTY: frozenset(
80 {
81 "dh_install",
82 "dh_installdocs",
83 "dh_installchangelogs",
84 "dh_installexamples",
85 "dh_installman",
86 "dh_installcatalogs",
87 "dh_installcron",
88 "dh_installdebconf",
89 "dh_installemacsen",
90 "dh_installifupdown",
91 "dh_installinfo",
92 "dh_installinit",
93 "dh_installsysusers",
94 "dh_installtmpfiles",
95 "dh_installsystemd",
96 "dh_installsystemduser",
97 "dh_installmenu",
98 "dh_installmime",
99 "dh_installmodules",
100 "dh_installlogcheck",
101 "dh_installlogrotate",
102 "dh_installpam",
103 "dh_installppp",
104 "dh_installudev",
105 "dh_installgsettings",
106 "dh_installinitramfs",
107 "dh_installalternatives",
108 "dh_bugfiles",
109 "dh_ucf",
110 "dh_lintian",
111 "dh_icons",
112 "dh_usrlocal",
113 "dh_perl",
114 "dh_link",
115 "dh_installwm",
116 "dh_installxfonts",
117 "dh_strip_nondeterminism",
118 "dh_compress",
119 "dh_fixperms",
120 "dh_dwz",
121 "dh_strip",
122 "dh_makeshlibs",
123 "dh_shlibdeps",
124 "dh_missing",
125 "dh_installdeb",
126 "dh_computeautosubstvars",
127 "dh_gencontrol",
128 "dh_md5sums",
129 "dh_builddeb",
130 }
131 ),
132 INTEGRATION_MODE_FULL: ContainsEverything(),
133}
135_GS_DOC = f"{debputy_doc_root_dir()}/GETTING-STARTED-WITH-dh-debputy.md"
136MIGRATION_AID_FOR_OVERRIDDEN_COMMANDS = {
137 "dh_installinit": f"{_GS_DOC}#covert-your-overrides-for-dh_installsystemd-dh_installinit-if-any",
138 "dh_installsystemd": f"{_GS_DOC}#covert-your-overrides-for-dh_installsystemd-dh_installinit-if-any",
139 "dh_fixperms": f"{_GS_DOC}#convert-your-overrides-or-excludes-for-dh_fixperms-if-any",
140 "dh_gencontrol": f"{_GS_DOC}#convert-your-overrides-for-dh_gencontrol-if-any",
141}
144@dataclasses.dataclass(frozen=True, slots=True)
145class UnsupportedDHConfig:
146 dh_config_basename: str
147 dh_tool: str
148 bug_950723_prefix_matching: bool = False
149 is_missing_migration: bool = False
152@dataclasses.dataclass(frozen=True, slots=True)
153class DHSequenceMigration:
154 debputy_plugin: str
155 remove_dh_sequence: bool = True
156 must_use_zz_debputy: bool = False
159UNSUPPORTED_DH_CONFIGS_AND_TOOLS_FOR_ZZ_DEBPUTY = [
160 UnsupportedDHConfig("config", "dh_installdebconf"),
161 UnsupportedDHConfig("templates", "dh_installdebconf"),
162 UnsupportedDHConfig("emacsen-compat", "dh_installemacsen"),
163 UnsupportedDHConfig("emacsen-install", "dh_installemacsen"),
164 UnsupportedDHConfig("emacsen-remove", "dh_installemacsen"),
165 UnsupportedDHConfig("emacsen-startup", "dh_installemacsen"),
166 # The `upstart` file should be long dead, but we might as well detect it.
167 UnsupportedDHConfig("upstart", "dh_installinit"),
168 # dh_installsystemduser
169 UnsupportedDHConfig(
170 "user.path", "dh_installsystemduser", bug_950723_prefix_matching=False
171 ),
172 UnsupportedDHConfig(
173 "user.path", "dh_installsystemduser", bug_950723_prefix_matching=True
174 ),
175 UnsupportedDHConfig(
176 "user.service", "dh_installsystemduser", bug_950723_prefix_matching=False
177 ),
178 UnsupportedDHConfig(
179 "user.service", "dh_installsystemduser", bug_950723_prefix_matching=True
180 ),
181 UnsupportedDHConfig(
182 "user.socket", "dh_installsystemduser", bug_950723_prefix_matching=False
183 ),
184 UnsupportedDHConfig(
185 "user.socket", "dh_installsystemduser", bug_950723_prefix_matching=True
186 ),
187 UnsupportedDHConfig(
188 "user.target", "dh_installsystemduser", bug_950723_prefix_matching=False
189 ),
190 UnsupportedDHConfig(
191 "user.target", "dh_installsystemduser", bug_950723_prefix_matching=True
192 ),
193 UnsupportedDHConfig(
194 "user.timer", "dh_installsystemduser", bug_950723_prefix_matching=False
195 ),
196 UnsupportedDHConfig(
197 "user.timer", "dh_installsystemduser", bug_950723_prefix_matching=True
198 ),
199 UnsupportedDHConfig("menu", "dh_installmenu"),
200 UnsupportedDHConfig("menu-method", "dh_installmenu"),
201 UnsupportedDHConfig("ucf", "dh_ucf"),
202 UnsupportedDHConfig("wm", "dh_installwm"),
203 UnsupportedDHConfig("triggers", "dh_installdeb"),
204 UnsupportedDHConfig("menutest", "dh_installdeb"),
205 UnsupportedDHConfig("isinstallable", "dh_installdeb"),
206]
207SUPPORTED_DH_ADDONS_WITH_ZZ_DEBPUTY = frozenset(
208 {
209 # debputy's own
210 "debputy",
211 "zz-debputy",
212 # debhelper provided sequences that should work.
213 "single-binary",
214 }
215)
216DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY = frozenset(
217 [
218 # The `zz-debputy` add-on replaces the `zz-debputy-rrr` plugin.
219 "zz-debputy-rrr",
220 # Sequences debputy directly replaces
221 "dwz",
222 "elf-tools",
223 "installinitramfs",
224 "installsysusers",
225 "doxygen",
226 # Sequences that are embedded fully into debputy
227 "bash-completion",
228 "shell-completions",
229 "sodeps",
230 "builtusing",
231 ]
232)
233DH_ADDONS_TO_PLUGINS = {
234 "gnome": DHSequenceMigration(
235 "gnome",
236 # The sequence still provides a command for the clean sequence
237 remove_dh_sequence=False,
238 must_use_zz_debputy=True,
239 ),
240 "grantlee": DHSequenceMigration(
241 "grantlee",
242 remove_dh_sequence=True,
243 must_use_zz_debputy=True,
244 ),
245 "numpy3": DHSequenceMigration(
246 "numpy3",
247 # The sequence provides (build-time) dependencies that we cannot provide
248 remove_dh_sequence=False,
249 must_use_zz_debputy=True,
250 ),
251 "perl-openssl": DHSequenceMigration(
252 "perl-openssl",
253 # The sequence provides (build-time) dependencies that we cannot provide
254 remove_dh_sequence=False,
255 must_use_zz_debputy=True,
256 ),
257}
260def _dh_config_file(
261 debian_dir: VirtualPath,
262 dctrl_bin: BinaryPackage,
263 basename: str,
264 helper_name: str,
265 acceptable_migration_issues: AcceptableMigrationIssues,
266 feature_migration: FeatureMigration,
267 manifest: HighLevelManifest,
268 support_executable_files: bool = False,
269 allow_dh_exec_rename: bool = False,
270 pkgfile_lookup: bool = True,
271 remove_on_migration: bool = True,
272) -> tuple[None, None] | tuple[VirtualPath, Iterable[DHConfigFileLine]]:
273 mutable_manifest = assume_not_none(manifest.mutable_manifest)
274 dh_config_file = (
275 dhe_pkgfile(debian_dir, dctrl_bin, basename)
276 if pkgfile_lookup
277 else debian_dir.get(basename)
278 )
279 if dh_config_file is None or dh_config_file.is_dir:
280 return None, None
281 if dh_config_file.is_executable and not support_executable_files:
282 primary_key = f"executable-{helper_name}-config"
283 if (
284 primary_key in acceptable_migration_issues
285 or "any-executable-dh-configs" in acceptable_migration_issues
286 ):
287 feature_migration.warn(
288 f'TODO: MANUAL MIGRATION of executable dh config "{dh_config_file}" is required.'
289 )
290 return None, None
291 raise UnsupportedFeature(
292 f"Executable configuration files not supported (found: {dh_config_file}).",
293 [primary_key, "any-executable-dh-configs"],
294 )
296 if remove_on_migration:
297 feature_migration.remove_on_success(dh_config_file.fs_path)
298 substitution = DHMigrationSubstitution(
299 DpkgArchitectureBuildProcessValuesTable(),
300 acceptable_migration_issues,
301 feature_migration,
302 mutable_manifest,
303 )
304 content = dhe_filedoublearray(
305 dh_config_file,
306 substitution,
307 allow_dh_exec_rename=allow_dh_exec_rename,
308 )
309 return dh_config_file, content
312def _validate_rm_mv_conffile(
313 package: str,
314 config_line: DHConfigFileLine,
315) -> tuple[str, str, str | None, str | None, str | None]:
316 cmd, *args = config_line.tokens
317 if "--" in config_line.tokens: 317 ↛ 318line 317 didn't jump to line 318 because the condition on line 317 was never true
318 raise ValueError(
319 f'The maintscripts file "{config_line.config_file.path}" for {package} includes a "--" in line'
320 f" {config_line.line_no}. The offending line is: {config_line.original_line}"
321 )
322 if cmd == "rm_conffile":
323 min_args = 1
324 max_args = 3
325 else:
326 min_args = 2
327 max_args = 4
328 if len(args) > max_args or len(args) < min_args: 328 ↛ 329line 328 didn't jump to line 329 because the condition on line 328 was never true
329 raise ValueError(
330 f'The "{cmd}" command takes at least {min_args} and at most {max_args} arguments. However,'
331 f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), there'
332 f" are {len(args)} arguments. The offending line is: {config_line.original_line}"
333 )
335 obsolete_conffile = args[0]
336 new_conffile = args[1] if cmd == "mv_conffile" else None
337 prior_version = args[min_args] if len(args) > min_args else None
338 owning_package = args[min_args + 1] if len(args) > min_args + 1 else None
339 if not obsolete_conffile.startswith("/"): 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true
340 raise ValueError(
341 f'The (old-)conffile parameter for {cmd} must be absolute (i.e., start with "/"). However,'
342 f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it was specified'
343 f' as "{obsolete_conffile}". The offending line is: {config_line.original_line}'
344 )
345 if new_conffile is not None and not new_conffile.startswith("/"): 345 ↛ 346line 345 didn't jump to line 346 because the condition on line 345 was never true
346 raise ValueError(
347 f'The new-conffile parameter for {cmd} must be absolute (i.e., start with "/"). However,'
348 f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it was specified'
349 f' as "{new_conffile}". The offending line is: {config_line.original_line}'
350 )
351 if prior_version is not None and not PKGVERSION_REGEX.fullmatch(prior_version): 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true
352 raise ValueError(
353 f"The prior-version parameter for {cmd} must be a valid package version (i.e., match"
354 f' {PKGVERSION_REGEX}). However, in "{config_line.config_file.path}" line {config_line.line_no}'
355 f' (for {package}), it was specified as "{prior_version}". The offending line is:'
356 f" {config_line.original_line}"
357 )
358 if owning_package is not None and not PKGNAME_REGEX.fullmatch(owning_package): 358 ↛ 359line 358 didn't jump to line 359 because the condition on line 358 was never true
359 raise ValueError(
360 f"The package parameter for {cmd} must be a valid package name (i.e., match {PKGNAME_REGEX})."
361 f' However, in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it'
362 f' was specified as "{owning_package}". The offending line is: {config_line.original_line}'
363 )
364 return cmd, obsolete_conffile, new_conffile, prior_version, owning_package
367_BASH_COMPLETION_RE = re.compile(
368 r"""
369 (^|[|&;])\s*complete.*-[A-Za-z].*
370 | \$\(.*\)
371 | \s*compgen.*-[A-Za-z].*
372 | \s*if.*;.*then/
373""",
374 re.VERBOSE,
375)
378def migrate_bash_completion(
379 migration_request: MigrationRequest,
380 feature_migration: FeatureMigration,
381) -> None:
382 feature_migration.tagline = "dh_bash-completion files"
383 is_single_binary = migration_request.is_single_binary_package
384 manifest = migration_request.manifest
385 debian_dir = migration_request.debian_dir
386 mutable_manifest = assume_not_none(manifest.mutable_manifest)
387 installations = mutable_manifest.installations(create_if_absent=False)
389 for dctrl_bin in migration_request.all_packages:
390 dh_file = dhe_pkgfile(debian_dir, dctrl_bin, "bash-completion")
391 if dh_file is None:
392 continue
393 is_bash_completion_file = False
394 with dh_file.open() as fd:
395 for line in fd:
396 line = line.strip()
397 if not line or line[0] == "#": 397 ↛ 398line 397 didn't jump to line 398 because the condition on line 397 was never true
398 continue
399 if _BASH_COMPLETION_RE.search(line):
400 is_bash_completion_file = True
401 break
402 if not is_bash_completion_file:
403 _, content = _dh_config_file(
404 debian_dir,
405 dctrl_bin,
406 "bash-completion",
407 "dh_bash-completion",
408 migration_request.acceptable_migration_issues,
409 feature_migration,
410 manifest,
411 support_executable_files=True,
412 )
413 else:
414 content = None
416 if content:
417 install_dest_sources: list[str] = []
418 install_as_rules: list[tuple[str, str]] = []
419 for dhe_line in content:
420 if len(dhe_line.tokens) > 2: 420 ↛ 421line 420 didn't jump to line 421 because the condition on line 420 was never true
421 raise UnsupportedFeature(
422 f"The dh_bash-completion file {dh_file.path} more than two words on"
423 f' line {dhe_line.line_no} (line: "{dhe_line.original_line}").'
424 )
425 source = dhe_line.tokens[0]
426 dest_basename = (
427 dhe_line.tokens[1]
428 if len(dhe_line.tokens) > 1
429 else os.path.basename(source)
430 )
431 if source.startswith("debian/") and not has_glob_magic(source):
432 if dctrl_bin.name != dest_basename:
433 dest_path = (
434 f"debian/{dctrl_bin.name}.{dest_basename}.bash-completion"
435 )
436 else:
437 dest_path = f"debian/{dest_basename}.bash-completion"
438 feature_migration.rename_on_success(source, dest_path)
439 elif len(dhe_line.tokens) == 1:
440 install_dest_sources.append(source)
441 else:
442 install_as_rules.append((source, dest_basename))
444 if install_dest_sources: 444 ↛ 458line 444 didn't jump to line 458 because the condition on line 444 was always true
445 sources: list[str] | str = (
446 install_dest_sources
447 if len(install_dest_sources) > 1
448 else install_dest_sources[0]
449 )
450 installations.append(
451 AbstractMutableYAMLInstallRule.install_dest(
452 sources=sources,
453 dest_dir="{{path:BASH_COMPLETION_DIR}}",
454 into=dctrl_bin.name if not is_single_binary else None,
455 )
456 )
458 for source, dest_basename in install_as_rules:
459 installations.append(
460 AbstractMutableYAMLInstallRule.install_as(
461 source=source,
462 install_as="{{path:BASH_COMPLETION_DIR}}/" + dest_basename,
463 into=dctrl_bin.name if not is_single_binary else None,
464 )
465 )
468_SHELL_COMPLETIONS_RE = re.compile(r"^\s*\S+\s+\S+\s+\S")
471def migrate_shell_completions(
472 migration_request: MigrationRequest,
473 feature_migration: FeatureMigration,
474) -> None:
475 feature_migration.tagline = "dh_shell_completions files"
476 manifest = migration_request.manifest
477 debian_dir = migration_request.debian_dir
478 is_single_binary = migration_request.is_single_binary_package
479 mutable_manifest = assume_not_none(manifest.mutable_manifest)
480 installations = mutable_manifest.installations(create_if_absent=False)
481 # Note: The bash completion script used `bash-completion` whereas `dh_shell_completions` uses
482 # `...-completions` (note the trailing `s`). In `debputy`, we always use the singular notation
483 # because we use "one file, one completion (ruleset)".
484 completions = ["bash", "fish", "zsh"]
486 for completion, dctrl_bin in product(completions, migration_request.all_packages):
487 dh_file = dhe_pkgfile(debian_dir, dctrl_bin, f"{completion}-completions")
488 if dh_file is None:
489 continue
490 is_completion_file = False
491 with dh_file.open() as fd:
492 for line in fd:
493 line = line.strip()
494 if not line or line[0] == "#":
495 continue
496 if _SHELL_COMPLETIONS_RE.search(line):
497 is_completion_file = True
498 break
499 if is_completion_file:
500 dest_path = f"debian/{dctrl_bin.name}.{completion}-completion"
501 feature_migration.rename_on_success(dh_file.fs_path, dest_path)
502 continue
504 _, content = _dh_config_file(
505 debian_dir,
506 dctrl_bin,
507 f"{completion}-completions",
508 "dh_shell_completions",
509 migration_request.acceptable_migration_issues,
510 feature_migration,
511 manifest,
512 remove_on_migration=True,
513 )
515 if content: 515 ↛ 486line 515 didn't jump to line 486 because the condition on line 515 was always true
516 install_dest_sources: list[str] = []
517 install_as_rules: list[tuple[str, str]] = []
518 for dhe_line in content:
519 if len(dhe_line.tokens) > 2: 519 ↛ 520line 519 didn't jump to line 520 because the condition on line 519 was never true
520 raise UnsupportedFeature(
521 f"The dh_shell_completions file {dh_file.path} more than two words on"
522 f' line {dhe_line.line_no} (line: "{dhe_line.original_line}").'
523 )
524 source = dhe_line.tokens[0]
525 dest_basename = (
526 dhe_line.tokens[1]
527 if len(dhe_line.tokens) > 1
528 else os.path.basename(source)
529 )
530 if source.startswith("debian/") and not has_glob_magic(source):
531 if dctrl_bin.name != dest_basename:
532 dest_path = f"debian/{dctrl_bin.name}.{dest_basename}.{completion}-completion"
533 else:
534 dest_path = f"debian/{dest_basename}.{completion}-completion"
535 feature_migration.rename_on_success(source, dest_path)
536 elif len(dhe_line.tokens) == 1:
537 install_dest_sources.append(source)
538 else:
539 install_as_rules.append((source, dest_basename))
541 completion_dir_variable = (
542 "{{path:" + f"{completion.upper()}_COMPLETION_DIR" + "}}"
543 )
545 if install_dest_sources: 545 ↛ 559line 545 didn't jump to line 559 because the condition on line 545 was always true
546 sources: list[str] | str = (
547 install_dest_sources
548 if len(install_dest_sources) > 1
549 else install_dest_sources[0]
550 )
551 installations.append(
552 AbstractMutableYAMLInstallRule.install_dest(
553 sources=sources,
554 dest_dir=completion_dir_variable,
555 into=dctrl_bin.name if not is_single_binary else None,
556 )
557 )
559 for source, dest_basename in install_as_rules:
560 installations.append(
561 AbstractMutableYAMLInstallRule.install_as(
562 source=source,
563 install_as=f"{completion_dir_variable}/{dest_basename}",
564 into=dctrl_bin.name if not is_single_binary else None,
565 )
566 )
569def migrate_dh_builtusing(
570 migration_request: MigrationRequest,
571 feature_migration: FeatureMigration,
572) -> None:
573 feature_migration.tagline = "dh_builtusing configuration"
574 for pkg in migration_request.all_packages:
575 built_using = pkg.fields.get("Built-Using", "")
576 static_built_using = pkg.fields.get("Static-Built-Using", "")
578 if (
579 "${dh-builtusing:" in built_using
580 or "${dh-builtusing:" in static_built_using
581 ):
582 # TODO: Automate when migration can update `d/control`.
583 feature_migration.warn(
584 f"Migrate all `${ dh-builtusing:custom-pattern} ` instances in the"
585 f" (Static-)Built-Using of {pkg.name} to"
586 f" `packages.{pkg.name}.(static-)built-using.sources-for: glob-pattern`"
587 f" in `debian/debputy.manifest`"
588 )
591def migrate_dh_installsystemd_files(
592 migration_request: MigrationRequest,
593 feature_migration: FeatureMigration,
594) -> None:
595 debian_dir = migration_request.debian_dir
596 feature_migration.tagline = "dh_installsystemd files"
597 for dctrl_bin in migration_request.all_packages:
598 for stem in [
599 "path",
600 "service",
601 "socket",
602 "target",
603 "timer",
604 ]:
605 pkgfile = dhe_pkgfile(
606 debian_dir, dctrl_bin, stem, bug_950723_prefix_matching=True
607 )
608 if not pkgfile:
609 continue
610 if not pkgfile.name.endswith(f".{stem}") or "@." not in pkgfile.name: 610 ↛ 611line 610 didn't jump to line 611 because the condition on line 610 was never true
611 raise UnsupportedFeature(
612 f'Unable to determine the correct name for {pkgfile.fs_path}. It should be a ".@{stem}"'
613 f" file now (foo@.service => foo.@service)"
614 )
615 newname = pkgfile.name.replace("@.", ".")
616 newname = newname[: -len(stem)] + f"@{stem}"
617 feature_migration.rename_on_success(
618 pkgfile.fs_path, os.path.join(debian_dir.fs_path, newname)
619 )
622def migrate_clean_file(
623 migration_request: MigrationRequest,
624 feature_migration: FeatureMigration,
625) -> None:
626 feature_migration.tagline = "debian/clean"
627 clean_file = migration_request.debian_dir.get("clean")
628 if clean_file is None:
629 return
631 mutable_manifest = assume_not_none(migration_request.manifest.mutable_manifest)
633 substitution = DHMigrationSubstitution(
634 DpkgArchitectureBuildProcessValuesTable(),
635 migration_request.acceptable_migration_issues,
636 feature_migration,
637 mutable_manifest,
638 )
639 content = dhe_filedoublearray(
640 clean_file,
641 substitution,
642 )
644 remove_during_clean_rules = mutable_manifest.remove_during_clean(
645 create_if_absent=False
646 )
647 tokens = chain.from_iterable(c.tokens for c in content)
648 rules_before = len(remove_during_clean_rules)
649 remove_during_clean_rules.extend(tokens)
650 rules_after = len(remove_during_clean_rules)
651 feature_migration.successful_manifest_changes += rules_after - rules_before
652 feature_migration.remove_on_success(clean_file.fs_path)
655def migrate_maintscript(
656 migration_request: MigrationRequest,
657 feature_migration: FeatureMigration,
658) -> None:
659 feature_migration.tagline = "dh_installdeb files"
660 manifest = migration_request.manifest
661 mutable_manifest = assume_not_none(manifest.mutable_manifest)
662 for dctrl_bin in migration_request.all_packages:
663 mainscript_file, content = _dh_config_file(
664 migration_request.debian_dir,
665 dctrl_bin,
666 "maintscript",
667 "dh_installdeb",
668 migration_request.acceptable_migration_issues,
669 feature_migration,
670 manifest,
671 )
673 if mainscript_file is None:
674 continue
675 assert content is not None
677 package_definition = mutable_manifest.package(dctrl_bin.name)
678 conffiles = {
679 it.obsolete_conffile: it
680 for it in package_definition.conffile_management_items()
681 }
682 seen_conffiles = set()
684 for dhe_line in content:
685 cmd = dhe_line.tokens[0]
686 if cmd not in {"rm_conffile", "mv_conffile"}: 686 ↛ 687line 686 didn't jump to line 687 because the condition on line 686 was never true
687 raise UnsupportedFeature(
688 f"The dh_installdeb file {mainscript_file.path} contains the (currently)"
689 f' unsupported command "{cmd}" on line {dhe_line.line_no}'
690 f' (line: "{dhe_line.original_line}")'
691 )
693 try:
694 (
695 _,
696 obsolete_conffile,
697 new_conffile,
698 prior_to_version,
699 owning_package,
700 ) = _validate_rm_mv_conffile(dctrl_bin.name, dhe_line)
701 except ValueError as e:
702 _error(
703 f"Validation error in {mainscript_file} on line {dhe_line.line_no}. The error was: {e.args[0]}."
704 )
706 if obsolete_conffile in seen_conffiles: 706 ↛ 707line 706 didn't jump to line 707 because the condition on line 706 was never true
707 raise ConflictingChange(
708 f'The {mainscript_file} file defines actions for "{obsolete_conffile}" twice!'
709 f" Please ensure that it is defined at most once in that file."
710 )
711 seen_conffiles.add(obsolete_conffile)
713 if cmd == "rm_conffile":
714 item = MutableYAMLConffileManagementItem.rm_conffile(
715 obsolete_conffile,
716 prior_to_version,
717 owning_package,
718 )
719 else:
720 assert cmd == "mv_conffile"
721 item = MutableYAMLConffileManagementItem.mv_conffile(
722 obsolete_conffile,
723 assume_not_none(new_conffile),
724 prior_to_version,
725 owning_package,
726 )
728 existing_def = conffiles.get(item.obsolete_conffile)
729 if existing_def is not None: 729 ↛ 730line 729 didn't jump to line 730 because the condition on line 729 was never true
730 if not (
731 item.command == existing_def.command
732 and item.new_conffile == existing_def.new_conffile
733 and item.prior_to_version == existing_def.prior_to_version
734 and item.owning_package == existing_def.owning_package
735 ):
736 raise ConflictingChange(
737 f"The maintscript defines the action {item.command} for"
738 f' "{obsolete_conffile}" in {mainscript_file}, but there is another'
739 f" conffile management definition for same path defined already (in the"
740 f" existing manifest or an migration e.g., inside {mainscript_file})"
741 )
742 continue
744 package_definition.add_conffile_management(item)
745 feature_migration.successful_manifest_changes += 1
748@dataclasses.dataclass(slots=True)
749class SourcesAndConditional:
750 dest_dir: str | None = None
751 sources: list[str] = dataclasses.field(default_factory=list)
752 conditional: str | Mapping[str, Any] | None = None
755def _raise_on_unsupported_path(
756 p: str,
757 path: VirtualPath,
758 line_no: int,
759) -> str:
760 if p.startswith("../") or any(s == ".." for s in p.split("/")): 760 ↛ 761line 760 didn't jump to line 761 because the condition on line 760 was never true
761 _error(
762 f"Sorry, the path name {p!r} provided in {path.fs_path} on line {line_no} is not supported. Please rewrite it to a path relative to the package root without upward segments"
763 )
764 return p
767def _strip_d_tmp(p: str, path: VirtualPath, line_no: int) -> str:
768 if p.startswith("debian/tmp/") and len(p) > 11:
769 return p[11:]
770 if p.startswith("../"):
771 pruned = p[3:]
772 if pruned.startswith("../"): 772 ↛ 773line 772 didn't jump to line 773 because the condition on line 772 was never true
773 _raise_on_unsupported_path(p, path, line_no)
774 _error(
775 f"Internal error: _raise_on_unsupported_path should have rejected the path at {p!r} ({path.fs_path}:{line_no})"
776 )
777 return f"debian/{pruned}"
779 return p
782def migrate_install_file(
783 migration_request: MigrationRequest,
784 feature_migration: FeatureMigration,
785) -> None:
786 feature_migration.tagline = "dh_install config files"
787 manifest = migration_request.manifest
788 mutable_manifest = assume_not_none(manifest.mutable_manifest)
789 installations = mutable_manifest.installations(create_if_absent=False)
790 priority_lines = []
791 remaining_install_lines = []
792 warn_about_fixmes_in_dest_dir = False
794 is_single_binary = migration_request.is_single_binary_package
796 for dctrl_bin in migration_request.all_packages:
797 install_file, content = _dh_config_file(
798 migration_request.debian_dir,
799 dctrl_bin,
800 "install",
801 "dh_install",
802 migration_request.acceptable_migration_issues,
803 feature_migration,
804 manifest,
805 support_executable_files=True,
806 allow_dh_exec_rename=True,
807 )
808 if not install_file or not content:
809 continue
810 current_sources = []
811 sources_by_destdir: dict[tuple[str, tuple[str, ...]], SourcesAndConditional] = (
812 {}
813 )
814 install_as_rules = []
815 multi_dest = collections.defaultdict(list)
816 seen_sources = set()
817 multi_dest_sources: set[str] = set()
819 for dhe_line in content:
820 special_rule = None
821 if "=>" in dhe_line.tokens:
822 if dhe_line.tokens[0] == "=>" and len(dhe_line.tokens) == 2:
823 # This rule must be as early as possible to retain the semantics
824 path = _strip_d_tmp(
825 _normalize_path(
826 dhe_line.tokens[1],
827 with_prefix=False,
828 allow_and_keep_upward_segments=True,
829 ),
830 dhe_line.config_file,
831 dhe_line.line_no,
832 )
833 special_rule = AbstractMutableYAMLInstallRule.install_dest(
834 path,
835 dctrl_bin.name if not is_single_binary else None,
836 dest_dir=None,
837 when=dhe_line.conditional(),
838 )
839 elif len(dhe_line.tokens) != 3: 839 ↛ 840line 839 didn't jump to line 840 because the condition on line 839 was never true
840 _error(
841 f"Validation error in {install_file.path} on line {dhe_line.line_no}. Cannot migrate dh-exec"
842 ' renames that is not exactly "SOURCE => TARGET" or "=> TARGET".'
843 )
844 else:
845 install_rule = AbstractMutableYAMLInstallRule.install_as(
846 _strip_d_tmp(
847 _normalize_path(
848 dhe_line.tokens[0],
849 with_prefix=False,
850 allow_and_keep_upward_segments=True,
851 ),
852 dhe_line.config_file,
853 dhe_line.line_no,
854 ),
855 _raise_on_unsupported_path(
856 _normalize_path(
857 dhe_line.tokens[2],
858 with_prefix=False,
859 allow_and_keep_upward_segments=True,
860 ),
861 dhe_line.config_file,
862 dhe_line.line_no,
863 ),
864 dctrl_bin.name if not is_single_binary else None,
865 when=dhe_line.conditional(),
866 )
867 install_as_rules.append(install_rule)
868 else:
869 if len(dhe_line.tokens) > 1:
870 sources = list(
871 _strip_d_tmp(
872 _normalize_path(
873 w,
874 with_prefix=False,
875 allow_and_keep_upward_segments=True,
876 ),
877 dhe_line.config_file,
878 dhe_line.line_no,
879 )
880 for w in dhe_line.tokens[:-1]
881 )
882 dest_dir = _raise_on_unsupported_path(
883 _normalize_path(
884 dhe_line.tokens[-1],
885 with_prefix=False,
886 allow_and_keep_upward_segments=True,
887 ),
888 dhe_line.config_file,
889 dhe_line.line_no,
890 )
891 else:
892 sources = list(
893 _strip_d_tmp(
894 _normalize_path(
895 w,
896 with_prefix=False,
897 allow_and_keep_upward_segments=True,
898 ),
899 dhe_line.config_file,
900 dhe_line.line_no,
901 )
902 for w in dhe_line.tokens
903 )
904 dest_dir = None
906 multi_dest_sources.update(s for s in sources if s in seen_sources)
907 seen_sources.update(sources)
909 if dest_dir is None and dhe_line.conditional() is None:
910 current_sources.extend(sources)
911 continue
912 key = (dest_dir, dhe_line.conditional_key())
913 ctor = functools.partial(
914 SourcesAndConditional,
915 dest_dir=dest_dir,
916 conditional=dhe_line.conditional(),
917 )
918 md = _fetch_or_create(
919 sources_by_destdir,
920 key,
921 ctor,
922 )
923 md.sources.extend(sources)
925 if special_rule:
926 priority_lines.append(special_rule)
928 remaining_install_lines.extend(install_as_rules)
930 for md in sources_by_destdir.values():
931 if multi_dest_sources:
932 sources = [s for s in md.sources if s not in multi_dest_sources]
933 already_installed = (s for s in md.sources if s in multi_dest_sources)
934 for s in already_installed:
935 # The sources are ignored, so we can reuse the object as-is
936 multi_dest[s].append(md)
937 if not sources:
938 continue
939 else:
940 sources = md.sources
941 install_rule = AbstractMutableYAMLInstallRule.install_dest(
942 sources[0] if len(sources) == 1 else sources,
943 dctrl_bin.name if not is_single_binary else None,
944 dest_dir=md.dest_dir,
945 when=md.conditional,
946 )
947 remaining_install_lines.append(install_rule)
949 if current_sources:
950 if multi_dest_sources:
951 sources = [s for s in current_sources if s not in multi_dest_sources]
952 already_installed = (
953 s for s in current_sources if s in multi_dest_sources
954 )
955 for s in already_installed:
956 # The sources are ignored, so we can reuse the object as-is
957 dest_dir = os.path.dirname(s)
958 if has_glob_magic(dest_dir):
959 warn_about_fixmes_in_dest_dir = True
960 dest_dir = f"FIXME: {dest_dir} (could not reliably compute the dest dir)"
961 multi_dest[s].append(
962 SourcesAndConditional(
963 dest_dir=dest_dir,
964 conditional=None,
965 )
966 )
967 else:
968 sources = current_sources
970 if sources:
971 install_rule = AbstractMutableYAMLInstallRule.install_dest(
972 sources[0] if len(sources) == 1 else sources,
973 dctrl_bin.name if not is_single_binary else None,
974 dest_dir=None,
975 )
976 remaining_install_lines.append(install_rule)
978 if multi_dest:
979 for source, dest_and_conditionals in multi_dest.items():
980 dest_dirs = [dac.dest_dir for dac in dest_and_conditionals]
981 # We assume the conditional is the same.
982 conditional = next(
983 iter(
984 dac.conditional
985 for dac in dest_and_conditionals
986 if dac.conditional is not None
987 ),
988 None,
989 )
990 remaining_install_lines.append(
991 AbstractMutableYAMLInstallRule.multi_dest_install(
992 source,
993 dest_dirs,
994 dctrl_bin.name if not is_single_binary else None,
995 when=conditional,
996 )
997 )
999 if priority_lines:
1000 installations.extend(priority_lines)
1002 if remaining_install_lines:
1003 installations.extend(remaining_install_lines)
1005 feature_migration.successful_manifest_changes += len(priority_lines) + len(
1006 remaining_install_lines
1007 )
1008 if warn_about_fixmes_in_dest_dir:
1009 feature_migration.warn(
1010 "TODO: FIXME left in dest-dir(s) of some installation rules."
1011 " Please review these and remove the FIXME (plus correct as necessary)"
1012 )
1015def migrate_installdocs_file(
1016 migration_request: MigrationRequest,
1017 feature_migration: FeatureMigration,
1018) -> None:
1019 feature_migration.tagline = "dh_installdocs config files"
1020 manifest = migration_request.manifest
1021 mutable_manifest = assume_not_none(manifest.mutable_manifest)
1022 installations = mutable_manifest.installations(create_if_absent=False)
1024 is_single_binary = migration_request.is_single_binary_package
1026 for dctrl_bin in migration_request.all_packages:
1027 install_file, content = _dh_config_file(
1028 migration_request.debian_dir,
1029 dctrl_bin,
1030 "docs",
1031 "dh_installdocs",
1032 migration_request.acceptable_migration_issues,
1033 feature_migration,
1034 manifest,
1035 support_executable_files=True,
1036 )
1037 if not install_file:
1038 continue
1039 assert content is not None
1040 docs: list[str] = []
1041 for dhe_line in content:
1042 if dhe_line.arch_filter or dhe_line.build_profile_filter: 1042 ↛ 1043line 1042 didn't jump to line 1043 because the condition on line 1042 was never true
1043 _error(
1044 f"Unable to migrate line {dhe_line.line_no} of {install_file.path}."
1045 " Missing support for conditions."
1046 )
1047 docs.extend(
1048 _raise_on_unsupported_path(
1049 _normalize_path(
1050 w, with_prefix=False, allow_and_keep_upward_segments=True
1051 ),
1052 dhe_line.config_file,
1053 dhe_line.line_no,
1054 )
1055 for w in dhe_line.tokens
1056 )
1058 if not docs: 1058 ↛ 1059line 1058 didn't jump to line 1059 because the condition on line 1058 was never true
1059 continue
1060 feature_migration.successful_manifest_changes += 1
1061 install_rule = AbstractMutableYAMLInstallRule.install_docs(
1062 docs if len(docs) > 1 else docs[0],
1063 dctrl_bin.name if not is_single_binary else None,
1064 )
1065 installations.create_definition_if_missing()
1066 installations.append(install_rule)
1069def migrate_installexamples_file(
1070 migration_request: MigrationRequest,
1071 feature_migration: FeatureMigration,
1072) -> None:
1073 feature_migration.tagline = "dh_installexamples config files"
1074 manifest = migration_request.manifest
1075 mutable_manifest = assume_not_none(manifest.mutable_manifest)
1076 installations = mutable_manifest.installations(create_if_absent=False)
1077 is_single_binary = migration_request.is_single_binary_package
1079 for dctrl_bin in manifest.all_packages:
1080 install_file, content = _dh_config_file(
1081 migration_request.debian_dir,
1082 dctrl_bin,
1083 "examples",
1084 "dh_installexamples",
1085 migration_request.acceptable_migration_issues,
1086 feature_migration,
1087 manifest,
1088 support_executable_files=True,
1089 )
1090 if not install_file:
1091 continue
1092 assert content is not None
1093 examples: list[str] = []
1094 for dhe_line in content:
1095 if dhe_line.arch_filter or dhe_line.build_profile_filter: 1095 ↛ 1096line 1095 didn't jump to line 1096 because the condition on line 1095 was never true
1096 _error(
1097 f"Unable to migrate line {dhe_line.line_no} of {install_file.path}."
1098 " Missing support for conditions."
1099 )
1100 examples.extend(
1101 _raise_on_unsupported_path(
1102 _normalize_path(
1103 w, with_prefix=False, allow_and_keep_upward_segments=True
1104 ),
1105 dhe_line.config_file,
1106 dhe_line.line_no,
1107 )
1108 for w in dhe_line.tokens
1109 )
1111 if not examples: 1111 ↛ 1112line 1111 didn't jump to line 1112 because the condition on line 1111 was never true
1112 continue
1113 feature_migration.successful_manifest_changes += 1
1114 install_rule = AbstractMutableYAMLInstallRule.install_examples(
1115 examples if len(examples) > 1 else examples[0],
1116 dctrl_bin.name if not is_single_binary else None,
1117 )
1118 installations.create_definition_if_missing()
1119 installations.append(install_rule)
1122@dataclasses.dataclass(slots=True)
1123class InfoFilesDefinition:
1124 sources: list[str] = dataclasses.field(default_factory=list)
1125 conditional: str | Mapping[str, Any] | None = None
1128def migrate_installinfo_file(
1129 migration_request: MigrationRequest,
1130 feature_migration: FeatureMigration,
1131) -> None:
1132 feature_migration.tagline = "dh_installinfo config files"
1133 manifest = migration_request.manifest
1134 mutable_manifest = assume_not_none(manifest.mutable_manifest)
1135 installations = mutable_manifest.installations(create_if_absent=False)
1136 is_single_binary = migration_request.is_single_binary_package
1138 for dctrl_bin in manifest.all_packages:
1139 info_file, content = _dh_config_file(
1140 migration_request.debian_dir,
1141 dctrl_bin,
1142 "info",
1143 "dh_installinfo",
1144 migration_request.acceptable_migration_issues,
1145 feature_migration,
1146 manifest,
1147 support_executable_files=True,
1148 )
1149 if not info_file:
1150 continue
1151 assert content is not None
1152 info_files_by_condition: dict[tuple[str, ...], InfoFilesDefinition] = {}
1153 for dhe_line in content:
1154 key = dhe_line.conditional_key()
1155 ctr = functools.partial(
1156 InfoFilesDefinition, conditional=dhe_line.conditional()
1157 )
1158 info_def = _fetch_or_create(
1159 info_files_by_condition,
1160 key,
1161 ctr,
1162 )
1163 info_def.sources.extend(
1164 _raise_on_unsupported_path(
1165 _normalize_path(
1166 w, with_prefix=False, allow_and_keep_upward_segments=True
1167 ),
1168 dhe_line.config_file,
1169 dhe_line.line_no,
1170 )
1171 for w in dhe_line.tokens
1172 )
1174 if not info_files_by_condition: 1174 ↛ 1175line 1174 didn't jump to line 1175 because the condition on line 1174 was never true
1175 continue
1176 feature_migration.successful_manifest_changes += 1
1177 installations.create_definition_if_missing()
1178 for info_def in info_files_by_condition.values():
1179 info_files = info_def.sources
1180 install_rule = AbstractMutableYAMLInstallRule.install_docs(
1181 info_files if len(info_files) > 1 else info_files[0],
1182 dctrl_bin.name if not is_single_binary else None,
1183 dest_dir="{{path:GNU_INFO_DIR}}",
1184 when=info_def.conditional,
1185 )
1186 installations.append(install_rule)
1189@dataclasses.dataclass(slots=True)
1190class ManpageDefinition:
1191 sources: list[str] = dataclasses.field(default_factory=list)
1192 language: str | None = None
1193 conditional: str | Mapping[str, Any] | None = None
1196DK = TypeVar("DK")
1197DV = TypeVar("DV")
1200def _fetch_or_create(d: dict[DK, DV], key: DK, factory: Callable[[], DV]) -> DV:
1201 v = d.get(key)
1202 if v is None:
1203 v = factory()
1204 d[key] = v
1205 return v
1208def migrate_installman_file(
1209 migration_request: MigrationRequest,
1210 feature_migration: FeatureMigration,
1211) -> None:
1212 feature_migration.tagline = "dh_installman config files"
1213 manifest = migration_request.manifest
1214 mutable_manifest = assume_not_none(manifest.mutable_manifest)
1215 installations = mutable_manifest.installations(create_if_absent=False)
1216 is_single_binary = migration_request.is_single_binary_package
1217 warn_about_basename = False
1219 for dctrl_bin in migration_request.all_packages:
1220 manpages_file, content = _dh_config_file(
1221 migration_request.debian_dir,
1222 dctrl_bin,
1223 "manpages",
1224 "dh_installman",
1225 migration_request.acceptable_migration_issues,
1226 feature_migration,
1227 manifest,
1228 support_executable_files=True,
1229 allow_dh_exec_rename=True,
1230 )
1231 if not manpages_file:
1232 continue
1233 assert content is not None
1235 vanilla_definitions = []
1236 install_as_rules = []
1237 complex_definitions: dict[
1238 tuple[str | None, tuple[str, ...]], ManpageDefinition
1239 ] = {}
1240 install_rule: AbstractMutableYAMLInstallRule
1241 for dhe_line in content:
1242 if "=>" in dhe_line.tokens: 1242 ↛ 1245line 1242 didn't jump to line 1245 because the condition on line 1242 was never true
1243 # dh-exec allows renaming features. For `debputy`, we degenerate it into an `install` (w. `as`) feature
1244 # without any of the `install-man` features.
1245 if dhe_line.tokens[0] == "=>" and len(dhe_line.tokens) == 2:
1246 _error(
1247 f'Unsupported "=> DEST" rule for error in {manpages_file.path} on line {dhe_line.line_no}."'
1248 f' Cannot migrate dh-exec renames that is not exactly "SOURCE => TARGET" for d/manpages files.'
1249 )
1250 elif len(dhe_line.tokens) != 3:
1251 _error(
1252 f"Validation error in {manpages_file.path} on line {dhe_line.line_no}. Cannot migrate dh-exec"
1253 ' renames that is not exactly "SOURCE => TARGET" or "=> TARGET".'
1254 )
1255 else:
1256 install_rule = AbstractMutableYAMLInstallRule.install_doc_as(
1257 _raise_on_unsupported_path(
1258 _normalize_path(
1259 dhe_line.tokens[0],
1260 with_prefix=False,
1261 allow_and_keep_upward_segments=True,
1262 ),
1263 dhe_line.config_file,
1264 dhe_line.line_no,
1265 ),
1266 _raise_on_unsupported_path(
1267 _normalize_path(
1268 dhe_line.tokens[2],
1269 with_prefix=False,
1270 allow_and_keep_upward_segments=True,
1271 ),
1272 dhe_line.config_file,
1273 dhe_line.line_no,
1274 ),
1275 dctrl_bin.name if not is_single_binary else None,
1276 when=dhe_line.conditional(),
1277 )
1278 install_as_rules.append(install_rule)
1279 continue
1281 sources = [
1282 _raise_on_unsupported_path(
1283 _normalize_path(
1284 w, with_prefix=False, allow_and_keep_upward_segments=True
1285 ),
1286 dhe_line.config_file,
1287 dhe_line.line_no,
1288 )
1289 for w in dhe_line.tokens
1290 ]
1291 needs_basename = any(
1292 MAN_GUESS_FROM_BASENAME.search(x)
1293 and not MAN_GUESS_LANG_FROM_PATH.search(x)
1294 for x in sources
1295 )
1296 if needs_basename or dhe_line.conditional() is not None:
1297 if needs_basename: 1297 ↛ 1301line 1297 didn't jump to line 1301 because the condition on line 1297 was always true
1298 warn_about_basename = True
1299 language = "derive-from-basename"
1300 else:
1301 language = None
1302 key = (language, dhe_line.conditional_key())
1303 ctor = functools.partial(
1304 ManpageDefinition,
1305 language=language,
1306 conditional=dhe_line.conditional(),
1307 )
1308 manpage_def = _fetch_or_create(
1309 complex_definitions,
1310 key,
1311 ctor,
1312 )
1313 manpage_def.sources.extend(sources)
1314 else:
1315 vanilla_definitions.extend(sources)
1317 if not install_as_rules and not vanilla_definitions and not complex_definitions: 1317 ↛ 1318line 1317 didn't jump to line 1318 because the condition on line 1317 was never true
1318 continue
1319 feature_migration.successful_manifest_changes += 1
1320 installations.create_definition_if_missing()
1321 installations.extend(install_as_rules)
1322 if vanilla_definitions: 1322 ↛ 1334line 1322 didn't jump to line 1334 because the condition on line 1322 was always true
1323 man_source = (
1324 vanilla_definitions
1325 if len(vanilla_definitions) > 1
1326 else vanilla_definitions[0]
1327 )
1328 install_rule = AbstractMutableYAMLInstallRule.install_man(
1329 man_source,
1330 dctrl_bin.name if not is_single_binary else None,
1331 None,
1332 )
1333 installations.append(install_rule)
1334 for manpage_def in complex_definitions.values():
1335 sources = manpage_def.sources
1336 install_rule = AbstractMutableYAMLInstallRule.install_man(
1337 sources if len(sources) > 1 else sources[0],
1338 dctrl_bin.name if not is_single_binary else None,
1339 manpage_def.language,
1340 when=manpage_def.conditional,
1341 )
1342 installations.append(install_rule)
1344 if warn_about_basename:
1345 feature_migration.warn(
1346 'Detected man pages that might rely on "derive-from-basename" logic. Please double check'
1347 " that the generated `install-man` rules are correct"
1348 )
1351def migrate_not_installed_file(
1352 migration_request: MigrationRequest,
1353 feature_migration: FeatureMigration,
1354) -> None:
1355 feature_migration.tagline = "dh_missing's not-installed config file"
1356 manifest = migration_request.manifest
1357 mutable_manifest = assume_not_none(manifest.mutable_manifest)
1358 installations = mutable_manifest.installations(create_if_absent=False)
1359 main_binary = migration_request.main_binary
1361 missing_file, content = _dh_config_file(
1362 migration_request.debian_dir,
1363 main_binary,
1364 "not-installed",
1365 "dh_missing",
1366 migration_request.acceptable_migration_issues,
1367 feature_migration,
1368 manifest,
1369 support_executable_files=False,
1370 pkgfile_lookup=False,
1371 )
1372 discard_rules: list[str] = []
1373 if missing_file:
1374 assert content is not None
1375 for dhe_line in content:
1376 discard_rules.extend(
1377 _raise_on_unsupported_path(
1378 _normalize_path(
1379 w, with_prefix=False, allow_and_keep_upward_segments=True
1380 ),
1381 dhe_line.config_file,
1382 dhe_line.line_no,
1383 )
1384 for w in dhe_line.tokens
1385 )
1387 if discard_rules:
1388 feature_migration.successful_manifest_changes += 1
1389 install_rule = AbstractMutableYAMLInstallRule.discard(
1390 discard_rules if len(discard_rules) > 1 else discard_rules[0],
1391 )
1392 installations.create_definition_if_missing()
1393 installations.append(install_rule)
1396def min_dh_compat_check(
1397 migration_request: MigrationRequest,
1398 feature_migration: FeatureMigration,
1399) -> None:
1400 feature_migration.tagline = "min dh compat level check"
1401 # We start on compat 12 for arch:any due to the new dh_makeshlibs and dh_installinit default
1402 # For arch:any, the min is compat 14 due to `dh_dwz` being removed.
1403 min_compat = (
1404 14 if any(not p.is_arch_all for p in migration_request.all_packages) else 12
1405 )
1406 feature_migration.assumed_compat = min_compat
1409def detect_modprobe_files(
1410 migration_request: MigrationRequest,
1411 feature_migration: FeatureMigration,
1412) -> None:
1413 feature_migration.tagline = "detect dh_installmodules files (min dh compat)"
1414 for dctrl_bin in migration_request.all_packages:
1415 dh_config_file = dhe_pkgfile(
1416 migration_request.debian_dir, dctrl_bin, "modprobe"
1417 )
1418 if dh_config_file is not None:
1419 feature_migration.assumed_compat = 14
1420 break
1423def migrate_tmpfile(
1424 migration_request: MigrationRequest,
1425 feature_migration: FeatureMigration,
1426) -> None:
1427 feature_migration.tagline = "dh_installtmpfiles config files"
1428 feature_migration.assumed_compat = 14
1429 debian_dir = migration_request.debian_dir
1430 for dctrl_bin in migration_request.all_packages:
1431 dh_config_file = dhe_pkgfile(debian_dir, dctrl_bin, "tmpfile")
1432 if dh_config_file is not None:
1433 target = (
1434 dh_config_file.name.replace(".tmpfile", ".tmpfiles")
1435 if "." in dh_config_file.name
1436 else "tmpfiles"
1437 )
1438 _rename_file_if_exists(
1439 debian_dir,
1440 dh_config_file.name,
1441 target,
1442 feature_migration,
1443 )
1446def migrate_lintian_overrides_files(
1447 migration_request: MigrationRequest,
1448 feature_migration: FeatureMigration,
1449) -> None:
1450 feature_migration.tagline = "dh_lintian config files"
1451 for dctrl_bin in migration_request.all_packages:
1452 # We do not support executable lintian-overrides and `_dh_config_file` handles all of that.
1453 # Therefore, the return value is irrelevant to us.
1454 _dh_config_file(
1455 migration_request.debian_dir,
1456 dctrl_bin,
1457 "lintian-overrides",
1458 "dh_lintian",
1459 migration_request.acceptable_migration_issues,
1460 feature_migration,
1461 migration_request.manifest,
1462 support_executable_files=False,
1463 remove_on_migration=False,
1464 )
1467def migrate_links_files(
1468 migration_request: MigrationRequest,
1469 feature_migration: FeatureMigration,
1470) -> None:
1471 feature_migration.tagline = "dh_link files"
1472 manifest = migration_request.manifest
1473 mutable_manifest = assume_not_none(manifest.mutable_manifest)
1474 for dctrl_bin in migration_request.all_packages:
1475 links_file, content = _dh_config_file(
1476 migration_request.debian_dir,
1477 dctrl_bin,
1478 "links",
1479 "dh_link",
1480 migration_request.acceptable_migration_issues,
1481 feature_migration,
1482 manifest,
1483 support_executable_files=True,
1484 )
1486 if links_file is None:
1487 continue
1488 assert content is not None
1490 package_definition = mutable_manifest.package(dctrl_bin.name)
1491 defined_symlink = {
1492 symlink.symlink_path: symlink.symlink_target
1493 for symlink in package_definition.symlinks()
1494 }
1496 seen_symlinks: set[str] = set()
1498 for dhe_line in content:
1499 if len(dhe_line.tokens) != 2: 1499 ↛ 1500line 1499 didn't jump to line 1500 because the condition on line 1499 was never true
1500 raise UnsupportedFeature(
1501 f"The dh_link file {links_file.fs_path} did not have exactly two paths on line"
1502 f' {dhe_line.line_no} (line: "{dhe_line.original_line}"'
1503 )
1504 target, source = dhe_line.tokens
1505 source = _raise_on_unsupported_path(
1506 source,
1507 dhe_line.config_file,
1508 dhe_line.line_no,
1509 )
1510 target = _raise_on_unsupported_path(
1511 target,
1512 dhe_line.config_file,
1513 dhe_line.line_no,
1514 )
1515 if source in seen_symlinks: 1515 ↛ 1517line 1515 didn't jump to line 1517 because the condition on line 1515 was never true
1516 # According to #934499, this has happened in the wild already
1517 raise ConflictingChange(
1518 f"The {links_file.fs_path} file defines the link path {source} twice! Please ensure"
1519 " that it is defined at most once in that file"
1520 )
1521 seen_symlinks.add(source)
1522 # Symlinks in .links are always considered absolute, but you were not required to have a leading slash.
1523 # However, in the debputy manifest, you can have relative links, so we should ensure it is explicitly
1524 # absolute.
1525 if not target.startswith("/"): 1525 ↛ 1527line 1525 didn't jump to line 1527 because the condition on line 1525 was always true
1526 target = "/" + target
1527 existing_target = defined_symlink.get(source)
1528 if existing_target is not None: 1528 ↛ 1529line 1528 didn't jump to line 1529 because the condition on line 1528 was never true
1529 if existing_target != target:
1530 raise ConflictingChange(
1531 f'The symlink "{source}" points to "{target}" in {links_file}, but there is'
1532 f' another symlink with same path pointing to "{existing_target}" defined'
1533 " already (in the existing manifest or an migration e.g., inside"
1534 f" {links_file.fs_path})"
1535 )
1536 continue
1537 condition = dhe_line.conditional()
1538 package_definition.add_symlink(
1539 MutableYAMLSymlink.new_symlink(
1540 source,
1541 target,
1542 condition,
1543 )
1544 )
1545 feature_migration.successful_manifest_changes += 1
1548def migrate_misspelled_readme_debian_files(
1549 migration_request: MigrationRequest,
1550 feature_migration: FeatureMigration,
1551) -> None:
1552 feature_migration.tagline = "misspelled README.Debian files"
1553 debian_dir = migration_request.debian_dir
1554 for dctrl_bin in migration_request.all_packages:
1555 readme, _ = _dh_config_file(
1556 debian_dir,
1557 dctrl_bin,
1558 "README.debian",
1559 "dh_installdocs",
1560 migration_request.acceptable_migration_issues,
1561 feature_migration,
1562 migration_request.manifest,
1563 support_executable_files=False,
1564 remove_on_migration=False,
1565 )
1566 if readme is None:
1567 continue
1568 new_name = readme.name.replace("README.debian", "README.Debian")
1569 assert readme.name != new_name
1570 _rename_file_if_exists(
1571 debian_dir,
1572 readme.name,
1573 new_name,
1574 feature_migration,
1575 )
1578def migrate_doc_base_files(
1579 migration_request: MigrationRequest,
1580 feature_migration: FeatureMigration,
1581) -> None:
1582 feature_migration.tagline = "doc-base files"
1583 debian_dir = migration_request.debian_dir
1584 # ignore the dh_make ".EX" file if one should still be present. The dh_installdocs tool ignores it too.
1585 possible_effected_doc_base_files = [
1586 f
1587 for f in debian_dir.iterdir()
1588 if (
1589 (".doc-base." in f.name or f.name.startswith("doc-base."))
1590 and not f.name.endswith("doc-base.EX")
1591 )
1592 ]
1593 known_packages = {d.name: d for d in migration_request.all_packages}
1594 main_package = migration_request.main_binary
1595 for doc_base_file in possible_effected_doc_base_files:
1596 parts = doc_base_file.name.split(".")
1597 owning_package = known_packages.get(parts[0])
1598 if owning_package is None: 1598 ↛ 1599line 1598 didn't jump to line 1599 because the condition on line 1598 was never true
1599 owning_package = main_package
1600 package_part = None
1601 else:
1602 package_part = parts[0]
1603 parts = parts[1:]
1605 if not parts or parts[0] != "doc-base": 1605 ↛ 1607line 1605 didn't jump to line 1607 because the condition on line 1605 was never true
1606 # Not a doc-base file after all
1607 continue
1609 if len(parts) > 1: 1609 ↛ 1616line 1609 didn't jump to line 1616 because the condition on line 1609 was always true
1610 name_part = ".".join(parts[1:])
1611 if package_part is None: 1611 ↛ 1613line 1611 didn't jump to line 1613 because the condition on line 1611 was never true
1612 # Named files must have a package prefix
1613 package_part = owning_package.name
1614 else:
1615 # No rename needed
1616 continue
1618 new_basename = ".".join(filter(None, (package_part, name_part, "doc-base")))
1619 _rename_file_if_exists(
1620 debian_dir,
1621 doc_base_file.name,
1622 new_basename,
1623 feature_migration,
1624 )
1627def migrate_dh_hook_targets(
1628 migration_request: MigrationRequest,
1629 feature_migration: FeatureMigration,
1630) -> None:
1631 feature_migration.tagline = "dh hook targets"
1632 source_root = os.path.dirname(migration_request.debian_dir.fs_path)
1633 if source_root == "":
1634 source_root = "."
1635 detected_hook_targets = json.loads(
1636 subprocess.check_output(
1637 ["dh_assistant", "detect-hook-targets"],
1638 cwd=source_root,
1639 ).decode("utf-8")
1640 )
1641 sample_hook_target: str | None = None
1642 debputy_integration_mode = migration_request.migration_target
1643 assert debputy_integration_mode is not None
1644 replaced_commands = DH_COMMANDS_REPLACED[debputy_integration_mode]
1646 for hook_target_def in detected_hook_targets["hook-targets"]:
1647 if hook_target_def["is-empty"]:
1648 continue
1649 command = hook_target_def["command"]
1650 if command not in replaced_commands:
1651 continue
1652 hook_target = hook_target_def["target-name"]
1653 advice = MIGRATION_AID_FOR_OVERRIDDEN_COMMANDS.get(command)
1654 if advice is None:
1655 if sample_hook_target is None:
1656 sample_hook_target = hook_target
1657 feature_migration.warn(
1658 f"TODO: MANUAL MIGRATION required for hook target {hook_target}"
1659 )
1660 else:
1661 feature_migration.warn(
1662 f"TODO: MANUAL MIGRATION required for hook target {hook_target}. Please see {advice}"
1663 f" for migration advice."
1664 )
1665 if (
1666 feature_migration.warnings
1667 and "dh-hook-targets" not in migration_request.acceptable_migration_issues
1668 and sample_hook_target is not None
1669 ):
1670 raise UnsupportedFeature(
1671 f"The debian/rules file contains one or more non empty dh hook targets that will not"
1672 f" be run with the requested debputy dh sequence with no known migration advice. One of these would be"
1673 f" {sample_hook_target}.",
1674 ["dh-hook-targets"],
1675 )
1678def detect_maintscript_needing_conversion(
1679 migration_request: MigrationRequest,
1680 feature_migration: FeatureMigration,
1681) -> None:
1682 feature_migration.tagline = "Known unsupported features"
1683 for dctrl_bin in migration_request.all_packages:
1684 relevant_scripts: list[str] = sorted(
1685 SUPPORTED_UDEB_SCRIPTS if dctrl_bin.is_udeb else DPKG_DEB_CONTROL_SCRIPTS
1686 )
1687 for script in relevant_scripts:
1688 dh_config_file = dhe_pkgfile(
1689 migration_request.debian_dir,
1690 dctrl_bin,
1691 script,
1692 )
1693 if dh_config_file:
1694 feature_migration.warn(
1695 f"TODO: MANUAL MIGRATION {dh_config_file.fs_path} needs porting"
1696 f" (keywords include `maintscript-snippets` & `clean-after-removal` depending on use-case)"
1697 )
1700def detect_unsupported_zz_debputy_features(
1701 migration_request: MigrationRequest,
1702 feature_migration: FeatureMigration,
1703) -> None:
1704 feature_migration.tagline = "Known unsupported features"
1706 for unsupported_config in UNSUPPORTED_DH_CONFIGS_AND_TOOLS_FOR_ZZ_DEBPUTY:
1707 _unsupported_debhelper_config_file(
1708 migration_request,
1709 unsupported_config,
1710 feature_migration,
1711 )
1714def detect_obsolete_substvars(
1715 migration_request: MigrationRequest,
1716 feature_migration: FeatureMigration,
1717) -> None:
1718 feature_migration.tagline = (
1719 "Check for obsolete ${foo:var} variables in debian/control"
1720 )
1721 ctrl_file = migration_request.debian_dir.get("control")
1722 if not ctrl_file: 1722 ↛ 1723line 1722 didn't jump to line 1723 because the condition on line 1722 was never true
1723 feature_migration.warn(
1724 "Cannot find debian/control. Detection of obsolete substvars could not be performed."
1725 )
1726 return
1727 with ctrl_file.open() as fd:
1728 ctrl = list(Deb822.iter_paragraphs(fd))
1730 relationship_fields = dpkg_field_list_pkg_dep()
1731 relationship_fields_lc = frozenset(x.lower() for x in relationship_fields)
1733 for p in ctrl[1:]:
1734 seen_obsolete_relationship_substvars = set()
1735 obsolete_fields = set()
1736 is_essential = p.get("Essential") == "yes"
1737 for df in relationship_fields:
1738 field: str | None = p.get(df)
1739 if field is None:
1740 continue
1741 df_lc = df.lower()
1742 number_of_relations = 0
1743 obsolete_substvars_in_field = set()
1744 for d in (d.strip() for d in field.strip().split(",")):
1745 if not d:
1746 continue
1747 number_of_relations += 1
1748 if not d.startswith("${"):
1749 continue
1750 try:
1751 end_idx = d.index("}")
1752 except ValueError:
1753 continue
1754 substvar_name = d[2:end_idx]
1755 if ":" not in substvar_name: 1755 ↛ 1756line 1755 didn't jump to line 1756 because the condition on line 1755 was never true
1756 continue
1757 _, field = substvar_name.rsplit(":", 1)
1758 field_lc = field.lower()
1759 if field_lc not in relationship_fields_lc: 1759 ↛ 1760line 1759 didn't jump to line 1760 because the condition on line 1759 was never true
1760 continue
1761 is_obsolete = field_lc == df_lc
1762 if (
1763 not is_obsolete
1764 and is_essential
1765 and substvar_name.lower() == "shlibs:depends"
1766 and df_lc == "pre-depends"
1767 ):
1768 is_obsolete = True
1770 if is_obsolete:
1771 obsolete_substvars_in_field.add(d)
1773 if number_of_relations == len(obsolete_substvars_in_field):
1774 obsolete_fields.add(df)
1775 else:
1776 seen_obsolete_relationship_substvars.update(obsolete_substvars_in_field)
1778 package = p.get("Package", "(Missing package name!?)")
1779 fo = feature_migration.fo
1780 if obsolete_fields:
1781 fields = ", ".join(obsolete_fields)
1782 feature_migration.warn(
1783 f"The following relationship fields can be removed from {package}: {fields}."
1784 f" (The content in them would be applied automatically. Note: {fo.bts('1067653')})"
1785 )
1786 if seen_obsolete_relationship_substvars:
1787 v = ", ".join(sorted(seen_obsolete_relationship_substvars))
1788 feature_migration.warn(
1789 f"The following relationship substitution variables can be removed from {package}: {v}"
1790 f" (Note: {fo.bts('1067653')})"
1791 )
1794def detect_dh_addons_zz_debputy_rrr(
1795 migration_request: MigrationRequest,
1796 feature_migration: FeatureMigration,
1797) -> None:
1798 feature_migration.tagline = "Check for dh-sequence-addons"
1799 r = read_dh_addon_sequences(migration_request.debian_dir)
1800 if r is None:
1801 feature_migration.warn(
1802 "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon"
1803 " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy-rrr."
1804 )
1805 return
1807 bd_sequences, dr_sequences, _ = r
1809 remaining_sequences = bd_sequences | dr_sequences
1810 saw_dh_debputy = "zz-debputy-rrr" in remaining_sequences
1812 if not saw_dh_debputy:
1813 feature_migration.warn("Missing Build-Depends on dh-sequence-zz-debputy-rrr")
1816def detect_dh_addons_with_full_integration(
1817 _migration_request: MigrationRequest,
1818 feature_migration: FeatureMigration,
1819) -> None:
1820 feature_migration.tagline = "Check for dh-sequence-addons and Build-Depends"
1821 feature_migration.warn(
1822 "TODO: Not implemented: Please remove any dh-sequence Build-Dependency"
1823 )
1824 feature_migration.warn(
1825 "TODO: Not implemented: Please ensure there is a Build-Dependency on `debputy (>= 0.1.45~)"
1826 )
1827 feature_migration.warn(
1828 "TODO: Not implemented: Please ensure there is a Build-Dependency on `dpkg-dev (>= 1.22.7~)"
1829 )
1832def detect_dh_addons_with_zz_integration(
1833 migration_request: MigrationRequest,
1834 feature_migration: FeatureMigration,
1835) -> None:
1836 feature_migration.tagline = "Check for dh-sequence-addons"
1837 r = read_dh_addon_sequences(migration_request.debian_dir)
1838 acceptable_migration_issues = migration_request.acceptable_migration_issues
1839 if r is None:
1840 feature_migration.warn(
1841 "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon"
1842 " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy"
1843 " and not rely on any other debhelper sequence addons except those debputy explicitly supports."
1844 )
1845 return
1847 assert migration_request.migration_target != INTEGRATION_MODE_FULL
1849 bd_sequences, dr_sequences, _ = r
1851 remaining_sequences = bd_sequences | dr_sequences
1852 saw_dh_debputy = (
1853 "debputy" in remaining_sequences or "zz-debputy" in remaining_sequences
1854 )
1855 saw_zz_debputy = "zz-debputy" in remaining_sequences
1856 must_use_zz_debputy = False
1857 remaining_sequences -= SUPPORTED_DH_ADDONS_WITH_ZZ_DEBPUTY
1858 for sequence in remaining_sequences & DH_ADDONS_TO_PLUGINS.keys():
1859 migration = DH_ADDONS_TO_PLUGINS[sequence]
1860 feature_migration.require_plugin(migration.debputy_plugin)
1861 if migration.remove_dh_sequence: 1861 ↛ 1862line 1861 didn't jump to line 1862 because the condition on line 1861 was never true
1862 if migration.must_use_zz_debputy:
1863 must_use_zz_debputy = True
1864 if sequence in bd_sequences:
1865 feature_migration.warn(
1866 f"TODO: MANUAL MIGRATION - Remove build-dependency on dh-sequence-{sequence}"
1867 f" (replaced by debputy-plugin-{migration.debputy_plugin})"
1868 )
1869 else:
1870 feature_migration.warn(
1871 f"TODO: MANUAL MIGRATION - Remove --with {sequence} from dh in d/rules"
1872 f" (replaced by debputy-plugin-{migration.debputy_plugin})"
1873 )
1875 remaining_sequences -= DH_ADDONS_TO_PLUGINS.keys()
1877 alt_key = "unsupported-dh-sequences"
1878 for sequence in remaining_sequences & DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY: 1878 ↛ 1879line 1878 didn't jump to line 1879 because the loop on line 1878 never started
1879 if sequence in bd_sequences:
1880 feature_migration.warn(
1881 f"TODO: MANUAL MIGRATION - Remove build dependency on dh-sequence-{sequence}"
1882 )
1883 else:
1884 feature_migration.warn(
1885 f"TODO: MANUAL MIGRATION - Remove --with {sequence} from dh in d/rules"
1886 )
1888 remaining_sequences -= DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY
1890 for sequence in remaining_sequences:
1891 key = f"unsupported-dh-sequence-{sequence}"
1892 msg = f'The dh addon "{sequence}" is not known to work with dh-debputy and might malfunction'
1893 if (
1894 key not in acceptable_migration_issues
1895 and alt_key not in acceptable_migration_issues
1896 ):
1897 raise UnsupportedFeature(msg, [key, alt_key])
1898 feature_migration.warn(msg)
1900 if not saw_dh_debputy:
1901 feature_migration.warn("Missing Build-Depends on dh-sequence-zz-debputy")
1902 elif must_use_zz_debputy and not saw_zz_debputy: 1902 ↛ 1903line 1902 didn't jump to line 1903 because the condition on line 1902 was never true
1903 feature_migration.warn(
1904 "Please use the zz-debputy sequence rather than the debputy (needed due to dh add-on load order)"
1905 )
1908def _rename_file_if_exists(
1909 debian_dir: VirtualPath,
1910 source: str,
1911 dest: str,
1912 feature_migration: FeatureMigration,
1913) -> None:
1914 source_path = debian_dir.get(source)
1915 dest_path = debian_dir.get(dest)
1916 spath = (
1917 source_path.path
1918 if source_path is not None
1919 else os.path.join(debian_dir.path, source)
1920 )
1921 dpath = (
1922 dest_path.path if dest_path is not None else os.path.join(debian_dir.path, dest)
1923 )
1924 if source_path is not None and source_path.is_file:
1925 if dest_path is not None:
1926 if not dest_path.is_file:
1927 feature_migration.warnings.append(
1928 f'TODO: MANUAL MIGRATION - there is a "{spath}" (file) and "{dpath}" (not a file).'
1929 f' The migration wanted to replace "{spath}" with "{dpath}", but since "{dpath}" is not'
1930 " a file, this step is left as a manual migration."
1931 )
1932 return
1933 if (
1934 subprocess.call(["cmp", "-s", source_path.fs_path, dest_path.fs_path])
1935 != 0
1936 ):
1937 feature_migration.warnings.append(
1938 f'TODO: MANUAL MIGRATION - there is a "{source_path.path}" and "{dest_path.path}"'
1939 f" file. Normally these files are for the same package and there would only be one of"
1940 f" them. In this case, they both exist but their content differs. Be advised that"
1941 f' debputy tool will use the "{dest_path.path}".'
1942 )
1943 else:
1944 feature_migration.remove_on_success(source_path.fs_path)
1945 else:
1946 feature_migration.rename_on_success(
1947 source_path.fs_path,
1948 os.path.join(debian_dir.fs_path, dest),
1949 )
1950 elif source_path is not None: 1950 ↛ exitline 1950 didn't return from function '_rename_file_if_exists' because the condition on line 1950 was always true
1951 feature_migration.warnings.append(
1952 f'TODO: MANUAL MIGRATION - The migration would normally have renamed "{spath}" to "{dpath}".'
1953 f' However, the migration assumed "{spath}" would be a file and it is not. Therefore, this step'
1954 " as a manual migration."
1955 )
1958def _find_dh_config_file_for_any_pkg(
1959 migration_request: MigrationRequest,
1960 unsupported_config: UnsupportedDHConfig,
1961) -> Iterable[VirtualPath]:
1962 for dctrl_bin in migration_request.all_packages:
1963 dh_config_file = dhe_pkgfile(
1964 migration_request.debian_dir,
1965 dctrl_bin,
1966 unsupported_config.dh_config_basename,
1967 bug_950723_prefix_matching=unsupported_config.bug_950723_prefix_matching,
1968 )
1969 if dh_config_file is not None:
1970 yield dh_config_file
1973def _unsupported_debhelper_config_file(
1974 migration_request: MigrationRequest,
1975 unsupported_config: UnsupportedDHConfig,
1976 feature_migration: FeatureMigration,
1977) -> None:
1978 dh_config_files = list(
1979 _find_dh_config_file_for_any_pkg(migration_request, unsupported_config)
1980 )
1981 if not dh_config_files:
1982 return
1983 dh_tool = unsupported_config.dh_tool
1984 basename = unsupported_config.dh_config_basename
1985 file_stem = (
1986 f"@{basename}" if unsupported_config.bug_950723_prefix_matching else basename
1987 )
1988 dh_config_file = dh_config_files[0]
1989 if unsupported_config.is_missing_migration:
1990 feature_migration.warn(
1991 f'Missing migration support for the "{dh_config_file.path}" debhelper config file'
1992 f" (used by {dh_tool}). Manual migration may be feasible depending on the exact features"
1993 " required."
1994 )
1995 return
1996 primary_key = f"unsupported-dh-config-file-{file_stem}"
1997 secondary_key = "any-unsupported-dh-config-file"
1998 if (
1999 primary_key not in migration_request.acceptable_migration_issues
2000 and secondary_key not in migration_request.acceptable_migration_issues
2001 ):
2002 msg = (
2003 f'The "{dh_config_file.path}" debhelper config file (used by {dh_tool} is currently not'
2004 " supported by debputy."
2005 )
2006 raise UnsupportedFeature(
2007 msg,
2008 [primary_key, secondary_key],
2009 )
2010 for dh_config_file in dh_config_files:
2011 feature_migration.warn(
2012 f'TODO: MANUAL MIGRATION - Use of unsupported "{dh_config_file.path}" file (used by {dh_tool})'
2013 )