'''
Code related to the configuration of Sprite.
'''
###############################
# Logging set-up.
import logging, os, sys
_LOG_FILE_ = os.environ.get('SPRITE_LOG_FILE', '-')
_LOG_LEVEL_NAME_ = os.environ.get('SPRITE_LOG_LEVEL', 'WARNING').upper()
_LOG_LEVEL_NAMES_ = 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
_LOG_LEVEL_VALUES_ = tuple(getattr(logging, s) for s in _LOG_LEVEL_NAMES_)
if _LOG_LEVEL_NAME_ not in _LOG_LEVEL_NAMES_:
raise EnvironmentError(
'SPRITE_LOG_LEVEL should be one of %s or %r, not %r' % (
', '.join(repr(x) for x in _LOG_LEVEL_NAMES_[:-1])
, _LOG_LEVEL_NAMES_[-1]
, _LOG_LEVEL_NAME_
)
)
else:
_LOG_LEVEL_ = getattr(logging, _LOG_LEVEL_NAME_)
logging.basicConfig(
level=_LOG_LEVEL_
, format='%(asctime)s [%(levelname)s] %(message)s'
, datefmt='%m/%d/%Y %H:%M:%S'
, **({'filename': _LOG_FILE_} if _LOG_FILE_ not in ['-', ''] else {})
)
logger = logging.getLogger(__name__)
# Logging query API.
[docs]
def log_file_name():
return _LOG_FILE_
[docs]
def log_level():
return _LOG_LEVEL_
[docs]
def log_level_names():
return _LOG_LEVEL_NAMES_
[docs]
def log_level_map():
return dict(zip(_LOG_LEVEL_NAMES_, _LOG_LEVEL_VALUES_))
[docs]
def logging_enabled_for(level):
if isinstance(level, str) and level in log_level_names():
level = log_level_map()[level]
if level not in _LOG_LEVEL_VALUES_:
raise ValueError('logging level %r is not valid')
return level <= _LOG_LEVEL_
###############################
# Debugging set-up.
[docs]
def debugging():
'''Indicates whether debugging is turned on.'''
return 'SPRITE_DEBUG' in os.environ
if debugging():
logger.info('Debugging is enabled because SPRITE_DEBUG is set.')
[docs]
def interactive_modname():
return 'sprite__interactive_'
[docs]
def expression_modname(n):
'''
The name of the module that holds compiled expression number ``n``. Each
expression gets its own name: the C++ backend resolves a module's symbols
against the same-named module loaded first, so two expression modules must
never share a name.
'''
return 'sprite__expression_%d' % n
[docs]
def is_expression_modname(name):
return name.startswith('sprite__expression_')
[docs]
def is_anonymous_modname(name):
'''
Tells whether ``name`` belongs to an anonymous module: an interactive module
compiled without a name, or an expression module. curry.compile numbers
these per process, so the same text gets another name in another process.
The ICurry cache leaves the name out of its key for these modules.
'''
return name.startswith(interactive_modname()) or is_expression_modname(name)
class _Variable(object):
def __init__(self, name, type=str, use_env=False):
self.use_env = use_env
self.name = name
self.type = type
self.value = None
def __call__(self):
if self.value is None:
filename = installed_path('sysconfig', self.name)
assert os.path.exists(filename)
with open(filename) as istream:
self.value = self.convert(istream.read().strip('\n'))
logger.debug(
'Config %r read from file %r (%s: %r)'
, self.name, filename, self.type.__name__, self.value
)
return self.value
def convert(self, x):
from .utility.strings import ensure_str
x = ensure_str(x)
if self.type is bool:
if x.strip().lower() in ('true', 'yes', 'on'):
return True
elif x.strip().lower() in ('false', 'no', 'off', ''):
return False
else:
try:
return bool(int(x))
except:
logger.warning(
'Failed to interpret %r as an integer for configuration variable '
'%s. The feature will be disabled. Please update Make.config.'
, x, self.name.upper()
)
return False
if self.use_env:
x = str(x).format(**os.environ)
return self.type(x)
# These are read from under $PREFIX/sysconfig. The source files are under
# $ROOT/src/export/sysconfig. They can be set in Make.config. This machanism
# is essentially a way of passing information contained in Make variables to
# the Sprite runtime.
# The default location of the cache file.
default_sprite_cache_file = _Variable('default_sprite_cache_file', use_env=True)
# Whether to enable caching (by default).
enable_icurry_cache = _Variable('enable_icurry_cache', type=bool)
# Whether to cache parsed JSON. The stored objects are pickled Python.
enable_parsed_json_cache = _Variable('enable_parsed_json_cache', type=bool)
# The name of the subdirectory of .curry in which to place Sprite files.
intermediate_subdir = _Variable('intermediate_subdir')
# The name of the subdirectory of .curry into which the Curry front end
# writes FlatCurry. It names the front end, e.g., pakcs-3.4.1.
frontend_subdir = _Variable('frontend_subdir')
# The options passed to the Curry front end after the output directory and
# the search path, as one string.
frontend_flags = _Variable('frontend_flags')
# The route from Curry to ICurry chosen at configuration time: 'frontend' or
# 'icurry'. Empty when configure left the choice open.
default_curry2icurry_tool = _Variable('curry2icurry_tool')
# The name of the top-level Python package. By default, 'curry'.
python_package_name = _Variable('python_package_name')
# The version of the Curry library. It names the PAKCS release whose library
# Sprite ships under curry/lib. Some tests key their expectations on it.
currylib_version = _Variable('currylib_version')
# The names of all modules in the system Curry library, the Prelude first.
currylib_module_names = _Variable('currylib_module_names')
# The names of the system library modules that Sprite cannot compile. See
# CURRYLIB_UNSUPPORTED_MODULES in Make.include.
currylib_unsupported_modules = _Variable('currylib_unsupported_modules')
# The name of the default backend.
default_backend = _Variable('default_backend')
# The location of ld.so.
ld_interpreter_path = _Variable('ld_interpreter_path')
# The flavor of the installed C++ runtime, as the build wrote it: 'release'
# or 'debug'. See cxx_flavor.
_cxx_flavor = _Variable('cxx_flavor')
# The collector of the installed C++ runtime, as the build wrote it: 'wdgc'
# or 'mps'. See cxx_gc.
_cxx_gc = _Variable('cxx_gc')
# Whether the installed C++ runtime counts the writes into old nodes (make
# GC_WRITE_COUNTERS=1), as the build wrote it: 0 or 1. See
# cxx_gc_write_counters.
_cxx_gc_write_counters = _Variable('cxx_gc_write_counters', type=bool)
# The flavors of the C++ runtime and of generated code.
CXX_FLAVORS = 'release', 'debug'
# The collectors of the C++ runtime (make GC=...).
CXX_GCS = 'wdgc', 'mps'
[docs]
def cxx_flavor():
'''
The flavor of the installed C++ runtime: 'release', or 'debug' for a build
with make DEBUG=1. The release flavor has no assertions. The C++ backend
compiles generated modules in the same flavor unless the interpreter flag
``debug`` is set (see curry.backends.cxx.toolchain.Cpp2So.flavor). An
installation that names no flavor, one staged before the flavors existed,
is a release build.
'''
if not os.path.exists(installed_path('sysconfig', 'cxx_flavor')):
return 'release'
return 'debug' if _cxx_flavor().strip() == 'debug' else 'release'
[docs]
def cxx_gc():
'''
The collector of the installed C++ runtime: 'wdgc', the block heap and the
mark-and-sweep collector of the tree, or 'mps' for a build with make
GC=mps, the Memory Pool System. The C++ backend compiles generated
modules for the same collector (see curry.backends.cxx.toolchain.gc_flags).
An installation that names no collector is a wdgc build.
'''
if not os.path.exists(installed_path('sysconfig', 'cxx_gc')):
return 'wdgc'
return 'mps' if _cxx_gc().strip() == 'mps' else 'wdgc'
[docs]
def cxx_gc_write_counters():
'''
True when the installed C++ runtime counts the writes into old nodes (make
GC_WRITE_COUNTERS=1; see src/cyrt/graph/gc/wdgc.cpp). Some of the counted
sites are in the runtime headers, so the C++ backend compiles generated
modules with the same macro (see curry.backends.cxx.toolchain.gc_flags).
An installation that names no value is a build without them.
'''
if not os.path.exists(installed_path('sysconfig', 'cxx_gc_write_counters')):
return False
return _cxx_gc_write_counters()
[docs]
def syslibs():
'''
The names of the modules of the system Curry library. Each one is found as
a source under :func:`system_curry_path`.
'''
return currylib_module_names().split()
[docs]
def unsupported_syslibs():
'''
The names of the system library modules that Sprite cannot compile: their
externals have no implementation in a backend, they import such a module,
or the importer cannot load them. See CURRYLIB_UNSUPPORTED_MODULES in
Make.include.
'''
return currylib_unsupported_modules().split()
[docs]
def supported_syslibs():
'''
The names of the system library modules that Sprite compiles, the Prelude
first: :func:`syslibs` without :func:`unsupported_syslibs`. The
installation compiles them for both backends.
'''
unsupported = set(unsupported_syslibs())
return [name for name in syslibs() if name not in unsupported]
[docs]
def syslibversion():
return tuple(int(x) for x in currylib_version().split('.'))
[docs]
def prefix():
return os.environ['SPRITE_HOME']
[docs]
def force_recompile_cxx():
return os.environ.get('SPRITE_FORCE_RECOMPILE_CXX', False)
[docs]
def cxx_pch_root():
'''
The directory under which the C++ backend keeps its precompiled header.
Returns None when the header is disabled.
SPRITE_CXX_PCH_ROOT names the directory. Set it to the empty string to
compile without the header. Without the variable the header lives beside
the installed headers, where the C++ compiler finds it with no extra flag,
unless the installation is a package: its include directory cannot be
written, or it lies in a conda environment (see in_conda_prefix). Then
the header goes to the cache directory of the user (see cxx_pch_cache_dir),
so that nothing is written into the package at run time.
'''
root = os.environ.get('SPRITE_CXX_PCH_ROOT')
if root is not None:
root = root.strip()
return os.path.abspath(root) if root else None
include = installed_path('include')
if os.access(include, os.W_OK) and not in_conda_prefix(include):
return include
return cxx_pch_cache_dir()
[docs]
def in_conda_prefix(path):
'''
Tells whether ``path`` lies in a conda environment: a directory with a
``conda-meta`` entry is the path or one of its parents, by real path. A
conda package must not be written at run time; conda does not track the
files, and they stay behind when the package is removed.
'''
path = os.path.realpath(path)
while True:
if os.path.isdir(os.path.join(path, 'conda-meta')):
return True
parent = os.path.dirname(path)
if parent == path:
return False
path = parent
[docs]
def user_cache_dir():
'''
The cache directory of the user for Sprite: ``$XDG_CACHE_HOME/sprite``, or
``~/.cache/sprite``. Nothing creates it here.
'''
base = os.environ.get('XDG_CACHE_HOME', '').strip()
if not base:
base = os.path.join(os.path.expanduser('~'), '.cache')
return os.path.join(base, 'sprite')
[docs]
def installation_key():
'''
A short digest of the real path of the installation. It names the files
of this installation in a directory that several installations share.
'''
import hashlib
real = os.path.realpath(prefix()).encode('utf-8')
return hashlib.sha256(real).hexdigest()[:16]
[docs]
def cxx_pch_cache_dir():
'''
The root of the precompiled header in the cache directory of the user:
``<user_cache_dir>/pch/<installation_key>``. Each installation gets a
directory of its own, because the toolchain removes the stale members of
the directory it uses (PrecompiledHeader.remove_stale_members), and the
members of two installations would otherwise remove each other.
'''
return os.path.join(user_cache_dir(), 'pch', installation_key())
[docs]
def cxx_compiler(cached=[]):
'''
The compiler the installed C++ runtime was built with, as the build
recorded it in sysconfig/cxx_compiler: the version and the target of
CXX_POSTINSTALL (``g++ -dumpfullversion`` and ``-dumpmachine``), one
line. The ABI stamp of a compiled module digests it
(curry.backends.cxx.toolchain.object_digest), so a runtime built with
another compiler compiles every module again. The empty string for an
installation that names no compiler: one staged before the record
existed, or one configured without a compiler. Read once per process,
as the other tools are.
'''
if not cached:
filename = installed_path('sysconfig', 'cxx_compiler')
if os.path.exists(filename):
with open(filename) as stream:
cached.append(' '.join(stream.read().split()))
else:
cached.append('')
return cached[0]
[docs]
def product_cache_dir():
'''
The root of the product cache (curry.toolchain._productcache), where the
C++ backend keeps a copy of each shared object it compiles, with the
generated C++ and the ABI stamp, so that a product compiled once is not
compiled again by another tree of the same user, or by the same tree after
its products were removed. Returns None when the cache is off.
SPRITE_PRODUCT_CACHE names the directory. Without the variable, or with
the empty string, the cache is off: every product is compiled in place,
as before the cache existed. The test runner and the worktree script
name a directory of their own; a developer who wants one across trees
names it, for instance ``<user_cache_dir>/products``.
'''
root = os.environ.get('SPRITE_PRODUCT_CACHE', '').strip()
return os.path.abspath(root) if root else None
# The path to system Curry files, such as the Prelude. This is appended to
# whatever the user might supply via the CURRYPATH environment variable.
[docs]
def system_curry_path():
return os.path.join(prefix(), 'curry')
[docs]
def currypath(reset=False, cache=[]):
'''
Gets the Curry path from the environment variable CURRYPATH and appends the
system path.
Args:
reset:
If true, the cache will be cleared and the Curry path reloaded from the
environment.
cache:
A list into which the Cury path is cached.
'''
if reset:
cache[:] = ()
if not cache:
from .utility import curryname
envpath = os.environ.get('CURRYPATH', '').split(':')
syspath = system_curry_path().split(':')
currypath = curryname.makeCurryPath(envpath + syspath)
cache.append(currypath)
verify_syslibs()
return cache[0]
[docs]
def verify_syslibs():
from .utility import filesys
if 'SPRITE_DISABLE_SYSLIB_CHECKS' in os.environ:
return
cypath = currypath()
syspath = system_curry_path().split(':')
for name in syslibs():
name = name.replace('.', os.sep) + '.curry'
found = list(filesys.findfiles(cypath, name))
if not found or not (found[-1].startswith(p) for p in syspath):
logger.critical('System library %r was not found in the CURRYPATH' % name)
logger.critical('The CURRYPATH is %r' % ':'.join(cypath))
elif not any(found[0].startswith(p) for p in syspath):
logger.critical('System library %r is shadowed by a file from the CURRYPATH.' % name)
logger.critical('The CURRYPATH is %r' % ':'.join(cypath))
logger.critical('The system %r is %r' % (name, found[-1]))
logger.critical('%r was found at %r' % (name, found[0]))
else:
continue
logger.critical('Set SPRITE_DISABLE_SYSLIB_CHECKS to ignore')
sys.exit(1)
[docs]
def installed_path(*relpath):
return os.path.join(prefix(), *relpath)
# External tools and libraries.
[docs]
def curry_frontend(cached=[]):
'''The Curry front end, if it is configured. Otherwise, None.'''
if not cached:
path = installed_path('tools', 'curry-frontend')
cached.append(os.path.abspath(path) if os.path.exists(path) else None)
return cached[0]
# The names of the routes from Curry to ICurry.
CURRY2ICURRY_TOOLS = 'frontend', 'icurry'
[docs]
def python_exe(cached=[]):
if not cached:
path = installed_path('bin', 'python')
path = os.path.abspath(path)
cached.append(path)
return cached[0]
[docs]
def sprite_exec(cached=[]):
if not cached:
path = installed_path('bin', 'sprite-exec')
path = os.path.abspath(path)
cached.append(path)
return cached[0]
[docs]
def cyrt_lib(cached=[]):
if not cached:
path = installed_path('lib', 'libcyrt.so')
path = os.path.abspath(path)
assert os.path.exists(path)
cached.append(path)
return cached[0]