Coverage for apio / apio_context.py: 85%

307 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01:53 +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 "scons_shell_id", 

148 "all_packages", 

149 "required_packages", 

150 "env_was_already_set", 

151 "_project_dir", 

152 "_project", 

153 "_project_resources", 

154 "_definitions", 

155 ) 

156 

157 def __init__( 

158 self, 

159 *, 

160 project_policy: ProjectPolicy, 

161 remote_config_policy: RemoteConfigPolicy, 

162 packages_policy: PackagesPolicy, 

163 project_dir_arg: Optional[Path] = None, 

164 env_arg: Optional[str] = None, 

165 report_env=True, 

166 ): 

167 """Initializes the ApioContext object. 

168 

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

170 that controls the initialization of the context. 

171 

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

173 Must be None if project_policy is NO_PROJECT. 

174 

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

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

177 project_policy is PROJECT_REQUIRED (enforced by an assertion). 

178 

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

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

181 """ 

182 

183 # pylint: disable=too-many-arguments 

184 # pylint: disable=too-many-statements 

185 # pylint: disable=too-many-locals 

186 

187 # -- Sanity check the policies. 

188 assert isinstance(project_policy, ProjectPolicy) 

189 assert isinstance(remote_config_policy, RemoteConfigPolicy) 

190 assert isinstance(packages_policy, PackagesPolicy) 

191 

192 if packages_policy == PackagesPolicy.IGNORE_PACKAGES: 

193 assert project_policy == ProjectPolicy.NO_PROJECT 

194 

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

196 # -- that modify its default behavior. 

197 defined_env_options = env_options.get_defined() 

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

199 cout( 

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

201 style=INFO, 

202 ) 

203 

204 # -- Store the project_policy 

205 assert isinstance( 

206 project_policy, ProjectPolicy 

207 ), "Not an ApioContextScope" 

208 self.project_policy = project_policy 

209 

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

211 # -- PROJECT_REQUIRED. 

212 if env_arg is not None: 

213 assert project_policy == ProjectPolicy.PROJECT_REQUIRED 

214 

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

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

217 # -- make the path longer and longer. 

218 self.env_was_already_set = False 

219 

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

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

222 self._project_dir: Path = None 

223 if project_policy == ProjectPolicy.PROJECT_REQUIRED: 

224 self._project_dir = util.user_directory_or_cwd( 

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

226 ) 

227 elif project_policy == ProjectPolicy.PROJECT_OPTIONAL: 

228 project_dir = util.user_directory_or_cwd( 

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

230 ) 

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

232 self._project_dir = project_dir 

233 else: 

234 assert ( 

235 project_policy == ProjectPolicy.NO_PROJECT 

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

237 assert ( 

238 project_dir_arg is None 

239 ), "project_dir_arg specified for project policy None" 

240 

241 # -- Determine apio home and packages dirs 

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

243 self.apio_packages_dir: Path = util.resolve_packages_dir( 

244 self.apio_home_dir 

245 ) 

246 

247 # -- Get the jsonc source dirs. 

248 resources_dir = util.get_path_in_apio_package(RESOURCES_DIR) 

249 

250 # -- Read and validate the config information 

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

252 validate_config(self.config) 

253 

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

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

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

257 remote_config_url = env_options.get( 

258 env_options.APIO_REMOTE_CONFIG_URL, 

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

260 ) 

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

262 remote_config_retry_minutes = self.config[ 

263 "remote-config-retry-minutes" 

264 ] 

265 self.profile = Profile( 

266 self.apio_home_dir, 

267 self.apio_packages_dir, 

268 remote_config_url, 

269 remote_config_ttl_days, 

270 remote_config_retry_minutes, 

271 remote_config_policy, 

272 ) 

273 

274 # -- Read the platforms information. 

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

276 validate_platforms(self.platforms) 

277 

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

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

280 

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

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

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

284 

285 # -- Read the apio packages information 

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

287 validate_packages(self.all_packages) 

288 

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

290 ApioContext._resolve_package_envs( 

291 self.all_packages, self.apio_packages_dir 

292 ) 

293 

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

295 self.required_packages = self._select_required_packages_for_platform( 

296 self.all_packages, self.platform_id, self.platforms 

297 ) 

298 

299 # -- Case 1: IGNORE_PACKAGES 

300 if packages_policy == PackagesPolicy.IGNORE_PACKAGES: 

301 self._definitions = None 

302 

303 # -- Case 2: ENSURE_PACKAGES 

304 else: 

305 assert packages_policy == PackagesPolicy.ENSURE_PACKAGES 

306 

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

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

309 packages.install_missing_packages_on_the_fly( 

310 self.packages_context, verbose=False 

311 ) 

312 

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

314 # -- override by the optional project file. 

315 definitions_dir = self.apio_packages_dir / "definitions" 

316 boards = self._load_resource( 

317 BOARDS_JSONC, definitions_dir, self._project_dir 

318 ) 

319 fpgas = self._load_resource( 

320 FPGAS_JSONC, definitions_dir, self._project_dir 

321 ) 

322 programmers = self._load_resource( 

323 PROGRAMMERS_JSONC, definitions_dir, self._project_dir 

324 ) 

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

326 

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

328 # -- apio.ini data. 

329 self._project: Optional[Project] = None 

330 self._project_resources: ProjectResources = None 

331 

332 if self._project_dir: 

333 # -- Load the project object 

334 self._project = load_project_from_file( 

335 self._project_dir, env_arg, self.boards 

336 ) 

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

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

339 if report_env: 

340 self.report_env() 

341 # -- Collect and validate the project resources. 

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

343 self._project_resources = collect_project_resources( 

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

345 self.boards, 

346 self.fpgas, 

347 self.programmers, 

348 ) 

349 # -- Validate the project resources. 

350 validate_project_resources(self._project_resources) 

351 else: 

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

353 

354 def report_env(self): 

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

356 project is loaded.""" 

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

358 assert self.has_project 

359 

360 # -- Env name string in color 

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

362 

363 # -- Board id string in color 

364 styled_board_id = cstyle( 

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

366 ) 

367 

368 # -- Report. 

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

370 

371 @property 

372 def has_project(self): 

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

374 return self._project is not None 

375 

376 @property 

377 def project_dir(self): 

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

379 is true.""" 

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

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

382 return self._project_dir 

383 

384 @property 

385 def project(self) -> Project: 

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

387 True.""" 

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

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

390 return self._project 

391 

392 @property 

393 def project_resources(self) -> ProjectResources: 

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

395 has_project() is True.""" 

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

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

398 return self._project_resources 

399 

400 @property 

401 def definitions(self) -> ApioDefinitions: 

402 """Return apio definitions.""" 

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

404 return self._definitions 

405 

406 @property 

407 def boards(self) -> dict: 

408 """Returns the apio board definitions""" 

409 return self.definitions.boards 

410 

411 @property 

412 def fpgas(self) -> dict: 

413 """Returns the apio fpgas definitions""" 

414 return self.definitions.fpgas 

415 

416 @property 

417 def programmers(self) -> dict: 

418 """Returns the apio programmers definitions""" 

419 return self.definitions.programmers 

420 

421 @property 

422 def env_build_path(self) -> str: 

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

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

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

426 return env_build_path(self.project.env_name) 

427 

428 def _load_resource( 

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

430 ) -> dict: 

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

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

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

434 """ 

435 

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

437 filepath = standard_dir / name 

438 result = self._load_resource_file(filepath) 

439 

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

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

442 if custom_dir: 

443 filepath = custom_dir / name 

444 if filepath.exists(): 

445 # -- Load the override json dict. 

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

447 override = self._load_resource_file(filepath) 

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

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

450 result.update(override) 

451 

452 # -- All done. 

453 return result 

454 

455 @staticmethod 

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

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

458 * OUTPUT: A dictionary with the jsonc file data 

459 In case of error it raises an exception and finish 

460 """ 

461 

462 # -- Read the jsonc file 

463 try: 

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

465 

466 # -- Read the json with comments file 

467 data_jsonc = file.read() 

468 

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

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

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

472 # -- passing a wrong file 

473 except FileNotFoundError as exc: 

474 

475 # -- Display error information 

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

477 

478 # -- Abort! 

479 sys.exit(1) 

480 

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

482 data_json = jsonc.to_json(data_jsonc) 

483 

484 # -- Parse the json format! 

485 try: 

486 resource = json.loads(data_json) 

487 

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

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

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

491 except json.decoder.JSONDecodeError as exc: 

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

493 sys.exit(1) 

494 

495 # -- Return the object for the resource 

496 return resource 

497 

498 @staticmethod 

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

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

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

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

503 at the beginning of the template. 

504 

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

506 

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

508 needs. If needed, extend or modify it. 

509 """ 

510 

511 # Case 1: No place holder. 

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

513 return template 

514 

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

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

517 return str(package_path) 

518 

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

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

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

522 

523 # Case 4: Unsupported. 

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

525 

526 @staticmethod 

527 def _resolve_package_envs( 

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

529 ) -> None: 

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

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

532 the package's absolute path.""" 

533 

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

535 

536 # -- Get the package root dir. 

537 package_path = packages_dir / package_name 

538 

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

540 # -- for clarity reasons. 

541 assert "env" in package_config 

542 package_env = package_config["env"] 

543 

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

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

546 for i, path_template in enumerate(path_section): 

547 path_section[i] = ApioContext._expand_env_template( 

548 path_template, package_path 

549 ) 

550 

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

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

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

554 vars_section[var_name] = ApioContext._expand_env_template( 

555 val_template, package_path 

556 ) 

557 

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

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

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

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

562 """ 

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

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

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

566 sys.exit(1) 

567 

568 return package_info 

569 

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

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

572 

573 return self.apio_packages_dir / package_name 

574 

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

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

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

578 tmp_dir = self.apio_home_dir / "tmp" 

579 if create: 

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

581 return tmp_dir 

582 

583 @staticmethod 

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

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

586 optional override.""" 

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

588 platform_id_override = env_options.get(env_options.APIO_PLATFORM) 

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

590 platform_id = platform_id_override 

591 else: 

592 platform_id = ApioContext._get_system_platform_id() 

593 

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

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

596 

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

598 # -- is invalid. 

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

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

601 cout( 

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

603 style=INFO, 

604 ) 

605 sys.exit(1) 

606 

607 # -- All done ok. 

608 return platform_id 

609 

610 @staticmethod 

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

612 """ 

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

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

615 values. 

616 """ 

617 

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

619 

620 # -- Handle windows. 

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

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

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

624 return "powershell" 

625 if "cmd.exe" in comspec: 

626 return "cmd" 

627 return "unknown" 

628 

629 # -- Handle macOS, Linux, etc. 

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

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

632 return "bash" 

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

634 return "zsh" 

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

636 return "fish" 

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

638 return "dash" 

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

640 return "ksh" 

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

642 return "cshell" 

643 return "unknown" 

644 

645 @property 

646 def packages_context(self) -> PackagesContext: 

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

648 ApioContext.""" 

649 return PackagesContext( 

650 profile=self.profile, 

651 required_packages=self.required_packages, 

652 platform_id=self.platform_id, 

653 packages_dir=self.apio_packages_dir, 

654 ) 

655 

656 @staticmethod 

657 def _select_required_packages_for_platform( 

658 all_packages: Dict[str, Dict], 

659 platform_id: str, 

660 platforms: Dict[str, Dict], 

661 ) -> Dict: 

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

663 returns subset dictionary with packages that are available for 

664 'platform_id'. 

665 """ 

666 

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

668 assert platform_id in platforms, platform 

669 

670 # -- Final dict with the output packages 

671 filtered_packages = {} 

672 

673 # -- Check all the packages 

674 for package_name in all_packages.keys(): 

675 

676 # -- Get the package info. 

677 package_info = all_packages[package_name] 

678 

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

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

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

682 required_for_platforms = package_info.get( 

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

684 ) 

685 

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

687 # -- a programming error. 

688 for p in required_for_platforms: 

689 assert p in platforms.keys(), platform 

690 

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

692 if platform_id in required_for_platforms: 

693 filtered_packages[package_name] = all_packages[package_name] 

694 

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

696 return filtered_packages 

697 

698 @staticmethod 

699 def _get_system_platform_id() -> str: 

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

701 ex. linux-x86-64 

702 ex. windows-amd64""" 

703 

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

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

706 platform_str = f"{type_}" 

707 

708 # -- Get the architecture 

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

710 

711 # -- Special case for windows 

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

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

714 arch = "amd64" 

715 

716 # -- Add the architecture, if it exists 

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

718 platform_str += f"_{arch}" 

719 

720 # -- Return the full platform 

721 return platform_str 

722 

723 @property 

724 def is_linux(self) -> bool: 

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

726 return "linux" in self.platform_id 

727 

728 @property 

729 def is_darwin(self) -> bool: 

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

731 return "darwin" in self.platform_id 

732 

733 @property 

734 def is_windows(self) -> bool: 

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

736 return "windows" in self.platform_id 

737 

738 def _get_env_mutations_for_packages(self) -> EnvMutations: 

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

740 in the order they are defined.""" 

741 

742 result = EnvMutations([], []) 

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

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

745 # -- for clarity reasons. 

746 assert "env" in package_config 

747 package_env = package_config["env"] 

748 

749 # -- Collect the path values. 

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

751 result.paths.extend(path_list) 

752 

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

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

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

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

757 

758 return result 

759 

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

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

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

763 

764 # -- Print PATH mutations. 

765 windows = self.is_windows 

766 for p in reversed(mutations.paths): 

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

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

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

770 else: 

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

772 

773 # -- Print vars mutations. 

774 for name, val in mutations.vars_list: 

775 styled_name = cstyle(name, style=EMPH3) 

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

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

778 else: 

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

780 

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

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

783 

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

785 old_val = os.environ["PATH"] 

786 items = mutations.paths + [old_val] 

787 new_val = os.pathsep.join(items) 

788 os.environ["PATH"] = new_val 

789 

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

791 for name, value in mutations.vars_list: 

792 os.environ[name] = value 

793 

794 def set_env_for_packages( 

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

796 ) -> None: 

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

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

799 

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

801 calls skips the operation silently. 

802 

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

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

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

806 see are executed in a modified env settings. 

807 """ 

808 

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

810 # -- cannot be combined. 

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

812 

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

814 mutations = self._get_env_mutations_for_packages() 

815 

816 if verbose: 

817 self._dump_env_mutations(mutations) 

818 

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

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

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

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

823 # -- scons processes. 

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

825 self._apply_env_mutations(mutations) 

826 self.env_was_already_set = True 

827 if not verbose and not quiet: 

828 cout("Setting shell vars.")