use crate::artifact::{ArtifactKind, ArtifactRegistry};
use crate::config::{Config, CrateConfig, PublishConfig};
use std::collections::HashSet;
const BINARY_PRESENCE_KINDS: &[ArtifactKind] = &[
ArtifactKind::Binary,
ArtifactKind::UploadableBinary,
ArtifactKind::UniversalBinary,
ArtifactKind::Archive,
];
pub fn check(
config: &Config,
artifacts: &ArtifactRegistry,
selected_crates: &[String],
built_crate_names: Option<&HashSet<String>>,
) -> anyhow::Result<()> {
for krate in config.crate_universe() {
if !selected_crates.is_empty() && !selected_crates.contains(&krate.name) {
continue;
}
if let Some(built) = built_crate_names
&& !built.contains(&krate.name)
{
continue;
}
let surfaces = binary_requiring_surfaces(krate);
if surfaces.is_empty() {
continue;
}
let has_binary = !artifacts
.by_kinds_and_crate(BINARY_PRESENCE_KINDS, &krate.name)
.is_empty();
if has_binary {
continue;
}
anyhow::bail!(
"release: crate '{}' configures {}, which require a compiled binary, \
but the build produced no binary artifacts for it — check build.targets \
and that the release is scoped to the right crate",
krate.name,
surfaces.join(" + "),
);
}
Ok(())
}
fn binary_requiring_surfaces(krate: &CrateConfig) -> Vec<&'static str> {
let mut surfaces = Vec::new();
if has_entries(&krate.dockers_v2) {
surfaces.push("dockers_v2");
}
if has_entries(&krate.nfpms) {
surfaces.push("nfpm");
}
if has_entries(&krate.snapcrafts) {
surfaces.push("snapcraft");
}
if has_entries(&krate.dmgs) {
surfaces.push("dmg");
}
if has_entries(&krate.msis) {
surfaces.push("msi");
}
if has_entries(&krate.pkgs) {
surfaces.push("pkg");
}
if has_entries(&krate.nsis) {
surfaces.push("nsis");
}
if has_entries(&krate.flatpaks) {
surfaces.push("flatpak");
}
if has_entries(&krate.app_bundles) {
surfaces.push("app_bundle");
}
if binstall_enabled(krate) {
surfaces.push("binstall");
}
if let Some(publish) = &krate.publish {
surfaces.extend(binary_requiring_publishers(publish));
}
surfaces
}
fn binary_requiring_publishers(publish: &PublishConfig) -> Vec<&'static str> {
let mut names = Vec::new();
if publish.homebrew.is_some() {
names.push("homebrew");
}
if publish.homebrew_cask.is_some() {
names.push("homebrew_cask");
}
if publish.scoop.is_some() {
names.push("scoop");
}
if publish.chocolatey.is_some() {
names.push("chocolatey");
}
if publish.winget.is_some() {
names.push("winget");
}
names
}
fn has_entries<T>(field: &Option<Vec<T>>) -> bool {
field.as_ref().is_some_and(|v| !v.is_empty())
}
fn binstall_enabled(krate: &CrateConfig) -> bool {
krate
.binstall
.as_ref()
.is_some_and(|b| b.enabled == Some(true))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::artifact::Artifact;
use crate::config::{BinstallConfig, DockerV2Config, ScoopConfig};
use std::collections::HashMap;
use std::path::PathBuf;
fn crate_named(name: &str) -> CrateConfig {
CrateConfig {
name: name.to_string(),
..CrateConfig::default()
}
}
fn binary_artifact(crate_name: &str) -> Artifact {
Artifact {
kind: ArtifactKind::Binary,
path: PathBuf::from(format!("dist/{crate_name}")),
name: crate_name.to_string(),
target: Some("x86_64-unknown-linux-gnu".to_string()),
crate_name: crate_name.to_string(),
metadata: HashMap::new(),
size: None,
}
}
fn source_artifact(crate_name: &str) -> Artifact {
Artifact {
kind: ArtifactKind::SourceArchive,
path: PathBuf::from(format!("dist/{crate_name}.tar.gz")),
name: format!("{crate_name}.tar.gz"),
target: None,
crate_name: crate_name.to_string(),
metadata: HashMap::new(),
size: None,
}
}
fn config_with(krate: CrateConfig) -> Config {
Config {
crates: vec![krate],
..Config::default()
}
}
#[test]
fn errors_when_binary_surface_configured_but_no_binary() {
let mut krate = crate_named("svc");
krate.dockers_v2 = Some(vec![DockerV2Config::default()]);
krate.publish = Some(PublishConfig {
scoop: Some(ScoopConfig::default()),
..PublishConfig::default()
});
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("svc"));
let err = check(&config, &artifacts, &[], None)
.unwrap_err()
.to_string();
assert!(err.contains("crate 'svc'"), "{err}");
assert!(err.contains("dockers_v2"), "{err}");
assert!(err.contains("scoop"), "{err}");
assert!(err.contains("no binary artifacts"), "{err}");
}
#[test]
fn ok_for_library_crate_with_no_binary_surface() {
let config = config_with(crate_named("libonly"));
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("libonly"));
check(&config, &artifacts, &[], None).expect("library crate must pass");
}
#[test]
fn ok_when_binary_surface_has_binary() {
let mut krate = crate_named("svc");
krate.dockers_v2 = Some(vec![DockerV2Config::default()]);
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(binary_artifact("svc"));
check(&config, &artifacts, &[], None).expect("binary present must pass");
}
#[test]
fn ok_when_archive_wraps_binary() {
let mut krate = crate_named("svc");
krate.dockers_v2 = Some(vec![DockerV2Config::default()]);
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(Artifact {
kind: ArtifactKind::Archive,
path: PathBuf::from("dist/svc.tar.gz"),
name: "svc.tar.gz".to_string(),
target: Some("x86_64-unknown-linux-gnu".to_string()),
crate_name: "svc".to_string(),
metadata: HashMap::new(),
size: None,
});
check(&config, &artifacts, &[], None).expect("archive-wrapped binary must pass");
}
#[test]
fn empty_surface_list_does_not_arm_guard() {
let mut krate = crate_named("svc");
krate.dockers_v2 = Some(vec![]);
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("svc"));
check(&config, &artifacts, &[], None).expect("empty surface list must pass");
}
#[test]
fn out_of_scope_crate_is_not_checked() {
let mut bad = crate_named("svc");
bad.dockers_v2 = Some(vec![DockerV2Config::default()]);
let config = config_with(bad);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("svc"));
check(&config, &artifacts, &["other".to_string()], None)
.expect("out-of-scope crate must not be checked");
}
#[test]
fn skips_crate_absent_from_built_set() {
let mut krate = crate_named("cfgd-csi");
krate.dockers_v2 = Some(vec![DockerV2Config::default()]);
let config = config_with(krate);
let artifacts = ArtifactRegistry::new();
let built: HashSet<String> = ["cfgd".to_string()].into_iter().collect();
check(&config, &artifacts, &[], Some(&built))
.expect("crate with no in-scope target must be skipped");
}
#[test]
fn bails_when_built_crate_has_no_binary() {
let mut krate = crate_named("svc");
krate.dockers_v2 = Some(vec![DockerV2Config::default()]);
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("svc"));
let built: HashSet<String> = ["svc".to_string()].into_iter().collect();
let err = check(&config, &artifacts, &[], Some(&built))
.unwrap_err()
.to_string();
assert!(err.contains("crate 'svc'"), "{err}");
assert!(err.contains("no binary artifacts"), "{err}");
}
#[test]
fn errors_when_binstall_enabled_but_no_binary() {
let mut krate = crate_named("tool");
krate.binstall = Some(BinstallConfig {
enabled: Some(true),
..BinstallConfig::default()
});
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("tool"));
let err = check(&config, &artifacts, &[], None)
.unwrap_err()
.to_string();
assert!(err.contains("crate 'tool'"), "{err}");
assert!(err.contains("binstall"), "{err}");
assert!(err.contains("no binary artifacts"), "{err}");
}
#[test]
fn binstall_enabled_with_binary_passes() {
let mut krate = crate_named("tool");
krate.binstall = Some(BinstallConfig {
enabled: Some(true),
..BinstallConfig::default()
});
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(binary_artifact("tool"));
check(&config, &artifacts, &[], None).expect("binstall + binary must pass");
}
#[test]
fn binstall_disabled_does_not_arm_guard() {
let mut krate = crate_named("lib");
krate.binstall = Some(BinstallConfig {
enabled: Some(false),
..BinstallConfig::default()
});
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("lib"));
check(&config, &artifacts, &[], None).expect("disabled binstall must not arm guard");
}
#[test]
fn binstall_enabled_per_crate_scoped_check() {
let mut bad = crate_named("bin-crate");
bad.binstall = Some(BinstallConfig {
enabled: Some(true),
..BinstallConfig::default()
});
let mut other = crate_named("other");
other.binstall = Some(BinstallConfig {
enabled: Some(true),
..BinstallConfig::default()
});
let config = Config {
crates: vec![bad, other],
..Config::default()
};
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("bin-crate"));
artifacts.add(binary_artifact("other"));
check(&config, &artifacts, &["other".to_string()], None)
.expect("in-scope binstall crate with a binary must pass");
let err = check(&config, &artifacts, &["bin-crate".to_string()], None)
.unwrap_err()
.to_string();
assert!(err.contains("crate 'bin-crate'"), "{err}");
assert!(err.contains("binstall"), "{err}");
}
#[test]
fn workspace_only_crate_with_missing_binary_is_caught() {
let mut member = crate_named("ws-svc");
member.dockers_v2 = Some(vec![DockerV2Config::default()]);
let config = Config {
workspaces: Some(vec![crate::config::WorkspaceConfig {
name: "grp".to_string(),
crates: vec![member],
..crate::config::WorkspaceConfig::default()
}]),
..Config::default()
};
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("ws-svc"));
let err = check(&config, &artifacts, &[], None)
.unwrap_err()
.to_string();
assert!(err.contains("crate 'ws-svc'"), "{err}");
assert!(err.contains("dockers_v2"), "{err}");
assert!(err.contains("no binary artifacts"), "{err}");
}
#[test]
fn none_built_set_still_bails_on_missing_binary() {
let mut krate = crate_named("svc");
krate.dockers_v2 = Some(vec![DockerV2Config::default()]);
let config = config_with(krate);
let mut artifacts = ArtifactRegistry::new();
artifacts.add(source_artifact("svc"));
let err = check(&config, &artifacts, &[], None)
.unwrap_err()
.to_string();
assert!(err.contains("crate 'svc'"), "{err}");
assert!(err.contains("no binary artifacts"), "{err}");
}
}