use crate::paths::expand_tilde;
use crate::util::{check_status, run_with_cancel};
use async_trait::async_trait;
use repolith_core::action::Action;
use repolith_core::types::{ActionId, BuildError, BuildOutput, Ctx, Sha256};
use sha2::{Digest, Sha256 as ShaHasher};
use std::path::PathBuf;
use tokio::process::Command;
#[derive(Clone, Debug)]
pub enum CargoSource {
Git {
url: String,
branch: Option<String>,
},
Path {
path: PathBuf,
},
}
pub struct CargoInstall {
pub id: ActionId,
pub source: CargoSource,
pub crate_name: Option<String>,
pub features: Vec<String>,
pub install_to: PathBuf,
pub deps: Vec<ActionId>,
}
impl CargoInstall {
fn resolved_crate_name(&self) -> String {
if let Some(name) = &self.crate_name {
name.clone()
} else {
self.id
.0
.rsplit("::")
.next()
.unwrap_or(&self.id.0)
.to_string()
}
}
}
#[async_trait]
impl Action for CargoInstall {
fn id(&self) -> ActionId {
self.id.clone()
}
fn deps(&self) -> Vec<ActionId> {
self.deps.clone()
}
async fn input_hash(&self, ctx: &Ctx) -> Result<Sha256, BuildError> {
let mut h = ShaHasher::new();
match &self.source {
CargoSource::Git { url, branch } => {
h.update(b"git:");
h.update(url.as_bytes());
h.update(b":");
h.update(branch.as_deref().unwrap_or("").as_bytes());
}
CargoSource::Path { path } => {
h.update(b"path:");
h.update(path.to_string_lossy().as_bytes());
}
}
for f in &self.features {
h.update(b":feat:");
h.update(f.as_bytes());
}
h.update(b":crate:");
h.update(self.resolved_crate_name().as_bytes());
let mut cmd = Command::new("cargo");
cmd.arg("--version");
let v = run_with_cancel(cmd, &ctx.cancel).await?;
if !v.status.success() {
return Err(BuildError::UpstreamUnreachable(
"cargo --version failed".to_string(),
));
}
h.update(b":cargo:");
h.update(&v.stdout);
Ok(Sha256(h.finalize().into()))
}
async fn execute(&self, ctx: &Ctx) -> Result<BuildOutput, BuildError> {
let install_root = expand_tilde(&self.install_to);
let install_to = install_root.to_str().ok_or_else(|| {
BuildError::Io(format!("non-utf8 install_to: {}", install_root.display()))
})?;
let mut cmd = Command::new("cargo");
cmd.arg("install");
match &self.source {
CargoSource::Git { url, branch } => {
cmd.args(["--git", url]);
if let Some(b) = branch {
cmd.args(["--branch", b]);
}
}
CargoSource::Path { path } => {
let p = path.to_str().ok_or_else(|| {
BuildError::Io(format!("non-utf8 source path: {}", path.display()))
})?;
cmd.args(["--path", p]);
}
}
let crate_name = self.resolved_crate_name();
cmd.args(["--bin", &crate_name]);
if !self.features.is_empty() {
cmd.args(["--features", &self.features.join(",")]);
}
cmd.args(["--locked", "--force", "--root", install_to]);
check_status(&run_with_cancel(cmd, &ctx.cancel).await?)?;
let bin_name = format!("{crate_name}{}", std::env::consts::EXE_SUFFIX);
let bin = install_root.join("bin").join(&bin_name);
let bytes = tokio::fs::read(&bin)
.await
.map_err(|e| BuildError::Io(format!("read installed binary {}: {e}", bin.display())))?;
let mut h = ShaHasher::new();
h.update(&bytes);
Ok(BuildOutput {
output_hash: Sha256(h.finalize().into()),
stdout: format!("installed -> {}", bin.display()),
})
}
}