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 (WAVE 4).
129///
130/// Used at both call-sites:
131/// - `homebrew_casks:` — top-level array (GoReleaser parity); 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}
233
234/// Structured URL configuration for Homebrew Cask downloads.
235#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
236#[serde(default)]
237pub struct HomebrewCaskURL {
238 /// URL template for the download.
239 pub template: Option<String>,
240 /// Verification string (domain shown to user).
241 pub verified: Option<String>,
242 /// Custom downloader (e.g. `:homebrew_curl`, `:post`).
243 pub using: Option<String>,
244 /// HTTP cookies for the download.
245 pub cookies: Option<HashMap<String, String>>,
246 /// Referer header for the download.
247 pub referer: Option<String>,
248 /// Custom HTTP headers.
249 pub headers: Option<Vec<String>>,
250 /// Custom user agent string.
251 pub user_agent: Option<String>,
252 /// POST data for form submissions.
253 pub data: Option<HashMap<String, String>>,
254}
255
256/// Structured uninstall/zap configuration for Homebrew Cask.
257/// Used for both `uninstall` and `zap` stanzas.
258#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
259#[serde(default)]
260pub struct HomebrewCaskUninstall {
261 /// Launch daemon/agent identifiers to stop.
262 pub launchctl: Option<Vec<String>>,
263 /// Application bundle IDs to quit.
264 pub quit: Option<Vec<String>>,
265 /// Login item names to remove.
266 pub login_item: Option<Vec<String>>,
267 /// File paths to delete.
268 pub delete: Option<Vec<String>>,
269 /// File paths to trash (preserves app state).
270 pub trash: Option<Vec<String>>,
271}
272
273/// Pre/post install/uninstall hooks for Homebrew Cask.
274#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
275#[serde(default)]
276pub struct HomebrewCaskHooks {
277 /// Pre-install/uninstall hooks.
278 pub pre: Option<HomebrewCaskHook>,
279 /// Post-install/uninstall hooks.
280 pub post: Option<HomebrewCaskHook>,
281}
282
283/// Individual hook for install/uninstall phases.
284#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
285#[serde(default)]
286pub struct HomebrewCaskHook {
287 /// Ruby code for preflight/postflight during install.
288 pub install: Option<String>,
289 /// Ruby code for uninstall_preflight/uninstall_postflight.
290 pub uninstall: Option<String>,
291}
292
293/// Shell completion file paths for Homebrew Cask.
294#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
295#[serde(default)]
296pub struct HomebrewCaskCompletions {
297 /// Path to bash completion file.
298 pub bash: Option<String>,
299 /// Path to zsh completion file.
300 pub zsh: Option<String>,
301 /// Path to fish completion file.
302 pub fish: Option<String>,
303}
304
305/// Cask `binary` stanza entry.
306///
307/// Two shapes accepted in YAML:
308/// - bare string — `"my-cli"` → renders `binary "my-cli"`.
309/// - `{ name, target }` object — `{ name: "my-cli", target: "mycli" }`
310/// → renders `binary "my-cli", target: "mycli"`. The `target:` form is
311/// the Homebrew Ruby cask DSL rename: install the symlink at
312/// `/usr/local/bin/<target>` instead of `/usr/local/bin/<name>`.
313///
314/// GR ref: `internal/pipe/brew/templates/cask.rb.tmpl` — search for
315/// `binary` to see the canonical Ruby form.
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
317#[serde(untagged)]
318pub enum HomebrewCaskBinary {
319 /// Bare binary name. Equivalent to `{ name: "<n>", target: None }`.
320 Name(String),
321 /// Structured `{ name, target }` rename form.
322 WithTarget {
323 /// Path inside the .app bundle (e.g. `"my-cli"`).
324 name: String,
325 /// Optional rename target — the symlink name in `/usr/local/bin`.
326 /// When `None`, the symlink uses `name`.
327 #[serde(skip_serializing_if = "Option::is_none")]
328 target: Option<String>,
329 },
330}
331
332impl HomebrewCaskBinary {
333 /// The binary name (the path inside the .app bundle).
334 pub fn name(&self) -> &str {
335 match self {
336 Self::Name(n) => n,
337 Self::WithTarget { name, .. } => name,
338 }
339 }
340 /// The optional rename target. `None` for bare-string entries and for
341 /// `{ name, target }` objects without `target` set.
342 pub fn target(&self) -> Option<&str> {
343 match self {
344 Self::Name(_) => None,
345 Self::WithTarget { target, .. } => target.as_deref(),
346 }
347 }
348}
349
350/// Cask dependency (on another cask or formula).
351#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
352#[serde(default)]
353pub struct HomebrewCaskDependencyEntry {
354 /// Dependent cask name.
355 pub cask: Option<String>,
356 /// Dependent formula name.
357 pub formula: Option<String>,
358}
359
360/// Cask conflict (with another cask or formula).
361#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
362#[serde(default)]
363pub struct HomebrewCaskConflictEntry {
364 /// Conflicting cask name.
365 pub cask: Option<String>,
366 /// Conflicting formula name (deprecated by Homebrew).
367 pub formula: Option<String>,
368}
369
370/// Auto-generate shell completions from an executable.
371#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
372#[serde(default, deny_unknown_fields)]
373pub struct HomebrewCaskGeneratedCompletions {
374 /// Binary to generate completions from.
375 pub executable: Option<String>,
376 /// Arguments to pass to the executable.
377 pub args: Option<Vec<String>>,
378 /// Base name for completion files.
379 pub base_name: Option<String>,
380 /// Shell completion framework type (arg, clap, click, cobra, flag, none, typer).
381 pub shell_parameter_format: Option<String>,
382 /// Target shells (bash, zsh, fish, pwsh).
383 pub shells: Option<Vec<String>>,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
387#[serde(default, deny_unknown_fields)]
388pub struct ScoopConfig {
389 /// Unified repository config with branch, token, PR, git SSH support.
390 /// (Replaces the legacy `bucket: BucketConfig` owner/name-only form.)
391 pub repository: Option<RepositoryConfig>,
392 /// Commit author with optional signing.
393 pub commit_author: Option<CommitAuthorConfig>,
394 /// Override the manifest name (default: crate name).
395 pub name: Option<String>,
396 /// Subdirectory in the bucket repo for manifest placement.
397 pub directory: Option<String>,
398 /// Short description of the package (shown in `scoop info`).
399 pub description: Option<String>,
400 /// SPDX license identifier (e.g., "MIT", "Apache-2.0").
401 pub license: Option<String>,
402 /// Project homepage URL. Falls back to the GitHub-derived URL when unset.
403 pub homepage: Option<String>,
404 /// Data paths persisted between Scoop updates.
405 pub persist: Option<Vec<String>>,
406 /// Application dependencies (other Scoop packages).
407 pub depends: Option<Vec<String>>,
408 /// Commands to run before installation.
409 pub pre_install: Option<Vec<String>>,
410 /// Commands to run after installation.
411 pub post_install: Option<Vec<String>>,
412 /// Start menu shortcuts as `[executable, label]` pairs.
413 pub shortcuts: Option<Vec<Vec<String>>>,
414 /// Skip publishing the manifest. `"true"` always skips; `"auto"` skips
415 /// for prerelease versions. Accepts bool or template string.
416 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
417 pub skip_upload: Option<StringOrBool>,
418 /// Custom commit message template.
419 pub commit_msg_template: Option<String>,
420 // Use the structured `commit_author: { name, email, signing }` form for
421 // commit author identity (legacy flat `commit_author_name` /
422 // `commit_author_email` fields are not accepted).
423 /// Build IDs filter: only include artifacts whose `id` is in this list.
424 pub ids: Option<Vec<String>>,
425 /// Custom URL template for download URLs (overrides release URL).
426 pub url_template: Option<String>,
427 /// Artifact selection: "archive" (default), "msi", or "nsis".
428 #[serde(rename = "use")]
429 pub use_artifact: Option<String>,
430 /// amd64 microarchitecture variant filter (e.g. "v1", "v2", "v3", "v4").
431 /// Only artifacts matching this variant are included. Default: "v1".
432 pub amd64_variant: Option<String>,
433}
434
435// `TapConfig` / `BucketConfig` (legacy {owner, name}-only repo types) live
436// nowhere — every publisher now carries `repository: RepositoryConfig`
437// with the broader feature set (token / branch / git SSH / pull_request).