repolith_actions/
cargo_install.rs1use crate::paths::expand_tilde;
9use crate::util::{check_status, run_with_cancel};
10use async_trait::async_trait;
11use repolith_core::action::Action;
12use repolith_core::types::{ActionId, BuildError, BuildOutput, Ctx, Sha256};
13use sha2::{Digest, Sha256 as ShaHasher};
14use std::path::PathBuf;
15use tokio::process::Command;
16
17#[derive(Clone, Debug)]
19pub enum CargoSource {
20 Git {
22 url: String,
24 branch: Option<String>,
26 },
27 Path {
29 path: PathBuf,
31 },
32}
33
34pub struct CargoInstall {
39 pub id: ActionId,
41 pub source: CargoSource,
43 pub crate_name: Option<String>,
46 pub features: Vec<String>,
48 pub install_to: PathBuf,
50 pub deps: Vec<ActionId>,
52}
53
54impl CargoInstall {
55 fn resolved_crate_name(&self) -> String {
56 if let Some(name) = &self.crate_name {
57 name.clone()
58 } else {
59 self.id
60 .0
61 .rsplit("::")
62 .next()
63 .unwrap_or(&self.id.0)
64 .to_string()
65 }
66 }
67}
68
69#[async_trait]
70impl Action for CargoInstall {
71 fn id(&self) -> ActionId {
72 self.id.clone()
73 }
74
75 fn deps(&self) -> Vec<ActionId> {
76 self.deps.clone()
77 }
78
79 async fn input_hash(&self, ctx: &Ctx) -> Result<Sha256, BuildError> {
80 let mut h = ShaHasher::new();
81 match &self.source {
82 CargoSource::Git { url, branch } => {
83 h.update(b"git:");
84 h.update(url.as_bytes());
85 h.update(b":");
86 h.update(branch.as_deref().unwrap_or("").as_bytes());
87 }
88 CargoSource::Path { path } => {
89 h.update(b"path:");
90 h.update(path.to_string_lossy().as_bytes());
91 }
92 }
93 for f in &self.features {
94 h.update(b":feat:");
95 h.update(f.as_bytes());
96 }
97 h.update(b":crate:");
98 h.update(self.resolved_crate_name().as_bytes());
99
100 let mut cmd = Command::new("cargo");
102 cmd.arg("--version");
103 let v = run_with_cancel(cmd, &ctx.cancel).await?;
104 if !v.status.success() {
105 return Err(BuildError::UpstreamUnreachable(
106 "cargo --version failed".to_string(),
107 ));
108 }
109 h.update(b":cargo:");
110 h.update(&v.stdout);
111 Ok(Sha256(h.finalize().into()))
112 }
113
114 async fn execute(&self, ctx: &Ctx) -> Result<BuildOutput, BuildError> {
115 let install_root = expand_tilde(&self.install_to);
116 let install_to = install_root.to_str().ok_or_else(|| {
117 BuildError::Io(format!("non-utf8 install_to: {}", install_root.display()))
118 })?;
119
120 let mut cmd = Command::new("cargo");
121 cmd.arg("install");
122 match &self.source {
123 CargoSource::Git { url, branch } => {
124 cmd.args(["--git", url]);
125 if let Some(b) = branch {
126 cmd.args(["--branch", b]);
127 }
128 }
129 CargoSource::Path { path } => {
130 let p = path.to_str().ok_or_else(|| {
131 BuildError::Io(format!("non-utf8 source path: {}", path.display()))
132 })?;
133 cmd.args(["--path", p]);
134 }
135 }
136 let crate_name = self.resolved_crate_name();
142 cmd.args(["--bin", &crate_name]);
143 if !self.features.is_empty() {
144 cmd.args(["--features", &self.features.join(",")]);
145 }
146 cmd.args(["--locked", "--force", "--root", install_to]);
147
148 check_status(&run_with_cancel(cmd, &ctx.cancel).await?)?;
149
150 let bin_name = format!("{crate_name}{}", std::env::consts::EXE_SUFFIX);
160 let bin = install_root.join("bin").join(&bin_name);
161 let bytes = tokio::fs::read(&bin)
162 .await
163 .map_err(|e| BuildError::Io(format!("read installed binary {}: {e}", bin.display())))?;
164 let mut h = ShaHasher::new();
165 h.update(&bytes);
166 Ok(BuildOutput {
167 output_hash: Sha256(h.finalize().into()),
168 stdout: format!("installed -> {}", bin.display()),
169 })
170 }
171}