Skip to content

build: refactor grass.py in preparation for FHS #5933

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

nilason
Copy link
Contributor

@nilason nilason commented Jun 21, 2025

(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

@nilason nilason added this to the 8.5.0 milestone Jun 21, 2025
@github-actions github-actions bot added Python Related code is in Python libraries CMake labels Jun 21, 2025
Comment on lines 27 to 28
if "GISBASE" in os.environ and len(os.getenv("GISBASE")) > 0:
GISBASE = os.path.normpath(os.environ["GISBASE"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the current behavior. I'm little unsure if the setup code should respect or overwrite the current GISBASE.

@nilason
Copy link
Contributor Author

nilason commented Jun 25, 2025

How about to move the content of my added "resource_paths.py" file to the existing "runtime.py" and move all other configuration variables like GRASS_VERSION = "@GRASS_VERSION_NUMBER@". What must remain is GRASS_PYDIR to be able to find the Python module. That way all configuration info will be available through use of Python module only.
Perhaps rework runtime.py to create a (singleton) class named Runtime or something similar?

from grass.app import Runtime
startup_location = os.path.join(Runtime().share_dir, "demolocation")
color_dir = Runtime().color_dir

@wenzeslaus
Copy link
Member

How about to move the content of my added "resource_paths.py" file to the existing "runtime.py" and move all other configuration variables like GRASS_VERSION = "@GRASS_VERSION_NUMBER@".

My idea for runtime.py was to provide interface and functionality for all things runtime, so yes, the runtime.py should hide what is in the background and should make it easy to use. On the other hand, maybe there is a value in having all the generated stuff at one place, so interfacing through runtime.py, but having the @NAME@ pieces in resource_paths.py.

# resource_paths.py
GRASS_VERSION_NUMBER = "@GRASS_VERSION_NUMBER@"
GRASS_COLOR_DIR = "@GRASS_COLOR_DIR@"
# runtime.py
# Actually deal with paths and partial paths, e.g.,
def get_color_dir():
    return os.path.normpath(os.path.join(get_prefix(), "@GRASS_COLOR_DIR@")

What must remain is GRASS_PYDIR to be able to find the Python module.

I'm not sure what you refer to. Remain where? In grass.py? Yes. My question in the review was about having the environmental variable.

That way all configuration info will be available through use of Python module only.

+1

Perhaps rework runtime.py to create a (singleton) class named Runtime or something similar?

from grass.app import Runtime
startup_location = os.path.join(Runtime().share_dir, "demolocation")
color_dir = Runtime().color_dir

Singleton may not be necessary, at least not on the level where a user needs to see it. I recently used module-level __getattr__ which can make it seem as simple variables in a module.

# runtime.py
def __getattr__(name):
    if name == "GRASS_VERSION":
        return "@GRASS_VERSION_NUMBER@"
    if name == "GRASS_COLOR_DIR":
        # Runs only when actually asked for
        return os.path.normpath(os.path.join(GRASS_PREFIX, "@GRASS_COLOR_DIR@")

Or, if we want snake case names on module level and no logic is need for some:

# runtime.py
version = "@GRASS_VERSION_NUMBER@"
def __getattr__(name):
    if name == "color_dir":
        # Runs only when actually asked for
        return os.path.normpath(os.path.join(prefix, "@GRASS_COLOR_DIR@")

Either way:

from grass.app.runtime import version
import grass.app.runtime as runtime
runtime.color_dir

However, a function returning an object which may or may not be singleton is possible too.

# runtime.py
class RuntimePaths:
    @property
    def color_dir(self):
        if self._color_dir is None:
            self._color_dir = os.path.normpath(os.path.join(self.prefix, "@GRASS_COLOR_DIR@")
        return self._color_dir

_runtime_paths = RuntimePaths()
def runtime_paths():
    return _runtime_paths

User code:

from grass.app import runtime_paths
startup_location = os.path.join(runtime_paths().share_dir, "demolocation")
color_dir = runtime_paths().color_dir

Or even through an attribute rather than a function call:

_runtime_paths = None
def __getattr__(name):
    if name == "runtime_paths":
        if _runtime_paths is None:
            _runtime_paths = RuntimePaths()
        return _runtime_paths
from grass.app import runtime_paths
startup_location = os.path.join(runtime_paths.share_dir, "demolocation")
color_dir = runtime_paths.color_dir

@nilason
Copy link
Contributor Author

nilason commented Jun 25, 2025

Thanks for the input! I'll see what I can come up with.

@nilason nilason marked this pull request as draft June 25, 2025 15:49
@nilason
Copy link
Contributor Author

nilason commented Jun 26, 2025

Currently GISBASE is initialised in grass.py in this way:

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

The new path env. variables should also be set up in similar way, if we are to keep current behaviour. E.g., if the env. variable is set, then return the value of that, or set it with a configured value. The functions in runtime.py and setup.py also reflects the factor of environment. To achieve that using a simple class may perhaps be an approach:

# runtime.py
class RuntimePaths:
    def __init__(self, env=os.environ):
        self.env = env

    def __getattr__(self, name):
        if name == "grass_version":
            return runtime_paths.GRASS_VERSION_NUMBER


# user code
from runtime import RuntimePaths

gisbase = RuntimePaths(env=my_env).gisbase
# or:
runtime_paths = RuntimePaths()  # using default "os.environ"
version = runtime_paths.grass_version
share_dir = runtime_paths.share_dir

@nilason
Copy link
Contributor Author

nilason commented Jun 27, 2025

Just pushed update with the alternative approach I mentioned before. It is not completely ready yet, especially the cmake build needs some more work. But you can see if this could be an acceptable and/or promising solution.

Copy link
Member

@wenzeslaus wenzeslaus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, when the environment is involved, even this should accept env parameter.

Comment on lines 28 to 33
version = grass.app.resource_paths.GRASS_VERSION
version_major = grass.app.resource_paths.GRASS_VERSION_MAJOR
version_minor = grass.app.resource_paths.GRASS_VERSION_MINOR
ld_library_path_var = grass.app.resource_paths.LD_LIBRARY_PATH_VAR
grass_exe_name = grass.app.resource_paths.GRASS_EXE_NAME
grass_version_git = grass.app.resource_paths.GRASS_VERSION_GIT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are defined as "global constants" because they are defined during the compilation as opposed to the other ones which may be influenced by environment variables, or?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, at least this is the current behaviour. These are "hardcoded", get-only variables. The "RuntimePaths" also sets environment variables (all derived from the use of GISBASE).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative approach would be to combine the two, but rename the class to e.g., "RuntimeConfigs".

runtime_config = RuntimeConfig()
GISBASE = runtime_config.gisbase
GRASS_VERSION_GIT = runtime_config.grass_version_git

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in "runtime and config"? That's not obvious to me from the name. Single place sounds good though, regardless of the name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integrated all variables as properties of RuntimePaths.

Copy link
Contributor Author

@nilason nilason Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeSettings could be an alternative name...

runtime_settings = RuntimeSettings()
GISBASE = runtime_settings.gisbase
GRASS_VERSION_GIT = runtime_settings.grass_version_git

@nilason nilason marked this pull request as ready for review June 30, 2025 18:37
(This is extracted from OSGeo#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
@nilason
Copy link
Contributor Author

nilason commented Jul 3, 2025

Any more suggestions, objections. #5630 builds upon this and that one is starting to shape up, so it would be good to move on.

@echoix
Copy link
Member

echoix commented Jul 4, 2025

Are some changes in the mswindows/ folder needed, regarding the changes of @var@ substitutions? There are some instances in that folder.

Copy link
Member

@wenzeslaus wenzeslaus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the Python part and the CMake changes look reasonable. We can merge this as is. I have just some minor comments which you can ignore.


# The "@...@" variables are being substituted during build process

if "GRASS_PYDIR" in os.environ and len(os.getenv("GRASS_PYDIR")) > 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure when len(os.getenv("GRASS_PYDIR")) > 0 is different from os.getenv("GRASS_PYDIR"), but if it is the same, Ruff or something will tell us.

@@ -17,12 +17,73 @@
import subprocess
import sys

from . import resource_paths as res_paths
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should work without an alias.

Suggested change
from . import resource_paths as res_paths
from . import resource_paths

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CMake libraries Python Related code is in Python
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants