class QMakePackage(PackageBase): """Specialized class for packages built using qmake. For more information on the qmake build system, see: http://doc.qt.io/qt-5/qmake-manual.html This class provides three phases that can be overridden: 1. :py:meth:`~.QMakePackage.qmake` 2. :py:meth:`~.QMakePackage.build` 3. :py:meth:`~.QMakePackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override :py:meth:`~.QMakePackage.qmake_args`. """ #: Phases of a qmake package phases = ['qmake', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'QMakePackage' #: Callback names for build-time test build_time_test_callbacks = ['check'] depends_on('qt', type='build') def qmake_args(self): """Produces a list containing all the arguments that must be passed to qmake """ return [] def qmake(self, spec, prefix): """Run ``qmake`` to configure the project and generate a Makefile.""" inspect.getmodule(self).qmake(*self.qmake_args()) def build(self, spec, prefix): """Make the build targets""" inspect.getmodule(self).make() def install(self, spec, prefix): """Make the install targets""" inspect.getmodule(self).make('install') # Tests def check(self): """Searches the Makefile for a ``check:`` target and runs it if found. """ self._if_make_target_execute('check') run_after('build')(PackageBase._run_default_build_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class RPackage(PackageBase): """Specialized class for packages that are built using R This class provides a single phase that can be overridden: 1. :py:meth:`~.RPackage.install` It has sensible defaults, and for many packages the only thing necessary will be to add dependencies """ phases = ['install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'RPackage' extends('r') depends_on('r', type=('build', 'run')) def install(self, spec, prefix): """Installs an R package.""" inspect.getmodule(self).R( 'CMD', 'INSTALL', '--library={0}'.format(self.module.r_lib_dir), self.stage.source_path) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class RPackage(PackageBase): """Specialized class for packages that are built using R. For more information on the R build system, see: https://stat.ethz.ch/R-manual/R-devel/library/utils/html/INSTALL.html This class provides a single phase that can be overridden: 1. :py:meth:`~.RPackage.install` It has sensible defaults, and for many packages the only thing necessary will be to add dependencies """ phases = ['install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'RPackage' extends('r') depends_on('r', type=('build', 'run')) def configure_args(self): """Arguments to pass to install via ``--configure-args``.""" return [] def configure_vars(self): """Arguments to pass to install via ``--configure-vars``.""" return [] def install(self, spec, prefix): """Installs an R package.""" config_args = self.configure_args() config_vars = self.configure_vars() args = [ 'CMD', 'INSTALL' ] if config_args: args.append('--configure-args={0}'.format(' '.join(config_args))) if config_vars: args.append('--configure-vars={0}'.format(' '.join(config_vars))) args.extend([ '--library={0}'.format(self.module.r_lib_dir), self.stage.source_path ]) inspect.getmodule(self).R(*args) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class RubyPackage(PackageBase): """Specialized class for building Ruby gems. This class provides two phases that can be overridden if required: #. :py:meth:`~.RubyPackage.build` #. :py:meth:`~.RubyPackage.install` """ maintainers = ['Kerilk'] #: Phases of a Ruby package phases = ['build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'RubyPackage' extends('ruby') def build(self, spec, prefix): """Build a Ruby gem.""" # ruby-rake provides both rake.gemspec and Rakefile, but only # rake.gemspec can be built without an existing rake installation gemspecs = glob.glob('*.gemspec') rakefiles = glob.glob('Rakefile') if gemspecs: inspect.getmodule(self).gem('build', '--norc', gemspecs[0]) elif rakefiles: jobs = inspect.getmodule(self).make_jobs inspect.getmodule(self).rake('package', '-j{0}'.format(jobs)) else: # Some Ruby packages only ship `*.gem` files, so nothing to build pass def install(self, spec, prefix): """Install a Ruby gem. The ruby package sets ``GEM_HOME`` to tell gem where to install to.""" gems = glob.glob('*.gem') if gems: # if --install-dir is not used, GEM_PATH is deleted from the # environement, and Gems required to build native extensions will # not be found. Those extensions are built during `gem install`. inspect.getmodule(self).gem('install', '--norc', '--ignore-dependencies', '--install-dir', prefix, gems[0]) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class MavenPackage(PackageBase): """Specialized class for packages that are built using the Maven build system. See https://maven.apache.org/index.html for more information. This class provides the following phases that can be overridden: * build * install """ # Default phases phases = ['build', 'install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'MavenPackage' depends_on('java', type=('build', 'run')) depends_on('maven', type='build') @property def build_directory(self): """The directory containing the ``pom.xml`` file.""" return self.stage.source_path def build_args(self): """List of args to pass to build phase.""" return [] def build(self, spec, prefix): """Compile code and package into a JAR file.""" with working_dir(self.build_directory): mvn = which('mvn') if self.run_tests: mvn('verify', *self.build_args()) else: mvn('package', '-DskipTests', *self.build_args()) def install(self, spec, prefix): """Copy to installation prefix.""" with working_dir(self.build_directory): install_tree('.', prefix) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class TestInstallCallbacks(Package): """This package illustrates install callback test failure.""" homepage = "http://www.example.com/test-install-callbacks" url = "http://www.test-failure.test/test-install-callbacks-1.0.tar.gz" version('1.0', '0123456789abcdef0123456789abcdef') # Include an undefined callback method install_time_test_callbacks = ['undefined-install-test', 'test'] run_after('install')(Package._run_default_install_time_test_callbacks) def install(self, spec, prefix): mkdirp(prefix.bin) def test(self): print('test: test-install-callbacks') print('PASSED')
class OctavePackage(PackageBase): """Specialized class for Octave packages. See https://www.gnu.org/software/octave/doc/v4.2.0/Installing-and-Removing-Packages.html for more information. This class provides the following phases that can be overridden: 1. :py:meth:`~.OctavePackage.install` """ # Default phases phases = ['install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'OctavePackage' extends('octave') depends_on('octave', type=('build', 'run')) def setup_environment(self, spack_env, run_env): """Set up the compile and runtime environments for a package.""" # octave does not like those environment variables to be set: spack_env.unset('CC') spack_env.unset('CXX') spack_env.unset('FC') def install(self, spec, prefix): """Install the package from the archive file""" inspect.getmodule(self).octave( '--quiet', '--norc', '--built-in-docstrings-file=/dev/null', '--texi-macros-file=/dev/null', '--eval', 'pkg prefix %s; pkg install %s' % (prefix, self.stage.archive_file)) # Testing # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class TestBuildCallbacks(Package): """This package illustrates build callback test failure.""" homepage = "http://www.example.com/test-build-callbacks" url = "http://www.test-failure.test/test-build-callbacks-1.0.tar.gz" version('1.0', '0123456789abcdef0123456789abcdef') phases = ['build', 'install'] # Include undefined method (runtime failure) and 'test' (audit failure) build_time_test_callbacks = ['undefined-build-test', 'test'] run_after('build')(Package._run_default_build_time_test_callbacks) def build(self, spec, prefix): pass def install(self, spec, prefix): mkdirp(prefix.bin) def test(self): print('test: running test-build-callbacks') print('PASSED')
class CMakePackage(PackageBase): """Specialized class for packages built using CMake For more information on the CMake build system, see: https://cmake.org/cmake/help/latest/ This class provides three phases that can be overridden: 1. :py:meth:`~.CMakePackage.cmake` 2. :py:meth:`~.CMakePackage.build` 3. :py:meth:`~.CMakePackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override :py:meth:`~.CMakePackage.cmake_args`. For a finer tuning you may also override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:meth:`~.CMakePackage.root_cmakelists_dir` | Location of the | | | root CMakeLists.txt| +-----------------------------------------------+--------------------+ | :py:meth:`~.CMakePackage.build_directory` | Directory where to | | | build the package | +-----------------------------------------------+--------------------+ """ #: Phases of a CMake package phases = ['cmake', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'CMakePackage' build_targets = [] install_targets = ['install'] build_time_test_callbacks = ['check'] #: The build system generator to use. #: #: See ``cmake --help`` for a list of valid generators. #: Currently, "Unix Makefiles" and "Ninja" are the only generators #: that Spack supports. Defaults to "Unix Makefiles". #: #: See https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html #: for more information. generator = 'Unix Makefiles' # https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html variant('build_type', default='RelWithDebInfo', description='CMake build type', values=('Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel')) depends_on('cmake', type='build') @property def archive_files(self): """Files to archive for packages based on CMake""" return [os.path.join(self.build_directory, 'CMakeCache.txt')] @property def root_cmakelists_dir(self): """The relative path to the directory containing CMakeLists.txt This path is relative to the root of the extracted tarball, not to the ``build_directory``. Defaults to the current directory. :return: directory containing CMakeLists.txt """ return self.stage.source_path @property def std_cmake_args(self): """Standard cmake arguments provided as a property for convenience of package writers :return: standard cmake arguments """ # standard CMake arguments std_cmake_args = CMakePackage._std_args(self) std_cmake_args += getattr(self, 'cmake_flag_args', []) return std_cmake_args @staticmethod def _std_args(pkg): """Computes the standard cmake arguments for a generic package""" try: generator = pkg.generator except AttributeError: generator = 'Unix Makefiles' # Make sure a valid generator was chosen valid_generators = ['Unix Makefiles', 'Ninja'] if generator not in valid_generators: msg = "Invalid CMake generator: '{0}'\n".format(generator) msg += "CMakePackage currently supports the following " msg += "generators: '{0}'".format("', '".join(valid_generators)) raise InstallError(msg) try: build_type = pkg.spec.variants['build_type'].value except KeyError: build_type = 'RelWithDebInfo' args = [ '-G', generator, '-DCMAKE_INSTALL_PREFIX:PATH={0}'.format(pkg.prefix), '-DCMAKE_BUILD_TYPE:STRING={0}'.format(build_type), '-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON' ] if platform.mac_ver()[0]: args.extend([ '-DCMAKE_FIND_FRAMEWORK:STRING=LAST', '-DCMAKE_FIND_APPBUNDLE:STRING=LAST' ]) # Set up CMake rpath args.append('-DCMAKE_INSTALL_RPATH_USE_LINK_PATH:BOOL=FALSE') rpaths = ';'.join(spack.build_environment.get_rpaths(pkg)) args.append('-DCMAKE_INSTALL_RPATH:STRING={0}'.format(rpaths)) # CMake's find_package() looks in CMAKE_PREFIX_PATH first, help CMake # to find immediate link dependencies in right places: deps = [ d.prefix for d in pkg.spec.dependencies(deptype=('build', 'link')) ] deps = filter_system_paths(deps) args.append('-DCMAKE_PREFIX_PATH:STRING={0}'.format(';'.join(deps))) return args def flags_to_build_system_args(self, flags): """Produces a list of all command line arguments to pass the specified compiler flags to cmake. Note CMAKE does not have a cppflags option, so cppflags will be added to cflags, cxxflags, and fflags to mimic the behavior in other tools.""" # Has to be dynamic attribute due to caching setattr(self, 'cmake_flag_args', []) flag_string = '-DCMAKE_{0}_FLAGS={1}' langs = {'C': 'c', 'CXX': 'cxx', 'Fortran': 'f'} # Handle language compiler flags for lang, pre in langs.items(): flag = pre + 'flags' # cmake has no explicit cppflags support -> add it to all langs lang_flags = ' '.join( flags.get(flag, []) + flags.get('cppflags', [])) if lang_flags: self.cmake_flag_args.append( flag_string.format(lang, lang_flags)) # Cmake has different linker arguments for different build types. # We specify for each of them. if flags['ldflags']: ldflags = ' '.join(flags['ldflags']) ld_string = '-DCMAKE_{0}_LINKER_FLAGS={1}' # cmake has separate linker arguments for types of builds. for type in ['EXE', 'MODULE', 'SHARED', 'STATIC']: self.cmake_flag_args.append(ld_string.format(type, ldflags)) # CMake has libs options separated by language. Apply ours to each. if flags['ldlibs']: libs_flags = ' '.join(flags['ldlibs']) libs_string = '-DCMAKE_{0}_STANDARD_LIBRARIES={1}' for lang in langs: self.cmake_flag_args.append( libs_string.format(lang, libs_flags)) @property def build_directory(self): """Returns the directory to use when building the package :return: directory where to build the package """ return os.path.join(self.stage.source_path, 'spack-build') def cmake_args(self): """Produces a list containing all the arguments that must be passed to cmake, except: * CMAKE_INSTALL_PREFIX * CMAKE_BUILD_TYPE which will be set automatically. :return: list of arguments for cmake """ return [] def cmake(self, spec, prefix): """Runs ``cmake`` in the build directory""" options = [os.path.abspath(self.root_cmakelists_dir)] options += self.std_cmake_args options += self.cmake_args() with working_dir(self.build_directory, create=True): inspect.getmodule(self).cmake(*options) def build(self, spec, prefix): """Make the build targets""" with working_dir(self.build_directory): if self.generator == 'Unix Makefiles': inspect.getmodule(self).make(*self.build_targets) elif self.generator == 'Ninja': inspect.getmodule(self).ninja(*self.build_targets) def install(self, spec, prefix): """Make the install targets""" with working_dir(self.build_directory): if self.generator == 'Unix Makefiles': inspect.getmodule(self).make(*self.install_targets) elif self.generator == 'Ninja': inspect.getmodule(self).ninja(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the CMake-generated Makefile for the target ``test`` and runs it if found. """ with working_dir(self.build_directory): if self.generator == 'Unix Makefiles': self._if_make_target_execute('test') self._if_make_target_execute('check') elif self.generator == 'Ninja': self._if_ninja_target_execute('test') self._if_ninja_target_execute('check') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class SIPPackage(PackageBase): """Specialized class for packages that are built using the SIP build system. See https://www.riverbankcomputing.com/software/sip/intro for more information. This class provides the following phases that can be overridden: * configure * build * install The configure phase already adds a set of default flags. To see more options, run ``python configure.py --help``. """ # Default phases phases = ['configure', 'build', 'install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'SIPPackage' #: Name of private sip module to install alongside package sip_module = 'sip' #: Callback names for install-time test install_time_test_callbacks = ['import_module_test'] extends('python') depends_on('qt') depends_on('py-sip') def python(self, *args, **kwargs): """The python ``Executable``.""" inspect.getmodule(self).python(*args, **kwargs) def configure_file(self): """Returns the name of the configure file to use.""" return 'configure.py' def configure(self, spec, prefix): """Configure the package.""" configure = self.configure_file() args = self.configure_args() python_include_dir = 'python' + str(spec['python'].version.up_to(2)) args.extend([ '--verbose', '--confirm-license', '--qmake', spec['qt'].prefix.bin.qmake, '--sip', spec['py-sip'].prefix.bin.sip, '--sip-incdir', join_path(spec['py-sip'].prefix.include, python_include_dir), '--bindir', prefix.bin, '--destdir', inspect.getmodule(self).site_packages_dir, ]) self.python(configure, *args) def configure_args(self): """Arguments to pass to configure.""" return [] def build(self, spec, prefix): """Build the package.""" args = self.build_args() inspect.getmodule(self).make(*args) def build_args(self): """Arguments to pass to build.""" return [] def install(self, spec, prefix): """Install the package.""" args = self.install_args() inspect.getmodule(self).make('install', parallel=False, *args) def install_args(self): """Arguments to pass to install.""" return [] # Testing def import_module_test(self): """Attempts to import the module that was just installed. This test is only run if the package overrides :py:attr:`import_modules` with a list of module names.""" # Make sure we are importing the installed modules, # not the ones in the current directory with working_dir('spack-test', create=True): for module in self.import_modules: self.python('-c', 'import {0}'.format(module)) run_after('install')(PackageBase._run_default_install_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix) @run_after('install') def extend_path_setup(self): # See github issue #14121 and PR #15297 module = self.spec['py-sip'].variants['module'].value if module != 'sip': module = module.split('.')[0] with working_dir(inspect.getmodule(self).site_packages_dir): with open(os.path.join(module, '__init__.py'), 'a') as f: f.write('from pkgutil import extend_path\n') f.write('__path__ = extend_path(__path__, __name__)\n')
class MesonPackage(PackageBase): """Specialized class for packages built using Meson For more information on the Meson build system, see: https://mesonbuild.com/ This class provides three phases that can be overridden: 1. :py:meth:`~.MesonPackage.meson` 2. :py:meth:`~.MesonPackage.build` 3. :py:meth:`~.MesonPackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override :py:meth:`~.MesonPackage.meson_args`. For a finer tuning you may also override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:meth:`~.MesonPackage.root_mesonlists_dir` | Location of the | | | root MesonLists.txt| +-----------------------------------------------+--------------------+ | :py:meth:`~.MesonPackage.build_directory` | Directory where to | | | build the package | +-----------------------------------------------+--------------------+ """ #: Phases of a Meson package phases = ['meson', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'MesonPackage' build_targets = [] # type: List[str] install_targets = ['install'] build_time_test_callbacks = ['check'] variant('buildtype', default='debugoptimized', description='Meson build type', values=('plain', 'debug', 'debugoptimized', 'release', 'minsize')) variant('default_library', default='shared', description=' Default library type', values=('shared', 'static', 'both')) variant('strip', default=False, description='Strip targets on install') depends_on('meson', type='build') depends_on('ninja', type='build') @property def archive_files(self): """Files to archive for packages based on Meson""" return [os.path.join(self.build_directory, 'meson-logs/meson-log.txt')] @property def root_mesonlists_dir(self): """The relative path to the directory containing meson.build This path is relative to the root of the extracted tarball, not to the ``build_directory``. Defaults to the current directory. :return: directory containing meson.build """ return self.stage.source_path @property def std_meson_args(self): """Standard meson arguments provided as a property for convenience of package writers :return: standard meson arguments """ # standard Meson arguments std_meson_args = MesonPackage._std_args(self) std_meson_args += getattr(self, 'meson_flag_args', []) return std_meson_args @staticmethod def _std_args(pkg): """Computes the standard meson arguments for a generic package""" try: build_type = pkg.spec.variants['buildtype'].value except KeyError: build_type = 'release' strip = 'true' if '+strip' in pkg.spec else 'false' try: default_library = pkg.spec.variants['default_library'].value except KeyError: default_library = 'shared' args = [ '--prefix={0}'.format(pkg.prefix), # If we do not specify libdir explicitly, Meson chooses something # like lib/x86_64-linux-gnu, which causes problems when trying to # find libraries and pkg-config files. # See https://github.com/mesonbuild/meson/issues/2197 '--libdir={0}'.format(pkg.prefix.lib), '-Dbuildtype={0}'.format(build_type), '-Dstrip={0}'.format(strip), '-Ddefault_library={0}'.format(default_library) ] return args def flags_to_build_system_args(self, flags): """Produces a list of all command line arguments to pass the specified compiler flags to meson.""" # Has to be dynamic attribute due to caching setattr(self, 'meson_flag_args', []) @property def build_directory(self): """Returns the directory to use when building the package :return: directory where to build the package """ return os.path.join(self.stage.source_path, 'spack-build') def meson_args(self): """Produces a list containing all the arguments that must be passed to meson, except: * ``--prefix`` * ``--libdir`` * ``--buildtype`` * ``--strip`` * ``--default_library`` which will be set automatically. :return: list of arguments for meson """ return [] def meson(self, spec, prefix): """Runs ``meson`` in the build directory""" options = [os.path.abspath(self.root_mesonlists_dir)] options += self.std_meson_args options += self.meson_args() with working_dir(self.build_directory, create=True): inspect.getmodule(self).meson(*options) def build(self, spec, prefix): """Make the build targets""" options = ['-v'] options += self.build_targets with working_dir(self.build_directory): inspect.getmodule(self).ninja(*options) def install(self, spec, prefix): """Make the install targets""" with working_dir(self.build_directory): inspect.getmodule(self).ninja(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the Meson-generated file for the target ``test`` and runs it if found. """ with working_dir(self.build_directory): self._if_ninja_target_execute('test') self._if_ninja_target_execute('check') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class AutotoolsPackage(PackageBase): """Specialized class for packages built using GNU Autotools. This class provides four phases that can be overridden: 1. :py:meth:`~.AutotoolsPackage.autoreconf` 2. :py:meth:`~.AutotoolsPackage.configure` 3. :py:meth:`~.AutotoolsPackage.build` 4. :py:meth:`~.AutotoolsPackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override the helper method :meth:`~spack.build_systems.autotools.AutotoolsPackage.configure_args`. For a finer tuning you may also override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:attr:`~.AutotoolsPackage.build_targets` | Specify ``make`` | | | targets for the | | | build phase | +-----------------------------------------------+--------------------+ | :py:attr:`~.AutotoolsPackage.install_targets` | Specify ``make`` | | | targets for the | | | install phase | +-----------------------------------------------+--------------------+ | :py:meth:`~.AutotoolsPackage.check` | Run build time | | | tests if required | +-----------------------------------------------+--------------------+ """ #: Phases of a GNU Autotools package phases = ['autoreconf', 'configure', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'AutotoolsPackage' #: Whether or not to update ``config.guess`` and ``config.sub`` on old #: architectures patch_config_files = True #: Whether or not to update ``libtool`` #: (currently only for Arm/Clang/Fujitsu compilers) patch_libtool = True #: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.build` #: phase build_targets = [] # type: List[str] #: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.install` #: phase install_targets = ['install'] #: Callback names for build-time test build_time_test_callbacks = ['check'] #: Callback names for install-time test install_time_test_callbacks = ['installcheck'] #: Set to true to force the autoreconf step even if configure is present force_autoreconf = False #: Options to be passed to autoreconf when using the default implementation autoreconf_extra_args = [] # type: List[str] #: If False deletes all the .la files in the prefix folder #: after the installation. If True instead it installs them. install_libtool_archives = False @property def _removed_la_files_log(self): """File containing the list of remove libtool archives""" build_dir = self.build_directory if not os.path.isabs(self.build_directory): build_dir = os.path.join(self.stage.path, build_dir) return os.path.join(build_dir, 'removed_la_files.txt') @property def archive_files(self): """Files to archive for packages based on autotools""" files = [os.path.join(self.build_directory, 'config.log')] if not self.install_libtool_archives: files.append(self._removed_la_files_log) return files @run_after('autoreconf') def _do_patch_config_files(self): """Some packages ship with older config.guess/config.sub files and need to have these updated when installed on a newer architecture. In particular, config.guess fails for PPC64LE for version prior to a 2013-06-10 build date (automake 1.13.4) and for ARM (aarch64). """ if not self.patch_config_files or ( not self.spec.satisfies('target=ppc64le:') and not self.spec.satisfies('target=aarch64:')): return # TODO: Expand this to select the 'config.sub'-compatible architecture # for each platform (e.g. 'config.sub' doesn't accept 'power9le', but # does accept 'ppc64le'). if self.spec.satisfies('target=ppc64le:'): config_arch = 'ppc64le' elif self.spec.satisfies('target=aarch64:'): config_arch = 'aarch64' else: config_arch = 'local' def runs_ok(script_abs_path): # Construct the list of arguments for the call additional_args = {'config.sub': [config_arch]} script_name = os.path.basename(script_abs_path) args = [script_abs_path] + additional_args.get(script_name, []) try: check_call(args, stdout=PIPE, stderr=PIPE) except Exception as e: tty.debug(e) return False return True # Compute the list of files that needs to be patched search_dir = self.stage.path to_be_patched = fs.find(search_dir, files=['config.sub', 'config.guess'], recursive=True) to_be_patched = [f for f in to_be_patched if not runs_ok(f)] # If there are no files to be patched, return early if not to_be_patched: return # Directories where to search for files to be copied # over the failing ones good_file_dirs = ['/usr/share'] if 'automake' in self.spec: good_file_dirs.insert(0, self.spec['automake'].prefix) # List of files to be found in the directories above to_be_found = list(set(os.path.basename(f) for f in to_be_patched)) substitutes = {} for directory in good_file_dirs: candidates = fs.find(directory, files=to_be_found, recursive=True) candidates = [f for f in candidates if runs_ok(f)] for name, good_files in itertools.groupby(candidates, key=os.path.basename): substitutes[name] = next(good_files) to_be_found.remove(name) # Check that we found everything we needed if to_be_found: msg = 'Failed to find suitable substitutes for {0}' raise RuntimeError(msg.format(', '.join(to_be_found))) # Copy the good files over the bad ones for abs_path in to_be_patched: name = os.path.basename(abs_path) mode = os.stat(abs_path).st_mode os.chmod(abs_path, stat.S_IWUSR) fs.copy(substitutes[name], abs_path) os.chmod(abs_path, mode) @run_before('configure') def _set_autotools_environment_variables(self): """Many autotools builds use a version of mknod.m4 that fails when running as root unless FORCE_UNSAFE_CONFIGURE is set to 1. We set this to 1 and expect the user to take responsibility if they are running as root. They have to anyway, as this variable doesn't actually prevent configure from doing bad things as root. Without it, configure just fails halfway through, but it can still run things *before* this check. Forcing this just removes a nuisance -- this is not circumventing any real protection. """ os.environ["FORCE_UNSAFE_CONFIGURE"] = "1" @run_after('configure') def _do_patch_libtool(self): """If configure generates a "libtool" script that does not correctly detect the compiler (and patch_libtool is set), patch in the correct flags for the Arm, Clang/Flang, and Fujitsu compilers.""" # Exit early if we are required not to patch libtool if not self.patch_libtool: return for libtool_path in fs.find(self.build_directory, 'libtool', recursive=True): self._patch_libtool(libtool_path) def _patch_libtool(self, libtool_path): if self.spec.satisfies('%arm')\ or self.spec.satisfies('%clang')\ or self.spec.satisfies('%fj'): fs.filter_file('wl=""\n', 'wl="-Wl,"\n', libtool_path) fs.filter_file( 'pic_flag=""\n', 'pic_flag="{0}"\n'.format(self.compiler.cc_pic_flag), libtool_path) if self.spec.satisfies('%fj'): fs.filter_file('-nostdlib', '', libtool_path) rehead = r'/\S*/' objfile = [ 'fjhpctag.o', 'fjcrt0.o', 'fjlang08.o', 'fjomp.o', 'crti.o', 'crtbeginS.o', 'crtendS.o' ] for o in objfile: fs.filter_file(rehead + o, '', libtool_path) @property def configure_directory(self): """Returns the directory where 'configure' resides. :return: directory where to find configure """ return self.stage.source_path @property def configure_abs_path(self): # Absolute path to configure configure_abs_path = os.path.join( os.path.abspath(self.configure_directory), 'configure') return configure_abs_path @property def build_directory(self): """Override to provide another place to build the package""" return self.configure_directory @run_before('autoreconf') def delete_configure_to_force_update(self): if self.force_autoreconf: force_remove(self.configure_abs_path) def autoreconf(self, spec, prefix): """Not needed usually, configure should be already there""" # If configure exists nothing needs to be done if os.path.exists(self.configure_abs_path): return # Else try to regenerate it autotools = ['m4', 'autoconf', 'automake', 'libtool'] missing = [x for x in autotools if x not in spec] if missing: msg = 'Cannot generate configure: missing dependencies {0}' raise RuntimeError(msg.format(missing)) tty.msg('Configure script not found: trying to generate it') tty.warn('*********************************************************') tty.warn('* If the default procedure fails, consider implementing *') tty.warn('* a custom AUTORECONF phase in the package *') tty.warn('*********************************************************') with working_dir(self.configure_directory): m = inspect.getmodule(self) # This line is what is needed most of the time # --install, --verbose, --force autoreconf_args = ['-ivf'] autoreconf_args += self.autoreconf_search_path_args autoreconf_args += self.autoreconf_extra_args m.autoreconf(*autoreconf_args) @property def autoreconf_search_path_args(self): """Arguments to autoreconf to modify the search paths""" search_path_args = [] for dep in self.spec.dependencies(deptype='build'): if os.path.exists(dep.prefix.share.aclocal): search_path_args.extend(['-I', dep.prefix.share.aclocal]) return search_path_args @run_after('autoreconf') def set_configure_or_die(self): """Checks the presence of a ``configure`` file after the autoreconf phase. If it is found sets a module attribute appropriately, otherwise raises an error. :raises RuntimeError: if a configure script is not found in :py:meth:`~AutotoolsPackage.configure_directory` """ # Check if a configure script is there. If not raise a RuntimeError. if not os.path.exists(self.configure_abs_path): msg = 'configure script not found in {0}' raise RuntimeError(msg.format(self.configure_directory)) # Monkey-patch the configure script in the corresponding module inspect.getmodule(self).configure = Executable(self.configure_abs_path) def configure_args(self): """Produces a list containing all the arguments that must be passed to configure, except ``--prefix`` which will be pre-pended to the list. :return: list of arguments for configure """ return [] def flags_to_build_system_args(self, flags): """Produces a list of all command line arguments to pass specified compiler flags to configure.""" # Has to be dynamic attribute due to caching. setattr(self, 'configure_flag_args', []) for flag, values in flags.items(): if values: values_str = '{0}={1}'.format(flag.upper(), ' '.join(values)) self.configure_flag_args.append(values_str) # Spack's fflags are meant for both F77 and FC, therefore we # additionaly set FCFLAGS if required. values = flags.get('fflags', None) if values: values_str = 'FCFLAGS={0}'.format(' '.join(values)) self.configure_flag_args.append(values_str) def configure(self, spec, prefix): """Runs configure with the arguments specified in :meth:`~spack.build_systems.autotools.AutotoolsPackage.configure_args` and an appropriately set prefix. """ options = getattr(self, 'configure_flag_args', []) options += ['--prefix={0}'.format(prefix)] options += self.configure_args() with working_dir(self.build_directory, create=True): inspect.getmodule(self).configure(*options) def build(self, spec, prefix): """Makes the build targets specified by :py:attr:``~.AutotoolsPackage.build_targets`` """ # See https://autotools.io/automake/silent.html params = ['V=1'] params += self.build_targets with working_dir(self.build_directory): inspect.getmodule(self).make(*params) def install(self, spec, prefix): """Makes the install targets specified by :py:attr:``~.AutotoolsPackage.install_targets`` """ with working_dir(self.build_directory): inspect.getmodule(self).make(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the Makefile for targets ``test`` and ``check`` and runs them if found. """ with working_dir(self.build_directory): self._if_make_target_execute('test') self._if_make_target_execute('check') def _activate_or_not(self, name, activation_word, deactivation_word, activation_value=None): """This function contains the current implementation details of :meth:`~spack.build_systems.autotools.AutotoolsPackage.with_or_without` and :meth:`~spack.build_systems.autotools.AutotoolsPackage.enable_or_disable`. Args: name (str): name of the variant that is being processed activation_word (str): the default activation word ('with' in the case of ``with_or_without``) deactivation_word (str): the default deactivation word ('without' in the case of ``with_or_without``) activation_value (typing.Callable): callable that accepts a single value. This value is either one of the allowed values for a multi-valued variant or the name of a bool-valued variant. Returns the parameter to be used when the value is activated. The special value 'prefix' can also be assigned and will return ``spec[name].prefix`` as activation parameter. Examples: Given a package with: .. code-block:: python variant('foo', values=('x', 'y'), description='') variant('bar', default=True, description='') calling this function like: .. code-block:: python _activate_or_not( 'foo', 'with', 'without', activation_value='prefix' ) _activate_or_not('bar', 'with', 'without') will generate the following configuration options: .. code-block:: console --with-x=<prefix-to-x> --without-y --with-bar for ``<spec-name> foo=x +bar`` Returns: list: list of strings that corresponds to the activation/deactivation of the variant that has been processed Raises: KeyError: if name is not among known variants """ spec = self.spec args = [] if activation_value == 'prefix': activation_value = lambda x: spec[x].prefix # Defensively look that the name passed as argument is among # variants if name not in self.variants: msg = '"{0}" is not a variant of "{1}"' raise KeyError(msg.format(name, self.name)) # Create a list of pairs. Each pair includes a configuration # option and whether or not that option is activated if set(self.variants[name].values) == set((True, False)): # BoolValuedVariant carry information about a single option. # Nonetheless, for uniformity of treatment we'll package them # in an iterable of one element. condition = '+{name}'.format(name=name) options = [(name, condition in spec)] else: condition = '{name}={value}' # "feature_values" is used to track values which correspond to # features which can be enabled or disabled as understood by the # package's build system. It excludes values which have special # meanings and do not correspond to features (e.g. "none") feature_values = getattr(self.variants[name].values, 'feature_values', None) or self.variants[name].values options = [(value, condition.format(name=name, value=value) in spec) for value in feature_values] # For each allowed value in the list of values for option_value, activated in options: # Search for an override in the package for this value override_name = '{0}_or_{1}_{2}'.format(activation_word, deactivation_word, option_value) line_generator = getattr(self, override_name, None) # If not available use a sensible default if line_generator is None: def _default_generator(is_activated): if is_activated: line = '--{0}-{1}'.format(activation_word, option_value) if activation_value is not None and activation_value( option_value): # NOQA=ignore=E501 line += '={0}'.format( activation_value(option_value)) return line return '--{0}-{1}'.format(deactivation_word, option_value) line_generator = _default_generator args.append(line_generator(activated)) return args def with_or_without(self, name, activation_value=None): """Inspects a variant and returns the arguments that activate or deactivate the selected feature(s) for the configure options. This function works on all type of variants. For bool-valued variants it will return by default ``--with-{name}`` or ``--without-{name}``. For other kinds of variants it will cycle over the allowed values and return either ``--with-{value}`` or ``--without-{value}``. If activation_value is given, then for each possible value of the variant, the option ``--with-{value}=activation_value(value)`` or ``--without-{value}`` will be added depending on whether or not ``variant=value`` is in the spec. Args: name (str): name of a valid multi-valued variant activation_value (typing.Callable): callable that accepts a single value and returns the parameter to be used leading to an entry of the type ``--with-{name}={parameter}``. The special value 'prefix' can also be assigned and will return ``spec[name].prefix`` as activation parameter. Returns: list of arguments to configure """ return self._activate_or_not(name, 'with', 'without', activation_value) def enable_or_disable(self, name, activation_value=None): """Same as :meth:`~spack.build_systems.autotools.AutotoolsPackage.with_or_without` but substitute ``with`` with ``enable`` and ``without`` with ``disable``. Args: name (str): name of a valid multi-valued variant activation_value (typing.Callable): if present accepts a single value and returns the parameter to be used leading to an entry of the type ``--enable-{name}={parameter}`` The special value 'prefix' can also be assigned and will return ``spec[name].prefix`` as activation parameter. Returns: list of arguments to configure """ return self._activate_or_not(name, 'enable', 'disable', activation_value) run_after('install')(PackageBase._run_default_install_time_test_callbacks) def installcheck(self): """Searches the Makefile for an ``installcheck`` target and runs it if found. """ with working_dir(self.build_directory): self._if_make_target_execute('installcheck') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix) @run_after('install') def remove_libtool_archives(self): """Remove all .la files in prefix sub-folders if the package sets ``install_libtool_archives`` to be False. """ # If .la files are to be installed there's nothing to do if self.install_libtool_archives: return # Remove the files and create a log of what was removed libtool_files = fs.find(str(self.prefix), '*.la', recursive=True) with fs.safe_remove(*libtool_files): fs.mkdirp(os.path.dirname(self._removed_la_files_log)) with open(self._removed_la_files_log, mode='w') as f: f.write('\n'.join(libtool_files))
class SConsPackage(PackageBase): """Specialized class for packages built using SCons. See http://scons.org/documentation.html for more information. This class provides the following phases that can be overridden: 1. :py:meth:`~.SConsPackage.build` 2. :py:meth:`~.SConsPackage.install` Packages that use SCons as a build system are less uniform than packages that use other build systems. Developers can add custom subcommands or variables that control the build. You will likely need to override :py:meth:`~.SConsPackage.build_args` to pass the appropriate variables. """ #: Phases of a SCons package phases = ['build', 'install'] #: To be used in UI queries that require to know which #: build-system class we are using build_system_class = 'SConsPackage' #: Callback names for build-time test build_time_test_callbacks = ['test'] depends_on('scons', type='build') def build_args(self, spec, prefix): """Arguments to pass to build.""" return [] def build(self, spec, prefix): """Build the package.""" args = self.build_args(spec, prefix) inspect.getmodule(self).scons(*args) def install_args(self, spec, prefix): """Arguments to pass to install.""" return [] def install(self, spec, prefix): """Install the package.""" args = self.install_args(spec, prefix) inspect.getmodule(self).scons('install', *args) # Testing def test(self): """Run unit tests after build. By default, does nothing. Override this if you want to add package-specific tests. """ pass run_after('build')(PackageBase._run_default_build_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class AutotoolsPackage(PackageBase): """Specialized class for packages built using GNU Autotools. This class provides four phases that can be overridden: 1. :py:meth:`~.AutotoolsPackage.autoreconf` 2. :py:meth:`~.AutotoolsPackage.configure` 3. :py:meth:`~.AutotoolsPackage.build` 4. :py:meth:`~.AutotoolsPackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override the helper method :py:meth:`~.AutotoolsPackage.configure_args`. For a finer tuning you may also override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:attr:`~.AutotoolsPackage.build_targets` | Specify ``make`` | | | targets for the | | | build phase | +-----------------------------------------------+--------------------+ | :py:attr:`~.AutotoolsPackage.install_targets` | Specify ``make`` | | | targets for the | | | install phase | +-----------------------------------------------+--------------------+ | :py:meth:`~.AutotoolsPackage.check` | Run build time | | | tests if required | +-----------------------------------------------+--------------------+ """ #: Phases of a GNU Autotools package phases = ['autoreconf', 'configure', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'AutotoolsPackage' #: Whether or not to update ``config.guess`` on old architectures patch_config_guess = True #: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.build` #: phase build_targets = [] #: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.install` #: phase install_targets = ['install'] #: Callback names for build-time test build_time_test_callbacks = ['check'] #: Callback names for install-time test install_time_test_callbacks = ['installcheck'] #: Set to true to force the autoreconf step even if configure is present force_autoreconf = False #: Options to be passed to autoreconf when using the default implementation autoreconf_extra_args = [] def _do_patch_config_guess(self): """Some packages ship with an older config.guess and need to have this updated when installed on a newer architecture.""" my_config_guess = None config_guess = None if os.path.exists('config.guess'): # First search the top-level source directory my_config_guess = 'config.guess' else: # Then search in all sub directories. # We would like to use AC_CONFIG_AUX_DIR, but not all packages # ship with their configure.in or configure.ac. d = '.' dirs = [os.path.join(d, o) for o in os.listdir(d) if os.path.isdir(os.path.join(d, o))] for dirname in dirs: path = os.path.join(dirname, 'config.guess') if os.path.exists(path): my_config_guess = path if my_config_guess is not None: try: check_call([my_config_guess], stdout=PIPE, stderr=PIPE) # The package's config.guess already runs OK, so just use it return True except Exception: pass else: return True # Look for a spack-installed automake package if 'automake' in self.spec: automake_path = os.path.join(self.spec['automake'].prefix, 'share', 'automake-' + str(self.spec['automake'].version)) path = os.path.join(automake_path, 'config.guess') if os.path.exists(path): config_guess = path # Look for the system's config.guess if config_guess is None and os.path.exists('/usr/share'): automake_dir = [s for s in os.listdir('/usr/share') if "automake" in s] if automake_dir: automake_path = os.path.join('/usr/share', automake_dir[0]) path = os.path.join(automake_path, 'config.guess') if os.path.exists(path): config_guess = path if config_guess is not None: try: check_call([config_guess], stdout=PIPE, stderr=PIPE) mod = stat(my_config_guess).st_mode & 0o777 | S_IWUSR os.chmod(my_config_guess, mod) shutil.copyfile(config_guess, my_config_guess) return True except Exception: pass return False @property def configure_directory(self): """Returns the directory where 'configure' resides. :return: directory where to find configure """ return self.stage.source_path @property def configure_abs_path(self): # Absolute path to configure configure_abs_path = join_path( os.path.abspath(self.configure_directory), 'configure' ) return configure_abs_path @property def build_directory(self): """Override to provide another place to build the package""" return self.configure_directory def patch(self): """Patches config.guess if :py:attr:``~.AutotoolsPackage.patch_config_guess`` is True :raise RuntimeError: if something goes wrong when patching ``config.guess`` """ if self.patch_config_guess and self.spec.satisfies( 'arch=linux-rhel7-ppc64le' ): if not self._do_patch_config_guess(): raise RuntimeError('Failed to find suitable config.guess') @run_before('autoreconf') def delete_configure_to_force_update(self): if self.force_autoreconf: force_remove(self.configure_abs_path) def autoreconf(self, spec, prefix): """Not needed usually, configure should be already there""" # If configure exists nothing needs to be done if os.path.exists(self.configure_abs_path): return # Else try to regenerate it autotools = ['m4', 'autoconf', 'automake', 'libtool'] missing = [x for x in autotools if x not in spec] if missing: msg = 'Cannot generate configure: missing dependencies {0}' raise RuntimeError(msg.format(missing)) tty.msg('Configure script not found: trying to generate it') tty.warn('*********************************************************') tty.warn('* If the default procedure fails, consider implementing *') tty.warn('* a custom AUTORECONF phase in the package *') tty.warn('*********************************************************') with working_dir(self.configure_directory): m = inspect.getmodule(self) # This part should be redundant in principle, but # won't hurt m.libtoolize() m.aclocal() # This line is what is needed most of the time # --install, --verbose, --force autoreconf_args = ['-ivf'] if 'pkg-config' in spec: autoreconf_args += [ '-I', join_path(spec['pkg-config'].prefix, 'share', 'aclocal'), ] autoreconf_args += self.autoreconf_extra_args m.autoreconf(*autoreconf_args) @run_after('autoreconf') def set_configure_or_die(self): """Checks the presence of a ``configure`` file after the autoreconf phase. If it is found sets a module attribute appropriately, otherwise raises an error. :raises RuntimeError: if a configure script is not found in :py:meth:`~AutotoolsPackage.configure_directory` """ # Check if a configure script is there. If not raise a RuntimeError. if not os.path.exists(self.configure_abs_path): msg = 'configure script not found in {0}' raise RuntimeError(msg.format(self.configure_directory)) # Monkey-patch the configure script in the corresponding module inspect.getmodule(self).configure = Executable( self.configure_abs_path ) def configure_args(self): """Produces a list containing all the arguments that must be passed to configure, except ``--prefix`` which will be pre-pended to the list. :return: list of arguments for configure """ return [] def configure(self, spec, prefix): """Runs configure with the arguments specified in :py:meth:`~.AutotoolsPackage.configure_args` and an appropriately set prefix. """ options = ['--prefix={0}'.format(prefix)] + self.configure_args() with working_dir(self.build_directory, create=True): inspect.getmodule(self).configure(*options) def build(self, spec, prefix): """Makes the build targets specified by :py:attr:``~.AutotoolsPackage.build_targets`` """ with working_dir(self.build_directory): inspect.getmodule(self).make(*self.build_targets) def install(self, spec, prefix): """Makes the install targets specified by :py:attr:``~.AutotoolsPackage.install_targets`` """ with working_dir(self.build_directory): inspect.getmodule(self).make(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the Makefile for targets ``test`` and ``check`` and runs them if found. """ with working_dir(self.build_directory): self._if_make_target_execute('test') self._if_make_target_execute('check') def _activate_or_not(self, active, inactive, name, active_parameters=None): spec = self.spec args = [] # For each allowed value in the list of values for value in self.variants[name].values: # Check if the value is active in the current spec condition = '{name}={value}'.format(name=name, value=value) activated = condition in spec # Search for an override in the package for this value override_name = '{0}_or_{1}_{2}'.format(active, inactive, value) line_generator = getattr(self, override_name, None) # If not available use a sensible default if line_generator is None: def _default_generator(is_activated): if is_activated: line = '--{0}-{1}'.format(active, value) if active_parameters is not None and active_parameters(value): # NOQA=ignore=E501 line += '={0}'.format(active_parameters(value)) return line return '--{0}-{1}'.format(inactive, value) line_generator = _default_generator args.append(line_generator(activated)) return args def with_or_without(self, name, active_parameters=None): """Inspects the multi-valued variant 'name' and returns the configure arguments that activate / deactivate the selected feature. :param str name: name of a valid multi-valued variant :param callable active_parameters: if present accepts a single value and returns the parameter to be used leading to an entry of the type '--with-{name}={parameter} """ return self._activate_or_not( 'with', 'without', name, active_parameters ) def enable_or_disable(self, name, active_parameters=None): """Inspects the multi-valued variant 'name' and returns the configure arguments that activate / deactivate the selected feature. """ return self._activate_or_not( 'enable', 'disable', name, active_parameters ) run_after('install')(PackageBase._run_default_install_time_test_callbacks) def installcheck(self): """Searches the Makefile for an ``installcheck`` target and runs it if found. """ with working_dir(self.build_directory): self._if_make_target_execute('installcheck') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class AutotoolsPackage(PackageBase): """Specialized class for packages built using GNU Autotools. This class provides four phases that can be overridden: 1. :py:meth:`~.AutotoolsPackage.autoreconf` 2. :py:meth:`~.AutotoolsPackage.configure` 3. :py:meth:`~.AutotoolsPackage.build` 4. :py:meth:`~.AutotoolsPackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override the helper method :py:meth:`~.AutotoolsPackage.configure_args`. For a finer tuning you may also override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:attr:`~.AutotoolsPackage.build_targets` | Specify ``make`` | | | targets for the | | | build phase | +-----------------------------------------------+--------------------+ | :py:attr:`~.AutotoolsPackage.install_targets` | Specify ``make`` | | | targets for the | | | install phase | +-----------------------------------------------+--------------------+ | :py:meth:`~.AutotoolsPackage.check` | Run build time | | | tests if required | +-----------------------------------------------+--------------------+ """ #: Phases of a GNU Autotools package phases = ['autoreconf', 'configure', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'AutotoolsPackage' #: Whether or not to update ``config.guess`` on old architectures patch_config_guess = True #: Whether or not to update ``libtool`` #: (currently only for Arm/Clang/Fujitsu compilers) patch_libtool = True #: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.build` #: phase build_targets = [] #: Targets for ``make`` during the :py:meth:`~.AutotoolsPackage.install` #: phase install_targets = ['install'] #: Callback names for build-time test build_time_test_callbacks = ['check'] #: Callback names for install-time test install_time_test_callbacks = ['installcheck'] #: Set to true to force the autoreconf step even if configure is present force_autoreconf = False #: Options to be passed to autoreconf when using the default implementation autoreconf_extra_args = [] @property def archive_files(self): """Files to archive for packages based on autotools""" return [os.path.join(self.build_directory, 'config.log')] @run_after('autoreconf') def _do_patch_config_guess(self): """Some packages ship with an older config.guess and need to have this updated when installed on a newer architecture. In particular, config.guess fails for PPC64LE for version prior to a 2013-06-10 build date (automake 1.13.4) and for ARM (aarch64).""" if not self.patch_config_guess or ( not self.spec.satisfies('target=ppc64le:') and not self.spec.satisfies('target=aarch64:') ): return my_config_guess = None config_guess = None if os.path.exists('config.guess'): # First search the top-level source directory my_config_guess = 'config.guess' else: # Then search in all sub directories. # We would like to use AC_CONFIG_AUX_DIR, but not all packages # ship with their configure.in or configure.ac. d = '.' dirs = [os.path.join(d, o) for o in os.listdir(d) if os.path.isdir(os.path.join(d, o))] for dirname in dirs: path = os.path.join(dirname, 'config.guess') if os.path.exists(path): my_config_guess = path if my_config_guess is not None: try: check_call([my_config_guess], stdout=PIPE, stderr=PIPE) # The package's config.guess already runs OK, so just use it return except Exception as e: tty.debug(e) else: return # Look for a spack-installed automake package if 'automake' in self.spec: automake_path = os.path.join(self.spec['automake'].prefix, 'share', 'automake-' + str(self.spec['automake'].version)) path = os.path.join(automake_path, 'config.guess') if os.path.exists(path): config_guess = path # Look for the system's config.guess if config_guess is None and os.path.exists('/usr/share'): automake_dir = [s for s in os.listdir('/usr/share') if "automake" in s] if automake_dir: automake_path = os.path.join('/usr/share', automake_dir[0]) path = os.path.join(automake_path, 'config.guess') if os.path.exists(path): config_guess = path if config_guess is not None: try: check_call([config_guess], stdout=PIPE, stderr=PIPE) mod = os.stat(my_config_guess).st_mode & 0o777 | stat.S_IWUSR os.chmod(my_config_guess, mod) shutil.copyfile(config_guess, my_config_guess) return except Exception as e: tty.debug(e) raise RuntimeError('Failed to find suitable config.guess') @run_after('configure') def _do_patch_libtool(self): """If configure generates a "libtool" script that does not correctly detect the compiler (and patch_libtool is set), patch in the correct flags for the Arm, Clang/Flang, and Fujitsu compilers.""" libtool = os.path.join(self.build_directory, "libtool") if self.patch_libtool and os.path.exists(libtool): if self.spec.satisfies('%arm') or self.spec.satisfies('%clang') \ or self.spec.satisfies('%fj'): for line in fileinput.input(libtool, inplace=True): # Replace missing flags with those for Arm/Clang if line == 'wl=""\n': line = 'wl="-Wl,"\n' if line == 'pic_flag=""\n': line = 'pic_flag="{0}"\n'\ .format(self.compiler.pic_flag) sys.stdout.write(line) @property def configure_directory(self): """Returns the directory where 'configure' resides. :return: directory where to find configure """ return self.stage.source_path @property def configure_abs_path(self): # Absolute path to configure configure_abs_path = os.path.join( os.path.abspath(self.configure_directory), 'configure' ) return configure_abs_path @property def build_directory(self): """Override to provide another place to build the package""" return self.configure_directory @run_before('autoreconf') def delete_configure_to_force_update(self): if self.force_autoreconf: force_remove(self.configure_abs_path) def autoreconf(self, spec, prefix): """Not needed usually, configure should be already there""" # If configure exists nothing needs to be done if os.path.exists(self.configure_abs_path): return # Else try to regenerate it autotools = ['m4', 'autoconf', 'automake', 'libtool'] missing = [x for x in autotools if x not in spec] if missing: msg = 'Cannot generate configure: missing dependencies {0}' raise RuntimeError(msg.format(missing)) tty.msg('Configure script not found: trying to generate it') tty.warn('*********************************************************') tty.warn('* If the default procedure fails, consider implementing *') tty.warn('* a custom AUTORECONF phase in the package *') tty.warn('*********************************************************') with working_dir(self.configure_directory): m = inspect.getmodule(self) # This line is what is needed most of the time # --install, --verbose, --force autoreconf_args = ['-ivf'] if 'pkgconfig' in spec: autoreconf_args += [ '-I', os.path.join(spec['pkgconfig'].prefix, 'share', 'aclocal'), ] autoreconf_args += self.autoreconf_extra_args m.autoreconf(*autoreconf_args) @run_after('autoreconf') def set_configure_or_die(self): """Checks the presence of a ``configure`` file after the autoreconf phase. If it is found sets a module attribute appropriately, otherwise raises an error. :raises RuntimeError: if a configure script is not found in :py:meth:`~AutotoolsPackage.configure_directory` """ # Check if a configure script is there. If not raise a RuntimeError. if not os.path.exists(self.configure_abs_path): msg = 'configure script not found in {0}' raise RuntimeError(msg.format(self.configure_directory)) # Monkey-patch the configure script in the corresponding module inspect.getmodule(self).configure = Executable( self.configure_abs_path ) def configure_args(self): """Produces a list containing all the arguments that must be passed to configure, except ``--prefix`` which will be pre-pended to the list. :return: list of arguments for configure """ return [] def flags_to_build_system_args(self, flags): """Produces a list of all command line arguments to pass specified compiler flags to configure.""" # Has to be dynamic attribute due to caching. setattr(self, 'configure_flag_args', []) for flag, values in flags.items(): if values: values_str = '{0}={1}'.format(flag.upper(), ' '.join(values)) self.configure_flag_args.append(values_str) def configure(self, spec, prefix): """Runs configure with the arguments specified in :py:meth:`~.AutotoolsPackage.configure_args` and an appropriately set prefix. """ options = getattr(self, 'configure_flag_args', []) options += ['--prefix={0}'.format(prefix)] options += self.configure_args() with working_dir(self.build_directory, create=True): inspect.getmodule(self).configure(*options) def build(self, spec, prefix): """Makes the build targets specified by :py:attr:``~.AutotoolsPackage.build_targets`` """ with working_dir(self.build_directory): inspect.getmodule(self).make(*self.build_targets) def install(self, spec, prefix): """Makes the install targets specified by :py:attr:``~.AutotoolsPackage.install_targets`` """ with working_dir(self.build_directory): inspect.getmodule(self).make(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the Makefile for targets ``test`` and ``check`` and runs them if found. """ with working_dir(self.build_directory): self._if_make_target_execute('test') self._if_make_target_execute('check') def _activate_or_not( self, name, activation_word, deactivation_word, activation_value=None ): """This function contains the current implementation details of :py:meth:`~.AutotoolsPackage.with_or_without` and :py:meth:`~.AutotoolsPackage.enable_or_disable`. Args: name (str): name of the variant that is being processed activation_word (str): the default activation word ('with' in the case of ``with_or_without``) deactivation_word (str): the default deactivation word ('without' in the case of ``with_or_without``) activation_value (callable): callable that accepts a single value. This value is either one of the allowed values for a multi-valued variant or the name of a bool-valued variant. Returns the parameter to be used when the value is activated. The special value 'prefix' can also be assigned and will return ``spec[name].prefix`` as activation parameter. Examples: Given a package with: .. code-block:: python variant('foo', values=('x', 'y'), description='') variant('bar', default=True, description='') calling this function like: .. code-block:: python _activate_or_not( 'foo', 'with', 'without', activation_value='prefix' ) _activate_or_not('bar', 'with', 'without') will generate the following configuration options: .. code-block:: console --with-x=<prefix-to-x> --without-y --with-bar for ``<spec-name> foo=x +bar`` Returns: list of strings that corresponds to the activation/deactivation of the variant that has been processed Raises: KeyError: if name is not among known variants """ spec = self.spec args = [] if activation_value == 'prefix': activation_value = lambda x: spec[x].prefix # Defensively look that the name passed as argument is among # variants if name not in self.variants: msg = '"{0}" is not a variant of "{1}"' raise KeyError(msg.format(name, self.name)) # Create a list of pairs. Each pair includes a configuration # option and whether or not that option is activated if set(self.variants[name].values) == set((True, False)): # BoolValuedVariant carry information about a single option. # Nonetheless, for uniformity of treatment we'll package them # in an iterable of one element. condition = '+{name}'.format(name=name) options = [(name, condition in spec)] else: condition = '{name}={value}' # "feature_values" is used to track values which correspond to # features which can be enabled or disabled as understood by the # package's build system. It excludes values which have special # meanings and do not correspond to features (e.g. "none") feature_values = getattr( self.variants[name].values, 'feature_values', None ) or self.variants[name].values options = [ (value, condition.format(name=name, value=value) in spec) for value in feature_values ] # For each allowed value in the list of values for option_value, activated in options: # Search for an override in the package for this value override_name = '{0}_or_{1}_{2}'.format( activation_word, deactivation_word, option_value ) line_generator = getattr(self, override_name, None) # If not available use a sensible default if line_generator is None: def _default_generator(is_activated): if is_activated: line = '--{0}-{1}'.format( activation_word, option_value ) if activation_value is not None and activation_value(option_value): # NOQA=ignore=E501 line += '={0}'.format( activation_value(option_value) ) return line return '--{0}-{1}'.format(deactivation_word, option_value) line_generator = _default_generator args.append(line_generator(activated)) return args def with_or_without(self, name, activation_value=None): """Inspects a variant and returns the arguments that activate or deactivate the selected feature(s) for the configure options. This function works on all type of variants. For bool-valued variants it will return by default ``--with-{name}`` or ``--without-{name}``. For other kinds of variants it will cycle over the allowed values and return either ``--with-{value}`` or ``--without-{value}``. If activation_value is given, then for each possible value of the variant, the option ``--with-{value}=activation_value(value)`` or ``--without-{value}`` will be added depending on whether or not ``variant=value`` is in the spec. Args: name (str): name of a valid multi-valued variant activation_value (callable): callable that accepts a single value and returns the parameter to be used leading to an entry of the type ``--with-{name}={parameter}``. The special value 'prefix' can also be assigned and will return ``spec[name].prefix`` as activation parameter. Returns: list of arguments to configure """ return self._activate_or_not(name, 'with', 'without', activation_value) def enable_or_disable(self, name, activation_value=None): """Same as :py:meth:`~.AutotoolsPackage.with_or_without` but substitute ``with`` with ``enable`` and ``without`` with ``disable``. Args: name (str): name of a valid multi-valued variant activation_value (callable): if present accepts a single value and returns the parameter to be used leading to an entry of the type ``--enable-{name}={parameter}`` The special value 'prefix' can also be assigned and will return ``spec[name].prefix`` as activation parameter. Returns: list of arguments to configure """ return self._activate_or_not( name, 'enable', 'disable', activation_value ) run_after('install')(PackageBase._run_default_install_time_test_callbacks) def installcheck(self): """Searches the Makefile for an ``installcheck`` target and runs it if found. """ with working_dir(self.build_directory): self._if_make_target_execute('installcheck') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class PythonPackage(PackageBase): """Specialized class for packages that are built using Python setup.py files This class provides the following phases that can be overridden: * build * build_py * build_ext * build_clib * build_scripts * clean * install * install_lib * install_headers * install_scripts * install_data * sdist * register * bdist * bdist_dumb * bdist_rpm * bdist_wininst * upload * check These are all standard setup.py commands and can be found by running: .. code-block:: console $ python setup.py --help-commands By default, only the 'build' and 'install' phases are run, but if you need to run more phases, simply modify your ``phases`` list like so: .. code-block:: python phases = ['build_ext', 'install', 'bdist'] Each phase provides a function <phase> that runs: .. code-block:: console $ python -s setup.py --no-user-cfg <phase> Each phase also has a <phase_args> function that can pass arguments to this call. All of these functions are empty except for the ``install_args`` function, which passes ``--prefix=/path/to/installation/directory``. If you need to run a phase which is not a standard setup.py command, you'll need to define a function for it like so: .. code-block:: python def configure(self, spec, prefix): self.setup_py('configure') """ # Default phases phases = ['build', 'install'] # Name of modules that the Python package provides # This is used to test whether or not the installation succeeded # These names generally come from running: # # >>> import setuptools # >>> setuptools.find_packages() # # in the source tarball directory import_modules = [] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'PythonPackage' #: Callback names for build-time test build_time_test_callbacks = ['test'] #: Callback names for install-time test install_time_test_callbacks = ['import_module_test'] extends('python') depends_on('python', type=('build', 'run')) py_namespace = None def setup_file(self): """Returns the name of the setup file to use.""" return 'setup.py' @property def build_directory(self): """The directory containing the ``setup.py`` file.""" return self.stage.source_path def python(self, *args, **kwargs): inspect.getmodule(self).python(*args, **kwargs) def setup_py(self, *args, **kwargs): setup = self.setup_file() with working_dir(self.build_directory): self.python('-s', setup, '--no-user-cfg', *args, **kwargs) def _setup_command_available(self, command): """Determines whether or not a setup.py command exists. Args: command (str): The command to look for Returns: bool: True if the command is found, else False """ kwargs = { 'output': os.devnull, 'error': os.devnull, 'fail_on_error': False } python = inspect.getmodule(self).python setup = self.setup_file() python('-s', setup, '--no-user-cfg', command, '--help', **kwargs) return python.returncode == 0 # The following phases and their descriptions come from: # $ python setup.py --help-commands # Standard commands def build(self, spec, prefix): """Build everything needed to install.""" args = self.build_args(spec, prefix) self.setup_py('build', *args) def build_args(self, spec, prefix): """Arguments to pass to build.""" return [] def build_py(self, spec, prefix): '''"Build" pure Python modules (copy to build directory).''' args = self.build_py_args(spec, prefix) self.setup_py('build_py', *args) def build_py_args(self, spec, prefix): """Arguments to pass to build_py.""" return [] def build_ext(self, spec, prefix): """Build C/C++ extensions (compile/link to build directory).""" args = self.build_ext_args(spec, prefix) self.setup_py('build_ext', *args) def build_ext_args(self, spec, prefix): """Arguments to pass to build_ext.""" return [] def build_clib(self, spec, prefix): """Build C/C++ libraries used by Python extensions.""" args = self.build_clib_args(spec, prefix) self.setup_py('build_clib', *args) def build_clib_args(self, spec, prefix): """Arguments to pass to build_clib.""" return [] def build_scripts(self, spec, prefix): '''"Build" scripts (copy and fixup #! line).''' args = self.build_scripts_args(spec, prefix) self.setup_py('build_scripts', *args) def clean(self, spec, prefix): """Clean up temporary files from 'build' command.""" args = self.clean_args(spec, prefix) self.setup_py('clean', *args) def clean_args(self, spec, prefix): """Arguments to pass to clean.""" return [] def install(self, spec, prefix): """Install everything from build directory.""" args = self.install_args(spec, prefix) self.setup_py('install', *args) def install_args(self, spec, prefix): """Arguments to pass to install.""" args = ['--prefix={0}'.format(prefix)] # This option causes python packages (including setuptools) NOT # to create eggs or easy-install.pth files. Instead, they # install naturally into $prefix/pythonX.Y/site-packages. # # Eggs add an extra level of indirection to sys.path, slowing # down large HPC runs. They are also deprecated in favor of # wheels, which use a normal layout when installed. # # Spack manages the package directory on its own by symlinking # extensions into the site-packages directory, so we don't really # need the .pth files or egg directories, anyway. # # We need to make sure this is only for build dependencies. A package # such as py-basemap will not build properly with this flag since # it does not use setuptools to build and those does not recognize # the --single-version-externally-managed flag if ('py-setuptools' == spec.name or # this is setuptools, or 'py-setuptools' in spec._dependencies and # it's an immediate dep 'build' in spec._dependencies['py-setuptools'].deptypes): args += ['--single-version-externally-managed', '--root=/'] return args def install_lib(self, spec, prefix): """Install all Python modules (extensions and pure Python).""" args = self.install_lib_args(spec, prefix) self.setup_py('install_lib', *args) def install_lib_args(self, spec, prefix): """Arguments to pass to install_lib.""" return [] def install_headers(self, spec, prefix): """Install C/C++ header files.""" args = self.install_headers_args(spec, prefix) self.setup_py('install_headers', *args) def install_headers_args(self, spec, prefix): """Arguments to pass to install_headers.""" return [] def install_scripts(self, spec, prefix): """Install scripts (Python or otherwise).""" args = self.install_scripts_args(spec, prefix) self.setup_py('install_scripts', *args) def install_scripts_args(self, spec, prefix): """Arguments to pass to install_scripts.""" return [] def install_data(self, spec, prefix): """Install data files.""" args = self.install_data_args(spec, prefix) self.setup_py('install_data', *args) def install_data_args(self, spec, prefix): """Arguments to pass to install_data.""" return [] def sdist(self, spec, prefix): """Create a source distribution (tarball, zip file, etc.).""" args = self.sdist_args(spec, prefix) self.setup_py('sdist', *args) def sdist_args(self, spec, prefix): """Arguments to pass to sdist.""" return [] def register(self, spec, prefix): """Register the distribution with the Python package index.""" args = self.register_args(spec, prefix) self.setup_py('register', *args) def register_args(self, spec, prefix): """Arguments to pass to register.""" return [] def bdist(self, spec, prefix): """Create a built (binary) distribution.""" args = self.bdist_args(spec, prefix) self.setup_py('bdist', *args) def bdist_args(self, spec, prefix): """Arguments to pass to bdist.""" return [] def bdist_dumb(self, spec, prefix): '''Create a "dumb" built distribution.''' args = self.bdist_dumb_args(spec, prefix) self.setup_py('bdist_dumb', *args) def bdist_dumb_args(self, spec, prefix): """Arguments to pass to bdist_dumb.""" return [] def bdist_rpm(self, spec, prefix): """Create an RPM distribution.""" args = self.bdist_rpm(spec, prefix) self.setup_py('bdist_rpm', *args) def bdist_rpm_args(self, spec, prefix): """Arguments to pass to bdist_rpm.""" return [] def bdist_wininst(self, spec, prefix): """Create an executable installer for MS Windows.""" args = self.bdist_wininst_args(spec, prefix) self.setup_py('bdist_wininst', *args) def bdist_wininst_args(self, spec, prefix): """Arguments to pass to bdist_wininst.""" return [] def upload(self, spec, prefix): """Upload binary package to PyPI.""" args = self.upload_args(spec, prefix) self.setup_py('upload', *args) def upload_args(self, spec, prefix): """Arguments to pass to upload.""" return [] def check(self, spec, prefix): """Perform some checks on the package.""" args = self.check_args(spec, prefix) self.setup_py('check', *args) def check_args(self, spec, prefix): """Arguments to pass to check.""" return [] # Testing def test(self): """Run unit tests after in-place build. These tests are only run if the package actually has a 'test' command. """ if self._setup_command_available('test'): args = self.test_args(self.spec, self.prefix) self.setup_py('test', *args) def test_args(self, spec, prefix): """Arguments to pass to test.""" return [] run_after('build')(PackageBase._run_default_build_time_test_callbacks) def import_module_test(self): """Attempts to import the module that was just installed. This test is only run if the package overrides :py:attr:`import_modules` with a list of module names.""" # Make sure we are importing the installed modules, # not the ones in the current directory with working_dir('..'): for module in self.import_modules: self.python('-c', 'import {0}'.format(module)) run_after('install')(PackageBase._run_default_install_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix) def view_file_conflicts(self, view, merge_map): """Report all file conflicts, excepting special cases for python. Specifically, this does not report errors for duplicate __init__.py files for packages in the same namespace. """ conflicts = list(dst for src, dst in merge_map.items() if os.path.exists(dst)) if conflicts and self.py_namespace: ext_map = view.extensions_layout.extension_map(self.extendee_spec) namespaces = set(x.package.py_namespace for x in ext_map.values()) namespace_re = (r'site-packages/{0}/__init__.py'.format( self.py_namespace)) find_namespace = match_predicate(namespace_re) if self.py_namespace in namespaces: conflicts = list(x for x in conflicts if not find_namespace(x)) return conflicts def add_files_to_view(self, view, merge_map): bin_dir = self.spec.prefix.bin python_prefix = self.extendee_spec.prefix global_view = same_path(python_prefix, view.root) for src, dst in merge_map.items(): if os.path.exists(dst): continue elif global_view or not path_contains_subdirectory(src, bin_dir): view.link(src, dst) elif not os.path.islink(src): shutil.copy2(src, dst) if 'script' in get_filetype(src): filter_file(python_prefix, os.path.abspath(view.root), dst) else: orig_link_target = os.path.realpath(src) new_link_target = os.path.abspath(merge_map[orig_link_target]) view.link(new_link_target, dst) def remove_files_from_view(self, view, merge_map): ignore_namespace = False if self.py_namespace: ext_map = view.extensions_layout.extension_map(self.extendee_spec) remaining_namespaces = set(spec.package.py_namespace for name, spec in ext_map.items() if name != self.name) if self.py_namespace in remaining_namespaces: namespace_init = match_predicate( r'site-packages/{0}/__init__.py'.format(self.py_namespace)) ignore_namespace = True bin_dir = self.spec.prefix.bin global_view = self.extendee_spec.prefix == view.root for src, dst in merge_map.items(): if ignore_namespace and namespace_init(dst): continue if global_view or not path_contains_subdirectory(src, bin_dir): view.remove_file(src, dst) else: os.remove(dst)
class WafPackage(PackageBase): """Specialized class for packages that are built using the Waf build system. See https://waf.io/book/ for more information. This class provides the following phases that can be overridden: * configure * build * install These are all standard Waf commands and can be found by running: .. code-block:: console $ python waf --help Each phase provides a function <phase> that runs: .. code-block:: console $ python waf -j<jobs> <phase> where <jobs> is the number of parallel jobs to build with. Each phase also has a <phase_args> function that can pass arguments to this call. All of these functions are empty except for the ``configure_args`` function, which passes ``--prefix=/path/to/installation/prefix``. """ # Default phases phases = ['configure', 'build', 'install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'WafPackage' # Callback names for build-time test build_time_test_callbacks = ['build_test'] # Callback names for install-time test install_time_test_callbacks = ['install_test'] # Much like AutotoolsPackage does not require automake and autoconf # to build, WafPackage does not require waf to build. It only requires # python to run the waf build script. depends_on('[email protected]:', type='build') @property def build_directory(self): """The directory containing the ``waf`` file.""" return self.stage.source_path def python(self, *args, **kwargs): """The python ``Executable``.""" inspect.getmodule(self).python(*args, **kwargs) def waf(self, *args, **kwargs): """Runs the waf ``Executable``.""" jobs = inspect.getmodule(self).make_jobs with working_dir(self.build_directory): self.python('waf', '-j{0}'.format(jobs), *args, **kwargs) def configure(self, spec, prefix): """Configures the project.""" args = ['--prefix={0}'.format(self.prefix)] args += self.configure_args() self.waf('configure', *args) def configure_args(self): """Arguments to pass to configure.""" return [] def build(self, spec, prefix): """Executes the build.""" args = self.build_args() self.waf('build', *args) def build_args(self): """Arguments to pass to build.""" return [] def install(self, spec, prefix): """Installs the targets on the system.""" args = self.install_args() self.waf('install', *args) def install_args(self): """Arguments to pass to install.""" return [] # Testing def build_test(self): """Run unit tests after build. By default, does nothing. Override this if you want to add package-specific tests. """ pass run_after('build')(PackageBase._run_default_build_time_test_callbacks) def install_test(self): """Run unit tests after install. By default, does nothing. Override this if you want to add package-specific tests. """ pass run_after('install')(PackageBase._run_default_install_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class MakefilePackage(PackageBase): """Specialized class for packages that are built using editable Makefiles This class provides three phases that can be overridden: 1. :py:meth:`~.MakefilePackage.edit` 2. :py:meth:`~.MakefilePackage.build` 3. :py:meth:`~.MakefilePackage.install` It is usually necessary to override the :py:meth:`~.MakefilePackage.edit` phase, while :py:meth:`~.MakefilePackage.build` and :py:meth:`~.MakefilePackage.install` have sensible defaults. For a finer tuning you may override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:attr:`~.MakefilePackage.build_targets` | Specify ``make`` | | | targets for the | | | build phase | +-----------------------------------------------+--------------------+ | :py:attr:`~.MakefilePackage.install_targets` | Specify ``make`` | | | targets for the | | | install phase | +-----------------------------------------------+--------------------+ | :py:meth:`~.MakefilePackage.build_directory` | Directory where the| | | Makefile is located| +-----------------------------------------------+--------------------+ """ #: Phases of a package that is built with an hand-written Makefile phases = ['edit', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'MakefilePackage' #: Targets for ``make`` during the :py:meth:`~.MakefilePackage.build` #: phase build_targets = [] #: Targets for ``make`` during the :py:meth:`~.MakefilePackage.install` #: phase install_targets = ['install'] #: Callback names for build-time test build_time_test_callbacks = ['check'] #: Callback names for install-time test install_time_test_callbacks = ['installcheck'] @property def build_directory(self): """Returns the directory containing the main Makefile :return: build directory """ return self.stage.source_path def edit(self, spec, prefix): """Edits the Makefile before calling make. This phase cannot be defaulted. """ tty.msg('Using default implementation: skipping edit phase.') def build(self, spec, prefix): """Calls make, passing :py:attr:`~.MakefilePackage.build_targets` as targets. """ with working_dir(self.build_directory): inspect.getmodule(self).make(*self.build_targets) def install(self, spec, prefix): """Calls make, passing :py:attr:`~.MakefilePackage.install_targets` as targets. """ with working_dir(self.build_directory): inspect.getmodule(self).make(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the Makefile for targets ``test`` and ``check`` and runs them if found. """ with working_dir(self.build_directory): self._if_make_target_execute('test') self._if_make_target_execute('check') run_after('install')(PackageBase._run_default_install_time_test_callbacks) def installcheck(self): """Searches the Makefile for an ``installcheck`` target and runs it if found. """ with working_dir(self.build_directory): self._if_make_target_execute('installcheck') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class SIPPackage(PackageBase): """Specialized class for packages that are built using the SIP build system. See https://www.riverbankcomputing.com/software/sip/intro for more information. This class provides the following phases that can be overridden: * configure * build * install The configure phase already adds a set of default flags. To see more options, run ``python configure.py --help``. """ # Default phases phases = ['configure', 'build', 'install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'SIPPackage' #: Name of private sip module to install alongside package sip_module = 'sip' #: Callback names for install-time test install_time_test_callbacks = ['import_module_test'] extends('python') depends_on('qt') resource(name='sip', url='https://www.riverbankcomputing.com/static/Downloads/sip/4.19.18/sip-4.19.18.tar.gz', sha256='c0bd863800ed9b15dcad477c4017cdb73fa805c25908b0240564add74d697e1e', destination='.') def python(self, *args, **kwargs): """The python ``Executable``.""" inspect.getmodule(self).python(*args, **kwargs) @run_before('configure') def install_sip(self): args = [ '--sip-module={0}'.format(self.sip_module), '--bindir={0}'.format(self.prefix.bin), '--destdir={0}'.format(inspect.getmodule(self).site_packages_dir), '--incdir={0}'.format(inspect.getmodule(self).python_include_dir), '--sipdir={0}'.format(self.prefix.share.sip), '--stubsdir={0}'.format(inspect.getmodule(self).site_packages_dir), ] with working_dir('sip-4.19.18'): self.python('configure.py', *args) inspect.getmodule(self).make() inspect.getmodule(self).make('install') def configure_file(self): """Returns the name of the configure file to use.""" return 'configure.py' def configure(self, spec, prefix): """Configure the package.""" configure = self.configure_file() args = self.configure_args() args.extend([ '--verbose', '--confirm-license', '--qmake', spec['qt'].prefix.bin.qmake, '--sip', prefix.bin.sip, '--sip-incdir', inspect.getmodule(self).python_include_dir, '--bindir', prefix.bin, '--destdir', inspect.getmodule(self).site_packages_dir, ]) self.python(configure, *args) def configure_args(self): """Arguments to pass to configure.""" return [] def build(self, spec, prefix): """Build the package.""" args = self.build_args() inspect.getmodule(self).make(*args) def build_args(self): """Arguments to pass to build.""" return [] def install(self, spec, prefix): """Install the package.""" args = self.install_args() inspect.getmodule(self).make('install', parallel=False, *args) def install_args(self): """Arguments to pass to install.""" return [] # Testing def import_module_test(self): """Attempts to import the module that was just installed. This test is only run if the package overrides :py:attr:`import_modules` with a list of module names.""" # Make sure we are importing the installed modules, # not the ones in the current directory with working_dir('spack-test', create=True): for module in self.import_modules: self.python('-c', 'import {0}'.format(module)) run_after('install')(PackageBase._run_default_install_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class CMakePackage(PackageBase): """Specialized class for packages built using CMake For more information on the CMake build system, see: https://cmake.org/cmake/help/latest/ This class provides three phases that can be overridden: 1. :py:meth:`~.CMakePackage.cmake` 2. :py:meth:`~.CMakePackage.build` 3. :py:meth:`~.CMakePackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override :py:meth:`~.CMakePackage.cmake_args`. For a finer tuning you may also override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:meth:`~.CMakePackage.root_cmakelists_dir` | Location of the | | | root CMakeLists.txt| +-----------------------------------------------+--------------------+ | :py:meth:`~.CMakePackage.build_directory` | Directory where to | | | build the package | +-----------------------------------------------+--------------------+ The generator used by CMake can be specified by providing the generator attribute. Per https://cmake.org/cmake/help/git-master/manual/cmake-generators.7.html, the format is: [<secondary-generator> - ]<primary_generator>. The full list of primary and secondary generators supported by CMake may be found in the documentation for the version of CMake used; however, at this time Spack supports only the primary generators "Unix Makefiles" and "Ninja." Spack's CMake support is agnostic with respect to primary generators. Spack will generate a runtime error if the generator string does not follow the prescribed format, or if the primary generator is not supported. """ #: Phases of a CMake package phases = ['cmake', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'CMakePackage' build_targets = [] # type: List[str] install_targets = ['install'] build_time_test_callbacks = ['check'] #: The build system generator to use. #: #: See ``cmake --help`` for a list of valid generators. #: Currently, "Unix Makefiles" and "Ninja" are the only generators #: that Spack supports. Defaults to "Unix Makefiles". #: #: See https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html #: for more information. generator = 'Unix Makefiles' # https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html variant('build_type', default='RelWithDebInfo', description='CMake build type', values=('Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel')) # https://cmake.org/cmake/help/latest/variable/CMAKE_INTERPROCEDURAL_OPTIMIZATION.html variant('ipo', default=False, description='CMake interprocedural optimization') # CMAKE_INTERPROCEDURAL_OPTIMIZATION only exists for CMake >= 3.9 conflicts('+ipo', when='^cmake@:3.8', msg='+ipo is not supported by CMake < 3.9') depends_on('cmake', type='build') @property def archive_files(self): """Files to archive for packages based on CMake""" return [os.path.join(self.build_directory, 'CMakeCache.txt')] @property def root_cmakelists_dir(self): """The relative path to the directory containing CMakeLists.txt This path is relative to the root of the extracted tarball, not to the ``build_directory``. Defaults to the current directory. :return: directory containing CMakeLists.txt """ return self.stage.source_path @property def std_cmake_args(self): """Standard cmake arguments provided as a property for convenience of package writers :return: standard cmake arguments """ # standard CMake arguments std_cmake_args = CMakePackage._std_args(self) std_cmake_args += getattr(self, 'cmake_flag_args', []) return std_cmake_args @staticmethod def _std_args(pkg): """Computes the standard cmake arguments for a generic package""" try: generator = pkg.generator except AttributeError: generator = 'Unix Makefiles' # Make sure a valid generator was chosen valid_primary_generators = ['Unix Makefiles', 'Ninja'] primary_generator = _extract_primary_generator(generator) if primary_generator not in valid_primary_generators: msg = "Invalid CMake generator: '{0}'\n".format(generator) msg += "CMakePackage currently supports the following " msg += "primary generators: '{0}'".\ format("', '".join(valid_primary_generators)) raise InstallError(msg) try: build_type = pkg.spec.variants['build_type'].value except KeyError: build_type = 'RelWithDebInfo' try: ipo = pkg.spec.variants['ipo'].value except KeyError: ipo = False define = CMakePackage.define args = [ '-G', generator, define('CMAKE_INSTALL_PREFIX', pkg.prefix), define('CMAKE_BUILD_TYPE', build_type), ] # CMAKE_INTERPROCEDURAL_OPTIMIZATION only exists for CMake >= 3.9 if pkg.spec.satisfies('^[email protected]:'): args.append(define('CMAKE_INTERPROCEDURAL_OPTIMIZATION', ipo)) if primary_generator == 'Unix Makefiles': args.append(define('CMAKE_VERBOSE_MAKEFILE', True)) if platform.mac_ver()[0]: args.extend([ define('CMAKE_FIND_FRAMEWORK', "LAST"), define('CMAKE_FIND_APPBUNDLE', "LAST"), ]) # Set up CMake rpath args.extend([ define('CMAKE_INSTALL_RPATH_USE_LINK_PATH', False), define('CMAKE_INSTALL_RPATH', spack.build_environment.get_rpaths(pkg)), ]) # CMake's find_package() looks in CMAKE_PREFIX_PATH first, help CMake # to find immediate link dependencies in right places: deps = [d.prefix for d in pkg.spec.dependencies(deptype=('build', 'link'))] deps = filter_system_paths(deps) args.append(define('CMAKE_PREFIX_PATH', deps)) return args @staticmethod def define(cmake_var, value): """Return a CMake command line argument that defines a variable. The resulting argument will convert boolean values to OFF/ON and lists/tuples to CMake semicolon-separated string lists. All other values will be interpreted as strings. Examples: .. code-block:: python [define('BUILD_SHARED_LIBS', True), define('CMAKE_CXX_STANDARD', 14), define('swr', ['avx', 'avx2'])] will generate the following configuration options: .. code-block:: console ["-DBUILD_SHARED_LIBS:BOOL=ON", "-DCMAKE_CXX_STANDARD:STRING=14", "-DSWR:STRING=avx;avx2] """ # Create a list of pairs. Each pair includes a configuration # option and whether or not that option is activated if isinstance(value, bool): kind = 'BOOL' value = "ON" if value else "OFF" else: kind = 'STRING' if isinstance(value, (list, tuple)): value = ";".join(str(v) for v in value) else: value = str(value) return "".join(["-D", cmake_var, ":", kind, "=", value]) def define_from_variant(self, cmake_var, variant=None): """Return a CMake command line argument from the given variant's value. The optional ``variant`` argument defaults to the lower-case transform of ``cmake_var``. This utility function is similar to :py:meth:`~.AutotoolsPackage.with_or_without`. Examples: Given a package with: .. code-block:: python variant('cxxstd', default='11', values=('11', '14'), multi=False, description='') variant('shared', default=True, description='') variant('swr', values=any_combination_of('avx', 'avx2'), description='') calling this function like: .. code-block:: python [define_from_variant('BUILD_SHARED_LIBS', 'shared'), define_from_variant('CMAKE_CXX_STANDARD', 'cxxstd'), define_from_variant('SWR')] will generate the following configuration options: .. code-block:: console ["-DBUILD_SHARED_LIBS:BOOL=ON", "-DCMAKE_CXX_STANDARD:STRING=14", "-DSWR:STRING=avx;avx2] for ``<spec-name> cxxstd=14 +shared swr=avx,avx2`` """ if variant is None: variant = cmake_var.lower() if variant not in self.variants: raise KeyError( '"{0}" is not a variant of "{1}"'.format(variant, self.name)) value = self.spec.variants[variant].value if isinstance(value, (tuple, list)): # Sort multi-valued variants for reproducibility value = sorted(value) return self.define(cmake_var, value) def flags_to_build_system_args(self, flags): """Produces a list of all command line arguments to pass the specified compiler flags to cmake. Note CMAKE does not have a cppflags option, so cppflags will be added to cflags, cxxflags, and fflags to mimic the behavior in other tools.""" # Has to be dynamic attribute due to caching setattr(self, 'cmake_flag_args', []) flag_string = '-DCMAKE_{0}_FLAGS={1}' langs = {'C': 'c', 'CXX': 'cxx', 'Fortran': 'f'} # Handle language compiler flags for lang, pre in langs.items(): flag = pre + 'flags' # cmake has no explicit cppflags support -> add it to all langs lang_flags = ' '.join(flags.get(flag, []) + flags.get('cppflags', [])) if lang_flags: self.cmake_flag_args.append(flag_string.format(lang, lang_flags)) # Cmake has different linker arguments for different build types. # We specify for each of them. if flags['ldflags']: ldflags = ' '.join(flags['ldflags']) ld_string = '-DCMAKE_{0}_LINKER_FLAGS={1}' # cmake has separate linker arguments for types of builds. for type in ['EXE', 'MODULE', 'SHARED', 'STATIC']: self.cmake_flag_args.append(ld_string.format(type, ldflags)) # CMake has libs options separated by language. Apply ours to each. if flags['ldlibs']: libs_flags = ' '.join(flags['ldlibs']) libs_string = '-DCMAKE_{0}_STANDARD_LIBRARIES={1}' for lang in langs: self.cmake_flag_args.append(libs_string.format(lang, libs_flags)) @property def build_dirname(self): """Returns the directory name to use when building the package :return: name of the subdirectory for building the package """ return 'spack-build-%s' % self.spec.dag_hash(7) @property def build_directory(self): """Returns the directory to use when building the package :return: directory where to build the package """ return os.path.join(self.stage.path, self.build_dirname) def cmake_args(self): """Produces a list containing all the arguments that must be passed to cmake, except: * CMAKE_INSTALL_PREFIX * CMAKE_BUILD_TYPE which will be set automatically. :return: list of arguments for cmake """ return [] def cmake(self, spec, prefix): """Runs ``cmake`` in the build directory""" options = self.std_cmake_args options += self.cmake_args() options.append(os.path.abspath(self.root_cmakelists_dir)) with working_dir(self.build_directory, create=True): inspect.getmodule(self).cmake(*options) def build(self, spec, prefix): """Make the build targets""" with working_dir(self.build_directory): if self.generator == 'Unix Makefiles': inspect.getmodule(self).make(*self.build_targets) elif self.generator == 'Ninja': self.build_targets.append("-v") inspect.getmodule(self).ninja(*self.build_targets) def install(self, spec, prefix): """Make the install targets""" with working_dir(self.build_directory): if self.generator == 'Unix Makefiles': inspect.getmodule(self).make(*self.install_targets) elif self.generator == 'Ninja': inspect.getmodule(self).ninja(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the CMake-generated Makefile for the target ``test`` and runs it if found. """ with working_dir(self.build_directory): if self.generator == 'Unix Makefiles': self._if_make_target_execute('test', jobs_env='CTEST_PARALLEL_LEVEL') self._if_make_target_execute('check') elif self.generator == 'Ninja': self._if_ninja_target_execute('test', jobs_env='CTEST_PARALLEL_LEVEL') self._if_ninja_target_execute('check') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class IntelPackage(PackageBase): """Specialized class for licensed Intel software. This class provides two phases that can be overridden: 1. :py:meth:`~.IntelPackage.configure` 2. :py:meth:`~.IntelPackage.install` They both have sensible defaults and for many packages the only thing necessary will be to override ``setup_environment`` to set the appropriate environment variables. """ #: Phases of an Intel package phases = ['configure', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'IntelPackage' #: By default, we assume that all Intel software requires a license. #: This can be overridden for packages that do not require a license. license_required = True #: Comment symbol used in the ``license.lic`` file license_comment = '#' #: Location where Intel searches for a license file license_files = ['Licenses/license.lic'] #: Environment variables that Intel searches for a license file license_vars = ['INTEL_LICENSE_FILE'] #: URL providing information on how to acquire a license key license_url = 'https://software.intel.com/en-us/articles/intel-license-manager-faq' #: Components of the package to install. #: By default, install 'ALL' components. components = ['ALL'] @property def _filtered_components(self): """Returns a list or set of valid components that match the requested components from ``components``.""" # Don't filter 'ALL' if self.components == ['ALL']: return self.components # mediaconfig.xml is known to contain duplicate components. # If more than one copy of the same component is used, you # will get an error message about invalid components. # Use a set to store components to prevent duplicates. matches = set() for valid in _valid_components(): for requested in self.components: if valid.startswith(requested): matches.add(valid) return matches @property def global_license_file(self): """Returns the path where a global license file should be stored. All Intel software shares the same license, so we store it in a common 'intel' directory.""" return join_path(self.global_license_dir, 'intel', os.path.basename(self.license_files[0])) def configure(self, spec, prefix): """Writes the ``silent.cfg`` file used to configure the installation. See https://software.intel.com/en-us/articles/configuration-file-format """ # Patterns used to check silent configuration file # # anythingpat - any string # filepat - the file location pattern (/path/to/license.lic) # lspat - the license server address pattern (0123@hostname) # snpat - the serial number pattern (ABCD-01234567) config = { # Accept EULA, valid values are: {accept, decline} 'ACCEPT_EULA': 'accept', # Optional error behavior, valid values are: {yes, no} 'CONTINUE_WITH_OPTIONAL_ERROR': 'yes', # Install location, valid values are: {/opt/intel, filepat} 'PSET_INSTALL_DIR': prefix, # Continue with overwrite of existing installation directory, # valid values are: {yes, no} 'CONTINUE_WITH_INSTALLDIR_OVERWRITE': 'yes', # List of components to install, # valid values are: {ALL, DEFAULTS, anythingpat} 'COMPONENTS': ';'.join(self._filtered_components), # Installation mode, valid values are: {install, repair, uninstall} 'PSET_MODE': 'install', # Directory for non-RPM database, valid values are: {filepat} 'NONRPM_DB_DIR': prefix, # Perform validation of digital signatures of RPM files, # valid values are: {yes, no} 'SIGNING_ENABLED': 'no', # Select target architecture of your applications, # valid values are: {IA32, INTEL64, ALL} 'ARCH_SELECTED': 'ALL', } # Not all Intel software requires a license. Trying to specify # one anyway will cause the installation to fail. if self.license_required: config.update({ # License file or license server, # valid values are: {lspat, filepat} 'ACTIVATION_LICENSE_FILE': self.global_license_file, # Activation type, valid values are: {exist_lic, # license_server, license_file, trial_lic, serial_number} 'ACTIVATION_TYPE': 'license_file', # Intel(R) Software Improvement Program opt-in, # valid values are: {yes, no} 'PHONEHOME_SEND_USAGE_DATA': 'no', }) with open('silent.cfg', 'w') as cfg: for key in config: cfg.write('{0}={1}\n'.format(key, config[key])) def install(self, spec, prefix): """Runs the ``install.sh`` installation script.""" install_script = Executable('./install.sh') install_script('--silent', 'silent.cfg') @run_after('install') def save_silent_cfg(self): """Copies the silent.cfg configuration file to ``<prefix>/.spack``.""" install('silent.cfg', join_path(self.prefix, '.spack')) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class PythonPackage(PackageBase): """Specialized class for packages that are built using Python setup.py files This class provides the following phases that can be overridden: * build * build_py * build_ext * build_clib * build_scripts * clean * install * install_lib * install_headers * install_scripts * install_data * sdist * register * bdist * bdist_dumb * bdist_rpm * bdist_wininst * upload * check These are all standard setup.py commands and can be found by running: .. code-block:: console $ python setup.py --help-commands By default, only the 'build' and 'install' phases are run, but if you need to run more phases, simply modify your ``phases`` list like so: .. code-block:: python phases = ['build_ext', 'install', 'bdist'] Each phase provides a function <phase> that runs: .. code-block:: console $ python -s setup.py --no-user-cfg <phase> Each phase also has a <phase_args> function that can pass arguments to this call. All of these functions are empty except for the ``install_args`` function, which passes ``--prefix=/path/to/installation/directory``. If you need to run a phase which is not a standard setup.py command, you'll need to define a function for it like so: .. code-block:: python def configure(self, spec, prefix): self.setup_py('configure') """ #: Package name, version, and extension on PyPI pypi = None # Default phases phases = ['build', 'install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'PythonPackage' #: Callback names for install-time test install_time_test_callbacks = ['test'] extends('python') py_namespace = None @property def homepage(self): if self.pypi: name = self.pypi.split('/')[0] return 'https://pypi.org/project/' + name + '/' @property def url(self): if self.pypi: return ('https://files.pythonhosted.org/packages/source/' + self.pypi[0] + '/' + self.pypi) @property def list_url(self): if self.pypi: name = self.pypi.split('/')[0] return 'https://pypi.org/simple/' + name + '/' @property def import_modules(self): """Names of modules that the Python package provides. These are used to test whether or not the installation succeeded. These names generally come from running: .. code-block:: python >> import setuptools >> setuptools.find_packages() in the source tarball directory. If the module names are incorrectly detected, this property can be overridden by the package. Returns: list: list of strings of module names """ modules = [] # Python libraries may be installed in lib or lib64 # See issues #18520 and #17126 for lib in ['lib', 'lib64']: root = os.path.join( self.prefix, lib, 'python{0}'.format(self.spec['python'].version.up_to(2)), 'site-packages') # Some Python libraries are packages: collections of modules # distributed in directories containing __init__.py files for path in find(root, '__init__.py', recursive=True): modules.append( path.replace(root + os.sep, '', 1).replace(os.sep + '__init__.py', '').replace('/', '.')) # Some Python libraries are modules: individual *.py files # found in the site-packages directory for path in find(root, '*.py', recursive=False): modules.append( path.replace(root + os.sep, '', 1).replace('.py', '').replace('/', '.')) tty.debug('Detected the following modules: {0}'.format(modules)) return modules def setup_file(self): """Returns the name of the setup file to use.""" return 'setup.py' @property def build_directory(self): """The directory containing the ``setup.py`` file.""" return self.stage.source_path def python(self, *args, **kwargs): inspect.getmodule(self).python(*args, **kwargs) def setup_py(self, *args, **kwargs): setup = self.setup_file() with working_dir(self.build_directory): self.python('-s', setup, '--no-user-cfg', *args, **kwargs) # The following phases and their descriptions come from: # $ python setup.py --help-commands # Standard commands def build(self, spec, prefix): """Build everything needed to install.""" args = self.build_args(spec, prefix) self.setup_py('build', *args) def build_args(self, spec, prefix): """Arguments to pass to build.""" return [] def build_py(self, spec, prefix): '''"Build" pure Python modules (copy to build directory).''' args = self.build_py_args(spec, prefix) self.setup_py('build_py', *args) def build_py_args(self, spec, prefix): """Arguments to pass to build_py.""" return [] def build_ext(self, spec, prefix): """Build C/C++ extensions (compile/link to build directory).""" args = self.build_ext_args(spec, prefix) self.setup_py('build_ext', *args) def build_ext_args(self, spec, prefix): """Arguments to pass to build_ext.""" return [] def build_clib(self, spec, prefix): """Build C/C++ libraries used by Python extensions.""" args = self.build_clib_args(spec, prefix) self.setup_py('build_clib', *args) def build_clib_args(self, spec, prefix): """Arguments to pass to build_clib.""" return [] def build_scripts(self, spec, prefix): '''"Build" scripts (copy and fixup #! line).''' args = self.build_scripts_args(spec, prefix) self.setup_py('build_scripts', *args) def build_scripts_args(self, spec, prefix): """Arguments to pass to build_scripts.""" return [] def clean(self, spec, prefix): """Clean up temporary files from 'build' command.""" args = self.clean_args(spec, prefix) self.setup_py('clean', *args) def clean_args(self, spec, prefix): """Arguments to pass to clean.""" return [] def install(self, spec, prefix): """Install everything from build directory.""" args = self.install_args(spec, prefix) self.setup_py('install', *args) def install_args(self, spec, prefix): """Arguments to pass to install.""" args = ['--prefix={0}'.format(prefix)] # This option causes python packages (including setuptools) NOT # to create eggs or easy-install.pth files. Instead, they # install naturally into $prefix/pythonX.Y/site-packages. # # Eggs add an extra level of indirection to sys.path, slowing # down large HPC runs. They are also deprecated in favor of # wheels, which use a normal layout when installed. # # Spack manages the package directory on its own by symlinking # extensions into the site-packages directory, so we don't really # need the .pth files or egg directories, anyway. # # We need to make sure this is only for build dependencies. A package # such as py-basemap will not build properly with this flag since # it does not use setuptools to build and those does not recognize # the --single-version-externally-managed flag if ('py-setuptools' == spec.name or # this is setuptools, or 'py-setuptools' in spec._dependencies and # it's an immediate dep 'build' in spec._dependencies['py-setuptools'].deptypes): args += ['--single-version-externally-managed', '--root=/'] return args def install_lib(self, spec, prefix): """Install all Python modules (extensions and pure Python).""" args = self.install_lib_args(spec, prefix) self.setup_py('install_lib', *args) def install_lib_args(self, spec, prefix): """Arguments to pass to install_lib.""" return [] def install_headers(self, spec, prefix): """Install C/C++ header files.""" args = self.install_headers_args(spec, prefix) self.setup_py('install_headers', *args) def install_headers_args(self, spec, prefix): """Arguments to pass to install_headers.""" return [] def install_scripts(self, spec, prefix): """Install scripts (Python or otherwise).""" args = self.install_scripts_args(spec, prefix) self.setup_py('install_scripts', *args) def install_scripts_args(self, spec, prefix): """Arguments to pass to install_scripts.""" return [] def install_data(self, spec, prefix): """Install data files.""" args = self.install_data_args(spec, prefix) self.setup_py('install_data', *args) def install_data_args(self, spec, prefix): """Arguments to pass to install_data.""" return [] def sdist(self, spec, prefix): """Create a source distribution (tarball, zip file, etc.).""" args = self.sdist_args(spec, prefix) self.setup_py('sdist', *args) def sdist_args(self, spec, prefix): """Arguments to pass to sdist.""" return [] def register(self, spec, prefix): """Register the distribution with the Python package index.""" args = self.register_args(spec, prefix) self.setup_py('register', *args) def register_args(self, spec, prefix): """Arguments to pass to register.""" return [] def bdist(self, spec, prefix): """Create a built (binary) distribution.""" args = self.bdist_args(spec, prefix) self.setup_py('bdist', *args) def bdist_args(self, spec, prefix): """Arguments to pass to bdist.""" return [] def bdist_dumb(self, spec, prefix): '''Create a "dumb" built distribution.''' args = self.bdist_dumb_args(spec, prefix) self.setup_py('bdist_dumb', *args) def bdist_dumb_args(self, spec, prefix): """Arguments to pass to bdist_dumb.""" return [] def bdist_rpm(self, spec, prefix): """Create an RPM distribution.""" args = self.bdist_rpm(spec, prefix) self.setup_py('bdist_rpm', *args) def bdist_rpm_args(self, spec, prefix): """Arguments to pass to bdist_rpm.""" return [] def bdist_wininst(self, spec, prefix): """Create an executable installer for MS Windows.""" args = self.bdist_wininst_args(spec, prefix) self.setup_py('bdist_wininst', *args) def bdist_wininst_args(self, spec, prefix): """Arguments to pass to bdist_wininst.""" return [] def upload(self, spec, prefix): """Upload binary package to PyPI.""" args = self.upload_args(spec, prefix) self.setup_py('upload', *args) def upload_args(self, spec, prefix): """Arguments to pass to upload.""" return [] def check(self, spec, prefix): """Perform some checks on the package.""" args = self.check_args(spec, prefix) self.setup_py('check', *args) def check_args(self, spec, prefix): """Arguments to pass to check.""" return [] # Testing def test(self): """Attempts to import modules of the installed package.""" # Make sure we are importing the installed modules, # not the ones in the source directory for module in self.import_modules: self.run_test(inspect.getmodule(self).python.path, ['-c', 'import {0}'.format(module)], purpose='checking import of {0}'.format(module), work_dir='spack-test') run_after('install')(PackageBase._run_default_install_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix) def view_file_conflicts(self, view, merge_map): """Report all file conflicts, excepting special cases for python. Specifically, this does not report errors for duplicate __init__.py files for packages in the same namespace. """ conflicts = list(dst for src, dst in merge_map.items() if os.path.exists(dst)) if conflicts and self.py_namespace: ext_map = view.extensions_layout.extension_map(self.extendee_spec) namespaces = set(x.package.py_namespace for x in ext_map.values()) namespace_re = (r'site-packages/{0}/__init__.py'.format( self.py_namespace)) find_namespace = match_predicate(namespace_re) if self.py_namespace in namespaces: conflicts = list(x for x in conflicts if not find_namespace(x)) return conflicts def add_files_to_view(self, view, merge_map): bin_dir = self.spec.prefix.bin python_prefix = self.extendee_spec.prefix python_is_external = self.extendee_spec.external global_view = same_path(python_prefix, view.get_projection_for_spec(self.spec)) for src, dst in merge_map.items(): if os.path.exists(dst): continue elif global_view or not path_contains_subdirectory(src, bin_dir): view.link(src, dst) elif not os.path.islink(src): shutil.copy2(src, dst) is_script = 'script' in get_filetype(src) if is_script and not python_is_external: filter_file( python_prefix, os.path.abspath(view.get_projection_for_spec( self.spec)), dst) else: orig_link_target = os.path.realpath(src) new_link_target = os.path.abspath(merge_map[orig_link_target]) view.link(new_link_target, dst) def remove_files_from_view(self, view, merge_map): ignore_namespace = False if self.py_namespace: ext_map = view.extensions_layout.extension_map(self.extendee_spec) remaining_namespaces = set(spec.package.py_namespace for name, spec in ext_map.items() if name != self.name) if self.py_namespace in remaining_namespaces: namespace_init = match_predicate( r'site-packages/{0}/__init__.py'.format(self.py_namespace)) ignore_namespace = True bin_dir = self.spec.prefix.bin global_view = ( self.extendee_spec.prefix == view.get_projection_for_spec( self.spec)) for src, dst in merge_map.items(): if ignore_namespace and namespace_init(dst): continue if global_view or not path_contains_subdirectory(src, bin_dir): view.remove_file(src, dst) else: os.remove(dst)
class RPackage(PackageBase): """Specialized class for packages that are built using R. For more information on the R build system, see: https://stat.ethz.ch/R-manual/R-devel/library/utils/html/INSTALL.html This class provides a single phase that can be overridden: 1. :py:meth:`~.RPackage.install` It has sensible defaults, and for many packages the only thing necessary will be to add dependencies """ phases = ['install'] # package attributes that can be expanded to set the homepage, url, # list_url, and git values # For CRAN packages cran = None # For Bioconductor packages bioc = None maintainers = ['glennpj'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'RPackage' extends('r') @property def homepage(self): if self.cran: return 'https://cloud.r-project.org/package=' + self.cran elif self.bioc: return 'https://bioconductor.org/packages/' + self.bioc @property def url(self): if self.cran: return ('https://cloud.r-project.org/src/contrib/' + self.cran + '_' + str(list(self.versions)[0]) + '.tar.gz') @property def list_url(self): if self.cran: return ('https://cloud.r-project.org/src/contrib/Archive/' + self.cran + '/') @property def git(self): if self.bioc: return 'https://git.bioconductor.org/packages/' + self.bioc def configure_args(self): """Arguments to pass to install via ``--configure-args``.""" return [] def configure_vars(self): """Arguments to pass to install via ``--configure-vars``.""" return [] def install(self, spec, prefix): """Installs an R package.""" config_args = self.configure_args() config_vars = self.configure_vars() args = ['--vanilla', 'CMD', 'INSTALL'] if config_args: args.append('--configure-args={0}'.format(' '.join(config_args))) if config_vars: args.append('--configure-vars={0}'.format(' '.join(config_vars))) args.extend([ '--library={0}'.format(self.module.r_lib_dir), self.stage.source_path ]) inspect.getmodule(self).R(*args) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class SIPPackage(PackageBase): """Specialized class for packages that are built using the SIP build system. See https://www.riverbankcomputing.com/software/sip/intro for more information. This class provides the following phases that can be overridden: * configure * build * install The configure phase already adds a set of default flags. To see more options, run ``python configure.py --help``. """ # Default phases phases = ['configure', 'build', 'install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'SIPPackage' #: Name of private sip module to install alongside package sip_module = 'sip' #: Callback names for install-time test install_time_test_callbacks = ['test'] extends('python') depends_on('qt') depends_on('py-sip') @property def import_modules(self): """Names of modules that the Python package provides. These are used to test whether or not the installation succeeded. These names generally come from running: .. code-block:: python >> import setuptools >> setuptools.find_packages() in the source tarball directory. If the module names are incorrectly detected, this property can be overridden by the package. Returns: list: list of strings of module names """ modules = [] root = os.path.join( self.prefix, self.spec['python'].package.config_vars['python_lib']['false'] ['false'], ) # Some Python libraries are packages: collections of modules # distributed in directories containing __init__.py files for path in find(root, '__init__.py', recursive=True): modules.append( path.replace(root + os.sep, '', 1).replace(os.sep + '__init__.py', '').replace('/', '.')) # Some Python libraries are modules: individual *.py files # found in the site-packages directory for path in find(root, '*.py', recursive=False): modules.append( path.replace(root + os.sep, '', 1).replace('.py', '').replace('/', '.')) tty.debug('Detected the following modules: {0}'.format(modules)) return modules def python(self, *args, **kwargs): """The python ``Executable``.""" inspect.getmodule(self).python(*args, **kwargs) def configure_file(self): """Returns the name of the configure file to use.""" return 'configure.py' def configure(self, spec, prefix): """Configure the package.""" configure = self.configure_file() args = self.configure_args() python_include_dir = 'python' + str(spec['python'].version.up_to(2)) args.extend([ '--verbose', '--confirm-license', '--qmake', spec['qt'].prefix.bin.qmake, '--sip', spec['py-sip'].prefix.bin.sip, '--sip-incdir', join_path(spec['py-sip'].prefix.include, python_include_dir), '--bindir', prefix.bin, '--destdir', inspect.getmodule(self).site_packages_dir, ]) self.python(configure, *args) def configure_args(self): """Arguments to pass to configure.""" return [] def build(self, spec, prefix): """Build the package.""" args = self.build_args() inspect.getmodule(self).make(*args) def build_args(self): """Arguments to pass to build.""" return [] def install(self, spec, prefix): """Install the package.""" args = self.install_args() inspect.getmodule(self).make('install', parallel=False, *args) def install_args(self): """Arguments to pass to install.""" return [] # Testing def test(self): """Attempts to import modules of the installed package.""" # Make sure we are importing the installed modules, # not the ones in the source directory for module in self.import_modules: self.run_test(inspect.getmodule(self).python.path, ['-c', 'import {0}'.format(module)], purpose='checking import of {0}'.format(module), work_dir='spack-test') run_after('install')(PackageBase._run_default_install_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix) @run_after('install') def extend_path_setup(self): # See github issue #14121 and PR #15297 module = self.spec['py-sip'].variants['module'].value if module != 'sip': module = module.split('.')[0] with working_dir(inspect.getmodule(self).site_packages_dir): with open(os.path.join(module, '__init__.py'), 'a') as f: f.write('from pkgutil import extend_path\n') f.write('__path__ = extend_path(__path__, __name__)\n')
class PerlPackage(PackageBase): """Specialized class for packages that are built using Perl. This class provides four phases that can be overridden if required: 1. :py:meth:`~.PerlPackage.configure` 2. :py:meth:`~.PerlPackage.build` 3. :py:meth:`~.PerlPackage.check` 4. :py:meth:`~.PerlPackage.install` The default methods use, in order of preference: (1) Makefile.PL, (2) Build.PL. Some packages may need to override :py:meth:`~.PerlPackage.configure_args`, which produces a list of arguments for :py:meth:`~.PerlPackage.configure`. Arguments should not include the installation base directory. """ #: Phases of a Perl package phases = ['configure', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'PerlPackage' #: Callback names for build-time test build_time_test_callbacks = ['check'] extends('perl') depends_on('perl', type=('build', 'run')) def configure_args(self): """Produces a list containing the arguments that must be passed to :py:meth:`~.PerlPackage.configure`. Arguments should not include the installation base directory, which is prepended automatically. :return: list of arguments for Makefile.PL or Build.PL """ return [] def configure(self, spec, prefix): """Runs Makefile.PL or Build.PL with arguments consisting of an appropriate installation base directory followed by the list returned by :py:meth:`~.PerlPackage.configure_args`. :raise RuntimeError: if neither Makefile.PL or Build.PL exist """ if os.path.isfile('Makefile.PL'): self.build_method = 'Makefile.PL' self.build_executable = inspect.getmodule(self).make elif os.path.isfile('Build.PL'): self.build_method = 'Build.PL' self.build_executable = Executable( join_path(self.stage.source_path, 'Build')) else: raise RuntimeError('Unknown build_method for perl package') if self.build_method == 'Makefile.PL': options = ['Makefile.PL', 'INSTALL_BASE={0}'.format(prefix)] elif self.build_method == 'Build.PL': options = ['Build.PL', '--install_base', prefix] options += self.configure_args() inspect.getmodule(self).perl(*options) def build(self, spec, prefix): """Builds a Perl package.""" self.build_executable() # Ensure that tests run after build (if requested): run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Runs built-in tests of a Perl package.""" self.build_executable('test') def install(self, spec, prefix): """Installs a Perl package.""" self.build_executable('install') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)
class PythonPackage(PackageBase): """Specialized class for packages that are built using pip.""" #: Package name, version, and extension on PyPI pypi = None maintainers = ['adamjstewart'] # Default phases phases = ['install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'PythonPackage' #: Callback names for install-time test install_time_test_callbacks = ['test'] extends('python') depends_on('py-pip', type='build') # FIXME: technically wheel is only needed when building from source, not when # installing a downloaded wheel, but I don't want to add wheel as a dep to every # package manually depends_on('py-wheel', type='build') py_namespace = None @staticmethod def _std_args(cls): return [ # Verbose '-vvv', # Disable prompting for input '--no-input', # Disable the cache '--no-cache-dir', # Don't check to see if pip is up-to-date '--disable-pip-version-check', # Install packages 'install', # Don't install package dependencies '--no-deps', # Overwrite existing packages '--ignore-installed', # Use env vars like PYTHONPATH '--no-build-isolation', # Don't warn that prefix.bin is not in PATH '--no-warn-script-location', # Ignore the PyPI package index '--no-index', ] @property def homepage(self): if self.pypi: name = self.pypi.split('/')[0] return 'https://pypi.org/project/' + name + '/' @property def url(self): if self.pypi: return ('https://files.pythonhosted.org/packages/source/' + self.pypi[0] + '/' + self.pypi) @property def list_url(self): if self.pypi: name = self.pypi.split('/')[0] return 'https://pypi.org/simple/' + name + '/' @property def import_modules(self): """Names of modules that the Python package provides. These are used to test whether or not the installation succeeded. These names generally come from running: .. code-block:: python >> import setuptools >> setuptools.find_packages() in the source tarball directory. If the module names are incorrectly detected, this property can be overridden by the package. Returns: list: list of strings of module names """ modules = [] pkg = self.spec['python'].package # Packages may be installed in platform-specific or platform-independent # site-packages directories for directory in {pkg.platlib, pkg.purelib}: root = os.path.join(self.prefix, directory) # Some Python libraries are packages: collections of modules # distributed in directories containing __init__.py files for path in find(root, '__init__.py', recursive=True): modules.append( path.replace(root + os.sep, '', 1).replace(os.sep + '__init__.py', '').replace('/', '.')) # Some Python libraries are modules: individual *.py files # found in the site-packages directory for path in find(root, '*.py', recursive=False): modules.append( path.replace(root + os.sep, '', 1).replace('.py', '').replace('/', '.')) modules = [mod for mod in modules if re.match('[a-zA-Z0-9._]+$', mod)] tty.debug('Detected the following modules: {0}'.format(modules)) return modules @property def build_directory(self): """The root directory of the Python package. This is usually the directory containing one of the following files: * ``pyproject.toml`` * ``setup.cfg`` * ``setup.py`` """ return self.stage.source_path def install_options(self, spec, prefix): """Extra arguments to be supplied to the setup.py install command.""" return [] def global_options(self, spec, prefix): """Extra global options to be supplied to the setup.py call before the install or bdist_wheel command.""" return [] def install(self, spec, prefix): """Install everything from build directory.""" args = PythonPackage._std_args(self) + ['--prefix=' + prefix] for option in self.install_options(spec, prefix): args.append('--install-option=' + option) for option in self.global_options(spec, prefix): args.append('--global-option=' + option) if self.stage.archive_file and self.stage.archive_file.endswith( '.whl'): args.append(self.stage.archive_file) else: args.append('.') pip = inspect.getmodule(self).pip with working_dir(self.build_directory): pip(*args) # Testing def test(self): """Attempts to import modules of the installed package.""" # Make sure we are importing the installed modules, # not the ones in the source directory for module in self.import_modules: self.run_test(inspect.getmodule(self).python.path, ['-c', 'import {0}'.format(module)], purpose='checking import of {0}'.format(module), work_dir='spack-test') run_after('install')(PackageBase._run_default_install_time_test_callbacks) # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix) def view_file_conflicts(self, view, merge_map): """Report all file conflicts, excepting special cases for python. Specifically, this does not report errors for duplicate __init__.py files for packages in the same namespace. """ conflicts = list(dst for src, dst in merge_map.items() if os.path.exists(dst)) if conflicts and self.py_namespace: ext_map = view.extensions_layout.extension_map(self.extendee_spec) namespaces = set(x.package.py_namespace for x in ext_map.values()) namespace_re = (r'site-packages/{0}/__init__.py'.format( self.py_namespace)) find_namespace = match_predicate(namespace_re) if self.py_namespace in namespaces: conflicts = list(x for x in conflicts if not find_namespace(x)) return conflicts def add_files_to_view(self, view, merge_map): bin_dir = self.spec.prefix.bin python_prefix = self.extendee_spec.prefix python_is_external = self.extendee_spec.external global_view = same_path(python_prefix, view.get_projection_for_spec(self.spec)) for src, dst in merge_map.items(): if os.path.exists(dst): continue elif global_view or not path_contains_subdirectory(src, bin_dir): view.link(src, dst) elif not os.path.islink(src): shutil.copy2(src, dst) is_script = 'script' in get_filetype(src) if is_script and not python_is_external: filter_file( python_prefix, os.path.abspath(view.get_projection_for_spec( self.spec)), dst) else: orig_link_target = os.path.realpath(src) new_link_target = os.path.abspath(merge_map[orig_link_target]) view.link(new_link_target, dst) def remove_files_from_view(self, view, merge_map): ignore_namespace = False if self.py_namespace: ext_map = view.extensions_layout.extension_map(self.extendee_spec) remaining_namespaces = set(spec.package.py_namespace for name, spec in ext_map.items() if name != self.name) if self.py_namespace in remaining_namespaces: namespace_init = match_predicate( r'site-packages/{0}/__init__.py'.format(self.py_namespace)) ignore_namespace = True bin_dir = self.spec.prefix.bin global_view = ( self.extendee_spec.prefix == view.get_projection_for_spec( self.spec)) to_remove = [] for src, dst in merge_map.items(): if ignore_namespace and namespace_init(dst): continue if global_view or not path_contains_subdirectory(src, bin_dir): to_remove.append(dst) else: os.remove(dst) view.remove_files(to_remove)
class CMakePackage(PackageBase): """Specialized class for packages built using CMake This class provides three phases that can be overridden: 1. :py:meth:`~.CMakePackage.cmake` 2. :py:meth:`~.CMakePackage.build` 3. :py:meth:`~.CMakePackage.install` They all have sensible defaults and for many packages the only thing necessary will be to override :py:meth:`~.CMakePackage.cmake_args`. For a finer tuning you may also override: +-----------------------------------------------+--------------------+ | **Method** | **Purpose** | +===============================================+====================+ | :py:meth:`~.CMakePackage.build_type` | Specify the value | | | for the | | | CMAKE_BUILD_TYPE | | | variable | +-----------------------------------------------+--------------------+ | :py:meth:`~.CMakePackage.root_cmakelists_dir` | Location of the | | | root CMakeLists.txt| +-----------------------------------------------+--------------------+ | :py:meth:`~.CMakePackage.build_directory` | Directory where to | | | build the package | +-----------------------------------------------+--------------------+ """ #: Phases of a CMake package phases = ['cmake', 'build', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'CMakePackage' build_targets = [] install_targets = ['install'] build_time_test_callbacks = ['check'] depends_on('cmake', type='build') def build_type(self): """Returns the correct value for the ``CMAKE_BUILD_TYPE`` variable :return: value for ``CMAKE_BUILD_TYPE`` """ return 'RelWithDebInfo' @property def root_cmakelists_dir(self): """Returns the location of the root CMakeLists.txt :return: directory containing the root CMakeLists.txt """ return self.stage.source_path @property def std_cmake_args(self): """Standard cmake arguments provided as a property for convenience of package writers :return: standard cmake arguments """ # standard CMake arguments return CMakePackage._std_args(self) @staticmethod def _std_args(pkg): """Computes the standard cmake arguments for a generic package""" try: build_type = pkg.build_type() except AttributeError: build_type = 'RelWithDebInfo' args = [ '-DCMAKE_INSTALL_PREFIX:PATH={0}'.format(pkg.prefix), '-DCMAKE_BUILD_TYPE:STRING={0}'.format(build_type), '-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON' ] if platform.mac_ver()[0]: args.append('-DCMAKE_FIND_FRAMEWORK:STRING=LAST') # Set up CMake rpath args.append('-DCMAKE_INSTALL_RPATH_USE_LINK_PATH:BOOL=FALSE') rpaths = ':'.join(spack.build_environment.get_rpaths(pkg)) args.append('-DCMAKE_INSTALL_RPATH:STRING={0}'.format(rpaths)) return args @property def build_directory(self): """Returns the directory to use when building the package :return: directory where to build the package """ return join_path(self.stage.source_path, 'spack-build') def cmake_args(self): """Produces a list containing all the arguments that must be passed to cmake, except: * CMAKE_INSTALL_PREFIX * CMAKE_BUILD_TYPE which will be set automatically. :return: list of arguments for cmake """ return [] def cmake(self, spec, prefix): """Runs ``cmake`` in the build directory""" options = [self.root_cmakelists_dir] + self.std_cmake_args + \ self.cmake_args() with working_dir(self.build_directory, create=True): inspect.getmodule(self).cmake(*options) def build(self, spec, prefix): """Make the build targets""" with working_dir(self.build_directory): inspect.getmodule(self).make(*self.build_targets) def install(self, spec, prefix): """Make the install targets""" with working_dir(self.build_directory): inspect.getmodule(self).make(*self.install_targets) run_after('build')(PackageBase._run_default_build_time_test_callbacks) def check(self): """Searches the CMake-generated Makefile for the target ``test`` and runs it if found. """ with working_dir(self.build_directory): self._if_make_target_execute('test') # Check that self.prefix is there after installation run_after('install')(PackageBase.sanity_check_prefix)