"""Provides functions for the main GRASS executable
(C) 2024-2025 by Vaclav Petras and the GRASS Development Team
This program is free software under the GNU General Public
License (>=v2). Read the file COPYING that comes with GRASS
for details.
.. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
This is not a stable part of the API. Use at your own risk.
"""
import collections
import os
import shutil
import subprocess
import sys
from . import resource_paths as res_paths
# Get the system name
WINDOWS = sys.platform.startswith("win")
CYGWIN = sys.platform.startswith("cygwin")
MACOS = sys.platform.startswith("darwin")
[docs]
class RuntimePaths:
"""Get runtime paths to resources and basic GRASS build properties
The resource paths are also set as environmental variables.
"""
def __init__(self, env=None):
if env is None:
env = os.environ
self.env = env
@property
def version(self):
return res_paths.GRASS_VERSION
@property
def version_major(self):
return res_paths.GRASS_VERSION_MAJOR
@property
def version_minor(self):
return res_paths.GRASS_VERSION_MINOR
@property
def ld_library_path_var(self):
return res_paths.LD_LIBRARY_PATH_VAR
@property
def grass_exe_name(self):
return res_paths.GRASS_EXE_NAME
@property
def grass_version_git(self):
return res_paths.GRASS_VERSION_GIT
@property
def gisbase(self):
return self.__get_dir("GISBASE")
@property
def prefix(self):
return self.__get_dir("GRASS_PREFIX")
@property
def config_projshare(self):
return self.env.get("GRASS_PROJSHARE", res_paths.CONFIG_PROJSHARE)
def __get_dir(self, env_var):
"""Get the directory stored in the environmental variable 'env_var'
If the environmental variable not yet set, it is retrived and
set from resource_paths."""
if env_var in self.env and len(self.env[env_var]) > 0:
res = os.path.normpath(self.env[env_var])
else:
path = getattr(res_paths, env_var)
res = os.path.normpath(os.path.join(res_paths.GRASS_PREFIX, path))
self.env[env_var] = res
return res
[docs]
def get_grass_config_dir(major_version, minor_version, env):
"""Get configuration directory
Determines path of GRASS user configuration directory.
"""
if env.get("GRASS_CONFIG_DIR"):
# use GRASS_CONFIG_DIR environmental variable is defined
env_dirname = "GRASS_CONFIG_DIR"
else:
env_dirname = "APPDATA" if WINDOWS else "HOME"
config_dir = env.get(env_dirname)
if config_dir is None:
msg = (
f"The {env_dirname} variable is not set, ask your operating system support"
)
raise RuntimeError(msg)
if not os.path.isdir(config_dir):
msg = (
f"The {env_dirname} variable points to directory which does"
" not exist, ask your operating system support"
)
raise NotADirectoryError(msg)
if WINDOWS:
config_dirname = f"GRASS{major_version}"
elif MACOS:
config_dirname = os.path.join(
"Library", "GRASS", f"{major_version}.{minor_version}"
)
else:
config_dirname = f".grass{major_version}"
return os.path.join(config_dir, config_dirname)
[docs]
def append_left_main_executable_paths(paths, install_path):
"""Add executables to PATH"""
paths.appendleft(os.path.join(install_path, "bin"))
if WINDOWS:
# Standalone installer has dependencies which are on path in other cases.
path = os.path.join(install_path, "extrabin")
if os.path.exists(path):
paths.appendleft(path)
else:
# Without FHS, scripts are separated like in the source code.
path = os.path.join(install_path, "scripts")
if os.path.exists(path):
paths.appendleft(path)
[docs]
def append_left_addon_paths(paths, config_dir, env):
"""Add addons to path"""
# addons (base)
addon_base = env.get("GRASS_ADDON_BASE")
if not addon_base:
name = "addons" if not MACOS else "Addons"
addon_base = os.path.join(config_dir, name)
env["GRASS_ADDON_BASE"] = addon_base
if not WINDOWS:
script_path = os.path.join(addon_base, "scripts")
if os.path.exists(script_path):
paths.appendleft(script_path)
paths.appendleft(os.path.join(addon_base, "bin"))
# addons (path)
addon_path = env.get("GRASS_ADDON_PATH")
if addon_path:
for path in addon_path.split(os.pathsep):
paths.appendleft(path)
[docs]
def set_executable_paths(install_path, grass_config_dir, env):
"""Add paths with executables to PATH in _env_"""
paths = collections.deque()
# Addons
append_left_addon_paths(paths, grass_config_dir, env=env)
# Standard installation
append_left_main_executable_paths(paths, install_path=install_path)
paths.append(env.get("PATH"))
env["PATH"] = os.pathsep.join(paths)
[docs]
def set_paths(install_path, grass_config_dir, ld_library_path_variable_name):
"""Set variables with executable paths, library paths, and other paths"""
set_executable_paths(
install_path=install_path, grass_config_dir=grass_config_dir, env=os.environ
)
# Set LD_LIBRARY_PATH (etc) to find GRASS shared libraries
# this works for subprocesses but won't affect the current process
if ld_library_path_variable_name:
set_dynamic_library_path(
variable_name=ld_library_path_variable_name,
install_path=install_path,
env=os.environ,
)
set_python_path_variable(install_path=install_path, env=os.environ)
# retrieving second time, but now it is always set
addon_base = os.getenv("GRASS_ADDON_BASE")
set_man_path(install_path=install_path, addon_base=addon_base, env=os.environ)
set_isis()
[docs]
def set_man_path(install_path, addon_base, env):
"""Set path for the GRASS man pages"""
grass_man_path = os.path.join(install_path, "docs", "man")
addons_man_path = os.path.join(addon_base, "docs", "man")
man_path = env.get("MANPATH")
paths = collections.deque()
if man_path:
paths.appendleft(man_path)
else:
system_path = None
if manpath_executable := shutil.which("manpath"):
try:
system_path = subprocess.run(
[manpath_executable],
text=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
timeout=2,
).stdout.strip()
except (OSError, subprocess.SubprocessError):
pass
if system_path:
# Variable does not exist, but the system has the information.
paths.appendleft(system_path)
paths.appendleft(addons_man_path)
paths.appendleft(grass_man_path)
os.environ["MANPATH"] = os.pathsep.join(paths)
[docs]
def set_dynamic_library_path(variable_name, install_path, env):
"""Define path to dynamic libraries (LD_LIBRARY_PATH on Linux)"""
if variable_name not in env:
env[variable_name] = ""
env[variable_name] += os.pathsep + os.path.join(install_path, "lib")
[docs]
def set_python_path_variable(install_path, env):
"""Set PYTHONPATH to find GRASS Python package in subprocesses"""
path = env.get("PYTHONPATH")
etcpy = os.path.join(install_path, "etc", "python")
path = etcpy + os.pathsep + path if path else etcpy
env["PYTHONPATH"] = path
[docs]
def set_path_to_python_executable(env):
"""Set GRASS_PYTHON environment variable"""
if not env.get("GRASS_PYTHON"):
env["GRASS_PYTHON"] = sys.executable
[docs]
def set_defaults(config_projshare_path):
"""Set paths or commands for dependencies and auxiliary utilities"""
# GRASS_PAGER
if not os.getenv("GRASS_PAGER"):
if shutil.which("more"):
pager = "more"
elif shutil.which("less"):
pager = "less"
elif WINDOWS:
pager = "more"
else:
pager = "cat"
os.environ["GRASS_PAGER"] = pager
# GRASS_PYTHON
set_path_to_python_executable(env=os.environ)
# GRASS_GNUPLOT
if not os.getenv("GRASS_GNUPLOT"):
os.environ["GRASS_GNUPLOT"] = "gnuplot -persist"
# GRASS_PROJSHARE
if not os.getenv("GRASS_PROJSHARE") and config_projshare_path:
os.environ["GRASS_PROJSHARE"] = config_projshare_path
[docs]
def set_display_defaults():
"""Predefine monitor size for certain architectures"""
if os.getenv("HOSTTYPE") == "arm":
# small monitor on ARM (iPAQ, zaurus... etc)
os.environ["GRASS_RENDER_HEIGHT"] = "320"
os.environ["GRASS_RENDER_WIDTH"] = "240"
[docs]
def set_browser(install_path):
"""Set path to HTML browser"""
# GRASS_HTML_BROWSER
browser = os.getenv("GRASS_HTML_BROWSER")
if not browser:
if MACOS:
# OSX doesn't execute browsers from the shell PATH - route through a script
browser = os.path.join(install_path, "etc", "html_browser_mac.sh")
os.environ["GRASS_HTML_BROWSER_MACOSX"] = "-b com.apple.helpviewer"
if WINDOWS:
browser = "start"
elif CYGWIN:
browser = "explorer"
else:
# the usual suspects
browsers = [
"xdg-open",
"x-www-browser",
"htmlview",
"konqueror",
"mozilla",
"mozilla-firefox",
"firefox",
"iceweasel",
"opera",
"google-chrome",
"chromium",
"netscape",
"dillo",
"lynx",
"links",
"w3c",
]
for candidate in browsers:
if shutil.which(candidate):
browser = candidate
break
elif MACOS:
# OSX doesn't execute browsers from the shell PATH - route through a script
os.environ["GRASS_HTML_BROWSER_MACOSX"] = "-b %s" % browser
browser = os.path.join(install_path, "etc", "html_browser_mac.sh")
if not browser:
# even so we set to 'xdg-open' as a generic fallback
browser = "xdg-open"
os.environ["GRASS_HTML_BROWSER"] = browser
[docs]
def set_isis():
"""Enable a mixed ISIS-GRASS environment
ISIS is Integrated Software for Imagers and Spectrometers by USGS.
"""
if os.getenv("ISISROOT"):
isis = os.getenv("ISISROOT")
os.environ["ISIS_LIB"] = isis + os.sep + "lib"
os.environ["ISIS_3RDPARTY"] = isis + os.sep + "3rdParty" + os.sep + "lib"
os.environ["QT_PLUGIN_PATH"] = isis + os.sep + "3rdParty" + os.sep + "plugins"
# os.environ['ISIS3DATA'] = isis + "$ISIS3DATA"
libpath = os.getenv("LD_LIBRARY_PATH", "")
isislibpath = os.getenv("ISIS_LIB")
isis3rdparty = os.getenv("ISIS_3RDPARTY")
os.environ["LD_LIBRARY_PATH"] = (
libpath + os.pathsep + isislibpath + os.pathsep + isis3rdparty
)
[docs]
def ensure_home():
"""Set HOME if not set on MS Windows"""
if WINDOWS and not os.getenv("HOME"):
os.environ["HOME"] = os.path.join(os.getenv("HOMEDRIVE"), os.getenv("HOMEPATH"))