diff --git a/build/pkgs/sagelib/spkg-install.in b/build/pkgs/sagelib/spkg-install.in index 2a638df17f1..e223e0d236c 100644 --- a/build/pkgs/sagelib/spkg-install.in +++ b/build/pkgs/sagelib/spkg-install.in @@ -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) @@ -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. diff --git a/conftest.py b/conftest.py index 1ccd5f48d68..74f0747b50b 100644 --- a/conftest.py +++ b/conftest.py @@ -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}" ) diff --git a/meson.options b/meson.options index 294e1171b03..5c31cf4c95d 100644 --- a/meson.options +++ b/meson.options @@ -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 # diff --git a/src/doc/en/reference/spkg/index.rst b/src/doc/en/reference/spkg/index.rst index e9ba934a540..bffd8afb9b7 100644 --- a/src/doc/en/reference/spkg/index.rst +++ b/src/doc/en/reference/spkg/index.rst @@ -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 @@ -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 diff --git a/src/sage/config.py.in b/src/sage/config.py.in index 3a95538d313..aca1247cc11 100644 --- a/src/sage/config.py.in +++ b/src/sage/config.py.in @@ -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: """ diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index 026ca7f08cd..e8afae17bbd 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -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): """ @@ -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): """ diff --git a/src/sage/features/bliss.py b/src/sage/features/bliss.py index f14893b5a89..450010f83a0 100644 --- a/src/sage/features/bliss.py +++ b/src/sage/features/bliss.py @@ -13,9 +13,11 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import CythonFeature, PythonModule -from .join_feature import JoinFeature +from . import CythonFeature +from sage.config import bliss_enabled +from sage.features import PythonModule +from sage.features.build_feature import BuildFeature TEST_CODE = """ # distutils: language=c++ @@ -57,16 +59,19 @@ 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:: @@ -74,11 +79,27 @@ def __init__(self): 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()] diff --git a/src/sage/features/build_feature.py b/src/sage/features/build_feature.py new file mode 100644 index 00000000000..9c9bb0cd463 --- /dev/null +++ b/src/sage/features/build_feature.py @@ -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 +`__. +""" + +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. + + 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) diff --git a/src/sage/features/coxeter3.py b/src/sage/features/coxeter3.py index 3fdc3c88e03..eed4d844e10 100644 --- a/src/sage/features/coxeter3.py +++ b/src/sage/features/coxeter3.py @@ -13,20 +13,24 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import coxeter3_enabled +from sage.features import PythonModule +from sage.features.build_feature import BuildFeature - -class Coxeter3(JoinFeature): +class Coxeter3(BuildFeature): r""" - A :class:`~sage.features.Feature` which describes whether the :mod:`sage.libs.coxeter3` - module is available in this installation of Sage. + A :class:`~sage.features.Feature` which describes whether the + :mod:`sage.libs.coxeter3` module is available in this installation + of Sage. EXAMPLES:: sage: from sage.features.coxeter3 import Coxeter3 - sage: Coxeter3().require() # optional - coxeter3 + sage: Coxeter3().require() # needs coxeter3 + """ + _enabled_in_build = coxeter3_enabled + def __init__(self): r""" TESTS:: @@ -34,11 +38,31 @@ def __init__(self): sage: from sage.features.coxeter3 import Coxeter3 sage: Coxeter3() Feature('coxeter3') + """ - JoinFeature.__init__(self, "coxeter3", - [PythonModule("sage.libs.coxeter3.coxeter", - spkg='coxeter3')]) + super().__init__("coxeter3", spkg='coxeter3') + + def is_present_at_runtime(self): + r""" + TESTS:: + sage: from sage.features import FeatureTestResult + sage: from sage.features.coxeter3 import Coxeter3 + sage: result = Coxeter3().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs coxeter3 + FeatureTestResult('coxeter3', True) + + """ + # The build system installs the sage/libs/coxeter3 source code + # even when coxeter3 support is disabled, so a naive import of + # that module will actually succeed. We check for a + # conditionally-compiled cython module instead. + cython_modname = "sage.libs.coxeter3.coxeter" + result = PythonModule(cython_modname)._is_present() + result.feature = self + return result def all_features(): return [Coxeter3()] diff --git a/src/sage/features/mcqd.py b/src/sage/features/mcqd.py index 7f0df8eedda..95211ca4e69 100644 --- a/src/sage/features/mcqd.py +++ b/src/sage/features/mcqd.py @@ -11,21 +11,24 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import mcqd_enabled +from sage.features import PythonModule +from sage.features.build_feature import BuildFeature - -class Mcqd(JoinFeature): +class Mcqd(BuildFeature): r""" - A :class:`~sage.features.Feature` describing the presence of the :mod:`~sage.graphs.mcqd` module, - which is the SageMath interface to the :ref:`mcqd ` library + A :class:`~sage.features.Feature` describing the presence of + the :mod:`~sage.graphs.mcqd` module, which is the SageMath + interface to the :ref:`mcqd ` library EXAMPLES:: sage: from sage.features.mcqd import Mcqd - sage: Mcqd().is_present() # optional - mcqd + sage: Mcqd().is_present() # needs mcqd FeatureTestResult('mcqd', True) + """ + _enabled_in_build = mcqd_enabled def __init__(self): """ @@ -34,11 +37,26 @@ def __init__(self): sage: from sage.features.mcqd import Mcqd sage: isinstance(Mcqd(), Mcqd) True + """ - JoinFeature.__init__(self, 'mcqd', - [PythonModule('sage.graphs.mcqd', - spkg='mcqd')]) + super().__init__('mcqd', spkg='mcqd') + + def is_present_at_runtime(self): + r""" + TESTS:: + sage: from sage.features import FeatureTestResult + sage: from sage.features.mcqd import Mcqd + sage: result = Mcqd().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs mcqd + FeatureTestResult('mcqd', True) + + """ + result = PythonModule("sage.graphs.mcqd")._is_present() + result.feature = self + return result def all_features(): return [Mcqd()] diff --git a/src/sage/features/meataxe.py b/src/sage/features/meataxe.py index 3276b3cdba7..a8126661f0f 100644 --- a/src/sage/features/meataxe.py +++ b/src/sage/features/meataxe.py @@ -12,22 +12,25 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** +from sage.config import meataxe_enabled +from sage.features import PythonModule +from sage.features.build_feature import BuildFeature -from . import PythonModule -from .join_feature import JoinFeature - - -class Meataxe(JoinFeature): +class Meataxe(BuildFeature): r""" - A :class:`~sage.features.Feature` describing the presence of the Sage modules - that depend on the :ref:`meataxe ` library. + A :class:`~sage.features.Feature` describing the presence of + the Sage modules that depend on the :ref:`meataxe ` + library. EXAMPLES:: sage: from sage.features.meataxe import Meataxe - sage: Meataxe().is_present() # optional - meataxe + sage: Meataxe().is_present() # needs meataxe FeatureTestResult('meataxe', True) + """ + _enabled_in_build = meataxe_enabled + def __init__(self): r""" TESTS:: @@ -35,11 +38,27 @@ def __init__(self): sage: from sage.features.meataxe import Meataxe sage: isinstance(Meataxe(), Meataxe) True + """ - JoinFeature.__init__(self, 'meataxe', - [PythonModule('sage.matrix.matrix_gfpn_dense', - spkg='meataxe')]) + super().__init__('meataxe', spkg='meataxe') + + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.meataxe import Meataxe + sage: result = Meataxe().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs meataxe + FeatureTestResult('meataxe', True) + """ + modname = "sage.matrix.matrix_gfpn_dense" + result = PythonModule(modname)._is_present() + result.feature = self + return result def all_features(): return [Meataxe()] diff --git a/src/sage/features/mwrank.py b/src/sage/features/mwrank.py new file mode 100644 index 00000000000..05182073961 --- /dev/null +++ b/src/sage/features/mwrank.py @@ -0,0 +1,54 @@ +r""" +Feature for testing the presence of the ``mwrank`` program + +This is part of the eclib package. +""" + +from sage.config import eclib_enabled +from sage.features import Executable +from sage.features.build_feature import BuildFeature + + +class Mwrank(BuildFeature, Executable): + r""" + A :class:`~sage.features.Feature` describing the presence of + the ``mwrank`` program. + + EXAMPLES:: + + sage: from sage.features.mwrank import Mwrank + sage: Mwrank().is_present() # needs mwrank + FeatureTestResult('mwrank', True) + + """ + _enabled_in_build = eclib_enabled + + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.mwrank import Mwrank + sage: isinstance(Mwrank(), Mwrank) + True + + """ + Executable.__init__(self, 'mwrank', executable='mwrank', + spkg='eclib', type='standard') + + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.mwrank import Mwrank + sage: result = Mwrank().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs mwrank + FeatureTestResult('mwrank', True) + + """ + return Executable._is_present(self) + +def all_features(): + return [Mwrank()] diff --git a/src/sage/features/rankwidth.py b/src/sage/features/rankwidth.py new file mode 100644 index 00000000000..bfea87d457d --- /dev/null +++ b/src/sage/features/rankwidth.py @@ -0,0 +1,49 @@ +r""" +Feature for testing the availability of the ``rw`` library +""" + +from sage.config import rankwidth_enabled +from sage.features import PythonModule +from sage.features.build_feature import BuildFeature + +class RankWidth(BuildFeature): + r""" + A :class:`~sage.features.Feature` for the availability of + the ``rankwidth`` library as determined at build-time. + + This library is required for the rank-width graph decomposition + found in :mod:`sage.graphs.graph_decompositions.rankwidth`. + + EXAMPLES:: + + sage: from sage.features.rankwidth import RankWidth + sage: RankWidth().is_present() # needs rankwidth + FeatureTestResult('rankwidth', True) + + """ + _enabled_in_build = rankwidth_enabled + + def __init__(self): + super().__init__("rankwidth", spkg="rw", type="standard") + + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.rankwidth import RankWidth + sage: result = RankWidth().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs rankwidth + FeatureTestResult('rankwidth', True) + + """ + modname = "sage.graphs.graph_decompositions.rankwidth" + result = PythonModule(modname)._is_present() + result.feature = self + return result + + +def all_features(): + return [RankWidth()] diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index 6313f1c715a..987e489474b 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -39,9 +39,10 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule, StaticFile -from .join_feature import JoinFeature - +from sage.config import brial_enabled, eclib_enabled +from sage.features import PythonModule, StaticFile +from sage.features.build_feature import BuildFeature +from sage.features.join_feature import JoinFeature class sagemath_doc_html(StaticFile): r""" @@ -512,6 +513,63 @@ def __init__(self): spkg='sagemath_ntl', type='standard') +class sage__libs__eclib(BuildFeature): + r""" + A :class:`sage.features.Feature` describing the presence of + :mod:`sage.libs.eclib`. + + EXAMPLES: + + This module is runtime detectable if feature checks are deferred:: + + sage: from sage.features.sagemath import sage__libs__eclib + sage: from sage.config import defer_feature_checks + sage: e = sage__libs__eclib() + sage: (not defer_feature_checks) or e.is_runtime_detectable() + True + + TESTS:: + + sage: from sage.features.sagemath import sage__libs__eclib + sage: sage__libs__eclib().is_present() # needs sage.libs.eclib + FeatureTestResult('sage.libs.eclib', True) + + """ + _enabled_in_build = eclib_enabled + + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.sagemath import sage__libs__eclib + sage: isinstance(sage__libs__eclib(), sage__libs__eclib) + True + + """ + super().__init__("sage.libs.eclib", spkg="eclib", type="standard") + + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.sagemath import sage__libs__eclib + sage: result = sage__libs__eclib().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs sage.libs.eclib + FeatureTestResult('sage.libs.eclib', True) + + """ + # The build system installs the sage/libs/eclib source code + # even when eclib support was disabled, so a naive import of + # that module will actually succeed. We check for a + # conditionally-compiled cython module instead. + result = PythonModule("sage.libs.eclib.mwrank")._is_present() + result.feature = self + return result + + class sage__libs__giac(JoinFeature): r""" A :class:`sage.features.Feature` describing the presence of :mod:`sage.libs.giac`. @@ -941,28 +999,48 @@ def __init__(self): type='standard') -class sage__rings__polynomial__pbori(JoinFeature): +class sage__rings__polynomial__pbori(BuildFeature, PythonModule): r""" A :class:`sage.features.Feature` describing the presence of :mod:`sage.rings.polynomial.pbori`. TESTS:: sage: from sage.features.sagemath import sage__rings__polynomial__pbori - sage: sage__rings__polynomial__pbori().is_present() # needs sage.rings.polynomial.pbori + sage: sage__rings__polynomial__pbori().is_present() # needs sage.rings.polynomial.pbori FeatureTestResult('sage.rings.polynomial.pbori', True) + """ + _enabled_in_build = brial_enabled + def __init__(self): r""" TESTS:: sage: from sage.features.sagemath import sage__rings__polynomial__pbori - sage: isinstance(sage__rings__polynomial__pbori(), sage__rings__polynomial__pbori) + sage: isinstance(sage__rings__polynomial__pbori(), + ....: sage__rings__polynomial__pbori) True + """ - JoinFeature.__init__(self, 'sage.rings.polynomial.pbori', - [PythonModule('sage.rings.polynomial.pbori.pbori')], - spkg='sagemath_brial', type='standard') + super().__init__('sage.rings.polynomial.pbori', + spkg='sagemath_brial', + type='standard') + + def is_present_at_runtime(self): + r""" + TESTS:: + sage: from sage.features import FeatureTestResult + sage: from sage.features.sagemath import sage__rings__polynomial__pbori + sage: pbori = sage__rings__polynomial__pbori() + sage: result = pbori.is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs sage.rings.polynomial.pbori + FeatureTestResult('sage.rings.polynomial.pbori', True) + + """ + return PythonModule._is_present(self) class sage__rings__real_double(PythonModule): r""" diff --git a/src/sage/features/sirocco.py b/src/sage/features/sirocco.py index 727513f940f..587cbc65304 100644 --- a/src/sage/features/sirocco.py +++ b/src/sage/features/sirocco.py @@ -13,20 +13,24 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import sirocco_enabled +from sage.features import PythonModule +from sage.features.build_feature import BuildFeature - -class Sirocco(JoinFeature): +class Sirocco(BuildFeature): r""" - A :class:`~sage.features.Feature` which describes whether the :mod:`sage.libs.sirocco` - module is available in this installation of Sage. + A :class:`~sage.features.Feature` which describes whether the + :mod:`sage.libs.sirocco` module is available in this installation + of Sage. EXAMPLES:: sage: from sage.features.sirocco import Sirocco - sage: Sirocco().require() # optional - sirocco + sage: Sirocco().require() # needs sirocco + """ + _enabled_in_build = sirocco_enabled + def __init__(self): r""" TESTS:: @@ -34,11 +38,26 @@ def __init__(self): sage: from sage.features.sirocco import Sirocco sage: Sirocco() Feature('sirocco') + """ - JoinFeature.__init__(self, "sirocco", - [PythonModule("sage.libs.sirocco", - spkg='sirocco')]) + super().__init__("sirocco", spkg='sirocco') + + def is_present_at_runtime(self): + r""" + TESTS:: + sage: from sage.features import FeatureTestResult + sage: from sage.features.sirocco import Sirocco + sage: result = Sirocco().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs sirocco + FeatureTestResult('sirocco', True) + + """ + result = PythonModule("sage.libs.sirocco")._is_present() + result.feature = self + return result def all_features(): return [Sirocco()] diff --git a/src/sage/features/tdlib.py b/src/sage/features/tdlib.py index b47f9d8db9d..e11848dc41f 100644 --- a/src/sage/features/tdlib.py +++ b/src/sage/features/tdlib.py @@ -12,14 +12,17 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import tdlib_enabled +from sage.features import PythonModule +from sage.features.build_feature import BuildFeature - -class Tdlib(JoinFeature): +class Tdlib(BuildFeature): r""" - A :class:`~sage.features.Feature` describing the presence of the SageMath interface to the :ref:`tdlib ` library. + A :class:`~sage.features.Feature` describing the presence of + the SageMath interface to the :ref:`tdlib ` library. """ + _enabled_in_build = tdlib_enabled + def __init__(self): r""" TESTS:: @@ -27,11 +30,27 @@ def __init__(self): sage: from sage.features.tdlib import Tdlib sage: isinstance(Tdlib(), Tdlib) True + """ - JoinFeature.__init__(self, 'tdlib', - [PythonModule('sage.graphs.graph_decompositions.tdlib', - spkg='tdlib')]) + super().__init__('tdlib', spkg='tdlib') + + def is_present_at_runtime(self): + r""" + TESTS:: + sage: from sage.features import FeatureTestResult + sage: from sage.features.tdlib import Tdlib + sage: result = Tdlib().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs tdlib + FeatureTestResult('tdlib', True) + + """ + modname = "sage.graphs.graph_decompositions.tdlib" + result = PythonModule(modname)._is_present() + result.feature = self + return result def all_features(): return [Tdlib()] diff --git a/src/sage/graphs/graph_decompositions/meson.build b/src/sage/graphs/graph_decompositions/meson.build index 06763ab1771..bfe106dc0d0 100644 --- a/src/sage/graphs/graph_decompositions/meson.build +++ b/src/sage/graphs/graph_decompositions/meson.build @@ -83,3 +83,7 @@ py.extension_module( dependencies: [py_dep, cysignals, tdlib], ) +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('RANKWIDTH_ENABLED', rw.found()) +conf_data.set10('TDLIB_ENABLED', tdlib.found()) diff --git a/src/sage/graphs/graph_decompositions/rankwidth.pyx b/src/sage/graphs/graph_decompositions/rankwidth.pyx index f5714cd9387..e1092a200ee 100644 --- a/src/sage/graphs/graph_decompositions/rankwidth.pyx +++ b/src/sage/graphs/graph_decompositions/rankwidth.pyx @@ -46,6 +46,7 @@ i.e. singletons. :: + sage: # needs rankwidth sage: g = graphs.PetersenGraph() sage: rw, tree = g.rank_decomposition() sage: all(len(v)==1 for v in tree if tree.degree(v) == 1) @@ -58,6 +59,7 @@ from the smaller of the two and its complement. :: + sage: # needs rankwidth sage: g = graphs.PetersenGraph() sage: rw, tree = g.rank_decomposition() sage: u = Set([8, 9, 3, 7]) @@ -80,6 +82,7 @@ from the smaller of the two and its complement. EXAMPLES:: + sage: # needs rankwidth sage: g = graphs.PetersenGraph() sage: g.rank_decomposition() (3, Graph on 19 vertices) @@ -137,6 +140,7 @@ def rank_decomposition(G, verbose=False, immutable=None): EXAMPLES:: + sage: # needs rankwidth sage: from sage.graphs.graph_decompositions.rankwidth import rank_decomposition sage: g = graphs.PetersenGraph() sage: rank_decomposition(g) @@ -144,6 +148,7 @@ def rank_decomposition(G, verbose=False, immutable=None): On more than 32 vertices:: + sage: # needs rankwidth sage: g = graphs.RandomGNP(40, .5) sage: rank_decomposition(g) Traceback (most recent call last): @@ -152,6 +157,7 @@ def rank_decomposition(G, verbose=False, immutable=None): The empty graph:: + sage: # needs rankwidth sage: g = Graph() sage: rank_decomposition(g) (0, Graph on 0 vertices) @@ -302,6 +308,7 @@ def mkgraph(int num_vertices): EXAMPLES:: + sage: # needs rankwidth sage: from sage.graphs.graph_decompositions.rankwidth import rank_decomposition sage: g = graphs.PetersenGraph() sage: rank_decomposition(g) diff --git a/src/sage/graphs/meson.build b/src/sage/graphs/meson.build index fce249794c7..6ab180db024 100644 --- a/src/sage/graphs/meson.build +++ b/src/sage/graphs/meson.build @@ -177,6 +177,11 @@ py.extension_module( dependencies: [py_dep, cysignals, mcqd], ) +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('BLISS_ENABLED', bliss.found()) +conf_data.set10('MCQD_ENABLED', mcqd.found()) + subdir('base') subdir('generators') subdir('graph_decompositions') diff --git a/src/sage/libs/coxeter3/meson.build b/src/sage/libs/coxeter3/meson.build index caf5f30b535..af484dc98a2 100644 --- a/src/sage/libs/coxeter3/meson.build +++ b/src/sage/libs/coxeter3/meson.build @@ -25,3 +25,7 @@ foreach name, pyx : extension_data_cpp dependencies: [py_dep, cysignals, coxeter3], ) endforeach + +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('COXETER3_ENABLED', coxeter3.found()) diff --git a/src/sage/libs/meson.build b/src/sage/libs/meson.build index 923fd5416b2..5710553c74b 100644 --- a/src/sage/libs/meson.build +++ b/src/sage/libs/meson.build @@ -169,6 +169,11 @@ foreach name, pyx : extension_data_cpp ) endforeach +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('MEATAXE_ENABLED', mtx.found()) +conf_data.set10('SIROCCO_ENABLED', sirocco.found()) + subdir('arb') subdir('coxeter3') install_subdir('cremona', install_dir: sage_install_dir / 'libs') diff --git a/src/sage/meson.build b/src/sage/meson.build index d43e636a4e8..8682482bb58 100644 --- a/src/sage/meson.build +++ b/src/sage/meson.build @@ -173,6 +173,12 @@ configure_file( configuration: kernel_data, ) +# Record what build-time features (e.g. external package support) were +# enabled or disabled, and whether or not we should defer detection to +# runtime. +conf_data.set10('DEFER_FEATURE_CHECKS', get_option('defer_feature_checks')) +conf_data.set10('ECLIB_ENABLED', ec.found()) + # Write config file # Should be last so that subdir calls can modify the config data config_file = configure_file( diff --git a/src/sage/rings/polynomial/pbori/meson.build b/src/sage/rings/polynomial/pbori/meson.build index 3456379c864..f12a5a70273 100644 --- a/src/sage/rings/polynomial/pbori/meson.build +++ b/src/sage/rings/polynomial/pbori/meson.build @@ -62,3 +62,6 @@ foreach name, pyx : extension_data_cpp ) endforeach +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('BRIAL_ENABLED', brial.found() and brial_groebner.found())