Coverage for src/debputy/highlevel_manifest.py: 63%

904 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-07-22 10:58 +0000

1import dataclasses 

2import functools 

3import os 

4import textwrap 

5import typing 

6from contextlib import suppress 

7from dataclasses import dataclass, field 

8from typing import ( 

9 Any, 

10 TypeVar, 

11 Generic, 

12 cast, 

13) 

14from collections.abc import Iterable, Mapping, Sequence, Callable 

15 

16from debian.debian_support import DpkgArchTable 

17 

18from debputy.dh.debhelper_emulation import ( 

19 dhe_dbgsym_root_dir, 

20 assert_no_dbgsym_migration, 

21 read_dbgsym_file, 

22) 

23from ._deb_options_profiles import DebBuildOptionsAndProfiles 

24from ._manifest_constants import * 

25from .architecture_support import DpkgArchitectureBuildProcessValuesTable 

26from .builtin_manifest_rules import builtin_mode_normalization_rules 

27from .exceptions import ( 

28 DebputySubstitutionError, 

29 DebputyRuntimeErrorWithPreamble, 

30) 

31from .filesystem_scan import ( 

32 InMemoryVirtualRootDir, 

33 OSFSROOverlay, 

34 FSControlRootDir, 

35 InMemoryVirtualPathBase, 

36 InMemoryOverlayFSRootDirectory, 

37) 

38from .installations import ( 

39 InstallRule, 

40 SourcePathMatcher, 

41 PathAlreadyInstalledOrDiscardedError, 

42 NoMatchForInstallPatternError, 

43 InstallRuleContext, 

44 BinaryPackageInstallRuleContext, 

45 InstallSearchDirContext, 

46 SearchDir, 

47) 

48from .intermediate_manifest import TarMember, PathType, IntermediateManifest 

49from .maintscript_snippet import ( 

50 DpkgMaintscriptHelperCommand, 

51 PackageMaintscriptSnippetContainer, 

52) 

53from .manifest_conditions import ConditionContext 

54from .manifest_parser.base_types import ( 

55 FileSystemMatchRule, 

56 FileSystemExactMatchRule, 

57 BuildEnvironments, 

58) 

59from .manifest_parser.util import AttributePath 

60from .packager_provided_files import PackagerProvidedFile 

61from .packages import BinaryPackage, SourcePackage 

62from .plugin.api.feature_set import PluginProvidedFeatureSet 

63from .plugin.api.impl import BinaryCtrlAccessorProviderCreator 

64from .plugin.api.impl_types import ( 

65 PackageProcessingContextProvider, 

66 PackageDataTable, 

67) 

68from .plugin.api.spec import ( 

69 FlushableSubstvars, 

70 VirtualPath, 

71 DebputyIntegrationMode, 

72 INTEGRATION_MODE_DH_DEBPUTY_RRR, 

73 INTEGRATION_MODE_FULL, 

74) 

75from debputy.plugins.debputy.binary_package_rules import ServiceRule 

76from debputy.plugins.debputy.build_system_rules import BuildRule 

77from .plugin.plugin_state import run_in_context_of_plugin 

78from .substitution import Substitution 

79from .transformation_rules import ( 

80 TransformationRule, 

81 ModeNormalizationTransformationRule, 

82 NormalizeShebangLineTransformation, 

83) 

84from .util import ( 

85 _error, 

86 _warn, 

87 debian_policy_normalize_symlink_target, 

88 generated_content_dir, 

89 _info, 

90 assume_not_none, 

91) 

92from .yaml import MANIFEST_YAML 

93from .yaml.compat import CommentedMap, CommentedSeq 

94 

95 

96def tar_path(p: VirtualPath) -> str: 

97 path = p.path 

98 if p.is_dir: 

99 return path + "/" 

100 return path 

101 

102 

103def tar_owner_info(path: InMemoryVirtualPathBase) -> tuple[str, int, str, int]: 

104 owner = path._owner # noqa 

105 group = path._group # noqa 

106 return ( 

107 owner.entity_name, 

108 owner.entity_id, 

109 group.entity_name, 

110 group.entity_id, 

111 ) 

112 

113 

114class PathNotCoveredByInstallRulesError(DebputyRuntimeErrorWithPreamble): 

115 

116 @property 

117 def unmatched_paths(self) -> Sequence[VirtualPath]: 

118 return self.args[1] 

119 

120 @property 

121 def search_dir(self) -> VirtualPath: 

122 return self.args[2] 

123 

124 def render_preamble(self) -> None: 

125 _warn( 

126 f"The following paths were present in {self.search_dir.fs_path}, but not installed (nor explicitly discarded)." 

127 ) 

128 _warn("") 

129 for entry in self.unmatched_paths: 

130 desc = _describe_missing_path(entry) 

131 _warn(f" * {desc}") 

132 _warn("") 

133 

134 

135@dataclass(slots=True) 

136class DbgsymInfo: 

137 binary_package: BinaryPackage 

138 dbgsym_fs_root: InMemoryVirtualPathBase 

139 _dbgsym_root_fs: str | None 

140 dbgsym_ids: list[str] 

141 run_dwz: bool 

142 

143 @property 

144 def dbgsym_root_dir(self) -> str: 

145 root_dir = self._dbgsym_root_fs 

146 if root_dir is None: 

147 root_dir = generated_content_dir( 

148 package=self.binary_package, 

149 subdir_key="dbgsym-fs-root", 

150 ) 

151 self._dbgsym_root_fs = root_dir 

152 return root_dir 

153 

154 @property 

155 def dbgsym_ctrl_dir(self) -> FSControlRootDir: 

156 return FSControlRootDir.create_root_dir( 

157 os.path.join(self.dbgsym_root_dir, "DEBIAN") 

158 ) 

159 

160 

161@dataclass(slots=True, frozen=True) 

162class BinaryPackageData: 

163 source_package: SourcePackage 

164 binary_package: BinaryPackage 

165 binary_staging_root_dir: str 

166 fs_root: InMemoryVirtualPathBase 

167 substvars: FlushableSubstvars 

168 package_metadata_context: PackageProcessingContextProvider 

169 ctrl_creator: BinaryCtrlAccessorProviderCreator 

170 dbgsym_info: DbgsymInfo 

171 

172 @property 

173 def control_output_dir(self) -> FSControlRootDir: 

174 return FSControlRootDir.create_root_dir( 

175 generated_content_dir( 

176 package=self.binary_package, 

177 subdir_key="DEBIAN", 

178 ) 

179 ) 

180 

181 

182@dataclass(slots=True) 

183class PackageTransformationDefinition: 

184 binary_package: BinaryPackage 

185 substitution: Substitution 

186 is_auto_generated_package: bool 

187 binary_version: str | None = None 

188 search_dirs: list[FileSystemExactMatchRule] | None = None 

189 dpkg_maintscript_helper_snippets: list[DpkgMaintscriptHelperCommand] = field( 

190 default_factory=list 

191 ) 

192 maintscript_snippets: PackageMaintscriptSnippetContainer = field( 

193 default_factory=PackageMaintscriptSnippetContainer 

194 ) 

195 transformations: list[TransformationRule] = field(default_factory=list) 

196 reserved_packager_provided_files: dict[str, list[PackagerProvidedFile]] = field( 

197 default_factory=dict 

198 ) 

199 install_rules: list[InstallRule] = field(default_factory=list) 

200 requested_service_rules: list[ServiceRule] = field(default_factory=list) 

201 

202 

203def _path_to_tar_member( 

204 path: InMemoryVirtualPathBase, 

205 clamp_mtime_to: int, 

206) -> TarMember: 

207 mtime = float(clamp_mtime_to) 

208 owner, uid, group, gid = tar_owner_info(path) 

209 mode = path.mode 

210 

211 if path.has_fs_path: 

212 mtime = min(mtime, path.mtime) 

213 

214 if path.is_dir: 

215 path_type = PathType.DIRECTORY 

216 elif path.is_file: 

217 # TODO: someday we will need to deal with hardlinks and it might appear here. 

218 path_type = PathType.FILE 

219 elif path.is_symlink: 219 ↛ 239line 219 didn't jump to line 239 because the condition on line 219 was always true

220 # Special-case that we resolve immediately (since we need to normalize the target anyway) 

221 link_target = debian_policy_normalize_symlink_target( 

222 path.path, 

223 path.readlink(), 

224 ) 

225 return TarMember.virtual_path( 

226 tar_path(path), 

227 PathType.SYMLINK, 

228 mtime, 

229 link_target=link_target, 

230 # Force mode to be 0777 as that is the mode we see in the data.tar. In theory, tar lets you set 

231 # it to whatever. However, for reproducibility, we have to be well-behaved - and that is 0777. 

232 mode=0o0777, 

233 owner=owner, 

234 uid=uid, 

235 group=group, 

236 gid=gid, 

237 ) 

238 else: 

239 assert not path.is_symlink 

240 raise AssertionError( 

241 f"Unsupported file type: {path.path} - not a file, dir nor a symlink!" 

242 ) 

243 

244 if not path.has_fs_path: 

245 assert not path.is_file 

246 return TarMember.virtual_path( 

247 tar_path(path), 

248 path_type, 

249 mtime, 

250 mode=mode, 

251 owner=owner, 

252 uid=uid, 

253 group=group, 

254 gid=gid, 

255 ) 

256 may_steal_fs_path = getattr(path, "_can_replace_inline", False) 

257 return TarMember.from_file( 

258 tar_path(path), 

259 path.fs_path, 

260 mode=mode, 

261 uid=uid, 

262 owner=owner, 

263 gid=gid, 

264 group=group, 

265 path_type=path_type, 

266 path_mtime=mtime, 

267 clamp_mtime_to=clamp_mtime_to, 

268 may_steal_fs_path=may_steal_fs_path, 

269 ) 

270 

271 

272def _generate_intermediate_manifest( 

273 fs_root: InMemoryVirtualPathBase, 

274 clamp_mtime_to: int, 

275) -> Iterable[TarMember]: 

276 symlinks = [] 

277 for path in fs_root.all_paths(): 

278 tar_member = _path_to_tar_member(path, clamp_mtime_to) 

279 if tar_member.path_type == PathType.SYMLINK: 

280 symlinks.append(tar_member) 

281 continue 

282 yield tar_member 

283 yield from symlinks 

284 

285 

286ST = TypeVar("ST") 

287T = TypeVar("T") 

288 

289 

290class AbstractYAMLSubStore(Generic[ST]): 

291 def __init__( 

292 self, 

293 parent_store: Any, 

294 parent_key: int | str | None, 

295 store: ST | None = None, 

296 ) -> None: 

297 if parent_store is not None and parent_key is not None: 

298 try: 

299 from_parent_store = parent_store[parent_key] 

300 except (KeyError, IndexError): 

301 from_parent_store = None 

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

303 store is not None 

304 and from_parent_store is not None 

305 and store is not parent_store 

306 ): 

307 raise ValueError( 

308 "Store is provided but is not the one already in the parent store" 

309 ) 

310 if store is None: 310 ↛ 312line 310 didn't jump to line 312 because the condition on line 310 was always true

311 store = from_parent_store 

312 self._parent_store = parent_store 

313 self._parent_key = parent_key 

314 self._is_detached = ( 

315 parent_key is None or parent_store is None or parent_key not in parent_store 

316 ) 

317 assert self._is_detached or store is not None 

318 if store is None: 

319 store = self._create_new_instance() 

320 self._store: ST = store 

321 

322 def _create_new_instance(self) -> ST: 

323 raise NotImplementedError 

324 

325 def create_definition_if_missing(self) -> None: 

326 if self._is_detached: 

327 self.create_definition() 

328 

329 def create_definition(self) -> None: 

330 if not self._is_detached: 330 ↛ 331line 330 didn't jump to line 331 because the condition on line 330 was never true

331 raise RuntimeError("Definition is already present") 

332 parent_store = self._parent_store 

333 if parent_store is None: 333 ↛ 334line 333 didn't jump to line 334 because the condition on line 333 was never true

334 raise RuntimeError( 

335 f"Definition is not attached to any parent!? ({self.__class__.__name__})" 

336 ) 

337 if isinstance(parent_store, list): 

338 assert self._parent_key is None 

339 self._parent_key = len(parent_store) 

340 self._parent_store.append(self._store) 

341 else: 

342 parent_store[self._parent_key] = self._store 

343 self._is_detached = False 

344 

345 def remove_definition(self) -> None: 

346 self._ensure_attached() 

347 del self._parent_store[self._parent_key] 

348 if isinstance(self._parent_store, list): 

349 self._parent_key = None 

350 self._is_detached = True 

351 

352 def _ensure_attached(self) -> None: 

353 if self._is_detached: 

354 raise RuntimeError("The definition has been removed!") 

355 

356 

357class AbstractYAMLListSubStore(Generic[T], AbstractYAMLSubStore[list[T]]): 

358 def _create_new_instance(self) -> list[T]: 

359 return CommentedSeq() 

360 

361 

362class AbstractYAMLDictSubStore(Generic[T], AbstractYAMLSubStore[dict[str, T]]): 

363 def _create_new_instance(self) -> dict[str, T]: 

364 return CommentedMap() 

365 

366 

367class MutableCondition: 

368 @classmethod 

369 def arch_matches(cls, arch_filter: str) -> CommentedMap: 

370 return CommentedMap({MK_CONDITION_ARCH_MATCHES: arch_filter}) 

371 

372 @classmethod 

373 def build_profiles_matches(cls, build_profiles_matches: str) -> CommentedMap: 

374 return CommentedMap( 

375 {MK_CONDITION_BUILD_PROFILES_MATCHES: build_profiles_matches} 

376 ) 

377 

378 

379class MutableYAMLSymlink(AbstractYAMLDictSubStore[Any]): 

380 @classmethod 

381 def new_symlink( 

382 cls, link_path: str, link_target: str, condition: Any | None 

383 ) -> "MutableYAMLSymlink": 

384 inner = { 

385 MK_TRANSFORMATIONS_CREATE_SYMLINK_LINK_PATH: link_path, 

386 MK_TRANSFORMATIONS_CREATE_SYMLINK_LINK_TARGET: link_target, 

387 } 

388 content = {MK_TRANSFORMATIONS_CREATE_SYMLINK: inner} 

389 if condition is not None: 389 ↛ 390line 389 didn't jump to line 390 because the condition on line 389 was never true

390 inner["when"] = condition 

391 return cls(None, None, store=CommentedMap(content)) 

392 

393 @property 

394 def symlink_path(self) -> str: 

395 return self._store[MK_TRANSFORMATIONS_CREATE_SYMLINK][ 

396 MK_TRANSFORMATIONS_CREATE_SYMLINK_LINK_PATH 

397 ] 

398 

399 @symlink_path.setter 

400 def symlink_path(self, path: str) -> None: 

401 self._store[MK_TRANSFORMATIONS_CREATE_SYMLINK][ 

402 MK_TRANSFORMATIONS_CREATE_SYMLINK_LINK_PATH 

403 ] = path 

404 

405 @property 

406 def symlink_target(self) -> str | None: 

407 return self._store[MK_TRANSFORMATIONS_CREATE_SYMLINK][ 

408 MK_TRANSFORMATIONS_CREATE_SYMLINK_LINK_TARGET 

409 ] 

410 

411 @symlink_target.setter 

412 def symlink_target(self, target: str) -> None: 

413 self._store[MK_TRANSFORMATIONS_CREATE_SYMLINK][ 

414 MK_TRANSFORMATIONS_CREATE_SYMLINK_LINK_TARGET 

415 ] = target 

416 

417 

418class MutableYAMLConffileManagementItem(AbstractYAMLDictSubStore[Any]): 

419 @classmethod 

420 def rm_conffile( 

421 cls, 

422 conffile: str, 

423 prior_to_version: str | None, 

424 owning_package: str | None, 

425 ) -> "MutableYAMLConffileManagementItem": 

426 r = cls( 

427 None, 

428 None, 

429 store=CommentedMap( 

430 { 

431 MK_CONFFILE_MANAGEMENT_REMOVE: CommentedMap( 

432 {MK_CONFFILE_MANAGEMENT_REMOVE_PATH: conffile} 

433 ) 

434 } 

435 ), 

436 ) 

437 r.prior_to_version = prior_to_version 

438 r.owning_package = owning_package 

439 return r 

440 

441 @classmethod 

442 def mv_conffile( 

443 cls, 

444 old_conffile: str, 

445 new_conffile: str, 

446 prior_to_version: str | None, 

447 owning_package: str | None, 

448 ) -> "MutableYAMLConffileManagementItem": 

449 r = cls( 

450 None, 

451 None, 

452 store=CommentedMap( 

453 { 

454 MK_CONFFILE_MANAGEMENT_RENAME: CommentedMap( 

455 { 

456 MK_CONFFILE_MANAGEMENT_RENAME_SOURCE: old_conffile, 

457 MK_CONFFILE_MANAGEMENT_RENAME_TARGET: new_conffile, 

458 } 

459 ) 

460 } 

461 ), 

462 ) 

463 r.prior_to_version = prior_to_version 

464 r.owning_package = owning_package 

465 return r 

466 

467 @property 

468 def _container(self) -> dict[str, Any]: 

469 assert len(self._store) == 1 

470 return next(iter(self._store.values())) 

471 

472 @property 

473 def command(self) -> str: 

474 assert len(self._store) == 1 

475 return next(iter(self._store)) 

476 

477 @property 

478 def obsolete_conffile(self) -> str: 

479 if self.command == MK_CONFFILE_MANAGEMENT_REMOVE: 

480 return self._container[MK_CONFFILE_MANAGEMENT_REMOVE_PATH] 

481 assert self.command == MK_CONFFILE_MANAGEMENT_RENAME 

482 return self._container[MK_CONFFILE_MANAGEMENT_RENAME_SOURCE] 

483 

484 @obsolete_conffile.setter 

485 def obsolete_conffile(self, value: str) -> None: 

486 if self.command == MK_CONFFILE_MANAGEMENT_REMOVE: 

487 self._container[MK_CONFFILE_MANAGEMENT_REMOVE_PATH] = value 

488 else: 

489 assert self.command == MK_CONFFILE_MANAGEMENT_RENAME 

490 self._container[MK_CONFFILE_MANAGEMENT_RENAME_SOURCE] = value 

491 

492 @property 

493 def new_conffile(self) -> str: 

494 if self.command != MK_CONFFILE_MANAGEMENT_RENAME: 

495 raise TypeError( 

496 f"The new_conffile attribute is only applicable to command {MK_CONFFILE_MANAGEMENT_RENAME}." 

497 f" This is a {self.command}" 

498 ) 

499 return self._container[MK_CONFFILE_MANAGEMENT_RENAME_TARGET] 

500 

501 @new_conffile.setter 

502 def new_conffile(self, value: str) -> None: 

503 if self.command != MK_CONFFILE_MANAGEMENT_RENAME: 

504 raise TypeError( 

505 f"The new_conffile attribute is only applicable to command {MK_CONFFILE_MANAGEMENT_RENAME}." 

506 f" This is a {self.command}" 

507 ) 

508 self._container[MK_CONFFILE_MANAGEMENT_RENAME_TARGET] = value 

509 

510 @property 

511 def prior_to_version(self) -> str | None: 

512 return self._container.get(MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION) 

513 

514 @prior_to_version.setter 

515 def prior_to_version(self, value: str | None) -> None: 

516 if value is None: 

517 try: 

518 del self._container[MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION] 

519 except KeyError: 

520 pass 

521 else: 

522 self._container[MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION] = value 

523 

524 @property 

525 def owning_package(self) -> str | None: 

526 return self._container[MK_CONFFILE_MANAGEMENT_X_PRIOR_TO_VERSION] 

527 

528 @owning_package.setter 

529 def owning_package(self, value: str | None) -> None: 

530 if value is None: 

531 try: 

532 del self._container[MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE] 

533 except KeyError: 

534 pass 

535 else: 

536 self._container[MK_CONFFILE_MANAGEMENT_X_OWNING_PACKAGE] = value 

537 

538 

539class MutableYAMLPackageDefinition(AbstractYAMLDictSubStore): 

540 def _list_store( 

541 self, key, *, create_if_absent: bool = False 

542 ) -> list[dict[str, Any]] | None: 

543 if self._is_detached or key not in self._store: 

544 if create_if_absent: 544 ↛ 545line 544 didn't jump to line 545 because the condition on line 544 was never true

545 return None 

546 self.create_definition_if_missing() 

547 self._store[key] = [] 

548 return self._store[key] 

549 

550 def _insert_item(self, key: str, item: AbstractYAMLDictSubStore) -> None: 

551 parent_store = self._list_store(key, create_if_absent=True) 

552 assert parent_store is not None 

553 if not item._is_detached or ( 553 ↛ 556line 553 didn't jump to line 556 because the condition on line 553 was never true

554 item._parent_store is not None and item._parent_store is not parent_store 

555 ): 

556 raise RuntimeError( 

557 "Item is already attached or associated with a different container" 

558 ) 

559 item._parent_store = parent_store 

560 item.create_definition() 

561 

562 def add_symlink(self, symlink: MutableYAMLSymlink) -> None: 

563 self._insert_item(MK_TRANSFORMATIONS, symlink) 

564 

565 def symlinks(self) -> Iterable[MutableYAMLSymlink]: 

566 store = self._list_store(MK_TRANSFORMATIONS) 

567 if store is None: 567 ↛ 568line 567 didn't jump to line 568 because the condition on line 567 was never true

568 return 

569 for i in range(len(store)): 569 ↛ 570line 569 didn't jump to line 570 because the loop on line 569 never started

570 d = store[i] 

571 if d and isinstance(d, dict) and len(d) == 1 and "symlink" in d: 

572 yield MutableYAMLSymlink(store, i) 

573 

574 def conffile_management_items(self) -> Iterable[MutableYAMLConffileManagementItem]: 

575 store = self._list_store(MK_CONFFILE_MANAGEMENT) 

576 if store is None: 576 ↛ 577line 576 didn't jump to line 577 because the condition on line 576 was never true

577 return 

578 yield from ( 

579 MutableYAMLConffileManagementItem(store, i) for i in range(len(store)) 

580 ) 

581 

582 def add_conffile_management( 

583 self, conffile_management_item: MutableYAMLConffileManagementItem 

584 ) -> None: 

585 self._insert_item(MK_CONFFILE_MANAGEMENT, conffile_management_item) 

586 

587 

588class AbstractMutableYAMLInstallRule(AbstractYAMLDictSubStore): 

589 @property 

590 def _container(self) -> dict[str, Any]: 

591 assert len(self._store) == 1 

592 return next(iter(self._store.values())) 

593 

594 @property 

595 def into(self) -> list[str] | None: 

596 v = self._container[MK_INSTALLATIONS_INSTALL_INTO] 

597 if v is None: 

598 return None 

599 if isinstance(v, str): 

600 return [v] 

601 return v 

602 

603 @into.setter 

604 def into(self, new_value: str | list[str] | None) -> None: 

605 if new_value is None: 605 ↛ 609line 605 didn't jump to line 609 because the condition on line 605 was always true

606 with suppress(KeyError): 

607 del self._container[MK_INSTALLATIONS_INSTALL_INTO] 

608 return 

609 if isinstance(new_value, str): 

610 self._container[MK_INSTALLATIONS_INSTALL_INTO] = new_value 

611 return 

612 new_list = CommentedSeq(new_value) 

613 self._container[MK_INSTALLATIONS_INSTALL_INTO] = new_list 

614 

615 @property 

616 def when(self) -> str | Mapping[str, Any] | None: 

617 return self._container[MK_CONDITION_WHEN] 

618 

619 @when.setter 

620 def when(self, new_value: str | Mapping[str, Any] | None) -> None: 

621 if new_value is None: 621 ↛ 622line 621 didn't jump to line 622 because the condition on line 621 was never true

622 with suppress(KeyError): 

623 del self._container[MK_CONDITION_WHEN] 

624 return 

625 if isinstance(new_value, str): 625 ↛ 626line 625 didn't jump to line 626 because the condition on line 625 was never true

626 self._container[MK_CONDITION_WHEN] = new_value 

627 return 

628 new_map = CommentedMap(new_value) 

629 self._container[MK_CONDITION_WHEN] = new_map 

630 

631 @classmethod 

632 def install_dest( 

633 cls, 

634 sources: str | list[str], 

635 into: str | list[str] | None, 

636 *, 

637 dest_dir: str | None = None, 

638 when: str | Mapping[str, Any] | None = None, 

639 ) -> "MutableYAMLInstallRuleInstall": 

640 k = MK_INSTALLATIONS_INSTALL_SOURCES 

641 if isinstance(sources, str): 

642 k = MK_INSTALLATIONS_INSTALL_SOURCE 

643 r = MutableYAMLInstallRuleInstall( 

644 None, 

645 None, 

646 store=CommentedMap( 

647 { 

648 MK_INSTALLATIONS_INSTALL: CommentedMap( 

649 { 

650 k: sources, 

651 } 

652 ) 

653 } 

654 ), 

655 ) 

656 r.dest_dir = dest_dir 

657 r.into = into 

658 if when is not None: 

659 r.when = when 

660 return r 

661 

662 @classmethod 

663 def multi_dest_install( 

664 cls, 

665 sources: str | list[str], 

666 dest_dirs: Sequence[str], 

667 into: str | list[str] | None, 

668 *, 

669 when: str | Mapping[str, Any] | None = None, 

670 ) -> "MutableYAMLInstallRuleInstall": 

671 k = MK_INSTALLATIONS_INSTALL_SOURCES 

672 if isinstance(sources, str): 672 ↛ 674line 672 didn't jump to line 674 because the condition on line 672 was always true

673 k = MK_INSTALLATIONS_INSTALL_SOURCE 

674 r = MutableYAMLInstallRuleInstall( 

675 None, 

676 None, 

677 store=CommentedMap( 

678 { 

679 MK_INSTALLATIONS_MULTI_DEST_INSTALL: CommentedMap( 

680 { 

681 k: sources, 

682 "dest-dirs": dest_dirs, 

683 } 

684 ) 

685 } 

686 ), 

687 ) 

688 r.into = into 

689 if when is not None: 689 ↛ 690line 689 didn't jump to line 690 because the condition on line 689 was never true

690 r.when = when 

691 return r 

692 

693 @classmethod 

694 def install_as( 

695 cls, 

696 source: str, 

697 install_as: str, 

698 into: str | list[str] | None, 

699 when: str | Mapping[str, Any] | None = None, 

700 ) -> "MutableYAMLInstallRuleInstall": 

701 r = MutableYAMLInstallRuleInstall( 

702 None, 

703 None, 

704 store=CommentedMap( 

705 { 

706 MK_INSTALLATIONS_INSTALL: CommentedMap( 

707 { 

708 MK_INSTALLATIONS_INSTALL_SOURCE: source, 

709 MK_INSTALLATIONS_INSTALL_AS: install_as, 

710 } 

711 ) 

712 } 

713 ), 

714 ) 

715 r.into = into 

716 if when is not None: 716 ↛ 717line 716 didn't jump to line 717 because the condition on line 716 was never true

717 r.when = when 

718 return r 

719 

720 @classmethod 

721 def install_doc_as( 

722 cls, 

723 source: str, 

724 install_as: str, 

725 into: str | list[str] | None, 

726 when: str | Mapping[str, Any] | None = None, 

727 ) -> "MutableYAMLInstallRuleInstall": 

728 r = MutableYAMLInstallRuleInstall( 

729 None, 

730 None, 

731 store=CommentedMap( 

732 { 

733 MK_INSTALLATIONS_INSTALL_DOCS: CommentedMap( 

734 { 

735 MK_INSTALLATIONS_INSTALL_SOURCE: source, 

736 MK_INSTALLATIONS_INSTALL_AS: install_as, 

737 } 

738 ) 

739 } 

740 ), 

741 ) 

742 r.into = into 

743 if when is not None: 

744 r.when = when 

745 return r 

746 

747 @classmethod 

748 def install_docs( 

749 cls, 

750 sources: str | list[str], 

751 into: str | list[str] | None, 

752 *, 

753 dest_dir: str | None = None, 

754 when: str | Mapping[str, Any] | None = None, 

755 ) -> "MutableYAMLInstallRuleInstall": 

756 k = MK_INSTALLATIONS_INSTALL_SOURCES 

757 if isinstance(sources, str): 

758 k = MK_INSTALLATIONS_INSTALL_SOURCE 

759 r = MutableYAMLInstallRuleInstall( 

760 None, 

761 None, 

762 store=CommentedMap( 

763 { 

764 MK_INSTALLATIONS_INSTALL_DOCS: CommentedMap( 

765 { 

766 k: sources, 

767 } 

768 ) 

769 } 

770 ), 

771 ) 

772 r.into = into 

773 r.dest_dir = dest_dir 

774 if when is not None: 

775 r.when = when 

776 return r 

777 

778 @classmethod 

779 def install_examples( 

780 cls, 

781 sources: str | list[str], 

782 into: str | list[str] | None, 

783 when: str | Mapping[str, Any] | None = None, 

784 ) -> "MutableYAMLInstallRuleInstallExamples": 

785 k = MK_INSTALLATIONS_INSTALL_SOURCES 

786 if isinstance(sources, str): 

787 k = MK_INSTALLATIONS_INSTALL_SOURCE 

788 r = MutableYAMLInstallRuleInstallExamples( 

789 None, 

790 None, 

791 store=CommentedMap( 

792 { 

793 MK_INSTALLATIONS_INSTALL_EXAMPLES: CommentedMap( 

794 { 

795 k: sources, 

796 } 

797 ) 

798 } 

799 ), 

800 ) 

801 r.into = into 

802 if when is not None: 802 ↛ 803line 802 didn't jump to line 803 because the condition on line 802 was never true

803 r.when = when 

804 return r 

805 

806 @classmethod 

807 def install_man( 

808 cls, 

809 sources: str | list[str], 

810 into: str | list[str] | None, 

811 language: str | None, 

812 when: str | Mapping[str, Any] | None = None, 

813 ) -> "MutableYAMLInstallRuleMan": 

814 k = MK_INSTALLATIONS_INSTALL_SOURCES 

815 if isinstance(sources, str): 815 ↛ 816line 815 didn't jump to line 816 because the condition on line 815 was never true

816 k = MK_INSTALLATIONS_INSTALL_SOURCE 

817 r = MutableYAMLInstallRuleMan( 

818 None, 

819 None, 

820 store=CommentedMap( 

821 { 

822 MK_INSTALLATIONS_INSTALL_MAN: CommentedMap( 

823 { 

824 k: sources, 

825 } 

826 ) 

827 } 

828 ), 

829 ) 

830 r.language = language 

831 r.into = into 

832 if when is not None: 832 ↛ 833line 832 didn't jump to line 833 because the condition on line 832 was never true

833 r.when = when 

834 return r 

835 

836 @classmethod 

837 def discard( 

838 cls, 

839 sources: str | list[str], 

840 ) -> "MutableYAMLInstallRuleDiscard": 

841 return MutableYAMLInstallRuleDiscard( 

842 None, 

843 None, 

844 store=CommentedMap({MK_INSTALLATIONS_DISCARD: sources}), 

845 ) 

846 

847 

848class MutableYAMLInstallRuleInstallExamples(AbstractMutableYAMLInstallRule): 

849 pass 

850 

851 

852class MutableYAMLInstallRuleMan(AbstractMutableYAMLInstallRule): 

853 @property 

854 def language(self) -> str | None: 

855 return self._container[MK_INSTALLATIONS_INSTALL_MAN_LANGUAGE] 

856 

857 @language.setter 

858 def language(self, new_value: str | None) -> None: 

859 if new_value is not None: 

860 self._container[MK_INSTALLATIONS_INSTALL_MAN_LANGUAGE] = new_value 

861 return 

862 with suppress(KeyError): 

863 del self._container[MK_INSTALLATIONS_INSTALL_MAN_LANGUAGE] 

864 

865 

866class MutableYAMLInstallRuleDiscard(AbstractMutableYAMLInstallRule): 

867 pass 

868 

869 

870class MutableYAMLInstallRuleInstall(AbstractMutableYAMLInstallRule): 

871 @property 

872 def sources(self) -> list[str]: 

873 v = self._container[MK_INSTALLATIONS_INSTALL_SOURCES] 

874 if isinstance(v, str): 

875 return [v] 

876 return v 

877 

878 @sources.setter 

879 def sources(self, new_value: str | list[str]) -> None: 

880 if isinstance(new_value, str): 

881 self._container[MK_INSTALLATIONS_INSTALL_SOURCES] = new_value 

882 return 

883 new_list = CommentedSeq(new_value) 

884 self._container[MK_INSTALLATIONS_INSTALL_SOURCES] = new_list 

885 

886 @property 

887 def dest_dir(self) -> str | None: 

888 return self._container.get(MK_INSTALLATIONS_INSTALL_DEST_DIR) 

889 

890 @dest_dir.setter 

891 def dest_dir(self, new_value: str | None) -> None: 

892 if new_value is not None and self.dest_as is not None: 892 ↛ 893line 892 didn't jump to line 893 because the condition on line 892 was never true

893 raise ValueError( 

894 f'Cannot both have a "{MK_INSTALLATIONS_INSTALL_DEST_DIR}" and' 

895 f' "{MK_INSTALLATIONS_INSTALL_AS}"' 

896 ) 

897 if new_value is not None: 

898 self._container[MK_INSTALLATIONS_INSTALL_DEST_DIR] = new_value 

899 else: 

900 with suppress(KeyError): 

901 del self._container[MK_INSTALLATIONS_INSTALL_DEST_DIR] 

902 

903 @property 

904 def dest_as(self) -> str | None: 

905 return self._container.get(MK_INSTALLATIONS_INSTALL_AS) 

906 

907 @dest_as.setter 

908 def dest_as(self, new_value: str | None) -> None: 

909 if new_value is not None: 

910 if self.dest_dir is not None: 

911 raise ValueError( 

912 f'Cannot both have a "{MK_INSTALLATIONS_INSTALL_DEST_DIR}" and' 

913 f' "{MK_INSTALLATIONS_INSTALL_AS}"' 

914 ) 

915 

916 sources = self._container[MK_INSTALLATIONS_INSTALL_SOURCES] 

917 if isinstance(sources, list): 

918 if len(sources) != 1: 

919 raise ValueError( 

920 f'Cannot have "{MK_INSTALLATIONS_INSTALL_AS}" when' 

921 f' "{MK_INSTALLATIONS_INSTALL_SOURCES}" is not exactly one item' 

922 ) 

923 self.sources = sources[0] 

924 self._container[MK_INSTALLATIONS_INSTALL_AS] = new_value 

925 else: 

926 with suppress(KeyError): 

927 del self._container[MK_INSTALLATIONS_INSTALL_AS] 

928 

929 

930class MutableYAMLInstallationsDefinition(AbstractYAMLListSubStore[Any]): 

931 def append(self, install_rule: AbstractMutableYAMLInstallRule) -> None: 

932 parent_store = self._store 

933 if not install_rule._is_detached or ( 933 ↛ 937line 933 didn't jump to line 937 because the condition on line 933 was never true

934 install_rule._parent_store is not None 

935 and install_rule._parent_store is not parent_store 

936 ): 

937 raise RuntimeError( 

938 "Item is already attached or associated with a different container" 

939 ) 

940 self.create_definition_if_missing() 

941 install_rule._parent_store = parent_store 

942 install_rule.create_definition() 

943 

944 def extend(self, install_rules: Iterable[AbstractMutableYAMLInstallRule]) -> None: 

945 parent_store = self._store 

946 for install_rule in install_rules: 

947 if not install_rule._is_detached or ( 947 ↛ 951line 947 didn't jump to line 951 because the condition on line 947 was never true

948 install_rule._parent_store is not None 

949 and install_rule._parent_store is not parent_store 

950 ): 

951 raise RuntimeError( 

952 "Item is already attached or associated with a different container" 

953 ) 

954 self.create_definition_if_missing() 

955 install_rule._parent_store = parent_store 

956 install_rule.create_definition() 

957 

958 

959class MutableYAMLManifestVariables(AbstractYAMLDictSubStore): 

960 @property 

961 def variables(self) -> dict[str, Any]: 

962 return self._store 

963 

964 def __setitem__(self, key: str, value: Any) -> None: 

965 self._store[key] = value 

966 self.create_definition_if_missing() 

967 

968 

969class MutableYAMLManifestDefinitions(AbstractYAMLDictSubStore): 

970 def manifest_variables( 

971 self, *, create_if_absent: bool = True 

972 ) -> MutableYAMLManifestVariables: 

973 d = MutableYAMLManifestVariables(self._store, MK_MANIFEST_VARIABLES) 

974 if create_if_absent: 974 ↛ 975line 974 didn't jump to line 975 because the condition on line 974 was never true

975 d.create_definition_if_missing() 

976 return d 

977 

978 

979class MutableYAMLRemoveDuringCleanDefinitions(AbstractYAMLListSubStore[str]): 

980 def append(self, rule: str) -> None: 

981 self.create_definition_if_missing() 

982 self._store.append(rule) 

983 

984 def __len__(self) -> int: 

985 return len(self._store) 

986 

987 def extend(self, rules: Iterable[str]) -> None: 

988 it = iter(rules) 

989 try: 

990 first_rule = next(it) 

991 except StopIteration: 

992 return 

993 self.create_definition_if_missing() 

994 self._store.append(first_rule) 

995 self._store.extend(it) 

996 

997 

998class MutableYAMLManifest: 

999 def __init__(self, store: Any) -> None: 

1000 self._store = store 

1001 

1002 @classmethod 

1003 def empty_manifest(cls) -> "MutableYAMLManifest": 

1004 return cls(CommentedMap({MK_MANIFEST_VERSION: DEFAULT_MANIFEST_VERSION})) 

1005 

1006 @property 

1007 def manifest_version(self) -> str: 

1008 return self._store[MK_MANIFEST_VERSION] 

1009 

1010 @manifest_version.setter 

1011 def manifest_version(self, version: str) -> None: 

1012 if version not in SUPPORTED_MANIFEST_VERSIONS: 

1013 raise ValueError("Unsupported version") 

1014 self._store[MK_MANIFEST_VERSION] = version 

1015 

1016 def remove_during_clean( 

1017 self, 

1018 *, 

1019 create_if_absent: bool = True, 

1020 ) -> MutableYAMLRemoveDuringCleanDefinitions: 

1021 d = MutableYAMLRemoveDuringCleanDefinitions( 

1022 self._store, MK_MANIFEST_REMOVE_DURING_CLEAN 

1023 ) 

1024 if create_if_absent: 1024 ↛ 1025line 1024 didn't jump to line 1025 because the condition on line 1024 was never true

1025 d.create_definition_if_missing() 

1026 return d 

1027 

1028 def installations( 

1029 self, 

1030 *, 

1031 create_if_absent: bool = True, 

1032 ) -> MutableYAMLInstallationsDefinition: 

1033 d = MutableYAMLInstallationsDefinition(self._store, MK_INSTALLATIONS) 

1034 if create_if_absent: 1034 ↛ 1035line 1034 didn't jump to line 1035 because the condition on line 1034 was never true

1035 d.create_definition_if_missing() 

1036 return d 

1037 

1038 def manifest_definitions( 

1039 self, 

1040 *, 

1041 create_if_absent: bool = True, 

1042 ) -> MutableYAMLManifestDefinitions: 

1043 d = MutableYAMLManifestDefinitions(self._store, MK_MANIFEST_DEFINITIONS) 

1044 if create_if_absent: 1044 ↛ 1045line 1044 didn't jump to line 1045 because the condition on line 1044 was never true

1045 d.create_definition_if_missing() 

1046 return d 

1047 

1048 def package( 

1049 self, name: str, *, create_if_absent: bool = True 

1050 ) -> MutableYAMLPackageDefinition: 

1051 if MK_PACKAGES not in self._store: 1051 ↛ 1053line 1051 didn't jump to line 1053 because the condition on line 1051 was always true

1052 self._store[MK_PACKAGES] = CommentedMap() 

1053 packages_store = self._store[MK_PACKAGES] 

1054 package = packages_store.get(name) 

1055 if package is None: 1055 ↛ 1062line 1055 didn't jump to line 1062 because the condition on line 1055 was always true

1056 if not create_if_absent: 1056 ↛ 1057line 1056 didn't jump to line 1057 because the condition on line 1056 was never true

1057 raise KeyError(name) 

1058 assert packages_store is not None 

1059 d = MutableYAMLPackageDefinition(packages_store, name) 

1060 d.create_definition() 

1061 else: 

1062 d = MutableYAMLPackageDefinition(packages_store, name) 

1063 return d 

1064 

1065 def write_to(self, fd) -> None: 

1066 MANIFEST_YAML.dump(self._store, fd) 

1067 

1068 

1069def _describe_missing_path(entry: VirtualPath) -> str: 

1070 if entry.is_dir: 

1071 return f"{entry.fs_path}/ (empty directory; possible integration point)" 

1072 if entry.is_symlink: 

1073 target = os.readlink(entry.fs_path) 

1074 return f"{entry.fs_path} (symlink; links to {target})" 

1075 if entry.is_file: 

1076 return f"{entry.fs_path} (file)" 

1077 return f"{entry.fs_path} (other!? Probably not supported by debputy and may need a `remove`)" 

1078 

1079 

1080def _detect_missing_installations( 

1081 path_matcher: SourcePathMatcher, 

1082 search_dir: VirtualPath, 

1083) -> None: 

1084 if not search_dir.is_dir: 1084 ↛ 1085line 1084 didn't jump to line 1085 because the condition on line 1084 was never true

1085 return 

1086 missing = list(path_matcher.detect_missing(search_dir)) 

1087 if not missing: 

1088 return 

1089 

1090 excl = textwrap.dedent("""\ 

1091 - discard: "*" 

1092 """) 

1093 

1094 raise PathNotCoveredByInstallRulesError( 

1095 "Please review the list and add either install rules or exclusions to `installations` in" 

1096 " debian/debputy.manifest. If you do not need any of these paths, add the following to the" 

1097 f" end of your 'installations`:\n\n{excl}\n", 

1098 missing, 

1099 search_dir, 

1100 ) 

1101 

1102 

1103def _list_automatic_discard_rules(path_matcher: SourcePathMatcher) -> None: 

1104 used_discard_rules = path_matcher.used_auto_discard_rules 

1105 # Discard rules can match and then be overridden. In that case, they appear 

1106 # but have 0 matches. 

1107 if not sum((len(v) for v in used_discard_rules.values()), 0): 

1108 return 

1109 _info("The following automatic discard rules were triggered:") 

1110 example_path: str | None = None 

1111 for rule in sorted(used_discard_rules): 

1112 for fs_path in sorted(used_discard_rules[rule]): 

1113 if example_path is None: 1113 ↛ 1115line 1113 didn't jump to line 1115 because the condition on line 1113 was always true

1114 example_path = fs_path 

1115 _info(f" * {rule} -> {fs_path}") 

1116 assert example_path is not None 

1117 _info("") 

1118 _info( 

1119 "Note that some of these may have been overruled. The overrule detection logic is not" 

1120 ) 

1121 _info("100% reliable.") 

1122 _info("") 

1123 _info( 

1124 "You can overrule an automatic discard rule by explicitly listing the path. As an example:" 

1125 ) 

1126 _info(" installations:") 

1127 _info(" - install:") 

1128 _info(f" source: {example_path}") 

1129 

1130 

1131def _install_everything_from_source_dir_if_present( 

1132 dctrl_bin: BinaryPackage, 

1133 substitution: Substitution, 

1134 path_matcher: SourcePathMatcher, 

1135 install_rule_context: InstallRuleContext, 

1136 source_condition_context: ConditionContext, 

1137 source_dir: VirtualPath, 

1138 *, 

1139 into_dir: VirtualPath | None = None, 

1140) -> None: 

1141 attribute_path = AttributePath.builtin_path()[f"installing {source_dir.fs_path}"] 

1142 pkg_set = frozenset([dctrl_bin]) 

1143 install_rule = run_in_context_of_plugin( 

1144 "debputy", 

1145 InstallRule.install_dest, 

1146 [FileSystemMatchRule.from_path_match("*", attribute_path, substitution)], 

1147 None, 

1148 pkg_set, 

1149 f"Built-in; install everything from {source_dir.fs_path} into {dctrl_bin.name}", 

1150 None, 

1151 ) 

1152 pkg_search_dir: tuple[SearchDir] = ( 

1153 SearchDir( 

1154 source_dir, 

1155 pkg_set, 

1156 ), 

1157 ) 

1158 replacements = { 

1159 "search_dirs": pkg_search_dir, 

1160 } 

1161 if into_dir is not None: 1161 ↛ 1162line 1161 didn't jump to line 1162 because the condition on line 1161 was never true

1162 binary_package_contexts = dict(install_rule_context.binary_package_contexts) 

1163 updated = binary_package_contexts[dctrl_bin.name].replace(fs_root=into_dir) 

1164 binary_package_contexts[dctrl_bin.name] = updated 

1165 replacements["binary_package_contexts"] = binary_package_contexts 

1166 

1167 fake_install_rule_context = install_rule_context.replace(**replacements) 

1168 try: 

1169 install_rule.perform_install( 

1170 path_matcher, 

1171 fake_install_rule_context, 

1172 source_condition_context, 

1173 ) 

1174 except ( 

1175 NoMatchForInstallPatternError, 

1176 PathAlreadyInstalledOrDiscardedError, 

1177 ): 

1178 # Empty directory or everything excluded by default; ignore the error 

1179 pass 

1180 

1181 

1182def _add_build_install_dirs_to_per_package_search_dirs( 

1183 build_system_install_dirs: Sequence[tuple[str, frozenset[BinaryPackage]]], 

1184 per_package_search_dirs: dict[BinaryPackage, list[VirtualPath]], 

1185 as_path: Callable[[str], VirtualPath], 

1186) -> None: 

1187 seen_pp_search_dirs: set[tuple[BinaryPackage, str]] = set() 

1188 for dest_dir, for_packages in build_system_install_dirs: 

1189 dest_path = as_path(dest_dir) 

1190 for pkg in for_packages: 

1191 seen_key = (pkg, dest_dir) 

1192 if seen_key in seen_pp_search_dirs: 

1193 continue 

1194 seen_pp_search_dirs.add(seen_key) 

1195 if pkg not in per_package_search_dirs: 

1196 per_package_search_dirs[pkg] = [dest_path] 

1197 else: 

1198 per_package_search_dirs[pkg].append(dest_path) 

1199 

1200 

1201class HighLevelManifest: 

1202 def __init__( 

1203 self, 

1204 manifest_path: str, 

1205 mutable_manifest: MutableYAMLManifest | None, 

1206 remove_during_clean_rules: list[FileSystemMatchRule], 

1207 install_rules: list[InstallRule] | None, 

1208 source_package: SourcePackage, 

1209 binary_packages: Mapping[str, BinaryPackage], 

1210 substitution: Substitution, 

1211 package_transformations: Mapping[str, PackageTransformationDefinition], 

1212 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable, 

1213 dpkg_arch_query_table: DpkgArchTable, 

1214 build_env: DebBuildOptionsAndProfiles, 

1215 build_environments: BuildEnvironments, 

1216 build_rules: list[BuildRule] | None, 

1217 value_table: Mapping[ 

1218 tuple[SourcePackage | BinaryPackage, type[Any]], 

1219 Any, 

1220 ], 

1221 plugin_provided_feature_set: PluginProvidedFeatureSet, 

1222 debian_dir: VirtualPath, 

1223 ) -> None: 

1224 self.manifest_path = manifest_path 

1225 self.mutable_manifest = mutable_manifest 

1226 self._remove_during_clean_rules: list[FileSystemMatchRule] = ( 

1227 remove_during_clean_rules 

1228 ) 

1229 self._install_rules = install_rules 

1230 self.source_package = source_package 

1231 self._binary_packages = binary_packages 

1232 self.substitution = substitution 

1233 self.package_transformations = package_transformations 

1234 self._dpkg_architecture_variables = dpkg_architecture_variables 

1235 self.dpkg_arch_query_table = dpkg_arch_query_table 

1236 self._build_env = build_env 

1237 self._used_for: set[str] = set() 

1238 self.build_environments = build_environments 

1239 self.build_rules = build_rules 

1240 self._value_table = value_table 

1241 self._plugin_provided_feature_set = plugin_provided_feature_set 

1242 self._debian_dir = debian_dir 

1243 self._source_condition_context = ConditionContext( 

1244 binary_package=None, 

1245 substitution=self.substitution, 

1246 deb_options_and_profiles=self._build_env, 

1247 dpkg_architecture_variables=self._dpkg_architecture_variables, 

1248 dpkg_arch_query_table=self.dpkg_arch_query_table, 

1249 ) 

1250 

1251 def source_version(self, include_binnmu_version: bool = True) -> str: 

1252 # TODO: There should an easier way to determine the source version; really. 

1253 version_var = "{{DEB_VERSION}}" 

1254 if not include_binnmu_version: 

1255 version_var = "{{_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE}}" 

1256 try: 

1257 return self.substitution.substitute( 

1258 version_var, "internal (resolve version)" 

1259 ) 

1260 except DebputySubstitutionError as e: 

1261 raise AssertionError(f"Could not resolve {version_var}") from e 

1262 

1263 @property 

1264 def source_condition_context(self) -> ConditionContext: 

1265 return self._source_condition_context 

1266 

1267 @property 

1268 def debian_dir(self) -> VirtualPath: 

1269 return self._debian_dir 

1270 

1271 @property 

1272 def dpkg_architecture_variables(self) -> DpkgArchitectureBuildProcessValuesTable: 

1273 return self._dpkg_architecture_variables 

1274 

1275 @property 

1276 def deb_options_and_profiles(self) -> DebBuildOptionsAndProfiles: 

1277 return self._build_env 

1278 

1279 @property 

1280 def plugin_provided_feature_set(self) -> PluginProvidedFeatureSet: 

1281 return self._plugin_provided_feature_set 

1282 

1283 @property 

1284 def remove_during_clean_rules(self) -> list[FileSystemMatchRule]: 

1285 return self._remove_during_clean_rules 

1286 

1287 @property 

1288 def active_packages(self) -> Iterable[BinaryPackage]: 

1289 yield from (p for p in self._binary_packages.values() if p.should_be_acted_on) 

1290 

1291 @property 

1292 def all_packages(self) -> Iterable[BinaryPackage]: 

1293 yield from self._binary_packages.values() 

1294 

1295 def manifest_configuration[T]( 

1296 self, 

1297 context_package: SourcePackage | BinaryPackage, 

1298 value_type: type[T], 

1299 ) -> T | None: 

1300 res = self._value_table.get((context_package, value_type)) 

1301 return typing.cast("T | None", res) 

1302 

1303 def package_state_for(self, package: str) -> PackageTransformationDefinition: 

1304 return self.package_transformations[package] 

1305 

1306 def _detect_doc_main_package_for(self, package: BinaryPackage) -> BinaryPackage: 

1307 name = package.name 

1308 for doc_main_field in ("Doc-Main-Package", "X-Doc-Main-Package"): 

1309 doc_main_package_name = package.fields.get(doc_main_field) 

1310 if doc_main_package_name: 1310 ↛ 1311line 1310 didn't jump to line 1311 because the condition on line 1310 was never true

1311 main_package = self._binary_packages.get(doc_main_package_name) 

1312 if main_package is None: 

1313 _error( 

1314 f"Invalid Doc-Main-Package for {name}: The package {doc_main_package_name!r} is not listed in d/control" 

1315 ) 

1316 return main_package 

1317 # If it is not a -doc package, then docs should be installed 

1318 # under its own package name. 

1319 if not name.endswith("-doc"): 1319 ↛ 1321line 1319 didn't jump to line 1321 because the condition on line 1319 was always true

1320 return package 

1321 name = name[:-4] 

1322 main_package = self._binary_packages.get(name) 

1323 if main_package: 

1324 return main_package 

1325 if name.startswith("lib"): 

1326 dev_pkg = self._binary_packages.get(f"{name}-dev") 

1327 if dev_pkg: 

1328 return dev_pkg 

1329 

1330 # If we found no better match; default to the doc package itself. 

1331 return package 

1332 

1333 def perform_installations( 

1334 self, 

1335 integration_mode: DebputyIntegrationMode, 

1336 build_system_install_dirs: Sequence[tuple[str, frozenset[BinaryPackage]]], 

1337 *, 

1338 install_request_context: InstallSearchDirContext | None = None, 

1339 ) -> PackageDataTable: 

1340 package_data_dict = {} 

1341 package_data_table = PackageDataTable(package_data_dict) 

1342 enable_manifest_installation_feature = ( 

1343 integration_mode != INTEGRATION_MODE_DH_DEBPUTY_RRR 

1344 ) 

1345 

1346 if build_system_install_dirs: 1346 ↛ 1347line 1346 didn't jump to line 1347 because the condition on line 1346 was never true

1347 if integration_mode != INTEGRATION_MODE_FULL: 

1348 raise ValueError( 

1349 "The build_system_install_dirs parameter can only be used in full integration mode" 

1350 ) 

1351 if install_request_context: 

1352 raise ValueError( 

1353 "The build_system_install_dirs parameter cannot be used with install_request_context" 

1354 " (not implemented)" 

1355 ) 

1356 

1357 if install_request_context is None: 1357 ↛ 1359line 1357 didn't jump to line 1359 because the condition on line 1357 was never true

1358 

1359 @functools.lru_cache(None) 

1360 def _as_path(fs_path: str) -> VirtualPath: 

1361 return OSFSROOverlay.create_root_dir(".", fs_path) 

1362 

1363 dtmp_dir = _as_path("debian/tmp") 

1364 dtmp_deb_dir: VirtualPath | None = _as_path("debian/tmp-deb") 

1365 dtmp_udeb_dir: VirtualPath | None = _as_path("debian/tmp-udeb") 

1366 if not os.path.isdir(assume_not_none(dtmp_dir).fs_path): 

1367 dtmp_dir = None 

1368 if not os.path.isdir(assume_not_none(dtmp_deb_dir).fs_path): 

1369 dtmp_deb_dir = None 

1370 if not os.path.isdir(assume_not_none(dtmp_udeb_dir).fs_path): 

1371 dtmp_udeb_dir = None 

1372 source_root_dir = _as_path(".") 

1373 into = frozenset(self._binary_packages.values()) 

1374 per_package_search_dirs = { 

1375 t.binary_package: [_as_path(f.match_rule.path) for f in t.search_dirs] 

1376 for t in self.package_transformations.values() 

1377 if t.search_dirs is not None 

1378 } 

1379 if integration_mode == INTEGRATION_MODE_FULL: 

1380 # We can end here with per_package_search_dirs having no search dirs for any package 

1381 # (single binary, where everything is installed into d/<pkg> is the most common case). 

1382 # 

1383 # This is not a problem in itself as the installation rules can still apply to the 

1384 # source root and there should be no reason to install something from d/<pkg> into 

1385 # d/<another-pkg> 

1386 _add_build_install_dirs_to_per_package_search_dirs( 

1387 build_system_install_dirs, 

1388 per_package_search_dirs, 

1389 _as_path, 

1390 ) 

1391 else: 

1392 # When we interact with `debhelper`, look for `debian/tmp-deb`, `debian/tmp-udeb`, and `debian/tmp` 

1393 # as likely candidates 

1394 for p in self.all_packages: 

1395 if p not in per_package_search_dirs: 

1396 per_package_search_dirs[p] = [] 

1397 search_path = dtmp_udeb_dir if p.is_udeb else dtmp_deb_dir 

1398 existing = ( 

1399 {d.fs_path for d in per_package_search_dirs[p]} 

1400 if search_path 

1401 else frozenset() 

1402 ) 

1403 if search_path and search_path.fs_path not in existing: 

1404 _info( 

1405 f"Implicit search path {search_path.fs_path} for {p.name}" 

1406 ) 

1407 per_package_search_dirs[p].append(search_path) 

1408 if dtmp_dir and dtmp_dir.fs_path not in existing: 

1409 _info(f"Implicit search path {dtmp_dir.fs_path} for {p.name}") 

1410 per_package_search_dirs[p].append(dtmp_dir) 

1411 

1412 search_dirs = _determine_search_dir_order( 

1413 per_package_search_dirs, 

1414 into, 

1415 source_root_dir, 

1416 ) 

1417 check_for_uninstalled_dirs = tuple( 

1418 s.search_dir 

1419 for s in search_dirs 

1420 if s.search_dir.fs_path != source_root_dir.fs_path 

1421 ) 

1422 if enable_manifest_installation_feature: 

1423 _present_installation_dirs( 

1424 search_dirs, check_for_uninstalled_dirs, into 

1425 ) 

1426 else: 

1427 dtmp_dir = None 

1428 dtmp_deb_dir = None 

1429 dtmp_udeb_dir = None 

1430 search_dirs = install_request_context.search_dirs 

1431 into = frozenset(self._binary_packages.values()) 

1432 seen: set[BinaryPackage] = set() 

1433 for search_dir in search_dirs: 

1434 seen.update(search_dir.applies_to) 

1435 

1436 missing = into - seen 

1437 if missing: 1437 ↛ 1438line 1437 didn't jump to line 1438 because the condition on line 1437 was never true

1438 names = ", ".join(p.name for p in missing) 

1439 raise ValueError( 

1440 f"The following package(s) had no search dirs: {names}." 

1441 " (Generally, the source root would be applicable to all packages)" 

1442 ) 

1443 extra_names = seen - into 

1444 if extra_names: 1444 ↛ 1445line 1444 didn't jump to line 1445 because the condition on line 1444 was never true

1445 names = ", ".join(p.name for p in extra_names) 

1446 raise ValueError( 

1447 f"The install_request_context referenced the following unknown package(s): {names}" 

1448 ) 

1449 

1450 check_for_uninstalled_dirs = ( 

1451 install_request_context.check_for_uninstalled_dirs 

1452 ) 

1453 

1454 install_rule_context = InstallRuleContext(search_dirs) 

1455 check_dirs = [dtmp_dir, dtmp_deb_dir, dtmp_udeb_dir] 

1456 

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

1458 enable_manifest_installation_feature 

1459 and self._install_rules is None 

1460 # TODO: Should we also do this for full mode when build systems provided search dirs? 

1461 and (tmpdirs := [d for d in check_dirs if d and os.path.isdir(d.fs_path)]) 

1462 ): 

1463 first_tmp_dir = tmpdirs[0] 

1464 msg = ( 

1465 "The build system appears to have provided the output of upstream build system's" 

1466 f" install in {first_tmp_dir.fs_path}. However, these are no provisions for debputy to install" 

1467 " any of that into any of the debian packages listed in debian/control." 

1468 " To avoid accidentally creating empty packages, debputy will insist that you " 

1469 " explicitly define an empty installation definition if you did not want to " 

1470 " install any of those files even though they have been provided." 

1471 ' Example: "installations: []"' 

1472 ) 

1473 _error(msg) 

1474 elif ( 1474 ↛ 1477line 1474 didn't jump to line 1477 because the condition on line 1474 was never true

1475 not enable_manifest_installation_feature and self._install_rules is not None 

1476 ): 

1477 _error( 

1478 f"The `installations` feature cannot be used in {self.manifest_path} with this integration mode." 

1479 f" Please remove or comment out the `installations` keyword." 

1480 ) 

1481 

1482 for dctrl_bin in self.all_packages: 

1483 package = dctrl_bin.name 

1484 doc_main_package = self._detect_doc_main_package_for(dctrl_bin) 

1485 

1486 install_rule_context[package] = BinaryPackageInstallRuleContext( 

1487 dctrl_bin, 

1488 InMemoryVirtualRootDir(), 

1489 doc_main_package, 

1490 ) 

1491 

1492 if enable_manifest_installation_feature: 1492 ↛ 1497line 1492 didn't jump to line 1497 because the condition on line 1492 was always true

1493 discard_rules = list( 

1494 self.plugin_provided_feature_set.auto_discard_rules.values() 

1495 ) 

1496 else: 

1497 discard_rules = [ 

1498 self.plugin_provided_feature_set.auto_discard_rules["debian-dir"] 

1499 ] 

1500 path_matcher = SourcePathMatcher(discard_rules) 

1501 

1502 source_condition_context = self._source_condition_context 

1503 

1504 for dctrl_bin in self.active_packages: 

1505 package = dctrl_bin.name 

1506 if install_request_context: 1506 ↛ 1511line 1506 didn't jump to line 1511 because the condition on line 1506 was always true

1507 build_system_staging_dir = install_request_context.debian_pkg_dirs.get( 

1508 package 

1509 ) 

1510 else: 

1511 build_system_staging_dir_fs_path = os.path.join("debian", package) 

1512 if os.path.isdir(build_system_staging_dir_fs_path): 

1513 build_system_staging_dir = OSFSROOverlay.create_root_dir( 

1514 ".", 

1515 build_system_staging_dir_fs_path, 

1516 ) 

1517 else: 

1518 build_system_staging_dir = None 

1519 

1520 if build_system_staging_dir is not None: 

1521 _install_everything_from_source_dir_if_present( 

1522 dctrl_bin, 

1523 self.substitution, 

1524 path_matcher, 

1525 install_rule_context, 

1526 source_condition_context, 

1527 build_system_staging_dir, 

1528 ) 

1529 

1530 if self._install_rules: 

1531 # FIXME: Check that every install rule remains used after transformations have run. 

1532 # What we want to check is transformations do not exclude everything from an install 

1533 # rule. The hard part here is that renaming (etc.) is fine, so we cannot 1:1 string 

1534 # match. 

1535 for install_rule in self._install_rules: 

1536 install_rule.perform_install( 

1537 path_matcher, 

1538 install_rule_context, 

1539 source_condition_context, 

1540 ) 

1541 

1542 if enable_manifest_installation_feature: 1542 ↛ 1546line 1542 didn't jump to line 1546 because the condition on line 1542 was always true

1543 for search_dir in check_for_uninstalled_dirs: 

1544 _detect_missing_installations(path_matcher, search_dir) 

1545 

1546 for dctrl_bin in self.all_packages: 

1547 package = dctrl_bin.name 

1548 binary_install_rule_context = install_rule_context[package] 

1549 build_system_pkg_staging_dir = os.path.join("debian", package) 

1550 fs_root = binary_install_rule_context.fs_root 

1551 

1552 context = self.package_transformations[package] 

1553 if dctrl_bin.should_be_acted_on and enable_manifest_installation_feature: 

1554 for special_install_rule in context.install_rules: 1554 ↛ 1555line 1554 didn't jump to line 1555 because the loop on line 1554 never started

1555 special_install_rule.perform_install( 

1556 path_matcher, 

1557 install_rule_context, 

1558 source_condition_context, 

1559 ) 

1560 

1561 if dctrl_bin.should_be_acted_on: 

1562 self.apply_fs_transformations(package, fs_root) 

1563 substvars_file = f"debian/{package}.substvars" 

1564 substvars = FlushableSubstvars.load_from_path( 

1565 substvars_file, missing_ok=True 

1566 ) 

1567 # We do not want to touch the substvars file (non-clean rebuild contamination) 

1568 substvars.substvars_path = None 

1569 else: 

1570 substvars = FlushableSubstvars() 

1571 

1572 udeb_package = self._binary_packages.get(f"{package}-udeb") 

1573 if udeb_package and not udeb_package.is_udeb: 1573 ↛ 1574line 1573 didn't jump to line 1574 because the condition on line 1573 was never true

1574 udeb_package = None 

1575 

1576 package_metadata_context = PackageProcessingContextProvider( 

1577 self, 

1578 dctrl_bin, 

1579 udeb_package, 

1580 package_data_table, 

1581 ) 

1582 

1583 ctrl_creator = BinaryCtrlAccessorProviderCreator( 

1584 package_metadata_context, 

1585 substvars, 

1586 context.maintscript_snippets, 

1587 context.substitution, 

1588 ) 

1589 

1590 if not enable_manifest_installation_feature: 1590 ↛ 1591line 1590 didn't jump to line 1591 because the condition on line 1590 was never true

1591 assert_no_dbgsym_migration(dctrl_bin) 

1592 dh_dbgsym_root_fs = dhe_dbgsym_root_dir(dctrl_bin) 

1593 dh_dbgsym_root_path = OSFSROOverlay.create_root_dir( 

1594 "", 

1595 dh_dbgsym_root_fs, 

1596 ) 

1597 dbgsym_root_fs = InMemoryVirtualRootDir() 

1598 _install_everything_from_source_dir_if_present( 

1599 dctrl_bin, 

1600 self.substitution, 

1601 path_matcher, 

1602 install_rule_context, 

1603 source_condition_context, 

1604 dh_dbgsym_root_path, 

1605 into_dir=dbgsym_root_fs, 

1606 ) 

1607 dbgsym_build_ids = read_dbgsym_file(dctrl_bin) 

1608 dbgsym_info = DbgsymInfo( 

1609 dctrl_bin, 

1610 dbgsym_root_fs, 

1611 os.path.join(dh_dbgsym_root_fs, "DEBIAN"), 

1612 dbgsym_build_ids, 

1613 # TODO: Provide manifest feature to support this. 

1614 False, 

1615 ) 

1616 else: 

1617 dbgsym_info = DbgsymInfo( 

1618 dctrl_bin, 

1619 InMemoryVirtualRootDir(), 

1620 None, 

1621 [], 

1622 False, 

1623 ) 

1624 

1625 package_data_dict[package] = BinaryPackageData( 

1626 self.source_package, 

1627 dctrl_bin, 

1628 build_system_pkg_staging_dir, 

1629 fs_root, 

1630 substvars, 

1631 package_metadata_context, 

1632 ctrl_creator, 

1633 dbgsym_info, 

1634 ) 

1635 

1636 if enable_manifest_installation_feature: 1636 ↛ 1639line 1636 didn't jump to line 1639 because the condition on line 1636 was always true

1637 _list_automatic_discard_rules(path_matcher) 

1638 

1639 return package_data_table 

1640 

1641 def condition_context( 

1642 self, binary_package: BinaryPackage | str | None 

1643 ) -> ConditionContext: 

1644 if binary_package is None: 1644 ↛ 1645line 1644 didn't jump to line 1645 because the condition on line 1644 was never true

1645 return self._source_condition_context 

1646 if not isinstance(binary_package, str): 

1647 binary_package = binary_package.name 

1648 

1649 package_transformation = self.package_transformations[binary_package] 

1650 return self._source_condition_context.replace( 

1651 binary_package=package_transformation.binary_package, 

1652 substitution=package_transformation.substitution, 

1653 ) 

1654 

1655 def apply_fs_transformations( 

1656 self, 

1657 package: str, 

1658 fs_root: InMemoryVirtualPathBase, 

1659 ) -> None: 

1660 if package in self._used_for: 1660 ↛ 1661line 1660 didn't jump to line 1661 because the condition on line 1660 was never true

1661 raise ValueError( 

1662 f"data.tar contents for {package} has already been finalized!?" 

1663 ) 

1664 if package not in self.package_transformations: 1664 ↛ 1665line 1664 didn't jump to line 1665 because the condition on line 1664 was never true

1665 raise ValueError( 

1666 f'The package "{package}" was not relevant for the manifest!?' 

1667 ) 

1668 package_transformation = self.package_transformations[package] 

1669 condition_context = ConditionContext( 

1670 binary_package=package_transformation.binary_package, 

1671 substitution=package_transformation.substitution, 

1672 deb_options_and_profiles=self._build_env, 

1673 dpkg_architecture_variables=self._dpkg_architecture_variables, 

1674 dpkg_arch_query_table=self.dpkg_arch_query_table, 

1675 ) 

1676 norm_rules = list( 

1677 builtin_mode_normalization_rules( 

1678 self._dpkg_architecture_variables, 

1679 package_transformation.binary_package, 

1680 package_transformation.substitution, 

1681 ) 

1682 ) 

1683 norm_mode_transformation_rule = ModeNormalizationTransformationRule(norm_rules) 

1684 norm_mode_transformation_rule.transform_file_system(fs_root, condition_context) 

1685 for transformation in package_transformation.transformations: 

1686 transformation.run_transform_file_system(fs_root, condition_context) 

1687 interpreter_normalization = NormalizeShebangLineTransformation() 

1688 interpreter_normalization.transform_file_system(fs_root, condition_context) 

1689 

1690 def finalize_data_tar_contents( 

1691 self, 

1692 package: str, 

1693 fs_root: InMemoryVirtualPathBase, 

1694 clamp_mtime_to: int, 

1695 ) -> IntermediateManifest: 

1696 if package in self._used_for: 1696 ↛ 1697line 1696 didn't jump to line 1697 because the condition on line 1696 was never true

1697 raise ValueError( 

1698 f"data.tar contents for {package} has already been finalized!?" 

1699 ) 

1700 if package not in self.package_transformations: 1700 ↛ 1701line 1700 didn't jump to line 1701 because the condition on line 1700 was never true

1701 raise ValueError( 

1702 f'The package "{package}" was not relevant for the manifest!?' 

1703 ) 

1704 self._used_for.add(package) 

1705 

1706 # At this point, there so be no further mutations to the file system (because they will not 

1707 # be present in the intermediate manifest) 

1708 # 

1709 # We use `setattr` because the official API says it is a read-only property, but we know 

1710 # that is a InMemoryOverlayFSRootDirectory and it allows write. 

1711 setattr(fs_root, "is_read_write", False) 

1712 

1713 intermediate_manifest = list( 

1714 _generate_intermediate_manifest( 

1715 fs_root, 

1716 clamp_mtime_to, 

1717 ) 

1718 ) 

1719 return intermediate_manifest 

1720 

1721 def apply_to_binary_staging_directory( 

1722 self, 

1723 package: str, 

1724 fs_root: InMemoryVirtualPathBase, 

1725 clamp_mtime_to: int, 

1726 ) -> IntermediateManifest: 

1727 self.apply_fs_transformations(package, fs_root) 

1728 return self.finalize_data_tar_contents(package, fs_root, clamp_mtime_to) 

1729 

1730 

1731@dataclasses.dataclass(slots=True) 

1732class SearchDirOrderState: 

1733 search_dir: VirtualPath 

1734 applies_to: set[BinaryPackage] = dataclasses.field(default_factory=set) 

1735 after: set[str] = dataclasses.field(default_factory=set) 

1736 

1737 

1738def _present_installation_dirs( 

1739 search_dirs: Sequence[SearchDir], 

1740 checked_missing_dirs: Sequence[VirtualPath], 

1741 all_pkgs: frozenset[BinaryPackage], 

1742) -> None: 

1743 _info("The following directories are considered search dirs (in order):") 

1744 max_len = max((len(s.search_dir.fs_path) for s in search_dirs), default=1) 

1745 for search_dir in search_dirs: 

1746 applies_to = "" 

1747 if search_dir.applies_to < all_pkgs: 

1748 names = ", ".join(p.name for p in search_dir.applies_to) 

1749 applies_to = f" [only applicable to: {names}]" 

1750 remark = "" 

1751 if not os.path.isdir(search_dir.search_dir.fs_path): 

1752 remark = " (skipped; absent)" 

1753 _info(f" * {search_dir.search_dir.fs_path:{max_len}}{applies_to}{remark}") 

1754 

1755 if checked_missing_dirs: 

1756 _info('The following directories are considered for "not-installed" paths;') 

1757 for d in checked_missing_dirs: 

1758 remark = "" 

1759 if not os.path.isdir(d.fs_path): 

1760 remark = " (skipped; absent)" 

1761 _info(f" * {d.fs_path:{max_len}}{remark}") 

1762 

1763 

1764def _determine_search_dir_order( 

1765 requested: Mapping[BinaryPackage, list[VirtualPath]], 

1766 all_pkgs: frozenset[BinaryPackage], 

1767 source_root: VirtualPath, 

1768) -> Sequence[SearchDir]: 

1769 search_dir_table = {} 

1770 assert requested.keys() <= all_pkgs 

1771 for pkg in all_pkgs: 

1772 paths = requested.get(pkg) or [] 

1773 previous_search_dir: SearchDirOrderState | None = None 

1774 for path in paths: 

1775 try: 

1776 search_dir_state = search_dir_table[path.fs_path] 

1777 except KeyError: 

1778 search_dir_state = SearchDirOrderState(path) 

1779 search_dir_table[path.fs_path] = search_dir_state 

1780 search_dir_state.applies_to.add(pkg) 

1781 if previous_search_dir is not None: 

1782 search_dir_state.after.add(previous_search_dir.search_dir.fs_path) 

1783 previous_search_dir = search_dir_state 

1784 

1785 search_dirs_in_order = [] 

1786 released = set[str]() 

1787 remaining = set() 

1788 for search_dir_state in search_dir_table.values(): 

1789 if not (search_dir_state.after <= released): 

1790 remaining.add(search_dir_state.search_dir.fs_path) 

1791 continue 

1792 search_dirs_in_order.append(search_dir_state) 

1793 released.add(search_dir_state.search_dir.fs_path) 

1794 

1795 while remaining: 

1796 current_released = len(released) 

1797 for fs_path in remaining: 

1798 search_dir_state = search_dir_table[fs_path] 

1799 if not search_dir_state.after.issubset(released): 

1800 remaining.add(search_dir_state.search_dir.fs_path) 

1801 continue 

1802 search_dirs_in_order.append(search_dir_state) 

1803 released.add(search_dir_state.search_dir.fs_path) 

1804 

1805 if current_released == len(released): 

1806 names = ", ".join(remaining) 

1807 _error( 

1808 f"There is a circular dependency (somewhere) between the search dirs: {names}." 

1809 " Note that the search directories across all packages have to be ordered (and the" 

1810 " source root should generally be last)" 

1811 ) 

1812 remaining -= released 

1813 

1814 search_dirs_in_order.append( 

1815 SearchDirOrderState( 

1816 source_root, 

1817 set(all_pkgs), 

1818 ) 

1819 ) 

1820 

1821 return tuple( 

1822 # Avoid duplicating all_pkgs 

1823 SearchDir( 

1824 s.search_dir, 

1825 frozenset(s.applies_to) if s.applies_to != all_pkgs else all_pkgs, 

1826 ) 

1827 for s in search_dirs_in_order 

1828 )