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

496 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-09-06 13:40 +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_python_cache_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_python_cache_files, 

703 rule_reference_documentation="Discards any python cache files/directories including test/coverage related cache artifacts", 

704 examples=automatic_discard_rule_example( 

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

706 ".../__pycache__/", 

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

708 ".../foo.pyc", 

709 ".../foo.pyo", 

710 ".../.coverage", 

711 ".../.pdm-build/...", 

712 ".../.pytest_cache/...", 

713 ), 

714 ) 

715 api.automatic_discard_rule( 

716 "la-files", 

717 _debputy_prune_la_files, 

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

719 examples=automatic_discard_rule_example( 

720 "usr/lib/libfoo.la", 

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

722 ), 

723 ) 

724 api.automatic_discard_rule( 

725 "backup-files", 

726 _debputy_prune_backup_files, 

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

728 examples=( 

729 automatic_discard_rule_example( 

730 ".../foo~", 

731 ".../foo.orig", 

732 ".../foo.rej", 

733 ".../DEADJOE", 

734 ".../.foo.sw.", 

735 ), 

736 ), 

737 ) 

738 api.automatic_discard_rule( 

739 "version-control-paths", 

740 _debputy_prune_vcs_paths, 

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

742 examples=automatic_discard_rule_example( 

743 ("tools/foo", False), 

744 ".../CVS/", 

745 ".../CVS/...", 

746 ".../.gitignore", 

747 ".../.gitattributes", 

748 ".../.git/", 

749 ".../.git/...", 

750 ), 

751 ) 

752 api.automatic_discard_rule( 

753 "gnu-info-dir-file", 

754 _debputy_prune_info_dir_file, 

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

756 examples=automatic_discard_rule_example( 

757 "usr/share/info/dir", 

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

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

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

761 ), 

762 ) 

763 api.automatic_discard_rule( 

764 "debian-dir", 

765 _debputy_prune_binary_debian_dir, 

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

767 " literally in the file listing", 

768 examples=( 

769 automatic_discard_rule_example( 

770 "DEBIAN/", 

771 "DEBIAN/control", 

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

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

774 ), 

775 ), 

776 ) 

777 api.automatic_discard_rule( 

778 "doxygen-cruft-files", 

779 _debputy_prune_doxygen_cruft, 

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

781 examples=automatic_discard_rule_example( 

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

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

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

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

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

787 ), 

788 ) 

789 

790 

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

792 api.package_processor("manpages", process_manpages) 

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

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

795 api.package_processor( 

796 "strip-nondeterminism", 

797 cast("Any", strip_non_determinism), 

798 depends_on_processor=["manpages"], 

799 ) 

800 api.package_processor( 

801 "compression", 

802 apply_compression, 

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

804 ) 

805 

806 

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

808 api.manifest_variable_provider( 

809 load_source_variables, 

810 { 

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

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

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

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

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

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

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

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

819 this variable. 

820 """), 

821 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

822 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

823 }, 

824 ) 

825 

826 

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

828 api.document_builtin_variable( 

829 "PACKAGE", 

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

831 is_context_specific=True, 

832 ) 

833 

834 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

835 

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

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

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

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

840 {arch_var_doc} ({arch_type_tag}) 

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

842 * Machine type: {arch_type_doc} 

843 * Value description: {arch_var_doc} 

844 

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

846 """) 

847 api.document_builtin_variable( 

848 full_var, 

849 documentation, 

850 is_for_special_case=arch_type != "HOST", 

851 ) 

852 

853 

854def _format_docbase_filename( 

855 path_format: str, 

856 format_param: PPFFormatParam, 

857 docbase_file: VirtualPath, 

858) -> str: 

859 with docbase_file.open() as fd: 

860 content = Deb822(fd) 

861 proper_name = content["Document"] 

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

863 format_param["name"] = proper_name 

864 else: 

865 _warn( 

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

867 ) 

868 return path_format.format(**format_param) 

869 

870 

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

872 api.packager_provided_file( 

873 "doc-base", 

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

875 format_callback=_format_docbase_filename, 

876 ) 

877 

878 api.packager_provided_file( 

879 "shlibs", 

880 "DEBIAN/shlibs", 

881 allow_name_segment=False, 

882 reservation_only=True, 

883 reference_documentation=packager_provided_file_reference_documentation( 

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

885 ), 

886 ) 

887 api.packager_provided_file( 

888 "symbols", 

889 "DEBIAN/symbols", 

890 allow_name_segment=False, 

891 allow_architecture_segment=True, 

892 reservation_only=True, 

893 reference_documentation=packager_provided_file_reference_documentation( 

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

895 ), 

896 ) 

897 api.packager_provided_file( 

898 "conffiles", 

899 "DEBIAN/conffiles", 

900 allow_name_segment=False, 

901 allow_architecture_segment=True, 

902 reservation_only=True, 

903 ) 

904 api.packager_provided_file( 

905 "templates", 

906 "DEBIAN/templates", 

907 allow_name_segment=False, 

908 allow_architecture_segment=False, 

909 reservation_only=True, 

910 ) 

911 api.packager_provided_file( 

912 "alternatives", 

913 "DEBIAN/alternatives", 

914 allow_name_segment=False, 

915 allow_architecture_segment=True, 

916 reservation_only=True, 

917 ) 

918 

919 

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

921 api.pluggable_manifest_rule( 

922 InstallRule, 

923 MK_INSTALLATIONS_INSTALL, 

924 ParsedInstallRule, 

925 _install_rule_handler, 

926 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

927 inline_reference_documentation=reference_documentation( 

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

929 description=textwrap.dedent("""\ 

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

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

932 

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

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

935 

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

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

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

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

940 non_mapping_description=textwrap.dedent("""\ 

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

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

943 not required. 

944 """), 

945 attributes=[ 

946 documented_attr( 

947 ["source", "sources"], 

948 textwrap.dedent("""\ 

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

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

951 is tried against default search directories. 

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

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

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

955 """), 

956 ), 

957 documented_attr( 

958 "dest_dir", 

959 textwrap.dedent("""\ 

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

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

962 to the directory name of the `source`. 

963 """), 

964 ), 

965 documented_attr( 

966 "into", 

967 textwrap.dedent("""\ 

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

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

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

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

972 """), 

973 ), 

974 documented_attr( 

975 "install_as", 

976 textwrap.dedent("""\ 

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

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

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

980 """), 

981 ), 

982 *docs_from(DebputyParsedContentStandardConditional), 

983 ], 

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

985 ), 

986 ) 

987 api.pluggable_manifest_rule( 

988 InstallRule, 

989 [ 

990 MK_INSTALLATIONS_INSTALL_DOCS, 

991 "install-doc", 

992 ], 

993 ParsedInstallRule, 

994 _install_docs_rule_handler, 

995 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

996 inline_reference_documentation=reference_documentation( 

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

998 description=textwrap.dedent("""\ 

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

1000 `install` rule with the following key features: 

1001 

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

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

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

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

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

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

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

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

1010 package listed in `debian/control`. 

1011 

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

1013 

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

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

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

1017 use-case. 

1018 """), 

1019 non_mapping_description=textwrap.dedent("""\ 

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

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

1022 not required. 

1023 """), 

1024 attributes=[ 

1025 documented_attr( 

1026 ["source", "sources"], 

1027 textwrap.dedent("""\ 

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

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

1030 is tried against default search directories. 

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

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

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

1034 

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

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

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

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

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

1040 """), 

1041 ), 

1042 documented_attr( 

1043 "dest_dir", 

1044 textwrap.dedent("""\ 

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

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

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

1048 """), 

1049 ), 

1050 documented_attr( 

1051 "into", 

1052 textwrap.dedent("""\ 

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

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

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

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

1057 the key is required. 

1058 """), 

1059 ), 

1060 documented_attr( 

1061 "install_as", 

1062 textwrap.dedent("""\ 

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

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

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

1066 """), 

1067 ), 

1068 documented_attr( 

1069 "when", 

1070 textwrap.dedent("""\ 

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

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

1073 (rather than replacing it). 

1074 """), 

1075 ), 

1076 ], 

1077 reference_documentation_url=manifest_format_doc( 

1078 "install-documentation-install-docs" 

1079 ), 

1080 ), 

1081 ) 

1082 api.pluggable_manifest_rule( 

1083 InstallRule, 

1084 [ 

1085 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

1086 "install-example", 

1087 ], 

1088 ParsedInstallExamplesRule, 

1089 _install_examples_rule_handler, 

1090 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

1091 inline_reference_documentation=reference_documentation( 

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

1093 description=textwrap.dedent("""\ 

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

1095 install` rule with the following key features: 

1096 

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

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

1099 dir. 

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

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

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

1103 package listed in `debian/control`. 

1104 

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

1106 """), 

1107 non_mapping_description=textwrap.dedent("""\ 

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

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

1110 not required. 

1111 """), 

1112 attributes=[ 

1113 documented_attr( 

1114 ["source", "sources"], 

1115 textwrap.dedent("""\ 

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

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

1118 is tried against default search directories. 

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

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

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

1122 

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

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

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

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

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

1128 """), 

1129 ), 

1130 documented_attr( 

1131 "into", 

1132 textwrap.dedent("""\ 

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

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

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

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

1137 Otherwise, the key is required. 

1138 """), 

1139 ), 

1140 documented_attr( 

1141 "when", 

1142 textwrap.dedent("""\ 

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

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

1145 (rather than replacing it). 

1146 """), 

1147 ), 

1148 ], 

1149 reference_documentation_url=manifest_format_doc( 

1150 "install-examples-install-examples" 

1151 ), 

1152 ), 

1153 ) 

1154 api.pluggable_manifest_rule( 

1155 InstallRule, 

1156 MK_INSTALLATIONS_INSTALL_MAN, 

1157 ParsedInstallManpageRule, 

1158 _install_man_rule_handler, 

1159 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1160 inline_reference_documentation=reference_documentation( 

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

1162 description=textwrap.dedent("""\ 

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

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

1165 

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

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

1168 language. 

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

1170 package listed in `debian/control`. 

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

1172 for when the auto-detection is insufficient. 

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

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

1175 

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

1177 """), 

1178 non_mapping_description=textwrap.dedent("""\ 

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

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

1181 not required. 

1182 """), 

1183 attributes=[ 

1184 documented_attr( 

1185 ["source", "sources"], 

1186 textwrap.dedent("""\ 

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

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

1189 is tried against default search directories. 

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

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

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

1193 """), 

1194 ), 

1195 documented_attr( 

1196 "into", 

1197 textwrap.dedent("""\ 

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

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

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

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

1202 """), 

1203 ), 

1204 documented_attr( 

1205 "section", 

1206 textwrap.dedent("""\ 

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

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

1209 have performed. 

1210 """), 

1211 ), 

1212 documented_attr( 

1213 "language", 

1214 textwrap.dedent("""\ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1232 always assumed to be a language. 

1233 """), 

1234 ), 

1235 *docs_from(DebputyParsedContentStandardConditional), 

1236 ], 

1237 reference_documentation_url=manifest_format_doc( 

1238 "install-manpages-install-man" 

1239 ), 

1240 ), 

1241 ) 

1242 api.pluggable_manifest_rule( 

1243 InstallRule, 

1244 MK_INSTALLATIONS_DISCARD, 

1245 ParsedInstallDiscardRule, 

1246 _install_discard_rule_handler, 

1247 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1248 inline_reference_documentation=reference_documentation( 

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

1250 description=textwrap.dedent("""\ 

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

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

1253 

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

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

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

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

1258 """), 

1259 non_mapping_description=textwrap.dedent("""\ 

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

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

1262 """), 

1263 attributes=[ 

1264 documented_attr( 

1265 ["path", "paths"], 

1266 textwrap.dedent("""\ 

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

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

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

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

1271 contents that have not already been installed somewhere. 

1272 """), 

1273 ), 

1274 documented_attr( 

1275 ["search_dir", "search_dirs"], 

1276 textwrap.dedent("""\ 

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

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

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

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

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

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

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

1284 will make `debputy` report an error. 

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

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

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

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

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

1290 applicable to certain builds that are only performed conditionally. 

1291 """), 

1292 ), 

1293 documented_attr( 

1294 "required_when", 

1295 textwrap.dedent("""\ 

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

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

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

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

1300 """), 

1301 ), 

1302 ], 

1303 reference_documentation_url=manifest_format_doc( 

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

1305 ), 

1306 ), 

1307 ) 

1308 api.pluggable_manifest_rule( 

1309 InstallRule, 

1310 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1311 ParsedMultiDestInstallRule, 

1312 _multi_dest_install_rule_handler, 

1313 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1314 inline_reference_documentation=reference_documentation( 

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

1316 description=textwrap.dedent("""\ 

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

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

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

1320 "primary" uses. 

1321 

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

1323 except you list 2+ destination directories. 

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

1325 2+ `as` names. 

1326 """), 

1327 attributes=[ 

1328 documented_attr( 

1329 ["source", "sources"], 

1330 textwrap.dedent("""\ 

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

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

1333 is tried against default search directories. 

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

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

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

1337 """), 

1338 ), 

1339 documented_attr( 

1340 "dest_dirs", 

1341 textwrap.dedent("""\ 

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

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

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

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

1346 """), 

1347 ), 

1348 documented_attr( 

1349 "into", 

1350 textwrap.dedent("""\ 

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

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

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

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

1355 """), 

1356 ), 

1357 documented_attr( 

1358 "install_as", 

1359 textwrap.dedent("""\ 

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

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

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

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

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

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

1366 """), 

1367 ), 

1368 *docs_from(DebputyParsedContentStandardConditional), 

1369 ], 

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

1371 ), 

1372 ) 

1373 

1374 

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

1376 api.pluggable_manifest_rule( 

1377 TransformationRule, 

1378 "move", 

1379 TransformationMoveRuleSpec, 

1380 _transformation_move_handler, 

1381 inline_reference_documentation=reference_documentation( 

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

1383 description=textwrap.dedent("""\ 

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

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

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

1387 Debian specific requirements. 

1388 

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

1390 `mv` command line tool. 

1391 """), 

1392 attributes=[ 

1393 documented_attr( 

1394 "source", 

1395 textwrap.dedent("""\ 

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

1397 and substitutions. 

1398 """), 

1399 ), 

1400 documented_attr( 

1401 "target", 

1402 textwrap.dedent("""\ 

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

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

1405 the target will *always* be a directory. 

1406 """), 

1407 ), 

1408 *docs_from(DebputyParsedContentStandardConditional), 

1409 ], 

1410 reference_documentation_url=manifest_format_doc( 

1411 "move-transformation-rule-move" 

1412 ), 

1413 ), 

1414 ) 

1415 api.pluggable_manifest_rule( 

1416 TransformationRule, 

1417 "remove", 

1418 TransformationRemoveRuleSpec, 

1419 _transformation_remove_handler, 

1420 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1421 inline_reference_documentation=reference_documentation( 

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

1423 description=textwrap.dedent("""\ 

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

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

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

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

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

1429 of `debian/copyright`). 

1430 

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

1432 the `remove` transformation rule. 

1433 

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

1435 """), 

1436 non_mapping_description=textwrap.dedent("""\ 

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

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

1439 """), 

1440 attributes=[ 

1441 documented_attr( 

1442 ["path", "paths"], 

1443 textwrap.dedent("""\ 

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

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

1446 can use globs. 

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

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

1449 along with all the contents. 

1450 """), 

1451 ), 

1452 documented_attr( 

1453 "keep_empty_parent_dirs", 

1454 textwrap.dedent("""\ 

1455 A boolean determining whether to prune parent directories that become 

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

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

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

1459 """), 

1460 ), 

1461 documented_attr( 

1462 "when", 

1463 textwrap.dedent("""\ 

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

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

1466 (rather than replacing it). 

1467 """), 

1468 ), 

1469 ], 

1470 reference_documentation_url=manifest_format_doc( 

1471 "remove-transformation-rule-remove" 

1472 ), 

1473 ), 

1474 ) 

1475 api.pluggable_manifest_rule( 

1476 TransformationRule, 

1477 "create-symlink", 

1478 CreateSymlinkRule, 

1479 _transformation_create_symlink, 

1480 inline_reference_documentation=reference_documentation( 

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

1482 description=textwrap.dedent("""\ 

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

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

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

1486 """), 

1487 attributes=[ 

1488 documented_attr( 

1489 "path", 

1490 textwrap.dedent("""\ 

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

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

1493 Parent directories are implicitly created as necessary. 

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

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

1496 """), 

1497 ), 

1498 documented_attr( 

1499 "target", 

1500 textwrap.dedent("""\ 

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

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

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

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

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

1506 preference. 

1507 """), 

1508 ), 

1509 documented_attr( 

1510 "replacement_rule", 

1511 textwrap.dedent("""\ 

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

1513 be set to one of the following values: 

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

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

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

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

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

1519 similar to `ln -sf` semantics. 

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

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

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

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

1524 will stop with an error. 

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

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

1527 be removed recursively along with the directory. Finally, 

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

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

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

1531 `when` if any). 

1532 

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

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

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

1536 the value in `replacement-rule`. 

1537 """), 

1538 ), 

1539 *docs_from(DebputyParsedContentStandardConditional), 

1540 ], 

1541 reference_documentation_url=manifest_format_doc( 

1542 "create-symlinks-transformation-rule-create-symlink" 

1543 ), 

1544 ), 

1545 ) 

1546 api.pluggable_manifest_rule( 

1547 TransformationRule, 

1548 "path-metadata", 

1549 PathManifestRule, 

1550 _transformation_path_metadata, 

1551 source_format=PathManifestSourceDictFormat, 

1552 inline_reference_documentation=reference_documentation( 

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

1554 description=textwrap.dedent("""\ 

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

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

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

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

1559 transformation. 

1560 

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

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

1563 """), 

1564 attributes=[ 

1565 documented_attr( 

1566 ["path", "paths"], 

1567 textwrap.dedent("""\ 

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

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

1570 and substitution variables. Special-rules for matches: 

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

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

1573 """), 

1574 ), 

1575 documented_attr( 

1576 "owner", 

1577 textwrap.dedent("""\ 

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

1579 no change of owner is done. 

1580 """), 

1581 ), 

1582 documented_attr( 

1583 "group", 

1584 textwrap.dedent("""\ 

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

1586 no change of group is done. 

1587 """), 

1588 ), 

1589 documented_attr( 

1590 "mode", 

1591 textwrap.dedent("""\ 

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

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

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

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

1596 relative to the matched path's current mode. 

1597 """), 

1598 ), 

1599 documented_attr( 

1600 "capabilities", 

1601 textwrap.dedent("""\ 

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

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

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

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

1606 run. 

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

1608 to those paths. 

1609 

1610 """), 

1611 ), 

1612 documented_attr( 

1613 "capability_mode", 

1614 textwrap.dedent("""\ 

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

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

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

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

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

1620 `capabilities` is omitted. 

1621 """), 

1622 ), 

1623 documented_attr( 

1624 "recursive", 

1625 textwrap.dedent("""\ 

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

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

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

1629 this attribute is `false`. 

1630 """), 

1631 ), 

1632 *docs_from(DebputyParsedContentStandardConditional), 

1633 ], 

1634 reference_documentation_url=manifest_format_doc( 

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

1636 ), 

1637 ), 

1638 ) 

1639 api.pluggable_manifest_rule( 

1640 TransformationRule, 

1641 "create-directories", 

1642 EnsureDirectoryRule, 

1643 _transformation_mkdirs, 

1644 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1645 inline_reference_documentation=reference_documentation( 

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

1647 description=textwrap.dedent("""\ 

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

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

1650 transformations will create directories as required. 

1651 

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

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

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

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

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

1657 

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

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

1660 """), 

1661 non_mapping_description=textwrap.dedent("""\ 

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

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

1664 """), 

1665 attributes=[ 

1666 documented_attr( 

1667 ["path", "paths"], 

1668 textwrap.dedent("""\ 

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

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

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

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

1673 are affected by the owner/mode options) 

1674 """), 

1675 ), 

1676 documented_attr( 

1677 "owner", 

1678 textwrap.dedent("""\ 

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

1680 Default is "root". 

1681 """), 

1682 ), 

1683 documented_attr( 

1684 "group", 

1685 textwrap.dedent("""\ 

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

1687 Default is "root". 

1688 """), 

1689 ), 

1690 documented_attr( 

1691 "mode", 

1692 textwrap.dedent("""\ 

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

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

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

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

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

1698 transformation. The default is "0755". 

1699 """), 

1700 ), 

1701 *docs_from(DebputyParsedContentStandardConditional), 

1702 ], 

1703 reference_documentation_url=manifest_format_doc( 

1704 "create-directories-transformation-rule-directories" 

1705 ), 

1706 ), 

1707 ) 

1708 

1709 

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

1711 api.provide_manifest_keyword( 

1712 ManifestCondition, 

1713 "cross-compiling", 

1714 lambda *_: ManifestCondition.is_cross_building(), 

1715 ) 

1716 api.provide_manifest_keyword( 

1717 ManifestCondition, 

1718 "can-execute-compiled-binaries", 

1719 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 

1720 ) 

1721 api.provide_manifest_keyword( 

1722 ManifestCondition, 

1723 "run-build-time-tests", 

1724 lambda *_: ManifestCondition.run_build_time_tests(), 

1725 ) 

1726 

1727 api.pluggable_manifest_rule( 

1728 ManifestCondition, 

1729 "not", 

1730 MCNot, 

1731 _mc_not, 

1732 source_format=ManifestCondition, 

1733 ) 

1734 api.pluggable_manifest_rule( 

1735 ManifestCondition, 

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

1737 MCAnyOfAllOf, 

1738 _mc_any_of, 

1739 source_format=list[ManifestCondition], 

1740 ) 

1741 api.pluggable_manifest_rule( 

1742 ManifestCondition, 

1743 "arch-matches", 

1744 MCArchMatches, 

1745 _mc_arch_matches, 

1746 source_format=str, 

1747 inline_reference_documentation=reference_documentation( 

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

1749 description=textwrap.dedent("""\ 

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

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

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

1753 and practically behaves like a comparison against 

1754 `dpkg-architecture -qDEB_HOST_ARCH`. 

1755 

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

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

1758 in the context of a binary package and like 

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

1760 are covered in their own keywords. 

1761 """), 

1762 non_mapping_description=textwrap.dedent("""\ 

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

1764 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1769 have it. 

1770 """), 

1771 reference_documentation_url=manifest_format_doc( 

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

1773 ), 

1774 ), 

1775 ) 

1776 

1777 context_arch_doc = reference_documentation( 

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

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

1780 description=textwrap.dedent("""\ 

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

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

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

1784 `arch-matches` condition. 

1785 

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

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

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

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

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

1791 to the packager that condition does not make sense. 

1792 

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

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

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

1796 very special cases). 

1797 

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

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

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

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

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

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

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

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

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

1807 need to care about nor use any of this. 

1808 

1809 Accordingly, the possible conditions are: 

1810 

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

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

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

1814 

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

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

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

1818 

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

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

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

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

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

1824 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1825 

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

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

1828 

1829 All conditions are used the same way as `arch-matches`. See the `arch-matches` description for an example. 

1830 """), 

1831 non_mapping_description=textwrap.dedent("""\ 

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

1833 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1838 have it. 

1839 """), 

1840 ) 

1841 

1842 api.pluggable_manifest_rule( 

1843 ManifestCondition, 

1844 "source-context-arch-matches", 

1845 MCArchMatches, 

1846 _mc_source_context_arch_matches, 

1847 source_format=str, 

1848 inline_reference_documentation=context_arch_doc, 

1849 ) 

1850 api.pluggable_manifest_rule( 

1851 ManifestCondition, 

1852 "package-context-arch-matches", 

1853 MCArchMatches, 

1854 _mc_arch_matches, 

1855 source_format=str, 

1856 inline_reference_documentation=context_arch_doc, 

1857 ) 

1858 api.pluggable_manifest_rule( 

1859 ManifestCondition, 

1860 "build-profiles-matches", 

1861 MCBuildProfileMatches, 

1862 _mc_build_profile_matches, 

1863 source_format=str, 

1864 ) 

1865 

1866 

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

1868 api.provide_manifest_keyword( 

1869 MaintscriptCondition, 

1870 "purge", 

1871 lambda *_: MaintscriptCondition.on_purge(), 

1872 inline_reference_documentation=reference_documentation( 

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

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

1875 description=textwrap.dedent("""\ 

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

1877 being `purge`. 

1878 """), 

1879 ), 

1880 ) 

1881 api.provide_manifest_keyword( 

1882 MaintscriptCondition, 

1883 "configure", 

1884 lambda *_: MaintscriptCondition.on_configure(), 

1885 inline_reference_documentation=reference_documentation( 

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

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

1888 description=textwrap.dedent("""\ 

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

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

1891 

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

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

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

1895 """), 

1896 ), 

1897 ) 

1898 api.provide_manifest_keyword( 

1899 MaintscriptCondition, 

1900 "initial-install", 

1901 lambda *_: MaintscriptCondition.on_initial_install(), 

1902 inline_reference_documentation=reference_documentation( 

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

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

1905 description=textwrap.dedent("""\ 

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

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

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

1909 """), 

1910 ), 

1911 ) 

1912 api.pluggable_manifest_rule( 

1913 MaintscriptCondition, 

1914 "upgrade", 

1915 UpgradeFromVersion, 

1916 _parse_upgrade_from_version, 

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

1918 register_value=False, 

1919 inline_reference_documentation=reference_documentation( 

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

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

1922 description=textwrap.dedent("""\ 

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

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

1925 

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

1927 

1928 ```yaml 

1929 maintscript-snippets: 

1930 - on: upgrade 

1931 snippet: run this on every upgrade 

1932 ``` 

1933 

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

1935 run when upgrading from that version or earlier. 

1936 ```yaml 

1937 maintscript-snippets: 

1938 - on: 

1939 upgrade: 

1940 from-version: "0.7" 

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

1942 ``` 

1943 """), 

1944 attributes=[ 

1945 documented_attr( 

1946 "from_version", 

1947 textwrap.dedent("""\ 

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

1949 """), 

1950 ) 

1951 ], 

1952 ), 

1953 ) 

1954 api.provide_manifest_keyword( 

1955 MaintscriptCondition, 

1956 "before-upgrade", 

1957 lambda *_: MaintscriptCondition.on_before_upgrade(), 

1958 inline_reference_documentation=reference_documentation( 

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

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

1961 description=textwrap.dedent("""\ 

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

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

1964 """), 

1965 ), 

1966 ) 

1967 api.provide_manifest_keyword( 

1968 MaintscriptCondition, 

1969 "before-removal", 

1970 lambda *_: MaintscriptCondition.on_before_removal(), 

1971 inline_reference_documentation=reference_documentation( 

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

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

1974 description=textwrap.dedent("""\ 

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

1976 

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

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

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

1980 functionality of the package or its dependencies might not 

1981 be present. 

1982 

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

1984 the snippet fail. 

1985 """), 

1986 ), 

1987 ) 

1988 api.provide_manifest_keyword( 

1989 MaintscriptCondition, 

1990 "after-removal", 

1991 lambda *_: MaintscriptCondition.on_after_removal(), 

1992 inline_reference_documentation=reference_documentation( 

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

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

1995 description=textwrap.dedent("""\ 

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

1997 

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

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

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

2001 

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

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

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

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

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

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

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

2009 

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

2011 remain. 

2012 """), 

2013 ), 

2014 ) 

2015 api.pluggable_manifest_rule( 

2016 MaintscriptCondition, 

2017 "unconditionally-in-script", 

2018 UnconditionallyInScript, 

2019 _parse_unconditionally_in_script, 

2020 source_format=str, 

2021 register_value=False, 

2022 inline_reference_documentation=reference_documentation( 

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

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

2025 description=textwrap.dedent("""\ 

2026 Unconditionally insert a snippet in a given maintscript. 

2027 

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

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

2030 guards or conditions in the snippet. 

2031 

2032 Example: 

2033 

2034 ```yaml 

2035 maintscript-snippets: 

2036 - on: 

2037 unconditionally-in-script: postinst 

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

2039 ``` 

2040 """), 

2041 ), 

2042 ) 

2043 

2044 

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

2046 api.pluggable_manifest_rule( 

2047 DpkgMaintscriptHelperCommand, 

2048 "remove", 

2049 DpkgRemoveConffileRule, 

2050 _dpkg_conffile_remove, 

2051 inline_reference_documentation=None, # TODO: write and add 

2052 ) 

2053 

2054 api.pluggable_manifest_rule( 

2055 DpkgMaintscriptHelperCommand, 

2056 "rename", 

2057 DpkgRenameConffileRule, 

2058 _dpkg_conffile_rename, 

2059 inline_reference_documentation=None, # TODO: write and add 

2060 ) 

2061 

2062 

2063class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

2064 mode: NotRequired[FileSystemMode] 

2065 owner: NotRequired[StaticFileSystemOwner] 

2066 group: NotRequired[StaticFileSystemGroup] 

2067 

2068 

2069class PathManifestSourceDictFormat(_ModeOwnerBase): 

2070 path: NotRequired[ 

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

2072 ] 

2073 paths: NotRequired[list[FileSystemMatchRule]] 

2074 recursive: NotRequired[bool] 

2075 capabilities: NotRequired[Capability] 

2076 capability_mode: NotRequired[FileSystemMode] 

2077 

2078 

2079class PathManifestRule(_ModeOwnerBase): 

2080 paths: list[FileSystemMatchRule] 

2081 recursive: NotRequired[bool] 

2082 capabilities: NotRequired[Capability] 

2083 capability_mode: NotRequired[FileSystemMode] 

2084 

2085 

2086class EnsureDirectorySourceFormat(_ModeOwnerBase): 

2087 path: NotRequired[ 

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

2089 ] 

2090 paths: NotRequired[list[FileSystemExactMatchRule]] 

2091 

2092 

2093class EnsureDirectoryRule(_ModeOwnerBase): 

2094 paths: list[FileSystemExactMatchRule] 

2095 

2096 

2097class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

2098 path: FileSystemExactMatchRule 

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

2100 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

2101 

2102 

2103class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

2104 source: FileSystemMatchRule 

2105 target: FileSystemExactMatchRule 

2106 

2107 

2108class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

2109 paths: list[FileSystemMatchRule] 

2110 keep_empty_parent_dirs: NotRequired[bool] 

2111 

2112 

2113class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

2114 path: NotRequired[ 

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

2116 ] 

2117 paths: NotRequired[list[FileSystemMatchRule]] 

2118 keep_empty_parent_dirs: NotRequired[bool] 

2119 

2120 

2121class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2122 sources: NotRequired[list[FileSystemMatchRule]] 

2123 source: NotRequired[ 

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

2125 ] 

2126 into: NotRequired[ 

2127 Annotated[ 

2128 str | list[str], 

2129 DebputyParseHint.required_when_multi_binary(), 

2130 ] 

2131 ] 

2132 dest_dir: NotRequired[ 

2133 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2134 ] 

2135 install_as: NotRequired[ 

2136 Annotated[ 

2137 FileSystemExactMatchRule, 

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

2139 DebputyParseHint.manifest_attribute("as"), 

2140 DebputyParseHint.not_path_error_hint(), 

2141 ] 

2142 ] 

2143 

2144 

2145class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

2146 sources: NotRequired[list[FileSystemMatchRule]] 

2147 source: NotRequired[ 

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

2149 ] 

2150 into: NotRequired[ 

2151 Annotated[ 

2152 str | list[str], 

2153 DebputyParseHint.required_when_multi_binary( 

2154 package_types=PackageTypeSelector.DEB 

2155 ), 

2156 ] 

2157 ] 

2158 dest_dir: NotRequired[ 

2159 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2160 ] 

2161 install_as: NotRequired[ 

2162 Annotated[ 

2163 FileSystemExactMatchRule, 

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

2165 DebputyParseHint.manifest_attribute("as"), 

2166 DebputyParseHint.not_path_error_hint(), 

2167 ] 

2168 ] 

2169 

2170 

2171class ParsedInstallRule(DebputyParsedContentStandardConditional): 

2172 sources: list[FileSystemMatchRule] 

2173 into: NotRequired[list[BinaryPackage]] 

2174 dest_dir: NotRequired[FileSystemExactMatchRule] 

2175 install_as: NotRequired[FileSystemExactMatchRule] 

2176 

2177 

2178class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2179 sources: NotRequired[list[FileSystemMatchRule]] 

2180 source: NotRequired[ 

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

2182 ] 

2183 into: NotRequired[ 

2184 Annotated[ 

2185 str | list[str], 

2186 DebputyParseHint.required_when_multi_binary(), 

2187 ] 

2188 ] 

2189 dest_dirs: NotRequired[ 

2190 Annotated[ 

2191 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

2192 ] 

2193 ] 

2194 install_as: NotRequired[ 

2195 Annotated[ 

2196 list[FileSystemExactMatchRule], 

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

2198 DebputyParseHint.not_path_error_hint(), 

2199 DebputyParseHint.manifest_attribute("as"), 

2200 ] 

2201 ] 

2202 

2203 

2204class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

2205 sources: list[FileSystemMatchRule] 

2206 into: NotRequired[list[BinaryPackage]] 

2207 dest_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2208 install_as: NotRequired[list[FileSystemExactMatchRule]] 

2209 

2210 

2211class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

2212 sources: list[FileSystemMatchRule] 

2213 into: NotRequired[list[BinaryPackage]] 

2214 

2215 

2216class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

2217 sources: NotRequired[list[FileSystemMatchRule]] 

2218 source: NotRequired[ 

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

2220 ] 

2221 into: NotRequired[ 

2222 Annotated[ 

2223 str | list[str], 

2224 DebputyParseHint.required_when_multi_binary( 

2225 package_types=PackageTypeSelector.DEB 

2226 ), 

2227 ] 

2228 ] 

2229 

2230 

2231class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

2232 sources: list[FileSystemMatchRule] 

2233 language: NotRequired[str] 

2234 section: NotRequired[int] 

2235 into: NotRequired[list[BinaryPackage]] 

2236 

2237 

2238class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

2239 sources: NotRequired[list[FileSystemMatchRule]] 

2240 source: NotRequired[ 

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

2242 ] 

2243 language: NotRequired[str] 

2244 section: NotRequired[int] 

2245 into: NotRequired[ 

2246 Annotated[ 

2247 str | list[str], 

2248 DebputyParseHint.required_when_multi_binary( 

2249 package_types=PackageTypeSelector.DEB 

2250 ), 

2251 ] 

2252 ] 

2253 

2254 

2255class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2256 paths: NotRequired[list[FileSystemMatchRule]] 

2257 path: NotRequired[ 

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

2259 ] 

2260 search_dir: NotRequired[ 

2261 Annotated[ 

2262 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2263 ] 

2264 ] 

2265 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2266 required_when: NotRequired[ManifestCondition] 

2267 

2268 

2269class ParsedInstallDiscardRule(DebputyParsedContent): 

2270 paths: list[FileSystemMatchRule] 

2271 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2272 required_when: NotRequired[ManifestCondition] 

2273 

2274 

2275class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2276 prior_to_version: NotRequired[str] 

2277 owning_package: NotRequired[str] 

2278 

2279 

2280class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2281 source: str 

2282 target: str 

2283 

2284 

2285class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2286 path: str 

2287 

2288 

2289class MCAnyOfAllOf(DebputyParsedContent): 

2290 conditions: list[ManifestCondition] 

2291 

2292 

2293class MCNot(DebputyParsedContent): 

2294 negated_condition: ManifestCondition 

2295 

2296 

2297class MCArchMatches(DebputyParsedContent): 

2298 arch_matches: str 

2299 

2300 

2301class MCBuildProfileMatches(DebputyParsedContent): 

2302 build_profile_matches: str 

2303 

2304 

2305class UnconditionallyInScript(typing.TypedDict): 

2306 script_name: MaintscriptForBinary 

2307 

2308 

2309class UpgradeFromVersion(typing.TypedDict): 

2310 from_version: debian.debian_support.Version 

2311 

2312 

2313def _parse_filename( 

2314 filename: str, 

2315 attribute_path: AttributePath, 

2316 *, 

2317 allow_directories: bool = True, 

2318) -> str: 

2319 try: 

2320 normalized_path = _normalize_path(filename, with_prefix=False) 

2321 except ValueError as e: 

2322 raise ManifestParseException( 

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

2324 ) from None 

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

2326 raise ManifestParseException( 

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

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

2329 ) 

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

2331 raise ManifestParseException( 

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

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

2334 ) 

2335 return normalized_path 

2336 

2337 

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

2339 return Union[ 

2340 t, 

2341 list[str], 

2342 str, 

2343 ] 

2344 

2345 

2346def _dpkg_conffile_rename( 

2347 _name: str, 

2348 parsed_data: DpkgRenameConffileRule, 

2349 path: AttributePath, 

2350 _context: ParserContextData, 

2351) -> DpkgMaintscriptHelperCommand: 

2352 source_file = parsed_data["source"] 

2353 target_file = parsed_data["target"] 

2354 normalized_source = _parse_filename( 

2355 source_file, 

2356 path["source"], 

2357 allow_directories=False, 

2358 ) 

2359 path.path_hint = source_file 

2360 

2361 normalized_target = _parse_filename( 

2362 target_file, 

2363 path["target"], 

2364 allow_directories=False, 

2365 ) 

2366 normalized_source = "/" + normalized_source 

2367 normalized_target = "/" + normalized_target 

2368 

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

2370 raise ManifestParseException( 

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

2372 ) 

2373 

2374 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2375 parsed_data, path 

2376 ) 

2377 return DpkgMaintscriptHelperCommand.mv_conffile( 

2378 path, 

2379 normalized_source, 

2380 normalized_target, 

2381 version, 

2382 owning_package, 

2383 ) 

2384 

2385 

2386def _dpkg_conffile_remove( 

2387 _name: str, 

2388 parsed_data: DpkgRemoveConffileRule, 

2389 path: AttributePath, 

2390 _context: ParserContextData, 

2391) -> DpkgMaintscriptHelperCommand: 

2392 source_file = parsed_data["path"] 

2393 normalized_source = _parse_filename( 

2394 source_file, 

2395 path["path"], 

2396 allow_directories=False, 

2397 ) 

2398 path.path_hint = source_file 

2399 

2400 normalized_source = "/" + normalized_source 

2401 

2402 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2403 parsed_data, path 

2404 ) 

2405 return DpkgMaintscriptHelperCommand.rm_conffile( 

2406 path, 

2407 normalized_source, 

2408 version, 

2409 owning_package, 

2410 ) 

2411 

2412 

2413def _parse_conffile_prior_version_and_owning_package( 

2414 d: DpkgConffileManagementRuleBase, 

2415 attribute_path: AttributePath, 

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

2417 prior_version = d.get("prior_to_version") 

2418 owning_package = d.get("owning_package") 

2419 

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

2421 p = attribute_path["prior_to_version"] 

2422 raise ManifestParseException( 

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

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

2425 ) 

2426 

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

2428 p = attribute_path["owning_package"] 

2429 raise ManifestParseException( 

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

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

2432 ) 

2433 

2434 return prior_version, owning_package 

2435 

2436 

2437def _install_rule_handler( 

2438 _name: str, 

2439 parsed_data: ParsedInstallRule, 

2440 path: AttributePath, 

2441 context: ParserContextData, 

2442) -> InstallRule: 

2443 sources = parsed_data["sources"] 

2444 install_as = parsed_data.get("install_as") 

2445 into = frozenset( 

2446 parsed_data.get("into") 

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

2448 ) 

2449 dest_dir = parsed_data.get("dest_dir") 

2450 condition = parsed_data.get("when") 

2451 if install_as is not None: 

2452 assert len(sources) == 1 

2453 assert dest_dir is None 

2454 return InstallRule.install_as( 

2455 sources[0], 

2456 install_as.match_rule.path, 

2457 into, 

2458 path.path, 

2459 condition, 

2460 ) 

2461 return InstallRule.install_dest( 

2462 sources, 

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

2464 into, 

2465 path.path, 

2466 condition, 

2467 ) 

2468 

2469 

2470def _multi_dest_install_rule_handler( 

2471 _name: str, 

2472 parsed_data: ParsedMultiDestInstallRule, 

2473 path: AttributePath, 

2474 context: ParserContextData, 

2475) -> InstallRule: 

2476 sources = parsed_data["sources"] 

2477 install_as = parsed_data.get("install_as") 

2478 into = frozenset( 

2479 parsed_data.get("into") 

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

2481 ) 

2482 dest_dirs = parsed_data.get("dest_dirs") 

2483 condition = parsed_data.get("when") 

2484 if install_as is not None: 

2485 assert len(sources) == 1 

2486 assert dest_dirs is None 

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

2488 raise ManifestParseException( 

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

2490 ) 

2491 return InstallRule.install_multi_as( 

2492 sources[0], 

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

2494 into, 

2495 path.path, 

2496 condition, 

2497 ) 

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

2499 raise ManifestParseException( 

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

2501 ) 

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

2503 raise ManifestParseException( 

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

2505 ) 

2506 return InstallRule.install_multi_dest( 

2507 sources, 

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

2509 into, 

2510 path.path, 

2511 condition, 

2512 ) 

2513 

2514 

2515def _install_docs_rule_handler( 

2516 _name: str, 

2517 parsed_data: ParsedInstallRule, 

2518 path: AttributePath, 

2519 context: ParserContextData, 

2520) -> InstallRule: 

2521 sources = parsed_data["sources"] 

2522 install_as = parsed_data.get("install_as") 

2523 dest_dir = parsed_data.get("dest_dir") 

2524 condition = parsed_data.get("when") 

2525 into = frozenset( 

2526 parsed_data.get("into") 

2527 or ( 

2528 context.single_binary_package( 

2529 path, 

2530 package_types=PackageTypeSelector.DEB, 

2531 package_attribute="into", 

2532 ), 

2533 ) 

2534 ) 

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

2536 assert len(sources) == 1 

2537 assert dest_dir is None 

2538 return InstallRule.install_doc_as( 

2539 sources[0], 

2540 install_as.match_rule.path, 

2541 into, 

2542 path.path, 

2543 condition, 

2544 ) 

2545 return InstallRule.install_doc( 

2546 sources, 

2547 None if dest_dir is None else dest_dir.raw_match_rule, 

2548 into, 

2549 path.path, 

2550 condition, 

2551 ) 

2552 

2553 

2554def _install_examples_rule_handler( 

2555 _name: str, 

2556 parsed_data: ParsedInstallExamplesRule, 

2557 path: AttributePath, 

2558 context: ParserContextData, 

2559) -> InstallRule: 

2560 return InstallRule.install_examples( 

2561 sources=parsed_data["sources"], 

2562 into=frozenset( 

2563 parsed_data.get("into") 

2564 or ( 

2565 context.single_binary_package( 

2566 path, 

2567 package_types=PackageTypeSelector.DEB, 

2568 package_attribute="into", 

2569 ), 

2570 ) 

2571 ), 

2572 definition_source=path.path, 

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

2574 ) 

2575 

2576 

2577def _install_man_rule_handler( 

2578 _name: str, 

2579 parsed_data: ParsedInstallManpageRule, 

2580 attribute_path: AttributePath, 

2581 context: ParserContextData, 

2582) -> InstallRule: 

2583 sources = parsed_data["sources"] 

2584 language = parsed_data.get("language") 

2585 section = parsed_data.get("section") 

2586 

2587 if language is not None: 

2588 is_lang_ok = language in ( 

2589 "C", 

2590 "derive-from-basename", 

2591 "derive-from-path", 

2592 ) 

2593 

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

2595 is_lang_ok = True 

2596 

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

2598 not is_lang_ok 

2599 and len(language) == 5 

2600 and language[2] == "_" 

2601 and language[:2].islower() 

2602 and language[3:].isupper() 

2603 ): 

2604 is_lang_ok = True 

2605 

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

2607 raise ManifestParseException( 

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

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

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

2611 ) 

2612 

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

2614 raise ManifestParseException( 

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

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

2617 ) 

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

2619 raise ManifestParseException( 

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

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

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

2623 ) 

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

2625 raise ManifestParseException( 

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

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

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

2629 ) 

2630 return InstallRule.install_man( 

2631 sources=sources, 

2632 into=frozenset( 

2633 parsed_data.get("into") 

2634 or ( 

2635 context.single_binary_package( 

2636 attribute_path, 

2637 package_types=PackageTypeSelector.DEB, 

2638 package_attribute="into", 

2639 ), 

2640 ) 

2641 ), 

2642 section=section, 

2643 language=language, 

2644 definition_source=attribute_path.path, 

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

2646 ) 

2647 

2648 

2649def _install_discard_rule_handler( 

2650 _name: str, 

2651 parsed_data: ParsedInstallDiscardRule, 

2652 path: AttributePath, 

2653 _context: ParserContextData, 

2654) -> InstallRule: 

2655 limit_to = parsed_data.get("search_dirs") 

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

2657 p = path["search_dirs"] 

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

2659 condition = parsed_data.get("required_when") 

2660 return InstallRule.discard_paths( 

2661 parsed_data["paths"], 

2662 path.path, 

2663 condition, 

2664 limit_to=limit_to, 

2665 ) 

2666 

2667 

2668def _transformation_move_handler( 

2669 _name: str, 

2670 parsed_data: TransformationMoveRuleSpec, 

2671 path: AttributePath, 

2672 _context: ParserContextData, 

2673) -> TransformationRule: 

2674 source_match = parsed_data["source"] 

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

2676 condition = parsed_data.get("when") 

2677 

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

2679 isinstance(source_match, ExactFileSystemPath) 

2680 and source_match.path == target_path 

2681 ): 

2682 raise ManifestParseException( 

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

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

2685 ) 

2686 return MoveTransformationRule( 

2687 source_match.match_rule, 

2688 target_path, 

2689 target_path.endswith("/"), 

2690 path, 

2691 condition, 

2692 ) 

2693 

2694 

2695def _transformation_remove_handler( 

2696 _name: str, 

2697 parsed_data: TransformationRemoveRuleSpec, 

2698 attribute_path: AttributePath, 

2699 _context: ParserContextData, 

2700) -> TransformationRule: 

2701 paths = parsed_data["paths"] 

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

2703 

2704 return RemoveTransformationRule( 

2705 [m.match_rule for m in paths], 

2706 keep_empty_parent_dirs, 

2707 attribute_path, 

2708 ) 

2709 

2710 

2711def _transformation_create_symlink( 

2712 _name: str, 

2713 parsed_data: CreateSymlinkRule, 

2714 attribute_path: AttributePath, 

2715 _context: ParserContextData, 

2716) -> TransformationRule: 

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

2718 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2719 "replacement_rule", 

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

2721 ) 

2722 try: 

2723 link_target = debian_policy_normalize_symlink_target( 

2724 link_dest, 

2725 parsed_data["target"].symlink_target, 

2726 ) 

2727 except ValueError as e: # pragma: no cover 

2728 raise AssertionError( 

2729 "Debian Policy normalization should not raise ValueError here" 

2730 ) from e 

2731 

2732 condition = parsed_data.get("when") 

2733 

2734 return CreateSymlinkPathTransformationRule( 

2735 link_target, 

2736 link_dest, 

2737 replacement_rule, 

2738 attribute_path, 

2739 condition, 

2740 ) 

2741 

2742 

2743def _transformation_path_metadata( 

2744 _name: str, 

2745 parsed_data: PathManifestRule, 

2746 attribute_path: AttributePath, 

2747 context: ParserContextData, 

2748) -> TransformationRule: 

2749 match_rules = parsed_data["paths"] 

2750 owner = parsed_data.get("owner") 

2751 group = parsed_data.get("group") 

2752 mode = parsed_data.get("mode") 

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

2754 capabilities = parsed_data.get("capabilities") 

2755 capability_mode = parsed_data.get("capability_mode") 

2756 cap: str | None = None 

2757 

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

2759 check_integration_mode( 

2760 attribute_path["capabilities"], 

2761 context, 

2762 _NOT_INTEGRATION_RRR, 

2763 ) 

2764 if capability_mode is None: 

2765 capability_mode = SymbolicMode.parse_filesystem_mode( 

2766 "a-s", 

2767 attribute_path["capability-mode"], 

2768 ) 

2769 cap = capabilities.value 

2770 validate_cap = check_cap_checker() 

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

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

2773 check_integration_mode( 

2774 attribute_path["capability_mode"], 

2775 context, 

2776 _NOT_INTEGRATION_RRR, 

2777 ) 

2778 raise ManifestParseException( 

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

2780 f" in {attribute_path.path}" 

2781 ) 

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

2783 raise ManifestParseException( 

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

2785 f" in {attribute_path.path}" 

2786 ) 

2787 condition = parsed_data.get("when") 

2788 

2789 return PathMetadataTransformationRule( 

2790 [m.match_rule for m in match_rules], 

2791 owner, 

2792 group, 

2793 mode, 

2794 recursive, 

2795 cap, 

2796 capability_mode, 

2797 attribute_path.path, 

2798 condition, 

2799 ) 

2800 

2801 

2802def _transformation_mkdirs( 

2803 _name: str, 

2804 parsed_data: EnsureDirectoryRule, 

2805 attribute_path: AttributePath, 

2806 _context: ParserContextData, 

2807) -> TransformationRule: 

2808 provided_paths = parsed_data["paths"] 

2809 owner = parsed_data.get("owner") 

2810 group = parsed_data.get("group") 

2811 mode = parsed_data.get("mode") 

2812 

2813 condition = parsed_data.get("when") 

2814 

2815 return CreateDirectoryTransformationRule( 

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

2817 owner, 

2818 group, 

2819 mode, 

2820 attribute_path.path, 

2821 condition, 

2822 ) 

2823 

2824 

2825def _at_least_two( 

2826 content: list[Any], 

2827 attribute_path: AttributePath, 

2828 attribute_name: str, 

2829) -> None: 

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

2831 raise ManifestParseException( 

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

2833 ) 

2834 

2835 

2836def _mc_any_of( 

2837 name: str, 

2838 parsed_data: MCAnyOfAllOf, 

2839 attribute_path: AttributePath, 

2840 _context: ParserContextData, 

2841) -> ManifestCondition: 

2842 conditions = parsed_data["conditions"] 

2843 _at_least_two(conditions, attribute_path, "conditions") 

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

2845 return ManifestCondition.any_of(conditions) 

2846 assert name == "all-of" 

2847 return ManifestCondition.all_of(conditions) 

2848 

2849 

2850def _mc_not( 

2851 _name: str, 

2852 parsed_data: MCNot, 

2853 _attribute_path: AttributePath, 

2854 _context: ParserContextData, 

2855) -> ManifestCondition: 

2856 condition = parsed_data["negated_condition"] 

2857 return condition.negated() 

2858 

2859 

2860def _extract_arch_matches( 

2861 parsed_data: MCArchMatches, 

2862 attribute_path: AttributePath, 

2863) -> list[str]: 

2864 arch_matches_as_str = parsed_data["arch_matches"] 

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

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

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

2868 # of each other. 

2869 arch_matches_as_list = arch_matches_as_str.split() 

2870 attr_path = attribute_path["arch_matches"] 

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

2872 raise ManifestParseException( 

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

2874 ) 

2875 

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

2877 "]" 

2878 ): 

2879 raise ManifestParseException( 

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

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

2882 ) 

2883 return arch_matches_as_list 

2884 

2885 

2886def _mc_source_context_arch_matches( 

2887 _name: str, 

2888 parsed_data: MCArchMatches, 

2889 attribute_path: AttributePath, 

2890 _context: ParserContextData, 

2891) -> ManifestCondition: 

2892 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2893 return SourceContextArchMatchManifestCondition(arch_matches) 

2894 

2895 

2896def _mc_package_context_arch_matches( 

2897 name: str, 

2898 parsed_data: MCArchMatches, 

2899 attribute_path: AttributePath, 

2900 context: ParserContextData, 

2901) -> ManifestCondition: 

2902 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2903 

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

2905 raise ManifestParseException( 

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

2907 ) 

2908 

2909 package_state = context.current_binary_package_state 

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

2911 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2912 "all", arch_matches 

2913 ) 

2914 attr_path = attribute_path["arch_matches"] 

2915 raise ManifestParseException( 

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

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

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

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

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

2921 ) 

2922 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2923 

2924 

2925def _mc_arch_matches( 

2926 name: str, 

2927 parsed_data: MCArchMatches, 

2928 attribute_path: AttributePath, 

2929 context: ParserContextData, 

2930) -> ManifestCondition: 

2931 if context.is_in_binary_package_state: 

2932 return _mc_package_context_arch_matches( 

2933 name, parsed_data, attribute_path, context 

2934 ) 

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

2936 

2937 

2938def _mc_build_profile_matches( 

2939 _name: str, 

2940 parsed_data: MCBuildProfileMatches, 

2941 attribute_path: AttributePath, 

2942 _context: ParserContextData, 

2943) -> ManifestCondition: 

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

2945 attr_path = attribute_path["build_profile_matches"] 

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

2947 raise ManifestParseException( 

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

2949 ) 

2950 try: 

2951 active_profiles_match(build_profile_spec, frozenset()) 

2952 except ValueError as e: 

2953 raise ManifestParseException( 

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

2955 ) 

2956 return BuildProfileMatch(build_profile_spec) 

2957 

2958 

2959def _parse_unconditionally_in_script( 

2960 _name: str, 

2961 parsed_data: UnconditionallyInScript, 

2962 _path: AttributePath, 

2963 _context: ParserContextData, 

2964) -> MaintscriptCondition: 

2965 script_name = parsed_data["script_name"] 

2966 return MaintscriptCondition.on_unconditionally_in_script(script_name.name) 

2967 

2968 

2969def _parse_upgrade_from_version( 

2970 _name: str, 

2971 parsed_data: UpgradeFromVersion, 

2972 _path: AttributePath, 

2973 _context: ParserContextData, 

2974) -> MaintscriptCondition: 

2975 from_version = parsed_data["from_version"] 

2976 return MaintscriptCondition.on_upgrade_from(from_version)