use crate::build_spec::{BuildSpec, CrateSource, DepKind};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "rule", rename_all = "kebab-case")]
pub enum Violation {
UnresolvedDep {
from: String,
dep_name: String,
missing_key: String,
},
RegistryWithoutSha256 { crate_key: String, name: String },
WorkspaceMemberNotInCrates { key: String },
RootCrateNotInCrates { key: String },
DevDepInRuntimeOrBuild {
from: String,
dep_name: String,
package_key: String,
},
RenameVersionMismatch {
from: String,
canonical_name: String,
version: String,
},
DuplicateCrateKey { key: String },
MissingBuildRustCrateName { crate_key: String, name: String },
MissingUniversalPreBuild { crate_key: String, name: String },
RegistryUrlNotCanonical { crate_key: String, name: String, url: String },
StaleSchemaVersion { found: u32, expected: u32 },
WorkspaceMemberMissingLibTarget { key: String, name: String },
QuirkRegisteredButNotEmitted {
crate_key: String,
name: String,
expected_quirks: usize,
},
}
#[must_use]
pub fn check(spec: &BuildSpec) -> Vec<Violation> {
let mut out = Vec::new();
check_unresolved_deps(spec, &mut out);
check_registry_sha256(spec, &mut out);
check_workspace_members(spec, &mut out);
check_root_crate(spec, &mut out);
check_no_dev_in_runtime(spec, &mut out);
check_renames(spec, &mut out);
check_build_rust_crate_args(spec, &mut out);
check_registry_url_canonical(spec, &mut out);
check_schema_version(spec, &mut out);
check_workspace_member_lib_targets(spec, &mut out);
check_quirks_registry_consistency(spec, &mut out);
out
}
fn check_build_rust_crate_args(spec: &BuildSpec, out: &mut Vec<Violation>) {
for (key, c) in &spec.crates {
let args = &c.build_rust_crate_args;
if args.crate_name.is_none() {
out.push(Violation::MissingBuildRustCrateName {
crate_key: key.clone(),
name: c.name.clone(),
});
}
if args.pre_build.is_none() {
out.push(Violation::MissingUniversalPreBuild {
crate_key: key.clone(),
name: c.name.clone(),
});
}
}
}
fn check_registry_url_canonical(spec: &BuildSpec, out: &mut Vec<Violation>) {
for (key, c) in &spec.crates {
if let CrateSource::Registry { url, .. } = &c.source {
if url.starts_with("https://crates.io/api/v1/crates/") {
out.push(Violation::RegistryUrlNotCanonical {
crate_key: key.clone(),
name: c.name.clone(),
url: url.clone(),
});
}
}
}
}
fn check_quirks_registry_consistency(spec: &BuildSpec, out: &mut Vec<Violation>) {
for registered in crate::quirks::registered_crate_names() {
for (key, c) in &spec.crates {
if c.name == registered && c.quirks.is_empty() {
let expected = crate::quirks::for_crate(registered).len();
out.push(Violation::QuirkRegisteredButNotEmitted {
crate_key: key.clone(),
name: c.name.clone(),
expected_quirks: expected,
});
}
}
}
}
fn check_schema_version(spec: &BuildSpec, out: &mut Vec<Violation>) {
if spec.version < crate::build_spec::SCHEMA_VERSION {
out.push(Violation::StaleSchemaVersion {
found: spec.version,
expected: crate::build_spec::SCHEMA_VERSION,
});
}
}
fn check_workspace_member_lib_targets(spec: &BuildSpec, out: &mut Vec<Violation>) {
for key in &spec.workspace_members {
let Some(c) = spec.crates.get(key) else {
continue; };
if c.binaries.is_empty() && c.lib_target.is_none() {
if matches!(c.source, CrateSource::Path { .. }) {
out.push(Violation::WorkspaceMemberMissingLibTarget {
key: key.clone(),
name: c.name.clone(),
});
}
}
}
}
fn check_unresolved_deps(spec: &BuildSpec, out: &mut Vec<Violation>) {
for (from_key, crate_spec) in &spec.crates {
for dep in &crate_spec.dependencies {
if !spec.crates.contains_key(&dep.package_key) {
out.push(Violation::UnresolvedDep {
from: from_key.clone(),
dep_name: dep.name.clone(),
missing_key: dep.package_key.clone(),
});
}
}
}
}
fn check_registry_sha256(spec: &BuildSpec, out: &mut Vec<Violation>) {
for (key, crate_spec) in &spec.crates {
if let CrateSource::Registry { sha256, .. } = &crate_spec.source {
if sha256.is_empty() {
out.push(Violation::RegistryWithoutSha256 {
crate_key: key.clone(),
name: crate_spec.name.clone(),
});
}
}
}
}
fn check_workspace_members(spec: &BuildSpec, out: &mut Vec<Violation>) {
for key in &spec.workspace_members {
if !spec.crates.contains_key(key) {
out.push(Violation::WorkspaceMemberNotInCrates { key: key.clone() });
}
}
}
fn check_root_crate(spec: &BuildSpec, out: &mut Vec<Violation>) {
if spec.root_crate.is_empty() {
return;
}
if !spec.crates.contains_key(&spec.root_crate) {
out.push(Violation::RootCrateNotInCrates {
key: spec.root_crate.clone(),
});
}
}
fn check_no_dev_in_runtime(spec: &BuildSpec, out: &mut Vec<Violation>) {
for (from_key, crate_spec) in &spec.crates {
for dep in &crate_spec.runtime_dependencies {
if matches!(dep.kind, DepKind::Dev) {
out.push(Violation::DevDepInRuntimeOrBuild {
from: from_key.clone(),
dep_name: dep.name.clone(),
package_key: dep.package_key.clone(),
});
}
}
for dep in &crate_spec.build_dependencies {
if matches!(dep.kind, DepKind::Dev) {
out.push(Violation::DevDepInRuntimeOrBuild {
from: from_key.clone(),
dep_name: dep.name.clone(),
package_key: dep.package_key.clone(),
});
}
}
}
}
fn check_renames(spec: &BuildSpec, out: &mut Vec<Violation>) {
for (from_key, crate_spec) in &spec.crates {
for (canonical_name, records) in &crate_spec.crate_renames {
for r in records {
let matches_some = spec
.crates
.values()
.any(|c| c.name == *canonical_name && c.version == r.version);
if !matches_some {
out.push(Violation::RenameVersionMismatch {
from: from_key.clone(),
canonical_name: canonical_name.clone(),
version: r.version.clone(),
});
}
}
}
}
}
pub fn assert_well_formed(spec: &BuildSpec) -> Result<(), Vec<Violation>> {
let v = check(spec);
if v.is_empty() {
Ok(())
} else {
Err(v)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::build_spec::{
CrateDepSpec, CrateRenameRecord, CrateSpec, WorkspaceMemberSpec, WorkspaceSpec,
};
use std::collections::BTreeMap;
fn empty_spec() -> BuildSpec {
BuildSpec {
version: crate::build_spec::SCHEMA_VERSION,
workspace: WorkspaceSpec {
root: "/x".into(),
members: vec![],
},
crates: BTreeMap::new(),
root_crate: String::new(),
workspace_members: vec![],
flake_metadata: BTreeMap::new(),
target_resolves: None,
cargo_lock_sha256: None,
}
}
fn crate_at(key: &str, name: &str, version: &str, source: CrateSource) -> (String, CrateSpec) {
let rustc_crate_name = name.replace('-', "_");
let pre_build = format!("export CARGO_CRATE_NAME={};", rustc_crate_name);
let build_rust_crate_args = crate::build_spec::BuildRustCrateArgs {
crate_name: Some(name.into()),
version: Some(version.into()),
edition: Some("2024".into()),
features: vec![],
crate_renames: BTreeMap::new(),
release: Some(true),
proc_macro: None,
build: None,
links: None,
lib_name: None,
lib_path: None,
pre_build: Some(pre_build),
};
(
key.into(),
CrateSpec {
name: name.into(),
version: version.into(),
edition: "2024".into(),
source,
features: vec![],
proc_macro: false,
build_script: None,
links: None,
quirks: vec![],
build_rust_crate_args,
binaries: vec![],
lib_target: None,
dependencies: vec![],
runtime_dependencies: vec![],
build_dependencies: vec![],
crate_renames: BTreeMap::new(),
},
)
}
fn registry_src(sha: &str) -> CrateSource {
CrateSource::Registry {
url: "https://crates.io/x".into(),
sha256: sha.into(),
name_with_ext: "x.tar.gz".into(),
}
}
fn path_src() -> CrateSource {
CrateSource::Path {
relative_path: ".".into(),
}
}
fn dep(name: &str, key: &str, kind: DepKind) -> CrateDepSpec {
use crate::build_spec::BuildTree;
CrateDepSpec {
name: name.into(),
package_key: key.into(),
kind,
features: vec![],
uses_default_features: true,
optional: false,
target: None,
tree: BuildTree::Target,
}
}
#[test]
fn empty_spec_is_well_formed() {
let s = empty_spec();
assert!(assert_well_formed(&s).is_ok());
}
#[test]
fn unresolved_dep_is_caught() {
let mut s = empty_spec();
let (k, mut c) = crate_at("a-1.0.0", "a", "1.0.0", path_src());
c.dependencies = vec![dep("b", "b-2.0.0", DepKind::Normal)];
c.runtime_dependencies = c.dependencies.clone();
s.crates.insert(k, c);
let v = check(&s);
assert!(matches!(
v.first(),
Some(Violation::UnresolvedDep {
from,
dep_name,
missing_key,
}) if from == "a-1.0.0" && dep_name == "b" && missing_key == "b-2.0.0"
));
}
#[test]
fn registry_without_sha256_is_caught() {
let mut s = empty_spec();
let (k, c) = crate_at("a-1.0.0", "a", "1.0.0", registry_src(""));
s.crates.insert(k, c);
let v = check(&s);
assert!(matches!(
v.first(),
Some(Violation::RegistryWithoutSha256 { .. })
));
}
#[test]
fn registry_with_sha256_is_fine() {
let mut s = empty_spec();
let (k, c) = crate_at("a-1.0.0", "a", "1.0.0", registry_src("deadbeef"));
s.crates.insert(k, c);
assert!(assert_well_formed(&s).is_ok());
}
#[test]
fn workspace_member_not_in_crates_is_caught() {
let mut s = empty_spec();
s.workspace_members = vec!["missing-0.1.0".into()];
let v = check(&s);
assert!(matches!(
v.first(),
Some(Violation::WorkspaceMemberNotInCrates { key }) if key == "missing-0.1.0"
));
}
#[test]
fn root_crate_not_in_crates_is_caught() {
let mut s = empty_spec();
s.root_crate = "ghost-0.0.0".into();
let v = check(&s);
assert!(matches!(
v.first(),
Some(Violation::RootCrateNotInCrates { .. })
));
}
#[test]
fn dev_dep_in_runtime_is_caught() {
let mut s = empty_spec();
let (k1, c1) = crate_at("a-1.0.0", "a", "1.0.0", path_src());
let (k2, c2) = crate_at("b-2.0.0", "b", "2.0.0", path_src());
s.crates.insert(k2.clone(), c2);
let mut a = c1;
a.runtime_dependencies = vec![dep("b", &k2, DepKind::Dev)];
s.crates.insert(k1.clone(), a);
let v = check(&s);
assert!(v
.iter()
.any(|x| matches!(x, Violation::DevDepInRuntimeOrBuild { .. })));
}
#[test]
fn rename_pointing_to_nonexistent_version_is_caught() {
let mut s = empty_spec();
let (k1, mut c1) = crate_at("a-1.0.0", "a", "1.0.0", path_src());
let (k2, c2) = crate_at("b-2.0.0", "b", "2.0.0", path_src());
c1.crate_renames.insert(
"b".into(),
vec![CrateRenameRecord {
version: "9.9.9".into(),
rename: "bee".into(),
}],
);
s.crates.insert(k1, c1);
s.crates.insert(k2, c2);
let v = check(&s);
assert!(matches!(
v.first(),
Some(Violation::RenameVersionMismatch { .. })
));
}
#[test]
fn rename_pointing_to_existing_crate_is_fine() {
let mut s = empty_spec();
let (k1, mut c1) = crate_at("a-1.0.0", "a", "1.0.0", path_src());
let (k2, c2) = crate_at("b-2.0.0", "b", "2.0.0", path_src());
c1.crate_renames.insert(
"b".into(),
vec![CrateRenameRecord {
version: "2.0.0".into(),
rename: "bee".into(),
}],
);
s.crates.insert(k1, c1);
s.crates.insert(k2, c2);
assert!(assert_well_formed(&s).is_ok());
}
#[test]
fn generate_is_deterministic_against_caixa_sha2() {
let root = std::path::Path::new("/Users/drzzln/code/github/pleme-io/caixa-sha2");
if !root.exists() {
return; }
let a = crate::build_spec::generate(root);
let b = crate::build_spec::generate(root);
let (a, b) = match (a, b) {
(Ok(a), Ok(b)) => (a, b),
_ => return,
};
let ja = serde_json::to_string(&a).unwrap();
let jb = serde_json::to_string(&b).unwrap();
assert_eq!(ja, jb, "spec must be byte-deterministic across runs");
}
#[test]
fn workspace_member_with_no_targets_is_caught() {
let mut s = empty_spec();
let (k, c) = crate_at("ishou-render-0.1.0", "ishou-render", "0.1.0", path_src());
s.crates.insert(k.clone(), c);
s.workspace_members.push(k.clone());
s.root_crate = k.clone();
let v = check(&s);
assert!(
v.iter().any(|x| matches!(
x,
Violation::WorkspaceMemberMissingLibTarget { key, .. } if key == "ishou-render-0.1.0"
)),
"expected WorkspaceMemberMissingLibTarget for ishou-render, got: {v:?}"
);
}
#[test]
fn workspace_member_with_lib_target_is_well_formed() {
use crate::build_spec::LibTargetSpec;
let mut s = empty_spec();
let (k, mut c) = crate_at("ishou-render-0.1.0", "ishou-render", "0.1.0", path_src());
c.lib_target = Some(LibTargetSpec {
name: "ishou_render".into(),
path: "src/lib.rs".into(),
});
s.crates.insert(k.clone(), c);
s.workspace_members.push(k.clone());
s.root_crate = k;
let v = check(&s);
assert!(
!v.iter()
.any(|x| matches!(x, Violation::WorkspaceMemberMissingLibTarget { .. })),
"expected no WorkspaceMemberMissingLibTarget when lib_target is Some, got: {v:?}"
);
}
#[test]
fn workspace_member_with_only_binaries_is_well_formed() {
use crate::build_spec::CrateBinSpec;
let mut s = empty_spec();
let (k, mut c) = crate_at("ryn-cli-0.1.0", "ryn-cli", "0.1.0", path_src());
c.binaries = vec![CrateBinSpec {
name: "ryn-cli".into(),
path: "src/main.rs".into(),
}];
s.crates.insert(k.clone(), c);
s.workspace_members.push(k.clone());
s.root_crate = k;
let v = check(&s);
assert!(
!v.iter()
.any(|x| matches!(x, Violation::WorkspaceMemberMissingLibTarget { .. })),
"expected no WorkspaceMemberMissingLibTarget when binaries is non-empty, got: {v:?}"
);
}
#[test]
fn live_fleet_spec_is_well_formed() {
let root = std::path::Path::new("/Users/drzzln/code/github/pleme-io/caixa-sha2");
if !root.exists() {
return;
}
let Ok(spec) = crate::build_spec::generate(root) else { return };
match assert_well_formed(&spec) {
Ok(()) => (),
Err(violations) => panic!(
"live fleet spec violated invariants:\n{}",
serde_json::to_string_pretty(&violations).unwrap()
),
}
}
}