Repository: spack/spack

Issue:
`spack env depfile` without `spack concretize` generates an incomplete Makefile
```console
% spack env create -d .
==> Created environment in /tmp/tmp.EkpdIyZFt9
==> You can activate this environment with:
==>   spack env activate /tmp/tmp.EkpdIyZFt9
[mose@dream]/tmp/tmp.EkpdIyZFt9% spack -e . add zlib
==> Adding zlib to environment /tmp/tmp.EkpdIyZFt9
% spack -e . env depfile > Makefile
% make
touch: cannot touch '/tmp/tmp.EkpdIyZFt9/.spack-env/makedeps/env': No such file or directory
make: *** [Makefile:12: /tmp/tmp.EkpdIyZFt9/.spack-env/makedeps/env] Error 1
```
I forgot to run `spack -e . concretize -f` before `env depfile` and the Makefile was missing some rules, but I feel like Spack should either error in a nicer way, or automatically do the concretization if necessary :slightly_smiling_face: 


Discussion:
@haampie Fyi, when bugs are triaged they'll show up in our "Known bugs" table :wink: 

Resolving patch:
diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py
--- a/lib/spack/spack/cmd/env.py
+++ b/lib/spack/spack/cmd/env.py
@@ -674,18 +674,31 @@ def env_depfile(args):
     # Currently only make is supported.
     spack.cmd.require_active_env(cmd_name="env depfile")
 
+    env = ev.active_environment()
+
     # What things do we build when running make? By default, we build the
     # root specs. If specific specs are provided as input, we build those.
     filter_specs = spack.cmd.parse_specs(args.specs) if args.specs else None
     template = spack.tengine.make_environment().get_template(os.path.join("depfile", "Makefile"))
     model = depfile.MakefileModel.from_env(
-        ev.active_environment(),
+        env,
         filter_specs=filter_specs,
         pkg_buildcache=depfile.UseBuildCache.from_string(args.use_buildcache[0]),
         dep_buildcache=depfile.UseBuildCache.from_string(args.use_buildcache[1]),
         make_prefix=args.make_prefix,
         jobserver=args.jobserver,
     )
+
+    # Warn in case we're generating a depfile for an empty environment. We don't automatically
+    # concretize; the user should do that explicitly. Could be changed in the future if requested.
+    if model.empty:
+        if not env.user_specs:
+            tty.warn("no specs in the environment")
+        elif filter_specs is not None:
+            tty.warn("no concrete matching specs found in environment")
+        else:
+            tty.warn("environment is not concretized. Run `spack concretize` first")
+
     makefile = template.render(model.to_dict())
 
     # Finally write to stdout/file.
diff --git a/lib/spack/spack/environment/depfile.py b/lib/spack/spack/environment/depfile.py
--- a/lib/spack/spack/environment/depfile.py
+++ b/lib/spack/spack/environment/depfile.py
@@ -232,6 +232,10 @@ def to_dict(self):
             "pkg_ids": " ".join(self.all_pkg_identifiers),
         }
 
+    @property
+    def empty(self):
+        return len(self.roots) == 0
+
     @staticmethod
     def from_env(
         env: ev.Environment,
@@ -254,15 +258,10 @@ def from_env(
             jobserver: when enabled, make will invoke Spack with jobserver support. For
                 dry-run this should be disabled.
         """
-        # If no specs are provided as a filter, build all the specs in the environment.
-        if filter_specs:
-            entrypoints = [env.matching_spec(s) for s in filter_specs]
-        else:
-            entrypoints = [s for _, s in env.concretized_specs()]
-
+        roots = env.all_matching_specs(*filter_specs) if filter_specs else env.concrete_roots()
         visitor = DepfileSpecVisitor(pkg_buildcache, dep_buildcache)
         traverse.traverse_breadth_first_with_visitor(
-            entrypoints, traverse.CoverNodesVisitor(visitor, key=lambda s: s.dag_hash())
+            roots, traverse.CoverNodesVisitor(visitor, key=lambda s: s.dag_hash())
         )
 
-        return MakefileModel(env, entrypoints, visitor.adjacency_list, make_prefix, jobserver)
+        return MakefileModel(env, roots, visitor.adjacency_list, make_prefix, jobserver)


Test patch:
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
--- a/lib/spack/spack/test/cmd/env.py
+++ b/lib/spack/spack/test/cmd/env.py
@@ -3346,6 +3346,20 @@ def test_spack_package_ids_variable(tmpdir, mock_packages):
             assert "post-install: {}".format(s.dag_hash()) in out
 
 
+def test_depfile_empty_does_not_error(tmp_path):
+    # For empty environments Spack should create a depfile that does nothing
+    make = Executable("make")
+    makefile = str(tmp_path / "Makefile")
+
+    env("create", "test")
+    with ev.read("test"):
+        env("depfile", "-o", makefile)
+
+    make("-f", makefile)
+
+    assert make.returncode == 0
+
+
 def test_unify_when_possible_works_around_conflicts():
     e = ev.create("coconcretization")
     e.unify = "when_possible"