From 0da639298c8d2d956c76d8beb9f08c54c87cfec7 Mon Sep 17 00:00:00 2001
From: Todd Gamblin <tgamblin@llnl.gov>
Date: Thu, 27 Oct 2016 22:36:59 -0700
Subject: [PATCH] Move temp directory configuration to config.yaml

- Moved temp finding logic to spack.stage
- Updated stage tests
- Added tests for new path substaitution of $user, $spack, $tempdir
---
 etc/spack/defaults/config.yaml             |  10 +-
 lib/spack/llnl/util/filesystem.py          |  12 --
 lib/spack/spack/__init__.py                |  21 ---
 lib/spack/spack/repository.py              |  14 +-
 lib/spack/spack/stage.py                   | 117 +++++++++----
 lib/spack/spack/test/config.py             |  52 ++++++
 lib/spack/spack/test/mock_packages_test.py |  28 +++-
 lib/spack/spack/test/stage.py              | 184 +++++++++++----------
 lib/spack/spack/util/path.py               |  68 ++++++++
 9 files changed, 330 insertions(+), 176 deletions(-)
 create mode 100644 lib/spack/spack/util/path.py

diff --git a/etc/spack/defaults/config.yaml b/etc/spack/defaults/config.yaml
index 5ce60049ce..6a749b5f6d 100644
--- a/etc/spack/defaults/config.yaml
+++ b/etc/spack/defaults/config.yaml
@@ -31,13 +31,13 @@ config:
   # You can use $tempdir to refer to the system default temp directory
   # (as returned by tempfile.gettempdir()).
   #
-  # A value of $local indicates that Spack should run builds directly
-  # inside its install directory without staging them in temporary space.
+  # A value of $spack/var/spack/stage indicates that Spack should run
+  # builds directly inside its install directory without staging them in
+  # temporary space.
   build_stage:
-    - /usr/workspace/*/%u
     - $tempdir
-    - /nfs/tmp2/%u
-    - $local
+    - /nfs/tmp2/$user
+    - $spack/var/spack/stage
 
 
   # Cache directory already downloaded source tarballs and archived
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
index e522fdda6d..31e09f2fe6 100644
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -25,7 +25,6 @@
 import collections
 import errno
 import fileinput
-import getpass
 import glob
 import numbers
 import os
@@ -46,7 +45,6 @@
     'can_access',
     'change_sed_delimiter',
     'copy_mode',
-    'expand_user',
     'filter_file',
     'find_libraries',
     'fix_darwin_install_name',
@@ -229,16 +227,6 @@ def is_exe(path):
     return os.path.isfile(path) and os.access(path, os.X_OK)
 
 
-def expand_user(path):
-    """Find instances of '%u' in a path and replace with the current user's
-       username."""
-    username = getpass.getuser()
-    if not username and '%u' in path:
-        tty.die("Couldn't get username to complete path '%s'" % path)
-
-    return path.replace('%u', username)
-
-
 def mkdirp(*paths):
     """Creates a directory, as well as parent directories if needed."""
     for path in paths:
diff --git a/lib/spack/spack/__init__.py b/lib/spack/spack/__init__.py
index ab03c0c848..782a9b8a9f 100644
--- a/lib/spack/spack/__init__.py
+++ b/lib/spack/spack/__init__.py
@@ -119,27 +119,6 @@
 # certifiates. e.g., curl should use the -k option.
 insecure = False
 
-# Whether to build in tmp space or directly in the stage_path.
-# If this is true, then spack will make stage directories in
-# a tmp filesystem, and it will symlink them into stage_path.
-use_tmp_stage = True
-
-# Locations to use for staging and building, in order of preference
-# Use a %u to add a username to the stage paths here, in case this
-# is a shared filesystem.  Spack will use the first of these paths
-# that it can create.
-tmp_dirs = []
-_default_tmp = tempfile.gettempdir()
-_tmp_user = getpass.getuser()
-
-_tmp_candidates = (_default_tmp, '/nfs/tmp2', '/tmp', '/var/tmp')
-for path in _tmp_candidates:
-    # don't add a second username if it's already unique by user.
-    if _tmp_user not in path:
-        tmp_dirs.append(join_path(path, '%u', 'spack-stage'))
-    else:
-        tmp_dirs.append(join_path(path, 'spack-stage'))
-
 # Whether spack should allow installation of unsafe versions of
 # software.  "Unsafe" versions are ones it doesn't have a checksum
 # for.
diff --git a/lib/spack/spack/repository.py b/lib/spack/spack/repository.py
index 4696d1d5bc..47d6df85b3 100644
--- a/lib/spack/spack/repository.py
+++ b/lib/spack/spack/repository.py
@@ -44,6 +44,7 @@
 import spack.error
 import spack.spec
 from spack.provider_index import ProviderIndex
+from spack.util.path import canonicalize_path
 from spack.util.naming import *
 
 #
@@ -93,19 +94,6 @@ def __getattr__(self, name):
         return getattr(self, name)
 
 
-def substitute_spack_prefix(path):
-    """Replaces instances of $spack with Spack's prefix."""
-    return re.sub(r'^\$spack', spack.prefix, path)
-
-
-def canonicalize_path(path):
-    """Substitute $spack, expand user home, take abspath."""
-    path = substitute_spack_prefix(path)
-    path = os.path.expanduser(path)
-    path = os.path.abspath(path)
-    return path
-
-
 class RepoPath(object):
     """A RepoPath is a list of repos that function as one.
 
diff --git a/lib/spack/spack/stage.py b/lib/spack/spack/stage.py
index 7e6b543799..ff10a38ca8 100644
--- a/lib/spack/spack/stage.py
+++ b/lib/spack/spack/stage.py
@@ -28,6 +28,7 @@
 import hashlib
 import shutil
 import tempfile
+import getpass
 from urlparse import urljoin
 
 import llnl.util.tty as tty
@@ -41,9 +42,72 @@
 import spack.fetch_strategy as fs
 import spack.error
 from spack.version import *
+from spack.util.path import canonicalize_path
 from spack.util.crypto import prefix_bits, bit_length
 
-STAGE_PREFIX = 'spack-stage-'
+_stage_prefix = 'spack-stage-'
+
+
+def _first_accessible_path(paths):
+    """Find a tmp dir that exists that we can access."""
+    for path in paths:
+        try:
+            # try to create the path if it doesn't exist.
+            path = canonicalize_path(path)
+            mkdirp(path)
+
+            # ensure accessible
+            if not can_access(path):
+                continue
+
+            # return it if successful.
+            return path
+
+        except OSError:
+            tty.debug('OSError while checking temporary path: %s' % path)
+            continue
+
+    return None
+
+
+# cached temporary root
+_tmp_root = None
+_use_tmp_stage = True
+
+
+def get_tmp_root():
+    global _tmp_root, _use_tmp_stage
+
+    if not _use_tmp_stage:
+        return None
+
+    if _tmp_root is None:
+        config = spack.config.get_config('config')
+        candidates = config['build_stage']
+        if isinstance(candidates, basestring):
+            candidates = [candidates]
+
+        path = _first_accessible_path(candidates)
+        if not path:
+            raise StageError("No accessible stage paths in %s", candidates)
+
+        # Return None to indicate we're using a local staging area.
+        if path == canonicalize_path(spack.stage_path):
+            _use_tmp_stage = False
+            return None
+
+        # ensure that any temp path is unique per user, so users don't
+        # fight over shared temporary space.
+        user = getpass.getuser()
+        if user not in path:
+            path = os.path.join(path, user, 'spack-stage')
+        else:
+            path = os.path.join(path, 'spack-stage')
+
+        mkdirp(path)
+        _tmp_root = path
+
+    return _tmp_root
 
 
 class Stage(object):
@@ -141,9 +205,8 @@ def __init__(
         # TODO : won't be the same as the temporary stage area in tmp_root
         self.name = name
         if name is None:
-            self.name = STAGE_PREFIX + next(tempfile._get_candidate_names())
+            self.name = _stage_prefix + next(tempfile._get_candidate_names())
         self.mirror_path = mirror_path
-        self.tmp_root = find_tmp_root()
 
         # Try to construct here a temporary name for the stage directory
         # If this is a named stage, then construct a named path.
@@ -217,10 +280,11 @@ def _need_to_create_path(self):
 
         # Path looks ok, but need to check the target of the link.
         if os.path.islink(self.path):
-            real_path = os.path.realpath(self.path)
-            real_tmp = os.path.realpath(self.tmp_root)
+            tmp_root = get_tmp_root()
+            if tmp_root is not None:
+                real_path = os.path.realpath(self.path)
+                real_tmp = os.path.realpath(tmp_root)
 
-            if spack.use_tmp_stage:
                 # If we're using a tmp dir, it's a link, and it points at the
                 # right spot, then keep it.
                 if (real_path.startswith(real_tmp) and
@@ -416,11 +480,11 @@ def chdir_to_source(self):
         """
         path = self.source_path
         if not path:
-            tty.die("Attempt to chdir before expanding archive.")
+            raise StageError("Attempt to chdir before expanding archive.")
         else:
             os.chdir(path)
             if not os.listdir(path):
-                tty.die("Archive was empty for %s" % self.name)
+                raise StageError("Archive was empty for %s" % self.name)
 
     def restage(self):
         """Removes the expanded archive path if it exists, then re-expands
@@ -429,17 +493,17 @@ def restage(self):
         self.fetcher.reset()
 
     def create(self):
-        """
-        Creates the stage directory
+        """Creates the stage directory.
 
-        If self.tmp_root evaluates to False, the stage directory is
-        created directly under spack.stage_path, otherwise this will
-        attempt to create a stage in a temporary directory and link it
-        into spack.stage_path.
+        If get_tmp_root() is None, the stage directory is created
+        directly under spack.stage_path, otherwise this will attempt to
+        create a stage in a temporary directory and link it into
+        spack.stage_path.
 
         Spack will use the first writable location in spack.tmp_dirs
         to create a stage. If there is no valid location in tmp_dirs,
         fall back to making the stage inside spack.stage_path.
+
         """
         # Create the top-level stage directory
         mkdirp(spack.stage_path)
@@ -448,8 +512,10 @@ def create(self):
         # If a tmp_root exists then create a directory there and then link it
         # in the stage area, otherwise create the stage directory in self.path
         if self._need_to_create_path():
-            if self.tmp_root:
-                tmp_dir = tempfile.mkdtemp('', STAGE_PREFIX, self.tmp_root)
+            tmp_root = get_tmp_root()
+            if tmp_root is not None:
+                tmp_dir = tempfile.mkdtemp('', _stage_prefix, tmp_root)
+                tty.debug('link %s -> %s' % (self.path, tmp_dir))
                 os.symlink(tmp_dir, self.path)
             else:
                 mkdirp(self.path)
@@ -614,25 +680,6 @@ def purge():
             remove_linked_tree(stage_path)
 
 
-def find_tmp_root():
-    if spack.use_tmp_stage:
-        for tmp in spack.tmp_dirs:
-            try:
-                # Replace %u with username
-                expanded = expand_user(tmp)
-
-                # try to create a directory for spack stuff
-                mkdirp(expanded)
-
-                # return it if successful.
-                return expanded
-
-            except OSError:
-                continue
-
-    return None
-
-
 class StageError(spack.error.SpackError):
     """"Superclass for all errors encountered during staging."""
 
diff --git a/lib/spack/spack/test/config.py b/lib/spack/spack/test/config.py
index 02f53a5261..de6cd79594 100644
--- a/lib/spack/spack/test/config.py
+++ b/lib/spack/spack/test/config.py
@@ -24,10 +24,12 @@
 ##############################################################################
 import os
 import shutil
+import getpass
 from tempfile import mkdtemp
 
 import spack
 import spack.config
+from spack.util.path import canonicalize_path
 from ordereddict_backport import OrderedDict
 from spack.test.mock_packages_test import *
 
@@ -217,3 +219,53 @@ def test_write_to_same_priority_file(self):
         # Same check again, to ensure consistency.
         self.check_config(a_comps, *self.a_comp_specs)
         self.check_config(b_comps, *self.b_comp_specs)
+
+    def check_canonical(self, var, expected):
+        """ensure things are substituted properly and canonicalized."""
+        path = '/foo/bar/baz'
+
+        self.assertEqual(canonicalize_path(var + path),
+                         expected + path)
+
+        self.assertEqual(canonicalize_path(path + var),
+                         path + '/' + expected)
+
+        self.assertEqual(canonicalize_path(path + var + path),
+                         expected + path)
+
+    def test_substitute_config_variables(self):
+        prefix = spack.prefix.lstrip('/')
+
+        self.assertEqual(os.path.join('/foo/bar/baz', prefix),
+                         canonicalize_path('/foo/bar/baz/$spack'))
+
+        self.assertEqual(os.path.join(spack.prefix, 'foo/bar/baz'),
+                         canonicalize_path('$spack/foo/bar/baz/'))
+
+        self.assertEqual(os.path.join('/foo/bar/baz', prefix, 'foo/bar/baz'),
+                         canonicalize_path('/foo/bar/baz/$spack/foo/bar/baz/'))
+
+        self.assertEqual(os.path.join('/foo/bar/baz', prefix),
+                         canonicalize_path('/foo/bar/baz/${spack}'))
+
+        self.assertEqual(os.path.join(spack.prefix, 'foo/bar/baz'),
+                         canonicalize_path('${spack}/foo/bar/baz/'))
+
+        self.assertEqual(
+            os.path.join('/foo/bar/baz', prefix, 'foo/bar/baz'),
+            canonicalize_path('/foo/bar/baz/${spack}/foo/bar/baz/'))
+
+        self.assertNotEqual(
+            os.path.join('/foo/bar/baz', prefix, 'foo/bar/baz'),
+            canonicalize_path('/foo/bar/baz/${spack/foo/bar/baz/'))
+
+    def test_substitute_user(self):
+        user = getpass.getuser()
+        self.assertEqual('/foo/bar/' + user + '/baz',
+                         canonicalize_path('/foo/bar/$user/baz'))
+
+    def test_substitute_tempdir(self):
+        tempdir = tempfile.gettempdir()
+        self.assertEqual(tempdir, canonicalize_path('$tempdir'))
+        self.assertEqual(tempdir + '/foo/bar/baz',
+                         canonicalize_path('$tempdir/foo/bar/baz'))
diff --git a/lib/spack/spack/test/mock_packages_test.py b/lib/spack/spack/test/mock_packages_test.py
index cba9c81e9d..4e1f243c82 100644
--- a/lib/spack/spack/test/mock_packages_test.py
+++ b/lib/spack/spack/test/mock_packages_test.py
@@ -180,6 +180,27 @@
       externalmodule@1.0%gcc@4.5.0: external-module
 """
 
+mock_config = """\
+config:
+  install_tree: $spack/opt/spack
+  build_stage:
+  - $tempdir
+  - /nfs/tmp2/$user
+  - $spack/var/spack/stage
+  source_cache: $spack/var/spack/cache
+  misc_cache: ~/.spack/cache
+  verify_ssl: true
+  checksum: true
+  dirty: false
+"""
+
+# these are written out to mock config files.
+mock_configs = {
+    'config.yaml': mock_config,
+    'compilers.yaml': mock_compiler_config,
+    'packages.yaml': mock_packages_config,
+}
+
 
 class MockPackagesTest(unittest.TestCase):
 
@@ -199,11 +220,10 @@ def initmock(self):
         self.mock_user_config = os.path.join(self.temp_config, 'user')
         mkdirp(self.mock_site_config)
         mkdirp(self.mock_user_config)
-        for confs in [('compilers.yaml', mock_compiler_config),
-                      ('packages.yaml', mock_packages_config)]:
-            conf_yaml = os.path.join(self.mock_site_config, confs[0])
+        for filename, data in mock_configs.items():
+            conf_yaml = os.path.join(self.mock_site_config, filename)
             with open(conf_yaml, 'w') as f:
-                f.write(confs[1])
+                f.write(data)
 
         # TODO: Mocking this up is kind of brittle b/c ConfigScope
         # TODO: constructor modifies config_scopes.  Make it cleaner.
diff --git a/lib/spack/spack/test/stage.py b/lib/spack/spack/test/stage.py
index ec661bfe50..a21142c2cb 100644
--- a/lib/spack/spack/test/stage.py
+++ b/lib/spack/spack/test/stage.py
@@ -27,64 +27,71 @@
 """
 import os
 import shutil
-import unittest
+import tempfile
 from contextlib import *
 
 import spack
+import spack.stage
 from llnl.util.filesystem import *
 from spack.stage import Stage
 from spack.util.executable import which
+from spack.test.mock_packages_test import *
 
-test_files_dir = os.path.realpath(join_path(spack.stage_path, '.test'))
-test_tmp_path  = os.path.realpath(join_path(test_files_dir, 'tmp'))
-
-archive_dir      = 'test-files'
-archive_name     = archive_dir + '.tar.gz'
-archive_dir_path = join_path(test_files_dir, archive_dir)
-archive_url      = 'file://' + join_path(test_files_dir, archive_name)
-readme_name      = 'README.txt'
-test_readme      = join_path(archive_dir_path, readme_name)
-readme_text      = "hello world!\n"
-
-stage_name = 'spack-test-stage'
+_test_tmp_path = None
 
 
 @contextmanager
 def use_tmp(use_tmp):
-    """Allow some test code to be executed with spack.use_tmp_stage
-       set to a certain value.  Context manager makes sure it's reset
-       on failure.
+    """Allow some test code to be executed such that spack will either use or
+       not use temporary space for stages.
     """
-    old_tmp = spack.use_tmp_stage
-    spack.use_tmp_stage = use_tmp
+    # mock up config
+    path = _test_tmp_path if use_tmp else spack.stage_path
+    spack.config.update_config(
+        'config', {'build_stage': [path]}, scope='user')
     yield
-    spack.use_tmp_stage = old_tmp
 
 
-class StageTest(unittest.TestCase):
+class StageTest(MockPackagesTest):
 
     def setUp(self):
         """This sets up a mock archive to fetch, and a mock temp space for use
            by the Stage class.  It doesn't actually create the Stage -- that
            is done by individual tests.
         """
-        if os.path.exists(test_files_dir):
-            shutil.rmtree(test_files_dir)
+        global _test_tmp_path
+
+        self.test_files_dir = tempfile.mkdtemp()
+        self.test_tmp_path  = os.path.realpath(
+            os.path.join(self.test_files_dir, 'tmp'))
+        _test_tmp_path = self.test_tmp_path
+
+        self.archive_dir = 'test-files'
+        self.archive_name = self.archive_dir + '.tar.gz'
+        archive_dir_path = os.path.join(self.test_files_dir,
+                                        self.archive_dir)
+        self.archive_url = 'file://' + os.path.join(self.test_files_dir,
+                                                    self.archive_name)
+        test_readme = join_path(archive_dir_path, 'README.txt')
+        self.readme_text = "hello world!\n"
+
+        self.stage_name = 'spack-test-stage'
 
-        mkdirp(test_files_dir)
         mkdirp(archive_dir_path)
-        mkdirp(test_tmp_path)
+        mkdirp(self.test_tmp_path)
 
         with open(test_readme, 'w') as readme:
-            readme.write(readme_text)
+            readme.write(self.readme_text)
 
-        with working_dir(test_files_dir):
-            tar = which('tar')
-            tar('czf', archive_name, archive_dir)
+        with working_dir(self.test_files_dir):
+            tar = which('tar', required=True)
+            tar('czf', self.archive_name, self.archive_dir)
 
         # Make spack use the test environment for tmp stuff.
-        self.old_tmp_dirs = spack.tmp_dirs
-        spack.tmp_dirs = [test_tmp_path]
+        self._old_tmp_root = spack.stage._tmp_root
+        self._old_use_tmp_stage = spack.stage._use_tmp_stage
+        spack.stage._tmp_root = None
+        spack.stage._use_tmp_stage = True
 
         # record this since this test changes to directories that will
         # be removed.
@@ -92,13 +99,14 @@ def setUp(self):
 
     def tearDown(self):
         """Blows away the test environment directory."""
-        shutil.rmtree(test_files_dir)
+        shutil.rmtree(self.test_files_dir, ignore_errors=True)
 
         # chdir back to original working dir
         os.chdir(self.working_dir)
 
         # restore spack's original tmp environment
-        spack.tmp_dirs = self.old_tmp_dirs
+        spack.stage._tmp_root = self._old_tmp_root
+        spack.stage._use_tmp_stage = self._old_use_tmp_stage
 
     def get_stage_path(self, stage, stage_name):
         """Figure out where a stage should be living.  This depends on
@@ -120,7 +128,7 @@ def check_setup(self, stage, stage_name):
         # Ensure stage was created in the spack stage directory
         self.assertTrue(os.path.isdir(stage_path))
 
-        if spack.use_tmp_stage:
+        if spack.stage.get_tmp_root():
             # Check that the stage dir is really a symlink.
             self.assertTrue(os.path.islink(stage_path))
 
@@ -131,7 +139,7 @@ def check_setup(self, stage, stage_name):
 
             # Make sure the directory is in the place we asked it to
             # be (see setUp and tearDown)
-            self.assertTrue(target.startswith(test_tmp_path))
+            self.assertTrue(target.startswith(self.test_tmp_path))
 
         else:
             # Make sure the stage path is NOT a link for a non-tmp stage
@@ -139,24 +147,24 @@ def check_setup(self, stage, stage_name):
 
     def check_fetch(self, stage, stage_name):
         stage_path = self.get_stage_path(stage, stage_name)
-        self.assertTrue(archive_name in os.listdir(stage_path))
-        self.assertEqual(join_path(stage_path, archive_name),
+        self.assertTrue(self.archive_name in os.listdir(stage_path))
+        self.assertEqual(join_path(stage_path, self.archive_name),
                          stage.fetcher.archive_file)
 
     def check_expand_archive(self, stage, stage_name):
         stage_path = self.get_stage_path(stage, stage_name)
-        self.assertTrue(archive_name in os.listdir(stage_path))
-        self.assertTrue(archive_dir in os.listdir(stage_path))
+        self.assertTrue(self.archive_name in os.listdir(stage_path))
+        self.assertTrue(self.archive_dir in os.listdir(stage_path))
 
         self.assertEqual(
-            join_path(stage_path, archive_dir),
+            join_path(stage_path, self.archive_dir),
             stage.source_path)
 
-        readme = join_path(stage_path, archive_dir, readme_name)
+        readme = join_path(stage_path, self.archive_dir, 'README.txt')
         self.assertTrue(os.path.isfile(readme))
 
         with open(readme) as file:
-            self.assertEqual(readme_text, file.read())
+            self.assertEqual(self.readme_text, file.read())
 
     def check_chdir(self, stage, stage_name):
         stage_path = self.get_stage_path(stage, stage_name)
@@ -165,7 +173,7 @@ def check_chdir(self, stage, stage_name):
     def check_chdir_to_source(self, stage, stage_name):
         stage_path = self.get_stage_path(stage, stage_name)
         self.assertEqual(
-            join_path(os.path.realpath(stage_path), archive_dir),
+            join_path(os.path.realpath(stage_path), self.archive_dir),
             os.getcwd())
 
     def check_destroy(self, stage, stage_name):
@@ -176,76 +184,76 @@ def check_destroy(self, stage, stage_name):
         self.assertFalse(os.path.exists(stage_path))
 
         # tmp stage needs to remove tmp dir too.
-        if spack.use_tmp_stage:
+        if spack.stage._use_tmp_stage:
             target = os.path.realpath(stage_path)
             self.assertFalse(os.path.exists(target))
 
     def test_setup_and_destroy_name_with_tmp(self):
         with use_tmp(True):
-            with Stage(archive_url, name=stage_name) as stage:
-                self.check_setup(stage, stage_name)
-            self.check_destroy(stage, stage_name)
+            with Stage(self.archive_url, name=self.stage_name) as stage:
+                self.check_setup(stage, self.stage_name)
+            self.check_destroy(stage, self.stage_name)
 
     def test_setup_and_destroy_name_without_tmp(self):
         with use_tmp(False):
-            with Stage(archive_url, name=stage_name) as stage:
-                self.check_setup(stage, stage_name)
-            self.check_destroy(stage, stage_name)
+            with Stage(self.archive_url, name=self.stage_name) as stage:
+                self.check_setup(stage, self.stage_name)
+            self.check_destroy(stage, self.stage_name)
 
     def test_setup_and_destroy_no_name_with_tmp(self):
         with use_tmp(True):
-            with Stage(archive_url) as stage:
+            with Stage(self.archive_url) as stage:
                 self.check_setup(stage, None)
             self.check_destroy(stage, None)
 
     def test_setup_and_destroy_no_name_without_tmp(self):
         with use_tmp(False):
-            with Stage(archive_url) as stage:
+            with Stage(self.archive_url) as stage:
                 self.check_setup(stage, None)
             self.check_destroy(stage, None)
 
     def test_chdir(self):
-        with Stage(archive_url, name=stage_name) as stage:
+        with Stage(self.archive_url, name=self.stage_name) as stage:
             stage.chdir()
-            self.check_setup(stage, stage_name)
-            self.check_chdir(stage, stage_name)
-        self.check_destroy(stage, stage_name)
+            self.check_setup(stage, self.stage_name)
+            self.check_chdir(stage, self.stage_name)
+        self.check_destroy(stage, self.stage_name)
 
     def test_fetch(self):
-        with Stage(archive_url, name=stage_name) as stage:
+        with Stage(self.archive_url, name=self.stage_name) as stage:
             stage.fetch()
-            self.check_setup(stage, stage_name)
-            self.check_chdir(stage, stage_name)
-            self.check_fetch(stage, stage_name)
-        self.check_destroy(stage, stage_name)
+            self.check_setup(stage, self.stage_name)
+            self.check_chdir(stage, self.stage_name)
+            self.check_fetch(stage, self.stage_name)
+        self.check_destroy(stage, self.stage_name)
 
     def test_expand_archive(self):
-        with Stage(archive_url, name=stage_name) as stage:
+        with Stage(self.archive_url, name=self.stage_name) as stage:
             stage.fetch()
-            self.check_setup(stage, stage_name)
-            self.check_fetch(stage, stage_name)
+            self.check_setup(stage, self.stage_name)
+            self.check_fetch(stage, self.stage_name)
             stage.expand_archive()
-            self.check_expand_archive(stage, stage_name)
-        self.check_destroy(stage, stage_name)
+            self.check_expand_archive(stage, self.stage_name)
+        self.check_destroy(stage, self.stage_name)
 
     def test_expand_archive_with_chdir(self):
-        with Stage(archive_url, name=stage_name) as stage:
+        with Stage(self.archive_url, name=self.stage_name) as stage:
             stage.fetch()
-            self.check_setup(stage, stage_name)
-            self.check_fetch(stage, stage_name)
+            self.check_setup(stage, self.stage_name)
+            self.check_fetch(stage, self.stage_name)
             stage.expand_archive()
             stage.chdir_to_source()
-            self.check_expand_archive(stage, stage_name)
-            self.check_chdir_to_source(stage, stage_name)
-        self.check_destroy(stage, stage_name)
+            self.check_expand_archive(stage, self.stage_name)
+            self.check_chdir_to_source(stage, self.stage_name)
+        self.check_destroy(stage, self.stage_name)
 
     def test_restage(self):
-        with Stage(archive_url, name=stage_name) as stage:
+        with Stage(self.archive_url, name=self.stage_name) as stage:
             stage.fetch()
             stage.expand_archive()
             stage.chdir_to_source()
-            self.check_expand_archive(stage, stage_name)
-            self.check_chdir_to_source(stage, stage_name)
+            self.check_expand_archive(stage, self.stage_name)
+            self.check_chdir_to_source(stage, self.stage_name)
 
             # Try to make a file in the old archive dir
             with open('foobar', 'w') as file:
@@ -255,40 +263,44 @@ def test_restage(self):
 
             # Make sure the file is not there after restage.
             stage.restage()
-            self.check_chdir(stage, stage_name)
-            self.check_fetch(stage, stage_name)
+            self.check_chdir(stage, self.stage_name)
+            self.check_fetch(stage, self.stage_name)
             stage.chdir_to_source()
-            self.check_chdir_to_source(stage, stage_name)
+            self.check_chdir_to_source(stage, self.stage_name)
             self.assertFalse('foobar' in os.listdir(stage.source_path))
-        self.check_destroy(stage, stage_name)
+        self.check_destroy(stage, self.stage_name)
 
     def test_no_keep_without_exceptions(self):
-        with Stage(archive_url, name=stage_name, keep=False) as stage:
+        with Stage(self.archive_url,
+                   name=self.stage_name, keep=False) as stage:
             pass
-        self.check_destroy(stage, stage_name)
+        self.check_destroy(stage, self.stage_name)
 
     def test_keep_without_exceptions(self):
-        with Stage(archive_url, name=stage_name, keep=True) as stage:
+        with Stage(self.archive_url,
+                   name=self.stage_name, keep=True) as stage:
             pass
-        path = self.get_stage_path(stage, stage_name)
+        path = self.get_stage_path(stage, self.stage_name)
         self.assertTrue(os.path.isdir(path))
 
     def test_no_keep_with_exceptions(self):
         try:
-            with Stage(archive_url, name=stage_name, keep=False) as stage:
+            with Stage(self.archive_url,
+                       name=self.stage_name, keep=False) as stage:
                 raise Exception()
 
-            path = self.get_stage_path(stage, stage_name)
+            path = self.get_stage_path(stage, self.stage_name)
             self.assertTrue(os.path.isdir(path))
         except:
             pass  # ignore here.
 
     def test_keep_exceptions(self):
         try:
-            with Stage(archive_url, name=stage_name, keep=True) as stage:
+            with Stage(self.archive_url,
+                       name=self.stage_name, keep=True) as stage:
                 raise Exception()
 
-            path = self.get_stage_path(stage, stage_name)
+            path = self.get_stage_path(stage, self.stage_name)
             self.assertTrue(os.path.isdir(path))
         except:
             pass  # ignore here.
diff --git a/lib/spack/spack/util/path.py b/lib/spack/spack/util/path.py
new file mode 100644
index 0000000000..5332115ae9
--- /dev/null
+++ b/lib/spack/spack/util/path.py
@@ -0,0 +1,68 @@
+##############################################################################
+# Copyright (c) 2013-2016, 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 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 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
+##############################################################################
+"""Utilities for managing paths in Spack.
+"""
+import os
+import re
+import spack
+import getpass
+import tempfile
+
+__all__ = [
+    'substitute_config_variables',
+    'canonicalize_path']
+
+# Substitutions to perform
+replacements = {
+    'spack': spack.prefix,
+    'user': getpass.getuser(),
+    'tempdir': tempfile.gettempdir(),
+}
+
+
+def substitute_config_variables(path):
+    """Substitute placeholders into paths.
+
+    Spack allows paths in configs to have some placeholders, as follows:
+
+    - $spack     The Spack instance's prefix
+    - $user      The current user's username
+    - $tempdir   Default temporary directory returned by tempfile.gettempdir()
+    """
+    # Look up replacements for re.sub in the replacements dict.
+    def repl(match):
+        m = match.group(0).strip('${}')
+        return replacements.get(m, match.group(0))
+
+    # Replace $var or ${var}.
+    return re.sub(r'(\$\w+\b|\$\{\w+\})', repl, path)
+
+
+def canonicalize_path(path):
+    """Substitute $spack, expand user home, take abspath."""
+    path = substitute_config_variables(path)
+    path = os.path.expanduser(path)
+    path = os.path.abspath(path)
+    return path
-- 
GitLab