Skip to main content

anodizer_core/config/publishers/
homebrew.rs

1use std::collections::HashMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::super::{StringOrBool, deserialize_string_or_bool_opt};
7use super::{CommitAuthorConfig, RepositoryConfig};
8
9// ---------------------------------------------------------------------------
10// HomebrewConfig / ScoopConfig / TapConfig / BucketConfig
11// ---------------------------------------------------------------------------
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
14#[serde(default, deny_unknown_fields)]
15pub struct HomebrewConfig {
16    /// Unified repository config with branch, token, PR, git SSH support.
17    /// (Replaces the legacy `tap: TapConfig` owner/name-only form.)
18    pub repository: Option<RepositoryConfig>,
19    /// Commit author with optional signing.
20    pub commit_author: Option<CommitAuthorConfig>,
21    /// Formula directory in the tap (e.g. "Formula"). Matches GoReleaser `directory`.
22    pub directory: Option<String>,
23    /// Override the formula name (default: crate name).
24    pub name: Option<String>,
25    /// Short description of the formula (shown in `brew info`).
26    pub description: Option<String>,
27    /// SPDX license identifier (e.g., "MIT", "Apache-2.0").
28    pub license: Option<String>,
29    /// Ruby `install` block content for the formula.
30    pub install: Option<String>,
31    /// Additional install commands appended after the main install block.
32    pub extra_install: Option<String>,
33    /// Post-install commands (separate `def post_install` block in formula).
34    pub post_install: Option<String>,
35    /// Ruby `test` block content for the formula (run by `brew test`).
36    pub test: Option<String>,
37    /// Project homepage URL. Falls back to the GitHub release URL when unset.
38    pub homepage: Option<String>,
39    /// Package dependencies (e.g. `openssl`, `libgit2`).
40    pub dependencies: Option<Vec<HomebrewDependency>>,
41    /// Conflicting formula names with optional reason.
42    pub conflicts: Option<Vec<HomebrewConflict>>,
43    /// Post-install user-facing notes shown by `brew info`.
44    pub caveats: Option<String>,
45    /// Skip publishing the formula.  `"true"` always skips; `"auto"` skips
46    /// for prerelease versions. Accepts bool or template string.
47    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
48    pub skip_upload: Option<StringOrBool>,
49    /// Custom commit message template. Rendered via Tera with the standard
50    /// release template variables (`ProjectName`, `Tag`, `Version`, etc.).
51    /// Default: `"Brew formula update for {{ ProjectName }} version {{ Tag }}"`
52    /// (set in `crates/stage-publish/src/homebrew.rs::default_commit_msg_template`).
53    pub commit_msg_template: Option<String>,
54    // Legacy flat `commit_author_name` / `commit_author_email` fields are
55    // gone; use the structured `commit_author: { name, email, signing }`.
56    /// Build IDs filter: only include artifacts whose `id` is in this list.
57    pub ids: Option<Vec<String>>,
58    /// Custom URL template for download URLs (overrides release URL).
59    pub url_template: Option<String>,
60    /// HTTP headers to include in download requests (e.g. for private repos).
61    pub url_headers: Option<Vec<String>>,
62    /// Custom download strategy class name (e.g. `:using => GitHubPrivateRepositoryReleaseDownloadStrategy`).
63    pub download_strategy: Option<String>,
64    /// Ruby `require` statement for custom download strategies.
65    pub custom_require: Option<String>,
66    /// Custom Ruby code block inserted into the formula class body.
67    pub custom_block: Option<String>,
68    /// Launchd plist content for `brew services`.
69    pub plist: Option<String>,
70    /// Homebrew service block content (alternative to plist).
71    pub service: Option<String>,
72    /// Homebrew Cask configuration (macOS .app bundles).
73    pub cask: Option<HomebrewCaskConfig>,
74    /// amd64 microarchitecture variant filter (e.g. "v1", "v2", "v3", "v4").
75    /// Only artifacts matching this variant are included. Default: "v1".
76    pub amd64_variant: Option<String>,
77    /// ARM version filter (e.g. "6", "7"). Only artifacts matching this
78    /// variant are included.
79    pub arm_variant: Option<String>,
80}
81
82#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
83#[serde(default)]
84pub struct HomebrewDependency {
85    /// Homebrew formula name of the dependency.
86    pub name: String,
87    /// Restrict to a specific OS: `"mac"` or `"linux"`.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub os: Option<String>,
90    /// Dependency type, e.g. `"optional"`.
91    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
92    pub dep_type: Option<String>,
93    /// Version constraint for the dependency (e.g. `">= 1.1"`).
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub version: Option<String>,
96}
97
98/// A Homebrew conflict entry, supporting both a bare name string and a
99/// structured object with an optional `because` reason.
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
101#[serde(untagged)]
102pub enum HomebrewConflict {
103    /// Just the formula name (e.g. `"other-tool"`).
104    Name(String),
105    /// Name with reason (e.g. `{name: "other-tool", because: "both install a bin/foo binary"}`).
106    WithReason {
107        name: String,
108        #[serde(skip_serializing_if = "Option::is_none")]
109        because: Option<String>,
110    },
111}
112
113impl HomebrewConflict {
114    pub fn name(&self) -> &str {
115        match self {
116            Self::Name(n) => n,
117            Self::WithReason { name, .. } => name,
118        }
119    }
120    pub fn because(&self) -> Option<&str> {
121        match self {
122            Self::Name(_) => None,
123            Self::WithReason { because, .. } => because.as_deref(),
124        }
125    }
126}
127
128/// Unified Homebrew Cask configuration.
129///
130/// Used at both call-sites:
131/// - `homebrew_casks:` — top-level array; carries `repository`,
132///   `commit_author`, `directory`, `ids`, `url`, structured `uninstall`/`zap`, etc.
133/// - `crates[].publish.homebrew_cask:` — per-crate override; same shape, with
134///   `url_template` as the simpler URL alternative.
135///
136/// Fields from both original types are present; any field may be `None` at either
137/// call-site. The union avoids a two-type bifurcation while keeping both axes.
138#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
139#[serde(default, deny_unknown_fields)]
140pub struct HomebrewCaskConfig {
141    // ----- Identity -----
142    /// Cask name (default: crate / project name).
143    pub name: Option<String>,
144    /// Alternative cask names (aliases).
145    pub alternative_names: Option<Vec<String>>,
146
147    // ----- Tap repository (top-level axis) -----
148    /// Unified repository config for the Homebrew tap.
149    pub repository: Option<RepositoryConfig>,
150    /// Commit author with optional signing.
151    pub commit_author: Option<CommitAuthorConfig>,
152    /// Custom commit message template.
153    /// Default: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}"
154    pub commit_msg_template: Option<String>,
155    /// Subdirectory in the tap repo for cask placement (default: "Casks").
156    pub directory: Option<String>,
157
158    // ----- Artifact selection -----
159    /// Build IDs filter: only include artifacts from builds whose `id` is in this list.
160    pub ids: Option<Vec<String>>,
161
162    // ----- Download URL -----
163    /// Simple URL template for the .dmg/.zip download (per-crate shorthand).
164    ///
165    /// Cannot be combined with `url.template:` — set one or the other.
166    /// If both are present, config validation rejects the config at parse time.
167    /// Use `url:` for the structured form (verified domain, custom headers, etc.)
168    /// or `url_template:` for a bare string shorthand — never both simultaneously.
169    pub url_template: Option<String>,
170    /// Structured download URL configuration (top-level axis).
171    pub url: Option<HomebrewCaskURL>,
172
173    // ----- macOS bundle -----
174    /// macOS .app bundle name (e.g. "MyApp.app").
175    pub app: Option<String>,
176    /// Binary stubs to create in /usr/local/bin.
177    ///
178    /// Each entry is either a bare string (`"my-cli"` → emits
179    /// `binary "my-cli"`) or a structured `{ name, target }` object
180    /// (`{ name: "my-cli", target: "mycli" }` → emits
181    /// `binary "my-cli", target: "mycli"`). The `target:` form mirrors
182    /// the Homebrew Ruby cask DSL for binary renames — without it, a
183    /// wrapped binary installs at the wrong path.
184    /// Mirrors GoReleaser `internal/pipe/brew/templates/cask.rb.tmpl`.
185    pub binaries: Option<Vec<HomebrewCaskBinary>>,
186
187    // ----- Metadata -----
188    /// Cask description.
189    pub description: Option<String>,
190    /// Project homepage URL.
191    pub homepage: Option<String>,
192    /// License identifier (SPDX).
193    pub license: Option<String>,
194    /// Custom caveats shown after install.
195    pub caveats: Option<String>,
196
197    // ----- Ruby block -----
198    /// Arbitrary Ruby code inserted into the cask block.
199    pub custom_block: Option<String>,
200    /// Homebrew service definition.
201    pub service: Option<String>,
202
203    // ----- Completions / manpages -----
204    /// Manual page references to install.
205    pub manpages: Option<Vec<String>>,
206    /// Shell completion definitions.
207    pub completions: Option<HomebrewCaskCompletions>,
208    /// Auto-generate shell completions from an executable.
209    pub generate_completions_from_executable: Option<HomebrewCaskGeneratedCompletions>,
210
211    // ----- Dependencies / conflicts -----
212    /// Cask dependencies (other casks or formulae).
213    pub dependencies: Option<Vec<HomebrewCaskDependencyEntry>>,
214    /// Conflicting casks or formulae.
215    pub conflicts: Option<Vec<HomebrewCaskConflictEntry>>,
216
217    // ----- Lifecycle hooks -----
218    /// Pre/post install/uninstall hooks.
219    pub hooks: Option<HomebrewCaskHooks>,
220
221    // ----- Uninstall / zap -----
222    /// Structured uninstall stanza configuration.
223    pub uninstall: Option<HomebrewCaskUninstall>,
224    /// Deep uninstall (zap) stanza configuration.
225    pub zap: Option<HomebrewCaskUninstall>,
226
227    // ----- Publishing control -----
228    /// Skip publishing the cask. `"true"` always skips; `"auto"` skips
229    /// for prerelease versions. Accepts bool or template string.
230    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
231    pub skip_upload: Option<StringOrBool>,
232    /// When true, force-push the updated cask file to the existing PR branch
233    /// when a PR for the same head branch already exists. The PR content is
234    /// updated in place rather than creating a duplicate. When false (default),
235    /// the push is skipped and a warning is emitted so the operator sees that
236    /// the publisher did not update the PR.
237    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
238    pub update_existing_pr: Option<StringOrBool>,
239}
240
241/// Structured URL configuration for Homebrew Cask downloads.
242#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
243#[serde(default)]
244pub struct HomebrewCaskURL {
245    /// URL template for the download.
246    pub template: Option<String>,
247    /// Verification string (domain shown to user).
248    pub verified: Option<String>,
249    /// Custom downloader (e.g. `:homebrew_curl`, `:post`).
250    pub using: Option<String>,
251    /// HTTP cookies for the download.
252    pub cookies: Option<HashMap<String, String>>,
253    /// Referer header for the download.
254    pub referer: Option<String>,
255    /// Custom HTTP headers.
256    pub headers: Option<Vec<String>>,
257    /// Custom user agent string.
258    pub user_agent: Option<String>,
259    /// POST data for form submissions.
260    pub data: Option<HashMap<String, String>>,
261}
262
263/// Structured uninstall/zap configuration for Homebrew Cask.
264/// Used for both `uninstall` and `zap` stanzas.
265#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
266#[serde(default)]
267pub struct HomebrewCaskUninstall {
268    /// Launch daemon/agent identifiers to stop.
269    pub launchctl: Option<Vec<String>>,
270    /// Application bundle IDs to quit.
271    pub quit: Option<Vec<String>>,
272    /// Login item names to remove.
273    pub login_item: Option<Vec<String>>,
274    /// File paths to delete.
275    pub delete: Option<Vec<String>>,
276    /// File paths to trash (preserves app state).
277    pub trash: Option<Vec<String>>,
278}
279
280/// Pre/post install/uninstall hooks for Homebrew Cask.
281#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
282#[serde(default)]
283pub struct HomebrewCaskHooks {
284    /// Pre-install/uninstall hooks.
285    pub pre: Option<HomebrewCaskHook>,
286    /// Post-install/uninstall hooks.
287    pub post: Option<HomebrewCaskHook>,
288}
289
290/// Individual hook for install/uninstall phases.
291#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
292#[serde(default)]
293pub struct HomebrewCaskHook {
294    /// Ruby code for preflight/postflight during install.
295    pub install: Option<String>,
296    /// Ruby code for uninstall_preflight/uninstall_postflight.
297    pub uninstall: Option<String>,
298}
299
300/// Shell completion file paths for Homebrew Cask.
301#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
302#[serde(default)]
303pub struct HomebrewCaskCompletions {
304    /// Path to bash completion file.
305    pub bash: Option<String>,
306    /// Path to zsh completion file.
307    pub zsh: Option<String>,
308    /// Path to fish completion file.
309    pub fish: Option<String>,
310}
311
312/// Cask `binary` stanza entry.
313///
314/// Two shapes accepted in YAML:
315/// - bare string — `"my-cli"` → renders `binary "my-cli"`.
316/// - `{ name, target }` object — `{ name: "my-cli", target: "mycli" }`
317///   → renders `binary "my-cli", target: "mycli"`. The `target:` form is
318///   the Homebrew Ruby cask DSL rename: install the symlink at
319///   `/usr/local/bin/<target>` instead of `/usr/local/bin/<name>`.
320///
321/// GR ref: `internal/pipe/brew/templates/cask.rb.tmpl` — search for
322/// `binary` to see the canonical Ruby form.
323#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
324#[serde(untagged)]
325pub enum HomebrewCaskBinary {
326    /// Bare binary name. Equivalent to `{ name: "<n>", target: None }`.
327    Name(String),
328    /// Structured `{ name, target }` rename form.
329    WithTarget {
330        /// Path inside the .app bundle (e.g. `"my-cli"`).
331        name: String,
332        /// Optional rename target — the symlink name in `/usr/local/bin`.
333        /// When `None`, the symlink uses `name`.
334        #[serde(skip_serializing_if = "Option::is_none")]
335        target: Option<String>,
336    },
337}
338
339impl HomebrewCaskBinary {
340    /// The binary name (the path inside the .app bundle).
341    pub fn name(&self) -> &str {
342        match self {
343            Self::Name(n) => n,
344            Self::WithTarget { name, .. } => name,
345        }
346    }
347    /// The optional rename target. `None` for bare-string entries and for
348    /// `{ name, target }` objects without `target` set.
349    pub fn target(&self) -> Option<&str> {
350        match self {
351            Self::Name(_) => None,
352            Self::WithTarget { target, .. } => target.as_deref(),
353        }
354    }
355}
356
357/// Cask dependency (on another cask or formula).
358#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
359#[serde(default)]
360pub struct HomebrewCaskDependencyEntry {
361    /// Dependent cask name.
362    pub cask: Option<String>,
363    /// Dependent formula name.
364    pub formula: Option<String>,
365}
366
367/// Cask conflict (with another cask or formula).
368#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
369#[serde(default)]
370pub struct HomebrewCaskConflictEntry {
371    /// Conflicting cask name.
372    pub cask: Option<String>,
373    /// Conflicting formula name (deprecated by Homebrew).
374    pub formula: Option<String>,
375}
376
377/// Auto-generate shell completions from an executable.
378#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
379#[serde(default, deny_unknown_fields)]
380pub struct HomebrewCaskGeneratedCompletions {
381    /// Binary to generate completions from.
382    pub executable: Option<String>,
383    /// Arguments to pass to the executable.
384    pub args: Option<Vec<String>>,
385    /// Base name for completion files.
386    pub base_name: Option<String>,
387    /// Shell completion framework type (arg, clap, click, cobra, flag, none, typer).
388    pub shell_parameter_format: Option<String>,
389    /// Target shells (bash, zsh, fish, pwsh).
390    pub shells: Option<Vec<String>>,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
394#[serde(default, deny_unknown_fields)]
395pub struct ScoopConfig {
396    /// Unified repository config with branch, token, PR, git SSH support.
397    /// (Replaces the legacy `bucket: BucketConfig` owner/name-only form.)
398    pub repository: Option<RepositoryConfig>,
399    /// Commit author with optional signing.
400    pub commit_author: Option<CommitAuthorConfig>,
401    /// Override the manifest name (default: crate name).
402    pub name: Option<String>,
403    /// Subdirectory in the bucket repo for manifest placement.
404    pub directory: Option<String>,
405    /// Short description of the package (shown in `scoop info`).
406    pub description: Option<String>,
407    /// SPDX license identifier (e.g., "MIT", "Apache-2.0").
408    pub license: Option<String>,
409    /// Project homepage URL. Falls back to the GitHub-derived URL when unset.
410    pub homepage: Option<String>,
411    /// Data paths persisted between Scoop updates.
412    pub persist: Option<Vec<String>>,
413    /// Application dependencies (other Scoop packages).
414    pub depends: Option<Vec<String>>,
415    /// Commands to run before installation.
416    pub pre_install: Option<Vec<String>>,
417    /// Commands to run after installation.
418    pub post_install: Option<Vec<String>>,
419    /// Start menu shortcuts as `[executable, label]` pairs.
420    pub shortcuts: Option<Vec<Vec<String>>>,
421    /// Skip publishing the manifest.  `"true"` always skips; `"auto"` skips
422    /// for prerelease versions. Accepts bool or template string.
423    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
424    pub skip_upload: Option<StringOrBool>,
425    /// Custom commit message template.
426    pub commit_msg_template: Option<String>,
427    // Use the structured `commit_author: { name, email, signing }` form for
428    // commit author identity (legacy flat `commit_author_name` /
429    // `commit_author_email` fields are not accepted).
430    /// Build IDs filter: only include artifacts whose `id` is in this list.
431    pub ids: Option<Vec<String>>,
432    /// Custom URL template for download URLs (overrides release URL).
433    pub url_template: Option<String>,
434    /// Artifact selection: "archive" (default), "msi", or "nsis".
435    #[serde(rename = "use")]
436    pub use_artifact: Option<String>,
437    /// amd64 microarchitecture variant filter (e.g. "v1", "v2", "v3", "v4").
438    /// Only artifacts matching this variant are included. Default: "v1".
439    pub amd64_variant: Option<String>,
440}
441
442// `TapConfig` / `BucketConfig` (legacy {owner, name}-only repo types) live
443// nowhere — every publisher now carries `repository: RepositoryConfig`
444// with the broader feature set (token / branch / git SSH / pull_request).