Coverage for src/debputy/plugin/debputy/private_api.py: 81%

551 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1import ctypes 

2import ctypes.util 

3import dataclasses 

4import functools 

5import textwrap 

6import time 

7from datetime import datetime 

8from typing import ( 

9 cast, 

10 NotRequired, 

11 Optional, 

12 Tuple, 

13 Union, 

14 Type, 

15 TypedDict, 

16 List, 

17 Annotated, 

18 Any, 

19 Dict, 

20 Callable, 

21) 

22 

23from debian.changelog import Changelog 

24from debian.deb822 import Deb822 

25 

26from debputy._manifest_constants import ( 

27 MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE, 

28 MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION, 

29 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

30 MK_INSTALLATIONS_INSTALL, 

31 MK_INSTALLATIONS_INSTALL_DOCS, 

32 MK_INSTALLATIONS_INSTALL_MAN, 

33 MK_INSTALLATIONS_DISCARD, 

34 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

35) 

36from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError 

37from debputy.installations import InstallRule 

38from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand 

39from debputy.manifest_conditions import ( 

40 ManifestCondition, 

41 BinaryPackageContextArchMatchManifestCondition, 

42 BuildProfileMatch, 

43 SourceContextArchMatchManifestCondition, 

44) 

45from debputy.manifest_parser.base_types import ( 

46 FileSystemMode, 

47 StaticFileSystemOwner, 

48 StaticFileSystemGroup, 

49 SymlinkTarget, 

50 FileSystemExactMatchRule, 

51 FileSystemMatchRule, 

52 SymbolicMode, 

53 OctalMode, 

54 FileSystemExactNonDirMatchRule, 

55 BuildEnvironmentDefinition, 

56 DebputyParsedContentStandardConditional, 

57) 

58from debputy.manifest_parser.exceptions import ManifestParseException 

59from debputy.manifest_parser.mapper_code import type_mapper_str2package 

60from debputy.manifest_parser.parse_hints import DebputyParseHint 

61from debputy.manifest_parser.parser_data import ParserContextData 

62from debputy.manifest_parser.tagging_types import ( 

63 DebputyParsedContent, 

64 TypeMapping, 

65) 

66from debputy.manifest_parser.util import AttributePath, check_integration_mode 

67from debputy.packages import BinaryPackage 

68from debputy.path_matcher import ExactFileSystemPath 

69from debputy.plugin.api import ( 

70 DebputyPluginInitializer, 

71 documented_attr, 

72 reference_documentation, 

73 VirtualPath, 

74 packager_provided_file_reference_documentation, 

75) 

76from debputy.plugin.api.impl import DebputyPluginInitializerProvider 

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

78from debputy.plugin.api.spec import ( 

79 type_mapping_reference_documentation, 

80 type_mapping_example, 

81 not_integrations, 

82 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

83) 

84from debputy.plugin.api.std_docs import docs_from 

85from debputy.plugin.debputy.binary_package_rules import register_binary_package_rules 

86from debputy.plugin.debputy.discard_rules import ( 

87 _debputy_discard_pyc_files, 

88 _debputy_prune_la_files, 

89 _debputy_prune_doxygen_cruft, 

90 _debputy_prune_binary_debian_dir, 

91 _debputy_prune_info_dir_file, 

92 _debputy_prune_backup_files, 

93 _debputy_prune_vcs_paths, 

94) 

95from debputy.plugin.debputy.manifest_root_rules import register_manifest_root_rules 

96from debputy.plugin.debputy.package_processors import ( 

97 process_manpages, 

98 apply_compression, 

99 clean_la_files, 

100) 

101from debputy.plugin.debputy.service_management import ( 

102 detect_systemd_service_files, 

103 generate_snippets_for_systemd_units, 

104 detect_sysv_init_service_files, 

105 generate_snippets_for_init_scripts, 

106) 

107from debputy.plugin.debputy.shlib_metadata_detectors import detect_shlibdeps 

108from debputy.plugin.debputy.strip_non_determinism import strip_non_determinism 

109from debputy.substitution import VariableContext 

110from debputy.transformation_rules import ( 

111 CreateSymlinkReplacementRule, 

112 TransformationRule, 

113 CreateDirectoryTransformationRule, 

114 RemoveTransformationRule, 

115 MoveTransformationRule, 

116 PathMetadataTransformationRule, 

117 CreateSymlinkPathTransformationRule, 

118) 

119from debputy.util import ( 

120 _normalize_path, 

121 PKGNAME_REGEX, 

122 PKGVERSION_REGEX, 

123 debian_policy_normalize_symlink_target, 

124 active_profiles_match, 

125 _error, 

126 _warn, 

127 _info, 

128 assume_not_none, 

129 manifest_format_doc, 

130) 

131 

132_DOCUMENTED_DPKG_ARCH_TYPES = { 

133 "HOST": ( 

134 "installed on", 

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

136 ), 

137 "BUILD": ( 

138 "compiled on", 

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

140 ), 

141 "TARGET": ( 

142 "cross-compiler output", 

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

144 ), 

145} 

146 

147_DOCUMENTED_DPKG_ARCH_VARS = { 

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

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

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

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

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

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

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

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

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

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

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

159} 

160 

161 

162_NOT_INTEGRATION_RRR = not_integrations(INTEGRATION_MODE_DH_DEBPUTY_RRR) 

163 

164 

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

166class Capability: 

167 value: str 

168 

169 @classmethod 

170 def parse( 

171 cls, 

172 raw_value: str, 

173 _attribute_path: AttributePath, 

174 _parser_context: "ParserContextData", 

175 ) -> "Capability": 

176 return cls(raw_value) 

177 

178 

179@functools.lru_cache 

180def load_libcap() -> Tuple[bool, Optional[str], Callable[[str], bool]]: 

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

182 has_libcap = False 

183 libcap = None 

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

185 try: 

186 libcap = ctypes.cdll.LoadLibrary(cap_library_path) 

187 has_libcap = True 

188 except OSError: 

189 pass 

190 

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

192 warned = False 

193 

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

195 nonlocal warned 

196 if not warned: 

197 _info( 

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

199 " checking of capabilities." 

200 ) 

201 warned = True 

202 return True 

203 

204 else: 

205 # cap_t cap_from_text(const char *path_p) 

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

207 libcap.cap_from_text.restype = ctypes.c_char_p 

208 

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

210 libcap.cap_free.restype = None 

211 

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

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

214 ok = cap_t is not None 

215 libcap.cap_free(cap_t) 

216 return ok 

217 

218 return has_libcap, cap_library_path, _is_valid_cap 

219 

220 

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

222 _, libcap_path, is_valid_cap = load_libcap() 

223 

224 seen_cap = set() 

225 

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

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

228 seen_cap.add(cap) 

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

230 _warn( 

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

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

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

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

235 ) 

236 

237 return _check_cap 

238 

239 

240def load_source_variables(variable_context: VariableContext) -> Dict[str, str]: 

241 try: 

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

243 if changelog is None: 

244 raise DebputyManifestVariableRequiresDebianDirError( 

245 "The changelog was not present" 

246 ) 

247 with changelog.open() as fd: 

248 dch = Changelog(fd, max_blocks=2) 

249 except FileNotFoundError as e: 

250 raise DebputyManifestVariableRequiresDebianDirError( 

251 "The changelog was not present" 

252 ) from e 

253 first_entry = dch[0] 

254 first_non_binnmu_entry = dch[0] 

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

256 first_non_binnmu_entry = dch[1] 

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

258 source_version = first_entry.version 

259 epoch = source_version.epoch 

260 upstream_version = source_version.upstream_version 

261 debian_revision = source_version.debian_revision 

262 epoch_upstream = upstream_version 

263 upstream_debian_revision = upstream_version 

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

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

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

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

268 

269 package = first_entry.package 

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

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

272 

273 date = first_entry.date 

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

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

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

277 else: 

278 _warn( 

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

280 " for SOURCE_DATE_EPOCH" 

281 ) 

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

283 

284 if first_non_binnmu_entry is not first_entry: 

285 non_binnmu_date = first_non_binnmu_entry.date 

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

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

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

289 else: 

290 _warn( 

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

292 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)" 

293 ) 

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

295 else: 

296 snd_source_date_epoch = source_date_epoch 

297 return { 

298 "DEB_SOURCE": package, 

299 "DEB_VERSION": source_version.full_version, 

300 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream, 

301 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision, 

302 "DEB_VERSION_UPSTREAM": upstream_version, 

303 "SOURCE_DATE_EPOCH": source_date_epoch, 

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

305 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch, 

306 } 

307 

308 

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

310 api = cast("DebputyPluginInitializerProvider", public_api) 

311 

312 api.metadata_or_maintscript_detector( 

313 "dpkg-shlibdeps", 

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

315 cast("MetadataAutoDetector", detect_shlibdeps), 

316 package_type={"deb", "udeb"}, 

317 ) 

318 register_type_mappings(api) 

319 register_variables_via_private_api(api) 

320 document_builtin_variables(api) 

321 register_automatic_discard_rules(api) 

322 register_special_ppfs(api) 

323 register_install_rules(api) 

324 register_transformation_rules(api) 

325 register_manifest_condition_rules(api) 

326 register_dpkg_conffile_rules(api) 

327 register_processing_steps(api) 

328 register_service_managers(api) 

329 register_manifest_root_rules(api) 

330 register_binary_package_rules(api) 

331 

332 

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

334 api.register_mapped_type( 

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

336 reference_documentation=type_mapping_reference_documentation( 

337 description=textwrap.dedent( 

338 """\ 

339 A Linux capability 

340 

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

342 

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

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

345 currently never emit hard errors for unknown capabilities. 

346 """, 

347 ), 

348 examples=[ 

349 type_mapping_example("cap_chown=p"), 

350 type_mapping_example("cap_chown=ep"), 

351 type_mapping_example("cap_kill-pe"), 

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

353 ], 

354 ), 

355 ) 

356 api.register_mapped_type( 

357 TypeMapping( 

358 FileSystemMatchRule, 

359 str, 

360 FileSystemMatchRule.parse_path_match, 

361 ), 

362 reference_documentation=type_mapping_reference_documentation( 

363 description=textwrap.dedent( 

364 """\ 

365 A generic file system path match with globs. 

366 

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

368 

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

370 

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

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

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

374 

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

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

377 

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

379 or relevant search directories will match. 

380 

381 Please keep in mind that: 

382 

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

384 an anchor reference. 

385 

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

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

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

389 directories (similar to the shell). 

390 

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

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

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

394 """, 

395 ), 

396 examples=[ 

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

398 type_mapping_example("*.txt"), 

399 type_mapping_example("**/foo"), 

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

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

402 ], 

403 ), 

404 ) 

405 

406 api.register_mapped_type( 

407 TypeMapping( 

408 FileSystemExactMatchRule, 

409 str, 

410 FileSystemExactMatchRule.parse_path_match, 

411 ), 

412 reference_documentation=type_mapping_reference_documentation( 

413 description=textwrap.dedent( 

414 """\ 

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

416 

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

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

419 

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

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

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

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

424 """, 

425 ), 

426 examples=[ 

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

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

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

430 ], 

431 ), 

432 ) 

433 

434 api.register_mapped_type( 

435 TypeMapping( 

436 FileSystemExactNonDirMatchRule, 

437 str, 

438 FileSystemExactNonDirMatchRule.parse_path_match, 

439 ), 

440 reference_documentation=type_mapping_reference_documentation( 

441 description=textwrap.dedent( 

442 f"""\ 

443 \ 

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

445 

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

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

448 

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

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

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

452 with a "/". 

453 """, 

454 ), 

455 examples=[ 

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

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

458 ], 

459 ), 

460 ) 

461 

462 api.register_mapped_type( 

463 TypeMapping( 

464 SymlinkTarget, 

465 str, 

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

467 v, ap, assume_not_none(pc).substitution 

468 ), 

469 ), 

470 reference_documentation=type_mapping_reference_documentation( 

471 description=textwrap.dedent( 

472 """\ 

473 A symlink target. 

474 

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

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

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

478 

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

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

481 """, 

482 ), 

483 examples=[ 

484 type_mapping_example("../foo"), 

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

486 ], 

487 ), 

488 ) 

489 

490 api.register_mapped_type( 

491 TypeMapping( 

492 StaticFileSystemOwner, 

493 Union[int, str], 

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

495 ), 

496 reference_documentation=type_mapping_reference_documentation( 

497 description=textwrap.dedent( 

498 """\ 

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

500 

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

502 

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

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

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

506 

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

508 

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

510 """ 

511 ), 

512 examples=[ 

513 type_mapping_example("root"), 

514 type_mapping_example(0), 

515 type_mapping_example("root:0"), 

516 type_mapping_example("bin"), 

517 ], 

518 ), 

519 ) 

520 api.register_mapped_type( 

521 TypeMapping( 

522 StaticFileSystemGroup, 

523 Union[int, str], 

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

525 ), 

526 reference_documentation=type_mapping_reference_documentation( 

527 description=textwrap.dedent( 

528 """\ 

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

530 

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

532 

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

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

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

536 

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

538 

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

540 """ 

541 ), 

542 examples=[ 

543 type_mapping_example("root"), 

544 type_mapping_example(0), 

545 type_mapping_example("root:0"), 

546 type_mapping_example("tty"), 

547 ], 

548 ), 

549 ) 

550 

551 api.register_mapped_type( 

552 TypeMapping( 

553 BinaryPackage, 

554 str, 

555 type_mapper_str2package, 

556 ), 

557 reference_documentation=type_mapping_reference_documentation( 

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

559 ), 

560 ) 

561 

562 api.register_mapped_type( 

563 TypeMapping( 

564 FileSystemMode, 

565 str, 

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

567 ), 

568 reference_documentation=type_mapping_reference_documentation( 

569 description="Either an octal mode or symbolic mode", 

570 examples=[ 

571 type_mapping_example("a+x"), 

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

573 type_mapping_example("0755"), 

574 ], 

575 ), 

576 ) 576 ↛ exitline 576 didn't jump to the function exit

577 api.register_mapped_type( 

578 TypeMapping( 

579 OctalMode, 

580 str, 

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

582 ), 

583 reference_documentation=type_mapping_reference_documentation( 

584 description="An octal mode. Must always be a string.", 

585 examples=[ 

586 type_mapping_example("0644"), 

587 type_mapping_example("0755"), 

588 ], 

589 ), 

590 ) 

591 api.register_mapped_type( 

592 TypeMapping( 

593 BuildEnvironmentDefinition, 

594 str, 

595 lambda v, ap, pc: pc.resolve_build_environment(v, ap), 

596 ), 

597 reference_documentation=type_mapping_reference_documentation( 

598 description="Reference to an build environment defined in `build-environments`", 

599 ), 

600 ) 

601 

602 

603def register_service_managers( 

604 api: DebputyPluginInitializerProvider, 

605) -> None: 

606 api.service_provider( 

607 "systemd", 

608 detect_systemd_service_files, 

609 generate_snippets_for_systemd_units, 

610 ) 

611 api.service_provider( 

612 "sysvinit", 

613 detect_sysv_init_service_files, 

614 generate_snippets_for_init_scripts, 

615 ) 

616 

617 

618def register_automatic_discard_rules( 

619 api: DebputyPluginInitializerProvider, 

620) -> None: 

621 api.automatic_discard_rule( 

622 "python-cache-files", 

623 _debputy_discard_pyc_files, 

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

625 examples=automatic_discard_rule_example( 

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

627 ".../__pycache__/", 

628 ".../__pycache__/...", 

629 ".../foo.pyc", 

630 ".../foo.pyo", 

631 ), 

632 ) 

633 api.automatic_discard_rule( 

634 "la-files", 

635 _debputy_prune_la_files, 

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

637 examples=automatic_discard_rule_example( 

638 "usr/lib/libfoo.la", 

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

640 ), 

641 ) 

642 api.automatic_discard_rule( 

643 "backup-files", 

644 _debputy_prune_backup_files, 

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

646 examples=( 

647 automatic_discard_rule_example( 

648 ".../foo~", 

649 ".../foo.orig", 

650 ".../foo.rej", 

651 ".../DEADJOE", 

652 ".../.foo.sw.", 

653 ), 

654 ), 

655 ) 

656 api.automatic_discard_rule( 

657 "version-control-paths", 

658 _debputy_prune_vcs_paths, 

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

660 examples=automatic_discard_rule_example( 

661 ("tools/foo", False), 

662 ".../CVS/", 

663 ".../CVS/...", 

664 ".../.gitignore", 

665 ".../.gitattributes", 

666 ".../.git/", 

667 ".../.git/...", 

668 ), 

669 ) 

670 api.automatic_discard_rule( 

671 "gnu-info-dir-file", 

672 _debputy_prune_info_dir_file, 

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

674 examples=automatic_discard_rule_example( 

675 "usr/share/info/dir", 

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

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

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

679 ), 

680 ) 

681 api.automatic_discard_rule( 

682 "debian-dir", 

683 _debputy_prune_binary_debian_dir, 

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

685 " literally in the file listing", 

686 examples=( 

687 automatic_discard_rule_example( 

688 "DEBIAN/", 

689 "DEBIAN/control", 

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

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

692 ), 

693 ), 

694 ) 

695 api.automatic_discard_rule( 

696 "doxygen-cruft-files", 

697 _debputy_prune_doxygen_cruft, 

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

699 examples=automatic_discard_rule_example( 

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

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

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

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

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

705 ), 

706 ) 

707 

708 

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

710 api.package_processor("manpages", process_manpages) 

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

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

713 api.package_processor( 

714 "strip-nondeterminism", 

715 cast("Any", strip_non_determinism), 

716 depends_on_processor=["manpages"], 

717 ) 

718 api.package_processor( 

719 "compression", 

720 apply_compression, 

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

722 ) 

723 

724 

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

726 api.manifest_variable_provider( 

727 load_source_variables, 

728 { 

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

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

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

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

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

734 "SOURCE_DATE_EPOCH": textwrap.dedent( 

735 """\ 

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

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

738 this variable. 

739 """ 

740 ), 

741 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

742 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

743 }, 

744 ) 

745 

746 

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

748 api.document_builtin_variable( 

749 "PACKAGE", 

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

751 is_context_specific=True, 

752 ) 

753 

754 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

755 

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

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

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

759 documentation = textwrap.dedent( 

760 f"""\ 

761 \ 

762 {arch_var_doc} ({arch_type_tag}) 

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

764 * Machine type: {arch_type_doc} 

765 * Value description: {arch_var_doc} 

766 

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

768 """ 

769 ) 

770 api.document_builtin_variable( 

771 full_var, 

772 documentation, 

773 is_for_special_case=arch_type != "HOST", 

774 ) 

775 

776 

777def _format_docbase_filename( 

778 path_format: str, 

779 format_param: PPFFormatParam, 

780 docbase_file: VirtualPath, 

781) -> str: 

782 with docbase_file.open() as fd: 

783 content = Deb822(fd) 783 ↛ 786line 783 didn't jump to line 786 because the condition on line 783 was always true

784 proper_name = content["Document"] 

785 if proper_name is not None: 

786 format_param["name"] = proper_name 

787 else: 

788 _warn( 

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

790 ) 

791 return path_format.format(**format_param) 

792 

793 

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

795 api.packager_provided_file( 

796 "doc-base", 

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

798 format_callback=_format_docbase_filename, 

799 ) 

800 

801 api.packager_provided_file( 

802 "shlibs", 

803 "DEBIAN/shlibs", 

804 allow_name_segment=False, 

805 reservation_only=True, 

806 reference_documentation=packager_provided_file_reference_documentation( 

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

808 ), 

809 ) 

810 api.packager_provided_file( 

811 "symbols", 

812 "DEBIAN/symbols", 

813 allow_name_segment=False, 

814 allow_architecture_segment=True, 

815 reservation_only=True, 

816 reference_documentation=packager_provided_file_reference_documentation( 

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

818 ), 

819 ) 

820 api.packager_provided_file( 

821 "templates", 

822 "DEBIAN/templates", 

823 allow_name_segment=False, 

824 allow_architecture_segment=False, 

825 reservation_only=True, 

826 ) 

827 api.packager_provided_file( 

828 "alternatives", 

829 "DEBIAN/alternatives", 

830 allow_name_segment=False, 

831 allow_architecture_segment=True, 

832 reservation_only=True, 

833 ) 

834 

835 

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

837 api.pluggable_manifest_rule( 

838 InstallRule, 

839 MK_INSTALLATIONS_INSTALL, 

840 ParsedInstallRule, 

841 _install_rule_handler, 

842 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

843 inline_reference_documentation=reference_documentation( 

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

845 description=textwrap.dedent( 

846 """\ 

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

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

849 

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

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

852 

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

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

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

856 """.format( 

857 MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL 

858 ) 

859 ), 

860 non_mapping_description=textwrap.dedent( 

861 """\ 

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

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

864 not required. 

865 """ 

866 ), 

867 attributes=[ 

868 documented_attr( 

869 ["source", "sources"], 

870 textwrap.dedent( 

871 """\ 

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

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

874 is tried against default search directories. 

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

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

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

878 """ 

879 ), 

880 ), 

881 documented_attr( 

882 "dest_dir", 

883 textwrap.dedent( 

884 """\ 

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

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

887 to the directory name of the `source`. 

888 """ 

889 ), 

890 ), 

891 documented_attr( 

892 "into", 

893 textwrap.dedent( 

894 """\ 

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

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

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

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

899 """ 

900 ), 

901 ), 

902 documented_attr( 

903 "install_as", 

904 textwrap.dedent( 

905 """\ 

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

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

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

909 """ 

910 ), 

911 ), 

912 *docs_from(DebputyParsedContentStandardConditional), 

913 ], 

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

915 ), 

916 ) 

917 api.pluggable_manifest_rule( 

918 InstallRule, 

919 [ 

920 MK_INSTALLATIONS_INSTALL_DOCS, 

921 "install-doc", 

922 ], 

923 ParsedInstallRule, 

924 _install_docs_rule_handler, 

925 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

926 inline_reference_documentation=reference_documentation( 

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

928 description=textwrap.dedent( 

929 """\ 

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

931 `install` rule with the following key features: 

932 

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

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

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

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

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

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

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

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

941 package listed in `debian/control`. 

942 

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

944 

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

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

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

948 use-case. 

949 """ 

950 ), 

951 non_mapping_description=textwrap.dedent( 

952 """\ 

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

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

955 not required. 

956 """ 

957 ), 

958 attributes=[ 

959 documented_attr( 

960 ["source", "sources"], 

961 textwrap.dedent( 

962 """\ 

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

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

965 is tried against default search directories. 

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

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

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

969 

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

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

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

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

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

975 """ 

976 ), 

977 ), 

978 documented_attr( 

979 "dest_dir", 

980 textwrap.dedent( 

981 """\ 

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

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

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

985 """ 

986 ), 

987 ), 

988 documented_attr( 

989 "into", 

990 textwrap.dedent( 

991 """\ 

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

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

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

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

996 the key is required. 

997 """ 

998 ), 

999 ), 

1000 documented_attr( 

1001 "install_as", 

1002 textwrap.dedent( 

1003 """\ 

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

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

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

1007 """ 

1008 ), 

1009 ), 

1010 documented_attr( 

1011 "when", 

1012 textwrap.dedent( 

1013 """\ 

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

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

1016 (rather than replacing it). 

1017 """ 

1018 ), 

1019 ), 

1020 ], 

1021 reference_documentation_url=manifest_format_doc( 

1022 "install-documentation-install-docs" 

1023 ), 

1024 ), 

1025 ) 

1026 api.pluggable_manifest_rule( 

1027 InstallRule, 

1028 [ 

1029 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

1030 "install-example", 

1031 ], 

1032 ParsedInstallExamplesRule, 

1033 _install_examples_rule_handler, 

1034 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

1035 inline_reference_documentation=reference_documentation( 

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

1037 description=textwrap.dedent( 

1038 """\ 

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

1040 install` rule with the following key features: 

1041 

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

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

1044 dir. 

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

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

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

1048 package listed in `debian/control`. 

1049 

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

1051 """ 

1052 ), 

1053 non_mapping_description=textwrap.dedent( 

1054 """\ 

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

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

1057 not required. 

1058 """ 

1059 ), 

1060 attributes=[ 

1061 documented_attr( 

1062 ["source", "sources"], 

1063 textwrap.dedent( 

1064 """\ 

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

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

1067 is tried against default search directories. 

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

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

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

1071 

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

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

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

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

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

1077 """ 

1078 ), 

1079 ), 

1080 documented_attr( 

1081 "into", 

1082 textwrap.dedent( 

1083 """\ 

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

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

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

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

1088 Otherwise, the key is required. 

1089 """ 

1090 ), 

1091 ), 

1092 documented_attr( 

1093 "when", 

1094 textwrap.dedent( 

1095 """\ 

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

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

1098 (rather than replacing it). 

1099 """ 

1100 ), 

1101 ), 

1102 ], 

1103 reference_documentation_url=manifest_format_doc( 

1104 "install-examples-install-examples" 

1105 ), 

1106 ), 

1107 ) 

1108 api.pluggable_manifest_rule( 

1109 InstallRule, 

1110 MK_INSTALLATIONS_INSTALL_MAN, 

1111 ParsedInstallManpageRule, 

1112 _install_man_rule_handler, 

1113 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1114 inline_reference_documentation=reference_documentation( 

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

1116 description=textwrap.dedent( 

1117 """\ 

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

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

1120 

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

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

1123 language. 

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

1125 package listed in `debian/control`. 

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

1127 for when the auto-detection is insufficient. 

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

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

1130 

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

1132 """ 

1133 ), 

1134 non_mapping_description=textwrap.dedent( 

1135 """\ 

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

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

1138 not required. 

1139 """ 

1140 ), 

1141 attributes=[ 

1142 documented_attr( 

1143 ["source", "sources"], 

1144 textwrap.dedent( 

1145 """\ 

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

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

1148 is tried against default search directories. 

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

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

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

1152 """ 

1153 ), 

1154 ), 

1155 documented_attr( 

1156 "into", 

1157 textwrap.dedent( 

1158 """\ 

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

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

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

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

1163 """ 

1164 ), 

1165 ), 

1166 documented_attr( 

1167 "section", 

1168 textwrap.dedent( 

1169 """\ 

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

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

1172 have performed. 

1173 """ 

1174 ), 

1175 ), 

1176 documented_attr( 

1177 "language", 

1178 textwrap.dedent( 

1179 """\ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1197 always assumed to be a language. 

1198 """ 

1199 ), 

1200 ), 

1201 *docs_from(DebputyParsedContentStandardConditional), 

1202 ], 

1203 reference_documentation_url=manifest_format_doc( 

1204 "install-manpages-install-man" 

1205 ), 

1206 ), 

1207 ) 

1208 api.pluggable_manifest_rule( 

1209 InstallRule, 

1210 MK_INSTALLATIONS_DISCARD, 

1211 ParsedInstallDiscardRule, 

1212 _install_discard_rule_handler, 

1213 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1214 inline_reference_documentation=reference_documentation( 

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

1216 description=textwrap.dedent( 

1217 """\ 

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

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

1220 

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

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

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

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

1225 """ 

1226 ), 

1227 non_mapping_description=textwrap.dedent( 

1228 """\ 

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

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

1231 """ 

1232 ), 

1233 attributes=[ 

1234 documented_attr( 

1235 ["path", "paths"], 

1236 textwrap.dedent( 

1237 """\ 

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

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

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

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

1242 contents that have not already been installed somewhere. 

1243 """ 

1244 ), 

1245 ), 

1246 documented_attr( 

1247 ["search_dir", "search_dirs"], 

1248 textwrap.dedent( 

1249 """\ 

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

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

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

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

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

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

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

1257 will make `debputy` report an error. 

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

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

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

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

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

1263 applicable to certain builds that are only performed conditionally. 

1264 """ 

1265 ), 

1266 ), 

1267 documented_attr( 

1268 "required_when", 

1269 textwrap.dedent( 

1270 """\ 

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

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

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

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

1275 """ 

1276 ), 

1277 ), 

1278 ], 

1279 reference_documentation_url=manifest_format_doc( 

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

1281 ), 

1282 ), 

1283 ) 

1284 api.pluggable_manifest_rule( 

1285 InstallRule, 

1286 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1287 ParsedMultiDestInstallRule, 

1288 _multi_dest_install_rule_handler, 

1289 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1290 inline_reference_documentation=reference_documentation( 

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

1292 description=textwrap.dedent( 

1293 """\ 

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

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

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

1297 "primary" uses. 

1298 

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

1300 except you list 2+ destination directories. 

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

1302 2+ `as` names. 

1303 """ 

1304 ), 

1305 attributes=[ 

1306 documented_attr( 

1307 ["source", "sources"], 

1308 textwrap.dedent( 

1309 """\ 

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

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

1312 is tried against default search directories. 

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

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

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

1316 """ 

1317 ), 

1318 ), 

1319 documented_attr( 

1320 "dest_dirs", 

1321 textwrap.dedent( 

1322 """\ 

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

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

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

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

1327 """ 

1328 ), 

1329 ), 

1330 documented_attr( 

1331 "into", 

1332 textwrap.dedent( 

1333 """\ 

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

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

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

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

1338 """ 

1339 ), 

1340 ), 

1341 documented_attr( 

1342 "install_as", 

1343 textwrap.dedent( 

1344 """\ 

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

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

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

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

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

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

1351 """ 

1352 ), 

1353 ), 

1354 *docs_from(DebputyParsedContentStandardConditional), 

1355 ], 

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

1357 ), 

1358 ) 

1359 

1360 

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

1362 api.pluggable_manifest_rule( 

1363 TransformationRule, 

1364 "move", 

1365 TransformationMoveRuleSpec, 

1366 _transformation_move_handler, 

1367 inline_reference_documentation=reference_documentation( 

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

1369 description=textwrap.dedent( 

1370 """\ 

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

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

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

1374 Debian specific requirements. 

1375 

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

1377 `mv` command line tool. 

1378 """ 

1379 ), 

1380 attributes=[ 

1381 documented_attr( 

1382 "source", 

1383 textwrap.dedent( 

1384 """\ 

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

1386 and substitutions. 

1387 """ 

1388 ), 

1389 ), 

1390 documented_attr( 

1391 "target", 

1392 textwrap.dedent( 

1393 """\ 

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

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

1396 the target will *always* be a directory. 

1397 """ 

1398 ), 

1399 ), 

1400 *docs_from(DebputyParsedContentStandardConditional), 

1401 ], 

1402 reference_documentation_url=manifest_format_doc( 

1403 "move-transformation-rule-move" 

1404 ), 

1405 ), 

1406 ) 

1407 api.pluggable_manifest_rule( 

1408 TransformationRule, 

1409 "remove", 

1410 TransformationRemoveRuleSpec, 

1411 _transformation_remove_handler, 

1412 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1413 inline_reference_documentation=reference_documentation( 

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

1415 description=textwrap.dedent( 

1416 """\ 

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

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

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

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

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

1422 of `debian/copyright`). 

1423 

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

1425 the `remove` transformation rule. 

1426 

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

1428 """ 

1429 ), 

1430 non_mapping_description=textwrap.dedent( 

1431 """\ 

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

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

1434 """ 

1435 ), 

1436 attributes=[ 

1437 documented_attr( 

1438 ["path", "paths"], 

1439 textwrap.dedent( 

1440 """\ 

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

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

1443 can use globs. 

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

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

1446 along with all the contents. 

1447 """ 

1448 ), 

1449 ), 

1450 documented_attr( 

1451 "keep_empty_parent_dirs", 

1452 textwrap.dedent( 

1453 """\ 

1454 A boolean determining whether to prune parent directories that become 

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

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

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

1458 """ 

1459 ), 

1460 ), 

1461 documented_attr( 

1462 "when", 

1463 textwrap.dedent( 

1464 """\ 

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

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

1467 (rather than replacing it). 

1468 """ 

1469 ), 

1470 ), 

1471 ], 

1472 reference_documentation_url=manifest_format_doc( 

1473 "remove-transformation-rule-remove" 

1474 ), 

1475 ), 

1476 ) 

1477 api.pluggable_manifest_rule( 

1478 TransformationRule, 

1479 "create-symlink", 

1480 CreateSymlinkRule, 

1481 _transformation_create_symlink, 

1482 inline_reference_documentation=reference_documentation( 

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

1484 description=textwrap.dedent( 

1485 """\ 

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

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

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

1489 """ 

1490 ), 

1491 attributes=[ 

1492 documented_attr( 

1493 "path", 

1494 textwrap.dedent( 

1495 """\ 

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

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

1498 Parent directories are implicitly created as necessary. 

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

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

1501 """ 

1502 ), 

1503 ), 

1504 documented_attr( 

1505 "target", 

1506 textwrap.dedent( 

1507 """\ 

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

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

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

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

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

1513 preference. 

1514 """ 

1515 ), 

1516 ), 

1517 documented_attr( 

1518 "replacement_rule", 

1519 textwrap.dedent( 

1520 """\ 

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

1522 be set to one of the following values: 

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

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

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

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

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

1528 similar to `ln -sf` semantics. 

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

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

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

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

1533 will stop with an error. 

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

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

1536 be removed recursively along with the directory. Finally, 

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

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

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

1540 `when` if any). 

1541 

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

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

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

1545 the value in `replacement-rule`. 

1546 """ 

1547 ), 

1548 ), 

1549 *docs_from(DebputyParsedContentStandardConditional), 

1550 ], 

1551 reference_documentation_url=manifest_format_doc( 

1552 "create-symlinks-transformation-rule-create-symlink" 

1553 ), 

1554 ), 

1555 ) 

1556 api.pluggable_manifest_rule( 

1557 TransformationRule, 

1558 "path-metadata", 

1559 PathManifestRule, 

1560 _transformation_path_metadata, 

1561 source_format=PathManifestSourceDictFormat, 

1562 inline_reference_documentation=reference_documentation( 

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

1564 description=textwrap.dedent( 

1565 """\ 

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

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

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

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

1570 transformation. 

1571 

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

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

1574 """ 

1575 ), 

1576 attributes=[ 

1577 documented_attr( 

1578 ["path", "paths"], 

1579 textwrap.dedent( 

1580 """\ 

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

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

1583 and substitution variables. Special-rules for matches: 

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

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

1586 """ 

1587 ), 

1588 ), 

1589 documented_attr( 

1590 "owner", 

1591 textwrap.dedent( 

1592 """\ 

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

1594 no change of owner is done. 

1595 """ 

1596 ), 

1597 ), 

1598 documented_attr( 

1599 "group", 

1600 textwrap.dedent( 

1601 """\ 

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

1603 no change of group is done. 

1604 """ 

1605 ), 

1606 ), 

1607 documented_attr( 

1608 "mode", 

1609 textwrap.dedent( 

1610 """\ 

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

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

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

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

1615 relative to the matched path's current mode. 

1616 """ 

1617 ), 

1618 ), 

1619 documented_attr( 

1620 "capabilities", 

1621 textwrap.dedent( 

1622 """\ 

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

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

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

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

1627 run. 

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

1629 to those paths. 

1630 

1631 """ 

1632 ), 

1633 ), 

1634 documented_attr( 

1635 "capability_mode", 

1636 textwrap.dedent( 

1637 """\ 

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

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

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

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

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

1643 `capabilities` is omitted. 

1644 """ 

1645 ), 

1646 ), 

1647 documented_attr( 

1648 "recursive", 

1649 textwrap.dedent( 

1650 """\ 

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

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

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

1654 this attribute is `false`. 

1655 """ 

1656 ), 

1657 ), 

1658 *docs_from(DebputyParsedContentStandardConditional), 

1659 ], 

1660 reference_documentation_url=manifest_format_doc( 

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

1662 ), 

1663 ), 

1664 ) 

1665 api.pluggable_manifest_rule( 

1666 TransformationRule, 

1667 "create-directories", 

1668 EnsureDirectoryRule, 

1669 _transformation_mkdirs, 

1670 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1671 inline_reference_documentation=reference_documentation( 

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

1673 description=textwrap.dedent( 

1674 """\ 

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

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

1677 transformations will create directories as required. 

1678 

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

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

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

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

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

1684 

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

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

1687 """ 

1688 ), 

1689 non_mapping_description=textwrap.dedent( 

1690 """\ 

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

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

1693 """ 

1694 ), 

1695 attributes=[ 

1696 documented_attr( 

1697 ["path", "paths"], 

1698 textwrap.dedent( 

1699 """\ 

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

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

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

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

1704 are affected by the owner/mode options) 

1705 """ 

1706 ), 

1707 ), 

1708 documented_attr( 

1709 "owner", 

1710 textwrap.dedent( 

1711 """\ 

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

1713 Default is "root". 

1714 """ 

1715 ), 

1716 ), 

1717 documented_attr( 

1718 "group", 

1719 textwrap.dedent( 

1720 """\ 

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

1722 Default is "root". 

1723 """ 

1724 ), 

1725 ), 

1726 documented_attr( 

1727 "mode", 

1728 textwrap.dedent( 

1729 """\ 

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

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

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

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

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

1735 transformation. The default is "0755". 

1736 """ 

1737 ), 

1738 ), 

1739 *docs_from(DebputyParsedContentStandardConditional), 

1740 ], 

1741 reference_documentation_url=manifest_format_doc( 

1742 "create-directories-transformation-rule-directories" 

1743 ), 

1744 ), 

1745 ) 

1746 

1747 1747 ↛ exitline 1747 didn't jump to the function exit

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

1749 api.provide_manifest_keyword( 

1750 ManifestCondition, 

1751 "cross-compiling", 

1752 lambda *_: ManifestCondition.is_cross_building(), 1752 ↛ exitline 1752 didn't jump to the function exit

1753 ) 

1754 api.provide_manifest_keyword( 

1755 ManifestCondition, 

1756 "can-execute-compiled-binaries", 

1757 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 1757 ↛ exitline 1757 didn't jump to the function exit

1758 ) 

1759 api.provide_manifest_keyword( 

1760 ManifestCondition, 

1761 "run-build-time-tests", 

1762 lambda *_: ManifestCondition.run_build_time_tests(), 

1763 ) 

1764 

1765 api.pluggable_manifest_rule( 

1766 ManifestCondition, 

1767 "not", 

1768 MCNot, 

1769 _mc_not, 

1770 ) 

1771 api.pluggable_manifest_rule( 

1772 ManifestCondition, 

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

1774 MCAnyOfAllOf, 

1775 _mc_any_of, 

1776 source_format=List[ManifestCondition], 

1777 ) 

1778 api.pluggable_manifest_rule( 

1779 ManifestCondition, 

1780 "arch-matches", 

1781 MCArchMatches, 

1782 _mc_arch_matches, 

1783 source_format=str, 

1784 inline_reference_documentation=reference_documentation( 

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

1786 description=textwrap.dedent( 

1787 """\ 

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

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

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

1791 and practically behaves like a comparison against 

1792 `dpkg-architecture -qDEB_HOST_ARCH`. 

1793 

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

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

1796 in the context of a binary package and like 

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

1798 are covered in their own keywords. 

1799 """ 

1800 ), 

1801 non_mapping_description=textwrap.dedent( 

1802 """\ 

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

1804 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1809 have it. 

1810 """ 

1811 ), 

1812 reference_documentation_url=manifest_format_doc( 

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

1814 ), 

1815 ), 

1816 ) 

1817 

1818 context_arch_doc = reference_documentation( 

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

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

1821 description=textwrap.dedent( 

1822 """\ 

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

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

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

1826 `arch-matches` condition. 

1827 

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

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

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

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

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

1833 to the packager that condition does not make sense. 

1834 

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

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

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

1838 very special cases). 

1839 

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

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

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

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

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

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

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

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

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

1849 need to care about nor use any of this. 

1850 

1851 Accordingly, the possible conditions are: 

1852 

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

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

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

1856 

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

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

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

1860 

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

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

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

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

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

1866 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1867 

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

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

1870 

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

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

1873 """ 

1874 ), 

1875 non_mapping_description=textwrap.dedent( 

1876 """\ 

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

1878 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1883 have it. 

1884 """ 

1885 ), 

1886 ) 

1887 

1888 api.pluggable_manifest_rule( 

1889 ManifestCondition, 

1890 "source-context-arch-matches", 

1891 MCArchMatches, 

1892 _mc_source_context_arch_matches, 

1893 source_format=str, 

1894 inline_reference_documentation=context_arch_doc, 

1895 ) 

1896 api.pluggable_manifest_rule( 

1897 ManifestCondition, 

1898 "package-context-arch-matches", 

1899 MCArchMatches, 

1900 _mc_arch_matches, 

1901 source_format=str, 

1902 inline_reference_documentation=context_arch_doc, 

1903 ) 

1904 api.pluggable_manifest_rule( 

1905 ManifestCondition, 

1906 "build-profiles-matches", 

1907 MCBuildProfileMatches, 

1908 _mc_build_profile_matches, 

1909 source_format=str, 

1910 ) 

1911 

1912 

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

1914 api.pluggable_manifest_rule( 

1915 DpkgMaintscriptHelperCommand, 

1916 "remove", 

1917 DpkgRemoveConffileRule, 

1918 _dpkg_conffile_remove, 

1919 inline_reference_documentation=None, # TODO: write and add 

1920 ) 

1921 

1922 api.pluggable_manifest_rule( 

1923 DpkgMaintscriptHelperCommand, 

1924 "rename", 

1925 DpkgRenameConffileRule, 

1926 _dpkg_conffile_rename, 

1927 inline_reference_documentation=None, # TODO: write and add 

1928 ) 

1929 

1930 

1931class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

1932 mode: NotRequired[FileSystemMode] 

1933 owner: NotRequired[StaticFileSystemOwner] 

1934 group: NotRequired[StaticFileSystemGroup] 

1935 

1936 

1937class PathManifestSourceDictFormat(_ModeOwnerBase): 

1938 path: NotRequired[ 

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

1940 ] 

1941 paths: NotRequired[List[FileSystemMatchRule]] 

1942 recursive: NotRequired[bool] 

1943 capabilities: NotRequired[Capability] 

1944 capability_mode: NotRequired[FileSystemMode] 

1945 

1946 

1947class PathManifestRule(_ModeOwnerBase): 

1948 paths: List[FileSystemMatchRule] 

1949 recursive: NotRequired[bool] 

1950 capabilities: NotRequired[Capability] 

1951 capability_mode: NotRequired[FileSystemMode] 

1952 

1953 

1954class EnsureDirectorySourceFormat(_ModeOwnerBase): 

1955 path: NotRequired[ 

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

1957 ] 

1958 paths: NotRequired[List[FileSystemExactMatchRule]] 

1959 

1960 

1961class EnsureDirectoryRule(_ModeOwnerBase): 

1962 paths: List[FileSystemExactMatchRule] 

1963 

1964 

1965class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

1966 path: FileSystemExactMatchRule 

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

1968 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

1969 

1970 

1971class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

1972 source: FileSystemMatchRule 

1973 target: FileSystemExactMatchRule 

1974 

1975 

1976class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

1977 paths: List[FileSystemMatchRule] 

1978 keep_empty_parent_dirs: NotRequired[bool] 

1979 

1980 

1981class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

1982 path: NotRequired[ 

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

1984 ] 

1985 paths: NotRequired[List[FileSystemMatchRule]] 

1986 keep_empty_parent_dirs: NotRequired[bool] 

1987 

1988 

1989class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

1990 sources: NotRequired[List[FileSystemMatchRule]] 

1991 source: NotRequired[ 

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

1993 ] 

1994 into: NotRequired[ 

1995 Annotated[ 

1996 Union[str, List[str]], 

1997 DebputyParseHint.required_when_multi_binary(), 

1998 ] 

1999 ] 

2000 dest_dir: NotRequired[ 

2001 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2002 ] 

2003 install_as: NotRequired[ 

2004 Annotated[ 

2005 FileSystemExactMatchRule, 

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

2007 DebputyParseHint.manifest_attribute("as"), 

2008 DebputyParseHint.not_path_error_hint(), 

2009 ] 

2010 ] 

2011 

2012 

2013class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

2014 sources: NotRequired[List[FileSystemMatchRule]] 

2015 source: NotRequired[ 

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

2017 ] 

2018 into: NotRequired[ 

2019 Annotated[ 

2020 Union[str, List[str]], 

2021 DebputyParseHint.required_when_multi_binary(package_type="deb"), 

2022 ] 

2023 ] 

2024 dest_dir: NotRequired[ 

2025 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2026 ] 

2027 install_as: NotRequired[ 

2028 Annotated[ 

2029 FileSystemExactMatchRule, 

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

2031 DebputyParseHint.manifest_attribute("as"), 

2032 DebputyParseHint.not_path_error_hint(), 

2033 ] 

2034 ] 

2035 

2036 

2037class ParsedInstallRule(DebputyParsedContentStandardConditional): 

2038 sources: List[FileSystemMatchRule] 

2039 into: NotRequired[List[BinaryPackage]] 

2040 dest_dir: NotRequired[FileSystemExactMatchRule] 

2041 install_as: NotRequired[FileSystemExactMatchRule] 

2042 

2043 

2044class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2045 sources: NotRequired[List[FileSystemMatchRule]] 

2046 source: NotRequired[ 

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

2048 ] 

2049 into: NotRequired[ 

2050 Annotated[ 

2051 Union[str, List[str]], 

2052 DebputyParseHint.required_when_multi_binary(), 

2053 ] 

2054 ] 

2055 dest_dirs: NotRequired[ 

2056 Annotated[ 

2057 List[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

2058 ] 

2059 ] 

2060 install_as: NotRequired[ 

2061 Annotated[ 

2062 List[FileSystemExactMatchRule], 

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

2064 DebputyParseHint.not_path_error_hint(), 

2065 DebputyParseHint.manifest_attribute("as"), 

2066 ] 

2067 ] 

2068 

2069 

2070class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

2071 sources: List[FileSystemMatchRule] 

2072 into: NotRequired[List[BinaryPackage]] 

2073 dest_dirs: NotRequired[List[FileSystemExactMatchRule]] 

2074 install_as: NotRequired[List[FileSystemExactMatchRule]] 

2075 

2076 

2077class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

2078 sources: List[FileSystemMatchRule] 

2079 into: NotRequired[List[BinaryPackage]] 

2080 

2081 

2082class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

2083 sources: NotRequired[List[FileSystemMatchRule]] 

2084 source: NotRequired[ 

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

2086 ] 

2087 into: NotRequired[ 

2088 Annotated[ 

2089 Union[str, List[str]], 

2090 DebputyParseHint.required_when_multi_binary(package_type="deb"), 

2091 ] 

2092 ] 

2093 

2094 

2095class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

2096 sources: List[FileSystemMatchRule] 

2097 language: NotRequired[str] 

2098 section: NotRequired[int] 

2099 into: NotRequired[List[BinaryPackage]] 

2100 

2101 

2102class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

2103 sources: NotRequired[List[FileSystemMatchRule]] 

2104 source: NotRequired[ 

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

2106 ] 

2107 language: NotRequired[str] 

2108 section: NotRequired[int] 

2109 into: NotRequired[ 

2110 Annotated[ 

2111 Union[str, List[str]], 

2112 DebputyParseHint.required_when_multi_binary(package_type="deb"), 

2113 ] 

2114 ] 

2115 

2116 

2117class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2118 paths: NotRequired[List[FileSystemMatchRule]] 

2119 path: NotRequired[ 

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

2121 ] 

2122 search_dir: NotRequired[ 

2123 Annotated[ 

2124 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2125 ] 

2126 ] 

2127 search_dirs: NotRequired[List[FileSystemExactMatchRule]] 

2128 required_when: NotRequired[ManifestCondition] 

2129 

2130 

2131class ParsedInstallDiscardRule(DebputyParsedContent): 

2132 paths: List[FileSystemMatchRule] 

2133 search_dirs: NotRequired[List[FileSystemExactMatchRule]] 

2134 required_when: NotRequired[ManifestCondition] 

2135 

2136 

2137class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2138 prior_to_version: NotRequired[str] 

2139 owning_package: NotRequired[str] 

2140 

2141 

2142class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2143 source: str 

2144 target: str 

2145 

2146 

2147class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2148 path: str 

2149 

2150 

2151class MCAnyOfAllOf(DebputyParsedContent): 

2152 conditions: List[ManifestCondition] 

2153 

2154 

2155class MCNot(DebputyParsedContent): 

2156 negated_condition: Annotated[ 

2157 ManifestCondition, DebputyParseHint.manifest_attribute("not") 

2158 ] 

2159 

2160 

2161class MCArchMatches(DebputyParsedContent): 

2162 arch_matches: str 

2163 

2164 

2165class MCBuildProfileMatches(DebputyParsedContent): 

2166 build_profile_matches: str 

2167 

2168 

2169def _parse_filename( 

2170 filename: str, 

2171 attribute_path: AttributePath, 

2172 *, 

2173 allow_directories: bool = True, 

2174) -> str: 

2175 try: 

2176 normalized_path = _normalize_path(filename, with_prefix=False) 

2177 except ValueError as e: 

2178 raise ManifestParseException( 

2179 f'Error parsing the path "{filename}" defined in {attribute_path.path}: {e.args[0]}' 2179 ↛ 2180line 2179 didn't jump to line 2180 because the condition on line 2179 was never true

2180 ) from None 

2181 if not allow_directories and filename.endswith("/"): 

2182 raise ManifestParseException( 

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

2184 f" but this feature can only be used for files" 2184 ↛ 2185line 2184 didn't jump to line 2185 because the condition on line 2184 was never true

2185 ) 

2186 if normalized_path == ".": 

2187 raise ManifestParseException( 

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

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

2190 ) 

2191 return normalized_path 

2192 

2193 

2194def _with_alt_form(t: Type[TypedDict]): 

2195 return Union[ 

2196 t, 

2197 List[str], 

2198 str, 

2199 ] 

2200 

2201 

2202def _dpkg_conffile_rename( 

2203 _name: str, 

2204 parsed_data: DpkgRenameConffileRule, 

2205 path: AttributePath, 

2206 _context: ParserContextData, 

2207) -> DpkgMaintscriptHelperCommand: 

2208 source_file = parsed_data["source"] 

2209 target_file = parsed_data["target"] 

2210 normalized_source = _parse_filename( 

2211 source_file, 

2212 path["source"], 

2213 allow_directories=False, 

2214 ) 

2215 path.path_hint = source_file 

2216 

2217 normalized_target = _parse_filename( 

2218 target_file, 

2219 path["target"], 

2220 allow_directories=False, 

2221 ) 

2222 normalized_source = "/" + normalized_source 

2223 normalized_target = "/" + normalized_target 2223 ↛ 2224line 2223 didn't jump to line 2224 because the condition on line 2223 was never true

2224 

2225 if normalized_source == normalized_target: 

2226 raise ManifestParseException( 

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

2228 ) 

2229 

2230 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2231 parsed_data, path 

2232 ) 

2233 return DpkgMaintscriptHelperCommand.mv_conffile( 

2234 path, 

2235 normalized_source, 

2236 normalized_target, 

2237 version, 

2238 owning_package, 

2239 ) 

2240 

2241 

2242def _dpkg_conffile_remove( 

2243 _name: str, 

2244 parsed_data: DpkgRemoveConffileRule, 

2245 path: AttributePath, 

2246 _context: ParserContextData, 

2247) -> DpkgMaintscriptHelperCommand: 

2248 source_file = parsed_data["path"] 

2249 normalized_source = _parse_filename( 

2250 source_file, 

2251 path["path"], 

2252 allow_directories=False, 

2253 ) 

2254 path.path_hint = source_file 

2255 

2256 normalized_source = "/" + normalized_source 

2257 

2258 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2259 parsed_data, path 

2260 ) 

2261 return DpkgMaintscriptHelperCommand.rm_conffile( 

2262 path, 

2263 normalized_source, 

2264 version, 

2265 owning_package, 

2266 ) 

2267 

2268 

2269def _parse_conffile_prior_version_and_owning_package( 

2270 d: DpkgConffileManagementRuleBase, 

2271 attribute_path: AttributePath, 

2272) -> Tuple[Optional[str], Optional[str]]: 

2273 prior_version = d.get("prior_to_version") 

2274 owning_package = d.get("owning_package") 2274 ↛ 2275line 2274 didn't jump to line 2275 because the condition on line 2274 was never true

2275 

2276 if prior_version is not None and not PKGVERSION_REGEX.match(prior_version): 

2277 p = attribute_path["prior_to_version"] 

2278 raise ManifestParseException( 

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

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

2281 ) 2281 ↛ 2282line 2281 didn't jump to line 2282 because the condition on line 2281 was never true

2282 

2283 if owning_package is not None and not PKGNAME_REGEX.match(owning_package): 

2284 p = attribute_path["owning_package"] 

2285 raise ManifestParseException( 

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

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

2288 ) 

2289 

2290 return prior_version, owning_package 

2291 

2292 

2293def _install_rule_handler( 

2294 _name: str, 

2295 parsed_data: ParsedInstallRule, 

2296 path: AttributePath, 

2297 context: ParserContextData, 

2298) -> InstallRule: 

2299 sources = parsed_data["sources"] 

2300 install_as = parsed_data.get("install_as") 

2301 into = parsed_data.get("into") 

2302 dest_dir = parsed_data.get("dest_dir") 

2303 condition = parsed_data.get("when") 

2304 if not into: 

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

2306 into = frozenset(into) 

2307 if install_as is not None: 

2308 assert len(sources) == 1 

2309 assert dest_dir is None 

2310 return InstallRule.install_as( 

2311 sources[0], 

2312 install_as.match_rule.path, 

2313 into, 

2314 path.path, 

2315 condition, 

2316 ) 

2317 return InstallRule.install_dest( 

2318 sources, 

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

2320 into, 

2321 path.path, 

2322 condition, 

2323 ) 

2324 

2325 

2326def _multi_dest_install_rule_handler( 

2327 _name: str, 

2328 parsed_data: ParsedMultiDestInstallRule, 

2329 path: AttributePath, 

2330 context: ParserContextData, 

2331) -> InstallRule: 

2332 sources = parsed_data["sources"] 

2333 install_as = parsed_data.get("install_as") 

2334 into = parsed_data.get("into") 

2335 dest_dirs = parsed_data.get("dest_dirs") 2335 ↛ 2337line 2335 didn't jump to line 2337 because the condition on line 2335 was always true

2336 condition = parsed_data.get("when") 

2337 if not into: 

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

2339 into = frozenset(into) 

2340 if install_as is not None: 

2341 assert len(sources) == 1 2341 ↛ 2342line 2341 didn't jump to line 2342 because the condition on line 2341 was never true

2342 assert dest_dirs is None 

2343 if len(install_as) < 2: 

2344 raise ManifestParseException( 

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

2346 ) 

2347 return InstallRule.install_multi_as( 

2348 sources[0], 

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

2350 into, 

2351 path.path, 

2352 condition, 2352 ↛ 2353line 2352 didn't jump to line 2353 because the condition on line 2352 was never true

2353 ) 

2354 if dest_dirs is None: 

2355 raise ManifestParseException( 

2356 f"Either the `as` or the `dest-dirs` key must be provided at {path.path}" 2356 ↛ 2357line 2356 didn't jump to line 2357 because the condition on line 2356 was never true

2357 ) 

2358 if len(dest_dirs) < 2: 

2359 raise ManifestParseException( 

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

2361 ) 

2362 return InstallRule.install_multi_dest( 

2363 sources, 

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

2365 into, 

2366 path.path, 

2367 condition, 

2368 ) 

2369 

2370 

2371def _install_docs_rule_handler( 

2372 _name: str, 

2373 parsed_data: ParsedInstallRule, 

2374 path: AttributePath, 

2375 context: ParserContextData, 

2376) -> InstallRule: 

2377 sources = parsed_data["sources"] 

2378 install_as = parsed_data.get("install_as") 

2379 into = parsed_data.get("into") 

2380 dest_dir = parsed_data.get("dest_dir") 2380 ↛ 2386line 2380 didn't jump to line 2386 because the condition on line 2380 was always true

2381 condition = parsed_data.get("when") 

2382 if not into: 

2383 into = [ 

2384 context.single_binary_package( 

2385 path, package_type="deb", package_attribute="into" 

2386 ) 2386 ↛ 2387line 2386 didn't jump to line 2387 because the condition on line 2386 was never true

2387 ] 

2388 if install_as is not None: 

2389 assert len(sources) == 1 

2390 assert dest_dir is None 

2391 return InstallRule.install_doc_as( 

2392 sources[0], 

2393 install_as.match_rule.path, 

2394 frozenset(into), 

2395 path.path, 

2396 condition, 

2397 ) 

2398 return InstallRule.install_doc( 

2399 sources, 

2400 dest_dir, 

2401 frozenset(into), 

2402 path.path, 

2403 condition, 

2404 ) 

2405 

2406 

2407def _install_examples_rule_handler( 

2408 _name: str, 

2409 parsed_data: ParsedInstallExamplesRule, 

2410 path: AttributePath, 

2411 context: ParserContextData, 

2412) -> InstallRule: 

2413 sources = parsed_data["sources"] 

2414 into = parsed_data.get("into") 

2415 if not into: 

2416 into = [ 

2417 context.single_binary_package( 

2418 path, package_type="deb", package_attribute="into" 

2419 ) 

2420 ] 

2421 condition = parsed_data.get("when") 

2422 into = frozenset(into) 

2423 return InstallRule.install_examples( 

2424 sources, 

2425 into, 

2426 path.path, 

2427 condition, 

2428 ) 

2429 

2430 

2431def _install_man_rule_handler( 

2432 _name: str, 

2433 parsed_data: ParsedInstallManpageRule, 

2434 attribute_path: AttributePath, 

2435 context: ParserContextData, 

2436) -> InstallRule: 

2437 sources = parsed_data["sources"] 

2438 language = parsed_data.get("language") 

2439 section = parsed_data.get("section") 

2440 

2441 if language is not None: 

2442 is_lang_ok = language in ( 

2443 "C", 

2444 "derive-from-basename", 

2445 "derive-from-path", 

2446 ) 2446 ↛ 2447line 2446 didn't jump to line 2447 because the condition on line 2446 was never true

2447 

2448 if not is_lang_ok and len(language) == 2 and language.islower(): 

2449 is_lang_ok = True 2449 ↛ 2456line 2449 didn't jump to line 2456

2450 

2451 if ( 

2452 not is_lang_ok 

2453 and len(language) == 5 

2454 and language[2] == "_" 

2455 and language[:2].islower() 

2456 and language[3:].isupper() 

2457 ): 

2458 is_lang_ok = True 2458 ↛ 2459line 2458 didn't jump to line 2459 because the condition on line 2458 was never true

2459 

2460 if not is_lang_ok: 

2461 raise ManifestParseException( 

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

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

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

2465 ) 2465 ↛ 2466line 2465 didn't jump to line 2466 because the condition on line 2465 was never true

2466 

2467 if section is not None and (section < 1 or section > 10): 

2468 raise ManifestParseException( 

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

2470 f' {attribute_path["section"]}' 2470 ↛ 2471line 2470 didn't jump to line 2471 because the condition on line 2470 was never true

2471 ) 

2472 if section is None and any(s.raw_match_rule.endswith(".gz") for s in sources): 

2473 raise ManifestParseException( 

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

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

2476 f' {attribute_path["sources"]}' 2476 ↛ 2477line 2476 didn't jump to line 2477 because the condition on line 2476 was never true

2477 ) 

2478 if any(s.raw_match_rule.endswith("/") for s in sources): 

2479 raise ManifestParseException( 

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

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

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

2483 ) 2483 ↛ 2489line 2483 didn't jump to line 2489 because the condition on line 2483 was always true

2484 into = parsed_data.get("into") 

2485 if not into: 

2486 into = [ 

2487 context.single_binary_package( 

2488 attribute_path, package_type="deb", package_attribute="into" 

2489 ) 

2490 ] 

2491 condition = parsed_data.get("when") 

2492 return InstallRule.install_man( 

2493 sources, 

2494 frozenset(into), 

2495 section, 

2496 language, 

2497 attribute_path.path, 

2498 condition, 

2499 ) 

2500 

2501 

2502def _install_discard_rule_handler( 

2503 _name: str, 

2504 parsed_data: ParsedInstallDiscardRule, 

2505 path: AttributePath, 

2506 _context: ParserContextData, 

2507) -> InstallRule: 2507 ↛ 2508line 2507 didn't jump to line 2508 because the condition on line 2507 was never true

2508 limit_to = parsed_data.get("search_dirs") 

2509 if limit_to is not None and not limit_to: 

2510 p = path["search_dirs"] 

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

2512 condition = parsed_data.get("required_when") 

2513 return InstallRule.discard_paths( 

2514 parsed_data["paths"], 

2515 path.path, 

2516 condition, 

2517 limit_to=limit_to, 

2518 ) 

2519 

2520 

2521def _transformation_move_handler( 

2522 _name: str, 

2523 parsed_data: TransformationMoveRuleSpec, 

2524 path: AttributePath, 

2525 _context: ParserContextData, 

2526) -> TransformationRule: 

2527 source_match = parsed_data["source"] 

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

2529 condition = parsed_data.get("when") 2529 ↛ 2533line 2529 didn't jump to line 2533

2530 

2531 if ( 

2532 isinstance(source_match, ExactFileSystemPath) 

2533 and source_match.path == target_path 

2534 ): 

2535 raise ManifestParseException( 

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

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

2538 ) 

2539 return MoveTransformationRule( 

2540 source_match.match_rule, 

2541 target_path, 

2542 target_path.endswith("/"), 

2543 path, 

2544 condition, 

2545 ) 

2546 

2547 

2548def _transformation_remove_handler( 

2549 _name: str, 

2550 parsed_data: TransformationRemoveRuleSpec, 

2551 attribute_path: AttributePath, 

2552 _context: ParserContextData, 

2553) -> TransformationRule: 

2554 paths = parsed_data["paths"] 

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

2556 

2557 return RemoveTransformationRule( 

2558 [m.match_rule for m in paths], 

2559 keep_empty_parent_dirs, 

2560 attribute_path, 

2561 ) 

2562 

2563 

2564def _transformation_create_symlink( 

2565 _name: str, 

2566 parsed_data: CreateSymlinkRule, 

2567 attribute_path: AttributePath, 

2568 _context: ParserContextData, 

2569) -> TransformationRule: 

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

2571 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2572 "replacement_rule", 

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

2574 ) 

2575 try: 

2576 link_target = debian_policy_normalize_symlink_target( 

2577 link_dest, 

2578 parsed_data["target"].symlink_target, 

2579 ) 

2580 except ValueError as e: # pragma: no cover 

2581 raise AssertionError( 

2582 "Debian Policy normalization should not raise ValueError here" 

2583 ) from e 

2584 

2585 condition = parsed_data.get("when") 

2586 

2587 return CreateSymlinkPathTransformationRule( 

2588 link_target, 

2589 link_dest, 

2590 replacement_rule, 

2591 attribute_path, 

2592 condition, 

2593 ) 

2594 

2595 

2596def _transformation_path_metadata( 

2597 _name: str, 

2598 parsed_data: PathManifestRule, 

2599 attribute_path: AttributePath, 

2600 context: ParserContextData, 

2601) -> TransformationRule: 

2602 match_rules = parsed_data["paths"] 

2603 owner = parsed_data.get("owner") 

2604 group = parsed_data.get("group") 

2605 mode = parsed_data.get("mode") 

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

2607 capabilities = parsed_data.get("capabilities") 

2608 capability_mode = parsed_data.get("capability_mode") 

2609 cap: Optional[str] = None 2609 ↛ 2610line 2609 didn't jump to line 2610 because the condition on line 2609 was never true

2610 

2611 if capabilities is not None: 

2612 check_integration_mode( 

2613 attribute_path["capabilities"], 

2614 context, 

2615 _NOT_INTEGRATION_RRR, 

2616 ) 

2617 if capability_mode is None: 

2618 capability_mode = SymbolicMode.parse_filesystem_mode( 

2619 "a-s", 

2620 attribute_path["capability-mode"], 

2621 ) 

2622 cap = capabilities.value 

2623 validate_cap = check_cap_checker() 2623 ↛ 2624line 2623 didn't jump to line 2624 because the condition on line 2623 was never true

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

2625 elif capability_mode is not None and capabilities is None: 

2626 check_integration_mode( 

2627 attribute_path["capability_mode"], 

2628 context, 

2629 _NOT_INTEGRATION_RRR, 

2630 ) 

2631 raise ManifestParseException( 

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

2633 f" in {attribute_path.path}" 2633 ↛ 2634line 2633 didn't jump to line 2634 because the condition on line 2633 was never true

2634 ) 

2635 if owner is None and group is None and mode is None and capabilities is None: 

2636 raise ManifestParseException( 

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

2638 f" in {attribute_path.path}" 

2639 ) 

2640 condition = parsed_data.get("when") 

2641 

2642 return PathMetadataTransformationRule( 

2643 [m.match_rule for m in match_rules], 

2644 owner, 

2645 group, 

2646 mode, 

2647 recursive, 

2648 cap, 

2649 capability_mode, 

2650 attribute_path.path, 

2651 condition, 

2652 ) 

2653 

2654 

2655def _transformation_mkdirs( 

2656 _name: str, 

2657 parsed_data: EnsureDirectoryRule, 

2658 attribute_path: AttributePath, 

2659 _context: ParserContextData, 

2660) -> TransformationRule: 

2661 provided_paths = parsed_data["paths"] 

2662 owner = parsed_data.get("owner") 

2663 group = parsed_data.get("group") 

2664 mode = parsed_data.get("mode") 

2665 

2666 condition = parsed_data.get("when") 

2667 

2668 return CreateDirectoryTransformationRule( 

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

2670 owner, 

2671 group, 

2672 mode, 

2673 attribute_path.path, 

2674 condition, 

2675 ) 

2676 

2677 

2678def _at_least_two( 

2679 content: List[Any], 

2680 attribute_path: AttributePath, 

2681 attribute_name: str, 2681 ↛ 2682line 2681 didn't jump to line 2682 because the condition on line 2681 was never true

2682) -> None: 

2683 if len(content) < 2: 

2684 raise ManifestParseException( 

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

2686 ) 

2687 

2688 

2689def _mc_any_of( 

2690 name: str, 

2691 parsed_data: MCAnyOfAllOf, 

2692 attribute_path: AttributePath, 

2693 _context: ParserContextData, 

2694) -> ManifestCondition: 

2695 conditions = parsed_data["conditions"] 2695 ↛ 2696line 2695 didn't jump to line 2696 because the condition on line 2695 was never true

2696 _at_least_two(conditions, attribute_path, "conditions") 

2697 if name == "any-of": 

2698 return ManifestCondition.any_of(conditions) 

2699 assert name == "all-of" 

2700 return ManifestCondition.all_of(conditions) 

2701 

2702 

2703def _mc_not( 

2704 _name: str, 

2705 parsed_data: MCNot, 

2706 _attribute_path: AttributePath, 

2707 _context: ParserContextData, 

2708) -> ManifestCondition: 

2709 condition = parsed_data["negated_condition"] 

2710 return condition.negated() 

2711 

2712 

2713def _extract_arch_matches( 

2714 parsed_data: MCArchMatches, 

2715 attribute_path: AttributePath, 

2716) -> List[str]: 

2717 arch_matches_as_str = parsed_data["arch_matches"] 

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

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

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

2721 # of each other. 

2722 arch_matches_as_list = arch_matches_as_str.split() 2722 ↛ 2723line 2722 didn't jump to line 2723 because the condition on line 2722 was never true

2723 attr_path = attribute_path["arch_matches"] 

2724 if not arch_matches_as_list: 

2725 raise ManifestParseException( 

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

2727 ) 2727 ↛ 2730line 2727 didn't jump to line 2730 because the condition on line 2727 was never true

2728 

2729 if arch_matches_as_list[0].startswith("[") or arch_matches_as_list[-1].endswith( 

2730 "]" 

2731 ): 

2732 raise ManifestParseException( 

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

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

2735 ) 

2736 return arch_matches_as_list 

2737 

2738 

2739def _mc_source_context_arch_matches( 

2740 _name: str, 

2741 parsed_data: MCArchMatches, 

2742 attribute_path: AttributePath, 

2743 _context: ParserContextData, 

2744) -> ManifestCondition: 

2745 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2746 return SourceContextArchMatchManifestCondition(arch_matches) 

2747 

2748 

2749def _mc_package_context_arch_matches( 

2750 name: str, 

2751 parsed_data: MCArchMatches, 

2752 attribute_path: AttributePath, 

2753 context: ParserContextData, 

2754) -> ManifestCondition: 

2755 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2756 

2757 if not context.is_in_binary_package_state: 

2758 raise ManifestParseException( 

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

2760 ) 

2761 

2762 package_state = context.current_binary_package_state 

2763 if package_state.binary_package.is_arch_all: 

2764 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2765 "all", arch_matches 

2766 ) 

2767 attr_path = attribute_path["arch_matches"] 

2768 raise ManifestParseException( 

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

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

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

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

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

2774 ) 

2775 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2776 

2777 

2778def _mc_arch_matches( 

2779 name: str, 

2780 parsed_data: MCArchMatches, 

2781 attribute_path: AttributePath, 

2782 context: ParserContextData, 2782 ↛ 2783line 2782 didn't jump to line 2783 because the condition on line 2782 was never true

2783) -> ManifestCondition: 

2784 if context.is_in_binary_package_state: 

2785 return _mc_package_context_arch_matches( 

2786 name, parsed_data, attribute_path, context 

2787 ) 

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

2789 

2790 

2791def _mc_build_profile_matches( 

2792 _name: str, 

2793 parsed_data: MCBuildProfileMatches, 

2794 attribute_path: AttributePath, 

2795 _context: ParserContextData, 

2796) -> ManifestCondition: 

2797 build_profile_spec = parsed_data["build_profile_matches"].strip() 2797 ↛ 2798line 2797 didn't jump to line 2798 because the condition on line 2797 was never true

2798 attr_path = attribute_path["build_profile_matches"] 

2799 if not build_profile_spec: 

2800 raise ManifestParseException( 

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

2802 ) 

2803 try: 

2804 active_profiles_match(build_profile_spec, frozenset()) 

2805 except ValueError as e: 

2806 raise ManifestParseException( 

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

2808 ) 

2809 return BuildProfileMatch(build_profile_spec)