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

537 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-02-14 10:41 +0000

1import ctypes 

2import ctypes.util 

3import dataclasses 

4import functools 

5import textwrap 

6import time 

7from datetime import datetime 

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

9from collections.abc import Callable 

10 

11from debian.changelog import Changelog 

12from debian.deb822 import Deb822 

13 

14import debputy.plugin.api.spec 

15from debputy._manifest_constants import ( 

16 MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE, 

17 MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION, 

18 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

19 MK_INSTALLATIONS_INSTALL, 

20 MK_INSTALLATIONS_INSTALL_DOCS, 

21 MK_INSTALLATIONS_INSTALL_MAN, 

22 MK_INSTALLATIONS_DISCARD, 

23 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

24) 

25from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError 

26from debputy.installations import InstallRule 

27from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand 

28from debputy.manifest_conditions import ( 

29 ManifestCondition, 

30 BinaryPackageContextArchMatchManifestCondition, 

31 BuildProfileMatch, 

32 SourceContextArchMatchManifestCondition, 

33) 

34from debputy.manifest_parser.base_types import ( 

35 FileSystemMode, 

36 StaticFileSystemOwner, 

37 StaticFileSystemGroup, 

38 SymlinkTarget, 

39 FileSystemExactMatchRule, 

40 FileSystemMatchRule, 

41 SymbolicMode, 

42 OctalMode, 

43 FileSystemExactNonDirMatchRule, 

44 BuildEnvironmentDefinition, 

45 DebputyParsedContentStandardConditional, 

46) 

47from debputy.manifest_parser.exceptions import ManifestParseException 

48from debputy.manifest_parser.mapper_code import type_mapper_str2package, PackageSelector 

49from debputy.manifest_parser.parse_hints import DebputyParseHint 

50from debputy.manifest_parser.parser_data import ParserContextData 

51from debputy.manifest_parser.tagging_types import ( 

52 DebputyParsedContent, 

53 TypeMapping, 

54) 

55from debputy.manifest_parser.util import AttributePath, check_integration_mode 

56from debputy.packages import BinaryPackage 

57from debputy.path_matcher import ExactFileSystemPath 

58from debputy.plugin.api import ( 

59 DebputyPluginInitializer, 

60 documented_attr, 

61 reference_documentation, 

62 VirtualPath, 

63 packager_provided_file_reference_documentation, 

64) 

65from debputy.plugin.api.impl import DebputyPluginInitializerProvider 

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

67from debputy.plugin.api.spec import ( 

68 type_mapping_reference_documentation, 

69 type_mapping_example, 

70 not_integrations, 

71 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

72) 

73from debputy.plugin.api.std_docs import docs_from 

74from debputy.plugins.debputy.binary_package_rules import register_binary_package_rules 

75from debputy.plugins.debputy.discard_rules import ( 

76 _debputy_discard_pyc_files, 

77 _debputy_prune_la_files, 

78 _debputy_prune_doxygen_cruft, 

79 _debputy_prune_binary_debian_dir, 

80 _debputy_prune_info_dir_file, 

81 _debputy_prune_backup_files, 

82 _debputy_prune_vcs_paths, 

83) 

84from debputy.plugins.debputy.manifest_root_rules import register_manifest_root_rules 

85from debputy.plugins.debputy.package_processors import ( 

86 process_manpages, 

87 apply_compression, 

88 clean_la_files, 

89) 

90from debputy.plugins.debputy.service_management import ( 

91 detect_systemd_service_files, 

92 generate_snippets_for_systemd_units, 

93 detect_sysv_init_service_files, 

94 generate_snippets_for_init_scripts, 

95) 

96from debputy.plugins.debputy.shlib_metadata_detectors import detect_shlibdeps 

97from debputy.plugins.debputy.strip_non_determinism import strip_non_determinism 

98from debputy.substitution import VariableContext 

99from debputy.transformation_rules import ( 

100 CreateSymlinkReplacementRule, 

101 TransformationRule, 

102 CreateDirectoryTransformationRule, 

103 RemoveTransformationRule, 

104 MoveTransformationRule, 

105 PathMetadataTransformationRule, 

106 CreateSymlinkPathTransformationRule, 

107) 

108from debputy.util import ( 

109 _normalize_path, 

110 PKGNAME_REGEX, 

111 PKGVERSION_REGEX, 

112 debian_policy_normalize_symlink_target, 

113 active_profiles_match, 

114 _error, 

115 _warn, 

116 _info, 

117 assume_not_none, 

118 manifest_format_doc, 

119 PackageTypeSelector, 

120) 

121 

122_DOCUMENTED_DPKG_ARCH_TYPES = { 

123 "HOST": ( 

124 "installed on", 

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

126 ), 

127 "BUILD": ( 

128 "compiled on", 

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

130 ), 

131 "TARGET": ( 

132 "cross-compiler output", 

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

134 ), 

135} 

136 

137_DOCUMENTED_DPKG_ARCH_VARS = { 

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

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

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

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

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

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

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

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

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

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

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

149} 

150 

151 

152_NOT_INTEGRATION_RRR = not_integrations(INTEGRATION_MODE_DH_DEBPUTY_RRR) 

153 

154 

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

156class Capability: 

157 value: str 

158 

159 @classmethod 

160 def parse( 

161 cls, 

162 raw_value: str, 

163 _attribute_path: AttributePath, 

164 _parser_context: ParserContextData | None, 

165 ) -> "Capability": 

166 return cls(raw_value) 

167 

168 

169@functools.lru_cache 

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

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

172 has_libcap = False 

173 libcap = None 

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

175 try: 

176 libcap = ctypes.cdll.LoadLibrary(cap_library_path) 

177 has_libcap = True 

178 except OSError: 

179 pass 

180 

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

182 warned = False 

183 

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

185 nonlocal warned 

186 if not warned: 

187 _info( 

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

189 " checking of capabilities." 

190 ) 

191 warned = True 

192 return True 

193 

194 else: 

195 # cap_t cap_from_text(const char *path_p) 

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

197 libcap.cap_from_text.restype = ctypes.c_char_p 

198 

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

200 libcap.cap_free.restype = None 

201 

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

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

204 ok = cap_t is not None 

205 libcap.cap_free(cap_t) 

206 return ok 

207 

208 return has_libcap, cap_library_path, _is_valid_cap 

209 

210 

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

212 _, libcap_path, is_valid_cap = load_libcap() 

213 

214 seen_cap = set() 

215 

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

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

218 seen_cap.add(cap) 

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

220 _warn( 

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

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

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

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

225 ) 

226 

227 return _check_cap 

228 

229 

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

231 try: 

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

233 if changelog is None: 

234 raise DebputyManifestVariableRequiresDebianDirError( 

235 "The changelog was not present" 

236 ) 

237 with changelog.open() as fd: 

238 dch = Changelog(fd, max_blocks=2) 

239 except FileNotFoundError as e: 

240 raise DebputyManifestVariableRequiresDebianDirError( 

241 "The changelog was not present" 

242 ) from e 

243 first_entry = dch[0] 

244 first_non_binnmu_entry = dch[0] 

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

246 first_non_binnmu_entry = dch[1] 

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

248 source_version = first_entry.version 

249 epoch = source_version.epoch 

250 upstream_version = source_version.upstream_version 

251 debian_revision = source_version.debian_revision 

252 epoch_upstream = upstream_version 

253 upstream_debian_revision = upstream_version 

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

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

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

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

258 

259 package = first_entry.package 

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

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

262 

263 date = first_entry.date 

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

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

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

267 else: 

268 _warn( 

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

270 " for SOURCE_DATE_EPOCH" 

271 ) 

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

273 

274 if first_non_binnmu_entry is not first_entry: 

275 non_binnmu_date = first_non_binnmu_entry.date 

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

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

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

279 else: 

280 _warn( 

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

282 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)" 

283 ) 

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

285 else: 

286 snd_source_date_epoch = source_date_epoch 

287 return { 

288 "DEB_SOURCE": package, 

289 "DEB_VERSION": source_version.full_version, 

290 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream, 

291 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision, 

292 "DEB_VERSION_UPSTREAM": upstream_version, 

293 "SOURCE_DATE_EPOCH": source_date_epoch, 

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

295 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch, 

296 } 

297 

298 

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

300 api = cast("DebputyPluginInitializerProvider", public_api) 

301 

302 api.metadata_or_maintscript_detector( 

303 "dpkg-shlibdeps", 

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

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

306 package_types=PackageTypeSelector.DEB | PackageTypeSelector.UDEB, 

307 ) 

308 register_type_mappings(api) 

309 register_variables_via_private_api(api) 

310 document_builtin_variables(api) 

311 register_automatic_discard_rules(api) 

312 register_special_ppfs(api) 

313 register_install_rules(api) 

314 register_transformation_rules(api) 

315 register_manifest_condition_rules(api) 

316 register_dpkg_conffile_rules(api) 

317 register_processing_steps(api) 

318 register_service_managers(api) 

319 register_manifest_root_rules(api) 

320 register_binary_package_rules(api) 

321 

322 

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

324 api.register_mapped_type( 

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

326 reference_documentation=type_mapping_reference_documentation( 

327 description=textwrap.dedent( 

328 """\ 

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

330 

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

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

333 currently never emit hard errors for unknown capabilities. 

334 """, 

335 ), 

336 examples=[ 

337 type_mapping_example("cap_chown=p"), 

338 type_mapping_example("cap_chown=ep"), 

339 type_mapping_example("cap_kill-pe"), 

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

341 ], 

342 ), 

343 ) 

344 api.register_mapped_type( 

345 TypeMapping( 

346 FileSystemMatchRule, 

347 str, 

348 FileSystemMatchRule.parse_path_match, 

349 ), 

350 reference_documentation=type_mapping_reference_documentation( 

351 description=textwrap.dedent( 

352 """\ 

353 A generic file system path match with globs. 

354 

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

356 

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

358 

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

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

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

362 

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

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

365 

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

367 or relevant search directories will match. 

368 

369 Please keep in mind that: 

370 

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

372 an anchor reference. 

373 

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

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

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

377 directories (similar to the shell). 

378 

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

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

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

382 """, 

383 ), 

384 examples=[ 

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

386 type_mapping_example("*.txt"), 

387 type_mapping_example("**/foo"), 

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

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

390 ], 

391 ), 

392 ) 

393 

394 api.register_mapped_type( 

395 TypeMapping( 

396 FileSystemExactMatchRule, 

397 str, 

398 FileSystemExactMatchRule.parse_path_match, 

399 ), 

400 reference_documentation=type_mapping_reference_documentation( 

401 description=textwrap.dedent( 

402 """\ 

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

404 

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

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

407 

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

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

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

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

412 """, 

413 ), 

414 examples=[ 

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

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

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

418 ], 

419 ), 

420 ) 

421 

422 api.register_mapped_type( 

423 TypeMapping( 

424 FileSystemExactNonDirMatchRule, 

425 str, 

426 FileSystemExactNonDirMatchRule.parse_path_match, 

427 ), 

428 reference_documentation=type_mapping_reference_documentation( 

429 description=textwrap.dedent( 

430 f"""\ 

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

432 

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

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

435 

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

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

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

439 with a "/". 

440 """, 

441 ), 

442 examples=[ 

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

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

445 ], 

446 ), 

447 ) 

448 

449 api.register_mapped_type( 

450 TypeMapping( 

451 SymlinkTarget, 

452 str, 

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

454 v, ap, assume_not_none(pc).substitution 

455 ), 

456 ), 

457 reference_documentation=type_mapping_reference_documentation( 

458 description=textwrap.dedent( 

459 """\ 

460 A symlink target. 

461 

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

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

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

465 

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

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

468 """, 

469 ), 

470 examples=[ 

471 type_mapping_example("../foo"), 

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

473 ], 

474 ), 

475 ) 

476 

477 api.register_mapped_type( 

478 TypeMapping( 

479 StaticFileSystemOwner, 

480 Union[int, str], 

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

482 ), 

483 reference_documentation=type_mapping_reference_documentation( 

484 description=textwrap.dedent( 

485 """\ 

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

487 

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

489 

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

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

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

493 

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

495 

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

497 """ 

498 ), 

499 examples=[ 

500 type_mapping_example("root"), 

501 type_mapping_example(0), 

502 type_mapping_example("root:0"), 

503 type_mapping_example("bin"), 

504 ], 

505 ), 

506 ) 

507 api.register_mapped_type( 

508 TypeMapping( 

509 StaticFileSystemGroup, 

510 Union[int, str], 

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

512 ), 

513 reference_documentation=type_mapping_reference_documentation( 

514 description=textwrap.dedent( 

515 """\ 

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

517 

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

519 

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

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

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

523 

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

525 

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

527 """ 

528 ), 

529 examples=[ 

530 type_mapping_example("root"), 

531 type_mapping_example(0), 

532 type_mapping_example("root:0"), 

533 type_mapping_example("tty"), 

534 ], 

535 ), 

536 ) 

537 

538 api.register_mapped_type( 

539 TypeMapping( 

540 BinaryPackage, 

541 str, 

542 type_mapper_str2package, 

543 ), 

544 reference_documentation=type_mapping_reference_documentation( 

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

546 ), 

547 ) 

548 

549 api.register_mapped_type( 

550 TypeMapping( 

551 PackageSelector, 

552 str, 

553 PackageSelector.parse, 

554 ), 

555 reference_documentation=type_mapping_reference_documentation( 

556 description=textwrap.dedent( 

557 """\ 

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

559 

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

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

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

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

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

565 respectively). 

566 """ 

567 ), 

568 ), 

569 ) 

570 

571 api.register_mapped_type( 

572 TypeMapping( 

573 FileSystemMode, 

574 str, 

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

576 ), 

577 reference_documentation=type_mapping_reference_documentation( 

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

579 examples=[ 

580 type_mapping_example("a+x"), 

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

582 type_mapping_example("0755"), 

583 ], 

584 ), 

585 ) 

586 api.register_mapped_type( 

587 TypeMapping( 

588 OctalMode, 

589 str, 

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

591 ), 

592 reference_documentation=type_mapping_reference_documentation( 

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

594 examples=[ 

595 type_mapping_example("0644"), 

596 type_mapping_example("0755"), 

597 ], 

598 ), 

599 ) 

600 api.register_mapped_type( 

601 TypeMapping( 

602 BuildEnvironmentDefinition, 

603 str, 

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

605 ), 

606 reference_documentation=type_mapping_reference_documentation( 

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

608 ), 

609 ) 

610 

611 

612def register_service_managers( 

613 api: DebputyPluginInitializerProvider, 

614) -> None: 

615 api.service_provider( 

616 "systemd", 

617 detect_systemd_service_files, 

618 generate_snippets_for_systemd_units, 

619 ) 

620 api.service_provider( 

621 "sysvinit", 

622 detect_sysv_init_service_files, 

623 generate_snippets_for_init_scripts, 

624 ) 

625 

626 

627def register_automatic_discard_rules( 

628 api: DebputyPluginInitializerProvider, 

629) -> None: 

630 api.automatic_discard_rule( 

631 "python-cache-files", 

632 _debputy_discard_pyc_files, 

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

634 examples=automatic_discard_rule_example( 

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

636 ".../__pycache__/", 

637 ".../__pycache__/...", 

638 ".../foo.pyc", 

639 ".../foo.pyo", 

640 ), 

641 ) 

642 api.automatic_discard_rule( 

643 "la-files", 

644 _debputy_prune_la_files, 

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

646 examples=automatic_discard_rule_example( 

647 "usr/lib/libfoo.la", 

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

649 ), 

650 ) 

651 api.automatic_discard_rule( 

652 "backup-files", 

653 _debputy_prune_backup_files, 

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

655 examples=( 

656 automatic_discard_rule_example( 

657 ".../foo~", 

658 ".../foo.orig", 

659 ".../foo.rej", 

660 ".../DEADJOE", 

661 ".../.foo.sw.", 

662 ), 

663 ), 

664 ) 

665 api.automatic_discard_rule( 

666 "version-control-paths", 

667 _debputy_prune_vcs_paths, 

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

669 examples=automatic_discard_rule_example( 

670 ("tools/foo", False), 

671 ".../CVS/", 

672 ".../CVS/...", 

673 ".../.gitignore", 

674 ".../.gitattributes", 

675 ".../.git/", 

676 ".../.git/...", 

677 ), 

678 ) 

679 api.automatic_discard_rule( 

680 "gnu-info-dir-file", 

681 _debputy_prune_info_dir_file, 

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

683 examples=automatic_discard_rule_example( 

684 "usr/share/info/dir", 

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

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

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

688 ), 

689 ) 

690 api.automatic_discard_rule( 

691 "debian-dir", 

692 _debputy_prune_binary_debian_dir, 

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

694 " literally in the file listing", 

695 examples=( 

696 automatic_discard_rule_example( 

697 "DEBIAN/", 

698 "DEBIAN/control", 

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

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

701 ), 

702 ), 

703 ) 

704 api.automatic_discard_rule( 

705 "doxygen-cruft-files", 

706 _debputy_prune_doxygen_cruft, 

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

708 examples=automatic_discard_rule_example( 

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

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

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

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

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

714 ), 

715 ) 

716 

717 

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

719 api.package_processor("manpages", process_manpages) 

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

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

722 api.package_processor( 

723 "strip-nondeterminism", 

724 cast("Any", strip_non_determinism), 

725 depends_on_processor=["manpages"], 

726 ) 

727 api.package_processor( 

728 "compression", 

729 apply_compression, 

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

731 ) 

732 

733 

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

735 api.manifest_variable_provider( 

736 load_source_variables, 

737 { 

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

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

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

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

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

743 "SOURCE_DATE_EPOCH": textwrap.dedent( 

744 """\ 

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

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

747 this variable. 

748 """ 

749 ), 

750 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

751 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

752 }, 

753 ) 

754 

755 

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

757 api.document_builtin_variable( 

758 "PACKAGE", 

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

760 is_context_specific=True, 

761 ) 

762 

763 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

764 

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

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

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

768 documentation = textwrap.dedent( 

769 f"""\ 

770 {arch_var_doc} ({arch_type_tag}) 

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

772 * Machine type: {arch_type_doc} 

773 * Value description: {arch_var_doc} 

774 

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

776 """ 

777 ) 

778 api.document_builtin_variable( 

779 full_var, 

780 documentation, 

781 is_for_special_case=arch_type != "HOST", 

782 ) 

783 

784 

785def _format_docbase_filename( 

786 path_format: str, 

787 format_param: PPFFormatParam, 

788 docbase_file: VirtualPath, 

789) -> str: 

790 with docbase_file.open() as fd: 

791 content = Deb822(fd) 

792 proper_name = content["Document"] 

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

794 format_param["name"] = proper_name 

795 else: 

796 _warn( 

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

798 ) 

799 return path_format.format(**format_param) 

800 

801 

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

803 api.packager_provided_file( 

804 "doc-base", 

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

806 format_callback=_format_docbase_filename, 

807 ) 

808 

809 api.packager_provided_file( 

810 "shlibs", 

811 "DEBIAN/shlibs", 

812 allow_name_segment=False, 

813 reservation_only=True, 

814 reference_documentation=packager_provided_file_reference_documentation( 

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

816 ), 

817 ) 

818 api.packager_provided_file( 

819 "symbols", 

820 "DEBIAN/symbols", 

821 allow_name_segment=False, 

822 allow_architecture_segment=True, 

823 reservation_only=True, 

824 reference_documentation=packager_provided_file_reference_documentation( 

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

826 ), 

827 ) 

828 api.packager_provided_file( 

829 "conffiles", 

830 "DEBIAN/conffiles", 

831 allow_name_segment=False, 

832 allow_architecture_segment=True, 

833 reservation_only=True, 

834 ) 

835 api.packager_provided_file( 

836 "templates", 

837 "DEBIAN/templates", 

838 allow_name_segment=False, 

839 allow_architecture_segment=False, 

840 reservation_only=True, 

841 ) 

842 api.packager_provided_file( 

843 "alternatives", 

844 "DEBIAN/alternatives", 

845 allow_name_segment=False, 

846 allow_architecture_segment=True, 

847 reservation_only=True, 

848 ) 

849 

850 

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

852 api.pluggable_manifest_rule( 

853 InstallRule, 

854 MK_INSTALLATIONS_INSTALL, 

855 ParsedInstallRule, 

856 _install_rule_handler, 

857 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

858 inline_reference_documentation=reference_documentation( 

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

860 description=textwrap.dedent( 

861 """\ 

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

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

864 

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

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

867 

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

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

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

871 """.format( 

872 MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL 

873 ) 

874 ), 

875 non_mapping_description=textwrap.dedent( 

876 """\ 

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

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

879 not required. 

880 """ 

881 ), 

882 attributes=[ 

883 documented_attr( 

884 ["source", "sources"], 

885 textwrap.dedent( 

886 """\ 

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

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

889 is tried against default search directories. 

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

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

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

893 """ 

894 ), 

895 ), 

896 documented_attr( 

897 "dest_dir", 

898 textwrap.dedent( 

899 """\ 

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

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

902 to the directory name of the `source`. 

903 """ 

904 ), 

905 ), 

906 documented_attr( 

907 "into", 

908 textwrap.dedent( 

909 """\ 

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

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

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

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

914 """ 

915 ), 

916 ), 

917 documented_attr( 

918 "install_as", 

919 textwrap.dedent( 

920 """\ 

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

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

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

924 """ 

925 ), 

926 ), 

927 *docs_from(DebputyParsedContentStandardConditional), 

928 ], 

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

930 ), 

931 ) 

932 api.pluggable_manifest_rule( 

933 InstallRule, 

934 [ 

935 MK_INSTALLATIONS_INSTALL_DOCS, 

936 "install-doc", 

937 ], 

938 ParsedInstallRule, 

939 _install_docs_rule_handler, 

940 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

941 inline_reference_documentation=reference_documentation( 

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

943 description=textwrap.dedent( 

944 """\ 

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

946 `install` rule with the following key features: 

947 

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

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

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

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

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

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

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

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

956 package listed in `debian/control`. 

957 

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

959 

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

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

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

963 use-case. 

964 """ 

965 ), 

966 non_mapping_description=textwrap.dedent( 

967 """\ 

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

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

970 not required. 

971 """ 

972 ), 

973 attributes=[ 

974 documented_attr( 

975 ["source", "sources"], 

976 textwrap.dedent( 

977 """\ 

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

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

980 is tried against default search directories. 

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

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

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

984 

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

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

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

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

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

990 """ 

991 ), 

992 ), 

993 documented_attr( 

994 "dest_dir", 

995 textwrap.dedent( 

996 """\ 

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

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

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

1000 """ 

1001 ), 

1002 ), 

1003 documented_attr( 

1004 "into", 

1005 textwrap.dedent( 

1006 """\ 

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

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

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

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

1011 the key is required. 

1012 """ 

1013 ), 

1014 ), 

1015 documented_attr( 

1016 "install_as", 

1017 textwrap.dedent( 

1018 """\ 

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

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

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

1022 """ 

1023 ), 

1024 ), 

1025 documented_attr( 

1026 "when", 

1027 textwrap.dedent( 

1028 """\ 

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

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

1031 (rather than replacing it). 

1032 """ 

1033 ), 

1034 ), 

1035 ], 

1036 reference_documentation_url=manifest_format_doc( 

1037 "install-documentation-install-docs" 

1038 ), 

1039 ), 

1040 ) 

1041 api.pluggable_manifest_rule( 

1042 InstallRule, 

1043 [ 

1044 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

1045 "install-example", 

1046 ], 

1047 ParsedInstallExamplesRule, 

1048 _install_examples_rule_handler, 

1049 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

1050 inline_reference_documentation=reference_documentation( 

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

1052 description=textwrap.dedent( 

1053 """\ 

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

1055 install` rule with the following key features: 

1056 

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

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

1059 dir. 

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

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

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

1063 package listed in `debian/control`. 

1064 

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

1066 """ 

1067 ), 

1068 non_mapping_description=textwrap.dedent( 

1069 """\ 

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

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

1072 not required. 

1073 """ 

1074 ), 

1075 attributes=[ 

1076 documented_attr( 

1077 ["source", "sources"], 

1078 textwrap.dedent( 

1079 """\ 

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

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

1082 is tried against default search directories. 

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

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

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

1086 

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

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

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

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

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

1092 """ 

1093 ), 

1094 ), 

1095 documented_attr( 

1096 "into", 

1097 textwrap.dedent( 

1098 """\ 

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

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

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

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

1103 Otherwise, the key is required. 

1104 """ 

1105 ), 

1106 ), 

1107 documented_attr( 

1108 "when", 

1109 textwrap.dedent( 

1110 """\ 

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

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

1113 (rather than replacing it). 

1114 """ 

1115 ), 

1116 ), 

1117 ], 

1118 reference_documentation_url=manifest_format_doc( 

1119 "install-examples-install-examples" 

1120 ), 

1121 ), 

1122 ) 

1123 api.pluggable_manifest_rule( 

1124 InstallRule, 

1125 MK_INSTALLATIONS_INSTALL_MAN, 

1126 ParsedInstallManpageRule, 

1127 _install_man_rule_handler, 

1128 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1129 inline_reference_documentation=reference_documentation( 

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

1131 description=textwrap.dedent( 

1132 """\ 

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

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

1135 

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

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

1138 language. 

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

1140 package listed in `debian/control`. 

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

1142 for when the auto-detection is insufficient. 

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

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

1145 

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

1147 """ 

1148 ), 

1149 non_mapping_description=textwrap.dedent( 

1150 """\ 

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

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

1153 not required. 

1154 """ 

1155 ), 

1156 attributes=[ 

1157 documented_attr( 

1158 ["source", "sources"], 

1159 textwrap.dedent( 

1160 """\ 

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

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

1163 is tried against default search directories. 

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

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

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

1167 """ 

1168 ), 

1169 ), 

1170 documented_attr( 

1171 "into", 

1172 textwrap.dedent( 

1173 """\ 

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

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

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

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

1178 """ 

1179 ), 

1180 ), 

1181 documented_attr( 

1182 "section", 

1183 textwrap.dedent( 

1184 """\ 

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

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

1187 have performed. 

1188 """ 

1189 ), 

1190 ), 

1191 documented_attr( 

1192 "language", 

1193 textwrap.dedent( 

1194 """\ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1212 always assumed to be a language. 

1213 """ 

1214 ), 

1215 ), 

1216 *docs_from(DebputyParsedContentStandardConditional), 

1217 ], 

1218 reference_documentation_url=manifest_format_doc( 

1219 "install-manpages-install-man" 

1220 ), 

1221 ), 

1222 ) 

1223 api.pluggable_manifest_rule( 

1224 InstallRule, 

1225 MK_INSTALLATIONS_DISCARD, 

1226 ParsedInstallDiscardRule, 

1227 _install_discard_rule_handler, 

1228 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1229 inline_reference_documentation=reference_documentation( 

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

1231 description=textwrap.dedent( 

1232 """\ 

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

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

1235 

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

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

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

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

1240 """ 

1241 ), 

1242 non_mapping_description=textwrap.dedent( 

1243 """\ 

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

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

1246 """ 

1247 ), 

1248 attributes=[ 

1249 documented_attr( 

1250 ["path", "paths"], 

1251 textwrap.dedent( 

1252 """\ 

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

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

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

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

1257 contents that have not already been installed somewhere. 

1258 """ 

1259 ), 

1260 ), 

1261 documented_attr( 

1262 ["search_dir", "search_dirs"], 

1263 textwrap.dedent( 

1264 """\ 

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

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

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

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

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

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

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

1272 will make `debputy` report an error. 

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

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

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

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

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

1278 applicable to certain builds that are only performed conditionally. 

1279 """ 

1280 ), 

1281 ), 

1282 documented_attr( 

1283 "required_when", 

1284 textwrap.dedent( 

1285 """\ 

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

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

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

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

1290 """ 

1291 ), 

1292 ), 

1293 ], 

1294 reference_documentation_url=manifest_format_doc( 

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

1296 ), 

1297 ), 

1298 ) 

1299 api.pluggable_manifest_rule( 

1300 InstallRule, 

1301 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1302 ParsedMultiDestInstallRule, 

1303 _multi_dest_install_rule_handler, 

1304 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1305 inline_reference_documentation=reference_documentation( 

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

1307 description=textwrap.dedent( 

1308 """\ 

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

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

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

1312 "primary" uses. 

1313 

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

1315 except you list 2+ destination directories. 

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

1317 2+ `as` names. 

1318 """ 

1319 ), 

1320 attributes=[ 

1321 documented_attr( 

1322 ["source", "sources"], 

1323 textwrap.dedent( 

1324 """\ 

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

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

1327 is tried against default search directories. 

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

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

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

1331 """ 

1332 ), 

1333 ), 

1334 documented_attr( 

1335 "dest_dirs", 

1336 textwrap.dedent( 

1337 """\ 

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

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

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

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

1342 """ 

1343 ), 

1344 ), 

1345 documented_attr( 

1346 "into", 

1347 textwrap.dedent( 

1348 """\ 

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

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

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

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

1353 """ 

1354 ), 

1355 ), 

1356 documented_attr( 

1357 "install_as", 

1358 textwrap.dedent( 

1359 """\ 

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

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

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

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

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

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

1366 """ 

1367 ), 

1368 ), 

1369 *docs_from(DebputyParsedContentStandardConditional), 

1370 ], 

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

1372 ), 

1373 ) 

1374 

1375 

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

1377 api.pluggable_manifest_rule( 

1378 TransformationRule, 

1379 "move", 

1380 TransformationMoveRuleSpec, 

1381 _transformation_move_handler, 

1382 inline_reference_documentation=reference_documentation( 

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

1384 description=textwrap.dedent( 

1385 """\ 

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

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

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

1389 Debian specific requirements. 

1390 

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

1392 `mv` command line tool. 

1393 """ 

1394 ), 

1395 attributes=[ 

1396 documented_attr( 

1397 "source", 

1398 textwrap.dedent( 

1399 """\ 

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

1401 and substitutions. 

1402 """ 

1403 ), 

1404 ), 

1405 documented_attr( 

1406 "target", 

1407 textwrap.dedent( 

1408 """\ 

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

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

1411 the target will *always* be a directory. 

1412 """ 

1413 ), 

1414 ), 

1415 *docs_from(DebputyParsedContentStandardConditional), 

1416 ], 

1417 reference_documentation_url=manifest_format_doc( 

1418 "move-transformation-rule-move" 

1419 ), 

1420 ), 

1421 ) 

1422 api.pluggable_manifest_rule( 

1423 TransformationRule, 

1424 "remove", 

1425 TransformationRemoveRuleSpec, 

1426 _transformation_remove_handler, 

1427 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1428 inline_reference_documentation=reference_documentation( 

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

1430 description=textwrap.dedent( 

1431 """\ 

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

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

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

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

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

1437 of `debian/copyright`). 

1438 

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

1440 the `remove` transformation rule. 

1441 

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

1443 """ 

1444 ), 

1445 non_mapping_description=textwrap.dedent( 

1446 """\ 

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

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

1449 """ 

1450 ), 

1451 attributes=[ 

1452 documented_attr( 

1453 ["path", "paths"], 

1454 textwrap.dedent( 

1455 """\ 

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

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

1458 can use globs. 

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

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

1461 along with all the contents. 

1462 """ 

1463 ), 

1464 ), 

1465 documented_attr( 

1466 "keep_empty_parent_dirs", 

1467 textwrap.dedent( 

1468 """\ 

1469 A boolean determining whether to prune parent directories that become 

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

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

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

1473 """ 

1474 ), 

1475 ), 

1476 documented_attr( 

1477 "when", 

1478 textwrap.dedent( 

1479 """\ 

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

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

1482 (rather than replacing it). 

1483 """ 

1484 ), 

1485 ), 

1486 ], 

1487 reference_documentation_url=manifest_format_doc( 

1488 "remove-transformation-rule-remove" 

1489 ), 

1490 ), 

1491 ) 

1492 api.pluggable_manifest_rule( 

1493 TransformationRule, 

1494 "create-symlink", 

1495 CreateSymlinkRule, 

1496 _transformation_create_symlink, 

1497 inline_reference_documentation=reference_documentation( 

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

1499 description=textwrap.dedent( 

1500 """\ 

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

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

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

1504 """ 

1505 ), 

1506 attributes=[ 

1507 documented_attr( 

1508 "path", 

1509 textwrap.dedent( 

1510 """\ 

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

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

1513 Parent directories are implicitly created as necessary. 

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

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

1516 """ 

1517 ), 

1518 ), 

1519 documented_attr( 

1520 "target", 

1521 textwrap.dedent( 

1522 """\ 

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

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

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

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

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

1528 preference. 

1529 """ 

1530 ), 

1531 ), 

1532 documented_attr( 

1533 "replacement_rule", 

1534 textwrap.dedent( 

1535 """\ 

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

1537 be set to one of the following values: 

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

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

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

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

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

1543 similar to `ln -sf` semantics. 

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

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

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

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

1548 will stop with an error. 

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

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

1551 be removed recursively along with the directory. Finally, 

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

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

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

1555 `when` if any). 

1556 

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

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

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

1560 the value in `replacement-rule`. 

1561 """ 

1562 ), 

1563 ), 

1564 *docs_from(DebputyParsedContentStandardConditional), 

1565 ], 

1566 reference_documentation_url=manifest_format_doc( 

1567 "create-symlinks-transformation-rule-create-symlink" 

1568 ), 

1569 ), 

1570 ) 

1571 api.pluggable_manifest_rule( 

1572 TransformationRule, 

1573 "path-metadata", 

1574 PathManifestRule, 

1575 _transformation_path_metadata, 

1576 source_format=PathManifestSourceDictFormat, 

1577 inline_reference_documentation=reference_documentation( 

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

1579 description=textwrap.dedent( 

1580 """\ 

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

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

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

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

1585 transformation. 

1586 

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

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

1589 """ 

1590 ), 

1591 attributes=[ 

1592 documented_attr( 

1593 ["path", "paths"], 

1594 textwrap.dedent( 

1595 """\ 

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

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

1598 and substitution variables. Special-rules for matches: 

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

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

1601 """ 

1602 ), 

1603 ), 

1604 documented_attr( 

1605 "owner", 

1606 textwrap.dedent( 

1607 """\ 

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

1609 no change of owner is done. 

1610 """ 

1611 ), 

1612 ), 

1613 documented_attr( 

1614 "group", 

1615 textwrap.dedent( 

1616 """\ 

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

1618 no change of group is done. 

1619 """ 

1620 ), 

1621 ), 

1622 documented_attr( 

1623 "mode", 

1624 textwrap.dedent( 

1625 """\ 

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

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

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

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

1630 relative to the matched path's current mode. 

1631 """ 

1632 ), 

1633 ), 

1634 documented_attr( 

1635 "capabilities", 

1636 textwrap.dedent( 

1637 """\ 

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

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

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

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

1642 run. 

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

1644 to those paths. 

1645 

1646 """ 

1647 ), 

1648 ), 

1649 documented_attr( 

1650 "capability_mode", 

1651 textwrap.dedent( 

1652 """\ 

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

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

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

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

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

1658 `capabilities` is omitted. 

1659 """ 

1660 ), 

1661 ), 

1662 documented_attr( 

1663 "recursive", 

1664 textwrap.dedent( 

1665 """\ 

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

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

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

1669 this attribute is `false`. 

1670 """ 

1671 ), 

1672 ), 

1673 *docs_from(DebputyParsedContentStandardConditional), 

1674 ], 

1675 reference_documentation_url=manifest_format_doc( 

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

1677 ), 

1678 ), 

1679 ) 

1680 api.pluggable_manifest_rule( 

1681 TransformationRule, 

1682 "create-directories", 

1683 EnsureDirectoryRule, 

1684 _transformation_mkdirs, 

1685 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1686 inline_reference_documentation=reference_documentation( 

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

1688 description=textwrap.dedent( 

1689 """\ 

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

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

1692 transformations will create directories as required. 

1693 

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

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

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

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

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

1699 

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

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

1702 """ 

1703 ), 

1704 non_mapping_description=textwrap.dedent( 

1705 """\ 

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

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

1708 """ 

1709 ), 

1710 attributes=[ 

1711 documented_attr( 

1712 ["path", "paths"], 

1713 textwrap.dedent( 

1714 """\ 

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

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

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

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

1719 are affected by the owner/mode options) 

1720 """ 

1721 ), 

1722 ), 

1723 documented_attr( 

1724 "owner", 

1725 textwrap.dedent( 

1726 """\ 

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

1728 Default is "root". 

1729 """ 

1730 ), 

1731 ), 

1732 documented_attr( 

1733 "group", 

1734 textwrap.dedent( 

1735 """\ 

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

1737 Default is "root". 

1738 """ 

1739 ), 

1740 ), 

1741 documented_attr( 

1742 "mode", 

1743 textwrap.dedent( 

1744 """\ 

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

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

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

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

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

1750 transformation. The default is "0755". 

1751 """ 

1752 ), 

1753 ), 

1754 *docs_from(DebputyParsedContentStandardConditional), 

1755 ], 

1756 reference_documentation_url=manifest_format_doc( 

1757 "create-directories-transformation-rule-directories" 

1758 ), 

1759 ), 

1760 ) 

1761 

1762 

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

1764 api.provide_manifest_keyword( 

1765 ManifestCondition, 

1766 "cross-compiling", 

1767 lambda *_: ManifestCondition.is_cross_building(), 

1768 ) 

1769 api.provide_manifest_keyword( 

1770 ManifestCondition, 

1771 "can-execute-compiled-binaries", 

1772 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 

1773 ) 

1774 api.provide_manifest_keyword( 

1775 ManifestCondition, 

1776 "run-build-time-tests", 

1777 lambda *_: ManifestCondition.run_build_time_tests(), 

1778 ) 

1779 

1780 api.pluggable_manifest_rule( 

1781 ManifestCondition, 

1782 "not", 

1783 MCNot, 

1784 _mc_not, 

1785 source_format=ManifestCondition, 

1786 ) 

1787 api.pluggable_manifest_rule( 

1788 ManifestCondition, 

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

1790 MCAnyOfAllOf, 

1791 _mc_any_of, 

1792 source_format=list[ManifestCondition], 

1793 ) 

1794 api.pluggable_manifest_rule( 

1795 ManifestCondition, 

1796 "arch-matches", 

1797 MCArchMatches, 

1798 _mc_arch_matches, 

1799 source_format=str, 

1800 inline_reference_documentation=reference_documentation( 

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

1802 description=textwrap.dedent( 

1803 """\ 

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

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

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

1807 and practically behaves like a comparison against 

1808 `dpkg-architecture -qDEB_HOST_ARCH`. 

1809 

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

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

1812 in the context of a binary package and like 

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

1814 are covered in their own keywords. 

1815 """ 

1816 ), 

1817 non_mapping_description=textwrap.dedent( 

1818 """\ 

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

1820 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1825 have it. 

1826 """ 

1827 ), 

1828 reference_documentation_url=manifest_format_doc( 

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

1830 ), 

1831 ), 

1832 ) 

1833 

1834 context_arch_doc = reference_documentation( 

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

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

1837 description=textwrap.dedent( 

1838 """\ 

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

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

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

1842 `arch-matches` condition. 

1843 

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

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

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

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

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

1849 to the packager that condition does not make sense. 

1850 

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

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

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

1854 very special cases). 

1855 

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

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

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

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

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

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

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

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

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

1865 need to care about nor use any of this. 

1866 

1867 Accordingly, the possible conditions are: 

1868 

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

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

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

1872 

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

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

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

1876 

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

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

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

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

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

1882 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1883 

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

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

1886 

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

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

1889 """ 

1890 ), 

1891 non_mapping_description=textwrap.dedent( 

1892 """\ 

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

1894 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1899 have it. 

1900 """ 

1901 ), 

1902 ) 

1903 

1904 api.pluggable_manifest_rule( 

1905 ManifestCondition, 

1906 "source-context-arch-matches", 

1907 MCArchMatches, 

1908 _mc_source_context_arch_matches, 

1909 source_format=str, 

1910 inline_reference_documentation=context_arch_doc, 

1911 ) 

1912 api.pluggable_manifest_rule( 

1913 ManifestCondition, 

1914 "package-context-arch-matches", 

1915 MCArchMatches, 

1916 _mc_arch_matches, 

1917 source_format=str, 

1918 inline_reference_documentation=context_arch_doc, 

1919 ) 

1920 api.pluggable_manifest_rule( 

1921 ManifestCondition, 

1922 "build-profiles-matches", 

1923 MCBuildProfileMatches, 

1924 _mc_build_profile_matches, 

1925 source_format=str, 

1926 ) 

1927 

1928 

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

1930 api.pluggable_manifest_rule( 

1931 DpkgMaintscriptHelperCommand, 

1932 "remove", 

1933 DpkgRemoveConffileRule, 

1934 _dpkg_conffile_remove, 

1935 inline_reference_documentation=None, # TODO: write and add 

1936 ) 

1937 

1938 api.pluggable_manifest_rule( 

1939 DpkgMaintscriptHelperCommand, 

1940 "rename", 

1941 DpkgRenameConffileRule, 

1942 _dpkg_conffile_rename, 

1943 inline_reference_documentation=None, # TODO: write and add 

1944 ) 

1945 

1946 

1947class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

1948 mode: NotRequired[FileSystemMode] 

1949 owner: NotRequired[StaticFileSystemOwner] 

1950 group: NotRequired[StaticFileSystemGroup] 

1951 

1952 

1953class PathManifestSourceDictFormat(_ModeOwnerBase): 

1954 path: NotRequired[ 

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

1956 ] 

1957 paths: NotRequired[list[FileSystemMatchRule]] 

1958 recursive: NotRequired[bool] 

1959 capabilities: NotRequired[Capability] 

1960 capability_mode: NotRequired[FileSystemMode] 

1961 

1962 

1963class PathManifestRule(_ModeOwnerBase): 

1964 paths: list[FileSystemMatchRule] 

1965 recursive: NotRequired[bool] 

1966 capabilities: NotRequired[Capability] 

1967 capability_mode: NotRequired[FileSystemMode] 

1968 

1969 

1970class EnsureDirectorySourceFormat(_ModeOwnerBase): 

1971 path: NotRequired[ 

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

1973 ] 

1974 paths: NotRequired[list[FileSystemExactMatchRule]] 

1975 

1976 

1977class EnsureDirectoryRule(_ModeOwnerBase): 

1978 paths: list[FileSystemExactMatchRule] 

1979 

1980 

1981class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

1982 path: FileSystemExactMatchRule 

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

1984 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

1985 

1986 

1987class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

1988 source: FileSystemMatchRule 

1989 target: FileSystemExactMatchRule 

1990 

1991 

1992class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

1993 paths: list[FileSystemMatchRule] 

1994 keep_empty_parent_dirs: NotRequired[bool] 

1995 

1996 

1997class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

1998 path: NotRequired[ 

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

2000 ] 

2001 paths: NotRequired[list[FileSystemMatchRule]] 

2002 keep_empty_parent_dirs: NotRequired[bool] 

2003 

2004 

2005class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2006 sources: NotRequired[list[FileSystemMatchRule]] 

2007 source: NotRequired[ 

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

2009 ] 

2010 into: NotRequired[ 

2011 Annotated[ 

2012 str | list[str], 

2013 DebputyParseHint.required_when_multi_binary(), 

2014 ] 

2015 ] 

2016 dest_dir: NotRequired[ 

2017 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2018 ] 

2019 install_as: NotRequired[ 

2020 Annotated[ 

2021 FileSystemExactMatchRule, 

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

2023 DebputyParseHint.manifest_attribute("as"), 

2024 DebputyParseHint.not_path_error_hint(), 

2025 ] 

2026 ] 

2027 

2028 

2029class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

2030 sources: NotRequired[list[FileSystemMatchRule]] 

2031 source: NotRequired[ 

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

2033 ] 

2034 into: NotRequired[ 

2035 Annotated[ 

2036 str | list[str], 

2037 DebputyParseHint.required_when_multi_binary( 

2038 package_types=PackageTypeSelector.DEB 

2039 ), 

2040 ] 

2041 ] 

2042 dest_dir: NotRequired[ 

2043 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2044 ] 

2045 install_as: NotRequired[ 

2046 Annotated[ 

2047 FileSystemExactMatchRule, 

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

2049 DebputyParseHint.manifest_attribute("as"), 

2050 DebputyParseHint.not_path_error_hint(), 

2051 ] 

2052 ] 

2053 

2054 

2055class ParsedInstallRule(DebputyParsedContentStandardConditional): 

2056 sources: list[FileSystemMatchRule] 

2057 into: NotRequired[list[BinaryPackage]] 

2058 dest_dir: NotRequired[FileSystemExactMatchRule] 

2059 install_as: NotRequired[FileSystemExactMatchRule] 

2060 

2061 

2062class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2063 sources: NotRequired[list[FileSystemMatchRule]] 

2064 source: NotRequired[ 

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

2066 ] 

2067 into: NotRequired[ 

2068 Annotated[ 

2069 str | list[str], 

2070 DebputyParseHint.required_when_multi_binary(), 

2071 ] 

2072 ] 

2073 dest_dirs: NotRequired[ 

2074 Annotated[ 

2075 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

2076 ] 

2077 ] 

2078 install_as: NotRequired[ 

2079 Annotated[ 

2080 list[FileSystemExactMatchRule], 

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

2082 DebputyParseHint.not_path_error_hint(), 

2083 DebputyParseHint.manifest_attribute("as"), 

2084 ] 

2085 ] 

2086 

2087 

2088class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

2089 sources: list[FileSystemMatchRule] 

2090 into: NotRequired[list[BinaryPackage]] 

2091 dest_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2092 install_as: NotRequired[list[FileSystemExactMatchRule]] 

2093 

2094 

2095class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

2096 sources: list[FileSystemMatchRule] 

2097 into: NotRequired[list[BinaryPackage]] 

2098 

2099 

2100class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

2101 sources: NotRequired[list[FileSystemMatchRule]] 

2102 source: NotRequired[ 

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

2104 ] 

2105 into: NotRequired[ 

2106 Annotated[ 

2107 str | list[str], 

2108 DebputyParseHint.required_when_multi_binary( 

2109 package_types=PackageTypeSelector.DEB 

2110 ), 

2111 ] 

2112 ] 

2113 

2114 

2115class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

2116 sources: list[FileSystemMatchRule] 

2117 language: NotRequired[str] 

2118 section: NotRequired[int] 

2119 into: NotRequired[list[BinaryPackage]] 

2120 

2121 

2122class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

2123 sources: NotRequired[list[FileSystemMatchRule]] 

2124 source: NotRequired[ 

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

2126 ] 

2127 language: NotRequired[str] 

2128 section: NotRequired[int] 

2129 into: NotRequired[ 

2130 Annotated[ 

2131 str | list[str], 

2132 DebputyParseHint.required_when_multi_binary( 

2133 package_types=PackageTypeSelector.DEB 

2134 ), 

2135 ] 

2136 ] 

2137 

2138 

2139class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2140 paths: NotRequired[list[FileSystemMatchRule]] 

2141 path: NotRequired[ 

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

2143 ] 

2144 search_dir: NotRequired[ 

2145 Annotated[ 

2146 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2147 ] 

2148 ] 

2149 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2150 required_when: NotRequired[ManifestCondition] 

2151 

2152 

2153class ParsedInstallDiscardRule(DebputyParsedContent): 

2154 paths: list[FileSystemMatchRule] 

2155 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2156 required_when: NotRequired[ManifestCondition] 

2157 

2158 

2159class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2160 prior_to_version: NotRequired[str] 

2161 owning_package: NotRequired[str] 

2162 

2163 

2164class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2165 source: str 

2166 target: str 

2167 

2168 

2169class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2170 path: str 

2171 

2172 

2173class MCAnyOfAllOf(DebputyParsedContent): 

2174 conditions: list[ManifestCondition] 

2175 

2176 

2177class MCNot(DebputyParsedContent): 

2178 negated_condition: ManifestCondition 

2179 

2180 

2181class MCArchMatches(DebputyParsedContent): 

2182 arch_matches: str 

2183 

2184 

2185class MCBuildProfileMatches(DebputyParsedContent): 

2186 build_profile_matches: str 

2187 

2188 

2189def _parse_filename( 

2190 filename: str, 

2191 attribute_path: AttributePath, 

2192 *, 

2193 allow_directories: bool = True, 

2194) -> str: 

2195 try: 

2196 normalized_path = _normalize_path(filename, with_prefix=False) 

2197 except ValueError as e: 

2198 raise ManifestParseException( 

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

2200 ) from None 

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

2202 raise ManifestParseException( 

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

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

2205 ) 

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

2207 raise ManifestParseException( 

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

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

2210 ) 

2211 return normalized_path 

2212 

2213 

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

2215 return Union[ 

2216 t, 

2217 list[str], 

2218 str, 

2219 ] 

2220 

2221 

2222def _dpkg_conffile_rename( 

2223 _name: str, 

2224 parsed_data: DpkgRenameConffileRule, 

2225 path: AttributePath, 

2226 _context: ParserContextData, 

2227) -> DpkgMaintscriptHelperCommand: 

2228 source_file = parsed_data["source"] 

2229 target_file = parsed_data["target"] 

2230 normalized_source = _parse_filename( 

2231 source_file, 

2232 path["source"], 

2233 allow_directories=False, 

2234 ) 

2235 path.path_hint = source_file 

2236 

2237 normalized_target = _parse_filename( 

2238 target_file, 

2239 path["target"], 

2240 allow_directories=False, 

2241 ) 

2242 normalized_source = "/" + normalized_source 

2243 normalized_target = "/" + normalized_target 

2244 

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

2246 raise ManifestParseException( 

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

2248 ) 

2249 

2250 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2251 parsed_data, path 

2252 ) 

2253 return DpkgMaintscriptHelperCommand.mv_conffile( 

2254 path, 

2255 normalized_source, 

2256 normalized_target, 

2257 version, 

2258 owning_package, 

2259 ) 

2260 

2261 

2262def _dpkg_conffile_remove( 

2263 _name: str, 

2264 parsed_data: DpkgRemoveConffileRule, 

2265 path: AttributePath, 

2266 _context: ParserContextData, 

2267) -> DpkgMaintscriptHelperCommand: 

2268 source_file = parsed_data["path"] 

2269 normalized_source = _parse_filename( 

2270 source_file, 

2271 path["path"], 

2272 allow_directories=False, 

2273 ) 

2274 path.path_hint = source_file 

2275 

2276 normalized_source = "/" + normalized_source 

2277 

2278 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2279 parsed_data, path 

2280 ) 

2281 return DpkgMaintscriptHelperCommand.rm_conffile( 

2282 path, 

2283 normalized_source, 

2284 version, 

2285 owning_package, 

2286 ) 

2287 

2288 

2289def _parse_conffile_prior_version_and_owning_package( 

2290 d: DpkgConffileManagementRuleBase, 

2291 attribute_path: AttributePath, 

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

2293 prior_version = d.get("prior_to_version") 

2294 owning_package = d.get("owning_package") 

2295 

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

2297 p = attribute_path["prior_to_version"] 

2298 raise ManifestParseException( 

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

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

2301 ) 

2302 

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

2304 p = attribute_path["owning_package"] 

2305 raise ManifestParseException( 

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

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

2308 ) 

2309 

2310 return prior_version, owning_package 

2311 

2312 

2313def _install_rule_handler( 

2314 _name: str, 

2315 parsed_data: ParsedInstallRule, 

2316 path: AttributePath, 

2317 context: ParserContextData, 

2318) -> InstallRule: 

2319 sources = parsed_data["sources"] 

2320 install_as = parsed_data.get("install_as") 

2321 into = frozenset( 

2322 parsed_data.get("into") 

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

2324 ) 

2325 dest_dir = parsed_data.get("dest_dir") 

2326 condition = parsed_data.get("when") 

2327 if install_as is not None: 

2328 assert len(sources) == 1 

2329 assert dest_dir is None 

2330 return InstallRule.install_as( 

2331 sources[0], 

2332 install_as.match_rule.path, 

2333 into, 

2334 path.path, 

2335 condition, 

2336 ) 

2337 return InstallRule.install_dest( 

2338 sources, 

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

2340 into, 

2341 path.path, 

2342 condition, 

2343 ) 

2344 

2345 

2346def _multi_dest_install_rule_handler( 

2347 _name: str, 

2348 parsed_data: ParsedMultiDestInstallRule, 

2349 path: AttributePath, 

2350 context: ParserContextData, 

2351) -> InstallRule: 

2352 sources = parsed_data["sources"] 

2353 install_as = parsed_data.get("install_as") 

2354 into = frozenset( 

2355 parsed_data.get("into") 

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

2357 ) 

2358 dest_dirs = parsed_data.get("dest_dirs") 

2359 condition = parsed_data.get("when") 

2360 if install_as is not None: 

2361 assert len(sources) == 1 

2362 assert dest_dirs is None 

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

2364 raise ManifestParseException( 

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

2366 ) 

2367 return InstallRule.install_multi_as( 

2368 sources[0], 

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

2370 into, 

2371 path.path, 

2372 condition, 

2373 ) 

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

2375 raise ManifestParseException( 

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

2377 ) 

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

2379 raise ManifestParseException( 

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

2381 ) 

2382 return InstallRule.install_multi_dest( 

2383 sources, 

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

2385 into, 

2386 path.path, 

2387 condition, 

2388 ) 

2389 

2390 

2391def _install_docs_rule_handler( 

2392 _name: str, 

2393 parsed_data: ParsedInstallRule, 

2394 path: AttributePath, 

2395 context: ParserContextData, 

2396) -> InstallRule: 

2397 sources = parsed_data["sources"] 

2398 install_as = parsed_data.get("install_as") 

2399 dest_dir = parsed_data.get("dest_dir") 

2400 condition = parsed_data.get("when") 

2401 into = frozenset( 

2402 parsed_data.get("into") 

2403 or ( 

2404 context.single_binary_package( 

2405 path, 

2406 package_types=PackageTypeSelector.DEB, 

2407 package_attribute="into", 

2408 ), 

2409 ) 

2410 ) 

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

2412 assert len(sources) == 1 

2413 assert dest_dir is None 

2414 return InstallRule.install_doc_as( 

2415 sources[0], 

2416 install_as.match_rule.path, 

2417 into, 

2418 path.path, 

2419 condition, 

2420 ) 

2421 return InstallRule.install_doc( 

2422 sources, 

2423 dest_dir, 

2424 into, 

2425 path.path, 

2426 condition, 

2427 ) 

2428 

2429 

2430def _install_examples_rule_handler( 

2431 _name: str, 

2432 parsed_data: ParsedInstallExamplesRule, 

2433 path: AttributePath, 

2434 context: ParserContextData, 

2435) -> InstallRule: 

2436 return InstallRule.install_examples( 

2437 sources=parsed_data["sources"], 

2438 into=frozenset( 

2439 parsed_data.get("into") 

2440 or ( 

2441 context.single_binary_package( 

2442 path, 

2443 package_types=PackageTypeSelector.DEB, 

2444 package_attribute="into", 

2445 ), 

2446 ) 

2447 ), 

2448 definition_source=path.path, 

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

2450 ) 

2451 

2452 

2453def _install_man_rule_handler( 

2454 _name: str, 

2455 parsed_data: ParsedInstallManpageRule, 

2456 attribute_path: AttributePath, 

2457 context: ParserContextData, 

2458) -> InstallRule: 

2459 sources = parsed_data["sources"] 

2460 language = parsed_data.get("language") 

2461 section = parsed_data.get("section") 

2462 

2463 if language is not None: 

2464 is_lang_ok = language in ( 

2465 "C", 

2466 "derive-from-basename", 

2467 "derive-from-path", 

2468 ) 

2469 

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

2471 is_lang_ok = True 

2472 

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

2474 not is_lang_ok 

2475 and len(language) == 5 

2476 and language[2] == "_" 

2477 and language[:2].islower() 

2478 and language[3:].isupper() 

2479 ): 

2480 is_lang_ok = True 

2481 

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

2483 raise ManifestParseException( 

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

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

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

2487 ) 

2488 

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

2490 raise ManifestParseException( 

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

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

2493 ) 

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

2495 raise ManifestParseException( 

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

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

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

2499 ) 

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

2501 raise ManifestParseException( 

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

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

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

2505 ) 

2506 return InstallRule.install_man( 

2507 sources=sources, 

2508 into=frozenset( 

2509 parsed_data.get("into") 

2510 or ( 

2511 context.single_binary_package( 

2512 attribute_path, 

2513 package_types=PackageTypeSelector.DEB, 

2514 package_attribute="into", 

2515 ), 

2516 ) 

2517 ), 

2518 section=section, 

2519 language=language, 

2520 definition_source=attribute_path.path, 

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

2522 ) 

2523 

2524 

2525def _install_discard_rule_handler( 

2526 _name: str, 

2527 parsed_data: ParsedInstallDiscardRule, 

2528 path: AttributePath, 

2529 _context: ParserContextData, 

2530) -> InstallRule: 

2531 limit_to = parsed_data.get("search_dirs") 

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

2533 p = path["search_dirs"] 

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

2535 condition = parsed_data.get("required_when") 

2536 return InstallRule.discard_paths( 

2537 parsed_data["paths"], 

2538 path.path, 

2539 condition, 

2540 limit_to=limit_to, 

2541 ) 

2542 

2543 

2544def _transformation_move_handler( 

2545 _name: str, 

2546 parsed_data: TransformationMoveRuleSpec, 

2547 path: AttributePath, 

2548 _context: ParserContextData, 

2549) -> TransformationRule: 

2550 source_match = parsed_data["source"] 

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

2552 condition = parsed_data.get("when") 

2553 

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

2555 isinstance(source_match, ExactFileSystemPath) 

2556 and source_match.path == target_path 

2557 ): 

2558 raise ManifestParseException( 

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

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

2561 ) 

2562 return MoveTransformationRule( 

2563 source_match.match_rule, 

2564 target_path, 

2565 target_path.endswith("/"), 

2566 path, 

2567 condition, 

2568 ) 

2569 

2570 

2571def _transformation_remove_handler( 

2572 _name: str, 

2573 parsed_data: TransformationRemoveRuleSpec, 

2574 attribute_path: AttributePath, 

2575 _context: ParserContextData, 

2576) -> TransformationRule: 

2577 paths = parsed_data["paths"] 

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

2579 

2580 return RemoveTransformationRule( 

2581 [m.match_rule for m in paths], 

2582 keep_empty_parent_dirs, 

2583 attribute_path, 

2584 ) 

2585 

2586 

2587def _transformation_create_symlink( 

2588 _name: str, 

2589 parsed_data: CreateSymlinkRule, 

2590 attribute_path: AttributePath, 

2591 _context: ParserContextData, 

2592) -> TransformationRule: 

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

2594 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2595 "replacement_rule", 

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

2597 ) 

2598 try: 

2599 link_target = debian_policy_normalize_symlink_target( 

2600 link_dest, 

2601 parsed_data["target"].symlink_target, 

2602 ) 

2603 except ValueError as e: # pragma: no cover 

2604 raise AssertionError( 

2605 "Debian Policy normalization should not raise ValueError here" 

2606 ) from e 

2607 

2608 condition = parsed_data.get("when") 

2609 

2610 return CreateSymlinkPathTransformationRule( 

2611 link_target, 

2612 link_dest, 

2613 replacement_rule, 

2614 attribute_path, 

2615 condition, 

2616 ) 

2617 

2618 

2619def _transformation_path_metadata( 

2620 _name: str, 

2621 parsed_data: PathManifestRule, 

2622 attribute_path: AttributePath, 

2623 context: ParserContextData, 

2624) -> TransformationRule: 

2625 match_rules = parsed_data["paths"] 

2626 owner = parsed_data.get("owner") 

2627 group = parsed_data.get("group") 

2628 mode = parsed_data.get("mode") 

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

2630 capabilities = parsed_data.get("capabilities") 

2631 capability_mode = parsed_data.get("capability_mode") 

2632 cap: str | None = None 

2633 

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

2635 check_integration_mode( 

2636 attribute_path["capabilities"], 

2637 context, 

2638 _NOT_INTEGRATION_RRR, 

2639 ) 

2640 if capability_mode is None: 

2641 capability_mode = SymbolicMode.parse_filesystem_mode( 

2642 "a-s", 

2643 attribute_path["capability-mode"], 

2644 ) 

2645 cap = capabilities.value 

2646 validate_cap = check_cap_checker() 

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

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

2649 check_integration_mode( 

2650 attribute_path["capability_mode"], 

2651 context, 

2652 _NOT_INTEGRATION_RRR, 

2653 ) 

2654 raise ManifestParseException( 

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

2656 f" in {attribute_path.path}" 

2657 ) 

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

2659 raise ManifestParseException( 

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

2661 f" in {attribute_path.path}" 

2662 ) 

2663 condition = parsed_data.get("when") 

2664 

2665 return PathMetadataTransformationRule( 

2666 [m.match_rule for m in match_rules], 

2667 owner, 

2668 group, 

2669 mode, 

2670 recursive, 

2671 cap, 

2672 capability_mode, 

2673 attribute_path.path, 

2674 condition, 

2675 ) 

2676 

2677 

2678def _transformation_mkdirs( 

2679 _name: str, 

2680 parsed_data: EnsureDirectoryRule, 

2681 attribute_path: AttributePath, 

2682 _context: ParserContextData, 

2683) -> TransformationRule: 

2684 provided_paths = parsed_data["paths"] 

2685 owner = parsed_data.get("owner") 

2686 group = parsed_data.get("group") 

2687 mode = parsed_data.get("mode") 

2688 

2689 condition = parsed_data.get("when") 

2690 

2691 return CreateDirectoryTransformationRule( 

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

2693 owner, 

2694 group, 

2695 mode, 

2696 attribute_path.path, 

2697 condition, 

2698 ) 

2699 

2700 

2701def _at_least_two( 

2702 content: list[Any], 

2703 attribute_path: AttributePath, 

2704 attribute_name: str, 

2705) -> None: 

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

2707 raise ManifestParseException( 

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

2709 ) 

2710 

2711 

2712def _mc_any_of( 

2713 name: str, 

2714 parsed_data: MCAnyOfAllOf, 

2715 attribute_path: AttributePath, 

2716 _context: ParserContextData, 

2717) -> ManifestCondition: 

2718 conditions = parsed_data["conditions"] 

2719 _at_least_two(conditions, attribute_path, "conditions") 

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

2721 return ManifestCondition.any_of(conditions) 

2722 assert name == "all-of" 

2723 return ManifestCondition.all_of(conditions) 

2724 

2725 

2726def _mc_not( 

2727 _name: str, 

2728 parsed_data: MCNot, 

2729 _attribute_path: AttributePath, 

2730 _context: ParserContextData, 

2731) -> ManifestCondition: 

2732 condition = parsed_data["negated_condition"] 

2733 return condition.negated() 

2734 

2735 

2736def _extract_arch_matches( 

2737 parsed_data: MCArchMatches, 

2738 attribute_path: AttributePath, 

2739) -> list[str]: 

2740 arch_matches_as_str = parsed_data["arch_matches"] 

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

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

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

2744 # of each other. 

2745 arch_matches_as_list = arch_matches_as_str.split() 

2746 attr_path = attribute_path["arch_matches"] 

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

2748 raise ManifestParseException( 

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

2750 ) 

2751 

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

2753 "]" 

2754 ): 

2755 raise ManifestParseException( 

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

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

2758 ) 

2759 return arch_matches_as_list 

2760 

2761 

2762def _mc_source_context_arch_matches( 

2763 _name: str, 

2764 parsed_data: MCArchMatches, 

2765 attribute_path: AttributePath, 

2766 _context: ParserContextData, 

2767) -> ManifestCondition: 

2768 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2769 return SourceContextArchMatchManifestCondition(arch_matches) 

2770 

2771 

2772def _mc_package_context_arch_matches( 

2773 name: str, 

2774 parsed_data: MCArchMatches, 

2775 attribute_path: AttributePath, 

2776 context: ParserContextData, 

2777) -> ManifestCondition: 

2778 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2779 

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

2781 raise ManifestParseException( 

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

2783 ) 

2784 

2785 package_state = context.current_binary_package_state 

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

2787 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2788 "all", arch_matches 

2789 ) 

2790 attr_path = attribute_path["arch_matches"] 

2791 raise ManifestParseException( 

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

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

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

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

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

2797 ) 

2798 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2799 

2800 

2801def _mc_arch_matches( 

2802 name: str, 

2803 parsed_data: MCArchMatches, 

2804 attribute_path: AttributePath, 

2805 context: ParserContextData, 

2806) -> ManifestCondition: 

2807 if context.is_in_binary_package_state: 

2808 return _mc_package_context_arch_matches( 

2809 name, parsed_data, attribute_path, context 

2810 ) 

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

2812 

2813 

2814def _mc_build_profile_matches( 

2815 _name: str, 

2816 parsed_data: MCBuildProfileMatches, 

2817 attribute_path: AttributePath, 

2818 _context: ParserContextData, 

2819) -> ManifestCondition: 

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

2821 attr_path = attribute_path["build_profile_matches"] 

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

2823 raise ManifestParseException( 

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

2825 ) 

2826 try: 

2827 active_profiles_match(build_profile_spec, frozenset()) 

2828 except ValueError as e: 

2829 raise ManifestParseException( 

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

2831 ) 

2832 return BuildProfileMatch(build_profile_spec)