# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Service functions and classes to implement the hooks
for Spack's command extensions.
"""
import os
import re
import sys
import types
import llnl.util.lang
import spack.config
import spack.error
_extension_regexp = re.compile(r'spack-(\w[-\w]*)$')
# TODO: For consistency we should use spack.cmd.python_name(), but
# currently this would create a circular relationship between
# spack.cmd and spack.extensions.
def _python_name(cmd_name):
return cmd_name.replace('-', '_')
[docs]def extension_name(path):
"""Returns the name of the extension in the path passed as argument.
Args:
path (str): path where the extension resides
Returns:
The extension name.
Raises:
ExtensionNamingError: if path does not match the expected format
for a Spack command extension.
"""
regexp_match = re.search(_extension_regexp,
os.path.basename(os.path.normpath(path)))
if not regexp_match:
raise ExtensionNamingError(path)
return regexp_match.group(1)
[docs]def load_command_extension(command, path):
"""Loads a command extension from the path passed as argument.
Args:
command (str): name of the command (contains ``-``, not ``_``).
path (str): base path of the command extension
Returns:
A valid module if found and loadable; None if not found. Module
loading exceptions are passed through.
"""
extension = _python_name(extension_name(path))
# Compute the name of the module we search, exit early if already imported
cmd_package = '{0}.{1}.cmd'.format(__name__, extension)
python_name = _python_name(command)
module_name = '{0}.{1}'.format(cmd_package, python_name)
if module_name in sys.modules:
return sys.modules[module_name]
# Compute the absolute path of the file to be loaded, along with the
# name of the python module where it will be stored
cmd_path = os.path.join(path, extension, 'cmd', python_name + '.py')
# Short circuit if the command source file does not exist
if not os.path.exists(cmd_path):
return None
def ensure_package_creation(name):
package_name = '{0}.{1}'.format(__name__, name)
if package_name in sys.modules:
return
parts = [path] + name.split('.') + ['__init__.py']
init_file = os.path.join(*parts)
if os.path.exists(init_file):
m = llnl.util.lang.load_module_from_file(package_name, init_file)
else:
m = types.ModuleType(package_name)
# Setting __path__ to give spack extensions the
# ability to import from their own tree, see:
#
# https://docs.python.org/3/reference/import.html#package-path-rules
#
m.__path__ = [os.path.dirname(init_file)]
sys.modules[package_name] = m
# Create a searchable package for both the root folder of the extension
# and the subfolder containing the commands
ensure_package_creation(extension)
ensure_package_creation(extension + '.cmd')
# TODO: Upon removal of support for Python 2.6 substitute the call
# TODO: below with importlib.import_module(module_name)
module = llnl.util.lang.load_module_from_file(module_name, cmd_path)
sys.modules[module_name] = module
return module
[docs]def get_command_paths():
"""Return the list of paths where to search for command files."""
command_paths = []
extension_paths = spack.config.get('config:extensions') or []
for path in extension_paths:
extension = _python_name(extension_name(path))
command_paths.append(os.path.join(path, extension, 'cmd'))
return command_paths
[docs]def path_for_extension(target_name, *paths):
"""Return the test root dir for a given extension.
Args:
target_name (str): name of the extension to test
*paths: paths where the extensions reside
Returns:
Root directory where tests should reside or None
"""
for path in paths:
name = extension_name(path)
if name == target_name:
return path
else:
raise IOError('extension "{0}" not found'.format(target_name))
[docs]def get_module(cmd_name):
"""Imports the extension module for a particular command name
and returns it.
Args:
cmd_name (str): name of the command for which to get a module
(contains ``-``, not ``_``).
"""
# If built-in failed the import search the extension
# directories in order
extensions = spack.config.get('config:extensions') or []
for folder in extensions:
module = load_command_extension(cmd_name, folder)
if module:
return module
else:
raise CommandNotFoundError(cmd_name)
[docs]def get_template_dirs():
"""Returns the list of directories where to search for templates
in extensions.
"""
extension_dirs = spack.config.get('config:extensions') or []
extensions = [os.path.join(x, 'templates') for x in extension_dirs]
return extensions
[docs]class CommandNotFoundError(spack.error.SpackError):
"""Exception class thrown when a requested command is not recognized as
such.
"""
def __init__(self, cmd_name):
super(CommandNotFoundError, self).__init__(
'{0} is not a recognized Spack command or extension command;'
' check with `spack commands`.'.format(cmd_name))
[docs]class ExtensionNamingError(spack.error.SpackError):
"""Exception class thrown when a configured extension does not follow
the expected naming convention.
"""
def __init__(self, path):
super(ExtensionNamingError, self).__init__(
'{0} does not match the format for a Spack extension path.'
.format(path))