Coverage for src/debputy/dh_migration/migrators_impl.py: 81%

761 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-19 09:24 +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 import DEBPUTY_DOC_ROOT_DIR 

18from debputy.architecture_support import dpkg_architecture_table 

19from debputy.deb_packaging_support import dpkg_field_list_pkg_dep 

20from debputy.dh.debhelper_emulation import ( 

21 dhe_filedoublearray, 

22 DHConfigFileLine, 

23 dhe_pkgfile, 

24) 

25from debputy.dh.dh_assistant import ( 

26 read_dh_addon_sequences, 

27) 

28from debputy.dh_migration.models import ( 

29 ConflictingChange, 

30 FeatureMigration, 

31 UnsupportedFeature, 

32 AcceptableMigrationIssues, 

33 DHMigrationSubstitution, 

34 MigrationRequest, 

35) 

36from debputy.highlevel_manifest import ( 

37 MutableYAMLSymlink, 

38 HighLevelManifest, 

39 MutableYAMLConffileManagementItem, 

40 AbstractMutableYAMLInstallRule, 

41) 

42from debputy.installations import MAN_GUESS_FROM_BASENAME, MAN_GUESS_LANG_FROM_PATH 

43from debputy.packages import BinaryPackage 

44from debputy.plugin.api import VirtualPath 

45from debputy.plugin.api.spec import ( 

46 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

47 INTEGRATION_MODE_DH_DEBPUTY, 

48 DebputyIntegrationMode, 

49 INTEGRATION_MODE_FULL, 

50) 

51from debputy.util import ( 

52 _error, 

53 PKGVERSION_REGEX, 

54 PKGNAME_REGEX, 

55 _normalize_path, 

56 assume_not_none, 

57 has_glob_magic, 

58) 

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

126 "dh_md5sums", 

127 "dh_builddeb", 

128 } 

129 ), 

130 INTEGRATION_MODE_FULL: ContainsEverything(), 

131} 

132 

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} 

140 

141 

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 

148 

149 

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 

155 

156 

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 ] 

233) 

234DH_ADDONS_TO_PLUGINS = { 

235 "gnome": DHSequenceMigration( 

236 "gnome", 

237 # The sequence still provides a command for the clean sequence 

238 remove_dh_sequence=False, 

239 must_use_zz_debputy=True, 

240 ), 

241 "grantlee": DHSequenceMigration( 

242 "grantlee", 

243 remove_dh_sequence=True, 

244 must_use_zz_debputy=True, 

245 ), 

246 "numpy3": DHSequenceMigration( 

247 "numpy3", 

248 # The sequence provides (build-time) dependencies that we cannot provide 

249 remove_dh_sequence=False, 

250 must_use_zz_debputy=True, 

251 ), 

252 "perl-openssl": DHSequenceMigration( 

253 "perl-openssl", 

254 # The sequence provides (build-time) dependencies that we cannot provide 

255 remove_dh_sequence=False, 

256 must_use_zz_debputy=True, 

257 ), 

258} 

259 

260 

261def _dh_config_file( 

262 debian_dir: VirtualPath, 

263 dctrl_bin: BinaryPackage, 

264 basename: str, 

265 helper_name: str, 

266 acceptable_migration_issues: AcceptableMigrationIssues, 

267 feature_migration: FeatureMigration, 

268 manifest: HighLevelManifest, 

269 support_executable_files: bool = False, 

270 allow_dh_exec_rename: bool = False, 

271 pkgfile_lookup: bool = True, 

272 remove_on_migration: bool = True, 

273) -> tuple[None, None] | tuple[VirtualPath, Iterable[DHConfigFileLine]]: 

274 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

275 dh_config_file = ( 

276 dhe_pkgfile(debian_dir, dctrl_bin, basename) 

277 if pkgfile_lookup 

278 else debian_dir.get(basename) 

279 ) 

280 if dh_config_file is None or dh_config_file.is_dir: 

281 return None, None 

282 if dh_config_file.is_executable and not support_executable_files: 

283 primary_key = f"executable-{helper_name}-config" 

284 if ( 

285 primary_key in acceptable_migration_issues 

286 or "any-executable-dh-configs" in acceptable_migration_issues 

287 ): 

288 feature_migration.warn( 

289 f'TODO: MANUAL MIGRATION of executable dh config "{dh_config_file}" is required.' 

290 ) 

291 return None, None 

292 raise UnsupportedFeature( 

293 f"Executable configuration files not supported (found: {dh_config_file}).", 

294 [primary_key, "any-executable-dh-configs"], 

295 ) 

296 

297 if remove_on_migration: 

298 feature_migration.remove_on_success(dh_config_file.fs_path) 

299 substitution = DHMigrationSubstitution( 

300 dpkg_architecture_table(), 

301 acceptable_migration_issues, 

302 feature_migration, 

303 mutable_manifest, 

304 ) 

305 content = dhe_filedoublearray( 

306 dh_config_file, 

307 substitution, 

308 allow_dh_exec_rename=allow_dh_exec_rename, 

309 ) 

310 return dh_config_file, content 

311 

312 

313def _validate_rm_mv_conffile( 

314 package: str, 

315 config_line: DHConfigFileLine, 

316) -> tuple[str, str, str | None, str | None, str | None]: 

317 cmd, *args = config_line.tokens 

318 if "--" in config_line.tokens: 318 ↛ 319line 318 didn't jump to line 319 because the condition on line 318 was never true

319 raise ValueError( 

320 f'The maintscripts file "{config_line.config_file.path}" for {package} includes a "--" in line' 

321 f" {config_line.line_no}. The offending line is: {config_line.original_line}" 

322 ) 

323 if cmd == "rm_conffile": 

324 min_args = 1 

325 max_args = 3 

326 else: 

327 min_args = 2 

328 max_args = 4 

329 if len(args) > max_args or len(args) < min_args: 329 ↛ 330line 329 didn't jump to line 330 because the condition on line 329 was never true

330 raise ValueError( 

331 f'The "{cmd}" command takes at least {min_args} and at most {max_args} arguments. However,' 

332 f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), there' 

333 f" are {len(args)} arguments. The offending line is: {config_line.original_line}" 

334 ) 

335 

336 obsolete_conffile = args[0] 

337 new_conffile = args[1] if cmd == "mv_conffile" else None 

338 prior_version = args[min_args] if len(args) > min_args else None 

339 owning_package = args[min_args + 1] if len(args) > min_args + 1 else None 

340 if not obsolete_conffile.startswith("/"): 340 ↛ 341line 340 didn't jump to line 341 because the condition on line 340 was never true

341 raise ValueError( 

342 f'The (old-)conffile parameter for {cmd} must be absolute (i.e., start with "/"). However,' 

343 f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it was specified' 

344 f' as "{obsolete_conffile}". The offending line is: {config_line.original_line}' 

345 ) 

346 if new_conffile is not None and not new_conffile.startswith("/"): 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true

347 raise ValueError( 

348 f'The new-conffile parameter for {cmd} must be absolute (i.e., start with "/"). However,' 

349 f' in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it was specified' 

350 f' as "{new_conffile}". The offending line is: {config_line.original_line}' 

351 ) 

352 if prior_version is not None and not PKGVERSION_REGEX.fullmatch(prior_version): 352 ↛ 353line 352 didn't jump to line 353 because the condition on line 352 was never true

353 raise ValueError( 

354 f"The prior-version parameter for {cmd} must be a valid package version (i.e., match" 

355 f' {PKGVERSION_REGEX}). However, in "{config_line.config_file.path}" line {config_line.line_no}' 

356 f' (for {package}), it was specified as "{prior_version}". The offending line is:' 

357 f" {config_line.original_line}" 

358 ) 

359 if owning_package is not None and not PKGNAME_REGEX.fullmatch(owning_package): 359 ↛ 360line 359 didn't jump to line 360 because the condition on line 359 was never true

360 raise ValueError( 

361 f"The package parameter for {cmd} must be a valid package name (i.e., match {PKGNAME_REGEX})." 

362 f' However, in "{config_line.config_file.path}" line {config_line.line_no} (for {package}), it' 

363 f' was specified as "{owning_package}". The offending line is: {config_line.original_line}' 

364 ) 

365 return cmd, obsolete_conffile, new_conffile, prior_version, owning_package 

366 

367 

368_BASH_COMPLETION_RE = re.compile( 

369 r""" 

370 (^|[|&;])\s*complete.*-[A-Za-z].* 

371 | \$\(.*\) 

372 | \s*compgen.*-[A-Za-z].* 

373 | \s*if.*;.*then/ 

374""", 

375 re.VERBOSE, 

376) 

377 

378 

379def migrate_bash_completion( 

380 migration_request: MigrationRequest, 

381 feature_migration: FeatureMigration, 

382) -> None: 

383 feature_migration.tagline = "dh_bash-completion files" 

384 is_single_binary = migration_request.is_single_binary_package 

385 manifest = migration_request.manifest 

386 debian_dir = migration_request.debian_dir 

387 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

388 installations = mutable_manifest.installations(create_if_absent=False) 

389 

390 for dctrl_bin in migration_request.all_packages: 

391 dh_file = dhe_pkgfile(debian_dir, dctrl_bin, "bash-completion") 

392 if dh_file is None: 

393 continue 

394 is_bash_completion_file = False 

395 with dh_file.open() as fd: 

396 for line in fd: 

397 line = line.strip() 

398 if not line or line[0] == "#": 398 ↛ 399line 398 didn't jump to line 399 because the condition on line 398 was never true

399 continue 

400 if _BASH_COMPLETION_RE.search(line): 

401 is_bash_completion_file = True 

402 break 

403 if not is_bash_completion_file: 

404 _, content = _dh_config_file( 

405 debian_dir, 

406 dctrl_bin, 

407 "bash-completion", 

408 "dh_bash-completion", 

409 migration_request.acceptable_migration_issues, 

410 feature_migration, 

411 manifest, 

412 support_executable_files=True, 

413 ) 

414 else: 

415 content = None 

416 

417 if content: 

418 install_dest_sources: list[str] = [] 

419 install_as_rules: list[tuple[str, str]] = [] 

420 for dhe_line in content: 

421 if len(dhe_line.tokens) > 2: 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true

422 raise UnsupportedFeature( 

423 f"The dh_bash-completion file {dh_file.path} more than two words on" 

424 f' line {dhe_line.line_no} (line: "{dhe_line.original_line}").' 

425 ) 

426 source = dhe_line.tokens[0] 

427 dest_basename = ( 

428 dhe_line.tokens[1] 

429 if len(dhe_line.tokens) > 1 

430 else os.path.basename(source) 

431 ) 

432 if source.startswith("debian/") and not has_glob_magic(source): 

433 if dctrl_bin.name != dest_basename: 

434 dest_path = ( 

435 f"debian/{dctrl_bin.name}.{dest_basename}.bash-completion" 

436 ) 

437 else: 

438 dest_path = f"debian/{dest_basename}.bash-completion" 

439 feature_migration.rename_on_success(source, dest_path) 

440 elif len(dhe_line.tokens) == 1: 

441 install_dest_sources.append(source) 

442 else: 

443 install_as_rules.append((source, dest_basename)) 

444 

445 if install_dest_sources: 445 ↛ 459line 445 didn't jump to line 459 because the condition on line 445 was always true

446 sources: list[str] | str = ( 

447 install_dest_sources 

448 if len(install_dest_sources) > 1 

449 else install_dest_sources[0] 

450 ) 

451 installations.append( 

452 AbstractMutableYAMLInstallRule.install_dest( 

453 sources=sources, 

454 dest_dir="{{path:BASH_COMPLETION_DIR}}", 

455 into=dctrl_bin.name if not is_single_binary else None, 

456 ) 

457 ) 

458 

459 for source, dest_basename in install_as_rules: 

460 installations.append( 

461 AbstractMutableYAMLInstallRule.install_as( 

462 source=source, 

463 install_as="{{path:BASH_COMPLETION_DIR}}/" + dest_basename, 

464 into=dctrl_bin.name if not is_single_binary else None, 

465 ) 

466 ) 

467 

468 

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

470 

471 

472def migrate_shell_completions( 

473 migration_request: MigrationRequest, 

474 feature_migration: FeatureMigration, 

475) -> None: 

476 feature_migration.tagline = "dh_shell_completions files" 

477 manifest = migration_request.manifest 

478 debian_dir = migration_request.debian_dir 

479 is_single_binary = migration_request.is_single_binary_package 

480 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

481 installations = mutable_manifest.installations(create_if_absent=False) 

482 # Note: The bash completion script used `bash-completion` whereas `dh_shell_completions` uses 

483 # `...-completions` (note the trailing `s`). In `debputy`, we always use the singular notation 

484 # because we use "one file, one completion (ruleset)". 

485 completions = ["bash", "fish", "zsh"] 

486 

487 for completion, dctrl_bin in product(completions, migration_request.all_packages): 

488 dh_file = dhe_pkgfile(debian_dir, dctrl_bin, f"{completion}-completions") 

489 if dh_file is None: 

490 continue 

491 is_completion_file = False 

492 with dh_file.open() as fd: 

493 for line in fd: 

494 line = line.strip() 

495 if not line or line[0] == "#": 

496 continue 

497 if _SHELL_COMPLETIONS_RE.search(line): 

498 is_completion_file = True 

499 break 

500 if is_completion_file: 

501 dest_path = f"debian/{dctrl_bin.name}.{completion}-completion" 

502 feature_migration.rename_on_success(dh_file.fs_path, dest_path) 

503 continue 

504 

505 _, content = _dh_config_file( 

506 debian_dir, 

507 dctrl_bin, 

508 f"{completion}-completions", 

509 "dh_shell_completions", 

510 migration_request.acceptable_migration_issues, 

511 feature_migration, 

512 manifest, 

513 remove_on_migration=True, 

514 ) 

515 

516 if content: 516 ↛ 487line 516 didn't jump to line 487 because the condition on line 516 was always true

517 install_dest_sources: list[str] = [] 

518 install_as_rules: list[tuple[str, str]] = [] 

519 for dhe_line in content: 

520 if len(dhe_line.tokens) > 2: 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true

521 raise UnsupportedFeature( 

522 f"The dh_shell_completions file {dh_file.path} more than two words on" 

523 f' line {dhe_line.line_no} (line: "{dhe_line.original_line}").' 

524 ) 

525 source = dhe_line.tokens[0] 

526 dest_basename = ( 

527 dhe_line.tokens[1] 

528 if len(dhe_line.tokens) > 1 

529 else os.path.basename(source) 

530 ) 

531 if source.startswith("debian/") and not has_glob_magic(source): 

532 if dctrl_bin.name != dest_basename: 

533 dest_path = f"debian/{dctrl_bin.name}.{dest_basename}.{completion}-completion" 

534 else: 

535 dest_path = f"debian/{dest_basename}.{completion}-completion" 

536 feature_migration.rename_on_success(source, dest_path) 

537 elif len(dhe_line.tokens) == 1: 

538 install_dest_sources.append(source) 

539 else: 

540 install_as_rules.append((source, dest_basename)) 

541 

542 completion_dir_variable = ( 

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

544 ) 

545 

546 if install_dest_sources: 546 ↛ 560line 546 didn't jump to line 560 because the condition on line 546 was always true

547 sources: list[str] | str = ( 

548 install_dest_sources 

549 if len(install_dest_sources) > 1 

550 else install_dest_sources[0] 

551 ) 

552 installations.append( 

553 AbstractMutableYAMLInstallRule.install_dest( 

554 sources=sources, 

555 dest_dir=completion_dir_variable, 

556 into=dctrl_bin.name if not is_single_binary else None, 

557 ) 

558 ) 

559 

560 for source, dest_basename in install_as_rules: 

561 installations.append( 

562 AbstractMutableYAMLInstallRule.install_as( 

563 source=source, 

564 install_as=f"{completion_dir_variable}/{dest_basename}", 

565 into=dctrl_bin.name if not is_single_binary else None, 

566 ) 

567 ) 

568 

569 

570def migrate_dh_installsystemd_files( 

571 migration_request: MigrationRequest, 

572 feature_migration: FeatureMigration, 

573) -> None: 

574 debian_dir = migration_request.debian_dir 

575 feature_migration.tagline = "dh_installsystemd files" 

576 for dctrl_bin in migration_request.all_packages: 

577 for stem in [ 

578 "path", 

579 "service", 

580 "socket", 

581 "target", 

582 "timer", 

583 ]: 

584 pkgfile = dhe_pkgfile( 

585 debian_dir, dctrl_bin, stem, bug_950723_prefix_matching=True 

586 ) 

587 if not pkgfile: 

588 continue 

589 if not pkgfile.name.endswith(f".{stem}") or "@." not in pkgfile.name: 589 ↛ 590line 589 didn't jump to line 590 because the condition on line 589 was never true

590 raise UnsupportedFeature( 

591 f'Unable to determine the correct name for {pkgfile.fs_path}. It should be a ".@{stem}"' 

592 f" file now (foo@.service => foo.@service)" 

593 ) 

594 newname = pkgfile.name.replace("@.", ".") 

595 newname = newname[: -len(stem)] + f"@{stem}" 

596 feature_migration.rename_on_success( 

597 pkgfile.fs_path, os.path.join(debian_dir.fs_path, newname) 

598 ) 

599 

600 

601def migrate_clean_file( 

602 migration_request: MigrationRequest, 

603 feature_migration: FeatureMigration, 

604) -> None: 

605 feature_migration.tagline = "debian/clean" 

606 clean_file = migration_request.debian_dir.get("clean") 

607 if clean_file is None: 

608 return 

609 

610 mutable_manifest = assume_not_none(migration_request.manifest.mutable_manifest) 

611 

612 substitution = DHMigrationSubstitution( 

613 dpkg_architecture_table(), 

614 migration_request.acceptable_migration_issues, 

615 feature_migration, 

616 mutable_manifest, 

617 ) 

618 content = dhe_filedoublearray( 

619 clean_file, 

620 substitution, 

621 ) 

622 

623 remove_during_clean_rules = mutable_manifest.remove_during_clean( 

624 create_if_absent=False 

625 ) 

626 tokens = chain.from_iterable(c.tokens for c in content) 

627 rules_before = len(remove_during_clean_rules) 

628 remove_during_clean_rules.extend(tokens) 

629 rules_after = len(remove_during_clean_rules) 

630 feature_migration.successful_manifest_changes += rules_after - rules_before 

631 feature_migration.remove_on_success(clean_file.fs_path) 

632 

633 

634def migrate_maintscript( 

635 migration_request: MigrationRequest, 

636 feature_migration: FeatureMigration, 

637) -> None: 

638 feature_migration.tagline = "dh_installdeb files" 

639 manifest = migration_request.manifest 

640 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

641 for dctrl_bin in migration_request.all_packages: 

642 mainscript_file, content = _dh_config_file( 

643 migration_request.debian_dir, 

644 dctrl_bin, 

645 "maintscript", 

646 "dh_installdeb", 

647 migration_request.acceptable_migration_issues, 

648 feature_migration, 

649 manifest, 

650 ) 

651 

652 if mainscript_file is None: 

653 continue 

654 assert content is not None 

655 

656 package_definition = mutable_manifest.package(dctrl_bin.name) 

657 conffiles = { 

658 it.obsolete_conffile: it 

659 for it in package_definition.conffile_management_items() 

660 } 

661 seen_conffiles = set() 

662 

663 for dhe_line in content: 

664 cmd = dhe_line.tokens[0] 

665 if cmd not in {"rm_conffile", "mv_conffile"}: 665 ↛ 666line 665 didn't jump to line 666 because the condition on line 665 was never true

666 raise UnsupportedFeature( 

667 f"The dh_installdeb file {mainscript_file.path} contains the (currently)" 

668 f' unsupported command "{cmd}" on line {dhe_line.line_no}' 

669 f' (line: "{dhe_line.original_line}")' 

670 ) 

671 

672 try: 

673 ( 

674 _, 

675 obsolete_conffile, 

676 new_conffile, 

677 prior_to_version, 

678 owning_package, 

679 ) = _validate_rm_mv_conffile(dctrl_bin.name, dhe_line) 

680 except ValueError as e: 

681 _error( 

682 f"Validation error in {mainscript_file} on line {dhe_line.line_no}. The error was: {e.args[0]}." 

683 ) 

684 

685 if obsolete_conffile in seen_conffiles: 685 ↛ 686line 685 didn't jump to line 686 because the condition on line 685 was never true

686 raise ConflictingChange( 

687 f'The {mainscript_file} file defines actions for "{obsolete_conffile}" twice!' 

688 f" Please ensure that it is defined at most once in that file." 

689 ) 

690 seen_conffiles.add(obsolete_conffile) 

691 

692 if cmd == "rm_conffile": 

693 item = MutableYAMLConffileManagementItem.rm_conffile( 

694 obsolete_conffile, 

695 prior_to_version, 

696 owning_package, 

697 ) 

698 else: 

699 assert cmd == "mv_conffile" 

700 item = MutableYAMLConffileManagementItem.mv_conffile( 

701 obsolete_conffile, 

702 assume_not_none(new_conffile), 

703 prior_to_version, 

704 owning_package, 

705 ) 

706 

707 existing_def = conffiles.get(item.obsolete_conffile) 

708 if existing_def is not None: 708 ↛ 709line 708 didn't jump to line 709 because the condition on line 708 was never true

709 if not ( 

710 item.command == existing_def.command 

711 and item.new_conffile == existing_def.new_conffile 

712 and item.prior_to_version == existing_def.prior_to_version 

713 and item.owning_package == existing_def.owning_package 

714 ): 

715 raise ConflictingChange( 

716 f"The maintscript defines the action {item.command} for" 

717 f' "{obsolete_conffile}" in {mainscript_file}, but there is another' 

718 f" conffile management definition for same path defined already (in the" 

719 f" existing manifest or an migration e.g., inside {mainscript_file})" 

720 ) 

721 continue 

722 

723 package_definition.add_conffile_management(item) 

724 feature_migration.successful_manifest_changes += 1 

725 

726 

727@dataclasses.dataclass(slots=True) 

728class SourcesAndConditional: 

729 dest_dir: str | None = None 

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

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

732 

733 

734def _raise_on_unsupported_path( 

735 p: str, 

736 path: VirtualPath, 

737 line_no: int, 

738) -> str: 

739 if p.startswith("../") or any(s == ".." for s in p.split("/")): 739 ↛ 740line 739 didn't jump to line 740 because the condition on line 739 was never true

740 _error( 

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

742 ) 

743 return p 

744 

745 

746def _strip_d_tmp(p: str, path: VirtualPath, line_no: int) -> str: 

747 if p.startswith("debian/tmp/") and len(p) > 11: 

748 return p[11:] 

749 if p.startswith("../"): 

750 pruned = p[3:] 

751 if pruned.startswith("../"): 751 ↛ 752line 751 didn't jump to line 752 because the condition on line 751 was never true

752 _raise_on_unsupported_path(p, path, line_no) 

753 _error( 

754 f"Internal error: _raise_on_unsupported_path should have rejected the path at {p!r} ({path.fs_path}:{line_no})" 

755 ) 

756 return f"debian/{pruned}" 

757 

758 return p 

759 

760 

761def migrate_install_file( 

762 migration_request: MigrationRequest, 

763 feature_migration: FeatureMigration, 

764) -> None: 

765 feature_migration.tagline = "dh_install config files" 

766 manifest = migration_request.manifest 

767 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

768 installations = mutable_manifest.installations(create_if_absent=False) 

769 priority_lines = [] 

770 remaining_install_lines = [] 

771 warn_about_fixmes_in_dest_dir = False 

772 

773 is_single_binary = migration_request.is_single_binary_package 

774 

775 for dctrl_bin in migration_request.all_packages: 

776 install_file, content = _dh_config_file( 

777 migration_request.debian_dir, 

778 dctrl_bin, 

779 "install", 

780 "dh_install", 

781 migration_request.acceptable_migration_issues, 

782 feature_migration, 

783 manifest, 

784 support_executable_files=True, 

785 allow_dh_exec_rename=True, 

786 ) 

787 if not install_file or not content: 

788 continue 

789 current_sources = [] 

790 sources_by_destdir: dict[tuple[str, tuple[str, ...]], SourcesAndConditional] = ( 

791 {} 

792 ) 

793 install_as_rules = [] 

794 multi_dest = collections.defaultdict(list) 

795 seen_sources = set() 

796 multi_dest_sources: set[str] = set() 

797 

798 for dhe_line in content: 

799 special_rule = None 

800 if "=>" in dhe_line.tokens: 

801 if dhe_line.tokens[0] == "=>" and len(dhe_line.tokens) == 2: 

802 # This rule must be as early as possible to retain the semantics 

803 path = _strip_d_tmp( 

804 _normalize_path( 

805 dhe_line.tokens[1], 

806 with_prefix=False, 

807 allow_and_keep_upward_segments=True, 

808 ), 

809 dhe_line.config_file, 

810 dhe_line.line_no, 

811 ) 

812 special_rule = AbstractMutableYAMLInstallRule.install_dest( 

813 path, 

814 dctrl_bin.name if not is_single_binary else None, 

815 dest_dir=None, 

816 when=dhe_line.conditional(), 

817 ) 

818 elif len(dhe_line.tokens) != 3: 818 ↛ 819line 818 didn't jump to line 819 because the condition on line 818 was never true

819 _error( 

820 f"Validation error in {install_file.path} on line {dhe_line.line_no}. Cannot migrate dh-exec" 

821 ' renames that is not exactly "SOURCE => TARGET" or "=> TARGET".' 

822 ) 

823 else: 

824 install_rule = AbstractMutableYAMLInstallRule.install_as( 

825 _strip_d_tmp( 

826 _normalize_path( 

827 dhe_line.tokens[0], 

828 with_prefix=False, 

829 allow_and_keep_upward_segments=True, 

830 ), 

831 dhe_line.config_file, 

832 dhe_line.line_no, 

833 ), 

834 _raise_on_unsupported_path( 

835 _normalize_path( 

836 dhe_line.tokens[2], 

837 with_prefix=False, 

838 allow_and_keep_upward_segments=True, 

839 ), 

840 dhe_line.config_file, 

841 dhe_line.line_no, 

842 ), 

843 dctrl_bin.name if not is_single_binary else None, 

844 when=dhe_line.conditional(), 

845 ) 

846 install_as_rules.append(install_rule) 

847 else: 

848 if len(dhe_line.tokens) > 1: 

849 sources = list( 

850 _strip_d_tmp( 

851 _normalize_path( 

852 w, 

853 with_prefix=False, 

854 allow_and_keep_upward_segments=True, 

855 ), 

856 dhe_line.config_file, 

857 dhe_line.line_no, 

858 ) 

859 for w in dhe_line.tokens[:-1] 

860 ) 

861 dest_dir = _raise_on_unsupported_path( 

862 _normalize_path( 

863 dhe_line.tokens[-1], 

864 with_prefix=False, 

865 allow_and_keep_upward_segments=True, 

866 ), 

867 dhe_line.config_file, 

868 dhe_line.line_no, 

869 ) 

870 else: 

871 sources = list( 

872 _strip_d_tmp( 

873 _normalize_path( 

874 w, 

875 with_prefix=False, 

876 allow_and_keep_upward_segments=True, 

877 ), 

878 dhe_line.config_file, 

879 dhe_line.line_no, 

880 ) 

881 for w in dhe_line.tokens 

882 ) 

883 dest_dir = None 

884 

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

886 seen_sources.update(sources) 

887 

888 if dest_dir is None and dhe_line.conditional() is None: 

889 current_sources.extend(sources) 

890 continue 

891 key = (dest_dir, dhe_line.conditional_key()) 

892 ctor = functools.partial( 

893 SourcesAndConditional, 

894 dest_dir=dest_dir, 

895 conditional=dhe_line.conditional(), 

896 ) 

897 md = _fetch_or_create( 

898 sources_by_destdir, 

899 key, 

900 ctor, 

901 ) 

902 md.sources.extend(sources) 

903 

904 if special_rule: 

905 priority_lines.append(special_rule) 

906 

907 remaining_install_lines.extend(install_as_rules) 

908 

909 for md in sources_by_destdir.values(): 

910 if multi_dest_sources: 

911 sources = [s for s in md.sources if s not in multi_dest_sources] 

912 already_installed = (s for s in md.sources if s in multi_dest_sources) 

913 for s in already_installed: 

914 # The sources are ignored, so we can reuse the object as-is 

915 multi_dest[s].append(md) 

916 if not sources: 

917 continue 

918 else: 

919 sources = md.sources 

920 install_rule = AbstractMutableYAMLInstallRule.install_dest( 

921 sources[0] if len(sources) == 1 else sources, 

922 dctrl_bin.name if not is_single_binary else None, 

923 dest_dir=md.dest_dir, 

924 when=md.conditional, 

925 ) 

926 remaining_install_lines.append(install_rule) 

927 

928 if current_sources: 

929 if multi_dest_sources: 

930 sources = [s for s in current_sources if s not in multi_dest_sources] 

931 already_installed = ( 

932 s for s in current_sources if s in multi_dest_sources 

933 ) 

934 for s in already_installed: 

935 # The sources are ignored, so we can reuse the object as-is 

936 dest_dir = os.path.dirname(s) 

937 if has_glob_magic(dest_dir): 

938 warn_about_fixmes_in_dest_dir = True 

939 dest_dir = f"FIXME: {dest_dir} (could not reliably compute the dest dir)" 

940 multi_dest[s].append( 

941 SourcesAndConditional( 

942 dest_dir=dest_dir, 

943 conditional=None, 

944 ) 

945 ) 

946 else: 

947 sources = current_sources 

948 

949 if sources: 

950 install_rule = AbstractMutableYAMLInstallRule.install_dest( 

951 sources[0] if len(sources) == 1 else sources, 

952 dctrl_bin.name if not is_single_binary else None, 

953 dest_dir=None, 

954 ) 

955 remaining_install_lines.append(install_rule) 

956 

957 if multi_dest: 

958 for source, dest_and_conditionals in multi_dest.items(): 

959 dest_dirs = [dac.dest_dir for dac in dest_and_conditionals] 

960 # We assume the conditional is the same. 

961 conditional = next( 

962 iter( 

963 dac.conditional 

964 for dac in dest_and_conditionals 

965 if dac.conditional is not None 

966 ), 

967 None, 

968 ) 

969 remaining_install_lines.append( 

970 AbstractMutableYAMLInstallRule.multi_dest_install( 

971 source, 

972 dest_dirs, 

973 dctrl_bin.name if not is_single_binary else None, 

974 when=conditional, 

975 ) 

976 ) 

977 

978 if priority_lines: 

979 installations.extend(priority_lines) 

980 

981 if remaining_install_lines: 

982 installations.extend(remaining_install_lines) 

983 

984 feature_migration.successful_manifest_changes += len(priority_lines) + len( 

985 remaining_install_lines 

986 ) 

987 if warn_about_fixmes_in_dest_dir: 

988 feature_migration.warn( 

989 "TODO: FIXME left in dest-dir(s) of some installation rules." 

990 " Please review these and remove the FIXME (plus correct as necessary)" 

991 ) 

992 

993 

994def migrate_installdocs_file( 

995 migration_request: MigrationRequest, 

996 feature_migration: FeatureMigration, 

997) -> None: 

998 feature_migration.tagline = "dh_installdocs config files" 

999 manifest = migration_request.manifest 

1000 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1001 installations = mutable_manifest.installations(create_if_absent=False) 

1002 

1003 is_single_binary = migration_request.is_single_binary_package 

1004 

1005 for dctrl_bin in migration_request.all_packages: 

1006 install_file, content = _dh_config_file( 

1007 migration_request.debian_dir, 

1008 dctrl_bin, 

1009 "docs", 

1010 "dh_installdocs", 

1011 migration_request.acceptable_migration_issues, 

1012 feature_migration, 

1013 manifest, 

1014 support_executable_files=True, 

1015 ) 

1016 if not install_file: 

1017 continue 

1018 assert content is not None 

1019 docs: list[str] = [] 

1020 for dhe_line in content: 

1021 if dhe_line.arch_filter or dhe_line.build_profile_filter: 1021 ↛ 1022line 1021 didn't jump to line 1022 because the condition on line 1021 was never true

1022 _error( 

1023 f"Unable to migrate line {dhe_line.line_no} of {install_file.path}." 

1024 " Missing support for conditions." 

1025 ) 

1026 docs.extend( 

1027 _raise_on_unsupported_path( 

1028 _normalize_path( 

1029 w, with_prefix=False, allow_and_keep_upward_segments=True 

1030 ), 

1031 dhe_line.config_file, 

1032 dhe_line.line_no, 

1033 ) 

1034 for w in dhe_line.tokens 

1035 ) 

1036 

1037 if not docs: 1037 ↛ 1038line 1037 didn't jump to line 1038 because the condition on line 1037 was never true

1038 continue 

1039 feature_migration.successful_manifest_changes += 1 

1040 install_rule = AbstractMutableYAMLInstallRule.install_docs( 

1041 docs if len(docs) > 1 else docs[0], 

1042 dctrl_bin.name if not is_single_binary else None, 

1043 ) 

1044 installations.create_definition_if_missing() 

1045 installations.append(install_rule) 

1046 

1047 

1048def migrate_installexamples_file( 

1049 migration_request: MigrationRequest, 

1050 feature_migration: FeatureMigration, 

1051) -> None: 

1052 feature_migration.tagline = "dh_installexamples config files" 

1053 manifest = migration_request.manifest 

1054 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1055 installations = mutable_manifest.installations(create_if_absent=False) 

1056 is_single_binary = migration_request.is_single_binary_package 

1057 

1058 for dctrl_bin in manifest.all_packages: 

1059 install_file, content = _dh_config_file( 

1060 migration_request.debian_dir, 

1061 dctrl_bin, 

1062 "examples", 

1063 "dh_installexamples", 

1064 migration_request.acceptable_migration_issues, 

1065 feature_migration, 

1066 manifest, 

1067 support_executable_files=True, 

1068 ) 

1069 if not install_file: 

1070 continue 

1071 assert content is not None 

1072 examples: list[str] = [] 

1073 for dhe_line in content: 

1074 if dhe_line.arch_filter or dhe_line.build_profile_filter: 1074 ↛ 1075line 1074 didn't jump to line 1075 because the condition on line 1074 was never true

1075 _error( 

1076 f"Unable to migrate line {dhe_line.line_no} of {install_file.path}." 

1077 " Missing support for conditions." 

1078 ) 

1079 examples.extend( 

1080 _raise_on_unsupported_path( 

1081 _normalize_path( 

1082 w, with_prefix=False, allow_and_keep_upward_segments=True 

1083 ), 

1084 dhe_line.config_file, 

1085 dhe_line.line_no, 

1086 ) 

1087 for w in dhe_line.tokens 

1088 ) 

1089 

1090 if not examples: 1090 ↛ 1091line 1090 didn't jump to line 1091 because the condition on line 1090 was never true

1091 continue 

1092 feature_migration.successful_manifest_changes += 1 

1093 install_rule = AbstractMutableYAMLInstallRule.install_examples( 

1094 examples if len(examples) > 1 else examples[0], 

1095 dctrl_bin.name if not is_single_binary else None, 

1096 ) 

1097 installations.create_definition_if_missing() 

1098 installations.append(install_rule) 

1099 

1100 

1101@dataclasses.dataclass(slots=True) 

1102class InfoFilesDefinition: 

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

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

1105 

1106 

1107def migrate_installinfo_file( 

1108 migration_request: MigrationRequest, 

1109 feature_migration: FeatureMigration, 

1110) -> None: 

1111 feature_migration.tagline = "dh_installinfo config files" 

1112 manifest = migration_request.manifest 

1113 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1114 installations = mutable_manifest.installations(create_if_absent=False) 

1115 is_single_binary = migration_request.is_single_binary_package 

1116 

1117 for dctrl_bin in manifest.all_packages: 

1118 info_file, content = _dh_config_file( 

1119 migration_request.debian_dir, 

1120 dctrl_bin, 

1121 "info", 

1122 "dh_installinfo", 

1123 migration_request.acceptable_migration_issues, 

1124 feature_migration, 

1125 manifest, 

1126 support_executable_files=True, 

1127 ) 

1128 if not info_file: 

1129 continue 

1130 assert content is not None 

1131 info_files_by_condition: dict[tuple[str, ...], InfoFilesDefinition] = {} 

1132 for dhe_line in content: 

1133 key = dhe_line.conditional_key() 

1134 ctr = functools.partial( 

1135 InfoFilesDefinition, conditional=dhe_line.conditional() 

1136 ) 

1137 info_def = _fetch_or_create( 

1138 info_files_by_condition, 

1139 key, 

1140 ctr, 

1141 ) 

1142 info_def.sources.extend( 

1143 _raise_on_unsupported_path( 

1144 _normalize_path( 

1145 w, with_prefix=False, allow_and_keep_upward_segments=True 

1146 ), 

1147 dhe_line.config_file, 

1148 dhe_line.line_no, 

1149 ) 

1150 for w in dhe_line.tokens 

1151 ) 

1152 

1153 if not info_files_by_condition: 1153 ↛ 1154line 1153 didn't jump to line 1154 because the condition on line 1153 was never true

1154 continue 

1155 feature_migration.successful_manifest_changes += 1 

1156 installations.create_definition_if_missing() 

1157 for info_def in info_files_by_condition.values(): 

1158 info_files = info_def.sources 

1159 install_rule = AbstractMutableYAMLInstallRule.install_docs( 

1160 info_files if len(info_files) > 1 else info_files[0], 

1161 dctrl_bin.name if not is_single_binary else None, 

1162 dest_dir="{{path:GNU_INFO_DIR}}", 

1163 when=info_def.conditional, 

1164 ) 

1165 installations.append(install_rule) 

1166 

1167 

1168@dataclasses.dataclass(slots=True) 

1169class ManpageDefinition: 

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

1171 language: str | None = None 

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

1173 

1174 

1175DK = TypeVar("DK") 

1176DV = TypeVar("DV") 

1177 

1178 

1179def _fetch_or_create(d: dict[DK, DV], key: DK, factory: Callable[[], DV]) -> DV: 

1180 v = d.get(key) 

1181 if v is None: 

1182 v = factory() 

1183 d[key] = v 

1184 return v 

1185 

1186 

1187def migrate_installman_file( 

1188 migration_request: MigrationRequest, 

1189 feature_migration: FeatureMigration, 

1190) -> None: 

1191 feature_migration.tagline = "dh_installman config files" 

1192 manifest = migration_request.manifest 

1193 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1194 installations = mutable_manifest.installations(create_if_absent=False) 

1195 is_single_binary = migration_request.is_single_binary_package 

1196 warn_about_basename = False 

1197 

1198 for dctrl_bin in migration_request.all_packages: 

1199 manpages_file, content = _dh_config_file( 

1200 migration_request.debian_dir, 

1201 dctrl_bin, 

1202 "manpages", 

1203 "dh_installman", 

1204 migration_request.acceptable_migration_issues, 

1205 feature_migration, 

1206 manifest, 

1207 support_executable_files=True, 

1208 allow_dh_exec_rename=True, 

1209 ) 

1210 if not manpages_file: 

1211 continue 

1212 assert content is not None 

1213 

1214 vanilla_definitions = [] 

1215 install_as_rules = [] 

1216 complex_definitions: dict[ 

1217 tuple[str | None, tuple[str, ...]], ManpageDefinition 

1218 ] = {} 

1219 install_rule: AbstractMutableYAMLInstallRule 

1220 for dhe_line in content: 

1221 if "=>" in dhe_line.tokens: 1221 ↛ 1224line 1221 didn't jump to line 1224 because the condition on line 1221 was never true

1222 # dh-exec allows renaming features. For `debputy`, we degenerate it into an `install` (w. `as`) feature 

1223 # without any of the `install-man` features. 

1224 if dhe_line.tokens[0] == "=>" and len(dhe_line.tokens) == 2: 

1225 _error( 

1226 f'Unsupported "=> DEST" rule for error in {manpages_file.path} on line {dhe_line.line_no}."' 

1227 f' Cannot migrate dh-exec renames that is not exactly "SOURCE => TARGET" for d/manpages files.' 

1228 ) 

1229 elif len(dhe_line.tokens) != 3: 

1230 _error( 

1231 f"Validation error in {manpages_file.path} on line {dhe_line.line_no}. Cannot migrate dh-exec" 

1232 ' renames that is not exactly "SOURCE => TARGET" or "=> TARGET".' 

1233 ) 

1234 else: 

1235 install_rule = AbstractMutableYAMLInstallRule.install_doc_as( 

1236 _raise_on_unsupported_path( 

1237 _normalize_path( 

1238 dhe_line.tokens[0], 

1239 with_prefix=False, 

1240 allow_and_keep_upward_segments=True, 

1241 ), 

1242 dhe_line.config_file, 

1243 dhe_line.line_no, 

1244 ), 

1245 _raise_on_unsupported_path( 

1246 _normalize_path( 

1247 dhe_line.tokens[2], 

1248 with_prefix=False, 

1249 allow_and_keep_upward_segments=True, 

1250 ), 

1251 dhe_line.config_file, 

1252 dhe_line.line_no, 

1253 ), 

1254 dctrl_bin.name if not is_single_binary else None, 

1255 when=dhe_line.conditional(), 

1256 ) 

1257 install_as_rules.append(install_rule) 

1258 continue 

1259 

1260 sources = [ 

1261 _raise_on_unsupported_path( 

1262 _normalize_path( 

1263 w, with_prefix=False, allow_and_keep_upward_segments=True 

1264 ), 

1265 dhe_line.config_file, 

1266 dhe_line.line_no, 

1267 ) 

1268 for w in dhe_line.tokens 

1269 ] 

1270 needs_basename = any( 

1271 MAN_GUESS_FROM_BASENAME.search(x) 

1272 and not MAN_GUESS_LANG_FROM_PATH.search(x) 

1273 for x in sources 

1274 ) 

1275 if needs_basename or dhe_line.conditional() is not None: 

1276 if needs_basename: 1276 ↛ 1280line 1276 didn't jump to line 1280 because the condition on line 1276 was always true

1277 warn_about_basename = True 

1278 language = "derive-from-basename" 

1279 else: 

1280 language = None 

1281 key = (language, dhe_line.conditional_key()) 

1282 ctor = functools.partial( 

1283 ManpageDefinition, 

1284 language=language, 

1285 conditional=dhe_line.conditional(), 

1286 ) 

1287 manpage_def = _fetch_or_create( 

1288 complex_definitions, 

1289 key, 

1290 ctor, 

1291 ) 

1292 manpage_def.sources.extend(sources) 

1293 else: 

1294 vanilla_definitions.extend(sources) 

1295 

1296 if not install_as_rules and not vanilla_definitions and not complex_definitions: 1296 ↛ 1297line 1296 didn't jump to line 1297 because the condition on line 1296 was never true

1297 continue 

1298 feature_migration.successful_manifest_changes += 1 

1299 installations.create_definition_if_missing() 

1300 installations.extend(install_as_rules) 

1301 if vanilla_definitions: 1301 ↛ 1313line 1301 didn't jump to line 1313 because the condition on line 1301 was always true

1302 man_source = ( 

1303 vanilla_definitions 

1304 if len(vanilla_definitions) > 1 

1305 else vanilla_definitions[0] 

1306 ) 

1307 install_rule = AbstractMutableYAMLInstallRule.install_man( 

1308 man_source, 

1309 dctrl_bin.name if not is_single_binary else None, 

1310 None, 

1311 ) 

1312 installations.append(install_rule) 

1313 for manpage_def in complex_definitions.values(): 

1314 sources = manpage_def.sources 

1315 install_rule = AbstractMutableYAMLInstallRule.install_man( 

1316 sources if len(sources) > 1 else sources[0], 

1317 dctrl_bin.name if not is_single_binary else None, 

1318 manpage_def.language, 

1319 when=manpage_def.conditional, 

1320 ) 

1321 installations.append(install_rule) 

1322 

1323 if warn_about_basename: 

1324 feature_migration.warn( 

1325 'Detected man pages that might rely on "derive-from-basename" logic. Please double check' 

1326 " that the generated `install-man` rules are correct" 

1327 ) 

1328 

1329 

1330def migrate_not_installed_file( 

1331 migration_request: MigrationRequest, 

1332 feature_migration: FeatureMigration, 

1333) -> None: 

1334 feature_migration.tagline = "dh_missing's not-installed config file" 

1335 manifest = migration_request.manifest 

1336 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1337 installations = mutable_manifest.installations(create_if_absent=False) 

1338 main_binary = migration_request.main_binary 

1339 

1340 missing_file, content = _dh_config_file( 

1341 migration_request.debian_dir, 

1342 main_binary, 

1343 "not-installed", 

1344 "dh_missing", 

1345 migration_request.acceptable_migration_issues, 

1346 feature_migration, 

1347 manifest, 

1348 support_executable_files=False, 

1349 pkgfile_lookup=False, 

1350 ) 

1351 discard_rules: list[str] = [] 

1352 if missing_file: 

1353 assert content is not None 

1354 for dhe_line in content: 

1355 discard_rules.extend( 

1356 _raise_on_unsupported_path( 

1357 _normalize_path( 

1358 w, with_prefix=False, allow_and_keep_upward_segments=True 

1359 ), 

1360 dhe_line.config_file, 

1361 dhe_line.line_no, 

1362 ) 

1363 for w in dhe_line.tokens 

1364 ) 

1365 

1366 if discard_rules: 

1367 feature_migration.successful_manifest_changes += 1 

1368 install_rule = AbstractMutableYAMLInstallRule.discard( 

1369 discard_rules if len(discard_rules) > 1 else discard_rules[0], 

1370 ) 

1371 installations.create_definition_if_missing() 

1372 installations.append(install_rule) 

1373 

1374 

1375def min_dh_compat_check( 

1376 migration_request: MigrationRequest, 

1377 feature_migration: FeatureMigration, 

1378) -> None: 

1379 feature_migration.tagline = "min dh compat level check" 

1380 # We start on compat 12 for arch:any due to the new dh_makeshlibs and dh_installinit default 

1381 # For arch:any, the min is compat 14 due to `dh_dwz` being removed. 

1382 min_compat = ( 

1383 14 if any(not p.is_arch_all for p in migration_request.all_packages) else 12 

1384 ) 

1385 feature_migration.assumed_compat = min_compat 

1386 

1387 

1388def detect_pam_files( 

1389 migration_request: MigrationRequest, 

1390 feature_migration: FeatureMigration, 

1391) -> None: 

1392 feature_migration.tagline = "detect dh_installpam files (min dh compat)" 

1393 for dctrl_bin in migration_request.all_packages: 

1394 dh_config_file = dhe_pkgfile(migration_request.debian_dir, dctrl_bin, "pam") 

1395 if dh_config_file is not None: 

1396 feature_migration.assumed_compat = 14 

1397 break 

1398 

1399 

1400def migrate_tmpfile( 

1401 migration_request: MigrationRequest, 

1402 feature_migration: FeatureMigration, 

1403) -> None: 

1404 feature_migration.tagline = "dh_installtmpfiles config files" 

1405 debian_dir = migration_request.debian_dir 

1406 for dctrl_bin in migration_request.all_packages: 

1407 dh_config_file = dhe_pkgfile(debian_dir, dctrl_bin, "tmpfile") 

1408 if dh_config_file is not None: 

1409 target = ( 

1410 dh_config_file.name.replace(".tmpfile", ".tmpfiles") 

1411 if "." in dh_config_file.name 

1412 else "tmpfiles" 

1413 ) 

1414 _rename_file_if_exists( 

1415 debian_dir, 

1416 dh_config_file.name, 

1417 target, 

1418 feature_migration, 

1419 ) 

1420 

1421 

1422def migrate_lintian_overrides_files( 

1423 migration_request: MigrationRequest, 

1424 feature_migration: FeatureMigration, 

1425) -> None: 

1426 feature_migration.tagline = "dh_lintian config files" 

1427 for dctrl_bin in migration_request.all_packages: 

1428 # We do not support executable lintian-overrides and `_dh_config_file` handles all of that. 

1429 # Therefore, the return value is irrelevant to us. 

1430 _dh_config_file( 

1431 migration_request.debian_dir, 

1432 dctrl_bin, 

1433 "lintian-overrides", 

1434 "dh_lintian", 

1435 migration_request.acceptable_migration_issues, 

1436 feature_migration, 

1437 migration_request.manifest, 

1438 support_executable_files=False, 

1439 remove_on_migration=False, 

1440 ) 

1441 

1442 

1443def migrate_links_files( 

1444 migration_request: MigrationRequest, 

1445 feature_migration: FeatureMigration, 

1446) -> None: 

1447 feature_migration.tagline = "dh_link files" 

1448 manifest = migration_request.manifest 

1449 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1450 for dctrl_bin in migration_request.all_packages: 

1451 links_file, content = _dh_config_file( 

1452 migration_request.debian_dir, 

1453 dctrl_bin, 

1454 "links", 

1455 "dh_link", 

1456 migration_request.acceptable_migration_issues, 

1457 feature_migration, 

1458 manifest, 

1459 support_executable_files=True, 

1460 ) 

1461 

1462 if links_file is None: 

1463 continue 

1464 assert content is not None 

1465 

1466 package_definition = mutable_manifest.package(dctrl_bin.name) 

1467 defined_symlink = { 

1468 symlink.symlink_path: symlink.symlink_target 

1469 for symlink in package_definition.symlinks() 

1470 } 

1471 

1472 seen_symlinks: set[str] = set() 

1473 

1474 for dhe_line in content: 

1475 if len(dhe_line.tokens) != 2: 1475 ↛ 1476line 1475 didn't jump to line 1476 because the condition on line 1475 was never true

1476 raise UnsupportedFeature( 

1477 f"The dh_link file {links_file.fs_path} did not have exactly two paths on line" 

1478 f' {dhe_line.line_no} (line: "{dhe_line.original_line}"' 

1479 ) 

1480 target, source = dhe_line.tokens 

1481 source = _raise_on_unsupported_path( 

1482 source, 

1483 dhe_line.config_file, 

1484 dhe_line.line_no, 

1485 ) 

1486 target = _raise_on_unsupported_path( 

1487 target, 

1488 dhe_line.config_file, 

1489 dhe_line.line_no, 

1490 ) 

1491 if source in seen_symlinks: 1491 ↛ 1493line 1491 didn't jump to line 1493 because the condition on line 1491 was never true

1492 # According to #934499, this has happened in the wild already 

1493 raise ConflictingChange( 

1494 f"The {links_file.fs_path} file defines the link path {source} twice! Please ensure" 

1495 " that it is defined at most once in that file" 

1496 ) 

1497 seen_symlinks.add(source) 

1498 # Symlinks in .links are always considered absolute, but you were not required to have a leading slash. 

1499 # However, in the debputy manifest, you can have relative links, so we should ensure it is explicitly 

1500 # absolute. 

1501 if not target.startswith("/"): 1501 ↛ 1503line 1501 didn't jump to line 1503 because the condition on line 1501 was always true

1502 target = "/" + target 

1503 existing_target = defined_symlink.get(source) 

1504 if existing_target is not None: 1504 ↛ 1505line 1504 didn't jump to line 1505 because the condition on line 1504 was never true

1505 if existing_target != target: 

1506 raise ConflictingChange( 

1507 f'The symlink "{source}" points to "{target}" in {links_file}, but there is' 

1508 f' another symlink with same path pointing to "{existing_target}" defined' 

1509 " already (in the existing manifest or an migration e.g., inside" 

1510 f" {links_file.fs_path})" 

1511 ) 

1512 continue 

1513 condition = dhe_line.conditional() 

1514 package_definition.add_symlink( 

1515 MutableYAMLSymlink.new_symlink( 

1516 source, 

1517 target, 

1518 condition, 

1519 ) 

1520 ) 

1521 feature_migration.successful_manifest_changes += 1 

1522 

1523 

1524def migrate_misspelled_readme_debian_files( 

1525 migration_request: MigrationRequest, 

1526 feature_migration: FeatureMigration, 

1527) -> None: 

1528 feature_migration.tagline = "misspelled README.Debian files" 

1529 debian_dir = migration_request.debian_dir 

1530 for dctrl_bin in migration_request.all_packages: 

1531 readme, _ = _dh_config_file( 

1532 debian_dir, 

1533 dctrl_bin, 

1534 "README.debian", 

1535 "dh_installdocs", 

1536 migration_request.acceptable_migration_issues, 

1537 feature_migration, 

1538 migration_request.manifest, 

1539 support_executable_files=False, 

1540 remove_on_migration=False, 

1541 ) 

1542 if readme is None: 

1543 continue 

1544 new_name = readme.name.replace("README.debian", "README.Debian") 

1545 assert readme.name != new_name 

1546 _rename_file_if_exists( 

1547 debian_dir, 

1548 readme.name, 

1549 new_name, 

1550 feature_migration, 

1551 ) 

1552 

1553 

1554def migrate_doc_base_files( 

1555 migration_request: MigrationRequest, 

1556 feature_migration: FeatureMigration, 

1557) -> None: 

1558 feature_migration.tagline = "doc-base files" 

1559 debian_dir = migration_request.debian_dir 

1560 # ignore the dh_make ".EX" file if one should still be present. The dh_installdocs tool ignores it too. 

1561 possible_effected_doc_base_files = [ 

1562 f 

1563 for f in debian_dir.iterdir 

1564 if ( 

1565 (".doc-base." in f.name or f.name.startswith("doc-base.")) 

1566 and not f.name.endswith("doc-base.EX") 

1567 ) 

1568 ] 

1569 known_packages = {d.name: d for d in migration_request.all_packages} 

1570 main_package = migration_request.main_binary 

1571 for doc_base_file in possible_effected_doc_base_files: 

1572 parts = doc_base_file.name.split(".") 

1573 owning_package = known_packages.get(parts[0]) 

1574 if owning_package is None: 1574 ↛ 1575line 1574 didn't jump to line 1575 because the condition on line 1574 was never true

1575 owning_package = main_package 

1576 package_part = None 

1577 else: 

1578 package_part = parts[0] 

1579 parts = parts[1:] 

1580 

1581 if not parts or parts[0] != "doc-base": 1581 ↛ 1583line 1581 didn't jump to line 1583 because the condition on line 1581 was never true

1582 # Not a doc-base file after all 

1583 continue 

1584 

1585 if len(parts) > 1: 1585 ↛ 1592line 1585 didn't jump to line 1592 because the condition on line 1585 was always true

1586 name_part = ".".join(parts[1:]) 

1587 if package_part is None: 1587 ↛ 1589line 1587 didn't jump to line 1589 because the condition on line 1587 was never true

1588 # Named files must have a package prefix 

1589 package_part = owning_package.name 

1590 else: 

1591 # No rename needed 

1592 continue 

1593 

1594 new_basename = ".".join(filter(None, (package_part, name_part, "doc-base"))) 

1595 _rename_file_if_exists( 

1596 debian_dir, 

1597 doc_base_file.name, 

1598 new_basename, 

1599 feature_migration, 

1600 ) 

1601 

1602 

1603def migrate_dh_hook_targets( 

1604 migration_request: MigrationRequest, 

1605 feature_migration: FeatureMigration, 

1606) -> None: 

1607 feature_migration.tagline = "dh hook targets" 

1608 source_root = os.path.dirname(migration_request.debian_dir.fs_path) 

1609 if source_root == "": 

1610 source_root = "." 

1611 detected_hook_targets = json.loads( 

1612 subprocess.check_output( 

1613 ["dh_assistant", "detect-hook-targets"], 

1614 cwd=source_root, 

1615 ).decode("utf-8") 

1616 ) 

1617 sample_hook_target: str | None = None 

1618 debputy_integration_mode = migration_request.migration_target 

1619 assert debputy_integration_mode is not None 

1620 replaced_commands = DH_COMMANDS_REPLACED[debputy_integration_mode] 

1621 

1622 for hook_target_def in detected_hook_targets["hook-targets"]: 

1623 if hook_target_def["is-empty"]: 

1624 continue 

1625 command = hook_target_def["command"] 

1626 if command not in replaced_commands: 

1627 continue 

1628 hook_target = hook_target_def["target-name"] 

1629 advice = MIGRATION_AID_FOR_OVERRIDDEN_COMMANDS.get(command) 

1630 if advice is None: 

1631 if sample_hook_target is None: 

1632 sample_hook_target = hook_target 

1633 feature_migration.warn( 

1634 f"TODO: MANUAL MIGRATION required for hook target {hook_target}" 

1635 ) 

1636 else: 

1637 feature_migration.warn( 

1638 f"TODO: MANUAL MIGRATION required for hook target {hook_target}. Please see {advice}" 

1639 f" for migration advice." 

1640 ) 

1641 if ( 

1642 feature_migration.warnings 

1643 and "dh-hook-targets" not in migration_request.acceptable_migration_issues 

1644 and sample_hook_target is not None 

1645 ): 

1646 raise UnsupportedFeature( 

1647 f"The debian/rules file contains one or more non empty dh hook targets that will not" 

1648 f" be run with the requested debputy dh sequence with no known migration advice. One of these would be" 

1649 f" {sample_hook_target}.", 

1650 ["dh-hook-targets"], 

1651 ) 

1652 

1653 

1654def detect_unsupported_zz_debputy_features( 

1655 migration_request: MigrationRequest, 

1656 feature_migration: FeatureMigration, 

1657) -> None: 

1658 feature_migration.tagline = "Known unsupported features" 

1659 

1660 for unsupported_config in UNSUPPORTED_DH_CONFIGS_AND_TOOLS_FOR_ZZ_DEBPUTY: 

1661 _unsupported_debhelper_config_file( 

1662 migration_request, 

1663 unsupported_config, 

1664 feature_migration, 

1665 ) 

1666 

1667 

1668def detect_obsolete_substvars( 

1669 migration_request: MigrationRequest, 

1670 feature_migration: FeatureMigration, 

1671) -> None: 

1672 feature_migration.tagline = ( 

1673 "Check for obsolete ${foo:var} variables in debian/control" 

1674 ) 

1675 ctrl_file = migration_request.debian_dir.get("control") 

1676 if not ctrl_file: 1676 ↛ 1677line 1676 didn't jump to line 1677 because the condition on line 1676 was never true

1677 feature_migration.warn( 

1678 "Cannot find debian/control. Detection of obsolete substvars could not be performed." 

1679 ) 

1680 return 

1681 with ctrl_file.open() as fd: 

1682 ctrl = list(Deb822.iter_paragraphs(fd)) 

1683 

1684 relationship_fields = dpkg_field_list_pkg_dep() 

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

1686 

1687 for p in ctrl[1:]: 

1688 seen_obsolete_relationship_substvars = set() 

1689 obsolete_fields = set() 

1690 is_essential = p.get("Essential") == "yes" 

1691 for df in relationship_fields: 

1692 field: str | None = p.get(df) 

1693 if field is None: 

1694 continue 

1695 df_lc = df.lower() 

1696 number_of_relations = 0 

1697 obsolete_substvars_in_field = set() 

1698 for d in (d.strip() for d in field.strip().split(",")): 

1699 if not d: 

1700 continue 

1701 number_of_relations += 1 

1702 if not d.startswith("${"): 

1703 continue 

1704 try: 

1705 end_idx = d.index("}") 

1706 except ValueError: 

1707 continue 

1708 substvar_name = d[2:end_idx] 

1709 if ":" not in substvar_name: 1709 ↛ 1710line 1709 didn't jump to line 1710 because the condition on line 1709 was never true

1710 continue 

1711 _, field = substvar_name.rsplit(":", 1) 

1712 field_lc = field.lower() 

1713 if field_lc not in relationship_fields_lc: 1713 ↛ 1714line 1713 didn't jump to line 1714 because the condition on line 1713 was never true

1714 continue 

1715 is_obsolete = field_lc == df_lc 

1716 if ( 

1717 not is_obsolete 

1718 and is_essential 

1719 and substvar_name.lower() == "shlibs:depends" 

1720 and df_lc == "pre-depends" 

1721 ): 

1722 is_obsolete = True 

1723 

1724 if is_obsolete: 

1725 obsolete_substvars_in_field.add(d) 

1726 

1727 if number_of_relations == len(obsolete_substvars_in_field): 

1728 obsolete_fields.add(df) 

1729 else: 

1730 seen_obsolete_relationship_substvars.update(obsolete_substvars_in_field) 

1731 

1732 package = p.get("Package", "(Missing package name!?)") 

1733 fo = feature_migration.fo 

1734 if obsolete_fields: 

1735 fields = ", ".join(obsolete_fields) 

1736 feature_migration.warn( 

1737 f"The following relationship fields can be removed from {package}: {fields}." 

1738 f" (The content in them would be applied automatically. Note: {fo.bts('1067653')})" 

1739 ) 

1740 if seen_obsolete_relationship_substvars: 

1741 v = ", ".join(sorted(seen_obsolete_relationship_substvars)) 

1742 feature_migration.warn( 

1743 f"The following relationship substitution variables can be removed from {package}: {v}" 

1744 f" (Note: {fo.bts('1067653')})" 

1745 ) 

1746 

1747 

1748def detect_dh_addons_zz_debputy_rrr( 

1749 migration_request: MigrationRequest, 

1750 feature_migration: FeatureMigration, 

1751) -> None: 

1752 feature_migration.tagline = "Check for dh-sequence-addons" 

1753 r = read_dh_addon_sequences(migration_request.debian_dir) 

1754 if r is None: 

1755 feature_migration.warn( 

1756 "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon" 

1757 " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy-rrr." 

1758 ) 

1759 return 

1760 

1761 bd_sequences, dr_sequences, _ = r 

1762 

1763 remaining_sequences = bd_sequences | dr_sequences 

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

1765 

1766 if not saw_dh_debputy: 

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

1768 

1769 

1770def detect_dh_addons_with_full_integration( 

1771 _migration_request: MigrationRequest, 

1772 feature_migration: FeatureMigration, 

1773) -> None: 

1774 feature_migration.tagline = "Check for dh-sequence-addons and Build-Depends" 

1775 feature_migration.warn( 

1776 "TODO: Not implemented: Please remove any dh-sequence Build-Dependency" 

1777 ) 

1778 feature_migration.warn( 

1779 "TODO: Not implemented: Please ensure there is a Build-Dependency on `debputy (>= 0.1.45~)" 

1780 ) 

1781 feature_migration.warn( 

1782 "TODO: Not implemented: Please ensure there is a Build-Dependency on `dpkg-dev (>= 1.22.7~)" 

1783 ) 

1784 

1785 

1786def detect_dh_addons_with_zz_integration( 

1787 migration_request: MigrationRequest, 

1788 feature_migration: FeatureMigration, 

1789) -> None: 

1790 feature_migration.tagline = "Check for dh-sequence-addons" 

1791 r = read_dh_addon_sequences(migration_request.debian_dir) 

1792 acceptable_migration_issues = migration_request.acceptable_migration_issues 

1793 if r is None: 

1794 feature_migration.warn( 

1795 "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon" 

1796 " could not be performed. Please ensure the package will Build-Depend on dh-sequence-zz-debputy" 

1797 " and not rely on any other debhelper sequence addons except those debputy explicitly supports." 

1798 ) 

1799 return 

1800 

1801 assert migration_request.migration_target != INTEGRATION_MODE_FULL 

1802 

1803 bd_sequences, dr_sequences, _ = r 

1804 

1805 remaining_sequences = bd_sequences | dr_sequences 

1806 saw_dh_debputy = ( 

1807 "debputy" in remaining_sequences or "zz-debputy" in remaining_sequences 

1808 ) 

1809 saw_zz_debputy = "zz-debputy" in remaining_sequences 

1810 must_use_zz_debputy = False 

1811 remaining_sequences -= SUPPORTED_DH_ADDONS_WITH_ZZ_DEBPUTY 

1812 for sequence in remaining_sequences & DH_ADDONS_TO_PLUGINS.keys(): 

1813 migration = DH_ADDONS_TO_PLUGINS[sequence] 

1814 feature_migration.require_plugin(migration.debputy_plugin) 

1815 if migration.remove_dh_sequence: 1815 ↛ 1816line 1815 didn't jump to line 1816 because the condition on line 1815 was never true

1816 if migration.must_use_zz_debputy: 

1817 must_use_zz_debputy = True 

1818 if sequence in bd_sequences: 

1819 feature_migration.warn( 

1820 f"TODO: MANUAL MIGRATION - Remove build-dependency on dh-sequence-{sequence}" 

1821 f" (replaced by debputy-plugin-{migration.debputy_plugin})" 

1822 ) 

1823 else: 

1824 feature_migration.warn( 

1825 f"TODO: MANUAL MIGRATION - Remove --with {sequence} from dh in d/rules" 

1826 f" (replaced by debputy-plugin-{migration.debputy_plugin})" 

1827 ) 

1828 

1829 remaining_sequences -= DH_ADDONS_TO_PLUGINS.keys() 

1830 

1831 alt_key = "unsupported-dh-sequences" 

1832 for sequence in remaining_sequences & DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY: 1832 ↛ 1833line 1832 didn't jump to line 1833 because the loop on line 1832 never started

1833 if sequence in bd_sequences: 

1834 feature_migration.warn( 

1835 f"TODO: MANUAL MIGRATION - Remove build dependency on dh-sequence-{sequence}" 

1836 ) 

1837 else: 

1838 feature_migration.warn( 

1839 f"TODO: MANUAL MIGRATION - Remove --with {sequence} from dh in d/rules" 

1840 ) 

1841 

1842 remaining_sequences -= DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY 

1843 

1844 for sequence in remaining_sequences: 

1845 key = f"unsupported-dh-sequence-{sequence}" 

1846 msg = f'The dh addon "{sequence}" is not known to work with dh-debputy and might malfunction' 

1847 if ( 

1848 key not in acceptable_migration_issues 

1849 and alt_key not in acceptable_migration_issues 

1850 ): 

1851 raise UnsupportedFeature(msg, [key, alt_key]) 

1852 feature_migration.warn(msg) 

1853 

1854 if not saw_dh_debputy: 

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

1856 elif must_use_zz_debputy and not saw_zz_debputy: 1856 ↛ 1857line 1856 didn't jump to line 1857 because the condition on line 1856 was never true

1857 feature_migration.warn( 

1858 "Please use the zz-debputy sequence rather than the debputy (needed due to dh add-on load order)" 

1859 ) 

1860 

1861 

1862def _rename_file_if_exists( 

1863 debian_dir: VirtualPath, 

1864 source: str, 

1865 dest: str, 

1866 feature_migration: FeatureMigration, 

1867) -> None: 

1868 source_path = debian_dir.get(source) 

1869 dest_path = debian_dir.get(dest) 

1870 spath = ( 

1871 source_path.path 

1872 if source_path is not None 

1873 else os.path.join(debian_dir.path, source) 

1874 ) 

1875 dpath = ( 

1876 dest_path.path if dest_path is not None else os.path.join(debian_dir.path, dest) 

1877 ) 

1878 if source_path is not None and source_path.is_file: 

1879 if dest_path is not None: 

1880 if not dest_path.is_file: 

1881 feature_migration.warnings.append( 

1882 f'TODO: MANUAL MIGRATION - there is a "{spath}" (file) and "{dpath}" (not a file).' 

1883 f' The migration wanted to replace "{spath}" with "{dpath}", but since "{dpath}" is not' 

1884 " a file, this step is left as a manual migration." 

1885 ) 

1886 return 

1887 if ( 

1888 subprocess.call(["cmp", "-s", source_path.fs_path, dest_path.fs_path]) 

1889 != 0 

1890 ): 

1891 feature_migration.warnings.append( 

1892 f'TODO: MANUAL MIGRATION - there is a "{source_path.path}" and "{dest_path.path}"' 

1893 f" file. Normally these files are for the same package and there would only be one of" 

1894 f" them. In this case, they both exist but their content differs. Be advised that" 

1895 f' debputy tool will use the "{dest_path.path}".' 

1896 ) 

1897 else: 

1898 feature_migration.remove_on_success(dest_path.fs_path) 

1899 else: 

1900 feature_migration.rename_on_success( 

1901 source_path.fs_path, 

1902 os.path.join(debian_dir.fs_path, dest), 

1903 ) 

1904 elif source_path is not None: 1904 ↛ exitline 1904 didn't return from function '_rename_file_if_exists' because the condition on line 1904 was always true

1905 feature_migration.warnings.append( 

1906 f'TODO: MANUAL MIGRATION - The migration would normally have renamed "{spath}" to "{dpath}".' 

1907 f' However, the migration assumed "{spath}" would be a file and it is not. Therefore, this step' 

1908 " as a manual migration." 

1909 ) 

1910 

1911 

1912def _find_dh_config_file_for_any_pkg( 

1913 migration_request: MigrationRequest, 

1914 unsupported_config: UnsupportedDHConfig, 

1915) -> Iterable[VirtualPath]: 

1916 for dctrl_bin in migration_request.all_packages: 

1917 dh_config_file = dhe_pkgfile( 

1918 migration_request.debian_dir, 

1919 dctrl_bin, 

1920 unsupported_config.dh_config_basename, 

1921 bug_950723_prefix_matching=unsupported_config.bug_950723_prefix_matching, 

1922 ) 

1923 if dh_config_file is not None: 

1924 yield dh_config_file 

1925 

1926 

1927def _unsupported_debhelper_config_file( 

1928 migration_request: MigrationRequest, 

1929 unsupported_config: UnsupportedDHConfig, 

1930 feature_migration: FeatureMigration, 

1931) -> None: 

1932 dh_config_files = list( 

1933 _find_dh_config_file_for_any_pkg(migration_request, unsupported_config) 

1934 ) 

1935 if not dh_config_files: 

1936 return 

1937 dh_tool = unsupported_config.dh_tool 

1938 basename = unsupported_config.dh_config_basename 

1939 file_stem = ( 

1940 f"@{basename}" if unsupported_config.bug_950723_prefix_matching else basename 

1941 ) 

1942 dh_config_file = dh_config_files[0] 

1943 if unsupported_config.is_missing_migration: 

1944 feature_migration.warn( 

1945 f'Missing migration support for the "{dh_config_file.path}" debhelper config file' 

1946 f" (used by {dh_tool}). Manual migration may be feasible depending on the exact features" 

1947 " required." 

1948 ) 

1949 return 

1950 primary_key = f"unsupported-dh-config-file-{file_stem}" 

1951 secondary_key = "any-unsupported-dh-config-file" 

1952 if ( 

1953 primary_key not in migration_request.acceptable_migration_issues 

1954 and secondary_key not in migration_request.acceptable_migration_issues 

1955 ): 

1956 msg = ( 

1957 f'The "{dh_config_file.path}" debhelper config file (used by {dh_tool} is currently not' 

1958 " supported by debputy." 

1959 ) 

1960 raise UnsupportedFeature( 

1961 msg, 

1962 [primary_key, secondary_key], 

1963 ) 

1964 for dh_config_file in dh_config_files: 

1965 feature_migration.warn( 

1966 f'TODO: MANUAL MIGRATION - Use of unsupported "{dh_config_file.path}" file (used by {dh_tool})' 

1967 )