From 79b8775539b94b8cc17ade28d2577574b9ef2352 Mon Sep 17 00:00:00 2001 From: Nicklas Larsson Date: Fri, 20 Jun 2025 16:56:36 +0200 Subject: [PATCH] build: refactor grass.py in preparation for FHS (This is extracted from #5630 to simplify review.) The transitional period from the traditional GRASS installation structure to a Filesystem Hierarchy Standard (FHS) complying structure (enabled by CMake build) requires an alternative way to find resources. Currently all resources are located in relation to GISBASE, that is no longer the case with a FHS installation. In addition, and instead of GISBASE, the following environment variables will be needed (not included in this update): - GRASS_SHAREDIR - GRASS_LOCALEDIR - GRASS_PYDIR - GRASS_GUIWXDIR - GRASS_GUISCRIPTDIR - GRASS_GUIRESDIR - GRASS_FONTSDIR - GRASS_ETCDIR Setting up all these variables would bloat the grass.py file, therefore this update suggests to move this to the grass.app Python module. Summary of changes: - grass.py: to simplify Conda adoption, the variable GRASS_PREFIX is added, which is essentially equal to installation prefix, but easily changeable if needed. - grass.py need to find the Python module to be able to set necessary variables, a new variable GRASS_PYDIR is configured into the file to do just this. - python/grass/app/resource_paths.py is a configurable file (like grass.py) - update copy_python_files_in_subdir.cmake to be able to exclude files --- .../modules/copy_python_files_in_subdir.cmake | 5 ++- include/Make/Install.make | 10 ++++- lib/init/CMakeLists.txt | 8 ++-- lib/init/Makefile | 3 ++ lib/init/grass.py | 41 +++++++++---------- python/grass/CMakeLists.txt | 19 +++++++-- python/grass/app/Makefile | 15 ++++++- python/grass/app/resource_paths.py | 31 ++++++++++++++ 8 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 python/grass/app/resource_paths.py diff --git a/cmake/modules/copy_python_files_in_subdir.cmake b/cmake/modules/copy_python_files_in_subdir.cmake index be477f6066c..0cd33a7e655 100644 --- a/cmake/modules/copy_python_files_in_subdir.cmake +++ b/cmake/modules/copy_python_files_in_subdir.cmake @@ -9,9 +9,12 @@ SPDX-License-Identifier: GPL-2.0-or-later #]] function(copy_python_files_in_subdir dir_name dst_prefix) - cmake_parse_arguments(G "PRE_BUILD;PRE_LINK;POST_BUILD" "TARGET" "" ${ARGN}) + cmake_parse_arguments(G "PRE_BUILD;PRE_LINK;POST_BUILD" "TARGET" "EXCLUDE" ${ARGN}) file(GLOB PY_FILES "${CMAKE_CURRENT_SOURCE_DIR}/${dir_name}/*.py") + foreach(exclude_file ${G_EXCLUDE}) + list(FILTER PY_FILES EXCLUDE REGEX ".*/${exclude_file}") + endforeach() if(DEFINED G_TARGET) if(${G_PRE_BUILD}) diff --git a/include/Make/Install.make b/include/Make/Install.make index 799a695e80d..44fcd0dcf34 100644 --- a/include/Make/Install.make +++ b/include/Make/Install.make @@ -95,11 +95,14 @@ FONTCAP = etc/fontcap TMPGISRC = demolocation/.grassrc$(GRASS_VERSION_MAJOR)$(GRASS_VERSION_MINOR) PLATMAKE = include/Make/Platform.make GRASSMAKE = include/Make/Grass.make +RESOURCE_PATHS = etc/python/grass/app/resource_paths.py real-install: | $(DESTDIR) $(DESTDIR)$(INST_DIR) $(DESTDIR)$(UNIX_BIN) -tar cBCf $(GISBASE) - . | tar xBCf $(DESTDIR)$(INST_DIR) - 2>/dev/null -rm $(DESTDIR)$(INST_DIR)/$(GRASS_NAME).tmp + -rm $(DESTDIR)$(INST_DIR)/$(RESOURCE_PATHS) $(MAKE) $(STARTUP) + $(MAKE) $(DESTDIR)$(INST_DIR)/$(RESOURCE_PATHS) -rm $(DESTDIR)$(INST_DIR)/$(FONTCAP) $(MAKE) $(DESTDIR)$(INST_DIR)/$(FONTCAP) @@ -122,12 +125,17 @@ $(DESTDIR)$(INST_DIR) $(DESTDIR)$(UNIX_BIN): $(MAKE_DIR_CMD) $@ $(STARTUP): $(ARCH_DISTDIR)/$(GRASS_NAME).tmp - sed -e 's#'@GISBASE_INSTALL_PATH@'#'$(INST_DIR)'#g' \ + sed -e 's#'@GRASS_PREFIX@'#'$(INST_DIR)'#g' \ -e 's#'@LD_LIBRARY_PATH_VAR@'#'$(LD_LIBRARY_PATH_VAR)'#g' \ -e 's#'@CONFIG_PROJSHARE@'#'$(PROJSHARE)'#g' \ $< > $@ -$(CHMOD) a+x $@ +$(DESTDIR)$(INST_DIR)/$(RESOURCE_PATHS): $(ARCH_DISTDIR)/resource_paths.py + sed -e 's#'@GRASS_PREFIX@'#'$(INST_DIR)'#g' \ + -e 's#'@GISBASE_INSTALL_PATH@'##g' \ + $< > $@ + define fix_gisbase sed -e 's#$(GISBASE)#$(INST_DIR)#g' $< > $@ endef diff --git a/lib/init/CMakeLists.txt b/lib/init/CMakeLists.txt index 89509db5c69..9f52c2ecf47 100644 --- a/lib/init/CMakeLists.txt +++ b/lib/init/CMakeLists.txt @@ -59,12 +59,14 @@ elseif(WIN32) endif() # configure and install grass.py -set(GISBASE_INSTALL_PATH ${RUNTIME_GISBASE}) +set(GRASS_PREFIX ${OUTDIR}) +set(GRASS_PYDIR ${GRASS_INSTALL_PYDIR}) configure_file(grass.py ${OUTDIR}/${CMAKE_INSTALL_BINDIR}/${START_UP} @ONLY) -set(GISBASE_INSTALL_PATH ${GISBASE}) +set(GRASS_PREFIX ${CMAKE_INSTALL_PREFIX}) configure_file(grass.py ${CMAKE_CURRENT_BINARY_DIR}/${START_UP} @ONLY) -unset(GISBASE_INSTALL_PATH) +unset(GRASS_PYDIR) +unset(GRASS_PREFIX) install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${START_UP} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/lib/init/Makefile b/lib/init/Makefile index 99366b060a9..8a7127189d9 100644 --- a/lib/init/Makefile +++ b/lib/init/Makefile @@ -77,6 +77,8 @@ endif -e 's#@LD_LIBRARY_PATH_VAR@#$(LD_LIBRARY_PATH_VAR)#' \ -e 's#@START_UP@#$(START_UP)#' \ -e 's#@CONFIG_PROJSHARE@#$(PROJSHARE)#' \ + -e 's#@GRASS_PREFIX@#$(RUN_GISBASE)#' \ + -e 's#@GRASS_PYDIR@#etc/python#' \ $< > $@ chmod +x $@ @@ -91,6 +93,7 @@ $(ARCH_DISTDIR)/$(START_UP).tmp: grass.py -e 's#@GRASS_CONFIG_DIR@#$(GRASS_CONFIG_DIR)#' \ -e 's#@LD_LIBRARY_PATH_VAR@#$(LD_LIBRARY_PATH_VAR)#' \ -e 's#@CONFIG_PROJSHARE@#$(PROJSHARE)#' \ + -e 's#@GRASS_PYDIR@#etc/python#' \ $< > $@ $(ETC)/echo$(EXE) $(ETC)/run$(EXE): $(ETC)/%$(EXE): $(OBJDIR)/%.o diff --git a/lib/init/grass.py b/lib/init/grass.py index add393d29e1..11caba10c9d 100755 --- a/lib/init/grass.py +++ b/lib/init/grass.py @@ -71,6 +71,8 @@ # for wxpath _WXPYTHON_BASE = None +GISBASE = None + try: # Python >= 3.11 ENCODING = locale.getencoding() @@ -81,25 +83,7 @@ print("Default locale not found, using UTF-8") # intentionally not translatable # The "@...@" variables are being substituted during build process -# -# TODO: should GISBASE be renamed to something like GRASS_PATH? -# GISBASE marks complete runtime, so no need to get it here when -# setting it up, possible scenario: existing runtime and starting -# GRASS in that, we want to overwrite the settings, not to take it -# possibly same for GRASS_PROJSHARE and others but maybe not -# -# We need to simultaneously make sure that: -# - we get GISBASE from os.environ if it is defined (doesn't this mean that we are -# already inside a GRASS session? If we are, why do we need to run this script -# again???). -# - GISBASE exists as an ENV variable -# -# pmav99: Ugly as hell, but that's what the code before the refactoring was doing. -if "GISBASE" in os.environ and len(os.getenv("GISBASE")) > 0: - GISBASE = os.path.normpath(os.environ["GISBASE"]) -else: - GISBASE = os.path.normpath("@GISBASE_INSTALL_PATH@") - os.environ["GISBASE"] = GISBASE + CMD_NAME = "@START_UP@" GRASS_VERSION = "@GRASS_VERSION_NUMBER@" GRASS_VERSION_MAJOR = "@GRASS_VERSION_MAJOR@" @@ -2103,9 +2087,16 @@ def validate_cmdline(params: Parameters) -> None: def find_grass_python_package() -> None: """Find path to grass package and add it to path""" - if os.path.exists(gpath("etc", "python")): - path_to_package = gpath("etc", "python") - sys.path.append(path_to_package) + GRASS_PREFIX = "@GRASS_PREFIX@" + + if "GRASS_PYDIR" in os.environ and len(os.getenv("GRASS_PYDIR")) > 0: + GRASS_PYDIR = os.path.normpath(os.environ["GRASS_PYDIR"]) + else: + GRASS_PYDIR = os.path.normpath(os.path.join(GRASS_PREFIX, "@GRASS_PYDIR@")) + os.environ["GRASS_PYDIR"] = GRASS_PYDIR + + if os.path.exists(GRASS_PYDIR): + sys.path.append(GRASS_PYDIR) # now we can import stuff from grass package else: # Not translatable because we don't have translations loaded. @@ -2115,6 +2106,12 @@ def find_grass_python_package() -> None: ) raise RuntimeError(msg) + from grass.app import resource_paths + + resource_paths.set_resource_paths() + global GISBASE + GISBASE = resource_paths.GISBASE + def main() -> None: """The main function which does the whole setup and run procedure diff --git a/python/grass/CMakeLists.txt b/python/grass/CMakeLists.txt index eeb0be1deb2..6ed3c8a21a8 100644 --- a/python/grass/CMakeLists.txt +++ b/python/grass/CMakeLists.txt @@ -1,5 +1,4 @@ set(PYDIRS - app benchmark exceptions grassdb @@ -28,12 +27,23 @@ set(PYDIR_GRASS ${GRASS_INSTALL_PYDIR}/grass) foreach(pydir ${PYDIRS}) copy_python_files_in_subdir(${pydir} ${PYDIR_GRASS}) endforeach() +copy_python_files_in_subdir(app ${PYDIR_GRASS} EXCLUDE resource_paths.py) configure_file(__init__.py ${OUTDIR}/${PYDIR_GRASS}/ COPYONLY) configure_file(script/setup.py ${OUTDIR}/${PYDIR_GRASS}/script/setup.py COPYONLY) -set(pydir_targets ${PYDIRS}) +# configure and install resource_paths.py +set(GRASS_PREFIX ${OUTDIR}) +set(GISBASE_INSTALL_PATH ${GISBASE_DIR}) +configure_file(app/resource_paths.py ${OUTDIR}/${PYDIR_GRASS}/app/resource_paths.py @ONLY) + +set(GRASS_PREFIX ${CMAKE_INSTALL_PREFIX}) +configure_file(app/resource_paths.py ${CMAKE_CURRENT_BINARY_DIR}/resource_paths.py @ONLY) +unset(GISBASE_INSTALL_PATH) +unset(GRASS_PREFIX) + +set(pydir_targets ${PYDIRS} app) list(TRANSFORM pydir_targets REPLACE "/" "_") list(TRANSFORM pydir_targets PREPEND "python_") @@ -44,4 +54,7 @@ add_custom_target( set_target_properties(LIB_PYTHON PROPERTIES FOLDER lib) -install(DIRECTORY ${OUTDIR}/${PYDIR_GRASS} DESTINATION ${GRASS_INSTALL_PYDIR}) +install(DIRECTORY ${OUTDIR}/${PYDIR_GRASS} DESTINATION ${GRASS_INSTALL_PYDIR} + PATTERN "*/resource_paths.py" EXCLUDE) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/resource_paths.py + DESTINATION ${GRASS_INSTALL_PYDIR}/grass/app) diff --git a/python/grass/app/Makefile b/python/grass/app/Makefile index eebbcef8f12..9fe5ba95259 100644 --- a/python/grass/app/Makefile +++ b/python/grass/app/Makefile @@ -14,7 +14,20 @@ MODULES = \ PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__) PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__) -default: $(PYFILES) $(PYCFILES) +PYFILES := $(filter-out resource_paths.py,$(PYFILES)) +PYCFILES := $(filter-out resource_paths.pyc,$(PYCFILES)) + +default: $(PYFILES) $(PYCFILES) $(DSTDIR)/resource_paths.py $(ARCH_DISTDIR)/resource_paths.py + +$(DSTDIR)/resource_paths.py: resource_paths.py + rm -f $@ + sed \ + -e 's#@GRASS_PREFIX@#$(RUN_GISBASE)#' \ + -e 's#@GISBASE_INSTALL_PATH@##' \ + $< > $@ + +$(ARCH_DISTDIR)/resource_paths.py: + cp resource_paths.py $@ $(DSTDIR): $(MKDIR) $@ diff --git a/python/grass/app/resource_paths.py b/python/grass/app/resource_paths.py new file mode 100644 index 00000000000..18bc7754949 --- /dev/null +++ b/python/grass/app/resource_paths.py @@ -0,0 +1,31 @@ +""" + +(C) 2025 by Nicklas Larsson 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. + + +This is not a stable part of the API. Use at your own risk. + +The "@...@" variables are being substituted during build process + +""" + +import os + +GRASS_PREFIX = None +GISBASE = None + + +def set_resource_paths(): + global GRASS_PREFIX, GISBASE + + GRASS_PREFIX = "@GRASS_PREFIX@" + + if "GISBASE" in os.environ and len(os.getenv("GISBASE")) > 0: + GISBASE = os.path.normpath(os.environ["GISBASE"]) + else: + GISBASE = os.path.normpath(os.path.join(GRASS_PREFIX, "@GISBASE_INSTALL_PATH@")) + os.environ["GISBASE"] = GISBASE