Coverage for apio / apio_context.py: 85%
321 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +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
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)
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 # -- List of env vars to unset.
103 unset_vars: List[str]
105 # -- PATH items to add.
106 paths: List[str]
108 # -- Dict with env vars name/value to set.
109 set_vars: Dict[str, str]
112class ProjectPolicy(Enum):
113 """Represents the possible context policies regarding loading apio.ini.
114 and project related information."""
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
124class PackagesPolicy(Enum):
125 """Represents the possible context policies regarding loading apio.ini.
126 and project related information."""
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
137class ApioContext:
138 """Apio context. Class for accessing apio resources and configurations."""
140 # pylint: disable=too-many-instance-attributes
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 )
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.
173 'project_policy', 'config_policy', and 'packages_policy' are modifiers
174 that controls the initialization of the context.
176 'project_dir_arg' is an optional user specification of the project dir.
177 Must be None if project_policy is NO_PROJECT.
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).
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 """
187 # pylint: disable=too-many-arguments
188 # pylint: disable=too-many-statements
189 # pylint: disable=too-many-locals
191 # -- Sanity check the policies.
192 assert isinstance(project_policy, ProjectPolicy)
193 assert isinstance(remote_config_policy, RemoteConfigPolicy)
194 assert isinstance(packages_policy, PackagesPolicy)
196 if packages_policy == PackagesPolicy.IGNORE_PACKAGES:
197 assert project_policy == ProjectPolicy.NO_PROJECT
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 )
208 # -- Store the project_policy
209 assert isinstance(
210 project_policy, ProjectPolicy
211 ), "Not an ApioContextScope"
212 self.project_policy = project_policy
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
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
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"
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 )
251 # -- Get the jsonc source dirs.
252 resources_dir = util.get_path_in_apio_package(RESOURCES_DIR)
254 # -- Read and validate the config information
255 self.config = self._load_resource(CONFIG_JSONC, resources_dir)
256 validate_config(self.config)
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 )
278 # -- Read the platforms information.
279 self.platforms = self._load_resource(PLATFORMS_JSONC, resources_dir)
280 validate_platforms(self.platforms)
282 # -- Determine the platform_id for this APIO session.
283 self.platform_id = self._determine_platform_id(self.platforms)
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)
289 # -- Read the apio packages information
290 self.all_packages = self._load_resource(PACKAGES_JSONC, resources_dir)
291 validate_packages(self.all_packages)
293 # -- Expand in place the env templates in all_packages.
294 ApioContext._resolve_package_envs(
295 self.all_packages, self.apio_packages_dir
296 )
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 )
303 # -- Case 1: IGNORE_PACKAGES
304 if packages_policy == PackagesPolicy.IGNORE_PACKAGES:
305 self._definitions = None
307 # -- Case 2: ENSURE_PACKAGES
308 else:
309 assert packages_policy == PackagesPolicy.ENSURE_PACKAGES
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 )
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)
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
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"
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
364 # -- Env name string in color
365 styled_env_name = cstyle(self.project.env_name, style=EMPH1)
367 # -- Board id string in color
368 styled_board_id = cstyle(
369 self.project.get_str_option("board"), style=EMPH1
370 )
372 # -- Report.
373 cout(f"Using env {styled_env_name} ({styled_board_id})")
375 @property
376 def has_project(self):
377 """Returns True if the project is loaded."""
378 return self._project is not None
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
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
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
404 @property
405 def definitions(self) -> ApioDefinitions:
406 """Return apio definitions."""
407 assert self._definitions, "Apio context as no definitions"
408 return self._definitions
410 @property
411 def boards(self) -> dict:
412 """Returns the apio board definitions"""
413 return self.definitions.boards
415 @property
416 def fpgas(self) -> dict:
417 """Returns the apio fpgas definitions"""
418 return self.definitions.fpgas
420 @property
421 def programmers(self) -> dict:
422 """Returns the apio programmers definitions"""
423 return self.definitions.programmers
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)
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 """
440 # -- Load the standard definition as a json dict.
441 filepath = standard_dir / name
442 result = self._load_resource_file(filepath)
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)
456 # -- All done.
457 return result
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 """
466 # -- Read the jsonc file
467 try:
468 with filepath.open(encoding="utf8") as file:
470 # -- Read the json with comments file
471 data_jsonc = file.read()
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:
479 # -- Display error information
480 cerror("[Internal] .jsonc file not found", f"{exc}")
482 # -- Abort!
483 sys.exit(1)
485 # -- Convert the jsonc to json by removing '//' comments.
486 data_json = jsonc.to_json(data_jsonc)
488 # -- Parse the json format!
489 try:
490 resource = json.loads(data_json)
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)
499 # -- Return the object for the resource
500 return resource
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.
509 E.g. '%p/bin' -> '/users/user/.apio/packages/drivers/bin'
511 NOTE: This format is very basic but is sufficient for the current
512 needs. If needed, extend or modify it.
513 """
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
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)
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:])
527 # Case 4: Unsupported.
528 raise RuntimeError(f"Invalid env template: [{template}]")
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."""
538 for package_name, package_config in packages_.items():
540 # -- Get the package root dir.
541 package_path = packages_dir / package_name
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"]
548 # -- NOTE: There is no need to expand values in the "unset-env"
549 # -- section since it contains env names only.
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 )
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 )
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)
575 return package_info
577 def get_package_dir(self, package_name: str) -> Path:
578 """Returns the root path of a package with given name."""
580 return self.apio_packages_dir / package_name
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
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()
601 # Stick to the naming conventions we use for boards, fpgas, etc.
602 platform_id = platform_id.replace("_", "-")
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)
614 # -- All done ok.
615 return platform_id
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 """
625 # pylint: disable=too-many-return-statements
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"
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"
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 )
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 """
674 # -- If fails, this is a programming error.
675 assert platform_id in platforms, platform
677 # -- Final dict with the output packages
678 filtered_packages = {}
680 # -- Check all the packages
681 for package_name in all_packages.keys():
683 # -- Get the package info.
684 package_info = all_packages[package_name]
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 )
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
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]
702 # -- Return the subset dict with the packages for 'platform_id'.
703 return filtered_packages
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"""
711 # -- Get the platform: linux, windows, darwin
712 type_ = platform.system().lower()
713 platform_str = f"{type_}"
715 # -- Get the architecture
716 arch = platform.machine().lower()
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"
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}"
727 # -- Return the full platform
728 return platform_str
730 @property
731 def is_linux(self) -> bool:
732 """Returns True iff platform_id indicates linux."""
733 return "linux" in self.platform_id
735 @property
736 def is_darwin(self) -> bool:
737 """Returns True iff platform_id indicates Mac OSX."""
738 return "darwin" in self.platform_id
740 @property
741 def is_windows(self) -> bool:
742 """Returns True iff platform_id indicates windows."""
743 return "windows" in self.platform_id
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."""
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"]
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)
765 # -- Collect the path values.
766 package_paths = package_env.get("path", [])
767 paths.extend(package_paths)
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
776 return EnvMutations(unset_vars, paths, set_vars)
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)
782 # -- Print PATH mutations.
783 windows = self.is_windows
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}")
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"')
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}"')
809 def _apply_env_mutations(self, mutations: EnvMutations) -> None:
810 """Apply a given set of env mutations, while preserving their order."""
812 # -- Apply the unset var mutations
813 for name in mutations.unset_vars:
814 os.environ.pop(name, None)
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
823 # -- Apply the set var mutations
824 for name, value in mutations.set_vars.items():
825 os.environ[name] = value
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.
833 The function sets the environment only on first call and in latter
834 calls skips the operation silently.
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 """
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."
846 # -- Collect the env mutations for all packages.
847 mutations = self._get_env_mutations_for_packages()
849 if verbose:
850 self._dump_env_mutations(mutations)
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.")