use anodizer_core::config::{Config, WorkspaceConfig};
use anodizer_core::log::StageLogger;
use anyhow::Result;
pub fn collect_build_targets(config: &Config, selected_crates: &[String]) -> Vec<String> {
let mut targets: Vec<String> = Vec::new();
let default_targets = config.effective_default_targets();
for krate in config.crate_universe() {
if !selected_crates.is_empty() && !selected_crates.contains(&krate.name) {
continue;
}
for t in anodizer_core::build_plan::crate_target_list(krate, &default_targets) {
if !targets.contains(&t) {
targets.push(t);
}
}
}
if let Some(ignores) = config
.defaults
.as_ref()
.and_then(|d| d.builds.as_ref())
.and_then(|b| b.ignore.as_ref())
{
targets.retain(|t| {
let (os, arch) = anodizer_core::target::map_target(t);
!ignores.iter().any(|ig| ig.os == os && ig.arch == arch)
});
}
targets
}
pub fn apply_workspace_overlay(config: &mut Config, ws: &WorkspaceConfig) {
config.crates = ws.crates.clone();
config.workspaces = None;
if ws.changelog.is_some() {
config.changelog = ws.changelog.clone();
}
if !ws.signs.is_empty() {
config.signs = ws.signs.clone();
}
if !ws.binary_signs.is_empty() {
config.binary_signs = ws.binary_signs.clone();
}
if ws.before.is_some() {
config.before = ws.before.clone();
}
if ws.after.is_some() {
config.after = ws.after.clone();
}
if let Some(ref env_list) = ws.env {
let merged = config.env.get_or_insert_with(Vec::new);
merged.extend(env_list.iter().cloned());
}
}
pub fn workspace_containing_crate<'a>(
config: &'a Config,
name: &str,
) -> Option<&'a WorkspaceConfig> {
if config.crates.iter().any(|c| c.name == name) {
return None;
}
config
.workspaces
.iter()
.flatten()
.find(|ws| ws.crates.iter().any(|c| c.name == name))
}
pub fn resolve_workspace<'a>(config: &'a Config, name: &str) -> Result<&'a WorkspaceConfig> {
let workspaces = config.workspaces.as_ref().ok_or_else(|| {
anyhow::anyhow!("--workspace specified but no workspaces defined in config")
})?;
workspaces.iter().find(|ws| ws.name == name).ok_or_else(|| {
let available: Vec<&str> = workspaces.iter().map(|ws| ws.name.as_str()).collect();
anyhow::anyhow!(
"workspace '{}' not found (available: {})",
name,
available.join(", ")
)
})
}
pub fn apply_workspace_scope(
config: &mut Config,
workspace: Option<&str>,
crate_names: &[String],
log: &StageLogger,
) -> Result<Vec<String>> {
let mut workspace_skip: Vec<String> = Vec::new();
let mut applied_ws: Option<String> = workspace.map(str::to_string);
if let Some(ws_name) = workspace {
let ws = resolve_workspace(config, ws_name)?.clone();
workspace_skip = ws.skip.clone();
apply_workspace_overlay(config, &ws);
} else if let Some(ws_name) = infer_workspace_for_selection(config, crate_names)? {
log.verbose(&format!(
"--crate selection lives in workspace '{}'; applying workspace overlay",
ws_name
));
let ws = resolve_workspace(config, &ws_name)?.clone();
workspace_skip = ws.skip.clone();
apply_workspace_overlay(config, &ws);
applied_ws = Some(ws_name);
}
validate_selection_against_universe(config, crate_names, applied_ws.as_deref())?;
Ok(workspace_skip)
}
pub fn infer_workspace_for_selection(
config: &Config,
crate_names: &[String],
) -> Result<Option<String>> {
if crate_names.is_empty() {
return Ok(None);
}
let mut homes: Vec<(String, String)> = Vec::new();
let mut ws_names: Vec<String> = Vec::new();
let mut has_top_level = false;
for name in crate_names {
if config.crates.iter().any(|c| &c.name == name) {
has_top_level = true;
homes.push((name.clone(), "top-level".to_string()));
} else if let Some(ws) = workspace_containing_crate(config, name) {
if !ws_names.contains(&ws.name) {
ws_names.push(ws.name.clone());
}
homes.push((name.clone(), format!("workspace '{}'", ws.name)));
}
}
if ws_names.is_empty() {
return Ok(None);
}
if has_top_level || ws_names.len() > 1 {
let listing = homes
.iter()
.map(|(name, home)| format!("'{name}' ({home})"))
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(
"--crate selection spans multiple release scopes: {listing}. One run applies a \
single workspace's overlay (env/signs/skip), so a mixed selection cannot release \
every named crate correctly — select crates from one scope per run (or pass \
--workspace <name>)"
);
}
Ok(Some(ws_names.remove(0)))
}
pub fn validate_selection_against_universe(
config: &Config,
crate_names: &[String],
scope: Option<&str>,
) -> Result<()> {
let universe: Vec<&str> = config
.crate_universe()
.into_iter()
.map(|c| c.name.as_str())
.collect();
let unknown: Vec<&str> = crate_names
.iter()
.map(|n| n.as_str())
.filter(|n| !universe.contains(n))
.collect();
if unknown.is_empty() {
return Ok(());
}
let known = if universe.is_empty() {
"(none)".to_string()
} else {
universe.join(", ")
};
match scope {
Some(ws) => anyhow::bail!(
"--crate {}: not in workspace '{}' (its crates: {}); a workspace-scoped run \
releases only that workspace's crates",
unknown.join(", "),
ws,
known
),
None if universe.is_empty() => anyhow::bail!(
"--crate {}: the configuration defines no crates; drop --crate to run at the \
repo level, or add a `crates:` entry for '{}'",
unknown.join(", "),
unknown.join(", ")
),
None => anyhow::bail!(
"--crate {}: no such crate in the configuration (known crates: {})",
unknown.join(", "),
known
),
}
}
pub fn merge_skip_stages<S: AsRef<str>>(skip_stages: &mut Vec<String>, extra: &[S]) {
for stage in extra {
let stage = stage.as_ref();
if !skip_stages.iter().any(|s| s == stage) {
skip_stages.push(stage.to_string());
}
}
}