Coverage for apio/apio_context.py: 89%

282 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +0000

1"""The apio context.""" 

2 

3# -*- coding: utf-8 -*- 

4# -- This file is part of the Apio project 

5# -- (C) 2016-2019 FPGAwars 

6# -- Author Jesús Arroyo 

7# -- License GPLv2 

8 

9import os 

10import sys 

11import json 

12import platform 

13from dataclasses import dataclass 

14from enum import Enum 

15from pathlib import Path 

16from typing import List, Optional, Dict, Tuple 

17from apio.common.apio_console import cout, cerror, cstyle 

18from apio.common.apio_styles import EMPH3, INFO, EMPH1 

19from apio.common.common_util import env_build_path 

20from apio.profile import Profile, RemoteConfigPolicy 

21from apio.utils import jsonc, util, env_options 

22from apio.managers.project import Project, load_project_from_file 

23from apio.managers import packages 

24from apio.managers.packages import PackagesContext 

25from apio.utils.resource_util import ( 

26 ProjectResources, 

27 collect_project_resources, 

28 validate_project_resources, 

29 validate_config, 

30 validate_platforms, 

31 validate_packages, 

32) 

33 

34 

35# ---------- RESOURCES 

36RESOURCES_DIR = "resources" 

37 

38# --------------------------------------- 

39# ---- File: resources/platforms.jsonc 

40# -------------------------------------- 

41# -- This file contains the information regarding the supported platforms 

42# -- and their attributes. 

43PLATFORMS_JSONC = "platforms.jsonc" 

44 

45# --------------------------------------- 

46# ---- File: resources/packages.jsonc 

47# -------------------------------------- 

48# -- This file contains all the information regarding the available apio 

49# -- packages: Repository, version, name... 

50PACKAGES_JSONC = "packages.jsonc" 

51 

52# ----------------------------------------- 

53# ---- File: resources/boards.jsonc 

54# ----------------------------------------- 

55# -- Information about all the supported boards 

56# -- names, fpga family, programmer, ftdi description, vendor id, product id 

57BOARDS_JSONC = "boards.jsonc" 

58 

59# ----------------------------------------- 

60# ---- File: resources/fpgas.jsonc 

61# ----------------------------------------- 

62# -- Information about all the supported fpgas 

63# -- arch, type, size, packaging 

64FPGAS_JSONC = "fpgas.jsonc" 

65 

66# ----------------------------------------- 

67# ---- File: resources/programmers.jsonc 

68# ----------------------------------------- 

69# -- Information about all the supported programmers 

70# -- name, command to execute, arguments... 

71PROGRAMMERS_JSONC = "programmers.jsonc" 

72 

73# ----------------------------------------- 

74# ---- File: resources/config.jsonc 

75# ----------------------------------------- 

76# -- General config information. 

77CONFIG_JSONC = "config.jsonc" 

78 

79 

80@dataclass(frozen=True) 

81class ApioDefinitions: 

82 """Contains the apio definitions in the form of json dictionaries.""" 

83 

84 # -- A json dir with the content of boards.jsonc 

85 boards: dict 

86 # -- A json dir with the content of fpgas.jsonc 

87 fpgas: dict 

88 # -- A json dir with the content of programmers.jsonc. 

89 programmers: dict 

90 

91 def __post_init__(self): 

92 """Assert that all fields initialized to actual values.""" 

93 assert self.boards 

94 assert self.fpgas 

95 assert self.programmers 

96 

97 

98@dataclass(frozen=True) 

99class EnvMutations: 

100 """Contains mutations to the system env.""" 

101 

102 # -- PATH items to add. 

103 paths: List[str] 

104 # -- Vars name/value pairs. 

105 vars_list: List[Tuple[str, str]] 

106 

107 

108class ProjectPolicy(Enum): 

109 """Represents the possible context policies regarding loading apio.ini. 

110 and project related information.""" 

111 

112 # -- Project information is not loaded. 

113 NO_PROJECT = 1 

114 # -- Project information is loaded if apio.ini is found. 

115 PROJECT_OPTIONAL = 2 

116 # -- Apio.ini is required and project information must be loaded. 

117 PROJECT_REQUIRED = 3 

118 

119 

120class PackagesPolicy(Enum): 

121 """Represents the possible context policies regarding loading apio.ini. 

122 and project related information.""" 

123 

124 # -- Do not change the package state, they may exist or not, updated or 

125 # -- not. This policy requires project policy NO_PROJECT and with it, 

126 # -- the definitions are not loaded. 

127 IGNORE_PACKAGES = 1 

128 # -- Normal policy, verify that the packages are installed correctly and 

129 # -- update them if needed. 

130 ENSURE_PACKAGES = 2 

131 

132 

133class ApioContext: 

134 """Apio context. Class for accessing apio resources and configurations.""" 

135 

136 # pylint: disable=too-many-instance-attributes 

137 

138 # -- List of allowed instance vars. 

139 __slots__ = ( 

140 "project_policy", 

141 "apio_home_dir", 

142 "apio_packages_dir", 

143 "config", 

144 "profile", 

145 "platforms", 

146 "platform_id", 

147 "all_packages", 

148 "required_packages", 

149 "env_was_already_set", 

150 "_project_dir", 

151 "_project", 

152 "_project_resources", 

153 "_definitions", 

154 ) 

155 

156 def __init__( 

157 self, 

158 *, 

159 project_policy: ProjectPolicy, 

160 remote_config_policy: RemoteConfigPolicy, 

161 packages_policy: PackagesPolicy, 

162 project_dir_arg: Optional[Path] = None, 

163 env_arg: Optional[str] = None, 

164 report_env=True, 

165 ): 

166 """Initializes the ApioContext object. 

167 

168 'project_policy', 'config_policy', and 'packages_policy' are modifiers 

169 that controls the initialization of the context. 

170 

171 'project_dir_arg' is an optional user specification of the project dir. 

172 Must be None if project_policy is NO_PROJECT. 

173 

174 'env_arg' is an optional command line option value that select the 

175 apio.ini env if the project is loaded. it makes sense only when 

176 project_policy is PROJECT_REQUIRED (enforced by an assertion). 

177 

178 If an apio.ini project is loaded, the method prints to the user the 

179 selected env and board, unless if report_env = False. 

180 """ 

181 

182 # pylint: disable=too-many-arguments 

183 # pylint: disable=too-many-statements 

184 # pylint: disable=too-many-locals 

185 

186 # -- Sanity check the policies. 

187 assert isinstance(project_policy, ProjectPolicy) 

188 assert isinstance(remote_config_policy, RemoteConfigPolicy) 

189 assert isinstance(packages_policy, PackagesPolicy) 

190 

191 if packages_policy == PackagesPolicy.IGNORE_PACKAGES: 

192 assert project_policy == ProjectPolicy.NO_PROJECT 

193 

194 # -- Inform as soon as possible about the list of apio env options 

195 # -- that modify its default behavior. 

196 defined_env_options = env_options.get_defined() 

197 if defined_env_options: 197 ↛ 204line 197 didn't jump to line 204 because the condition on line 197 was always true

198 cout( 

199 f"Active env options [{', '.join(defined_env_options)}].", 

200 style=INFO, 

201 ) 

202 

203 # -- Store the project_policy 

204 assert isinstance( 

205 project_policy, ProjectPolicy 

206 ), "Not an ApioContextScope" 

207 self.project_policy = project_policy 

208 

209 # -- Sanity check, env_arg makes sense only when project_policy is 

210 # -- PROJECT_REQUIRED. 

211 if env_arg is not None: 

212 assert project_policy == ProjectPolicy.PROJECT_REQUIRED 

213 

214 # -- A flag to indicate if the system env was already set in this 

215 # -- apio session. Used to avoid multiple repeated settings that 

216 # -- make the path longer and longer. 

217 self.env_was_already_set = False 

218 

219 # -- Determine if we need to load the project, and if so, set 

220 # -- self._project_dir to the project dir, otherwise, leave it None. 

221 self._project_dir: Path = None 

222 if project_policy == ProjectPolicy.PROJECT_REQUIRED: 

223 self._project_dir = util.user_directory_or_cwd( 

224 project_dir_arg, description="Project", must_exist=True 

225 ) 

226 elif project_policy == ProjectPolicy.PROJECT_OPTIONAL: 

227 project_dir = util.user_directory_or_cwd( 

228 project_dir_arg, description="Project", must_exist=False 

229 ) 

230 if (project_dir / "apio.ini").exists(): 

231 self._project_dir = project_dir 

232 else: 

233 assert ( 

234 project_policy == ProjectPolicy.NO_PROJECT 

235 ), f"Unexpected project policy: {project_policy}" 

236 assert ( 

237 project_dir_arg is None 

238 ), "project_dir_arg specified for project policy None" 

239 

240 # -- Determine apio home and packages dirs 

241 self.apio_home_dir: Path = util.resolve_home_dir() 

242 self.apio_packages_dir: Path = util.resolve_packages_dir( 

243 self.apio_home_dir 

244 ) 

245 

246 # -- Get the jsonc source dirs. 

247 resources_dir = util.get_path_in_apio_package(RESOURCES_DIR) 

248 

249 # -- Read and validate the config information 

250 self.config = self._load_resource(CONFIG_JSONC, resources_dir) 

251 validate_config(self.config) 

252 

253 # -- Profile information, from ~/.apio/profile.json. We provide it with 

254 # -- the remote config url template from distribution.jsonc such that 

255 # -- can it fetch the remote config on demand. 

256 remote_config_url = env_options.get( 

257 env_options.APIO_REMOTE_CONFIG_URL, 

258 default=self.config["remote-config-url"], 

259 ) 

260 remote_config_ttl_days = self.config["remote-config-ttl-days"] 

261 remote_config_retry_minutes = self.config[ 

262 "remote-config-retry-minutes" 

263 ] 

264 self.profile = Profile( 

265 self.apio_home_dir, 

266 self.apio_packages_dir, 

267 remote_config_url, 

268 remote_config_ttl_days, 

269 remote_config_retry_minutes, 

270 remote_config_policy, 

271 ) 

272 

273 # -- Read the platforms information. 

274 self.platforms = self._load_resource(PLATFORMS_JSONC, resources_dir) 

275 validate_platforms(self.platforms) 

276 

277 # -- Determine the platform_id for this APIO session. 

278 self.platform_id = self._determine_platform_id(self.platforms) 

279 

280 # -- Read the apio packages information 

281 self.all_packages = self._load_resource(PACKAGES_JSONC, resources_dir) 

282 validate_packages(self.all_packages) 

283 

284 # -- Expand in place the env templates in all_packages. 

285 ApioContext._resolve_package_envs( 

286 self.all_packages, self.apio_packages_dir 

287 ) 

288 

289 # The subset of packages that are applicable to this platform. 

290 self.required_packages = self._select_required_packages_for_platform( 

291 self.all_packages, self.platform_id, self.platforms 

292 ) 

293 

294 # -- Case 1: IGNORE_PACKAGES 

295 if packages_policy == PackagesPolicy.IGNORE_PACKAGES: 

296 self._definitions = None 

297 

298 # -- Case 2: ENSURE_PACKAGES 

299 else: 

300 assert packages_policy == PackagesPolicy.ENSURE_PACKAGES 

301 

302 # -- Install missing packages. At this point, the fields that are 

303 # -- required by self.packages_context are already initialized. 

304 packages.install_missing_packages_on_the_fly( 

305 self.packages_context, verbose=False 

306 ) 

307 

308 # -- Load the definitions from the definitions file with possible 

309 # -- override by the optional project file. 

310 definitions_dir = self.apio_packages_dir / "definitions" 

311 boards = self._load_resource( 

312 BOARDS_JSONC, definitions_dir, self._project_dir 

313 ) 

314 fpgas = self._load_resource( 

315 FPGAS_JSONC, definitions_dir, self._project_dir 

316 ) 

317 programmers = self._load_resource( 

318 PROGRAMMERS_JSONC, definitions_dir, self._project_dir 

319 ) 

320 self._definitions = ApioDefinitions(boards, fpgas, programmers) 

321 

322 # -- If we determined that we need to load the project, load the 

323 # -- apio.ini data. 

324 self._project: Optional[Project] = None 

325 self._project_resources: ProjectResources = None 

326 

327 if self._project_dir: 

328 # -- Load the project object 

329 self._project = load_project_from_file( 

330 self._project_dir, env_arg, self.boards 

331 ) 

332 assert self.has_project, "init(): project not loaded" 

333 # -- Inform the user about the active env, if needed.. 

334 if report_env: 

335 self.report_env() 

336 # -- Collect and validate the project resources. 

337 # -- The project is already validated to have the required "board. 

338 self._project_resources = collect_project_resources( 

339 self._project.get_str_option("board"), 

340 self.boards, 

341 self.fpgas, 

342 self.programmers, 

343 ) 

344 # -- Validate the project resources. 

345 validate_project_resources(self._project_resources) 

346 else: 

347 assert not self.has_project, "init(): project loaded" 

348 

349 def report_env(self): 

350 """Report to the user the env and board used. Asserts that the 

351 project is loaded.""" 

352 # -- Do not call if project is not loaded. 

353 assert self.has_project 

354 

355 # -- Env name string in color 

356 styled_env_name = cstyle(self.project.env_name, style=EMPH1) 

357 

358 # -- Board id string in color 

359 styled_board_id = cstyle( 

360 self.project.env_options["board"], style=EMPH1 

361 ) 

362 

363 # -- Report. 

364 cout(f"Using env {styled_env_name} ({styled_board_id})") 

365 

366 @property 

367 def has_project(self): 

368 """Returns True if the project is loaded.""" 

369 return self._project is not None 

370 

371 @property 

372 def project_dir(self): 

373 """Returns the project dir. Should be called only if has_project_loaded 

374 is true.""" 

375 assert self.has_project, "project_dir(): project is not loaded" 

376 assert self._project_dir, "project_dir(): missing value." 

377 return self._project_dir 

378 

379 @property 

380 def project(self) -> Project: 

381 """Return the project. Should be called only if has_project() is 

382 True.""" 

383 # -- Failure here is a programming error, not a user error. 

384 assert self.has_project, "project(): project is not loaded" 

385 return self._project 

386 

387 @property 

388 def project_resources(self) -> ProjectResources: 

389 """Return the project resources. Should be called only if 

390 has_project() is True.""" 

391 # -- Failure here is a programming error, not a user error. 

392 assert self.has_project, "project(): project is not loaded" 

393 return self._project_resources 

394 

395 @property 

396 def definitions(self) -> ApioDefinitions: 

397 """Return apio definitions.""" 

398 assert self._definitions, "Apio context as no definitions" 

399 return self._definitions 

400 

401 @property 

402 def boards(self) -> dict: 

403 """Returns the apio board definitions""" 

404 return self.definitions.boards 

405 

406 @property 

407 def fpgas(self) -> dict: 

408 """Returns the apio fpgas definitions""" 

409 return self.definitions.fpgas 

410 

411 @property 

412 def programmers(self) -> dict: 

413 """Returns the apio programmers definitions""" 

414 return self.definitions.programmers 

415 

416 @property 

417 def env_build_path(self) -> str: 

418 """Returns the relative path of the current env build directory from 

419 the project dir. Should be called only when has_project is True.""" 

420 assert self.has_project, "project(): project is not loaded" 

421 return env_build_path(self.project.env_name) 

422 

423 def _load_resource( 

424 self, name: str, standard_dir: Path, custom_dir: Optional[Path] = None 

425 ) -> dict: 

426 """Load a jsonc file. Try first from custom_dir, if given, and then 

427 from standard dir. This method is called for resource files in 

428 apio/resources and definitions files in the definitions packages. 

429 """ 

430 # -- First try to load from custom dir. 

431 if custom_dir: 

432 filepath = custom_dir / name 

433 if filepath.exists(): 

434 cout(f"Loading custom '{name}'.") 

435 return self._load_resource_file(filepath) 

436 

437 # -- Else, load from the default dir. 

438 filepath = standard_dir / name 

439 return self._load_resource_file(filepath) 

440 

441 @staticmethod 

442 def _load_resource_file(filepath: Path) -> dict: 

443 """Load the resources from a given jsonc file path 

444 * OUTPUT: A dictionary with the jsonc file data 

445 In case of error it raises an exception and finish 

446 """ 

447 

448 # -- Read the jsonc file 

449 try: 

450 with filepath.open(encoding="utf8") as file: 

451 

452 # -- Read the json with comments file 

453 data_jsonc = file.read() 

454 

455 # -- The jsonc file NOT FOUND! This is an apio system error 

456 # -- It should never occur unless there is a bug in the 

457 # -- apio system files, or a bug when calling this function 

458 # -- passing a wrong file 

459 except FileNotFoundError as exc: 

460 

461 # -- Display error information 

462 cerror("[Internal] .jsonc file not found", f"{exc}") 

463 

464 # -- Abort! 

465 sys.exit(1) 

466 

467 # -- Convert the jsonc to json by removing '//' comments. 

468 data_json = jsonc.to_json(data_jsonc) 

469 

470 # -- Parse the json format! 

471 try: 

472 resource = json.loads(data_json) 

473 

474 # -- Invalid json format! This is an apio system error 

475 # -- It should never occur unless a developer has 

476 # -- made a mistake when changing the jsonc file 

477 except json.decoder.JSONDecodeError as exc: 

478 

479 # -- Display Main error 

480 cerror("Invalid .jsonc file", f"{exc}") 

481 cout(f"File: {filepath}", style=INFO) 

482 

483 # -- Abort! 

484 sys.exit(1) 

485 

486 # -- Return the object for the resource 

487 return resource 

488 

489 @staticmethod 

490 def _expand_env_template(template: str, package_path: Path) -> str: 

491 """Fills a packages env value template as they appear in 

492 packages.jsonc. Currently it recognizes only a single place holder 

493 '%p' representing the package absolute path. The '%p" can appear only 

494 at the beginning of the template. 

495 

496 E.g. '%p/bin' -> '/users/user/.apio/packages/drivers/bin' 

497 

498 NOTE: This format is very basic but is sufficient for the current 

499 needs. If needed, extend or modify it. 

500 """ 

501 

502 # Case 1: No place holder. 

503 if "%p" not in template: 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true

504 return template 

505 

506 # Case 2: The template contains only the placeholder. 

507 if template == "%p": 507 ↛ 508line 507 didn't jump to line 508 because the condition on line 507 was never true

508 return str(package_path) 

509 

510 # Case 3: The place holder is the prefix of the template's path. 

511 if template.startswith("%p/"): 511 ↛ 515line 511 didn't jump to line 515 because the condition on line 511 was always true

512 return str(package_path / template[3:]) 

513 

514 # Case 4: Unsupported. 

515 raise RuntimeError(f"Invalid env template: [{template}]") 

516 

517 @staticmethod 

518 def _resolve_package_envs( 

519 packages_: Dict[str, Dict], packages_dir: Path 

520 ) -> None: 

521 """Resolve in place the path and var value templates in the 

522 given packages dictionary. For example, %p is replaced with 

523 the package's absolute path.""" 

524 

525 for package_name, package_config in packages_.items(): 

526 

527 # -- Get the package root dir. 

528 package_path = packages_dir / package_name 

529 

530 # -- Get the json 'env' section. We require it, even if empty, 

531 # -- for clarity reasons. 

532 assert "env" in package_config 

533 package_env = package_config["env"] 

534 

535 # -- Expand the values in the "path" section, if any. 

536 path_section = package_env.get("path", []) 

537 for i, path_template in enumerate(path_section): 

538 path_section[i] = ApioContext._expand_env_template( 

539 path_template, package_path 

540 ) 

541 

542 # -- Expand the values in the "vars" section, if any. 

543 vars_section = package_env.get("vars", {}) 

544 for var_name, val_template in vars_section.items(): 

545 vars_section[var_name] = ApioContext._expand_env_template( 

546 val_template, package_path 

547 ) 

548 

549 def get_required_package_info(self, package_name: str) -> str: 

550 """Returns the information of the package with given name. 

551 The information is a JSON dict originated at packages.json(). 

552 Exits with an error message if the package is not defined. 

553 """ 

554 package_info = self.required_packages.get(package_name, None) 

555 if package_info is None: 555 ↛ 556line 555 didn't jump to line 556 because the condition on line 555 was never true

556 cerror(f"Unknown package '{package_name}'") 

557 sys.exit(1) 

558 

559 return package_info 

560 

561 def get_package_dir(self, package_name: str) -> Path: 

562 """Returns the root path of a package with given name.""" 

563 

564 return self.apio_packages_dir / package_name 

565 

566 def get_tmp_dir(self, create: bool = True) -> Path: 

567 """Return the tmp dir under the apio home dir. If 'create' is true 

568 create the dir and its parents if they do not exist.""" 

569 tmp_dir = self.apio_home_dir / "tmp" 

570 if create: 

571 tmp_dir.mkdir(parents=True, exist_ok=True) 

572 return tmp_dir 

573 

574 @staticmethod 

575 def _determine_platform_id(platforms: Dict[str, Dict]) -> str: 

576 """Determines and returns the platform io based on system info and 

577 optional override.""" 

578 # -- Use override and get from the underlying system. 

579 platform_id_override = env_options.get(env_options.APIO_PLATFORM) 

580 if platform_id_override: 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true

581 platform_id = platform_id_override 

582 else: 

583 platform_id = ApioContext._get_system_platform_id() 

584 

585 # Stick to the naming conventions we use for boards, fpgas, etc. 

586 platform_id = platform_id.replace("_", "-") 

587 

588 # -- Verify it's valid. This can be a user error if the override 

589 # -- is invalid. 

590 if platform_id not in platforms.keys(): 590 ↛ 591line 590 didn't jump to line 591 because the condition on line 590 was never true

591 cerror(f"Unknown platform id: [{platform_id}]") 

592 cout( 

593 "For the list of supported platforms " 

594 "type 'apio system platforms'.", 

595 style=INFO, 

596 ) 

597 sys.exit(1) 

598 

599 # -- All done ok. 

600 return platform_id 

601 

602 @property 

603 def packages_context(self) -> PackagesContext: 

604 """Return a PackagesContext with info extracted from this 

605 ApioContext.""" 

606 return PackagesContext( 

607 profile=self.profile, 

608 required_packages=self.required_packages, 

609 platform_id=self.platform_id, 

610 packages_dir=self.apio_packages_dir, 

611 ) 

612 

613 @staticmethod 

614 def _select_required_packages_for_platform( 

615 all_packages: Dict[str, Dict], 

616 platform_id: str, 

617 platforms: Dict[str, Dict], 

618 ) -> Dict: 

619 """Given a dictionary with the packages.jsonc packages infos, 

620 returns subset dictionary with packages that are available for 

621 'platform_id'. 

622 """ 

623 

624 # -- If fails, this is a programming error. 

625 assert platform_id in platforms, platform 

626 

627 # -- Final dict with the output packages 

628 filtered_packages = {} 

629 

630 # -- Check all the packages 

631 for package_name in all_packages.keys(): 

632 

633 # -- Get the package info. 

634 package_info = all_packages[package_name] 

635 

636 # -- Get the list of platforms ids on which this package is 

637 # -- available. The package is available on all platforms unless 

638 # -- restricted by the ""restricted-to-platforms" field. 

639 required_for_platforms = package_info.get( 

640 "restricted-to-platforms", platforms.keys() 

641 ) 

642 

643 # -- Sanity check that all platform ids are valid. If fails it's 

644 # -- a programming error. 

645 for p in required_for_platforms: 

646 assert p in platforms.keys(), platform 

647 

648 # -- If available for 'platform_id', add it. 

649 if platform_id in required_for_platforms: 

650 filtered_packages[package_name] = all_packages[package_name] 

651 

652 # -- Return the subset dict with the packages for 'platform_id'. 

653 return filtered_packages 

654 

655 @staticmethod 

656 def _get_system_platform_id() -> str: 

657 """Return a String with the current platform: 

658 ex. linux-x86-64 

659 ex. windows-amd64""" 

660 

661 # -- Get the platform: linux, windows, darwin 

662 type_ = platform.system().lower() 

663 platform_str = f"{type_}" 

664 

665 # -- Get the architecture 

666 arch = platform.machine().lower() 

667 

668 # -- Special case for windows 

669 if type_ == "windows": 669 ↛ 671line 669 didn't jump to line 671 because the condition on line 669 was never true

670 # -- Assume all the windows to be 64-bits 

671 arch = "amd64" 

672 

673 # -- Add the architecture, if it exists 

674 if arch: 674 ↛ 678line 674 didn't jump to line 678 because the condition on line 674 was always true

675 platform_str += f"_{arch}" 

676 

677 # -- Return the full platform 

678 return platform_str 

679 

680 @property 

681 def is_linux(self) -> bool: 

682 """Returns True iff platform_id indicates linux.""" 

683 return "linux" in self.platform_id 

684 

685 @property 

686 def is_darwin(self) -> bool: 

687 """Returns True iff platform_id indicates Mac OSX.""" 

688 return "darwin" in self.platform_id 

689 

690 @property 

691 def is_windows(self) -> bool: 

692 """Returns True iff platform_id indicates windows.""" 

693 return "windows" in self.platform_id 

694 

695 def _get_env_mutations_for_packages(self) -> EnvMutations: 

696 """Collects the env mutation for each of the defined packages, 

697 in the order they are defined.""" 

698 

699 result = EnvMutations([], []) 

700 for _, package_config in self.required_packages.items(): 

701 # -- Get the json 'env' section. We require it, even if it's empty, 

702 # -- for clarity reasons. 

703 assert "env" in package_config 

704 package_env = package_config["env"] 

705 

706 # -- Collect the path values. 

707 path_list = package_env.get("path", []) 

708 result.paths.extend(path_list) 

709 

710 # -- Collect the env vars (name, value) pairs. 

711 vars_section = package_env.get("vars", {}) 

712 for var_name, var_value in vars_section.items(): 

713 result.vars_list.append((var_name, var_value)) 

714 

715 return result 

716 

717 def _dump_env_mutations(self, mutations: EnvMutations) -> None: 

718 """Dumps a user friendly representation of the env mutations.""" 

719 cout("Environment settings:", style=EMPH3) 

720 

721 # -- Print PATH mutations. 

722 windows = self.is_windows 

723 for p in reversed(mutations.paths): 

724 styled_name = cstyle("PATH", style=EMPH3) 

725 if windows: 725 ↛ 726line 725 didn't jump to line 726 because the condition on line 725 was never true

726 cout(f"set {styled_name}={p};%PATH%") 

727 else: 

728 cout(f'{styled_name}="{p}:$PATH"') 

729 

730 # -- Print vars mutations. 

731 for name, val in mutations.vars_list: 

732 styled_name = cstyle(name, style=EMPH3) 

733 if windows: 733 ↛ 734line 733 didn't jump to line 734 because the condition on line 733 was never true

734 cout(f"set {styled_name}={val}") 

735 else: 

736 cout(f'{styled_name}="{val}"') 

737 

738 def _apply_env_mutations(self, mutations: EnvMutations) -> None: 

739 """Apply a given set of env mutations, while preserving their order.""" 

740 

741 # -- Apply the path mutations, while preserving order. 

742 old_val = os.environ["PATH"] 

743 items = mutations.paths + [old_val] 

744 new_val = os.pathsep.join(items) 

745 os.environ["PATH"] = new_val 

746 

747 # -- Apply the vars mutations, while preserving order. 

748 for name, value in mutations.vars_list: 

749 os.environ[name] = value 

750 

751 def set_env_for_packages( 

752 self, *, quiet: bool = False, verbose: bool = False 

753 ) -> None: 

754 """Sets the environment variables for using all the that are 

755 available for this platform, even if currently not installed. 

756 

757 The function sets the environment only on first call and in latter 

758 calls skips the operation silently. 

759 

760 If quite is set, no output is printed. When verbose is set, additional 

761 output such as the env vars mutations are printed, otherwise, a minimal 

762 information is printed to make the user aware that they commands they 

763 see are executed in a modified env settings. 

764 """ 

765 

766 # -- If this fails, this is a programming error. Quiet and verbose 

767 # -- cannot be combined. 

768 assert not (quiet and verbose), "Can't have both quite and verbose." 

769 

770 # -- Collect the env mutations for all packages. 

771 mutations = self._get_env_mutations_for_packages() 

772 

773 if verbose: 

774 self._dump_env_mutations(mutations) 

775 

776 # -- If this is the first call in this apio invocation, apply the 

777 # -- mutations. These mutations are temporary for the lifetime of this 

778 # -- process and does not affect the user's shell environment. 

779 # -- The mutations are also inherited by child processes such as the 

780 # -- scons processes. 

781 if not self.env_was_already_set: 781 ↛ exitline 781 didn't return from function 'set_env_for_packages' because the condition on line 781 was always true

782 self._apply_env_mutations(mutations) 

783 self.env_was_already_set = True 

784 if not verbose and not quiet: 

785 cout("Setting shell vars.")