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

540 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-04-19 20:37 +0000

1import ctypes 

2import ctypes.util 

3import dataclasses 

4import functools 

5import textwrap 

6import time 

7from datetime import datetime 

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

9from collections.abc import Callable 

10 

11from debian.changelog import Changelog 

12from debian.deb822 import Deb822 

13 

14import debputy.plugin.api.spec 

15from debputy._manifest_constants import ( 

16 MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE, 

17 MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION, 

18 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

19 MK_INSTALLATIONS_INSTALL, 

20 MK_INSTALLATIONS_INSTALL_DOCS, 

21 MK_INSTALLATIONS_INSTALL_MAN, 

22 MK_INSTALLATIONS_DISCARD, 

23 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

24) 

25from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError 

26from debputy.installations import InstallRule 

27from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand 

28from debputy.manifest_conditions import ( 

29 ManifestCondition, 

30 BinaryPackageContextArchMatchManifestCondition, 

31 BuildProfileMatch, 

32 SourceContextArchMatchManifestCondition, 

33) 

34from debputy.manifest_parser.base_types import ( 

35 FileSystemMode, 

36 StaticFileSystemOwner, 

37 StaticFileSystemGroup, 

38 SymlinkTarget, 

39 FileSystemExactMatchRule, 

40 FileSystemMatchRule, 

41 SymbolicMode, 

42 OctalMode, 

43 FileSystemExactNonDirMatchRule, 

44 BuildEnvironmentDefinition, 

45 DebputyParsedContentStandardConditional, 

46) 

47from debputy.manifest_parser.exceptions import ManifestParseException 

48from debputy.manifest_parser.mapper_code import type_mapper_str2package, PackageSelector 

49from debputy.manifest_parser.parse_hints import DebputyParseHint 

50from debputy.manifest_parser.parser_data import ParserContextData 

51from debputy.manifest_parser.tagging_types import ( 

52 DebputyParsedContent, 

53 TypeMapping, 

54) 

55from debputy.manifest_parser.util import AttributePath, check_integration_mode 

56from debputy.packages import BinaryPackage 

57from debputy.path_matcher import ExactFileSystemPath 

58from debputy.plugin.api import ( 

59 DebputyPluginInitializer, 

60 documented_attr, 

61 reference_documentation, 

62 VirtualPath, 

63 packager_provided_file_reference_documentation, 

64) 

65from debputy.plugin.api.impl import DebputyPluginInitializerProvider 

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

67from debputy.plugin.api.spec import ( 

68 type_mapping_reference_documentation, 

69 type_mapping_example, 

70 not_integrations, 

71 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

72) 

73from debputy.plugin.api.std_docs import docs_from 

74from debputy.plugins.debputy.binary_package_rules import register_binary_package_rules 

75from debputy.plugins.debputy.discard_rules import ( 

76 _debputy_discard_pyc_files, 

77 _debputy_prune_la_files, 

78 _debputy_prune_doxygen_cruft, 

79 _debputy_prune_binary_debian_dir, 

80 _debputy_prune_info_dir_file, 

81 _debputy_prune_backup_files, 

82 _debputy_prune_vcs_paths, 

83) 

84from debputy.plugins.debputy.manifest_root_rules import register_manifest_root_rules 

85from debputy.plugins.debputy.package_processors import ( 

86 process_manpages, 

87 apply_compression, 

88 clean_la_files, 

89) 

90from debputy.plugins.debputy.service_management import ( 

91 detect_systemd_service_files, 

92 generate_snippets_for_systemd_units, 

93 detect_sysv_init_service_files, 

94 generate_snippets_for_init_scripts, 

95) 

96from debputy.plugins.debputy.shlib_metadata_detectors import detect_shlibdeps 

97from debputy.plugins.debputy.strip_non_determinism import strip_non_determinism 

98from debputy.substitution import VariableContext 

99from debputy.transformation_rules import ( 

100 CreateSymlinkReplacementRule, 

101 TransformationRule, 

102 CreateDirectoryTransformationRule, 

103 RemoveTransformationRule, 

104 MoveTransformationRule, 

105 PathMetadataTransformationRule, 

106 CreateSymlinkPathTransformationRule, 

107) 

108from debputy.util import ( 

109 _normalize_path, 

110 PKGNAME_REGEX, 

111 PKGVERSION_REGEX, 

112 debian_policy_normalize_symlink_target, 

113 active_profiles_match, 

114 _error, 

115 _warn, 

116 _info, 

117 assume_not_none, 

118 manifest_format_doc, 

119 PackageTypeSelector, 

120) 

121 

122_DOCUMENTED_DPKG_ARCH_TYPES = { 

123 "HOST": ( 

124 "installed on", 

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

126 ), 

127 "BUILD": ( 

128 "compiled on", 

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

130 ), 

131 "TARGET": ( 

132 "cross-compiler output", 

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

134 ), 

135} 

136 

137_DOCUMENTED_DPKG_ARCH_VARS = { 

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

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

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

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

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

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

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

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

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

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

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

149} 

150 

151 

152_NOT_INTEGRATION_RRR = not_integrations(INTEGRATION_MODE_DH_DEBPUTY_RRR) 

153 

154 

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

156class Capability: 

157 value: str 

158 

159 @classmethod 

160 def parse( 

161 cls, 

162 raw_value: str, 

163 _attribute_path: AttributePath, 

164 _parser_context: ParserContextData | None, 

165 ) -> "Capability": 

166 return cls(raw_value) 

167 

168 

169@functools.lru_cache 

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

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

172 has_libcap = False 

173 libcap = None 

174 if cap_library_path: 

175 try: 

176 libcap = ctypes.cdll.LoadLibrary(cap_library_path) 

177 has_libcap = True 

178 except OSError: 

179 pass 

180 

181 if libcap is None: 

182 warned = False 

183 

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

185 nonlocal warned 

186 if not warned: 

187 _info( 

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

189 " checking of capabilities." 

190 ) 

191 warned = True 

192 return True 

193 

194 else: 

195 # cap_t cap_from_text(const char *path_p) 

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

197 libcap.cap_from_text.restype = ctypes.c_char_p 

198 

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

200 libcap.cap_free.restype = None 

201 

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

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

204 ok = cap_t is not None 

205 libcap.cap_free(cap_t) 

206 return ok 

207 

208 return has_libcap, cap_library_path, _is_valid_cap 

209 

210 

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

212 _, libcap_path, is_valid_cap = load_libcap() 

213 

214 seen_cap = set() 

215 

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

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

218 seen_cap.add(cap) 

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

220 _warn( 

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

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

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

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

225 ) 

226 

227 return _check_cap 

228 

229 

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

231 try: 

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

233 if changelog is None: 

234 raise DebputyManifestVariableRequiresDebianDirError( 

235 "The changelog was not present" 

236 ) 

237 with changelog.open() as fd: 

238 dch = Changelog(fd, max_blocks=2) 

239 except FileNotFoundError as e: 

240 raise DebputyManifestVariableRequiresDebianDirError( 

241 "The changelog was not present" 

242 ) from e 

243 first_entry = dch[0] 

244 first_non_binnmu_entry = dch[0] 

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

246 first_non_binnmu_entry = dch[1] 

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

248 source_version = first_entry.version 

249 epoch = source_version.epoch 

250 upstream_version = source_version.upstream_version 

251 debian_revision = source_version.debian_revision 

252 epoch_upstream = upstream_version 

253 upstream_debian_revision = upstream_version 

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

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

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

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

258 

259 package = first_entry.package 

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

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

262 

263 date = first_entry.date 

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

265 try: 

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

267 except ValueError: 

268 _error( 

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

270 ) 

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

272 else: 

273 _warn( 

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

275 " for SOURCE_DATE_EPOCH" 

276 ) 

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

278 

279 if first_non_binnmu_entry is not first_entry: 

280 non_binnmu_date = first_non_binnmu_entry.date 

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

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

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

284 else: 

285 _warn( 

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

287 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)" 

288 ) 

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

290 else: 

291 snd_source_date_epoch = source_date_epoch 

292 return { 

293 "DEB_SOURCE": package, 

294 "DEB_VERSION": source_version.full_version, 

295 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream, 

296 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision, 

297 "DEB_VERSION_UPSTREAM": upstream_version, 

298 "SOURCE_DATE_EPOCH": source_date_epoch, 

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

300 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch, 

301 } 

302 

303 

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

305 api = cast("DebputyPluginInitializerProvider", public_api) 

306 

307 api.metadata_or_maintscript_detector( 

308 "dpkg-shlibdeps", 

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

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

311 package_types=PackageTypeSelector.DEB | PackageTypeSelector.UDEB, 

312 ) 

313 register_type_mappings(api) 

314 register_variables_via_private_api(api) 

315 document_builtin_variables(api) 

316 register_automatic_discard_rules(api) 

317 register_special_ppfs(api) 

318 register_install_rules(api) 

319 register_transformation_rules(api) 

320 register_manifest_condition_rules(api) 

321 register_dpkg_conffile_rules(api) 

322 register_processing_steps(api) 

323 register_service_managers(api) 

324 register_manifest_root_rules(api) 

325 register_binary_package_rules(api) 

326 

327 

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

329 api.register_mapped_type( 

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

331 reference_documentation=type_mapping_reference_documentation( 

332 description=textwrap.dedent( 

333 """\ 

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

335 

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

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

338 currently never emit hard errors for unknown capabilities. 

339 """, 

340 ), 

341 examples=[ 

342 type_mapping_example("cap_chown=p"), 

343 type_mapping_example("cap_chown=ep"), 

344 type_mapping_example("cap_kill-pe"), 

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

346 ], 

347 ), 

348 ) 

349 api.register_mapped_type( 

350 TypeMapping( 

351 FileSystemMatchRule, 

352 str, 

353 FileSystemMatchRule.parse_path_match, 

354 ), 

355 reference_documentation=type_mapping_reference_documentation( 

356 description=textwrap.dedent( 

357 """\ 

358 A generic file system path match with globs. 

359 

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

361 

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

363 

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

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

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

367 

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

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

370 

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

372 or relevant search directories will match. 

373 

374 Please keep in mind that: 

375 

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

377 an anchor reference. 

378 

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

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

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

382 directories (similar to the shell). 

383 

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

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

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

387 """, 

388 ), 

389 examples=[ 

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

391 type_mapping_example("*.txt"), 

392 type_mapping_example("**/foo"), 

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

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

395 ], 

396 ), 

397 ) 

398 

399 api.register_mapped_type( 

400 TypeMapping( 

401 FileSystemExactMatchRule, 

402 str, 

403 FileSystemExactMatchRule.parse_path_match, 

404 ), 

405 reference_documentation=type_mapping_reference_documentation( 

406 description=textwrap.dedent( 

407 """\ 

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

409 

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

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

412 

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

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

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

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

417 """, 

418 ), 

419 examples=[ 

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

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

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

423 ], 

424 ), 

425 ) 

426 

427 api.register_mapped_type( 

428 TypeMapping( 

429 FileSystemExactNonDirMatchRule, 

430 str, 

431 FileSystemExactNonDirMatchRule.parse_path_match, 

432 ), 

433 reference_documentation=type_mapping_reference_documentation( 

434 description=textwrap.dedent( 

435 f"""\ 

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

437 

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

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

440 

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

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

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

444 with a "/". 

445 """, 

446 ), 

447 examples=[ 

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

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

450 ], 

451 ), 

452 ) 

453 

454 api.register_mapped_type( 

455 TypeMapping( 

456 SymlinkTarget, 

457 str, 

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

459 v, ap, assume_not_none(pc).substitution 

460 ), 

461 ), 

462 reference_documentation=type_mapping_reference_documentation( 

463 description=textwrap.dedent( 

464 """\ 

465 A symlink target. 

466 

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

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

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

470 

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

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

473 """, 

474 ), 

475 examples=[ 

476 type_mapping_example("../foo"), 

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

478 ], 

479 ), 

480 ) 

481 

482 api.register_mapped_type( 

483 TypeMapping( 

484 StaticFileSystemOwner, 

485 Union[int, str], 

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

487 ), 

488 reference_documentation=type_mapping_reference_documentation( 

489 description=textwrap.dedent("""\ 

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

491 

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

493 

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

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

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

497 

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

499 

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

501 """), 

502 examples=[ 

503 type_mapping_example("root"), 

504 type_mapping_example(0), 

505 type_mapping_example("root:0"), 

506 type_mapping_example("bin"), 

507 ], 

508 ), 

509 ) 

510 api.register_mapped_type( 

511 TypeMapping( 

512 StaticFileSystemGroup, 

513 Union[int, str], 

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

515 ), 

516 reference_documentation=type_mapping_reference_documentation( 

517 description=textwrap.dedent("""\ 

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

519 

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

521 

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

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

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

525 

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

527 

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

529 """), 

530 examples=[ 

531 type_mapping_example("root"), 

532 type_mapping_example(0), 

533 type_mapping_example("root:0"), 

534 type_mapping_example("tty"), 

535 ], 

536 ), 

537 ) 

538 

539 api.register_mapped_type( 

540 TypeMapping( 

541 BinaryPackage, 

542 str, 

543 type_mapper_str2package, 

544 ), 

545 reference_documentation=type_mapping_reference_documentation( 

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

547 ), 

548 ) 

549 

550 api.register_mapped_type( 

551 TypeMapping( 

552 PackageSelector, 

553 str, 

554 PackageSelector.parse, 

555 ), 

556 reference_documentation=type_mapping_reference_documentation( 

557 description=textwrap.dedent("""\ 

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

559 

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

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

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

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

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

565 respectively). 

566 """), 

567 ), 

568 ) 

569 

570 api.register_mapped_type( 

571 TypeMapping( 

572 FileSystemMode, 

573 str, 

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

575 ), 

576 reference_documentation=type_mapping_reference_documentation( 

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

578 examples=[ 

579 type_mapping_example("a+x"), 

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

581 type_mapping_example("0755"), 

582 ], 

583 ), 

584 ) 

585 api.register_mapped_type( 

586 TypeMapping( 

587 OctalMode, 

588 str, 

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

590 ), 

591 reference_documentation=type_mapping_reference_documentation( 

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

593 examples=[ 

594 type_mapping_example("0644"), 

595 type_mapping_example("0755"), 

596 ], 

597 ), 

598 ) 

599 api.register_mapped_type( 

600 TypeMapping( 

601 BuildEnvironmentDefinition, 

602 str, 

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

604 ), 

605 reference_documentation=type_mapping_reference_documentation( 

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

607 ), 

608 ) 

609 

610 

611def register_service_managers( 

612 api: DebputyPluginInitializerProvider, 

613) -> None: 

614 api.service_provider( 

615 "systemd", 

616 detect_systemd_service_files, 

617 generate_snippets_for_systemd_units, 

618 ) 

619 api.service_provider( 

620 "sysvinit", 

621 detect_sysv_init_service_files, 

622 generate_snippets_for_init_scripts, 

623 ) 

624 

625 

626def register_automatic_discard_rules( 

627 api: DebputyPluginInitializerProvider, 

628) -> None: 

629 api.automatic_discard_rule( 

630 "python-cache-files", 

631 _debputy_discard_pyc_files, 

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

633 examples=automatic_discard_rule_example( 

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

635 ".../__pycache__/", 

636 ".../__pycache__/...", 

637 ".../foo.pyc", 

638 ".../foo.pyo", 

639 ), 

640 ) 

641 api.automatic_discard_rule( 

642 "la-files", 

643 _debputy_prune_la_files, 

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

645 examples=automatic_discard_rule_example( 

646 "usr/lib/libfoo.la", 

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

648 ), 

649 ) 

650 api.automatic_discard_rule( 

651 "backup-files", 

652 _debputy_prune_backup_files, 

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

654 examples=( 

655 automatic_discard_rule_example( 

656 ".../foo~", 

657 ".../foo.orig", 

658 ".../foo.rej", 

659 ".../DEADJOE", 

660 ".../.foo.sw.", 

661 ), 

662 ), 

663 ) 

664 api.automatic_discard_rule( 

665 "version-control-paths", 

666 _debputy_prune_vcs_paths, 

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

668 examples=automatic_discard_rule_example( 

669 ("tools/foo", False), 

670 ".../CVS/", 

671 ".../CVS/...", 

672 ".../.gitignore", 

673 ".../.gitattributes", 

674 ".../.git/", 

675 ".../.git/...", 

676 ), 

677 ) 

678 api.automatic_discard_rule( 

679 "gnu-info-dir-file", 

680 _debputy_prune_info_dir_file, 

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

682 examples=automatic_discard_rule_example( 

683 "usr/share/info/dir", 

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

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

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

687 ), 

688 ) 

689 api.automatic_discard_rule( 

690 "debian-dir", 

691 _debputy_prune_binary_debian_dir, 

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

693 " literally in the file listing", 

694 examples=( 

695 automatic_discard_rule_example( 

696 "DEBIAN/", 

697 "DEBIAN/control", 

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

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

700 ), 

701 ), 

702 ) 

703 api.automatic_discard_rule( 

704 "doxygen-cruft-files", 

705 _debputy_prune_doxygen_cruft, 

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

707 examples=automatic_discard_rule_example( 

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

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

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

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

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

713 ), 

714 ) 

715 

716 

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

718 api.package_processor("manpages", process_manpages) 

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

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

721 api.package_processor( 

722 "strip-nondeterminism", 

723 cast("Any", strip_non_determinism), 

724 depends_on_processor=["manpages"], 

725 ) 

726 api.package_processor( 

727 "compression", 

728 apply_compression, 

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

730 ) 

731 

732 

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

734 api.manifest_variable_provider( 

735 load_source_variables, 

736 { 

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

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

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

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

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

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

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

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

745 this variable. 

746 """), 

747 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

748 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

749 }, 

750 ) 

751 

752 

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

754 api.document_builtin_variable( 

755 "PACKAGE", 

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

757 is_context_specific=True, 

758 ) 

759 

760 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

761 

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

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

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

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

766 {arch_var_doc} ({arch_type_tag}) 

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

768 * Machine type: {arch_type_doc} 

769 * Value description: {arch_var_doc} 

770 

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

772 """) 

773 api.document_builtin_variable( 

774 full_var, 

775 documentation, 

776 is_for_special_case=arch_type != "HOST", 

777 ) 

778 

779 

780def _format_docbase_filename( 

781 path_format: str, 

782 format_param: PPFFormatParam, 

783 docbase_file: VirtualPath, 

784) -> str: 

785 with docbase_file.open() as fd: 

786 content = Deb822(fd) 

787 proper_name = content["Document"] 

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

789 format_param["name"] = proper_name 

790 else: 

791 _warn( 

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

793 ) 

794 return path_format.format(**format_param) 

795 

796 

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

798 api.packager_provided_file( 

799 "doc-base", 

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

801 format_callback=_format_docbase_filename, 

802 ) 

803 

804 api.packager_provided_file( 

805 "shlibs", 

806 "DEBIAN/shlibs", 

807 allow_name_segment=False, 

808 reservation_only=True, 

809 reference_documentation=packager_provided_file_reference_documentation( 

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

811 ), 

812 ) 

813 api.packager_provided_file( 

814 "symbols", 

815 "DEBIAN/symbols", 

816 allow_name_segment=False, 

817 allow_architecture_segment=True, 

818 reservation_only=True, 

819 reference_documentation=packager_provided_file_reference_documentation( 

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

821 ), 

822 ) 

823 api.packager_provided_file( 

824 "conffiles", 

825 "DEBIAN/conffiles", 

826 allow_name_segment=False, 

827 allow_architecture_segment=True, 

828 reservation_only=True, 

829 ) 

830 api.packager_provided_file( 

831 "templates", 

832 "DEBIAN/templates", 

833 allow_name_segment=False, 

834 allow_architecture_segment=False, 

835 reservation_only=True, 

836 ) 

837 api.packager_provided_file( 

838 "alternatives", 

839 "DEBIAN/alternatives", 

840 allow_name_segment=False, 

841 allow_architecture_segment=True, 

842 reservation_only=True, 

843 ) 

844 

845 

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

847 api.pluggable_manifest_rule( 

848 InstallRule, 

849 MK_INSTALLATIONS_INSTALL, 

850 ParsedInstallRule, 

851 _install_rule_handler, 

852 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

853 inline_reference_documentation=reference_documentation( 

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

855 description=textwrap.dedent("""\ 

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

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

858 

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

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

861 

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

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

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

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

866 non_mapping_description=textwrap.dedent("""\ 

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

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

869 not required. 

870 """), 

871 attributes=[ 

872 documented_attr( 

873 ["source", "sources"], 

874 textwrap.dedent("""\ 

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

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

877 is tried against default search directories. 

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

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

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

881 """), 

882 ), 

883 documented_attr( 

884 "dest_dir", 

885 textwrap.dedent("""\ 

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

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

888 to the directory name of the `source`. 

889 """), 

890 ), 

891 documented_attr( 

892 "into", 

893 textwrap.dedent("""\ 

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

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

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

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

898 """), 

899 ), 

900 documented_attr( 

901 "install_as", 

902 textwrap.dedent("""\ 

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

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

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

906 """), 

907 ), 

908 *docs_from(DebputyParsedContentStandardConditional), 

909 ], 

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

911 ), 

912 ) 

913 api.pluggable_manifest_rule( 

914 InstallRule, 

915 [ 

916 MK_INSTALLATIONS_INSTALL_DOCS, 

917 "install-doc", 

918 ], 

919 ParsedInstallRule, 

920 _install_docs_rule_handler, 

921 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

922 inline_reference_documentation=reference_documentation( 

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

924 description=textwrap.dedent("""\ 

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

926 `install` rule with the following key features: 

927 

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

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

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

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

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

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

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

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

936 package listed in `debian/control`. 

937 

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

939 

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

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

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

943 use-case. 

944 """), 

945 non_mapping_description=textwrap.dedent("""\ 

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

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

948 not required. 

949 """), 

950 attributes=[ 

951 documented_attr( 

952 ["source", "sources"], 

953 textwrap.dedent("""\ 

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

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

956 is tried against default search directories. 

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

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

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

960 

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

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

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

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

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

966 """), 

967 ), 

968 documented_attr( 

969 "dest_dir", 

970 textwrap.dedent("""\ 

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

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

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

974 """), 

975 ), 

976 documented_attr( 

977 "into", 

978 textwrap.dedent("""\ 

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

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

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

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

983 the key is required. 

984 """), 

985 ), 

986 documented_attr( 

987 "install_as", 

988 textwrap.dedent("""\ 

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

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

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

992 """), 

993 ), 

994 documented_attr( 

995 "when", 

996 textwrap.dedent("""\ 

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

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

999 (rather than replacing it). 

1000 """), 

1001 ), 

1002 ], 

1003 reference_documentation_url=manifest_format_doc( 

1004 "install-documentation-install-docs" 

1005 ), 

1006 ), 

1007 ) 

1008 api.pluggable_manifest_rule( 

1009 InstallRule, 

1010 [ 

1011 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

1012 "install-example", 

1013 ], 

1014 ParsedInstallExamplesRule, 

1015 _install_examples_rule_handler, 

1016 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

1017 inline_reference_documentation=reference_documentation( 

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

1019 description=textwrap.dedent("""\ 

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

1021 install` rule with the following key features: 

1022 

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

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

1025 dir. 

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

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

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

1029 package listed in `debian/control`. 

1030 

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

1032 """), 

1033 non_mapping_description=textwrap.dedent("""\ 

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

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

1036 not required. 

1037 """), 

1038 attributes=[ 

1039 documented_attr( 

1040 ["source", "sources"], 

1041 textwrap.dedent("""\ 

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

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

1044 is tried against default search directories. 

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

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

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

1048 

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

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

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

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

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

1054 """), 

1055 ), 

1056 documented_attr( 

1057 "into", 

1058 textwrap.dedent("""\ 

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

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

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

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

1063 Otherwise, the key is required. 

1064 """), 

1065 ), 

1066 documented_attr( 

1067 "when", 

1068 textwrap.dedent("""\ 

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

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

1071 (rather than replacing it). 

1072 """), 

1073 ), 

1074 ], 

1075 reference_documentation_url=manifest_format_doc( 

1076 "install-examples-install-examples" 

1077 ), 

1078 ), 

1079 ) 

1080 api.pluggable_manifest_rule( 

1081 InstallRule, 

1082 MK_INSTALLATIONS_INSTALL_MAN, 

1083 ParsedInstallManpageRule, 

1084 _install_man_rule_handler, 

1085 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1086 inline_reference_documentation=reference_documentation( 

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

1088 description=textwrap.dedent("""\ 

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

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

1091 

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

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

1094 language. 

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

1096 package listed in `debian/control`. 

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

1098 for when the auto-detection is insufficient. 

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

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

1101 

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

1103 """), 

1104 non_mapping_description=textwrap.dedent("""\ 

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

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

1107 not required. 

1108 """), 

1109 attributes=[ 

1110 documented_attr( 

1111 ["source", "sources"], 

1112 textwrap.dedent("""\ 

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

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

1115 is tried against default search directories. 

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

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

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

1119 """), 

1120 ), 

1121 documented_attr( 

1122 "into", 

1123 textwrap.dedent("""\ 

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

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

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

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

1128 """), 

1129 ), 

1130 documented_attr( 

1131 "section", 

1132 textwrap.dedent("""\ 

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

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

1135 have performed. 

1136 """), 

1137 ), 

1138 documented_attr( 

1139 "language", 

1140 textwrap.dedent("""\ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1158 always assumed to be a language. 

1159 """), 

1160 ), 

1161 *docs_from(DebputyParsedContentStandardConditional), 

1162 ], 

1163 reference_documentation_url=manifest_format_doc( 

1164 "install-manpages-install-man" 

1165 ), 

1166 ), 

1167 ) 

1168 api.pluggable_manifest_rule( 

1169 InstallRule, 

1170 MK_INSTALLATIONS_DISCARD, 

1171 ParsedInstallDiscardRule, 

1172 _install_discard_rule_handler, 

1173 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1174 inline_reference_documentation=reference_documentation( 

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

1176 description=textwrap.dedent("""\ 

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

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

1179 

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

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

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

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

1184 """), 

1185 non_mapping_description=textwrap.dedent("""\ 

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

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

1188 """), 

1189 attributes=[ 

1190 documented_attr( 

1191 ["path", "paths"], 

1192 textwrap.dedent("""\ 

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

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

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

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

1197 contents that have not already been installed somewhere. 

1198 """), 

1199 ), 

1200 documented_attr( 

1201 ["search_dir", "search_dirs"], 

1202 textwrap.dedent("""\ 

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

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

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

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

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

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

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

1210 will make `debputy` report an error. 

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

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

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

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

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

1216 applicable to certain builds that are only performed conditionally. 

1217 """), 

1218 ), 

1219 documented_attr( 

1220 "required_when", 

1221 textwrap.dedent("""\ 

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

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

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

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

1226 """), 

1227 ), 

1228 ], 

1229 reference_documentation_url=manifest_format_doc( 

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

1231 ), 

1232 ), 

1233 ) 

1234 api.pluggable_manifest_rule( 

1235 InstallRule, 

1236 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1237 ParsedMultiDestInstallRule, 

1238 _multi_dest_install_rule_handler, 

1239 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1240 inline_reference_documentation=reference_documentation( 

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

1242 description=textwrap.dedent("""\ 

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

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

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

1246 "primary" uses. 

1247 

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

1249 except you list 2+ destination directories. 

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

1251 2+ `as` names. 

1252 """), 

1253 attributes=[ 

1254 documented_attr( 

1255 ["source", "sources"], 

1256 textwrap.dedent("""\ 

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

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

1259 is tried against default search directories. 

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

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

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

1263 """), 

1264 ), 

1265 documented_attr( 

1266 "dest_dirs", 

1267 textwrap.dedent("""\ 

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

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

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

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

1272 """), 

1273 ), 

1274 documented_attr( 

1275 "into", 

1276 textwrap.dedent("""\ 

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

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

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

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

1281 """), 

1282 ), 

1283 documented_attr( 

1284 "install_as", 

1285 textwrap.dedent("""\ 

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

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

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

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

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

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

1292 """), 

1293 ), 

1294 *docs_from(DebputyParsedContentStandardConditional), 

1295 ], 

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

1297 ), 

1298 ) 

1299 

1300 

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

1302 api.pluggable_manifest_rule( 

1303 TransformationRule, 

1304 "move", 

1305 TransformationMoveRuleSpec, 

1306 _transformation_move_handler, 

1307 inline_reference_documentation=reference_documentation( 

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

1309 description=textwrap.dedent("""\ 

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

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

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

1313 Debian specific requirements. 

1314 

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

1316 `mv` command line tool. 

1317 """), 

1318 attributes=[ 

1319 documented_attr( 

1320 "source", 

1321 textwrap.dedent("""\ 

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

1323 and substitutions. 

1324 """), 

1325 ), 

1326 documented_attr( 

1327 "target", 

1328 textwrap.dedent("""\ 

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

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

1331 the target will *always* be a directory. 

1332 """), 

1333 ), 

1334 *docs_from(DebputyParsedContentStandardConditional), 

1335 ], 

1336 reference_documentation_url=manifest_format_doc( 

1337 "move-transformation-rule-move" 

1338 ), 

1339 ), 

1340 ) 

1341 api.pluggable_manifest_rule( 

1342 TransformationRule, 

1343 "remove", 

1344 TransformationRemoveRuleSpec, 

1345 _transformation_remove_handler, 

1346 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1347 inline_reference_documentation=reference_documentation( 

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

1349 description=textwrap.dedent("""\ 

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

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

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

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

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

1355 of `debian/copyright`). 

1356 

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

1358 the `remove` transformation rule. 

1359 

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

1361 """), 

1362 non_mapping_description=textwrap.dedent("""\ 

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

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

1365 """), 

1366 attributes=[ 

1367 documented_attr( 

1368 ["path", "paths"], 

1369 textwrap.dedent("""\ 

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

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

1372 can use globs. 

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

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

1375 along with all the contents. 

1376 """), 

1377 ), 

1378 documented_attr( 

1379 "keep_empty_parent_dirs", 

1380 textwrap.dedent("""\ 

1381 A boolean determining whether to prune parent directories that become 

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

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

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

1385 """), 

1386 ), 

1387 documented_attr( 

1388 "when", 

1389 textwrap.dedent("""\ 

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

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

1392 (rather than replacing it). 

1393 """), 

1394 ), 

1395 ], 

1396 reference_documentation_url=manifest_format_doc( 

1397 "remove-transformation-rule-remove" 

1398 ), 

1399 ), 

1400 ) 

1401 api.pluggable_manifest_rule( 

1402 TransformationRule, 

1403 "create-symlink", 

1404 CreateSymlinkRule, 

1405 _transformation_create_symlink, 

1406 inline_reference_documentation=reference_documentation( 

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

1408 description=textwrap.dedent("""\ 

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

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

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

1412 """), 

1413 attributes=[ 

1414 documented_attr( 

1415 "path", 

1416 textwrap.dedent("""\ 

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

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

1419 Parent directories are implicitly created as necessary. 

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

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

1422 """), 

1423 ), 

1424 documented_attr( 

1425 "target", 

1426 textwrap.dedent("""\ 

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

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

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

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

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

1432 preference. 

1433 """), 

1434 ), 

1435 documented_attr( 

1436 "replacement_rule", 

1437 textwrap.dedent("""\ 

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

1439 be set to one of the following values: 

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

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

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

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

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

1445 similar to `ln -sf` semantics. 

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

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

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

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

1450 will stop with an error. 

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

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

1453 be removed recursively along with the directory. Finally, 

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

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

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

1457 `when` if any). 

1458 

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

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

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

1462 the value in `replacement-rule`. 

1463 """), 

1464 ), 

1465 *docs_from(DebputyParsedContentStandardConditional), 

1466 ], 

1467 reference_documentation_url=manifest_format_doc( 

1468 "create-symlinks-transformation-rule-create-symlink" 

1469 ), 

1470 ), 

1471 ) 

1472 api.pluggable_manifest_rule( 

1473 TransformationRule, 

1474 "path-metadata", 

1475 PathManifestRule, 

1476 _transformation_path_metadata, 

1477 source_format=PathManifestSourceDictFormat, 

1478 inline_reference_documentation=reference_documentation( 

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

1480 description=textwrap.dedent("""\ 

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

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

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

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

1485 transformation. 

1486 

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

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

1489 """), 

1490 attributes=[ 

1491 documented_attr( 

1492 ["path", "paths"], 

1493 textwrap.dedent("""\ 

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

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

1496 and substitution variables. Special-rules for matches: 

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

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

1499 """), 

1500 ), 

1501 documented_attr( 

1502 "owner", 

1503 textwrap.dedent("""\ 

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

1505 no change of owner is done. 

1506 """), 

1507 ), 

1508 documented_attr( 

1509 "group", 

1510 textwrap.dedent("""\ 

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

1512 no change of group is done. 

1513 """), 

1514 ), 

1515 documented_attr( 

1516 "mode", 

1517 textwrap.dedent("""\ 

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

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

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

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

1522 relative to the matched path's current mode. 

1523 """), 

1524 ), 

1525 documented_attr( 

1526 "capabilities", 

1527 textwrap.dedent("""\ 

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

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

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

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

1532 run. 

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

1534 to those paths. 

1535 

1536 """), 

1537 ), 

1538 documented_attr( 

1539 "capability_mode", 

1540 textwrap.dedent("""\ 

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

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

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

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

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

1546 `capabilities` is omitted. 

1547 """), 

1548 ), 

1549 documented_attr( 

1550 "recursive", 

1551 textwrap.dedent("""\ 

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

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

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

1555 this attribute is `false`. 

1556 """), 

1557 ), 

1558 *docs_from(DebputyParsedContentStandardConditional), 

1559 ], 

1560 reference_documentation_url=manifest_format_doc( 

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

1562 ), 

1563 ), 

1564 ) 

1565 api.pluggable_manifest_rule( 

1566 TransformationRule, 

1567 "create-directories", 

1568 EnsureDirectoryRule, 

1569 _transformation_mkdirs, 

1570 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1571 inline_reference_documentation=reference_documentation( 

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

1573 description=textwrap.dedent("""\ 

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

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

1576 transformations will create directories as required. 

1577 

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

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

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

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

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

1583 

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

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

1586 """), 

1587 non_mapping_description=textwrap.dedent("""\ 

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

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

1590 """), 

1591 attributes=[ 

1592 documented_attr( 

1593 ["path", "paths"], 

1594 textwrap.dedent("""\ 

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

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

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

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

1599 are affected by the owner/mode options) 

1600 """), 

1601 ), 

1602 documented_attr( 

1603 "owner", 

1604 textwrap.dedent("""\ 

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

1606 Default is "root". 

1607 """), 

1608 ), 

1609 documented_attr( 

1610 "group", 

1611 textwrap.dedent("""\ 

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

1613 Default is "root". 

1614 """), 

1615 ), 

1616 documented_attr( 

1617 "mode", 

1618 textwrap.dedent("""\ 

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

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

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

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

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

1624 transformation. The default is "0755". 

1625 """), 

1626 ), 

1627 *docs_from(DebputyParsedContentStandardConditional), 

1628 ], 

1629 reference_documentation_url=manifest_format_doc( 

1630 "create-directories-transformation-rule-directories" 

1631 ), 

1632 ), 

1633 ) 

1634 

1635 

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

1637 api.provide_manifest_keyword( 

1638 ManifestCondition, 

1639 "cross-compiling", 

1640 lambda *_: ManifestCondition.is_cross_building(), 

1641 ) 

1642 api.provide_manifest_keyword( 

1643 ManifestCondition, 

1644 "can-execute-compiled-binaries", 

1645 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 

1646 ) 

1647 api.provide_manifest_keyword( 

1648 ManifestCondition, 

1649 "run-build-time-tests", 

1650 lambda *_: ManifestCondition.run_build_time_tests(), 

1651 ) 

1652 

1653 api.pluggable_manifest_rule( 

1654 ManifestCondition, 

1655 "not", 

1656 MCNot, 

1657 _mc_not, 

1658 source_format=ManifestCondition, 

1659 ) 

1660 api.pluggable_manifest_rule( 

1661 ManifestCondition, 

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

1663 MCAnyOfAllOf, 

1664 _mc_any_of, 

1665 source_format=list[ManifestCondition], 

1666 ) 

1667 api.pluggable_manifest_rule( 

1668 ManifestCondition, 

1669 "arch-matches", 

1670 MCArchMatches, 

1671 _mc_arch_matches, 

1672 source_format=str, 

1673 inline_reference_documentation=reference_documentation( 

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

1675 description=textwrap.dedent("""\ 

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

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

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

1679 and practically behaves like a comparison against 

1680 `dpkg-architecture -qDEB_HOST_ARCH`. 

1681 

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

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

1684 in the context of a binary package and like 

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

1686 are covered in their own keywords. 

1687 """), 

1688 non_mapping_description=textwrap.dedent("""\ 

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

1690 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1695 have it. 

1696 """), 

1697 reference_documentation_url=manifest_format_doc( 

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

1699 ), 

1700 ), 

1701 ) 

1702 

1703 context_arch_doc = reference_documentation( 

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

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

1706 description=textwrap.dedent("""\ 

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

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

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

1710 `arch-matches` condition. 

1711 

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

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

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

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

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

1717 to the packager that condition does not make sense. 

1718 

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

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

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

1722 very special cases). 

1723 

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

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

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

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

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

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

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

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

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

1733 need to care about nor use any of this. 

1734 

1735 Accordingly, the possible conditions are: 

1736 

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

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

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

1740 

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

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

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

1744 

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

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

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

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

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

1750 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1751 

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

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

1754 

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

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

1757 """), 

1758 non_mapping_description=textwrap.dedent("""\ 

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

1760 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1765 have it. 

1766 """), 

1767 ) 

1768 

1769 api.pluggable_manifest_rule( 

1770 ManifestCondition, 

1771 "source-context-arch-matches", 

1772 MCArchMatches, 

1773 _mc_source_context_arch_matches, 

1774 source_format=str, 

1775 inline_reference_documentation=context_arch_doc, 

1776 ) 

1777 api.pluggable_manifest_rule( 

1778 ManifestCondition, 

1779 "package-context-arch-matches", 

1780 MCArchMatches, 

1781 _mc_arch_matches, 

1782 source_format=str, 

1783 inline_reference_documentation=context_arch_doc, 

1784 ) 

1785 api.pluggable_manifest_rule( 

1786 ManifestCondition, 

1787 "build-profiles-matches", 

1788 MCBuildProfileMatches, 

1789 _mc_build_profile_matches, 

1790 source_format=str, 

1791 ) 

1792 

1793 

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

1795 api.pluggable_manifest_rule( 

1796 DpkgMaintscriptHelperCommand, 

1797 "remove", 

1798 DpkgRemoveConffileRule, 

1799 _dpkg_conffile_remove, 

1800 inline_reference_documentation=None, # TODO: write and add 

1801 ) 

1802 

1803 api.pluggable_manifest_rule( 

1804 DpkgMaintscriptHelperCommand, 

1805 "rename", 

1806 DpkgRenameConffileRule, 

1807 _dpkg_conffile_rename, 

1808 inline_reference_documentation=None, # TODO: write and add 

1809 ) 

1810 

1811 

1812class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

1813 mode: NotRequired[FileSystemMode] 

1814 owner: NotRequired[StaticFileSystemOwner] 

1815 group: NotRequired[StaticFileSystemGroup] 

1816 

1817 

1818class PathManifestSourceDictFormat(_ModeOwnerBase): 

1819 path: NotRequired[ 

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

1821 ] 

1822 paths: NotRequired[list[FileSystemMatchRule]] 

1823 recursive: NotRequired[bool] 

1824 capabilities: NotRequired[Capability] 

1825 capability_mode: NotRequired[FileSystemMode] 

1826 

1827 

1828class PathManifestRule(_ModeOwnerBase): 

1829 paths: list[FileSystemMatchRule] 

1830 recursive: NotRequired[bool] 

1831 capabilities: NotRequired[Capability] 

1832 capability_mode: NotRequired[FileSystemMode] 

1833 

1834 

1835class EnsureDirectorySourceFormat(_ModeOwnerBase): 

1836 path: NotRequired[ 

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

1838 ] 

1839 paths: NotRequired[list[FileSystemExactMatchRule]] 

1840 

1841 

1842class EnsureDirectoryRule(_ModeOwnerBase): 

1843 paths: list[FileSystemExactMatchRule] 

1844 

1845 

1846class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

1847 path: FileSystemExactMatchRule 

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

1849 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

1850 

1851 

1852class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

1853 source: FileSystemMatchRule 

1854 target: FileSystemExactMatchRule 

1855 

1856 

1857class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

1858 paths: list[FileSystemMatchRule] 

1859 keep_empty_parent_dirs: NotRequired[bool] 

1860 

1861 

1862class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

1863 path: NotRequired[ 

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

1865 ] 

1866 paths: NotRequired[list[FileSystemMatchRule]] 

1867 keep_empty_parent_dirs: NotRequired[bool] 

1868 

1869 

1870class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

1871 sources: NotRequired[list[FileSystemMatchRule]] 

1872 source: NotRequired[ 

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

1874 ] 

1875 into: NotRequired[ 

1876 Annotated[ 

1877 str | list[str], 

1878 DebputyParseHint.required_when_multi_binary(), 

1879 ] 

1880 ] 

1881 dest_dir: NotRequired[ 

1882 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

1883 ] 

1884 install_as: NotRequired[ 

1885 Annotated[ 

1886 FileSystemExactMatchRule, 

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

1888 DebputyParseHint.manifest_attribute("as"), 

1889 DebputyParseHint.not_path_error_hint(), 

1890 ] 

1891 ] 

1892 

1893 

1894class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

1895 sources: NotRequired[list[FileSystemMatchRule]] 

1896 source: NotRequired[ 

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

1898 ] 

1899 into: NotRequired[ 

1900 Annotated[ 

1901 str | list[str], 

1902 DebputyParseHint.required_when_multi_binary( 

1903 package_types=PackageTypeSelector.DEB 

1904 ), 

1905 ] 

1906 ] 

1907 dest_dir: NotRequired[ 

1908 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

1909 ] 

1910 install_as: NotRequired[ 

1911 Annotated[ 

1912 FileSystemExactMatchRule, 

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

1914 DebputyParseHint.manifest_attribute("as"), 

1915 DebputyParseHint.not_path_error_hint(), 

1916 ] 

1917 ] 

1918 

1919 

1920class ParsedInstallRule(DebputyParsedContentStandardConditional): 

1921 sources: list[FileSystemMatchRule] 

1922 into: NotRequired[list[BinaryPackage]] 

1923 dest_dir: NotRequired[FileSystemExactMatchRule] 

1924 install_as: NotRequired[FileSystemExactMatchRule] 

1925 

1926 

1927class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

1928 sources: NotRequired[list[FileSystemMatchRule]] 

1929 source: NotRequired[ 

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

1931 ] 

1932 into: NotRequired[ 

1933 Annotated[ 

1934 str | list[str], 

1935 DebputyParseHint.required_when_multi_binary(), 

1936 ] 

1937 ] 

1938 dest_dirs: NotRequired[ 

1939 Annotated[ 

1940 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

1941 ] 

1942 ] 

1943 install_as: NotRequired[ 

1944 Annotated[ 

1945 list[FileSystemExactMatchRule], 

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

1947 DebputyParseHint.not_path_error_hint(), 

1948 DebputyParseHint.manifest_attribute("as"), 

1949 ] 

1950 ] 

1951 

1952 

1953class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

1954 sources: list[FileSystemMatchRule] 

1955 into: NotRequired[list[BinaryPackage]] 

1956 dest_dirs: NotRequired[list[FileSystemExactMatchRule]] 

1957 install_as: NotRequired[list[FileSystemExactMatchRule]] 

1958 

1959 

1960class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

1961 sources: list[FileSystemMatchRule] 

1962 into: NotRequired[list[BinaryPackage]] 

1963 

1964 

1965class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

1966 sources: NotRequired[list[FileSystemMatchRule]] 

1967 source: NotRequired[ 

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

1969 ] 

1970 into: NotRequired[ 

1971 Annotated[ 

1972 str | list[str], 

1973 DebputyParseHint.required_when_multi_binary( 

1974 package_types=PackageTypeSelector.DEB 

1975 ), 

1976 ] 

1977 ] 

1978 

1979 

1980class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

1981 sources: list[FileSystemMatchRule] 

1982 language: NotRequired[str] 

1983 section: NotRequired[int] 

1984 into: NotRequired[list[BinaryPackage]] 

1985 

1986 

1987class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

1988 sources: NotRequired[list[FileSystemMatchRule]] 

1989 source: NotRequired[ 

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

1991 ] 

1992 language: NotRequired[str] 

1993 section: NotRequired[int] 

1994 into: NotRequired[ 

1995 Annotated[ 

1996 str | list[str], 

1997 DebputyParseHint.required_when_multi_binary( 

1998 package_types=PackageTypeSelector.DEB 

1999 ), 

2000 ] 

2001 ] 

2002 

2003 

2004class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2005 paths: NotRequired[list[FileSystemMatchRule]] 

2006 path: NotRequired[ 

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

2008 ] 

2009 search_dir: NotRequired[ 

2010 Annotated[ 

2011 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2012 ] 

2013 ] 

2014 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2015 required_when: NotRequired[ManifestCondition] 

2016 

2017 

2018class ParsedInstallDiscardRule(DebputyParsedContent): 

2019 paths: list[FileSystemMatchRule] 

2020 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2021 required_when: NotRequired[ManifestCondition] 

2022 

2023 

2024class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2025 prior_to_version: NotRequired[str] 

2026 owning_package: NotRequired[str] 

2027 

2028 

2029class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2030 source: str 

2031 target: str 

2032 

2033 

2034class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2035 path: str 

2036 

2037 

2038class MCAnyOfAllOf(DebputyParsedContent): 

2039 conditions: list[ManifestCondition] 

2040 

2041 

2042class MCNot(DebputyParsedContent): 

2043 negated_condition: ManifestCondition 

2044 

2045 

2046class MCArchMatches(DebputyParsedContent): 

2047 arch_matches: str 

2048 

2049 

2050class MCBuildProfileMatches(DebputyParsedContent): 

2051 build_profile_matches: str 

2052 

2053 

2054def _parse_filename( 

2055 filename: str, 

2056 attribute_path: AttributePath, 

2057 *, 

2058 allow_directories: bool = True, 

2059) -> str: 

2060 try: 

2061 normalized_path = _normalize_path(filename, with_prefix=False) 

2062 except ValueError as e: 

2063 raise ManifestParseException( 

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

2065 ) from None 

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

2067 raise ManifestParseException( 

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

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

2070 ) 

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

2072 raise ManifestParseException( 

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

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

2075 ) 

2076 return normalized_path 

2077 

2078 

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

2080 return Union[ 

2081 t, 

2082 list[str], 

2083 str, 

2084 ] 

2085 

2086 

2087def _dpkg_conffile_rename( 

2088 _name: str, 

2089 parsed_data: DpkgRenameConffileRule, 

2090 path: AttributePath, 

2091 _context: ParserContextData, 

2092) -> DpkgMaintscriptHelperCommand: 

2093 source_file = parsed_data["source"] 

2094 target_file = parsed_data["target"] 

2095 normalized_source = _parse_filename( 

2096 source_file, 

2097 path["source"], 

2098 allow_directories=False, 

2099 ) 

2100 path.path_hint = source_file 

2101 

2102 normalized_target = _parse_filename( 

2103 target_file, 

2104 path["target"], 

2105 allow_directories=False, 

2106 ) 

2107 normalized_source = "/" + normalized_source 

2108 normalized_target = "/" + normalized_target 

2109 

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

2111 raise ManifestParseException( 

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

2113 ) 

2114 

2115 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2116 parsed_data, path 

2117 ) 

2118 return DpkgMaintscriptHelperCommand.mv_conffile( 

2119 path, 

2120 normalized_source, 

2121 normalized_target, 

2122 version, 

2123 owning_package, 

2124 ) 

2125 

2126 

2127def _dpkg_conffile_remove( 

2128 _name: str, 

2129 parsed_data: DpkgRemoveConffileRule, 

2130 path: AttributePath, 

2131 _context: ParserContextData, 

2132) -> DpkgMaintscriptHelperCommand: 

2133 source_file = parsed_data["path"] 

2134 normalized_source = _parse_filename( 

2135 source_file, 

2136 path["path"], 

2137 allow_directories=False, 

2138 ) 

2139 path.path_hint = source_file 

2140 

2141 normalized_source = "/" + normalized_source 

2142 

2143 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2144 parsed_data, path 

2145 ) 

2146 return DpkgMaintscriptHelperCommand.rm_conffile( 

2147 path, 

2148 normalized_source, 

2149 version, 

2150 owning_package, 

2151 ) 

2152 

2153 

2154def _parse_conffile_prior_version_and_owning_package( 

2155 d: DpkgConffileManagementRuleBase, 

2156 attribute_path: AttributePath, 

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

2158 prior_version = d.get("prior_to_version") 

2159 owning_package = d.get("owning_package") 

2160 

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

2162 p = attribute_path["prior_to_version"] 

2163 raise ManifestParseException( 

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

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

2166 ) 

2167 

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

2169 p = attribute_path["owning_package"] 

2170 raise ManifestParseException( 

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

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

2173 ) 

2174 

2175 return prior_version, owning_package 

2176 

2177 

2178def _install_rule_handler( 

2179 _name: str, 

2180 parsed_data: ParsedInstallRule, 

2181 path: AttributePath, 

2182 context: ParserContextData, 

2183) -> InstallRule: 

2184 sources = parsed_data["sources"] 

2185 install_as = parsed_data.get("install_as") 

2186 into = frozenset( 

2187 parsed_data.get("into") 

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

2189 ) 

2190 dest_dir = parsed_data.get("dest_dir") 

2191 condition = parsed_data.get("when") 

2192 if install_as is not None: 

2193 assert len(sources) == 1 

2194 assert dest_dir is None 

2195 return InstallRule.install_as( 

2196 sources[0], 

2197 install_as.match_rule.path, 

2198 into, 

2199 path.path, 

2200 condition, 

2201 ) 

2202 return InstallRule.install_dest( 

2203 sources, 

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

2205 into, 

2206 path.path, 

2207 condition, 

2208 ) 

2209 

2210 

2211def _multi_dest_install_rule_handler( 

2212 _name: str, 

2213 parsed_data: ParsedMultiDestInstallRule, 

2214 path: AttributePath, 

2215 context: ParserContextData, 

2216) -> InstallRule: 

2217 sources = parsed_data["sources"] 

2218 install_as = parsed_data.get("install_as") 

2219 into = frozenset( 

2220 parsed_data.get("into") 

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

2222 ) 

2223 dest_dirs = parsed_data.get("dest_dirs") 

2224 condition = parsed_data.get("when") 

2225 if install_as is not None: 

2226 assert len(sources) == 1 

2227 assert dest_dirs is None 

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

2229 raise ManifestParseException( 

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

2231 ) 

2232 return InstallRule.install_multi_as( 

2233 sources[0], 

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

2235 into, 

2236 path.path, 

2237 condition, 

2238 ) 

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

2240 raise ManifestParseException( 

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

2242 ) 

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

2244 raise ManifestParseException( 

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

2246 ) 

2247 return InstallRule.install_multi_dest( 

2248 sources, 

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

2250 into, 

2251 path.path, 

2252 condition, 

2253 ) 

2254 

2255 

2256def _install_docs_rule_handler( 

2257 _name: str, 

2258 parsed_data: ParsedInstallRule, 

2259 path: AttributePath, 

2260 context: ParserContextData, 

2261) -> InstallRule: 

2262 sources = parsed_data["sources"] 

2263 install_as = parsed_data.get("install_as") 

2264 dest_dir = parsed_data.get("dest_dir") 

2265 condition = parsed_data.get("when") 

2266 into = frozenset( 

2267 parsed_data.get("into") 

2268 or ( 

2269 context.single_binary_package( 

2270 path, 

2271 package_types=PackageTypeSelector.DEB, 

2272 package_attribute="into", 

2273 ), 

2274 ) 

2275 ) 

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

2277 assert len(sources) == 1 

2278 assert dest_dir is None 

2279 return InstallRule.install_doc_as( 

2280 sources[0], 

2281 install_as.match_rule.path, 

2282 into, 

2283 path.path, 

2284 condition, 

2285 ) 

2286 return InstallRule.install_doc( 

2287 sources, 

2288 None if dest_dir is None else dest_dir.raw_match_rule, 

2289 into, 

2290 path.path, 

2291 condition, 

2292 ) 

2293 

2294 

2295def _install_examples_rule_handler( 

2296 _name: str, 

2297 parsed_data: ParsedInstallExamplesRule, 

2298 path: AttributePath, 

2299 context: ParserContextData, 

2300) -> InstallRule: 

2301 return InstallRule.install_examples( 

2302 sources=parsed_data["sources"], 

2303 into=frozenset( 

2304 parsed_data.get("into") 

2305 or ( 

2306 context.single_binary_package( 

2307 path, 

2308 package_types=PackageTypeSelector.DEB, 

2309 package_attribute="into", 

2310 ), 

2311 ) 

2312 ), 

2313 definition_source=path.path, 

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

2315 ) 

2316 

2317 

2318def _install_man_rule_handler( 

2319 _name: str, 

2320 parsed_data: ParsedInstallManpageRule, 

2321 attribute_path: AttributePath, 

2322 context: ParserContextData, 

2323) -> InstallRule: 

2324 sources = parsed_data["sources"] 

2325 language = parsed_data.get("language") 

2326 section = parsed_data.get("section") 

2327 

2328 if language is not None: 

2329 is_lang_ok = language in ( 

2330 "C", 

2331 "derive-from-basename", 

2332 "derive-from-path", 

2333 ) 

2334 

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

2336 is_lang_ok = True 

2337 

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

2339 not is_lang_ok 

2340 and len(language) == 5 

2341 and language[2] == "_" 

2342 and language[:2].islower() 

2343 and language[3:].isupper() 

2344 ): 

2345 is_lang_ok = True 

2346 

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

2348 raise ManifestParseException( 

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

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

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

2352 ) 

2353 

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

2355 raise ManifestParseException( 

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

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

2358 ) 

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

2360 raise ManifestParseException( 

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

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

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

2364 ) 

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

2366 raise ManifestParseException( 

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

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

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

2370 ) 

2371 return InstallRule.install_man( 

2372 sources=sources, 

2373 into=frozenset( 

2374 parsed_data.get("into") 

2375 or ( 

2376 context.single_binary_package( 

2377 attribute_path, 

2378 package_types=PackageTypeSelector.DEB, 

2379 package_attribute="into", 

2380 ), 

2381 ) 

2382 ), 

2383 section=section, 

2384 language=language, 

2385 definition_source=attribute_path.path, 

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

2387 ) 

2388 

2389 

2390def _install_discard_rule_handler( 

2391 _name: str, 

2392 parsed_data: ParsedInstallDiscardRule, 

2393 path: AttributePath, 

2394 _context: ParserContextData, 

2395) -> InstallRule: 

2396 limit_to = parsed_data.get("search_dirs") 

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

2398 p = path["search_dirs"] 

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

2400 condition = parsed_data.get("required_when") 

2401 return InstallRule.discard_paths( 

2402 parsed_data["paths"], 

2403 path.path, 

2404 condition, 

2405 limit_to=limit_to, 

2406 ) 

2407 

2408 

2409def _transformation_move_handler( 

2410 _name: str, 

2411 parsed_data: TransformationMoveRuleSpec, 

2412 path: AttributePath, 

2413 _context: ParserContextData, 

2414) -> TransformationRule: 

2415 source_match = parsed_data["source"] 

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

2417 condition = parsed_data.get("when") 

2418 

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

2420 isinstance(source_match, ExactFileSystemPath) 

2421 and source_match.path == target_path 

2422 ): 

2423 raise ManifestParseException( 

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

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

2426 ) 

2427 return MoveTransformationRule( 

2428 source_match.match_rule, 

2429 target_path, 

2430 target_path.endswith("/"), 

2431 path, 

2432 condition, 

2433 ) 

2434 

2435 

2436def _transformation_remove_handler( 

2437 _name: str, 

2438 parsed_data: TransformationRemoveRuleSpec, 

2439 attribute_path: AttributePath, 

2440 _context: ParserContextData, 

2441) -> TransformationRule: 

2442 paths = parsed_data["paths"] 

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

2444 

2445 return RemoveTransformationRule( 

2446 [m.match_rule for m in paths], 

2447 keep_empty_parent_dirs, 

2448 attribute_path, 

2449 ) 

2450 

2451 

2452def _transformation_create_symlink( 

2453 _name: str, 

2454 parsed_data: CreateSymlinkRule, 

2455 attribute_path: AttributePath, 

2456 _context: ParserContextData, 

2457) -> TransformationRule: 

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

2459 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2460 "replacement_rule", 

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

2462 ) 

2463 try: 

2464 link_target = debian_policy_normalize_symlink_target( 

2465 link_dest, 

2466 parsed_data["target"].symlink_target, 

2467 ) 

2468 except ValueError as e: # pragma: no cover 

2469 raise AssertionError( 

2470 "Debian Policy normalization should not raise ValueError here" 

2471 ) from e 

2472 

2473 condition = parsed_data.get("when") 

2474 

2475 return CreateSymlinkPathTransformationRule( 

2476 link_target, 

2477 link_dest, 

2478 replacement_rule, 

2479 attribute_path, 

2480 condition, 

2481 ) 

2482 

2483 

2484def _transformation_path_metadata( 

2485 _name: str, 

2486 parsed_data: PathManifestRule, 

2487 attribute_path: AttributePath, 

2488 context: ParserContextData, 

2489) -> TransformationRule: 

2490 match_rules = parsed_data["paths"] 

2491 owner = parsed_data.get("owner") 

2492 group = parsed_data.get("group") 

2493 mode = parsed_data.get("mode") 

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

2495 capabilities = parsed_data.get("capabilities") 

2496 capability_mode = parsed_data.get("capability_mode") 

2497 cap: str | None = None 

2498 

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

2500 check_integration_mode( 

2501 attribute_path["capabilities"], 

2502 context, 

2503 _NOT_INTEGRATION_RRR, 

2504 ) 

2505 if capability_mode is None: 

2506 capability_mode = SymbolicMode.parse_filesystem_mode( 

2507 "a-s", 

2508 attribute_path["capability-mode"], 

2509 ) 

2510 cap = capabilities.value 

2511 validate_cap = check_cap_checker() 

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

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

2514 check_integration_mode( 

2515 attribute_path["capability_mode"], 

2516 context, 

2517 _NOT_INTEGRATION_RRR, 

2518 ) 

2519 raise ManifestParseException( 

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

2521 f" in {attribute_path.path}" 

2522 ) 

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

2524 raise ManifestParseException( 

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

2526 f" in {attribute_path.path}" 

2527 ) 

2528 condition = parsed_data.get("when") 

2529 

2530 return PathMetadataTransformationRule( 

2531 [m.match_rule for m in match_rules], 

2532 owner, 

2533 group, 

2534 mode, 

2535 recursive, 

2536 cap, 

2537 capability_mode, 

2538 attribute_path.path, 

2539 condition, 

2540 ) 

2541 

2542 

2543def _transformation_mkdirs( 

2544 _name: str, 

2545 parsed_data: EnsureDirectoryRule, 

2546 attribute_path: AttributePath, 

2547 _context: ParserContextData, 

2548) -> TransformationRule: 

2549 provided_paths = parsed_data["paths"] 

2550 owner = parsed_data.get("owner") 

2551 group = parsed_data.get("group") 

2552 mode = parsed_data.get("mode") 

2553 

2554 condition = parsed_data.get("when") 

2555 

2556 return CreateDirectoryTransformationRule( 

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

2558 owner, 

2559 group, 

2560 mode, 

2561 attribute_path.path, 

2562 condition, 

2563 ) 

2564 

2565 

2566def _at_least_two( 

2567 content: list[Any], 

2568 attribute_path: AttributePath, 

2569 attribute_name: str, 

2570) -> None: 

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

2572 raise ManifestParseException( 

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

2574 ) 

2575 

2576 

2577def _mc_any_of( 

2578 name: str, 

2579 parsed_data: MCAnyOfAllOf, 

2580 attribute_path: AttributePath, 

2581 _context: ParserContextData, 

2582) -> ManifestCondition: 

2583 conditions = parsed_data["conditions"] 

2584 _at_least_two(conditions, attribute_path, "conditions") 

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

2586 return ManifestCondition.any_of(conditions) 

2587 assert name == "all-of" 

2588 return ManifestCondition.all_of(conditions) 

2589 

2590 

2591def _mc_not( 

2592 _name: str, 

2593 parsed_data: MCNot, 

2594 _attribute_path: AttributePath, 

2595 _context: ParserContextData, 

2596) -> ManifestCondition: 

2597 condition = parsed_data["negated_condition"] 

2598 return condition.negated() 

2599 

2600 

2601def _extract_arch_matches( 

2602 parsed_data: MCArchMatches, 

2603 attribute_path: AttributePath, 

2604) -> list[str]: 

2605 arch_matches_as_str = parsed_data["arch_matches"] 

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

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

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

2609 # of each other. 

2610 arch_matches_as_list = arch_matches_as_str.split() 

2611 attr_path = attribute_path["arch_matches"] 

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

2613 raise ManifestParseException( 

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

2615 ) 

2616 

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

2618 "]" 

2619 ): 

2620 raise ManifestParseException( 

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

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

2623 ) 

2624 return arch_matches_as_list 

2625 

2626 

2627def _mc_source_context_arch_matches( 

2628 _name: str, 

2629 parsed_data: MCArchMatches, 

2630 attribute_path: AttributePath, 

2631 _context: ParserContextData, 

2632) -> ManifestCondition: 

2633 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2634 return SourceContextArchMatchManifestCondition(arch_matches) 

2635 

2636 

2637def _mc_package_context_arch_matches( 

2638 name: str, 

2639 parsed_data: MCArchMatches, 

2640 attribute_path: AttributePath, 

2641 context: ParserContextData, 

2642) -> ManifestCondition: 

2643 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2644 

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

2646 raise ManifestParseException( 

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

2648 ) 

2649 

2650 package_state = context.current_binary_package_state 

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

2652 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2653 "all", arch_matches 

2654 ) 

2655 attr_path = attribute_path["arch_matches"] 

2656 raise ManifestParseException( 

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

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

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

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

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

2662 ) 

2663 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2664 

2665 

2666def _mc_arch_matches( 

2667 name: str, 

2668 parsed_data: MCArchMatches, 

2669 attribute_path: AttributePath, 

2670 context: ParserContextData, 

2671) -> ManifestCondition: 

2672 if context.is_in_binary_package_state: 

2673 return _mc_package_context_arch_matches( 

2674 name, parsed_data, attribute_path, context 

2675 ) 

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

2677 

2678 

2679def _mc_build_profile_matches( 

2680 _name: str, 

2681 parsed_data: MCBuildProfileMatches, 

2682 attribute_path: AttributePath, 

2683 _context: ParserContextData, 

2684) -> ManifestCondition: 

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

2686 attr_path = attribute_path["build_profile_matches"] 

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

2688 raise ManifestParseException( 

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

2690 ) 

2691 try: 

2692 active_profiles_match(build_profile_spec, frozenset()) 

2693 except ValueError as e: 

2694 raise ManifestParseException( 

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

2696 ) 

2697 return BuildProfileMatch(build_profile_spec)