Skip to content

Commit 186c5c4

Browse files
committed
feat: avoid checking the whole project during initial loading
1 parent 7be0613 commit 186c5c4

File tree

14 files changed

+248
-121
lines changed

14 files changed

+248
-121
lines changed

crates/project_model/src/build_data.rs

Lines changed: 129 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,32 @@ impl PartialEq for BuildDataConfig {
5858

5959
impl Eq for BuildDataConfig {}
6060

61-
#[derive(Debug, Default)]
61+
#[derive(Debug)]
6262
pub struct BuildDataCollector {
63+
wrap_rustc: bool,
6364
configs: FxHashMap<AbsPathBuf, BuildDataConfig>,
6465
}
6566

6667
impl BuildDataCollector {
68+
pub fn new(wrap_rustc: bool) -> Self {
69+
Self { wrap_rustc, configs: FxHashMap::default() }
70+
}
71+
6772
pub(crate) fn add_config(&mut self, workspace_root: &AbsPath, config: BuildDataConfig) {
6873
self.configs.insert(workspace_root.to_path_buf(), config);
6974
}
7075

7176
pub fn collect(&mut self, progress: &dyn Fn(String)) -> Result<BuildDataResult> {
7277
let mut res = BuildDataResult::default();
7378
for (path, config) in self.configs.iter() {
74-
res.per_workspace.insert(
75-
path.clone(),
76-
collect_from_workspace(
77-
&config.cargo_toml,
78-
&config.cargo_features,
79-
&config.packages,
80-
progress,
81-
)?,
82-
);
79+
let workspace_build_data = WorkspaceBuildData::collect(
80+
&config.cargo_toml,
81+
&config.cargo_features,
82+
&config.packages,
83+
self.wrap_rustc,
84+
progress,
85+
)?;
86+
res.per_workspace.insert(path.clone(), workspace_build_data);
8387
}
8488
Ok(res)
8589
}
@@ -120,119 +124,137 @@ impl BuildDataConfig {
120124
}
121125
}
122126

123-
fn collect_from_workspace(
124-
cargo_toml: &AbsPath,
125-
cargo_features: &CargoConfig,
126-
packages: &Vec<cargo_metadata::Package>,
127-
progress: &dyn Fn(String),
128-
) -> Result<WorkspaceBuildData> {
129-
let mut cmd = Command::new(toolchain::cargo());
130-
cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"])
131-
.arg(cargo_toml.as_ref());
132-
133-
// --all-targets includes tests, benches and examples in addition to the
134-
// default lib and bins. This is an independent concept from the --targets
135-
// flag below.
136-
cmd.arg("--all-targets");
137-
138-
if let Some(target) = &cargo_features.target {
139-
cmd.args(&["--target", target]);
140-
}
127+
impl WorkspaceBuildData {
128+
fn collect(
129+
cargo_toml: &AbsPath,
130+
cargo_features: &CargoConfig,
131+
packages: &Vec<cargo_metadata::Package>,
132+
wrap_rustc: bool,
133+
progress: &dyn Fn(String),
134+
) -> Result<WorkspaceBuildData> {
135+
let mut cmd = Command::new(toolchain::cargo());
136+
137+
if wrap_rustc {
138+
// Setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself. We use
139+
// that to compile only proc macros and build scripts during the initial
140+
// `cargo check`.
141+
let myself = std::env::current_exe()?;
142+
cmd.env("RUSTC_WRAPPER", myself);
143+
cmd.env("RA_RUSTC_WRAPPER", "1");
144+
}
145+
146+
cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"])
147+
.arg(cargo_toml.as_ref());
141148

142-
if cargo_features.all_features {
143-
cmd.arg("--all-features");
144-
} else {
145-
if cargo_features.no_default_features {
146-
// FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
147-
// https://github.com/oli-obk/cargo_metadata/issues/79
148-
cmd.arg("--no-default-features");
149+
// --all-targets includes tests, benches and examples in addition to the
150+
// default lib and bins. This is an independent concept from the --targets
151+
// flag below.
152+
cmd.arg("--all-targets");
153+
154+
if let Some(target) = &cargo_features.target {
155+
cmd.args(&["--target", target]);
149156
}
150-
if !cargo_features.features.is_empty() {
151-
cmd.arg("--features");
152-
cmd.arg(cargo_features.features.join(" "));
157+
158+
if cargo_features.all_features {
159+
cmd.arg("--all-features");
160+
} else {
161+
if cargo_features.no_default_features {
162+
// FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
163+
// https://github.com/oli-obk/cargo_metadata/issues/79
164+
cmd.arg("--no-default-features");
165+
}
166+
if !cargo_features.features.is_empty() {
167+
cmd.arg("--features");
168+
cmd.arg(cargo_features.features.join(" "));
169+
}
153170
}
154-
}
155171

156-
cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
157-
158-
let mut child = cmd.spawn().map(JodChild)?;
159-
let child_stdout = child.stdout.take().unwrap();
160-
let stdout = BufReader::new(child_stdout);
161-
162-
let mut res = WorkspaceBuildData::default();
163-
for message in cargo_metadata::Message::parse_stream(stdout).flatten() {
164-
match message {
165-
Message::BuildScriptExecuted(BuildScript {
166-
package_id, out_dir, cfgs, env, ..
167-
}) => {
168-
let cfgs = {
169-
let mut acc = Vec::new();
170-
for cfg in cfgs {
171-
match cfg.parse::<CfgFlag>() {
172-
Ok(it) => acc.push(it),
173-
Err(err) => {
174-
anyhow::bail!("invalid cfg from cargo-metadata: {}", err)
175-
}
176-
};
172+
cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
173+
174+
let mut child = cmd.spawn().map(JodChild)?;
175+
let child_stdout = child.stdout.take().unwrap();
176+
let stdout = BufReader::new(child_stdout);
177+
178+
let mut res = WorkspaceBuildData::default();
179+
for message in cargo_metadata::Message::parse_stream(stdout).flatten() {
180+
match message {
181+
Message::BuildScriptExecuted(BuildScript {
182+
package_id,
183+
out_dir,
184+
cfgs,
185+
env,
186+
..
187+
}) => {
188+
let cfgs = {
189+
let mut acc = Vec::new();
190+
for cfg in cfgs {
191+
match cfg.parse::<CfgFlag>() {
192+
Ok(it) => acc.push(it),
193+
Err(err) => {
194+
anyhow::bail!("invalid cfg from cargo-metadata: {}", err)
195+
}
196+
};
197+
}
198+
acc
199+
};
200+
let package_build_data =
201+
res.per_package.entry(package_id.repr.clone()).or_default();
202+
// cargo_metadata crate returns default (empty) path for
203+
// older cargos, which is not absolute, so work around that.
204+
if !out_dir.as_str().is_empty() {
205+
let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir.into_os_string()));
206+
package_build_data.out_dir = Some(out_dir);
207+
package_build_data.cfgs = cfgs;
177208
}
178-
acc
179-
};
180-
let package_build_data =
181-
res.per_package.entry(package_id.repr.clone()).or_default();
182-
// cargo_metadata crate returns default (empty) path for
183-
// older cargos, which is not absolute, so work around that.
184-
if !out_dir.as_str().is_empty() {
185-
let out_dir = AbsPathBuf::assert(PathBuf::from(out_dir.into_os_string()));
186-
package_build_data.out_dir = Some(out_dir);
187-
package_build_data.cfgs = cfgs;
188-
}
189209

190-
package_build_data.envs = env;
191-
}
192-
Message::CompilerArtifact(message) => {
193-
progress(format!("metadata {}", message.target.name));
194-
195-
if message.target.kind.contains(&"proc-macro".to_string()) {
196-
let package_id = message.package_id;
197-
// Skip rmeta file
198-
if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name)) {
199-
let filename = AbsPathBuf::assert(PathBuf::from(&filename));
200-
let package_build_data =
201-
res.per_package.entry(package_id.repr.clone()).or_default();
202-
package_build_data.proc_macro_dylib_path = Some(filename);
210+
package_build_data.envs = env;
211+
}
212+
Message::CompilerArtifact(message) => {
213+
progress(format!("metadata {}", message.target.name));
214+
215+
if message.target.kind.contains(&"proc-macro".to_string()) {
216+
let package_id = message.package_id;
217+
// Skip rmeta file
218+
if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name))
219+
{
220+
let filename = AbsPathBuf::assert(PathBuf::from(&filename));
221+
let package_build_data =
222+
res.per_package.entry(package_id.repr.clone()).or_default();
223+
package_build_data.proc_macro_dylib_path = Some(filename);
224+
}
203225
}
204226
}
227+
Message::CompilerMessage(message) => {
228+
progress(message.target.name.clone());
229+
}
230+
Message::BuildFinished(_) => {}
231+
Message::TextLine(_) => {}
232+
_ => {}
205233
}
206-
Message::CompilerMessage(message) => {
207-
progress(message.target.name.clone());
208-
}
209-
Message::BuildFinished(_) => {}
210-
Message::TextLine(_) => {}
211-
_ => {}
212234
}
213-
}
214235

215-
for package in packages {
216-
let package_build_data = res.per_package.entry(package.id.repr.clone()).or_default();
217-
inject_cargo_env(package, package_build_data);
218-
if let Some(out_dir) = &package_build_data.out_dir {
219-
// NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
220-
if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) {
221-
package_build_data.envs.push(("OUT_DIR".to_string(), out_dir));
236+
for package in packages {
237+
let package_build_data = res.per_package.entry(package.id.repr.clone()).or_default();
238+
inject_cargo_env(package, package_build_data);
239+
if let Some(out_dir) = &package_build_data.out_dir {
240+
// NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
241+
if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) {
242+
package_build_data.envs.push(("OUT_DIR".to_string(), out_dir));
243+
}
222244
}
223245
}
224-
}
225246

226-
let output = child.into_inner().wait_with_output()?;
227-
if !output.status.success() {
228-
let mut stderr = String::from_utf8(output.stderr).unwrap_or_default();
229-
if stderr.is_empty() {
230-
stderr = "cargo check failed".to_string();
247+
let output = child.into_inner().wait_with_output()?;
248+
if !output.status.success() {
249+
let mut stderr = String::from_utf8(output.stderr).unwrap_or_default();
250+
if stderr.is_empty() {
251+
stderr = "cargo check failed".to_string();
252+
}
253+
res.error = Some(stderr)
231254
}
232-
res.error = Some(stderr)
233-
}
234255

235-
Ok(res)
256+
Ok(res)
257+
}
236258
}
237259

238260
// FIXME: File a better way to know if it is a dylib

crates/rust-analyzer/src/benchmarks.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ fn benchmark_integrated_highlighting() {
3030
let file = "./crates/ide_db/src/apply_change.rs";
3131

3232
let cargo_config = Default::default();
33-
let load_cargo_config =
34-
LoadCargoConfig { load_out_dirs_from_check: true, with_proc_macro: false };
33+
let load_cargo_config = LoadCargoConfig {
34+
load_out_dirs_from_check: true,
35+
wrap_rustc: false,
36+
with_proc_macro: false,
37+
};
3538

3639
let (mut host, vfs, _proc_macro) = {
3740
let _it = stdx::timeit("workspace loading");

crates/rust-analyzer/src/bin/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Based on cli flags, either spawns an LSP server, or runs a batch analysis
44
mod flags;
55
mod logger;
6+
mod rustc_wrapper;
67

78
use std::{convert::TryFrom, env, fs, path::Path, process};
89

@@ -26,6 +27,20 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
2627
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
2728

2829
fn main() {
30+
if std::env::var("RA_RUSTC_WRAPPER").is_ok() {
31+
let mut args = std::env::args_os();
32+
let _me = args.next().unwrap();
33+
let rustc = args.next().unwrap();
34+
let code = match rustc_wrapper::run_rustc_skipping_cargo_checking(rustc, args.collect()) {
35+
Ok(rustc_wrapper::ExitCode(code)) => code.unwrap_or(102),
36+
Err(err) => {
37+
eprintln!("{}", err);
38+
101
39+
}
40+
};
41+
process::exit(code);
42+
}
43+
2944
if let Err(err) = try_main() {
3045
log::error!("Unexpected error: {}", err);
3146
eprintln!("{}", err);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//! We setup RUSTC_WRAPPER to point to `rust-analyzer` binary itself during the
2+
//! initial `cargo check`. That way, we avoid checking the actual project, and
3+
//! only build proc macros and build.rs.
4+
//!
5+
//! Code taken from IntelliJ :0)
6+
//! https://github.com/intellij-rust/intellij-rust/blob/master/native-helper/src/main.rs
7+
use std::{
8+
ffi::OsString,
9+
io,
10+
process::{Command, Stdio},
11+
};
12+
13+
/// ExitCode/ExitStatus are impossible to create :(.
14+
pub(crate) struct ExitCode(pub(crate) Option<i32>);
15+
16+
pub(crate) fn run_rustc_skipping_cargo_checking(
17+
rustc_executable: OsString,
18+
args: Vec<OsString>,
19+
) -> io::Result<ExitCode> {
20+
let is_cargo_check = args.iter().any(|arg| {
21+
let arg = arg.to_string_lossy();
22+
// `cargo check` invokes `rustc` with `--emit=metadata` argument.
23+
//
24+
// https://doc.rust-lang.org/rustc/command-line-arguments.html#--emit-specifies-the-types-of-output-files-to-generate
25+
// link — Generates the crates specified by --crate-type. The default
26+
// output filenames depend on the crate type and platform. This
27+
// is the default if --emit is not specified.
28+
// metadata — Generates a file containing metadata about the crate.
29+
// The default output filename is CRATE_NAME.rmeta.
30+
arg.starts_with("--emit=") && arg.contains("metadata") && !arg.contains("link")
31+
});
32+
if is_cargo_check {
33+
return Ok(ExitCode(Some(0)));
34+
}
35+
run_rustc(rustc_executable, args)
36+
}
37+
38+
fn run_rustc(rustc_executable: OsString, args: Vec<OsString>) -> io::Result<ExitCode> {
39+
let mut child = Command::new(rustc_executable)
40+
.args(args)
41+
.stdin(Stdio::inherit())
42+
.stdout(Stdio::inherit())
43+
.stderr(Stdio::inherit())
44+
.spawn()?;
45+
Ok(ExitCode(child.wait()?.code()))
46+
}

crates/rust-analyzer/src/cli/analysis_stats.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ impl AnalysisStatsCmd {
6868
cargo_config.no_sysroot = self.no_sysroot;
6969
let load_cargo_config = LoadCargoConfig {
7070
load_out_dirs_from_check: self.load_output_dirs,
71+
wrap_rustc: false,
7172
with_proc_macro: self.with_proc_macro,
7273
};
7374
let (host, vfs, _proc_macro) =

crates/rust-analyzer/src/cli/diagnostics.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ pub fn diagnostics(
3434
with_proc_macro: bool,
3535
) -> Result<()> {
3636
let cargo_config = Default::default();
37-
let load_cargo_config = LoadCargoConfig { load_out_dirs_from_check, with_proc_macro };
37+
let load_cargo_config =
38+
LoadCargoConfig { load_out_dirs_from_check, with_proc_macro, wrap_rustc: false };
3839
let (host, _vfs, _proc_macro) =
3940
load_workspace_at(path, &cargo_config, &load_cargo_config, &|_| {})?;
4041
let db = host.raw_database();

0 commit comments

Comments
 (0)