diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index 96b5a6bb9aa..a5e8a334291 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -64,7 +64,6 @@ jobs: libsystre libtre libwinpthread-git - openblas pcre pkgconf toolchain diff --git a/mswindows/osgeo4w/build_osgeo4w.sh b/mswindows/osgeo4w/build_osgeo4w.sh index f1549f0e637..71bf096c484 100755 --- a/mswindows/osgeo4w/build_osgeo4w.sh +++ b/mswindows/osgeo4w/build_osgeo4w.sh @@ -37,7 +37,7 @@ CXXFLAGS="$CXXFLAGS -pipe" \ --includedir=${OSGEO4W_ROOT_MSYS}/include \ --libexecdir=${OSGEO4W_ROOT_MSYS}/bin \ --prefix=${OSGEO4W_ROOT_MSYS}/apps/grass \ - --with-blas \ + --without-blas \ --with-bzlib \ --with-cairo \ --with-cairo-includes=${OSGEO4W_ROOT_MSYS}/include/cairo \ diff --git a/python/grass/pygrass/messages/__init__.py b/python/grass/pygrass/messages/__init__.py index 2d06c2c9a31..cf20e259e43 100644 --- a/python/grass/pygrass/messages/__init__.py +++ b/python/grass/pygrass/messages/__init__.py @@ -15,13 +15,15 @@ from __future__ import annotations import sys -from multiprocessing import Lock, Pipe, Process +from multiprocessing import Lock, Pipe from typing import TYPE_CHECKING, Literal, NoReturn import grass.lib.gis as libgis from grass.exceptions import FatalError +from grass.script.utils import get_fork_context if TYPE_CHECKING: + from multiprocessing import Process from multiprocessing.connection import Connection from multiprocessing.context import _LockLike @@ -181,7 +183,9 @@ def __init__(self, raise_on_error: bool = False) -> None: self.raise_on_error = raise_on_error self.client_conn, self.server_conn = Pipe() self.lock = Lock() - self.server = Process(target=message_server, args=(self.lock, self.server_conn)) + self.server = get_fork_context().Process( + target=message_server, args=(self.lock, self.server_conn) + ) self.server.daemon = True self.server.start() @@ -189,7 +193,9 @@ def start_server(self) -> None: """Start the messenger server and open the pipe""" self.client_conn, self.server_conn = Pipe() self.lock = Lock() - self.server = Process(target=message_server, args=(self.lock, self.server_conn)) + self.server = get_fork_context().Process( + target=message_server, args=(self.lock, self.server_conn) + ) self.server.daemon = True self.server.start() diff --git a/python/grass/pygrass/modules/interface/module.py b/python/grass/pygrass/modules/interface/module.py index def9638217e..488daeb41a8 100644 --- a/python/grass/pygrass/modules/interface/module.py +++ b/python/grass/pygrass/modules/interface/module.py @@ -912,7 +912,7 @@ class MultiModule: >>> from grass.pygrass.modules import Module >>> from grass.pygrass.modules import MultiModule - >>> from multiprocessing import Process + >>> from multiprocessing.process import BaseProcess >>> import copy Synchronous module run @@ -950,7 +950,7 @@ class MultiModule: ... sync=False, ... ) >>> t = mm.run() - >>> isinstance(t, Process) + >>> isinstance(t, BaseProcess) True >>> m_list = mm.wait() >>> m_list[0].returncode @@ -976,7 +976,7 @@ class MultiModule: >>> str(mm) 'g.region format=plain -p ; g.region format=plain -p ; g.region format=plain -p ; g.region format=plain -p ; g.region format=plain -p' >>> t = mm.run() - >>> isinstance(t, Process) + >>> isinstance(t, BaseProcess) True >>> m_list = mm.wait() >>> m_list[0].returncode diff --git a/python/grass/pygrass/rpc/base.py b/python/grass/pygrass/rpc/base.py index 964d2a22b04..163b9f89288 100644 --- a/python/grass/pygrass/rpc/base.py +++ b/python/grass/pygrass/rpc/base.py @@ -14,10 +14,11 @@ import sys import threading import time -from multiprocessing import Lock, Pipe, Process +from multiprocessing import Lock, Pipe from typing import TYPE_CHECKING, NoReturn from grass.exceptions import FatalError +from grass.script.utils import get_fork_context if TYPE_CHECKING: from multiprocessing.connection import Connection @@ -142,7 +143,9 @@ def start_server(self): self.client_conn, self.server_conn = Pipe(True) self.lock = Lock() - self.server = Process(target=dummy_server, args=(self.lock, self.server_conn)) + self.server = get_fork_context().Process( + target=dummy_server, args=(self.lock, self.server_conn) + ) self.server.daemon = True self.server.start() diff --git a/python/grass/script/__init__.py b/python/grass/script/__init__.py index d7009380347..53b3a0b32c6 100644 --- a/python/grass/script/__init__.py +++ b/python/grass/script/__init__.py @@ -93,6 +93,7 @@ diff_files, encode, float_or_dms, + get_fork_context, get_lib_path, get_num_suffix, legalize_vector_name, @@ -159,6 +160,7 @@ "float_or_dms", "get_capture_stderr", "get_commands", + "get_fork_context", "get_lib_path", "get_num_suffix", "get_raise_on_error", diff --git a/python/grass/script/utils.py b/python/grass/script/utils.py index c9e915a0de6..522c96a637c 100644 --- a/python/grass/script/utils.py +++ b/python/grass/script/utils.py @@ -27,6 +27,7 @@ import uuid import random import string +import sys from pathlib import Path from typing import TYPE_CHECKING, AnyStr, TypeVar, cast, overload @@ -125,6 +126,24 @@ def resolve_nprocs(nprocs: int | str) -> int: return max(1, available + n) +def get_fork_context(): + """Return a suitable :mod:`multiprocessing` fork context. + + The GRASS Python APIs does not yet support `spawn` and `forkserver` + start-methods for multiprocessing, which became new default in + Python 3.14. This function provides a temporary workaround that + switches back to `fork` on Linux until the new default methods are + supported. + The function should be removed as soon as the GRASS Python APIs support + the new default start-methods. + """ + import multiprocessing + + if sys.platform != "darwin" and "fork" in multiprocessing.get_all_start_methods(): + return multiprocessing.get_context("fork") + return multiprocessing.get_context() + + def diff_files( filename_a: FileDescriptorOrPath, filename_b: FileDescriptorOrPath ) -> list[str]: diff --git a/python/grass/temporal/c_libraries_interface.py b/python/grass/temporal/c_libraries_interface.py index ceb0eea72ab..65b5879910c 100644 --- a/python/grass/temporal/c_libraries_interface.py +++ b/python/grass/temporal/c_libraries_interface.py @@ -14,7 +14,7 @@ import sys from ctypes import CFUNCTYPE, POINTER, byref, c_int, c_void_p, cast from datetime import datetime -from multiprocessing import Lock, Pipe, Process +from multiprocessing import Lock, Pipe from typing import TYPE_CHECKING, Any, Literal import grass.lib.date as libdate @@ -1471,7 +1471,7 @@ def __init__(self) -> None: def start_server(self) -> None: self.client_conn, self.server_conn = Pipe(True) self.lock = Lock() - self.server = Process( + self.server = gs.get_fork_context().Process( target=c_library_server, args=(self.lock, self.server_conn) ) self.server.daemon = True diff --git a/python/grass/temporal/extract.py b/python/grass/temporal/extract.py index 2d7bff85c54..dbfedf6c33e 100644 --- a/python/grass/temporal/extract.py +++ b/python/grass/temporal/extract.py @@ -10,7 +10,6 @@ import re import sys -from multiprocessing import Process import grass.script as gs from grass.exceptions import CalledModuleError @@ -207,6 +206,7 @@ def extract_dataset( # Run the mapcalc expression if expression: + mp_context = gs.get_fork_context() proc_count = 0 proc_list = [] @@ -262,15 +262,15 @@ def extract_dataset( # Add process to the process list if type == "raster": msgr.verbose(_('Applying r.mapcalc expression: "%s"') % expr) - proc_list.append(Process(target=run_mapcalc2d, args=(expr,))) + proc_list.append(mp_context.Process(target=run_mapcalc2d, args=(expr,))) elif type == "raster3d": msgr.verbose(_('Applying r3.mapcalc expression: "%s"') % expr) - proc_list.append(Process(target=run_mapcalc3d, args=(expr,))) + proc_list.append(mp_context.Process(target=run_mapcalc3d, args=(expr,))) elif type == "vector": msgr.verbose(_('Applying v.extract where statement: "%s"') % expression) if row["layer"]: proc_list.append( - Process( + mp_context.Process( target=run_vector_extraction, args=( row["name"] + "@" + row["mapset"], @@ -283,7 +283,7 @@ def extract_dataset( ) else: proc_list.append( - Process( + mp_context.Process( target=run_vector_extraction, args=( row["name"] + "@" + row["mapset"], diff --git a/python/grass/temporal/mapcalc.py b/python/grass/temporal/mapcalc.py index ed3ca9cbfef..d3695e92d64 100644 --- a/python/grass/temporal/mapcalc.py +++ b/python/grass/temporal/mapcalc.py @@ -10,7 +10,6 @@ import copy import sys from datetime import datetime -from multiprocessing import Process import grass.script as gs from grass.exceptions import CalledModuleError @@ -234,6 +233,7 @@ def dataset_mapcalculator( num = len(map_matrix[0]) # Parallel processing + mp_context = gs.get_fork_context() proc_list = [] proc_count = 0 @@ -309,9 +309,13 @@ def dataset_mapcalculator( # Start the parallel r.mapcalc computation if type == "raster": - proc_list.append(Process(target=_run_mapcalc2d, args=(expr,))) + proc_list.append( + mp_context.Process(target=_run_mapcalc2d, args=(expr,)) + ) else: - proc_list.append(Process(target=_run_mapcalc3d, args=(expr,))) + proc_list.append( + mp_context.Process(target=_run_mapcalc3d, args=(expr,)) + ) proc_list[proc_count].start() proc_count += 1