use super::super::helpers;
use crate::pipeline;
use anodizer_core::config::{Config, CrateConfig};
use anodizer_core::log::{StageLogger, Verbosity};
use anyhow::{Result, bail};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
pub fn run(
config_override: Option<&Path>,
workspace: Option<&str>,
publishers: &[String],
verbose: bool,
debug: bool,
quiet: bool,
) -> Result<()> {
let log = StageLogger::new("check", Verbosity::from_flags(quiet, verbose, debug));
let path = pipeline::find_config_with_logger(config_override, Some(&log))?;
log.verbose(&format!("loading config from {}", path.display()));
let mut config = pipeline::load_config_logged(&path, &log)?;
helpers::infer_project_name(&mut config, &log);
if workspace.is_none() {
log.status("validating configuration");
run_checks(&config, true, &log, Path::new("."))?;
}
let overlaid: Option<(&str, Config)> = match workspace {
Some(ws_name) => {
let ws = helpers::resolve_workspace(&config, ws_name)?;
let mut resolved = config.clone();
helpers::apply_workspace_overlay(&mut resolved, ws);
Some((ws_name, resolved))
}
None => None,
};
if !publishers.is_empty() {
let publisher_config = overlaid
.as_ref()
.map(|(_, resolved)| resolved.clone())
.unwrap_or_else(|| config.clone());
let ctx = anodizer_core::context::Context::new(
publisher_config,
anodizer_core::context::ContextOptions::default(),
);
if let Err(msg) = anodizer_stage_publish::registry::validate_publisher_allowlist_configured(
publishers, &ctx,
) {
bail!("{msg}");
}
}
if let Some((ws_name, resolved)) = &overlaid {
log.status(&format!(
"validating resolved config for workspace '{}'",
ws_name
));
run_checks(resolved, true, &log, Path::new("."))?;
}
Ok(())
}
pub fn run_checks(
config: &Config,
check_env: bool,
log: &StageLogger,
base_dir: &Path,
) -> Result<()> {
let mut errors: Vec<String> = vec![];
let mut warnings: Vec<String> = vec![];
let all_crate_names = flatten_crate_names(config);
check_workspaces(config, &all_crate_names, &mut errors);
check_top_level_crate_names(config, &mut errors);
check_top_level_depends_on(config, &all_crate_names, &mut errors);
check_cycles(config, &mut errors);
check_top_level_tag_templates(config, &mut errors);
check_copy_from(config, &mut errors);
check_target_triples(config, &mut warnings);
check_changelog(config, &mut warnings);
check_announce_secret_exposure(config, &mut warnings);
check_checksum_skip_conflicts(config, &mut warnings);
check_crate_paths(config, &mut errors);
check_workspace_membership(config, base_dir, &all_crate_names, &mut errors);
check_sign_artifact_filters(config, &mut warnings);
check_checksum_algorithms(config, &mut warnings);
check_source_format(config, &mut errors);
check_sbom_configs(config, &mut errors);
check_blob_configs(config, &mut errors);
if check_env {
check_environment(config, &mut warnings);
}
report_results(log, &warnings, &errors)
}
fn flatten_crate_names(config: &Config) -> HashSet<&str> {
config
.crate_universe()
.into_iter()
.map(|c| c.name.as_str())
.collect()
}
mod content;
mod structure;
mod tooling;
use content::*;
use structure::*;
use tooling::*;
#[cfg(test)]
mod tests;
fn report_results(log: &StageLogger, warnings: &[String], errors: &[String]) -> Result<()> {
for w in warnings {
log.warn(w);
}
if errors.is_empty() {
log.status("Config is valid.");
Ok(())
} else {
for e in errors {
log.error(e);
}
bail!("config validation failed with {} error(s)", errors.len());
}
}
pub fn find_cycle(crates: &[CrateConfig]) -> Option<Vec<String>> {
let name_to_idx: HashMap<&str, usize> = crates
.iter()
.enumerate()
.map(|(i, c)| (c.name.as_str(), i))
.collect();
let mut adj: Vec<Vec<usize>> = vec![vec![]; crates.len()];
for (i, c) in crates.iter().enumerate() {
if let Some(deps) = &c.depends_on {
for dep in deps {
if let Some(&j) = name_to_idx.get(dep.as_str()) {
adj[i].push(j);
}
}
}
}
let mut color = vec![0u8; crates.len()];
let mut parent = vec![usize::MAX; crates.len()];
for start in 0..crates.len() {
if color[start] != 0 {
continue;
}
let mut stack: Vec<(usize, usize)> = vec![(start, 0)]; color[start] = 1;
while let Some((node, adj_idx)) = stack.last_mut() {
let node = *node;
if *adj_idx < adj[node].len() {
let next = adj[node][*adj_idx];
*adj_idx += 1;
match color[next] {
0 => {
color[next] = 1;
parent[next] = node;
stack.push((next, 0));
}
1 => {
let mut cycle = vec![crates[next].name.clone()];
let mut cur = node;
while cur != next {
cycle.push(crates[cur].name.clone());
cur = parent[cur];
if cur == usize::MAX {
break;
}
}
cycle.push(crates[next].name.clone());
cycle.reverse();
return Some(cycle);
}
_ => {} }
} else {
color[node] = 2;
stack.pop();
}
}
}
None
}
fn validate_tag_template(tag_template: &str, context: &str, errors: &mut Vec<String>) {
if !tag_template.is_empty() && !anodizer_core::git::has_version_placeholder(tag_template) {
errors.push(format!(
"{}: tag_template '{}' must contain '{{{{ .Version }}}}' or '{{{{ Version }}}}' \
(e.g. 'v{{{{ .Version }}}}' or 'myapp-v{{{{ Version }}}}')",
context, tag_template
));
}
}