Skip to main content

cargo_artifact_dependency/
lib.rs

1//! Stable crate alternative for [cargo artifact dependency](https://doc.rust-lang.org/cargo/reference/unstable.html#artifact-dependencies).
2//!
3//! Warning:
4//! This crate currently only supports binary artifacts. If you need other
5//! artifact types, please open an [issue on github](https://github.com/RoxyOS/cargo-artifact-dependency/issues).
6//!
7//! # Why
8//!
9//! Cargo artifact dependencies are still an unstable Cargo feature and may
10//! still have bugs. This crate exists as a temporary alternative while artifact
11//! dependency support remains unstable.
12//!
13//! Use [`ArtifactDependencyBuilder`] to describe a dependency and call
14//! [`ArtifactDependency::resolve`] to resolve its artifact path.
15//!
16//! # Notes
17//!
18//! This crate does not emulate Cargo.toml's full `version + path` dependency
19//! resolution semantics. If you need a local development path, you can gate
20//! `.path(...)` behind a feature.
21//!
22//! # Example
23//!
24//! ```no_run
25//! use cargo_artifact_dependency::{ArtifactDependencyBuilder, BuildProfile};
26//! // Describe the ripgrep dependency and resolve its artifact.
27//! let artifact_path = ArtifactDependencyBuilder::default()
28//!     .crate_name("ripgrep")
29//!     .version("^14")
30//!     .bin_name("rg")
31//!     .profile(BuildProfile::Release)
32//!     .build()
33//!     .unwrap()
34//!     .resolve()?;
35//!
36//! // Use the resolved artifact path in your own workflow.
37//! println!("{}", artifact_path.display());
38//! # Ok::<(), cargo_artifact_dependency::Error>(())
39//! ```
40
41mod error;
42mod install_root;
43mod utils;
44
45#[cfg(test)]
46mod network_tests;
47#[cfg(test)]
48mod tests;
49
50use std::{
51    io,
52    path::{Path, PathBuf},
53};
54
55use apply_if::ApplyIf;
56use cargo_install::CargoInstallBuilder;
57use derive_builder::Builder;
58
59pub use crate::error::{Error, Result};
60use crate::utils::{cargo_install_version_req, executable_name, files_in_dir};
61
62#[derive(Clone, Debug, PartialEq, Default, Eq)]
63pub enum BuildProfile {
64    Debug,
65    #[default]
66    Release,
67    Custom(String),
68}
69
70/// Describes an artifact dependency.
71///
72/// Use [`ArtifactDependencyBuilder`] to construct values. `crate_name` is
73/// required; all other fields are optional.
74#[derive(Builder, Clone, Debug, PartialEq, Eq)]
75#[builder(
76    pattern = "owned",
77    setter(into, strip_option),
78    build_fn(validate = "Self::validate")
79)]
80pub struct ArtifactDependency {
81    /// Crate name to resolve.
82    pub crate_name: String,
83    #[builder(default)]
84    /// Version requirement.
85    pub version: Option<String>,
86    #[builder(default)]
87    /// Local crate directory to resolve from.
88    pub path: Option<PathBuf>,
89    #[builder(default)]
90    /// Binary name.
91    pub bin_name: Option<String>,
92    #[builder(default)]
93    /// Build profile.
94    pub profile: BuildProfile,
95    #[builder(default)]
96    /// Target triple.
97    pub target: Option<String>,
98    #[builder(default = "true")]
99    /// Whether to use the exact versions from the dependency's lockfile.
100    pub locked: bool,
101}
102
103impl Default for ArtifactDependency {
104    fn default() -> Self {
105        Self {
106            crate_name: String::new(),
107            version: None,
108            path: None,
109            bin_name: None,
110            profile: BuildProfile::Release,
111            target: None,
112            locked: true,
113        }
114    }
115}
116
117impl ArtifactDependencyBuilder {
118    fn validate(&self) -> std::result::Result<(), String> {
119        if let Some(Some(path)) = &self.path
120            && !path.is_absolute()
121        {
122            return Err(format!("path must be absolute: `{}`", path.display()));
123        }
124
125        Ok(())
126    }
127}
128
129impl ArtifactDependency {
130    /// Resolves the artifact path.
131    pub fn resolve(&self) -> Result<PathBuf> {
132        let install_root = self.install_root();
133
134        if let Some(artifact_path) = cached_artifact(&install_root, self.bin_name.as_deref())? {
135            return Ok(artifact_path);
136        }
137
138        CargoInstallBuilder::default()
139            .crate_name(&self.crate_name)
140            .root(&install_root)
141            .locked(self.locked)
142            .apply_if_some(self.version.as_deref(), |builder, version_req| {
143                builder.version(cargo_install_version_req(version_req).into_owned())
144            })
145            .apply_if_some(self.path(), |builder, path| builder.path(path))
146            .apply_if_some(self.bin_name.as_deref(), |builder, bin_name| {
147                builder.bin(bin_name)
148            })
149            .apply_if_some(self.target.as_deref(), |builder, target| {
150                builder.target(target)
151            })
152            .apply_if(matches!(self.profile, BuildProfile::Debug), |builder| {
153                builder.debug(true)
154            })
155            .apply_if_some(
156                match &self.profile {
157                    BuildProfile::Custom(profile) => Some(profile.as_str()),
158                    _ => None,
159                },
160                |builder, profile| builder.profile(profile),
161            )
162            .build()
163            .expect("CargoInstallBuilder should not fail with optional-only fields")
164            .run()?;
165
166        find_artifact(&install_root, self.bin_name.as_deref())
167    }
168}
169
170// Returns the artifact path when the install root already contains one.
171fn cached_artifact(install_root: &Path, bin_name: Option<&str>) -> Result<Option<PathBuf>> {
172    match find_artifact(install_root, bin_name) {
173        Ok(artifact_path) => Ok(Some(artifact_path)),
174        Err(Error::Io(err)) if err.kind() == io::ErrorKind::NotFound => Ok(None),
175        Err(Error::NoInstalledBinaries { .. } | Error::InvalidArtifactPath { .. }) => Ok(None),
176        Err(err) => Err(err),
177    }
178}
179
180// Find the artifact in the provided root.
181fn find_artifact(install_root: &Path, bin_name: Option<&str>) -> Result<PathBuf> {
182    let bin_dir = install_root.join("bin");
183
184    match bin_name {
185        Some(bin_name) => find_binary_with_name(bin_dir, bin_name),
186        None => find_single_binary(bin_dir),
187    }
188}
189
190fn find_binary_with_name(dir: PathBuf, name: &str) -> Result<PathBuf> {
191    let artifact_path = dir.join(executable_name(name));
192    if artifact_path.is_file() {
193        Ok(artifact_path)
194    } else {
195        Err(Error::InvalidArtifactPath {
196            path: artifact_path,
197        })
198    }
199}
200
201// Find the singular binary in the binary directory when no name is provided.
202fn find_single_binary(dir: PathBuf) -> Result<PathBuf> {
203    let mut binaries = files_in_dir(&dir)?;
204
205    match binaries.len() {
206        0 => Err(Error::NoInstalledBinaries { dir }),
207        1 => Ok(binaries.remove(0)),
208        _ => Err(Error::AmbiguousInstalledBinaries),
209    }
210}