From 186c5c47cbfde4ae9d81dc67450c958cb6aece2c Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 12 Apr 2021 11:04:36 +0300 Subject: [PATCH] feat: avoid checking the whole project during initial loading --- crates/project_model/src/build_data.rs | 242 ++++++++++-------- crates/rust-analyzer/src/benchmarks.rs | 7 +- crates/rust-analyzer/src/bin/main.rs | 15 ++ crates/rust-analyzer/src/bin/rustc_wrapper.rs | 46 ++++ .../rust-analyzer/src/cli/analysis_stats.rs | 1 + crates/rust-analyzer/src/cli/diagnostics.rs | 3 +- crates/rust-analyzer/src/cli/load_cargo.rs | 10 +- crates/rust-analyzer/src/cli/ssr.rs | 9 +- crates/rust-analyzer/src/config.rs | 6 + crates/rust-analyzer/src/main_loop.rs | 3 +- .../rust-analyzer/tests/rust-analyzer/main.rs | 2 +- .../tests/rust-analyzer/support.rs | 20 +- docs/user/generated_config.adoc | 6 + editors/code/package.json | 5 + 14 files changed, 251 insertions(+), 124 deletions(-) create mode 100644 crates/rust-analyzer/src/bin/rustc_wrapper.rs diff --git a/crates/project_model/src/build_data.rs b/crates/project_model/src/build_data.rs index 0d4d39fefff..ab5cc8c4913 100644 --- a/crates/project_model/src/build_data.rs +++ b/crates/project_model/src/build_data.rs @@ -58,12 +58,17 @@ impl PartialEq for BuildDataConfig { impl Eq for BuildDataConfig {} -#[derive(Debug, Default)] +#[derive(Debug)] pub struct BuildDataCollector { + wrap_rustc: bool, configs: FxHashMap, } impl BuildDataCollector { + pub fn new(wrap_rustc: bool) -> Self { + Self { wrap_rustc, configs: FxHashMap::default() } + } + pub(crate) fn add_config(&mut self, workspace_root: &AbsPath, config: BuildDataConfig) { self.configs.insert(workspace_root.to_path_buf(), config); } @@ -71,15 +76,14 @@ impl BuildDataCollector { pub fn collect(&mut self, progress: &dyn Fn(String)) -> Result { let mut res = BuildDataResult::default(); for (path, config) in self.configs.iter() { - res.per_workspace.insert( - path.clone(), - collect_from_workspace( - &config.cargo_toml, - &config.cargo_features, - &config.packages, - progress, - )?, - ); + let workspace_build_data = WorkspaceBuildData::collect( + &config.cargo_toml, + &config.cargo_features, + &config.packages, + self.wrap_rustc, + progress, + )?; + res.per_workspace.insert(path.clone(), workspace_build_data); } Ok(res) } @@ -120,119 +124,137 @@ impl BuildDataConfig { } } -fn collect_from_workspace( - cargo_toml: &AbsPath, - cargo_features: &CargoConfig, - packages: &Vec, - progress: &dyn Fn(String), -) -> Result { - let mut cmd = Command::new(toolchain::cargo()); - cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"]) - .arg(cargo_toml.as_ref()); +impl WorkspaceBuildData { + fn collect( + cargo_toml: &AbsPath, + cargo_features: &CargoConfig, + packages: &Vec, + wrap_rustc: bool, + progress: &dyn Fn(String), + ) -> Result { + let mut cmd = Command::new(toolchain::cargo()); - // --all-targets includes tests, benches and examples in addition to the - // default lib and bins. This is an independent concept from the --targets - // flag below. - cmd.arg("--all-targets"); - - if let Some(target) = &cargo_features.target { - cmd.args(&["--target", target]); - } - - if cargo_features.all_features { - cmd.arg("--all-features"); - } else { - if cargo_features.no_default_features { - // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures` - // https://github.com/oli-obk/cargo_metadata/issues/79 - cmd.arg("--no-default-features"); + if wrap_rustc { + // Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use + // that to compile only proc macros and build scripts during the initial + // `cargo check`. + let myself = std::env::current_exe()?; + cmd.env("RUSTC_WRAPPER", myself); + cmd.env("RA_RUSTC_WRAPPER", "1"); } - if !cargo_features.features.is_empty() { - cmd.arg("--features"); - cmd.arg(cargo_features.features.join(" ")); + + cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"]) + .arg(cargo_toml.as_ref()); + + // --all-targets includes tests, benches and examples in addition to the + // default lib and bins. This is an independent concept from the --targets + // flag below. + cmd.arg("--all-targets"); + + if let Some(target) = &cargo_features.target { + cmd.args(&["--target", target]); } - } - cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null()); - - let mut child = cmd.spawn().map(JodChild)?; - let child_stdout = child.stdout.take().unwrap(); - let stdout = BufReader::new(child_stdout); - - let mut res = WorkspaceBuildData::default(); - for message in cargo_metadata::Message::parse_stream(stdout).flatten() { - match message { - Message::BuildScriptExecuted(BuildScript { - package_id, out_dir, cfgs, env, .. - }) => { - let cfgs = { - let mut acc = Vec::new(); - for cfg in cfgs { - match cfg.parse::() { - Ok(it) => acc.push(it), - Err(err) => { - anyhow::bail!("invalid cfg from cargo-metadata: {}", err) - } - }; - } - acc - }; - let package_build_data = - res.per_package.entry(package_id.repr.clone()).or_default(); - // cargo_metadata crate returns default (empty) path for - // older cargos, which is not absolute, so work around that. - if !out_dir.as_str().is_empty() { - let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir.into_os_string())); - package_build_data.out_dir = Some(out_dir); - package_build_data.cfgs = cfgs; - } - - package_build_data.envs = env; + if cargo_features.all_features { + cmd.arg("--all-features"); + } else { + if cargo_features.no_default_features { + // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures` + // https://github.com/oli-obk/cargo_metadata/issues/79 + cmd.arg("--no-default-features"); } - Message::CompilerArtifact(message) => { - progress(format!("metadata {}", message.target.name)); + if !cargo_features.features.is_empty() { + cmd.arg("--features"); + cmd.arg(cargo_features.features.join(" ")); + } + } - if message.target.kind.contains(&"proc-macro".to_string()) { - let package_id = message.package_id; - // Skip rmeta file - if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name)) { - let filename = AbsPathBuf::assert(PathBuf::from(&filename)); - let package_build_data = - res.per_package.entry(package_id.repr.clone()).or_default(); - package_build_data.proc_macro_dylib_path = Some(filename); + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null()); + + let mut child = cmd.spawn().map(JodChild)?; + let child_stdout = child.stdout.take().unwrap(); + let stdout = BufReader::new(child_stdout); + + let mut res = WorkspaceBuildData::default(); + for message in cargo_metadata::Message::parse_stream(stdout).flatten() { + match message { + Message::BuildScriptExecuted(BuildScript { + package_id, + out_dir, + cfgs, + env, + .. + }) => { + let cfgs = { + let mut acc = Vec::new(); + for cfg in cfgs { + match cfg.parse::() { + Ok(it) => acc.push(it), + Err(err) => { + anyhow::bail!("invalid cfg from cargo-metadata: {}", err) + } + }; + } + acc + }; + let package_build_data = + res.per_package.entry(package_id.repr.clone()).or_default(); + // cargo_metadata crate returns default (empty) path for + // older cargos, which is not absolute, so work around that. + if !out_dir.as_str().is_empty() { + let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir.into_os_string())); + package_build_data.out_dir = Some(out_dir); + package_build_data.cfgs = cfgs; + } + + package_build_data.envs = env; + } + Message::CompilerArtifact(message) => { + progress(format!("metadata {}", message.target.name)); + + if message.target.kind.contains(&"proc-macro".to_string()) { + let package_id = message.package_id; + // Skip rmeta file + if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name)) + { + let filename = AbsPathBuf::assert(PathBuf::from(&filename)); + let package_build_data = + res.per_package.entry(package_id.repr.clone()).or_default(); + package_build_data.proc_macro_dylib_path = Some(filename); + } } } - } - Message::CompilerMessage(message) => { - progress(message.target.name.clone()); - } - Message::BuildFinished(_) => {} - Message::TextLine(_) => {} - _ => {} - } - } - - for package in packages { - let package_build_data = res.per_package.entry(package.id.repr.clone()).or_default(); - inject_cargo_env(package, package_build_data); - if let Some(out_dir) = &package_build_data.out_dir { - // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!() - if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) { - package_build_data.envs.push(("OUT_DIR".to_string(), out_dir)); + Message::CompilerMessage(message) => { + progress(message.target.name.clone()); + } + Message::BuildFinished(_) => {} + Message::TextLine(_) => {} + _ => {} } } - } - let output = child.into_inner().wait_with_output()?; - if !output.status.success() { - let mut stderr = String::from_utf8(output.stderr).unwrap_or_default(); - if stderr.is_empty() { - stderr = "cargo check failed".to_string(); + for package in packages { + let package_build_data = res.per_package.entry(package.id.repr.clone()).or_default(); + inject_cargo_env(package, package_build_data); + if let Some(out_dir) = &package_build_data.out_dir { + // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!() + if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) { + package_build_data.envs.push(("OUT_DIR".to_string(), out_dir)); + } + } } - res.error = Some(stderr) - } - Ok(res) + let output = child.into_inner().wait_with_output()?; + if !output.status.success() { + let mut stderr = String::from_utf8(output.stderr).unwrap_or_default(); + if stderr.is_empty() { + stderr = "cargo check failed".to_string(); + } + res.error = Some(stderr) + } + + Ok(res) + } } // FIXME: File a better way to know if it is a dylib diff --git a/crates/rust-analyzer/src/benchmarks.rs b/crates/rust-analyzer/src/benchmarks.rs index bf569b40bb6..bdd94b1c4e5 100644 --- a/crates/rust-analyzer/src/benchmarks.rs +++ b/crates/rust-analyzer/src/benchmarks.rs @@ -30,8 +30,11 @@ fn benchmark_integrated_highlighting() { let file = "./crates/ide_db/src/apply_change.rs"; let cargo_config = Default::default(); - let load_cargo_config = - LoadCargoConfig { load_out_dirs_from_check: true, with_proc_macro: false }; + let load_cargo_config = LoadCargoConfig { + load_out_dirs_from_check: true, + wrap_rustc: false, + with_proc_macro: false, + }; let (mut host, vfs, _proc_macro) = { let _it = stdx::timeit("workspace loading"); diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index 873e82c7b2b..f0abb5b15f2 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -3,6 +3,7 @@ //! Based on cli flags, either spawns an LSP server, or runs a batch analysis mod flags; mod logger; +mod rustc_wrapper; use std::{convert::TryFrom, env, fs, path::Path, process}; @@ -26,6 +27,20 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; fn main() { + if std::env::var("RA_RUSTC_WRAPPER").is_ok() { + let mut args = std::env::args_os(); + let _me = args.next().unwrap(); + let rustc = args.next().unwrap(); + let code = match rustc_wrapper::run_rustc_skipping_cargo_checking(rustc, args.collect()) { + Ok(rustc_wrapper::ExitCode(code)) => code.unwrap_or(102), + Err(err) => { + eprintln!("{}", err); + 101 + } + }; + process::exit(code); + } + if let Err(err) = try_main() { log::error!("Unexpected error: {}", err); eprintln!("{}", err); diff --git a/crates/rust-analyzer/src/bin/rustc_wrapper.rs b/crates/rust-analyzer/src/bin/rustc_wrapper.rs new file mode 100644 index 00000000000..2f6d4706d87 --- /dev/null +++ b/crates/rust-analyzer/src/bin/rustc_wrapper.rs @@ -0,0 +1,46 @@ +//! We setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself during the +//! initial `cargo check`. That way, we avoid checking the actual project, and +//! only build proc macros and build.rs. +//! +//! Code taken from IntelliJ :0) +//! https://github.com/intellij-rust/intellij-rust/blob/master/native-helper/src/main.rs +use std::{ + ffi::OsString, + io, + process::{Command, Stdio}, +}; + +/// ExitCode/ExitStatus are impossible to create :(. +pub(crate) struct ExitCode(pub(crate) Option); + +pub(crate) fn run_rustc_skipping_cargo_checking( + rustc_executable: OsString, + args: Vec, +) -> io::Result { + let is_cargo_check = args.iter().any(|arg| { + let arg = arg.to_string_lossy(); + // `cargo check` invokes `rustc` with `--emit=metadata` argument. + // + // https://doc.rust-lang.org/rustc/command-line-arguments.html#--emit-specifies-the-types-of-output-files-to-generate + // link — Generates the crates specified by --crate-type. The default + // output filenames depend on the crate type and platform. This + // is the default if --emit is not specified. + // metadata — Generates a file containing metadata about the crate. + // The default output filename is CRATE_NAME.rmeta. + arg.starts_with("--emit=") && arg.contains("metadata") && !arg.contains("link") + }); + if is_cargo_check { + return Ok(ExitCode(Some(0))); + } + run_rustc(rustc_executable, args) +} + +fn run_rustc(rustc_executable: OsString, args: Vec) -> io::Result { + let mut child = Command::new(rustc_executable) + .args(args) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()?; + Ok(ExitCode(child.wait()?.code())) +} diff --git a/crates/rust-analyzer/src/cli/analysis_stats.rs b/crates/rust-analyzer/src/cli/analysis_stats.rs index fe9f273b0aa..3f313456265 100644 --- a/crates/rust-analyzer/src/cli/analysis_stats.rs +++ b/crates/rust-analyzer/src/cli/analysis_stats.rs @@ -68,6 +68,7 @@ impl AnalysisStatsCmd { cargo_config.no_sysroot = self.no_sysroot; let load_cargo_config = LoadCargoConfig { load_out_dirs_from_check: self.load_output_dirs, + wrap_rustc: false, with_proc_macro: self.with_proc_macro, }; let (host, vfs, _proc_macro) = diff --git a/crates/rust-analyzer/src/cli/diagnostics.rs b/crates/rust-analyzer/src/cli/diagnostics.rs index 8b985716b03..0085d0e4dff 100644 --- a/crates/rust-analyzer/src/cli/diagnostics.rs +++ b/crates/rust-analyzer/src/cli/diagnostics.rs @@ -34,7 +34,8 @@ pub fn diagnostics( with_proc_macro: bool, ) -> Result<()> { let cargo_config = Default::default(); - let load_cargo_config = LoadCargoConfig { load_out_dirs_from_check, with_proc_macro }; + let load_cargo_config = + LoadCargoConfig { load_out_dirs_from_check, with_proc_macro, wrap_rustc: false }; let (host, _vfs, _proc_macro) = load_workspace_at(path, &cargo_config, &load_cargo_config, &|_| {})?; let db = host.raw_database(); diff --git a/crates/rust-analyzer/src/cli/load_cargo.rs b/crates/rust-analyzer/src/cli/load_cargo.rs index 310c36904e0..75bad1112bf 100644 --- a/crates/rust-analyzer/src/cli/load_cargo.rs +++ b/crates/rust-analyzer/src/cli/load_cargo.rs @@ -15,6 +15,7 @@ use crate::reload::{ProjectFolders, SourceRootConfig}; pub struct LoadCargoConfig { pub load_out_dirs_from_check: bool, + pub wrap_rustc: bool, pub with_proc_macro: bool, } @@ -52,7 +53,7 @@ pub fn load_workspace( }; let build_data = if config.load_out_dirs_from_check { - let mut collector = BuildDataCollector::default(); + let mut collector = BuildDataCollector::new(config.wrap_rustc); ws.collect_build_data_configs(&mut collector); Some(collector.collect(progress)?) } else { @@ -136,8 +137,11 @@ mod tests { fn test_loading_rust_analyzer() -> Result<()> { let path = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap(); let cargo_config = Default::default(); - let load_cargo_config = - LoadCargoConfig { load_out_dirs_from_check: false, with_proc_macro: false }; + let load_cargo_config = LoadCargoConfig { + load_out_dirs_from_check: false, + wrap_rustc: false, + with_proc_macro: false, + }; let (host, _vfs, _proc_macro) = load_workspace_at(path, &cargo_config, &load_cargo_config, &|_| {})?; diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs index 79f426fff15..1fd9b5a9b47 100644 --- a/crates/rust-analyzer/src/cli/ssr.rs +++ b/crates/rust-analyzer/src/cli/ssr.rs @@ -9,8 +9,11 @@ use ide_ssr::{MatchFinder, SsrPattern, SsrRule}; pub fn apply_ssr_rules(rules: Vec) -> Result<()> { use ide_db::base_db::SourceDatabaseExt; let cargo_config = Default::default(); - let load_cargo_config = - LoadCargoConfig { load_out_dirs_from_check: true, with_proc_macro: true }; + let load_cargo_config = LoadCargoConfig { + load_out_dirs_from_check: true, + wrap_rustc: false, + with_proc_macro: true, + }; let (host, vfs, _proc_macro) = load_workspace_at(&std::env::current_dir()?, &cargo_config, &load_cargo_config, &|_| {})?; let db = host.raw_database(); @@ -37,7 +40,7 @@ pub fn search_for_patterns(patterns: Vec, debug_snippet: Option bool { self.data.cargo_runBuildScripts || self.data.procMacro_enable } + pub fn wrap_rustc(&self) -> bool { + self.data.cargo_useRustcWrapperForBuildScripts + } pub fn cargo(&self) -> CargoConfig { let rustc_source = self.data.rustcSource.as_ref().map(|rustc_src| { if rustc_src == "discover" { diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index 47c6c6d7761..b3d4c6ec5e0 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -236,7 +236,8 @@ impl GlobalState { let workspaces_updated = !Arc::ptr_eq(&old, &self.workspaces); if self.config.run_build_scripts() && workspaces_updated { - let mut collector = BuildDataCollector::default(); + let mut collector = + BuildDataCollector::new(self.config.wrap_rustc()); for ws in self.workspaces.iter() { ws.collect_build_data_configs(&mut collector); } diff --git a/crates/rust-analyzer/tests/rust-analyzer/main.rs b/crates/rust-analyzer/tests/rust-analyzer/main.rs index 4442cbff681..1e4c04bbf6d 100644 --- a/crates/rust-analyzer/tests/rust-analyzer/main.rs +++ b/crates/rust-analyzer/tests/rust-analyzer/main.rs @@ -527,7 +527,7 @@ version = \"0.0.0\" #[test] fn out_dirs_check() { if skip_slow_tests() { - return; + // return; } let server = Project::with_fixture( diff --git a/crates/rust-analyzer/tests/rust-analyzer/support.rs b/crates/rust-analyzer/tests/rust-analyzer/support.rs index 8d68f1b7d53..5e388c0f028 100644 --- a/crates/rust-analyzer/tests/rust-analyzer/support.rs +++ b/crates/rust-analyzer/tests/rust-analyzer/support.rs @@ -32,8 +32,12 @@ impl<'a> Project<'a> { tmp_dir: None, roots: vec![], config: serde_json::json!({ - // Loading standard library is costly, let's ignore it by default - "cargo": { "noSysroot": true } + "cargo": { + // Loading standard library is costly, let's ignore it by default + "noSysroot": true, + // Can't use test binary as rustc wrapper. + "useRustcWrapperForBuildScripts": false, + } }), } } @@ -49,7 +53,17 @@ impl<'a> Project<'a> { } pub(crate) fn with_config(mut self, config: serde_json::Value) -> Project<'a> { - self.config = config; + fn merge(dst: &mut serde_json::Value, src: serde_json::Value) { + match (dst, src) { + (Value::Object(dst), Value::Object(src)) => { + for (k, v) in src { + merge(dst.entry(k).or_insert(v.clone()), v) + } + } + (dst, src) => *dst = src, + } + } + merge(&mut self.config, config); self } diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index 871c65adde9..e0ee35b4e8c 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -39,6 +39,12 @@ List of features to activate. -- Run build scripts (`build.rs`) for more precise code analysis. -- +[[rust-analyzer.cargo.useRustcWrapperForBuildScripts]]rust-analyzer.cargo.useRustcWrapperForBuildScripts (default: `true`):: ++ +-- +Use `RUSTC_WRAPPER=rust-analyzer` when running build scripts to +avoid compiling unnecessary things. +-- [[rust-analyzer.cargo.noDefaultFeatures]]rust-analyzer.cargo.noDefaultFeatures (default: `false`):: + -- diff --git a/editors/code/package.json b/editors/code/package.json index d263610f54e..06ed62d8d7c 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -434,6 +434,11 @@ "default": true, "type": "boolean" }, + "rust-analyzer.cargo.useRustcWrapperForBuildScripts": { + "markdownDescription": "Use `RUSTC_WRAPPER=rust-analyzer` when running build scripts to\navoid compiling unnecessary things.", + "default": true, + "type": "boolean" + }, "rust-analyzer.cargo.noDefaultFeatures": { "markdownDescription": "Do not activate the `default` feature.", "default": false,