1#![deny(clippy::unwrap_used)]
10#![forbid(unsafe_code)]
11
12use anyhow::{anyhow, bail, ensure, Result};
13use cargo_metadata::{camino::Utf8PathBuf, MetadataCommand, Package};
14use serde::{Deserialize, Serialize};
15use std::{
16 env::var,
17 hash::{Hash, Hasher},
18 path::PathBuf,
19 process::{Command, Stdio},
20 str::FromStr,
21};
22use typed_builder::TypedBuilder;
23
24#[derive(Clone, Debug, Copy)]
25pub enum CrateType {
27 Executable,
28 CDynamicLibrary,
29 Dylib,
30 StaticLibrary,
31 RustLibrary,
32 ProcMacro,
33 }
36
37#[derive(Clone, Debug)]
38pub enum Profile {
41 Release,
42 Dev,
43 Other(String),
44}
45
46impl FromStr for Profile {
47 type Err = anyhow::Error;
48
49 fn from_str(s: &str) -> Result<Self> {
50 match s {
51 "release" => Ok(Self::Release),
52 "dev" => Ok(Self::Dev),
53 "debug" => Ok(Self::Dev),
54 _ => Ok(Self::Other(s.to_string())),
55 }
56 }
57}
58
59#[derive(TypedBuilder, Clone, Debug)]
60pub struct ArtifactDependency {
62 #[builder(setter(into, strip_option), default)]
63 pub workspace_root: Option<PathBuf>,
66 #[builder(setter(into, strip_option), default)]
70 pub crate_name: Option<String>,
71 pub artifact_type: CrateType,
73 #[builder(setter(into))]
74 pub profile: Profile,
76 pub build_missing: bool,
78 #[builder(default = true)]
79 pub build_always: bool,
83 #[builder(setter(into), default)]
84 pub features: Vec<String>,
85 #[builder(setter(into, strip_option), default)]
86 pub target_name: Option<String>,
87 #[builder(setter(into), default)]
88 pub capture_output: bool,
89 #[builder(setter(into, strip_option), default)]
90 pub env: Option<Vec<(String, String)>>,
91}
92
93#[cfg(target_family = "unix")]
109pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("lib", ".so", "lib", ".a", "");
111#[cfg(target_family = "darwin")]
112pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("lib", ".dylib", "lib", ".a", "");
114#[cfg(all(target_os = "windows", target_env = "msvc"))]
115pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("", ".dll", "", ".lib", ".exe");
117#[cfg(all(target_os = "windows", any(target_env = "gnu", target_env = "gnullvm")))]
118pub const ARTIFACT_NAMEPARTS: (&str, &str, &str, &str, &str) = ("", ".dll", "lib", ".a", ".exe");
120
121#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
122pub struct Artifact {
124 pub path: PathBuf,
127 pub package: Package,
129}
130
131impl Hash for Artifact {
132 fn hash<H>(&self, state: &mut H)
133 where
134 H: Hasher,
135 {
136 self.path.hash(state);
137 self.package.name.hash(state);
138 self.package.version.hash(state);
139 self.package.authors.hash(state);
140 self.package.id.hash(state);
141 self.package.description.hash(state);
142 self.package.license.hash(state);
143 self.package.license_file.hash(state);
144 self.package.targets.hash(state);
145 self.package.manifest_path.hash(state);
146 self.package.categories.hash(state);
147 self.package.keywords.hash(state);
148 self.package.readme.hash(state);
149 self.package.repository.hash(state);
150 self.package.homepage.hash(state);
151 self.package.documentation.hash(state);
152 self.package.edition.hash(state);
153 self.package.links.hash(state);
154 self.package.publish.hash(state);
155 self.package.default_run.hash(state);
156 self.package.rust_version.hash(state);
157 }
158}
159
160impl Artifact {
161 fn new(path: PathBuf, package: Package) -> Self {
163 Self { path, package }
164 }
165}
166
167impl ArtifactDependency {
168 pub fn build(&mut self) -> Result<Artifact> {
170 let workspace_root = if let Some(workspace_root) = self.workspace_root.clone() {
171 workspace_root
172 } else {
173 MetadataCommand::new()
174 .no_deps()
175 .exec()?
176 .workspace_root
177 .into()
178 };
179
180 let metadata = MetadataCommand::new()
181 .current_dir(&workspace_root)
182 .no_deps()
183 .manifest_path(workspace_root.join("Cargo.toml"))
184 .exec()?;
185
186 self.crate_name = if let Some(crate_name) = self.crate_name.as_ref() {
187 Some(crate_name.clone())
188 } else if let Ok(crate_name) = var("CARGO_PKG_NAME") {
189 Some(crate_name)
190 } else if let Some(root_package) = metadata.root_package() {
191 Some(root_package.name.clone())
192 } else {
193 bail!("No name provided and no root package in provided workspace at {}, could not determine crate name.", workspace_root.display());
194 };
195
196 let crate_name = self
197 .crate_name
198 .as_ref()
199 .cloned()
200 .ok_or_else(|| anyhow!("self.crate_name must have a value at this point"))?;
201
202 let package = metadata
203 .packages
204 .iter()
205 .find(|p| p.name == crate_name)
206 .ok_or_else(|| {
207 anyhow!(
208 "No package matching name {} found in packages {:?} workspace at {}",
209 crate_name,
210 metadata
211 .packages
212 .iter()
213 .map(|p| p.name.clone())
214 .collect::<Vec<_>>(),
215 workspace_root.display()
216 )
217 })?;
218
219 let package_name = package.name.clone();
220 let package_result_name = package_name.replace('-', "_");
221
222 let (dll_prefix, dll_suffix, staticlib_prefix, staticlib_suffix, exe_suffix) =
223 ARTIFACT_NAMEPARTS;
224
225 let profile = self.profile.clone();
226
227 let profile_target_path = metadata.target_directory.join(match &profile {
228 Profile::Release => "release".to_string(),
229 Profile::Dev => "debug".to_string(),
230 Profile::Other(o) => o.clone(),
231 });
232
233 let artifact_path = match self.artifact_type {
234 CrateType::Executable => {
235 profile_target_path.join(format!("{}{}", &package_result_name, exe_suffix))
236 }
237 CrateType::CDynamicLibrary => profile_target_path.join(format!(
238 "{}{}{}",
239 dll_prefix, &package_result_name, dll_suffix
240 )),
241 CrateType::StaticLibrary => profile_target_path.join(format!(
242 "{}{}{}",
243 staticlib_prefix, package_result_name, staticlib_suffix
244 )),
245 _ => bail!(
246 "Crate type {:?} is not supported as an artifact dependency source",
247 self.artifact_type
248 ),
249 };
250
251 let artifact_path = if (self.build_missing && !artifact_path.exists()) || self.build_always
252 {
253 let cargo = var("CARGO").unwrap_or("cargo".to_string());
254 let mut cargo_command = Command::new(cargo);
255 cargo_command
256 .arg("build")
257 .arg("--manifest-path")
258 .arg(workspace_root.join("Cargo.toml"))
259 .arg("--package")
260 .arg(&package_name);
261
262 let build_target_dir = if let Some(target_name) = self.target_name.as_ref() {
266 metadata.target_directory.join(target_name)
267 } else {
268 metadata.target_directory
269 };
270
271 cargo_command.arg("--target-dir").arg(&build_target_dir);
272
273 match &profile {
274 Profile::Release => {
275 cargo_command.arg("--release");
276 }
277 Profile::Other(o) => {
278 cargo_command.args(vec!["--profile".to_string(), o.clone()]);
279 }
280 _ => {}
281 }
282
283 cargo_command.arg(format!("--features={}", self.features.join(",")));
284
285 if let Some(env) = self.env.as_ref() {
286 cargo_command.envs(env.iter().cloned());
287 }
288
289 if self.capture_output {
290 let output = cargo_command
291 .stderr(Stdio::piped())
292 .stdout(Stdio::piped())
293 .output()?;
294
295 if !output.status.success() {
296 bail!(
297 "Failed to build artifact crate:\nstdout: {}\nstderr: {}",
298 String::from_utf8_lossy(&output.stdout),
299 String::from_utf8_lossy(&output.stderr)
300 );
301 }
302 } else {
303 let status = cargo_command.status()?;
304
305 if !status.success() {
306 bail!("Failed to build artifact crate");
307 }
308 }
309
310 let artifact_path: PathBuf = build_target_dir
311 .join({
312 let components = artifact_path
313 .components()
314 .rev()
315 .take(2)
316 .map(|c| c.to_string())
317 .collect::<Vec<_>>();
318 components.iter().rev().collect::<Utf8PathBuf>()
319 })
320 .into();
321
322 ensure!(
323 artifact_path.exists(),
324 "Artifact build succeeded, but artifact not found in {}",
325 artifact_path.display()
326 );
327
328 artifact_path
329 } else {
330 artifact_path.into()
331 };
332
333 Ok(Artifact::new(artifact_path, package.clone()))
334 }
335}