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

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 

8 

9"""Utilities related to the runtime environment of tools and other 

10subprocesses.""" 

11 

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 

21 

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" 

26 

27 

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 """ 

34 

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

36 

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" 

45 

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" 

61 

62 

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).""" 

72 

73 assert env_spec is None or isinstance(env_spec, ApioPlatformEnvSpec), type( 

74 env_spec 

75 ) 

76 

77 delete_env_var: list[str] = [] 

78 paths: list[str] = [] 

79 add_env_vars: list[NameValue] = [] 

80 

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) 

87 

88 # -- Collect the path values to add. 

89 paths.extend(env_spec.add_to_path) 

90 

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)) 

98 

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 

101 

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 ) 

112 

113 # -- All done. 

114 return result 

115 

116 

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) 

122 

123 # -- Special case for windows. 

124 windows = apio_platform.is_windows 

125 

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"') 

133 

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}") 

141 

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}"') 

151 

152 # -- NOTE For now we don't dump the pyinstaller fix mutation since 

153 # -- it's internal to Apio (not user facing) 

154 

155 

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.""" 

160 

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) 

168 

169 # -- Apply the unset var mutations 

170 for name in mutations.delete_env_var: 

171 env.pop(name, None) 

172 

173 # -- Apply the set var mutations 

174 for sv in mutations.add_env_vars: 

175 env[sv.name] = sv.value 

176 

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 

181 

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.") 

184 

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) 

188 

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}") 

192 

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) 

206 

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 )