summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Gamblin <tgamblin@llnl.gov>2014-06-17 19:23:14 -0500
committerTodd Gamblin <tgamblin@llnl.gov>2014-06-22 12:50:13 -0700
commitc8414a8a40d73e7c93b3a0f3d84d5141246f518c (patch)
tree0f323e82a71a21a0ed255fd9eeff087420df32a2
parent042a4730e345ac0ee58a43bfedd11a7d32efb490 (diff)
downloadspack-c8414a8a40d73e7c93b3a0f3d84d5141246f518c.tar.gz
spack-c8414a8a40d73e7c93b3a0f3d84d5141246f518c.tar.bz2
spack-c8414a8a40d73e7c93b3a0f3d84d5141246f518c.tar.xz
spack-c8414a8a40d73e7c93b3a0f3d84d5141246f518c.zip
Add support for configuration files. Fix SPACK-24.
-rw-r--r--.gitignore1
-rw-r--r--lib/spack/spack/cmd/config.py77
-rw-r--r--lib/spack/spack/config.py449
-rw-r--r--lib/spack/spack/test/__init__.py3
-rw-r--r--lib/spack/spack/test/config.py69
5 files changed, 598 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index 7010bf7ede..ed2012d208 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
*~
.DS_Store
.idea
+/etc/spackconfig
diff --git a/lib/spack/spack/cmd/config.py b/lib/spack/spack/cmd/config.py
new file mode 100644
index 0000000000..25d302f94b
--- /dev/null
+++ b/lib/spack/spack/cmd/config.py
@@ -0,0 +1,77 @@
+##############################################################################
+# Copyright (c) 2013, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://scalability-llnl.github.io/spack
+# Please also see the LICENSE file 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 General Public License (as published by
+# the Free Software Foundation) version 2.1 dated 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 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 sys
+import argparse
+
+import llnl.util.tty as tty
+
+import spack.config
+
+description = "Get and set configuration options."
+
+def setup_parser(subparser):
+ scope_group = subparser.add_mutually_exclusive_group()
+
+ # File scope
+ scope_group.add_argument(
+ '--user', action='store_const', const='user', dest='scope',
+ help="Use config file in user home directory (default).")
+ scope_group.add_argument(
+ '--site', action='store_const', const='site', dest='scope',
+ help="Use config file in spack prefix.")
+
+ # Get (vs. default set)
+ subparser.add_argument(
+ '--get', action='store_true', dest='get',
+ help="Get the value associated with a key.")
+
+ # positional arguments (value is only used on set)
+ subparser.add_argument(
+ 'key', help="Get the value associated with KEY")
+ subparser.add_argument(
+ 'value', nargs='?', default=None,
+ help="Value to associate with key")
+
+
+def config(parser, args):
+ key, value = args.key, args.value
+
+ # If we're writing need to do a few checks.
+ if not args.get:
+ # Default scope for writing is user scope.
+ if not args.scope:
+ args.scope = 'user'
+
+ if args.value is None:
+ tty.die("No value for '%s'. " % args.key
+ + "Spack config requires a key and a value.")
+
+ config = spack.config.get_config(args.scope)
+
+ if args.get:
+ print config.get_value(key)
+ else:
+ config.set_value(key, value)
+ config.write()
diff --git a/lib/spack/spack/config.py b/lib/spack/spack/config.py
new file mode 100644
index 0000000000..b36b83bfaa
--- /dev/null
+++ b/lib/spack/spack/config.py
@@ -0,0 +1,449 @@
+##############################################################################
+# Copyright (c) 2013, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://scalability-llnl.github.io/spack
+# Please also see the LICENSE file 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 General Public License (as published by
+# the Free Software Foundation) version 2.1 dated 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 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
+##############################################################################
+"""This module implements Spack's configuration file handling.
+
+Configuration file scopes
+===============================
+
+When Spack runs, it pulls configuration data from several config
+files, much like bash shells. In Spack, there are two configuration
+scopes:
+
+ 1. ``site``: Spack loads site-wide configuration options from
+ ``$(prefix)/etc/spackconfig``.
+
+ 2. ``user``: Spack next loads per-user configuration options from
+ ~/.spackconfig.
+
+If user options have the same names as site options, the user options
+take precedence.
+
+
+Configuration file format
+===============================
+
+Configuration files are formatted using .gitconfig syntax, which is
+much like Windows .INI format. This format is implemented by Python's
+ConfigParser class, and it's easy to read and versatile.
+
+The file is divided into sections, like this ``compiler`` section::
+
+ [compiler]
+ cc = /usr/bin/gcc
+
+In each section there are options (cc), and each option has a value
+(/usr/bin/gcc).
+
+Borrowing from git, we also allow named sections, e.g.:
+
+ [compiler "gcc@4.7.3"]
+ cc = /usr/bin/gcc
+
+This is a compiler section, but it's for the specific compiler,
+``gcc@4.7.3``. ``gcc@4.7.3`` is the name.
+
+
+Keys
+===============================
+
+Together, the section, name, and option, separated by periods, are
+called a ``key``. Keys can be used on the command line to set
+configuration options explicitly (this is also borrowed from git).
+
+For example, to change the C compiler used by gcc@4.7.3, you could do
+this:
+
+ spack config compiler.gcc@4.7.3.cc /usr/local/bin/gcc
+
+That will create a named compiler section in the user's .spackconfig
+like the one shown above.
+"""
+import os
+import re
+import inspect
+from collections import OrderedDict
+import ConfigParser as cp
+
+from llnl.util.lang import memoized
+
+import spack
+import spack.error
+
+__all__ = [
+ 'SpackConfigParser', 'get_config', 'SpackConfigurationError',
+ 'InvalidConfigurationScopeError', 'InvalidSectionNameError',
+ 'ReadOnlySpackConfigError', 'ConfigParserError', 'NoOptionError',
+ 'NoSectionError']
+
+_named_section_re = r'([^ ]+) "([^"]+)"'
+
+"""Names of scopes and their corresponding configuration files."""
+_scopes = OrderedDict({
+ 'site' : os.path.join(spack.etc_path, 'spackconfig'),
+ 'user' : os.path.expanduser('~/.spackconfig')
+})
+
+_field_regex = r'^([\w-]*)' \
+ r'(?:\.(.*(?=.)))?' \
+ r'(?:\.([\w-]+))?$'
+
+_section_regex = r'^([\w-]*)\s*' \
+ r'\"([^"]*\)\"$'
+
+
+def get_config(scope=None):
+ """Get a Spack configuration object, which can be used to set options.
+
+ With no arguments, this returns a SpackConfigParser with config
+ options loaded from all config files. This is how client code
+ should read Spack configuration options.
+
+ Optionally, a scope parameter can be provided. Valid scopes
+ are ``site`` and ``user``. If a scope is provided, only the
+ options from that scope's configuration file are loaded. The
+ caller can set or unset options, then call ``write()`` on the
+ config object to write it back out to the original config file.
+ """
+ if scope is None:
+ return SpackConfigParser()
+ elif scope not in _scopes:
+ raise UnknownConfigurationScopeError(scope)
+ else:
+ return SpackConfigParser(_scopes[scope])
+
+
+def _parse_key(key):
+ """Return the section, name, and option the field describes.
+ Values are returned in a 3-tuple.
+
+ e.g.:
+ The field name ``compiler.gcc@4.7.3.cc`` refers to the 'cc' key
+ in a section that looks like this:
+
+ [compiler "gcc@4.7.3"]
+ cc = /usr/local/bin/gcc
+
+ * The section is ``compiler``
+ * The name is ``gcc@4.7.3``
+ * The key is ``cc``
+ """
+ match = re.search(_field_regex, key)
+ if match:
+ return match.groups()
+ else:
+ raise InvalidSectionNameError(key)
+
+
+def _make_section_name(section, name):
+ if not name:
+ return section
+ return '%s "%s"' % (section, name)
+
+
+def _autokey(fun):
+ """Allow a function to be called with a string key like
+ 'compiler.gcc.cc', or with the section, name, and option
+ separated. Function should take at least three args, e.g.:
+
+ fun(self, section, name, option, [...])
+
+ This will allow the function above to be called normally or
+ with a string key, e.g.:
+
+ fun(self, key, [...])
+ """
+ argspec = inspect.getargspec(fun)
+ fun_nargs = len(argspec[0])
+
+ def string_key_func(*args):
+ nargs = len(args)
+ if nargs == fun_nargs - 2:
+ section, name, option = _parse_key(args[1])
+ return fun(args[0], section, name, option, *args[2:])
+
+ elif nargs == fun_nargs:
+ return fun(*args)
+
+ else:
+ raise TypeError(
+ "%s takes %d or %d args (found %d)."
+ % (fun.__name__, fun_nargs - 2, fun_nargs, len(args)))
+ return string_key_func
+
+
+
+class SpackConfigParser(cp.RawConfigParser):
+ """Slightly modified from Python's raw config file parser to accept
+ leading whitespace.
+ """
+ # Slightly modified Python option expression. This one allows
+ # leading whitespace.
+ OPTCRE = re.compile(
+ r'\s*(?P<option>[^:=\s][^:=]*)' # allow leading whitespace
+ r'\s*(?P<vi>[:=])\s*'
+ r'(?P<value>.*)$'
+ )
+
+ def __init__(self, file_or_files=None):
+ cp.RawConfigParser.__init__(self, dict_type=OrderedDict)
+
+ if not file_or_files:
+ file_or_files = [path for path in _scopes.values()]
+
+ if isinstance(file_or_files, basestring):
+ self.read([file_or_files])
+ self.filename = file_or_files
+
+ else:
+ self.read(file_or_files)
+ self.filename = None
+
+
+ @_autokey
+ def set_value(self, section, name, option, value):
+ """Set the value for a key. If the key is in a section or named
+ section that does not yet exist, add that section.
+ """
+ sn = _make_section_name(section, name)
+ if not self.has_section(sn):
+ self.add_section(sn)
+ self.set(sn, option, value)
+
+
+ @_autokey
+ def get_value(self, section, name, option):
+ """Get the value for a key. Raises NoOptionError or NoSectionError if
+ the key is not present."""
+ sn = _make_section_name(section, name)
+ try:
+ return self.get(sn, option)
+
+ except cp.NoOptionError, e: raise NoOptionError(e)
+ except cp.NoSectionError, e: raise NoSectionError(e)
+ except cp.Error, e: raise ConfigParserError(e)
+
+
+ @_autokey
+ def has_value(self, section, name, option):
+ """Return whether the configuration file has a value for a
+ particular key."""
+ sn = _make_section_name(section, name)
+ return self.has_option(sn, option)
+
+
+ def get_section_names(self, sectype):
+ """Get all named sections with the specified type.
+ A named section looks like this:
+
+ [compiler "gcc@4.7"]
+
+ Names of sections are returned as a list, e.g.:
+
+ ['gcc@4.7', 'intel@12.3', 'pgi@4.2']
+
+ You can get items in the sections like this:
+ """
+ sections = []
+ for secname in self.sections():
+ match = re.match(_named_section_re, secname)
+ if match:
+ t, name = match.groups()
+ if t == sectype:
+ sections.append(name)
+ return sections
+
+
+ def write(self, path_or_fp=None):
+ """Write this configuration out to a file.
+
+ If called with no arguments, this will write the
+ configuration out to the file from which it was read. If
+ this config was read from multiple files, e.g. site
+ configuration and then user configuration, write will
+ simply raise an error.
+
+ If called with a path or file object, this will write the
+ configuration out to the supplied path or file object.
+ """
+ if path_or_fp is None:
+ if not self.filename:
+ raise ReadOnlySpackConfigError()
+ path_or_fp = self.filename
+
+ if isinstance(path_or_fp, basestring):
+ path_or_fp = open(path_or_fp, 'w')
+
+ self._write(path_or_fp)
+
+
+ def _read(self, fp, fpname):
+ """This is a copy of Python 2.7's _read() method, with support for
+ continuation lines removed.
+ """
+ cursect = None # None, or a dictionary
+ optname = None
+ lineno = 0
+ e = None # None, or an exception
+ while True:
+ line = fp.readline()
+ if not line:
+ break
+ lineno = lineno + 1
+ # comment or blank line?
+ if line.strip() == '' or line[0] in '#;':
+ continue
+ if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
+ # no leading whitespace
+ continue
+ # a section header or option header?
+ else:
+ # is it a section header?
+ mo = self.SECTCRE.match(line)
+ if mo:
+ sectname = mo.group('header')
+ if sectname in self._sections:
+ cursect = self._sections[sectname]
+ elif sectname == cp.DEFAULTSECT:
+ cursect = self._defaults
+ else:
+ cursect = self._dict()
+ cursect['__name__'] = sectname
+ self._sections[sectname] = cursect
+ # So sections can't start with a continuation line
+ optname = None
+ # no section header in the file?
+ elif cursect is None:
+ raise cp.MissingSectionHeaderError(fpname, lineno, line)
+ # an option line?
+ else:
+ mo = self._optcre.match(line)
+ if mo:
+ optname, vi, optval = mo.group('option', 'vi', 'value')
+ optname = self.optionxform(optname.rstrip())
+ # This check is fine because the OPTCRE cannot
+ # match if it would set optval to None
+ if optval is not None:
+ if vi in ('=', ':') and ';' in optval:
+ # ';' is a comment delimiter only if it follows
+ # a spacing character
+ pos = optval.find(';')
+ if pos != -1 and optval[pos-1].isspace():
+ optval = optval[:pos]
+ optval = optval.strip()
+ # allow empty values
+ if optval == '""':
+ optval = ''
+ cursect[optname] = [optval]
+ else:
+ # valueless option handling
+ cursect[optname] = optval
+ else:
+ # a non-fatal parsing error occurred. set up the
+ # exception but keep going. the exception will be
+ # raised at the end of the file and will contain a
+ # list of all bogus lines
+ if not e:
+ e = cp.ParsingError(fpname)
+ e.append(lineno, repr(line))
+ # if any parsing errors occurred, raise an exception
+ if e:
+ raise e
+
+ # join the multi-line values collected while reading
+ all_sections = [self._defaults]
+ all_sections.extend(self._sections.values())
+ for options in all_sections:
+ for name, val in options.items():
+ if isinstance(val, list):
+ options[name] = '\n'.join(val)
+
+
+ def _write(self, fp):
+ """Write an .ini-format representation of the configuration state.
+
+ This is taken from the default Python 2.7 source. It writes 4
+ spaces at the beginning of lines instead of no leading space.
+ """
+ if self._defaults:
+ fp.write("[%s]\n" % cp.DEFAULTSECT)
+ for (key, value) in self._defaults.items():
+ fp.write(" %s = %s\n" % (key, str(value).replace('\n', '\n\t')))
+ fp.write("\n")
+ for section in self._sections:
+ # Allow leading whitespace
+ fp.write("[%s]\n" % section)
+ for (key, value) in self._sections[section].items():
+ if key == "__name__":
+ continue
+ if (value is not None) or (self._optcre == self.OPTCRE):
+ key = " = ".join((key, str(value).replace('\n', '\n\t')))
+ fp.write(" %s\n" % (key))
+ fp.write("\n")
+
+
+
+class SpackConfigurationError(spack.error.SpackError):
+ def __init__(self, *args):
+ super(SpackConfigurationError, self).__init__(*args)
+
+
+class InvalidConfigurationScopeError(SpackConfigurationError):
+ def __init__(self, scope):
+ super(InvalidConfigurationScopeError, self).__init__(
+ "Invalid configuration scope: '%s'" % scope,
+ "Options are: %s" % ", ".join(*_scopes.values()))
+
+
+class InvalidSectionNameError(SpackConfigurationError):
+ """Raised when the name for a section is invalid."""
+ def __init__(self, name):
+ super(InvalidSectionNameError, self).__init__(
+ "Invalid section specifier: '%s'" % name)
+
+
+class ReadOnlySpackConfigError(SpackConfigurationError):
+ """Raised when user attempts to write to a config read from multiple files."""
+ def __init__(self):
+ super(ReadOnlySpackConfigError, self).__init__(
+ "Can only write to a single-file SpackConfigParser")
+
+
+class ConfigParserError(SpackConfigurationError):
+ """Wrapper for the Python ConfigParser's errors"""
+ def __init__(self, error):
+ super(ConfigParserError, self).__init__(str(error))
+ self.error = error
+
+
+class NoOptionError(ConfigParserError):
+ """Wrapper for ConfigParser NoOptionError"""
+ def __init__(self, error):
+ super(NoOptionError, self).__init__(error)
+
+
+class NoSectionError(ConfigParserError):
+ """Wrapper for ConfigParser NoOptionError"""
+ def __init__(self, error):
+ super(NoSectionError, self).__init__(error)
diff --git a/lib/spack/spack/test/__init__.py b/lib/spack/spack/test/__init__.py
index 5aac710119..5442189c2e 100644
--- a/lib/spack/spack/test/__init__.py
+++ b/lib/spack/spack/test/__init__.py
@@ -44,7 +44,8 @@ test_names = ['versions',
'concretize',
'multimethod',
'install',
- 'package_sanity']
+ 'package_sanity',
+ 'config']
def list_tests():
diff --git a/lib/spack/spack/test/config.py b/lib/spack/spack/test/config.py
new file mode 100644
index 0000000000..c676e9a35b
--- /dev/null
+++ b/lib/spack/spack/test/config.py
@@ -0,0 +1,69 @@
+##############################################################################
+# Copyright (c) 2013, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://scalability-llnl.github.io/spack
+# Please also see the LICENSE file 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 General Public License (as published by
+# the Free Software Foundation) version 2.1 dated 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 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 unittest
+import shutil
+import os
+from tempfile import mkdtemp
+
+from spack.config import *
+
+
+class ConfigTest(unittest.TestCase):
+
+ @classmethod
+ def setUp(cls):
+ cls.tmp_dir = mkdtemp('.tmp', 'spack-config-test-')
+
+
+ @classmethod
+ def tearDown(cls):
+ shutil.rmtree(cls.tmp_dir, True)
+
+
+ def get_path(self):
+ return os.path.join(ConfigTest.tmp_dir, "spackconfig")
+
+
+ def test_write_key(self):
+ config = SpackConfigParser(self.get_path())
+ config.set_value('compiler.cc', 'a')
+ config.set_value('compiler.cxx', 'b')
+ config.set_value('compiler', 'gcc@4.7.3', 'cc', 'c')
+ config.set_value('compiler', 'gcc@4.7.3', 'cxx', 'd')
+ config.write()
+
+ config = SpackConfigParser(self.get_path())
+
+ self.assertEqual(config.get_value('compiler.cc'), 'a')
+ self.assertEqual(config.get_value('compiler.cxx'), 'b')
+ self.assertEqual(config.get_value('compiler', 'gcc@4.7.3', 'cc'), 'c')
+ self.assertEqual(config.get_value('compiler', 'gcc@4.7.3', 'cxx'), 'd')
+
+ self.assertEqual(config.get_value('compiler', None, 'cc'), 'a')
+ self.assertEqual(config.get_value('compiler', None, 'cxx'), 'b')
+ self.assertEqual(config.get_value('compiler.gcc@4.7.3.cc'), 'c')
+ self.assertEqual(config.get_value('compiler.gcc@4.7.3.cxx'), 'd')
+
+ self.assertRaises(NoOptionError, config.get_value, 'compiler', None, 'fc')