From e892a856d311e5d19d749a2f05041b3dba485cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 10:19:30 -0300 Subject: [PATCH 01/10] interfaces/gap: remove __make_workspace, not used since 6be205b03c9 Note that `self.__make_workspace` is true only when `use_workspace_cache` is false, hence `self.__use_workspace_cache and self.__make_workspace` is never true. --- src/sage/interfaces/gap.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sage/interfaces/gap.py b/src/sage/interfaces/gap.py index 4b9e619ae3d..05d645d56c3 100644 --- a/src/sage/interfaces/gap.py +++ b/src/sage/interfaces/gap.py @@ -1073,7 +1073,7 @@ def __init__(self, max_workspace_size=None, True """ self.__use_workspace_cache = use_workspace_cache - cmd, self.__make_workspace = gap_command(use_workspace_cache, server is None) + cmd, _ = gap_command(use_workspace_cache, server is None) # -b: suppress banner # -p: enable "package output mode"; this confusingly named option # causes GAP to output special control characters that are normally @@ -1197,12 +1197,9 @@ def _start(self): gap_reset_workspace(verbose=False) Expect._start(self, "Failed to start GAP.") self._session_number = n - self.__make_workspace = False else: raise - if self.__use_workspace_cache and self.__make_workspace: - self.save_workspace() # Now, as self._expect exists, we can compile some useful pattern: self._compiled_full_pattern = self._expect.compile_pattern_list([ r'@p\d+\.', '@@', '@[A-Z]', r'@[123456!"#$%&][^+]*\+', From f836f8e31dcb0a79a261ff2d5ae612875b2b0ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 10:22:33 -0300 Subject: [PATCH 02/10] interfaces/gap: cleanup gap_command() --- src/sage/interfaces/gap.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/sage/interfaces/gap.py b/src/sage/interfaces/gap.py index 05d645d56c3..80f1e6c8d6a 100644 --- a/src/sage/interfaces/gap.py +++ b/src/sage/interfaces/gap.py @@ -232,17 +232,6 @@ gap_cmd = 'prctl --unaligned=silent ' + gap_cmd -def gap_command(use_workspace_cache=True, local=True): - if use_workspace_cache: - if local: - return "%s -L %s" % (gap_cmd, WORKSPACE), False - else: - # TO DO: Use remote workspace - return gap_cmd, False - else: - return gap_cmd, True - - # ########### Classes with methods for both the GAP3 and GAP4 interface class Gap_generic(ExtraTabCompletion, Expect): @@ -1073,7 +1062,10 @@ def __init__(self, max_workspace_size=None, True """ self.__use_workspace_cache = use_workspace_cache - cmd, _ = gap_command(use_workspace_cache, server is None) + cmd = gap_cmd + # -L: restore a saved workspace (TO DO: Use remote workspace) + if use_workspace_cache and server is None: + cmd += f" -L {WORKSPACE}" # -b: suppress banner # -p: enable "package output mode"; this confusingly named option # causes GAP to output special control characters that are normally @@ -1805,8 +1797,8 @@ def gap_console(): TESTS:: sage: import subprocess as sp - sage: from sage.interfaces.gap import gap_command - sage: cmd = 'echo "quit;" | ' + gap_command(use_workspace_cache=False)[0] + sage: from sage.interfaces.gap import gap_cmd + sage: cmd = 'echo "quit;" | ' + gap_cmd sage: gap_startup = sp.check_output(cmd, shell=True, ....: stderr=sp.STDOUT, ....: encoding='latin1') @@ -1820,6 +1812,6 @@ def gap_console(): from sage.repl.rich_output.display_manager import get_display_manager if not get_display_manager().is_in_terminal(): raise RuntimeError('Can use the console only in the terminal. Try %%gap magics instead.') - cmd, _ = gap_command(use_workspace_cache=False) + cmd = gap_cmd cmd += ' ' + os.path.join(SAGE_EXTCODE, 'gap', 'console.g') os.system(cmd) From bf0d4ae8dbc98e0b733c48c1f8f209d7b6ea0708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 10:02:35 -0300 Subject: [PATCH 03/10] interfaces/gap: replace global WORKSPACE by Gap() attribute --- src/sage/interfaces/gap.py | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/sage/interfaces/gap.py b/src/sage/interfaces/gap.py index 80f1e6c8d6a..959f5a8f71d 100644 --- a/src/sage/interfaces/gap.py +++ b/src/sage/interfaces/gap.py @@ -193,7 +193,6 @@ # **************************************************************************** from .expect import Expect, ExpectElement, FunctionElement, ExpectFunction -from .gap_workspace import gap_workspace_file, prepare_workspace_dir from sage.cpython.string import bytes_to_str from sage.env import SAGE_EXTCODE, SAGE_GAP_COMMAND, SAGE_GAP_MEMORY, GAP_ROOT_PATHS from sage.misc.misc import is_in_string @@ -212,8 +211,6 @@ import string import warnings -WORKSPACE = gap_workspace_file() - first_try = True if SAGE_GAP_COMMAND is None: @@ -1050,6 +1047,7 @@ class Gap(Gap_generic): def __init__(self, max_workspace_size=None, maxread=None, script_subdirectory=None, use_workspace_cache=True, + workspace_file=None, server=None, server_tmpdir=None, logfile=None, @@ -1061,11 +1059,14 @@ def __init__(self, max_workspace_size=None, sage: gap == loads(dumps(gap)) True """ + from .gap_workspace import gap_workspace_file + self.__workspace_file = workspace_file or gap_workspace_file() self.__use_workspace_cache = use_workspace_cache + cmd = gap_cmd # -L: restore a saved workspace (TO DO: Use remote workspace) if use_workspace_cache and server is None: - cmd += f" -L {WORKSPACE}" + cmd += f" -L {self.__workspace_file}" # -b: suppress banner # -p: enable "package output mode"; this confusingly named option # causes GAP to output special control characters that are normally @@ -1169,14 +1170,14 @@ def _start(self): # workspace, i.e., if the gap script is more recent # than the saved workspace, which signals that gap has # been upgraded. - if os.path.getmtime(WORKSPACE) < timestamp(): + if os.path.getmtime(self.__workspace_file) < timestamp(): raise OSError("GAP workspace too old") # Set the modification time of the workspace to the # current time. This ensures the workspace doesn't # get deleted too soon by gap_reset_workspace(). - os.utime(WORKSPACE, None) + os.utime(self.__workspace_file, None) except OSError: - gap_reset_workspace(verbose=False) + gap_reset_workspace(workspace_file=self.__workspace_file) global first_try n = self._session_number @@ -1186,7 +1187,7 @@ def _start(self): if self.__use_workspace_cache and first_try: first_try = False self.quit() - gap_reset_workspace(verbose=False) + gap_reset_workspace(workspace_file=self.__workspace_file) Expect._start(self, "Failed to start GAP.") self._session_number = n else: @@ -1250,15 +1251,13 @@ def save_workspace(self): to the GAP workspace file contains more than 82 characters) is fixed:: - sage: ORIGINAL_WORKSPACE = sage.interfaces.gap.WORKSPACE sage: import tempfile sage: with tempfile.NamedTemporaryFile(prefix="0"*80) as f: # long time (4s on sage.math, 2013) - ....: sage.interfaces.gap.WORKSPACE = f.name - ....: gap = Gap() + ....: gap = Gap(workspace_file=f.name) ....: gap('3+2') 5 - sage: sage.interfaces.gap.WORKSPACE = ORIGINAL_WORKSPACE """ + from .gap_workspace import prepare_workspace_dir prepare_workspace_dir() # According to the GAP Reference Manual, @@ -1267,7 +1266,7 @@ def save_workspace(self): # be included in the body of a loop or function, or called from a # break loop. from sage.misc.temporary_file import atomic_write - with atomic_write(WORKSPACE) as f: + with atomic_write(self.__workspace_file) as f: f.close() self.eval('SaveWorkspace("%s");' % (f.name), allow_use_file=False) @@ -1473,7 +1472,9 @@ def _tab_completion(self): return [n for n in names if n[0] in string.ascii_letters] -def gap_reset_workspace(max_workspace_size=None, verbose=False): +def gap_reset_workspace(max_workspace_size=None, + verbose=False, + workspace_file=None): r""" Call this to completely reset the GAP workspace, which is used by default when Sage first starts GAP. @@ -1492,26 +1493,24 @@ def gap_reset_workspace(max_workspace_size=None, verbose=False): ``first_try=True`` to ensure that the new workspace is created:: sage: # long time - sage: ORIGINAL_WORKSPACE = sage.interfaces.gap.WORKSPACE sage: saved_first_try = sage.interfaces.gap.first_try sage: sage.interfaces.gap.first_try = True - sage: sage.interfaces.gap.WORKSPACE = tmp_filename() + sage: fname = tmp_filename() sage: from multiprocessing import Process sage: import time - sage: gap = Gap() # reset GAP session + sage: gap = Gap(workspace_file=fname) # reset GAP session sage: P = [Process(target=gap, args=("14242",)) for i in range(4)] sage: for p in P: # indirect doctest ....: p.start() ....: time.sleep(float(0.2)) sage: for p in P: ....: p.join() - sage: os.unlink(sage.interfaces.gap.WORKSPACE) - sage: sage.interfaces.gap.WORKSPACE = ORIGINAL_WORKSPACE + sage: os.unlink(fname) sage: sage.interfaces.gap.first_try = saved_first_try """ # The use_workspace_cache=False causes a new workspace to # be created, and we save it immediately thereafter. - g = Gap(use_workspace_cache=False, max_workspace_size=None) + g = Gap(use_workspace_cache=False, workspace_file=workspace_file) g.save_workspace() g.quit() From 1b283baccbe434b6cd6aac79abd3ee4b6399e820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 12:44:12 -0300 Subject: [PATCH 04/10] interfaces/gap: don't use GAP_ROOT_PATHS Since the `gap` binary itself knows the paths, there's no need. In fact, configure got the paths from running `gap`. --- src/sage/interfaces/gap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/interfaces/gap.py b/src/sage/interfaces/gap.py index 959f5a8f71d..41f5b70d730 100644 --- a/src/sage/interfaces/gap.py +++ b/src/sage/interfaces/gap.py @@ -194,7 +194,7 @@ from .expect import Expect, ExpectElement, FunctionElement, ExpectFunction from sage.cpython.string import bytes_to_str -from sage.env import SAGE_EXTCODE, SAGE_GAP_COMMAND, SAGE_GAP_MEMORY, GAP_ROOT_PATHS +from sage.env import SAGE_EXTCODE, SAGE_GAP_COMMAND, SAGE_GAP_MEMORY from sage.misc.misc import is_in_string from sage.misc.cachefunc import cached_method from sage.misc.instancedoc import instancedoc @@ -217,7 +217,7 @@ # Passing -A allows us to use a minimal GAP installation without # producing errors at start-up. The files sage.g and sage.gaprc are # used to load any additional packages that may be available. - gap_cmd = f'gap -A -l "{GAP_ROOT_PATHS}"' + gap_cmd = f'gap -r -A' if SAGE_GAP_MEMORY is not None: gap_cmd += " -s " + SAGE_GAP_MEMORY + " -o " + SAGE_GAP_MEMORY else: From 94cc1ff6a0ab0bfb1ff1a3bb48fbd84e0da63df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 12:50:48 -0300 Subject: [PATCH 05/10] gap: fast implementation of KERNEL_INFO() --- src/sage/interfaces/gap.py | 39 +++++++++++++++++++++++++++ src/sage/interfaces/gap_kernel_info.g | 22 +++++++++++++++ src/setup.cfg.m4 | 1 + 3 files changed, 62 insertions(+) create mode 100644 src/sage/interfaces/gap_kernel_info.g diff --git a/src/sage/interfaces/gap.py b/src/sage/interfaces/gap.py index 41f5b70d730..4306b452ecf 100644 --- a/src/sage/interfaces/gap.py +++ b/src/sage/interfaces/gap.py @@ -229,6 +229,45 @@ gap_cmd = 'prctl --unaligned=silent ' + gap_cmd +class KERNEL_INFO(dict): + """ + doc + """ + + # singleton pattern + __instance = None + + def __new__(cls): + if cls.__instance is None: + cls.__instance = super().__new__(cls) + + return cls.__instance + + def __init__(self): + + if self.__dict__ is self: + # already initialized + return + + from sage.misc.timing import cputime, walltime + t = cputime() + w = walltime() + + # access keys as object attributes + self.__dict__ = self + + from importlib.resources import files + import subprocess + + gap_kernel_info = files(__package__).joinpath('gap_kernel_info.g') + + for line in subprocess.getoutput( + f"{gap_cmd} --systemfile {gap_kernel_info}" + ).split('\n'): + var, value = line.split("=") + self[var] = value + + # ########### Classes with methods for both the GAP3 and GAP4 interface class Gap_generic(ExtraTabCompletion, Expect): diff --git a/src/sage/interfaces/gap_kernel_info.g b/src/sage/interfaces/gap_kernel_info.g new file mode 100644 index 00000000000..82cb8f02c20 --- /dev/null +++ b/src/sage/interfaces/gap_kernel_info.g @@ -0,0 +1,22 @@ +# Prints gap kernel info without initializing gap. +# +# Usage: +# +# $ gap -r --systemfile gap_kernel_info.g +# KERNEL_VERSION=4.12.2 +# GAP_ARCHITECTURE=x86_64-unknown-linux-gnu-default64-kv8 +# GAP_ROOT_PATHS=/usr/lib64/gap/;/usr/share/gap/ + +KernelInfo := KERNEL_INFO(); + +PRINT_TO( "*stdout*", "KERNEL_VERSION=", KernelInfo.KERNEL_VERSION, "\n"); +PRINT_TO( "*stdout*", "GAP_ARCHITECTURE=", KernelInfo.GAP_ARCHITECTURE, "\n"); + +p := KernelInfo.GAP_ROOT_PATHS; +PRINT_TO( "*stdout*", "GAP_ROOT_PATHS=", p[1] ); +for i in [2 .. LENGTH(p)] do + PRINT_TO( "*stdout*", ";", p[i]); +od; +PRINT_TO( "*stdout*", "\n"); + +QuitGap(); diff --git a/src/setup.cfg.m4 b/src/setup.cfg.m4 index e2a3330518d..05150476e83 100644 --- a/src/setup.cfg.m4 +++ b/src/setup.cfg.m4 @@ -97,6 +97,7 @@ sage.libs.gap = sage.gaprc sage.interfaces = + gap_kernel_info.g sage-maxima.lisp sage.doctest = From 01850d037f5a583257c26776d8243d40dc441c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 13:18:32 -0300 Subject: [PATCH 06/10] interfaces/gap_workspace: use KERNEL_INFO() to create filename --- src/sage/interfaces/gap_workspace.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/sage/interfaces/gap_workspace.py b/src/sage/interfaces/gap_workspace.py index a2c97260077..8ba43868f76 100644 --- a/src/sage/interfaces/gap_workspace.py +++ b/src/sage/interfaces/gap_workspace.py @@ -17,7 +17,7 @@ import time import hashlib import subprocess -from sage.env import DOT_SAGE, HOSTNAME, GAP_ROOT_PATHS +from sage.env import DOT_SAGE, HOSTNAME def gap_workspace_file(system="gap", name="workspace", dir=None): @@ -60,15 +60,10 @@ def gap_workspace_file(system="gap", name="workspace", dir=None): if dir is None: dir = os.path.join(DOT_SAGE, 'gap') - data = f'{GAP_ROOT_PATHS}' - for path in GAP_ROOT_PATHS.split(";"): - if not path: - # If GAP_ROOT_PATHS begins or ends with a semicolon, - # we'll get one empty path. - continue - sysinfo = os.path.join(path, "sysinfo.gap") - if os.path.exists(sysinfo): - data += subprocess.getoutput(f'. "{sysinfo}" && echo ":$GAP_VERSION:$GAParch"') + from sage.interfaces.gap import KERNEL_INFO + data = f'{KERNEL_INFO().GAP_ROOT_PATHS}' + data += f':{KERNEL_INFO().KERNEL_VERSION}' + data += f':{KERNEL_INFO().GAP_ARCHITECTURE}' h = hashlib.sha1(data.encode('utf-8')).hexdigest() return os.path.join(dir, f'{system}-{name}-{HOSTNAME}-{h}') From f8202afab2c3e69b46a6af3a1db7298a69a97fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 13:20:08 -0300 Subject: [PATCH 07/10] libs/gap/saved_workspace: use KERNEL_INFO() to compute gap timestamp --- src/sage/libs/gap/saved_workspace.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sage/libs/gap/saved_workspace.py b/src/sage/libs/gap/saved_workspace.py index fdaf18f4644..da751807eb2 100644 --- a/src/sage/libs/gap/saved_workspace.py +++ b/src/sage/libs/gap/saved_workspace.py @@ -8,8 +8,6 @@ import os import glob -from sage.env import GAP_ROOT_PATHS -from sage.interfaces.gap_workspace import gap_workspace_file def timestamp(): @@ -29,10 +27,11 @@ def timestamp(): sage: type(timestamp()) <... 'float'> """ + from sage.interfaces.gap import KERNEL_INFO libgap_dir = os.path.dirname(__file__) libgap_files = glob.glob(os.path.join(libgap_dir, '*')) gap_packages = [] - for d in GAP_ROOT_PATHS.split(";"): + for d in KERNEL_INFO().GAP_ROOT_PATHS.split(";"): if d: # If GAP_ROOT_PATHS begins or ends with a semicolon, # we'll get one empty d. @@ -71,6 +70,7 @@ def workspace(name='workspace'): sage: isinstance(up_to_date, bool) True """ + from sage.interfaces.gap_workspace import gap_workspace_file workspace = gap_workspace_file("libgap", name) try: workspace_mtime = os.path.getmtime(workspace) From f4ebc6b4eadf30c18b044ece206ea980200ea960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 13:20:46 -0300 Subject: [PATCH 08/10] libgap: get GAP_ROOT_PATHS from KERNEL_INFO() --- src/sage/libs/gap/util.pyx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sage/libs/gap/util.pyx b/src/sage/libs/gap/util.pyx index 8685dc08fa5..5102bcaabf1 100644 --- a/src/sage/libs/gap/util.pyx +++ b/src/sage/libs/gap/util.pyx @@ -28,6 +28,7 @@ from sage.libs.gap.gap_includes cimport * from sage.libs.gap.element cimport * from sage.cpython.string import FS_ENCODING from sage.cpython.string cimport str_to_bytes, char_to_str +from sage.interfaces.gap import KERNEL_INFO from sage.interfaces.gap_workspace import prepare_workspace_dir @@ -219,7 +220,9 @@ cdef initialize() noexcept: argv[0] = "sage" argv[1] = "-A" argv[2] = "-l" - s = str_to_bytes(sage.env.GAP_ROOT_PATHS, FS_ENCODING, "surrogateescape") + s = str_to_bytes(KERNEL_INFO().GAP_ROOT_PATHS, + FS_ENCODING, + "surrogateescape") argv[3] = s argv[4] = "-m" From de8cd2b7168c4a6f735fefa0b0ce6259e3d50fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 14 Feb 2024 23:13:58 -0300 Subject: [PATCH 09/10] gap: use GAP_BIN instead of hardcoded 'gap' --- src/sage/env.py | 1 + src/sage/interfaces/gap.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sage/env.py b/src/sage/env.py index cadcb820c5a..43f7a3805f2 100644 --- a/src/sage/env.py +++ b/src/sage/env.py @@ -236,6 +236,7 @@ def var(key: str, *fallbacks: Optional[str], force: bool = False) -> Optional[st NTL_LIBDIR = var("NTL_LIBDIR") LIE_INFO_DIR = var("LIE_INFO_DIR", join(SAGE_LOCAL, "lib", "LiE")) SINGULAR_BIN = var("SINGULAR_BIN") or "Singular" +GAP_BIN = var("GAP_BIN", "gap") # OpenMP OPENMP_CFLAGS = var("OPENMP_CFLAGS", "") diff --git a/src/sage/interfaces/gap.py b/src/sage/interfaces/gap.py index 4306b452ecf..2afad984b35 100644 --- a/src/sage/interfaces/gap.py +++ b/src/sage/interfaces/gap.py @@ -194,7 +194,7 @@ from .expect import Expect, ExpectElement, FunctionElement, ExpectFunction from sage.cpython.string import bytes_to_str -from sage.env import SAGE_EXTCODE, SAGE_GAP_COMMAND, SAGE_GAP_MEMORY +from sage.env import GAP_BIN, SAGE_EXTCODE, SAGE_GAP_COMMAND, SAGE_GAP_MEMORY from sage.misc.misc import is_in_string from sage.misc.cachefunc import cached_method from sage.misc.instancedoc import instancedoc @@ -217,7 +217,7 @@ # Passing -A allows us to use a minimal GAP installation without # producing errors at start-up. The files sage.g and sage.gaprc are # used to load any additional packages that may be available. - gap_cmd = f'gap -r -A' + gap_cmd = f'{GAP_BIN} -r -A' if SAGE_GAP_MEMORY is not None: gap_cmd += " -s " + SAGE_GAP_MEMORY + " -o " + SAGE_GAP_MEMORY else: From edcc08276c3305cb453b75510e282b22ef0cb633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Mon, 19 Feb 2024 09:56:19 -0300 Subject: [PATCH 10/10] sage.interfaces: make a package to avoid breakage in python 3.9 --- src/sage/interfaces/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/sage/interfaces/__init__.py diff --git a/src/sage/interfaces/__init__.py b/src/sage/interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d