Skip to main content

anodizer_core/config/
build.rs

1use std::collections::HashMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::{
7    AppBundleConfig, ArchiveConfig, ArchivesConfig, BinstallConfig, BlobConfig, ChecksumConfig,
8    DmgConfig, DockerDigestConfig, DockerManifestConfig, DockerV2Config, FlatpakConfig, HookEntry,
9    HooksConfig, MsiConfig, NfpmConfig, NsisConfig, PkgConfig, PublishConfig, ReleaseConfig,
10    SnapcraftConfig, StringOrBool, VersionSyncConfig, deserialize_archives_config,
11    deserialize_string_or_bool_opt,
12};
13
14// ---------------------------------------------------------------------------
15// BuildIgnore — exclude specific os/arch combos from builds
16// ---------------------------------------------------------------------------
17
18/// Exclude a specific os/arch combination from the build matrix.
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
20#[serde(deny_unknown_fields)]
21pub struct BuildIgnore {
22    /// Operating system to exclude (e.g., "linux", "darwin", "windows").
23    pub os: String,
24    /// Architecture to exclude (e.g., "amd64", "arm64", "386").
25    pub arch: String,
26}
27
28// ---------------------------------------------------------------------------
29// BuildOverride — per-target env, flags, features
30// ---------------------------------------------------------------------------
31
32/// Override env, flags, or features for targets matching glob patterns.
33#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
34#[serde(default, deny_unknown_fields)]
35pub struct BuildOverride {
36    /// Glob patterns to match against target triples (e.g., `["x86_64-*", "*-linux-*"]`).
37    pub targets: Vec<String>,
38    /// Extra environment variables to set for matching targets.
39    #[serde(default)]
40    pub env: Option<Vec<String>>,
41    /// Extra flags to append for matching targets, one per list entry.
42    pub flags: Option<Vec<String>>,
43    /// Extra features to enable for matching targets.
44    pub features: Option<Vec<String>>,
45}
46
47// ---------------------------------------------------------------------------
48// CrossStrategy
49// ---------------------------------------------------------------------------
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
52#[serde(rename_all = "lowercase")]
53pub enum CrossStrategy {
54    Auto,
55    Zigbuild,
56    Cross,
57    Cargo,
58}
59
60// ---------------------------------------------------------------------------
61// BuilderKind — `cargo` (compile from source) vs `prebuilt` (import binary)
62// ---------------------------------------------------------------------------
63
64/// Selects which builder a `builds[]` entry uses. `Cargo` (the default) runs
65/// `cargo build` and discovers the resulting binary under
66/// `target/<triple>/<profile>/`. `Prebuilt` skips compilation entirely and
67/// imports a binary the operator already placed on disk via the per-build
68/// `prebuilt:` block.
69#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
70#[serde(rename_all = "lowercase")]
71pub enum BuilderKind {
72    /// Build the binary by invoking `cargo build` (or `cross` / `zigbuild`
73    /// per the `cross:` strategy). Default when no `builder:` is set.
74    #[default]
75    Cargo,
76    /// Import a binary already staged on disk instead of compiling. Pairs
77    /// with the `prebuilt:` block on the same build entry.
78    Prebuilt,
79}
80
81// ---------------------------------------------------------------------------
82// PrebuiltConfig — path template for the `prebuilt` builder
83// ---------------------------------------------------------------------------
84
85/// Per-build options for `builder: prebuilt`. Required when `builder: prebuilt`
86/// is set on the same `builds[]` entry.
87#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
88#[serde(default, deny_unknown_fields)]
89pub struct PrebuiltConfig {
90    /// Template path to the imported binary on disk. Rendered once per
91    /// target with these template variables available in addition to the
92    /// project-wide globals (`Version`, `ProjectName`, ...):
93    ///
94    /// - `{{ Target }}` — the full Rust target triple
95    ///   (e.g. `x86_64-unknown-linux-gnu`).
96    /// - `{{ Os }}` — the OS slug (`linux`, `darwin`,
97    ///   `windows`, ...).
98    /// - `{{ Arch }}` — the architecture slug (`amd64`,
99    ///   `arm64`, `armv7`, ...).
100    /// - `{{ Amd64 }}` — AMD64 micro-architecture variant
101    ///   (`v1` / `v2` / `v3` / `v4`); set for `x86_64-*` triples.
102    /// - `{{ Arm64 }}` — ARM64 micro-architecture variant (`v8`); set for
103    ///   `aarch64-*` triples.
104    /// - `{{ Arm }}` — ARM micro-architecture variant (`6` / `7`); set for
105    ///   `armv6*` / `armv7*` triples.
106    /// - `{{ I386 }}` — i386 micro-architecture variant (`sse2`); set for
107    ///   `i686-*` / `i386-*` / `i586-*` triples.
108    /// - `{{ ArtifactExt }}` — `.exe` on Windows targets, empty elsewhere.
109    /// - `{{ ArtifactID }}` — the build entry's `id:` (empty when unset).
110    ///
111    /// The rendered path is `stat()`-ed before the import, unless
112    /// `--dry-run` is active, in which case the stat is skipped and the
113    /// path is accepted as given. A missing file, a permission error, or
114    /// any other I/O failure aborts the build with a message that names
115    /// both the rendered path and the originating target triple, matching
116    /// the "build will fail" contract.
117    ///
118    /// Recommendation: place the staged binaries OUTSIDE `dist/`. The
119    /// release pipeline removes `dist/` on every run; pointing `path:` at
120    /// `dist/...` will resolve against an empty directory.
121    pub path: String,
122}
123
124// ---------------------------------------------------------------------------
125// CrateConfig
126// ---------------------------------------------------------------------------
127
128#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
129#[serde(default, deny_unknown_fields)]
130pub struct CrateConfig {
131    /// Crate name as published (must match the Cargo.toml package name).
132    pub name: String,
133    /// Relative path to the crate directory from the project root.
134    pub path: String,
135    /// Git tag template used to tag and identify releases (supports templates).
136    pub tag_template: String,
137    /// Pinned semver version. When set, `anodizer bump --strict` refuses to
138    /// edit this crate's `Cargo.toml` to anything other than this value;
139    /// without `--strict`, the bump proceeds with a warning. Lets a release
140    /// captain freeze a crate's version while still running broad
141    /// `--workspace` bumps.
142    pub version: Option<String>,
143    /// Other crates this crate depends on; ensures release ordering.
144    pub depends_on: Option<Vec<String>>,
145    /// Build configurations for this crate. One entry per binary by default.
146    pub builds: Option<Vec<BuildConfig>>,
147    /// Cross-compilation strategy for this crate: auto, zigbuild, cross, or cargo.
148    pub cross: Option<CrossStrategy>,
149    #[serde(default, deserialize_with = "deserialize_archives_config")]
150    #[schemars(schema_with = "archives_schema")]
151    pub archives: ArchivesConfig,
152    /// Checksum configuration for this crate.
153    pub checksum: Option<ChecksumConfig>,
154    /// GitHub release configuration for this crate.
155    pub release: Option<ReleaseConfig>,
156    /// Publishing targets (Homebrew, Scoop, AUR, etc.) for this crate.
157    pub publish: Option<PublishConfig>,
158    /// Docker V2 image build configurations for this crate (canonical API:
159    /// images+tags, annotations, build_args, sbom, disable). The legacy
160    /// `docker:` block was removed; this is the only docker surface. The
161    /// `docker_v2:` spelling is still accepted via serde alias for back-compat.
162    #[serde(alias = "docker_v2")]
163    pub dockers_v2: Option<Vec<DockerV2Config>>,
164    /// Docker image digest file configuration for this crate.
165    pub docker_digest: Option<DockerDigestConfig>,
166    /// Docker multi-platform manifest configurations for this crate.
167    pub docker_manifests: Option<Vec<DockerManifestConfig>>,
168    /// Linux package (deb, rpm, apk) configurations for this crate. Renamed
169    /// from `nfpm:` (singular) for spelling parity with `Defaults.nfpms` and
170    /// the rest of the plural-name per-crate packaging lists (`dmgs`, `msis`,
171    /// `pkgs`, `nsis`, ...). The `nfpm:` spelling is still accepted via serde
172    /// alias for back-compat.
173    #[serde(alias = "nfpm")]
174    pub nfpms: Option<Vec<NfpmConfig>>,
175    /// Snapcraft package configurations for this crate.
176    pub snapcrafts: Option<Vec<SnapcraftConfig>>,
177    /// macOS DMG disk image configurations for this crate.
178    pub dmgs: Option<Vec<DmgConfig>>,
179    /// Windows MSI installer configurations for this crate.
180    pub msis: Option<Vec<MsiConfig>>,
181    /// macOS PKG installer configurations for this crate.
182    pub pkgs: Option<Vec<PkgConfig>>,
183    /// NSIS installer configurations for this crate.
184    pub nsis: Option<Vec<NsisConfig>>,
185    /// macOS app bundle configurations for this crate.
186    pub app_bundles: Option<Vec<AppBundleConfig>>,
187    /// Linux Flatpak bundle configurations for this crate.
188    pub flatpaks: Option<Vec<FlatpakConfig>>,
189    /// Cloud storage (S3/GCS/Azure) upload configurations for this crate.
190    pub blobs: Option<Vec<BlobConfig>>,
191    /// cargo-binstall metadata configuration for this crate.
192    pub binstall: Option<BinstallConfig>,
193    /// Automatic version number synchronization configuration for this crate.
194    pub version_sync: Option<VersionSyncConfig>,
195    /// Repo-committed files that embed this crate's release version outside
196    /// `Cargo.toml` (repo-root-relative path strings). At `tag` time each file
197    /// has its occurrences of the old version rewritten to the new version —
198    /// both bare and `v`-prefixed forms, word-boundary anchored — and is staged
199    /// into the same bump commit as this crate's `Cargo.toml`. Overrides the
200    /// workspace-level `defaults.version_files`.
201    pub version_files: Option<Vec<String>>,
202    /// macOS universal binary (fat binary) configurations for this crate.
203    pub universal_binaries: Option<Vec<UniversalBinaryConfig>>,
204    /// When true (or template evaluating to "true"), all build outputs are
205    /// placed in a flat `dist/` directory instead of `dist/{target}/`.
206    #[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
207    pub no_unique_dist_dir: Option<StringOrBool>,
208    /// Hooks that run inside THIS crate's scope at the start of the release,
209    /// before the build. Distinct from the top-level `before:`, which fires
210    /// ONCE around the whole release; these fire once per crate with that
211    /// crate's version/tag template vars anchored, so `cmd` / `dir` / `env` /
212    /// `if` render against the crate's own `Version` / `Tag` / `ProjectName`.
213    /// A non-zero exit aborts the release.
214    ///
215    /// Fires once per crate in EVERY multi-crate mode — workspace per-crate
216    /// AND workspace lockstep with multiple publisher crates — in both a full
217    /// `anodizer release` and `anodizer release --publish-only`, matching the
218    /// per-crate iteration of `before_publish:` and the publishers. With an
219    /// explicit `--crate` subset only the selected crates' hooks fire. No-op
220    /// in a single-crate config with no `crates:` block (use the top-level
221    /// `before:` there).
222    pub before: Option<HooksConfig>,
223    /// Hooks that run inside THIS crate's scope at the end of the release,
224    /// after the crate's publish dispatch (and post-publish verification)
225    /// completes. Per-crate counterpart of the top-level `after:` (which fires
226    /// once around the whole release). Same per-crate firing semantics across
227    /// all modes, template surface, and abort semantics as the per-crate
228    /// `before:`.
229    pub after: Option<HooksConfig>,
230    /// Hooks that run immediately before THIS crate's publishers dispatch,
231    /// once per matching artifact (the same per-artifact semantics as the
232    /// top-level `before_publish:`), scoped to the crate's own artifacts and
233    /// template vars. Honors the per-entry `ids:` / `artifacts:` filters. A
234    /// non-zero exit aborts the release before that crate publishes to any
235    /// registry. The top-level `before_publish:` still fires once over the
236    /// full artifact set; this one targets a single crate's artifacts.
237    pub before_publish: Option<HooksConfig>,
238}
239
240/// Helper schema function for archives (accepts false or array).
241pub(super) fn archives_schema(
242    generator: &mut schemars::r#gen::SchemaGenerator,
243) -> schemars::schema::Schema {
244    let mut schema = generator.subschema_for::<Option<Vec<ArchiveConfig>>>();
245    if let schemars::schema::Schema::Object(ref mut obj) = schema {
246        obj.metadata().description = Some("Archive configurations for this crate. Set to false to disable archiving, or provide an array of archive configs.".to_owned());
247    }
248    schema
249}
250
251impl Default for CrateConfig {
252    fn default() -> Self {
253        CrateConfig {
254            name: String::new(),
255            path: String::new(),
256            tag_template: String::new(),
257            version: None,
258            depends_on: None,
259            builds: None,
260            cross: None,
261            archives: ArchivesConfig::Configs(vec![]),
262            checksum: None,
263            release: None,
264            publish: None,
265            dockers_v2: None,
266            docker_digest: None,
267            docker_manifests: None,
268            nfpms: None,
269            snapcrafts: None,
270            dmgs: None,
271            msis: None,
272            pkgs: None,
273            nsis: None,
274            app_bundles: None,
275            flatpaks: None,
276            blobs: None,
277            binstall: None,
278            version_sync: None,
279            version_files: None,
280            universal_binaries: None,
281            no_unique_dist_dir: None,
282            before: None,
283            after: None,
284            before_publish: None,
285        }
286    }
287}
288
289// ---------------------------------------------------------------------------
290// UniversalBinaryConfig
291// ---------------------------------------------------------------------------
292
293#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
294#[serde(default, deny_unknown_fields)]
295pub struct UniversalBinaryConfig {
296    /// Unique identifier for this universal binary, propagated into the
297    /// artifact's metadata as `id`.
298    #[serde(default)]
299    pub id: Option<String>,
300    /// Output filename template for the universal binary (supports templates).
301    pub name_template: Option<String>,
302    /// When true, remove the individual arch binaries after creating the universal binary.
303    pub replace: Option<bool>,
304    /// Build IDs filter: only combine artifacts from builds whose `id` is in this list.
305    pub ids: Option<Vec<String>>,
306    /// Pre/post hooks around universal binary creation.
307    pub hooks: Option<BuildHooksConfig>,
308    /// Override the modification timestamp for reproducible universal binaries.
309    pub mod_timestamp: Option<String>,
310}
311
312// ---------------------------------------------------------------------------
313// BuildConfig
314// ---------------------------------------------------------------------------
315
316#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
317#[serde(default, deny_unknown_fields)]
318pub struct BuildConfig {
319    /// Unique identifier for this build, used to reference it from archives and other configs.
320    pub id: Option<String>,
321    /// Binary name to build (must match a Cargo binary target in the crate).
322    ///
323    /// Optional so that `defaults.builds` (a path-mirrored template that
324    /// applies to every crate) can omit `binary` — the per-crate `builds[]`
325    /// entry supplies it. When the binary is absent at the per-crate level
326    /// it falls back to the crate's `name` field.
327    pub binary: Option<String>,
328    /// When true (or template evaluating to "true"), skip this build entirely.
329    #[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
330    pub skip: Option<StringOrBool>,
331    /// Target triples to build for. When set, REPLACES `defaults.targets`
332    /// for this build (override semantics — the per-build value wins
333    /// outright, no concat). When `None`, this build inherits
334    /// `defaults.targets` verbatim. Both `cli::commands::helpers::
335    /// collect_build_targets` and `stage-build` enforce this rule.
336    pub targets: Option<Vec<String>>,
337    /// Cargo features to enable for this build.
338    pub features: Option<Vec<String>>,
339    /// When true, pass --no-default-features to cargo build.
340    pub no_default_features: Option<bool>,
341    /// Per-target environment variables keyed as {target: {KEY: VALUE}}.
342    pub env: Option<HashMap<String, HashMap<String, String>>>,
343    /// Copy the binary from another build ID instead of building it.
344    pub copy_from: Option<String>,
345    /// Extra flags passed to cargo build, one per list entry (e.g., `["--release", "--locked"]`).
346    ///
347    /// Each entry is template-rendered then passed verbatim as a single argv
348    /// token — there is no `sh -c` step and no shell tokenization, so a
349    /// rendered value containing spaces stays one argv entry (it is NOT
350    /// re-split). Use one list entry per flag, including the flag and its
351    /// value as separate entries (`["--target-dir", "/tmp/{{ Version }}"]`)
352    /// when the value itself may contain spaces.
353    pub flags: Option<Vec<String>>,
354    /// When true, enable reproducible builds by stripping timestamps.
355    pub reproducible: Option<bool>,
356    /// Per-build hooks executed before and after compilation.
357    pub hooks: Option<BuildHooksConfig>,
358    /// Exclude specific os/arch combinations from this build's target matrix.
359    /// Falls back to `defaults.builds.ignore` when not set.
360    pub ignore: Option<Vec<BuildIgnore>>,
361    /// Per-target overrides for env, flags, and features for this build.
362    /// Falls back to `defaults.builds.overrides` when not set.
363    pub overrides: Option<Vec<BuildOverride>>,
364    /// Override the cross-compilation tool binary path (e.g., a custom `cross` wrapper).
365    /// When set, this binary is used instead of cargo/cross/zigbuild.
366    pub cross_tool: Option<String>,
367    /// Override the modification timestamp of built binaries for reproducible builds.
368    /// Template string (e.g. `"{{ CommitTimestamp }}"`) or unix timestamp.
369    pub mod_timestamp: Option<String>,
370    /// Override the cargo subcommand (default: auto-detected "build" or "zigbuild").
371    /// Enables e.g. `cargo auditable build` by setting `command: "auditable build"`.
372    pub command: Option<String>,
373    /// When true (or template evaluating to "true"), place binaries in flat dist/
374    /// instead of dist/{target}/. Overrides the crate-level setting.
375    #[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
376    pub no_unique_dist_dir: Option<StringOrBool>,
377    /// Builder to use for this entry. `cargo` (the default when omitted)
378    /// runs `cargo build`. `prebuilt` skips compilation and imports a
379    /// binary the operator already produced via the `prebuilt:` block.
380    ///
381    /// When `builder: prebuilt`, `targets:` MUST be set explicitly — no
382    /// `defaults.targets` fallback — and the `prebuilt.path` template is
383    /// rendered per target then stat()-ed before the import.
384    pub builder: Option<BuilderKind>,
385    /// Options for the `prebuilt` builder. Required when
386    /// `builder: prebuilt`; ignored (with a config-load warning) otherwise.
387    pub prebuilt: Option<PrebuiltConfig>,
388    /// Accepted-but-ignored legacy key. anodizer always drives the cargo
389    /// toolchain (or `cross` / `zigbuild`) directly, so there is no
390    /// pluggable build binary to point at. The field is parsed only so that
391    /// configs imported from a Go-style tool keep loading instead of
392    /// hard-failing under `deny_unknown_fields`; its value is never read.
393    /// Use `command:` to override the cargo subcommand instead.
394    #[doc(hidden)]
395    #[serde(default, skip_serializing)]
396    pub gobinary: Option<String>,
397}
398
399/// Pre/post hook configuration shared across multiple stages. Despite the
400/// `Build` prefix in the name, this type is used by both the **build** stage
401/// (pre/post compilation hooks) and the **archive** stage (pre/post archiving
402/// hooks). The name is kept for backward compatibility with existing configs.
403/// **Not** to be confused with the top-level `HooksConfig` (which carries a
404/// flat `hooks: Vec<String>` list for `before`/`after` lifecycle hooks).
405#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
406#[serde(default, deny_unknown_fields)]
407pub struct BuildHooksConfig {
408    /// Commands to run before the build step.
409    pub pre: Option<Vec<HookEntry>>,
410    /// Commands to run after the build step.
411    pub post: Option<Vec<HookEntry>>,
412}
413
414/// Pre/post archive hook configuration.
415///
416/// Archive hooks use `before`/`after`; build hooks use `pre`/`post`.
417#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
418#[serde(default, deny_unknown_fields)]
419pub struct ArchiveHooksConfig {
420    /// Commands to run before the archive step.
421    pub before: Option<Vec<HookEntry>>,
422    /// Commands to run after the archive step.
423    pub after: Option<Vec<HookEntry>>,
424}
425
426#[cfg(test)]
427mod legacy_field_tests {
428    use super::*;
429
430    /// An imported config carrying the removed Go-style `gobinary:` key must
431    /// still parse (accept-and-ignore) rather than hard-fail under
432    /// `deny_unknown_fields`. The value is captured but never read.
433    #[test]
434    fn build_gobinary_is_accepted_and_ignored() {
435        let build: BuildConfig =
436            serde_yaml_ng::from_str("binary: myapp\ngobinary: /usr/local/bin/go")
437                .expect("gobinary: must parse as an accepted-but-ignored legacy field");
438        assert_eq!(build.binary.as_deref(), Some("myapp"));
439        assert_eq!(build.gobinary.as_deref(), Some("/usr/local/bin/go"));
440    }
441
442    /// `gobinary` is parse-only and must not round-trip into serialized output
443    /// (it carries no behavior).
444    #[test]
445    fn build_gobinary_is_not_serialized() {
446        let build = BuildConfig {
447            gobinary: Some("/usr/local/bin/go".to_string()),
448            ..Default::default()
449        };
450        let yaml = serde_yaml_ng::to_string(&build).unwrap();
451        assert!(
452            !yaml.contains("gobinary"),
453            "gobinary must be skipped on serialize: {yaml}"
454        );
455    }
456}