Skip to content

Commit 15569c1

Browse files
committed
feat(toolchain): consider external rust-analyzer when calling a proxy
1 parent eed6a15 commit 15569c1

File tree

3 files changed

+108
-0
lines changed

3 files changed

+108
-0
lines changed

src/test/mock_bin_src.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ fn main() {
102102
panic!("CARGO environment variable not set");
103103
}
104104
}
105+
Some("--echo-current-exe") => {
106+
let mut out = io::stderr();
107+
writeln!(out, "{}", std::env::current_exe().unwrap().display()).unwrap();
108+
}
105109
arg => panic!("bad mock proxy commandline: {:?}", arg),
106110
}
107111
}

src/toolchain.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::{
1212

1313
use anyhow::{Context, anyhow, bail};
1414
use fs_at::OpenOptions;
15+
use same_file::is_same_file;
1516
use tracing::info;
1617
use url::Url;
1718
use wait_timeout::ChildExt;
@@ -331,6 +332,8 @@ impl<'a> Toolchain<'a> {
331332
// perhaps a trait that create command layers on?
332333
if let Some(cmd) = self.maybe_do_cargo_fallback(binary)? {
333334
return Ok(cmd);
335+
} else if let Some(cmd) = self.maybe_do_rust_analyzer_fallback(binary)? {
336+
return Ok(cmd);
334337
}
335338

336339
self.create_command(binary)
@@ -371,6 +374,37 @@ impl<'a> Toolchain<'a> {
371374
Ok(None)
372375
}
373376

377+
fn maybe_do_rust_analyzer_fallback(&self, binary: &str) -> anyhow::Result<Option<Command>> {
378+
if binary != "rust-analyzer" && binary != "rust-analyzer.exe"
379+
|| self.binary_file("rust-analyzer").exists()
380+
{
381+
return Ok(None);
382+
}
383+
384+
let proc = self.cfg.process;
385+
let Some(path) = proc.var_os("PATH") else {
386+
return Ok(None);
387+
};
388+
389+
let me = env::current_exe()?;
390+
391+
// Try to find the first `rust-analyzer` under the `$PATH` that is both
392+
// an existing file and not the same file as `me`, i.e. not a rustup proxy.
393+
Ok(env::split_paths(&path).find_map(|mut p| {
394+
p.push(binary);
395+
let is_external_ra = p.is_file()
396+
// We report `true` on `is_same_file()` error to prevent an invalid `p`
397+
// from becoming the candidate.
398+
&& !is_same_file(&me, &p).unwrap_or(true);
399+
if !is_external_ra {
400+
return None;
401+
}
402+
let mut ra = Command::new(p);
403+
self.set_env(&mut ra);
404+
Some(ra)
405+
}))
406+
}
407+
374408
#[cfg_attr(feature="otel", tracing::instrument(err, fields(binary, recursion = self.cfg.process.var("RUST_RECURSION_COUNT").ok())))]
375409
fn create_command<T: AsRef<OsStr> + Debug>(&self, binary: T) -> Result<Command, anyhow::Error> {
376410
// Create the path to this binary within the current toolchain sysroot

tests/suite/cli_misc.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,3 +1277,73 @@ async fn rustup_updates_cargo_env_if_proxy() {
12771277
)
12781278
.await;
12791279
}
1280+
1281+
#[tokio::test]
1282+
async fn rust_analyzer_proxy_falls_back_external() {
1283+
let mut cx = CliTestContext::new(Scenario::SimpleV2).await;
1284+
cx.config
1285+
.expect_ok(&[
1286+
"rustup",
1287+
"toolchain",
1288+
"install",
1289+
"stable",
1290+
"--profile=minimal",
1291+
"--component=rls",
1292+
])
1293+
.await;
1294+
cx.config.expect_ok(&["rustup", "default", "stable"]).await;
1295+
1296+
// We pretend to have a `rust-analyzer` installation by reusing the `rls`
1297+
// proxy and mock binary.
1298+
let rls = format!("rls{EXE_SUFFIX}");
1299+
let ra = format!("rust-analyzer{EXE_SUFFIX}");
1300+
let exedir = &cx.config.exedir;
1301+
let bindir = &cx
1302+
.config
1303+
.rustupdir
1304+
.join("toolchains")
1305+
.join(for_host!("stable-{0}"))
1306+
.join("bin");
1307+
for dir in [exedir, bindir] {
1308+
fs::rename(dir.join(&rls), dir.join(&ra)).unwrap();
1309+
}
1310+
1311+
// Base case: rustup-hosted RA installed, external RA unavailable,
1312+
// use the former.
1313+
let real_path = cx
1314+
.config
1315+
.run("rust-analyzer", &["--echo-current-exe"], &[])
1316+
.await;
1317+
assert!(real_path.ok);
1318+
let real_path_str = real_path.stderr.lines().next().unwrap();
1319+
let real_path = Path::new(real_path_str);
1320+
1321+
assert!(real_path.is_file());
1322+
1323+
let tempdir = tempfile::Builder::new().prefix("rustup").tempdir().unwrap();
1324+
let extern_dir = tempdir.path();
1325+
let extern_path = &extern_dir.join("rust-analyzer");
1326+
fs::copy(real_path, extern_path).unwrap();
1327+
1328+
// First case: rustup-hosted and external RA both installed,
1329+
// prioritize the former.
1330+
cx.config
1331+
.expect_ok_ex_env(
1332+
&["rust-analyzer", "--echo-current-exe"],
1333+
&[("PATH", &extern_dir.to_string_lossy())],
1334+
"",
1335+
&format!("{real_path_str}\n"),
1336+
)
1337+
.await;
1338+
1339+
// Second case: rustup-hosted RA unavailable, fallback on the external RA.
1340+
fs::remove_file(bindir.join(&ra)).unwrap();
1341+
cx.config
1342+
.expect_ok_ex_env(
1343+
&["rust-analyzer", "--echo-current-exe"],
1344+
&[("PATH", &extern_dir.to_string_lossy())],
1345+
"",
1346+
&format!("{}\n", extern_path.display()),
1347+
)
1348+
.await;
1349+
}

0 commit comments

Comments
 (0)