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

GAP: make GAP_ROOT_PATHS configuration unnecessary #37344

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions src/sage/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand Down
Empty file added src/sage/interfaces/__init__.py
Empty file.
101 changes: 64 additions & 37 deletions src/sage/interfaces/gap.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,8 @@
# ****************************************************************************

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.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
Expand All @@ -212,15 +211,13 @@
import string
import warnings

WORKSPACE = gap_workspace_file()

first_try = True

if SAGE_GAP_COMMAND is None:
# 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_BIN} -r -A'
if SAGE_GAP_MEMORY is not None:
gap_cmd += " -s " + SAGE_GAP_MEMORY + " -o " + SAGE_GAP_MEMORY
else:
Expand All @@ -232,15 +229,43 @@
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
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
Expand Down Expand Up @@ -1061,6 +1086,7 @@
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,
Expand All @@ -1072,8 +1098,14 @@
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, self.__make_workspace = 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 {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
Expand Down Expand Up @@ -1177,14 +1209,14 @@
# 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
Expand All @@ -1194,15 +1226,12 @@
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
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!"#$%&][^+]*\+',
Expand Down Expand Up @@ -1261,15 +1290,13 @@
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,
Expand All @@ -1278,7 +1305,7 @@
# 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)

Expand Down Expand Up @@ -1484,7 +1511,9 @@
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.
Expand All @@ -1503,26 +1532,24 @@
``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()

Expand Down Expand Up @@ -1808,8 +1835,8 @@
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')
Expand All @@ -1823,6 +1850,6 @@
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

Check warning on line 1853 in src/sage/interfaces/gap.py

View check run for this annotation

Codecov / codecov/patch

src/sage/interfaces/gap.py#L1853

Added line #L1853 was not covered by tests
cmd += ' ' + os.path.join(SAGE_EXTCODE, 'gap', 'console.g')
os.system(cmd)
22 changes: 22 additions & 0 deletions src/sage/interfaces/gap_kernel_info.g
Original file line number Diff line number Diff line change
@@ -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();
15 changes: 5 additions & 10 deletions src/sage/interfaces/gap_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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}')

Expand Down
6 changes: 3 additions & 3 deletions src/sage/libs/gap/saved_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/sage/libs/gap/util.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/setup.cfg.m4
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ sage.libs.gap =
sage.gaprc

sage.interfaces =
gap_kernel_info.g
sage-maxima.lisp

sage.doctest =
Expand Down
Loading