Repository: axodotdev/cargo-dist (rust)

Issue:
Allow publishing under bin name instead of package name
Hello!

We want to publish our binary on homebrew under the binary name, but it is published under the crate name. Is there a way to do that?

Pull request:
feat(formula): add config
fixes #777 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0639a29..b2488b49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,21 +1,6 @@
 # Unreleased
 
-This release is a few minor improvements, and a new config for homebrew installers.
-
-## Features
-
-The name of your homebrew formula can now be overriden with `formula = "my-cool-formula"`.
-
-* [docs](opensource.axo.dev/cargo-dist/book/reference/config.html#formula)
-* @ashleygwilliams [impl](https://github.com/axodotdev/cargo-dist/pull/791)
-
-## Fixes
-
-* @gankra [powershell `iem | iex` exprs are now more robust and can be run from cmd](https://github.com/axodotdev/cargo-dist/pull/808). Only downside is they're more verbose.
-
-* @saraviera [All multi-value cli flags can now be passed as `--arg x y` or `--arg=x,y`](https://github.com/axodotdev/cargo-dist/pull/744). To make this work, a minor breaking change was made to the `cargo dist generate` plumbing command: you must now pass `--mode ci` instead of `ci`. This likely affects no one.
-
-* @mistydemeo [enabled pipefail in more places in cargo-dist's CI](https://github.com/axodotdev/cargo-dist/pull/619)
+Nothing Yet!
 
 
 # Version 0.10.0 (2024-02-09)
diff --git a/book/src/installers/homebrew.md b/book/src/installers/homebrew.md
index 99a4cbcb..0ce3c34c 100644
--- a/book/src/installers/homebrew.md
+++ b/book/src/installers/homebrew.md
@@ -11,6 +11,26 @@ tap = "axodotdev/homebrew-formulae"
 publish-jobs = ["homebrew"]
 ```
 
+Since 0.11.0, cargo-dist can, optionally, also customize your Homebrew formula name.
+By default, your formula will be named using the app name (in Rust, this is the crate
+name). If you are overriding the bin name, you may want to make your Homebrew formula
+match- you can do so with config like this:
+
+```toml
+[package]
+name = "myappname"
+version = "0.666.0"
+default-run = "mybinname"
+
+[[bin]]
+name = "mybinname"
+path = "src/main.rs"
+
+tap = "axodotdev/homebrew-formulae"
+publish-jobs = ["homebrew"]
+formula = "mybinname"
+```
+
 In order to write to a tap GitHub repository, cargo-dist needs a [personal access token](https://github.com/settings/tokens/new?scopes=repo) with the `repo` scope exposed as `HOMEBREW_TAP_TOKEN`. For more information on GitHub Actions secrets, [consult this documentation](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
 
 Limitations/Caveats:
diff --git a/book/src/reference/config.md b/book/src/reference/config.md
index cb859b8b..e5d39c1f 100644
--- a/book/src/reference/config.md
+++ b/book/src/reference/config.md
@@ -345,6 +345,17 @@ For instance for packages that are a library and a CLI binary, some developers p
 If you use this you *probably* want to set it on `[package.metadata.dist]` and
 not `[workspace.metadata.dist]`. See ["inferring precise-builds"](#inferring-precise-builds) for details.
 
+### formula
+
+> since 0.11.0
+
+Example: `formula = "axolotlbrew"`
+
+Specifies a string to override the default Homebrew formula name (the app name). This works
+well specifically for folks who are customizing their bin name and would like the Homebrew
+formula to match the bin name as opposed to the app name (which, in Rust, is the crate name).
+
+You must set this on `[package.metadata.dist]` and not `[workspace.metadata.dist]`.
 
 ### github-custom-runners
 
diff --git a/cargo-dist/src/config.rs b/cargo-dist/src/config.rs
index f8fec31e..c4b048fb 100644
--- a/cargo-dist/src/config.rs
+++ b/cargo-dist/src/config.rs
@@ -94,6 +94,8 @@ pub struct DistMetadata {
 
     /// A Homebrew tap to push the Homebrew formula to, if built
     pub tap: Option<String>,
+    /// Customize the name of the Homebrew formula
+    pub formula: Option<String>,
 
     /// A set of packages to install before building
     #[serde(rename = "dependencies")]
@@ -344,6 +346,7 @@ impl DistMetadata {
             ci: _,
             installers: _,
             tap: _,
+            formula: _,
             system_dependencies: _,
             targets: _,
             include,
@@ -399,6 +402,7 @@ impl DistMetadata {
             ci,
             installers,
             tap,
+            formula,
             system_dependencies,
             targets,
             include,
@@ -544,6 +548,9 @@ impl DistMetadata {
         if tap.is_none() {
             *tap = workspace_config.tap.clone();
         }
+        if formula.is_none() {
+            *formula = workspace_config.formula.clone();
+        }
         if system_dependencies.is_none() {
             *system_dependencies = workspace_config.system_dependencies.clone();
         }
diff --git a/cargo-dist/src/init.rs b/cargo-dist/src/init.rs
index 1331b22f..36600b41 100644
--- a/cargo-dist/src/init.rs
+++ b/cargo-dist/src/init.rs
@@ -229,6 +229,7 @@ fn get_new_dist_metadata(
             ci: None,
             installers: None,
             tap: None,
+            formula: None,
             system_dependencies: None,
             targets: None,
             dist: None,
@@ -763,6 +764,7 @@ fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
         ci,
         installers,
         tap,
+        formula,
         system_dependencies: _,
         targets,
         include,
@@ -828,6 +830,13 @@ fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
         tap.clone(),
     );
 
+    apply_optional_value(
+        table,
+        "formula",
+        "# Customize the Homebrew formula name\n",
+        formula.clone(),
+    );
+
     apply_string_list(
         table,
         "targets",
diff --git a/cargo-dist/src/tasks.rs b/cargo-dist/src/tasks.rs
index de7d133c..1ae4e332 100644
--- a/cargo-dist/src/tasks.rs
+++ b/cargo-dist/src/tasks.rs
@@ -601,6 +601,8 @@ pub struct Release {
     pub install_path: InstallPathStrategy,
     /// GitHub repository to push the Homebrew formula to, if built
     pub tap: Option<String>,
+    /// Customize the name of the Homebrew formula
+    pub formula: Option<String>,
     /// Packages to install from a system package manager
     pub system_dependencies: SystemDependencies,
 }
@@ -737,6 +739,8 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             // Only the final value merged into a package_config matters
             tap: _,
             // Only the final value merged into a package_config matters
+            formula: _,
+            // Only the final value merged into a package_config matters
             system_dependencies: _,
             // Only the final value merged into a package_config matters
             windows_archive: _,
@@ -1027,6 +1031,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             .clone()
             .unwrap_or(InstallPathStrategy::CargoHome);
         let tap = package_config.tap.clone();
+        let formula = package_config.formula.clone();
 
         let windows_archive = package_config.windows_archive.unwrap_or(ZipStyle::Zip);
         let unix_archive = package_config
@@ -1085,6 +1090,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             npm_scope,
             install_path,
             tap,
+            formula,
             system_dependencies,
         });
         idx
@@ -1733,21 +1739,25 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             return;
         }
         let release = self.release(to_release);
-        let release_id = &release.id;
+        let formula = if let Some(formula) = &release.formula {
+            formula
+        } else {
+            &release.id
+        };
         let Some(download_url) = self
             .manifest
-            .release_by_name(&release.app_name)
+            .release_by_name(&release.id)
             .and_then(|r| r.artifact_download_url())
         else {
             warn!("skipping Homebrew formula: couldn't compute a URL to download artifacts from");
             return;
         };
 
-        let artifact_name = format!("{release_id}.rb");
+        let artifact_name = format!("{formula}.rb");
         let artifact_path = self.inner.dist_dir.join(&artifact_name);
 
         // If tap is specified, include that in the `brew install` message
-        let mut install_target = release.app_name.clone();
+        let mut install_target = formula.clone();
         if let Some(tap) = &self.inner.tap {
             install_target = format!("{tap}/{install_target}").to_owned();
         }
@@ -1928,8 +1938,6 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             warn!("The Homebrew publish job is enabled but no tap was specified\n  consider setting the tap field in Cargo.toml");
         }
 
-        let formula_name = to_class_case(&app_name);
-
         let dependencies: Vec<String> = release
             .system_dependencies
             .homebrew
@@ -1956,7 +1964,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 x86_64_linux,
                 x86_64_linux_sha256: None,
                 name: app_name,
-                formula_class: formula_name,
+                formula_class: to_class_case(formula),
                 desc: app_desc,
                 license: app_license,
                 homepage: app_homepage_url,


Test patch:
diff --git a/cargo-dist/tests/gallery/dist.rs b/cargo-dist/tests/gallery/dist.rs
index 41f37e93..2f812c72 100644
--- a/cargo-dist/tests/gallery/dist.rs
+++ b/cargo-dist/tests/gallery/dist.rs
@@ -209,21 +209,51 @@ impl<'a> TestContext<'a, Tools> {
         // read/analyze installers
         eprintln!("loading results...");
         let app_name = &self.repo.app_name;
-        let ps_installer = Utf8PathBuf::from(format!("target/distrib/{app_name}-installer.ps1"));
-        let sh_installer = Utf8PathBuf::from(format!("target/distrib/{app_name}-installer.sh"));
-        let rb_installer = Utf8PathBuf::from(format!("target/distrib/{app_name}.rb"));
+        let target_dir = Utf8PathBuf::from("target/distrib");
+        let ps_installer = Utf8PathBuf::from(format!("{target_dir}/{app_name}-installer.ps1"));
+        let sh_installer = Utf8PathBuf::from(format!("{target_dir}/{app_name}-installer.sh"));
+        let homebrew_installer = Self::load_file_with_suffix(target_dir.clone(), ".rb");
         let npm_installer =
-            Utf8PathBuf::from(format!("target/distrib/{app_name}-npm-package.tar.gz"));
+            Utf8PathBuf::from(format!("{target_dir}/{app_name}-npm-package.tar.gz"));
 
         Ok(DistResult {
             test_name: test_name.to_owned(),
             shell_installer_path: sh_installer.exists().then_some(sh_installer),
             powershell_installer_path: ps_installer.exists().then_some(ps_installer),
-            homebrew_installer_path: rb_installer.exists().then_some(rb_installer),
+            homebrew_installer_path: homebrew_installer,
             npm_installer_package_path: npm_installer.exists().then_some(npm_installer),
         })
     }
 
+    fn load_file_with_suffix(dirname: Utf8PathBuf, suffix: &str) -> Option<Utf8PathBuf> {
+        let files = Self::load_files_with_suffix(dirname, suffix);
+        let number_found = files.len();
+        assert!(
+            number_found <= 1,
+            "found {} files with the suffix {}, expected 1 or 0",
+            number_found,
+            suffix
+        );
+        files.first().cloned()
+    }
+
+    fn load_files_with_suffix(dirname: Utf8PathBuf, suffix: &str) -> Vec<Utf8PathBuf> {
+        // Collect all dist-manifests and fetch the appropriate Mac ones
+        let mut files = vec![];
+        for file in dirname
+            .read_dir()
+            .expect("loading target dir failed, something has gone very wrong")
+        {
+            let path = file.unwrap().path();
+            if let Some(filename) = path.file_name() {
+                if filename.to_string_lossy().ends_with(suffix) {
+                    files.push(Utf8PathBuf::from_path_buf(path).unwrap())
+                }
+            }
+        }
+        files
+    }
+
     pub fn patch_cargo_toml(&self, new_toml: String) -> Result<()> {
         eprintln!("loading Cargo.toml...");
         let toml_src = axoasset::SourceFile::load_local("Cargo.toml")?;
diff --git a/cargo-dist/tests/integration-tests.rs b/cargo-dist/tests/integration-tests.rs
index dff7946d..a568885f 100644
--- a/cargo-dist/tests/integration-tests.rs
+++ b/cargo-dist/tests/integration-tests.rs
@@ -62,6 +62,44 @@ path-guid = "BFD25009-65A4-4D1E-97F1-0030465D90D6"
     })
 }
 
+#[test]
+fn axolotlsay_custom_formula() -> Result<(), miette::Report> {
+    let test_name = _function_name!();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
+        ctx.patch_cargo_toml(format!(r#"
+[workspace.metadata.dist]
+cargo-dist-version = "{dist_version}"
+installers = ["homebrew"]
+tap = "axodotdev/homebrew-packages"
+# https://rubydoc.brew.sh/Formula.html naming rules for Formulae
+# providing this config will make an AxolotlBrew formula
+formula = "axolotl-brew"
+publish-jobs = ["homebrew"]
+targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
+ci = ["github"]
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+[package.metadata.wix]
+upgrade-guid = "B36177BE-EA4D-44FB-B05C-EDDABDAA95CA"
+path-guid = "BFD25009-65A4-4D1E-97F1-0030465D90D6"
+
+"#
+        ))?;
+
+        // Run generate to make sure stuff is up to date before running other commands
+        let ci_result = ctx.cargo_dist_generate(test_name)?;
+        let ci_snap = ci_result.check_all()?;
+        // Do usual build+plan checks
+        let main_result = ctx.cargo_dist_build_and_plan(test_name)?;
+        let main_snap = main_result.check_all(ctx, ".cargo/bin/")?;
+        // snapshot all
+        main_snap.join(ci_snap).snap();
+        Ok(())
+    })
+}
+
 #[test]
 fn axolotlsay_abyss() -> Result<(), miette::Report> {
     let test_name = _function_name!();
diff --git a/cargo-dist/tests/snapshots/axolotlsay_custom_formula.snap b/cargo-dist/tests/snapshots/axolotlsay_custom_formula.snap
new file mode 100644
index 00000000..9c247a18
--- /dev/null
+++ b/cargo-dist/tests/snapshots/axolotlsay_custom_formula.snap
@@ -0,0 +1,626 @@
+---
+source: cargo-dist/tests/gallery/dist.rs
+expression: self.payload
+---
+================ formula.rb ================
+class AxolotlBrew < Formula
+  desc "💬 a CLI for learning to distribute CLIs in rust"
+  version "0.2.1"
+  on_macos do
+    on_arm do
+      url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-aarch64-apple-darwin.tar.gz"
+    end
+    on_intel do
+      url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-apple-darwin.tar.gz"
+    end
+  end
+  on_linux do
+    on_intel do
+      url "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+    end
+  end
+  license "MIT OR Apache-2.0"
+
+  def install
+    on_macos do
+      on_arm do
+        bin.install "axolotlsay"
+      end
+    end
+    on_macos do
+      on_intel do
+        bin.install "axolotlsay"
+      end
+    end
+    on_linux do
+      on_intel do
+        bin.install "axolotlsay"
+      end
+    end
+
+    # Homebrew will automatically install these, so we don't need to do that
+    doc_files = Dir["README.*", "readme.*", "LICENSE", "LICENSE.*", "CHANGELOG.*"]
+    leftover_contents = Dir["*"] - doc_files
+
+    # Install any leftover files in pkgshare; these are probably config or
+    # sample files.
+    pkgshare.install *leftover_contents unless leftover_contents.empty?
+  end
+end
+
+================ dist-manifest.json ================
+{
+  "dist_version": "CENSORED",
+  "announcement_tag": "v0.2.1",
+  "announcement_tag_is_implicit": true,
+  "announcement_is_prerelease": false,
+  "announcement_title": "Version 0.2.1",
+  "announcement_changelog": "```text\n         +--------------------------------------+\n         | now with linux static musl binary!!! |\n         +--------------------------------------+\n        /\n≽(◕ ᴗ ◕)≼\n```",
+  "announcement_github_body": "## Release Notes\n\n```text\n         +--------------------------------------+\n         | now with linux static musl binary!!! |\n         +--------------------------------------+\n        /\n≽(◕ ᴗ ◕)≼\n```\n\n## Install axolotlsay 0.2.1\n\n### Install prebuilt binaries via Homebrew\n\n```sh\nbrew install axodotdev/homebrew-packages/axolotl-brew\n```\n\n## Download axolotlsay 0.2.1\n\n|  File  | Platform | Checksum |\n|--------|----------|----------|\n| [axolotlsay-aarch64-apple-darwin.tar.gz](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-aarch64-apple-darwin.tar.gz) | Apple Silicon macOS | [checksum](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-aarch64-apple-darwin.tar.gz.sha256) |\n| [axolotlsay-x86_64-apple-darwin.tar.gz](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-apple-darwin.tar.gz) | Intel macOS | [checksum](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-apple-darwin.tar.gz.sha256) |\n| [axolotlsay-x86_64-pc-windows-msvc.tar.gz](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-pc-windows-msvc.tar.gz) | x64 Windows | [checksum](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-pc-windows-msvc.tar.gz.sha256) |\n| [axolotlsay-x86_64-unknown-linux-gnu.tar.gz](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-unknown-linux-gnu.tar.gz) | x64 Linux | [checksum](https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1/axolotlsay-x86_64-unknown-linux-gnu.tar.gz.sha256) |\n\n",
+  "system_info": {
+    "cargo_version_line": "CENSORED"
+  },
+  "releases": [
+    {
+      "app_name": "axolotlsay",
+      "app_version": "0.2.1",
+      "artifacts": [
+        "source.tar.gz",
+        "source.tar.gz.sha256",
+        "axolotl-brew.rb",
+        "axolotlsay-aarch64-apple-darwin.tar.gz",
+        "axolotlsay-aarch64-apple-darwin.tar.gz.sha256",
+        "axolotlsay-x86_64-apple-darwin.tar.gz",
+        "axolotlsay-x86_64-apple-darwin.tar.gz.sha256",
+        "axolotlsay-x86_64-pc-windows-msvc.tar.gz",
+        "axolotlsay-x86_64-pc-windows-msvc.tar.gz.sha256",
+        "axolotlsay-x86_64-unknown-linux-gnu.tar.gz",
+        "axolotlsay-x86_64-unknown-linux-gnu.tar.gz.sha256"
+      ],
+      "hosting": {
+        "github": {
+          "artifact_download_url": "https://github.com/axodotdev/axolotlsay/releases/download/v0.2.1"
+        }
+      }
+    }
+  ],
+  "artifacts": {
+    "axolotl-brew.rb": {
+      "name": "axolotl-brew.rb",
+      "kind": "installer",
+      "target_triples": [
+        "aarch64-apple-darwin",
+        "x86_64-apple-darwin",
+        "x86_64-unknown-linux-gnu"
+      ],
+      "install_hint": "brew install axodotdev/homebrew-packages/axolotl-brew",
+      "description": "Install prebuilt binaries via Homebrew"
+    },
+    "axolotlsay-aarch64-apple-darwin.tar.gz": {
+      "name": "axolotlsay-aarch64-apple-darwin.tar.gz",
+      "kind": "executable-zip",
+      "target_triples": [
+        "aarch64-apple-darwin"
+      ],
+      "assets": [
+        {
+          "name": "CHANGELOG.md",
+          "path": "CHANGELOG.md",
+          "kind": "changelog"
+        },
+        {
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
+        },
+        {
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
+        },
+        {
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
+        },
+        {
+          "name": "axolotlsay",
+          "path": "axolotlsay",
+          "kind": "executable"
+        }
+      ],
+      "checksum": "axolotlsay-aarch64-apple-darwin.tar.gz.sha256"
+    },
+    "axolotlsay-aarch64-apple-darwin.tar.gz.sha256": {
+      "name": "axolotlsay-aarch64-apple-darwin.tar.gz.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "aarch64-apple-darwin"
+      ]
+    },
+    "axolotlsay-x86_64-apple-darwin.tar.gz": {
+      "name": "axolotlsay-x86_64-apple-darwin.tar.gz",
+      "kind": "executable-zip",
+      "target_triples": [
+        "x86_64-apple-darwin"
+      ],
+      "assets": [
+        {
+          "name": "CHANGELOG.md",
+          "path": "CHANGELOG.md",
+          "kind": "changelog"
+        },
+        {
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
+        },
+        {
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
+        },
+        {
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
+        },
+        {
+          "name": "axolotlsay",
+          "path": "axolotlsay",
+          "kind": "executable"
+        }
+      ],
+      "checksum": "axolotlsay-x86_64-apple-darwin.tar.gz.sha256"
+    },
+    "axolotlsay-x86_64-apple-darwin.tar.gz.sha256": {
+      "name": "axolotlsay-x86_64-apple-darwin.tar.gz.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "x86_64-apple-darwin"
+      ]
+    },
+    "axolotlsay-x86_64-pc-windows-msvc.tar.gz": {
+      "name": "axolotlsay-x86_64-pc-windows-msvc.tar.gz",
+      "kind": "executable-zip",
+      "target_triples": [
+        "x86_64-pc-windows-msvc"
+      ],
+      "assets": [
+        {
+          "name": "CHANGELOG.md",
+          "path": "CHANGELOG.md",
+          "kind": "changelog"
+        },
+        {
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
+        },
+        {
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
+        },
+        {
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
+        },
+        {
+          "name": "axolotlsay",
+          "path": "axolotlsay.exe",
+          "kind": "executable"
+        }
+      ],
+      "checksum": "axolotlsay-x86_64-pc-windows-msvc.tar.gz.sha256"
+    },
+    "axolotlsay-x86_64-pc-windows-msvc.tar.gz.sha256": {
+      "name": "axolotlsay-x86_64-pc-windows-msvc.tar.gz.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "x86_64-pc-windows-msvc"
+      ]
+    },
+    "axolotlsay-x86_64-unknown-linux-gnu.tar.gz": {
+      "name": "axolotlsay-x86_64-unknown-linux-gnu.tar.gz",
+      "kind": "executable-zip",
+      "target_triples": [
+        "x86_64-unknown-linux-gnu"
+      ],
+      "assets": [
+        {
+          "name": "CHANGELOG.md",
+          "path": "CHANGELOG.md",
+          "kind": "changelog"
+        },
+        {
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
+        },
+        {
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
+        },
+        {
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
+        },
+        {
+          "name": "axolotlsay",
+          "path": "axolotlsay",
+          "kind": "executable"
+        }
+      ],
+      "checksum": "axolotlsay-x86_64-unknown-linux-gnu.tar.gz.sha256"
+    },
+    "axolotlsay-x86_64-unknown-linux-gnu.tar.gz.sha256": {
+      "name": "axolotlsay-x86_64-unknown-linux-gnu.tar.gz.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "x86_64-unknown-linux-gnu"
+      ]
+    },
+    "source.tar.gz": {
+      "name": "source.tar.gz",
+      "kind": "source-tarball",
+      "checksum": "source.tar.gz.sha256"
+    },
+    "source.tar.gz.sha256": {
+      "name": "source.tar.gz.sha256",
+      "kind": "checksum"
+    }
+  },
+  "publish_prereleases": false,
+  "ci": {
+    "github": {
+      "artifacts_matrix": {
+        "include": [
+          {
+            "targets": [
+              "aarch64-apple-darwin"
+            ],
+            "runner": "macos-12",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=aarch64-apple-darwin"
+          },
+          {
+            "targets": [
+              "x86_64-apple-darwin"
+            ],
+            "runner": "macos-12",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-apple-darwin"
+          },
+          {
+            "targets": [
+              "x86_64-pc-windows-msvc"
+            ],
+            "runner": "windows-2019",
+            "install_dist": "powershell -c \"irm https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.ps1 | iex\"",
+            "dist_args": "--artifacts=local --target=x86_64-pc-windows-msvc"
+          },
+          {
+            "targets": [
+              "x86_64-unknown-linux-gnu"
+            ],
+            "runner": "ubuntu-20.04",
+            "install_dist": "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh",
+            "dist_args": "--artifacts=local --target=x86_64-unknown-linux-gnu"
+          }
+        ]
+      },
+      "pr_run_mode": "plan"
+    }
+  },
+  "linkage": []
+}
+
+================ release.yml ================
+# Copyright 2022-2023, axodotdev
+# SPDX-License-Identifier: MIT or Apache-2.0
+#
+# CI that:
+#
+# * checks for a Git Tag that looks like a release
+# * builds artifacts with cargo-dist (archives, installers, hashes)
+# * uploads those artifacts to temporary workflow zip
+# * on success, uploads the artifacts to a Github Release
+#
+# Note that the Github Release will be created with a generated
+# title/body based on your changelogs.
+
+name: Release
+
+permissions:
+  contents: write
+
+# This task will run whenever you push a git tag that looks like a version
+# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
+# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
+# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
+# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
+#
+# If PACKAGE_NAME is specified, then the announcement will be for that
+# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
+#
+# If PACKAGE_NAME isn't specified, then the announcement will be for all
+# (cargo-dist-able) packages in the workspace with that version (this mode is
+# intended for workspaces with only one dist-able package, or with all dist-able
+# packages versioned/released in lockstep).
+#
+# If you push multiple tags at once, separate instances of this workflow will
+# spin up, creating an independent announcement for each one. However Github
+# will hard limit this to 3 tags per commit, as it will assume more tags is a
+# mistake.
+#
+# If there's a prerelease-style suffix to the version, then the release(s)
+# will be marked as a prerelease.
+on:
+  push:
+    tags:
+      - '**[0-9]+.[0-9]+.[0-9]+*'
+  pull_request:
+
+jobs:
+  # Run 'cargo dist plan' (or host) to determine what tasks we need to do
+  plan:
+    runs-on: ubuntu-latest
+    outputs:
+      val: ${{ steps.plan.outputs.manifest }}
+      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
+      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
+      publishing: ${{ !github.event.pull_request }}
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+      - name: Install cargo-dist
+        # we specify bash to get pipefail; it guards against the `curl` command
+        # failing. otherwise `sh` won't catch that `curl` returned non-0
+        shell: bash
+        run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
+      # sure would be cool if github gave us proper conditionals...
+      # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
+      # functionality based on whether this is a pull_request, and whether it's from a fork.
+      # (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
+      # but also really annoying to build CI around when it needs secrets to work right.)
+      - id: plan
+        run: |
+          cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
+          echo "cargo dist ran successfully"
+          cat plan-dist-manifest.json
+          echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v4
+        with:
+          name: artifacts-plan-dist-manifest
+          path: plan-dist-manifest.json
+
+  # Build and packages all the platform-specific things
+  build-local-artifacts:
+    name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
+    # Let the initial task tell us to not run (currently very blunt)
+    needs:
+      - plan
+    if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
+    strategy:
+      fail-fast: false
+      # Target platforms/runners are computed by cargo-dist in create-release.
+      # Each member of the matrix has the following arguments:
+      #
+      # - runner: the github runner
+      # - dist-args: cli flags to pass to cargo dist
+      # - install-dist: expression to run to install cargo-dist on the runner
+      #
+      # Typically there will be:
+      # - 1 "global" task that builds universal installers
+      # - N "local" tasks that build each platform's binaries and platform-specific installers
+      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
+    runs-on: ${{ matrix.runner }}
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+      - uses: swatinem/rust-cache@v2
+      - name: Install cargo-dist
+        run: ${{ matrix.install_dist }}
+      # Get the dist-manifest
+      - name: Fetch local artifacts
+        uses: actions/download-artifact@v4
+        with:
+          pattern: artifacts-*
+          path: target/distrib/
+          merge-multiple: true
+      - name: Install dependencies
+        run: |
+          ${{ matrix.packages_install }}
+      - name: Build artifacts
+        run: |
+          # Actually do builds and make zips and whatnot
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          echo "cargo dist ran successfully"
+      - id: cargo-dist
+        name: Post-build
+        # We force bash here just because github makes it really hard to get values up
+        # to "real" actions without writing to env-vars, and writing to env-vars has
+        # inconsistent syntax between shell and powershell.
+        shell: bash
+        run: |
+          # Parse out what we just built and upload it to scratch storage
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+
+          cp dist-manifest.json "$BUILD_MANIFEST_NAME"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v4
+        with:
+          name: artifacts-build-local-${{ join(matrix.targets, '_') }}
+          path: |
+            ${{ steps.cargo-dist.outputs.paths }}
+            ${{ env.BUILD_MANIFEST_NAME }}
+
+  # Build and package all the platform-agnostic(ish) things
+  build-global-artifacts:
+    needs:
+      - plan
+      - build-local-artifacts
+    runs-on: "ubuntu-20.04"
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+      - name: Install cargo-dist
+        shell: bash
+        run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
+      # Get all the local artifacts for the global tasks to use (for e.g. checksums)
+      - name: Fetch local artifacts
+        uses: actions/download-artifact@v4
+        with:
+          pattern: artifacts-*
+          path: target/distrib/
+          merge-multiple: true
+      - id: cargo-dist
+        shell: bash
+        run: |
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
+          echo "cargo dist ran successfully"
+
+          # Parse out what we just built and upload it to scratch storage
+          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
+          echo "EOF" >> "$GITHUB_OUTPUT"
+
+          cp dist-manifest.json "$BUILD_MANIFEST_NAME"
+      - name: "Upload artifacts"
+        uses: actions/upload-artifact@v4
+        with:
+          name: artifacts-build-global
+          path: |
+            ${{ steps.cargo-dist.outputs.paths }}
+            ${{ env.BUILD_MANIFEST_NAME }}
+  # Determines if we should publish/announce
+  host:
+    needs:
+      - plan
+      - build-local-artifacts
+      - build-global-artifacts
+    # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
+    if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    runs-on: "ubuntu-20.04"
+    outputs:
+      val: ${{ steps.host.outputs.manifest }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+      - name: Install cargo-dist
+        run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/vSOME_VERSION/cargo-dist-installer.sh | sh"
+      # Fetch artifacts from scratch-storage
+      - name: Fetch artifacts
+        uses: actions/download-artifact@v4
+        with:
+          pattern: artifacts-*
+          path: target/distrib/
+          merge-multiple: true
+      # This is a harmless no-op for Github Releases, hosting for that happens in "announce"
+      - id: host
+        shell: bash
+        run: |
+          cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
+          echo "artifacts uploaded and released successfully"
+          cat dist-manifest.json
+          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
+      - name: "Upload dist-manifest.json"
+        uses: actions/upload-artifact@v4
+        with:
+          # Overwrite the previous copy
+          name: artifacts-dist-manifest
+          path: dist-manifest.json
+
+  publish-homebrew-formula:
+    needs:
+      - plan
+      - host
+    runs-on: "ubuntu-20.04"
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      PLAN: ${{ needs.plan.outputs.val }}
+      GITHUB_USER: "axo bot"
+      GITHUB_EMAIL: "admin+bot@axo.dev"
+    if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          repository: "axodotdev/homebrew-packages"
+          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
+      # So we have access to the formula
+      - name: Fetch local artifacts
+        uses: actions/download-artifact@v4
+        with:
+          pattern: artifacts-*
+          path: Formula/
+          merge-multiple: true
+      - name: Commit formula files
+        run: |
+          git config --global user.name "${GITHUB_USER}"
+          git config --global user.email "${GITHUB_EMAIL}"
+
+          for release in $(echo "$PLAN" | jq --compact-output '.releases[]'); do
+            name=$(echo "$release" | jq .app_name --raw-output)
+            version=$(echo "$release" | jq .app_version --raw-output)
+
+            git add Formula/${name}.rb
+            git commit -m "${name} ${version}"
+          done
+          git push
+
+  # Create a Github Release while uploading all files to it
+  announce:
+    needs:
+      - plan
+      - host
+      - publish-homebrew-formula
+    # use "always() && ..." to allow us to wait for all publish jobs while
+    # still allowing individual publish jobs to skip themselves (for prereleases).
+    # "host" however must run to completion, no skipping allowed!
+    if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }}
+    runs-on: "ubuntu-20.04"
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+      - name: "Download Github Artifacts"
+        uses: actions/download-artifact@v4
+        with:
+          pattern: artifacts-*
+          path: artifacts
+          merge-multiple: true
+      - name: Cleanup
+        run: |
+          # Remove the granular manifests
+          rm -f artifacts/*-dist-manifest.json
+      - name: Create Github Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: ${{ needs.plan.outputs.tag }}
+          name: ${{ fromJson(needs.host.outputs.val).announcement_title }}
+          body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }}
+          prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }}
+          artifacts: "artifacts/*"