diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
index 9fb76d3a35d50ecea16bd1e6c4beb1e5f5d1a29b..05784156530629731cf024f6fc34fb57a43d93e3 100644
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -24,8 +24,7 @@
 ##############################################################################
 __all__ = ['set_install_permissions', 'install', 'expand_user', 'working_dir',
            'touch', 'mkdirp', 'force_remove', 'join_path', 'ancestor',
-           'can_access', 'filter_file', 'change_sed_delimiter', 'is_exe',
-           'check_link_tree', 'merge_link_tree', 'unmerge_link_tree']
+           'can_access', 'filter_file', 'change_sed_delimiter', 'is_exe']
 
 import os
 import sys
@@ -223,82 +222,3 @@ def ancestor(dir, n=1):
 def can_access(file_name):
     """True if we have read/write access to the file."""
     return os.access(file_name, os.R_OK|os.W_OK)
-
-
-def traverse_link_tree(src_root, dest_root, follow_nonexisting=True, **kwargs):
-    # Yield directories before or after their contents.
-    order  = kwargs.get('order', 'pre')
-    if order not in ('pre', 'post'):
-        raise ValueError("Order must be 'pre' or 'post'.")
-
-    # List of relative paths to ignore under the src root.
-    ignore = kwargs.get('ignore', None)
-    if isinstance(ignore, basestring):
-        ignore = (ignore,)
-
-    for dirpath, dirnames, filenames in os.walk(src_root):
-        rel_path  = dirpath[len(src_root):]
-        rel_path = rel_path.lstrip(os.path.sep)
-        dest_dirpath = os.path.join(dest_root, rel_path)
-
-        # Don't descend into ignored directories
-        if ignore and dest_dirpath in ignore:
-            return
-
-        # Don't descend into dirs in dest that do not exist in src.
-        if not follow_nonexisting:
-            dirnames[:] = [
-                d for d in dirnames
-                if os.path.exists(os.path.join(dest_dirpath, d))]
-
-        # preorder yields directories before children
-        if order == 'pre':
-            yield (dirpath, dest_dirpath)
-
-        for name in filenames:
-            src_file  = os.path.join(dirpath, name)
-            dest_file = os.path.join(dest_dirpath, name)
-
-            # Ignore particular paths inside the install root.
-            src_relpath = src_file[len(src_root):]
-            src_relpath = src_relpath.lstrip(os.path.sep)
-            if ignore and src_relpath in ignore:
-                continue
-
-            yield (src_file, dest_file)
-
-        # postorder yields directories after children
-        if order == 'post':
-            yield (dirpath, dest_dirpath)
-
-
-
-def check_link_tree(src_root, dest_root, **kwargs):
-    for src, dest in traverse_link_tree(src_root, dest_root, False, **kwargs):
-        if os.path.exists(dest) and not os.path.isdir(dest):
-            return dest
-    return None
-
-
-def merge_link_tree(src_root, dest_root, **kwargs):
-    kwargs['order'] = 'pre'
-    for src, dest in traverse_link_tree(src_root, dest_root, **kwargs):
-        if os.path.isdir(src):
-            mkdirp(dest)
-        else:
-            assert(not os.path.exists(dest))
-            os.symlink(src, dest)
-
-
-def unmerge_link_tree(src_root, dest_root, **kwargs):
-    kwargs['order'] = 'post'
-    for src, dest in traverse_link_tree(src_root, dest_root, **kwargs):
-        if os.path.isdir(dest):
-            if not os.listdir(dest):
-                # TODO: what if empty directories were present pre-merge?
-                shutil.rmtree(dest, ignore_errors=True)
-
-        elif os.path.exists(dest):
-            if not os.path.islink(dest):
-                raise ValueError("%s is not a link tree!" % dest)
-            os.remove(dest)
diff --git a/lib/spack/llnl/util/link_tree.py b/lib/spack/llnl/util/link_tree.py
new file mode 100644
index 0000000000000000000000000000000000000000..19c2d46938fbc27138e91ce8238b31939c60d0f6
--- /dev/null
+++ b/lib/spack/llnl/util/link_tree.py
@@ -0,0 +1,168 @@
+##############################################################################
+# 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
+##############################################################################
+"""LinkTree class for setting up trees of symbolic links."""
+__all__ = ['LinkTree']
+
+import os
+import shutil
+from llnl.util.filesystem import mkdirp
+
+
+class LinkTree(object):
+    """Class to create trees of symbolic links from a source directory.
+
+    LinkTree objects are constructed with a source root.  Their
+    methods allow you to create and delete trees of symbolic links
+    back to the source tree in specific destination directories.
+    Trees comprise symlinks only to files; directries are never
+    symlinked to, to prevent the source directory from ever being
+    modified.
+
+    """
+    def __init__(self, source_root):
+        self._root = source_root
+
+
+    def traverse(self, dest_root, **kwargs):
+        """Traverse LinkTree root and dest simultaneously.
+
+        Walks the LinkTree directory in pre or post order.  Yields
+        each file in the source directory with a matching path from
+        the dest directory.  e.g., for this tree::
+
+            root/
+              a/
+                file1
+                file2
+              b/
+                file3
+
+        When called on dest, this yields::
+
+            ('root',         'dest')
+            ('root/a',       'dest/a')
+            ('root/a/file1', 'dest/a/file1')
+            ('root/a/file2', 'dest/a/file2')
+            ('root/b',       'dest/b')
+            ('root/b/file3', 'dest/b/file3')
+
+        Optional args:
+
+        order=[pre|post] -- Whether to do pre- or post-order traveral.
+
+        ignore=<container> -- Optional container of root-relative
+                              paths to ignore.
+
+        follow_nonexisting -- Whether to descend into directories in
+                              src that do not exit in dest.
+
+        """
+        # Yield directories before or after their contents.
+        order  = kwargs.get('order', 'pre')
+        if order not in ('pre', 'post'):
+            raise ValueError("Order must be 'pre' or 'post'.")
+
+        # List of relative paths to ignore under the src root.
+        ignore = kwargs.get('ignore', None)
+        if isinstance(ignore, basestring):
+            ignore = (ignore,)
+
+        # Whether to descend when dirs dont' exist in dest.
+        follow_nonexisting = kwargs.get('follow_nonexisting', True)
+
+        for dirpath, dirnames, filenames in os.walk(self._root):
+            rel_path  = dirpath[len(self._root):]
+            rel_path = rel_path.lstrip(os.path.sep)
+            dest_dirpath = os.path.join(dest_root, rel_path)
+
+            # Don't descend into ignored directories
+            if ignore and dest_dirpath in ignore:
+                return
+
+            # Don't descend into dirs in dest that do not exist in src.
+            if not follow_nonexisting:
+                dirnames[:] = [
+                    d for d in dirnames
+                    if os.path.exists(os.path.join(dest_dirpath, d))]
+
+            # preorder yields directories before children
+            if order == 'pre':
+                yield (dirpath, dest_dirpath)
+
+            for name in filenames:
+                src_file  = os.path.join(dirpath, name)
+                dest_file = os.path.join(dest_dirpath, name)
+
+                # Ignore particular paths inside the install root.
+                src_relpath = src_file[len(self._root):]
+                src_relpath = src_relpath.lstrip(os.path.sep)
+                if ignore and src_relpath in ignore:
+                    continue
+
+                yield (src_file, dest_file)
+
+            # postorder yields directories after children
+            if order == 'post':
+                yield (dirpath, dest_dirpath)
+
+
+
+    def find_conflict(self, dest_root, **kwargs):
+        """Returns the first file in dest that also exists in src."""
+        kwargs['follow_nonexisting'] = False
+        for src, dest in self.traverse(dest_root, **kwargs):
+            if os.path.exists(dest) and not os.path.isdir(dest):
+                return dest
+        return None
+
+
+    def merge(self, dest_root, **kwargs):
+        """Link all files in src into dest, creating directories if necessary."""
+        kwargs['order'] = 'pre'
+        for src, dest in self.traverse(dest_root, **kwargs):
+            if os.path.isdir(src):
+                mkdirp(dest)
+            else:
+                assert(not os.path.exists(dest))
+                os.symlink(src, dest)
+
+
+    def unmerge(self, dest_root, **kwargs):
+        """Unlink all files in dest that exist in src.
+
+        Unlinks directories in dest if they are empty.
+
+        """
+        kwargs['order'] = 'post'
+        for src, dest in self.traverse(dest_root, **kwargs):
+            if os.path.isdir(dest):
+                if not os.listdir(dest):
+                    # TODO: what if empty directories were present pre-merge?
+                    shutil.rmtree(dest, ignore_errors=True)
+
+            elif os.path.exists(dest):
+                if not os.path.islink(dest):
+                    raise ValueError("%s is not a link tree!" % dest)
+                os.remove(dest)
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
index b7dae552e474ec7512611b244852b4ce95e20852..da251dc4e8396192080e2459eba47e79bf3f3a93 100644
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -45,6 +45,7 @@
 from StringIO import StringIO
 
 import llnl.util.tty as tty
+from llnl.util.link_tree import LinkTree
 from llnl.util.filesystem import *
 from llnl.util.lang import *
 
@@ -918,15 +919,12 @@ def activate(self, extension):
         always executed.
 
         """
-        conflict = check_link_tree(
-            extension.prefix, self.prefix,
-            ignore=spack.install_layout.hidden_file_paths)
-
+        tree = LinkTree(extension.prefix)
+        conflict = tree.find_conflict(
+            self.prefix, ignore=spack.install_layout.hidden_file_paths)
         if conflict:
             raise ExtensionConflictError(conflict)
-
-        merge_link_tree(extension.prefix, self.prefix,
-                        ignore=spack.install_layout.hidden_file_paths)
+        tree.merge(self.prefix, ignore=spack.install_layout.hidden_file_paths)
 
 
     def do_deactivate(self, extension):
@@ -950,8 +948,8 @@ def deactivate(self, extension):
         always executed.
 
         """
-        unmerge_link_tree(extension.prefix, self.prefix,
-                          ignore=spack.install_layout.hidden_file_paths)
+        tree = LinkTree(extension.prefix)
+        tree.unmerge(self.prefix, ignore=spack.install_layout.hidden_file_paths)
         tty.msg("Deactivated %s as extension of %s."
                 % (extension.spec.short_spec, self.spec.short_spec))