diff options
-rw-r--r-- | lib/spack/docs/packaging_guide.rst | 136 | ||||
-rw-r--r-- | lib/spack/docs/tutorial/examples/Autotools/autotools_class.py | 460 | ||||
-rw-r--r-- | lib/spack/docs/tutorial/examples/Cmake/cmake_class.py | 224 | ||||
-rw-r--r-- | lib/spack/docs/tutorial/examples/Makefile/makefile_class.py | 129 | ||||
-rw-r--r-- | lib/spack/docs/tutorial/examples/PyPackage/python_package_class.py | 399 | ||||
-rw-r--r-- | lib/spack/docs/tutorial_buildsystems.rst | 8 | ||||
-rw-r--r-- | lib/spack/spack/build_environment.py | 37 | ||||
-rw-r--r-- | lib/spack/spack/build_systems/autotools.py | 23 | ||||
-rw-r--r-- | lib/spack/spack/build_systems/cmake.py | 51 | ||||
-rw-r--r-- | lib/spack/spack/package.py | 41 | ||||
-rw-r--r-- | lib/spack/spack/test/cc.py | 8 | ||||
-rw-r--r-- | lib/spack/spack/test/flag_handlers.py | 176 |
12 files changed, 372 insertions, 1320 deletions
diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst index ac89f9724a..a066aa96df 100644 --- a/lib/spack/docs/packaging_guide.rst +++ b/lib/spack/docs/packaging_guide.rst @@ -2564,98 +2564,104 @@ build system. Compiler flags ^^^^^^^^^^^^^^ -Compiler flags set by the user through the Spec object can be passed to -the build in one of two ways. For packages inheriting from the -``CmakePackage`` or ``AutotoolsPackage`` classes, the build environment -passes those flags to the relevant environment variables (``CFLAGS``, -``CXXFLAGS``, etc) that are respected by the build system. For all other -packages, the default behavior is to inject the flags directly into the -compiler commands using Spack's compiler wrappers. +Compiler flags set by the user through the Spec object can be passed +to the build in one of three ways. By default, the build environment +injects these flags directly into the compiler commands using Spack's +compiler wrappers. In cases where the build system requires knowledge +of the compiler flags, they can be registered with the build system by +alternatively passing them through environment variables or as build +system arguments. The flag_handler method can be used to change this +behavior. + +Packages can override the flag_handler method with one of three +built-in flag_handlers. The built-in flag_handlers are named +``inject_flags``, ``env_flags``, and ``build_system_flags``. The +``inject_flags`` method is the default. The ``env_flags`` method puts +all of the flags into the environment variables that ``make`` uses as +implicit variables ('CFLAGS', 'CXXFLAGS', etc.). The +``build_system_flags`` method adds the flags as +arguments to the invocation of ``configure`` or ``cmake``, +respectively. .. warning:: - The flag handling methods described in this section are in beta. - The exact semantics are liable to change to improve usability. + Passing compiler flags using build system arguments is only + supported for CMake and Autotools packages. Individual packages may + also differ in whether they properly respect these arguments. -Individual packages can override the default behavior for the flag -handling. Packages can define a ``default_flag_handler`` method that -applies to all sets of flags handled by Spack, or may define -individual methods ``cflags_handler``, ``cxxflags_handler``, -etc. Spack will apply the individual method for a flag set if it -exists, otherwise the ``default_flag_handler`` method if it exists, -and fall back on the default for that package class if neither exists. +Individual packages may also define their own ``flag_handler`` +methods. The ``flag_handler`` method takes the package instance +(``self``), the name of the flag, and a list of the values of the +flag. It will be called on each of the six compiler flags supported in +Spack. It should return a triple of ``(injf, envf, bsf)`` where +``injf`` is a list of flags to inject via the Spack compiler wrappers, +``envf`` is a list of flags to set in the appropriate environment +variables, and ``bsf`` is a list of flags to pass to the build system +as arguments. -These methods are defined on the package class, and take two -parameters in addition to the packages itself. The ``env`` parameter -is an ``EnvironmentModifications`` object that can be used to change -the build environment. The ``flag_val`` parameter is a tuple. Its -first entry is the name of the flag (``cflags``, ``cxxflags``, etc.) -and its second entry is a list of the values for that flag. +.. warning:: -There are three primary idioms that can be combined to create whatever -behavior the package requires. + Passing a non-empty list of flags to ``bsf`` for a build system + that does not support build system arguments will result in an + error. -1. The default behavior for packages inheriting from -``AutotoolsPackage`` or ``CmakePackage``. +Here are the definitions of the three built-in flag handlers: .. code-block:: python - def default_flag_handler(self, env, flag_val): - env.append_flags(flag_val[0].upper(), ' '.join(flag_val[1])) - return [] + def inject_flags(self, name, flags): + return (flags, None, None) -2. The default behavior for other packages + def env_flags(self, name, flags): + return (None, flags, None) -.. code-block:: python + def build_system_flags(self, name, flags): + return (None, None, flags) - def default_flag_handler(self, env, flag_val): - return flag_val[1] +.. note:: + Returning ``[]`` and ``None`` are equivalent in a ``flag_handler`` + method. -3. Packages may have additional flags to add to the build. These flags -can be added to either idiom above. For example: +Packages can override the default behavior either by specifying one of +the built-in flag handlers, .. code-block:: python - def default_flag_handler(self, env, flag_val): - flags = flag_val[1] - flags.append('-flag') - return flags - -or - -.. code-block:: python + flag_handler = <PackageClass>.env_flags - def default_flag_handler(self, env, flag_val): - env.append_flags(flag_val[0].upper(), ' '.join(flag_val[1])) - env.append_flags(flag_val[0].upper(), '-flag') - return [] +where ``<PackageClass>`` can be any of the subclasses of PackageBase +discussed in :ref:`installation_procedure`, -Packages may also opt for methods that include aspects of any of the -idioms above. E.g. +or by implementing the flag_handler method. Suppose for a package +``Foo`` we need to pass ``cflags``, ``cxxflags``, and ``cppflags`` +through the environment, the rest of the flags through compiler +wrapper injection, and we need to add ``-lbar`` to ``ldlibs``. The +following flag handler method accomplishes that. .. code-block:: python - def default_flag_handler(self, env, flag_val): - flags = [] - if len(flag_val[1]) > 3: - env.append_flags(flag_val[0].upper(), ' '.join(flag_val[1][3:])) - flags = flag_val[1][:3] - else: - flags = flag_val[1] - flags.append('-flag') - return flags + def flag_handler(self, name, flags): + if name in ['cflags', 'cxxflags', 'cppflags']: + return (None, flags, None) + elif name == 'ldlibs': + flags.append('-lbar') + return (flags, None, None) Because these methods can pass values through environment variables, -it is important not to override these variables unnecessarily in other -package methods. In the ``setup_environment`` and +it is important not to override these variables unnecessarily +(E.g. setting ``env['CFLAGS']``) in other package methods when using +non-default flag handlers. In the ``setup_environment`` and ``setup_dependent_environment`` methods, use the ``append_flags`` method of the ``EnvironmentModifications`` class to append values to a -list of flags whenever there is no good reason to override the -existing value. In the ``install`` method and other methods that can -operate on the build environment directly through the ``env`` -variable, test for environment variable existance before overriding -values to add compiler flags. +list of flags whenever the flag handler is ``env_flags``. If the +package passes flags through the environment or the build system +manually (in the install method, for example), we recommend using the +default flag handler, or removind manual references and implementing a +custom flag handler method that adds the desired flags to export as +environment variables or pass to the build system. Manual flag passing +is likely to interfere with the ``env_flags`` and +``build_system_flags`` methods. In rare circumstances such as compiling and running small unit tests, a package developer may need to know what are the appropriate compiler diff --git a/lib/spack/docs/tutorial/examples/Autotools/autotools_class.py b/lib/spack/docs/tutorial/examples/Autotools/autotools_class.py deleted file mode 100644 index 90ff8540bd..0000000000 --- a/lib/spack/docs/tutorial/examples/Autotools/autotools_class.py +++ /dev/null @@ -1,460 +0,0 @@ -############################################################################## -# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. -# -# This file is part of Spack. -# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. -# LLNL-CODE-647188 -# -# For details, see https://github.com/llnl/spack -# Please also see the NOTICE and LICENSE files for our notice and the LGPL. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License (as -# published by the Free Software Foundation) version 2.1, February 1999. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and -# conditions of the GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -############################################################################## - -import inspect -import os -import os.path -import shutil -import stat -from subprocess import PIPE -from subprocess import check_call - -import llnl.util.tty as tty -from llnl.util.filesystem import working_dir, join_path, force_remove -from spack.package import PackageBase, run_after, run_before -from spack.util.executable import Executable - - -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 = [] - - @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).""" - - if not self.patch_config_guess or not self.spec.satisfies( - 'target=ppc64le' - ): - 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: - pass - 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: - pass - - raise RuntimeError('Failed to find suitable config.guess') - - @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 default_flag_handler(self, spack_env, flag_val): - # Relies on being the first thing that can affect the spack_env - # EnvironmentModification after it is instantiated or no other - # method trying to affect these variables. Currently both are true - # flag_val is a tuple (flag, value_list). - spack_env.set(flag_val[0].upper(), - ' '.join(flag_val[1])) - return [] - - @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, - 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}' - options = [ - (value, condition.format(name=name, value=value) in spec) - for value in self.variants[name].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) diff --git a/lib/spack/docs/tutorial/examples/Cmake/cmake_class.py b/lib/spack/docs/tutorial/examples/Cmake/cmake_class.py deleted file mode 100644 index 5b0f5526c9..0000000000 --- a/lib/spack/docs/tutorial/examples/Cmake/cmake_class.py +++ /dev/null @@ -1,224 +0,0 @@ -############################################################################## -# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. -# -# This file is part of Spack. -# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. -# LLNL-CODE-647188 -# -# For details, see https://github.com/llnl/spack -# Please also see the NOTICE and LICENSE files for our notice and the LGPL. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License (as -# published by the Free Software Foundation) version 2.1, February 1999. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and -# conditions of the GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -############################################################################## - -import inspect -import os -import platform - -import spack.build_environment -from llnl.util.filesystem import working_dir, join_path -from spack.util.environment import filter_system_paths -from spack.directives import depends_on, variant -from spack.package import PackageBase, InstallError, run_after - - -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 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 - return CMakePackage._std_args(self) - - @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.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)) - # 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 - - @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 default_flag_handler(self, spack_env, flag_val): - # Relies on being the first thing that can affect the spack_env - # EnvironmentModification after it is instantiated or no other - # method trying to affect these variables. Currently both are true - # flag_val is a tuple (flag, value_list) - spack_env.set(flag_val[0].upper(), - ' '.join(flag_val[1])) - return [] - - 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') - elif self.generator == 'Ninja': - self._if_ninja_target_execute('test') - - # Check that self.prefix is there after installation - run_after('install')(PackageBase.sanity_check_prefix) diff --git a/lib/spack/docs/tutorial/examples/Makefile/makefile_class.py b/lib/spack/docs/tutorial/examples/Makefile/makefile_class.py deleted file mode 100644 index 5ffb88f43d..0000000000 --- a/lib/spack/docs/tutorial/examples/Makefile/makefile_class.py +++ /dev/null @@ -1,129 +0,0 @@ -############################################################################## -# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. -# -# This file is part of Spack. -# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. -# LLNL-CODE-647188 -# -# For details, see https://github.com/llnl/spack -# Please also see the NOTICE and LICENSE files for our notice and the LGPL. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License (as -# published by the Free Software Foundation) version 2.1, February 1999. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and -# conditions of the GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -############################################################################## - -import inspect - -import llnl.util.tty as tty -from llnl.util.filesystem import working_dir -from spack.package import PackageBase, run_after - - -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) diff --git a/lib/spack/docs/tutorial/examples/PyPackage/python_package_class.py b/lib/spack/docs/tutorial/examples/PyPackage/python_package_class.py deleted file mode 100644 index 190620d7a2..0000000000 --- a/lib/spack/docs/tutorial/examples/PyPackage/python_package_class.py +++ /dev/null @@ -1,399 +0,0 @@ -############################################################################## -# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. -# -# This file is part of Spack. -# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. -# LLNL-CODE-647188 -# -# For details, see https://github.com/llnl/spack -# Please also see the NOTICE and LICENSE files for our notice and the LGPL. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License (as -# published by the Free Software Foundation) version 2.1, February 1999. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and -# conditions of the GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -############################################################################## - -import inspect -import os - -from spack.directives import depends_on, extends -from spack.package import PackageBase, run_after - -from llnl.util.filesystem import working_dir - - -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 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')) - - 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(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(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. - if ('py-setuptools' == spec.name or # this is setuptools, or - 'py-setuptools' in spec._dependencies): # it's an immediate dep - 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) diff --git a/lib/spack/docs/tutorial_buildsystems.rst b/lib/spack/docs/tutorial_buildsystems.rst index 8f5dce0892..d92db13062 100644 --- a/lib/spack/docs/tutorial_buildsystems.rst +++ b/lib/spack/docs/tutorial_buildsystems.rst @@ -103,7 +103,7 @@ This will open the :code:`AutotoolsPackage` file in your text editor. long examples. We only show what is relevant to the packager. -.. literalinclude:: tutorial/examples/Autotools/autotools_class.py +.. literalinclude:: ../../../lib/spack/spack/build_systems/autotools.py :language: python :emphasize-lines: 42,45,62 :lines: 40-95,259-267 @@ -202,7 +202,7 @@ Let's also take a look inside the :code:`MakefilePackage` class: Take note of the following: -.. literalinclude:: tutorial/examples/Makefile/makefile_class.py +.. literalinclude:: ../../../lib/spack/spack/build_systems/makefile.py :language: python :lines: 33-79,89-107 :emphasize-lines: 48,54,61 @@ -480,7 +480,7 @@ Let's look at these defaults in the :code:`CMakePackage` class: And go into a bit of detail on the highlighted sections: -.. literalinclude:: tutorial/examples/Cmake/cmake_class.py +.. literalinclude:: ../../../lib/spack/spack/build_systems/cmake.py :language: python :lines: 37-92, 94-155, 174-211 :emphasize-lines: 57,68,86,94,96,99,100,101,102,111,117,135,136 @@ -675,7 +675,7 @@ at the :code:`PythonPackage` class: We see the following: -.. literalinclude:: tutorial/examples/PyPackage/python_package_class.py +.. literalinclude:: ../../../lib/spack/spack/build_systems/python.py :language: python :lines: 35, 161-364 :linenos: diff --git a/lib/spack/spack/build_environment.py b/lib/spack/spack/build_environment.py index f1cf0d5049..cab455695e 100644 --- a/lib/spack/spack/build_environment.py +++ b/lib/spack/spack/build_environment.py @@ -128,7 +128,6 @@ class MakeExecutable(Executable): def set_compiler_environment_variables(pkg, env): assert(pkg.spec.concrete) compiler = pkg.compiler - flags = pkg.spec.compiler_flags # Set compiler variables used by CMake and autotools assert all(key in compiler.link_paths for key in ( @@ -160,11 +159,28 @@ def set_compiler_environment_variables(pkg, env): env.set('SPACK_F77_RPATH_ARG', compiler.f77_rpath_arg) env.set('SPACK_FC_RPATH_ARG', compiler.fc_rpath_arg) - # Add every valid compiler flag to the environment, prefixed with "SPACK_" + # Trap spack-tracked compiler flags as appropriate. + # env_flags are easy to accidentally override. + inject_flags = {} + env_flags = {} + build_system_flags = {} + for flag in spack.spec.FlagMap.valid_compiler_flags(): + injf, envf, bsf = pkg.flag_handler(flag, pkg.spec.compiler_flags[flag]) + inject_flags[flag] = injf or [] + env_flags[flag] = envf or [] + build_system_flags[flag] = bsf or [] + + # Place compiler flags as specified by flag_handler for flag in spack.spec.FlagMap.valid_compiler_flags(): # Concreteness guarantees key safety here - if flags[flag] != []: - env.set('SPACK_' + flag.upper(), ' '.join(f for f in flags[flag])) + if inject_flags[flag]: + # variables SPACK_<FLAG> inject flags through wrapper + var_name = 'SPACK_{0}'.format(flag.upper()) + env.set(var_name, ' '.join(f for f in inject_flags[flag])) + if env_flags[flag]: + # implicit variables + env.set(flag.upper(), ' '.join(f for f in env_flags[flag])) + pkg.flags_to_build_system_args(build_system_flags) env.set('SPACK_COMPILER_SPEC', str(pkg.spec.compiler)) @@ -559,19 +575,6 @@ def setup_package(pkg, dirty): for s in pkg.spec.traverse(): assert s.package.spec is s - # Trap spack-tracked compiler flags as appropriate. - # Must be before set_compiler_environment_variables - # Current implementation of default flag handler relies on this being - # the first thing to affect the spack_env (so there is no appending), or - # on no other build_environment methods trying to affect these variables - # (CFLAGS, CXXFLAGS, etc). Currently both are true, either is sufficient. - for flag in spack.spec.FlagMap.valid_compiler_flags(): - trap_func = getattr(pkg, flag + '_handler', - getattr(pkg, 'default_flag_handler', - lambda x, y: y[1])) - flag_val = pkg.spec.compiler_flags[flag] - pkg.spec.compiler_flags[flag] = trap_func(spack_env, (flag, flag_val)) - set_compiler_environment_variables(pkg, spack_env) set_build_environment_variables(pkg, spack_env, dirty) pkg.architecture.platform.setup_platform_environment(pkg, spack_env) diff --git a/lib/spack/spack/build_systems/autotools.py b/lib/spack/spack/build_systems/autotools.py index 8f2af3e21e..01a59feeba 100644 --- a/lib/spack/spack/build_systems/autotools.py +++ b/lib/spack/spack/build_systems/autotools.py @@ -182,15 +182,6 @@ class AutotoolsPackage(PackageBase): """Override to provide another place to build the package""" return self.configure_directory - def default_flag_handler(self, spack_env, flag_val): - # Relies on being the first thing that can affect the spack_env - # EnvironmentModification after it is instantiated or no other - # method trying to affect these variables. Currently both are true - # flag_val is a tuple (flag, value_list). - spack_env.set(flag_val[0].upper(), - ' '.join(flag_val[1])) - return [] - @run_before('autoreconf') def delete_configure_to_force_update(self): if self.force_autoreconf: @@ -256,12 +247,24 @@ class AutotoolsPackage(PackageBase): """ 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 = ['--prefix={0}'.format(prefix)] + self.configure_args() + 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) diff --git a/lib/spack/spack/build_systems/cmake.py b/lib/spack/spack/build_systems/cmake.py index ca9ec6f803..13c752eb2e 100644 --- a/lib/spack/spack/build_systems/cmake.py +++ b/lib/spack/spack/build_systems/cmake.py @@ -109,7 +109,9 @@ class CMakePackage(PackageBase): :return: standard cmake arguments """ # standard CMake arguments - return CMakePackage._std_args(self) + std_cmake_args = CMakePackage._std_args(self) + std_cmake_args += getattr(self, 'cmake_flag_args', []) + return std_cmake_args @staticmethod def _std_args(pkg): @@ -154,6 +156,44 @@ class CMakePackage(PackageBase): 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 @@ -162,15 +202,6 @@ class CMakePackage(PackageBase): """ return join_path(self.stage.source_path, 'spack-build') - def default_flag_handler(self, spack_env, flag_val): - # Relies on being the first thing that can affect the spack_env - # EnvironmentModification after it is instantiated or no other - # method trying to affect these variables. Currently both are true - # flag_val is a tuple (flag, value_list) - spack_env.set(flag_val[0].upper(), - ' '.join(flag_val[1])) - return [] - def cmake_args(self): """Produces a list containing all the arguments that must be passed to cmake, except: diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py index 712f252494..18091f2e69 100644 --- a/lib/spack/spack/package.py +++ b/lib/spack/spack/package.py @@ -1707,6 +1707,47 @@ class PackageBase(with_metaclass(PackageMeta, object)): """ pass + def inject_flags(self, name, flags): + """ + flag_handler that injects all flags through the compiler wrapper. + """ + return (flags, None, None) + + def env_flags(self, name, flags): + """ + flag_handler that adds all flags to canonical environment variables. + """ + return (None, flags, None) + + def build_system_flags(self, name, flags): + """ + flag_handler that passes flags to the build system arguments. Any + package using `build_system_flags` must also implement + `flags_to_build_system_args`, or derive from a class that + implements it. Currently, AutotoolsPackage and CMakePackage + implement it. + """ + return (None, None, flags) + + flag_handler = inject_flags + # The flag handler method is called for each of the allowed compiler flags. + # It returns a triple of inject_flags, env_flags, build_system_flags. + # The flags returned as inject_flags are injected through the spack + # compiler wrappers. + # The flags returned as env_flags are passed to the build system through + # the environment variables of the same name. + # The flags returned as build_system_flags are passed to the build system + # package subclass to be turned into the appropriate part of the standard + # arguments. This is implemented for build system classes where + # appropriate and will otherwise raise a NotImplementedError. + + def flags_to_build_system_args(self, flags): + # Takes flags as a dict name: list of values + if any(v for v in flags.values()): + msg = 'The {0} build system'.format(self.__class__.__name__) + msg += ' cannot take command line arguments for compiler flags' + raise NotImplementedError(msg) + @staticmethod def uninstall_by_spec(spec, force=False): if not os.path.isdir(spec.prefix): diff --git a/lib/spack/spack/test/cc.py b/lib/spack/spack/test/cc.py index a50af94ac2..671379c717 100644 --- a/lib/spack/spack/test/cc.py +++ b/lib/spack/spack/test/cc.py @@ -211,8 +211,12 @@ class CompilerWrapperTest(unittest.TestCase): ' '.join(test_command) + ' ' + '-lfoo') - os.environ['SPACK_LDFLAGS'] = '' - os.environ['SPACK_LDLIBS'] = '' + del os.environ['SPACK_CFLAGS'] + del os.environ['SPACK_CXXFLAGS'] + del os.environ['SPACK_FFLAGS'] + del os.environ['SPACK_CPPFLAGS'] + del os.environ['SPACK_LDFLAGS'] + del os.environ['SPACK_LDLIBS'] def test_dep_rpath(self): """Ensure RPATHs for root package are added.""" diff --git a/lib/spack/spack/test/flag_handlers.py b/lib/spack/spack/test/flag_handlers.py new file mode 100644 index 0000000000..0b785a948f --- /dev/null +++ b/lib/spack/spack/test/flag_handlers.py @@ -0,0 +1,176 @@ +############################################################################## +# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# +# This file is part of Spack. +# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. +# LLNL-CODE-647188 +# +# For details, see https://github.com/spack/spack +# Please also see the NOTICE and LICENSE files for our notice and the LGPL. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (as +# published by the Free Software Foundation) version 2.1, February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and +# conditions of the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## +import pytest +import os + +import spack.spec +import spack.build_environment + + +@pytest.fixture() +def temp_env(): + old_env = os.environ.copy() + yield + os.environ = old_env + + +def add_O3_to_build_system_cflags(name, flags): + build_system_flags = [] + if name == 'cflags': + build_system_flags.append('-O3') + return (flags, None, build_system_flags) + + +@pytest.mark.usefixtures('config') +class TestFlagHandlers(object): + def test_no_build_system_flags(self, temp_env): + # Test that both autotools and cmake work getting no build_system flags + s1 = spack.spec.Spec('callpath') + s1.concretize() + pkg1 = spack.repo.get(s1) + spack.build_environment.setup_package(pkg1, False) + + s2 = spack.spec.Spec('libelf') + s2.concretize() + pkg2 = spack.repo.get(s2) + spack.build_environment.setup_package(pkg2, False) + + # Use cppflags as a canary + assert 'SPACK_CPPFLAGS' not in os.environ + assert 'CPPFLAGS' not in os.environ + + def test_inject_flags(self, temp_env): + s = spack.spec.Spec('mpileaks cppflags=-g') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = pkg.inject_flags + spack.build_environment.setup_package(pkg, False) + + assert os.environ['SPACK_CPPFLAGS'] == '-g' + assert 'CPPFLAGS' not in os.environ + + def test_env_flags(self, temp_env): + s = spack.spec.Spec('mpileaks cppflags=-g') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = pkg.env_flags + spack.build_environment.setup_package(pkg, False) + + assert os.environ['CPPFLAGS'] == '-g' + assert 'SPACK_CPPFLAGS' not in os.environ + + def test_build_system_flags_cmake(self, temp_env): + s = spack.spec.Spec('callpath cppflags=-g') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = pkg.build_system_flags + spack.build_environment.setup_package(pkg, False) + + assert 'SPACK_CPPFLAGS' not in os.environ + assert 'CPPFLAGS' not in os.environ + + expected = set(['-DCMAKE_C_FLAGS=-g', '-DCMAKE_CXX_FLAGS=-g', + '-DCMAKE_Fortran_FLAGS=-g']) + assert set(pkg.cmake_flag_args) == expected + + def test_build_system_flags_autotools(self, temp_env): + s = spack.spec.Spec('libelf cppflags=-g') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = pkg.build_system_flags + spack.build_environment.setup_package(pkg, False) + + assert 'SPACK_CPPFLAGS' not in os.environ + assert 'CPPFLAGS' not in os.environ + + assert 'CPPFLAGS=-g' in pkg.configure_flag_args + + def test_build_system_flags_not_implemented(self, temp_env): + s = spack.spec.Spec('mpileaks cppflags=-g') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = pkg.build_system_flags + + # Test the command line flags method raises a NotImplementedError + try: + spack.build_environment.setup_package(pkg, False) + assert False + except NotImplementedError: + assert True + + def test_add_build_system_flags_autotools(self, temp_env): + s = spack.spec.Spec('libelf cppflags=-g') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = add_O3_to_build_system_cflags + spack.build_environment.setup_package(pkg, False) + + assert '-g' in os.environ['SPACK_CPPFLAGS'] + assert 'CPPFLAGS' not in os.environ + + assert pkg.configure_flag_args == ['CFLAGS=-O3'] + + def test_add_build_system_flags_cmake(self, temp_env): + s = spack.spec.Spec('callpath cppflags=-g') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = add_O3_to_build_system_cflags + spack.build_environment.setup_package(pkg, False) + + assert '-g' in os.environ['SPACK_CPPFLAGS'] + assert 'CPPFLAGS' not in os.environ + + assert pkg.cmake_flag_args == ['-DCMAKE_C_FLAGS=-O3'] + + def test_ld_flags_cmake(self, temp_env): + s = spack.spec.Spec('callpath ldflags=-mthreads') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = pkg.build_system_flags + spack.build_environment.setup_package(pkg, False) + + assert 'SPACK_LDFLAGS' not in os.environ + assert 'LDFLAGS' not in os.environ + + expected = set(['-DCMAKE_EXE_LINKER_FLAGS=-mthreads', + '-DCMAKE_MODULE_LINKER_FLAGS=-mthreads', + '-DCMAKE_SHARED_LINKER_FLAGS=-mthreads', + '-DCMAKE_STATIC_LINKER_FLAGS=-mthreads']) + assert set(pkg.cmake_flag_args) == expected + + def test_ld_libs_cmake(self, temp_env): + s = spack.spec.Spec('callpath ldlibs=-lfoo') + s.concretize() + pkg = spack.repo.get(s) + pkg.flag_handler = pkg.build_system_flags + spack.build_environment.setup_package(pkg, False) + + assert 'SPACK_LDLIBS' not in os.environ + assert 'LDLIBS' not in os.environ + + expected = set(['-DCMAKE_C_STANDARD_LIBRARIES=-lfoo', + '-DCMAKE_CXX_STANDARD_LIBRARIES=-lfoo', + '-DCMAKE_Fortran_STANDARD_LIBRARIES=-lfoo']) + assert set(pkg.cmake_flag_args) == expected |