Skip to content

Commit a92a3a0

Browse files
authored
grass.app: Separate env modification from path retrieval (#6469)
The RuntimePaths object would also modify the environment while a variable is being retrieved. This is unexpected side effect of an attribute access. It was also the way how the GISBASE environment variable was set in the main grass executable which is a very opaque way of setting it. The environment modification is now removed, and the caller code must set GISBASE explicitly. The main grass executable code now first gets the compile-time determined runtime path from RuntimePaths. Then it passes that to get_install_path which will evaluate if it is an existing (valid) path, and will find a fallback if it is not. Previously, the main grass executable fully relied on the compile-time determined path. Now, it can find a path itself the same way the Python API (grass.script.setup.init) does that, but the executable uses the compile-time determined path as a starting point. If the search for a install path fallback is successful in the main executable, it will hide issues with the path in the installation. To compensate for that, the get_install_path function is now tested for consistency with the compile-time determined runtime install path coming from a RuntimePaths object in pytest-based tests. This will allow a GRASS process to run even with a broken installation (some broken installations to be exect), but running the test will show that there is an issue. To further facilitate debugging, the get_install_path returns the originally provided path if none of the tested ways was successful. The get_install_path function now guarantees that the paths it returns exists except when none exists, then it returns whatever was the path provided as a parameter. This makes it possible to pass an invalid compile-time determined path, but fall back to path determined in some other way, while behaving consistently with other cases (some were already using existence as a test). The function now also always returns a string assuming the primary usage is to set an environment variable. The path for the main executable is now set in the same way as for the init function. Both are in the library, although still at two different places and with a slight difference of what is used as the starting point (compile-time determined runtime path and user-provided path or, usually, None).
1 parent c32823c commit a92a3a0

File tree

4 files changed

+118
-26
lines changed

4 files changed

+118
-26
lines changed

lib/init/grass.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2169,6 +2169,7 @@ def main() -> None:
21692169
find_grass_python_package()
21702170

21712171
from grass.app.runtime import RuntimePaths
2172+
from grass.script.setup import get_install_path
21722173

21732174
global \
21742175
CMD_NAME, \
@@ -2183,7 +2184,7 @@ def main() -> None:
21832184
GRASS_VERSION = runtime_paths.version
21842185
GRASS_VERSION_MAJOR = runtime_paths.version_major
21852186
GRASS_VERSION_GIT = runtime_paths.grass_version_git
2186-
GISBASE = runtime_paths.gisbase
2187+
GISBASE = get_install_path(runtime_paths.gisbase)
21872188
CONFIG_PROJSHARE = runtime_paths.config_projshare
21882189

21892190
grass_config_dir = create_grass_config_dir()

python/grass/app/runtime.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,9 @@ def __get_dir(self, env_var):
7878
If the environmental variable not yet set, it is retrived and
7979
set from resource_paths."""
8080
if env_var in self.env and len(self.env[env_var]) > 0:
81-
res = os.path.normpath(self.env[env_var])
82-
else:
83-
path = getattr(res_paths, env_var)
84-
res = os.path.normpath(os.path.join(res_paths.GRASS_PREFIX, path))
85-
self.env[env_var] = res
86-
return res
81+
return os.path.normpath(self.env[env_var])
82+
path = getattr(res_paths, env_var)
83+
return os.path.normpath(os.path.join(res_paths.GRASS_PREFIX, path))
8784

8885

8986
def get_grass_config_dir(*, env):
@@ -187,6 +184,9 @@ def set_executable_paths(install_path, grass_config_dir, env):
187184

188185
def set_paths(install_path, grass_config_dir):
189186
"""Set variables with executable paths, library paths, and other paths"""
187+
# Set main prefix.
188+
# See also grass.script.setup.setup_runtime_env.
189+
os.environ["GISBASE"] = install_path
190190
set_executable_paths(
191191
install_path=install_path, grass_config_dir=grass_config_dir, env=os.environ
192192
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Tests of runtime setup, mostly focused on paths
2+
3+
The install path (aka GISBASE) tests are assuming, or focusing on a non-FHS
4+
installation. Their potential to fail is with broken builds and installs.
5+
"""
6+
7+
from pathlib import Path
8+
9+
import pytest
10+
11+
from grass.app.runtime import RuntimePaths
12+
from grass.script.setup import get_install_path
13+
14+
15+
def return_as_is(x):
16+
"""Return the parameter exactly as received"""
17+
return x
18+
19+
20+
def test_install_path_consistent():
21+
"""Differently sourced install paths should be the same.
22+
23+
Dynamically determined install path and compile-time install path should be
24+
the same in a healthy installation.
25+
26+
This is a non-FHS oriented test.
27+
"""
28+
assert get_install_path() == RuntimePaths().gisbase
29+
30+
31+
@pytest.mark.parametrize("path_type", [str, Path, return_as_is])
32+
def test_install_path_used_as_result(path_type):
33+
"""Passing a valid compile-time install path should return the same path.
34+
35+
If the path is not recognized as install path or there is a problem with the
36+
dynamic determination of the install path, the test will fail.
37+
38+
This is a non-FHS oriented test.
39+
"""
40+
path = RuntimePaths().gisbase
41+
assert get_install_path(path_type(path)) == path
42+
43+
44+
def test_consistent_install_path_returned():
45+
"""Two subsequent calls should return the same result.
46+
47+
The environment should not be modified by the call, so the result should
48+
always be the same.
49+
50+
This is a non-FHS oriented test.
51+
"""
52+
assert get_install_path() == get_install_path()
53+
54+
55+
@pytest.mark.parametrize("path_type", [str, Path, return_as_is])
56+
def test_feeding_output_as_input_again(path_type):
57+
"""Passing result of the get_install_path back to it should give the same result.
58+
59+
When a dynamically path gets returned by the function, the same path should be
60+
the one returned again when called with that path (sort of like calling the same
61+
function twice because we can't tell here if it is a newly computed path or the
62+
provided path if they are the same).
63+
64+
We use this to test different path types.
65+
66+
This is a non-FHS oriented test.
67+
"""
68+
path = get_install_path()
69+
assert path == get_install_path(path_type(path))
70+
71+
72+
@pytest.mark.parametrize("path_type", [str, Path, return_as_is])
73+
def test_passing_non_existent_path(path_type):
74+
"""Passing result of the get_install_path back to it should give the same result.
75+
76+
When a dynamically path gets returned by the function, the same path should be
77+
the one returned again when called with that path (sort of like calling the same
78+
function twice because we can't tell here if it is a newly computed path or the
79+
provided path if they are the same).
80+
81+
This is a non-FHS oriented test.
82+
"""
83+
assert get_install_path(path_type("/does/not/exist")) == get_install_path()

python/grass/script/setup.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@
8080
# is known, this would allow moving things from there, here
8181
# then this could even do locking
8282

83+
from __future__ import annotations
84+
8385
from pathlib import Path
8486
import datetime
8587
import os
@@ -109,7 +111,7 @@ def set_gui_path():
109111
sys.path.insert(0, gui_path)
110112

111113

112-
def get_install_path(path=None):
114+
def get_install_path(path: str | Path | None = None) -> str:
113115
"""Get path to GRASS installation usable for setup of environmental variables.
114116
115117
The function tries to determine path tp GRASS installation so that the
@@ -145,27 +147,31 @@ def ask_executable(arg):
145147
[arg, "--config", "path"], text=True, check=True, capture_output=True
146148
).stdout.strip()
147149

150+
# Directory was provided as a parameter.
151+
if path and os.path.isdir(path):
152+
return os.fspath(path)
153+
148154
# Executable was provided as parameter.
149155
if path and shutil.which(path):
150156
# The path was provided by the user and it is an executable
151157
# (on path or provided with full path), so raise exception on failure.
152-
return ask_executable(path)
153-
154-
# Presumably directory was provided.
155-
if path:
156-
return path
158+
path_from_executable = ask_executable(path)
159+
if os.path.isdir(path_from_executable):
160+
return path_from_executable
157161

158-
# GISBASE is already set.
162+
# GISBASE is set from the outside or already set.
159163
env_gisbase = os.environ.get("GISBASE")
160-
if env_gisbase:
164+
if env_gisbase and os.path.isdir(env_gisbase):
161165
return env_gisbase
162166

163167
# Executable provided in environment (name is from grass-session).
164168
# The variable is supported (here), documented, but not widely promoted
165169
# at this point (to be re-evaluated).
166170
grass_bin = os.environ.get("GRASSBIN")
167171
if grass_bin and shutil.which(grass_bin):
168-
return ask_executable(grass_bin)
172+
path_from_executable = ask_executable(grass_bin)
173+
if os.path.isdir(path_from_executable):
174+
return path_from_executable
169175

170176
# Derive the path from path to this file (Python module).
171177
# This is the standard way when there is no user-provided settings.
@@ -176,17 +182,21 @@ def ask_executable(arg):
176182
bin_path = install_path / "bin"
177183
lib_path = install_path / "lib"
178184
if bin_path.is_dir() and lib_path.is_dir():
179-
return install_path
185+
return os.fspath(install_path)
180186

181187
# As a last resort, try running grass command if it exists.
182188
# This is less likely give the right result than the relative path on systems
183189
# with multiple installations (where an explicit setup is likely required).
184190
# However, it allows for non-standard installations with standard command.
185191
grass_bin = "grass"
186192
if grass_bin and shutil.which(grass_bin):
187-
return ask_executable(grass_bin)
193+
path_from_executable = ask_executable(grass_bin)
194+
if os.path.isdir(path_from_executable):
195+
return path_from_executable
188196

189-
return None
197+
# We fallback to whatever was provided. This may help trace the issue
198+
# in broken installations.
199+
return os.fspath(path) if path else path
190200

191201

192202
def setup_runtime_env(gisbase=None, *, env=None):
@@ -202,11 +212,7 @@ def setup_runtime_env(gisbase=None, *, env=None):
202212
If _gisbase_ is not provided, a heuristic is used to find the path to GRASS
203213
installation (see the :func:`get_install_path` function for details).
204214
"""
205-
if not gisbase:
206-
gisbase = get_install_path()
207-
208-
# Accept Path objects.
209-
gisbase = os.fspath(gisbase)
215+
gisbase = get_install_path(gisbase)
210216

211217
# If environment is not provided, use the global one.
212218
if not env:
@@ -221,15 +227,17 @@ def setup_runtime_env(gisbase=None, *, env=None):
221227
RuntimePaths,
222228
)
223229

224-
# Set GISBASE
230+
runtime_paths = RuntimePaths(env=env)
231+
# Set main prefix.
232+
# See also grass.app.runtime.set_paths.
225233
env["GISBASE"] = gisbase
226234
set_executable_paths(
227235
install_path=gisbase,
228236
grass_config_dir=get_grass_config_dir(env=env),
229237
env=env,
230238
)
231239
set_dynamic_library_path(
232-
variable_name=RuntimePaths().ld_library_path_var, install_path=gisbase, env=env
240+
variable_name=runtime_paths.ld_library_path_var, install_path=gisbase, env=env
233241
)
234242
set_python_path_variable(install_path=gisbase, env=env)
235243
set_path_to_python_executable(env=env)

0 commit comments

Comments
 (0)