Give useful error on missing workspace manifest (#1475)

Previously if the workspace root manifest was missing, a panic would be
triggered due to unwrapping a `None` option.

Now, we suggest the missing manifest if possible, or direct you to work
it out yourself if we can't.
diff --git a/crate_universe/src/cli/splice.rs b/crate_universe/src/cli/splice.rs
index 999b7dc..f5077bf 100644
--- a/crate_universe/src/cli/splice.rs
+++ b/crate_universe/src/cli/splice.rs
@@ -70,7 +70,7 @@
     let splicer = Splicer::new(splicing_dir, splicing_manifest)?;
 
     // Splice together the manifest
-    let manifest_path = splicer.splice_workspace()?;
+    let manifest_path = splicer.splice_workspace(&opt.cargo)?;
 
     // Generate a lockfile
     let cargo_lockfile = generate_lockfile(
diff --git a/crate_universe/src/cli/vendor.rs b/crate_universe/src/cli/vendor.rs
index 5a0b183..366bb68 100644
--- a/crate_universe/src/cli/vendor.rs
+++ b/crate_universe/src/cli/vendor.rs
@@ -127,7 +127,7 @@
 
     // Splice together the manifest
     let manifest_path = splicer
-        .splice_workspace()
+        .splice_workspace(&opt.cargo)
         .context("Failed to splice workspace")?;
 
     // Gather a cargo lockfile
diff --git a/crate_universe/src/splicing/splicer.rs b/crate_universe/src/splicing/splicer.rs
index 1a68d32..8374df4 100644
--- a/crate_universe/src/splicing/splicer.rs
+++ b/crate_universe/src/splicing/splicer.rs
@@ -5,6 +5,7 @@
 use std::path::{Path, PathBuf};
 
 use anyhow::{bail, Context, Result};
+use cargo_metadata::MetadataCommand;
 use cargo_toml::{Dependency, Manifest};
 use normpath::PathExt;
 
@@ -44,6 +45,7 @@
     pub fn new(
         manifests: &'a HashMap<PathBuf, Manifest>,
         splicing_manifest: &'a SplicingManifest,
+        cargo: &Path,
     ) -> Result<Self> {
         // First check for any workspaces in the provided manifests
         let workspace_owned: HashMap<&PathBuf, &Manifest> = manifests
@@ -67,7 +69,33 @@
                 bail!("When splicing manifests, there can only be 1 root workspace manifest");
             }
 
+            // This is an error case - we've detected some manifests are in a workspace, but can't
+            // find it.
+            // This block is just for trying to give as useful an error message as possible in this
+            // case.
+            if workspace_roots.is_empty() {
+                let sorted_manifests: BTreeSet<_> = manifests.keys().collect();
+                for manifest_path in sorted_manifests {
+                    let metadata_result = MetadataCommand::new()
+                        .cargo_path(cargo)
+                        .current_dir(manifest_path.parent().unwrap())
+                        .manifest_path(manifest_path)
+                        .no_deps()
+                        .exec();
+                    if let Ok(metadata) = metadata_result {
+                        let label = Label::from_absolute_path(
+                            metadata.workspace_root.join("Cargo.toml").as_std_path(),
+                        );
+                        if let Ok(label) = label {
+                            bail!("Missing root workspace manifest. Please add the following label to the `manifests` key: \"{}\"", label);
+                        }
+                    }
+                }
+                bail!("Missing root workspace manifest. Please add the label of the workspace root to the `manifests` key");
+            }
+
             // Ensure all workspace owned manifests are members of the one workspace root
+            // UNWRAP: Safe because we've checked workspace_roots isn't empty.
             let (root_manifest_path, root_manifest) = workspace_roots.drain().last().unwrap();
             let external_workspace_members: BTreeSet<String> = workspace_packages
                 .into_iter()
@@ -470,8 +498,9 @@
     }
 
     /// Build a new workspace root
-    pub fn splice_workspace(&self) -> Result<SplicedManifest> {
-        SplicerKind::new(&self.manifests, &self.splicing_manifest)?.splice(&self.workspace_dir)
+    pub fn splice_workspace(&self, cargo: &Path) -> Result<SplicedManifest> {
+        SplicerKind::new(&self.manifests, &self.splicing_manifest, cargo)?
+            .splice(&self.workspace_dir)
     }
 }
 
@@ -706,6 +735,10 @@
         (PathBuf::from("cargo"), PathBuf::from("rustc"))
     }
 
+    fn cargo() -> PathBuf {
+        get_cargo_and_rustc_paths().0
+    }
+
     fn generate_metadata(manifest_path: &Path) -> cargo_metadata::Metadata {
         let manifest_dir = manifest_path.parent().unwrap_or_else(|| {
             panic!(
@@ -746,16 +779,28 @@
     }
 
     fn mock_cargo_toml(path: &Path, name: &str) -> cargo_toml::Manifest {
+        mock_cargo_toml_with_dependencies(path, name, &[])
+    }
+
+    fn mock_cargo_toml_with_dependencies(
+        path: &Path,
+        name: &str,
+        deps: &[&str],
+    ) -> cargo_toml::Manifest {
         let manifest = cargo_toml::Manifest::from_str(&textwrap::dedent(&format!(
             r#"
             [package]
-            name = "{}"
+            name = "{name}"
             version = "0.0.1"
 
             [lib]
             path = "lib.rs"
+
+            [dependencies]
+            {dependencies}
             "#,
-            name
+            name = name,
+            dependencies = deps.join("\n")
         )))
         .unwrap();
 
@@ -809,7 +854,12 @@
                 .join("root_pkg")
                 .join(pkg)
                 .join("Cargo.toml");
-            mock_cargo_toml(&manifest_path, pkg);
+            let deps = if pkg == &"sub_pkg_b" {
+                vec![r#"sub_pkg_a = { path = "../sub_pkg_a" }"#]
+            } else {
+                vec![]
+            };
+            mock_cargo_toml_with_dependencies(&manifest_path, pkg, &deps);
 
             splicing_manifest.manifests.insert(
                 manifest_path,
@@ -842,6 +892,9 @@
         let manifest_path = root_pkg.join("Cargo.toml");
         fs::create_dir_all(&manifest_path.parent().unwrap()).unwrap();
         fs::write(&manifest_path, toml::to_string(&manifest).unwrap()).unwrap();
+        {
+            File::create(root_pkg.join("BUILD.bazel")).unwrap();
+        }
 
         let sub_pkg_a = root_pkg.join("sub_pkg_a");
         let sub_pkg_b = root_pkg.join("sub_pkg_b");
@@ -855,7 +908,7 @@
 
         splicing_manifest.manifests.insert(
             manifest_path,
-            Label::from_str("//pkg_root:Cargo.toml").unwrap(),
+            Label::from_str("//root_pkg:Cargo.toml").unwrap(),
         );
 
         (splicing_manifest, cache_dir)
@@ -977,7 +1030,7 @@
         let workspace_manifest =
             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
                 .unwrap()
-                .splice_workspace()
+                .splice_workspace(&cargo())
                 .unwrap();
 
         // Ensure metadata is valid
@@ -1010,7 +1063,7 @@
         let workspace_manifest =
             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
                 .unwrap()
-                .splice_workspace()
+                .splice_workspace(&cargo())
                 .unwrap();
 
         // Ensure metadata is valid
@@ -1041,7 +1094,7 @@
         // Remove everything but the root manifest
         splicing_manifest
             .manifests
-            .retain(|_, label| *label == Label::from_str("//pkg_root:Cargo.toml").unwrap());
+            .retain(|_, label| *label == Label::from_str("//root_pkg:Cargo.toml").unwrap());
         assert_eq!(splicing_manifest.manifests.len(), 1);
 
         // Splice the workspace
@@ -1049,7 +1102,7 @@
         let workspace_manifest =
             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
                 .unwrap()
-                .splice_workspace();
+                .splice_workspace(&cargo());
 
         assert!(workspace_manifest.is_err());
 
@@ -1063,6 +1116,33 @@
     }
 
     #[test]
+    fn splice_workspace_report_missing_root() {
+        let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace();
+
+        // Remove everything but the root manifest
+        splicing_manifest
+            .manifests
+            .retain(|_, label| *label != Label::from_str("//root_pkg:Cargo.toml").unwrap());
+        assert_eq!(splicing_manifest.manifests.len(), 2);
+
+        // Splice the workspace
+        let workspace_root = tempfile::tempdir().unwrap();
+        let workspace_manifest =
+            Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
+                .unwrap()
+                .splice_workspace(&cargo());
+
+        assert!(workspace_manifest.is_err());
+
+        // Ensure both the missing manifests are mentioned in the error string
+        let err_str = format!("{:?}", &workspace_manifest);
+        assert!(
+            err_str.contains("Missing root workspace manifest")
+                && err_str.contains("//root_pkg:Cargo.toml")
+        );
+    }
+
+    #[test]
     fn splice_workspace_report_external_workspace_members() {
         let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace();
 
@@ -1101,7 +1181,7 @@
         let workspace_manifest =
             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
                 .unwrap()
-                .splice_workspace();
+                .splice_workspace(&cargo());
 
         assert!(workspace_manifest.is_err());
 
@@ -1124,7 +1204,7 @@
         let workspace_manifest =
             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
                 .unwrap()
-                .splice_workspace()
+                .splice_workspace(&cargo())
                 .unwrap();
 
         // Ensure metadata is valid
@@ -1153,7 +1233,7 @@
         let workspace_manifest =
             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
                 .unwrap()
-                .splice_workspace()
+                .splice_workspace(&cargo())
                 .unwrap();
 
         // Check the default resolver version
@@ -1202,7 +1282,7 @@
         let workspace_manifest =
             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
                 .unwrap()
-                .splice_workspace()
+                .splice_workspace(&cargo())
                 .unwrap();
 
         // Check the specified resolver version
@@ -1253,7 +1333,7 @@
         let workspace_root = tempfile::tempdir().unwrap();
         Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
             .unwrap()
-            .splice_workspace()
+            .splice_workspace(&cargo())
             .unwrap();
 
         let cargo_config = workspace_root.as_ref().join(".cargo").join("config.toml");
@@ -1286,7 +1366,7 @@
         let workspace_root = tempfile::tempdir().unwrap();
         Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
             .unwrap()
-            .splice_workspace()
+            .splice_workspace(&cargo())
             .unwrap();
 
         let cargo_config = workspace_root.as_ref().join(".cargo").join("config.toml");
@@ -1313,7 +1393,7 @@
         let workspace_root = temp_dir.as_ref().join("workspace_root");
         let splicing_result = Splicer::new(workspace_root.clone(), splicing_manifest)
             .unwrap()
-            .splice_workspace();
+            .splice_workspace(&cargo());
 
         // Ensure cargo config files in parent directories lead to errors
         assert!(splicing_result.is_err());