use super::*;
pub(super) fn check_workspaces(
config: &Config,
all_crate_names: &HashSet<&str>,
errors: &mut Vec<String>,
) {
let Some(ref workspaces) = config.workspaces else {
return;
};
let mut seen_names: HashSet<&str> = HashSet::new();
for (i, ws) in workspaces.iter().enumerate() {
if ws.name.trim().is_empty() {
errors.push(format!("workspace at index {}: name must not be empty", i));
} else if !seen_names.insert(ws.name.as_str()) {
errors.push(format!("duplicate workspace name '{}'", ws.name));
}
}
for ws in workspaces {
let mut ws_crate_names: HashSet<&str> = HashSet::new();
for (i, c) in ws.crates.iter().enumerate() {
if c.name.trim().is_empty() {
errors.push(format!(
"workspace '{}': crate at index {}: name must not be empty",
ws.name, i
));
} else if !ws_crate_names.insert(c.name.as_str()) {
errors.push(format!(
"workspace '{}': duplicate crate name '{}'",
ws.name, c.name
));
}
}
for c in &ws.crates {
validate_tag_template(
c.tag_template.as_deref().unwrap_or(""),
&format!("workspace '{}': crate '{}'", ws.name, c.name),
errors,
);
}
for c in &ws.crates {
if let Some(deps) = &c.depends_on {
for dep in deps {
if !all_crate_names.contains(dep.as_str()) {
errors.push(format!(
"workspace '{}': crate '{}': depends_on '{}' does not exist",
ws.name, c.name, dep
));
}
}
}
}
}
}
pub(super) fn check_top_level_crate_names(config: &Config, errors: &mut Vec<String>) {
for (i, c) in config.crates.iter().enumerate() {
if c.name.trim().is_empty() {
errors.push(format!("crate at index {}: name must not be empty", i));
}
}
}
pub(super) fn check_top_level_depends_on(
config: &Config,
all_crate_names: &HashSet<&str>,
errors: &mut Vec<String>,
) {
for c in &config.crates {
if let Some(deps) = &c.depends_on {
for dep in deps {
if !all_crate_names.contains(dep.as_str()) {
errors.push(format!(
"crate '{}': depends_on '{}' does not exist",
c.name, dep
));
}
}
}
}
}
pub(super) fn check_cycles(config: &Config, errors: &mut Vec<String>) {
let universe: Vec<CrateConfig> = config.crate_universe().into_iter().cloned().collect();
if let Some(cycle) = find_cycle(&universe) {
errors.push(format!("depends_on cycle detected: {}", cycle.join(" → ")));
}
}
pub(super) fn check_top_level_tag_templates(config: &Config, errors: &mut Vec<String>) {
for c in &config.crates {
validate_tag_template(
c.tag_template.as_deref().unwrap_or(""),
&format!("crate '{}'", c.name),
errors,
);
}
}
pub(super) fn check_copy_from(config: &Config, errors: &mut Vec<String>) {
for c in config.crate_universe() {
if let Some(builds) = &c.builds {
let effective: Vec<&str> = builds
.iter()
.map(|b| b.binary.as_deref().unwrap_or(c.name.as_str()))
.collect();
let binaries: HashSet<&str> = effective.iter().copied().collect();
for (idx, build) in builds.iter().enumerate() {
let bin = effective[idx];
if let Some(copy_from) = &build.copy_from
&& !binaries.contains(copy_from.as_str())
{
errors.push(format!(
"crate '{}': build binary '{}' has copy_from '{}' which is not a binary in this crate",
c.name, bin, copy_from
));
}
}
}
}
}
pub(super) fn check_crate_paths(config: &Config, errors: &mut Vec<String>) {
for c in config.crate_universe() {
if !c.path.is_empty() {
let p = std::path::Path::new(&c.path);
if !p.exists() {
errors.push(format!(
"crate '{}': path '{}' does not exist",
c.name, c.path
));
}
}
}
}
pub(super) fn crate_has_active_cargo_publisher(c: &CrateConfig) -> bool {
c.publish
.as_ref()
.and_then(|p| p.cargo.as_ref())
.is_some_and(|cargo| !cargo.skip.as_ref().is_some_and(|s| s.as_bool()))
}
pub(super) fn find_cargo_workspace_root(start_dir: &Path) -> PathBuf {
let mut dir = start_dir;
loop {
let content = std::fs::read_to_string(dir.join("Cargo.toml")).ok();
let parsed = content.and_then(|s| toml::from_str::<toml::Value>(&s).ok());
let has_workspace_table = parsed.is_some_and(|doc| {
doc.get("workspace")
.and_then(toml::Value::as_table)
.is_some()
});
if has_workspace_table {
return dir.to_path_buf();
}
match dir.parent() {
Some(parent) => dir = parent,
None => return start_dir.to_path_buf(),
}
}
}
pub(super) fn check_workspace_membership(
config: &Config,
base_dir: &Path,
all_crate_names: &HashSet<&str>,
errors: &mut Vec<String>,
) {
let mut member_cache: HashMap<PathBuf, HashSet<String>> = HashMap::new();
for c in config.crate_universe() {
if c.path.is_empty() || !crate_has_active_cargo_publisher(c) {
continue;
}
let crate_dir = base_dir.join(&c.path);
let root = find_cargo_workspace_root(&crate_dir);
let member_names = member_cache
.entry(root.clone())
.or_insert_with(|| anodizer_core::config::discover_cargo_workspace_member_names(&root));
if member_names.is_empty() {
continue;
}
let deps =
anodizer_core::config::derive_depends_on_from_cargo_toml(&crate_dir, member_names);
for dep in deps {
if !all_crate_names.contains(dep.as_str()) {
errors.push(format!(
"crate '{dep}' is a workspace member and an intra-workspace \
dependency of published crate '{}', but is absent from \
`crates:` (cargo will fail publishing '{}')",
c.name, c.name
));
} else if let Some(dep_cfg) = config.find_crate(dep.as_str())
&& !crate_has_active_cargo_publisher(dep_cfg)
{
errors.push(format!(
"crate '{dep}' is an intra-workspace dependency of published \
crate '{}' but has no active cargo publisher (skipped or \
never configured for crates.io) — cargo will fail publishing \
'{}' because '{dep}' is never uploaded to the registry",
c.name, c.name
));
}
}
}
}