blob: 6d335cb536b1c70fe1ea0480975b6bf75427271c [file]
//! Exec-redirect shim that bridges a stable workspace-relative path
//! in the committed settings file (`${workspaceFolder}/.rules_rust_analyzer/<name>.exe`)
//! to the user-specific Bazel-cache path of the real toolchain binary.
//! `setup vscode` copies this binary into the launcher dir once per
//! logical name and writes the sidecar JSON it dispatches against.
//!
//! `.exe` everywhere is required by Node's `child_process.spawn` on
//! Windows (no `shell: true`, no PATHEXT lookup) and harmless on POSIX
//! (kernel ignores file extensions for `execve`). See
//! [`gen_rust_project_lib::DISCOVER_BINARY_FILENAME`] for the same
//! decision on the discover/flycheck side.
use std::{
collections::HashMap,
env, fs,
path::{Path, PathBuf},
process,
};
const PATHS_FILENAME: &str = "launcher_paths.json";
fn main() {
let exe = env::current_exe().unwrap_or_else(|e| die(format!("locating current_exe: {e}")));
let logical = logical_name(&exe)
.unwrap_or_else(|| die(format!("current_exe has no file stem: {}", exe.display())));
let paths_path = exe
.parent()
.unwrap_or_else(|| die(format!("current_exe has no parent: {}", exe.display())))
.join(PATHS_FILENAME);
let target = resolve_target(&paths_path, &logical)
.unwrap_or_else(|e| die(format!("resolving {logical}: {e}")));
exec_replace(&target);
}
fn logical_name(exe: &Path) -> Option<String> {
exe.file_stem().map(|s| s.to_string_lossy().into_owned())
}
/// Borrows strings out of `raw` to skip per-entry heap allocations on
/// the LSP-startup hot path.
fn resolve_target(paths_path: &Path, logical: &str) -> Result<PathBuf, String> {
let raw = fs::read_to_string(paths_path)
.map_err(|e| format!("reading {}: {e}", paths_path.display()))?;
let map: HashMap<&str, &str> =
serde_json::from_str(&raw).map_err(|e| format!("parsing {}: {e}", paths_path.display()))?;
map.get(logical)
.map(PathBuf::from)
.ok_or_else(|| format!("no entry for `{logical}` in {}", paths_path.display()))
}
#[cfg(unix)]
fn exec_replace(target: &Path) -> ! {
use std::os::unix::process::CommandExt;
let err = process::Command::new(target)
.args(env::args_os().skip(1))
.exec();
die(format!("exec {}: {err}", target.display()))
}
#[cfg(not(unix))]
fn exec_replace(target: &Path) -> ! {
let status = process::Command::new(target)
.args(env::args_os().skip(1))
.status()
.unwrap_or_else(|e| die(format!("spawn {}: {e}", target.display())));
process::exit(status.code().unwrap_or(1));
}
fn die(msg: String) -> ! {
eprintln!("rules_rust launcher: {msg}");
process::exit(127);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn logical_name_strips_exe_suffix() {
assert_eq!(
logical_name(Path::new("/x/rust_analyzer.exe")).as_deref(),
Some("rust_analyzer")
);
}
#[test]
fn logical_name_passes_through_when_no_suffix() {
assert_eq!(
logical_name(Path::new("/x/rustfmt")).as_deref(),
Some("rustfmt")
);
}
#[test]
fn resolve_target_returns_mapped_path() {
let tmp = std::env::temp_dir().join(format!("launcher_resolve_ok_{}", process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let paths = tmp.join(PATHS_FILENAME);
fs::write(
&paths,
r#"{"rust_analyzer": "/abs/rust-analyzer", "rustfmt": "/abs/rustfmt"}"#,
)
.unwrap();
let out = resolve_target(&paths, "rust_analyzer").unwrap();
assert_eq!(out, PathBuf::from("/abs/rust-analyzer"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn resolve_target_errors_on_missing_key_with_path_context() {
let tmp = std::env::temp_dir().join(format!("launcher_resolve_miss_{}", process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let paths = tmp.join(PATHS_FILENAME);
fs::write(&paths, r#"{"rust_analyzer": "/abs/rust-analyzer"}"#).unwrap();
let err = resolve_target(&paths, "rustfmt").unwrap_err();
assert!(err.contains("rustfmt"), "{err}");
assert!(err.contains(PATHS_FILENAME), "{err}");
let _ = fs::remove_dir_all(&tmp);
}
}