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
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01:53 +0000
1"""The apio context."""
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
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)
35# ---------- RESOURCES
36RESOURCES_DIR = "resources"
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"
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"
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"
59# -----------------------------------------
60# ---- File: resources/fpgas.jsonc
61# -----------------------------------------
62# -- Information about all the supported fpgas
63# -- arch, type, size, packaging
64FPGAS_JSONC = "fpgas.jsonc"
66# -----------------------------------------
67# ---- File: resources/programmers.jsonc
68# -----------------------------------------
69# -- Information about all the supported programmers
70# -- name, command to execute, arguments...
71PROGRAMMERS_JSONC = "programmers.jsonc"
73# -----------------------------------------
74# ---- File: resources/config.jsonc
75# -----------------------------------------
76# -- General config information.
77CONFIG_JSONC = "config.jsonc"
80@dataclass(frozen=True)
81class ApioDefinitions:
82 """Contains the apio definitions in the form of json dictionaries."""
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
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
98@dataclass(frozen=True)
99class EnvMutations:
100 """Contains mutations to the system env."""
102 # -- PATH items to add.
103 paths: List[str]
104 # -- Vars name/value pairs.
105 vars_list: List[Tuple[str, str]]
108class ProjectPolicy(Enum):
109 """Represents the possible context policies regarding loading apio.ini.
110 and project related information."""
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
120class PackagesPolicy(Enum):
121 """Represents the possible context policies regarding loading apio.ini.
122 and project related information."""
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
133class ApioContext:
134 """Apio context. Class for accessing apio resources and configurations."""
136 # pylint: disable=too-many-instance-attributes
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 )
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.
169 'project_policy', 'config_policy', and 'packages_policy' are modifiers
170 that controls the initialization of the context.
172 'project_dir_arg' is an optional user specification of the project dir.
173 Must be None if project_policy is NO_PROJECT.
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).
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 """
183 # pylint: disable=too-many-arguments
184 # pylint: disable=too-many-statements
185 # pylint: disable=too-many-locals
187 # -- Sanity check the policies.
188 assert isinstance(project_policy, ProjectPolicy)
189 assert isinstance(remote_config_policy, RemoteConfigPolicy)
190 assert isinstance(packages_policy, PackagesPolicy)
192 if packages_policy == PackagesPolicy.IGNORE_PACKAGES:
193 assert project_policy == ProjectPolicy.NO_PROJECT
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 )
204 # -- Store the project_policy
205 assert isinstance(
206 project_policy, ProjectPolicy
207 ), "Not an ApioContextScope"
208 self.project_policy = project_policy
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
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
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"
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 )
247 # -- Get the jsonc source dirs.
248 resources_dir = util.get_path_in_apio_package(RESOURCES_DIR)
250 # -- Read and validate the config information
251 self.config = self._load_resource(CONFIG_JSONC, resources_dir)
252 validate_config(self.config)
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 )
274 # -- Read the platforms information.
275 self.platforms = self._load_resource(PLATFORMS_JSONC, resources_dir)
276 validate_platforms(self.platforms)
278 # -- Determine the platform_id for this APIO session.
279 self.platform_id = self._determine_platform_id(self.platforms)
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)
285 # -- Read the apio packages information
286 self.all_packages = self._load_resource(PACKAGES_JSONC, resources_dir)
287 validate_packages(self.all_packages)
289 # -- Expand in place the env templates in all_packages.
290 ApioContext._resolve_package_envs(
291 self.all_packages, self.apio_packages_dir
292 )
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 )
299 # -- Case 1: IGNORE_PACKAGES
300 if packages_policy == PackagesPolicy.IGNORE_PACKAGES:
301 self._definitions = None
303 # -- Case 2: ENSURE_PACKAGES
304 else:
305 assert packages_policy == PackagesPolicy.ENSURE_PACKAGES
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 )
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)
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
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"
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
360 # -- Env name string in color
361 styled_env_name = cstyle(self.project.env_name, style=EMPH1)
363 # -- Board id string in color
364 styled_board_id = cstyle(
365 self.project.env_options["board"], style=EMPH1
366 )
368 # -- Report.
369 cout(f"Using env {styled_env_name} ({styled_board_id})")
371 @property
372 def has_project(self):
373 """Returns True if the project is loaded."""
374 return self._project is not None
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
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
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
400 @property
401 def definitions(self) -> ApioDefinitions:
402 """Return apio definitions."""
403 assert self._definitions, "Apio context as no definitions"
404 return self._definitions
406 @property
407 def boards(self) -> dict:
408 """Returns the apio board definitions"""
409 return self.definitions.boards
411 @property
412 def fpgas(self) -> dict:
413 """Returns the apio fpgas definitions"""
414 return self.definitions.fpgas
416 @property
417 def programmers(self) -> dict:
418 """Returns the apio programmers definitions"""
419 return self.definitions.programmers
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)
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 """
436 # -- Load the standard definition as a json dict.
437 filepath = standard_dir / name
438 result = self._load_resource_file(filepath)
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)
452 # -- All done.
453 return result
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 """
462 # -- Read the jsonc file
463 try:
464 with filepath.open(encoding="utf8") as file:
466 # -- Read the json with comments file
467 data_jsonc = file.read()
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:
475 # -- Display error information
476 cerror("[Internal] .jsonc file not found", f"{exc}")
478 # -- Abort!
479 sys.exit(1)
481 # -- Convert the jsonc to json by removing '//' comments.
482 data_json = jsonc.to_json(data_jsonc)
484 # -- Parse the json format!
485 try:
486 resource = json.loads(data_json)
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)
495 # -- Return the object for the resource
496 return resource
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.
505 E.g. '%p/bin' -> '/users/user/.apio/packages/drivers/bin'
507 NOTE: This format is very basic but is sufficient for the current
508 needs. If needed, extend or modify it.
509 """
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
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)
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:])
523 # Case 4: Unsupported.
524 raise RuntimeError(f"Invalid env template: [{template}]")
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."""
534 for package_name, package_config in packages_.items():
536 # -- Get the package root dir.
537 package_path = packages_dir / package_name
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"]
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 )
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 )
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)
568 return package_info
570 def get_package_dir(self, package_name: str) -> Path:
571 """Returns the root path of a package with given name."""
573 return self.apio_packages_dir / package_name
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
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()
594 # Stick to the naming conventions we use for boards, fpgas, etc.
595 platform_id = platform_id.replace("_", "-")
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)
607 # -- All done ok.
608 return platform_id
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 """
618 # pylint: disable=too-many-return-statements
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"
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"
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 )
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 """
667 # -- If fails, this is a programming error.
668 assert platform_id in platforms, platform
670 # -- Final dict with the output packages
671 filtered_packages = {}
673 # -- Check all the packages
674 for package_name in all_packages.keys():
676 # -- Get the package info.
677 package_info = all_packages[package_name]
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 )
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
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]
695 # -- Return the subset dict with the packages for 'platform_id'.
696 return filtered_packages
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"""
704 # -- Get the platform: linux, windows, darwin
705 type_ = platform.system().lower()
706 platform_str = f"{type_}"
708 # -- Get the architecture
709 arch = platform.machine().lower()
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"
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}"
720 # -- Return the full platform
721 return platform_str
723 @property
724 def is_linux(self) -> bool:
725 """Returns True iff platform_id indicates linux."""
726 return "linux" in self.platform_id
728 @property
729 def is_darwin(self) -> bool:
730 """Returns True iff platform_id indicates Mac OSX."""
731 return "darwin" in self.platform_id
733 @property
734 def is_windows(self) -> bool:
735 """Returns True iff platform_id indicates windows."""
736 return "windows" in self.platform_id
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."""
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"]
749 # -- Collect the path values.
750 path_list = package_env.get("path", [])
751 result.paths.extend(path_list)
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))
758 return result
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)
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"')
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}"')
781 def _apply_env_mutations(self, mutations: EnvMutations) -> None:
782 """Apply a given set of env mutations, while preserving their order."""
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
790 # -- Apply the vars mutations, while preserving order.
791 for name, value in mutations.vars_list:
792 os.environ[name] = value
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.
800 The function sets the environment only on first call and in latter
801 calls skips the operation silently.
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 """
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."
813 # -- Collect the env mutations for all packages.
814 mutations = self._get_env_mutations_for_packages()
816 if verbose:
817 self._dump_env_mutations(mutations)
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.")