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

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) 

14 

15from debian.deb822 import Deb822 

16 

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 

59 

60 

61class ContainsEverything: 

62 

63 def __contains__(self, item: str) -> bool: 

64 return True 

65 

66 

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} 

133 

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} 

141 

142 

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 

149 

150 

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 

156 

157 

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} 

261 

262 

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 ) 

298 

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 

313 

314 

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 ) 

337 

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 

368 

369 

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) 

379 

380 

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) 

391 

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 

418 

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)) 

446 

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 ) 

460 

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 ) 

469 

470 

471_SHELL_COMPLETIONS_RE = re.compile(r"^\s*\S+\s+\S+\s+\S") 

472 

473 

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"] 

488 

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 

506 

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 ) 

517 

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)) 

543 

544 completion_dir_variable = ( 

545 "{{path:" + f"{completion.upper()}_COMPLETION_DIR" + "}}" 

546 ) 

547 

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 ) 

561 

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 ) 

570 

571 

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", "") 

580 

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 ) 

592 

593 

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 ) 

623 

624 

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 

633 

634 mutable_manifest = assume_not_none(migration_request.manifest.mutable_manifest) 

635 

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 ) 

646 

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) 

656 

657 

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 ) 

675 

676 if mainscript_file is None: 

677 continue 

678 assert content is not None 

679 

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() 

686 

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 ) 

695 

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 ) 

708 

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) 

715 

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 ) 

730 

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 

746 

747 package_definition.add_conffile_management(item) 

748 feature_migration.successful_manifest_changes += 1 

749 

750 

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 

756 

757 

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 

768 

769 

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}" 

781 

782 return p 

783 

784 

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 

796 

797 is_single_binary = migration_request.is_single_binary_package 

798 

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() 

821 

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 

908 

909 multi_dest_sources.update(s for s in sources if s in seen_sources) 

910 seen_sources.update(sources) 

911 

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) 

927 

928 if special_rule: 

929 priority_lines.append(special_rule) 

930 

931 remaining_install_lines.extend(install_as_rules) 

932 

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) 

951 

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 

972 

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) 

980 

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 ) 

1001 

1002 if priority_lines: 

1003 installations.extend(priority_lines) 

1004 

1005 if remaining_install_lines: 

1006 installations.extend(remaining_install_lines) 

1007 

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 ) 

1016 

1017 

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) 

1026 

1027 is_single_binary = migration_request.is_single_binary_package 

1028 

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 ) 

1060 

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) 

1070 

1071 

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 

1081 

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 ) 

1113 

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) 

1123 

1124 

1125@dataclasses.dataclass(slots=True) 

1126class InfoFilesDefinition: 

1127 sources: list[str] = dataclasses.field(default_factory=list) 

1128 conditional: str | Mapping[str, Any] | None = None 

1129 

1130 

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 

1140 

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 ) 

1176 

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) 

1190 

1191 

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 

1197 

1198 

1199DK = TypeVar("DK") 

1200DV = TypeVar("DV") 

1201 

1202 

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 

1209 

1210 

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 

1221 

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 

1237 

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 

1283 

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) 

1319 

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) 

1346 

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 ) 

1352 

1353 

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 

1363 

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 ) 

1389 

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) 

1397 

1398 

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 

1410 

1411 

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 

1424 

1425 

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 ) 

1447 

1448 

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 ) 

1468 

1469 

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 ) 

1488 

1489 if links_file is None: 

1490 continue 

1491 assert content is not None 

1492 

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 } 

1498 

1499 seen_symlinks: set[str] = set() 

1500 

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 

1549 

1550 

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 ) 

1579 

1580 

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:] 

1607 

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 

1611 

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 

1620 

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 ) 

1628 

1629 

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] 

1648 

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 ) 

1679 

1680 

1681def detect_unsupported_zz_debputy_features( 

1682 migration_request: MigrationRequest, 

1683 feature_migration: FeatureMigration, 

1684) -> None: 

1685 feature_migration.tagline = "Known unsupported features" 

1686 

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 ) 

1693 

1694 

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)) 

1710 

1711 relationship_fields = dpkg_field_list_pkg_dep() 

1712 relationship_fields_lc = frozenset(x.lower() for x in relationship_fields) 

1713 

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 

1750 

1751 if is_obsolete: 

1752 obsolete_substvars_in_field.add(d) 

1753 

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) 

1758 

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 ) 

1773 

1774 

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 

1787 

1788 bd_sequences, dr_sequences, _ = r 

1789 

1790 remaining_sequences = bd_sequences | dr_sequences 

1791 saw_dh_debputy = "zz-debputy-rrr" in remaining_sequences 

1792 

1793 if not saw_dh_debputy: 

1794 feature_migration.warn("Missing Build-Depends on dh-sequence-zz-debputy-rrr") 

1795 

1796 

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 ) 

1811 

1812 

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 

1827 

1828 assert migration_request.migration_target != INTEGRATION_MODE_FULL 

1829 

1830 bd_sequences, dr_sequences, _ = r 

1831 

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 ) 

1855 

1856 remaining_sequences -= DH_ADDONS_TO_PLUGINS.keys() 

1857 

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 ) 

1868 

1869 remaining_sequences -= DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY 

1870 

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) 

1880 

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 ) 

1887 

1888 

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 ) 

1937 

1938 

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 

1952 

1953 

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 )