use std::path;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, OnceLock};
use anyhow::{Context, Result, bail, ensure};
use lsp_types::notification::ShowMessage;
use lsp_types::{MessageType, ShowMessageParams};
use scarb_metadata::{Metadata, MetadataCommand};
use tracing::{debug, error, warn};
use which::which;
use crate::env_config::{self, CAIRO_LS_LOG, scarb_cache_path};
use crate::lsp::ext::ScarbPathMissing;
use crate::server::client::Notifier;
pub const SCARB_TOML: &str = "Scarb.toml";
#[derive(Clone)]
pub struct ScarbToolchain {
scarb_path_cell: Arc<OnceLock<Option<PathBuf>>>,
version: Arc<OnceLock<Option<String>>>,
cache_path: Arc<OnceLock<Option<PathBuf>>>,
notifier: Notifier,
is_silent: bool,
}
impl ScarbToolchain {
pub fn new(notifier: Notifier) -> Self {
ScarbToolchain {
scarb_path_cell: Default::default(),
version: Default::default(),
cache_path: Default::default(),
notifier,
is_silent: false,
}
}
pub fn discover(&self) -> Option<&Path> {
self.scarb_path_cell
.get_or_init(|| {
if cfg!(feature = "testing") {
return Some(
which("scarb")
.expect("running tests requires a `scarb` binary available in `PATH`"),
);
}
let path = env_config::scarb_path();
if path.is_none() {
if self.is_silent {
warn!("attempt to use scarb without SCARB env being set");
} else {
error!("attempt to use scarb without SCARB env being set");
self.notifier.notify::<ScarbPathMissing>(());
}
}
path
})
.as_ref()
.map(PathBuf::as_path)
}
pub fn silent(&self) -> Self {
if self.is_silent {
self.clone()
} else {
Self {
scarb_path_cell: match self.scarb_path_cell.get() {
Some(_) => self.scarb_path_cell.clone(),
None => Default::default(),
},
version: self.version.clone(),
cache_path: self.cache_path.clone(),
notifier: self.notifier.clone(),
is_silent: true,
}
}
}
#[tracing::instrument(skip(self))]
pub fn metadata(&self, manifest: &Path) -> Result<Metadata> {
let Some(scarb_path) = self.discover() else {
bail!("could not find scarb executable");
};
let result = MetadataCommand::new()
.scarb_path(scarb_path)
.manifest_path(manifest)
.inherit_stderr()
.exec()
.context("failed to execute: scarb metadata");
if !self.is_silent && result.is_err() {
self.notifier.notify::<ShowMessage>(ShowMessageParams {
typ: MessageType::ERROR,
message: "`scarb metadata` failed. Check if your project builds correctly via \
`scarb build`."
.to_string(),
});
}
result
}
pub fn proc_macro_server(&self, cwd: &Path) -> Result<Child> {
let Some(scarb_path) = self.discover() else { bail!("failed to get scarb path") };
let proc_macro_server = Command::new(scarb_path)
.current_dir(cwd)
.arg("--quiet") .arg("proc-macro-server")
.envs(std::env::var("RUST_BACKTRACE").map(|value| ("RUST_BACKTRACE", value)))
.envs(std::env::var(CAIRO_LS_LOG).map(|value| ("SCARB_LOG", value)))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()?;
Ok(proc_macro_server)
}
pub fn version(&self) -> Option<String> {
self.version
.get_or_init(|| self.fetch_version().inspect_err(|err| error!("{err:#?}")).ok())
.clone()
}
pub fn cache_path(&self) -> Option<PathBuf> {
self.cache_path.get_or_init(|| self.fetch_cache_path().ok()).clone()
}
pub fn is_from_scarb_cache(&self, file_path: &Path) -> bool {
self.cache_path().is_some_and(|cache_path| file_path.starts_with(cache_path))
}
fn fetch_version(&self) -> Result<String> {
let Some(scarb_path) = self.discover() else { bail!("failed to get scarb path") };
let output = Command::new(scarb_path).arg("--version").output()?;
ensure!(output.status.success(), "failed to get scarb version");
let version = String::from_utf8_lossy(&output.stdout).to_string();
Ok(version)
}
fn fetch_cache_path(&self) -> Result<PathBuf> {
if let Some(scarb_cache_path) = scarb_cache_path() {
return Ok(scarb_cache_path);
}
let Some(scarb_path) = self.discover() else { bail!("failed to get scarb path") };
let output = Command::new(scarb_path).arg("cache").arg("path").output()?;
ensure!(output.status.success(), "failed to get scarb cache path");
let cache_path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim().to_string());
path::absolute(&cache_path)
.with_context(|| {
format!("failed to make scarb cache path absolute: {}", cache_path.display())
})
.inspect(|p| debug!("Scarb cache path: {}", p.display()))
.inspect_err(|err| error!("{err:#?}"))
}
}