From 8194d3590aba37f92888655606b86b9d504a171c Mon Sep 17 00:00:00 2001
From: Alan Egerton <eggyal@gmail.com>
Date: Wed, 29 Nov 2023 14:44:59 +0000
Subject: [PATCH] Use build scripts from rustc source workspace

---
 crates/load-cargo/src/lib.rs                  |   3 +-
 crates/project-model/src/lib.rs               |   2 +-
 crates/project-model/src/tests.rs             |   8 +-
 crates/project-model/src/workspace.rs         | 135 +++++++++++-------
 .../rust-analyzer/src/cli/analysis_stats.rs   |   2 +-
 crates/rust-analyzer/src/cli/lsif.rs          |   2 +-
 crates/rust-analyzer/src/reload.rs            |  31 +++-
 7 files changed, 117 insertions(+), 66 deletions(-)

diff --git a/crates/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs
index 68b592ffaa4d..0c8294846d53 100644
--- a/crates/load-cargo/src/lib.rs
+++ b/crates/load-cargo/src/lib.rs
@@ -40,7 +40,7 @@ pub fn load_workspace_at(
 ) -> anyhow::Result<(AnalysisHost, vfs::Vfs, Option<ProcMacroServer>)> {
     let root = AbsPathBuf::assert(std::env::current_dir()?.join(root));
     let root = ProjectManifest::discover_single(&root)?;
-    let mut workspace = ProjectWorkspace::load(root, cargo_config, progress)?;
+    let mut workspace = ProjectWorkspace::load(root, cargo_config, progress, false)?;
 
     if load_config.load_out_dirs_from_check {
         let build_scripts = workspace.run_build_scripts(cargo_config, progress)?;
@@ -81,6 +81,7 @@ pub fn load_workspace(
             vfs.file_id(&path)
         },
         extra_env,
+        None,
     );
     let proc_macros = {
         let proc_macro_server = match &proc_macro_server {
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs
index 901dcfd2b110..a94e72ce9d74 100644
--- a/crates/project-model/src/lib.rs
+++ b/crates/project-model/src/lib.rs
@@ -50,7 +50,7 @@ pub use crate::{
     manifest_path::ManifestPath,
     project_json::{ProjectJson, ProjectJsonData},
     sysroot::Sysroot,
-    workspace::{CfgOverrides, PackageRoot, ProjectWorkspace},
+    workspace::{CfgOverrides, PackageRoot, ProjectWorkspace, RustcWorkspace},
 };
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
diff --git a/crates/project-model/src/tests.rs b/crates/project-model/src/tests.rs
index 98f3063bb982..8a6b6c49dec1 100644
--- a/crates/project-model/src/tests.rs
+++ b/crates/project-model/src/tests.rs
@@ -11,7 +11,7 @@ use rustc_hash::FxHashMap;
 use serde::de::DeserializeOwned;
 
 use crate::{
-    CargoWorkspace, CfgOverrides, ProjectJson, ProjectJsonData, ProjectWorkspace, Sysroot,
+    CargoWorkspace, CfgOverrides, ProjectJson, ProjectJsonData, ProjectWorkspace, RustcWorkspace, Sysroot,
     WorkspaceBuildScripts,
 };
 
@@ -29,7 +29,7 @@ fn load_cargo_with_overrides(
         cargo: cargo_workspace,
         build_scripts: WorkspaceBuildScripts::default(),
         sysroot: Err(None),
-        rustc: Err(None),
+        rustc: RustcWorkspace::Loaded(Err(None)),
         rustc_cfg: Vec::new(),
         cfg_overrides,
         toolchain: None,
@@ -48,7 +48,7 @@ fn load_cargo_with_sysroot(
         cargo: cargo_workspace,
         build_scripts: WorkspaceBuildScripts::default(),
         sysroot: Ok(get_fake_sysroot()),
-        rustc: Err(None),
+        rustc: RustcWorkspace::Loaded(Err(None)),
         rustc_cfg: Vec::new(),
         cfg_overrides: Default::default(),
         toolchain: None,
@@ -62,6 +62,7 @@ fn load_cargo_with_sysroot(
             }
         },
         &Default::default(),
+        None,
     )
 }
 
@@ -146,6 +147,7 @@ fn to_crate_graph(project_workspace: ProjectWorkspace) -> (CrateGraph, ProcMacro
             }
         },
         &Default::default(),
+        None,
     )
 }
 
diff --git a/crates/project-model/src/workspace.rs b/crates/project-model/src/workspace.rs
index 9333570354a0..9dfb9cc18f70 100644
--- a/crates/project-model/src/workspace.rs
+++ b/crates/project-model/src/workspace.rs
@@ -23,7 +23,7 @@ use crate::{
     project_json::Crate,
     rustc_cfg::{self, RustcCfgConfig},
     sysroot::SysrootCrate,
-    target_data_layout, utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy, ManifestPath,
+    target_data_layout, utf8_stdout, CargoConfig, CargoWorkspace, InvocationStrategy,
     Package, ProjectJson, ProjectManifest, Sysroot, TargetData, TargetKind, WorkspaceBuildScripts,
 };
 
@@ -53,6 +53,25 @@ pub struct PackageRoot {
     pub exclude: Vec<AbsPathBuf>,
 }
 
+#[derive(Clone, PartialEq)]
+pub enum RustcWorkspace {
+    /// A globally-configured rustc source location is being opened as a rust-analyzer workspace
+    Opening,
+    /// The rustc source is loaded, e.g. from sysroot, but is not a rust-analyzer workspace
+    Loaded(Result<(CargoWorkspace, WorkspaceBuildScripts), Option<String>>),
+}
+
+impl RustcWorkspace {
+    /// Returns the loaded `CargoWorkspace` of the rustc source.
+    /// Will be `None` if either the rustc_source loading failed or a the 
+    fn loaded(&self) -> Option<&(CargoWorkspace, WorkspaceBuildScripts)> {
+        match self {
+            Self::Opening => None,
+            Self::Loaded(res) => res.as_ref().ok(),
+        }
+    }
+}
+
 #[derive(Clone)]
 pub enum ProjectWorkspace {
     /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`.
@@ -60,7 +79,7 @@ pub enum ProjectWorkspace {
         cargo: CargoWorkspace,
         build_scripts: WorkspaceBuildScripts,
         sysroot: Result<Sysroot, Option<String>>,
-        rustc: Result<(CargoWorkspace, WorkspaceBuildScripts), Option<String>>,
+        rustc: RustcWorkspace,
         /// Holds cfg flags for the current target. We get those by running
         /// `rustc --print cfg`.
         ///
@@ -119,7 +138,7 @@ impl fmt::Debug for ProjectWorkspace {
                 .field("sysroot", &sysroot.is_ok())
                 .field(
                     "n_rustc_compiler_crates",
-                    &rustc.as_ref().map_or(0, |(rc, _)| rc.packages().len()),
+                    &rustc.loaded().map_or(0, |(rc, _)| rc.packages().len()),
                 )
                 .field("n_rustc_cfg", &rustc_cfg.len())
                 .field("n_cfg_overrides", &cfg_overrides.len())
@@ -151,8 +170,9 @@ impl ProjectWorkspace {
         manifest: ProjectManifest,
         config: &CargoConfig,
         progress: &dyn Fn(String),
+        opening_rustc_workspace: bool,
     ) -> anyhow::Result<ProjectWorkspace> {
-        ProjectWorkspace::load_inner(&manifest, config, progress)
+        ProjectWorkspace::load_inner(&manifest, config, progress, opening_rustc_workspace)
             .with_context(|| format!("Failed to load the project at {manifest}"))
     }
 
@@ -160,6 +180,7 @@ impl ProjectWorkspace {
         manifest: &ProjectManifest,
         config: &CargoConfig,
         progress: &dyn Fn(String),
+        opening_rustc_workspace: bool,
     ) -> anyhow::Result<ProjectWorkspace> {
         let version = |current_dir, cmd_path, prefix: &str| {
             let cargo_version = utf8_stdout({
@@ -236,48 +257,54 @@ impl ProjectWorkspace {
                     tracing::info!(workspace = %cargo_toml, src_root = %sysroot.src_root(), root = %sysroot.root(), "Using sysroot");
                 }
 
-                let rustc_dir = match &config.rustc_source {
-                    Some(RustLibSource::Path(path)) => ManifestPath::try_from(path.clone())
-                        .map_err(|p| Some(format!("rustc source path is not absolute: {p}"))),
-                    Some(RustLibSource::Discover) => {
-                        sysroot.as_ref().ok().and_then(Sysroot::discover_rustc_src).ok_or_else(
-                            || Some(format!("Failed to discover rustc source for sysroot.")),
-                        )
-                    }
-                    None => Err(None),
-                };
-
-                let rustc =  rustc_dir.and_then(|rustc_dir| {
-                    tracing::info!(workspace = %cargo_toml, rustc_dir = %rustc_dir, "Using rustc source");
-                    match CargoWorkspace::fetch_metadata(
-                        &rustc_dir,
-                        cargo_toml.parent(),
-                        &CargoConfig {
-                            features: crate::CargoFeatures::default(),
-                            ..config.clone()
-                        },
-                        progress,
-                    ) {
-                        Ok(meta) => {
-                            let workspace = CargoWorkspace::new(meta);
-                            let buildscripts = WorkspaceBuildScripts::rustc_crates(
-                                &workspace,
-                                cargo_toml.parent(),
-                                &config.extra_env,
-                            );
-                            Ok((workspace, buildscripts))
+                let rustc = if opening_rustc_workspace {
+                    RustcWorkspace::Opening
+                } else {
+                    let rustc_dir = match &config.rustc_source {
+                        // `config.rustc_source == Some(Path(...))` while `!opening_rustc_workspace` should only occur if
+                        // `ManifestPath::try_from(rustc_dir)` failed in `fetch_workspaces`, so no need to attempt it here
+                        // again.
+                        Some(RustLibSource::Path(path)) => Err(Some(format!("rustc source path is not absolute: {path}"))),
+                        Some(RustLibSource::Discover) => {
+                            sysroot.as_ref().ok().and_then(Sysroot::discover_rustc_src).ok_or_else(
+                                || Some(format!("Failed to discover rustc source for sysroot.")),
+                            )
                         }
-                        Err(e) => {
-                            tracing::error!(
-                                %e,
-                                "Failed to read Cargo metadata from rustc source at {rustc_dir}",
-                            );
-                            Err(Some(format!(
-                                "Failed to read Cargo metadata from rustc source at {rustc_dir}: {e}"
-                            )))
+                        None => Err(None),
+                    };
+
+                    RustcWorkspace::Loaded(rustc_dir.and_then(|rustc_dir| {
+                        tracing::info!(workspace = %cargo_toml, rustc_dir = %rustc_dir, "Using rustc source");
+                        match CargoWorkspace::fetch_metadata(
+                            &rustc_dir,
+                            cargo_toml.parent(),
+                            &CargoConfig {
+                                features: crate::CargoFeatures::default(),
+                                ..config.clone()
+                            },
+                            progress,
+                        ) {
+                            Ok(meta) => {
+                                let workspace = CargoWorkspace::new(meta);
+                                let buildscripts = WorkspaceBuildScripts::rustc_crates(
+                                    &workspace,
+                                    cargo_toml.parent(),
+                                    &config.extra_env,
+                                );
+                                Ok((workspace, buildscripts))
+                            }
+                            Err(e) => {
+                                tracing::error!(
+                                    %e,
+                                    "Failed to read Cargo metadata from rustc source at {rustc_dir}",
+                                );
+                                Err(Some(format!(
+                                    "Failed to read Cargo metadata from rustc source at {rustc_dir}: {e}"
+                                )))
+                            }
                         }
-                    }
-                });
+                    }))
+                };
 
                 let rustc_cfg = rustc_cfg::get(
                     config.target.as_deref(),
@@ -564,7 +591,7 @@ impl ProjectWorkspace {
                         PackageRoot { is_local, include, exclude }
                     })
                     .chain(mk_sysroot(sysroot.as_ref(), Some(cargo.workspace_root())))
-                    .chain(rustc.iter().flat_map(|(rustc, _)| {
+                    .chain(rustc.loaded().iter().flat_map(|(rustc, _)| {
                         rustc.packages().map(move |krate| PackageRoot {
                             is_local: false,
                             include: vec![rustc[krate].manifest.parent().to_path_buf()],
@@ -592,7 +619,7 @@ impl ProjectWorkspace {
                 sysroot_package_len + project.n_crates()
             }
             ProjectWorkspace::Cargo { cargo, sysroot, rustc, .. } => {
-                let rustc_package_len = rustc.as_ref().map_or(0, |(it, _)| it.packages().len());
+                let rustc_package_len = rustc.loaded().map_or(0, |(it, _)| it.packages().len());
                 let sysroot_package_len = sysroot.as_ref().map_or(0, |it| it.crates().len());
                 cargo.packages().len() + sysroot_package_len + rustc_package_len
             }
@@ -607,6 +634,7 @@ impl ProjectWorkspace {
         &self,
         load: &mut dyn FnMut(&AbsPath) -> Option<FileId>,
         extra_env: &FxHashMap<String, String>,
+        opened_rustc_workspace: Option<(&CargoWorkspace, &WorkspaceBuildScripts)>,
     ) -> (CrateGraph, ProcMacroPaths) {
         let _p = profile::span("ProjectWorkspace::to_crate_graph");
 
@@ -633,7 +661,10 @@ impl ProjectWorkspace {
                 target_layout,
             } => cargo_to_crate_graph(
                 load,
-                rustc.as_ref().ok(),
+                match rustc {
+                    RustcWorkspace::Opening => opened_rustc_workspace,
+                    RustcWorkspace::Loaded(res) => res.as_ref().ok().map(|(a, b)| (a, b)),
+                },
                 cargo,
                 sysroot.as_ref().ok(),
                 rustc_cfg.clone(),
@@ -844,7 +875,7 @@ fn project_json_to_crate_graph(
 
 fn cargo_to_crate_graph(
     load: &mut dyn FnMut(&AbsPath) -> Option<FileId>,
-    rustc: Option<&(CargoWorkspace, WorkspaceBuildScripts)>,
+    rustc: Option<(&CargoWorkspace, &WorkspaceBuildScripts)>,
     cargo: &CargoWorkspace,
     sysroot: Option<&Sysroot>,
     rustc_cfg: Vec<CfgFlag>,
@@ -1030,13 +1061,7 @@ fn cargo_to_crate_graph(
                 &pkg_crates,
                 &cfg_options,
                 override_cfg,
-                if rustc_workspace.workspace_root() == cargo.workspace_root() {
-                    // the rustc workspace does not use the installed toolchain's proc-macro server
-                    // so we need to make sure we don't use the pre compiled proc-macros there either
-                    build_scripts
-                } else {
-                    rustc_build_scripts
-                },
+                rustc_build_scripts,
                 target_layout,
                 channel,
             );
diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs
index 0f6539f224d7..00026520f1b5 100644
--- a/crates/rust-analyzer/src/cli/analysis_stats.rs
+++ b/crates/rust-analyzer/src/cli/analysis_stats.rs
@@ -70,7 +70,7 @@ impl flags::AnalysisStats {
         let path = AbsPathBuf::assert(env::current_dir()?.join(&self.path));
         let manifest = ProjectManifest::discover_single(&path)?;
 
-        let mut workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress)?;
+        let mut workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress, false)?;
         let metadata_time = db_load_sw.elapsed();
         let load_cargo_config = LoadCargoConfig {
             load_out_dirs_from_check: !self.disable_build_scripts,
diff --git a/crates/rust-analyzer/src/cli/lsif.rs b/crates/rust-analyzer/src/cli/lsif.rs
index d6a45ce06f21..294268b30f9c 100644
--- a/crates/rust-analyzer/src/cli/lsif.rs
+++ b/crates/rust-analyzer/src/cli/lsif.rs
@@ -299,7 +299,7 @@ impl flags::Lsif {
         let path = AbsPathBuf::assert(env::current_dir()?.join(&self.path));
         let manifest = ProjectManifest::discover_single(&path)?;
 
-        let workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress)?;
+        let workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress, false)?;
 
         let (host, vfs, _proc_macro) =
             load_workspace(workspace, &cargo_config.extra_env, &load_cargo_config)?;
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index abe2191f4002..9d416cb0ada3 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -25,7 +25,7 @@ use ide_db::{
 use itertools::Itertools;
 use load_cargo::{load_proc_macro, ProjectFolders};
 use proc_macro_api::ProcMacroServer;
-use project_model::{ProjectWorkspace, WorkspaceBuildScripts};
+use project_model::{ProjectWorkspace, WorkspaceBuildScripts, RustLibSource, ProjectManifest, ManifestPath, RustcWorkspace};
 use rustc_hash::FxHashSet;
 use stdx::{format_to, thread::ThreadIntent};
 use triomphe::Arc;
@@ -166,7 +166,7 @@ impl GlobalState {
                     }
                 }
             }
-            if let ProjectWorkspace::Cargo { rustc: Err(Some(e)), .. } = ws {
+            if let ProjectWorkspace::Cargo { rustc: RustcWorkspace::Loaded(Err(Some(e))), .. } = ws {
                 status.health = lsp_ext::Health::Warning;
                 message.push_str(e);
                 message.push_str("\n\n");
@@ -188,7 +188,7 @@ impl GlobalState {
         tracing::info!(%cause, "will fetch workspaces");
 
         self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, {
-            let linked_projects = self.config.linked_projects();
+            let mut linked_projects = self.config.linked_projects();
             let detached_files = self.config.detached_files().to_vec();
             let cargo_config = self.config.cargo();
 
@@ -204,6 +204,21 @@ impl GlobalState {
 
                 sender.send(Task::FetchWorkspace(ProjectWorkspaceProgress::Begin)).unwrap();
 
+                let mut opening_rustc_workspace = false;
+                if let Some(RustLibSource::Path(rustc_dir)) = &cargo_config.rustc_source {
+                    if let Ok(rustc_manifest) = ManifestPath::try_from(rustc_dir.clone()) {
+                        if let Some(rustc_index) = linked_projects.iter().position(|project| match project {
+                            LinkedProject::ProjectManifest(ProjectManifest::CargoToml(project_manifest)) => *project_manifest == rustc_manifest,
+                            _ => false,
+                        }) {
+                            // move to the front so that the ensuing workspace's build scripts will be built and available
+                            // to other workspaces (in particular, any that have packages using rustc_private crates)
+                            linked_projects.swap(0, rustc_index);
+                            opening_rustc_workspace = true;
+                        }
+                    }
+                }
+
                 let mut workspaces = linked_projects
                     .iter()
                     .map(|project| match project {
@@ -212,6 +227,7 @@ impl GlobalState {
                                 manifest.clone(),
                                 &cargo_config,
                                 &progress,
+                                opening_rustc_workspace,
                             )
                         }
                         LinkedProject::InlineJsonProject(it) => {
@@ -506,9 +522,16 @@ impl GlobalState {
 
             let mut crate_graph = CrateGraph::default();
             let mut proc_macros = Vec::default();
+
+            // if the first workspace is `RustcWorkspace::Opening` then it is guaranteed to be the rustc source
+            // by virtue of the sorting in `fetch_workspaces`.
+            let opened_rustc_workspace = match self.workspaces.first() {
+                Some(ProjectWorkspace::Cargo { rustc: RustcWorkspace::Opening, cargo, build_scripts, .. }) => Some((cargo, build_scripts)),
+                _ => None,
+            };
             for ws in &**self.workspaces {
                 let (other, mut crate_proc_macros) =
-                    ws.to_crate_graph(&mut load, &self.config.extra_env());
+                    ws.to_crate_graph(&mut load, &self.config.extra_env(), opened_rustc_workspace);
                 crate_graph.extend(other, &mut crate_proc_macros);
                 proc_macros.push(crate_proc_macros);
             }