Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d0b252d
meson.options: add the defer_runtime_checks option
orlitzky Jan 28, 2026
40c9eed
src/sage/meson.build,src/sage/config.py.in: record defer_feature_checks
orlitzky Jan 28, 2026
02f2ff7
src/sage/features/build_feature.py: new class for build-time features
orlitzky Jan 28, 2026
3da593d
src/sage/config.py.in: add placeholder vars for build features
orlitzky Jan 28, 2026
327cd99
src/**/meson.build: record build-time feature information in conf_data
orlitzky Jan 28, 2026
1e825f4
src/sage/features/sagemath.py: make BRiAl a BuildFeature
orlitzky Jan 28, 2026
236b4f8
src/sage/features/bliss.py: convert to a BuildFeature
orlitzky Jan 28, 2026
eb4d871
src/sage/features/coxeter3.py: convert to a BuildFeature
orlitzky Jan 28, 2026
54f512b
src/sage/features/mcqd.py: convert to a BuildFeature
orlitzky Jan 28, 2026
f8dfc51
src/sage/features/meataxe.py: convert to a BuildFeature
orlitzky Jan 28, 2026
11df90b
src/sage/features/sirocco.py: convert to a BuildFeature
orlitzky Jan 28, 2026
3f17359
src/sage/features/tdlib.py: convert to a BuildFeature
orlitzky Jan 28, 2026
0a25137
src/sage/features/rankwidth.py: new BuildFeature
orlitzky Jan 28, 2026
8cd2e34
src/sage/features/mwrank.py: new feature for the mwrank program
orlitzky Aug 5, 2025
cf59d56
src/sage/features/sagemath.py: new feature for sage.libs.eclib
orlitzky Aug 5, 2025
a31d34b
src/doc/en/reference/spkg/index.rst: add sage.features.mwrank
orlitzky Sep 7, 2025
fd5c59d
src/doc/en/reference/spkg/index.rst: add sage.features.build_feature
orlitzky Jan 28, 2026
42a964f
src/doc/en/reference/spkg/index.rst: add sage.features.rankwidth
orlitzky Jan 28, 2026
31da84d
src/sage/doctest/external.py: no runtime detection for build features
orlitzky Jan 28, 2026
d02d9d6
conftest.py: only ignore ImportErrors for disabled features
orlitzky Oct 13, 2025
e25c8cb
conftest.py: ignore ImportErrors for sage.libs.eclib if disabled
orlitzky Oct 13, 2025
fba22ba
build/pkgs/sagelib: defer feature checks to runtime
orlitzky Jan 28, 2026
afe80dc
src/sage/features/build_feature.py: fix is_present() doctest
orlitzky Jan 31, 2026
a32706b
src/sage/features/mwrank.py: fix is_present_at_runtime()
orlitzky Jan 31, 2026
cadac34
src/sage/features/sagemath.py: make eclib runtime-detectable
orlitzky Jan 31, 2026
1239b36
src/sage/features/sirocco.py: make sirocco runtime-detectable
orlitzky Jan 31, 2026
2c364a3
src/sage/features/mcqd.py: make mcqd runtime-detectable
orlitzky Jan 31, 2026
2e7bf1d
src/sage/features/meataxe.py: make meataxe runtime-detectable
orlitzky Jan 31, 2026
6ce0b0c
src/sage/features/bliss.py: make bliss runtime-detectable
orlitzky Jan 31, 2026
4637eaf
src/sage/features/tdlib.py: make tdlib runtime-detectable
orlitzky Jan 31, 2026
e6a5150
src/sage/features/coxeter3.py: make coxeter3 runtime-detectable
orlitzky Jan 31, 2026
e167823
src/sage/features/rankwidth.py: make rankwidth runtime-detectable
orlitzky Jan 31, 2026
b7638be
src/sage/features/sagemath.py: make brial runtime-detectable
orlitzky Jan 31, 2026
4fadc5b
src/sage/features/bliss.py: don't import PythonModule twice
orlitzky Jan 31, 2026
c90afeb
src/sage/features/coxeter3.py: fix module name
orlitzky Jan 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions build/pkgs/sagelib/spkg-install.in
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ if [ "$SAGE_EDITABLE" = yes ]; then
--config-settings=build-dir="build/sage-distro" \
--config-settings=setup-args="--native-file=$SAGE_PKGS/../platform/meson/sage-configure-native-file.ini" \
--config-settings=setup-args="-DSAGE_LOCAL=$SAGE_LOCAL" \
--config-settings=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS"
--config-settings=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS" \
--config-settings=setup-args="-Ddefer_feature_checks=true"

if [ "$SAGE_WHEELS" = yes ]; then
# Additionally build a wheel (for use in other venvs)
Expand All @@ -73,7 +74,8 @@ else
# Compiling sage/interfaces/sagespawn.pyx because it depends on /private/var/folders/38/wnh4gf1552g_crsjnv2vmmww0000gp/T/pip-build-env-609n5985/overlay/lib/python3.10/site-packages/Cython/Includes/posix/unistd.pxd
sdh_pip_install --no-build-isolation . --config-setting=build-dir="build/sage-distro" \
--config-setting=setup-args="-DSAGE_LOCAL=$SAGE_LOCAL" \
--config-setting=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS"
--config-setting=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS" \
--config-settings=setup-args="-Ddefer_feature_checks=true"
fi

# Remove (potentially invalid) star import caches.
Expand Down
39 changes: 31 additions & 8 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,37 @@ def _find(
pytest.skip("unable to import module %r" % self.path)
else:
if isinstance(exception, ModuleNotFoundError):
# Ignore some missing features/modules for now
# TODO: Remove this once all optional things are using Features
if exception.name in (
"valgrind",
"rpy2",
"sage.libs.coxeter3.coxeter",
"sagemath_giac",
):
# Ignore some missing features/modules for
# now. Many of these are using custom
# "sage.doctest" headers that only our doctest
# runner (i.e. not pytest) can understand.
#
# TODO: we should remove this once all
# optional things are using Features. It
# wouldn't be too hard to move the
# "sage.doctest" header into pytest (as
# explicit ignore lists based on feature
# tests), but that would require duplication
# for as long as `sage -t` is still used.
from sage.features.coxeter3 import Coxeter3
from sage.features.sagemath import (sage__libs__eclib,
sage__libs__giac)
from sage.features.standard import PythonModule

exc_list = ["valgrind"]
if not sage__libs__eclib().is_present():
exc_list.append("sage.libs.eclib")
if not PythonModule("rpy2").is_present():
exc_list.append("rpy2")
if not Coxeter3().is_present():
exc_list.append("sage.libs.coxeter3.coxeter")
if not sage__libs__giac().is_present():
exc_list.append("sagemath_giac")

# Ignore import errors, but only when the associated
# feature is actually disabled. Use startswith() so
# that sage.libs.foo matches all of sage.libs.foo.*
if any(exception.name.startswith(e) for e in exc_list):
pytest.skip(
f"unable to import module {self.path} due to missing feature {exception.name}"
)
Expand Down
10 changes: 10 additions & 0 deletions meson.options
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ option(
description: 'Build the HTML / PDF documentation'
)

# Useful on binary distros, for example, to allow features to flip on as soon
# as the package that provides it is installed. If this is disabled, a rebuild
# of sagelib is required to toggle features on or off.
option(
'defer_feature_checks',
type: 'boolean',
value: false,
description: 'Defer feature checks to runtime'
)

#
# Features
#
Expand Down
3 changes: 3 additions & 0 deletions src/doc/en/reference/spkg/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Features
sage/features
sage/features/join_feature
sage/features/all
sage/features/build_feature
sage/features/sagemath
sage/features/pkg_systems
sage/features/bliss
Expand All @@ -52,10 +53,12 @@ Features
sage/features/mcqd
sage/features/meataxe
sage/features/mip_backends
sage/features/mwrank
sage/features/normaliz
sage/features/pandoc
sage/features/pdf2svg
sage/features/polymake
sage/features/rankwidth
sage/features/rubiks
sage/features/tdlib
sage/features/topcom
Expand Down
11 changes: 11 additions & 0 deletions src/sage/config.py.in
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ THREEJS_DIR = SAGE_LOCAL + "/share/threejs-sage"
OPENMP_CFLAGS = "@OPENMP_CFLAGS@"
OPENMP_CXXFLAGS = "@OPENMP_CXXFLAGS@"

# build-time feature flags
defer_feature_checks = @DEFER_FEATURE_CHECKS@
bliss_enabled = @BLISS_ENABLED@
brial_enabled = @BRIAL_ENABLED@
coxeter3_enabled = @COXETER3_ENABLED@
eclib_enabled = @ECLIB_ENABLED@
mcqd_enabled = @MCQD_ENABLED@
meataxe_enabled = @MEATAXE_ENABLED@
rankwidth_enabled = @RANKWIDTH_ENABLED@
sirocco_enabled = @SIROCCO_ENABLED@
tdlib_enabled = @TDLIB_ENABLED@

def is_editable_install() -> bool:
"""
Expand Down
24 changes: 22 additions & 2 deletions src/sage/doctest/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,20 @@ def detectable(self):
"""
Return the list of names of those features for which testing their presence is allowed.
"""
# Exclude build features that aren't runtime detectable from
# the list. Note that when defer_feature_checks is not set,
# *no* BuildFeatures are runtime-detectable.
from sage.features.build_feature import BuildFeature
def build_time_only(f):
return ( isinstance(f, BuildFeature)
and
not f.is_runtime_detectable() )

return [feature.name
for feature, seen in zip(self._features, self._seen)
if seen >= 0 and (self._allow_external or feature not in self._external_features)]
if seen >= 0
and (self._allow_external or feature not in self._external_features)
and not build_time_only(feature)]

def seen(self):
"""
Expand All @@ -527,9 +538,18 @@ def seen(self):
sage: available_software.seen() # random
['internet', 'latex', 'magma']
"""
# Exclude build features that aren't runtime detectable from
# the list. Note that when defer_feature_checks is not set,
# *no* BuildFeatures are runtime-detectable.
from sage.features.build_feature import BuildFeature
def build_time_only(f):
return ( isinstance(f, BuildFeature)
and
not f.is_runtime_detectable() )
return [feature.name
for feature, seen in zip(self._features, self._seen)
if seen > 0]
if seen > 0
and not build_time_only(feature)]

def hidden(self):
"""
Expand Down
37 changes: 29 additions & 8 deletions src/sage/features/bliss.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
# *****************************************************************************

from . import CythonFeature, PythonModule
from .join_feature import JoinFeature

from sage.config import bliss_enabled
from sage.features import PythonModule
from sage.features.build_feature import BuildFeature

TEST_CODE = """
# distutils: language=c++
Expand Down Expand Up @@ -57,28 +59,47 @@ def __init__(self):
url='http://www.tcs.hut.fi/Software/bliss/')


class Bliss(JoinFeature):
class Bliss(BuildFeature):
r"""
A :class:`~sage.features.Feature` which describes whether the :mod:`sage.graphs.bliss`
module is available in this installation of Sage.
A :class:`~sage.features.Feature` which describes whether the
:mod:`sage.graphs.bliss` module is available in this installation
of Sage.

EXAMPLES::

sage: from sage.features.bliss import Bliss
sage: Bliss().require() # optional - bliss
sage: Bliss().require() # needs bliss
"""
_enabled_in_build = bliss_enabled

def __init__(self):
r"""
TESTS::

sage: from sage.features.bliss import Bliss
sage: Bliss()
Feature('bliss')

"""
JoinFeature.__init__(self, "bliss",
[PythonModule("sage.graphs.bliss", spkg='bliss',
url='http://www.tcs.hut.fi/Software/bliss/')])
super().__init__("bliss",
url='http://www.tcs.hut.fi/Software/bliss/')

def is_present_at_runtime(self):
r"""
TESTS::

sage: from sage.features import FeatureTestResult
sage: from sage.features.bliss import Bliss
sage: result = Bliss().is_present_at_runtime()
sage: isinstance(result, FeatureTestResult)
True
sage: result # needs bliss
FeatureTestResult('bliss', True)

"""
result = PythonModule("sage.graphs.bliss")._is_present()
result.feature = self
return result

def all_features():
return [Bliss()]
138 changes: 138 additions & 0 deletions src/sage/features/build_feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
r"""
Features that can be explicitly enabled or disabled at build time

These features are unique in that, if they are enabled or disabled at
build-time, then we usually do not want to detect them on-the-fly.
Instead we trust the value supplied (or detected) at build-time. There
is however an overrride to defer these checks to run-time (the classic
behavior) for use on binary distros or anywhere it is desirable to
enable/disable features without rebuilding sage.

This is an implementation of Option 3 in `Github discussion 41067
<https://github.com/sagemath/sage/discussions/41067>`__.
"""

from sage.features import Feature, FeatureTestResult

class BuildFeature(Feature):
r"""
A class for features that can be enabled or disabled at
build-time.

The current implementation refers to build features that are
configurable in meson. For example::

option(
'foo',
type: 'feature',
value: 'auto',
description: 'support for foo'
)

At build time, support for this "foo" will be automatically
detected, and either enabled or disabled depending on whether or
not its requirements are met. Alternatively, users may pass either
``-Dfoo=enabled`` or ``-Dfoo=disabled`` to explicitly enable or
disable the feature. Features may be disabled regardless of
whether or not they are installed, but usually features may only
be enabled if their dependencies are present and usable.

In any event, after ``meson setup``, support for "foo" is either
enabled or disabled, and a boolean variable called something like
``foo_enabled`` is written to ``sage.config``. In your subclass,
you should set the member variable ``_enabled_in_build`` to the
value of that config variable.
Comment on lines +42 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

Just as an idea in the spirit of convention-over-configuration: you could query in _is_present below always the variable <name of feature>_enabled from sage.config. Then you don't have to set _enabled_in_build for each feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this at first and talked myself out of it! With the current implementation, the downside to doing it by convention is that when the convention fails (e.g. mwrank, brial), you have to override the whole _is_present() method leading to duplicated code.

Given that you have to create a whole new subclass for every new feature and give it a name anyway, just spelling out the variable name in one line felt simpler.


The :meth:`_is_present` method for this class will return the
value of that config variable unless ``defer_feature_checks`` is
set to ``True`` in ``sage.config``. If checks are deferred, the
:meth:`_is_present` method will try to return the value of
:meth:`is_present_at_runtime` instead. If your feature can be
detected at run-time, you should implement that check in
:meth:`is_present_at_runtime`. Otherwise, leave it unimplemented;
and :meth:`_is_present` will return ``False``.

EXAMPLES::

sage: from sage.features.build_feature import BuildFeature
sage: BuildFeature("foo")
Feature('foo')

"""

# Set this in subclasses.
_enabled_in_build = None

# Implement this method if your feature is detectable at run-time.
# Your test should only return True if the feature meets Sage's
# requirements; for example, if there are doctests for gzipped foo
# data files hidden behind "needs foo", then you should ensure
# that foo was compiled with (say) --enable-zlib in your check.
#
# def is_present_at_runtime(self):
# pass

def is_runtime_detectable(self):
r"""
Return whether or not this feature can (and should) be
detected at runtime.

A feature is runtime detectable if both of the following hold:

- Deferred feature checks have been enabled globally by
passing ``-Ddefer_feature_checks=true`` to ``meson setup``.

- An ``is_present_at_runtime`` method has been implemented for
the feature.

EXAMPLES:

The method returns ``False`` if you have not implemented
``is_present_at_runtime``::

sage: from sage.features.build_feature import BuildFeature
sage: bf = BuildFeature("example")
sage: bf.is_runtime_detectable()
False

"""
from sage.config import defer_feature_checks
if not defer_feature_checks:
return False
elif hasattr(self, "is_present_at_runtime"):
return True
else:
return False

def _is_present(self):
r"""
Default presence check for build features.

If this feature :meth:`is_runtime_detectable`, we return the
result of that method. Otherwise, we use the value of
``self._enabled_in_build``.

EXAMPLES:

When feature checks are deferred, runtime-detectable features
can be detected without ``self._enabled_in_build`` being set,
but this will fail by surprise when they are un-deferred::

sage: from sage.config import defer_feature_checks
sage: from sage.features.build_feature import BuildFeature
sage: bf = BuildFeature("example")
sage: const_True = lambda s: True
sage: bf.is_present_at_runtime = const_True.__get__(bf)
sage: (not defer_feature_checks) or bf.is_present().is_present
True

"""
if self.is_runtime_detectable():
return self.is_present_at_runtime()
else:
import sage.config
# Wrap with bool() so that we can be lazy and use meson's
# set10() rather than painstakingly writing "True" and
# "False" to the config file.
result = bool(self._enabled_in_build)
return FeatureTestResult(self, result)
Loading