use std::collections::HashMap;
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum IncludeSpec {
Path(String),
FromFile { from_file: IncludeFilePath },
FromUrl { from_url: IncludeUrlConfig },
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct IncludeFilePath {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct IncludeUrlConfig {
pub url: String,
pub headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub version: Option<u32>,
pub project_name: String,
#[serde(default = "default_dist")]
pub dist: PathBuf,
pub includes: Option<Vec<IncludeSpec>>,
pub env_files: Option<EnvFilesConfig>,
pub defaults: Option<Defaults>,
pub before: Option<HooksConfig>,
pub after: Option<HooksConfig>,
pub crates: Vec<CrateConfig>,
pub changelog: Option<ChangelogConfig>,
#[serde(default, deserialize_with = "deserialize_signs")]
#[schemars(schema_with = "signs_schema")]
pub signs: Vec<SignConfig>,
#[serde(default, deserialize_with = "deserialize_binary_signs")]
#[schemars(schema_with = "signs_schema")]
pub binary_signs: Vec<SignConfig>,
pub docker_signs: Option<Vec<DockerSignConfig>>,
#[serde(default, deserialize_with = "deserialize_upx")]
#[schemars(schema_with = "upx_schema")]
pub upx: Vec<UpxConfig>,
pub snapshot: Option<SnapshotConfig>,
pub nightly: Option<NightlyConfig>,
pub announce: Option<AnnounceConfig>,
pub report_sizes: Option<bool>,
#[serde(default)]
pub env: Option<Vec<String>>,
pub variables: Option<HashMap<String, String>>,
pub publishers: Option<Vec<PublisherConfig>>,
pub dockerhub: Option<Vec<DockerHubConfig>>,
pub artifactories: Option<Vec<ArtifactoryConfig>>,
pub cloudsmiths: Option<Vec<CloudSmithConfig>>,
pub homebrew_casks: Option<Vec<HomebrewCaskConfig>>,
pub tag: Option<TagConfig>,
pub git: Option<GitConfig>,
pub partial: Option<PartialConfig>,
pub workspaces: Option<Vec<WorkspaceConfig>>,
pub source: Option<SourceConfig>,
#[serde(default, deserialize_with = "deserialize_sboms")]
#[schemars(schema_with = "sboms_schema")]
pub sboms: Vec<SbomConfig>,
pub release: Option<ReleaseConfig>,
pub github_urls: Option<GitHubUrlsConfig>,
pub gitlab_urls: Option<GitLabUrlsConfig>,
pub gitea_urls: Option<GiteaUrlsConfig>,
pub force_token: Option<ForceTokenKind>,
pub notarize: Option<NotarizeConfig>,
pub metadata: Option<MetadataConfig>,
pub template_files: Option<Vec<TemplateFileConfig>>,
pub monorepo: Option<MonorepoConfig>,
#[serde(default, deserialize_with = "deserialize_makeselfs")]
#[schemars(schema_with = "makeselfs_schema")]
pub makeselfs: Vec<MakeselfConfig>,
#[serde(alias = "srpm")]
pub srpms: Option<SrpmConfig>,
pub milestones: Option<Vec<MilestoneConfig>>,
pub uploads: Option<Vec<UploadConfig>>,
pub aur_sources: Option<Vec<AurSourceConfig>>,
pub retry: Option<RetryConfig>,
#[serde(default)]
pub mcp: McpConfig,
}
fn signs_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<SignConfig>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description = Some("Artifact signing configurations (cosign, GPG, etc.). Accepts a single object or array.".to_owned());
}
schema
}
fn upx_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<UpxConfig>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description = Some(
"UPX binary compression configurations. Accepts a single object or array.".to_owned(),
);
}
schema
}
fn sboms_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = generator.subschema_for::<Vec<SbomConfig>>();
if let schemars::schema::Schema::Object(ref mut obj) = schema {
obj.metadata().description =
Some("SBOM generation configurations. Accepts a single object or array.".to_owned());
}
schema
}
fn default_dist() -> PathBuf {
PathBuf::from("./dist")
}
impl Default for Config {
fn default() -> Self {
Config {
version: None,
project_name: String::new(),
dist: default_dist(),
includes: None,
env_files: None,
defaults: None,
before: None,
after: None,
crates: Vec::new(),
changelog: None,
signs: Vec::new(),
binary_signs: Vec::new(),
docker_signs: None,
upx: Vec::new(),
snapshot: None,
nightly: None,
announce: None,
report_sizes: None,
env: None,
variables: None,
publishers: None,
dockerhub: None,
artifactories: None,
cloudsmiths: None,
homebrew_casks: None,
tag: None,
git: None,
partial: None,
workspaces: None,
source: None,
sboms: Vec::new(),
release: None,
github_urls: None,
gitlab_urls: None,
gitea_urls: None,
force_token: None,
notarize: None,
metadata: None,
template_files: None,
monorepo: None,
makeselfs: Vec::new(),
srpms: None,
milestones: None,
uploads: None,
aur_sources: None,
retry: None,
mcp: McpConfig::default(),
}
}
}
impl Config {
pub fn monorepo_tag_prefix(&self) -> Option<&str> {
self.monorepo.as_ref().and_then(|m| m.tag_prefix.as_deref())
}
pub fn monorepo_dir(&self) -> Option<&str> {
self.monorepo.as_ref().and_then(|m| m.dir.as_deref())
}
pub fn meta_homepage(&self) -> Option<&str> {
self.metadata.as_ref().and_then(|m| m.homepage.as_deref())
}
pub fn meta_license(&self) -> Option<&str> {
self.metadata.as_ref().and_then(|m| m.license.as_deref())
}
pub fn meta_description(&self) -> Option<&str> {
self.metadata
.as_ref()
.and_then(|m| m.description.as_deref())
}
pub fn meta_maintainers(&self) -> &[String] {
self.metadata
.as_ref()
.and_then(|m| m.maintainers.as_deref())
.unwrap_or(&[])
}
pub fn meta_first_maintainer(&self) -> Option<&str> {
self.meta_maintainers().first().map(|s| s.as_str())
}
pub fn has_gpg_sign_configured(&self) -> bool {
let top_level = self
.signs
.iter()
.chain(self.binary_signs.iter())
.any(|s| s.is_gpg());
if top_level {
return true;
}
self.workspaces.iter().flatten().any(|w| {
w.signs
.iter()
.chain(w.binary_signs.iter())
.any(|s| s.is_gpg())
})
}
}
pub fn deserialize_on_worker<F, T>(f: F) -> anyhow::Result<T>
where
F: FnOnce() -> anyhow::Result<T> + Send + 'static,
T: Send + 'static,
{
use anyhow::Context as _;
const WORKER_STACK_SIZE: usize = 8 * 1024 * 1024;
let handle = std::thread::Builder::new()
.stack_size(WORKER_STACK_SIZE)
.name("anodizer-config-deserialize".to_string())
.spawn(f)
.context("failed to spawn config deserialization worker thread")?;
match handle.join() {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
}
}
pub fn validate_version(config: &Config) -> Result<(), String> {
match config.version {
None | Some(1) | Some(2) => Ok(()),
Some(v) => Err(format!(
"unsupported config version: {}. Supported versions are 1 and 2.",
v
)),
}
}
pub fn validate_tag_sort(config: &Config) -> Result<(), String> {
if let Some(ref git) = config.git
&& let Some(ref sort) = git.tag_sort
{
match sort.as_str() {
"-version:refname" | "-version:creatordate" => {}
other => {
return Err(format!(
"unsupported git.tag_sort value: \"{}\". \
Accepted values: \"-version:refname\", \"-version:creatordate\".",
other
));
}
}
}
Ok(())
}
const KNOWN_GOOS: &[&str] = &[
"aix",
"android",
"darwin",
"dragonfly",
"freebsd",
"illumos",
"ios",
"js",
"linux",
"netbsd",
"openbsd",
"plan9",
"solaris",
"wasip1",
"windows",
];
pub fn validate_release_backends(config: &Config) -> Result<(), String> {
let check = |crate_name: &str, release: &ReleaseConfig| -> Result<(), String> {
let mut set = Vec::new();
if release.github.is_some() {
set.push("github");
}
if release.gitlab.is_some() {
set.push("gitlab");
}
if release.gitea.is_some() {
set.push("gitea");
}
if set.len() > 1 {
return Err(format!(
"crate {}: release config sets multiple mutually-exclusive SCM \
backends ({}). Pick one.",
crate_name,
set.join(" + ")
));
}
Ok(())
};
for krate in &config.crates {
if let Some(ref release) = krate.release {
check(&krate.name, release)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let Some(ref release) = krate.release {
check(&krate.name, release)?;
}
}
}
}
Ok(())
}
pub const ERR_DEFAULTS_AXIS_MISMATCH: &str = "DefaultsAxisMismatch";
pub fn validate_defaults_axis(config: &Config) -> Result<(), String> {
let Some(ref defaults) = config.defaults else {
return Ok(());
};
let has_crate_block = defaults.crates.is_some();
let has_workspace_block = defaults.workspaces.is_some();
if has_crate_block && has_workspace_block {
return Err(format!(
"{ERR_DEFAULTS_AXIS_MISMATCH}: defaults.crates and defaults.workspaces are \
mutually exclusive — pick the axis that matches the top-level config \
(`crates:` or `workspaces:`)",
));
}
let top_uses_workspaces = config.workspaces.as_ref().is_some_and(|w| !w.is_empty());
let top_uses_crates = !config.crates.is_empty();
if has_crate_block && !top_uses_crates {
return Err(format!(
"{ERR_DEFAULTS_AXIS_MISMATCH}: defaults.crates is set but top-level `crates:` \
is {}; move defaults under `defaults.workspaces:` or remove the block",
if top_uses_workspaces {
"absent (top-level uses `workspaces:`)"
} else {
"absent"
},
));
}
if has_workspace_block && !top_uses_workspaces {
return Err(format!(
"{ERR_DEFAULTS_AXIS_MISMATCH}: defaults.workspaces is set but top-level \
`workspaces:` is {}; move defaults under `defaults.crates:` or remove the block",
if top_uses_crates {
"absent (top-level uses `crates:`)"
} else {
"absent"
},
));
}
Ok(())
}
pub fn validate_format_overrides(config: &Config) -> Result<(), String> {
let check = |location: &str, archives: &[ArchiveConfig]| -> Result<(), String> {
for (idx, archive) in archives.iter().enumerate() {
let Some(ref overrides) = archive.format_overrides else {
continue;
};
for over in overrides {
if !KNOWN_GOOS.contains(&over.os.as_str()) {
let archive_id = archive.id.as_deref().unwrap_or("default");
return Err(format!(
"{}: archives[{}] (id={}): format_overrides.goos=\"{}\" is not a recognised OS. \
Accepted values: {}.",
location,
idx,
archive_id,
over.os,
KNOWN_GOOS.join(", ")
));
}
}
}
Ok(())
};
for krate in &config.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check(&format!("crate {}", krate.name), list)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check(&format!("crate {}", krate.name), list)?;
}
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref archive) = defaults.archives
{
check("defaults.archives", std::slice::from_ref(archive))?;
}
Ok(())
}
pub fn validate_homebrew_cask_url_template(config: &Config) -> Result<(), String> {
let check = |location: &str, cask: &HomebrewCaskConfig| -> Result<(), String> {
let has_url_template = cask.url_template.is_some();
let has_url_dot_template = cask.url.as_ref().is_some_and(|u| u.template.is_some());
if has_url_template && has_url_dot_template {
return Err(format!(
"{location}: homebrew_cask sets both `url_template` and `url.template`. \
These are mutually exclusive — use one or the other."
));
}
Ok(())
};
if let Some(ref casks) = config.homebrew_casks {
for (i, cask) in casks.iter().enumerate() {
check(&format!("homebrew_casks[{i}]"), cask)?;
}
}
for krate in &config.crates {
if let Some(ref publish) = krate.publish
&& let Some(ref cask) = publish.homebrew_cask
{
check(
&format!("crates[{}].publish.homebrew_cask", krate.name),
cask,
)?;
}
}
if let Some(ref workspaces) = config.workspaces {
for ws in workspaces {
for krate in &ws.crates {
if let Some(ref publish) = krate.publish
&& let Some(ref cask) = publish.homebrew_cask
{
check(
&format!(
"workspaces[{}].crates[{}].publish.homebrew_cask",
ws.name, krate.name
),
cask,
)?;
}
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref publish) = defaults.publish
&& let Some(ref cask) = publish.homebrew_cask
{
check("defaults.publish.homebrew_cask", cask)?;
}
Ok(())
}
pub fn validate_id_uniqueness(config: &Config) -> Result<(), String> {
fn check_unique<F>(
location: &str,
kind: &str,
ids: impl IntoIterator<Item = (usize, Option<String>)>,
empty_ok: F,
) -> Result<(), String>
where
F: Fn() -> bool,
{
let _ = empty_ok;
let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (idx, maybe_id) in ids {
let key = maybe_id.unwrap_or_else(|| "<unset>".to_string());
if let Some(prev_idx) = seen.insert(key.clone(), idx) {
return Err(format!(
"{location}: {kind} id \"{key}\" is used by both entry {prev_idx} and entry {idx} — \
ids must be unique within a {kind} list."
));
}
}
Ok(())
}
let check_archives = |location: &str, archives: &[ArchiveConfig]| -> Result<(), String> {
check_unique(
location,
"archives",
archives.iter().enumerate().map(|(i, a)| (i, a.id.clone())),
|| true,
)
};
let check_unibins = |location: &str, ubs: &[UniversalBinaryConfig]| -> Result<(), String> {
check_unique(
location,
"universal_binaries",
ubs.iter().enumerate().map(|(i, u)| (i, u.id.clone())),
|| true,
)
};
for krate in &config.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check_archives(&format!("crates[{}].archives", krate.name), list)?;
}
if let Some(ref ubs) = krate.universal_binaries {
check_unibins(&format!("crates[{}].universal_binaries", krate.name), ubs)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check_archives(
&format!("workspaces[{}].crates[{}].archives", ws.name, krate.name),
list,
)?;
}
if let Some(ref ubs) = krate.universal_binaries {
check_unibins(
&format!(
"workspaces[{}].crates[{}].universal_binaries",
ws.name, krate.name
),
ubs,
)?;
}
}
}
}
Ok(())
}
pub fn apply_archive_legacy_aliases(_config: &mut Config) {
}
pub fn validate_no_docker_v1(raw_yaml: &serde_yaml_ng::Value) -> Result<(), String> {
if raw_yaml.get("dockers").is_some() {
return Err(
"config: legacy GoReleaser `dockers:` block is not supported — anodizer ships \
docker_v2: only (multi-arch buildx flow). Port the config to `docker_v2:` per \
https://anodize.dev/docs/migration/docker.html."
.to_string(),
);
}
Ok(())
}
pub fn warn_on_legacy_snapshot_name_template(raw_yaml: &serde_yaml_ng::Value) {
if let Some(snap) = raw_yaml.get("snapshot")
&& snap.get("name_template").is_some()
{
tracing::warn!(
"DEPRECATION: snapshot.name_template is deprecated; use \
snapshot.version_template instead. Both spellings are accepted \
but the legacy key will be removed in a future release."
);
}
}
pub fn apply_build_legacy_aliases(config: &mut Config) {
let warn_one = |location: &str, legacy: &mut Option<String>| {
if let Some(go_bin) = legacy.take() {
tracing::warn!(
"DEPRECATION: {location}: 'gobinary: {go_bin}' is a Go-only field; anodizer \
builds with cargo unconditionally. The value has been ignored."
);
}
};
for krate in &mut config.crates {
if let Some(ref mut builds) = krate.builds {
for (i, b) in builds.iter_mut().enumerate() {
warn_one(
&format!("crates[{}].builds[{i}]", krate.name),
&mut b.legacy_gobinary,
);
}
}
}
if let Some(ref mut workspaces) = config.workspaces {
for ws in workspaces {
for krate in &mut ws.crates {
if let Some(ref mut builds) = krate.builds {
for (i, b) in builds.iter_mut().enumerate() {
warn_one(
&format!("workspaces[{}].crates[{}].builds[{i}]", ws.name, krate.name),
&mut b.legacy_gobinary,
);
}
}
}
}
}
if let Some(ref mut defaults) = config.defaults
&& let Some(ref mut b) = defaults.builds
{
warn_one("defaults.builds", &mut b.legacy_gobinary);
}
}
mod env_files;
pub use env_files::*;
mod defaults;
pub use defaults::*;
mod build;
pub use build::*;
mod archives;
pub use archives::*;
mod release;
pub use release::*;
mod publishers;
pub use publishers::*;
mod docker;
pub use docker::*;
mod nfpm;
pub use nfpm::*;
mod snapcraft;
pub use snapcraft::*;
mod installers;
pub use installers::*;
mod blob;
pub use blob::*;
mod partial;
pub use partial::*;
mod binstall;
pub use binstall::*;
mod notarize;
pub use notarize::*;
mod source;
pub use source::*;
mod sbom;
pub use sbom::*;
mod version_sync;
pub use version_sync::*;
mod changelog;
pub use changelog::*;
pub use crate::signing::{DockerSignConfig, SignConfig};
mod upx;
pub use upx::*;
mod snapshot_nightly;
pub use snapshot_nightly::*;
mod templatefiles;
pub use templatefiles::*;
mod announce;
pub use announce::*;
mod dockerhub;
pub use dockerhub::*;
mod artifactory;
pub use artifactory::*;
mod cloudsmith;
pub use cloudsmith::*;
mod publisher;
pub use publisher::*;
mod hooks;
pub use hooks::*;
mod git_config;
pub use git_config::*;
mod monorepo;
pub use monorepo::*;
mod tag;
pub use tag::*;
mod workspace;
pub use workspace::*;
mod retry;
pub use retry::*;
mod post_publish_poll;
pub use post_publish_poll::*;
mod string_or_bool;
pub use string_or_bool::*;
pub use crate::packagers::{MakeselfConfig, MakeselfFile, SrpmConfig};
pub(crate) use crate::packagers::{deserialize_makeselfs, makeselfs_schema};
mod milestone;
pub use milestone::*;
mod upload;
pub use upload::*;
mod aur_source;
pub use aur_source::*;
mod mcp;
pub use mcp::*;
#[cfg(test)]
mod tests;