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