Skip to content

Commit c15bb35

Browse files
rami3ldjc
authored andcommitted
feat(toolchain): consider external rust-analyzer when calling a proxy
1 parent f64bff3 commit c15bb35

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(unix)]
2+
use std::os::unix::fs::PermissionsExt as _;
13
use std::{
24
env::{self, consts::EXE_SUFFIX},
35
ffi::{OsStr, OsString},
@@ -12,6 +14,7 @@ use std::{
1214

1315
use anyhow::{Context, anyhow, bail};
1416
use fs_at::OpenOptions;
17+
use same_file::is_same_file;
1518
use tracing::info;
1619
use url::Url;
1720
use wait_timeout::ChildExt;
@@ -331,6 +334,8 @@ impl<'a> Toolchain<'a> {
331334
// perhaps a trait that create command layers on?
332335
if let Some(cmd) = self.maybe_do_cargo_fallback(binary)? {
333336
return Ok(cmd);
337+
} else if let Some(cmd) = self.maybe_do_rust_analyzer_fallback(binary)? {
338+
return Ok(cmd);
334339
}
335340

336341
self.create_command(binary)
@@ -371,6 +376,52 @@ impl<'a> Toolchain<'a> {
371376
Ok(None)
372377
}
373378

379+
/// Tries to find `rust-analyzer` on the PATH when the active toolchain does
380+
/// not have `rust-analyzer` installed.
381+
///
382+
/// This happens from time to time often because the user wants to use a
383+
/// more recent build of RA than the one shipped with rustup, or because
384+
/// rustup isn't shipping RA on their host platform at all.
385+
///
386+
/// See the following issues for more context:
387+
/// - <https://github.com/rust-lang/rustup/issues/3299>
388+
/// - <https://github.com/rust-lang/rustup/issues/3846>
389+
fn maybe_do_rust_analyzer_fallback(&self, binary: &str) -> anyhow::Result<Option<Command>> {
390+
if binary != "rust-analyzer" && binary != "rust-analyzer.exe"
391+
|| self.binary_file("rust-analyzer").exists()
392+
{
393+
return Ok(None);
394+
}
395+
396+
let proc = self.cfg.process;
397+
let Some(path) = proc.var_os("PATH") else {
398+
return Ok(None);
399+
};
400+
401+
let me = env::current_exe()?;
402+
403+
// Try to find the first `rust-analyzer` under the `$PATH` that is both
404+
// an existing file and not the same file as `me`, i.e. not a rustup proxy.
405+
for mut p in env::split_paths(&path) {
406+
p.push(binary);
407+
let is_external_ra = p.is_file()
408+
// We report `true` on `is_same_file()` error to prevent an invalid `p`
409+
// from becoming the candidate.
410+
&& !is_same_file(&me, &p).unwrap_or(true);
411+
// On Unix, we additionally check if the file is executable.
412+
#[cfg(unix)]
413+
let is_external_ra = is_external_ra
414+
&& p.metadata()
415+
.is_ok_and(|meta| meta.permissions().mode() & 0o111 != 0);
416+
if is_external_ra {
417+
let mut ra = Command::new(p);
418+
self.set_env(&mut ra);
419+
return Ok(Some(ra));
420+
}
421+
}
422+
Ok(None)
423+
}
424+
374425
#[cfg_attr(feature="otel", tracing::instrument(err, fields(binary, recursion = self.cfg.process.var("RUST_RECURSION_COUNT").ok())))]
375426
fn create_command<T: AsRef<OsStr> + Debug>(&self, binary: T) -> Result<Command, anyhow::Error> {
376427
// 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)