Coverage for apio / apio_context.py: 85%

321 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 02:38 +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 

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

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

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 # -- List of env vars to unset. 

103 unset_vars: List[str] 

104 

105 # -- PATH items to add. 

106 paths: List[str] 

107 

108 # -- Dict with env vars name/value to set. 

109 set_vars: Dict[str, str] 

110 

111 

112class ProjectPolicy(Enum): 

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

114 and project related information.""" 

115 

116 # -- Project information is not loaded. 

117 NO_PROJECT = 1 

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

119 PROJECT_OPTIONAL = 2 

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

121 PROJECT_REQUIRED = 3 

122 

123 

124class PackagesPolicy(Enum): 

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

126 and project related information.""" 

127 

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

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

130 # -- the definitions are not loaded. 

131 IGNORE_PACKAGES = 1 

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

133 # -- update them if needed. 

134 ENSURE_PACKAGES = 2 

135 

136 

137class ApioContext: 

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

139 

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

141 

142 # -- List of allowed instance vars. 

143 __slots__ = ( 

144 "project_policy", 

145 "apio_home_dir", 

146 "apio_packages_dir", 

147 "config", 

148 "profile", 

149 "platforms", 

150 "platform_id", 

151 "scons_shell_id", 

152 "all_packages", 

153 "required_packages", 

154 "env_was_already_set", 

155 "_project_dir", 

156 "_project", 

157 "_project_resources", 

158 "_definitions", 

159 ) 

160 

161 def __init__( 

162 self, 

163 *, 

164 project_policy: ProjectPolicy, 

165 remote_config_policy: RemoteConfigPolicy, 

166 packages_policy: PackagesPolicy, 

167 project_dir_arg: Optional[Path] = None, 

168 env_arg: Optional[str] = None, 

169 report_env=True, 

170 ): 

171 """Initializes the ApioContext object. 

172 

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

174 that controls the initialization of the context. 

175 

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

177 Must be None if project_policy is NO_PROJECT. 

178 

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

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

181 project_policy is PROJECT_REQUIRED (enforced by an assertion). 

182 

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

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

185 """ 

186 

187 # pylint: disable=too-many-arguments 

188 # pylint: disable=too-many-statements 

189 # pylint: disable=too-many-locals 

190 

191 # -- Sanity check the policies. 

192 assert isinstance(project_policy, ProjectPolicy) 

193 assert isinstance(remote_config_policy, RemoteConfigPolicy) 

194 assert isinstance(packages_policy, PackagesPolicy) 

195 

196 if packages_policy == PackagesPolicy.IGNORE_PACKAGES: 

197 assert project_policy == ProjectPolicy.NO_PROJECT 

198 

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

200 # -- that modify its default behavior. 

201 defined_env_options = env_options.get_defined() 

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

203 cout( 

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

205 style=INFO, 

206 ) 

207 

208 # -- Store the project_policy 

209 assert isinstance( 

210 project_policy, ProjectPolicy 

211 ), "Not an ApioContextScope" 

212 self.project_policy = project_policy 

213 

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

215 # -- PROJECT_REQUIRED. 

216 if env_arg is not None: 

217 assert project_policy == ProjectPolicy.PROJECT_REQUIRED 

218 

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

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

221 # -- make the path longer and longer. 

222 self.env_was_already_set = False 

223 

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

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

226 self._project_dir: Path = None 

227 if project_policy == ProjectPolicy.PROJECT_REQUIRED: 

228 self._project_dir = util.user_directory_or_cwd( 

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

230 ) 

231 elif project_policy == ProjectPolicy.PROJECT_OPTIONAL: 

232 project_dir = util.user_directory_or_cwd( 

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

234 ) 

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

236 self._project_dir = project_dir 

237 else: 

238 assert ( 

239 project_policy == ProjectPolicy.NO_PROJECT 

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

241 assert ( 

242 project_dir_arg is None 

243 ), "project_dir_arg specified for project policy None" 

244 

245 # -- Determine apio home and packages dirs 

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

247 self.apio_packages_dir: Path = util.resolve_packages_dir( 

248 self.apio_home_dir 

249 ) 

250 

251 # -- Get the jsonc source dirs. 

252 resources_dir = util.get_path_in_apio_package(RESOURCES_DIR) 

253 

254 # -- Read and validate the config information 

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

256 validate_config(self.config) 

257 

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

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

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

261 remote_config_url = env_options.get( 

262 env_options.APIO_REMOTE_CONFIG_URL, 

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

264 ) 

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

266 remote_config_retry_minutes = self.config[ 

267 "remote-config-retry-minutes" 

268 ] 

269 self.profile = Profile( 

270 self.apio_home_dir, 

271 self.apio_packages_dir, 

272 remote_config_url, 

273 remote_config_ttl_days, 

274 remote_config_retry_minutes, 

275 remote_config_policy, 

276 ) 

277 

278 # -- Read the platforms information. 

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

280 validate_platforms(self.platforms) 

281 

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

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

284 

285 # -- Determine the shell id that scons will use. 

286 # -- See _determine_scons_shell_id() for possible values. 

287 self.scons_shell_id = self._determine_scons_shell_id(self.platform_id) 

288 

289 # -- Read the apio packages information 

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

291 validate_packages(self.all_packages) 

292 

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

294 ApioContext._resolve_package_envs( 

295 self.all_packages, self.apio_packages_dir 

296 ) 

297 

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

299 self.required_packages = self._select_required_packages_for_platform( 

300 self.all_packages, self.platform_id, self.platforms 

301 ) 

302 

303 # -- Case 1: IGNORE_PACKAGES 

304 if packages_policy == PackagesPolicy.IGNORE_PACKAGES: 

305 self._definitions = None 

306 

307 # -- Case 2: ENSURE_PACKAGES 

308 else: 

309 assert packages_policy == PackagesPolicy.ENSURE_PACKAGES 

310 

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

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

313 packages.install_missing_packages_on_the_fly( 

314 self.packages_context, verbose=False 

315 ) 

316 

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

318 # -- override by the optional project file. 

319 definitions_dir = self.apio_packages_dir / "definitions" 

320 boards = self._load_resource( 

321 BOARDS_JSONC, definitions_dir, self._project_dir 

322 ) 

323 fpgas = self._load_resource( 

324 FPGAS_JSONC, definitions_dir, self._project_dir 

325 ) 

326 programmers = self._load_resource( 

327 PROGRAMMERS_JSONC, definitions_dir, self._project_dir 

328 ) 

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

330 

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

332 # -- apio.ini data. 

333 self._project: Optional[Project] = None 

334 self._project_resources: ProjectResources = None 

335 

336 if self._project_dir: 

337 # -- Load the project object 

338 self._project = load_project_from_file( 

339 self._project_dir, env_arg, self.boards 

340 ) 

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

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

343 if report_env: 

344 self.report_env() 

345 # -- Collect and validate the project resources. 

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

347 self._project_resources = collect_project_resources( 

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

349 self.boards, 

350 self.fpgas, 

351 self.programmers, 

352 ) 

353 # -- Validate the project resources. 

354 validate_project_resources(self._project_resources) 

355 else: 

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

357 

358 def report_env(self): 

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

360 project is loaded.""" 

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

362 assert self.has_project 

363 

364 # -- Env name string in color 

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

366 

367 # -- Board id string in color 

368 styled_board_id = cstyle( 

369 self.project.get_str_option("board"), style=EMPH1 

370 ) 

371 

372 # -- Report. 

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

374 

375 @property 

376 def has_project(self): 

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

378 return self._project is not None 

379 

380 @property 

381 def project_dir(self): 

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

383 is true.""" 

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

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

386 return self._project_dir 

387 

388 @property 

389 def project(self) -> Project: 

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

391 True.""" 

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

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

394 return self._project 

395 

396 @property 

397 def project_resources(self) -> ProjectResources: 

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

399 has_project() is True.""" 

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

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

402 return self._project_resources 

403 

404 @property 

405 def definitions(self) -> ApioDefinitions: 

406 """Return apio definitions.""" 

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

408 return self._definitions 

409 

410 @property 

411 def boards(self) -> dict: 

412 """Returns the apio board definitions""" 

413 return self.definitions.boards 

414 

415 @property 

416 def fpgas(self) -> dict: 

417 """Returns the apio fpgas definitions""" 

418 return self.definitions.fpgas 

419 

420 @property 

421 def programmers(self) -> dict: 

422 """Returns the apio programmers definitions""" 

423 return self.definitions.programmers 

424 

425 @property 

426 def env_build_path(self) -> str: 

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

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

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

430 return env_build_path(self.project.env_name) 

431 

432 def _load_resource( 

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

434 ) -> dict: 

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

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

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

438 """ 

439 

440 # -- Load the standard definition as a json dict. 

441 filepath = standard_dir / name 

442 result = self._load_resource_file(filepath) 

443 

444 # -- If there is a project specific override file, apply it on 

445 # -- top of the standard apio definition dict. 

446 if custom_dir: 

447 filepath = custom_dir / name 

448 if filepath.exists(): 

449 # -- Load the override json dict. 

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

451 override = self._load_resource_file(filepath) 

452 # -- Apply the override. Entries in override replace same 

453 # -- key entries in result or if unique are added. 

454 result.update(override) 

455 

456 # -- All done. 

457 return result 

458 

459 @staticmethod 

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

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

462 * OUTPUT: A dictionary with the jsonc file data 

463 In case of error it raises an exception and finish 

464 """ 

465 

466 # -- Read the jsonc file 

467 try: 

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

469 

470 # -- Read the json with comments file 

471 data_jsonc = file.read() 

472 

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

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

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

476 # -- passing a wrong file 

477 except FileNotFoundError as exc: 

478 

479 # -- Display error information 

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

481 

482 # -- Abort! 

483 sys.exit(1) 

484 

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

486 data_json = jsonc.to_json(data_jsonc) 

487 

488 # -- Parse the json format! 

489 try: 

490 resource = json.loads(data_json) 

491 

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

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

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

495 except json.decoder.JSONDecodeError as exc: 

496 cerror(f"'{filepath}' has bad format", f"{exc}") 

497 sys.exit(1) 

498 

499 # -- Return the object for the resource 

500 return resource 

501 

502 @staticmethod 

503 def _expand_env_values(template: Optional[str], package_path: Path) -> str: 

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

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

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

507 at the beginning of the template. 

508 

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

510 

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

512 needs. If needed, extend or modify it. 

513 """ 

514 

515 # Case 1: No place holder -> no change. 

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

517 return template 

518 

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

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

521 return str(package_path) 

522 

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

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

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

526 

527 # Case 4: Unsupported. 

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

529 

530 @staticmethod 

531 def _resolve_package_envs( 

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

533 ) -> None: 

534 """Resolve in-place the path and var value templates in the 

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

536 the package's absolute path.""" 

537 

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

539 

540 # -- Get the package root dir. 

541 package_path = packages_dir / package_name 

542 

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

544 # -- for clarity reasons. 

545 assert "env" in package_config 

546 package_env = package_config["env"] 

547 

548 # -- NOTE: There is no need to expand values in the "unset-env" 

549 # -- section since it contains env names only. 

550 

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

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

553 for i, path_template in enumerate(path_section): 

554 path_section[i] = ApioContext._expand_env_values( 

555 path_template, package_path 

556 ) 

557 

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

559 set_vars_section = package_env.get("set-vars", {}) 

560 for var_name, var_value in set_vars_section.items(): 

561 set_vars_section[var_name] = ApioContext._expand_env_values( 

562 var_value, package_path 

563 ) 

564 

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

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

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

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

569 """ 

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

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

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

573 sys.exit(1) 

574 

575 return package_info 

576 

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

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

579 

580 return self.apio_packages_dir / package_name 

581 

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

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

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

585 tmp_dir = self.apio_home_dir / "tmp" 

586 if create: 

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

588 return tmp_dir 

589 

590 @staticmethod 

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

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

593 optional override.""" 

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

595 platform_id_override = env_options.get(env_options.APIO_PLATFORM) 

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

597 platform_id = platform_id_override 

598 else: 

599 platform_id = ApioContext._get_system_platform_id() 

600 

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

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

603 

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

605 # -- is invalid. 

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

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

608 cout( 

609 "See Apio's documentation for supported platforms.", 

610 style=INFO, 

611 ) 

612 sys.exit(1) 

613 

614 # -- All done ok. 

615 return platform_id 

616 

617 @staticmethod 

618 def _determine_scons_shell_id(platform_id: str) -> str: 

619 """ 

620 Returns a simplified string name of the shell that SCons will use 

621 for executing shell-dependent commands. See code below for possible 

622 values. 

623 """ 

624 

625 # pylint: disable=too-many-return-statements 

626 

627 # -- Handle windows. 

628 if "windows" in platform_id: 628 ↛ 629line 628 didn't jump to line 629 because the condition on line 628 was never true

629 comspec = os.environ.get("COMSPEC", "").lower() 

630 if "powershell.exe" in comspec or "pwsh.exe" in comspec: 

631 return "powershell" 

632 if "cmd.exe" in comspec: 

633 return "cmd" 

634 return "unknown" 

635 

636 # -- Handle macOS, Linux, etc. 

637 shell_path = os.environ.get("SHELL", "").lower() 

638 if "bash" in shell_path: 638 ↛ 639line 638 didn't jump to line 639 because the condition on line 638 was never true

639 return "bash" 

640 if "zsh" in shell_path: 640 ↛ 641line 640 didn't jump to line 641 because the condition on line 640 was never true

641 return "zsh" 

642 if "fish" in shell_path: 642 ↛ 643line 642 didn't jump to line 643 because the condition on line 642 was never true

643 return "fish" 

644 if "dash" in shell_path: 644 ↛ 645line 644 didn't jump to line 645 because the condition on line 644 was never true

645 return "dash" 

646 if "ksh" in shell_path: 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true

647 return "ksh" 

648 if "csh" in shell_path or "tcsh" in shell_path: 648 ↛ 649line 648 didn't jump to line 649 because the condition on line 648 was never true

649 return "cshell" 

650 return "unknown" 

651 

652 @property 

653 def packages_context(self) -> PackagesContext: 

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

655 ApioContext.""" 

656 return PackagesContext( 

657 profile=self.profile, 

658 required_packages=self.required_packages, 

659 platform_id=self.platform_id, 

660 packages_dir=self.apio_packages_dir, 

661 ) 

662 

663 @staticmethod 

664 def _select_required_packages_for_platform( 

665 all_packages: Dict[str, Dict], 

666 platform_id: str, 

667 platforms: Dict[str, Dict], 

668 ) -> Dict: 

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

670 returns subset dictionary with packages that are available for 

671 'platform_id'. 

672 """ 

673 

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

675 assert platform_id in platforms, platform 

676 

677 # -- Final dict with the output packages 

678 filtered_packages = {} 

679 

680 # -- Check all the packages 

681 for package_name in all_packages.keys(): 

682 

683 # -- Get the package info. 

684 package_info = all_packages[package_name] 

685 

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

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

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

689 required_for_platforms = package_info.get( 

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

691 ) 

692 

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

694 # -- a programming error. 

695 for p in required_for_platforms: 

696 assert p in platforms.keys(), platform 

697 

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

699 if platform_id in required_for_platforms: 

700 filtered_packages[package_name] = all_packages[package_name] 

701 

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

703 return filtered_packages 

704 

705 @staticmethod 

706 def _get_system_platform_id() -> str: 

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

708 ex. linux-x86-64 

709 ex. windows-amd64""" 

710 

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

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

713 platform_str = f"{type_}" 

714 

715 # -- Get the architecture 

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

717 

718 # -- Special case for windows 

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

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

721 arch = "amd64" 

722 

723 # -- Add the architecture, if it exists 

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

725 platform_str += f"_{arch}" 

726 

727 # -- Return the full platform 

728 return platform_str 

729 

730 @property 

731 def is_linux(self) -> bool: 

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

733 return "linux" in self.platform_id 

734 

735 @property 

736 def is_darwin(self) -> bool: 

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

738 return "darwin" in self.platform_id 

739 

740 @property 

741 def is_windows(self) -> bool: 

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

743 return "windows" in self.platform_id 

744 

745 def _get_env_mutations_for_packages(self) -> EnvMutations: 

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

747 in the order they are defined.""" 

748 

749 unset_vars: List[str] = [] 

750 paths: List[str] = [] 

751 set_vars: Dict[str, str] = {} 

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

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

754 # -- for clarity reasons. 

755 assert "env" in package_config 

756 package_env = package_config["env"] 

757 

758 # -- Collect the env vars to unset. 

759 unset_vars_section = package_env.get("unset-vars", []) 

760 for var_name in unset_vars_section: 

761 # -- Detect duplicates. 

762 assert var_name not in unset_vars, var_name 

763 unset_vars.append(var_name) 

764 

765 # -- Collect the path values. 

766 package_paths = package_env.get("path", []) 

767 paths.extend(package_paths) 

768 

769 # -- Collect the env vars to set (name, value) pairs. 

770 set_vars_section = package_env.get("set-vars", {}) 

771 for var_name, var_value in set_vars_section.items(): 

772 # -- Detect duplicates. 

773 assert var_name not in set_vars, var_name 

774 set_vars[var_name] = var_value 

775 

776 return EnvMutations(unset_vars, paths, set_vars) 

777 

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

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

780 cout("Environment settings:", style=EMPH2) 

781 

782 # -- Print PATH mutations. 

783 windows = self.is_windows 

784 

785 # -- Print unset vars. 

786 for name in mutations.unset_vars: 

787 styled_name = cstyle(name, style=EMPH3) 

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

789 cout(f" set {styled_name}=") 

790 else: 

791 cout(f" unset {styled_name}") 

792 

793 # -- Dump paths. 

794 for p in reversed(mutations.paths): 

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

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

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

798 else: 

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

800 

801 # -- Print set vars. 

802 for name, val in mutations.set_vars.items(): 

803 styled_name = cstyle(name, style=EMPH3) 

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

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

806 else: 

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

808 

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

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

811 

812 # -- Apply the unset var mutations 

813 for name in mutations.unset_vars: 

814 os.environ.pop(name, None) 

815 

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

817 # -- NOTE: We treat the old path items as a single items. 

818 old_val = os.environ["PATH"] 

819 items = mutations.paths + [old_val] 

820 new_val = os.pathsep.join(items) 

821 os.environ["PATH"] = new_val 

822 

823 # -- Apply the set var mutations 

824 for name, value in mutations.set_vars.items(): 

825 os.environ[name] = value 

826 

827 def set_env_for_packages( 

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

829 ) -> None: 

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

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

832 

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

834 calls skips the operation silently. 

835 

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

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

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

839 see are executed in a modified env settings. 

840 """ 

841 

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

843 # -- cannot be combined. 

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

845 

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

847 mutations = self._get_env_mutations_for_packages() 

848 

849 if verbose: 

850 self._dump_env_mutations(mutations) 

851 

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

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

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

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

856 # -- scons processes. 

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

858 self._apply_env_mutations(mutations) 

859 self.env_was_already_set = True 

860 if not verbose and not quiet: 

861 cout("Setting shell vars.")