Coverage for apio/apio_context.py: 86%
299 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +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, apio_platforms
22from apio.utils.apio_platforms import ApioPlatform
23from apio.managers.project import Project, load_project_from_file
24from apio.managers import packages
25from apio.managers.packages import PackagesContext
26from apio.utils.resource_util import (
27 ProjectResources,
28 collect_project_resources,
29 validate_project_resources,
30 validate_config,
31 validate_packages,
32)
34# ---------- RESOURCES
35RESOURCES_DIR = "resources"
38# ---------------------------------------
39# ---- File: resources/packages.jsonc
40# --------------------------------------
41# -- This file contains all the information regarding the available apio
42# -- packages: Repository, version, name...
43PACKAGES_JSONC = "packages.jsonc"
45# -----------------------------------------
46# ---- File: resources/boards.jsonc
47# -----------------------------------------
48# -- Information about all the supported boards
49# -- names, fpga family, programmer, ftdi description, vendor id, product id
50BOARDS_JSONC = "boards.jsonc"
52# -----------------------------------------
53# ---- File: resources/fpgas.jsonc
54# -----------------------------------------
55# -- Information about all the supported fpgas
56# -- arch, type, size, packaging
57FPGAS_JSONC = "fpgas.jsonc"
59# -----------------------------------------
60# ---- File: resources/programmers.jsonc
61# -----------------------------------------
62# -- Information about all the supported programmers
63# -- name, command to execute, arguments...
64PROGRAMMERS_JSONC = "programmers.jsonc"
66# -----------------------------------------
67# ---- File: resources/config.jsonc
68# -----------------------------------------
69# -- General config information.
70CONFIG_JSONC = "config.jsonc"
73@dataclass(frozen=True)
74class ApioDefinitions:
75 """Contains the apio definitions in the form of json dictionaries."""
77 # -- A json dir with the content of boards.jsonc
78 boards: dict
79 # -- A json dir with the content of fpgas.jsonc
80 fpgas: dict
81 # -- A json dir with the content of programmers.jsonc.
82 programmers: dict
84 def __post_init__(self):
85 """Assert that all fields initialized to actual values."""
86 assert self.boards
87 assert self.fpgas
88 assert self.programmers
91@dataclass(frozen=True)
92class EnvMutations:
93 """Contains mutations to the system env."""
95 # -- List of env vars to unset.
96 unset_vars: List[str]
98 # -- PATH items to add.
99 paths: List[str]
101 # -- Dict with env vars name/value to set.
102 set_vars: Dict[str, str]
105class ProjectPolicy(Enum):
106 """Represents the possible context policies regarding loading apio.ini.
107 and project related information."""
109 # -- Project information is not loaded.
110 NO_PROJECT = 1
111 # -- Project information is loaded if apio.ini is found.
112 PROJECT_OPTIONAL = 2
113 # -- Apio.ini is required and project information must be loaded.
114 PROJECT_REQUIRED = 3
117class PackagesPolicy(Enum):
118 """Represents the possible context policies regarding loading apio.ini.
119 and project related information."""
121 # -- Do not change the package state, they may exist or not, updated or
122 # -- not. This policy requires project policy NO_PROJECT and with it,
123 # -- the definitions are not loaded.
124 IGNORE_PACKAGES = 1
125 # -- Normal policy, verify that the packages are installed correctly and
126 # -- update them if needed.
127 ENSURE_PACKAGES = 2
130class ApioContext:
131 """Apio context. Class for accessing apio resources and configurations."""
133 # pylint: disable=too-many-instance-attributes
135 # -- List of allowed instance vars.
136 __slots__ = (
137 "project_policy",
138 "apio_home_dir",
139 "apio_packages_dir",
140 "config",
141 "profile",
142 "platform",
143 "platform_id",
144 "scons_shell_id",
145 "all_packages",
146 "required_packages",
147 "env_was_already_set",
148 "_project_dir",
149 "_project",
150 "_project_resources",
151 "_definitions",
152 )
154 def __init__(
155 self,
156 *,
157 project_policy: ProjectPolicy,
158 remote_config_policy: RemoteConfigPolicy,
159 packages_policy: PackagesPolicy,
160 project_dir_arg: Optional[Path] = None,
161 env_arg: Optional[str] = None,
162 report_env=True,
163 ):
164 """Initializes the ApioContext object.
166 'project_policy', 'config_policy', and 'packages_policy' are modifiers
167 that controls the initialization of the context.
169 'project_dir_arg' is an optional user specification of the project dir.
170 Must be None if project_policy is NO_PROJECT.
172 'env_arg' is an optional command line option value that select the
173 apio.ini env if the project is loaded. it makes sense only when
174 project_policy is PROJECT_REQUIRED (enforced by an assertion).
176 If an apio.ini project is loaded, the method prints to the user the
177 selected env and board, unless if report_env = False.
178 """
180 # pylint: disable=too-many-arguments
181 # pylint: disable=too-many-statements
182 # pylint: disable=too-many-locals
184 # -- Sanity check the policies.
185 assert isinstance(project_policy, ProjectPolicy)
186 assert isinstance(remote_config_policy, RemoteConfigPolicy)
187 assert isinstance(packages_policy, PackagesPolicy)
189 if packages_policy == PackagesPolicy.IGNORE_PACKAGES:
190 assert project_policy == ProjectPolicy.NO_PROJECT
192 # -- Inform as soon as possible about the list of apio env options
193 # -- that modify its default behavior.
194 defined_env_options = env_options.get_defined()
195 if defined_env_options: 195 ↛ 202line 195 didn't jump to line 202 because the condition on line 195 was always true
196 cout(
197 f"Active env options [{', '.join(defined_env_options)}].",
198 style=INFO,
199 )
201 # -- Store the project_policy
202 assert isinstance(
203 project_policy, ProjectPolicy
204 ), "Not an ApioContextScope"
205 self.project_policy = project_policy
207 # -- Sanity check, env_arg makes sense only when project_policy is
208 # -- PROJECT_REQUIRED.
209 if env_arg is not None:
210 assert project_policy == ProjectPolicy.PROJECT_REQUIRED
212 # -- A flag to indicate if the system env was already set in this
213 # -- apio session. Used to avoid multiple repeated settings that
214 # -- make the path longer and longer.
215 self.env_was_already_set = False
217 # -- Determine if we need to load the project, and if so, set
218 # -- self._project_dir to the project dir, otherwise, leave it None.
219 self._project_dir: Path | None = None
220 if project_policy == ProjectPolicy.PROJECT_REQUIRED:
221 self._project_dir = util.user_directory_or_cwd(
222 project_dir_arg, description="Project", must_exist=True
223 )
224 elif project_policy == ProjectPolicy.PROJECT_OPTIONAL:
225 project_dir = util.user_directory_or_cwd(
226 project_dir_arg, description="Project", must_exist=False
227 )
228 if (project_dir / "apio.ini").exists():
229 self._project_dir = project_dir
230 else:
231 assert (
232 project_policy == ProjectPolicy.NO_PROJECT
233 ), f"Unexpected project policy: {project_policy}"
234 assert (
235 project_dir_arg is None
236 ), "project_dir_arg specified for project policy None"
238 # -- Determine apio home and packages dirs
239 self.apio_home_dir: Path = util.resolve_home_dir()
240 self.apio_packages_dir: Path = util.resolve_packages_dir(
241 self.apio_home_dir
242 )
244 # -- Get the jsonc source dirs.
245 resources_dir = util.get_path_in_apio_package(RESOURCES_DIR)
247 # -- Read and validate the config information
248 self.config = self._load_resource(CONFIG_JSONC, resources_dir)
249 validate_config(self.config)
251 # -- Profile information, from ~/.apio/profile.json. We provide it with
252 # -- the remote config url template from distribution.jsonc such that
253 # -- can it fetch the remote config on demand.
254 remote_config_url = env_options.get(
255 env_options.APIO_REMOTE_CONFIG_URL,
256 default=self.config["remote-config-url"],
257 )
258 remote_config_ttl_days = self.config["remote-config-ttl-days"]
259 remote_config_retry_minutes = self.config[
260 "remote-config-retry-minutes"
261 ]
262 self.profile = Profile(
263 self.apio_home_dir,
264 self.apio_packages_dir,
265 str(remote_config_url),
266 remote_config_ttl_days,
267 remote_config_retry_minutes,
268 remote_config_policy,
269 )
271 # -- Get the underlying platform information.
272 self.platform: ApioPlatform = apio_platforms.get_apio_platform()
273 self.platform_id: str = self.platform.id
275 # -- Determine the shell id that scons will use.
276 # -- See _determine_scons_shell_id() for possible values.
277 self.scons_shell_id = self._determine_scons_shell_id(self.platform)
279 # -- Read the apio packages information
280 self.all_packages = self._load_resource(PACKAGES_JSONC, resources_dir)
281 validate_packages(self.all_packages)
283 # -- Expand in place the env templates in all_packages.
284 ApioContext._resolve_package_envs(
285 self.all_packages, self.apio_packages_dir
286 )
288 # The subset of packages that are applicable to this platform.
289 self.required_packages = self._select_required_packages_for_platform(
290 self.all_packages,
291 self.platform_id,
292 )
294 # -- Case 1: IGNORE_PACKAGES
295 if packages_policy == PackagesPolicy.IGNORE_PACKAGES:
296 self._definitions = None
298 # -- Case 2: ENSURE_PACKAGES
299 else:
300 assert packages_policy == PackagesPolicy.ENSURE_PACKAGES
302 # -- Install missing packages. At this point, the fields that are
303 # -- required by self.packages_context are already initialized.
304 packages.install_missing_packages_on_the_fly(
305 self.packages_context, verbose=False
306 )
308 # -- Load the definitions from the definitions file with possible
309 # -- override by the optional project file.
310 definitions_dir = self.apio_packages_dir / "definitions"
311 boards = self._load_resource(
312 BOARDS_JSONC, definitions_dir, self._project_dir
313 )
314 fpgas = self._load_resource(
315 FPGAS_JSONC, definitions_dir, self._project_dir
316 )
317 programmers = self._load_resource(
318 PROGRAMMERS_JSONC, definitions_dir, self._project_dir
319 )
320 self._definitions = ApioDefinitions(boards, fpgas, programmers)
322 # -- If we determined that we need to load the project, load the
323 # -- apio.ini data.
324 self._project: Optional[Project] = None
325 self._project_resources: ProjectResources | None = None
327 if self._project_dir:
328 # -- Load the project object
329 self._project = load_project_from_file(
330 self._project_dir, env_arg, self.boards
331 )
332 assert self.has_project, "init(): project not loaded"
333 # -- Inform the user about the active env, if needed..
334 if report_env:
335 self.report_env()
336 # -- Collect and validate the project resources.
337 # -- The project is already validated to have the required "board.
338 self._project_resources = collect_project_resources(
339 self._project.get_str_option("board"),
340 self.boards,
341 self.fpgas,
342 self.programmers,
343 )
344 # -- Validate the project resources.
345 validate_project_resources(self._project_resources)
346 else:
347 assert not self.has_project, "init(): project loaded"
349 def report_env(self):
350 """Report to the user the env and board used. Asserts that the
351 project is loaded."""
352 # -- Do not call if project is not loaded.
353 assert self.has_project
355 # -- Env name string in color
356 styled_env_name = cstyle(self.project.env_name, style=EMPH1)
358 # -- Board id string in color
359 styled_board_id = cstyle(
360 self.project.get_str_option("board"), style=EMPH1
361 )
363 # -- Report.
364 cout(f"Using env {styled_env_name} ({styled_board_id})")
366 @property
367 def has_project(self):
368 """Returns True if the project is loaded."""
369 return self._project is not None
371 @property
372 def project_dir(self):
373 """Returns the project dir. Should be called only if has_project_loaded
374 is true."""
375 assert self.has_project, "project_dir(): project is not loaded"
376 assert self._project_dir, "project_dir(): missing value."
377 return self._project_dir
379 @property
380 def project(self) -> Project:
381 """Return the project. Should be called only if has_project() is
382 True."""
383 # -- Failure here is a programming error, not a user error.
384 assert self.has_project, "project(): project is not loaded"
385 return self._project # pyright: ignore[reportReturnType]
387 @property
388 def project_resources(self) -> ProjectResources:
389 """Return the project resources. Should be called only if
390 has_project() is True."""
391 # -- Failure here is a programming error, not a user error.
392 assert self.has_project, "project(): project is not loaded"
393 return self._project_resources # pyright: ignore[reportReturnType]
395 @property
396 def definitions(self) -> ApioDefinitions:
397 """Return apio definitions."""
398 assert self._definitions, "Apio context as no definitions"
399 return self._definitions
401 @property
402 def boards(self) -> dict:
403 """Returns the apio board definitions"""
404 return self.definitions.boards
406 @property
407 def fpgas(self) -> dict:
408 """Returns the apio fpgas definitions"""
409 return self.definitions.fpgas
411 @property
412 def programmers(self) -> dict:
413 """Returns the apio programmers definitions"""
414 return self.definitions.programmers
416 @property
417 def env_build_path(self) -> Path:
418 """Returns the relative path of the current env build directory from
419 the project dir. Should be called only when has_project is True."""
420 assert self.has_project, "project(): project is not loaded"
421 return env_build_path(self.project.env_name)
423 def _load_resource(
424 self, name: str, standard_dir: Path, custom_dir: Optional[Path] = None
425 ) -> dict:
426 """Load a jsonc file. Try first from custom_dir, if given, and then
427 from standard dir. This method is called for resource files in
428 apio/resources and definitions files in the definitions packages.
429 """
431 # -- Load the standard definition as a json dict.
432 filepath = standard_dir / name
433 result = self._load_resource_file(filepath)
435 # -- If there is a project specific override file, apply it on
436 # -- top of the standard apio definition dict.
437 if custom_dir:
438 filepath = custom_dir / name
439 if filepath.exists():
440 # -- Load the override json dict.
441 cout(f"Loading custom '{name}'.")
442 override = self._load_resource_file(filepath)
443 # -- Apply the override. Entries in override replace same
444 # -- key entries in result or if unique are added.
445 result.update(override)
447 # -- All done.
448 return result
450 @staticmethod
451 def _load_resource_file(filepath: Path) -> dict:
452 """Load the resources from a given jsonc file path
453 * OUTPUT: A dictionary with the jsonc file data
454 In case of error it raises an exception and finish
455 """
457 # -- Read the jsonc file
458 try:
459 with filepath.open(encoding="utf8") as file:
461 # -- Read the json with comments file
462 data_jsonc = file.read()
464 # -- The jsonc file NOT FOUND! This is an apio system error
465 # -- It should never occur unless there is a bug in the
466 # -- apio system files, or a bug when calling this function
467 # -- passing a wrong file
468 except FileNotFoundError as exc:
470 # -- Display error information
471 cerror("[Internal] .jsonc file not found", f"{exc}")
473 # -- Abort!
474 sys.exit(1)
476 # -- Convert the jsonc to json by removing '//' comments.
477 data_json = jsonc.to_json(data_jsonc)
479 # -- Parse the json format!
480 try:
481 resource = json.loads(data_json)
483 # -- Invalid json format! This is an apio system error
484 # -- It should never occur unless a developer has
485 # -- made a mistake when changing the jsonc file
486 except json.decoder.JSONDecodeError as exc:
487 cerror(f"'{filepath}' has bad format", f"{exc}")
488 sys.exit(1)
490 # -- Return the object for the resource
491 return resource
493 @staticmethod
494 def _expand_env_values(template: str, package_path: Path) -> str:
495 """Fills a packages env value template as they appear in
496 packages.jsonc. Currently it recognizes only a single place holder
497 '%p' representing the package absolute path. The '%p" can appear only
498 at the beginning of the template.
500 E.g. '%p/bin' -> '/users/user/.apio/packages/drivers/bin'
502 NOTE: This format is very basic but is sufficient for the current
503 needs. If needed, extend or modify it.
504 """
506 # Case 1: No place holder -> no change.
507 if "%p" not in template: 507 ↛ 508line 507 didn't jump to line 508 because the condition on line 507 was never true
508 return template
510 # Case 2: The template contains only the placeholder.
511 if template == "%p": 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true
512 return str(package_path)
514 # Case 3: The place holder is the prefix of the template's path.
515 if template.startswith("%p/"): 515 ↛ 519line 515 didn't jump to line 519 because the condition on line 515 was always true
516 return str(package_path / template[3:])
518 # Case 4: Unsupported.
519 raise RuntimeError(f"Invalid env template: [{template}]")
521 @staticmethod
522 def _resolve_package_envs(
523 packages_: Dict[str, Dict], packages_dir: Path
524 ) -> None:
525 """Resolve in-place the path and var value templates in the
526 given packages dictionary. For example, %p is replaced with
527 the package's absolute path."""
529 for package_name, package_config in packages_.items():
531 # -- Get the package root dir.
532 package_path = packages_dir / package_name
534 # -- Get the json 'env' section. We require it, even if empty,
535 # -- for clarity reasons.
536 assert "env" in package_config
537 package_env = package_config["env"]
539 # -- NOTE: There is no need to expand values in the "unset-env"
540 # -- section since it contains env names only.
542 # -- Expand the values in the "path" section, if any.
543 path_section = package_env.get("path", [])
544 for i, path_template in enumerate(path_section):
545 path_section[i] = ApioContext._expand_env_values(
546 path_template, package_path
547 )
549 # -- Expand the values in the "set-vars" section, if any.
550 set_vars_section = package_env.get("set-vars", {})
551 for var_name, var_value in set_vars_section.items():
552 set_vars_section[var_name] = ApioContext._expand_env_values(
553 var_value, package_path
554 )
556 def get_required_package_info(self, package_name: str) -> Dict:
557 """Returns the information of the package with given name.
558 The information is a JSON dict originated at packages.json().
559 Exits with an error message if the package is not defined.
560 """
561 package_info = self.required_packages.get(package_name, None)
562 if package_info is None: 562 ↛ 563line 562 didn't jump to line 563 because the condition on line 562 was never true
563 cerror(f"Unknown package '{package_name}'")
564 sys.exit(1)
566 return package_info
568 def get_package_dir(self, package_name: str) -> Path:
569 """Returns the root path of a package with given name."""
571 return self.apio_packages_dir / package_name
573 def get_tmp_dir(self, create: bool = True) -> Path:
574 """Return the tmp dir under the apio home dir. If 'create' is true
575 create the dir and its parents if they do not exist."""
576 tmp_dir = self.apio_home_dir / "tmp"
577 if create:
578 tmp_dir.mkdir(parents=True, exist_ok=True)
579 return tmp_dir
581 @staticmethod
582 def _determine_scons_shell_id(apio_platform: ApioPlatform) -> str:
583 """
584 Returns a simplified string name of the shell that SCons will use
585 for executing shell-dependent commands. See code below for possible
586 values.
587 """
589 # pylint: disable=too-many-return-statements
591 # -- Handle windows.
592 if apio_platform.is_windows: 592 ↛ 593line 592 didn't jump to line 593 because the condition on line 592 was never true
593 comspec = os.environ.get("COMSPEC", "").lower()
594 if "powershell.exe" in comspec or "pwsh.exe" in comspec:
595 return "powershell"
596 if "cmd.exe" in comspec:
597 return "cmd"
598 return "unknown"
600 # -- Handle the rest (macOS, Linux, etc.)
601 shell_path = os.environ.get("SHELL", "").lower()
602 if "bash" in shell_path: 602 ↛ 603line 602 didn't jump to line 603 because the condition on line 602 was never true
603 return "bash"
604 if "zsh" in shell_path: 604 ↛ 605line 604 didn't jump to line 605 because the condition on line 604 was never true
605 return "zsh"
606 if "fish" in shell_path: 606 ↛ 607line 606 didn't jump to line 607 because the condition on line 606 was never true
607 return "fish"
608 if "dash" in shell_path: 608 ↛ 609line 608 didn't jump to line 609 because the condition on line 608 was never true
609 return "dash"
610 if "ksh" in shell_path: 610 ↛ 611line 610 didn't jump to line 611 because the condition on line 610 was never true
611 return "ksh"
612 if "csh" in shell_path or "tcsh" in shell_path: 612 ↛ 613line 612 didn't jump to line 613 because the condition on line 612 was never true
613 return "cshell"
614 return "unknown"
616 @property
617 def packages_context(self) -> PackagesContext:
618 """Return a PackagesContext with info extracted from this
619 ApioContext."""
620 return PackagesContext(
621 profile=self.profile,
622 required_packages=self.required_packages,
623 platform=self.platform,
624 packages_dir=self.apio_packages_dir,
625 )
627 @staticmethod
628 def _select_required_packages_for_platform(
629 all_packages: Dict[str, Dict],
630 platform_id: str,
631 ) -> Dict:
632 """Given a dictionary with the packages.jsonc packages infos,
633 returns subset dictionary with packages that are available for
634 'platform_id'.
635 """
637 # -- Dict of all supported platforms.
638 all_apio_platforms = apio_platforms.get_all_apio_platforms()
640 # -- If fails, this is a programming error.
641 assert platform_id in all_apio_platforms, platform
643 # -- Final dict with the output packages
644 filtered_packages = {}
646 # -- Check all the packages
647 for package_name in all_packages.keys():
649 # -- Get the package info.
650 package_info = all_packages[package_name]
652 # -- Get the list of platforms ids on which this package is
653 # -- available. The package is available on all platforms unless
654 # -- restricted by the ""restricted-to-platforms" field.
655 required_for_platforms = package_info.get(
656 "restricted-to-platforms", all_apio_platforms.keys()
657 )
659 # -- Sanity check that all platform ids are valid. If fails it's
660 # -- a programming error.
661 for p in required_for_platforms:
662 assert p in all_apio_platforms.keys(), platform
664 # -- If available for 'platform_id', add it.
665 if platform_id in required_for_platforms:
666 filtered_packages[package_name] = all_packages[package_name]
668 # -- Return the subset dict with the packages for 'platform_id'.
669 return filtered_packages
671 @property
672 def is_linux(self) -> bool:
673 """Returns True iff underlying platform is a Linux."""
674 return self.platform.is_linux
676 @property
677 def is_darwin(self) -> bool:
678 """Returns True iff underlying platform is a Mac OSX."""
679 return self.platform.is_darwin
681 @property
682 def is_windows(self) -> bool:
683 """Returns True iff underlying platform is a Windows."""
684 return self.platform.is_windows
686 def _get_env_mutations_for_packages(self) -> EnvMutations:
687 """Collects the env mutation for each of the defined packages,
688 in the order they are defined."""
690 unset_vars: List[str] = []
691 paths: List[str] = []
692 set_vars: Dict[str, str] = {}
693 for _, package_config in self.required_packages.items():
694 # -- Get the json 'env' section. We require it, even if it's empty,
695 # -- for clarity reasons.
696 assert "env" in package_config
697 package_env = package_config["env"]
699 # -- Collect the env vars to unset.
700 unset_vars_section = package_env.get("unset-vars", [])
701 for var_name in unset_vars_section:
702 # -- Detect duplicates.
703 assert var_name not in unset_vars, var_name
704 unset_vars.append(var_name)
706 # -- Collect the path values.
707 package_paths = package_env.get("path", [])
708 paths.extend(package_paths)
710 # -- Collect the env vars to set (name, value) pairs.
711 set_vars_section = package_env.get("set-vars", {})
712 for var_name, var_value in set_vars_section.items():
713 # -- Detect duplicates.
714 assert var_name not in set_vars, var_name
715 set_vars[var_name] = var_value
717 return EnvMutations(unset_vars, paths, set_vars)
719 def _dump_env_mutations(self, mutations: EnvMutations) -> None:
720 """Dumps a user friendly representation of the env mutations."""
721 cout("Environment settings:", style=EMPH2)
723 # -- Print PATH mutations.
724 windows = self.is_windows
726 # -- Print unset vars.
727 for name in mutations.unset_vars:
728 styled_name = cstyle(name, style=EMPH3)
729 if windows: 729 ↛ 730line 729 didn't jump to line 730 because the condition on line 729 was never true
730 cout(f" set {styled_name}=")
731 else:
732 cout(f" unset {styled_name}")
734 # -- Dump paths.
735 for p in reversed(mutations.paths):
736 styled_name = cstyle("PATH", style=EMPH3)
737 if windows: 737 ↛ 738line 737 didn't jump to line 738 because the condition on line 737 was never true
738 cout(f" set {styled_name}={p};%PATH%")
739 else:
740 cout(f' {styled_name}="{p}:$PATH"')
742 # -- Print set vars.
743 for name, val in mutations.set_vars.items():
744 styled_name = cstyle(name, style=EMPH3)
745 if windows: 745 ↛ 746line 745 didn't jump to line 746 because the condition on line 745 was never true
746 cout(f" set {styled_name}={val}")
747 else:
748 cout(f' {styled_name}="{val}"')
750 def _apply_env_mutations(self, mutations: EnvMutations) -> None:
751 """Apply a given set of env mutations, while preserving their order."""
753 # -- Apply the unset var mutations
754 for name in mutations.unset_vars:
755 os.environ.pop(name, None)
757 # -- Apply the path mutations, while preserving order.
758 # -- NOTE: We treat the old path items as a single items.
759 old_val = os.environ["PATH"]
760 items = mutations.paths + [old_val]
761 new_val = os.pathsep.join(items)
762 os.environ["PATH"] = new_val
764 # -- Apply the set var mutations
765 for name, value in mutations.set_vars.items():
766 os.environ[name] = value
768 def set_env_for_packages(
769 self, *, quiet: bool = False, verbose: bool = False
770 ) -> None:
771 """Sets the environment variables for using all the that are
772 available for this platform, even if currently not installed.
774 The function sets the environment only on first call and in latter
775 calls skips the operation silently.
777 If quite is set, no output is printed. When verbose is set, additional
778 output such as the env vars mutations are printed, otherwise, a minimal
779 information is printed to make the user aware that they commands they
780 see are executed in a modified env settings.
781 """
783 # -- If this fails, this is a programming error. Quiet and verbose
784 # -- cannot be combined.
785 assert not (quiet and verbose), "Can't have both quite and verbose."
787 # -- Collect the env mutations for all packages.
788 mutations = self._get_env_mutations_for_packages()
790 if verbose:
791 self._dump_env_mutations(mutations)
793 # -- If this is the first call in this apio invocation, apply the
794 # -- mutations. These mutations are temporary for the lifetime of this
795 # -- process and does not affect the user's shell environment.
796 # -- The mutations are also inherited by child processes such as the
797 # -- scons processes.
798 if not self.env_was_already_set: 798 ↛ exitline 798 didn't return from function 'set_env_for_packages' because the condition on line 798 was always true
799 self._apply_env_mutations(mutations)
800 self.env_was_already_set = True
801 if not verbose and not quiet:
802 cout("Setting shell vars.")