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