Describe the bug
When gs.setup.init() is called repeatedly on the same environment, typically the global os.environ in a long-lived process (a pytest run, a Jupyter kernel), each call adds the GRASS paths to PATH, PYTHONPATH, and the dynamic library path variable again, without checking whether they are already there, and finish() does not remove them. The variables grow without bound. A single init() on a fresh environment, e.g., env=os.environ.copy() per session, is not affected.
Details (analysis generated with Claude Code)
init() calls setup_runtime_env() unconditionally (python/grass/script/setup.py:404). A guard exists (runtime_env_is_active() used by ensure_runtime_env(), setup.py:276), but init() does not use it. Inside setup_runtime_env(), none of the path setters in python/grass/app/runtime.py check whether an entry is already present:
set_executable_paths() (runtime.py:262) prepends GISBASE/bin, GISBASE/scripts (or extrabin on Windows), the addon bin and scripts, and any GRASS_ADDON_PATH entries to PATH.
set_dynamic_library_path() (runtime.py:325) appends GISBASE/lib to the platform's library path variable: LD_LIBRARY_PATH on Linux, LD_RUN_PATH on macOS, and PATH on Windows, so on Windows PATH grows at both ends. When the variable is unset, it first creates it empty and then appends, so the value starts with a separator, i.e., an empty entry, which for LD_LIBRARY_PATH means the current directory.
set_python_path_variable() (runtime.py:332) prepends GISBASE/etc/python to PYTHONPATH.
(GRASS_PYTHON and GRASS_ADDON_BASE are set only when unset, so they do not accumulate.)
finish() is documented as asymmetrical ("only closes the mapset, but doesn't undo the runtime environment setup"), so repeated init()/finish() cycles accumulate.
Measured on Linux, repeating init() on the same environment: 10 calls grow PATH from 443 to 4483 characters (20 to 60 entries), +404 characters and +4 entries per call. PYTHONPATH and LD_LIBRARY_PATH grow correspondingly.
Where it bites:
Suggested fix: make the three setters idempotent, skipping an entry that is already in the variable. First-call behavior stays identical, all callers are fixed, and switching between installations keeps working since the new installation's entries are still prepended in front. Making init() skip the setup via ensure_runtime_env() instead would be coarser: runtime_env_is_active() only checks that GISBASE is a substring of PATH, and would skip a needed re-setup when the installation path changes. The leading empty entry in set_dynamic_library_path() is worth fixing along the way.
To reproduce
import os
import grass.script as gs
for i in range(10):
project = f"/tmp/project_{i}"
gs.create_project(project)
with gs.setup.init(project):
pass
print(len(os.environ["PATH"]))
PATH grows by four entries per iteration; the same happens with PYTHONPATH and LD_LIBRARY_PATH.
Expected behavior
A second init() on an environment which already contains the GRASS paths does not add them again.
System description
All platforms; on Windows the growth is doubled into PATH. Observed on main, but the behavior is old.
Describe the bug
When
gs.setup.init()is called repeatedly on the same environment, typically the globalos.environin a long-lived process (a pytest run, a Jupyter kernel), each call adds the GRASS paths toPATH,PYTHONPATH, and the dynamic library path variable again, without checking whether they are already there, andfinish()does not remove them. The variables grow without bound. A singleinit()on a fresh environment, e.g.,env=os.environ.copy()per session, is not affected.Details (analysis generated with Claude Code)
init()callssetup_runtime_env()unconditionally (python/grass/script/setup.py:404). A guard exists (runtime_env_is_active()used byensure_runtime_env(),setup.py:276), butinit()does not use it. Insidesetup_runtime_env(), none of the path setters inpython/grass/app/runtime.pycheck whether an entry is already present:set_executable_paths()(runtime.py:262) prependsGISBASE/bin,GISBASE/scripts(orextrabinon Windows), the addonbinandscripts, and anyGRASS_ADDON_PATHentries toPATH.set_dynamic_library_path()(runtime.py:325) appendsGISBASE/libto the platform's library path variable:LD_LIBRARY_PATHon Linux,LD_RUN_PATHon macOS, andPATHon Windows, so on WindowsPATHgrows at both ends. When the variable is unset, it first creates it empty and then appends, so the value starts with a separator, i.e., an empty entry, which forLD_LIBRARY_PATHmeans the current directory.set_python_path_variable()(runtime.py:332) prependsGISBASE/etc/pythontoPYTHONPATH.(
GRASS_PYTHONandGRASS_ADDON_BASEare set only when unset, so they do not accumulate.)finish()is documented as asymmetrical ("only closes the mapset, but doesn't undo the runtime environment setup"), so repeatedinit()/finish()cycles accumulate.Measured on Linux, repeating
init()on the same environment: 10 calls growPATHfrom 443 to 4483 characters (20 to 60 entries), +404 characters and +4 entries per call.PYTHONPATHandLD_LIBRARY_PATHgrow correspondingly.Where it bites:
gs.setup.init(project)withoutenv=mutatesos.environ; the suite currently has about 18 such calls collected beforeraster/. On Windows this pushedPATHpast whatcmd.exehandles and broke r.coin, which runs r.stats through cmd.exe (grass.temporal: Use an explicit env in the gui_support test session #7732, [Bug] r.coin: Prints a category 0 table and exits with success when r.stats produces no output #7734).gj.init()(python/grass/jupyter/setup.py:166) callsgs.setup.init()withoutenv, so every re-run of the init cell grows the kernel's environment.Suggested fix: make the three setters idempotent, skipping an entry that is already in the variable. First-call behavior stays identical, all callers are fixed, and switching between installations keeps working since the new installation's entries are still prepended in front. Making
init()skip the setup viaensure_runtime_env()instead would be coarser:runtime_env_is_active()only checks that GISBASE is a substring ofPATH, and would skip a needed re-setup when the installation path changes. The leading empty entry inset_dynamic_library_path()is worth fixing along the way.To reproduce
PATHgrows by four entries per iteration; the same happens withPYTHONPATHandLD_LIBRARY_PATH.Expected behavior
A second
init()on an environment which already contains the GRASS paths does not add them again.System description
All platforms; on Windows the growth is doubled into
PATH. Observed onmain, but the behavior is old.