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

540 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-02-28 21:56 +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 ↛ 273line 264 didn't jump to line 273 because the condition on line 264 was always true

265 try: 

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

267 except ValueError: 

268 _error( 

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

270 ) 

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

272 else: 

273 _warn( 

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

275 " for SOURCE_DATE_EPOCH" 

276 ) 

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

278 

279 if first_non_binnmu_entry is not first_entry: 

280 non_binnmu_date = first_non_binnmu_entry.date 

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

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

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

284 else: 

285 _warn( 

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

287 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)" 

288 ) 

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

290 else: 

291 snd_source_date_epoch = source_date_epoch 

292 return { 

293 "DEB_SOURCE": package, 

294 "DEB_VERSION": source_version.full_version, 

295 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream, 

296 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision, 

297 "DEB_VERSION_UPSTREAM": upstream_version, 

298 "SOURCE_DATE_EPOCH": source_date_epoch, 

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

300 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch, 

301 } 

302 

303 

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

305 api = cast("DebputyPluginInitializerProvider", public_api) 

306 

307 api.metadata_or_maintscript_detector( 

308 "dpkg-shlibdeps", 

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

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

311 package_types=PackageTypeSelector.DEB | PackageTypeSelector.UDEB, 

312 ) 

313 register_type_mappings(api) 

314 register_variables_via_private_api(api) 

315 document_builtin_variables(api) 

316 register_automatic_discard_rules(api) 

317 register_special_ppfs(api) 

318 register_install_rules(api) 

319 register_transformation_rules(api) 

320 register_manifest_condition_rules(api) 

321 register_dpkg_conffile_rules(api) 

322 register_processing_steps(api) 

323 register_service_managers(api) 

324 register_manifest_root_rules(api) 

325 register_binary_package_rules(api) 

326 

327 

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

329 api.register_mapped_type( 

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

331 reference_documentation=type_mapping_reference_documentation( 

332 description=textwrap.dedent( 

333 """\ 

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

335 

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

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

338 currently never emit hard errors for unknown capabilities. 

339 """, 

340 ), 

341 examples=[ 

342 type_mapping_example("cap_chown=p"), 

343 type_mapping_example("cap_chown=ep"), 

344 type_mapping_example("cap_kill-pe"), 

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

346 ], 

347 ), 

348 ) 

349 api.register_mapped_type( 

350 TypeMapping( 

351 FileSystemMatchRule, 

352 str, 

353 FileSystemMatchRule.parse_path_match, 

354 ), 

355 reference_documentation=type_mapping_reference_documentation( 

356 description=textwrap.dedent( 

357 """\ 

358 A generic file system path match with globs. 

359 

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

361 

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

363 

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

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

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

367 

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

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

370 

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

372 or relevant search directories will match. 

373 

374 Please keep in mind that: 

375 

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

377 an anchor reference. 

378 

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

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

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

382 directories (similar to the shell). 

383 

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

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

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

387 """, 

388 ), 

389 examples=[ 

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

391 type_mapping_example("*.txt"), 

392 type_mapping_example("**/foo"), 

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

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

395 ], 

396 ), 

397 ) 

398 

399 api.register_mapped_type( 

400 TypeMapping( 

401 FileSystemExactMatchRule, 

402 str, 

403 FileSystemExactMatchRule.parse_path_match, 

404 ), 

405 reference_documentation=type_mapping_reference_documentation( 

406 description=textwrap.dedent( 

407 """\ 

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

409 

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

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

412 

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

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

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

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

417 """, 

418 ), 

419 examples=[ 

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

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

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

423 ], 

424 ), 

425 ) 

426 

427 api.register_mapped_type( 

428 TypeMapping( 

429 FileSystemExactNonDirMatchRule, 

430 str, 

431 FileSystemExactNonDirMatchRule.parse_path_match, 

432 ), 

433 reference_documentation=type_mapping_reference_documentation( 

434 description=textwrap.dedent( 

435 f"""\ 

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

437 

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

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

440 

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

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

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

444 with a "/". 

445 """, 

446 ), 

447 examples=[ 

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

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

450 ], 

451 ), 

452 ) 

453 

454 api.register_mapped_type( 

455 TypeMapping( 

456 SymlinkTarget, 

457 str, 

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

459 v, ap, assume_not_none(pc).substitution 

460 ), 

461 ), 

462 reference_documentation=type_mapping_reference_documentation( 

463 description=textwrap.dedent( 

464 """\ 

465 A symlink target. 

466 

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

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

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

470 

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

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

473 """, 

474 ), 

475 examples=[ 

476 type_mapping_example("../foo"), 

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

478 ], 

479 ), 

480 ) 

481 

482 api.register_mapped_type( 

483 TypeMapping( 

484 StaticFileSystemOwner, 

485 Union[int, str], 

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

487 ), 

488 reference_documentation=type_mapping_reference_documentation( 

489 description=textwrap.dedent( 

490 """\ 

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

492 

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

494 

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

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

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

498 

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

500 

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

502 """ 

503 ), 

504 examples=[ 

505 type_mapping_example("root"), 

506 type_mapping_example(0), 

507 type_mapping_example("root:0"), 

508 type_mapping_example("bin"), 

509 ], 

510 ), 

511 ) 

512 api.register_mapped_type( 

513 TypeMapping( 

514 StaticFileSystemGroup, 

515 Union[int, str], 

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

517 ), 

518 reference_documentation=type_mapping_reference_documentation( 

519 description=textwrap.dedent( 

520 """\ 

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

522 

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

524 

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

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

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

528 

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

530 

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

532 """ 

533 ), 

534 examples=[ 

535 type_mapping_example("root"), 

536 type_mapping_example(0), 

537 type_mapping_example("root:0"), 

538 type_mapping_example("tty"), 

539 ], 

540 ), 

541 ) 

542 

543 api.register_mapped_type( 

544 TypeMapping( 

545 BinaryPackage, 

546 str, 

547 type_mapper_str2package, 

548 ), 

549 reference_documentation=type_mapping_reference_documentation( 

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

551 ), 

552 ) 

553 

554 api.register_mapped_type( 

555 TypeMapping( 

556 PackageSelector, 

557 str, 

558 PackageSelector.parse, 

559 ), 

560 reference_documentation=type_mapping_reference_documentation( 

561 description=textwrap.dedent( 

562 """\ 

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

564 

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

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

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

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

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

570 respectively). 

571 """ 

572 ), 

573 ), 

574 ) 

575 

576 api.register_mapped_type( 

577 TypeMapping( 

578 FileSystemMode, 

579 str, 

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

581 ), 

582 reference_documentation=type_mapping_reference_documentation( 

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

584 examples=[ 

585 type_mapping_example("a+x"), 

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

587 type_mapping_example("0755"), 

588 ], 

589 ), 

590 ) 

591 api.register_mapped_type( 

592 TypeMapping( 

593 OctalMode, 

594 str, 

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

596 ), 

597 reference_documentation=type_mapping_reference_documentation( 

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

599 examples=[ 

600 type_mapping_example("0644"), 

601 type_mapping_example("0755"), 

602 ], 

603 ), 

604 ) 

605 api.register_mapped_type( 

606 TypeMapping( 

607 BuildEnvironmentDefinition, 

608 str, 

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

610 ), 

611 reference_documentation=type_mapping_reference_documentation( 

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

613 ), 

614 ) 

615 

616 

617def register_service_managers( 

618 api: DebputyPluginInitializerProvider, 

619) -> None: 

620 api.service_provider( 

621 "systemd", 

622 detect_systemd_service_files, 

623 generate_snippets_for_systemd_units, 

624 ) 

625 api.service_provider( 

626 "sysvinit", 

627 detect_sysv_init_service_files, 

628 generate_snippets_for_init_scripts, 

629 ) 

630 

631 

632def register_automatic_discard_rules( 

633 api: DebputyPluginInitializerProvider, 

634) -> None: 

635 api.automatic_discard_rule( 

636 "python-cache-files", 

637 _debputy_discard_pyc_files, 

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

639 examples=automatic_discard_rule_example( 

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

641 ".../__pycache__/", 

642 ".../__pycache__/...", 

643 ".../foo.pyc", 

644 ".../foo.pyo", 

645 ), 

646 ) 

647 api.automatic_discard_rule( 

648 "la-files", 

649 _debputy_prune_la_files, 

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

651 examples=automatic_discard_rule_example( 

652 "usr/lib/libfoo.la", 

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

654 ), 

655 ) 

656 api.automatic_discard_rule( 

657 "backup-files", 

658 _debputy_prune_backup_files, 

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

660 examples=( 

661 automatic_discard_rule_example( 

662 ".../foo~", 

663 ".../foo.orig", 

664 ".../foo.rej", 

665 ".../DEADJOE", 

666 ".../.foo.sw.", 

667 ), 

668 ), 

669 ) 

670 api.automatic_discard_rule( 

671 "version-control-paths", 

672 _debputy_prune_vcs_paths, 

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

674 examples=automatic_discard_rule_example( 

675 ("tools/foo", False), 

676 ".../CVS/", 

677 ".../CVS/...", 

678 ".../.gitignore", 

679 ".../.gitattributes", 

680 ".../.git/", 

681 ".../.git/...", 

682 ), 

683 ) 

684 api.automatic_discard_rule( 

685 "gnu-info-dir-file", 

686 _debputy_prune_info_dir_file, 

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

688 examples=automatic_discard_rule_example( 

689 "usr/share/info/dir", 

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

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

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

693 ), 

694 ) 

695 api.automatic_discard_rule( 

696 "debian-dir", 

697 _debputy_prune_binary_debian_dir, 

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

699 " literally in the file listing", 

700 examples=( 

701 automatic_discard_rule_example( 

702 "DEBIAN/", 

703 "DEBIAN/control", 

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

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

706 ), 

707 ), 

708 ) 

709 api.automatic_discard_rule( 

710 "doxygen-cruft-files", 

711 _debputy_prune_doxygen_cruft, 

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

713 examples=automatic_discard_rule_example( 

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

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

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

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

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

719 ), 

720 ) 

721 

722 

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

724 api.package_processor("manpages", process_manpages) 

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

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

727 api.package_processor( 

728 "strip-nondeterminism", 

729 cast("Any", strip_non_determinism), 

730 depends_on_processor=["manpages"], 

731 ) 

732 api.package_processor( 

733 "compression", 

734 apply_compression, 

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

736 ) 

737 

738 

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

740 api.manifest_variable_provider( 

741 load_source_variables, 

742 { 

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

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

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

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

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

748 "SOURCE_DATE_EPOCH": textwrap.dedent( 

749 """\ 

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

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

752 this variable. 

753 """ 

754 ), 

755 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

756 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

757 }, 

758 ) 

759 

760 

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

762 api.document_builtin_variable( 

763 "PACKAGE", 

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

765 is_context_specific=True, 

766 ) 

767 

768 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

769 

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

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

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

773 documentation = textwrap.dedent( 

774 f"""\ 

775 {arch_var_doc} ({arch_type_tag}) 

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

777 * Machine type: {arch_type_doc} 

778 * Value description: {arch_var_doc} 

779 

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

781 """ 

782 ) 

783 api.document_builtin_variable( 

784 full_var, 

785 documentation, 

786 is_for_special_case=arch_type != "HOST", 

787 ) 

788 

789 

790def _format_docbase_filename( 

791 path_format: str, 

792 format_param: PPFFormatParam, 

793 docbase_file: VirtualPath, 

794) -> str: 

795 with docbase_file.open() as fd: 

796 content = Deb822(fd) 

797 proper_name = content["Document"] 

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

799 format_param["name"] = proper_name 

800 else: 

801 _warn( 

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

803 ) 

804 return path_format.format(**format_param) 

805 

806 

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

808 api.packager_provided_file( 

809 "doc-base", 

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

811 format_callback=_format_docbase_filename, 

812 ) 

813 

814 api.packager_provided_file( 

815 "shlibs", 

816 "DEBIAN/shlibs", 

817 allow_name_segment=False, 

818 reservation_only=True, 

819 reference_documentation=packager_provided_file_reference_documentation( 

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

821 ), 

822 ) 

823 api.packager_provided_file( 

824 "symbols", 

825 "DEBIAN/symbols", 

826 allow_name_segment=False, 

827 allow_architecture_segment=True, 

828 reservation_only=True, 

829 reference_documentation=packager_provided_file_reference_documentation( 

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

831 ), 

832 ) 

833 api.packager_provided_file( 

834 "conffiles", 

835 "DEBIAN/conffiles", 

836 allow_name_segment=False, 

837 allow_architecture_segment=True, 

838 reservation_only=True, 

839 ) 

840 api.packager_provided_file( 

841 "templates", 

842 "DEBIAN/templates", 

843 allow_name_segment=False, 

844 allow_architecture_segment=False, 

845 reservation_only=True, 

846 ) 

847 api.packager_provided_file( 

848 "alternatives", 

849 "DEBIAN/alternatives", 

850 allow_name_segment=False, 

851 allow_architecture_segment=True, 

852 reservation_only=True, 

853 ) 

854 

855 

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

857 api.pluggable_manifest_rule( 

858 InstallRule, 

859 MK_INSTALLATIONS_INSTALL, 

860 ParsedInstallRule, 

861 _install_rule_handler, 

862 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

863 inline_reference_documentation=reference_documentation( 

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

865 description=textwrap.dedent( 

866 """\ 

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

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

869 

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

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

872 

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

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

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

876 """.format( 

877 MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL 

878 ) 

879 ), 

880 non_mapping_description=textwrap.dedent( 

881 """\ 

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

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

884 not required. 

885 """ 

886 ), 

887 attributes=[ 

888 documented_attr( 

889 ["source", "sources"], 

890 textwrap.dedent( 

891 """\ 

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

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

894 is tried against default search directories. 

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

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

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

898 """ 

899 ), 

900 ), 

901 documented_attr( 

902 "dest_dir", 

903 textwrap.dedent( 

904 """\ 

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

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

907 to the directory name of the `source`. 

908 """ 

909 ), 

910 ), 

911 documented_attr( 

912 "into", 

913 textwrap.dedent( 

914 """\ 

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

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

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

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

919 """ 

920 ), 

921 ), 

922 documented_attr( 

923 "install_as", 

924 textwrap.dedent( 

925 """\ 

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

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

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

929 """ 

930 ), 

931 ), 

932 *docs_from(DebputyParsedContentStandardConditional), 

933 ], 

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

935 ), 

936 ) 

937 api.pluggable_manifest_rule( 

938 InstallRule, 

939 [ 

940 MK_INSTALLATIONS_INSTALL_DOCS, 

941 "install-doc", 

942 ], 

943 ParsedInstallRule, 

944 _install_docs_rule_handler, 

945 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

946 inline_reference_documentation=reference_documentation( 

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

948 description=textwrap.dedent( 

949 """\ 

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

951 `install` rule with the following key features: 

952 

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

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

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

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

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

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

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

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

961 package listed in `debian/control`. 

962 

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

964 

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

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

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

968 use-case. 

969 """ 

970 ), 

971 non_mapping_description=textwrap.dedent( 

972 """\ 

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

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

975 not required. 

976 """ 

977 ), 

978 attributes=[ 

979 documented_attr( 

980 ["source", "sources"], 

981 textwrap.dedent( 

982 """\ 

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

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

985 is tried against default search directories. 

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

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

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

989 

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

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

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

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

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

995 """ 

996 ), 

997 ), 

998 documented_attr( 

999 "dest_dir", 

1000 textwrap.dedent( 

1001 """\ 

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

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

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

1005 """ 

1006 ), 

1007 ), 

1008 documented_attr( 

1009 "into", 

1010 textwrap.dedent( 

1011 """\ 

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

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

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

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

1016 the key is required. 

1017 """ 

1018 ), 

1019 ), 

1020 documented_attr( 

1021 "install_as", 

1022 textwrap.dedent( 

1023 """\ 

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

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

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

1027 """ 

1028 ), 

1029 ), 

1030 documented_attr( 

1031 "when", 

1032 textwrap.dedent( 

1033 """\ 

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

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

1036 (rather than replacing it). 

1037 """ 

1038 ), 

1039 ), 

1040 ], 

1041 reference_documentation_url=manifest_format_doc( 

1042 "install-documentation-install-docs" 

1043 ), 

1044 ), 

1045 ) 

1046 api.pluggable_manifest_rule( 

1047 InstallRule, 

1048 [ 

1049 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

1050 "install-example", 

1051 ], 

1052 ParsedInstallExamplesRule, 

1053 _install_examples_rule_handler, 

1054 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

1055 inline_reference_documentation=reference_documentation( 

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

1057 description=textwrap.dedent( 

1058 """\ 

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

1060 install` rule with the following key features: 

1061 

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

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

1064 dir. 

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

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

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

1068 package listed in `debian/control`. 

1069 

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

1071 """ 

1072 ), 

1073 non_mapping_description=textwrap.dedent( 

1074 """\ 

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

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

1077 not required. 

1078 """ 

1079 ), 

1080 attributes=[ 

1081 documented_attr( 

1082 ["source", "sources"], 

1083 textwrap.dedent( 

1084 """\ 

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

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

1087 is tried against default search directories. 

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

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

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

1091 

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

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

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

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

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

1097 """ 

1098 ), 

1099 ), 

1100 documented_attr( 

1101 "into", 

1102 textwrap.dedent( 

1103 """\ 

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

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

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

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

1108 Otherwise, the key is required. 

1109 """ 

1110 ), 

1111 ), 

1112 documented_attr( 

1113 "when", 

1114 textwrap.dedent( 

1115 """\ 

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

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

1118 (rather than replacing it). 

1119 """ 

1120 ), 

1121 ), 

1122 ], 

1123 reference_documentation_url=manifest_format_doc( 

1124 "install-examples-install-examples" 

1125 ), 

1126 ), 

1127 ) 

1128 api.pluggable_manifest_rule( 

1129 InstallRule, 

1130 MK_INSTALLATIONS_INSTALL_MAN, 

1131 ParsedInstallManpageRule, 

1132 _install_man_rule_handler, 

1133 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1134 inline_reference_documentation=reference_documentation( 

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

1136 description=textwrap.dedent( 

1137 """\ 

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

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

1140 

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

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

1143 language. 

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

1145 package listed in `debian/control`. 

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

1147 for when the auto-detection is insufficient. 

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

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

1150 

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

1152 """ 

1153 ), 

1154 non_mapping_description=textwrap.dedent( 

1155 """\ 

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

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

1158 not required. 

1159 """ 

1160 ), 

1161 attributes=[ 

1162 documented_attr( 

1163 ["source", "sources"], 

1164 textwrap.dedent( 

1165 """\ 

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

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

1168 is tried against default search directories. 

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

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

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

1172 """ 

1173 ), 

1174 ), 

1175 documented_attr( 

1176 "into", 

1177 textwrap.dedent( 

1178 """\ 

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

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

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

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

1183 """ 

1184 ), 

1185 ), 

1186 documented_attr( 

1187 "section", 

1188 textwrap.dedent( 

1189 """\ 

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

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

1192 have performed. 

1193 """ 

1194 ), 

1195 ), 

1196 documented_attr( 

1197 "language", 

1198 textwrap.dedent( 

1199 """\ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1217 always assumed to be a language. 

1218 """ 

1219 ), 

1220 ), 

1221 *docs_from(DebputyParsedContentStandardConditional), 

1222 ], 

1223 reference_documentation_url=manifest_format_doc( 

1224 "install-manpages-install-man" 

1225 ), 

1226 ), 

1227 ) 

1228 api.pluggable_manifest_rule( 

1229 InstallRule, 

1230 MK_INSTALLATIONS_DISCARD, 

1231 ParsedInstallDiscardRule, 

1232 _install_discard_rule_handler, 

1233 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1234 inline_reference_documentation=reference_documentation( 

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

1236 description=textwrap.dedent( 

1237 """\ 

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

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

1240 

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

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

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

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

1245 """ 

1246 ), 

1247 non_mapping_description=textwrap.dedent( 

1248 """\ 

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

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

1251 """ 

1252 ), 

1253 attributes=[ 

1254 documented_attr( 

1255 ["path", "paths"], 

1256 textwrap.dedent( 

1257 """\ 

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

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

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

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

1262 contents that have not already been installed somewhere. 

1263 """ 

1264 ), 

1265 ), 

1266 documented_attr( 

1267 ["search_dir", "search_dirs"], 

1268 textwrap.dedent( 

1269 """\ 

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

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

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

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

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

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

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

1277 will make `debputy` report an error. 

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

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

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

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

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

1283 applicable to certain builds that are only performed conditionally. 

1284 """ 

1285 ), 

1286 ), 

1287 documented_attr( 

1288 "required_when", 

1289 textwrap.dedent( 

1290 """\ 

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

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

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

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

1295 """ 

1296 ), 

1297 ), 

1298 ], 

1299 reference_documentation_url=manifest_format_doc( 

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

1301 ), 

1302 ), 

1303 ) 

1304 api.pluggable_manifest_rule( 

1305 InstallRule, 

1306 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1307 ParsedMultiDestInstallRule, 

1308 _multi_dest_install_rule_handler, 

1309 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1310 inline_reference_documentation=reference_documentation( 

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

1312 description=textwrap.dedent( 

1313 """\ 

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

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

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

1317 "primary" uses. 

1318 

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

1320 except you list 2+ destination directories. 

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

1322 2+ `as` names. 

1323 """ 

1324 ), 

1325 attributes=[ 

1326 documented_attr( 

1327 ["source", "sources"], 

1328 textwrap.dedent( 

1329 """\ 

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

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

1332 is tried against default search directories. 

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

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

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

1336 """ 

1337 ), 

1338 ), 

1339 documented_attr( 

1340 "dest_dirs", 

1341 textwrap.dedent( 

1342 """\ 

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

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

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

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

1347 """ 

1348 ), 

1349 ), 

1350 documented_attr( 

1351 "into", 

1352 textwrap.dedent( 

1353 """\ 

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

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

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

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

1358 """ 

1359 ), 

1360 ), 

1361 documented_attr( 

1362 "install_as", 

1363 textwrap.dedent( 

1364 """\ 

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

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

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

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

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

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

1371 """ 

1372 ), 

1373 ), 

1374 *docs_from(DebputyParsedContentStandardConditional), 

1375 ], 

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

1377 ), 

1378 ) 

1379 

1380 

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

1382 api.pluggable_manifest_rule( 

1383 TransformationRule, 

1384 "move", 

1385 TransformationMoveRuleSpec, 

1386 _transformation_move_handler, 

1387 inline_reference_documentation=reference_documentation( 

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

1389 description=textwrap.dedent( 

1390 """\ 

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

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

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

1394 Debian specific requirements. 

1395 

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

1397 `mv` command line tool. 

1398 """ 

1399 ), 

1400 attributes=[ 

1401 documented_attr( 

1402 "source", 

1403 textwrap.dedent( 

1404 """\ 

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

1406 and substitutions. 

1407 """ 

1408 ), 

1409 ), 

1410 documented_attr( 

1411 "target", 

1412 textwrap.dedent( 

1413 """\ 

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

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

1416 the target will *always* be a directory. 

1417 """ 

1418 ), 

1419 ), 

1420 *docs_from(DebputyParsedContentStandardConditional), 

1421 ], 

1422 reference_documentation_url=manifest_format_doc( 

1423 "move-transformation-rule-move" 

1424 ), 

1425 ), 

1426 ) 

1427 api.pluggable_manifest_rule( 

1428 TransformationRule, 

1429 "remove", 

1430 TransformationRemoveRuleSpec, 

1431 _transformation_remove_handler, 

1432 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1433 inline_reference_documentation=reference_documentation( 

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

1435 description=textwrap.dedent( 

1436 """\ 

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

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

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

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

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

1442 of `debian/copyright`). 

1443 

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

1445 the `remove` transformation rule. 

1446 

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

1448 """ 

1449 ), 

1450 non_mapping_description=textwrap.dedent( 

1451 """\ 

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

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

1454 """ 

1455 ), 

1456 attributes=[ 

1457 documented_attr( 

1458 ["path", "paths"], 

1459 textwrap.dedent( 

1460 """\ 

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

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

1463 can use globs. 

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

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

1466 along with all the contents. 

1467 """ 

1468 ), 

1469 ), 

1470 documented_attr( 

1471 "keep_empty_parent_dirs", 

1472 textwrap.dedent( 

1473 """\ 

1474 A boolean determining whether to prune parent directories that become 

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

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

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

1478 """ 

1479 ), 

1480 ), 

1481 documented_attr( 

1482 "when", 

1483 textwrap.dedent( 

1484 """\ 

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

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

1487 (rather than replacing it). 

1488 """ 

1489 ), 

1490 ), 

1491 ], 

1492 reference_documentation_url=manifest_format_doc( 

1493 "remove-transformation-rule-remove" 

1494 ), 

1495 ), 

1496 ) 

1497 api.pluggable_manifest_rule( 

1498 TransformationRule, 

1499 "create-symlink", 

1500 CreateSymlinkRule, 

1501 _transformation_create_symlink, 

1502 inline_reference_documentation=reference_documentation( 

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

1504 description=textwrap.dedent( 

1505 """\ 

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

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

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

1509 """ 

1510 ), 

1511 attributes=[ 

1512 documented_attr( 

1513 "path", 

1514 textwrap.dedent( 

1515 """\ 

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

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

1518 Parent directories are implicitly created as necessary. 

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

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

1521 """ 

1522 ), 

1523 ), 

1524 documented_attr( 

1525 "target", 

1526 textwrap.dedent( 

1527 """\ 

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

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

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

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

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

1533 preference. 

1534 """ 

1535 ), 

1536 ), 

1537 documented_attr( 

1538 "replacement_rule", 

1539 textwrap.dedent( 

1540 """\ 

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

1542 be set to one of the following values: 

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

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

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

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

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

1548 similar to `ln -sf` semantics. 

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

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

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

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

1553 will stop with an error. 

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

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

1556 be removed recursively along with the directory. Finally, 

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

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

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

1560 `when` if any). 

1561 

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

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

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

1565 the value in `replacement-rule`. 

1566 """ 

1567 ), 

1568 ), 

1569 *docs_from(DebputyParsedContentStandardConditional), 

1570 ], 

1571 reference_documentation_url=manifest_format_doc( 

1572 "create-symlinks-transformation-rule-create-symlink" 

1573 ), 

1574 ), 

1575 ) 

1576 api.pluggable_manifest_rule( 

1577 TransformationRule, 

1578 "path-metadata", 

1579 PathManifestRule, 

1580 _transformation_path_metadata, 

1581 source_format=PathManifestSourceDictFormat, 

1582 inline_reference_documentation=reference_documentation( 

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

1584 description=textwrap.dedent( 

1585 """\ 

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

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

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

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

1590 transformation. 

1591 

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

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

1594 """ 

1595 ), 

1596 attributes=[ 

1597 documented_attr( 

1598 ["path", "paths"], 

1599 textwrap.dedent( 

1600 """\ 

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

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

1603 and substitution variables. Special-rules for matches: 

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

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

1606 """ 

1607 ), 

1608 ), 

1609 documented_attr( 

1610 "owner", 

1611 textwrap.dedent( 

1612 """\ 

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

1614 no change of owner is done. 

1615 """ 

1616 ), 

1617 ), 

1618 documented_attr( 

1619 "group", 

1620 textwrap.dedent( 

1621 """\ 

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

1623 no change of group is done. 

1624 """ 

1625 ), 

1626 ), 

1627 documented_attr( 

1628 "mode", 

1629 textwrap.dedent( 

1630 """\ 

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

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

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

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

1635 relative to the matched path's current mode. 

1636 """ 

1637 ), 

1638 ), 

1639 documented_attr( 

1640 "capabilities", 

1641 textwrap.dedent( 

1642 """\ 

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

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

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

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

1647 run. 

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

1649 to those paths. 

1650 

1651 """ 

1652 ), 

1653 ), 

1654 documented_attr( 

1655 "capability_mode", 

1656 textwrap.dedent( 

1657 """\ 

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

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

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

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

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

1663 `capabilities` is omitted. 

1664 """ 

1665 ), 

1666 ), 

1667 documented_attr( 

1668 "recursive", 

1669 textwrap.dedent( 

1670 """\ 

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

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

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

1674 this attribute is `false`. 

1675 """ 

1676 ), 

1677 ), 

1678 *docs_from(DebputyParsedContentStandardConditional), 

1679 ], 

1680 reference_documentation_url=manifest_format_doc( 

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

1682 ), 

1683 ), 

1684 ) 

1685 api.pluggable_manifest_rule( 

1686 TransformationRule, 

1687 "create-directories", 

1688 EnsureDirectoryRule, 

1689 _transformation_mkdirs, 

1690 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1691 inline_reference_documentation=reference_documentation( 

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

1693 description=textwrap.dedent( 

1694 """\ 

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

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

1697 transformations will create directories as required. 

1698 

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

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

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

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

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

1704 

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

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

1707 """ 

1708 ), 

1709 non_mapping_description=textwrap.dedent( 

1710 """\ 

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

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

1713 """ 

1714 ), 

1715 attributes=[ 

1716 documented_attr( 

1717 ["path", "paths"], 

1718 textwrap.dedent( 

1719 """\ 

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

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

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

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

1724 are affected by the owner/mode options) 

1725 """ 

1726 ), 

1727 ), 

1728 documented_attr( 

1729 "owner", 

1730 textwrap.dedent( 

1731 """\ 

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

1733 Default is "root". 

1734 """ 

1735 ), 

1736 ), 

1737 documented_attr( 

1738 "group", 

1739 textwrap.dedent( 

1740 """\ 

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

1742 Default is "root". 

1743 """ 

1744 ), 

1745 ), 

1746 documented_attr( 

1747 "mode", 

1748 textwrap.dedent( 

1749 """\ 

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

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

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

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

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

1755 transformation. The default is "0755". 

1756 """ 

1757 ), 

1758 ), 

1759 *docs_from(DebputyParsedContentStandardConditional), 

1760 ], 

1761 reference_documentation_url=manifest_format_doc( 

1762 "create-directories-transformation-rule-directories" 

1763 ), 

1764 ), 

1765 ) 

1766 

1767 

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

1769 api.provide_manifest_keyword( 

1770 ManifestCondition, 

1771 "cross-compiling", 

1772 lambda *_: ManifestCondition.is_cross_building(), 

1773 ) 

1774 api.provide_manifest_keyword( 

1775 ManifestCondition, 

1776 "can-execute-compiled-binaries", 

1777 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 

1778 ) 

1779 api.provide_manifest_keyword( 

1780 ManifestCondition, 

1781 "run-build-time-tests", 

1782 lambda *_: ManifestCondition.run_build_time_tests(), 

1783 ) 

1784 

1785 api.pluggable_manifest_rule( 

1786 ManifestCondition, 

1787 "not", 

1788 MCNot, 

1789 _mc_not, 

1790 source_format=ManifestCondition, 

1791 ) 

1792 api.pluggable_manifest_rule( 

1793 ManifestCondition, 

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

1795 MCAnyOfAllOf, 

1796 _mc_any_of, 

1797 source_format=list[ManifestCondition], 

1798 ) 

1799 api.pluggable_manifest_rule( 

1800 ManifestCondition, 

1801 "arch-matches", 

1802 MCArchMatches, 

1803 _mc_arch_matches, 

1804 source_format=str, 

1805 inline_reference_documentation=reference_documentation( 

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

1807 description=textwrap.dedent( 

1808 """\ 

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

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

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

1812 and practically behaves like a comparison against 

1813 `dpkg-architecture -qDEB_HOST_ARCH`. 

1814 

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

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

1817 in the context of a binary package and like 

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

1819 are covered in their own keywords. 

1820 """ 

1821 ), 

1822 non_mapping_description=textwrap.dedent( 

1823 """\ 

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

1825 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1830 have it. 

1831 """ 

1832 ), 

1833 reference_documentation_url=manifest_format_doc( 

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

1835 ), 

1836 ), 

1837 ) 

1838 

1839 context_arch_doc = reference_documentation( 

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

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

1842 description=textwrap.dedent( 

1843 """\ 

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

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

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

1847 `arch-matches` condition. 

1848 

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

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

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

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

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

1854 to the packager that condition does not make sense. 

1855 

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

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

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

1859 very special cases). 

1860 

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

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

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

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

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

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

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

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

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

1870 need to care about nor use any of this. 

1871 

1872 Accordingly, the possible conditions are: 

1873 

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

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

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

1877 

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

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

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

1881 

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

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

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

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

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

1887 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1888 

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

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

1891 

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

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

1894 """ 

1895 ), 

1896 non_mapping_description=textwrap.dedent( 

1897 """\ 

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

1899 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1904 have it. 

1905 """ 

1906 ), 

1907 ) 

1908 

1909 api.pluggable_manifest_rule( 

1910 ManifestCondition, 

1911 "source-context-arch-matches", 

1912 MCArchMatches, 

1913 _mc_source_context_arch_matches, 

1914 source_format=str, 

1915 inline_reference_documentation=context_arch_doc, 

1916 ) 

1917 api.pluggable_manifest_rule( 

1918 ManifestCondition, 

1919 "package-context-arch-matches", 

1920 MCArchMatches, 

1921 _mc_arch_matches, 

1922 source_format=str, 

1923 inline_reference_documentation=context_arch_doc, 

1924 ) 

1925 api.pluggable_manifest_rule( 

1926 ManifestCondition, 

1927 "build-profiles-matches", 

1928 MCBuildProfileMatches, 

1929 _mc_build_profile_matches, 

1930 source_format=str, 

1931 ) 

1932 

1933 

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

1935 api.pluggable_manifest_rule( 

1936 DpkgMaintscriptHelperCommand, 

1937 "remove", 

1938 DpkgRemoveConffileRule, 

1939 _dpkg_conffile_remove, 

1940 inline_reference_documentation=None, # TODO: write and add 

1941 ) 

1942 

1943 api.pluggable_manifest_rule( 

1944 DpkgMaintscriptHelperCommand, 

1945 "rename", 

1946 DpkgRenameConffileRule, 

1947 _dpkg_conffile_rename, 

1948 inline_reference_documentation=None, # TODO: write and add 

1949 ) 

1950 

1951 

1952class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

1953 mode: NotRequired[FileSystemMode] 

1954 owner: NotRequired[StaticFileSystemOwner] 

1955 group: NotRequired[StaticFileSystemGroup] 

1956 

1957 

1958class PathManifestSourceDictFormat(_ModeOwnerBase): 

1959 path: NotRequired[ 

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

1961 ] 

1962 paths: NotRequired[list[FileSystemMatchRule]] 

1963 recursive: NotRequired[bool] 

1964 capabilities: NotRequired[Capability] 

1965 capability_mode: NotRequired[FileSystemMode] 

1966 

1967 

1968class PathManifestRule(_ModeOwnerBase): 

1969 paths: list[FileSystemMatchRule] 

1970 recursive: NotRequired[bool] 

1971 capabilities: NotRequired[Capability] 

1972 capability_mode: NotRequired[FileSystemMode] 

1973 

1974 

1975class EnsureDirectorySourceFormat(_ModeOwnerBase): 

1976 path: NotRequired[ 

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

1978 ] 

1979 paths: NotRequired[list[FileSystemExactMatchRule]] 

1980 

1981 

1982class EnsureDirectoryRule(_ModeOwnerBase): 

1983 paths: list[FileSystemExactMatchRule] 

1984 

1985 

1986class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

1987 path: FileSystemExactMatchRule 

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

1989 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

1990 

1991 

1992class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

1993 source: FileSystemMatchRule 

1994 target: FileSystemExactMatchRule 

1995 

1996 

1997class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

1998 paths: list[FileSystemMatchRule] 

1999 keep_empty_parent_dirs: NotRequired[bool] 

2000 

2001 

2002class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

2003 path: NotRequired[ 

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

2005 ] 

2006 paths: NotRequired[list[FileSystemMatchRule]] 

2007 keep_empty_parent_dirs: NotRequired[bool] 

2008 

2009 

2010class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2011 sources: NotRequired[list[FileSystemMatchRule]] 

2012 source: NotRequired[ 

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

2014 ] 

2015 into: NotRequired[ 

2016 Annotated[ 

2017 str | list[str], 

2018 DebputyParseHint.required_when_multi_binary(), 

2019 ] 

2020 ] 

2021 dest_dir: NotRequired[ 

2022 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2023 ] 

2024 install_as: NotRequired[ 

2025 Annotated[ 

2026 FileSystemExactMatchRule, 

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

2028 DebputyParseHint.manifest_attribute("as"), 

2029 DebputyParseHint.not_path_error_hint(), 

2030 ] 

2031 ] 

2032 

2033 

2034class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

2035 sources: NotRequired[list[FileSystemMatchRule]] 

2036 source: NotRequired[ 

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

2038 ] 

2039 into: NotRequired[ 

2040 Annotated[ 

2041 str | list[str], 

2042 DebputyParseHint.required_when_multi_binary( 

2043 package_types=PackageTypeSelector.DEB 

2044 ), 

2045 ] 

2046 ] 

2047 dest_dir: NotRequired[ 

2048 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2049 ] 

2050 install_as: NotRequired[ 

2051 Annotated[ 

2052 FileSystemExactMatchRule, 

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

2054 DebputyParseHint.manifest_attribute("as"), 

2055 DebputyParseHint.not_path_error_hint(), 

2056 ] 

2057 ] 

2058 

2059 

2060class ParsedInstallRule(DebputyParsedContentStandardConditional): 

2061 sources: list[FileSystemMatchRule] 

2062 into: NotRequired[list[BinaryPackage]] 

2063 dest_dir: NotRequired[FileSystemExactMatchRule] 

2064 install_as: NotRequired[FileSystemExactMatchRule] 

2065 

2066 

2067class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2068 sources: NotRequired[list[FileSystemMatchRule]] 

2069 source: NotRequired[ 

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

2071 ] 

2072 into: NotRequired[ 

2073 Annotated[ 

2074 str | list[str], 

2075 DebputyParseHint.required_when_multi_binary(), 

2076 ] 

2077 ] 

2078 dest_dirs: NotRequired[ 

2079 Annotated[ 

2080 list[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

2081 ] 

2082 ] 

2083 install_as: NotRequired[ 

2084 Annotated[ 

2085 list[FileSystemExactMatchRule], 

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

2087 DebputyParseHint.not_path_error_hint(), 

2088 DebputyParseHint.manifest_attribute("as"), 

2089 ] 

2090 ] 

2091 

2092 

2093class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

2094 sources: list[FileSystemMatchRule] 

2095 into: NotRequired[list[BinaryPackage]] 

2096 dest_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2097 install_as: NotRequired[list[FileSystemExactMatchRule]] 

2098 

2099 

2100class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

2101 sources: list[FileSystemMatchRule] 

2102 into: NotRequired[list[BinaryPackage]] 

2103 

2104 

2105class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

2106 sources: NotRequired[list[FileSystemMatchRule]] 

2107 source: NotRequired[ 

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

2109 ] 

2110 into: NotRequired[ 

2111 Annotated[ 

2112 str | list[str], 

2113 DebputyParseHint.required_when_multi_binary( 

2114 package_types=PackageTypeSelector.DEB 

2115 ), 

2116 ] 

2117 ] 

2118 

2119 

2120class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

2121 sources: list[FileSystemMatchRule] 

2122 language: NotRequired[str] 

2123 section: NotRequired[int] 

2124 into: NotRequired[list[BinaryPackage]] 

2125 

2126 

2127class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

2128 sources: NotRequired[list[FileSystemMatchRule]] 

2129 source: NotRequired[ 

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

2131 ] 

2132 language: NotRequired[str] 

2133 section: NotRequired[int] 

2134 into: NotRequired[ 

2135 Annotated[ 

2136 str | list[str], 

2137 DebputyParseHint.required_when_multi_binary( 

2138 package_types=PackageTypeSelector.DEB 

2139 ), 

2140 ] 

2141 ] 

2142 

2143 

2144class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2145 paths: NotRequired[list[FileSystemMatchRule]] 

2146 path: NotRequired[ 

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

2148 ] 

2149 search_dir: NotRequired[ 

2150 Annotated[ 

2151 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2152 ] 

2153 ] 

2154 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2155 required_when: NotRequired[ManifestCondition] 

2156 

2157 

2158class ParsedInstallDiscardRule(DebputyParsedContent): 

2159 paths: list[FileSystemMatchRule] 

2160 search_dirs: NotRequired[list[FileSystemExactMatchRule]] 

2161 required_when: NotRequired[ManifestCondition] 

2162 

2163 

2164class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2165 prior_to_version: NotRequired[str] 

2166 owning_package: NotRequired[str] 

2167 

2168 

2169class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2170 source: str 

2171 target: str 

2172 

2173 

2174class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2175 path: str 

2176 

2177 

2178class MCAnyOfAllOf(DebputyParsedContent): 

2179 conditions: list[ManifestCondition] 

2180 

2181 

2182class MCNot(DebputyParsedContent): 

2183 negated_condition: ManifestCondition 

2184 

2185 

2186class MCArchMatches(DebputyParsedContent): 

2187 arch_matches: str 

2188 

2189 

2190class MCBuildProfileMatches(DebputyParsedContent): 

2191 build_profile_matches: str 

2192 

2193 

2194def _parse_filename( 

2195 filename: str, 

2196 attribute_path: AttributePath, 

2197 *, 

2198 allow_directories: bool = True, 

2199) -> str: 

2200 try: 

2201 normalized_path = _normalize_path(filename, with_prefix=False) 

2202 except ValueError as e: 

2203 raise ManifestParseException( 

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

2205 ) from None 

2206 if not allow_directories and filename.endswith("/"): 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} ends with "/" implying it is a directory,' 

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

2210 ) 

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

2212 raise ManifestParseException( 

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

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

2215 ) 

2216 return normalized_path 

2217 

2218 

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

2220 return Union[ 

2221 t, 

2222 list[str], 

2223 str, 

2224 ] 

2225 

2226 

2227def _dpkg_conffile_rename( 

2228 _name: str, 

2229 parsed_data: DpkgRenameConffileRule, 

2230 path: AttributePath, 

2231 _context: ParserContextData, 

2232) -> DpkgMaintscriptHelperCommand: 

2233 source_file = parsed_data["source"] 

2234 target_file = parsed_data["target"] 

2235 normalized_source = _parse_filename( 

2236 source_file, 

2237 path["source"], 

2238 allow_directories=False, 

2239 ) 

2240 path.path_hint = source_file 

2241 

2242 normalized_target = _parse_filename( 

2243 target_file, 

2244 path["target"], 

2245 allow_directories=False, 

2246 ) 

2247 normalized_source = "/" + normalized_source 

2248 normalized_target = "/" + normalized_target 

2249 

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

2251 raise ManifestParseException( 

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

2253 ) 

2254 

2255 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2256 parsed_data, path 

2257 ) 

2258 return DpkgMaintscriptHelperCommand.mv_conffile( 

2259 path, 

2260 normalized_source, 

2261 normalized_target, 

2262 version, 

2263 owning_package, 

2264 ) 

2265 

2266 

2267def _dpkg_conffile_remove( 

2268 _name: str, 

2269 parsed_data: DpkgRemoveConffileRule, 

2270 path: AttributePath, 

2271 _context: ParserContextData, 

2272) -> DpkgMaintscriptHelperCommand: 

2273 source_file = parsed_data["path"] 

2274 normalized_source = _parse_filename( 

2275 source_file, 

2276 path["path"], 

2277 allow_directories=False, 

2278 ) 

2279 path.path_hint = source_file 

2280 

2281 normalized_source = "/" + normalized_source 

2282 

2283 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2284 parsed_data, path 

2285 ) 

2286 return DpkgMaintscriptHelperCommand.rm_conffile( 

2287 path, 

2288 normalized_source, 

2289 version, 

2290 owning_package, 

2291 ) 

2292 

2293 

2294def _parse_conffile_prior_version_and_owning_package( 

2295 d: DpkgConffileManagementRuleBase, 

2296 attribute_path: AttributePath, 

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

2298 prior_version = d.get("prior_to_version") 

2299 owning_package = d.get("owning_package") 

2300 

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

2302 p = attribute_path["prior_to_version"] 

2303 raise ManifestParseException( 

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

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

2306 ) 

2307 

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

2309 p = attribute_path["owning_package"] 

2310 raise ManifestParseException( 

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

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

2313 ) 

2314 

2315 return prior_version, owning_package 

2316 

2317 

2318def _install_rule_handler( 

2319 _name: str, 

2320 parsed_data: ParsedInstallRule, 

2321 path: AttributePath, 

2322 context: ParserContextData, 

2323) -> InstallRule: 

2324 sources = parsed_data["sources"] 

2325 install_as = parsed_data.get("install_as") 

2326 into = frozenset( 

2327 parsed_data.get("into") 

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

2329 ) 

2330 dest_dir = parsed_data.get("dest_dir") 

2331 condition = parsed_data.get("when") 

2332 if install_as is not None: 

2333 assert len(sources) == 1 

2334 assert dest_dir is None 

2335 return InstallRule.install_as( 

2336 sources[0], 

2337 install_as.match_rule.path, 

2338 into, 

2339 path.path, 

2340 condition, 

2341 ) 

2342 return InstallRule.install_dest( 

2343 sources, 

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

2345 into, 

2346 path.path, 

2347 condition, 

2348 ) 

2349 

2350 

2351def _multi_dest_install_rule_handler( 

2352 _name: str, 

2353 parsed_data: ParsedMultiDestInstallRule, 

2354 path: AttributePath, 

2355 context: ParserContextData, 

2356) -> InstallRule: 

2357 sources = parsed_data["sources"] 

2358 install_as = parsed_data.get("install_as") 

2359 into = frozenset( 

2360 parsed_data.get("into") 

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

2362 ) 

2363 dest_dirs = parsed_data.get("dest_dirs") 

2364 condition = parsed_data.get("when") 

2365 if install_as is not None: 

2366 assert len(sources) == 1 

2367 assert dest_dirs is None 

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

2369 raise ManifestParseException( 

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

2371 ) 

2372 return InstallRule.install_multi_as( 

2373 sources[0], 

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

2375 into, 

2376 path.path, 

2377 condition, 

2378 ) 

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

2380 raise ManifestParseException( 

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

2382 ) 

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

2384 raise ManifestParseException( 

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

2386 ) 

2387 return InstallRule.install_multi_dest( 

2388 sources, 

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

2390 into, 

2391 path.path, 

2392 condition, 

2393 ) 

2394 

2395 

2396def _install_docs_rule_handler( 

2397 _name: str, 

2398 parsed_data: ParsedInstallRule, 

2399 path: AttributePath, 

2400 context: ParserContextData, 

2401) -> InstallRule: 

2402 sources = parsed_data["sources"] 

2403 install_as = parsed_data.get("install_as") 

2404 dest_dir = parsed_data.get("dest_dir") 

2405 condition = parsed_data.get("when") 

2406 into = frozenset( 

2407 parsed_data.get("into") 

2408 or ( 

2409 context.single_binary_package( 

2410 path, 

2411 package_types=PackageTypeSelector.DEB, 

2412 package_attribute="into", 

2413 ), 

2414 ) 

2415 ) 

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

2417 assert len(sources) == 1 

2418 assert dest_dir is None 

2419 return InstallRule.install_doc_as( 

2420 sources[0], 

2421 install_as.match_rule.path, 

2422 into, 

2423 path.path, 

2424 condition, 

2425 ) 

2426 return InstallRule.install_doc( 

2427 sources, 

2428 None if dest_dir is None else dest_dir.raw_match_rule, 

2429 into, 

2430 path.path, 

2431 condition, 

2432 ) 

2433 

2434 

2435def _install_examples_rule_handler( 

2436 _name: str, 

2437 parsed_data: ParsedInstallExamplesRule, 

2438 path: AttributePath, 

2439 context: ParserContextData, 

2440) -> InstallRule: 

2441 return InstallRule.install_examples( 

2442 sources=parsed_data["sources"], 

2443 into=frozenset( 

2444 parsed_data.get("into") 

2445 or ( 

2446 context.single_binary_package( 

2447 path, 

2448 package_types=PackageTypeSelector.DEB, 

2449 package_attribute="into", 

2450 ), 

2451 ) 

2452 ), 

2453 definition_source=path.path, 

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

2455 ) 

2456 

2457 

2458def _install_man_rule_handler( 

2459 _name: str, 

2460 parsed_data: ParsedInstallManpageRule, 

2461 attribute_path: AttributePath, 

2462 context: ParserContextData, 

2463) -> InstallRule: 

2464 sources = parsed_data["sources"] 

2465 language = parsed_data.get("language") 

2466 section = parsed_data.get("section") 

2467 

2468 if language is not None: 

2469 is_lang_ok = language in ( 

2470 "C", 

2471 "derive-from-basename", 

2472 "derive-from-path", 

2473 ) 

2474 

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

2476 is_lang_ok = True 

2477 

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

2479 not is_lang_ok 

2480 and len(language) == 5 

2481 and language[2] == "_" 

2482 and language[:2].islower() 

2483 and language[3:].isupper() 

2484 ): 

2485 is_lang_ok = True 

2486 

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

2488 raise ManifestParseException( 

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

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

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

2492 ) 

2493 

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

2495 raise ManifestParseException( 

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

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

2498 ) 

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

2500 raise ManifestParseException( 

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

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

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

2504 ) 

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

2506 raise ManifestParseException( 

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

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

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

2510 ) 

2511 return InstallRule.install_man( 

2512 sources=sources, 

2513 into=frozenset( 

2514 parsed_data.get("into") 

2515 or ( 

2516 context.single_binary_package( 

2517 attribute_path, 

2518 package_types=PackageTypeSelector.DEB, 

2519 package_attribute="into", 

2520 ), 

2521 ) 

2522 ), 

2523 section=section, 

2524 language=language, 

2525 definition_source=attribute_path.path, 

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

2527 ) 

2528 

2529 

2530def _install_discard_rule_handler( 

2531 _name: str, 

2532 parsed_data: ParsedInstallDiscardRule, 

2533 path: AttributePath, 

2534 _context: ParserContextData, 

2535) -> InstallRule: 

2536 limit_to = parsed_data.get("search_dirs") 

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

2538 p = path["search_dirs"] 

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

2540 condition = parsed_data.get("required_when") 

2541 return InstallRule.discard_paths( 

2542 parsed_data["paths"], 

2543 path.path, 

2544 condition, 

2545 limit_to=limit_to, 

2546 ) 

2547 

2548 

2549def _transformation_move_handler( 

2550 _name: str, 

2551 parsed_data: TransformationMoveRuleSpec, 

2552 path: AttributePath, 

2553 _context: ParserContextData, 

2554) -> TransformationRule: 

2555 source_match = parsed_data["source"] 

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

2557 condition = parsed_data.get("when") 

2558 

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

2560 isinstance(source_match, ExactFileSystemPath) 

2561 and source_match.path == target_path 

2562 ): 

2563 raise ManifestParseException( 

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

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

2566 ) 

2567 return MoveTransformationRule( 

2568 source_match.match_rule, 

2569 target_path, 

2570 target_path.endswith("/"), 

2571 path, 

2572 condition, 

2573 ) 

2574 

2575 

2576def _transformation_remove_handler( 

2577 _name: str, 

2578 parsed_data: TransformationRemoveRuleSpec, 

2579 attribute_path: AttributePath, 

2580 _context: ParserContextData, 

2581) -> TransformationRule: 

2582 paths = parsed_data["paths"] 

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

2584 

2585 return RemoveTransformationRule( 

2586 [m.match_rule for m in paths], 

2587 keep_empty_parent_dirs, 

2588 attribute_path, 

2589 ) 

2590 

2591 

2592def _transformation_create_symlink( 

2593 _name: str, 

2594 parsed_data: CreateSymlinkRule, 

2595 attribute_path: AttributePath, 

2596 _context: ParserContextData, 

2597) -> TransformationRule: 

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

2599 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2600 "replacement_rule", 

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

2602 ) 

2603 try: 

2604 link_target = debian_policy_normalize_symlink_target( 

2605 link_dest, 

2606 parsed_data["target"].symlink_target, 

2607 ) 

2608 except ValueError as e: # pragma: no cover 

2609 raise AssertionError( 

2610 "Debian Policy normalization should not raise ValueError here" 

2611 ) from e 

2612 

2613 condition = parsed_data.get("when") 

2614 

2615 return CreateSymlinkPathTransformationRule( 

2616 link_target, 

2617 link_dest, 

2618 replacement_rule, 

2619 attribute_path, 

2620 condition, 

2621 ) 

2622 

2623 

2624def _transformation_path_metadata( 

2625 _name: str, 

2626 parsed_data: PathManifestRule, 

2627 attribute_path: AttributePath, 

2628 context: ParserContextData, 

2629) -> TransformationRule: 

2630 match_rules = parsed_data["paths"] 

2631 owner = parsed_data.get("owner") 

2632 group = parsed_data.get("group") 

2633 mode = parsed_data.get("mode") 

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

2635 capabilities = parsed_data.get("capabilities") 

2636 capability_mode = parsed_data.get("capability_mode") 

2637 cap: str | None = None 

2638 

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

2640 check_integration_mode( 

2641 attribute_path["capabilities"], 

2642 context, 

2643 _NOT_INTEGRATION_RRR, 

2644 ) 

2645 if capability_mode is None: 

2646 capability_mode = SymbolicMode.parse_filesystem_mode( 

2647 "a-s", 

2648 attribute_path["capability-mode"], 

2649 ) 

2650 cap = capabilities.value 

2651 validate_cap = check_cap_checker() 

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

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

2654 check_integration_mode( 

2655 attribute_path["capability_mode"], 

2656 context, 

2657 _NOT_INTEGRATION_RRR, 

2658 ) 

2659 raise ManifestParseException( 

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

2661 f" in {attribute_path.path}" 

2662 ) 

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

2664 raise ManifestParseException( 

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

2666 f" in {attribute_path.path}" 

2667 ) 

2668 condition = parsed_data.get("when") 

2669 

2670 return PathMetadataTransformationRule( 

2671 [m.match_rule for m in match_rules], 

2672 owner, 

2673 group, 

2674 mode, 

2675 recursive, 

2676 cap, 

2677 capability_mode, 

2678 attribute_path.path, 

2679 condition, 

2680 ) 

2681 

2682 

2683def _transformation_mkdirs( 

2684 _name: str, 

2685 parsed_data: EnsureDirectoryRule, 

2686 attribute_path: AttributePath, 

2687 _context: ParserContextData, 

2688) -> TransformationRule: 

2689 provided_paths = parsed_data["paths"] 

2690 owner = parsed_data.get("owner") 

2691 group = parsed_data.get("group") 

2692 mode = parsed_data.get("mode") 

2693 

2694 condition = parsed_data.get("when") 

2695 

2696 return CreateDirectoryTransformationRule( 

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

2698 owner, 

2699 group, 

2700 mode, 

2701 attribute_path.path, 

2702 condition, 

2703 ) 

2704 

2705 

2706def _at_least_two( 

2707 content: list[Any], 

2708 attribute_path: AttributePath, 

2709 attribute_name: str, 

2710) -> None: 

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

2712 raise ManifestParseException( 

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

2714 ) 

2715 

2716 

2717def _mc_any_of( 

2718 name: str, 

2719 parsed_data: MCAnyOfAllOf, 

2720 attribute_path: AttributePath, 

2721 _context: ParserContextData, 

2722) -> ManifestCondition: 

2723 conditions = parsed_data["conditions"] 

2724 _at_least_two(conditions, attribute_path, "conditions") 

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

2726 return ManifestCondition.any_of(conditions) 

2727 assert name == "all-of" 

2728 return ManifestCondition.all_of(conditions) 

2729 

2730 

2731def _mc_not( 

2732 _name: str, 

2733 parsed_data: MCNot, 

2734 _attribute_path: AttributePath, 

2735 _context: ParserContextData, 

2736) -> ManifestCondition: 

2737 condition = parsed_data["negated_condition"] 

2738 return condition.negated() 

2739 

2740 

2741def _extract_arch_matches( 

2742 parsed_data: MCArchMatches, 

2743 attribute_path: AttributePath, 

2744) -> list[str]: 

2745 arch_matches_as_str = parsed_data["arch_matches"] 

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

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

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

2749 # of each other. 

2750 arch_matches_as_list = arch_matches_as_str.split() 

2751 attr_path = attribute_path["arch_matches"] 

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

2753 raise ManifestParseException( 

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

2755 ) 

2756 

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

2758 "]" 

2759 ): 

2760 raise ManifestParseException( 

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

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

2763 ) 

2764 return arch_matches_as_list 

2765 

2766 

2767def _mc_source_context_arch_matches( 

2768 _name: str, 

2769 parsed_data: MCArchMatches, 

2770 attribute_path: AttributePath, 

2771 _context: ParserContextData, 

2772) -> ManifestCondition: 

2773 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2774 return SourceContextArchMatchManifestCondition(arch_matches) 

2775 

2776 

2777def _mc_package_context_arch_matches( 

2778 name: str, 

2779 parsed_data: MCArchMatches, 

2780 attribute_path: AttributePath, 

2781 context: ParserContextData, 

2782) -> ManifestCondition: 

2783 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2784 

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

2786 raise ManifestParseException( 

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

2788 ) 

2789 

2790 package_state = context.current_binary_package_state 

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

2792 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2793 "all", arch_matches 

2794 ) 

2795 attr_path = attribute_path["arch_matches"] 

2796 raise ManifestParseException( 

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

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

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

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

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

2802 ) 

2803 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2804 

2805 

2806def _mc_arch_matches( 

2807 name: str, 

2808 parsed_data: MCArchMatches, 

2809 attribute_path: AttributePath, 

2810 context: ParserContextData, 

2811) -> ManifestCondition: 

2812 if context.is_in_binary_package_state: 

2813 return _mc_package_context_arch_matches( 

2814 name, parsed_data, attribute_path, context 

2815 ) 

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

2817 

2818 

2819def _mc_build_profile_matches( 

2820 _name: str, 

2821 parsed_data: MCBuildProfileMatches, 

2822 attribute_path: AttributePath, 

2823 _context: ParserContextData, 

2824) -> ManifestCondition: 

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

2826 attr_path = attribute_path["build_profile_matches"] 

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

2828 raise ManifestParseException( 

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

2830 ) 

2831 try: 

2832 active_profiles_match(build_profile_spec, frozenset()) 

2833 except ValueError as e: 

2834 raise ManifestParseException( 

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

2836 ) 

2837 return BuildProfileMatch(build_profile_spec)