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

554 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-01-16 17:20 +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 

14from debputy._manifest_constants import ( 

15 MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE, 

16 MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION, 

17 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

18 MK_INSTALLATIONS_INSTALL, 

19 MK_INSTALLATIONS_INSTALL_DOCS, 

20 MK_INSTALLATIONS_INSTALL_MAN, 

21 MK_INSTALLATIONS_DISCARD, 

22 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

23) 

24from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError 

25from debputy.installations import InstallRule 

26from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand 

27from debputy.manifest_conditions import ( 

28 ManifestCondition, 

29 BinaryPackageContextArchMatchManifestCondition, 

30 BuildProfileMatch, 

31 SourceContextArchMatchManifestCondition, 

32) 

33from debputy.manifest_parser.base_types import ( 

34 FileSystemMode, 

35 StaticFileSystemOwner, 

36 StaticFileSystemGroup, 

37 SymlinkTarget, 

38 FileSystemExactMatchRule, 

39 FileSystemMatchRule, 

40 SymbolicMode, 

41 OctalMode, 

42 FileSystemExactNonDirMatchRule, 

43 BuildEnvironmentDefinition, 

44 DebputyParsedContentStandardConditional, 

45) 

46from debputy.manifest_parser.exceptions import ManifestParseException 

47from debputy.manifest_parser.mapper_code import type_mapper_str2package, PackageSelector 

48from debputy.manifest_parser.parse_hints import DebputyParseHint 

49from debputy.manifest_parser.parser_data import ParserContextData 

50from debputy.manifest_parser.tagging_types import ( 

51 DebputyParsedContent, 

52 TypeMapping, 

53) 

54from debputy.manifest_parser.util import AttributePath, check_integration_mode 

55from debputy.packages import BinaryPackage 

56from debputy.path_matcher import ExactFileSystemPath 

57from debputy.plugin.api import ( 

58 DebputyPluginInitializer, 

59 documented_attr, 

60 reference_documentation, 

61 VirtualPath, 

62 packager_provided_file_reference_documentation, 

63) 

64from debputy.plugin.api.impl import DebputyPluginInitializerProvider 

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

66from debputy.plugin.api.spec import ( 

67 type_mapping_reference_documentation, 

68 type_mapping_example, 

69 not_integrations, 

70 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

71) 

72from debputy.plugin.api.std_docs import docs_from 

73from debputy.plugins.debputy.binary_package_rules import register_binary_package_rules 

74from debputy.plugins.debputy.discard_rules import ( 

75 _debputy_discard_pyc_files, 

76 _debputy_prune_la_files, 

77 _debputy_prune_doxygen_cruft, 

78 _debputy_prune_binary_debian_dir, 

79 _debputy_prune_info_dir_file, 

80 _debputy_prune_backup_files, 

81 _debputy_prune_vcs_paths, 

82) 

83from debputy.plugins.debputy.manifest_root_rules import register_manifest_root_rules 

84from debputy.plugins.debputy.package_processors import ( 

85 process_manpages, 

86 apply_compression, 

87 clean_la_files, 

88) 

89from debputy.plugins.debputy.service_management import ( 

90 detect_systemd_service_files, 

91 generate_snippets_for_systemd_units, 

92 detect_sysv_init_service_files, 

93 generate_snippets_for_init_scripts, 

94) 

95from debputy.plugins.debputy.shlib_metadata_detectors import detect_shlibdeps 

96from debputy.plugins.debputy.strip_non_determinism import strip_non_determinism 

97from debputy.substitution import VariableContext 

98from debputy.transformation_rules import ( 

99 CreateSymlinkReplacementRule, 

100 TransformationRule, 

101 CreateDirectoryTransformationRule, 

102 RemoveTransformationRule, 

103 MoveTransformationRule, 

104 PathMetadataTransformationRule, 

105 CreateSymlinkPathTransformationRule, 

106) 

107from debputy.util import ( 

108 _normalize_path, 

109 PKGNAME_REGEX, 

110 PKGVERSION_REGEX, 

111 debian_policy_normalize_symlink_target, 

112 active_profiles_match, 

113 _error, 

114 _warn, 

115 _info, 

116 assume_not_none, 

117 manifest_format_doc, 

118 PackageTypeSelector, 

119) 

120 

121_DOCUMENTED_DPKG_ARCH_TYPES = { 

122 "HOST": ( 

123 "installed on", 

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

125 ), 

126 "BUILD": ( 

127 "compiled on", 

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

129 ), 

130 "TARGET": ( 

131 "cross-compiler output", 

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

133 ), 

134} 

135 

136_DOCUMENTED_DPKG_ARCH_VARS = { 

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

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

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

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

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

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

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

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

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

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

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

148} 

149 

150 

151_NOT_INTEGRATION_RRR = not_integrations(INTEGRATION_MODE_DH_DEBPUTY_RRR) 

152 

153 

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

155class Capability: 

156 value: str 

157 

158 @classmethod 

159 def parse( 

160 cls, 

161 raw_value: str, 

162 _attribute_path: AttributePath, 

163 _parser_context: "ParserContextData", 

164 ) -> "Capability": 

165 return cls(raw_value) 

166 

167 

168@functools.lru_cache 

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

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

171 has_libcap = False 

172 libcap = None 

173 if cap_library_path: 173 ↛ 180line 173 didn't jump to line 180 because the condition on line 173 was always true

174 try: 

175 libcap = ctypes.cdll.LoadLibrary(cap_library_path) 

176 has_libcap = True 

177 except OSError: 

178 pass 

179 

180 if libcap is None: 180 ↛ 181line 180 didn't jump to line 181 because the condition on line 180 was never true

181 warned = False 

182 

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

184 nonlocal warned 

185 if not warned: 

186 _info( 

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

188 " checking of capabilities." 

189 ) 

190 warned = True 

191 return True 

192 

193 else: 

194 # cap_t cap_from_text(const char *path_p) 

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

196 libcap.cap_from_text.restype = ctypes.c_char_p 

197 

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

199 libcap.cap_free.restype = None 

200 

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

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

203 ok = cap_t is not None 

204 libcap.cap_free(cap_t) 

205 return ok 

206 

207 return has_libcap, cap_library_path, _is_valid_cap 

208 

209 

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

211 _, libcap_path, is_valid_cap = load_libcap() 

212 

213 seen_cap = set() 

214 

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

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

217 seen_cap.add(cap) 

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

219 _warn( 

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

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

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

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

224 ) 

225 

226 return _check_cap 

227 

228 

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

230 try: 

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

232 if changelog is None: 

233 raise DebputyManifestVariableRequiresDebianDirError( 

234 "The changelog was not present" 

235 ) 

236 with changelog.open() as fd: 

237 dch = Changelog(fd, max_blocks=2) 

238 except FileNotFoundError as e: 

239 raise DebputyManifestVariableRequiresDebianDirError( 

240 "The changelog was not present" 

241 ) from e 

242 first_entry = dch[0] 

243 first_non_binnmu_entry = dch[0] 

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

245 first_non_binnmu_entry = dch[1] 

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

247 source_version = first_entry.version 

248 epoch = source_version.epoch 

249 upstream_version = source_version.upstream_version 

250 debian_revision = source_version.debian_revision 

251 epoch_upstream = upstream_version 

252 upstream_debian_revision = upstream_version 

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

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

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

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

257 

258 package = first_entry.package 

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

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

261 

262 date = first_entry.date 

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

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

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

266 else: 

267 _warn( 

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

269 " for SOURCE_DATE_EPOCH" 

270 ) 

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

272 

273 if first_non_binnmu_entry is not first_entry: 

274 non_binnmu_date = first_non_binnmu_entry.date 

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

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

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

278 else: 

279 _warn( 

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

281 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)" 

282 ) 

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

284 else: 

285 snd_source_date_epoch = source_date_epoch 

286 return { 

287 "DEB_SOURCE": package, 

288 "DEB_VERSION": source_version.full_version, 

289 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream, 

290 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision, 

291 "DEB_VERSION_UPSTREAM": upstream_version, 

292 "SOURCE_DATE_EPOCH": source_date_epoch, 

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

294 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch, 

295 } 

296 

297 

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

299 api = cast("DebputyPluginInitializerProvider", public_api) 

300 

301 api.metadata_or_maintscript_detector( 

302 "dpkg-shlibdeps", 

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

304 cast("MetadataAutoDetector", detect_shlibdeps), 

305 package_types=PackageTypeSelector.DEB | PackageTypeSelector.UDEB, 

306 ) 

307 register_type_mappings(api) 

308 register_variables_via_private_api(api) 

309 document_builtin_variables(api) 

310 register_automatic_discard_rules(api) 

311 register_special_ppfs(api) 

312 register_install_rules(api) 

313 register_transformation_rules(api) 

314 register_manifest_condition_rules(api) 

315 register_dpkg_conffile_rules(api) 

316 register_processing_steps(api) 

317 register_service_managers(api) 

318 register_manifest_root_rules(api) 

319 register_binary_package_rules(api) 

320 

321 

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

323 api.register_mapped_type( 

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

325 reference_documentation=type_mapping_reference_documentation( 

326 description=textwrap.dedent( 

327 """\ 

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

329 

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

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

332 currently never emit hard errors for unknown capabilities. 

333 """, 

334 ), 

335 examples=[ 

336 type_mapping_example("cap_chown=p"), 

337 type_mapping_example("cap_chown=ep"), 

338 type_mapping_example("cap_kill-pe"), 

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

340 ], 

341 ), 

342 ) 

343 api.register_mapped_type( 

344 TypeMapping( 

345 FileSystemMatchRule, 

346 str, 

347 FileSystemMatchRule.parse_path_match, 

348 ), 

349 reference_documentation=type_mapping_reference_documentation( 

350 description=textwrap.dedent( 

351 """\ 

352 A generic file system path match with globs. 

353 

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

355 

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

357 

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

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

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

361 

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

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

364 

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

366 or relevant search directories will match. 

367 

368 Please keep in mind that: 

369 

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

371 an anchor reference. 

372 

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

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

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

376 directories (similar to the shell). 

377 

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

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

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

381 """, 

382 ), 

383 examples=[ 

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

385 type_mapping_example("*.txt"), 

386 type_mapping_example("**/foo"), 

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

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

389 ], 

390 ), 

391 ) 

392 

393 api.register_mapped_type( 

394 TypeMapping( 

395 FileSystemExactMatchRule, 

396 str, 

397 FileSystemExactMatchRule.parse_path_match, 

398 ), 

399 reference_documentation=type_mapping_reference_documentation( 

400 description=textwrap.dedent( 

401 """\ 

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

403 

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

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

406 

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

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

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

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

411 """, 

412 ), 

413 examples=[ 

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

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

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

417 ], 

418 ), 

419 ) 

420 

421 api.register_mapped_type( 

422 TypeMapping( 

423 FileSystemExactNonDirMatchRule, 

424 str, 

425 FileSystemExactNonDirMatchRule.parse_path_match, 

426 ), 

427 reference_documentation=type_mapping_reference_documentation( 

428 description=textwrap.dedent( 

429 f"""\ 

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

431 

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

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

434 

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

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

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

438 with a "/". 

439 """, 

440 ), 

441 examples=[ 

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

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

444 ], 

445 ), 

446 ) 

447 

448 api.register_mapped_type( 

449 TypeMapping( 

450 SymlinkTarget, 

451 str, 

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

453 v, ap, assume_not_none(pc).substitution 

454 ), 

455 ), 

456 reference_documentation=type_mapping_reference_documentation( 

457 description=textwrap.dedent( 

458 """\ 

459 A symlink target. 

460 

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

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

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

464 

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

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

467 """, 

468 ), 

469 examples=[ 

470 type_mapping_example("../foo"), 

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

472 ], 

473 ), 

474 ) 

475 

476 api.register_mapped_type( 

477 TypeMapping( 

478 StaticFileSystemOwner, 

479 Union[int, str], 

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

481 ), 

482 reference_documentation=type_mapping_reference_documentation( 

483 description=textwrap.dedent( 

484 """\ 

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

486 

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

488 

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

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

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

492 

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

494 

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

496 """ 

497 ), 

498 examples=[ 

499 type_mapping_example("root"), 

500 type_mapping_example(0), 

501 type_mapping_example("root:0"), 

502 type_mapping_example("bin"), 

503 ], 

504 ), 

505 ) 

506 api.register_mapped_type( 

507 TypeMapping( 

508 StaticFileSystemGroup, 

509 Union[int, str], 

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

511 ), 

512 reference_documentation=type_mapping_reference_documentation( 

513 description=textwrap.dedent( 

514 """\ 

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

516 

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

518 

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

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

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

522 

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

524 

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

526 """ 

527 ), 

528 examples=[ 

529 type_mapping_example("root"), 

530 type_mapping_example(0), 

531 type_mapping_example("root:0"), 

532 type_mapping_example("tty"), 

533 ], 

534 ), 

535 ) 

536 

537 api.register_mapped_type( 

538 TypeMapping( 

539 BinaryPackage, 

540 str, 

541 type_mapper_str2package, 

542 ), 

543 reference_documentation=type_mapping_reference_documentation( 

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

545 ), 

546 ) 

547 

548 api.register_mapped_type( 

549 TypeMapping( 

550 PackageSelector, 

551 str, 

552 PackageSelector.parse, 

553 ), 

554 reference_documentation=type_mapping_reference_documentation( 

555 description=textwrap.dedent( 

556 """\ 

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

558 

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

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

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

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

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

564 respectively). 

565 """ 

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: 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 """\ 

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

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

746 this variable. 

747 """ 

748 ), 

749 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

750 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

751 }, 

752 ) 

753 

754 

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

756 api.document_builtin_variable( 

757 "PACKAGE", 

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

759 is_context_specific=True, 

760 ) 

761 

762 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

763 

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

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

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

767 documentation = textwrap.dedent( 

768 f"""\ 

769 {arch_var_doc} ({arch_type_tag}) 

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

771 * Machine type: {arch_type_doc} 

772 * Value description: {arch_var_doc} 

773 

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

775 """ 

776 ) 

777 api.document_builtin_variable( 

778 full_var, 

779 documentation, 

780 is_for_special_case=arch_type != "HOST", 

781 ) 

782 

783 

784def _format_docbase_filename( 

785 path_format: str, 

786 format_param: PPFFormatParam, 

787 docbase_file: VirtualPath, 

788) -> str: 

789 with docbase_file.open() as fd: 

790 content = Deb822(fd) 

791 proper_name = content["Document"] 

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

793 format_param["name"] = proper_name 

794 else: 

795 _warn( 

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

797 ) 

798 return path_format.format(**format_param) 

799 

800 

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

802 api.packager_provided_file( 

803 "doc-base", 

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

805 format_callback=_format_docbase_filename, 

806 ) 

807 

808 api.packager_provided_file( 

809 "shlibs", 

810 "DEBIAN/shlibs", 

811 allow_name_segment=False, 

812 reservation_only=True, 

813 reference_documentation=packager_provided_file_reference_documentation( 

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

815 ), 

816 ) 

817 api.packager_provided_file( 

818 "symbols", 

819 "DEBIAN/symbols", 

820 allow_name_segment=False, 

821 allow_architecture_segment=True, 

822 reservation_only=True, 

823 reference_documentation=packager_provided_file_reference_documentation( 

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

825 ), 

826 ) 

827 api.packager_provided_file( 

828 "conffiles", 

829 "DEBIAN/conffiles", 

830 allow_name_segment=False, 

831 allow_architecture_segment=True, 

832 reservation_only=True, 

833 ) 

834 api.packager_provided_file( 

835 "templates", 

836 "DEBIAN/templates", 

837 allow_name_segment=False, 

838 allow_architecture_segment=False, 

839 reservation_only=True, 

840 ) 

841 api.packager_provided_file( 

842 "alternatives", 

843 "DEBIAN/alternatives", 

844 allow_name_segment=False, 

845 allow_architecture_segment=True, 

846 reservation_only=True, 

847 ) 

848 

849 

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

851 api.pluggable_manifest_rule( 

852 InstallRule, 

853 MK_INSTALLATIONS_INSTALL, 

854 ParsedInstallRule, 

855 _install_rule_handler, 

856 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

857 inline_reference_documentation=reference_documentation( 

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

859 description=textwrap.dedent( 

860 """\ 

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

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

863 

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

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

866 

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

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

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

870 """.format( 

871 MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL 

872 ) 

873 ), 

874 non_mapping_description=textwrap.dedent( 

875 """\ 

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

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

878 not required. 

879 """ 

880 ), 

881 attributes=[ 

882 documented_attr( 

883 ["source", "sources"], 

884 textwrap.dedent( 

885 """\ 

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

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

888 is tried against default search directories. 

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

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

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

892 """ 

893 ), 

894 ), 

895 documented_attr( 

896 "dest_dir", 

897 textwrap.dedent( 

898 """\ 

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

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

901 to the directory name of the `source`. 

902 """ 

903 ), 

904 ), 

905 documented_attr( 

906 "into", 

907 textwrap.dedent( 

908 """\ 

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

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

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

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

913 """ 

914 ), 

915 ), 

916 documented_attr( 

917 "install_as", 

918 textwrap.dedent( 

919 """\ 

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

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

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

923 """ 

924 ), 

925 ), 

926 *docs_from(DebputyParsedContentStandardConditional), 

927 ], 

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

929 ), 

930 ) 

931 api.pluggable_manifest_rule( 

932 InstallRule, 

933 [ 

934 MK_INSTALLATIONS_INSTALL_DOCS, 

935 "install-doc", 

936 ], 

937 ParsedInstallRule, 

938 _install_docs_rule_handler, 

939 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

940 inline_reference_documentation=reference_documentation( 

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

942 description=textwrap.dedent( 

943 """\ 

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

945 `install` rule with the following key features: 

946 

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

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

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

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

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

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

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

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

955 package listed in `debian/control`. 

956 

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

958 

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

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

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

962 use-case. 

963 """ 

964 ), 

965 non_mapping_description=textwrap.dedent( 

966 """\ 

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

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

969 not required. 

970 """ 

971 ), 

972 attributes=[ 

973 documented_attr( 

974 ["source", "sources"], 

975 textwrap.dedent( 

976 """\ 

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

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

979 is tried against default search directories. 

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

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

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

983 

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

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

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

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

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

989 """ 

990 ), 

991 ), 

992 documented_attr( 

993 "dest_dir", 

994 textwrap.dedent( 

995 """\ 

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

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

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

999 """ 

1000 ), 

1001 ), 

1002 documented_attr( 

1003 "into", 

1004 textwrap.dedent( 

1005 """\ 

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

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

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

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

1010 the key is required. 

1011 """ 

1012 ), 

1013 ), 

1014 documented_attr( 

1015 "install_as", 

1016 textwrap.dedent( 

1017 """\ 

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

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

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

1021 """ 

1022 ), 

1023 ), 

1024 documented_attr( 

1025 "when", 

1026 textwrap.dedent( 

1027 """\ 

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

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

1030 (rather than replacing it). 

1031 """ 

1032 ), 

1033 ), 

1034 ], 

1035 reference_documentation_url=manifest_format_doc( 

1036 "install-documentation-install-docs" 

1037 ), 

1038 ), 

1039 ) 

1040 api.pluggable_manifest_rule( 

1041 InstallRule, 

1042 [ 

1043 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

1044 "install-example", 

1045 ], 

1046 ParsedInstallExamplesRule, 

1047 _install_examples_rule_handler, 

1048 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

1049 inline_reference_documentation=reference_documentation( 

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

1051 description=textwrap.dedent( 

1052 """\ 

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

1054 install` rule with the following key features: 

1055 

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

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

1058 dir. 

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

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

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

1062 package listed in `debian/control`. 

1063 

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

1065 """ 

1066 ), 

1067 non_mapping_description=textwrap.dedent( 

1068 """\ 

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

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

1071 not required. 

1072 """ 

1073 ), 

1074 attributes=[ 

1075 documented_attr( 

1076 ["source", "sources"], 

1077 textwrap.dedent( 

1078 """\ 

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

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

1081 is tried against default search directories. 

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

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

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

1085 

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

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

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

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

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

1091 """ 

1092 ), 

1093 ), 

1094 documented_attr( 

1095 "into", 

1096 textwrap.dedent( 

1097 """\ 

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

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

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

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

1102 Otherwise, the key is required. 

1103 """ 

1104 ), 

1105 ), 

1106 documented_attr( 

1107 "when", 

1108 textwrap.dedent( 

1109 """\ 

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

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

1112 (rather than replacing it). 

1113 """ 

1114 ), 

1115 ), 

1116 ], 

1117 reference_documentation_url=manifest_format_doc( 

1118 "install-examples-install-examples" 

1119 ), 

1120 ), 

1121 ) 

1122 api.pluggable_manifest_rule( 

1123 InstallRule, 

1124 MK_INSTALLATIONS_INSTALL_MAN, 

1125 ParsedInstallManpageRule, 

1126 _install_man_rule_handler, 

1127 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1128 inline_reference_documentation=reference_documentation( 

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

1130 description=textwrap.dedent( 

1131 """\ 

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

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

1134 

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

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

1137 language. 

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

1139 package listed in `debian/control`. 

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

1141 for when the auto-detection is insufficient. 

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

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

1144 

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

1146 """ 

1147 ), 

1148 non_mapping_description=textwrap.dedent( 

1149 """\ 

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

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

1152 not required. 

1153 """ 

1154 ), 

1155 attributes=[ 

1156 documented_attr( 

1157 ["source", "sources"], 

1158 textwrap.dedent( 

1159 """\ 

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

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

1162 is tried against default search directories. 

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

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

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

1166 """ 

1167 ), 

1168 ), 

1169 documented_attr( 

1170 "into", 

1171 textwrap.dedent( 

1172 """\ 

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

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

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

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

1177 """ 

1178 ), 

1179 ), 

1180 documented_attr( 

1181 "section", 

1182 textwrap.dedent( 

1183 """\ 

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

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

1186 have performed. 

1187 """ 

1188 ), 

1189 ), 

1190 documented_attr( 

1191 "language", 

1192 textwrap.dedent( 

1193 """\ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1211 always assumed to be a language. 

1212 """ 

1213 ), 

1214 ), 

1215 *docs_from(DebputyParsedContentStandardConditional), 

1216 ], 

1217 reference_documentation_url=manifest_format_doc( 

1218 "install-manpages-install-man" 

1219 ), 

1220 ), 

1221 ) 

1222 api.pluggable_manifest_rule( 

1223 InstallRule, 

1224 MK_INSTALLATIONS_DISCARD, 

1225 ParsedInstallDiscardRule, 

1226 _install_discard_rule_handler, 

1227 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1228 inline_reference_documentation=reference_documentation( 

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

1230 description=textwrap.dedent( 

1231 """\ 

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

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

1234 

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

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

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

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

1239 """ 

1240 ), 

1241 non_mapping_description=textwrap.dedent( 

1242 """\ 

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

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

1245 """ 

1246 ), 

1247 attributes=[ 

1248 documented_attr( 

1249 ["path", "paths"], 

1250 textwrap.dedent( 

1251 """\ 

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

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

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

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

1256 contents that have not already been installed somewhere. 

1257 """ 

1258 ), 

1259 ), 

1260 documented_attr( 

1261 ["search_dir", "search_dirs"], 

1262 textwrap.dedent( 

1263 """\ 

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

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

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

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

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

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

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

1271 will make `debputy` report an error. 

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

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

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

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

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

1277 applicable to certain builds that are only performed conditionally. 

1278 """ 

1279 ), 

1280 ), 

1281 documented_attr( 

1282 "required_when", 

1283 textwrap.dedent( 

1284 """\ 

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

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

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

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

1289 """ 

1290 ), 

1291 ), 

1292 ], 

1293 reference_documentation_url=manifest_format_doc( 

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

1295 ), 

1296 ), 

1297 ) 

1298 api.pluggable_manifest_rule( 

1299 InstallRule, 

1300 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1301 ParsedMultiDestInstallRule, 

1302 _multi_dest_install_rule_handler, 

1303 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1304 inline_reference_documentation=reference_documentation( 

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

1306 description=textwrap.dedent( 

1307 """\ 

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

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

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

1311 "primary" uses. 

1312 

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

1314 except you list 2+ destination directories. 

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

1316 2+ `as` names. 

1317 """ 

1318 ), 

1319 attributes=[ 

1320 documented_attr( 

1321 ["source", "sources"], 

1322 textwrap.dedent( 

1323 """\ 

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

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

1326 is tried against default search directories. 

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

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

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

1330 """ 

1331 ), 

1332 ), 

1333 documented_attr( 

1334 "dest_dirs", 

1335 textwrap.dedent( 

1336 """\ 

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

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

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

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

1341 """ 

1342 ), 

1343 ), 

1344 documented_attr( 

1345 "into", 

1346 textwrap.dedent( 

1347 """\ 

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

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

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

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

1352 """ 

1353 ), 

1354 ), 

1355 documented_attr( 

1356 "install_as", 

1357 textwrap.dedent( 

1358 """\ 

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

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

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

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

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

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

1365 """ 

1366 ), 

1367 ), 

1368 *docs_from(DebputyParsedContentStandardConditional), 

1369 ], 

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

1371 ), 

1372 ) 

1373 

1374 

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

1376 api.pluggable_manifest_rule( 

1377 TransformationRule, 

1378 "move", 

1379 TransformationMoveRuleSpec, 

1380 _transformation_move_handler, 

1381 inline_reference_documentation=reference_documentation( 

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

1383 description=textwrap.dedent( 

1384 """\ 

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

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

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

1388 Debian specific requirements. 

1389 

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

1391 `mv` command line tool. 

1392 """ 

1393 ), 

1394 attributes=[ 

1395 documented_attr( 

1396 "source", 

1397 textwrap.dedent( 

1398 """\ 

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

1400 and substitutions. 

1401 """ 

1402 ), 

1403 ), 

1404 documented_attr( 

1405 "target", 

1406 textwrap.dedent( 

1407 """\ 

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

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

1410 the target will *always* be a directory. 

1411 """ 

1412 ), 

1413 ), 

1414 *docs_from(DebputyParsedContentStandardConditional), 

1415 ], 

1416 reference_documentation_url=manifest_format_doc( 

1417 "move-transformation-rule-move" 

1418 ), 

1419 ), 

1420 ) 

1421 api.pluggable_manifest_rule( 

1422 TransformationRule, 

1423 "remove", 

1424 TransformationRemoveRuleSpec, 

1425 _transformation_remove_handler, 

1426 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1427 inline_reference_documentation=reference_documentation( 

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

1429 description=textwrap.dedent( 

1430 """\ 

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

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

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

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

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

1436 of `debian/copyright`). 

1437 

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

1439 the `remove` transformation rule. 

1440 

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

1442 """ 

1443 ), 

1444 non_mapping_description=textwrap.dedent( 

1445 """\ 

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

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

1448 """ 

1449 ), 

1450 attributes=[ 

1451 documented_attr( 

1452 ["path", "paths"], 

1453 textwrap.dedent( 

1454 """\ 

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

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

1457 can use globs. 

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

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

1460 along with all the contents. 

1461 """ 

1462 ), 

1463 ), 

1464 documented_attr( 

1465 "keep_empty_parent_dirs", 

1466 textwrap.dedent( 

1467 """\ 

1468 A boolean determining whether to prune parent directories that become 

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

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

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

1472 """ 

1473 ), 

1474 ), 

1475 documented_attr( 

1476 "when", 

1477 textwrap.dedent( 

1478 """\ 

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

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

1481 (rather than replacing it). 

1482 """ 

1483 ), 

1484 ), 

1485 ], 

1486 reference_documentation_url=manifest_format_doc( 

1487 "remove-transformation-rule-remove" 

1488 ), 

1489 ), 

1490 ) 

1491 api.pluggable_manifest_rule( 

1492 TransformationRule, 

1493 "create-symlink", 

1494 CreateSymlinkRule, 

1495 _transformation_create_symlink, 

1496 inline_reference_documentation=reference_documentation( 

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

1498 description=textwrap.dedent( 

1499 """\ 

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

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

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

1503 """ 

1504 ), 

1505 attributes=[ 

1506 documented_attr( 

1507 "path", 

1508 textwrap.dedent( 

1509 """\ 

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

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

1512 Parent directories are implicitly created as necessary. 

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

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

1515 """ 

1516 ), 

1517 ), 

1518 documented_attr( 

1519 "target", 

1520 textwrap.dedent( 

1521 """\ 

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

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

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

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

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

1527 preference. 

1528 """ 

1529 ), 

1530 ), 

1531 documented_attr( 

1532 "replacement_rule", 

1533 textwrap.dedent( 

1534 """\ 

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

1536 be set to one of the following values: 

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

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

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

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

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

1542 similar to `ln -sf` semantics. 

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

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

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

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

1547 will stop with an error. 

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

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

1550 be removed recursively along with the directory. Finally, 

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

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

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

1554 `when` if any). 

1555 

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

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

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

1559 the value in `replacement-rule`. 

1560 """ 

1561 ), 

1562 ), 

1563 *docs_from(DebputyParsedContentStandardConditional), 

1564 ], 

1565 reference_documentation_url=manifest_format_doc( 

1566 "create-symlinks-transformation-rule-create-symlink" 

1567 ), 

1568 ), 

1569 ) 

1570 api.pluggable_manifest_rule( 

1571 TransformationRule, 

1572 "path-metadata", 

1573 PathManifestRule, 

1574 _transformation_path_metadata, 

1575 source_format=PathManifestSourceDictFormat, 

1576 inline_reference_documentation=reference_documentation( 

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

1578 description=textwrap.dedent( 

1579 """\ 

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

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

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

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

1584 transformation. 

1585 

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

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

1588 """ 

1589 ), 

1590 attributes=[ 

1591 documented_attr( 

1592 ["path", "paths"], 

1593 textwrap.dedent( 

1594 """\ 

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

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

1597 and substitution variables. Special-rules for matches: 

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

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

1600 """ 

1601 ), 

1602 ), 

1603 documented_attr( 

1604 "owner", 

1605 textwrap.dedent( 

1606 """\ 

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

1608 no change of owner is done. 

1609 """ 

1610 ), 

1611 ), 

1612 documented_attr( 

1613 "group", 

1614 textwrap.dedent( 

1615 """\ 

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

1617 no change of group is done. 

1618 """ 

1619 ), 

1620 ), 

1621 documented_attr( 

1622 "mode", 

1623 textwrap.dedent( 

1624 """\ 

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

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

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

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

1629 relative to the matched path's current mode. 

1630 """ 

1631 ), 

1632 ), 

1633 documented_attr( 

1634 "capabilities", 

1635 textwrap.dedent( 

1636 """\ 

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

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

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

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

1641 run. 

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

1643 to those paths. 

1644 

1645 """ 

1646 ), 

1647 ), 

1648 documented_attr( 

1649 "capability_mode", 

1650 textwrap.dedent( 

1651 """\ 

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

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

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

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

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

1657 `capabilities` is omitted. 

1658 """ 

1659 ), 

1660 ), 

1661 documented_attr( 

1662 "recursive", 

1663 textwrap.dedent( 

1664 """\ 

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

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

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

1668 this attribute is `false`. 

1669 """ 

1670 ), 

1671 ), 

1672 *docs_from(DebputyParsedContentStandardConditional), 

1673 ], 

1674 reference_documentation_url=manifest_format_doc( 

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

1676 ), 

1677 ), 

1678 ) 

1679 api.pluggable_manifest_rule( 

1680 TransformationRule, 

1681 "create-directories", 

1682 EnsureDirectoryRule, 

1683 _transformation_mkdirs, 

1684 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1685 inline_reference_documentation=reference_documentation( 

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

1687 description=textwrap.dedent( 

1688 """\ 

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

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

1691 transformations will create directories as required. 

1692 

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

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

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

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

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

1698 

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

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

1701 """ 

1702 ), 

1703 non_mapping_description=textwrap.dedent( 

1704 """\ 

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

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

1707 """ 

1708 ), 

1709 attributes=[ 

1710 documented_attr( 

1711 ["path", "paths"], 

1712 textwrap.dedent( 

1713 """\ 

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

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

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

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

1718 are affected by the owner/mode options) 

1719 """ 

1720 ), 

1721 ), 

1722 documented_attr( 

1723 "owner", 

1724 textwrap.dedent( 

1725 """\ 

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

1727 Default is "root". 

1728 """ 

1729 ), 

1730 ), 

1731 documented_attr( 

1732 "group", 

1733 textwrap.dedent( 

1734 """\ 

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

1736 Default is "root". 

1737 """ 

1738 ), 

1739 ), 

1740 documented_attr( 

1741 "mode", 

1742 textwrap.dedent( 

1743 """\ 

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

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

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

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

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

1749 transformation. The default is "0755". 

1750 """ 

1751 ), 

1752 ), 

1753 *docs_from(DebputyParsedContentStandardConditional), 

1754 ], 

1755 reference_documentation_url=manifest_format_doc( 

1756 "create-directories-transformation-rule-directories" 

1757 ), 

1758 ), 

1759 ) 

1760 

1761 

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

1763 api.provide_manifest_keyword( 

1764 ManifestCondition, 

1765 "cross-compiling", 

1766 lambda *_: ManifestCondition.is_cross_building(), 

1767 ) 

1768 api.provide_manifest_keyword( 

1769 ManifestCondition, 

1770 "can-execute-compiled-binaries", 

1771 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 

1772 ) 

1773 api.provide_manifest_keyword( 

1774 ManifestCondition, 

1775 "run-build-time-tests", 

1776 lambda *_: ManifestCondition.run_build_time_tests(), 

1777 ) 

1778 

1779 api.pluggable_manifest_rule( 

1780 ManifestCondition, 

1781 "not", 

1782 MCNot, 

1783 _mc_not, 

1784 source_format=ManifestCondition, 

1785 ) 

1786 api.pluggable_manifest_rule( 

1787 ManifestCondition, 

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

1789 MCAnyOfAllOf, 

1790 _mc_any_of, 

1791 source_format=list[ManifestCondition], 

1792 ) 

1793 api.pluggable_manifest_rule( 

1794 ManifestCondition, 

1795 "arch-matches", 

1796 MCArchMatches, 

1797 _mc_arch_matches, 

1798 source_format=str, 

1799 inline_reference_documentation=reference_documentation( 

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

1801 description=textwrap.dedent( 

1802 """\ 

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

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

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

1806 and practically behaves like a comparison against 

1807 `dpkg-architecture -qDEB_HOST_ARCH`. 

1808 

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

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

1811 in the context of a binary package and like 

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

1813 are covered in their own keywords. 

1814 """ 

1815 ), 

1816 non_mapping_description=textwrap.dedent( 

1817 """\ 

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

1819 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1824 have it. 

1825 """ 

1826 ), 

1827 reference_documentation_url=manifest_format_doc( 

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

1829 ), 

1830 ), 

1831 ) 

1832 

1833 context_arch_doc = reference_documentation( 

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

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

1836 description=textwrap.dedent( 

1837 """\ 

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

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

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

1841 `arch-matches` condition. 

1842 

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

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

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

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

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

1848 to the packager that condition does not make sense. 

1849 

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

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

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

1853 very special cases). 

1854 

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

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

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

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

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

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

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

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

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

1864 need to care about nor use any of this. 

1865 

1866 Accordingly, the possible conditions are: 

1867 

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

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

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

1871 

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

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

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

1875 

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

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

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

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

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

1881 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1882 

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

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

1885 

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

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

1888 """ 

1889 ), 

1890 non_mapping_description=textwrap.dedent( 

1891 """\ 

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

1893 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1898 have it. 

1899 """ 

1900 ), 

1901 ) 

1902 

1903 api.pluggable_manifest_rule( 

1904 ManifestCondition, 

1905 "source-context-arch-matches", 

1906 MCArchMatches, 

1907 _mc_source_context_arch_matches, 

1908 source_format=str, 

1909 inline_reference_documentation=context_arch_doc, 

1910 ) 

1911 api.pluggable_manifest_rule( 

1912 ManifestCondition, 

1913 "package-context-arch-matches", 

1914 MCArchMatches, 

1915 _mc_arch_matches, 

1916 source_format=str, 

1917 inline_reference_documentation=context_arch_doc, 

1918 ) 

1919 api.pluggable_manifest_rule( 

1920 ManifestCondition, 

1921 "build-profiles-matches", 

1922 MCBuildProfileMatches, 

1923 _mc_build_profile_matches, 

1924 source_format=str, 

1925 ) 

1926 

1927 

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

1929 api.pluggable_manifest_rule( 

1930 DpkgMaintscriptHelperCommand, 

1931 "remove", 

1932 DpkgRemoveConffileRule, 

1933 _dpkg_conffile_remove, 

1934 inline_reference_documentation=None, # TODO: write and add 

1935 ) 

1936 

1937 api.pluggable_manifest_rule( 

1938 DpkgMaintscriptHelperCommand, 

1939 "rename", 

1940 DpkgRenameConffileRule, 

1941 _dpkg_conffile_rename, 

1942 inline_reference_documentation=None, # TODO: write and add 

1943 ) 

1944 

1945 

1946class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

1947 mode: NotRequired[FileSystemMode] 

1948 owner: NotRequired[StaticFileSystemOwner] 

1949 group: NotRequired[StaticFileSystemGroup] 

1950 

1951 

1952class PathManifestSourceDictFormat(_ModeOwnerBase): 

1953 path: NotRequired[ 

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

1955 ] 

1956 paths: NotRequired[list[FileSystemMatchRule]] 

1957 recursive: NotRequired[bool] 

1958 capabilities: NotRequired[Capability] 

1959 capability_mode: NotRequired[FileSystemMode] 

1960 

1961 

1962class PathManifestRule(_ModeOwnerBase): 

1963 paths: list[FileSystemMatchRule] 

1964 recursive: NotRequired[bool] 

1965 capabilities: NotRequired[Capability] 

1966 capability_mode: NotRequired[FileSystemMode] 

1967 

1968 

1969class EnsureDirectorySourceFormat(_ModeOwnerBase): 

1970 path: NotRequired[ 

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

1972 ] 

1973 paths: NotRequired[list[FileSystemExactMatchRule]] 

1974 

1975 

1976class EnsureDirectoryRule(_ModeOwnerBase): 

1977 paths: list[FileSystemExactMatchRule] 

1978 

1979 

1980class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

1981 path: FileSystemExactMatchRule 

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

1983 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

1984 

1985 

1986class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

1987 source: FileSystemMatchRule 

1988 target: FileSystemExactMatchRule 

1989 

1990 

1991class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

1992 paths: list[FileSystemMatchRule] 

1993 keep_empty_parent_dirs: NotRequired[bool] 

1994 

1995 

1996class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

1997 path: NotRequired[ 

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

1999 ] 

2000 paths: NotRequired[list[FileSystemMatchRule]] 

2001 keep_empty_parent_dirs: NotRequired[bool] 

2002 

2003 

2004class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2005 sources: NotRequired[list[FileSystemMatchRule]] 

2006 source: NotRequired[ 

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

2008 ] 

2009 into: NotRequired[ 

2010 Annotated[ 

2011 str | list[str], 

2012 DebputyParseHint.required_when_multi_binary(), 

2013 ] 

2014 ] 

2015 dest_dir: NotRequired[ 

2016 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2017 ] 

2018 install_as: NotRequired[ 

2019 Annotated[ 

2020 FileSystemExactMatchRule, 

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

2022 DebputyParseHint.manifest_attribute("as"), 

2023 DebputyParseHint.not_path_error_hint(), 

2024 ] 

2025 ] 

2026 

2027 

2028class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

2029 sources: NotRequired[list[FileSystemMatchRule]] 

2030 source: NotRequired[ 

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

2032 ] 

2033 into: NotRequired[ 

2034 Annotated[ 

2035 str | list[str], 

2036 DebputyParseHint.required_when_multi_binary( 

2037 package_types=PackageTypeSelector.DEB 

2038 ), 

2039 ] 

2040 ] 

2041 dest_dir: NotRequired[ 

2042 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2043 ] 

2044 install_as: NotRequired[ 

2045 Annotated[ 

2046 FileSystemExactMatchRule, 

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

2048 DebputyParseHint.manifest_attribute("as"), 

2049 DebputyParseHint.not_path_error_hint(), 

2050 ] 

2051 ] 

2052 

2053 

2054class ParsedInstallRule(DebputyParsedContentStandardConditional): 

2055 sources: list[FileSystemMatchRule] 

2056 into: NotRequired[list[BinaryPackage]] 

2057 dest_dir: NotRequired[FileSystemExactMatchRule] 

2058 install_as: NotRequired[FileSystemExactMatchRule] 

2059 

2060 

2061class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2062 sources: NotRequired[list[FileSystemMatchRule]] 

2063 source: NotRequired[ 

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

2065 ] 

2066 into: NotRequired[ 

2067 Annotated[ 

2068 str | list[str], 

2069 DebputyParseHint.required_when_multi_binary(), 

2070 ] 

2071 ] 

2072 dest_dirs: NotRequired[ 

2073 Annotated[ 

2074 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

2075 ] 

2076 ] 

2077 install_as: NotRequired[ 

2078 Annotated[ 

2079 list[FileSystemExactMatchRule], 

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

2081 DebputyParseHint.not_path_error_hint(), 

2082 DebputyParseHint.manifest_attribute("as"), 

2083 ] 

2084 ] 

2085 

2086 

2087class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

2088 sources: list[FileSystemMatchRule] 

2089 into: NotRequired[list[BinaryPackage]] 

2090 dest_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2091 install_as: NotRequired[list[FileSystemExactMatchRule]] 

2092 

2093 

2094class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

2095 sources: list[FileSystemMatchRule] 

2096 into: NotRequired[list[BinaryPackage]] 

2097 

2098 

2099class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

2100 sources: NotRequired[list[FileSystemMatchRule]] 

2101 source: NotRequired[ 

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

2103 ] 

2104 into: NotRequired[ 

2105 Annotated[ 

2106 str | list[str], 

2107 DebputyParseHint.required_when_multi_binary( 

2108 package_types=PackageTypeSelector.DEB 

2109 ), 

2110 ] 

2111 ] 

2112 

2113 

2114class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

2115 sources: list[FileSystemMatchRule] 

2116 language: NotRequired[str] 

2117 section: NotRequired[int] 

2118 into: NotRequired[list[BinaryPackage]] 

2119 

2120 

2121class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

2122 sources: NotRequired[list[FileSystemMatchRule]] 

2123 source: NotRequired[ 

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

2125 ] 

2126 language: NotRequired[str] 

2127 section: NotRequired[int] 

2128 into: NotRequired[ 

2129 Annotated[ 

2130 str | list[str], 

2131 DebputyParseHint.required_when_multi_binary( 

2132 package_types=PackageTypeSelector.DEB 

2133 ), 

2134 ] 

2135 ] 

2136 

2137 

2138class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2139 paths: NotRequired[list[FileSystemMatchRule]] 

2140 path: NotRequired[ 

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

2142 ] 

2143 search_dir: NotRequired[ 

2144 Annotated[ 

2145 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2146 ] 

2147 ] 

2148 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2149 required_when: NotRequired[ManifestCondition] 

2150 

2151 

2152class ParsedInstallDiscardRule(DebputyParsedContent): 

2153 paths: list[FileSystemMatchRule] 

2154 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2155 required_when: NotRequired[ManifestCondition] 

2156 

2157 

2158class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2159 prior_to_version: NotRequired[str] 

2160 owning_package: NotRequired[str] 

2161 

2162 

2163class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2164 source: str 

2165 target: str 

2166 

2167 

2168class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2169 path: str 

2170 

2171 

2172class MCAnyOfAllOf(DebputyParsedContent): 

2173 conditions: list[ManifestCondition] 

2174 

2175 

2176class MCNot(DebputyParsedContent): 

2177 negated_condition: ManifestCondition 

2178 

2179 

2180class MCArchMatches(DebputyParsedContent): 

2181 arch_matches: str 

2182 

2183 

2184class MCBuildProfileMatches(DebputyParsedContent): 

2185 build_profile_matches: str 

2186 

2187 

2188def _parse_filename( 

2189 filename: str, 

2190 attribute_path: AttributePath, 

2191 *, 

2192 allow_directories: bool = True, 

2193) -> str: 

2194 try: 

2195 normalized_path = _normalize_path(filename, with_prefix=False) 

2196 except ValueError as e: 

2197 raise ManifestParseException( 

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

2199 ) from None 

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

2201 raise ManifestParseException( 

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

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

2204 ) 

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

2206 raise ManifestParseException( 

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

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

2209 ) 

2210 return normalized_path 

2211 

2212 

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

2214 return Union[ 

2215 t, 

2216 list[str], 

2217 str, 

2218 ] 

2219 

2220 

2221def _dpkg_conffile_rename( 

2222 _name: str, 

2223 parsed_data: DpkgRenameConffileRule, 

2224 path: AttributePath, 

2225 _context: ParserContextData, 

2226) -> DpkgMaintscriptHelperCommand: 

2227 source_file = parsed_data["source"] 

2228 target_file = parsed_data["target"] 

2229 normalized_source = _parse_filename( 

2230 source_file, 

2231 path["source"], 

2232 allow_directories=False, 

2233 ) 

2234 path.path_hint = source_file 

2235 

2236 normalized_target = _parse_filename( 

2237 target_file, 

2238 path["target"], 

2239 allow_directories=False, 

2240 ) 

2241 normalized_source = "/" + normalized_source 

2242 normalized_target = "/" + normalized_target 

2243 

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

2245 raise ManifestParseException( 

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

2247 ) 

2248 

2249 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2250 parsed_data, path 

2251 ) 

2252 return DpkgMaintscriptHelperCommand.mv_conffile( 

2253 path, 

2254 normalized_source, 

2255 normalized_target, 

2256 version, 

2257 owning_package, 

2258 ) 

2259 

2260 

2261def _dpkg_conffile_remove( 

2262 _name: str, 

2263 parsed_data: DpkgRemoveConffileRule, 

2264 path: AttributePath, 

2265 _context: ParserContextData, 

2266) -> DpkgMaintscriptHelperCommand: 

2267 source_file = parsed_data["path"] 

2268 normalized_source = _parse_filename( 

2269 source_file, 

2270 path["path"], 

2271 allow_directories=False, 

2272 ) 

2273 path.path_hint = source_file 

2274 

2275 normalized_source = "/" + normalized_source 

2276 

2277 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2278 parsed_data, path 

2279 ) 

2280 return DpkgMaintscriptHelperCommand.rm_conffile( 

2281 path, 

2282 normalized_source, 

2283 version, 

2284 owning_package, 

2285 ) 

2286 

2287 

2288def _parse_conffile_prior_version_and_owning_package( 

2289 d: DpkgConffileManagementRuleBase, 

2290 attribute_path: AttributePath, 

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

2292 prior_version = d.get("prior_to_version") 

2293 owning_package = d.get("owning_package") 

2294 

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

2296 p = attribute_path["prior_to_version"] 

2297 raise ManifestParseException( 

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

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

2300 ) 

2301 

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

2303 p = attribute_path["owning_package"] 

2304 raise ManifestParseException( 

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

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

2307 ) 

2308 

2309 return prior_version, owning_package 

2310 

2311 

2312def _install_rule_handler( 

2313 _name: str, 

2314 parsed_data: ParsedInstallRule, 

2315 path: AttributePath, 

2316 context: ParserContextData, 

2317) -> InstallRule: 

2318 sources = parsed_data["sources"] 

2319 install_as = parsed_data.get("install_as") 

2320 into = parsed_data.get("into") 

2321 dest_dir = parsed_data.get("dest_dir") 

2322 condition = parsed_data.get("when") 

2323 if not into: 

2324 into = [context.single_binary_package(path, package_attribute="into")] 

2325 into = frozenset(into) 

2326 if install_as is not None: 

2327 assert len(sources) == 1 

2328 assert dest_dir is None 

2329 return InstallRule.install_as( 

2330 sources[0], 

2331 install_as.match_rule.path, 

2332 into, 

2333 path.path, 

2334 condition, 

2335 ) 

2336 return InstallRule.install_dest( 

2337 sources, 

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

2339 into, 

2340 path.path, 

2341 condition, 

2342 ) 

2343 

2344 

2345def _multi_dest_install_rule_handler( 

2346 _name: str, 

2347 parsed_data: ParsedMultiDestInstallRule, 

2348 path: AttributePath, 

2349 context: ParserContextData, 

2350) -> InstallRule: 

2351 sources = parsed_data["sources"] 

2352 install_as = parsed_data.get("install_as") 

2353 into = parsed_data.get("into") 

2354 dest_dirs = parsed_data.get("dest_dirs") 

2355 condition = parsed_data.get("when") 

2356 if not into: 2356 ↛ 2358line 2356 didn't jump to line 2358 because the condition on line 2356 was always true

2357 into = [context.single_binary_package(path, package_attribute="into")] 

2358 into = frozenset(into) 

2359 if install_as is not None: 

2360 assert len(sources) == 1 

2361 assert dest_dirs is None 

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

2363 raise ManifestParseException( 

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

2365 ) 

2366 return InstallRule.install_multi_as( 

2367 sources[0], 

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

2369 into, 

2370 path.path, 

2371 condition, 

2372 ) 

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

2374 raise ManifestParseException( 

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

2376 ) 

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

2378 raise ManifestParseException( 

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

2380 ) 

2381 return InstallRule.install_multi_dest( 

2382 sources, 

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

2384 into, 

2385 path.path, 

2386 condition, 

2387 ) 

2388 

2389 

2390def _install_docs_rule_handler( 

2391 _name: str, 

2392 parsed_data: ParsedInstallRule, 

2393 path: AttributePath, 

2394 context: ParserContextData, 

2395) -> InstallRule: 

2396 sources = parsed_data["sources"] 

2397 install_as = parsed_data.get("install_as") 

2398 into = parsed_data.get("into") 

2399 dest_dir = parsed_data.get("dest_dir") 

2400 condition = parsed_data.get("when") 

2401 if not into: 2401 ↛ 2407line 2401 didn't jump to line 2407 because the condition on line 2401 was always true

2402 into = [ 

2403 context.single_binary_package( 

2404 path, package_types=PackageTypeSelector.DEB, package_attribute="into" 

2405 ) 

2406 ] 

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

2408 assert len(sources) == 1 

2409 assert dest_dir is None 

2410 return InstallRule.install_doc_as( 

2411 sources[0], 

2412 install_as.match_rule.path, 

2413 frozenset(into), 

2414 path.path, 

2415 condition, 

2416 ) 

2417 return InstallRule.install_doc( 

2418 sources, 

2419 dest_dir, 

2420 frozenset(into), 

2421 path.path, 

2422 condition, 

2423 ) 

2424 

2425 

2426def _install_examples_rule_handler( 

2427 _name: str, 

2428 parsed_data: ParsedInstallExamplesRule, 

2429 path: AttributePath, 

2430 context: ParserContextData, 

2431) -> InstallRule: 

2432 sources = parsed_data["sources"] 

2433 into = parsed_data.get("into") 

2434 if not into: 

2435 into = [ 

2436 context.single_binary_package( 

2437 path, package_types=PackageTypeSelector.DEB, package_attribute="into" 

2438 ) 

2439 ] 

2440 condition = parsed_data.get("when") 

2441 into = frozenset(into) 

2442 return InstallRule.install_examples( 

2443 sources, 

2444 into, 

2445 path.path, 

2446 condition, 

2447 ) 

2448 

2449 

2450def _install_man_rule_handler( 

2451 _name: str, 

2452 parsed_data: ParsedInstallManpageRule, 

2453 attribute_path: AttributePath, 

2454 context: ParserContextData, 

2455) -> InstallRule: 

2456 sources = parsed_data["sources"] 

2457 language = parsed_data.get("language") 

2458 section = parsed_data.get("section") 

2459 

2460 if language is not None: 

2461 is_lang_ok = language in ( 

2462 "C", 

2463 "derive-from-basename", 

2464 "derive-from-path", 

2465 ) 

2466 

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

2468 is_lang_ok = True 

2469 

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

2471 not is_lang_ok 

2472 and len(language) == 5 

2473 and language[2] == "_" 

2474 and language[:2].islower() 

2475 and language[3:].isupper() 

2476 ): 

2477 is_lang_ok = True 

2478 

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

2480 raise ManifestParseException( 

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

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

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

2484 ) 

2485 

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

2487 raise ManifestParseException( 

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

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

2490 ) 

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

2492 raise ManifestParseException( 

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

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

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

2496 ) 

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

2498 raise ManifestParseException( 

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

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

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

2502 ) 

2503 into = parsed_data.get("into") 

2504 if not into: 2504 ↛ 2512line 2504 didn't jump to line 2512 because the condition on line 2504 was always true

2505 into = [ 

2506 context.single_binary_package( 

2507 attribute_path, 

2508 package_types=PackageTypeSelector.DEB, 

2509 package_attribute="into", 

2510 ) 

2511 ] 

2512 condition = parsed_data.get("when") 

2513 return InstallRule.install_man( 

2514 sources, 

2515 frozenset(into), 

2516 section, 

2517 language, 

2518 attribute_path.path, 

2519 condition, 

2520 ) 

2521 

2522 

2523def _install_discard_rule_handler( 

2524 _name: str, 

2525 parsed_data: ParsedInstallDiscardRule, 

2526 path: AttributePath, 

2527 _context: ParserContextData, 

2528) -> InstallRule: 

2529 limit_to = parsed_data.get("search_dirs") 

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

2531 p = path["search_dirs"] 

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

2533 condition = parsed_data.get("required_when") 

2534 return InstallRule.discard_paths( 

2535 parsed_data["paths"], 

2536 path.path, 

2537 condition, 

2538 limit_to=limit_to, 

2539 ) 

2540 

2541 

2542def _transformation_move_handler( 

2543 _name: str, 

2544 parsed_data: TransformationMoveRuleSpec, 

2545 path: AttributePath, 

2546 _context: ParserContextData, 

2547) -> TransformationRule: 

2548 source_match = parsed_data["source"] 

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

2550 condition = parsed_data.get("when") 

2551 

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

2553 isinstance(source_match, ExactFileSystemPath) 

2554 and source_match.path == target_path 

2555 ): 

2556 raise ManifestParseException( 

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

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

2559 ) 

2560 return MoveTransformationRule( 

2561 source_match.match_rule, 

2562 target_path, 

2563 target_path.endswith("/"), 

2564 path, 

2565 condition, 

2566 ) 

2567 

2568 

2569def _transformation_remove_handler( 

2570 _name: str, 

2571 parsed_data: TransformationRemoveRuleSpec, 

2572 attribute_path: AttributePath, 

2573 _context: ParserContextData, 

2574) -> TransformationRule: 

2575 paths = parsed_data["paths"] 

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

2577 

2578 return RemoveTransformationRule( 

2579 [m.match_rule for m in paths], 

2580 keep_empty_parent_dirs, 

2581 attribute_path, 

2582 ) 

2583 

2584 

2585def _transformation_create_symlink( 

2586 _name: str, 

2587 parsed_data: CreateSymlinkRule, 

2588 attribute_path: AttributePath, 

2589 _context: ParserContextData, 

2590) -> TransformationRule: 

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

2592 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2593 "replacement_rule", 

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

2595 ) 

2596 try: 

2597 link_target = debian_policy_normalize_symlink_target( 

2598 link_dest, 

2599 parsed_data["target"].symlink_target, 

2600 ) 

2601 except ValueError as e: # pragma: no cover 

2602 raise AssertionError( 

2603 "Debian Policy normalization should not raise ValueError here" 

2604 ) from e 

2605 

2606 condition = parsed_data.get("when") 

2607 

2608 return CreateSymlinkPathTransformationRule( 

2609 link_target, 

2610 link_dest, 

2611 replacement_rule, 

2612 attribute_path, 

2613 condition, 

2614 ) 

2615 

2616 

2617def _transformation_path_metadata( 

2618 _name: str, 

2619 parsed_data: PathManifestRule, 

2620 attribute_path: AttributePath, 

2621 context: ParserContextData, 

2622) -> TransformationRule: 

2623 match_rules = parsed_data["paths"] 

2624 owner = parsed_data.get("owner") 

2625 group = parsed_data.get("group") 

2626 mode = parsed_data.get("mode") 

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

2628 capabilities = parsed_data.get("capabilities") 

2629 capability_mode = parsed_data.get("capability_mode") 

2630 cap: str | None = None 

2631 

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

2633 check_integration_mode( 

2634 attribute_path["capabilities"], 

2635 context, 

2636 _NOT_INTEGRATION_RRR, 

2637 ) 

2638 if capability_mode is None: 

2639 capability_mode = SymbolicMode.parse_filesystem_mode( 

2640 "a-s", 

2641 attribute_path["capability-mode"], 

2642 ) 

2643 cap = capabilities.value 

2644 validate_cap = check_cap_checker() 

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

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

2647 check_integration_mode( 

2648 attribute_path["capability_mode"], 

2649 context, 

2650 _NOT_INTEGRATION_RRR, 

2651 ) 

2652 raise ManifestParseException( 

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

2654 f" in {attribute_path.path}" 

2655 ) 

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

2657 raise ManifestParseException( 

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

2659 f" in {attribute_path.path}" 

2660 ) 

2661 condition = parsed_data.get("when") 

2662 

2663 return PathMetadataTransformationRule( 

2664 [m.match_rule for m in match_rules], 

2665 owner, 

2666 group, 

2667 mode, 

2668 recursive, 

2669 cap, 

2670 capability_mode, 

2671 attribute_path.path, 

2672 condition, 

2673 ) 

2674 

2675 

2676def _transformation_mkdirs( 

2677 _name: str, 

2678 parsed_data: EnsureDirectoryRule, 

2679 attribute_path: AttributePath, 

2680 _context: ParserContextData, 

2681) -> TransformationRule: 

2682 provided_paths = parsed_data["paths"] 

2683 owner = parsed_data.get("owner") 

2684 group = parsed_data.get("group") 

2685 mode = parsed_data.get("mode") 

2686 

2687 condition = parsed_data.get("when") 

2688 

2689 return CreateDirectoryTransformationRule( 

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

2691 owner, 

2692 group, 

2693 mode, 

2694 attribute_path.path, 

2695 condition, 

2696 ) 

2697 

2698 

2699def _at_least_two( 

2700 content: list[Any], 

2701 attribute_path: AttributePath, 

2702 attribute_name: str, 

2703) -> None: 

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

2705 raise ManifestParseException( 

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

2707 ) 

2708 

2709 

2710def _mc_any_of( 

2711 name: str, 

2712 parsed_data: MCAnyOfAllOf, 

2713 attribute_path: AttributePath, 

2714 _context: ParserContextData, 

2715) -> ManifestCondition: 

2716 conditions = parsed_data["conditions"] 

2717 _at_least_two(conditions, attribute_path, "conditions") 

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

2719 return ManifestCondition.any_of(conditions) 

2720 assert name == "all-of" 

2721 return ManifestCondition.all_of(conditions) 

2722 

2723 

2724def _mc_not( 

2725 _name: str, 

2726 parsed_data: MCNot, 

2727 _attribute_path: AttributePath, 

2728 _context: ParserContextData, 

2729) -> ManifestCondition: 

2730 condition = parsed_data["negated_condition"] 

2731 return condition.negated() 

2732 

2733 

2734def _extract_arch_matches( 

2735 parsed_data: MCArchMatches, 

2736 attribute_path: AttributePath, 

2737) -> list[str]: 

2738 arch_matches_as_str = parsed_data["arch_matches"] 

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

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

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

2742 # of each other. 

2743 arch_matches_as_list = arch_matches_as_str.split() 

2744 attr_path = attribute_path["arch_matches"] 

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

2746 raise ManifestParseException( 

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

2748 ) 

2749 

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

2751 "]" 

2752 ): 

2753 raise ManifestParseException( 

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

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

2756 ) 

2757 return arch_matches_as_list 

2758 

2759 

2760def _mc_source_context_arch_matches( 

2761 _name: str, 

2762 parsed_data: MCArchMatches, 

2763 attribute_path: AttributePath, 

2764 _context: ParserContextData, 

2765) -> ManifestCondition: 

2766 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2767 return SourceContextArchMatchManifestCondition(arch_matches) 

2768 

2769 

2770def _mc_package_context_arch_matches( 

2771 name: str, 

2772 parsed_data: MCArchMatches, 

2773 attribute_path: AttributePath, 

2774 context: ParserContextData, 

2775) -> ManifestCondition: 

2776 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2777 

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

2779 raise ManifestParseException( 

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

2781 ) 

2782 

2783 package_state = context.current_binary_package_state 

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

2785 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2786 "all", arch_matches 

2787 ) 

2788 attr_path = attribute_path["arch_matches"] 

2789 raise ManifestParseException( 

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

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

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

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

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

2795 ) 

2796 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2797 

2798 

2799def _mc_arch_matches( 

2800 name: str, 

2801 parsed_data: MCArchMatches, 

2802 attribute_path: AttributePath, 

2803 context: ParserContextData, 

2804) -> ManifestCondition: 

2805 if context.is_in_binary_package_state: 

2806 return _mc_package_context_arch_matches( 

2807 name, parsed_data, attribute_path, context 

2808 ) 

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

2810 

2811 

2812def _mc_build_profile_matches( 

2813 _name: str, 

2814 parsed_data: MCBuildProfileMatches, 

2815 attribute_path: AttributePath, 

2816 _context: ParserContextData, 

2817) -> ManifestCondition: 

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

2819 attr_path = attribute_path["build_profile_matches"] 

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

2821 raise ManifestParseException( 

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

2823 ) 

2824 try: 

2825 active_profiles_match(build_profile_spec, frozenset()) 

2826 except ValueError as e: 

2827 raise ManifestParseException( 

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

2829 ) 

2830 return BuildProfileMatch(build_profile_spec)