class AspellDictPackage(AutotoolsPackage): """Specialized class for building aspell dictionairies.""" extends('aspell') def view_destination(self, view): aspell_spec = self.spec['aspell'] if view.root != aspell_spec.prefix: raise ExtensionError( 'aspell does not support non-global extensions') aspell = aspell_spec.command return aspell('dump', 'config', 'dict-dir', output=str).strip() def view_source(self): return self.prefix.lib def patch(self): filter_file(r'^dictdir=.*$', 'dictdir=/lib', 'configure') filter_file(r'^datadir=.*$', 'datadir=/lib', 'configure') def configure(self, spec, prefix): aspell = spec['aspell'].prefix.bin.aspell prezip = spec['aspell'].prefix.bin.prezip destdir = prefix sh = which('sh') sh('./configure', '--vars', "ASPELL={0}".format(aspell), "PREZIP={0}".format(prezip), "DESTDIR={0}".format(destdir))
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 This class provides a single phase that can be overridden: * install It has sensible defaults and for many packages the only thing necessary will be to add dependencies """ phases = ['install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'RPackage' extends('r') def install(self, spec, prefix): """Install the 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 PackageBase.sanity_check('install')(PackageBase.sanity_check_prefix)
class RacketPackage(PackageBase): """Specialized class for packages that are built using Racket's `raco pkg install` and `raco setup` commands. This class provides the following phases that can be overridden: * install * setup """ #: Package name, version, and extension on PyPI maintainers = ['elfprince13'] # Default phases phases = ['install'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'RacketPackage' extends('racket') pkgs = False subdirectory = None # type: Optional[str] name = None # type: Optional[str] parallel = True @lang.classproperty def homepage(cls): if cls.pkgs: return 'https://pkgs.racket-lang.org/package/{0}'.format(cls.name) @property def build_directory(self): ret = os.getcwd() if self.subdirectory: ret = os.path.join(ret, self.subdirectory) return ret def install(self, spec, prefix): """Install everything from build directory.""" raco = Executable("raco") with working_dir(self.build_directory): allow_parallel = self.parallel and ( not env_flag(SPACK_NO_PARALLEL_MAKE)) args = [ 'pkg', 'install', '-t', 'dir', '-n', self.name, '--deps', 'fail', '--ignore-implies', '--copy', '-i', '-j', str(determine_number_of_jobs(allow_parallel)), '--', os.getcwd() ] try: raco(*args) except ProcessError: args.insert(-2, "--skip-installed") raco(*args) tty.warn(( "Racket package {0} was already installed, uninstalling via " "Spack may make someone unhappy!").format(self.name))
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 AspellDictPackage(AutotoolsPackage): """Specialized class for builing aspell dictionairies.""" extends('aspell') def patch(self): filter_file(r'^dictdir=.*$', 'dictdir=/lib', 'configure') filter_file(r'^datadir=.*$', 'datadir=/lib', 'configure') def configure(self, spec, prefix): aspell = spec['aspell'].prefix.bin.aspell prezip = spec['aspell'].prefix.bin.prezip destdir = prefix sh = which('sh') sh('./configure', '--vars', "ASPELL={0}".format(aspell), "PREZIP={0}".format(prezip), "DESTDIR={0}".format(destdir))
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 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 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 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 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 = ['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 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 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 --no-user-cfg setup.py <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'] # To be used in UI queries that require to know which # build-system class we are using build_system_class = 'PythonPackage' extends('python') def setup_file(self, spec, prefix): """Returns the name of the setup file to use.""" return 'setup.py' def build_directory(self): """The directory containing the ``setup.py`` file.""" return self.stage.source_path def python(self, *args): inspect.getmodule(self).python(*args) def setup_py(self, *args): setup = self.setup_file(self.spec, self.prefix) with working_dir(self.build_directory()): self.python(setup, '--no-user-cfg', *args) # The following phases and their descriptions come from: # $ python setup.py --help-commands # Only standard commands are included here, but some packages # define extra commands as well 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.""" return ['--prefix={0}'.format(prefix)] 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 [] # Check that self.prefix is there after installation PackageBase.sanity_check('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 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 LuaPackage(PackageBase): """Specialized class for lua packages""" phases = ['unpack', 'generate_luarocks_config', 'preprocess', 'install'] #: This attribute is used in UI queries that need to know the build #: system base class build_system_class = 'LuaPackage' list_depth = 1 # LuaRocks requires at least one level of spidering to find versions depends_on('lua-lang') extends('lua', when='^lua') with when('^lua-luajit'): extends('lua-luajit') depends_on('luajit') depends_on('lua-luajit+lualinks') with when('^lua-luajit-openresty'): extends('lua-luajit-openresty') depends_on('luajit') depends_on('lua-luajit-openresty+lualinks') def unpack(self, spec, prefix): if os.path.splitext(self.stage.archive_file)[1] == '.rock': directory = self.luarocks('unpack', self.stage.archive_file, output=str) dirlines = directory.split('\n') # TODO: figure out how to scope this better os.chdir(dirlines[2]) def _generate_tree_line(self, name, prefix): return """{{ name = "{name}", root = "{prefix}" }};""".format( name=name, prefix=prefix, ) def _luarocks_config_path(self): return os.path.join(self.stage.source_path, 'spack_luarocks.lua') def generate_luarocks_config(self, spec, prefix): spec = self.spec table_entries = [] for d in spec.traverse(deptypes=("build", "run"), deptype_query="run"): if d.package.extends(self.extendee_spec): table_entries.append(self._generate_tree_line( d.name, d.prefix)) path = self._luarocks_config_path() with open(path, 'w') as config: config.write(""" deps_mode="all" rocks_trees={{ {} }} """.format("\n".join(table_entries))) return path def setup_build_environment(self, env): env.set('LUAROCKS_CONFIG', self._luarocks_config_path()) def preprocess(self, spec, prefix): """Override this to preprocess source before building with luarocks""" pass @property def lua(self): return Executable(self.spec['lua-lang'].prefix.bin.lua) @property def luarocks(self): lr = Executable(self.spec['lua-lang'].prefix.bin.luarocks) return lr def luarocks_args(self): return [] def install(self, spec, prefix): rock = '.' specs = find('.', '*.rockspec', recursive=False) if specs: rock = specs[0] rocks_args = self.luarocks_args() rocks_args.append(rock) self.luarocks('--tree=' + prefix, 'make', *rocks_args)