mod providers;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use providers::{BinstallProvider, GithubProvider, GitlabProvider, Provider, QuickinstallProvider};
use serde::{Deserialize, Serialize};
use snafu::{IntoError, ResultExt};
use tempfile::TempDir;
use tracing::warn;
use crate::{
Result,
builder::{BuildOptions, BuildTarget},
cache::Cache,
config::{BinaryProvider, Config, UsePrebuiltBinaries},
crate_resolver::ResolvedCrate,
downloader::DownloadedCrate,
error::{self, Error},
http::HttpClient,
messages::{MessageReporter, PrebuiltBinaryMessage, ProviderChangeReason},
target::TargetTriple,
};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ResolvedBinary {
pub krate: ResolvedCrate,
pub provider: BinaryProvider,
pub path: std::path::PathBuf,
pub target: String,
}
pub trait BinaryResolver {
fn resolve(
&self,
krate: &DownloadedCrate,
build_options: &BuildOptions,
) -> Result<Option<ResolvedBinary>>;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "outcome", rename_all = "snake_case")]
#[expect(
clippy::large_enum_variant,
reason = "only a handful of these exist at a time (one per resolved crate); the size disparity between \
Found and Nonexistent does not matter and boxing would only add indirection"
)]
pub(crate) enum ConclusiveResolution {
Found(ResolvedBinary),
Nonexistent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct BinaryCacheEntry {
#[serde(flatten)]
pub(crate) outcome: ConclusiveResolution,
pub(crate) enabled_providers: Vec<BinaryProvider>,
}
pub(crate) fn create_resolver(
config: Config,
cache: Cache,
reporter: MessageReporter,
http_client: HttpClient,
) -> Result<impl BinaryResolver> {
DefaultBinaryResolver::new(config, cache, reporter, http_client)
}
#[derive(Debug)]
#[expect(
clippy::large_enum_variant,
reason = "this is a short-lived return value (a handful per resolution), never stored in bulk; boxing \
the common Found payload would only add a heap allocation to the success path"
)]
enum BinaryResolution {
Found(ResolvedBinary),
Nonexistent,
Inconclusive { source: Box<Error> },
}
impl BinaryResolution {
fn to_cacheable(&self) -> Option<ConclusiveResolution> {
match self {
BinaryResolution::Found(binary) => Some(ConclusiveResolution::Found(binary.clone())),
BinaryResolution::Nonexistent => Some(ConclusiveResolution::Nonexistent),
BinaryResolution::Inconclusive { .. } => None,
}
}
}
impl From<ConclusiveResolution> for BinaryResolution {
fn from(value: ConclusiveResolution) -> Self {
match value {
ConclusiveResolution::Found(binary) => Self::Found(binary),
ConclusiveResolution::Nonexistent => Self::Nonexistent,
}
}
}
struct DefaultBinaryResolver {
config: Config,
cache: Cache,
reporter: MessageReporter,
mode: UsePrebuiltBinaries,
#[expect(
dead_code,
reason = "held for its Drop impl: the staging directory must stay alive for the providers that \
write into it, and dropping it is what cleans the staging area up"
)]
staging: TempDir,
providers: Vec<Box<dyn Provider + Send + Sync>>,
}
impl DefaultBinaryResolver {
fn new(config: Config, cache: Cache, reporter: MessageReporter, http_client: HttpClient) -> Result<Self> {
let staging = Self::create_staging_dir(&config)?;
let verify = config.prebuilt_binaries.verify_checksums;
let providers = config
.prebuilt_binaries
.binary_providers
.iter()
.map(|provider_type| -> Box<dyn Provider + Send + Sync> {
match provider_type {
BinaryProvider::Binstall => Box::new(BinstallProvider::new(
reporter.clone(),
&staging,
verify,
http_client.clone(),
)),
BinaryProvider::GithubReleases => Box::new(GithubProvider::new(
reporter.clone(),
&staging,
verify,
http_client.clone(),
)),
BinaryProvider::GitlabReleases => Box::new(GitlabProvider::new(
reporter.clone(),
&staging,
verify,
http_client.clone(),
)),
BinaryProvider::Quickinstall => Box::new(QuickinstallProvider::new(
reporter.clone(),
&staging,
http_client.clone(),
)),
}
})
.collect();
Ok(Self::with_providers(config, cache, reporter, staging, providers))
}
fn create_staging_dir(config: &Config) -> Result<TempDir> {
std::fs::create_dir_all(&config.bin_dir).with_context(|_| error::IoSnafu {
path: config.bin_dir.clone(),
})?;
tempfile::Builder::new()
.prefix("cgx-bin-resolver-temp")
.tempdir_in(&config.bin_dir)
.with_context(|_| error::TempDirInCreationSnafu {
parent: config.bin_dir.clone(),
})
}
fn with_providers(
config: Config,
cache: Cache,
reporter: MessageReporter,
staging: TempDir,
providers: Vec<Box<dyn Provider + Send + Sync>>,
) -> Self {
let mode = config.prebuilt_binaries.use_prebuilt_binaries;
Self {
config,
cache,
reporter,
mode,
staging,
providers,
}
}
fn is_disqualified(build_options: &BuildOptions) -> Option<&'static str> {
if build_options.build_target != BuildTarget::DefaultBin {
return Some("explicit --bin or --example specified");
}
if !build_options.features.is_empty() {
return Some("custom features specified");
}
if build_options.all_features {
return Some("--all-features specified");
}
if build_options.no_default_features {
return Some("--no-default-features specified");
}
if build_options.profile.is_some() {
return Some("custom profile specified");
}
if build_options.target.is_some() {
return Some("custom target specified");
}
if build_options.toolchain.is_some() {
return Some("custom toolchain specified");
}
None
}
fn combine_resolutions(resolutions: impl IntoIterator<Item = BinaryResolution>) -> BinaryResolution {
let mut inconclusive: Option<Box<Error>> = None;
for resolution in resolutions {
match resolution {
BinaryResolution::Found(binary) => return BinaryResolution::Found(binary),
BinaryResolution::Inconclusive { source } => {
inconclusive.get_or_insert(source);
}
BinaryResolution::Nonexistent => {}
}
}
match inconclusive {
Some(source) => BinaryResolution::Inconclusive { source },
None => BinaryResolution::Nonexistent,
}
}
fn apply_mode(
resolution: BinaryResolution,
mode: UsePrebuiltBinaries,
krate: &ResolvedCrate,
) -> Result<Option<ResolvedBinary>> {
debug_assert_ne!(mode, UsePrebuiltBinaries::Never);
match resolution {
BinaryResolution::Found(binary) => Ok(Some(binary)),
BinaryResolution::Nonexistent => {
if mode == UsePrebuiltBinaries::Always {
error::PrebuiltBinaryRequiredSnafu {
name: krate.name.clone(),
version: krate.version.to_string(),
}
.fail()
} else {
Ok(None)
}
}
BinaryResolution::Inconclusive { source } => {
if mode == UsePrebuiltBinaries::Always {
Err(error::PrebuiltBinaryResolutionFailedSnafu {
name: krate.name.clone(),
version: krate.version.to_string(),
}
.into_error(source))
} else {
Ok(None)
}
}
}
}
fn get_cached_resolution(&self, krate: &ResolvedCrate) -> Option<ConclusiveResolution> {
let entry = self.cache.get_cached_binary_resolution(krate).ok()??;
let enabled = &self.config.prebuilt_binaries.binary_providers;
if let Some(missing) = enabled.iter().find(|p| !entry.enabled_providers.contains(p)) {
self.reporter.report(|| {
PrebuiltBinaryMessage::cache_invalidated_by_provider_change(
krate,
ProviderChangeReason::RequiredProviderNotEnabled(*missing),
)
});
return None;
}
if let ConclusiveResolution::Found(binary) = &entry.outcome {
if !enabled.contains(&binary.provider) {
self.reporter.report(|| {
PrebuiltBinaryMessage::cache_invalidated_by_provider_change(
krate,
ProviderChangeReason::SourceProviderDisabled(binary.provider),
)
});
return None;
}
if !binary.path.exists() {
self.reporter.report(|| {
PrebuiltBinaryMessage::cache_invalidated_by_missing_binary(krate, &binary.path)
});
warn!(
"Cached binary resolution for {}@{} points to missing file {:?}; ignoring cache entry",
krate.name, krate.version, binary.path
);
return None;
}
}
match &entry.outcome {
ConclusiveResolution::Found(binary) => self
.reporter
.report(|| PrebuiltBinaryMessage::positive_cache_hit(krate, &binary.path, binary.provider)),
ConclusiveResolution::Nonexistent => self
.reporter
.report(|| PrebuiltBinaryMessage::negative_cache_hit(krate)),
}
Some(entry.outcome)
}
fn resolve_via_providers(
&self,
krate: &DownloadedCrate,
target: &TargetTriple,
) -> Result<BinaryResolution> {
if self.providers.is_empty() {
return error::NoProvidersConfiguredSnafu.fail();
}
let resolved = &krate.resolved;
let mut results = Vec::with_capacity(self.providers.len());
for provider in &self.providers {
let provider_kind = provider.kind();
self.reporter
.report(|| PrebuiltBinaryMessage::checking_provider(resolved, provider_kind));
let resolution = match provider.try_resolve(krate, target) {
Ok(resolution) => BinaryResolution::from(resolution),
Err(source) => {
self.reporter
.report(|| PrebuiltBinaryMessage::provider_failed(provider_kind, source.to_string()));
BinaryResolution::Inconclusive {
source: Box::new(source),
}
}
};
let found = matches!(resolution, BinaryResolution::Found(_));
results.push(resolution);
if found {
break;
}
}
Ok(Self::combine_resolutions(results))
}
fn relocate_to_bin_dir(
&self,
mut binary: ResolvedBinary,
krate: &ResolvedCrate,
target: &TargetTriple,
) -> Result<ResolvedBinary> {
let target_dir = self.cache.crate_bin_root(krate).join(format!(
"prebuilt-{:?}-{}",
binary.provider,
target.as_str()
));
std::fs::create_dir_all(&target_dir).with_context(|_| error::IoSnafu {
path: target_dir.clone(),
})?;
let binary_name = binary.path.file_name().ok_or_else(|| Error::Io {
path: binary.path.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, "binary path has no filename"),
})?;
let target_path = target_dir.join(binary_name);
std::fs::rename(&binary.path, &target_path).with_context(|_| error::RenameFileSnafu {
src: binary.path.clone(),
dst: target_path.clone(),
})?;
#[cfg(unix)]
{
let mut perms = std::fs::metadata(&target_path)
.with_context(|_| error::IoSnafu {
path: target_path.clone(),
})?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&target_path, perms).with_context(|_| error::IoSnafu {
path: target_path.clone(),
})?;
}
binary.path = target_path;
Ok(binary)
}
}
impl BinaryResolver for DefaultBinaryResolver {
fn resolve(
&self,
krate: &DownloadedCrate,
build_options: &BuildOptions,
) -> Result<Option<ResolvedBinary>> {
let resolved_krate = &krate.resolved;
tracing::debug!(
"BinaryResolver::resolve called for {}@{}",
resolved_krate.name,
resolved_krate.version
);
if self.mode == UsePrebuiltBinaries::Never {
self.reporter
.report(PrebuiltBinaryMessage::prebuilt_binaries_disabled);
return Ok(None);
}
if let Some(reason) = Self::is_disqualified(build_options) {
if self.mode == UsePrebuiltBinaries::Always {
return error::PrebuiltBinaryDisqualifiedSnafu {
name: resolved_krate.name.clone(),
version: resolved_krate.version.to_string(),
reason,
}
.fail();
}
self.reporter
.report(|| PrebuiltBinaryMessage::disqualified_due_to_customization(reason));
return Ok(None);
}
if !self.config.refresh {
if let Some(cached) = self.get_cached_resolution(resolved_krate) {
let resolution = match cached {
ConclusiveResolution::Found(binary) => BinaryResolution::Found(binary),
ConclusiveResolution::Nonexistent => {
self.reporter.report(|| {
PrebuiltBinaryMessage::no_binary_found(
resolved_krate,
vec!["negative cache hit - no binary available".to_string()],
)
});
BinaryResolution::Nonexistent
}
};
return Self::apply_mode(resolution, self.mode, resolved_krate);
}
}
let target = TargetTriple::host();
let resolution = self.resolve_via_providers(krate, target)?;
let resolution = match resolution {
BinaryResolution::Found(binary) => {
let relocated = self.relocate_to_bin_dir(binary, resolved_krate, target)?;
self.reporter
.report(|| PrebuiltBinaryMessage::resolved(&relocated));
BinaryResolution::Found(relocated)
}
BinaryResolution::Nonexistent => {
self.reporter.report(|| {
PrebuiltBinaryMessage::no_binary_found(
resolved_krate,
vec!["no binary found from any configured provider".to_string()],
)
});
BinaryResolution::Nonexistent
}
BinaryResolution::Inconclusive { source } => {
self.reporter
.report(|| PrebuiltBinaryMessage::resolution_inconclusive(source.to_string()));
BinaryResolution::Inconclusive { source }
}
};
if let Some(outcome) = resolution.to_cacheable() {
let entry = BinaryCacheEntry {
outcome,
enabled_providers: self.config.prebuilt_binaries.binary_providers.clone(),
};
self.cache.put_cached_binary_resolution(resolved_krate, entry)?;
}
Self::apply_mode(resolution, self.mode, resolved_krate)
}
}
#[cfg(test)]
mod tests {
use std::{
path::PathBuf,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use assert_matches::assert_matches;
use semver::Version;
use tempfile::TempDir;
use super::*;
use crate::{
builder::{BuildOptions, BuildTarget},
crate_resolver::ResolvedSource,
};
#[expect(
clippy::large_enum_variant,
reason = "test stub; at most one instance exists per test, so the size disparity is irrelevant"
)]
enum StubOutcome {
Found(ResolvedBinary),
Nonexistent,
Error,
}
struct StubProvider {
outcome: StubOutcome,
calls: Arc<AtomicUsize>,
}
impl StubProvider {
fn found(provider: BinaryProvider, path: PathBuf) -> Self {
Self {
outcome: StubOutcome::Found(ResolvedBinary {
krate: test_downloaded_crate().resolved,
provider,
path,
target: build_context::TARGET.to_string(),
}),
calls: Arc::new(AtomicUsize::new(0)),
}
}
fn nonexistent() -> Self {
Self {
outcome: StubOutcome::Nonexistent,
calls: Arc::new(AtomicUsize::new(0)),
}
}
fn error() -> Self {
Self {
outcome: StubOutcome::Error,
calls: Arc::new(AtomicUsize::new(0)),
}
}
}
impl Provider for StubProvider {
fn kind(&self) -> BinaryProvider {
BinaryProvider::GithubReleases
}
fn try_resolve(
&self,
_krate: &DownloadedCrate,
_target: &TargetTriple,
) -> Result<ConclusiveResolution> {
self.calls.fetch_add(1, Ordering::SeqCst);
match &self.outcome {
StubOutcome::Found(binary) => Ok(ConclusiveResolution::Found(binary.clone())),
StubOutcome::Nonexistent => Ok(ConclusiveResolution::Nonexistent),
StubOutcome::Error => Err(transient_error()),
}
}
}
fn transient_error() -> Error {
error::HttpStatusSnafu {
url: "https://api.github.com/repos/x/y/releases/tags/v1.0.0".to_string(),
status: 429u16,
}
.build()
}
fn boxed_transient() -> Box<Error> {
Box::new(transient_error())
}
fn test_env() -> (Cache, Config, TempDir) {
crate::logging::init_test_logging();
let (temp_dir, config) = crate::config::create_test_env();
let cache = Cache::new(config.clone(), MessageReporter::null());
(cache, config, temp_dir)
}
fn resolver_with(
cache: Cache,
config: Config,
mode: UsePrebuiltBinaries,
outcome: StubOutcome,
) -> (DefaultBinaryResolver, Arc<AtomicUsize>) {
let calls = Arc::new(AtomicUsize::new(0));
let mut config = config;
config.prebuilt_binaries.use_prebuilt_binaries = mode;
let providers: Vec<Box<dyn Provider + Send + Sync>> = vec![Box::new(StubProvider {
outcome,
calls: calls.clone(),
})];
let staging = DefaultBinaryResolver::create_staging_dir(&config).unwrap();
(
DefaultBinaryResolver::with_providers(config, cache, MessageReporter::null(), staging, providers),
calls,
)
}
fn resolver_with_enabled_providers(
cache: Cache,
config: Config,
enabled: Vec<BinaryProvider>,
providers: Vec<Box<dyn Provider + Send + Sync>>,
) -> DefaultBinaryResolver {
let mut config = config;
config.prebuilt_binaries.use_prebuilt_binaries = UsePrebuiltBinaries::Auto;
config.prebuilt_binaries.binary_providers = enabled;
let staging = DefaultBinaryResolver::create_staging_dir(&config).unwrap();
DefaultBinaryResolver::with_providers(config, cache, MessageReporter::null(), staging, providers)
}
fn test_downloaded_crate() -> DownloadedCrate {
DownloadedCrate {
resolved: ResolvedCrate {
name: "serde".to_string(),
version: Version::parse("1.0.0").unwrap(),
source: ResolvedSource::CratesIo,
},
crate_path: PathBuf::from("/nonexistent"),
}
}
fn test_resolved_binary() -> ResolvedBinary {
ResolvedBinary {
krate: test_downloaded_crate().resolved,
provider: BinaryProvider::GithubReleases,
path: PathBuf::from("/fake/bin/serde"),
target: build_context::TARGET.to_string(),
}
}
#[test]
fn test_disqualification_default_options_ok() {
let options = BuildOptions::default();
assert_eq!(DefaultBinaryResolver::is_disqualified(&options), None);
}
#[test]
fn test_disqualification_explicit_bin() {
let options = BuildOptions {
build_target: BuildTarget::Bin("specific-bin".to_string()),
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("explicit --bin or --example specified")
);
}
#[test]
fn test_disqualification_explicit_example() {
let options = BuildOptions {
build_target: BuildTarget::Example("my-example".to_string()),
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("explicit --bin or --example specified")
);
}
#[test]
fn test_disqualification_custom_features() {
let options = BuildOptions {
features: vec!["serde".to_string(), "json".to_string()],
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("custom features specified")
);
}
#[test]
fn test_disqualification_all_features() {
let options = BuildOptions {
all_features: true,
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("--all-features specified")
);
}
#[test]
fn test_disqualification_no_default_features() {
let options = BuildOptions {
no_default_features: true,
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("--no-default-features specified")
);
}
#[test]
fn test_disqualification_custom_profile() {
let options = BuildOptions {
profile: Some("release-with-debug".to_string()),
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("custom profile specified")
);
}
#[test]
fn test_disqualification_custom_target() {
let options = BuildOptions {
target: Some(TargetTriple::from_static("x86_64-unknown-linux-musl")),
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("custom target specified")
);
}
#[test]
fn test_disqualification_custom_toolchain() {
let options = BuildOptions {
toolchain: Some("nightly".to_string()),
..Default::default()
};
assert_eq!(
DefaultBinaryResolver::is_disqualified(&options),
Some("custom toolchain specified")
);
}
#[test]
fn combine_empty_is_nonexistent() {
assert_matches!(
DefaultBinaryResolver::combine_resolutions(Vec::<BinaryResolution>::new()),
BinaryResolution::Nonexistent
);
}
#[test]
fn combine_all_nonexistent_is_nonexistent() {
let combined = DefaultBinaryResolver::combine_resolutions([
BinaryResolution::Nonexistent,
BinaryResolution::Nonexistent,
]);
assert_matches!(combined, BinaryResolution::Nonexistent);
}
#[test]
fn combine_any_found_wins() {
let combined = DefaultBinaryResolver::combine_resolutions([
BinaryResolution::Inconclusive {
source: boxed_transient(),
},
BinaryResolution::Found(test_resolved_binary()),
BinaryResolution::Nonexistent,
]);
assert_matches!(combined, BinaryResolution::Found(_));
}
#[test]
fn combine_inconclusive_beats_nonexistent() {
let combined = DefaultBinaryResolver::combine_resolutions([
BinaryResolution::Nonexistent,
BinaryResolution::Inconclusive {
source: boxed_transient(),
},
BinaryResolution::Nonexistent,
]);
assert_matches!(combined, BinaryResolution::Inconclusive { .. });
}
#[test]
fn cacheable_found_and_nonexistent_but_never_inconclusive() {
assert_matches!(
BinaryResolution::Found(test_resolved_binary()).to_cacheable(),
Some(ConclusiveResolution::Found(_))
);
assert_matches!(
BinaryResolution::Nonexistent.to_cacheable(),
Some(ConclusiveResolution::Nonexistent)
);
assert_matches!(
BinaryResolution::Inconclusive {
source: boxed_transient()
}
.to_cacheable(),
None
);
}
#[test]
fn apply_mode_found_returns_binary_in_any_mode() {
let resolved = test_downloaded_crate().resolved;
for mode in [UsePrebuiltBinaries::Auto, UsePrebuiltBinaries::Always] {
let out = DefaultBinaryResolver::apply_mode(
BinaryResolution::Found(test_resolved_binary()),
mode,
&resolved,
)
.unwrap();
assert_matches!(out, Some(_));
}
}
#[test]
fn apply_mode_nonexistent_is_none_in_auto_but_errors_in_always() {
let resolved = test_downloaded_crate().resolved;
assert_matches!(
DefaultBinaryResolver::apply_mode(
BinaryResolution::Nonexistent,
UsePrebuiltBinaries::Auto,
&resolved
),
Ok(None)
);
assert_matches!(
DefaultBinaryResolver::apply_mode(
BinaryResolution::Nonexistent,
UsePrebuiltBinaries::Always,
&resolved
),
Err(Error::PrebuiltBinaryRequired { .. })
);
}
#[test]
fn apply_mode_inconclusive_is_none_in_auto_but_errors_with_source_in_always() {
let resolved = test_downloaded_crate().resolved;
assert_matches!(
DefaultBinaryResolver::apply_mode(
BinaryResolution::Inconclusive {
source: boxed_transient()
},
UsePrebuiltBinaries::Auto,
&resolved
),
Ok(None)
);
let err = DefaultBinaryResolver::apply_mode(
BinaryResolution::Inconclusive {
source: boxed_transient(),
},
UsePrebuiltBinaries::Always,
&resolved,
)
.unwrap_err();
assert_matches!(
err,
Error::PrebuiltBinaryResolutionFailed { ref name, .. } if name == "serde"
);
}
#[test]
fn always_mode_rejects_disqualifying_build_options() {
let (cache, config, _temp) = test_env();
let (resolver, calls) = resolver_with(
cache,
config,
UsePrebuiltBinaries::Always,
StubOutcome::Nonexistent,
);
let options = BuildOptions {
profile: Some("dev".to_string()),
..Default::default()
};
let result = resolver.resolve(&test_downloaded_crate(), &options);
assert_matches!(
result,
Err(Error::PrebuiltBinaryDisqualified { ref name, ref reason, .. })
if name == "serde" && reason.contains("profile")
);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[test]
fn auto_mode_skips_prebuilt_for_disqualifying_build_options() {
let (cache, config, _temp) = test_env();
let (resolver, calls) =
resolver_with(cache, config, UsePrebuiltBinaries::Auto, StubOutcome::Nonexistent);
let options = BuildOptions {
profile: Some("dev".to_string()),
..Default::default()
};
let result = resolver.resolve(&test_downloaded_crate(), &options).unwrap();
assert_eq!(result, None);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[test]
fn always_mode_errors_when_no_provider_has_binary() {
let (cache, config, _temp) = test_env();
let (resolver, calls) = resolver_with(
cache,
config,
UsePrebuiltBinaries::Always,
StubOutcome::Nonexistent,
);
let result = resolver.resolve(&test_downloaded_crate(), &BuildOptions::default());
assert_matches!(
result,
Err(Error::PrebuiltBinaryRequired { ref name, .. }) if name == "serde"
);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn always_mode_errors_on_cached_negative_result() {
let (cache, config, _temp) = test_env();
let (auto_resolver, auto_calls) = resolver_with(
cache.clone(),
config.clone(),
UsePrebuiltBinaries::Auto,
StubOutcome::Nonexistent,
);
assert_matches!(
auto_resolver.resolve(&test_downloaded_crate(), &BuildOptions::default()),
Ok(None)
);
assert_eq!(auto_calls.load(Ordering::SeqCst), 1);
let (always_resolver, always_calls) = resolver_with(
cache,
config,
UsePrebuiltBinaries::Always,
StubOutcome::Nonexistent,
);
let result = always_resolver.resolve(&test_downloaded_crate(), &BuildOptions::default());
assert_matches!(result, Err(Error::PrebuiltBinaryRequired { .. }));
assert_eq!(always_calls.load(Ordering::SeqCst), 0);
}
#[test]
fn never_mode_returns_none_without_consulting_providers() {
let (cache, config, temp) = test_env();
let src = temp.path().join("serde");
std::fs::write(&src, b"binary").unwrap();
let binary = ResolvedBinary {
krate: test_downloaded_crate().resolved,
provider: BinaryProvider::GithubReleases,
path: src,
target: build_context::TARGET.to_string(),
};
let (resolver, calls) = resolver_with(
cache,
config,
UsePrebuiltBinaries::Never,
StubOutcome::Found(binary),
);
let result = resolver
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap();
assert_eq!(result, None);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[test]
fn resolved_binary_relocated_and_returned_in_always_mode() {
let (cache, config, temp) = test_env();
let bin_dir = config.bin_dir.clone();
let src = temp.path().join("serde");
std::fs::write(&src, b"binary").unwrap();
let binary = ResolvedBinary {
krate: test_downloaded_crate().resolved,
provider: BinaryProvider::GithubReleases,
path: src.clone(),
target: build_context::TARGET.to_string(),
};
let (resolver, _calls) = resolver_with(
cache,
config,
UsePrebuiltBinaries::Always,
StubOutcome::Found(binary),
);
let result = resolver
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap()
.unwrap();
assert_eq!(result.provider, BinaryProvider::GithubReleases);
assert!(result.path.exists(), "relocated binary should exist");
assert!(
result.path.starts_with(&bin_dir),
"binary should be relocated under bin_dir"
);
assert_ne!(result.path, src);
}
#[test]
fn inconclusive_result_is_not_cached() {
let (cache, config, _temp) = test_env();
let (resolver, calls) = resolver_with(
cache.clone(),
config,
UsePrebuiltBinaries::Auto,
StubOutcome::Error,
);
let result = resolver
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap();
assert_eq!(result, None);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_matches!(
cache.get_cached_binary_resolution(&test_downloaded_crate().resolved),
Ok(None),
"an inconclusive resolution must not be persisted"
);
}
#[test]
fn auto_mode_continues_after_provider_error_and_returns_later_found() {
let (cache, config, temp) = test_env();
let src = temp.path().join("serde");
std::fs::write(&src, b"binary").unwrap();
let first = StubProvider::error();
let first_calls = first.calls.clone();
let second = StubProvider::found(BinaryProvider::GithubReleases, src);
let second_calls = second.calls.clone();
let resolver = resolver_with_enabled_providers(
cache,
config,
vec![BinaryProvider::GitlabReleases, BinaryProvider::GithubReleases],
vec![Box::new(first), Box::new(second)],
);
let result = resolver
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap()
.unwrap();
assert_eq!(result.provider, BinaryProvider::GithubReleases);
assert_eq!(first_calls.load(Ordering::SeqCst), 1);
assert_eq!(second_calls.load(Ordering::SeqCst), 1);
}
#[test]
fn nonexistent_result_is_cached() {
let (cache, config, _temp) = test_env();
let (resolver, _calls) = resolver_with(
cache.clone(),
config,
UsePrebuiltBinaries::Auto,
StubOutcome::Nonexistent,
);
resolver
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap();
assert_matches!(
cache.get_cached_binary_resolution(&test_downloaded_crate().resolved),
Ok(Some(BinaryCacheEntry {
outcome: ConclusiveResolution::Nonexistent,
..
}))
);
}
#[test]
fn always_mode_errors_on_inconclusive_resolution() {
let (cache, config, _temp) = test_env();
let (resolver, calls) = resolver_with(
cache.clone(),
config,
UsePrebuiltBinaries::Always,
StubOutcome::Error,
);
let result = resolver.resolve(&test_downloaded_crate(), &BuildOptions::default());
assert_matches!(
result,
Err(Error::PrebuiltBinaryResolutionFailed { ref name, ref source, .. })
if name == "serde" && matches!(source.as_ref(), Error::HttpStatus { status: 429, .. })
);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_matches!(
cache.get_cached_binary_resolution(&test_downloaded_crate().resolved),
Ok(None)
);
}
#[test]
fn negative_cache_invalidated_when_new_provider_enabled() {
let (cache, config, temp) = test_env();
let gitlab1 = StubProvider::nonexistent();
let gitlab1_calls = gitlab1.calls.clone();
let r1 = resolver_with_enabled_providers(
cache.clone(),
config.clone(),
vec![BinaryProvider::GitlabReleases],
vec![Box::new(gitlab1)],
);
assert_matches!(
r1.resolve(&test_downloaded_crate(), &BuildOptions::default()),
Ok(None)
);
assert_eq!(gitlab1_calls.load(Ordering::SeqCst), 1);
let src = temp.path().join("serde");
std::fs::write(&src, b"binary").unwrap();
let gitlab2 = StubProvider::nonexistent();
let github = StubProvider::found(BinaryProvider::GithubReleases, src);
let github_calls = github.calls.clone();
let r2 = resolver_with_enabled_providers(
cache.clone(),
config,
vec![BinaryProvider::GitlabReleases, BinaryProvider::GithubReleases],
vec![Box::new(gitlab2), Box::new(github)],
);
let result = r2
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap();
assert_matches!(result, Some(_));
assert!(
github_calls.load(Ordering::SeqCst) >= 1,
"GitHub must be consulted once the stale negative entry is invalidated"
);
}
#[test]
fn identical_provider_set_is_cache_hit() {
let (cache, config, _temp) = test_env();
let enabled = vec![BinaryProvider::GitlabReleases, BinaryProvider::GithubReleases];
let gl1 = StubProvider::nonexistent();
let gh1 = StubProvider::nonexistent();
let (gl1_calls, gh1_calls) = (gl1.calls.clone(), gh1.calls.clone());
let r1 = resolver_with_enabled_providers(
cache.clone(),
config.clone(),
enabled.clone(),
vec![Box::new(gl1), Box::new(gh1)],
);
assert_matches!(
r1.resolve(&test_downloaded_crate(), &BuildOptions::default()),
Ok(None)
);
assert_eq!(gl1_calls.load(Ordering::SeqCst), 1);
assert_eq!(gh1_calls.load(Ordering::SeqCst), 1);
let gl2 = StubProvider::nonexistent();
let gh2 = StubProvider::nonexistent();
let (gl2_calls, gh2_calls) = (gl2.calls.clone(), gh2.calls.clone());
let r2 = resolver_with_enabled_providers(cache, config, enabled, vec![Box::new(gl2), Box::new(gh2)]);
assert_matches!(
r2.resolve(&test_downloaded_crate(), &BuildOptions::default()),
Ok(None)
);
assert_eq!(
gl2_calls.load(Ordering::SeqCst),
0,
"cache hit must not re-consult providers"
);
assert_eq!(
gh2_calls.load(Ordering::SeqCst),
0,
"cache hit must not re-consult providers"
);
}
#[test]
fn positive_cache_hit_with_deleted_binary_reresolves() {
let (cache, config, temp) = test_env();
let src = temp.path().join("serde");
std::fs::write(&src, b"binary").unwrap();
let first = StubProvider::found(BinaryProvider::GithubReleases, src.clone());
let first_calls = first.calls.clone();
let r1 = resolver_with_enabled_providers(
cache.clone(),
config.clone(),
vec![BinaryProvider::GithubReleases],
vec![Box::new(first)],
);
let relocated = r1
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap()
.unwrap();
assert_eq!(first_calls.load(Ordering::SeqCst), 1);
assert!(relocated.path.exists());
std::fs::remove_file(&relocated.path).unwrap();
std::fs::write(&src, b"binary").unwrap();
let second = StubProvider::found(BinaryProvider::GithubReleases, src);
let second_calls = second.calls.clone();
let r2 = resolver_with_enabled_providers(
cache,
config,
vec![BinaryProvider::GithubReleases],
vec![Box::new(second)],
);
let result = r2
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap()
.unwrap();
assert_eq!(
second_calls.load(Ordering::SeqCst),
1,
"a positive entry pointing at a deleted binary must be re-resolved via providers"
);
assert!(
result.path.exists(),
"the re-resolved binary path must exist, got {}",
result.path.display()
);
}
#[test]
fn removing_non_finder_provider_keeps_positive_entry() {
let (cache, config, temp) = test_env();
let src = temp.path().join("serde");
std::fs::write(&src, b"binary").unwrap();
let github = StubProvider::found(BinaryProvider::GithubReleases, src);
let quick = StubProvider::nonexistent();
let r1 = resolver_with_enabled_providers(
cache.clone(),
config.clone(),
vec![BinaryProvider::GithubReleases, BinaryProvider::Quickinstall],
vec![Box::new(github), Box::new(quick)],
);
assert_matches!(
r1.resolve(&test_downloaded_crate(), &BuildOptions::default()),
Ok(Some(_))
);
let github2 = StubProvider::nonexistent();
let github2_calls = github2.calls.clone();
let r2 = resolver_with_enabled_providers(
cache,
config,
vec![BinaryProvider::GithubReleases],
vec![Box::new(github2)],
);
let result = r2
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap();
assert_matches!(result, Some(_));
assert_eq!(
github2_calls.load(Ordering::SeqCst),
0,
"a still-valid positive entry must not re-consult providers"
);
}
#[test]
fn disabling_finder_invalidates_positive_entry() {
let (cache, config, temp) = test_env();
let src = temp.path().join("serde");
std::fs::write(&src, b"binary").unwrap();
let gitlab = StubProvider::nonexistent();
let github = StubProvider::found(BinaryProvider::GithubReleases, src);
let r1 = resolver_with_enabled_providers(
cache.clone(),
config.clone(),
vec![BinaryProvider::GitlabReleases, BinaryProvider::GithubReleases],
vec![Box::new(gitlab), Box::new(github)],
);
assert_matches!(
r1.resolve(&test_downloaded_crate(), &BuildOptions::default()),
Ok(Some(_))
);
let gitlab2 = StubProvider::nonexistent();
let gitlab2_calls = gitlab2.calls.clone();
let r2 = resolver_with_enabled_providers(
cache,
config,
vec![BinaryProvider::GitlabReleases],
vec![Box::new(gitlab2)],
);
let result = r2
.resolve(&test_downloaded_crate(), &BuildOptions::default())
.unwrap();
assert_eq!(
result, None,
"a binary from a now-disabled provider must not be served"
);
assert_eq!(
gitlab2_calls.load(Ordering::SeqCst),
1,
"must re-resolve once the finder is disabled"
);
}
}