mod error;
mod install_root;
mod utils;
#[cfg(test)]
mod network_tests;
#[cfg(test)]
mod tests;
use std::{
io,
path::{Path, PathBuf},
};
use apply_if::ApplyIf;
use cargo_install::CargoInstallBuilder;
use derive_builder::Builder;
pub use crate::error::{Error, Result};
use crate::utils::{cargo_install_version_req, executable_name, files_in_dir};
#[derive(Clone, Debug, PartialEq, Default, Eq)]
pub enum BuildProfile {
Debug,
#[default]
Release,
Custom(String),
}
#[derive(Builder, Clone, Debug, PartialEq, Eq)]
#[builder(
pattern = "owned",
setter(into, strip_option),
build_fn(validate = "Self::validate")
)]
pub struct ArtifactDependency {
pub crate_name: String,
#[builder(default)]
pub version: Option<String>,
#[builder(default)]
pub path: Option<PathBuf>,
#[builder(default)]
pub bin_name: Option<String>,
#[builder(default)]
pub profile: BuildProfile,
#[builder(default)]
pub target: Option<String>,
#[builder(default = "true")]
pub locked: bool,
}
impl Default for ArtifactDependency {
fn default() -> Self {
Self {
crate_name: String::new(),
version: None,
path: None,
bin_name: None,
profile: BuildProfile::Release,
target: None,
locked: true,
}
}
}
impl ArtifactDependencyBuilder {
fn validate(&self) -> std::result::Result<(), String> {
if let Some(Some(path)) = &self.path
&& !path.is_absolute()
{
return Err(format!("path must be absolute: `{}`", path.display()));
}
Ok(())
}
}
impl ArtifactDependency {
pub fn resolve(&self) -> Result<PathBuf> {
let install_root = self.install_root();
if let Some(artifact_path) = cached_artifact(&install_root, self.bin_name.as_deref())? {
return Ok(artifact_path);
}
CargoInstallBuilder::default()
.crate_name(&self.crate_name)
.root(&install_root)
.locked(self.locked)
.apply_if_some(self.version.as_deref(), |builder, version_req| {
builder.version(cargo_install_version_req(version_req).into_owned())
})
.apply_if_some(self.path(), |builder, path| builder.path(path))
.apply_if_some(self.bin_name.as_deref(), |builder, bin_name| {
builder.bin(bin_name)
})
.apply_if_some(self.target.as_deref(), |builder, target| {
builder.target(target)
})
.apply_if(matches!(self.profile, BuildProfile::Debug), |builder| {
builder.debug(true)
})
.apply_if_some(
match &self.profile {
BuildProfile::Custom(profile) => Some(profile.as_str()),
_ => None,
},
|builder, profile| builder.profile(profile),
)
.build()
.expect("CargoInstallBuilder should not fail with optional-only fields")
.run()?;
find_artifact(&install_root, self.bin_name.as_deref())
}
}
fn cached_artifact(install_root: &Path, bin_name: Option<&str>) -> Result<Option<PathBuf>> {
match find_artifact(install_root, bin_name) {
Ok(artifact_path) => Ok(Some(artifact_path)),
Err(Error::Io(err)) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(Error::NoInstalledBinaries { .. } | Error::InvalidArtifactPath { .. }) => Ok(None),
Err(err) => Err(err),
}
}
fn find_artifact(install_root: &Path, bin_name: Option<&str>) -> Result<PathBuf> {
let bin_dir = install_root.join("bin");
match bin_name {
Some(bin_name) => find_binary_with_name(bin_dir, bin_name),
None => find_single_binary(bin_dir),
}
}
fn find_binary_with_name(dir: PathBuf, name: &str) -> Result<PathBuf> {
let artifact_path = dir.join(executable_name(name));
if artifact_path.is_file() {
Ok(artifact_path)
} else {
Err(Error::InvalidArtifactPath {
path: artifact_path,
})
}
}
fn find_single_binary(dir: PathBuf) -> Result<PathBuf> {
let mut binaries = files_in_dir(&dir)?;
match binaries.len() {
0 => Err(Error::NoInstalledBinaries { dir }),
1 => Ok(binaries.remove(0)),
_ => Err(Error::AmbiguousInstalledBinaries),
}
}