Coverage for src/debputy/plugins/debputy/private_api.py: 79%

496 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-07-22 10:58 +0000

1import ctypes 

2import ctypes.util 

3import dataclasses 

4import functools 

5import textwrap 

6import time 

7import typing 

8from datetime import datetime 

9from typing import cast, NotRequired, Union, TypedDict, Annotated, Any 

10from collections.abc import Callable 

11 

12import debian.debian_support 

13from debian.changelog import Changelog 

14from debian.deb822 import Deb822 

15 

16import debputy.plugin.api.spec 

17from debputy._manifest_constants import ( 

18 MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE, 

19 MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION, 

20 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

21 MK_INSTALLATIONS_INSTALL, 

22 MK_INSTALLATIONS_INSTALL_DOCS, 

23 MK_INSTALLATIONS_INSTALL_MAN, 

24 MK_INSTALLATIONS_DISCARD, 

25 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

26) 

27from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError 

28from debputy.installations import InstallRule 

29from debputy.maintscript_snippet import ( 

30 DpkgMaintscriptHelperCommand, 

31 MaintscriptCondition, 

32 MaintscriptForBinary, 

33 SUPPORTED_UDEB_SCRIPTS, 

34 DPKG_DEB_CONTROL_SCRIPTS, 

35 ALL_CONTROL_SCRIPTS, 

36) 

37from debputy.manifest_conditions import ( 

38 ManifestCondition, 

39 BinaryPackageContextArchMatchManifestCondition, 

40 BuildProfileMatch, 

41 SourceContextArchMatchManifestCondition, 

42) 

43from debputy.manifest_parser.base_types import ( 

44 FileSystemMode, 

45 StaticFileSystemOwner, 

46 StaticFileSystemGroup, 

47 SymlinkTarget, 

48 FileSystemExactMatchRule, 

49 FileSystemMatchRule, 

50 SymbolicMode, 

51 OctalMode, 

52 FileSystemExactNonDirMatchRule, 

53 BuildEnvironmentDefinition, 

54 DebputyParsedContentStandardConditional, 

55) 

56from debputy.manifest_parser.exceptions import ManifestParseException 

57from debputy.manifest_parser.mapper_code import type_mapper_str2package, PackageSelector 

58from debputy.manifest_parser.parse_hints import DebputyParseHint 

59from debputy.manifest_parser.parser_data import ParserContextData 

60from debputy.manifest_parser.tagging_types import ( 

61 DebputyParsedContent, 

62 TypeMapping, 

63) 

64from debputy.manifest_parser.util import AttributePath, check_integration_mode 

65from debputy.packages import BinaryPackage 

66from debputy.path_matcher import ExactFileSystemPath 

67from debputy.plugin.api import ( 

68 DebputyPluginInitializer, 

69 documented_attr, 

70 reference_documentation, 

71 VirtualPath, 

72 packager_provided_file_reference_documentation, 

73) 

74from debputy.plugin.api.impl import DebputyPluginInitializerProvider 

75from debputy.plugin.api.impl_types import automatic_discard_rule_example, PPFFormatParam 

76from debputy.plugin.api.spec import ( 

77 type_mapping_reference_documentation, 

78 type_mapping_example, 

79 not_integrations, 

80 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

81) 

82from debputy.plugin.api.std_docs import docs_from 

83from debputy.plugins.debputy.binary_package_rules import register_binary_package_rules 

84from debputy.plugins.debputy.discard_rules import ( 

85 _debputy_discard_pyc_files, 

86 _debputy_prune_la_files, 

87 _debputy_prune_doxygen_cruft, 

88 _debputy_prune_binary_debian_dir, 

89 _debputy_prune_info_dir_file, 

90 _debputy_prune_backup_files, 

91 _debputy_prune_vcs_paths, 

92) 

93from debputy.plugins.debputy.manifest_root_rules import register_manifest_root_rules 

94from debputy.plugins.debputy.package_processors import ( 

95 process_manpages, 

96 apply_compression, 

97 clean_la_files, 

98) 

99from debputy.plugins.debputy.service_management import ( 

100 detect_systemd_service_files, 

101 generate_snippets_for_systemd_units, 

102 detect_sysv_init_service_files, 

103 generate_snippets_for_init_scripts, 

104) 

105from debputy.plugins.debputy.shlib_metadata_detectors import detect_shlibdeps 

106from debputy.plugins.debputy.strip_non_determinism import strip_non_determinism 

107from debputy.substitution import VariableContext 

108from debputy.transformation_rules import ( 

109 CreateSymlinkReplacementRule, 

110 TransformationRule, 

111 CreateDirectoryTransformationRule, 

112 RemoveTransformationRule, 

113 MoveTransformationRule, 

114 PathMetadataTransformationRule, 

115 CreateSymlinkPathTransformationRule, 

116) 

117from debputy.util import ( 

118 _normalize_path, 

119 PKGNAME_REGEX, 

120 PKGVERSION_REGEX, 

121 debian_policy_normalize_symlink_target, 

122 active_profiles_match, 

123 _error, 

124 _warn, 

125 _info, 

126 assume_not_none, 

127 manifest_format_doc, 

128 PackageTypeSelector, 

129) 

130 

131_DOCUMENTED_DPKG_ARCH_TYPES = { 

132 "HOST": ( 

133 "installed on", 

134 "The package will be **installed** on this type of machine / system", 

135 ), 

136 "BUILD": ( 

137 "compiled on", 

138 "The compilation of this package will be performed **on** this kind of machine / system", 

139 ), 

140 "TARGET": ( 

141 "cross-compiler output", 

142 "When building a cross-compiler, it will produce output for this kind of machine/system", 

143 ), 

144} 

145 

146_DOCUMENTED_DPKG_ARCH_VARS = { 

147 "ARCH": "Debian's name for the architecture", 

148 "ARCH_ABI": "Debian's name for the architecture ABI", 

149 "ARCH_BITS": "Number of bits in the pointer size", 

150 "ARCH_CPU": "Debian's name for the CPU type", 

151 "ARCH_ENDIAN": "Endianness of the architecture (little/big)", 

152 "ARCH_LIBC": "Debian's name for the libc implementation", 

153 "ARCH_OS": "Debian name for the OS/kernel", 

154 "GNU_CPU": "GNU's name for the CPU", 

155 "GNU_SYSTEM": "GNU's name for the system", 

156 "GNU_TYPE": "GNU system type (GNU_CPU and GNU_SYSTEM combined)", 

157 "MULTIARCH": "Multi-arch tuple", 

158} 

159 

160 

161_NOT_INTEGRATION_RRR = not_integrations(INTEGRATION_MODE_DH_DEBPUTY_RRR) 

162 

163 

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

165class Capability: 

166 value: str 

167 

168 @classmethod 

169 def parse( 

170 cls, 

171 raw_value: str, 

172 _attribute_path: AttributePath, 

173 _parser_context: ParserContextData | None, 

174 ) -> "Capability": 

175 return cls(raw_value) 

176 

177 

178@functools.lru_cache 

179def load_libcap() -> tuple[bool, str | None, Callable[[str], bool]]: 

180 cap_library_path = ctypes.util.find_library("cap.so") 

181 has_libcap = False 

182 libcap = None 

183 if cap_library_path: 

184 try: 

185 libcap = ctypes.cdll.LoadLibrary(cap_library_path) 

186 has_libcap = True 

187 except OSError: 

188 pass 

189 

190 if libcap is None: 

191 warned = False 

192 

193 def _is_valid_cap(cap: str) -> bool: 

194 nonlocal warned 

195 if not warned: 

196 _info( 

197 "Could not load libcap.so; will not validate capabilities. Use `apt install libcap2` to provide" 

198 " checking of capabilities." 

199 ) 

200 warned = True 

201 return True 

202 

203 else: 

204 # cap_t cap_from_text(const char *path_p) 

205 libcap.cap_from_text.argtypes = [ctypes.c_char_p] 

206 libcap.cap_from_text.restype = ctypes.c_char_p 

207 

208 libcap.cap_free.argtypes = [ctypes.c_void_p] 

209 libcap.cap_free.restype = None 

210 

211 def _is_valid_cap(cap: str) -> bool: 

212 cap_t = libcap.cap_from_text(cap.encode("utf-8")) 

213 ok = cap_t is not None 

214 libcap.cap_free(cap_t) 

215 return ok 

216 

217 return has_libcap, cap_library_path, _is_valid_cap 

218 

219 

220def check_cap_checker() -> Callable[[str, str], None]: 

221 _, libcap_path, is_valid_cap = load_libcap() 

222 

223 seen_cap = set() 

224 

225 def _check_cap(cap: str, definition_source: str) -> None: 

226 if cap not in seen_cap and not is_valid_cap(cap): 

227 seen_cap.add(cap) 

228 cap_path = f" ({libcap_path})" if libcap_path is not None else "" 

229 _warn( 

230 f'The capabilities "{cap}" provided in {definition_source} were not understood by' 

231 f" libcap.so{cap_path}. Please verify you provided the correct capabilities." 

232 f" Note: This warning can be a false-positive if you are targeting a newer libcap.so" 

233 f" than the one installed on this system." 

234 ) 

235 

236 return _check_cap 

237 

238 

239def load_source_variables(variable_context: VariableContext) -> dict[str, str]: 

240 try: 

241 changelog = variable_context.debian_dir.lookup("changelog") 

242 if changelog is None: 

243 raise DebputyManifestVariableRequiresDebianDirError( 

244 "The changelog was not present" 

245 ) 

246 with changelog.open() as fd: 

247 dch = Changelog(fd, max_blocks=2) 

248 except FileNotFoundError as e: 

249 raise DebputyManifestVariableRequiresDebianDirError( 

250 "The changelog was not present" 

251 ) from e 

252 first_entry = dch[0] 

253 first_non_binnmu_entry = dch[0] 

254 if first_non_binnmu_entry.other_pairs.get("binary-only", "no") == "yes": 

255 first_non_binnmu_entry = dch[1] 

256 assert first_non_binnmu_entry.other_pairs.get("binary-only", "no") == "no" 

257 source_version = first_entry.version 

258 epoch = source_version.epoch 

259 upstream_version = source_version.upstream_version 

260 debian_revision = source_version.debian_revision 

261 epoch_upstream = upstream_version 

262 upstream_debian_revision = upstream_version 

263 if epoch is not None and epoch != "": 263 ↛ 265line 263 didn't jump to line 265 because the condition on line 263 was always true

264 epoch_upstream = f"{epoch}:{upstream_version}" 

265 if debian_revision is not None and debian_revision != "": 265 ↛ 268line 265 didn't jump to line 268 because the condition on line 265 was always true

266 upstream_debian_revision = f"{upstream_version}-{debian_revision}" 

267 

268 package = first_entry.package 

269 if package is None: 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true

270 _error("Cannot determine the source package name from debian/changelog.") 

271 

272 date = first_entry.date 

273 if date is not None: 273 ↛ 282line 273 didn't jump to line 282 because the condition on line 273 was always true

274 try: 

275 local_time = datetime.strptime(date, "%a, %d %b %Y %H:%M:%S %z") 

276 except ValueError: 

277 _error( 

278 f"Invalid date in the first changelog entry: {date!r} (Expected format: 'Thu, 26 Feb 2026 00:00:00 +0000')" 

279 ) 

280 source_date_epoch = str(int(local_time.timestamp())) 

281 else: 

282 _warn( 

283 "The latest changelog entry does not have a (parsable) date, using current time" 

284 " for SOURCE_DATE_EPOCH" 

285 ) 

286 source_date_epoch = str(int(time.time())) 

287 

288 if first_non_binnmu_entry is not first_entry: 

289 non_binnmu_date = first_non_binnmu_entry.date 

290 if non_binnmu_date is not None: 290 ↛ 294line 290 didn't jump to line 294 because the condition on line 290 was always true

291 local_time = datetime.strptime(non_binnmu_date, "%a, %d %b %Y %H:%M:%S %z") 

292 snd_source_date_epoch = str(int(local_time.timestamp())) 

293 else: 

294 _warn( 

295 "The latest (non-binNMU) changelog entry does not have a (parsable) date, using current time" 

296 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)" 

297 ) 

298 snd_source_date_epoch = source_date_epoch = str(int(time.time())) 

299 else: 

300 snd_source_date_epoch = source_date_epoch 

301 return { 

302 "DEB_SOURCE": package, 

303 "DEB_VERSION": source_version.full_version, 

304 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream, 

305 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision, 

306 "DEB_VERSION_UPSTREAM": upstream_version, 

307 "SOURCE_DATE_EPOCH": source_date_epoch, 

308 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": str(first_non_binnmu_entry.version), 

309 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch, 

310 } 

311 

312 

313def initialize_via_private_api(public_api: DebputyPluginInitializer) -> None: 

314 api = cast("DebputyPluginInitializerProvider", public_api) 

315 

316 api.metadata_or_maintscript_detector( 

317 "dpkg-shlibdeps", 

318 # Private because detect_shlibdeps expects private API (hench this cast) 

319 cast(debputy.plugin.api.spec.MetadataAutoDetector, detect_shlibdeps), 

320 package_types=PackageTypeSelector.DEB | PackageTypeSelector.UDEB, 

321 ) 

322 register_type_mappings(api) 

323 register_variables_via_private_api(api) 

324 document_builtin_variables(api) 

325 register_automatic_discard_rules(api) 

326 register_special_ppfs(api) 

327 register_install_rules(api) 

328 register_transformation_rules(api) 

329 register_manifest_condition_rules(api) 

330 register_maintscript_conditions(api) 

331 register_dpkg_conffile_rules(api) 

332 register_processing_steps(api) 

333 register_service_managers(api) 

334 register_manifest_root_rules(api) 

335 register_binary_package_rules(api) 

336 

337 

338def register_type_mappings(api: DebputyPluginInitializerProvider) -> None: 

339 api.register_mapped_type( 

340 TypeMapping(Capability, str, Capability.parse), 

341 reference_documentation=type_mapping_reference_documentation( 

342 description=textwrap.dedent( 

343 """\ 

344 The value is a Linux capability parsable by cap_from_text on the host system. 

345 

346 With `libcap2` installed, `debputy` will attempt to parse the value and provide 

347 warnings if the value cannot be parsed by `libcap2`. However, `debputy` will 

348 currently never emit hard errors for unknown capabilities. 

349 """, 

350 ), 

351 examples=[ 

352 type_mapping_example("cap_chown=p"), 

353 type_mapping_example("cap_chown=ep"), 

354 type_mapping_example("cap_kill-pe"), 

355 type_mapping_example("=ep cap_chown-e cap_kill-ep"), 

356 ], 

357 ), 

358 ) 

359 api.register_mapped_type( 

360 TypeMapping( 

361 FileSystemMatchRule, 

362 str, 

363 FileSystemMatchRule.parse_path_match, 

364 ).with_mapper_as_lint_validator(), 

365 reference_documentation=type_mapping_reference_documentation( 

366 description=textwrap.dedent( 

367 """\ 

368 A generic file system path match with globs. 

369 

370 Manifest variable substitution will be applied and glob expansion will be performed. 

371 

372 The match will be read as one of the following cases: 

373 

374 - Exact path match if there is no globs characters like `usr/bin/debputy` 

375 - A basename glob like `*.txt` or `**/foo` 

376 - A generic path glob otherwise like `usr/lib/*.so*` 

377 

378 Except for basename globs, all matches are always relative to the root directory of 

379 the match, which is typically the package root directory or a search directory. 

380 

381 For basename globs, any path matching that basename beneath the package root directory 

382 or relevant search directories will match. 

383 

384 Please keep in mind that: 

385 

386 * glob patterns often have to be quoted as YAML interpret the glob metacharacter as 

387 an anchor reference. 

388 

389 * Directories can be matched via this type. Whether the rule using this type 

390 recurse into the directory depends on the usage and not this type. Related, if 

391 value for this rule ends with a literal "/", then the definition can *only* match 

392 directories (similar to the shell). 

393 

394 * path matches involving glob expansion are often subject to different rules than 

395 path matches without them. As an example, automatic discard rules does not apply 

396 to exact path matches, but they will filter out glob matches. 

397 """, 

398 ), 

399 examples=[ 

400 type_mapping_example("usr/bin/debputy"), 

401 type_mapping_example("*.txt"), 

402 type_mapping_example("**/foo"), 

403 type_mapping_example("usr/lib/*.so*"), 

404 type_mapping_example("usr/share/foo/data-*/"), 

405 ], 

406 ), 

407 ) 

408 

409 api.register_mapped_type( 

410 TypeMapping( 

411 FileSystemExactMatchRule, 

412 str, 

413 FileSystemExactMatchRule.parse_path_match, 

414 ).with_mapper_as_lint_validator(), 

415 reference_documentation=type_mapping_reference_documentation( 

416 description=textwrap.dedent( 

417 """\ 

418 A file system match that does **not** expand globs. 

419 

420 Manifest variable substitution will be applied. However, globs will not be expanded. 

421 Any glob metacharacters will be interpreted as a literal part of path. 

422 

423 Note that a directory can be matched via this type. Whether the rule using this type 

424 recurse into the directory depends on the usage and is not defined by this type. 

425 Related, if value for this rule ends with a literal "/", then the definition can 

426 *only* match directories (similar to the shell). 

427 """, 

428 ), 

429 examples=[ 

430 type_mapping_example("usr/bin/dpkg"), 

431 type_mapping_example("usr/share/foo/"), 

432 type_mapping_example("usr/share/foo/data.txt"), 

433 ], 

434 ), 

435 ) 

436 

437 api.register_mapped_type( 

438 TypeMapping( 

439 FileSystemExactNonDirMatchRule, 

440 str, 

441 FileSystemExactNonDirMatchRule.parse_path_match, 

442 ).with_mapper_as_lint_validator(), 

443 reference_documentation=type_mapping_reference_documentation( 

444 description=textwrap.dedent( 

445 f"""\ 

446 A file system match that does **not** expand globs and must not match a directory. 

447 

448 Manifest variable substitution will be applied. However, globs will not be expanded. 

449 Any glob metacharacters will be interpreted as a literal part of path. 

450 

451 This is like {FileSystemExactMatchRule.__name__} except that the match will fail if the 

452 provided path matches a directory. Since a directory cannot be matched, it is an error 

453 for any input to end with a "/" as only directories can be matched if the path ends 

454 with a "/". 

455 """, 

456 ), 

457 examples=[ 

458 type_mapping_example("usr/bin/dh_debputy"), 

459 type_mapping_example("usr/share/foo/data.txt"), 

460 ], 

461 ), 

462 ) 

463 

464 api.register_mapped_type( 

465 TypeMapping( 

466 SymlinkTarget, 

467 str, 

468 lambda v, ap, pc: SymlinkTarget.parse_symlink_target( 

469 v, ap, assume_not_none(pc).substitution 

470 ), 

471 ), 

472 reference_documentation=type_mapping_reference_documentation( 

473 description=textwrap.dedent( 

474 """\ 

475 A symlink target. 

476 

477 Manifest variable substitution will be applied. This is distinct from an exact file 

478 system match in that a symlink target is not relative to the package root by default 

479 (explicitly prefix for "/" for absolute path targets) 

480 

481 Note that `debputy` will policy normalize symlinks when assembling the deb, so 

482 use of relative or absolute symlinks comes down to preference. 

483 """, 

484 ), 

485 examples=[ 

486 type_mapping_example("../foo"), 

487 type_mapping_example("/usr/share/doc/bar"), 

488 ], 

489 ), 

490 ) 

491 

492 api.register_mapped_type( 

493 TypeMapping( 

494 StaticFileSystemOwner, 

495 int | str, 

496 lambda v, ap, _: StaticFileSystemOwner.from_manifest_value(v, ap), 

497 ).with_mapper_as_lint_validator(), 

498 reference_documentation=type_mapping_reference_documentation( 

499 description=textwrap.dedent("""\ 

500 File system owner reference that is part of the passwd base data (such as "root"). 

501 

502 The group can be provided in either of the following three forms: 

503 

504 * A name (recommended), such as "root" 

505 * The UID in the form of an integer (that is, no quoting), such as 0 (for "root") 

506 * The name and the UID separated by colon such as "root:0" (for "root"). 

507 

508 Note in the last case, the `debputy` will validate that the name and the UID match. 

509 

510 Some owners (such as "nobody") are deliberately disallowed. 

511 """), 

512 examples=[ 

513 type_mapping_example("root"), 

514 type_mapping_example(0), 

515 type_mapping_example("root:0"), 

516 type_mapping_example("bin"), 

517 ], 

518 ), 

519 ) 

520 api.register_mapped_type( 

521 TypeMapping( 

522 StaticFileSystemGroup, 

523 int | str, 

524 lambda v, ap, _: StaticFileSystemGroup.from_manifest_value(v, ap), 

525 ).with_mapper_as_lint_validator(), 

526 reference_documentation=type_mapping_reference_documentation( 

527 description=textwrap.dedent("""\ 

528 File system group reference that is part of the passwd base data (such as "root"). 

529 

530 The group can be provided in either of the following three forms: 

531 

532 * A name (recommended), such as "root" 

533 * The GID in the form of an integer (that is, no quoting), such as 0 (for "root") 

534 * The name and the GID separated by colon such as "root:0" (for "root"). 

535 

536 Note in the last case, the `debputy` will validate that the name and the GID match. 

537 

538 Some owners (such as "nobody") are deliberately disallowed. 

539 """), 

540 examples=[ 

541 type_mapping_example("root"), 

542 type_mapping_example(0), 

543 type_mapping_example("root:0"), 

544 type_mapping_example("tty"), 

545 ], 

546 ), 

547 ) 

548 

549 api.register_mapped_type( 

550 TypeMapping( 

551 BinaryPackage, 

552 str, 

553 type_mapper_str2package, 

554 ), 

555 reference_documentation=type_mapping_reference_documentation( 

556 description="Name of a package in debian/control", 

557 ), 

558 ) 

559 

560 api.register_mapped_type( 

561 TypeMapping( 

562 PackageSelector, 

563 str, 

564 PackageSelector.parse, 

565 ), 

566 reference_documentation=type_mapping_reference_documentation( 

567 description=textwrap.dedent("""\ 

568 Match a package or set of a packages from debian/control 

569 

570 The simplest package selector is the name of a binary package from `debian/control`. 

571 However, selections can also match multiple packages based on a given criteria, such 

572 as `arch:all`/`arch:any` (matches packages where the `Architecture` field is set to 

573 `all` or is not set to `all` respectively) or `package-type:deb` / `package-type:udeb` 

574 (matches packages where `Package-Type` is set to `deb` or is set to `udeb` 

575 respectively). 

576 """), 

577 ), 

578 ) 

579 

580 api.register_mapped_type( 

581 TypeMapping( 

582 FileSystemMode, 

583 str, 

584 lambda v, ap, _: FileSystemMode.parse_filesystem_mode(v, ap), 

585 ).with_mapper_as_lint_validator(), 

586 reference_documentation=type_mapping_reference_documentation( 

587 description="A file system mode either in the form of an octal mode or a symbolic mode.", 

588 examples=[ 

589 type_mapping_example("a+x"), 

590 type_mapping_example("u=rwX,go=rX"), 

591 type_mapping_example("0755"), 

592 ], 

593 ), 

594 ) 

595 api.register_mapped_type( 

596 TypeMapping( 

597 OctalMode, 

598 str, 

599 lambda v, ap, _: OctalMode.parse_filesystem_mode(v, ap), 

600 ).with_mapper_as_lint_validator(), 

601 reference_documentation=type_mapping_reference_documentation( 

602 description="A file system mode using the octal mode representation. Must always be a provided as a string (that is, quoted).", 

603 examples=[ 

604 type_mapping_example("0644"), 

605 type_mapping_example("0755"), 

606 ], 

607 ), 

608 ) 

609 api.register_mapped_type( 

610 TypeMapping( 

611 BuildEnvironmentDefinition, 

612 str, 

613 lambda v, ap, pc: assume_not_none(pc).resolve_build_environment(v, ap), 

614 ), 

615 reference_documentation=type_mapping_reference_documentation( 

616 description="Reference to a build environment defined in `build-environments`", 

617 ), 

618 ) 

619 

620 def _parse_version( 

621 unparsed_version: str, 

622 attribute_path: AttributePath, 

623 _pc: ParserContextData | None, 

624 ) -> debian.debian_support.Version: 

625 try: 

626 return debian.debian_support.Version(unparsed_version) 

627 except (ValueError, TypeError) as e: 

628 raise ManifestParseException( 

629 f"Could not parse {unparsed_version!r} at {attribute_path.path} as a Debian version string: {e}" 

630 ) 

631 

632 api.register_mapped_type( 

633 TypeMapping( 

634 debian.debian_support.Version, 

635 str, 

636 _parse_version, 

637 ).with_mapper_as_lint_validator(), 

638 reference_documentation=type_mapping_reference_documentation( 

639 description="A Debian version such as `2.3-1~bpo13+1`", 

640 examples=[ 

641 type_mapping_example("2.3-1~bpo13+1"), 

642 ], 

643 ), 

644 ) 

645 

646 def _parse_maintscript_name( 

647 unparsed_name: str, 

648 attribute_path: AttributePath, 

649 pc: ParserContextData | None, 

650 ) -> MaintscriptForBinary: 

651 is_udeb = assume_not_none( 

652 pc 

653 ).current_binary_package_state.binary_package.is_udeb 

654 supported_names = ( 

655 SUPPORTED_UDEB_SCRIPTS if is_udeb else DPKG_DEB_CONTROL_SCRIPTS 

656 ) 

657 if unparsed_name in supported_names: 

658 return MaintscriptForBinary(unparsed_name) 

659 supported_names = ", ".join(sorted(supported_names)) 

660 if unparsed_name in ALL_CONTROL_SCRIPTS: 

661 raise ManifestParseException( 

662 f"The maintscript name {unparsed_name!r} at {attribute_path.path} is a valid name" 

663 f" but not supported here. Supported values are: {supported_names} " 

664 ) 

665 raise ManifestParseException( 

666 f"Unknown (or unsupported) maintscript name {unparsed_name!r} at {attribute_path.path}." 

667 f" Supported values are: {supported_names}" 

668 ) 

669 

670 api.register_mapped_type( 

671 TypeMapping( 

672 MaintscriptForBinary, 

673 str, 

674 _parse_maintscript_name, 

675 ), 

676 reference_documentation=type_mapping_reference_documentation( 

677 description="Name of maintscript such as `postinst` relevant for the binary package", 

678 ), 

679 ) 

680 

681 

682def register_service_managers( 

683 api: DebputyPluginInitializerProvider, 

684) -> None: 

685 api.service_provider( 

686 "systemd", 

687 detect_systemd_service_files, 

688 generate_snippets_for_systemd_units, 

689 ) 

690 api.service_provider( 

691 "sysvinit", 

692 detect_sysv_init_service_files, 

693 generate_snippets_for_init_scripts, 

694 ) 

695 

696 

697def register_automatic_discard_rules( 

698 api: DebputyPluginInitializerProvider, 

699) -> None: 

700 api.automatic_discard_rule( 

701 "python-cache-files", 

702 _debputy_discard_pyc_files, 

703 rule_reference_documentation="Discards any *.pyc, *.pyo files and any __pycache__ directories", 

704 examples=automatic_discard_rule_example( 

705 (".../foo.py", False), 

706 ".../__pycache__/", 

707 ".../__pycache__/...", 

708 ".../foo.pyc", 

709 ".../foo.pyo", 

710 ), 

711 ) 

712 api.automatic_discard_rule( 

713 "la-files", 

714 _debputy_prune_la_files, 

715 rule_reference_documentation="Discards any file with the extension .la beneath the directory /usr/lib", 

716 examples=automatic_discard_rule_example( 

717 "usr/lib/libfoo.la", 

718 ("usr/lib/libfoo.so.1.0.0", False), 

719 ), 

720 ) 

721 api.automatic_discard_rule( 

722 "backup-files", 

723 _debputy_prune_backup_files, 

724 rule_reference_documentation="Discards common back up files such as foo~, foo.bak or foo.orig", 

725 examples=( 

726 automatic_discard_rule_example( 

727 ".../foo~", 

728 ".../foo.orig", 

729 ".../foo.rej", 

730 ".../DEADJOE", 

731 ".../.foo.sw.", 

732 ), 

733 ), 

734 ) 

735 api.automatic_discard_rule( 

736 "version-control-paths", 

737 _debputy_prune_vcs_paths, 

738 rule_reference_documentation="Discards common version control paths such as .git, .gitignore, CVS, etc.", 

739 examples=automatic_discard_rule_example( 

740 ("tools/foo", False), 

741 ".../CVS/", 

742 ".../CVS/...", 

743 ".../.gitignore", 

744 ".../.gitattributes", 

745 ".../.git/", 

746 ".../.git/...", 

747 ), 

748 ) 

749 api.automatic_discard_rule( 

750 "gnu-info-dir-file", 

751 _debputy_prune_info_dir_file, 

752 rule_reference_documentation="Discards the /usr/share/info/dir file (causes package file conflicts)", 

753 examples=automatic_discard_rule_example( 

754 "usr/share/info/dir", 

755 ("usr/share/info/foo.info", False), 

756 ("usr/share/info/dir.info", False), 

757 ("usr/share/random/case/dir", False), 

758 ), 

759 ) 

760 api.automatic_discard_rule( 

761 "debian-dir", 

762 _debputy_prune_binary_debian_dir, 

763 rule_reference_documentation="(Implementation detail) Discards any DEBIAN directory to avoid it from appearing" 

764 " literally in the file listing", 

765 examples=( 

766 automatic_discard_rule_example( 

767 "DEBIAN/", 

768 "DEBIAN/control", 

769 ("usr/bin/foo", False), 

770 ("usr/share/DEBIAN/foo", False), 

771 ), 

772 ), 

773 ) 

774 api.automatic_discard_rule( 

775 "doxygen-cruft-files", 

776 _debputy_prune_doxygen_cruft, 

777 rule_reference_documentation="Discards cruft files generated by doxygen", 

778 examples=automatic_discard_rule_example( 

779 ("usr/share/doc/foo/api/doxygen.css", False), 

780 ("usr/share/doc/foo/api/doxygen.svg", False), 

781 ("usr/share/doc/foo/api/index.html", False), 

782 "usr/share/doc/foo/api/.../cruft.map", 

783 "usr/share/doc/foo/api/.../cruft.md5", 

784 ), 

785 ) 

786 

787 

788def register_processing_steps(api: DebputyPluginInitializerProvider) -> None: 

789 api.package_processor("manpages", process_manpages) 

790 api.package_processor("clean-la-files", clean_la_files) 

791 # strip-non-determinism makes assumptions about the PackageProcessingContext implementation 

792 api.package_processor( 

793 "strip-nondeterminism", 

794 cast("Any", strip_non_determinism), 

795 depends_on_processor=["manpages"], 

796 ) 

797 api.package_processor( 

798 "compression", 

799 apply_compression, 

800 depends_on_processor=["manpages", "strip-nondeterminism"], 

801 ) 

802 

803 

804def register_variables_via_private_api(api: DebputyPluginInitializerProvider) -> None: 

805 api.manifest_variable_provider( 

806 load_source_variables, 

807 { 

808 "DEB_SOURCE": "Name of the source package (`dpkg-parsechangelog -SSource`)", 

809 "DEB_VERSION": "Version from the top most changelog entry (`dpkg-parsechangelog -SVersion`)", 

810 "DEB_VERSION_EPOCH_UPSTREAM": "Version from the top most changelog entry *without* the Debian revision", 

811 "DEB_VERSION_UPSTREAM_REVISION": "Version from the top most changelog entry *without* the epoch", 

812 "DEB_VERSION_UPSTREAM": "Upstream version from the top most changelog entry (that is, *without* epoch and Debian revision)", 

813 "SOURCE_DATE_EPOCH": textwrap.dedent("""\ 

814 Timestamp from the top most changelog entry (`dpkg-parsechangelog -STimestamp`) 

815 Please see <https://reproducible-builds.org/docs/source-date-epoch/> for the full definition of 

816 this variable. 

817 """), 

818 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

819 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

820 }, 

821 ) 

822 

823 

824def document_builtin_variables(api: DebputyPluginInitializerProvider) -> None: 

825 api.document_builtin_variable( 

826 "PACKAGE", 

827 "Name of the binary package (only available in binary context)", 

828 is_context_specific=True, 

829 ) 

830 

831 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

832 

833 for arch_type, (arch_type_tag, arch_type_doc) in arch_types.items(): 

834 for arch_var, arch_var_doc in _DOCUMENTED_DPKG_ARCH_VARS.items(): 

835 full_var = f"DEB_{arch_type}_{arch_var}" 

836 documentation = textwrap.dedent(f"""\ 

837 {arch_var_doc} ({arch_type_tag}) 

838 This variable describes machine information used when the package is compiled and assembled. 

839 * Machine type: {arch_type_doc} 

840 * Value description: {arch_var_doc} 

841 

842 The value is the output of: `dpkg-architecture -q{full_var}` 

843 """) 

844 api.document_builtin_variable( 

845 full_var, 

846 documentation, 

847 is_for_special_case=arch_type != "HOST", 

848 ) 

849 

850 

851def _format_docbase_filename( 

852 path_format: str, 

853 format_param: PPFFormatParam, 

854 docbase_file: VirtualPath, 

855) -> str: 

856 with docbase_file.open() as fd: 

857 content = Deb822(fd) 

858 proper_name = content["Document"] 

859 if proper_name is not None: 859 ↛ 862line 859 didn't jump to line 862 because the condition on line 859 was always true

860 format_param["name"] = proper_name 

861 else: 

862 _warn( 

863 f"The docbase file {docbase_file.fs_path} is missing the Document field" 

864 ) 

865 return path_format.format(**format_param) 

866 

867 

868def register_special_ppfs(api: DebputyPluginInitializerProvider) -> None: 

869 api.packager_provided_file( 

870 "doc-base", 

871 "/usr/share/doc-base/{owning_package}.{name}", 

872 format_callback=_format_docbase_filename, 

873 ) 

874 

875 api.packager_provided_file( 

876 "shlibs", 

877 "DEBIAN/shlibs", 

878 allow_name_segment=False, 

879 reservation_only=True, 

880 reference_documentation=packager_provided_file_reference_documentation( 

881 format_documentation_uris=["man:deb-shlibs(5)"], 

882 ), 

883 ) 

884 api.packager_provided_file( 

885 "symbols", 

886 "DEBIAN/symbols", 

887 allow_name_segment=False, 

888 allow_architecture_segment=True, 

889 reservation_only=True, 

890 reference_documentation=packager_provided_file_reference_documentation( 

891 format_documentation_uris=["man:deb-symbols(5)"], 

892 ), 

893 ) 

894 api.packager_provided_file( 

895 "conffiles", 

896 "DEBIAN/conffiles", 

897 allow_name_segment=False, 

898 allow_architecture_segment=True, 

899 reservation_only=True, 

900 ) 

901 api.packager_provided_file( 

902 "templates", 

903 "DEBIAN/templates", 

904 allow_name_segment=False, 

905 allow_architecture_segment=False, 

906 reservation_only=True, 

907 ) 

908 api.packager_provided_file( 

909 "alternatives", 

910 "DEBIAN/alternatives", 

911 allow_name_segment=False, 

912 allow_architecture_segment=True, 

913 reservation_only=True, 

914 ) 

915 

916 

917def register_install_rules(api: DebputyPluginInitializerProvider) -> None: 

918 api.pluggable_manifest_rule( 

919 InstallRule, 

920 MK_INSTALLATIONS_INSTALL, 

921 ParsedInstallRule, 

922 _install_rule_handler, 

923 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

924 inline_reference_documentation=reference_documentation( 

925 title="Generic install (`install`)", 

926 description=textwrap.dedent("""\ 

927 The generic `install` rule can be used to install arbitrary paths into packages 

928 and is *similar* to how `dh_install` from debhelper works. It is a two "primary" uses. 

929 

930 1) The classic "install into directory" similar to the standard `dh_install` 

931 2) The "install as" similar to `dh-exec`'s `foo => bar` feature. 

932 

933 The `install` rule installs a path exactly once into each package it acts on. In 

934 the rare case that you want to install the same source *multiple* times into the 

935 *same* packages, please have a look at `{MULTI_DEST_INSTALL}`. 

936 """.format(MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL)), 

937 non_mapping_description=textwrap.dedent("""\ 

938 When the input is a string or a list of string, then that value is used as shorthand 

939 for `source` or `sources` (respectively). This form can only be used when `into` is 

940 not required. 

941 """), 

942 attributes=[ 

943 documented_attr( 

944 ["source", "sources"], 

945 textwrap.dedent("""\ 

946 A path match (`source`) or a list of path matches (`sources`) defining the 

947 source path(s) to be installed. The path match(es) can use globs. Each match 

948 is tried against default search directories. 

949 - When a symlink is matched, then the symlink (not its target) is installed 

950 as-is. When a directory is matched, then the directory is installed along 

951 with all the contents that have not already been installed somewhere. 

952 """), 

953 ), 

954 documented_attr( 

955 "dest_dir", 

956 textwrap.dedent("""\ 

957 A path defining the destination *directory*. The value *cannot* use globs, but can 

958 use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults 

959 to the directory name of the `source`. 

960 """), 

961 ), 

962 documented_attr( 

963 "into", 

964 textwrap.dedent("""\ 

965 Either a package name or a list of package names for which these paths should be 

966 installed. This key is conditional on whether there are multiple binary packages listed 

967 in `debian/control`. When there is only one binary package, then that binary is the 

968 default for `into`. Otherwise, the key is required. 

969 """), 

970 ), 

971 documented_attr( 

972 "install_as", 

973 textwrap.dedent("""\ 

974 A path defining the path to install the source as. This is a full path. This option 

975 is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is 

976 given, then `source` must match exactly one "not yet matched" path. 

977 """), 

978 ), 

979 *docs_from(DebputyParsedContentStandardConditional), 

980 ], 

981 reference_documentation_url=manifest_format_doc("generic-install-install"), 

982 ), 

983 ) 

984 api.pluggable_manifest_rule( 

985 InstallRule, 

986 [ 

987 MK_INSTALLATIONS_INSTALL_DOCS, 

988 "install-doc", 

989 ], 

990 ParsedInstallRule, 

991 _install_docs_rule_handler, 

992 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

993 inline_reference_documentation=reference_documentation( 

994 title="Install documentation (`install-docs`)", 

995 description=textwrap.dedent("""\ 

996 This install rule resemble that of `dh_installdocs`. It is a shorthand over the generic 

997 `install` rule with the following key features: 

998 

999 1) The default `dest-dir` is to use the package's documentation directory (usually something 

1000 like `/usr/share/doc/{{PACKAGE}}`, though it respects the "main documentation package" 

1001 recommendation from Debian Policy). The `dest-dir` or `as` can be set in case the 

1002 documentation in question goes into another directory or with a concrete path. In this 

1003 case, it is still "better" than `install` due to the remaining benefits. 

1004 2) The rule comes with pre-defined conditional logic for skipping the rule under 

1005 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself. 

1006 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb` 

1007 package listed in `debian/control`. 

1008 

1009 With these two things in mind, it behaves just like the `install` rule. 

1010 

1011 Note: It is often worth considering to use a more specialized version of the `install-docs` 

1012 rule when one such is available. If you are looking to install an example or a man page, 

1013 consider whether `install-examples` or `install-man` might be a better fit for your 

1014 use-case. 

1015 """), 

1016 non_mapping_description=textwrap.dedent("""\ 

1017 When the input is a string or a list of string, then that value is used as shorthand 

1018 for `source` or `sources` (respectively). This form can only be used when `into` is 

1019 not required. 

1020 """), 

1021 attributes=[ 

1022 documented_attr( 

1023 ["source", "sources"], 

1024 textwrap.dedent("""\ 

1025 A path match (`source`) or a list of path matches (`sources`) defining the 

1026 source path(s) to be installed. The path match(es) can use globs. Each match 

1027 is tried against default search directories. 

1028 - When a symlink is matched, then the symlink (not its target) is installed 

1029 as-is. When a directory is matched, then the directory is installed along 

1030 with all the contents that have not already been installed somewhere. 

1031 

1032 - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a 

1033 directory for `install-examples` will give you an `examples/examples` 

1034 directory in the package, which is rarely what you want. Often, you 

1035 can solve this by using `examples/*` instead. Similar for `install-docs` 

1036 and a `doc` or `docs` directory. 

1037 """), 

1038 ), 

1039 documented_attr( 

1040 "dest_dir", 

1041 textwrap.dedent("""\ 

1042 A path defining the destination *directory*. The value *cannot* use globs, but can 

1043 use substitution. If neither `as` nor `dest-dir` is given, then `dest-dir` defaults 

1044 to the relevant package documentation directory (a la `/usr/share/doc/{{PACKAGE}}`). 

1045 """), 

1046 ), 

1047 documented_attr( 

1048 "into", 

1049 textwrap.dedent("""\ 

1050 Either a package name or a list of package names for which these paths should be 

1051 installed as documentation. This key is conditional on whether there are multiple 

1052 (non-`udeb`) binary packages listed in `debian/control`. When there is only one 

1053 (non-`udeb`) binary package, then that binary is the default for `into`. Otherwise, 

1054 the key is required. 

1055 """), 

1056 ), 

1057 documented_attr( 

1058 "install_as", 

1059 textwrap.dedent("""\ 

1060 A path defining the path to install the source as. This is a full path. This option 

1061 is mutually exclusive with `dest-dir` and `sources` (but not `source`). When `as` is 

1062 given, then `source` must match exactly one "not yet matched" path. 

1063 """), 

1064 ), 

1065 documented_attr( 

1066 "when", 

1067 textwrap.dedent("""\ 

1068 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules). 

1069 This condition will be combined with the built-in condition provided by these rules 

1070 (rather than replacing it). 

1071 """), 

1072 ), 

1073 ], 

1074 reference_documentation_url=manifest_format_doc( 

1075 "install-documentation-install-docs" 

1076 ), 

1077 ), 

1078 ) 

1079 api.pluggable_manifest_rule( 

1080 InstallRule, 

1081 [ 

1082 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

1083 "install-example", 

1084 ], 

1085 ParsedInstallExamplesRule, 

1086 _install_examples_rule_handler, 

1087 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

1088 inline_reference_documentation=reference_documentation( 

1089 title="Install examples (`install-examples`)", 

1090 description=textwrap.dedent("""\ 

1091 This install rule resemble that of `dh_installexamples`. It is a shorthand over the generic ` 

1092 install` rule with the following key features: 

1093 

1094 1) It pre-defines the `dest-dir` that respects the "main documentation package" recommendation from 

1095 Debian Policy. The `install-examples` will use the `examples` subdir for the package documentation 

1096 dir. 

1097 2) The rule comes with pre-defined conditional logic for skipping the rule under 

1098 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself. 

1099 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb` 

1100 package listed in `debian/control`. 

1101 

1102 With these two things in mind, it behaves just like the `install` rule. 

1103 """), 

1104 non_mapping_description=textwrap.dedent("""\ 

1105 When the input is a string or a list of string, then that value is used as shorthand 

1106 for `source` or `sources` (respectively). This form can only be used when `into` is 

1107 not required. 

1108 """), 

1109 attributes=[ 

1110 documented_attr( 

1111 ["source", "sources"], 

1112 textwrap.dedent("""\ 

1113 A path match (`source`) or a list of path matches (`sources`) defining the 

1114 source path(s) to be installed. The path match(es) can use globs. Each match 

1115 is tried against default search directories. 

1116 - When a symlink is matched, then the symlink (not its target) is installed 

1117 as-is. When a directory is matched, then the directory is installed along 

1118 with all the contents that have not already been installed somewhere. 

1119 

1120 - **CAVEAT**: Specifying `source: examples` where `examples` resolves to a 

1121 directory for `install-examples` will give you an `examples/examples` 

1122 directory in the package, which is rarely what you want. Often, you 

1123 can solve this by using `examples/*` instead. Similar for `install-docs` 

1124 and a `doc` or `docs` directory. 

1125 """), 

1126 ), 

1127 documented_attr( 

1128 "into", 

1129 textwrap.dedent("""\ 

1130 Either a package name or a list of package names for which these paths should be 

1131 installed as examples. This key is conditional on whether there are (non-`udeb`) 

1132 multiple binary packages listed in `debian/control`. When there is only one 

1133 (non-`udeb`) binary package, then that binary is the default for `into`. 

1134 Otherwise, the key is required. 

1135 """), 

1136 ), 

1137 documented_attr( 

1138 "when", 

1139 textwrap.dedent("""\ 

1140 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules). 

1141 This condition will be combined with the built-in condition provided by these rules 

1142 (rather than replacing it). 

1143 """), 

1144 ), 

1145 ], 

1146 reference_documentation_url=manifest_format_doc( 

1147 "install-examples-install-examples" 

1148 ), 

1149 ), 

1150 ) 

1151 api.pluggable_manifest_rule( 

1152 InstallRule, 

1153 MK_INSTALLATIONS_INSTALL_MAN, 

1154 ParsedInstallManpageRule, 

1155 _install_man_rule_handler, 

1156 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1157 inline_reference_documentation=reference_documentation( 

1158 title="Install man pages (`install-man`)", 

1159 description=textwrap.dedent("""\ 

1160 Install rule for installing man pages similar to `dh_installman`. It is a shorthand 

1161 over the generic `install` rule with the following key features: 

1162 

1163 1) The rule can only match files (notably, symlinks cannot be matched by this rule). 

1164 2) The `dest-dir` is computed per source file based on the man page's section and 

1165 language. 

1166 3) The `into` parameter can be omitted as long as there is a exactly one non-`udeb` 

1167 package listed in `debian/control`. 

1168 4) The rule comes with man page specific attributes such as `language` and `section` 

1169 for when the auto-detection is insufficient. 

1170 5) The rule comes with pre-defined conditional logic for skipping the rule under 

1171 `DEB_BUILD_OPTIONS=nodoc`, so you do not have to write that conditional yourself. 

1172 

1173 With these things in mind, the rule behaves similar to the `install` rule. 

1174 """), 

1175 non_mapping_description=textwrap.dedent("""\ 

1176 When the input is a string or a list of string, then that value is used as shorthand 

1177 for `source` or `sources` (respectively). This form can only be used when `into` is 

1178 not required. 

1179 """), 

1180 attributes=[ 

1181 documented_attr( 

1182 ["source", "sources"], 

1183 textwrap.dedent("""\ 

1184 A path match (`source`) or a list of path matches (`sources`) defining the 

1185 source path(s) to be installed. The path match(es) can use globs. Each match 

1186 is tried against default search directories. 

1187 - When a symlink is matched, then the symlink (not its target) is installed 

1188 as-is. When a directory is matched, then the directory is installed along 

1189 with all the contents that have not already been installed somewhere. 

1190 """), 

1191 ), 

1192 documented_attr( 

1193 "into", 

1194 textwrap.dedent("""\ 

1195 Either a package name or a list of package names for which these paths should be 

1196 installed as man pages. This key is conditional on whether there are multiple (non-`udeb`) 

1197 binary packages listed in `debian/control`. When there is only one (non-`udeb`) binary 

1198 package, then that binary is the default for `into`. Otherwise, the key is required. 

1199 """), 

1200 ), 

1201 documented_attr( 

1202 "section", 

1203 textwrap.dedent("""\ 

1204 If provided, it must be an integer between 1 and 9 (both inclusive), defining the 

1205 section the man pages belong overriding any auto-detection that `debputy` would 

1206 have performed. 

1207 """), 

1208 ), 

1209 documented_attr( 

1210 "language", 

1211 textwrap.dedent("""\ 

1212 If provided, it must be either a 2 letter language code (such as `de`), a 5 letter 

1213 language + dialect code (such as `pt_BR`), or one of the special keywords `C`, 

1214 `derive-from-path`, or `derive-from-basename`. The default is `derive-from-path`. 

1215 - When `language` is `C`, then the man pages are assumed to be "untranslated". 

1216 - When `language` is a language code (with or without dialect), then all man pages 

1217 matched will be assumed to be translated to that concrete language / dialect. 

1218 - When `language` is `derive-from-path`, then `debputy` attempts to derive the 

1219 language from the path (`man/<language>/man<section>`). This matches the 

1220 default of `dh_installman`. When no language can be found for a given source, 

1221 `debputy` behaves like language was `C`. 

1222 - When `language` is `derive-from-basename`, then `debputy` attempts to derive 

1223 the language from the basename (`foo.<language>.1`) similar to `dh_installman` 

1224 previous default. When no language can be found for a given source, `debputy` 

1225 behaves like language was `C`. Note this is prone to false positives where 

1226 `.pl`, `.so` or similar two-letter extensions gets mistaken for a language code 

1227 (`.pl` can both be "Polish" or "Perl Script", `.so` can both be "Somali" and 

1228 "Shared Object" documentation). In this configuration, such extensions are 

1229 always assumed to be a language. 

1230 """), 

1231 ), 

1232 *docs_from(DebputyParsedContentStandardConditional), 

1233 ], 

1234 reference_documentation_url=manifest_format_doc( 

1235 "install-manpages-install-man" 

1236 ), 

1237 ), 

1238 ) 

1239 api.pluggable_manifest_rule( 

1240 InstallRule, 

1241 MK_INSTALLATIONS_DISCARD, 

1242 ParsedInstallDiscardRule, 

1243 _install_discard_rule_handler, 

1244 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1245 inline_reference_documentation=reference_documentation( 

1246 title="Discard (or exclude) upstream provided paths (`discard`)", 

1247 description=textwrap.dedent("""\ 

1248 When installing paths from `debian/tmp` into packages, it might be useful to ignore 

1249 some paths that you never need installed. This can be done with the `discard` rule. 

1250 

1251 Once a path is discarded, it cannot be matched by any other install rules. A path 

1252 that is discarded, is considered handled when `debputy` checks for paths you might 

1253 have forgotten to install. The `discard` feature therefore *also* replaces the 

1254 `debian/not-installed` file used by `debhelper` and `cdbs`. 

1255 """), 

1256 non_mapping_description=textwrap.dedent("""\ 

1257 When the input is a string or a list of string, then that value is used as shorthand 

1258 for `path` or `paths` (respectively). 

1259 """), 

1260 attributes=[ 

1261 documented_attr( 

1262 ["path", "paths"], 

1263 textwrap.dedent("""\ 

1264 A path match (`path`) or a list of path matches (`paths`) defining the source 

1265 path(s) that should not be installed anywhere. The path match(es) can use globs. 

1266 - When a symlink is matched, then the symlink (not its target) is discarded as-is. 

1267 When a directory is matched, then the directory is discarded along with all the 

1268 contents that have not already been installed somewhere. 

1269 """), 

1270 ), 

1271 documented_attr( 

1272 ["search_dir", "search_dirs"], 

1273 textwrap.dedent("""\ 

1274 A path (`search-dir`) or a list to paths (`search-dirs`) that defines 

1275 which search directories apply to. This attribute is primarily useful 

1276 for source packages that uses "per package search dirs", and you want 

1277 to restrict a discard rule to a subset of the relevant search dirs. 

1278 Note all listed search directories must be either an explicit search 

1279 requested by the packager or a search directory that `debputy` 

1280 provided automatically (such as `debian/tmp`). Listing other paths 

1281 will make `debputy` report an error. 

1282 - Note that the `path` or `paths` must match at least one entry in 

1283 any of the search directories unless *none* of the search directories 

1284 exist (or the condition in `required-when` evaluates to false). When 

1285 none of the search directories exist, the discard rule is silently 

1286 skipped. This special-case enables you to have discard rules only 

1287 applicable to certain builds that are only performed conditionally. 

1288 """), 

1289 ), 

1290 documented_attr( 

1291 "required_when", 

1292 textwrap.dedent("""\ 

1293 A condition as defined in [Conditional rules](#conditional-rules). The discard 

1294 rule is always applied. When the conditional is present and evaluates to false, 

1295 the discard rule can silently match nothing.When the condition is absent, *or* 

1296 it evaluates to true, then each pattern provided must match at least one path. 

1297 """), 

1298 ), 

1299 ], 

1300 reference_documentation_url=manifest_format_doc( 

1301 "discard-or-exclude-upstream-provided-paths-discard" 

1302 ), 

1303 ), 

1304 ) 

1305 api.pluggable_manifest_rule( 

1306 InstallRule, 

1307 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1308 ParsedMultiDestInstallRule, 

1309 _multi_dest_install_rule_handler, 

1310 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1311 inline_reference_documentation=reference_documentation( 

1312 title=f"Multi destination install (`{MK_INSTALLATIONS_MULTI_DEST_INSTALL}`)", 

1313 description=textwrap.dedent("""\ 

1314 The `${RULE_NAME}` is a variant of the generic `install` rule that installs sources 

1315 into multiple destination paths. This is needed for the rare case where you want a 

1316 path to be installed *twice* (or more) into the *same* package. The rule is a two 

1317 "primary" uses. 

1318 

1319 1) The classic "install into directory" similar to the standard `dh_install`, 

1320 except you list 2+ destination directories. 

1321 2) The "install as" similar to `dh-exec`'s `foo => bar` feature, except you list 

1322 2+ `as` names. 

1323 """), 

1324 attributes=[ 

1325 documented_attr( 

1326 ["source", "sources"], 

1327 textwrap.dedent("""\ 

1328 A path match (`source`) or a list of path matches (`sources`) defining the 

1329 source path(s) to be installed. The path match(es) can use globs. Each match 

1330 is tried against default search directories. 

1331 - When a symlink is matched, then the symlink (not its target) is installed 

1332 as-is. When a directory is matched, then the directory is installed along 

1333 with all the contents that have not already been installed somewhere. 

1334 """), 

1335 ), 

1336 documented_attr( 

1337 "dest_dirs", 

1338 textwrap.dedent("""\ 

1339 A list of paths defining the destination *directories*. The value *cannot* use 

1340 globs, but can use substitution. It is mutually exclusive with `as` but must be 

1341 provided if `as` is not provided. The attribute must contain at least two paths 

1342 (if you do not have two paths, you want `install`). 

1343 """), 

1344 ), 

1345 documented_attr( 

1346 "into", 

1347 textwrap.dedent("""\ 

1348 Either a package name or a list of package names for which these paths should be 

1349 installed. This key is conditional on whether there are multiple binary packages listed 

1350 in `debian/control`. When there is only one binary package, then that binary is the 

1351 default for `into`. Otherwise, the key is required. 

1352 """), 

1353 ), 

1354 documented_attr( 

1355 "install_as", 

1356 textwrap.dedent("""\ 

1357 A list of paths, which defines all the places the source will be installed. 

1358 Each path must be a full path without globs (but can use substitution). 

1359 This option is mutually exclusive with `dest-dirs` and `sources` (but not 

1360 `source`). When `as` is given, then `source` must match exactly one 

1361 "not yet matched" path. The attribute must contain at least two paths 

1362 (if you do not have two paths, you want `install`). 

1363 """), 

1364 ), 

1365 *docs_from(DebputyParsedContentStandardConditional), 

1366 ], 

1367 reference_documentation_url=manifest_format_doc("generic-install-install"), 

1368 ), 

1369 ) 

1370 

1371 

1372def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None: 

1373 api.pluggable_manifest_rule( 

1374 TransformationRule, 

1375 "move", 

1376 TransformationMoveRuleSpec, 

1377 _transformation_move_handler, 

1378 inline_reference_documentation=reference_documentation( 

1379 title="Move transformation rule (`move`)", 

1380 description=textwrap.dedent("""\ 

1381 The move transformation rule is mostly only useful for single binary source packages, 

1382 where everything from upstream's build system is installed automatically into the package. 

1383 In those case, you might find yourself with some files that need to be renamed to match 

1384 Debian specific requirements. 

1385 

1386 This can be done with the `move` transformation rule, which is a rough emulation of the 

1387 `mv` command line tool. 

1388 """), 

1389 attributes=[ 

1390 documented_attr( 

1391 "source", 

1392 textwrap.dedent("""\ 

1393 A path match defining the source path(s) to be renamed. The value can use globs 

1394 and substitutions. 

1395 """), 

1396 ), 

1397 documented_attr( 

1398 "target", 

1399 textwrap.dedent("""\ 

1400 A path defining the target path. The value *cannot* use globs, but can use 

1401 substitution. If the target ends with a literal `/` (prior to substitution), 

1402 the target will *always* be a directory. 

1403 """), 

1404 ), 

1405 *docs_from(DebputyParsedContentStandardConditional), 

1406 ], 

1407 reference_documentation_url=manifest_format_doc( 

1408 "move-transformation-rule-move" 

1409 ), 

1410 ), 

1411 ) 

1412 api.pluggable_manifest_rule( 

1413 TransformationRule, 

1414 "remove", 

1415 TransformationRemoveRuleSpec, 

1416 _transformation_remove_handler, 

1417 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1418 inline_reference_documentation=reference_documentation( 

1419 title="Remove transformation rule (`remove`)", 

1420 description=textwrap.dedent("""\ 

1421 The remove transformation rule is mostly only useful for single binary source packages, 

1422 where everything from upstream's build system is installed automatically into the package. 

1423 In those case, you might find yourself with some files that are _not_ relevant for the 

1424 Debian package (but would be relevant for other distros or for non-distro local builds). 

1425 Common examples include `INSTALL` files or `LICENSE` files (when they are just a subset 

1426 of `debian/copyright`). 

1427 

1428 In the manifest, you can ask `debputy` to remove paths from the debian package by using 

1429 the `remove` transformation rule. 

1430 

1431 Note that `remove` removes paths from future glob matches and transformation rules. 

1432 """), 

1433 non_mapping_description=textwrap.dedent("""\ 

1434 When the input is a string or a list of string, then that value is used as shorthand 

1435 for `path` or `paths` (respectively). 

1436 """), 

1437 attributes=[ 

1438 documented_attr( 

1439 ["path", "paths"], 

1440 textwrap.dedent("""\ 

1441 A path match (`path`) or a list of path matches (`paths`) defining the 

1442 path(s) inside the package that should be removed. The path match(es) 

1443 can use globs. 

1444 - When a symlink is matched, then the symlink (not its target) is removed 

1445 as-is. When a directory is matched, then the directory is removed 

1446 along with all the contents. 

1447 """), 

1448 ), 

1449 documented_attr( 

1450 "keep_empty_parent_dirs", 

1451 textwrap.dedent("""\ 

1452 A boolean determining whether to prune parent directories that become 

1453 empty as a consequence of this rule. When provided and `true`, this 

1454 rule will leave empty directories behind. Otherwise, if this rule 

1455 causes a directory to become empty that directory will be removed. 

1456 """), 

1457 ), 

1458 documented_attr( 

1459 "when", 

1460 textwrap.dedent("""\ 

1461 A condition as defined in [Conditional rules](${MANIFEST_FORMAT_DOC}#conditional-rules). 

1462 This condition will be combined with the built-in condition provided by these rules 

1463 (rather than replacing it). 

1464 """), 

1465 ), 

1466 ], 

1467 reference_documentation_url=manifest_format_doc( 

1468 "remove-transformation-rule-remove" 

1469 ), 

1470 ), 

1471 ) 

1472 api.pluggable_manifest_rule( 

1473 TransformationRule, 

1474 "create-symlink", 

1475 CreateSymlinkRule, 

1476 _transformation_create_symlink, 

1477 inline_reference_documentation=reference_documentation( 

1478 title="Create symlinks transformation rule (`create-symlink`)", 

1479 description=textwrap.dedent("""\ 

1480 Often, the upstream build system will provide the symlinks for you. However, 

1481 in some cases, it is useful for the packager to define distribution specific 

1482 symlinks. This can be done via the `create-symlink` transformation rule. 

1483 """), 

1484 attributes=[ 

1485 documented_attr( 

1486 "path", 

1487 textwrap.dedent("""\ 

1488 The path that should be a symlink. The path may contain substitution 

1489 variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs. 

1490 Parent directories are implicitly created as necessary. 

1491 * Note that if `path` already exists, the behavior of this 

1492 transformation depends on the value of `replacement-rule`. 

1493 """), 

1494 ), 

1495 documented_attr( 

1496 "target", 

1497 textwrap.dedent("""\ 

1498 Where the symlink should point to. The target may contain substitution 

1499 variables such as `{{DEB_HOST_MULTIARCH}}` but _cannot_ use globs. 

1500 The link target is _not_ required to exist inside the package. 

1501 * The `debputy` tool will normalize the target according to the rules 

1502 of the Debian Policy. Use absolute or relative target at your own 

1503 preference. 

1504 """), 

1505 ), 

1506 documented_attr( 

1507 "replacement_rule", 

1508 textwrap.dedent("""\ 

1509 This attribute defines how to handle if `path` already exists. It can 

1510 be set to one of the following values: 

1511 - `error-if-exists`: When `path` already exists, `debputy` will 

1512 stop with an error. This is similar to `ln -s` semantics. 

1513 - `error-if-directory`: When `path` already exists, **and** it is 

1514 a directory, `debputy` will stop with an error. Otherwise, 

1515 remove the `path` first and then create the symlink. This is 

1516 similar to `ln -sf` semantics. 

1517 - `abort-on-non-empty-directory` (default): When `path` already 

1518 exists, then it will be removed provided it is a non-directory 

1519 **or** an *empty* directory and the symlink will then be 

1520 created. If the path is a *non-empty* directory, `debputy` 

1521 will stop with an error. 

1522 - `discard-existing`: When `path` already exists, it will be 

1523 removed. If the `path` is a directory, all its contents will 

1524 be removed recursively along with the directory. Finally, 

1525 the symlink is created. This is similar to having an explicit 

1526 `remove` rule just prior to the `create-symlink` that is 

1527 conditional on `path` existing (plus the condition defined in 

1528 `when` if any). 

1529 

1530 Keep in mind, that `replacement-rule` only applies if `path` exists. 

1531 If the symlink cannot be created, because a part of `path` exist and 

1532 is *not* a directory, then `create-symlink` will fail regardless of 

1533 the value in `replacement-rule`. 

1534 """), 

1535 ), 

1536 *docs_from(DebputyParsedContentStandardConditional), 

1537 ], 

1538 reference_documentation_url=manifest_format_doc( 

1539 "create-symlinks-transformation-rule-create-symlink" 

1540 ), 

1541 ), 

1542 ) 

1543 api.pluggable_manifest_rule( 

1544 TransformationRule, 

1545 "path-metadata", 

1546 PathManifestRule, 

1547 _transformation_path_metadata, 

1548 source_format=PathManifestSourceDictFormat, 

1549 inline_reference_documentation=reference_documentation( 

1550 title="Change path owner/group or mode (`path-metadata`)", 

1551 description=textwrap.dedent("""\ 

1552 The `debputy` command normalizes the path metadata (such as ownership and mode) similar 

1553 to `dh_fixperms`. For most packages, the default is what you want. However, in some 

1554 cases, the package has a special case or two that `debputy` does not cover. In that 

1555 case, you can tell `debputy` to use the metadata you want by using the `path-metadata` 

1556 transformation. 

1557 

1558 Common use-cases include setuid/setgid binaries (such `usr/bin/sudo`) or/and static 

1559 ownership (such as /usr/bin/write). 

1560 """), 

1561 attributes=[ 

1562 documented_attr( 

1563 ["path", "paths"], 

1564 textwrap.dedent("""\ 

1565 A path match (`path`) or a list of path matches (`paths`) defining the path(s) 

1566 inside the package that should be affected. The path match(es) can use globs 

1567 and substitution variables. Special-rules for matches: 

1568 - Symlinks are never followed and will never be matched by this rule. 

1569 - Directory handling depends on the `recursive` attribute. 

1570 """), 

1571 ), 

1572 documented_attr( 

1573 "owner", 

1574 textwrap.dedent("""\ 

1575 Denotes the owner of the paths matched by `path` or `paths`. When omitted, 

1576 no change of owner is done. 

1577 """), 

1578 ), 

1579 documented_attr( 

1580 "group", 

1581 textwrap.dedent("""\ 

1582 Denotes the group of the paths matched by `path` or `paths`. When omitted, 

1583 no change of group is done. 

1584 """), 

1585 ), 

1586 documented_attr( 

1587 "mode", 

1588 textwrap.dedent("""\ 

1589 Denotes the mode of the paths matched by `path` or `paths`. When omitted, 

1590 no change in mode is done. Note that numeric mode must always be given as 

1591 a string (i.e., with quotes). Symbolic mode can be used as well. If 

1592 symbolic mode uses a relative definition (e.g., `o-rx`), then it is 

1593 relative to the matched path's current mode. 

1594 """), 

1595 ), 

1596 documented_attr( 

1597 "capabilities", 

1598 textwrap.dedent("""\ 

1599 Denotes a Linux capability that should be applied to the path. When provided, 

1600 `debputy` will cause the capability to be applied to all *files* denoted by 

1601 the `path`/`paths` attribute on install (via `postinst configure`) provided 

1602 that `setcap` is installed on the system when the `postinst configure` is 

1603 run. 

1604 - If any non-file paths are matched, the `capabilities` will *not* be applied 

1605 to those paths. 

1606 

1607 """), 

1608 ), 

1609 documented_attr( 

1610 "capability_mode", 

1611 textwrap.dedent("""\ 

1612 Denotes the mode to apply to the path *if* the Linux capability denoted in 

1613 `capabilities` was successfully applied. If omitted, it defaults to `a-s` as 

1614 generally capabilities are used to avoid "setuid"/"setgid" binaries. The 

1615 `capability-mode` is relative to the *final* path mode (the mode of the path 

1616 in the produced `.deb`). The `capability-mode` attribute cannot be used if 

1617 `capabilities` is omitted. 

1618 """), 

1619 ), 

1620 documented_attr( 

1621 "recursive", 

1622 textwrap.dedent("""\ 

1623 When a directory is matched, then the metadata changes are applied to the 

1624 directory itself. When `recursive` is `true`, then the transformation is 

1625 *also* applied to all paths beneath the directory. The default value for 

1626 this attribute is `false`. 

1627 """), 

1628 ), 

1629 *docs_from(DebputyParsedContentStandardConditional), 

1630 ], 

1631 reference_documentation_url=manifest_format_doc( 

1632 "change-path-ownergroup-or-mode-path-metadata" 

1633 ), 

1634 ), 

1635 ) 

1636 api.pluggable_manifest_rule( 

1637 TransformationRule, 

1638 "create-directories", 

1639 EnsureDirectoryRule, 

1640 _transformation_mkdirs, 

1641 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1642 inline_reference_documentation=reference_documentation( 

1643 title="Create directories transformation rule (`create-directories`)", 

1644 description=textwrap.dedent("""\ 

1645 NOTE: This transformation is only really needed if you need to create an empty 

1646 directory somewhere in your package as an integration point. All `debputy` 

1647 transformations will create directories as required. 

1648 

1649 In most cases, upstream build systems and `debputy` will create all the relevant 

1650 directories. However, in some rare cases you may want to explicitly define a path 

1651 to be a directory. Maybe to silence a linter that is warning you about a directory 

1652 being empty, or maybe you need an empty directory that nothing else is creating for 

1653 you. This can be done via the `create-directories` transformation rule. 

1654 

1655 Unless you have a specific need for the mapping form, you are recommended to use the 

1656 shorthand form of just listing the directories you want created. 

1657 """), 

1658 non_mapping_description=textwrap.dedent("""\ 

1659 When the input is a string or a list of string, then that value is used as shorthand 

1660 for `path` or `paths` (respectively). 

1661 """), 

1662 attributes=[ 

1663 documented_attr( 

1664 ["path", "paths"], 

1665 textwrap.dedent("""\ 

1666 A path (`path`) or a list of path (`paths`) defining the path(s) inside the 

1667 package that should be created as directories. The path(es) _cannot_ use globs 

1668 but can use substitution variables. Parent directories are implicitly created 

1669 (with owner `root:root` and mode `0755` - only explicitly listed directories 

1670 are affected by the owner/mode options) 

1671 """), 

1672 ), 

1673 documented_attr( 

1674 "owner", 

1675 textwrap.dedent("""\ 

1676 Denotes the owner of the directory (but _not_ what is inside the directory). 

1677 Default is "root". 

1678 """), 

1679 ), 

1680 documented_attr( 

1681 "group", 

1682 textwrap.dedent("""\ 

1683 Denotes the group of the directory (but _not_ what is inside the directory). 

1684 Default is "root". 

1685 """), 

1686 ), 

1687 documented_attr( 

1688 "mode", 

1689 textwrap.dedent("""\ 

1690 Denotes the mode of the directory (but _not_ what is inside the directory). 

1691 Note that numeric mode must always be given as a string (i.e., with quotes). 

1692 Symbolic mode can be used as well. If symbolic mode uses a relative 

1693 definition (e.g., `o-rx`), then it is relative to the directory's current mode 

1694 (if it already exists) or `0755` if the directory is created by this 

1695 transformation. The default is "0755". 

1696 """), 

1697 ), 

1698 *docs_from(DebputyParsedContentStandardConditional), 

1699 ], 

1700 reference_documentation_url=manifest_format_doc( 

1701 "create-directories-transformation-rule-directories" 

1702 ), 

1703 ), 

1704 ) 

1705 

1706 

1707def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> None: 

1708 api.provide_manifest_keyword( 

1709 ManifestCondition, 

1710 "cross-compiling", 

1711 lambda *_: ManifestCondition.is_cross_building(), 

1712 ) 

1713 api.provide_manifest_keyword( 

1714 ManifestCondition, 

1715 "can-execute-compiled-binaries", 

1716 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 

1717 ) 

1718 api.provide_manifest_keyword( 

1719 ManifestCondition, 

1720 "run-build-time-tests", 

1721 lambda *_: ManifestCondition.run_build_time_tests(), 

1722 ) 

1723 

1724 api.pluggable_manifest_rule( 

1725 ManifestCondition, 

1726 "not", 

1727 MCNot, 

1728 _mc_not, 

1729 source_format=ManifestCondition, 

1730 ) 

1731 api.pluggable_manifest_rule( 

1732 ManifestCondition, 

1733 ["any-of", "all-of"], 

1734 MCAnyOfAllOf, 

1735 _mc_any_of, 

1736 source_format=list[ManifestCondition], 

1737 ) 

1738 api.pluggable_manifest_rule( 

1739 ManifestCondition, 

1740 "arch-matches", 

1741 MCArchMatches, 

1742 _mc_arch_matches, 

1743 source_format=str, 

1744 inline_reference_documentation=reference_documentation( 

1745 title="Architecture match condition `arch-matches`", 

1746 description=textwrap.dedent("""\ 

1747 Sometimes, a rule needs to be conditional on the architecture. 

1748 This can be done by using the `arch-matches` rule. In 99.99% 

1749 of the cases, `arch-matches` will be form you are looking for 

1750 and practically behaves like a comparison against 

1751 `dpkg-architecture -qDEB_HOST_ARCH`. 

1752 

1753 For the cross-compiling specialists or curious people: The 

1754 `arch-matches` rule behaves like a `package-context-arch-matches` 

1755 in the context of a binary package and like 

1756 `source-context-arch-matches` otherwise. The details of those 

1757 are covered in their own keywords. 

1758 """), 

1759 non_mapping_description=textwrap.dedent("""\ 

1760 The value must be a string in the form of a space separated list 

1761 architecture names or architecture wildcards (same syntax as the 

1762 architecture restriction in Build-Depends in debian/control except 

1763 there is no enclosing `[]` brackets). The names/wildcards can 

1764 optionally be prefixed by `!` to negate them. However, either 

1765 *all* names / wildcards must have negation or *none* of them may 

1766 have it. 

1767 """), 

1768 reference_documentation_url=manifest_format_doc( 

1769 "architecture-match-condition-arch-matches-mapping" 

1770 ), 

1771 ), 

1772 ) 

1773 

1774 context_arch_doc = reference_documentation( 

1775 title="Explicit source or binary package context architecture match condition" 

1776 " `source-context-arch-matches`, `package-context-arch-matches` (mapping)", 

1777 description=textwrap.dedent("""\ 

1778 **These are special-case conditions**. Unless you know that you have a very special-case, 

1779 you should probably use `arch-matches` instead. These conditions are aimed at people with 

1780 corner-case special architecture needs. It also assumes the reader is familiar with the 

1781 `arch-matches` condition. 

1782 

1783 To understand these rules, here is a quick primer on `debputy`'s concept of "source context" 

1784 vs "(binary) package context" architecture. For a native build, these two contexts are the 

1785 same except that in the package context an `Architecture: all` package always resolve to 

1786 `all` rather than `DEB_HOST_ARCH`. As a consequence, `debputy` forbids `arch-matches` and 

1787 `package-context-arch-matches` in the context of an `Architecture: all` package as a warning 

1788 to the packager that condition does not make sense. 

1789 

1790 In the very rare case that you need an architecture condition for an `Architecture: all` package, 

1791 you can use `source-context-arch-matches`. However, this means your `Architecture: all` package 

1792 is not reproducible between different build hosts (which has known to be relevant for some 

1793 very special cases). 

1794 

1795 Additionally, for the 0.0001% case you are building a cross-compiling compiler (that is, 

1796 `DEB_HOST_ARCH != DEB_TARGET_ARCH` and you are working with `gcc` or similar) `debputy` can be 

1797 instructed (opt-in) to use `DEB_TARGET_ARCH` rather than `DEB_HOST_ARCH` for certain packages when 

1798 evaluating an architecture condition in context of a binary package. This can be useful if the 

1799 compiler produces supporting libraries that need to be built for the `DEB_TARGET_ARCH` rather than 

1800 the `DEB_HOST_ARCH`. This is where `arch-matches` or `package-context-arch-matches` can differ 

1801 subtly from `source-context-arch-matches` in how they evaluate the condition. This opt-in currently 

1802 relies on setting `X-DH-Build-For-Type: target` for each of the relevant packages in 

1803 `debian/control`. However, unless you are a cross-compiling specialist, you will probably never 

1804 need to care about nor use any of this. 

1805 

1806 Accordingly, the possible conditions are: 

1807 

1808 * `arch-matches`: This is the form recommended to laymen and as the default use-case. This 

1809 conditional acts `package-context-arch-matches` if the condition is used in the context 

1810 of a binary package. Otherwise, it acts as `source-context-arch-matches`. 

1811 

1812 * `source-context-arch-matches`: With this conditional, the provided architecture constraint is compared 

1813 against the build time provided host architecture (`dpkg-architecture -qDEB_HOST_ARCH`). This can 

1814 be useful when an `Architecture: all` package needs an architecture condition for some reason. 

1815 

1816 * `package-context-arch-matches`: With this conditional, the provided architecture constraint is compared 

1817 against the package's resolved architecture. This condition can only be used in the context of a binary 

1818 package (usually, under `packages.<name>.`). If the package is an `Architecture: all` package, the 

1819 condition will fail with an error as the condition always have the same outcome. For all other 

1820 packages, the package's resolved architecture is the same as the build time provided host architecture 

1821 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1822 

1823 - However, as noted above there is a special case for when compiling a cross-compiling compiler, where 

1824 this behaves subtly different from `source-context-arch-matches`. 

1825 

1826 All conditions are used the same way as `arch-matches`. Simply replace `arch-matches` with the other 

1827 condition. See the `arch-matches` description for an example. 

1828 """), 

1829 non_mapping_description=textwrap.dedent("""\ 

1830 The value must be a string in the form of a space separated list 

1831 architecture names or architecture wildcards (same syntax as the 

1832 architecture restriction in Build-Depends in debian/control except 

1833 there is no enclosing `[]` brackets). The names/wildcards can 

1834 optionally be prefixed by `!` to negate them. However, either 

1835 *all* names / wildcards must have negation or *none* of them may 

1836 have it. 

1837 """), 

1838 ) 

1839 

1840 api.pluggable_manifest_rule( 

1841 ManifestCondition, 

1842 "source-context-arch-matches", 

1843 MCArchMatches, 

1844 _mc_source_context_arch_matches, 

1845 source_format=str, 

1846 inline_reference_documentation=context_arch_doc, 

1847 ) 

1848 api.pluggable_manifest_rule( 

1849 ManifestCondition, 

1850 "package-context-arch-matches", 

1851 MCArchMatches, 

1852 _mc_arch_matches, 

1853 source_format=str, 

1854 inline_reference_documentation=context_arch_doc, 

1855 ) 

1856 api.pluggable_manifest_rule( 

1857 ManifestCondition, 

1858 "build-profiles-matches", 

1859 MCBuildProfileMatches, 

1860 _mc_build_profile_matches, 

1861 source_format=str, 

1862 ) 

1863 

1864 

1865def register_maintscript_conditions(api: DebputyPluginInitializerProvider) -> None: 

1866 api.provide_manifest_keyword( 

1867 MaintscriptCondition, 

1868 "purge", 

1869 lambda *_: MaintscriptCondition.on_purge(), 

1870 inline_reference_documentation=reference_documentation( 

1871 title="When the package is being `purged` (`$RULE_NAME`)", 

1872 synopsis="When the package is being purged", 

1873 description=textwrap.dedent("""\ 

1874 The trigger is when `postrm` is run with its first argument 

1875 being `purge`. 

1876 """), 

1877 ), 

1878 ) 

1879 api.provide_manifest_keyword( 

1880 MaintscriptCondition, 

1881 "configure", 

1882 lambda *_: MaintscriptCondition.on_configure(), 

1883 inline_reference_documentation=reference_documentation( 

1884 title="When the package is about to end up in the `configured` state (`$RULE_NAME`)", 

1885 synopsis="When the package is about to end up in the configured state", 

1886 description=textwrap.dedent("""\ 

1887 The trigger is when `postinst` is run and the package would end up in the `configured` 

1888 state on success except the package being `triggered`. 

1889 

1890 This covers *more* cases than a simple `[ "$$1" = "configure" ]` as there are some 

1891 cases like `abort-deconfigure` where the package is expected to be fully operational 

1892 at the end of the script. In practice, it is the same logic needed in all these cases. 

1893 """), 

1894 ), 

1895 ) 

1896 api.provide_manifest_keyword( 

1897 MaintscriptCondition, 

1898 "initial-install", 

1899 lambda *_: MaintscriptCondition.on_initial_install(), 

1900 inline_reference_documentation=reference_documentation( 

1901 title="When the package is being installed and not upgraded (`$RULE_NAME`)", 

1902 synopsis="When the package is being installed (excluding upgrades)", 

1903 description=textwrap.dedent("""\ 

1904 The trigger is when `postinst` is run with `configure` as its first argument, 

1905 and `dpkg` provides no `old-version`. This is generally the first install, 

1906 but it can also happen with a `install, remove + purge, install`. 

1907 """), 

1908 ), 

1909 ) 

1910 api.pluggable_manifest_rule( 

1911 MaintscriptCondition, 

1912 "upgrade", 

1913 UpgradeFromVersion, 

1914 _parse_upgrade_from_version, 

1915 as_keyword_handler=lambda *_: MaintscriptCondition.on_upgrade(), 

1916 register_value=False, 

1917 inline_reference_documentation=reference_documentation( 

1918 title="When the package is being upgraded (`$RULE_NAME`)", 

1919 synopsis="When the package is being upgraded from a previous version", 

1920 description=textwrap.dedent("""\ 

1921 The trigger is when `postinst` is run with `configure` as its first argument, 

1922 and `dpkg` provides an `old-version`. 

1923 

1924 Can be used as a keyword to mean upgrade from any version: 

1925 

1926 ```yaml 

1927 maintscript-snippets: 

1928 - on: upgrade 

1929 snippet: run this on every upgrade 

1930 ``` 

1931 

1932 Alternatively, a `from-version` can be given, at which point the code is only 

1933 run when upgrading from that version or earlier. 

1934 ```yaml 

1935 maintscript-snippets: 

1936 - on: 

1937 upgrade: 

1938 from-version: "0.7" 

1939 snippet: run this on the first upgrade from <= 0.70 

1940 ``` 

1941 """), 

1942 attributes=[ 

1943 documented_attr( 

1944 "from_version", 

1945 textwrap.dedent("""\ 

1946 The latest version to upgrade from that should trigger the snippet. 

1947 """), 

1948 ) 

1949 ], 

1950 ), 

1951 ) 

1952 api.provide_manifest_keyword( 

1953 MaintscriptCondition, 

1954 "before-upgrade", 

1955 lambda *_: MaintscriptCondition.on_before_upgrade(), 

1956 inline_reference_documentation=reference_documentation( 

1957 title="When the package is about to be upgraded (`$RULE_NAME`)", 

1958 synopsis="When the package is about to be upgraded", 

1959 description=textwrap.dedent("""\ 

1960 The trigger is when `preinst` is run with `upgrade` as its first argument. 

1961 The second argument is the `old-version` and the third argument is `new-version`. 

1962 """), 

1963 ), 

1964 ) 

1965 api.provide_manifest_keyword( 

1966 MaintscriptCondition, 

1967 "before-removal", 

1968 lambda *_: MaintscriptCondition.on_before_removal(), 

1969 inline_reference_documentation=reference_documentation( 

1970 title="When the package is about to be removed (`$RULE_NAME`)", 

1971 synopsis="When the package is about to be removed", 

1972 description=textwrap.dedent("""\ 

1973 The trigger is when `prerm` is run with `remove` as its first argument. 

1974 

1975 This is before any files are removed from the file system. Note that 

1976 the package and its dependencies might be in the "Half-installed" 

1977 state if this occurs after a failed upgrade. Therefore, some 

1978 functionality of the package or its dependencies might not 

1979 be present. 

1980 

1981 This can be used to prevent the removal in some cases by having 

1982 the snippet fail. 

1983 """), 

1984 ), 

1985 ) 

1986 api.provide_manifest_keyword( 

1987 MaintscriptCondition, 

1988 "after-removal", 

1989 lambda *_: MaintscriptCondition.on_after_removal(), 

1990 inline_reference_documentation=reference_documentation( 

1991 title="When the package has been removed (`$RULE_NAME`)", 

1992 synopsis="When the package has been removed", 

1993 description=textwrap.dedent("""\ 

1994 The trigger is when `postrm` is run with `remove` as its first argument. 

1995 

1996 Most files will have been removed from the system and the package is now 

1997 gone when the snippet is run. This can be used to clean up things that 

1998 cannot be handled by the `clean-after-removal` feature. 

1999 

2000 Note the snippet must only rely on `Essential: yes` packages as those 

2001 are the only packages guaranteed to be present and *functional* at this 

2002 time. If the snippet still needs to call commands from non-essential 

2003 packages, then it must assume the command may fail in ways that cannot 

2004 be detected ahead of time (the command existing is *not* a reliable 

2005 indicator of it being able to run), and the snippet must then implement 

2006 a reasonable fallback instead rather than failing the script entirely. 

2007 

2008 The `conffiles` and other `purge`-only removed files (if any) might still 

2009 remain. 

2010 """), 

2011 ), 

2012 ) 

2013 api.pluggable_manifest_rule( 

2014 MaintscriptCondition, 

2015 "unconditionally-in-script", 

2016 UnconditionallyInScript, 

2017 _parse_unconditionally_in_script, 

2018 source_format=str, 

2019 register_value=False, 

2020 inline_reference_documentation=reference_documentation( 

2021 title="Unconditionally run something in a script (`$RULE_NAME`)", 

2022 synopsis="Unconditionally run something in a script (or bring-your-own condition)", 

2023 description=textwrap.dedent("""\ 

2024 Unconditionally insert a snippet in a given maintscript. 

2025 

2026 This can be useful when the code needs to go into a given script 

2027 and none of the other conditions apply. Remember to apply relevant 

2028 guards or conditions in the snippet. 

2029 

2030 Example: 

2031 

2032 ```yaml 

2033 maintscript-snippets: 

2034 - on: 

2035 unconditionally-in-script: postinst 

2036 snippet: "run this unconditionally in the postinst script" 

2037 ``` 

2038 """), 

2039 ), 

2040 ) 

2041 

2042 

2043def register_dpkg_conffile_rules(api: DebputyPluginInitializerProvider) -> None: 

2044 api.pluggable_manifest_rule( 

2045 DpkgMaintscriptHelperCommand, 

2046 "remove", 

2047 DpkgRemoveConffileRule, 

2048 _dpkg_conffile_remove, 

2049 inline_reference_documentation=None, # TODO: write and add 

2050 ) 

2051 

2052 api.pluggable_manifest_rule( 

2053 DpkgMaintscriptHelperCommand, 

2054 "rename", 

2055 DpkgRenameConffileRule, 

2056 _dpkg_conffile_rename, 

2057 inline_reference_documentation=None, # TODO: write and add 

2058 ) 

2059 

2060 

2061class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

2062 mode: NotRequired[FileSystemMode] 

2063 owner: NotRequired[StaticFileSystemOwner] 

2064 group: NotRequired[StaticFileSystemGroup] 

2065 

2066 

2067class PathManifestSourceDictFormat(_ModeOwnerBase): 

2068 path: NotRequired[ 

2069 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")] 

2070 ] 

2071 paths: NotRequired[list[FileSystemMatchRule]] 

2072 recursive: NotRequired[bool] 

2073 capabilities: NotRequired[Capability] 

2074 capability_mode: NotRequired[FileSystemMode] 

2075 

2076 

2077class PathManifestRule(_ModeOwnerBase): 

2078 paths: list[FileSystemMatchRule] 

2079 recursive: NotRequired[bool] 

2080 capabilities: NotRequired[Capability] 

2081 capability_mode: NotRequired[FileSystemMode] 

2082 

2083 

2084class EnsureDirectorySourceFormat(_ModeOwnerBase): 

2085 path: NotRequired[ 

2086 Annotated[FileSystemExactMatchRule, DebputyParseHint.target_attribute("paths")] 

2087 ] 

2088 paths: NotRequired[list[FileSystemExactMatchRule]] 

2089 

2090 

2091class EnsureDirectoryRule(_ModeOwnerBase): 

2092 paths: list[FileSystemExactMatchRule] 

2093 

2094 

2095class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

2096 path: FileSystemExactMatchRule 

2097 target: Annotated[SymlinkTarget, DebputyParseHint.not_path_error_hint()] 

2098 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

2099 

2100 

2101class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

2102 source: FileSystemMatchRule 

2103 target: FileSystemExactMatchRule 

2104 

2105 

2106class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

2107 paths: list[FileSystemMatchRule] 

2108 keep_empty_parent_dirs: NotRequired[bool] 

2109 

2110 

2111class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

2112 path: NotRequired[ 

2113 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")] 

2114 ] 

2115 paths: NotRequired[list[FileSystemMatchRule]] 

2116 keep_empty_parent_dirs: NotRequired[bool] 

2117 

2118 

2119class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2120 sources: NotRequired[list[FileSystemMatchRule]] 

2121 source: NotRequired[ 

2122 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")] 

2123 ] 

2124 into: NotRequired[ 

2125 Annotated[ 

2126 str | list[str], 

2127 DebputyParseHint.required_when_multi_binary(), 

2128 ] 

2129 ] 

2130 dest_dir: NotRequired[ 

2131 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2132 ] 

2133 install_as: NotRequired[ 

2134 Annotated[ 

2135 FileSystemExactMatchRule, 

2136 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"), 

2137 DebputyParseHint.manifest_attribute("as"), 

2138 DebputyParseHint.not_path_error_hint(), 

2139 ] 

2140 ] 

2141 

2142 

2143class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

2144 sources: NotRequired[list[FileSystemMatchRule]] 

2145 source: NotRequired[ 

2146 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")] 

2147 ] 

2148 into: NotRequired[ 

2149 Annotated[ 

2150 str | list[str], 

2151 DebputyParseHint.required_when_multi_binary( 

2152 package_types=PackageTypeSelector.DEB 

2153 ), 

2154 ] 

2155 ] 

2156 dest_dir: NotRequired[ 

2157 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2158 ] 

2159 install_as: NotRequired[ 

2160 Annotated[ 

2161 FileSystemExactMatchRule, 

2162 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dir"), 

2163 DebputyParseHint.manifest_attribute("as"), 

2164 DebputyParseHint.not_path_error_hint(), 

2165 ] 

2166 ] 

2167 

2168 

2169class ParsedInstallRule(DebputyParsedContentStandardConditional): 

2170 sources: list[FileSystemMatchRule] 

2171 into: NotRequired[list[BinaryPackage]] 

2172 dest_dir: NotRequired[FileSystemExactMatchRule] 

2173 install_as: NotRequired[FileSystemExactMatchRule] 

2174 

2175 

2176class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2177 sources: NotRequired[list[FileSystemMatchRule]] 

2178 source: NotRequired[ 

2179 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")] 

2180 ] 

2181 into: NotRequired[ 

2182 Annotated[ 

2183 str | list[str], 

2184 DebputyParseHint.required_when_multi_binary(), 

2185 ] 

2186 ] 

2187 dest_dirs: NotRequired[ 

2188 Annotated[ 

2189 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

2190 ] 

2191 ] 

2192 install_as: NotRequired[ 

2193 Annotated[ 

2194 list[FileSystemExactMatchRule], 

2195 DebputyParseHint.conflicts_with_source_attributes("sources", "dest_dirs"), 

2196 DebputyParseHint.not_path_error_hint(), 

2197 DebputyParseHint.manifest_attribute("as"), 

2198 ] 

2199 ] 

2200 

2201 

2202class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

2203 sources: list[FileSystemMatchRule] 

2204 into: NotRequired[list[BinaryPackage]] 

2205 dest_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2206 install_as: NotRequired[list[FileSystemExactMatchRule]] 

2207 

2208 

2209class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

2210 sources: list[FileSystemMatchRule] 

2211 into: NotRequired[list[BinaryPackage]] 

2212 

2213 

2214class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

2215 sources: NotRequired[list[FileSystemMatchRule]] 

2216 source: NotRequired[ 

2217 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")] 

2218 ] 

2219 into: NotRequired[ 

2220 Annotated[ 

2221 str | list[str], 

2222 DebputyParseHint.required_when_multi_binary( 

2223 package_types=PackageTypeSelector.DEB 

2224 ), 

2225 ] 

2226 ] 

2227 

2228 

2229class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

2230 sources: list[FileSystemMatchRule] 

2231 language: NotRequired[str] 

2232 section: NotRequired[int] 

2233 into: NotRequired[list[BinaryPackage]] 

2234 

2235 

2236class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

2237 sources: NotRequired[list[FileSystemMatchRule]] 

2238 source: NotRequired[ 

2239 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("sources")] 

2240 ] 

2241 language: NotRequired[str] 

2242 section: NotRequired[int] 

2243 into: NotRequired[ 

2244 Annotated[ 

2245 str | list[str], 

2246 DebputyParseHint.required_when_multi_binary( 

2247 package_types=PackageTypeSelector.DEB 

2248 ), 

2249 ] 

2250 ] 

2251 

2252 

2253class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2254 paths: NotRequired[list[FileSystemMatchRule]] 

2255 path: NotRequired[ 

2256 Annotated[FileSystemMatchRule, DebputyParseHint.target_attribute("paths")] 

2257 ] 

2258 search_dir: NotRequired[ 

2259 Annotated[ 

2260 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2261 ] 

2262 ] 

2263 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2264 required_when: NotRequired[ManifestCondition] 

2265 

2266 

2267class ParsedInstallDiscardRule(DebputyParsedContent): 

2268 paths: list[FileSystemMatchRule] 

2269 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2270 required_when: NotRequired[ManifestCondition] 

2271 

2272 

2273class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2274 prior_to_version: NotRequired[str] 

2275 owning_package: NotRequired[str] 

2276 

2277 

2278class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2279 source: str 

2280 target: str 

2281 

2282 

2283class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2284 path: str 

2285 

2286 

2287class MCAnyOfAllOf(DebputyParsedContent): 

2288 conditions: list[ManifestCondition] 

2289 

2290 

2291class MCNot(DebputyParsedContent): 

2292 negated_condition: ManifestCondition 

2293 

2294 

2295class MCArchMatches(DebputyParsedContent): 

2296 arch_matches: str 

2297 

2298 

2299class MCBuildProfileMatches(DebputyParsedContent): 

2300 build_profile_matches: str 

2301 

2302 

2303class UnconditionallyInScript(typing.TypedDict): 

2304 script_name: MaintscriptForBinary 

2305 

2306 

2307class UpgradeFromVersion(typing.TypedDict): 

2308 from_version: debian.debian_support.Version 

2309 

2310 

2311def _parse_filename( 

2312 filename: str, 

2313 attribute_path: AttributePath, 

2314 *, 

2315 allow_directories: bool = True, 

2316) -> str: 

2317 try: 

2318 normalized_path = _normalize_path(filename, with_prefix=False) 

2319 except ValueError as e: 

2320 raise ManifestParseException( 

2321 f'Error parsing the path "{filename}" defined in {attribute_path.path}: {e.args[0]}' 

2322 ) from None 

2323 if not allow_directories and filename.endswith("/"): 2323 ↛ 2324line 2323 didn't jump to line 2324 because the condition on line 2323 was never true

2324 raise ManifestParseException( 

2325 f'The path "{filename}" in {attribute_path.path} ends with "/" implying it is a directory,' 

2326 f" but this feature can only be used for files" 

2327 ) 

2328 if normalized_path == ".": 2328 ↛ 2329line 2328 didn't jump to line 2329 because the condition on line 2328 was never true

2329 raise ManifestParseException( 

2330 f'The path "{filename}" in {attribute_path.path} looks like the root directory,' 

2331 f" but this feature does not allow the root directory here." 

2332 ) 

2333 return normalized_path 

2334 

2335 

2336def _with_alt_form(t: type[TypedDict]): 

2337 return Union[ 

2338 t, 

2339 list[str], 

2340 str, 

2341 ] 

2342 

2343 

2344def _dpkg_conffile_rename( 

2345 _name: str, 

2346 parsed_data: DpkgRenameConffileRule, 

2347 path: AttributePath, 

2348 _context: ParserContextData, 

2349) -> DpkgMaintscriptHelperCommand: 

2350 source_file = parsed_data["source"] 

2351 target_file = parsed_data["target"] 

2352 normalized_source = _parse_filename( 

2353 source_file, 

2354 path["source"], 

2355 allow_directories=False, 

2356 ) 

2357 path.path_hint = source_file 

2358 

2359 normalized_target = _parse_filename( 

2360 target_file, 

2361 path["target"], 

2362 allow_directories=False, 

2363 ) 

2364 normalized_source = "/" + normalized_source 

2365 normalized_target = "/" + normalized_target 

2366 

2367 if normalized_source == normalized_target: 2367 ↛ 2368line 2367 didn't jump to line 2368 because the condition on line 2367 was never true

2368 raise ManifestParseException( 

2369 f"Invalid rename defined in {path.path}: The source and target path are the same!" 

2370 ) 

2371 

2372 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2373 parsed_data, path 

2374 ) 

2375 return DpkgMaintscriptHelperCommand.mv_conffile( 

2376 path, 

2377 normalized_source, 

2378 normalized_target, 

2379 version, 

2380 owning_package, 

2381 ) 

2382 

2383 

2384def _dpkg_conffile_remove( 

2385 _name: str, 

2386 parsed_data: DpkgRemoveConffileRule, 

2387 path: AttributePath, 

2388 _context: ParserContextData, 

2389) -> DpkgMaintscriptHelperCommand: 

2390 source_file = parsed_data["path"] 

2391 normalized_source = _parse_filename( 

2392 source_file, 

2393 path["path"], 

2394 allow_directories=False, 

2395 ) 

2396 path.path_hint = source_file 

2397 

2398 normalized_source = "/" + normalized_source 

2399 

2400 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2401 parsed_data, path 

2402 ) 

2403 return DpkgMaintscriptHelperCommand.rm_conffile( 

2404 path, 

2405 normalized_source, 

2406 version, 

2407 owning_package, 

2408 ) 

2409 

2410 

2411def _parse_conffile_prior_version_and_owning_package( 

2412 d: DpkgConffileManagementRuleBase, 

2413 attribute_path: AttributePath, 

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

2415 prior_version = d.get("prior_to_version") 

2416 owning_package = d.get("owning_package") 

2417 

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

2419 p = attribute_path["prior_to_version"] 

2420 raise ManifestParseException( 

2421 f"The {MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION} parameter in {p.path} must be a" 

2422 r" valid package version (i.e., match (?:\d+:)?\d[0-9A-Za-z.+:~]*(?:-[0-9A-Za-z.+:~]+)*)." 

2423 ) 

2424 

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

2426 p = attribute_path["owning_package"] 

2427 raise ManifestParseException( 

2428 f"The {MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE} parameter in {p.path} must be a valid" 

2429 f" package name (i.e., match {PKGNAME_REGEX.pattern})." 

2430 ) 

2431 

2432 return prior_version, owning_package 

2433 

2434 

2435def _install_rule_handler( 

2436 _name: str, 

2437 parsed_data: ParsedInstallRule, 

2438 path: AttributePath, 

2439 context: ParserContextData, 

2440) -> InstallRule: 

2441 sources = parsed_data["sources"] 

2442 install_as = parsed_data.get("install_as") 

2443 into = frozenset( 

2444 parsed_data.get("into") 

2445 or (context.single_binary_package(path, package_attribute="into"),) 

2446 ) 

2447 dest_dir = parsed_data.get("dest_dir") 

2448 condition = parsed_data.get("when") 

2449 if install_as is not None: 

2450 assert len(sources) == 1 

2451 assert dest_dir is None 

2452 return InstallRule.install_as( 

2453 sources[0], 

2454 install_as.match_rule.path, 

2455 into, 

2456 path.path, 

2457 condition, 

2458 ) 

2459 return InstallRule.install_dest( 

2460 sources, 

2461 dest_dir.match_rule.path if dest_dir is not None else None, 

2462 into, 

2463 path.path, 

2464 condition, 

2465 ) 

2466 

2467 

2468def _multi_dest_install_rule_handler( 

2469 _name: str, 

2470 parsed_data: ParsedMultiDestInstallRule, 

2471 path: AttributePath, 

2472 context: ParserContextData, 

2473) -> InstallRule: 

2474 sources = parsed_data["sources"] 

2475 install_as = parsed_data.get("install_as") 

2476 into = frozenset( 

2477 parsed_data.get("into") 

2478 or (context.single_binary_package(path, package_attribute="into"),) 

2479 ) 

2480 dest_dirs = parsed_data.get("dest_dirs") 

2481 condition = parsed_data.get("when") 

2482 if install_as is not None: 

2483 assert len(sources) == 1 

2484 assert dest_dirs is None 

2485 if len(install_as) < 2: 2485 ↛ 2486line 2485 didn't jump to line 2486 because the condition on line 2485 was never true

2486 raise ManifestParseException( 

2487 f"The {path['install_as'].path} attribute must contain at least two paths." 

2488 ) 

2489 return InstallRule.install_multi_as( 

2490 sources[0], 

2491 [p.match_rule.path for p in install_as], 

2492 into, 

2493 path.path, 

2494 condition, 

2495 ) 

2496 if dest_dirs is None: 2496 ↛ 2497line 2496 didn't jump to line 2497 because the condition on line 2496 was never true

2497 raise ManifestParseException( 

2498 f"Either the `as` or the `dest-dirs` key must be provided at {path.path}" 

2499 ) 

2500 if len(dest_dirs) < 2: 2500 ↛ 2501line 2500 didn't jump to line 2501 because the condition on line 2500 was never true

2501 raise ManifestParseException( 

2502 f"The {path['dest_dirs'].path} attribute must contain at least two paths." 

2503 ) 

2504 return InstallRule.install_multi_dest( 

2505 sources, 

2506 [dd.match_rule.path for dd in dest_dirs], 

2507 into, 

2508 path.path, 

2509 condition, 

2510 ) 

2511 

2512 

2513def _install_docs_rule_handler( 

2514 _name: str, 

2515 parsed_data: ParsedInstallRule, 

2516 path: AttributePath, 

2517 context: ParserContextData, 

2518) -> InstallRule: 

2519 sources = parsed_data["sources"] 

2520 install_as = parsed_data.get("install_as") 

2521 dest_dir = parsed_data.get("dest_dir") 

2522 condition = parsed_data.get("when") 

2523 into = frozenset( 

2524 parsed_data.get("into") 

2525 or ( 

2526 context.single_binary_package( 

2527 path, 

2528 package_types=PackageTypeSelector.DEB, 

2529 package_attribute="into", 

2530 ), 

2531 ) 

2532 ) 

2533 if install_as is not None: 2533 ↛ 2534line 2533 didn't jump to line 2534 because the condition on line 2533 was never true

2534 assert len(sources) == 1 

2535 assert dest_dir is None 

2536 return InstallRule.install_doc_as( 

2537 sources[0], 

2538 install_as.match_rule.path, 

2539 into, 

2540 path.path, 

2541 condition, 

2542 ) 

2543 return InstallRule.install_doc( 

2544 sources, 

2545 None if dest_dir is None else dest_dir.raw_match_rule, 

2546 into, 

2547 path.path, 

2548 condition, 

2549 ) 

2550 

2551 

2552def _install_examples_rule_handler( 

2553 _name: str, 

2554 parsed_data: ParsedInstallExamplesRule, 

2555 path: AttributePath, 

2556 context: ParserContextData, 

2557) -> InstallRule: 

2558 return InstallRule.install_examples( 

2559 sources=parsed_data["sources"], 

2560 into=frozenset( 

2561 parsed_data.get("into") 

2562 or ( 

2563 context.single_binary_package( 

2564 path, 

2565 package_types=PackageTypeSelector.DEB, 

2566 package_attribute="into", 

2567 ), 

2568 ) 

2569 ), 

2570 definition_source=path.path, 

2571 condition=parsed_data.get("when"), 

2572 ) 

2573 

2574 

2575def _install_man_rule_handler( 

2576 _name: str, 

2577 parsed_data: ParsedInstallManpageRule, 

2578 attribute_path: AttributePath, 

2579 context: ParserContextData, 

2580) -> InstallRule: 

2581 sources = parsed_data["sources"] 

2582 language = parsed_data.get("language") 

2583 section = parsed_data.get("section") 

2584 

2585 if language is not None: 

2586 is_lang_ok = language in ( 

2587 "C", 

2588 "derive-from-basename", 

2589 "derive-from-path", 

2590 ) 

2591 

2592 if not is_lang_ok and len(language) == 2 and language.islower(): 2592 ↛ 2593line 2592 didn't jump to line 2593 because the condition on line 2592 was never true

2593 is_lang_ok = True 

2594 

2595 if ( 2595 ↛ 2602line 2595 didn't jump to line 2602 because the condition on line 2595 was never true

2596 not is_lang_ok 

2597 and len(language) == 5 

2598 and language[2] == "_" 

2599 and language[:2].islower() 

2600 and language[3:].isupper() 

2601 ): 

2602 is_lang_ok = True 

2603 

2604 if not is_lang_ok: 2604 ↛ 2605line 2604 didn't jump to line 2605 because the condition on line 2604 was never true

2605 raise ManifestParseException( 

2606 f'The language attribute must in a 2-letter language code ("de"), a 5-letter language + dialect' 

2607 f' code ("pt_BR"), "derive-from-basename", "derive-from-path", or omitted. The problematic' 

2608 f' definition is {attribute_path["language"]}' 

2609 ) 

2610 

2611 if section is not None and (section < 1 or section > 10): 2611 ↛ 2612line 2611 didn't jump to line 2612 because the condition on line 2611 was never true

2612 raise ManifestParseException( 

2613 f"The section attribute must in the range [1-9] or omitted. The problematic definition is" 

2614 f' {attribute_path["section"]}' 

2615 ) 

2616 if section is None and any(s.raw_match_rule.endswith(".gz") for s in sources): 2616 ↛ 2617line 2616 didn't jump to line 2617 because the condition on line 2616 was never true

2617 raise ManifestParseException( 

2618 "Sorry, compressed man pages are not supported without an explicit `section` definition at the moment." 

2619 " This limitation may be removed in the future. Problematic definition from" 

2620 f' {attribute_path["sources"]}' 

2621 ) 

2622 if any(s.raw_match_rule.endswith("/") for s in sources): 2622 ↛ 2623line 2622 didn't jump to line 2623 because the condition on line 2622 was never true

2623 raise ManifestParseException( 

2624 'The install-man rule can only match non-directories. Therefore, none of the sources can end with "/".' 

2625 " as that implies the source is for a directory. Problematic definition from" 

2626 f' {attribute_path["sources"]}' 

2627 ) 

2628 return InstallRule.install_man( 

2629 sources=sources, 

2630 into=frozenset( 

2631 parsed_data.get("into") 

2632 or ( 

2633 context.single_binary_package( 

2634 attribute_path, 

2635 package_types=PackageTypeSelector.DEB, 

2636 package_attribute="into", 

2637 ), 

2638 ) 

2639 ), 

2640 section=section, 

2641 language=language, 

2642 definition_source=attribute_path.path, 

2643 condition=parsed_data.get("when"), 

2644 ) 

2645 

2646 

2647def _install_discard_rule_handler( 

2648 _name: str, 

2649 parsed_data: ParsedInstallDiscardRule, 

2650 path: AttributePath, 

2651 _context: ParserContextData, 

2652) -> InstallRule: 

2653 limit_to = parsed_data.get("search_dirs") 

2654 if limit_to is not None and not limit_to: 2654 ↛ 2655line 2654 didn't jump to line 2655 because the condition on line 2654 was never true

2655 p = path["search_dirs"] 

2656 raise ManifestParseException(f"The {p.path} attribute must not be empty.") 

2657 condition = parsed_data.get("required_when") 

2658 return InstallRule.discard_paths( 

2659 parsed_data["paths"], 

2660 path.path, 

2661 condition, 

2662 limit_to=limit_to, 

2663 ) 

2664 

2665 

2666def _transformation_move_handler( 

2667 _name: str, 

2668 parsed_data: TransformationMoveRuleSpec, 

2669 path: AttributePath, 

2670 _context: ParserContextData, 

2671) -> TransformationRule: 

2672 source_match = parsed_data["source"] 

2673 target_path = parsed_data["target"].match_rule.path 

2674 condition = parsed_data.get("when") 

2675 

2676 if ( 2676 ↛ 2680line 2676 didn't jump to line 2680 because the condition on line 2676 was never true

2677 isinstance(source_match, ExactFileSystemPath) 

2678 and source_match.path == target_path 

2679 ): 

2680 raise ManifestParseException( 

2681 f"The transformation rule {path.path} requests a move of {source_match} to" 

2682 f" {target_path}, which is the same path" 

2683 ) 

2684 return MoveTransformationRule( 

2685 source_match.match_rule, 

2686 target_path, 

2687 target_path.endswith("/"), 

2688 path, 

2689 condition, 

2690 ) 

2691 

2692 

2693def _transformation_remove_handler( 

2694 _name: str, 

2695 parsed_data: TransformationRemoveRuleSpec, 

2696 attribute_path: AttributePath, 

2697 _context: ParserContextData, 

2698) -> TransformationRule: 

2699 paths = parsed_data["paths"] 

2700 keep_empty_parent_dirs = parsed_data.get("keep_empty_parent_dirs", False) 

2701 

2702 return RemoveTransformationRule( 

2703 [m.match_rule for m in paths], 

2704 keep_empty_parent_dirs, 

2705 attribute_path, 

2706 ) 

2707 

2708 

2709def _transformation_create_symlink( 

2710 _name: str, 

2711 parsed_data: CreateSymlinkRule, 

2712 attribute_path: AttributePath, 

2713 _context: ParserContextData, 

2714) -> TransformationRule: 

2715 link_dest = parsed_data["path"].match_rule.path 

2716 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2717 "replacement_rule", 

2718 "abort-on-non-empty-directory", 

2719 ) 

2720 try: 

2721 link_target = debian_policy_normalize_symlink_target( 

2722 link_dest, 

2723 parsed_data["target"].symlink_target, 

2724 ) 

2725 except ValueError as e: # pragma: no cover 

2726 raise AssertionError( 

2727 "Debian Policy normalization should not raise ValueError here" 

2728 ) from e 

2729 

2730 condition = parsed_data.get("when") 

2731 

2732 return CreateSymlinkPathTransformationRule( 

2733 link_target, 

2734 link_dest, 

2735 replacement_rule, 

2736 attribute_path, 

2737 condition, 

2738 ) 

2739 

2740 

2741def _transformation_path_metadata( 

2742 _name: str, 

2743 parsed_data: PathManifestRule, 

2744 attribute_path: AttributePath, 

2745 context: ParserContextData, 

2746) -> TransformationRule: 

2747 match_rules = parsed_data["paths"] 

2748 owner = parsed_data.get("owner") 

2749 group = parsed_data.get("group") 

2750 mode = parsed_data.get("mode") 

2751 recursive = parsed_data.get("recursive", False) 

2752 capabilities = parsed_data.get("capabilities") 

2753 capability_mode = parsed_data.get("capability_mode") 

2754 cap: str | None = None 

2755 

2756 if capabilities is not None: 2756 ↛ 2757line 2756 didn't jump to line 2757 because the condition on line 2756 was never true

2757 check_integration_mode( 

2758 attribute_path["capabilities"], 

2759 context, 

2760 _NOT_INTEGRATION_RRR, 

2761 ) 

2762 if capability_mode is None: 

2763 capability_mode = SymbolicMode.parse_filesystem_mode( 

2764 "a-s", 

2765 attribute_path["capability-mode"], 

2766 ) 

2767 cap = capabilities.value 

2768 validate_cap = check_cap_checker() 

2769 validate_cap(cap, attribute_path["capabilities"].path) 

2770 elif capability_mode is not None and capabilities is None: 2770 ↛ 2771line 2770 didn't jump to line 2771 because the condition on line 2770 was never true

2771 check_integration_mode( 

2772 attribute_path["capability_mode"], 

2773 context, 

2774 _NOT_INTEGRATION_RRR, 

2775 ) 

2776 raise ManifestParseException( 

2777 "The attribute capability-mode cannot be provided without capabilities" 

2778 f" in {attribute_path.path}" 

2779 ) 

2780 if owner is None and group is None and mode is None and capabilities is None: 2780 ↛ 2781line 2780 didn't jump to line 2781 because the condition on line 2780 was never true

2781 raise ManifestParseException( 

2782 "At least one of owner, group, mode, or capabilities must be provided" 

2783 f" in {attribute_path.path}" 

2784 ) 

2785 condition = parsed_data.get("when") 

2786 

2787 return PathMetadataTransformationRule( 

2788 [m.match_rule for m in match_rules], 

2789 owner, 

2790 group, 

2791 mode, 

2792 recursive, 

2793 cap, 

2794 capability_mode, 

2795 attribute_path.path, 

2796 condition, 

2797 ) 

2798 

2799 

2800def _transformation_mkdirs( 

2801 _name: str, 

2802 parsed_data: EnsureDirectoryRule, 

2803 attribute_path: AttributePath, 

2804 _context: ParserContextData, 

2805) -> TransformationRule: 

2806 provided_paths = parsed_data["paths"] 

2807 owner = parsed_data.get("owner") 

2808 group = parsed_data.get("group") 

2809 mode = parsed_data.get("mode") 

2810 

2811 condition = parsed_data.get("when") 

2812 

2813 return CreateDirectoryTransformationRule( 

2814 [p.match_rule.path for p in provided_paths], 

2815 owner, 

2816 group, 

2817 mode, 

2818 attribute_path.path, 

2819 condition, 

2820 ) 

2821 

2822 

2823def _at_least_two( 

2824 content: list[Any], 

2825 attribute_path: AttributePath, 

2826 attribute_name: str, 

2827) -> None: 

2828 if len(content) < 2: 2828 ↛ 2829line 2828 didn't jump to line 2829 because the condition on line 2828 was never true

2829 raise ManifestParseException( 

2830 f"Must have at least two conditions in {attribute_path[attribute_name].path}" 

2831 ) 

2832 

2833 

2834def _mc_any_of( 

2835 name: str, 

2836 parsed_data: MCAnyOfAllOf, 

2837 attribute_path: AttributePath, 

2838 _context: ParserContextData, 

2839) -> ManifestCondition: 

2840 conditions = parsed_data["conditions"] 

2841 _at_least_two(conditions, attribute_path, "conditions") 

2842 if name == "any-of": 2842 ↛ 2843line 2842 didn't jump to line 2843 because the condition on line 2842 was never true

2843 return ManifestCondition.any_of(conditions) 

2844 assert name == "all-of" 

2845 return ManifestCondition.all_of(conditions) 

2846 

2847 

2848def _mc_not( 

2849 _name: str, 

2850 parsed_data: MCNot, 

2851 _attribute_path: AttributePath, 

2852 _context: ParserContextData, 

2853) -> ManifestCondition: 

2854 condition = parsed_data["negated_condition"] 

2855 return condition.negated() 

2856 

2857 

2858def _extract_arch_matches( 

2859 parsed_data: MCArchMatches, 

2860 attribute_path: AttributePath, 

2861) -> list[str]: 

2862 arch_matches_as_str = parsed_data["arch_matches"] 

2863 # Can we check arch list for typos? If we do, it must be tight in how close matches it does. 

2864 # Consider "arm" vs. "armel" (edit distance 2, but both are valid). Likewise, names often 

2865 # include a bit indicator "foo", "foo32", "foo64" - all of these have an edit distance of 2 

2866 # of each other. 

2867 arch_matches_as_list = arch_matches_as_str.split() 

2868 attr_path = attribute_path["arch_matches"] 

2869 if not arch_matches_as_list: 2869 ↛ 2870line 2869 didn't jump to line 2870 because the condition on line 2869 was never true

2870 raise ManifestParseException( 

2871 f"The condition at {attr_path.path} must not be empty" 

2872 ) 

2873 

2874 if arch_matches_as_list[0].startswith("[") or arch_matches_as_list[-1].endswith( 2874 ↛ 2877line 2874 didn't jump to line 2877 because the condition on line 2874 was never true

2875 "]" 

2876 ): 

2877 raise ManifestParseException( 

2878 f"The architecture match at {attr_path.path} must be defined without enclosing it with " 

2879 '"[" or/and "]" brackets' 

2880 ) 

2881 return arch_matches_as_list 

2882 

2883 

2884def _mc_source_context_arch_matches( 

2885 _name: str, 

2886 parsed_data: MCArchMatches, 

2887 attribute_path: AttributePath, 

2888 _context: ParserContextData, 

2889) -> ManifestCondition: 

2890 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2891 return SourceContextArchMatchManifestCondition(arch_matches) 

2892 

2893 

2894def _mc_package_context_arch_matches( 

2895 name: str, 

2896 parsed_data: MCArchMatches, 

2897 attribute_path: AttributePath, 

2898 context: ParserContextData, 

2899) -> ManifestCondition: 

2900 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2901 

2902 if not context.is_in_binary_package_state: 2902 ↛ 2903line 2902 didn't jump to line 2903 because the condition on line 2902 was never true

2903 raise ManifestParseException( 

2904 f'The condition "{name}" at {attribute_path.path} can only be used in the context of a binary package.' 

2905 ) 

2906 

2907 package_state = context.current_binary_package_state 

2908 if package_state.binary_package.is_arch_all: 2908 ↛ 2909line 2908 didn't jump to line 2909 because the condition on line 2908 was never true

2909 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2910 "all", arch_matches 

2911 ) 

2912 attr_path = attribute_path["arch_matches"] 

2913 raise ManifestParseException( 

2914 f"The package architecture restriction at {attr_path.path} is applied to the" 

2915 f' "Architecture: all" package {package_state.binary_package.name}, which does not make sense' 

2916 f" as the condition will always resolves to `{str(result).lower()}`." 

2917 f" If you **really** need an architecture specific constraint for this rule, consider using" 

2918 f' "source-context-arch-matches" instead. However, this is a very rare use-case!' 

2919 ) 

2920 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2921 

2922 

2923def _mc_arch_matches( 

2924 name: str, 

2925 parsed_data: MCArchMatches, 

2926 attribute_path: AttributePath, 

2927 context: ParserContextData, 

2928) -> ManifestCondition: 

2929 if context.is_in_binary_package_state: 

2930 return _mc_package_context_arch_matches( 

2931 name, parsed_data, attribute_path, context 

2932 ) 

2933 return _mc_source_context_arch_matches(name, parsed_data, attribute_path, context) 

2934 

2935 

2936def _mc_build_profile_matches( 

2937 _name: str, 

2938 parsed_data: MCBuildProfileMatches, 

2939 attribute_path: AttributePath, 

2940 _context: ParserContextData, 

2941) -> ManifestCondition: 

2942 build_profile_spec = parsed_data["build_profile_matches"].strip() 

2943 attr_path = attribute_path["build_profile_matches"] 

2944 if not build_profile_spec: 2944 ↛ 2945line 2944 didn't jump to line 2945 because the condition on line 2944 was never true

2945 raise ManifestParseException( 

2946 f"The condition at {attr_path.path} must not be empty" 

2947 ) 

2948 try: 

2949 active_profiles_match(build_profile_spec, frozenset()) 

2950 except ValueError as e: 

2951 raise ManifestParseException( 

2952 f"Could not parse the build specification at {attr_path.path}: {e.args[0]}" 

2953 ) 

2954 return BuildProfileMatch(build_profile_spec) 

2955 

2956 

2957def _parse_unconditionally_in_script( 

2958 _name: str, 

2959 parsed_data: UnconditionallyInScript, 

2960 _path: AttributePath, 

2961 _context: ParserContextData, 

2962) -> MaintscriptCondition: 

2963 script_name = parsed_data["script_name"] 

2964 return MaintscriptCondition.on_unconditionally_in_script(script_name.name) 

2965 

2966 

2967def _parse_upgrade_from_version( 

2968 _name: str, 

2969 parsed_data: UpgradeFromVersion, 

2970 _path: AttributePath, 

2971 _context: ParserContextData, 

2972) -> MaintscriptCondition: 

2973 from_version = parsed_data["from_version"] 

2974 return MaintscriptCondition.on_upgrade_from(from_version)