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