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

742 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +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) 

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) 

58 

59 

60class ContainsEverything: 

61 

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

63 return True 

64 

65 

66# Align with debputy.py 

67DH_COMMANDS_REPLACED: Mapping[DebputyIntegrationMode, Container[str]] = { 

68 INTEGRATION_MODE_DH_DEBPUTY_RRR: frozenset( 

69 { 

70 "dh_fixperms", 

71 "dh_shlibdeps", 

72 "dh_gencontrol", 

73 "dh_md5sums", 

74 "dh_builddeb", 

75 } 

76 ), 

77 INTEGRATION_MODE_DH_DEBPUTY: frozenset( 

78 { 

79 "dh_install", 

80 "dh_installdocs", 

81 "dh_installchangelogs", 

82 "dh_installexamples", 

83 "dh_installman", 

84 "dh_installcatalogs", 

85 "dh_installcron", 

86 "dh_installdebconf", 

87 "dh_installemacsen", 

88 "dh_installifupdown", 

89 "dh_installinfo", 

90 "dh_installinit", 

91 "dh_installsysusers", 

92 "dh_installtmpfiles", 

93 "dh_installsystemd", 

94 "dh_installsystemduser", 

95 "dh_installmenu", 

96 "dh_installmime", 

97 "dh_installmodules", 

98 "dh_installlogcheck", 

99 "dh_installlogrotate", 

100 "dh_installpam", 

101 "dh_installppp", 

102 "dh_installudev", 

103 "dh_installgsettings", 

104 "dh_installinitramfs", 

105 "dh_installalternatives", 

106 "dh_bugfiles", 

107 "dh_ucf", 

108 "dh_lintian", 

109 "dh_icons", 

110 "dh_usrlocal", 

111 "dh_perl", 

112 "dh_link", 

113 "dh_installwm", 

114 "dh_installxfonts", 

115 "dh_strip_nondeterminism", 

116 "dh_compress", 

117 "dh_fixperms", 

118 "dh_dwz", 

119 "dh_strip", 

120 "dh_makeshlibs", 

121 "dh_shlibdeps", 

122 "dh_missing", 

123 "dh_installdeb", 

124 "dh_gencontrol", 

125 "dh_md5sums", 

126 "dh_builddeb", 

127 } 

128 ), 

129 INTEGRATION_MODE_FULL: ContainsEverything(), 

130} 

131 

132_GS_DOC = f"{DEBPUTY_DOC_ROOT_DIR}/GETTING-STARTED-WITH-dh-debputy.md" 

133MIGRATION_AID_FOR_OVERRIDDEN_COMMANDS = { 

134 "dh_installinit": f"{_GS_DOC}#covert-your-overrides-for-dh_installsystemd-dh_installinit-if-any", 

135 "dh_installsystemd": f"{_GS_DOC}#covert-your-overrides-for-dh_installsystemd-dh_installinit-if-any", 

136 "dh_fixperms": f"{_GS_DOC}#convert-your-overrides-or-excludes-for-dh_fixperms-if-any", 

137 "dh_gencontrol": f"{_GS_DOC}#convert-your-overrides-for-dh_gencontrol-if-any", 

138} 

139 

140 

141@dataclasses.dataclass(frozen=True, slots=True) 

142class UnsupportedDHConfig: 

143 dh_config_basename: str 

144 dh_tool: str 

145 bug_950723_prefix_matching: bool = False 

146 is_missing_migration: bool = False 

147 

148 

149@dataclasses.dataclass(frozen=True, slots=True) 

150class DHSequenceMigration: 

151 debputy_plugin: str 

152 remove_dh_sequence: bool = True 

153 must_use_zz_debputy: bool = False 

154 

155 

156UNSUPPORTED_DH_CONFIGS_AND_TOOLS_FOR_ZZ_DEBPUTY = [ 

157 UnsupportedDHConfig("config", "dh_installdebconf"), 

158 UnsupportedDHConfig("templates", "dh_installdebconf"), 

159 UnsupportedDHConfig("emacsen-compat", "dh_installemacsen"), 

160 UnsupportedDHConfig("emacsen-install", "dh_installemacsen"), 

161 UnsupportedDHConfig("emacsen-remove", "dh_installemacsen"), 

162 UnsupportedDHConfig("emacsen-startup", "dh_installemacsen"), 

163 # The `upstart` file should be long dead, but we might as well detect it. 

164 UnsupportedDHConfig("upstart", "dh_installinit"), 

165 # dh_installsystemduser 

166 UnsupportedDHConfig( 

167 "user.path", "dh_installsystemduser", bug_950723_prefix_matching=False 

168 ), 

169 UnsupportedDHConfig( 

170 "user.path", "dh_installsystemduser", bug_950723_prefix_matching=True 

171 ), 

172 UnsupportedDHConfig( 

173 "user.service", "dh_installsystemduser", bug_950723_prefix_matching=False 

174 ), 

175 UnsupportedDHConfig( 

176 "user.service", "dh_installsystemduser", bug_950723_prefix_matching=True 

177 ), 

178 UnsupportedDHConfig( 

179 "user.socket", "dh_installsystemduser", bug_950723_prefix_matching=False 

180 ), 

181 UnsupportedDHConfig( 

182 "user.socket", "dh_installsystemduser", bug_950723_prefix_matching=True 

183 ), 

184 UnsupportedDHConfig( 

185 "user.target", "dh_installsystemduser", bug_950723_prefix_matching=False 

186 ), 

187 UnsupportedDHConfig( 

188 "user.target", "dh_installsystemduser", bug_950723_prefix_matching=True 

189 ), 

190 UnsupportedDHConfig( 

191 "user.timer", "dh_installsystemduser", bug_950723_prefix_matching=False 

192 ), 

193 UnsupportedDHConfig( 

194 "user.timer", "dh_installsystemduser", bug_950723_prefix_matching=True 

195 ), 

196 UnsupportedDHConfig("menu", "dh_installmenu"), 

197 UnsupportedDHConfig("menu-method", "dh_installmenu"), 

198 UnsupportedDHConfig("ucf", "dh_ucf"), 

199 UnsupportedDHConfig("wm", "dh_installwm"), 

200 UnsupportedDHConfig("triggers", "dh_installdeb"), 

201 UnsupportedDHConfig("postinst", "dh_installdeb"), 

202 UnsupportedDHConfig("postrm", "dh_installdeb"), 

203 UnsupportedDHConfig("preinst", "dh_installdeb"), 

204 UnsupportedDHConfig("prerm", "dh_installdeb"), 

205 UnsupportedDHConfig("menutest", "dh_installdeb"), 

206 UnsupportedDHConfig("isinstallable", "dh_installdeb"), 

207] 

208SUPPORTED_DH_ADDONS_WITH_ZZ_DEBPUTY = frozenset( 

209 { 

210 # debputy's own 

211 "debputy", 

212 "zz-debputy", 

213 # debhelper provided sequences that should work. 

214 "single-binary", 

215 } 

216) 

217DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY = frozenset( 

218 [ 

219 # The `zz-debputy` add-on replaces the `zz-debputy-rrr` plugin. 

220 "zz-debputy-rrr", 

221 # Sequences debputy directly replaces 

222 "dwz", 

223 "elf-tools", 

224 "installinitramfs", 

225 "installsysusers", 

226 "doxygen", 

227 # Sequences that are embedded fully into debputy 

228 "bash-completion", 

229 "shell-completions", 

230 "sodeps", 

231 ] 

232) 

233DH_ADDONS_TO_PLUGINS = { 

234 "gnome": DHSequenceMigration( 

235 "gnome", 

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

237 remove_dh_sequence=False, 

238 must_use_zz_debputy=True, 

239 ), 

240 "grantlee": DHSequenceMigration( 

241 "grantlee", 

242 remove_dh_sequence=True, 

243 must_use_zz_debputy=True, 

244 ), 

245 "numpy3": DHSequenceMigration( 

246 "numpy3", 

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

248 remove_dh_sequence=False, 

249 must_use_zz_debputy=True, 

250 ), 

251 "perl-openssl": DHSequenceMigration( 

252 "perl-openssl", 

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

254 remove_dh_sequence=False, 

255 must_use_zz_debputy=True, 

256 ), 

257} 

258 

259 

260def _dh_config_file( 

261 debian_dir: VirtualPath, 

262 dctrl_bin: BinaryPackage, 

263 basename: str, 

264 helper_name: str, 

265 acceptable_migration_issues: AcceptableMigrationIssues, 

266 feature_migration: FeatureMigration, 

267 manifest: HighLevelManifest, 

268 support_executable_files: bool = False, 

269 allow_dh_exec_rename: bool = False, 

270 pkgfile_lookup: bool = True, 

271 remove_on_migration: bool = True, 

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

273 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

274 dh_config_file = ( 

275 dhe_pkgfile(debian_dir, dctrl_bin, basename) 

276 if pkgfile_lookup 

277 else debian_dir.get(basename) 

278 ) 

279 if dh_config_file is None or dh_config_file.is_dir: 

280 return None, None 

281 if dh_config_file.is_executable and not support_executable_files: 

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

283 if ( 

284 primary_key in acceptable_migration_issues 

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

286 ): 

287 feature_migration.warn( 

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

289 ) 

290 return None, None 

291 raise UnsupportedFeature( 

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

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

294 ) 

295 

296 if remove_on_migration: 

297 feature_migration.remove_on_success(dh_config_file.fs_path) 

298 substitution = DHMigrationSubstitution( 

299 dpkg_architecture_table(), 

300 acceptable_migration_issues, 

301 feature_migration, 

302 mutable_manifest, 

303 ) 

304 content = dhe_filedoublearray( 

305 dh_config_file, 

306 substitution, 

307 allow_dh_exec_rename=allow_dh_exec_rename, 

308 ) 

309 return dh_config_file, content 

310 

311 

312def _validate_rm_mv_conffile( 

313 package: str, 

314 config_line: DHConfigFileLine, 

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

316 cmd, *args = config_line.tokens 

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

318 raise ValueError( 

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

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

321 ) 

322 if cmd == "rm_conffile": 

323 min_args = 1 

324 max_args = 3 

325 else: 

326 min_args = 2 

327 max_args = 4 

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

329 raise ValueError( 

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

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

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

333 ) 

334 

335 obsolete_conffile = args[0] 

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

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

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

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

340 raise ValueError( 

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

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

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

344 ) 

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

346 raise ValueError( 

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

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

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

350 ) 

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

352 raise ValueError( 

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

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

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

356 f" {config_line.original_line}" 

357 ) 

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

359 raise ValueError( 

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

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

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

363 ) 

364 return cmd, obsolete_conffile, new_conffile, prior_version, owning_package 

365 

366 

367_BASH_COMPLETION_RE = re.compile( 

368 r""" 

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

370 | \$\(.*\) 

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

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

373""", 

374 re.VERBOSE, 

375) 

376 

377 

378def migrate_bash_completion( 

379 debian_dir: VirtualPath, 

380 manifest: HighLevelManifest, 

381 acceptable_migration_issues: AcceptableMigrationIssues, 

382 feature_migration: FeatureMigration, 

383 _migration_target: DebputyIntegrationMode | None, 

384) -> None: 

385 feature_migration.tagline = "dh_bash-completion files" 

386 is_single_binary = sum(1 for _ in manifest.all_packages) == 1 

387 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

388 installations = mutable_manifest.installations(create_if_absent=False) 

389 

390 for dctrl_bin in manifest.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 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 debian_dir: VirtualPath, 

474 manifest: HighLevelManifest, 

475 acceptable_migration_issues: AcceptableMigrationIssues, 

476 feature_migration: FeatureMigration, 

477 _migration_target: DebputyIntegrationMode | None, 

478) -> None: 

479 feature_migration.tagline = "dh_shell_completions files" 

480 is_single_binary = sum(1 for _ in manifest.all_packages) == 1 

481 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

482 installations = mutable_manifest.installations(create_if_absent=False) 

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

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

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

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

487 

488 for completion, dctrl_bin in product(completions, manifest.all_packages): 

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

490 if dh_file is None: 

491 continue 

492 is_completion_file = False 

493 with dh_file.open() as fd: 

494 for line in fd: 

495 line = line.strip() 

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

497 continue 

498 if _SHELL_COMPLETIONS_RE.search(line): 

499 is_completion_file = True 

500 break 

501 if is_completion_file: 

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

503 feature_migration.rename_on_success(dh_file.fs_path, dest_path) 

504 continue 

505 

506 _, content = _dh_config_file( 

507 debian_dir, 

508 dctrl_bin, 

509 f"{completion}-completions", 

510 "dh_shell_completions", 

511 acceptable_migration_issues, 

512 feature_migration, 

513 manifest, 

514 remove_on_migration=True, 

515 ) 

516 

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

518 install_dest_sources: list[str] = [] 

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

520 for dhe_line in content: 

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

522 raise UnsupportedFeature( 

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

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

525 ) 

526 source = dhe_line.tokens[0] 

527 dest_basename = ( 

528 dhe_line.tokens[1] 

529 if len(dhe_line.tokens) > 1 

530 else os.path.basename(source) 

531 ) 

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

533 if dctrl_bin.name != dest_basename: 

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

535 else: 

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

537 feature_migration.rename_on_success(source, dest_path) 

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

539 install_dest_sources.append(source) 

540 else: 

541 install_as_rules.append((source, dest_basename)) 

542 

543 completion_dir_variable = ( 

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

545 ) 

546 

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

548 sources: list[str] | str = ( 

549 install_dest_sources 

550 if len(install_dest_sources) > 1 

551 else install_dest_sources[0] 

552 ) 

553 installations.append( 

554 AbstractMutableYAMLInstallRule.install_dest( 

555 sources=sources, 

556 dest_dir=completion_dir_variable, 

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

558 ) 

559 ) 

560 

561 for source, dest_basename in install_as_rules: 

562 installations.append( 

563 AbstractMutableYAMLInstallRule.install_as( 

564 source=source, 

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

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

567 ) 

568 ) 

569 

570 

571def migrate_dh_installsystemd_files( 

572 debian_dir: VirtualPath, 

573 manifest: HighLevelManifest, 

574 _acceptable_migration_issues: AcceptableMigrationIssues, 

575 feature_migration: FeatureMigration, 

576 _migration_target: DebputyIntegrationMode | None, 

577) -> None: 

578 feature_migration.tagline = "dh_installsystemd files" 

579 for dctrl_bin in manifest.all_packages: 

580 for stem in [ 

581 "path", 

582 "service", 

583 "socket", 

584 "target", 

585 "timer", 

586 ]: 

587 pkgfile = dhe_pkgfile( 

588 debian_dir, dctrl_bin, stem, bug_950723_prefix_matching=True 

589 ) 

590 if not pkgfile: 

591 continue 

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

593 raise UnsupportedFeature( 

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

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

596 ) 

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

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

599 feature_migration.rename_on_success( 

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

601 ) 

602 

603 

604def migrate_clean_file( 

605 debian_dir: VirtualPath, 

606 manifest: HighLevelManifest, 

607 acceptable_migration_issues: AcceptableMigrationIssues, 

608 feature_migration: FeatureMigration, 

609 _migration_target: DebputyIntegrationMode | None, 

610) -> None: 

611 feature_migration.tagline = "debian/clean" 

612 clean_file = debian_dir.get("clean") 

613 if clean_file is None: 

614 return 

615 

616 substitution = DHMigrationSubstitution( 

617 dpkg_architecture_table(), 

618 acceptable_migration_issues, 

619 feature_migration, 

620 manifest.mutable_manifest, 

621 ) 

622 content = dhe_filedoublearray( 

623 clean_file, 

624 substitution, 

625 ) 

626 

627 remove_during_clean_rules = manifest.mutable_manifest.remove_during_clean( 

628 create_if_absent=False 

629 ) 

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

631 rules_before = len(remove_during_clean_rules) 

632 remove_during_clean_rules.extend(tokens) 

633 rules_after = len(remove_during_clean_rules) 

634 feature_migration.successful_manifest_changes += rules_after - rules_before 

635 feature_migration.remove_on_success(clean_file.fs_path) 

636 

637 

638def migrate_maintscript( 

639 debian_dir: VirtualPath, 

640 manifest: HighLevelManifest, 

641 acceptable_migration_issues: AcceptableMigrationIssues, 

642 feature_migration: FeatureMigration, 

643 _migration_target: DebputyIntegrationMode | None, 

644) -> None: 

645 feature_migration.tagline = "dh_installdeb files" 

646 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

647 for dctrl_bin in manifest.all_packages: 

648 mainscript_file, content = _dh_config_file( 

649 debian_dir, 

650 dctrl_bin, 

651 "maintscript", 

652 "dh_installdeb", 

653 acceptable_migration_issues, 

654 feature_migration, 

655 manifest, 

656 ) 

657 

658 if mainscript_file is None: 

659 continue 

660 assert content is not None 

661 

662 package_definition = mutable_manifest.package(dctrl_bin.name) 

663 conffiles = { 

664 it.obsolete_conffile: it 

665 for it in package_definition.conffile_management_items() 

666 } 

667 seen_conffiles = set() 

668 

669 for dhe_line in content: 

670 cmd = dhe_line.tokens[0] 

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

672 raise UnsupportedFeature( 

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

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

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

676 ) 

677 

678 try: 

679 ( 

680 _, 

681 obsolete_conffile, 

682 new_conffile, 

683 prior_to_version, 

684 owning_package, 

685 ) = _validate_rm_mv_conffile(dctrl_bin.name, dhe_line) 

686 except ValueError as e: 

687 _error( 

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

689 ) 

690 

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

692 raise ConflictingChange( 

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

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

695 ) 

696 seen_conffiles.add(obsolete_conffile) 

697 

698 if cmd == "rm_conffile": 

699 item = MutableYAMLConffileManagementItem.rm_conffile( 

700 obsolete_conffile, 

701 prior_to_version, 

702 owning_package, 

703 ) 

704 else: 

705 assert cmd == "mv_conffile" 

706 item = MutableYAMLConffileManagementItem.mv_conffile( 

707 obsolete_conffile, 

708 assume_not_none(new_conffile), 

709 prior_to_version, 

710 owning_package, 

711 ) 

712 

713 existing_def = conffiles.get(item.obsolete_conffile) 

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

715 if not ( 

716 item.command == existing_def.command 

717 and item.new_conffile == existing_def.new_conffile 

718 and item.prior_to_version == existing_def.prior_to_version 

719 and item.owning_package == existing_def.owning_package 

720 ): 

721 raise ConflictingChange( 

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

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

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

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

726 ) 

727 continue 

728 

729 package_definition.add_conffile_management(item) 

730 feature_migration.successful_manifest_changes += 1 

731 

732 

733@dataclasses.dataclass(slots=True) 

734class SourcesAndConditional: 

735 dest_dir: str | None = None 

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

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

738 

739 

740def _raise_on_unsupported_path( 

741 p: str, 

742 path: VirtualPath, 

743 line_no: int, 

744) -> str: 

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

746 _error( 

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

748 ) 

749 return p 

750 

751 

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

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

754 return p[11:] 

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

756 pruned = p[3:] 

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

758 _raise_on_unsupported_path(p, path, line_no) 

759 _error( 

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

761 ) 

762 return f"debian/{pruned}" 

763 

764 return p 

765 

766 

767def migrate_install_file( 

768 debian_dir: VirtualPath, 

769 manifest: HighLevelManifest, 

770 acceptable_migration_issues: AcceptableMigrationIssues, 

771 feature_migration: FeatureMigration, 

772 _migration_target: DebputyIntegrationMode | None, 

773) -> None: 

774 feature_migration.tagline = "dh_install config files" 

775 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

776 installations = mutable_manifest.installations(create_if_absent=False) 

777 priority_lines = [] 

778 remaining_install_lines = [] 

779 warn_about_fixmes_in_dest_dir = False 

780 

781 is_single_binary = sum(1 for _ in manifest.all_packages) == 1 

782 

783 for dctrl_bin in manifest.all_packages: 

784 install_file, content = _dh_config_file( 

785 debian_dir, 

786 dctrl_bin, 

787 "install", 

788 "dh_install", 

789 acceptable_migration_issues, 

790 feature_migration, 

791 manifest, 

792 support_executable_files=True, 

793 allow_dh_exec_rename=True, 

794 ) 

795 if not install_file or not content: 

796 continue 

797 current_sources = [] 

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

799 {} 

800 ) 

801 install_as_rules = [] 

802 multi_dest = collections.defaultdict(list) 

803 seen_sources = set() 

804 multi_dest_sources: set[str] = set() 

805 

806 for dhe_line in content: 

807 special_rule = None 

808 if "=>" in dhe_line.tokens: 

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

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

811 path = _strip_d_tmp( 

812 _normalize_path( 

813 dhe_line.tokens[1], 

814 with_prefix=False, 

815 allow_and_keep_upward_segments=True, 

816 ), 

817 dhe_line.config_file, 

818 dhe_line.line_no, 

819 ) 

820 special_rule = AbstractMutableYAMLInstallRule.install_dest( 

821 path, 

822 dctrl_bin.name if not is_single_binary else None, 

823 dest_dir=None, 

824 when=dhe_line.conditional(), 

825 ) 

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

827 _error( 

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

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

830 ) 

831 else: 

832 install_rule = AbstractMutableYAMLInstallRule.install_as( 

833 _strip_d_tmp( 

834 _normalize_path( 

835 dhe_line.tokens[0], 

836 with_prefix=False, 

837 allow_and_keep_upward_segments=True, 

838 ), 

839 dhe_line.config_file, 

840 dhe_line.line_no, 

841 ), 

842 _raise_on_unsupported_path( 

843 _normalize_path( 

844 dhe_line.tokens[2], 

845 with_prefix=False, 

846 allow_and_keep_upward_segments=True, 

847 ), 

848 dhe_line.config_file, 

849 dhe_line.line_no, 

850 ), 

851 dctrl_bin.name if not is_single_binary else None, 

852 when=dhe_line.conditional(), 

853 ) 

854 install_as_rules.append(install_rule) 

855 else: 

856 if len(dhe_line.tokens) > 1: 

857 sources = list( 

858 _strip_d_tmp( 

859 _normalize_path( 

860 w, 

861 with_prefix=False, 

862 allow_and_keep_upward_segments=True, 

863 ), 

864 dhe_line.config_file, 

865 dhe_line.line_no, 

866 ) 

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

868 ) 

869 dest_dir = _raise_on_unsupported_path( 

870 _normalize_path( 

871 dhe_line.tokens[-1], 

872 with_prefix=False, 

873 allow_and_keep_upward_segments=True, 

874 ), 

875 dhe_line.config_file, 

876 dhe_line.line_no, 

877 ) 

878 else: 

879 sources = list( 

880 _strip_d_tmp( 

881 _normalize_path( 

882 w, 

883 with_prefix=False, 

884 allow_and_keep_upward_segments=True, 

885 ), 

886 dhe_line.config_file, 

887 dhe_line.line_no, 

888 ) 

889 for w in dhe_line.tokens 

890 ) 

891 dest_dir = None 

892 

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

894 seen_sources.update(sources) 

895 

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

897 current_sources.extend(sources) 

898 continue 

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

900 ctor = functools.partial( 

901 SourcesAndConditional, 

902 dest_dir=dest_dir, 

903 conditional=dhe_line.conditional(), 

904 ) 

905 md = _fetch_or_create( 

906 sources_by_destdir, 

907 key, 

908 ctor, 

909 ) 

910 md.sources.extend(sources) 

911 

912 if special_rule: 

913 priority_lines.append(special_rule) 

914 

915 remaining_install_lines.extend(install_as_rules) 

916 

917 for md in sources_by_destdir.values(): 

918 if multi_dest_sources: 

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

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

921 for s in already_installed: 

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

923 multi_dest[s].append(md) 

924 if not sources: 

925 continue 

926 else: 

927 sources = md.sources 

928 install_rule = AbstractMutableYAMLInstallRule.install_dest( 

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

930 dctrl_bin.name if not is_single_binary else None, 

931 dest_dir=md.dest_dir, 

932 when=md.conditional, 

933 ) 

934 remaining_install_lines.append(install_rule) 

935 

936 if current_sources: 

937 if multi_dest_sources: 

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

939 already_installed = ( 

940 s for s in current_sources if s in multi_dest_sources 

941 ) 

942 for s in already_installed: 

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

944 dest_dir = os.path.dirname(s) 

945 if has_glob_magic(dest_dir): 

946 warn_about_fixmes_in_dest_dir = True 

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

948 multi_dest[s].append( 

949 SourcesAndConditional( 

950 dest_dir=dest_dir, 

951 conditional=None, 

952 ) 

953 ) 

954 else: 

955 sources = current_sources 

956 

957 if sources: 

958 install_rule = AbstractMutableYAMLInstallRule.install_dest( 

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

960 dctrl_bin.name if not is_single_binary else None, 

961 dest_dir=None, 

962 ) 

963 remaining_install_lines.append(install_rule) 

964 

965 if multi_dest: 

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

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

968 # We assume the conditional is the same. 

969 conditional = next( 

970 iter( 

971 dac.conditional 

972 for dac in dest_and_conditionals 

973 if dac.conditional is not None 

974 ), 

975 None, 

976 ) 

977 remaining_install_lines.append( 

978 AbstractMutableYAMLInstallRule.multi_dest_install( 

979 source, 

980 dest_dirs, 

981 dctrl_bin.name if not is_single_binary else None, 

982 when=conditional, 

983 ) 

984 ) 

985 

986 if priority_lines: 

987 installations.extend(priority_lines) 

988 

989 if remaining_install_lines: 

990 installations.extend(remaining_install_lines) 

991 

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

993 remaining_install_lines 

994 ) 

995 if warn_about_fixmes_in_dest_dir: 

996 feature_migration.warn( 

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

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

999 ) 

1000 

1001 

1002def migrate_installdocs_file( 

1003 debian_dir: VirtualPath, 

1004 manifest: HighLevelManifest, 

1005 acceptable_migration_issues: AcceptableMigrationIssues, 

1006 feature_migration: FeatureMigration, 

1007 _migration_target: DebputyIntegrationMode | None, 

1008) -> None: 

1009 feature_migration.tagline = "dh_installdocs config files" 

1010 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1011 installations = mutable_manifest.installations(create_if_absent=False) 

1012 

1013 is_single_binary = sum(1 for _ in manifest.all_packages) == 1 

1014 

1015 for dctrl_bin in manifest.all_packages: 

1016 install_file, content = _dh_config_file( 

1017 debian_dir, 

1018 dctrl_bin, 

1019 "docs", 

1020 "dh_installdocs", 

1021 acceptable_migration_issues, 

1022 feature_migration, 

1023 manifest, 

1024 support_executable_files=True, 

1025 ) 

1026 if not install_file: 

1027 continue 

1028 assert content is not None 

1029 docs: list[str] = [] 

1030 for dhe_line in content: 

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

1032 _error( 

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

1034 " Missing support for conditions." 

1035 ) 

1036 docs.extend( 

1037 _raise_on_unsupported_path( 

1038 _normalize_path( 

1039 w, with_prefix=False, allow_and_keep_upward_segments=True 

1040 ), 

1041 dhe_line.config_file, 

1042 dhe_line.line_no, 

1043 ) 

1044 for w in dhe_line.tokens 

1045 ) 

1046 

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

1048 continue 

1049 feature_migration.successful_manifest_changes += 1 

1050 install_rule = AbstractMutableYAMLInstallRule.install_docs( 

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

1052 dctrl_bin.name if not is_single_binary else None, 

1053 ) 

1054 installations.create_definition_if_missing() 

1055 installations.append(install_rule) 

1056 

1057 

1058def migrate_installexamples_file( 

1059 debian_dir: VirtualPath, 

1060 manifest: HighLevelManifest, 

1061 acceptable_migration_issues: AcceptableMigrationIssues, 

1062 feature_migration: FeatureMigration, 

1063 _migration_target: DebputyIntegrationMode | None, 

1064) -> None: 

1065 feature_migration.tagline = "dh_installexamples config files" 

1066 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1067 installations = mutable_manifest.installations(create_if_absent=False) 

1068 is_single_binary = sum(1 for _ in manifest.all_packages) == 1 

1069 

1070 for dctrl_bin in manifest.all_packages: 

1071 install_file, content = _dh_config_file( 

1072 debian_dir, 

1073 dctrl_bin, 

1074 "examples", 

1075 "dh_installexamples", 

1076 acceptable_migration_issues, 

1077 feature_migration, 

1078 manifest, 

1079 support_executable_files=True, 

1080 ) 

1081 if not install_file: 

1082 continue 

1083 assert content is not None 

1084 examples: list[str] = [] 

1085 for dhe_line in content: 

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

1087 _error( 

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

1089 " Missing support for conditions." 

1090 ) 

1091 examples.extend( 

1092 _raise_on_unsupported_path( 

1093 _normalize_path( 

1094 w, with_prefix=False, allow_and_keep_upward_segments=True 

1095 ), 

1096 dhe_line.config_file, 

1097 dhe_line.line_no, 

1098 ) 

1099 for w in dhe_line.tokens 

1100 ) 

1101 

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

1103 continue 

1104 feature_migration.successful_manifest_changes += 1 

1105 install_rule = AbstractMutableYAMLInstallRule.install_examples( 

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

1107 dctrl_bin.name if not is_single_binary else None, 

1108 ) 

1109 installations.create_definition_if_missing() 

1110 installations.append(install_rule) 

1111 

1112 

1113@dataclasses.dataclass(slots=True) 

1114class InfoFilesDefinition: 

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

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

1117 

1118 

1119def migrate_installinfo_file( 

1120 debian_dir: VirtualPath, 

1121 manifest: HighLevelManifest, 

1122 acceptable_migration_issues: AcceptableMigrationIssues, 

1123 feature_migration: FeatureMigration, 

1124 _migration_target: DebputyIntegrationMode | None, 

1125) -> None: 

1126 feature_migration.tagline = "dh_installinfo config files" 

1127 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1128 installations = mutable_manifest.installations(create_if_absent=False) 

1129 is_single_binary = sum(1 for _ in manifest.all_packages) == 1 

1130 

1131 for dctrl_bin in manifest.all_packages: 

1132 info_file, content = _dh_config_file( 

1133 debian_dir, 

1134 dctrl_bin, 

1135 "info", 

1136 "dh_installinfo", 

1137 acceptable_migration_issues, 

1138 feature_migration, 

1139 manifest, 

1140 support_executable_files=True, 

1141 ) 

1142 if not info_file: 

1143 continue 

1144 assert content is not None 

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

1146 for dhe_line in content: 

1147 key = dhe_line.conditional_key() 

1148 ctr = functools.partial( 

1149 InfoFilesDefinition, conditional=dhe_line.conditional() 

1150 ) 

1151 info_def = _fetch_or_create( 

1152 info_files_by_condition, 

1153 key, 

1154 ctr, 

1155 ) 

1156 info_def.sources.extend( 

1157 _raise_on_unsupported_path( 

1158 _normalize_path( 

1159 w, with_prefix=False, allow_and_keep_upward_segments=True 

1160 ), 

1161 dhe_line.config_file, 

1162 dhe_line.line_no, 

1163 ) 

1164 for w in dhe_line.tokens 

1165 ) 

1166 

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

1168 continue 

1169 feature_migration.successful_manifest_changes += 1 

1170 installations.create_definition_if_missing() 

1171 for info_def in info_files_by_condition.values(): 

1172 info_files = info_def.sources 

1173 install_rule = AbstractMutableYAMLInstallRule.install_docs( 

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

1175 dctrl_bin.name if not is_single_binary else None, 

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

1177 when=info_def.conditional, 

1178 ) 

1179 installations.append(install_rule) 

1180 

1181 

1182@dataclasses.dataclass(slots=True) 

1183class ManpageDefinition: 

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

1185 language: str | None = None 

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

1187 

1188 

1189DK = TypeVar("DK") 

1190DV = TypeVar("DV") 

1191 

1192 

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

1194 v = d.get(key) 

1195 if v is None: 

1196 v = factory() 

1197 d[key] = v 

1198 return v 

1199 

1200 

1201def migrate_installman_file( 

1202 debian_dir: VirtualPath, 

1203 manifest: HighLevelManifest, 

1204 acceptable_migration_issues: AcceptableMigrationIssues, 

1205 feature_migration: FeatureMigration, 

1206 _migration_target: DebputyIntegrationMode | None, 

1207) -> None: 

1208 feature_migration.tagline = "dh_installman config files" 

1209 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1210 installations = mutable_manifest.installations(create_if_absent=False) 

1211 is_single_binary = sum(1 for _ in manifest.all_packages) == 1 

1212 warn_about_basename = False 

1213 

1214 for dctrl_bin in manifest.all_packages: 

1215 manpages_file, content = _dh_config_file( 

1216 debian_dir, 

1217 dctrl_bin, 

1218 "manpages", 

1219 "dh_installman", 

1220 acceptable_migration_issues, 

1221 feature_migration, 

1222 manifest, 

1223 support_executable_files=True, 

1224 allow_dh_exec_rename=True, 

1225 ) 

1226 if not manpages_file: 

1227 continue 

1228 assert content is not None 

1229 

1230 vanilla_definitions = [] 

1231 install_as_rules = [] 

1232 complex_definitions: dict[ 

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

1234 ] = {} 

1235 install_rule: AbstractMutableYAMLInstallRule 

1236 for dhe_line in content: 

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

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

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

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

1241 _error( 

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

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

1244 ) 

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

1246 _error( 

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

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

1249 ) 

1250 else: 

1251 install_rule = AbstractMutableYAMLInstallRule.install_doc_as( 

1252 _raise_on_unsupported_path( 

1253 _normalize_path( 

1254 dhe_line.tokens[0], 

1255 with_prefix=False, 

1256 allow_and_keep_upward_segments=True, 

1257 ), 

1258 dhe_line.config_file, 

1259 dhe_line.line_no, 

1260 ), 

1261 _raise_on_unsupported_path( 

1262 _normalize_path( 

1263 dhe_line.tokens[2], 

1264 with_prefix=False, 

1265 allow_and_keep_upward_segments=True, 

1266 ), 

1267 dhe_line.config_file, 

1268 dhe_line.line_no, 

1269 ), 

1270 dctrl_bin.name if not is_single_binary else None, 

1271 when=dhe_line.conditional(), 

1272 ) 

1273 install_as_rules.append(install_rule) 

1274 continue 

1275 

1276 sources = [ 

1277 _raise_on_unsupported_path( 

1278 _normalize_path( 

1279 w, with_prefix=False, allow_and_keep_upward_segments=True 

1280 ), 

1281 dhe_line.config_file, 

1282 dhe_line.line_no, 

1283 ) 

1284 for w in dhe_line.tokens 

1285 ] 

1286 needs_basename = any( 

1287 MAN_GUESS_FROM_BASENAME.search(x) 

1288 and not MAN_GUESS_LANG_FROM_PATH.search(x) 

1289 for x in sources 

1290 ) 

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

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

1293 warn_about_basename = True 

1294 language = "derive-from-basename" 

1295 else: 

1296 language = None 

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

1298 ctor = functools.partial( 

1299 ManpageDefinition, 

1300 language=language, 

1301 conditional=dhe_line.conditional(), 

1302 ) 

1303 manpage_def = _fetch_or_create( 

1304 complex_definitions, 

1305 key, 

1306 ctor, 

1307 ) 

1308 manpage_def.sources.extend(sources) 

1309 else: 

1310 vanilla_definitions.extend(sources) 

1311 

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

1313 continue 

1314 feature_migration.successful_manifest_changes += 1 

1315 installations.create_definition_if_missing() 

1316 installations.extend(install_as_rules) 

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

1318 man_source = ( 

1319 vanilla_definitions 

1320 if len(vanilla_definitions) > 1 

1321 else vanilla_definitions[0] 

1322 ) 

1323 install_rule = AbstractMutableYAMLInstallRule.install_man( 

1324 man_source, 

1325 dctrl_bin.name if not is_single_binary else None, 

1326 None, 

1327 ) 

1328 installations.append(install_rule) 

1329 for manpage_def in complex_definitions.values(): 

1330 sources = manpage_def.sources 

1331 install_rule = AbstractMutableYAMLInstallRule.install_man( 

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

1333 dctrl_bin.name if not is_single_binary else None, 

1334 manpage_def.language, 

1335 when=manpage_def.conditional, 

1336 ) 

1337 installations.append(install_rule) 

1338 

1339 if warn_about_basename: 

1340 feature_migration.warn( 

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

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

1343 ) 

1344 

1345 

1346def migrate_not_installed_file( 

1347 debian_dir: VirtualPath, 

1348 manifest: HighLevelManifest, 

1349 acceptable_migration_issues: AcceptableMigrationIssues, 

1350 feature_migration: FeatureMigration, 

1351 _migration_target: DebputyIntegrationMode | None, 

1352) -> None: 

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

1354 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1355 installations = mutable_manifest.installations(create_if_absent=False) 

1356 main_binary = [p for p in manifest.all_packages if p.is_main_package][0] 

1357 

1358 missing_file, content = _dh_config_file( 

1359 debian_dir, 

1360 main_binary, 

1361 "not-installed", 

1362 "dh_missing", 

1363 acceptable_migration_issues, 

1364 feature_migration, 

1365 manifest, 

1366 support_executable_files=False, 

1367 pkgfile_lookup=False, 

1368 ) 

1369 discard_rules: list[str] = [] 

1370 if missing_file: 

1371 assert content is not None 

1372 for dhe_line in content: 

1373 discard_rules.extend( 

1374 _raise_on_unsupported_path( 

1375 _normalize_path( 

1376 w, with_prefix=False, allow_and_keep_upward_segments=True 

1377 ), 

1378 dhe_line.config_file, 

1379 dhe_line.line_no, 

1380 ) 

1381 for w in dhe_line.tokens 

1382 ) 

1383 

1384 if discard_rules: 

1385 feature_migration.successful_manifest_changes += 1 

1386 install_rule = AbstractMutableYAMLInstallRule.discard( 

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

1388 ) 

1389 installations.create_definition_if_missing() 

1390 installations.append(install_rule) 

1391 

1392 

1393def min_dh_compat_check( 

1394 _debian_dir: VirtualPath, 

1395 manifest: HighLevelManifest, 

1396 _acceptable_migration_issues: AcceptableMigrationIssues, 

1397 feature_migration: FeatureMigration, 

1398 _migration_target: DebputyIntegrationMode | None, 

1399) -> None: 

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

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

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

1403 min_compat = 14 if any(not p.is_arch_all for p in manifest.all_packages) else 12 

1404 feature_migration.assumed_compat = min_compat 

1405 

1406 

1407def detect_pam_files( 

1408 debian_dir: VirtualPath, 

1409 manifest: HighLevelManifest, 

1410 _acceptable_migration_issues: AcceptableMigrationIssues, 

1411 feature_migration: FeatureMigration, 

1412 _migration_target: DebputyIntegrationMode | None, 

1413) -> None: 

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

1415 for dctrl_bin in manifest.all_packages: 

1416 dh_config_file = dhe_pkgfile(debian_dir, dctrl_bin, "pam") 

1417 if dh_config_file is not None: 

1418 feature_migration.assumed_compat = 14 

1419 break 

1420 

1421 

1422def migrate_tmpfile( 

1423 debian_dir: VirtualPath, 

1424 manifest: HighLevelManifest, 

1425 _acceptable_migration_issues: AcceptableMigrationIssues, 

1426 feature_migration: FeatureMigration, 

1427 _migration_target: DebputyIntegrationMode | None, 

1428) -> None: 

1429 feature_migration.tagline = "dh_installtmpfiles config files" 

1430 for dctrl_bin in manifest.all_packages: 

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

1432 if dh_config_file is not None: 

1433 target = ( 

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

1435 if "." in dh_config_file.name 

1436 else "tmpfiles" 

1437 ) 

1438 _rename_file_if_exists( 

1439 debian_dir, 

1440 dh_config_file.name, 

1441 target, 

1442 feature_migration, 

1443 ) 

1444 

1445 

1446def migrate_lintian_overrides_files( 

1447 debian_dir: VirtualPath, 

1448 manifest: HighLevelManifest, 

1449 acceptable_migration_issues: AcceptableMigrationIssues, 

1450 feature_migration: FeatureMigration, 

1451 _migration_target: DebputyIntegrationMode | None, 

1452) -> None: 

1453 feature_migration.tagline = "dh_lintian config files" 

1454 for dctrl_bin in manifest.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 debian_dir, 

1459 dctrl_bin, 

1460 "lintian-overrides", 

1461 "dh_lintian", 

1462 acceptable_migration_issues, 

1463 feature_migration, 

1464 manifest, 

1465 support_executable_files=False, 

1466 remove_on_migration=False, 

1467 ) 

1468 

1469 

1470def migrate_links_files( 

1471 debian_dir: VirtualPath, 

1472 manifest: HighLevelManifest, 

1473 acceptable_migration_issues: AcceptableMigrationIssues, 

1474 feature_migration: FeatureMigration, 

1475 _migration_target: DebputyIntegrationMode | None, 

1476) -> None: 

1477 feature_migration.tagline = "dh_link files" 

1478 mutable_manifest = assume_not_none(manifest.mutable_manifest) 

1479 for dctrl_bin in manifest.all_packages: 

1480 links_file, content = _dh_config_file( 

1481 debian_dir, 

1482 dctrl_bin, 

1483 "links", 

1484 "dh_link", 

1485 acceptable_migration_issues, 

1486 feature_migration, 

1487 manifest, 

1488 support_executable_files=True, 

1489 ) 

1490 

1491 if links_file is None: 

1492 continue 

1493 assert content is not None 

1494 

1495 package_definition = mutable_manifest.package(dctrl_bin.name) 

1496 defined_symlink = { 

1497 symlink.symlink_path: symlink.symlink_target 

1498 for symlink in package_definition.symlinks() 

1499 } 

1500 

1501 seen_symlinks: set[str] = set() 

1502 

1503 for dhe_line in content: 

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

1505 raise UnsupportedFeature( 

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

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

1508 ) 

1509 target, source = dhe_line.tokens 

1510 source = _raise_on_unsupported_path( 

1511 source, 

1512 dhe_line.config_file, 

1513 dhe_line.line_no, 

1514 ) 

1515 target = _raise_on_unsupported_path( 

1516 target, 

1517 dhe_line.config_file, 

1518 dhe_line.line_no, 

1519 ) 

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

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

1522 raise ConflictingChange( 

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

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

1525 ) 

1526 seen_symlinks.add(source) 

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

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

1529 # absolute. 

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

1531 target = "/" + target 

1532 existing_target = defined_symlink.get(source) 

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

1534 if existing_target != target: 

1535 raise ConflictingChange( 

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

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

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

1539 f" {links_file.fs_path})" 

1540 ) 

1541 continue 

1542 condition = dhe_line.conditional() 

1543 package_definition.add_symlink( 

1544 MutableYAMLSymlink.new_symlink( 

1545 source, 

1546 target, 

1547 condition, 

1548 ) 

1549 ) 

1550 feature_migration.successful_manifest_changes += 1 

1551 

1552 

1553def migrate_misspelled_readme_debian_files( 

1554 debian_dir: VirtualPath, 

1555 manifest: HighLevelManifest, 

1556 acceptable_migration_issues: AcceptableMigrationIssues, 

1557 feature_migration: FeatureMigration, 

1558 _migration_target: DebputyIntegrationMode | None, 

1559) -> None: 

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

1561 for dctrl_bin in manifest.all_packages: 

1562 readme, _ = _dh_config_file( 

1563 debian_dir, 

1564 dctrl_bin, 

1565 "README.debian", 

1566 "dh_installdocs", 

1567 acceptable_migration_issues, 

1568 feature_migration, 

1569 manifest, 

1570 support_executable_files=False, 

1571 remove_on_migration=False, 

1572 ) 

1573 if readme is None: 

1574 continue 

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

1576 assert readme.name != new_name 

1577 _rename_file_if_exists( 

1578 debian_dir, 

1579 readme.name, 

1580 new_name, 

1581 feature_migration, 

1582 ) 

1583 

1584 

1585def migrate_doc_base_files( 

1586 debian_dir: VirtualPath, 

1587 manifest: HighLevelManifest, 

1588 _: AcceptableMigrationIssues, 

1589 feature_migration: FeatureMigration, 

1590 _migration_target: DebputyIntegrationMode | None, 

1591) -> None: 

1592 feature_migration.tagline = "doc-base files" 

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

1594 possible_effected_doc_base_files = [ 

1595 f 

1596 for f in debian_dir.iterdir 

1597 if ( 

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

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

1600 ) 

1601 ] 

1602 known_packages = {d.name: d for d in manifest.all_packages} 

1603 main_package = [d for d in manifest.all_packages if d.is_main_package][0] 

1604 for doc_base_file in possible_effected_doc_base_files: 

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

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

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

1608 owning_package = main_package 

1609 package_part = None 

1610 else: 

1611 package_part = parts[0] 

1612 parts = parts[1:] 

1613 

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

1615 # Not a doc-base file after all 

1616 continue 

1617 

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

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

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

1621 # Named files must have a package prefix 

1622 package_part = owning_package.name 

1623 else: 

1624 # No rename needed 

1625 continue 

1626 

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

1628 _rename_file_if_exists( 

1629 debian_dir, 

1630 doc_base_file.name, 

1631 new_basename, 

1632 feature_migration, 

1633 ) 

1634 

1635 

1636def migrate_dh_hook_targets( 

1637 debian_dir: VirtualPath, 

1638 _: HighLevelManifest, 

1639 acceptable_migration_issues: AcceptableMigrationIssues, 

1640 feature_migration: FeatureMigration, 

1641 migration_target: DebputyIntegrationMode | None, 

1642) -> None: 

1643 feature_migration.tagline = "dh hook targets" 

1644 source_root = os.path.dirname(debian_dir.fs_path) 

1645 if source_root == "": 

1646 source_root = "." 

1647 detected_hook_targets = json.loads( 

1648 subprocess.check_output( 

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

1650 cwd=source_root, 

1651 ).decode("utf-8") 

1652 ) 

1653 sample_hook_target: str | None = None 

1654 assert migration_target is not None 

1655 replaced_commands = DH_COMMANDS_REPLACED[migration_target] 

1656 

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

1658 if hook_target_def["is-empty"]: 

1659 continue 

1660 command = hook_target_def["command"] 

1661 if command not in replaced_commands: 

1662 continue 

1663 hook_target = hook_target_def["target-name"] 

1664 advice = MIGRATION_AID_FOR_OVERRIDDEN_COMMANDS.get(command) 

1665 if advice is None: 

1666 if sample_hook_target is None: 

1667 sample_hook_target = hook_target 

1668 feature_migration.warn( 

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

1670 ) 

1671 else: 

1672 feature_migration.warn( 

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

1674 f" for migration advice." 

1675 ) 

1676 if ( 

1677 feature_migration.warnings 

1678 and "dh-hook-targets" not in acceptable_migration_issues 

1679 and sample_hook_target is not None 

1680 ): 

1681 raise UnsupportedFeature( 

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

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

1684 f" {sample_hook_target}.", 

1685 ["dh-hook-targets"], 

1686 ) 

1687 

1688 

1689def detect_unsupported_zz_debputy_features( 

1690 debian_dir: VirtualPath, 

1691 manifest: HighLevelManifest, 

1692 acceptable_migration_issues: AcceptableMigrationIssues, 

1693 feature_migration: FeatureMigration, 

1694 _migration_target: DebputyIntegrationMode | None, 

1695) -> None: 

1696 feature_migration.tagline = "Known unsupported features" 

1697 

1698 for unsupported_config in UNSUPPORTED_DH_CONFIGS_AND_TOOLS_FOR_ZZ_DEBPUTY: 

1699 _unsupported_debhelper_config_file( 

1700 debian_dir, 

1701 manifest, 

1702 unsupported_config, 

1703 acceptable_migration_issues, 

1704 feature_migration, 

1705 ) 

1706 

1707 

1708def detect_obsolete_substvars( 

1709 debian_dir: VirtualPath, 

1710 _manifest: HighLevelManifest, 

1711 _acceptable_migration_issues: AcceptableMigrationIssues, 

1712 feature_migration: FeatureMigration, 

1713 _migration_target: DebputyIntegrationMode | None, 

1714) -> None: 

1715 feature_migration.tagline = ( 

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

1717 ) 

1718 ctrl_file = debian_dir.get("control") 

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

1720 feature_migration.warn( 

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

1722 ) 

1723 return 

1724 with ctrl_file.open() as fd: 

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

1726 

1727 relationship_fields = dpkg_field_list_pkg_dep() 

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

1729 

1730 for p in ctrl[1:]: 

1731 seen_obsolete_relationship_substvars = set() 

1732 obsolete_fields = set() 

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

1734 for df in relationship_fields: 

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

1736 if field is None: 

1737 continue 

1738 df_lc = df.lower() 

1739 number_of_relations = 0 

1740 obsolete_substvars_in_field = set() 

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

1742 if not d: 

1743 continue 

1744 number_of_relations += 1 

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

1746 continue 

1747 try: 

1748 end_idx = d.index("}") 

1749 except ValueError: 

1750 continue 

1751 substvar_name = d[2:end_idx] 

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

1753 continue 

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

1755 field_lc = field.lower() 

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

1757 continue 

1758 is_obsolete = field_lc == df_lc 

1759 if ( 

1760 not is_obsolete 

1761 and is_essential 

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

1763 and df_lc == "pre-depends" 

1764 ): 

1765 is_obsolete = True 

1766 

1767 if is_obsolete: 

1768 obsolete_substvars_in_field.add(d) 

1769 

1770 if number_of_relations == len(obsolete_substvars_in_field): 

1771 obsolete_fields.add(df) 

1772 else: 

1773 seen_obsolete_relationship_substvars.update(obsolete_substvars_in_field) 

1774 

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

1776 fo = feature_migration.fo 

1777 if obsolete_fields: 

1778 fields = ", ".join(obsolete_fields) 

1779 feature_migration.warn( 

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

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

1782 ) 

1783 if seen_obsolete_relationship_substvars: 

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

1785 feature_migration.warn( 

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

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

1788 ) 

1789 

1790 

1791def detect_dh_addons_zz_debputy_rrr( 

1792 debian_dir: VirtualPath, 

1793 _manifest: HighLevelManifest, 

1794 _acceptable_migration_issues: AcceptableMigrationIssues, 

1795 feature_migration: FeatureMigration, 

1796 _migration_target: DebputyIntegrationMode | None, 

1797) -> None: 

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

1799 r = read_dh_addon_sequences(debian_dir) 

1800 if r is None: 

1801 feature_migration.warn( 

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

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

1804 ) 

1805 return 

1806 

1807 bd_sequences, dr_sequences, _ = r 

1808 

1809 remaining_sequences = bd_sequences | dr_sequences 

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

1811 

1812 if not saw_dh_debputy: 

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

1814 

1815 

1816def detect_dh_addons_with_full_integration( 

1817 _debian_dir: VirtualPath, 

1818 _manifest: HighLevelManifest, 

1819 _acceptable_migration_issues: AcceptableMigrationIssues, 

1820 feature_migration: FeatureMigration, 

1821 _migration_target: DebputyIntegrationMode | None, 

1822) -> None: 

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

1824 feature_migration.warn( 

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

1826 ) 

1827 feature_migration.warn( 

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

1829 ) 

1830 feature_migration.warn( 

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

1832 ) 

1833 

1834 

1835def detect_dh_addons_with_zz_integration( 

1836 debian_dir: VirtualPath, 

1837 _manifest: HighLevelManifest, 

1838 acceptable_migration_issues: AcceptableMigrationIssues, 

1839 feature_migration: FeatureMigration, 

1840 _migration_target: DebputyIntegrationMode | None, 

1841) -> None: 

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

1843 r = read_dh_addon_sequences(debian_dir) 

1844 if r is None: 

1845 feature_migration.warn( 

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

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

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

1849 ) 

1850 return 

1851 

1852 assert _migration_target != INTEGRATION_MODE_FULL 

1853 

1854 bd_sequences, dr_sequences, _ = r 

1855 

1856 remaining_sequences = bd_sequences | dr_sequences 

1857 saw_dh_debputy = ( 

1858 "debputy" in remaining_sequences or "zz-debputy" in remaining_sequences 

1859 ) 

1860 saw_zz_debputy = "zz-debputy" in remaining_sequences 

1861 must_use_zz_debputy = False 

1862 remaining_sequences -= SUPPORTED_DH_ADDONS_WITH_ZZ_DEBPUTY 

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

1864 migration = DH_ADDONS_TO_PLUGINS[sequence] 

1865 feature_migration.require_plugin(migration.debputy_plugin) 

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

1867 if migration.must_use_zz_debputy: 

1868 must_use_zz_debputy = True 

1869 if sequence in bd_sequences: 

1870 feature_migration.warn( 

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

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

1873 ) 

1874 else: 

1875 feature_migration.warn( 

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

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

1878 ) 

1879 

1880 remaining_sequences -= DH_ADDONS_TO_PLUGINS.keys() 

1881 

1882 alt_key = "unsupported-dh-sequences" 

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

1884 if sequence in bd_sequences: 

1885 feature_migration.warn( 

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

1887 ) 

1888 else: 

1889 feature_migration.warn( 

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

1891 ) 

1892 

1893 remaining_sequences -= DH_ADDONS_TO_REMOVE_FOR_ZZ_DEBPUTY 

1894 

1895 for sequence in remaining_sequences: 

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

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

1898 if ( 

1899 key not in acceptable_migration_issues 

1900 and alt_key not in acceptable_migration_issues 

1901 ): 

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

1903 feature_migration.warn(msg) 

1904 

1905 if not saw_dh_debputy: 

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

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

1908 feature_migration.warn( 

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

1910 ) 

1911 

1912 

1913def _rename_file_if_exists( 

1914 debian_dir: VirtualPath, 

1915 source: str, 

1916 dest: str, 

1917 feature_migration: FeatureMigration, 

1918) -> None: 

1919 source_path = debian_dir.get(source) 

1920 dest_path = debian_dir.get(dest) 

1921 spath = ( 

1922 source_path.path 

1923 if source_path is not None 

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

1925 ) 

1926 dpath = ( 

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

1928 ) 

1929 if source_path is not None and source_path.is_file: 

1930 if dest_path is not None: 

1931 if not dest_path.is_file: 

1932 feature_migration.warnings.append( 

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

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

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

1936 ) 

1937 return 

1938 if ( 

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

1940 != 0 

1941 ): 

1942 feature_migration.warnings.append( 

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

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

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

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

1947 ) 

1948 else: 

1949 feature_migration.remove_on_success(dest_path.fs_path) 

1950 else: 

1951 feature_migration.rename_on_success( 

1952 source_path.fs_path, 

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

1954 ) 

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

1956 feature_migration.warnings.append( 

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

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

1959 " as a manual migration." 

1960 ) 

1961 

1962 

1963def _find_dh_config_file_for_any_pkg( 

1964 debian_dir: VirtualPath, 

1965 manifest: HighLevelManifest, 

1966 unsupported_config: UnsupportedDHConfig, 

1967) -> Iterable[VirtualPath]: 

1968 for dctrl_bin in manifest.all_packages: 

1969 dh_config_file = dhe_pkgfile( 

1970 debian_dir, 

1971 dctrl_bin, 

1972 unsupported_config.dh_config_basename, 

1973 bug_950723_prefix_matching=unsupported_config.bug_950723_prefix_matching, 

1974 ) 

1975 if dh_config_file is not None: 

1976 yield dh_config_file 

1977 

1978 

1979def _unsupported_debhelper_config_file( 

1980 debian_dir: VirtualPath, 

1981 manifest: HighLevelManifest, 

1982 unsupported_config: UnsupportedDHConfig, 

1983 acceptable_migration_issues: AcceptableMigrationIssues, 

1984 feature_migration: FeatureMigration, 

1985) -> None: 

1986 dh_config_files = list( 

1987 _find_dh_config_file_for_any_pkg(debian_dir, manifest, unsupported_config) 

1988 ) 

1989 if not dh_config_files: 

1990 return 

1991 dh_tool = unsupported_config.dh_tool 

1992 basename = unsupported_config.dh_config_basename 

1993 file_stem = ( 

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

1995 ) 

1996 dh_config_file = dh_config_files[0] 

1997 if unsupported_config.is_missing_migration: 

1998 feature_migration.warn( 

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

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

2001 " required." 

2002 ) 

2003 return 

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

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

2006 if ( 

2007 primary_key not in acceptable_migration_issues 

2008 and secondary_key not in acceptable_migration_issues 

2009 ): 

2010 msg = ( 

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

2012 " supported by debputy." 

2013 ) 

2014 raise UnsupportedFeature( 

2015 msg, 

2016 [primary_key, secondary_key], 

2017 ) 

2018 for dh_config_file in dh_config_files: 

2019 feature_migration.warn( 

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

2021 )