Coverage for apio/common/subprocess_env.py: 78%
98 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""Utilities related to the runtime environment of tools and other
10subprocesses."""
12import os
13from collections.abc import MutableMapping
14from apio.common.debug_util import is_debug
15from apio.common.apio_console import cout, cstyle, fatal_error
16from apio.common.apio_styles import EMPH2, EMPH3
17from apio.common.apio_platforms import ApioPlatform
18from apio.common import proto_util
19from apio.common.proto.stubs.apio_common_pb2 import EnvMutations, NameValue
20from apio.common.proto.stubs.apio_config_pb2 import ApioPlatformEnvSpec
22# -- Env vars involved in pyinstaller fixing under linux.
23# -- See See https://github.com/FPGAwars/apio/issues/887
24LD_LIBRARY_PATH = "LD_LIBRARY_PATH"
25LD_LIBRARY_PATH_ORIG = "LD_LIBRARY_PATH_ORIG"
28def scons_shell_id(apio_platform: ApioPlatform) -> str:
29 """
30 Returns a simplified string name of the shell that SCons will use
31 for executing shell-dependent commands. See code below for possible
32 values.
33 """
35 # pylint: disable=too-many-return-statements
37 # -- Handle windows.
38 if apio_platform.is_windows: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true
39 comspec = os.environ.get("COMSPEC", "").lower()
40 if "powershell.exe" in comspec or "pwsh.exe" in comspec:
41 return "powershell"
42 if "cmd.exe" in comspec:
43 return "cmd"
44 return "unknown"
46 # -- Handle the rest (macOS, Linux, etc.)
47 shell_path = os.environ.get("SHELL", "").lower()
48 if "bash" in shell_path: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 return "bash"
50 if "zsh" in shell_path: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 return "zsh"
52 if "fish" in shell_path: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 return "fish"
54 if "dash" in shell_path: 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true
55 return "dash"
56 if "ksh" in shell_path: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 return "ksh"
58 if "csh" in shell_path or "tcsh" in shell_path: 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true
59 return "cshell"
60 return "unknown"
63def get_env_mutations_for_subprocess(
64 apio_platform: ApioPlatform,
65 is_pyinstaller_app: bool,
66 env_spec: ApioPlatformEnvSpec | None,
67) -> EnvMutations:
68 """Return an EnvMutations with the system env mutations for running a
69 subprocess. If apio_packages is not empty, the mutations includes also
70 mutation to use the included apio packages (e.g. 'yosys' from
71 oss-cad-suite)."""
73 assert env_spec is None or isinstance(env_spec, ApioPlatformEnvSpec), type(
74 env_spec
75 )
77 delete_env_var: list[str] = []
78 paths: list[str] = []
79 add_env_vars: list[NameValue] = []
81 if env_spec is not None:
82 # -- Collect the env vars to delete.
83 for var_name in env_spec.delete_env_vars:
84 # -- Detect duplicates.
85 assert var_name not in delete_env_var, var_name
86 delete_env_var.append(var_name)
88 # -- Collect the path values to add.
89 paths.extend(env_spec.add_to_path)
91 # -- Collect the env vars to add (name, value) pairs.
92 for name, value in env_spec.add_env_vars.items():
93 # -- Detect duplicates.
94 for add_var in add_env_vars:
95 assert name != add_var.name, name
96 # -- Append.
97 add_env_vars.append(NameValue(name=name, value=value))
99 # -- Determine if we need to fix the env for pyinstaller linux.
100 pyinstaller_linux_fix: bool = is_pyinstaller_app and apio_platform.is_linux
102 # -- Construct the result.
103 result = EnvMutations(
104 add_to_path=paths,
105 delete_env_var=delete_env_var,
106 add_env_vars=add_env_vars,
107 pyinstaller_linux_fix=pyinstaller_linux_fix,
108 )
109 proto_util.check_is_initialized(
110 result, "Failed to initialized EnvMutations"
111 )
113 # -- All done.
114 return result
117def show_env_mutations(
118 mutations: EnvMutations, apio_platform: ApioPlatform
119) -> None:
120 """Dumps a user friendly representation of the env mutations."""
121 cout("Environment settings:", style=EMPH2)
123 # -- Special case for windows.
124 windows = apio_platform.is_windows
126 # -- Print paths.
127 for p in reversed(mutations.add_to_path):
128 styled_name = cstyle("PATH", style=EMPH3)
129 if windows:
130 cout(f" set {styled_name}={p};%PATH%")
131 else:
132 cout(f' {styled_name}="{p}:$PATH"')
134 # -- Print delete vars.
135 for name in mutations.delete_env_var:
136 styled_name = cstyle(name, style=EMPH3)
137 if windows:
138 cout(f" set {styled_name}=")
139 else:
140 cout(f" unset {styled_name}")
142 # -- Print add vars.
143 for sv in mutations.add_env_vars:
144 styled_name = cstyle(sv.name, style=EMPH3)
145 if windows:
146 cout(f" set {styled_name}={sv.value}")
147 else:
148 # -- Export so the var is visible to processes started by the
149 # -- shell, e.g. if the user copy/pastes these lines.
150 cout(f' export {styled_name}="{sv.value}"')
152 # -- NOTE For now we don't dump the pyinstaller fix mutation since
153 # -- it's internal to Apio (not user facing)
156def apply_env_mutations(
157 mutations: EnvMutations, env: MutableMapping[str, str]
158) -> None:
159 """Apply a given set of env mutations, while preserving their order."""
161 # -- Apply the path mutations, while preserving order.
162 # -- NOTE: We treat the old path items as a single items.
163 old_val = env.get("PATH")
164 if not old_val:
165 fatal_error("The PATH env var is not set or is empty.")
166 items = list(mutations.add_to_path) + [old_val]
167 env["PATH"] = os.pathsep.join(items)
169 # -- Apply the unset var mutations
170 for name in mutations.delete_env_var:
171 env.pop(name, None)
173 # -- Apply the set var mutations
174 for sv in mutations.add_env_vars:
175 env[sv.name] = sv.value
177 # -- For now we fix only for linux
178 proto_util.check_is_required(mutations, "pyinstaller_linux_fix")
179 if not mutations.pyinstaller_linux_fix:
180 return
182 if is_debug(1): 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 cout(f"Fixing {LD_LIBRARY_PATH} for linux pyinstaller.")
185 # -- Get the initial values of the vars
186 pre_ld_library_path: str | None = env.get(LD_LIBRARY_PATH)
187 pre_ld_library_path_orig: str | None = env.get(LD_LIBRARY_PATH_ORIG)
189 if is_debug(1): 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true
190 cout(f"[pre-fix] {LD_LIBRARY_PATH}={pre_ld_library_path}")
191 cout(f"[pre-fix] {LD_LIBRARY_PATH_ORIG}={pre_ld_library_path_orig}")
193 # -- Fix LD_LIBRARY_PATH and LD_LIBRARY_PATH_ORIG
194 if pre_ld_library_path_orig is None:
195 # -- LD_LIBRARY_PATH was originally unset but pyinstaller set it
196 # -- up. Unset it.
197 env.pop(LD_LIBRARY_PATH, None)
198 else:
199 # -- LD_LIBRARY_PATH was originally set, restore the original
200 # -- value.
201 env[LD_LIBRARY_PATH] = pre_ld_library_path_orig
202 # -- Unset LD_LIBRARY_PATH_ORIG. Since we will ignore future
203 # -- requests for fixing, we don't need it anymore and we want to
204 # -- hide it from the tool, to preserve the original env.
205 env.pop(LD_LIBRARY_PATH_ORIG, None)
207 # -- Show outcome.
208 if is_debug(1): 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true
209 post_ld_library_path = env.get(LD_LIBRARY_PATH)
210 post_ld_library_path_orig = env.get(LD_LIBRARY_PATH_ORIG)
211 cout(f"[post-fix] {LD_LIBRARY_PATH}={post_ld_library_path}")
212 cout(
213 f"[post-fix] {LD_LIBRARY_PATH_ORIG}="
214 + f"{post_ld_library_path_orig}"
215 )