Repository: spack/spack

Issue:
'spack install --only dependencies' does not work in a spack environment
If I am in a spack environment (a spack.yaml file is in my working directory) and I run a command like 'spack install --only dependencies foo' the package foo will be installed as well. If there is no active spack environment this works as intended (foo's dependencies will be installed but foo will not).


Discussion:

Resolving patch:
diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py
--- a/lib/spack/spack/cmd/install.py
+++ b/lib/spack/spack/cmd/install.py
@@ -39,8 +39,15 @@ def update_kwargs_from_args(args, kwargs):
         'fake': args.fake,
         'dirty': args.dirty,
         'use_cache': args.use_cache,
-        'cache_only': args.cache_only
+        'cache_only': args.cache_only,
+        'explicit': True  # Always true for install command
     })
+
+    kwargs.update({
+        'install_dependencies': ('dependencies' in args.things_to_install),
+        'install_package': ('package' in args.things_to_install)
+    })
+
     if hasattr(args, 'setup'):
         setups = set()
         for arglist_s in args.setup:
@@ -188,8 +195,8 @@ def default_log_file(spec):
 def install_spec(cli_args, kwargs, abstract_spec, spec):
     """Do the actual installation."""
 
-    # handle active environment, if any
-    def install(spec, kwargs):
+    try:
+        # handle active environment, if any
         env = ev.get_env(cli_args, 'install')
         if env:
             env.install(abstract_spec, spec, **kwargs)
@@ -197,17 +204,6 @@ def install(spec, kwargs):
         else:
             spec.package.do_install(**kwargs)
 
-    try:
-        if cli_args.things_to_install == 'dependencies':
-            # Install dependencies as-if they were installed
-            # for root (explicit=False in the DB)
-            kwargs['explicit'] = False
-            for s in spec.dependencies():
-                install(s, kwargs)
-        else:
-            kwargs['explicit'] = True
-            install(spec, kwargs)
-
     except spack.build_environment.InstallError as e:
         if cli_args.show_log_on_error:
             e.print_context()
@@ -242,10 +238,6 @@ def install(parser, args, **kwargs):
     # Parse cli arguments and construct a dictionary
     # that will be passed to Package.do_install API
     update_kwargs_from_args(args, kwargs)
-    kwargs.update({
-        'install_dependencies': ('dependencies' in args.things_to_install),
-        'install_package': ('package' in args.things_to_install)
-    })
 
     if args.run_tests:
         tty.warn("Deprecated option: --run-tests: use --test=all instead")
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -1519,6 +1519,15 @@ def do_install(self, **kwargs):
         dirty = kwargs.get('dirty', False)
         restage = kwargs.get('restage', False)
 
+        # install_self defaults True and is popped so that dependencies are
+        # always installed regardless of whether the root was installed
+        install_self = kwargs.pop('install_package', True)
+        # explicit defaults False so that dependents are implicit regardless
+        # of whether their dependents are implicitly or explicitly installed.
+        # Spack ensures root packages of install commands are always marked to
+        # install explicit
+        explicit = kwargs.pop('explicit', False)
+
         # For external packages the workflow is simplified, and basically
         # consists in module file generation and registration in the DB
         if self.spec.external:
@@ -1568,6 +1577,9 @@ def do_install(self, **kwargs):
         if install_deps:
             Package._install_bootstrap_compiler(self, **kwargs)
 
+        if not install_self:
+            return
+
         # Then, install the package proper
         tty.msg(colorize('@*{Installing} @*g{%s}' % self.name))
 
diff --git a/var/spack/repos/builtin.mock/packages/dependent-install/package.py b/var/spack/repos/builtin.mock/packages/dependent-install/package.py
--- a/var/spack/repos/builtin.mock/packages/dependent-install/package.py
+++ b/var/spack/repos/builtin.mock/packages/dependent-install/package.py
@@ -13,8 +13,10 @@ class DependentInstall(Package):
     url      = "http://www.example.com/a-1.0.tar.gz"
 
     version('1.0', '0123456789abcdef0123456789abcdef')
+    version('2.0', '0123456789abcdef0123456789abcdef')
 
-    depends_on('dependency-install')
+    depends_on('dependency-install@2.0', when='@2.0')
+    depends_on('dependency-install@1.0', when='@1.0')
 
     def install(self, spec, prefix):
         touch(join_path(prefix, 'an_installation_file'))


Test patch:
diff --git a/lib/spack/spack/test/cmd/install.py b/lib/spack/spack/test/cmd/install.py
--- a/lib/spack/spack/test/cmd/install.py
+++ b/lib/spack/spack/test/cmd/install.py
@@ -21,10 +21,13 @@
 from spack.error import SpackError
 from spack.spec import Spec
 from spack.main import SpackCommand
+import spack.environment as ev
 
 from six.moves.urllib.error import HTTPError, URLError
 
 install = SpackCommand('install')
+env = SpackCommand('env')
+add = SpackCommand('add')
 
 
 @pytest.fixture(scope='module')
@@ -600,3 +603,48 @@ def test_cache_only_fails(tmpdir, mock_fetch, install_mockery, capfd):
             assert False
         except spack.main.SpackCommandError:
             pass
+
+
+def test_install_only_dependencies(tmpdir, mock_fetch, install_mockery):
+    dep = Spec('dependency-install').concretized()
+    root = Spec('dependent-install').concretized()
+
+    install('--only', 'dependencies', 'dependent-install')
+
+    assert os.path.exists(dep.prefix)
+    assert not os.path.exists(root.prefix)
+
+
+@pytest.mark.regression('12002')
+def test_install_only_dependencies_in_env(tmpdir, mock_fetch, install_mockery,
+                                          mutable_mock_env_path):
+    env('create', 'test')
+
+    with ev.read('test'):
+        dep = Spec('dependency-install').concretized()
+        root = Spec('dependent-install').concretized()
+
+        install('-v', '--only', 'dependencies', 'dependent-install')
+
+        assert os.path.exists(dep.prefix)
+        assert not os.path.exists(root.prefix)
+
+
+@pytest.mark.regression('12002')
+def test_install_only_dependencies_of_all_in_env(
+    tmpdir, mock_fetch, install_mockery, mutable_mock_env_path
+):
+    env('create', '--without-view', 'test')
+
+    with ev.read('test'):
+        roots = [Spec('dependent-install@1.0').concretized(),
+                 Spec('dependent-install@2.0').concretized()]
+
+        add('dependent-install@1.0')
+        add('dependent-install@2.0')
+        install('--only', 'dependencies')
+
+        for root in roots:
+            assert not os.path.exists(root.prefix)
+            for dep in root.traverse(root=False):
+                assert os.path.exists(dep.prefix)