Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grass.app: Refactor PATH setup in grass init script and grass.script.setup #3694

Merged
merged 21 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bde9e69
Move path setting code from init script to the package
wenzeslaus May 8, 2024
3cb93ba
One common function for dynamic lib path setup
wenzeslaus May 8, 2024
5ea520e
Share all low-level env var setup between the grass init script and g…
wenzeslaus May 8, 2024
e8294da
Getting config dir needs to be duplicated because of locales, but it …
wenzeslaus May 8, 2024
0d83996
Put all the PATH manipulations into one function used in both places.
wenzeslaus May 8, 2024
d438a72
Get encoding when needed for the subprocess output
wenzeslaus May 9, 2024
c6cd5eb
Use same setup order
wenzeslaus May 9, 2024
ab1fac9
Remove need for global vars in grass.app.runtime, cleanup code
wenzeslaus May 9, 2024
d581d94
Use subprocess.run and shutil.which as recommended by subprocess doc …
wenzeslaus Jun 6, 2024
dffd92b
Order imports
wenzeslaus Jun 6, 2024
02b97b1
Merge better subprocess approach with better parametrization approach
wenzeslaus Jun 6, 2024
20c6bad
Use text=True and thus Python encoding functions for decoding, this w…
wenzeslaus Jun 6, 2024
d6e5113
Use shutils.which instead of custom find_exe to detect pagers and bro…
wenzeslaus Jun 6, 2024
014347c
Merge remote-tracking branch 'upstream/main' into refactor-path-setup…
wenzeslaus Jun 6, 2024
0082201
Better naming for package finding function
wenzeslaus Jun 6, 2024
f9371a1
Handle scripts and extrabin on Windows according to #3695
wenzeslaus Jun 15, 2024
5231e7f
Add documentation
wenzeslaus Jun 15, 2024
4f6ecff
Remove empty file
wenzeslaus Jun 15, 2024
70d4c16
Remove unused utils also from Makefile
wenzeslaus Jun 15, 2024
07c7ab7
Do not set path to GRASS-specific executables on Windows when that's …
wenzeslaus Jun 16, 2024
af1697a
Merge branch 'main' into refactor-path-setup-from-init
echoix Jun 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 34 additions & 252 deletions lib/init/grass.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,51 +106,6 @@
MACOS = sys.platform.startswith("darwin")


def decode(bytes_, encoding=ENCODING):
"""Decode bytes with default locale and return (unicode) string
Adapted from grass.script.core.utils.

No-op if parameter is not bytes (assumed unicode string).

:param bytes bytes_: the bytes to decode
:param encoding: encoding to be used, default value is the system's default
encoding or, if that cannot be determined, 'UTF-8'.
"""
if isinstance(bytes_, str):
return bytes_
elif isinstance(bytes_, bytes):
return bytes_.decode(encoding)
else:
# if something else than text
raise TypeError("can only accept types str and bytes")


def encode(string, encoding=ENCODING):
"""Encode string with default locale and return bytes with that encoding
Adapted from grass.script.core.utils.

No-op if parameter is bytes (assumed already encoded).
This ensures garbage in, garbage out.

:param str string: the string to encode
:param encoding: encoding to be used, default value is the system's default
encoding or, if that cannot be determined, 'UTF-8'.
"""
if isinstance(string, bytes):
return string
elif isinstance(string, str):
return string.encode(encoding)
else:
# if something else than text
raise TypeError("can only accept types str and bytes")


# see https://trac.osgeo.org/grass/ticket/3508
def to_text_string(obj, encoding=ENCODING):
"""Convert `obj` to (unicode) text string"""
return decode(obj, encoding=encoding)


def try_remove(path):
try:
os.remove(path)
Expand Down Expand Up @@ -247,21 +202,6 @@ def gpath(*args):
return os.path.join(GISBASE, *args)


def wxpath(*args):
"""Construct path to file or directory in GRASS wxGUI

Can be called only after GISBASE was set.

This function does not check if the directories exist or if GUI works
this must be done by the caller if needed.
"""
global _WXPYTHON_BASE
if not _WXPYTHON_BASE:
# this can be called only after GISBASE was set
_WXPYTHON_BASE = gpath("gui", "wxpython")
return os.path.join(_WXPYTHON_BASE, *args)


def count_wide_chars(s):
"""Returns the number of wide CJK characters in a string.

Expand Down Expand Up @@ -433,6 +373,7 @@ def get_grass_config_dir():
Configuration directory is for example used for grass env file
(the one which caries mapset settings from session to session).
"""
# The code is in sync with grass.app.runtime (but not the same).
if WINDOWS:
grass_config_dirname = f"GRASS{GRASS_VERSION_MAJOR}"
win_conf_path = os.getenv("APPDATA")
Expand All @@ -452,6 +393,9 @@ def get_grass_config_dir():
)
)
directory = os.path.join(win_conf_path, grass_config_dirname)
elif MACOS:
version = f"{GRASS_VERSION_MAJOR}.{GRASS_VERSION_MINOR}"
return os.path.join(env.get("HOME"), "Library", "GRASS", version)
else:
grass_config_dirname = f".grass{GRASS_VERSION_MAJOR}"
directory = os.path.join(os.getenv("HOME"), grass_config_dirname)
Expand Down Expand Up @@ -672,196 +616,6 @@ def read_gui(gisrc, default_gui):
return grass_gui


def path_prepend(directory, var):
path = os.getenv(var)
if path:
path = directory + os.pathsep + path
else:
path = directory
os.environ[var] = path


def path_append(directory, var):
path = os.getenv(var)
if path:
path = path + os.pathsep + directory
else:
path = directory
os.environ[var] = path


def set_paths(grass_config_dir):
# addons (path)
addon_path = os.getenv("GRASS_ADDON_PATH")
if addon_path:
for path in addon_path.split(os.pathsep):
path_prepend(addon_path, "PATH")

# addons (base)
addon_base = os.getenv("GRASS_ADDON_BASE")
if not addon_base:
if MACOS:
version = f"{GRASS_VERSION_MAJOR}.{GRASS_VERSION_MINOR}"
addon_base = os.path.join(
os.getenv("HOME"), "Library", "GRASS", version, "Addons"
)
else:
addon_base = os.path.join(grass_config_dir, "addons")
os.environ["GRASS_ADDON_BASE"] = addon_base
if not WINDOWS:
path_prepend(os.path.join(addon_base, "scripts"), "PATH")
path_prepend(os.path.join(addon_base, "bin"), "PATH")

# standard installation
if not WINDOWS:
path_prepend(gpath("scripts"), "PATH")
path_prepend(gpath("bin"), "PATH")

# Set PYTHONPATH to find GRASS Python modules
if os.path.exists(gpath("etc", "python")):
pythonpath = gpath("etc", "python")
path_prepend(pythonpath, "PYTHONPATH")
# the env var PYTHONPATH is only evaluated when python is started,
# thus:
sys.path.append(pythonpath)
# now we can import stuff from grass package

# set path for the GRASS man pages
grass_man_path = gpath("docs", "man")
addons_man_path = os.path.join(addon_base, "docs", "man")
man_path = os.getenv("MANPATH")
sys_man_path = None
if man_path:
path_prepend(addons_man_path, "MANPATH")
path_prepend(grass_man_path, "MANPATH")
else:
try:
nul = open(os.devnull, "w")
p = Popen(["manpath"], stdout=subprocess.PIPE, stderr=nul)
nul.close()
s = p.stdout.read()
p.wait()
sys_man_path = s.strip()
except:
pass

if sys_man_path:
os.environ["MANPATH"] = to_text_string(sys_man_path)
path_prepend(addons_man_path, "MANPATH")
path_prepend(grass_man_path, "MANPATH")
else:
os.environ["MANPATH"] = to_text_string(addons_man_path)
path_prepend(grass_man_path, "MANPATH")

# 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_VAR:
path_prepend(gpath("lib"), LD_LIBRARY_PATH_VAR)


def find_exe(pgm):
for directory in os.getenv("PATH").split(os.pathsep):
path = os.path.join(directory, pgm)
if os.access(path, os.X_OK):
return path
return None


def set_defaults():
# GRASS_PAGER
if not os.getenv("GRASS_PAGER"):
if find_exe("more"):
pager = "more"
elif find_exe("less"):
pager = "less"
elif WINDOWS:
pager = "more"
else:
pager = "cat"
os.environ["GRASS_PAGER"] = pager

# GRASS_PYTHON
if not os.getenv("GRASS_PYTHON"):
if WINDOWS:
os.environ["GRASS_PYTHON"] = "python3.exe"
else:
os.environ["GRASS_PYTHON"] = "python3"

# GRASS_GNUPLOT
if not os.getenv("GRASS_GNUPLOT"):
os.environ["GRASS_GNUPLOT"] = "gnuplot -persist"

# GRASS_PROJSHARE
if not os.getenv("GRASS_PROJSHARE"):
os.environ["GRASS_PROJSHARE"] = CONFIG_PROJSHARE


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"


def set_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 = gpath("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 b in browsers:
if find_exe(b):
browser = b
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 = gpath("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


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


def create_initial_gisrc(filename):
# for convenience, define GISDBASE as pwd:
s = (
Expand Down Expand Up @@ -2455,6 +2209,19 @@ def validate_cmdline(params):
# without --exec (usefulness to be evaluated).


def find_python_packages():
# Set PYTHONPATH to find GRASS Python modules
if os.path.exists(gpath("etc", "python")):
pythonpath = gpath("etc", "python")
# path_prepend(pythonpath, "PYTHONPATH")
# the env var PYTHONPATH is only evaluated when python is started,
# thus:
sys.path.append(pythonpath)
# now we can import stuff from grass package
else:
raise RuntimeError("Python package grass is missing")


def main():
"""The main function which does the whole setup and run procedure

Expand Down Expand Up @@ -2527,11 +2294,26 @@ def main():
# Create the session grassrc file
gisrc = create_gisrc(tmpdir, gisrcrc)

find_python_packages()

from grass.app.runtime import (
ensure_home,
set_paths,
set_defaults,
set_display_defaults,
set_browser,
set_gisbase,
)

set_gisbase(GISBASE)
ensure_home()
# Set PATH, PYTHONPATH, ...
set_paths(grass_config_dir=grass_config_dir)
set_paths(
grass_config_dir=grass_config_dir,
ld_library_path_variable_name=LD_LIBRARY_PATH_VAR,
)
# Set GRASS_PAGER, GRASS_PYTHON, GRASS_GNUPLOT, GRASS_PROJSHARE
set_defaults()
set_defaults(config_projshare_path=CONFIG_PROJSHARE)
set_display_defaults()
# Set GRASS_HTML_BROWSER
set_browser()
Expand Down
5 changes: 4 additions & 1 deletion python/grass/app/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ include $(MODULE_TOPDIR)/include/Make/Python.make

DSTDIR = $(ETC)/python/grass/app

MODULES = data
MODULES = \
data \
runtime \
utils

PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__)
PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__)
Expand Down
Loading
Loading