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

541 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import ctypes 

2import ctypes.util 

3import functools 

4import itertools 

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 import DEBPUTY_DOC_ROOT_DIR 

27from debputy._manifest_constants import ( 

28 MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE, 

29 MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION, 

30 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

31 MK_INSTALLATIONS_INSTALL, 

32 MK_INSTALLATIONS_INSTALL_DOCS, 

33 MK_INSTALLATIONS_INSTALL_MAN, 

34 MK_INSTALLATIONS_DISCARD, 

35 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

36) 

37from debputy.exceptions import DebputyManifestVariableRequiresDebianDirError 

38from debputy.installations import InstallRule 

39from debputy.maintscript_snippet import DpkgMaintscriptHelperCommand 

40from debputy.manifest_conditions import ( 

41 ManifestCondition, 

42 BinaryPackageContextArchMatchManifestCondition, 

43 BuildProfileMatch, 

44 SourceContextArchMatchManifestCondition, 

45) 

46from debputy.manifest_parser.base_types import ( 

47 DebputyParsedContent, 

48 DebputyParsedContentStandardConditional, 

49 FileSystemMode, 

50 StaticFileSystemOwner, 

51 StaticFileSystemGroup, 

52 SymlinkTarget, 

53 FileSystemExactMatchRule, 

54 FileSystemMatchRule, 

55 SymbolicMode, 

56 TypeMapping, 

57 OctalMode, 

58 FileSystemExactNonDirMatchRule, 

59) 

60from debputy.manifest_parser.declarative_parser import DebputyParseHint 

61from debputy.manifest_parser.exceptions import ManifestParseException 

62from debputy.manifest_parser.mapper_code import type_mapper_str2package 

63from debputy.manifest_parser.parser_data import ParserContextData 

64from debputy.manifest_parser.util import AttributePath 

65from debputy.packages import BinaryPackage 

66from debputy.path_matcher import ExactFileSystemPath 

67from debputy.plugin.api import ( 

68 DebputyPluginInitializer, 

69 documented_attr, 

70 reference_documentation, 

71 VirtualPath, 

72 packager_provided_file_reference_documentation, 

73) 

74from debputy.plugin.api.impl import DebputyPluginInitializerProvider 

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

76from debputy.plugin.api.spec import ( 

77 type_mapping_reference_documentation, 

78 type_mapping_example, 

79) 

80from debputy.plugin.debputy.binary_package_rules import register_binary_package_rules 

81from debputy.plugin.debputy.discard_rules import ( 

82 _debputy_discard_pyc_files, 

83 _debputy_prune_la_files, 

84 _debputy_prune_doxygen_cruft, 

85 _debputy_prune_binary_debian_dir, 

86 _debputy_prune_info_dir_file, 

87 _debputy_prune_backup_files, 

88 _debputy_prune_vcs_paths, 

89) 

90from debputy.plugin.debputy.manifest_root_rules import register_manifest_root_rules 

91from debputy.plugin.debputy.package_processors import ( 

92 process_manpages, 

93 apply_compression, 

94 clean_la_files, 

95) 

96from debputy.plugin.debputy.service_management import ( 

97 detect_systemd_service_files, 

98 generate_snippets_for_systemd_units, 

99 detect_sysv_init_service_files, 

100 generate_snippets_for_init_scripts, 

101) 

102from debputy.plugin.debputy.shlib_metadata_detectors import detect_shlibdeps 

103from debputy.plugin.debputy.strip_non_determinism import strip_non_determinism 

104from debputy.substitution import VariableContext 

105from debputy.transformation_rules import ( 

106 CreateSymlinkReplacementRule, 

107 TransformationRule, 

108 CreateDirectoryTransformationRule, 

109 RemoveTransformationRule, 

110 MoveTransformationRule, 

111 PathMetadataTransformationRule, 

112 CreateSymlinkPathTransformationRule, 

113) 

114from debputy.util import ( 

115 _normalize_path, 

116 PKGNAME_REGEX, 

117 PKGVERSION_REGEX, 

118 debian_policy_normalize_symlink_target, 

119 active_profiles_match, 

120 _error, 

121 _warn, 

122 _info, 

123 assume_not_none, 

124) 

125 

126_DOCUMENTED_DPKG_ARCH_TYPES = { 

127 "HOST": ( 

128 "installed on", 

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

130 ), 

131 "BUILD": ( 

132 "compiled on", 

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

134 ), 

135 "TARGET": ( 

136 "cross-compiler output", 

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

138 ), 

139} 

140 

141_DOCUMENTED_DPKG_ARCH_VARS = { 

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

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

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

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

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

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

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

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

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

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

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

153} 

154 

155 

156def _manifest_format_doc(anchor: str) -> str: 

157 return f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md#{anchor}" 

158 

159 

160@functools.lru_cache 

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

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

163 has_libcap = False 

164 libcap = None 

165 if cap_library_path: 165 ↛ 172line 165 didn't jump to line 172, because the condition on line 165 was never false

166 try: 

167 libcap = ctypes.cdll.LoadLibrary(cap_library_path) 

168 has_libcap = True 

169 except OSError: 

170 pass 

171 

172 if libcap is None: 172 ↛ 173line 172 didn't jump to line 173, because the condition on line 172 was never true

173 warned = False 

174 

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

176 nonlocal warned 

177 if not warned: 

178 _info( 

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

180 " checking of capabilities." 

181 ) 

182 warned = True 

183 return True 

184 

185 else: 

186 # cap_t cap_from_text(const char *path_p) 

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

188 libcap.cap_from_text.restype = ctypes.c_char_p 

189 

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

191 libcap.cap_free.restype = None 

192 

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

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

195 ok = cap_t is not None 

196 libcap.cap_free(cap_t) 

197 return ok 

198 

199 return has_libcap, cap_library_path, _is_valid_cap 

200 

201 

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

203 _, libcap_path, is_valid_cap = load_libcap() 

204 

205 seen_cap = set() 

206 

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

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

209 seen_cap.add(cap) 

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

211 _warn( 

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

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

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

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

216 ) 

217 

218 return _check_cap 

219 

220 

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

222 try: 

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

224 if changelog is None: 

225 raise DebputyManifestVariableRequiresDebianDirError( 

226 "The changelog was not present" 

227 ) 

228 with changelog.open() as fd: 

229 dch = Changelog(fd, max_blocks=2) 

230 except FileNotFoundError as e: 

231 raise DebputyManifestVariableRequiresDebianDirError( 

232 "The changelog was not present" 

233 ) from e 

234 first_entry = dch[0] 

235 first_non_binnmu_entry = dch[0] 

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

237 first_non_binnmu_entry = dch[1] 

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

239 source_version = first_entry.version 

240 epoch = source_version.epoch 

241 upstream_version = source_version.upstream_version 

242 debian_revision = source_version.debian_revision 

243 epoch_upstream = upstream_version 

244 upstream_debian_revision = upstream_version 

245 if epoch is not None and epoch != "": 245 ↛ 247line 245 didn't jump to line 247, because the condition on line 245 was never false

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

247 if debian_revision is not None and debian_revision != "": 247 ↛ 250line 247 didn't jump to line 250, because the condition on line 247 was never false

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

249 

250 package = first_entry.package 

251 if package is None: 251 ↛ 252line 251 didn't jump to line 252, because the condition on line 251 was never true

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

253 

254 date = first_entry.date 

255 if date is not None: 255 ↛ 259line 255 didn't jump to line 259, because the condition on line 255 was never false

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

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

258 else: 

259 _warn( 

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

261 " for SOURCE_DATE_EPOCH" 

262 ) 

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

264 

265 if first_non_binnmu_entry is not first_entry: 

266 non_binnmu_date = first_non_binnmu_entry.date 

267 if non_binnmu_date is not None: 267 ↛ 271line 267 didn't jump to line 271, because the condition on line 267 was never false

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

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

270 else: 

271 _warn( 

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

273 " for SOURCE_DATE_EPOCH (for strip-nondeterminism)" 

274 ) 

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

276 else: 

277 snd_source_date_epoch = source_date_epoch 

278 return { 

279 "DEB_SOURCE": package, 

280 "DEB_VERSION": source_version.full_version, 

281 "DEB_VERSION_EPOCH_UPSTREAM": epoch_upstream, 

282 "DEB_VERSION_UPSTREAM_REVISION": upstream_debian_revision, 

283 "DEB_VERSION_UPSTREAM": upstream_version, 

284 "SOURCE_DATE_EPOCH": source_date_epoch, 

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

286 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": snd_source_date_epoch, 

287 } 

288 

289 

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

291 api = cast("DebputyPluginInitializerProvider", public_api) 

292 

293 api.metadata_or_maintscript_detector( 

294 "dpkg-shlibdeps", 

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

296 cast("MetadataAutoDetector", detect_shlibdeps), 

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

298 ) 

299 register_type_mappings(api) 

300 register_variables_via_private_api(api) 

301 document_builtin_variables(api) 

302 register_automatic_discard_rules(api) 

303 register_special_ppfs(api) 

304 register_install_rules(api) 

305 register_transformation_rules(api) 

306 register_manifest_condition_rules(api) 

307 register_dpkg_conffile_rules(api) 

308 register_processing_steps(api) 

309 register_service_managers(api) 

310 register_manifest_root_rules(api) 

311 register_binary_package_rules(api) 

312 

313 

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

315 api.register_mapped_type( 

316 TypeMapping( 

317 FileSystemMatchRule, 

318 str, 

319 FileSystemMatchRule.parse_path_match, 

320 ), 

321 reference_documentation=type_mapping_reference_documentation( 

322 description=textwrap.dedent( 

323 """\ 

324 A generic file system path match with globs. 

325 

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

327 

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

329 

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

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

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

333 

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

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

336 

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

338 or relevant search directories will match. 

339 

340 Please keep in mind that: 

341 

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

343 an anchor reference. 

344 

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

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

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

348 directories (similar to the shell). 

349 

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

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

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

353 """, 

354 ), 

355 examples=[ 

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

357 type_mapping_example("*.txt"), 

358 type_mapping_example("**/foo"), 

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

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

361 ], 

362 ), 

363 ) 

364 

365 api.register_mapped_type( 

366 TypeMapping( 

367 FileSystemExactMatchRule, 

368 str, 

369 FileSystemExactMatchRule.parse_path_match, 

370 ), 

371 reference_documentation=type_mapping_reference_documentation( 

372 description=textwrap.dedent( 

373 """\ 

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

375 

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

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

378 

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

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

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

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

383 """, 

384 ), 

385 examples=[ 

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

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

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

389 ], 

390 ), 

391 ) 

392 

393 api.register_mapped_type( 

394 TypeMapping( 

395 FileSystemExactNonDirMatchRule, 

396 str, 

397 FileSystemExactNonDirMatchRule.parse_path_match, 

398 ), 

399 reference_documentation=type_mapping_reference_documentation( 

400 description=textwrap.dedent( 

401 f"""\ 

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

403 

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

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

406 

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

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

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

410 with a "/". 

411 """, 

412 ), 

413 examples=[ 

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

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

416 ], 

417 ), 

418 ) 

419 

420 api.register_mapped_type( 

421 TypeMapping( 

422 SymlinkTarget, 

423 str, 

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

425 v, ap, assume_not_none(pc).substitution 

426 ), 

427 ), 

428 reference_documentation=type_mapping_reference_documentation( 

429 description=textwrap.dedent( 

430 """\ 

431 A symlink target. 

432 

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

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

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

436 

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

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

439 """, 

440 ), 

441 examples=[ 

442 type_mapping_example("../foo"), 

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

444 ], 

445 ), 

446 ) 

447 

448 api.register_mapped_type( 

449 TypeMapping( 

450 StaticFileSystemOwner, 

451 Union[int, str], 

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

453 ), 

454 reference_documentation=type_mapping_reference_documentation( 

455 description=textwrap.dedent( 

456 """\ 

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

458 

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

460 

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

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

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

464 

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

466 

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

468 """ 

469 ), 

470 examples=[ 

471 type_mapping_example("root"), 

472 type_mapping_example(0), 

473 type_mapping_example("root:0"), 

474 type_mapping_example("bin"), 

475 ], 

476 ), 

477 ) 

478 api.register_mapped_type( 

479 TypeMapping( 

480 StaticFileSystemGroup, 

481 Union[int, str], 

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

483 ), 

484 reference_documentation=type_mapping_reference_documentation( 

485 description=textwrap.dedent( 

486 """\ 

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

488 

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

490 

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

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

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

494 

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

496 

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

498 """ 

499 ), 

500 examples=[ 

501 type_mapping_example("root"), 

502 type_mapping_example(0), 

503 type_mapping_example("root:0"), 

504 type_mapping_example("tty"), 

505 ], 

506 ), 

507 ) 

508 

509 api.register_mapped_type( 

510 TypeMapping( 

511 BinaryPackage, 

512 str, 

513 type_mapper_str2package, 

514 ), 

515 reference_documentation=type_mapping_reference_documentation( 

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

517 ), 

518 ) 

519 

520 api.register_mapped_type( 

521 TypeMapping( 

522 FileSystemMode, 

523 str, 

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

525 ), 

526 reference_documentation=type_mapping_reference_documentation( 

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

528 examples=[ 

529 type_mapping_example("a+x"), 

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

531 type_mapping_example("0755"), 

532 ], 

533 ), 

534 ) 

535 api.register_mapped_type( 535 ↛ exitline 535 didn't jump to the function exit

536 TypeMapping( 

537 OctalMode, 

538 str, 

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

540 ), 

541 reference_documentation=type_mapping_reference_documentation( 

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

543 examples=[ 

544 type_mapping_example("0644"), 

545 type_mapping_example("0755"), 

546 ], 

547 ), 

548 ) 

549 

550 

551def register_service_managers( 

552 api: DebputyPluginInitializerProvider, 

553) -> None: 

554 api.service_provider( 

555 "systemd", 

556 detect_systemd_service_files, 

557 generate_snippets_for_systemd_units, 

558 ) 

559 api.service_provider( 

560 "sysvinit", 

561 detect_sysv_init_service_files, 

562 generate_snippets_for_init_scripts, 

563 ) 

564 

565 

566def register_automatic_discard_rules( 

567 api: DebputyPluginInitializerProvider, 

568) -> None: 

569 api.automatic_discard_rule( 

570 "python-cache-files", 

571 _debputy_discard_pyc_files, 

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

573 examples=automatic_discard_rule_example( 

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

575 ".../__pycache__/", 

576 ".../__pycache__/...", 

577 ".../foo.pyc", 

578 ".../foo.pyo", 

579 ), 

580 ) 

581 api.automatic_discard_rule( 

582 "la-files", 

583 _debputy_prune_la_files, 

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

585 examples=automatic_discard_rule_example( 

586 "usr/lib/libfoo.la", 

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

588 ), 

589 ) 

590 api.automatic_discard_rule( 

591 "backup-files", 

592 _debputy_prune_backup_files, 

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

594 examples=( 

595 automatic_discard_rule_example( 

596 ".../foo~", 

597 ".../foo.orig", 

598 ".../foo.rej", 

599 ".../DEADJOE", 

600 ".../.foo.sw.", 

601 ), 

602 ), 

603 ) 

604 api.automatic_discard_rule( 

605 "version-control-paths", 

606 _debputy_prune_vcs_paths, 

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

608 examples=automatic_discard_rule_example( 

609 ("tools/foo", False), 

610 ".../CVS/", 

611 ".../CVS/...", 

612 ".../.gitignore", 

613 ".../.gitattributes", 

614 ".../.git/", 

615 ".../.git/...", 

616 ), 

617 ) 

618 api.automatic_discard_rule( 

619 "gnu-info-dir-file", 

620 _debputy_prune_info_dir_file, 

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

622 examples=automatic_discard_rule_example( 

623 "usr/share/info/dir", 

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

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

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

627 ), 

628 ) 

629 api.automatic_discard_rule( 

630 "debian-dir", 

631 _debputy_prune_binary_debian_dir, 

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

633 " literally in the file listing", 

634 examples=( 

635 automatic_discard_rule_example( 

636 "DEBIAN/", 

637 "DEBIAN/control", 

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

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

640 ), 

641 ), 

642 ) 

643 api.automatic_discard_rule( 

644 "doxygen-cruft-files", 

645 _debputy_prune_doxygen_cruft, 

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

647 examples=automatic_discard_rule_example( 

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

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

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

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

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

653 ), 

654 ) 

655 

656 

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

658 api.package_processor("manpages", process_manpages) 

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

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

661 api.package_processor( 

662 "strip-nondeterminism", 

663 cast("Any", strip_non_determinism), 

664 depends_on_processor=["manpages"], 

665 ) 

666 api.package_processor( 

667 "compression", 

668 apply_compression, 

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

670 ) 

671 

672 

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

674 api.manifest_variable_provider( 

675 load_source_variables, 

676 { 

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

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

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

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

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

682 "SOURCE_DATE_EPOCH": textwrap.dedent( 

683 """\ 

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

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

686 this variable. 

687 """ 

688 ), 

689 "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE": None, 

690 "_DEBPUTY_SND_SOURCE_DATE_EPOCH": None, 

691 }, 

692 ) 

693 

694 

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

696 api.document_builtin_variable( 

697 "PACKAGE", 

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

699 is_context_specific=True, 

700 ) 

701 

702 arch_types = _DOCUMENTED_DPKG_ARCH_TYPES 

703 

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

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

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

707 documentation = textwrap.dedent( 

708 f"""\ 

709 {arch_var_doc} ({arch_type_tag}) 

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

711 * Machine type: {arch_type_doc} 

712 * Value description: {arch_var_doc} 

713 

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

715 """ 

716 ) 

717 api.document_builtin_variable( 

718 full_var, 

719 documentation, 

720 is_for_special_case=arch_type != "HOST", 

721 ) 

722 

723 

724def _format_docbase_filename( 

725 path_format: str, 

726 format_param: PPFFormatParam, 

727 docbase_file: VirtualPath, 

728) -> str: 

729 with docbase_file.open() as fd: 

730 content = Deb822(fd) 

731 proper_name = content["Document"] 

732 if proper_name is not None: 732 ↛ 735line 732 didn't jump to line 735, because the condition on line 732 was never false

733 format_param["name"] = proper_name 

734 else: 

735 _warn( 

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

737 ) 

738 return path_format.format(**format_param) 

739 

740 

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

742 api.packager_provided_file( 

743 "doc-base", 

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

745 format_callback=_format_docbase_filename, 

746 ) 

747 

748 api.packager_provided_file( 

749 "shlibs", 

750 "DEBIAN/shlibs", 

751 allow_name_segment=False, 

752 reservation_only=True, 

753 reference_documentation=packager_provided_file_reference_documentation( 

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

755 ), 

756 ) 

757 api.packager_provided_file( 

758 "symbols", 

759 "DEBIAN/symbols", 

760 allow_name_segment=False, 

761 allow_architecture_segment=True, 

762 reservation_only=True, 

763 reference_documentation=packager_provided_file_reference_documentation( 

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

765 ), 

766 ) 

767 api.packager_provided_file( 

768 "templates", 

769 "DEBIAN/templates", 

770 allow_name_segment=False, 

771 allow_architecture_segment=False, 

772 reservation_only=True, 

773 ) 

774 api.packager_provided_file( 

775 "alternatives", 

776 "DEBIAN/alternatives", 

777 allow_name_segment=False, 

778 allow_architecture_segment=True, 

779 reservation_only=True, 

780 ) 

781 

782 

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

784 api.pluggable_manifest_rule( 

785 InstallRule, 

786 MK_INSTALLATIONS_INSTALL, 

787 ParsedInstallRule, 

788 _install_rule_handler, 

789 source_format=_with_alt_form(ParsedInstallRuleSourceFormat), 

790 inline_reference_documentation=reference_documentation( 

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

792 description=textwrap.dedent( 

793 """\ 

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

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

796 

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

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

799 

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

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

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

803 """.format( 

804 MULTI_DEST_INSTALL=MK_INSTALLATIONS_MULTI_DEST_INSTALL 

805 ) 

806 ), 

807 non_mapping_description=textwrap.dedent( 

808 """\ 

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

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

811 not required. 

812 """ 

813 ), 

814 attributes=[ 

815 documented_attr( 

816 ["source", "sources"], 

817 textwrap.dedent( 

818 """\ 

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

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

821 is tried against default search directories. 

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

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

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

825 """ 

826 ), 

827 ), 

828 documented_attr( 

829 "dest_dir", 

830 textwrap.dedent( 

831 """\ 

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

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

834 to the directory name of the `source`. 

835 """ 

836 ), 

837 ), 

838 documented_attr( 

839 "into", 

840 textwrap.dedent( 

841 """\ 

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

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

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

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

846 """ 

847 ), 

848 ), 

849 documented_attr( 

850 "install_as", 

851 textwrap.dedent( 

852 """\ 

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

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

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

856 """ 

857 ), 

858 ), 

859 documented_attr( 

860 "when", 

861 textwrap.dedent( 

862 """\ 

863 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

864 """ 

865 ), 

866 ), 

867 ], 

868 reference_documentation_url=_manifest_format_doc("generic-install-install"), 

869 ), 

870 ) 

871 api.pluggable_manifest_rule( 

872 InstallRule, 

873 [ 

874 MK_INSTALLATIONS_INSTALL_DOCS, 

875 "install-doc", 

876 ], 

877 ParsedInstallRule, 

878 _install_docs_rule_handler, 

879 source_format=_with_alt_form(ParsedInstallDocRuleSourceFormat), 

880 inline_reference_documentation=reference_documentation( 

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

882 description=textwrap.dedent( 

883 """\ 

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

885 `install` rule with the following key features: 

886 

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

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

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

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

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

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

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

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

895 package listed in `debian/control`. 

896 

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

898 

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

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

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

902 use-case. 

903 """ 

904 ), 

905 non_mapping_description=textwrap.dedent( 

906 """\ 

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

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

909 not required. 

910 """ 

911 ), 

912 attributes=[ 

913 documented_attr( 

914 ["source", "sources"], 

915 textwrap.dedent( 

916 """\ 

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

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

919 is tried against default search directories. 

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

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

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

923 

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

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

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

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

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

929 """ 

930 ), 

931 ), 

932 documented_attr( 

933 "dest_dir", 

934 textwrap.dedent( 

935 """\ 

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

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

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

939 """ 

940 ), 

941 ), 

942 documented_attr( 

943 "into", 

944 textwrap.dedent( 

945 """\ 

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

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

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

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

950 the key is required. 

951 """ 

952 ), 

953 ), 

954 documented_attr( 

955 "install_as", 

956 textwrap.dedent( 

957 """\ 

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

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

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

961 """ 

962 ), 

963 ), 

964 documented_attr( 

965 "when", 

966 textwrap.dedent( 

967 """\ 

968 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

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

970 (rather than replacing it). 

971 """ 

972 ), 

973 ), 

974 ], 

975 reference_documentation_url=_manifest_format_doc( 

976 "install-documentation-install-docs" 

977 ), 

978 ), 

979 ) 

980 api.pluggable_manifest_rule( 

981 InstallRule, 

982 [ 

983 MK_INSTALLATIONS_INSTALL_EXAMPLES, 

984 "install-example", 

985 ], 

986 ParsedInstallExamplesRule, 

987 _install_examples_rule_handler, 

988 source_format=_with_alt_form(ParsedInstallExamplesRuleSourceFormat), 

989 inline_reference_documentation=reference_documentation( 

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

991 description=textwrap.dedent( 

992 """\ 

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

994 install` rule with the following key features: 

995 

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

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

998 dir. 

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

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

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

1002 package listed in `debian/control`. 

1003 

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

1005 """ 

1006 ), 

1007 non_mapping_description=textwrap.dedent( 

1008 """\ 

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

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

1011 not required. 

1012 """ 

1013 ), 

1014 attributes=[ 

1015 documented_attr( 

1016 ["source", "sources"], 

1017 textwrap.dedent( 

1018 """\ 

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

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

1021 is tried against default search directories. 

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

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

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

1025 

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

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

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

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

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

1031 """ 

1032 ), 

1033 ), 

1034 documented_attr( 

1035 "into", 

1036 textwrap.dedent( 

1037 """\ 

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

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

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

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

1042 Otherwise, the key is required. 

1043 """ 

1044 ), 

1045 ), 

1046 documented_attr( 

1047 "when", 

1048 textwrap.dedent( 

1049 """\ 

1050 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

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

1052 (rather than replacing it). 

1053 """ 

1054 ), 

1055 ), 

1056 ], 

1057 reference_documentation_url=_manifest_format_doc( 

1058 "install-examples-install-examples" 

1059 ), 

1060 ), 

1061 ) 

1062 api.pluggable_manifest_rule( 

1063 InstallRule, 

1064 MK_INSTALLATIONS_INSTALL_MAN, 

1065 ParsedInstallManpageRule, 

1066 _install_man_rule_handler, 

1067 source_format=_with_alt_form(ParsedInstallManpageRuleSourceFormat), 

1068 inline_reference_documentation=reference_documentation( 

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

1070 description=textwrap.dedent( 

1071 """\ 

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

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

1074 

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

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

1077 language. 

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

1079 package listed in `debian/control`. 

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

1081 for when the auto-detection is insufficient. 

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

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

1084 

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

1086 """ 

1087 ), 

1088 non_mapping_description=textwrap.dedent( 

1089 """\ 

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

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

1092 not required. 

1093 """ 

1094 ), 

1095 attributes=[ 

1096 documented_attr( 

1097 ["source", "sources"], 

1098 textwrap.dedent( 

1099 """\ 

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

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

1102 is tried against default search directories. 

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

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

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

1106 """ 

1107 ), 

1108 ), 

1109 documented_attr( 

1110 "into", 

1111 textwrap.dedent( 

1112 """\ 

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

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

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

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

1117 """ 

1118 ), 

1119 ), 

1120 documented_attr( 

1121 "section", 

1122 textwrap.dedent( 

1123 """\ 

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

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

1126 have performed. 

1127 """ 

1128 ), 

1129 ), 

1130 documented_attr( 

1131 "language", 

1132 textwrap.dedent( 

1133 """\ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1151 always assumed to be a language. 

1152 """ 

1153 ), 

1154 ), 

1155 documented_attr( 

1156 "when", 

1157 textwrap.dedent( 

1158 """\ 

1159 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

1160 """ 

1161 ), 

1162 ), 

1163 ], 

1164 reference_documentation_url=_manifest_format_doc( 

1165 "install-manpages-install-man" 

1166 ), 

1167 ), 

1168 ) 

1169 api.pluggable_manifest_rule( 

1170 InstallRule, 

1171 MK_INSTALLATIONS_DISCARD, 

1172 ParsedInstallDiscardRule, 

1173 _install_discard_rule_handler, 

1174 source_format=_with_alt_form(ParsedInstallDiscardRuleSourceFormat), 

1175 inline_reference_documentation=reference_documentation( 

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

1177 description=textwrap.dedent( 

1178 """\ 

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

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

1181 

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

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

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

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

1186 """ 

1187 ), 

1188 non_mapping_description=textwrap.dedent( 

1189 """\ 

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

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

1192 """ 

1193 ), 

1194 attributes=[ 

1195 documented_attr( 

1196 ["path", "paths"], 

1197 textwrap.dedent( 

1198 """\ 

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

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

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

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

1203 contents that have not already been installed somewhere. 

1204 """ 

1205 ), 

1206 ), 

1207 documented_attr( 

1208 ["search_dir", "search_dirs"], 

1209 textwrap.dedent( 

1210 """\ 

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

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

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

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

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

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

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

1218 will make `debputy` report an error. 

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

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

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

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

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

1224 applicable to certain builds that are only performed conditionally. 

1225 """ 

1226 ), 

1227 ), 

1228 documented_attr( 

1229 "required_when", 

1230 textwrap.dedent( 

1231 """\ 

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

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

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

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

1236 """ 

1237 ), 

1238 ), 

1239 ], 

1240 reference_documentation_url=_manifest_format_doc( 

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

1242 ), 

1243 ), 

1244 ) 

1245 api.pluggable_manifest_rule( 

1246 InstallRule, 

1247 MK_INSTALLATIONS_MULTI_DEST_INSTALL, 

1248 ParsedMultiDestInstallRule, 

1249 _multi_dest_install_rule_handler, 

1250 source_format=ParsedMultiDestInstallRuleSourceFormat, 

1251 inline_reference_documentation=reference_documentation( 

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

1253 description=textwrap.dedent( 

1254 """\ 

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

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

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

1258 "primary" uses. 

1259 

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

1261 except you list 2+ destination directories. 

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

1263 2+ `as` names. 

1264 """.format( 

1265 RULE_NAME=MK_INSTALLATIONS_MULTI_DEST_INSTALL 

1266 ) 

1267 ), 

1268 attributes=[ 

1269 documented_attr( 

1270 ["source", "sources"], 

1271 textwrap.dedent( 

1272 """\ 

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

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

1275 is tried against default search directories. 

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

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

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

1279 """ 

1280 ), 

1281 ), 

1282 documented_attr( 

1283 "dest_dirs", 

1284 textwrap.dedent( 

1285 """\ 

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

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

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

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

1290 """ 

1291 ), 

1292 ), 

1293 documented_attr( 

1294 "into", 

1295 textwrap.dedent( 

1296 """\ 

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

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

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

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

1301 """ 

1302 ), 

1303 ), 

1304 documented_attr( 

1305 "install_as", 

1306 textwrap.dedent( 

1307 """\ 

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

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

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

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

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

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

1314 """ 

1315 ), 

1316 ), 

1317 documented_attr( 

1318 "when", 

1319 textwrap.dedent( 

1320 """\ 

1321 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

1322 """ 

1323 ), 

1324 ), 

1325 ], 

1326 reference_documentation_url=_manifest_format_doc("generic-install-install"), 

1327 ), 

1328 ) 

1329 

1330 

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

1332 api.pluggable_manifest_rule( 

1333 TransformationRule, 

1334 "move", 

1335 TransformationMoveRuleSpec, 

1336 _transformation_move_handler, 

1337 inline_reference_documentation=reference_documentation( 

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

1339 description=textwrap.dedent( 

1340 """\ 

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

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

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

1344 Debian specific requirements. 

1345 

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

1347 `mv` command line tool. 

1348 """ 

1349 ), 

1350 attributes=[ 

1351 documented_attr( 

1352 "source", 

1353 textwrap.dedent( 

1354 """\ 

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

1356 and substitutions. 

1357 """ 

1358 ), 

1359 ), 

1360 documented_attr( 

1361 "target", 

1362 textwrap.dedent( 

1363 """\ 

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

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

1366 the target will *always* be a directory. 

1367 """ 

1368 ), 

1369 ), 

1370 documented_attr( 

1371 "when", 

1372 textwrap.dedent( 

1373 """\ 

1374 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

1375 """ 

1376 ), 

1377 ), 

1378 ], 

1379 reference_documentation_url=_manifest_format_doc( 

1380 "move-transformation-rule-move" 

1381 ), 

1382 ), 

1383 ) 

1384 api.pluggable_manifest_rule( 

1385 TransformationRule, 

1386 "remove", 

1387 TransformationRemoveRuleSpec, 

1388 _transformation_remove_handler, 

1389 source_format=_with_alt_form(TransformationRemoveRuleInputFormat), 

1390 inline_reference_documentation=reference_documentation( 

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

1392 description=textwrap.dedent( 

1393 """\ 

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

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

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

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

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

1399 of `debian/copyright`). 

1400 

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

1402 the `remove` transformation rule. 

1403 

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

1405 """ 

1406 ), 

1407 non_mapping_description=textwrap.dedent( 

1408 """\ 

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

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

1411 """ 

1412 ), 

1413 attributes=[ 

1414 documented_attr( 

1415 ["path", "paths"], 

1416 textwrap.dedent( 

1417 """\ 

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

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

1420 can use globs. 

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

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

1423 along with all the contents. 

1424 """ 

1425 ), 

1426 ), 

1427 documented_attr( 

1428 "keep_empty_parent_dirs", 

1429 textwrap.dedent( 

1430 """\ 

1431 A boolean determining whether to prune parent directories that become 

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

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

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

1435 """ 

1436 ), 

1437 ), 

1438 documented_attr( 

1439 "when", 

1440 textwrap.dedent( 

1441 """\ 

1442 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

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

1444 (rather than replacing it). 

1445 """ 

1446 ), 

1447 ), 

1448 ], 

1449 reference_documentation_url=_manifest_format_doc( 

1450 "remove-transformation-rule-remove" 

1451 ), 

1452 ), 

1453 ) 

1454 api.pluggable_manifest_rule( 

1455 TransformationRule, 

1456 "create-symlink", 

1457 CreateSymlinkRule, 

1458 _transformation_create_symlink, 

1459 inline_reference_documentation=reference_documentation( 

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

1461 description=textwrap.dedent( 

1462 """\ 

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

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

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

1466 """ 

1467 ), 

1468 attributes=[ 

1469 documented_attr( 

1470 "path", 

1471 textwrap.dedent( 

1472 """\ 

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

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

1475 Parent directories are implicitly created as necessary. 

1476 * Note that if `path` already exists, the behaviour of this 

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

1478 """ 

1479 ), 

1480 ), 

1481 documented_attr( 

1482 "target", 

1483 textwrap.dedent( 

1484 """\ 

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

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

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

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

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

1490 preference. 

1491 """ 

1492 ), 

1493 ), 

1494 documented_attr( 

1495 "replacement_rule", 

1496 textwrap.dedent( 

1497 """\ 

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

1499 be set to one of the following values: 

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

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

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

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

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

1505 similar to `ln -sf` semantics. 

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

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

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

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

1510 will stop with an error. 

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

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

1513 be removed recursively along with the directory. Finally, 

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

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

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

1517 `when` if any). 

1518 

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

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

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

1522 the value in `replacement-rule`. 

1523 """ 

1524 ), 

1525 ), 

1526 documented_attr( 

1527 "when", 

1528 textwrap.dedent( 

1529 """\ 

1530 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

1531 """ 

1532 ), 

1533 ), 

1534 ], 

1535 reference_documentation_url=_manifest_format_doc( 

1536 "create-symlinks-transformation-rule-create-symlink" 

1537 ), 

1538 ), 

1539 ) 

1540 api.pluggable_manifest_rule( 

1541 TransformationRule, 

1542 "path-metadata", 

1543 PathManifestRule, 

1544 _transformation_path_metadata, 

1545 source_format=PathManifestSourceDictFormat, 

1546 inline_reference_documentation=reference_documentation( 

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

1548 description=textwrap.dedent( 

1549 """\ 

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

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

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

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

1554 transformation. 

1555 

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

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

1558 """ 

1559 ), 

1560 attributes=[ 

1561 documented_attr( 

1562 ["path", "paths"], 

1563 textwrap.dedent( 

1564 """\ 

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

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

1567 and substitution variables. Special-rules for matches: 

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

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

1570 """ 

1571 ), 

1572 ), 

1573 documented_attr( 

1574 "owner", 

1575 textwrap.dedent( 

1576 """\ 

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

1578 no change of owner is done. 

1579 """ 

1580 ), 

1581 ), 

1582 documented_attr( 

1583 "group", 

1584 textwrap.dedent( 

1585 """\ 

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

1587 no change of group is done. 

1588 """ 

1589 ), 

1590 ), 

1591 documented_attr( 

1592 "mode", 

1593 textwrap.dedent( 

1594 """\ 

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

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

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

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

1599 relative to the matched path's current mode. 

1600 """ 

1601 ), 

1602 ), 

1603 documented_attr( 

1604 "capabilities", 

1605 textwrap.dedent( 

1606 """\ 

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

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

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

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

1611 run. 

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

1613 to those paths. 

1614 

1615 """ 

1616 ), 

1617 ), 

1618 documented_attr( 

1619 "capability_mode", 

1620 textwrap.dedent( 

1621 """\ 

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

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

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

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

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

1627 `capabilities` is omitted. 

1628 """ 

1629 ), 

1630 ), 

1631 documented_attr( 

1632 "recursive", 

1633 textwrap.dedent( 

1634 """\ 

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

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

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

1638 this attribute is `false`. 

1639 """ 

1640 ), 

1641 ), 

1642 documented_attr( 

1643 "when", 

1644 textwrap.dedent( 

1645 """\ 

1646 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

1647 """ 

1648 ), 

1649 ), 

1650 ], 

1651 reference_documentation_url=_manifest_format_doc( 

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

1653 ), 

1654 ), 

1655 ) 

1656 api.pluggable_manifest_rule( 

1657 TransformationRule, 

1658 "create-directories", 

1659 EnsureDirectoryRule, 

1660 _transformation_mkdirs, 

1661 source_format=_with_alt_form(EnsureDirectorySourceFormat), 

1662 inline_reference_documentation=reference_documentation( 

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

1664 description=textwrap.dedent( 

1665 """\ 

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

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

1668 transformations will create directories as required. 

1669 

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

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

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

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

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

1675 

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

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

1678 """ 

1679 ), 

1680 non_mapping_description=textwrap.dedent( 

1681 """\ 

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

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

1684 """ 

1685 ), 

1686 attributes=[ 

1687 documented_attr( 

1688 ["path", "paths"], 

1689 textwrap.dedent( 

1690 """\ 

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

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

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

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

1695 are affected by the owner/mode options) 

1696 """ 

1697 ), 

1698 ), 

1699 documented_attr( 

1700 "owner", 

1701 textwrap.dedent( 

1702 """\ 

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

1704 Default is "root". 

1705 """ 

1706 ), 

1707 ), 

1708 documented_attr( 

1709 "group", 

1710 textwrap.dedent( 

1711 """\ 

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

1713 Default is "root". 

1714 """ 

1715 ), 

1716 ), 

1717 documented_attr( 

1718 "mode", 

1719 textwrap.dedent( 

1720 """\ 

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

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

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

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

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

1726 transformation. The default is "0755". 

1727 """ 

1728 ), 

1729 ), 

1730 documented_attr( 

1731 "when", 

1732 textwrap.dedent( 

1733 """\ 

1734 A condition as defined in [Conditional rules]({MANIFEST_FORMAT_DOC}#Conditional rules). 

1735 """ 

1736 ), 

1737 ), 

1738 ], 

1739 reference_documentation_url=_manifest_format_doc( 

1740 "create-directories-transformation-rule-directories" 

1741 ), 

1742 ), 

1743 ) 

1744 

1745 

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

1747 api.provide_manifest_keyword( 1747 ↛ exitline 1747 didn't jump to the function exit

1748 ManifestCondition, 

1749 "cross-compiling", 

1750 lambda *_: ManifestCondition.is_cross_building(), 

1751 inline_reference_documentation=reference_documentation( 

1752 title="Cross-Compiling condition `cross-compiling`", 

1753 description=textwrap.dedent( 

1754 """\ 

1755 The `cross-compiling` condition is used to determine if the current build is 

1756 performing a cross build (i.e., `DEB_BUILD_GNU_TYPE` != `DEB_HOST_GNU_TYPE`). 

1757 Often this has consequences for what is possible to do. 

1758 

1759 Note if you specifically want to know: 

1760 

1761 * whether build-time tests should be run, then please use the 

1762 `run-build-time-tests` condition. 

1763 * whether compiled binaries can be run as if it was a native binary, please 

1764 use the `can-execute-compiled-binaries` condition instead. That condition 

1765 accounts for cross-building in its evaluation. 

1766 """ 

1767 ), 

1768 reference_documentation_url=_manifest_format_doc( 

1769 "cross-compiling-condition-cross-compiling-string" 

1770 ), 

1771 ), 

1772 ) 

1773 api.provide_manifest_keyword( 1773 ↛ exitline 1773 didn't jump to the function exit

1774 ManifestCondition, 

1775 "can-execute-compiled-binaries", 

1776 lambda *_: ManifestCondition.can_execute_compiled_binaries(), 

1777 inline_reference_documentation=reference_documentation( 

1778 title="Can run produced binaries `can-execute-compiled-binaries`", 

1779 description=textwrap.dedent( 

1780 """\ 

1781 The `can-execute-compiled-binaries` condition is used to assert the build 

1782 can assume that all compiled binaries can be run as-if they were native 

1783 binaries. For native builds, this condition always evaluates to `true`. 

1784 For cross builds, the condition is generally evaluates to `false`. However, 

1785 there are special-cases where binaries can be run during cross-building. 

1786 Accordingly, this condition is subtly different from the `cross-compiling` 

1787 condition. 

1788 

1789 Note this condition should *not* be used when you know the binary has been 

1790 built for the build architecture (`DEB_BUILD_ARCH`) or for determining 

1791 whether build-time tests should be run (for build-time tests, please use 

1792 the `run-build-time-tests` condition instead). Some upstream build systems 

1793 are advanced enough to distinguish building a final product vs. building 

1794 a helper tool that needs to run during build. The latter will often be 

1795 compiled by a separate compiler (often using `$(CC_FOR_BUILD)`, 

1796 `cc_for_build` or similar variable names in upstream build systems for 

1797 that compiler). 

1798 """ 

1799 ), 

1800 reference_documentation_url=_manifest_format_doc( 

1801 "can-run-produced-binaries-can-execute-compiled-binaries-string" 

1802 ), 

1803 ), 

1804 ) 

1805 api.provide_manifest_keyword( 1805 ↛ exitline 1805 didn't jump to the function exit

1806 ManifestCondition, 

1807 "run-build-time-tests", 

1808 lambda *_: ManifestCondition.run_build_time_tests(), 

1809 inline_reference_documentation=reference_documentation( 

1810 title="Whether build time tests should be run `run-build-time-tests`", 

1811 description=textwrap.dedent( 

1812 """\ 

1813 The `run-build-time-tests` condition is used to determine whether (build 

1814 time) tests should be run for this build. This condition roughly 

1815 translates into whether `nocheck` is present in `DEB_BUILD_OPTIONS`. 

1816 

1817 In general, the manifest *should not* prevent build time tests from being 

1818 run during cross-builds. 

1819 """ 

1820 ), 

1821 reference_documentation_url=_manifest_format_doc( 

1822 "whether-build-time-tests-should-be-run-run-build-time-tests-string" 

1823 ), 

1824 ), 

1825 ) 

1826 

1827 api.pluggable_manifest_rule( 

1828 ManifestCondition, 

1829 "not", 

1830 MCNot, 

1831 _mc_not, 

1832 inline_reference_documentation=reference_documentation( 

1833 title="Negated condition `not` (mapping)", 

1834 description=textwrap.dedent( 

1835 """\ 

1836 It is possible to negate a condition via the `not` condition. 

1837 

1838 As an example: 

1839 

1840 packages: 

1841 util-linux: 

1842 transformations: 

1843 - create-symlink 

1844 path: sbin/getty 

1845 target: /sbin/agetty 

1846 when: 

1847 # On Hurd, the package "hurd" ships "sbin/getty". 

1848 # This example happens to also be alternative to `arch-marches: '!hurd-any` 

1849 not: 

1850 arch-matches: 'hurd-any' 

1851 

1852 The `not` condition is specified as a mapping, where the key is `not` and the 

1853 value is a nested condition. 

1854 """ 

1855 ), 

1856 attributes=[ 

1857 documented_attr( 

1858 "negated_condition", 

1859 textwrap.dedent( 

1860 """\ 

1861 The condition to be negated. 

1862 """ 

1863 ), 

1864 ), 

1865 ], 

1866 reference_documentation_url=_manifest_format_doc( 

1867 "whether-build-time-tests-should-be-run-run-build-time-tests-string" 

1868 ), 

1869 ), 

1870 ) 

1871 api.pluggable_manifest_rule( 

1872 ManifestCondition, 

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

1874 MCAnyOfAllOf, 

1875 _mc_any_of, 

1876 source_format=List[ManifestCondition], 

1877 inline_reference_documentation=reference_documentation( 

1878 title="All or any of a list of conditions `all-of`/`any-of`", 

1879 description=textwrap.dedent( 

1880 """\ 

1881 It is possible to aggregate conditions using the `all-of` or `any-of` 

1882 condition. This provide `X and Y` and `X or Y` semantics (respectively). 

1883 """ 

1884 ), 

1885 reference_documentation_url=_manifest_format_doc( 

1886 "all-or-any-of-a-list-of-conditions-all-ofany-of-list" 

1887 ), 

1888 ), 

1889 ) 

1890 api.pluggable_manifest_rule( 

1891 ManifestCondition, 

1892 "arch-matches", 

1893 MCArchMatches, 

1894 _mc_arch_matches, 

1895 source_format=str, 

1896 inline_reference_documentation=reference_documentation( 

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

1898 description=textwrap.dedent( 

1899 """\ 

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

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

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

1903 and practically behaves like a comparison against 

1904 `dpkg-architecture -qDEB_HOST_ARCH`. 

1905 

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

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

1908 in the context of a binary package and like 

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

1910 are covered in their own keywords. 

1911 """ 

1912 ), 

1913 non_mapping_description=textwrap.dedent( 

1914 """\ 

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

1916 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1921 have it. 

1922 """ 

1923 ), 

1924 reference_documentation_url=_manifest_format_doc( 

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

1926 ), 

1927 ), 

1928 ) 

1929 

1930 context_arch_doc = reference_documentation( 

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

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

1933 description=textwrap.dedent( 

1934 """\ 

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

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

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

1938 `arch-matches` condition. 

1939 

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

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

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

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

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

1945 to the packager that condition does not make sense. 

1946 

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

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

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

1950 very special cases). 

1951 

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

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

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

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

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

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

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

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

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

1961 need to care about nor use any of this. 

1962 

1963 Accordingly, the possible conditions are: 

1964 

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

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

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

1968 

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

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

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

1972 

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

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

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

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

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

1978 (`dpkg-architecture -qDEB_HOST_ARCH`). 

1979 

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

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

1982 

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

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

1985 """ 

1986 ), 

1987 non_mapping_description=textwrap.dedent( 

1988 """\ 

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

1990 architecture names or architecture wildcards (same syntax as the 

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

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

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

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

1995 have it. 

1996 """ 

1997 ), 

1998 ) 

1999 

2000 api.pluggable_manifest_rule( 

2001 ManifestCondition, 

2002 "source-context-arch-matches", 

2003 MCArchMatches, 

2004 _mc_source_context_arch_matches, 

2005 source_format=str, 

2006 inline_reference_documentation=context_arch_doc, 

2007 ) 

2008 api.pluggable_manifest_rule( 

2009 ManifestCondition, 

2010 "package-context-arch-matches", 

2011 MCArchMatches, 

2012 _mc_arch_matches, 

2013 source_format=str, 

2014 inline_reference_documentation=context_arch_doc, 

2015 ) 

2016 api.pluggable_manifest_rule( 

2017 ManifestCondition, 

2018 "build-profiles-matches", 

2019 MCBuildProfileMatches, 

2020 _mc_build_profile_matches, 

2021 source_format=str, 

2022 inline_reference_documentation=reference_documentation( 

2023 title="Active build profile match condition `build-profiles-matches`", 

2024 description=textwrap.dedent( 

2025 """\ 

2026 The `build-profiles-matches` condition is used to assert whether the 

2027 active build profiles (`DEB_BUILD_PROFILES` / `dpkg-buildpackage -P`) 

2028 matches a given build profile restriction. 

2029 """ 

2030 ), 

2031 non_mapping_description=textwrap.dedent( 

2032 """\ 

2033 The value is a string using the same syntax as the `Build-Profiles` 

2034 field from `debian/control` (i.e., a space separated list of 

2035 `<[!]profile ...>` groups). 

2036 """ 

2037 ), 

2038 reference_documentation_url=_manifest_format_doc( 

2039 "active-build-profile-match-condition-build-profiles-matches-mapping" 

2040 ), 

2041 ), 

2042 ) 

2043 

2044 

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

2046 api.pluggable_manifest_rule( 

2047 DpkgMaintscriptHelperCommand, 

2048 "remove", 

2049 DpkgRemoveConffileRule, 

2050 _dpkg_conffile_remove, 

2051 inline_reference_documentation=None, # TODO: write and add 

2052 ) 

2053 

2054 api.pluggable_manifest_rule( 

2055 DpkgMaintscriptHelperCommand, 

2056 "rename", 

2057 DpkgRenameConffileRule, 

2058 _dpkg_conffile_rename, 

2059 inline_reference_documentation=None, # TODO: write and add 

2060 ) 

2061 

2062 

2063class _ModeOwnerBase(DebputyParsedContentStandardConditional): 

2064 mode: NotRequired[FileSystemMode] 

2065 owner: NotRequired[StaticFileSystemOwner] 

2066 group: NotRequired[StaticFileSystemGroup] 

2067 

2068 

2069class PathManifestSourceDictFormat(_ModeOwnerBase): 

2070 path: NotRequired[ 

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

2072 ] 

2073 paths: NotRequired[List[FileSystemMatchRule]] 

2074 recursive: NotRequired[bool] 

2075 capabilities: NotRequired[str] 

2076 capability_mode: NotRequired[FileSystemMode] 

2077 

2078 

2079class PathManifestRule(_ModeOwnerBase): 

2080 paths: List[FileSystemMatchRule] 

2081 recursive: NotRequired[bool] 

2082 capabilities: NotRequired[str] 

2083 capability_mode: NotRequired[FileSystemMode] 

2084 

2085 

2086class EnsureDirectorySourceFormat(_ModeOwnerBase): 

2087 path: NotRequired[ 

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

2089 ] 

2090 paths: NotRequired[List[FileSystemExactMatchRule]] 

2091 

2092 

2093class EnsureDirectoryRule(_ModeOwnerBase): 

2094 paths: List[FileSystemExactMatchRule] 

2095 

2096 

2097class CreateSymlinkRule(DebputyParsedContentStandardConditional): 

2098 path: FileSystemExactMatchRule 

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

2100 replacement_rule: NotRequired[CreateSymlinkReplacementRule] 

2101 

2102 

2103class TransformationMoveRuleSpec(DebputyParsedContentStandardConditional): 

2104 source: FileSystemMatchRule 

2105 target: FileSystemExactMatchRule 

2106 

2107 

2108class TransformationRemoveRuleSpec(DebputyParsedContentStandardConditional): 

2109 paths: List[FileSystemMatchRule] 

2110 keep_empty_parent_dirs: NotRequired[bool] 

2111 

2112 

2113class TransformationRemoveRuleInputFormat(DebputyParsedContentStandardConditional): 

2114 path: NotRequired[ 

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

2116 ] 

2117 paths: NotRequired[List[FileSystemMatchRule]] 

2118 keep_empty_parent_dirs: NotRequired[bool] 

2119 

2120 

2121class ParsedInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2122 sources: NotRequired[List[FileSystemMatchRule]] 

2123 source: NotRequired[ 

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

2125 ] 

2126 into: NotRequired[ 

2127 Annotated[ 

2128 Union[str, List[str]], 

2129 DebputyParseHint.required_when_multi_binary(), 

2130 ] 

2131 ] 

2132 dest_dir: NotRequired[ 

2133 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2134 ] 

2135 install_as: NotRequired[ 

2136 Annotated[ 

2137 FileSystemExactMatchRule, 

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

2139 DebputyParseHint.manifest_attribute("as"), 

2140 DebputyParseHint.not_path_error_hint(), 

2141 ] 

2142 ] 

2143 

2144 

2145class ParsedInstallDocRuleSourceFormat(DebputyParsedContentStandardConditional): 

2146 sources: NotRequired[List[FileSystemMatchRule]] 

2147 source: NotRequired[ 

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

2149 ] 

2150 into: NotRequired[ 

2151 Annotated[ 

2152 Union[str, List[str]], 

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

2154 ] 

2155 ] 

2156 dest_dir: NotRequired[ 

2157 Annotated[FileSystemExactMatchRule, DebputyParseHint.not_path_error_hint()] 

2158 ] 

2159 install_as: NotRequired[ 

2160 Annotated[ 

2161 FileSystemExactMatchRule, 

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

2163 DebputyParseHint.manifest_attribute("as"), 

2164 DebputyParseHint.not_path_error_hint(), 

2165 ] 

2166 ] 

2167 

2168 

2169class ParsedInstallRule(DebputyParsedContentStandardConditional): 

2170 sources: List[FileSystemMatchRule] 

2171 into: NotRequired[List[BinaryPackage]] 

2172 dest_dir: NotRequired[FileSystemExactMatchRule] 

2173 install_as: NotRequired[FileSystemExactMatchRule] 

2174 

2175 

2176class ParsedMultiDestInstallRuleSourceFormat(DebputyParsedContentStandardConditional): 

2177 sources: NotRequired[List[FileSystemMatchRule]] 

2178 source: NotRequired[ 

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

2180 ] 

2181 into: NotRequired[ 

2182 Annotated[ 

2183 Union[str, List[str]], 

2184 DebputyParseHint.required_when_multi_binary(), 

2185 ] 

2186 ] 

2187 dest_dirs: NotRequired[ 

2188 Annotated[ 

2189 List[FileSystemExactMatchRule], DebputyParseHint.not_path_error_hint() 

2190 ] 

2191 ] 

2192 install_as: NotRequired[ 

2193 Annotated[ 

2194 List[FileSystemExactMatchRule], 

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

2196 DebputyParseHint.not_path_error_hint(), 

2197 DebputyParseHint.manifest_attribute("as"), 

2198 ] 

2199 ] 

2200 

2201 

2202class ParsedMultiDestInstallRule(DebputyParsedContentStandardConditional): 

2203 sources: List[FileSystemMatchRule] 

2204 into: NotRequired[List[BinaryPackage]] 

2205 dest_dirs: NotRequired[List[FileSystemExactMatchRule]] 

2206 install_as: NotRequired[List[FileSystemExactMatchRule]] 

2207 

2208 

2209class ParsedInstallExamplesRule(DebputyParsedContentStandardConditional): 

2210 sources: List[FileSystemMatchRule] 

2211 into: NotRequired[List[BinaryPackage]] 

2212 

2213 

2214class ParsedInstallExamplesRuleSourceFormat(DebputyParsedContentStandardConditional): 

2215 sources: NotRequired[List[FileSystemMatchRule]] 

2216 source: NotRequired[ 

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

2218 ] 

2219 into: NotRequired[ 

2220 Annotated[ 

2221 Union[str, List[str]], 

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

2223 ] 

2224 ] 

2225 

2226 

2227class ParsedInstallManpageRule(DebputyParsedContentStandardConditional): 

2228 sources: List[FileSystemMatchRule] 

2229 language: NotRequired[str] 

2230 section: NotRequired[int] 

2231 into: NotRequired[List[BinaryPackage]] 

2232 

2233 

2234class ParsedInstallManpageRuleSourceFormat(DebputyParsedContentStandardConditional): 

2235 sources: NotRequired[List[FileSystemMatchRule]] 

2236 source: NotRequired[ 

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

2238 ] 

2239 language: NotRequired[str] 

2240 section: NotRequired[int] 

2241 into: NotRequired[ 

2242 Annotated[ 

2243 Union[str, List[str]], 

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

2245 ] 

2246 ] 

2247 

2248 

2249class ParsedInstallDiscardRuleSourceFormat(DebputyParsedContent): 

2250 paths: NotRequired[List[FileSystemMatchRule]] 

2251 path: NotRequired[ 

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

2253 ] 

2254 search_dir: NotRequired[ 

2255 Annotated[ 

2256 FileSystemExactMatchRule, DebputyParseHint.target_attribute("search_dirs") 

2257 ] 

2258 ] 

2259 search_dirs: NotRequired[List[FileSystemExactMatchRule]] 

2260 required_when: NotRequired[ManifestCondition] 

2261 

2262 

2263class ParsedInstallDiscardRule(DebputyParsedContent): 

2264 paths: List[FileSystemMatchRule] 

2265 search_dirs: NotRequired[List[FileSystemExactMatchRule]] 

2266 required_when: NotRequired[ManifestCondition] 

2267 

2268 

2269class DpkgConffileManagementRuleBase(DebputyParsedContent): 

2270 prior_to_version: NotRequired[str] 

2271 owning_package: NotRequired[str] 

2272 

2273 

2274class DpkgRenameConffileRule(DpkgConffileManagementRuleBase): 

2275 source: str 

2276 target: str 

2277 

2278 

2279class DpkgRemoveConffileRule(DpkgConffileManagementRuleBase): 

2280 path: str 

2281 

2282 

2283class MCAnyOfAllOf(DebputyParsedContent): 

2284 conditions: List[ManifestCondition] 

2285 

2286 

2287class MCNot(DebputyParsedContent): 

2288 negated_condition: Annotated[ 

2289 ManifestCondition, DebputyParseHint.manifest_attribute("not") 

2290 ] 

2291 

2292 

2293class MCArchMatches(DebputyParsedContent): 

2294 arch_matches: str 

2295 

2296 

2297class MCBuildProfileMatches(DebputyParsedContent): 

2298 build_profile_matches: str 

2299 

2300 

2301def _parse_filename( 

2302 filename: str, 

2303 attribute_path: AttributePath, 

2304 *, 

2305 allow_directories: bool = True, 

2306) -> str: 

2307 try: 

2308 normalized_path = _normalize_path(filename, with_prefix=False) 

2309 except ValueError as e: 

2310 raise ManifestParseException( 

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

2312 ) from None 

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

2314 raise ManifestParseException( 

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

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

2317 ) 

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

2319 raise ManifestParseException( 

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

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

2322 ) 

2323 return normalized_path 

2324 

2325 

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

2327 return Union[ 

2328 t, 

2329 List[str], 

2330 str, 

2331 ] 

2332 

2333 

2334def _dpkg_conffile_rename( 

2335 _name: str, 

2336 parsed_data: DpkgRenameConffileRule, 

2337 path: AttributePath, 

2338 _context: ParserContextData, 

2339) -> DpkgMaintscriptHelperCommand: 

2340 source_file = parsed_data["source"] 

2341 target_file = parsed_data["target"] 

2342 normalized_source = _parse_filename( 

2343 source_file, 

2344 path["source"], 

2345 allow_directories=False, 

2346 ) 

2347 path.path_hint = source_file 

2348 

2349 normalized_target = _parse_filename( 

2350 target_file, 

2351 path["target"], 

2352 allow_directories=False, 

2353 ) 

2354 normalized_source = "/" + normalized_source 

2355 normalized_target = "/" + normalized_target 

2356 

2357 if normalized_source == normalized_target: 2357 ↛ 2358line 2357 didn't jump to line 2358, because the condition on line 2357 was never true

2358 raise ManifestParseException( 

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

2360 ) 

2361 

2362 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2363 parsed_data, path 

2364 ) 

2365 return DpkgMaintscriptHelperCommand.mv_conffile( 

2366 path, 

2367 normalized_source, 

2368 normalized_target, 

2369 version, 

2370 owning_package, 

2371 ) 

2372 

2373 

2374def _dpkg_conffile_remove( 

2375 _name: str, 

2376 parsed_data: DpkgRemoveConffileRule, 

2377 path: AttributePath, 

2378 _context: ParserContextData, 

2379) -> DpkgMaintscriptHelperCommand: 

2380 source_file = parsed_data["path"] 

2381 normalized_source = _parse_filename( 

2382 source_file, 

2383 path["path"], 

2384 allow_directories=False, 

2385 ) 

2386 path.path_hint = source_file 

2387 

2388 normalized_source = "/" + normalized_source 

2389 

2390 version, owning_package = _parse_conffile_prior_version_and_owning_package( 

2391 parsed_data, path 

2392 ) 

2393 return DpkgMaintscriptHelperCommand.rm_conffile( 

2394 path, 

2395 normalized_source, 

2396 version, 

2397 owning_package, 

2398 ) 

2399 

2400 

2401def _parse_conffile_prior_version_and_owning_package( 

2402 d: DpkgConffileManagementRuleBase, 

2403 attribute_path: AttributePath, 

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

2405 prior_version = d.get("prior_to_version") 

2406 owning_package = d.get("owning_package") 

2407 

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

2409 p = attribute_path["prior_to_version"] 

2410 raise ManifestParseException( 

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

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

2413 ) 

2414 

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

2416 p = attribute_path["owning_package"] 

2417 raise ManifestParseException( 

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

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

2420 ) 

2421 

2422 return prior_version, owning_package 

2423 

2424 

2425def _install_rule_handler( 

2426 _name: str, 

2427 parsed_data: ParsedInstallRule, 

2428 path: AttributePath, 

2429 context: ParserContextData, 

2430) -> InstallRule: 

2431 sources = parsed_data["sources"] 

2432 install_as = parsed_data.get("install_as") 

2433 into = parsed_data.get("into") 

2434 dest_dir = parsed_data.get("dest_dir") 

2435 condition = parsed_data.get("when") 

2436 if not into: 

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

2438 into = frozenset(into) 

2439 if install_as is not None: 

2440 assert len(sources) == 1 

2441 assert dest_dir is None 

2442 return InstallRule.install_as( 

2443 sources[0], 

2444 install_as.match_rule.path, 

2445 into, 

2446 path.path, 

2447 condition, 

2448 ) 

2449 return InstallRule.install_dest( 

2450 sources, 

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

2452 into, 

2453 path.path, 

2454 condition, 

2455 ) 

2456 

2457 

2458def _multi_dest_install_rule_handler( 

2459 _name: str, 

2460 parsed_data: ParsedMultiDestInstallRule, 

2461 path: AttributePath, 

2462 context: ParserContextData, 

2463) -> InstallRule: 

2464 sources = parsed_data["sources"] 

2465 install_as = parsed_data.get("install_as") 

2466 into = parsed_data.get("into") 

2467 dest_dirs = parsed_data.get("dest_dirs") 

2468 condition = parsed_data.get("when") 

2469 if not into: 2469 ↛ 2471line 2469 didn't jump to line 2471, because the condition on line 2469 was never false

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

2471 into = frozenset(into) 

2472 if install_as is not None: 

2473 assert len(sources) == 1 

2474 assert dest_dirs is None 

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

2476 raise ManifestParseException( 

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

2478 ) 

2479 return InstallRule.install_multi_as( 

2480 sources[0], 

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

2482 into, 

2483 path.path, 

2484 condition, 

2485 ) 

2486 if dest_dirs is None: 2486 ↛ 2487line 2486 didn't jump to line 2487, because the condition on line 2486 was never true

2487 raise ManifestParseException( 

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

2489 ) 

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

2491 raise ManifestParseException( 

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

2493 ) 

2494 return InstallRule.install_multi_dest( 

2495 sources, 

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

2497 into, 

2498 path.path, 

2499 condition, 

2500 ) 

2501 

2502 

2503def _install_docs_rule_handler( 

2504 _name: str, 

2505 parsed_data: ParsedInstallRule, 

2506 path: AttributePath, 

2507 context: ParserContextData, 

2508) -> InstallRule: 

2509 sources = parsed_data["sources"] 

2510 install_as = parsed_data.get("install_as") 

2511 into = parsed_data.get("into") 

2512 dest_dir = parsed_data.get("dest_dir") 

2513 condition = parsed_data.get("when") 

2514 if not into: 2514 ↛ 2520line 2514 didn't jump to line 2520, because the condition on line 2514 was never false

2515 into = [ 

2516 context.single_binary_package( 

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

2518 ) 

2519 ] 

2520 into = frozenset(into) 

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

2522 assert len(sources) == 1 

2523 assert dest_dir is None 

2524 return InstallRule.install_doc_as( 

2525 sources[0], 

2526 install_as.match_rule.path, 

2527 into, 

2528 path.path, 

2529 condition, 

2530 ) 

2531 return InstallRule.install_doc( 

2532 sources, 

2533 dest_dir, 

2534 into, 

2535 path.path, 

2536 condition, 

2537 ) 

2538 

2539 

2540def _install_examples_rule_handler( 

2541 _name: str, 

2542 parsed_data: ParsedInstallExamplesRule, 

2543 path: AttributePath, 

2544 context: ParserContextData, 

2545) -> InstallRule: 

2546 sources = parsed_data["sources"] 

2547 into = parsed_data.get("into") 

2548 if not into: 2548 ↛ 2554line 2548 didn't jump to line 2554, because the condition on line 2548 was never false

2549 into = [ 

2550 context.single_binary_package( 

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

2552 ) 

2553 ] 

2554 condition = parsed_data.get("when") 

2555 into = frozenset(into) 

2556 return InstallRule.install_examples( 

2557 sources, 

2558 into, 

2559 path.path, 

2560 condition, 

2561 ) 

2562 

2563 

2564def _install_man_rule_handler( 

2565 _name: str, 

2566 parsed_data: ParsedInstallManpageRule, 

2567 attribute_path: AttributePath, 

2568 context: ParserContextData, 

2569) -> InstallRule: 

2570 sources = parsed_data["sources"] 

2571 language = parsed_data.get("language") 

2572 section = parsed_data.get("section") 

2573 

2574 if language is not None: 

2575 is_lang_ok = language in ( 

2576 "C", 

2577 "derive-from-basename", 

2578 "derive-from-path", 

2579 ) 

2580 

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

2582 is_lang_ok = True 

2583 

2584 if ( 2584 ↛ 2591line 2584 didn't jump to line 2591

2585 not is_lang_ok 

2586 and len(language) == 5 

2587 and language[2] == "_" 

2588 and language[:2].islower() 

2589 and language[3:].isupper() 

2590 ): 

2591 is_lang_ok = True 

2592 

2593 if not is_lang_ok: 2593 ↛ 2594line 2593 didn't jump to line 2594, because the condition on line 2593 was never true

2594 raise ManifestParseException( 

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

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

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

2598 ) 

2599 

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

2601 raise ManifestParseException( 

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

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

2604 ) 

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

2606 raise ManifestParseException( 

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

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

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

2610 ) 

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

2612 raise ManifestParseException( 

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

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

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

2616 ) 

2617 into = parsed_data.get("into") 

2618 if not into: 2618 ↛ 2624line 2618 didn't jump to line 2624, because the condition on line 2618 was never false

2619 into = [ 

2620 context.single_binary_package( 

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

2622 ) 

2623 ] 

2624 condition = parsed_data.get("when") 

2625 into = frozenset(into) 

2626 return InstallRule.install_man( 

2627 sources, 

2628 into, 

2629 section, 

2630 language, 

2631 attribute_path.path, 

2632 condition, 

2633 ) 

2634 

2635 

2636def _install_discard_rule_handler( 

2637 _name: str, 

2638 parsed_data: ParsedInstallDiscardRule, 

2639 path: AttributePath, 

2640 _context: ParserContextData, 

2641) -> InstallRule: 

2642 limit_to = parsed_data.get("search_dirs") 

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

2644 p = path["search_dirs"] 

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

2646 condition = parsed_data.get("required_when") 

2647 return InstallRule.discard_paths( 

2648 parsed_data["paths"], 

2649 path.path, 

2650 condition, 

2651 limit_to=limit_to, 

2652 ) 

2653 

2654 

2655def _transformation_move_handler( 

2656 _name: str, 

2657 parsed_data: TransformationMoveRuleSpec, 

2658 path: AttributePath, 

2659 _context: ParserContextData, 

2660) -> TransformationRule: 

2661 source_match = parsed_data["source"] 

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

2663 condition = parsed_data.get("when") 

2664 

2665 if ( 2665 ↛ 2669line 2665 didn't jump to line 2669

2666 isinstance(source_match, ExactFileSystemPath) 

2667 and source_match.path == target_path 

2668 ): 

2669 raise ManifestParseException( 

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

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

2672 ) 

2673 return MoveTransformationRule( 

2674 source_match.match_rule, 

2675 target_path, 

2676 target_path.endswith("/"), 

2677 path, 

2678 condition, 

2679 ) 

2680 

2681 

2682def _transformation_remove_handler( 

2683 _name: str, 

2684 parsed_data: TransformationRemoveRuleSpec, 

2685 attribute_path: AttributePath, 

2686 _context: ParserContextData, 

2687) -> TransformationRule: 

2688 paths = parsed_data["paths"] 

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

2690 

2691 return RemoveTransformationRule( 

2692 [m.match_rule for m in paths], 

2693 keep_empty_parent_dirs, 

2694 attribute_path, 

2695 ) 

2696 

2697 

2698def _transformation_create_symlink( 

2699 _name: str, 

2700 parsed_data: CreateSymlinkRule, 

2701 attribute_path: AttributePath, 

2702 _context: ParserContextData, 

2703) -> TransformationRule: 

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

2705 replacement_rule: CreateSymlinkReplacementRule = parsed_data.get( 

2706 "replacement_rule", 

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

2708 ) 

2709 try: 

2710 link_target = debian_policy_normalize_symlink_target( 

2711 link_dest, 

2712 parsed_data["target"].symlink_target, 

2713 ) 

2714 except ValueError as e: # pragma: no cover 

2715 raise AssertionError( 

2716 "Debian Policy normalization should not raise ValueError here" 

2717 ) from e 

2718 

2719 condition = parsed_data.get("when") 

2720 

2721 return CreateSymlinkPathTransformationRule( 

2722 link_target, 

2723 link_dest, 

2724 replacement_rule, 

2725 attribute_path, 

2726 condition, 

2727 ) 

2728 

2729 

2730def _transformation_path_metadata( 

2731 _name: str, 

2732 parsed_data: PathManifestRule, 

2733 attribute_path: AttributePath, 

2734 _context: ParserContextData, 

2735) -> TransformationRule: 

2736 match_rules = parsed_data["paths"] 

2737 owner = parsed_data.get("owner") 

2738 group = parsed_data.get("group") 

2739 mode = parsed_data.get("mode") 

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

2741 capabilities = parsed_data.get("capabilities") 

2742 capability_mode = parsed_data.get("capability_mode") 

2743 

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

2745 if capability_mode is None: 

2746 capability_mode = SymbolicMode.parse_filesystem_mode( 

2747 "a-s", 

2748 attribute_path["capability-mode"], 

2749 ) 

2750 validate_cap = check_cap_checker() 

2751 validate_cap(capabilities, attribute_path["capabilities"].path) 

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

2753 raise ManifestParseException( 

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

2755 f" in {attribute_path.path}" 

2756 ) 

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

2758 raise ManifestParseException( 

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

2760 f" in {attribute_path.path}" 

2761 ) 

2762 condition = parsed_data.get("when") 

2763 

2764 return PathMetadataTransformationRule( 

2765 [m.match_rule for m in match_rules], 

2766 owner, 

2767 group, 

2768 mode, 

2769 recursive, 

2770 capabilities, 

2771 capability_mode, 

2772 attribute_path.path, 

2773 condition, 

2774 ) 

2775 

2776 

2777def _transformation_mkdirs( 

2778 _name: str, 

2779 parsed_data: EnsureDirectoryRule, 

2780 attribute_path: AttributePath, 

2781 _context: ParserContextData, 

2782) -> TransformationRule: 

2783 provided_paths = parsed_data["paths"] 

2784 owner = parsed_data.get("owner") 

2785 group = parsed_data.get("group") 

2786 mode = parsed_data.get("mode") 

2787 

2788 condition = parsed_data.get("when") 

2789 

2790 return CreateDirectoryTransformationRule( 

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

2792 owner, 

2793 group, 

2794 mode, 

2795 attribute_path.path, 

2796 condition, 

2797 ) 

2798 

2799 

2800def _at_least_two( 

2801 content: List[Any], 

2802 attribute_path: AttributePath, 

2803 attribute_name: str, 

2804) -> None: 

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

2806 raise ManifestParseException( 

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

2808 ) 

2809 

2810 

2811def _mc_any_of( 

2812 name: str, 

2813 parsed_data: MCAnyOfAllOf, 

2814 attribute_path: AttributePath, 

2815 _context: ParserContextData, 

2816) -> ManifestCondition: 

2817 conditions = parsed_data["conditions"] 

2818 _at_least_two(conditions, attribute_path, "conditions") 

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

2820 return ManifestCondition.any_of(conditions) 

2821 assert name == "all-of" 

2822 return ManifestCondition.all_of(conditions) 

2823 

2824 

2825def _mc_not( 

2826 _name: str, 

2827 parsed_data: MCNot, 

2828 _attribute_path: AttributePath, 

2829 _context: ParserContextData, 

2830) -> ManifestCondition: 

2831 condition = parsed_data["negated_condition"] 

2832 return condition.negated() 

2833 

2834 

2835def _extract_arch_matches( 

2836 parsed_data: MCArchMatches, 

2837 attribute_path: AttributePath, 

2838) -> List[str]: 

2839 arch_matches_as_str = parsed_data["arch_matches"] 

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

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

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

2843 # of each other. 

2844 arch_matches_as_list = arch_matches_as_str.split() 

2845 attr_path = attribute_path["arch_matches"] 

2846 if not arch_matches_as_list: 2846 ↛ 2847line 2846 didn't jump to line 2847, because the condition on line 2846 was never true

2847 raise ManifestParseException( 

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

2849 ) 

2850 

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

2852 "]" 

2853 ): 

2854 raise ManifestParseException( 

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

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

2857 ) 

2858 return arch_matches_as_list 

2859 

2860 

2861def _mc_source_context_arch_matches( 

2862 _name: str, 

2863 parsed_data: MCArchMatches, 

2864 attribute_path: AttributePath, 

2865 _context: ParserContextData, 

2866) -> ManifestCondition: 

2867 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2868 return SourceContextArchMatchManifestCondition(arch_matches) 

2869 

2870 

2871def _mc_package_context_arch_matches( 

2872 name: str, 

2873 parsed_data: MCArchMatches, 

2874 attribute_path: AttributePath, 

2875 context: ParserContextData, 

2876) -> ManifestCondition: 

2877 arch_matches = _extract_arch_matches(parsed_data, attribute_path) 

2878 

2879 if not context.is_in_binary_package_state: 

2880 raise ManifestParseException( 

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

2882 ) 

2883 

2884 package_state = context.current_binary_package_state 

2885 if package_state.binary_package.is_arch_all: 

2886 result = context.dpkg_arch_query_table.architecture_is_concerned( 

2887 "all", arch_matches 

2888 ) 

2889 attr_path = attribute_path["arch_matches"] 

2890 raise ManifestParseException( 

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

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

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

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

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

2896 ) 

2897 return BinaryPackageContextArchMatchManifestCondition(arch_matches) 

2898 

2899 

2900def _mc_arch_matches( 

2901 name: str, 

2902 parsed_data: MCArchMatches, 

2903 attribute_path: AttributePath, 

2904 context: ParserContextData, 

2905) -> ManifestCondition: 

2906 if context.is_in_binary_package_state: 2906 ↛ 2907line 2906 didn't jump to line 2907, because the condition on line 2906 was never true

2907 return _mc_package_context_arch_matches( 

2908 name, parsed_data, attribute_path, context 

2909 ) 

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

2911 

2912 

2913def _mc_build_profile_matches( 

2914 _name: str, 

2915 parsed_data: MCBuildProfileMatches, 

2916 attribute_path: AttributePath, 

2917 _context: ParserContextData, 

2918) -> ManifestCondition: 

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

2920 attr_path = attribute_path["build_profile_matches"] 

2921 if not build_profile_spec: 2921 ↛ 2922line 2921 didn't jump to line 2922, because the condition on line 2921 was never true

2922 raise ManifestParseException( 

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

2924 ) 

2925 try: 

2926 active_profiles_match(build_profile_spec, frozenset()) 

2927 except ValueError as e: 

2928 raise ManifestParseException( 

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

2930 ) 

2931 return BuildProfileMatch(build_profile_spec)