Repository: axodotdev/cargo-dist
Pull request #296 (2023-07-31 19:58:42)

chore: improve integration tests, clean up path handling

Gankra: merging so other things can use this

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e9d29b57..c0a60040 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -66,6 +66,9 @@ jobs:
   # probe all your features and rust versions (see tracing's ci)
   test:
     runs-on: ${{ matrix.os }}
+    env:
+      # runtest the installer scripts
+      RUIN_MY_COMPUTER_WITH_INSTALLERS: true
     strategy:
       # Test the cross-product of these platforms+toolchains
       matrix:
diff --git a/Cargo.toml b/Cargo.toml
index fd45b7e6..69e2cba5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ pre-release-commit-message = "chore: Release version {{version}}"
 # Config for 'cargo dist'
 [workspace.metadata.dist]
 # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
-cargo-dist-version = "0.1.0-prerelease.5"
+cargo-dist-version = "0.1.0-prerelease.6"
 # CI backends to support (see 'cargo dist generate-ci')
 ci = ["github"]
 # The installers to generate for each app
diff --git a/cargo-dist/src/backend/templates/installer.sh.j2 b/cargo-dist/src/backend/templates/installer.sh.j2
index d67aa2ee..20affc7f 100644
--- a/cargo-dist/src/backend/templates/installer.sh.j2
+++ b/cargo-dist/src/backend/templates/installer.sh.j2
@@ -161,7 +161,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -276,8 +275,8 @@ install() {
     # Install to this subdir of the user's {{ install_path.env_key }} dir.
     # In this case we want to be early-bound, as the env-var can't be trusted longterm.
     if [ -n "{{ "${" }}{{ install_path.env_key }}:-}" ]; then
-        _install_dir="${{ install_path.env_key }}/{{ install_path.subdir }}"
-        _env_script_path="${{ install_path.env_key }}/{{ install_path.subdir }}/env"
+        _install_dir="${{ install_path.env_key }}{% if install_path.subdir | length %}/{% endif %}{{ install_path.subdir }}"
+        _env_script_path="${{ install_path.env_key }}{% if install_path.subdir | length %}/{% endif %}{{ install_path.subdir }}/env"
         _install_dir_expr="$_install_dir"
         _env_script_path_expr="$_env_script_path"
     else
diff --git a/cargo-dist/src/config.rs b/cargo-dist/src/config.rs
index c79dfee9..fa331811 100644
--- a/cargo-dist/src/config.rs
+++ b/cargo-dist/src/config.rs
@@ -494,14 +494,16 @@ impl std::str::FromStr for InstallPathStrategy {
                 })
             } else {
                 Ok(InstallPathStrategy::HomeSubdir {
-                    subdir: subdir.to_owned(),
+                    // If there's a trailing slash, strip it to be uniform
+                    subdir: subdir.strip_suffix('/').unwrap_or(subdir).to_owned(),
                 })
             }
         } else if let Some(val) = path.strip_prefix('$') {
             if let Some((env_key, subdir)) = val.split_once('/') {
                 Ok(InstallPathStrategy::EnvSubdir {
                     env_key: env_key.to_owned(),
-                    subdir: subdir.to_owned(),
+                    // If there's a trailing slash, strip it to be uniform
+                    subdir: subdir.strip_suffix('/').unwrap_or(subdir).to_owned(),
                 })
             } else {
                 Err(DistError::InstallPathEnvSlash {
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index b3e42ba0..e80a5d47 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -497,7 +497,12 @@ fn init_artifact_dir(_dist: &DistGraph, artifact: &Artifact) -> Result<()> {
 }
 
 pub(crate) fn copy_file(src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
-    LocalAsset::copy(src_path, dest_path)?;
+    assert_eq!(
+        src_path.file_name(),
+        dest_path.file_name(),
+        "oops axoasset's LocalAsset::copy isn't usable for us"
+    );
+    LocalAsset::copy(src_path, dest_path.parent().unwrap())?;
     Ok(())
 }
 


Test patch:
diff --git a/cargo-dist/tests/gallery/command.rs b/cargo-dist/tests/gallery/command.rs
new file mode 100644
index 00000000..7c2fc78c
--- /dev/null
+++ b/cargo-dist/tests/gallery/command.rs
@@ -0,0 +1,127 @@
+use miette::{miette, Context, IntoDiagnostic};
+use std::process::Command;
+
+pub struct CommandInfo {
+    name: String,
+    cmd: String,
+    args: Vec<String>,
+    version: Option<String>,
+}
+
+impl CommandInfo {
+    /// Create a new command, checking that it works by running it with `--version`
+    pub fn new(name: &str, path: Option<&str>) -> Option<Self> {
+        let cmd = path.unwrap_or(name).to_owned();
+        let output = Command::new(&cmd).arg("--version").output().ok()?;
+
+        Some(CommandInfo {
+            name: name.to_owned(),
+            cmd,
+            args: vec![],
+            version: parse_version(output),
+        })
+    }
+
+    /// Create a new command, don't check that it works
+    #[allow(dead_code)]
+    pub fn new_unchecked(name: &str, path: Option<&str>) -> Self {
+        let cmd = path.unwrap_or(name).to_owned();
+
+        CommandInfo {
+            name: name.to_owned(),
+            cmd,
+            args: vec![],
+            version: None,
+        }
+    }
+
+    /// Create a new powershell command (for running things like powershell modules)
+    pub fn new_powershell_command(name: &str) -> Option<Self> {
+        let output = Command::new("powershell")
+            .arg("-Command")
+            .arg("Get-Command")
+            .arg(name)
+            .output()
+            .ok()?;
+        if !output.status.success() {
+            return None;
+        }
+        Some(CommandInfo {
+            name: name.to_owned(),
+            cmd: "powershell".to_owned(),
+            args: vec!["-Command".to_owned(), name.to_owned()],
+            version: parse_version(output),
+        })
+    }
+
+    /// Run with `.output` and check for errors/status
+    pub fn output_checked(
+        &self,
+        builder: impl FnOnce(&mut Command) -> &mut Command,
+    ) -> Result<std::process::Output, miette::Report> {
+        let mut command = Command::new(&self.cmd);
+        command.args(&self.args);
+        builder(&mut command);
+        let output = command
+            .output()
+            .into_diagnostic()
+            .wrap_err_with(|| format!("failed to run \"{}\"", pretty_cmd(&self.name, &command)))?;
+        if output.status.success() {
+            Ok(output)
+        } else {
+            let mut out = String::new();
+            let stderr = String::from_utf8_lossy(&output.stderr);
+            let stdout = String::from_utf8_lossy(&output.stdout);
+            out.push_str("\nstdout:\n");
+            out.push_str(&stdout);
+            out.push_str("\nstderr:\n");
+            out.push_str(&stderr);
+            Err(miette!("{out}")).wrap_err_with(|| {
+                format!(
+                    "\"{}\" failed ({})",
+                    pretty_cmd(&self.name, &command),
+                    output.status
+                )
+            })
+        }
+    }
+
+    /// Run with `.output` and only check for errors, DON'T check status
+    pub fn output(
+        &self,
+        builder: impl FnOnce(&mut Command) -> &mut Command,
+    ) -> Result<std::process::Output, miette::Report> {
+        let mut command = Command::new(&self.cmd);
+        command.args(&self.args);
+        builder(&mut command);
+        let output = command
+            .output()
+            .into_diagnostic()
+            .wrap_err_with(|| format!("failed to run \"{}\"", pretty_cmd(&self.name, &command)))?;
+        Ok(output)
+    }
+
+    pub fn version(&self) -> Option<&str> {
+        self.version.as_deref()
+    }
+}
+
+/// Parse out the version from `--version` assuming the standard `app-name 0.1.0` format
+fn parse_version(output: std::process::Output) -> Option<String> {
+    let version_bytes = output.stdout;
+    let version_full = String::from_utf8(version_bytes).ok()?;
+    let version_line = version_full.lines().next()?;
+    let version_suffix = version_line.split_once(' ')?.1.trim().to_owned();
+    Some(version_suffix)
+}
+
+/// Pretty print a command invocation
+fn pretty_cmd(name: &str, cmd: &Command) -> String {
+    let mut out = String::new();
+    out.push_str(name);
+    for arg in cmd.get_args() {
+        out.push(' ');
+        out.push_str(&arg.to_string_lossy())
+    }
+    out
+}
diff --git a/cargo-dist/tests/gallery/dist.rs b/cargo-dist/tests/gallery/dist.rs
new file mode 100644
index 00000000..632189db
--- /dev/null
+++ b/cargo-dist/tests/gallery/dist.rs
@@ -0,0 +1,423 @@
+use std::collections::BTreeMap;
+
+use axoasset::LocalAsset;
+use camino::{Utf8Path, Utf8PathBuf};
+use miette::miette;
+
+use super::command::CommandInfo;
+use super::errors::Result;
+use super::repo::{Repo, TestContext, TestContextLock, ToolsImpl};
+
+/// Set this env-var to enable running the installer scripts in temp dirs
+///
+/// If everything's working right, then no problem.
+/// Otherwise MEGA DANGER in messing up your computer.
+#[cfg(target_family = "unix")]
+const ENV_RUIN_ME: &str = "RUIN_MY_COMPUTER_WITH_INSTALLERS";
+/// Set this at runtime to override STATIC_CARGO_DIST_BIN
+const ENV_RUNTIME_CARGO_DIST_BIN: &str = "OVERRIDE_CARGO_BIN_EXE_cargo-dist";
+const STATIC_CARGO_DIST_BIN: &str = env!("CARGO_BIN_EXE_cargo-dist");
+
+/// axolotlsay 0.1.0 is a nice simple project with shell+powershell+npm installers in its release
+pub static AXOLOTLSAY: TestContextLock<Tools> = TestContextLock::new(&Repo {
+    repo_owner: "axodotdev",
+    repo_name: "axolotlsay",
+    commit_sha: "6b8337fb742908e506296eab3371bb71b76283d7",
+    app_name: "axolotlsay",
+    bins: &["axolotlsay"],
+});
+
+pub struct Tools {
+    pub git: CommandInfo,
+    pub cargo_dist: CommandInfo,
+    pub shellcheck: Option<CommandInfo>,
+    pub psanalyzer: Option<CommandInfo>,
+}
+
+impl Tools {
+    fn new() -> Self {
+        eprintln!("getting tools...");
+        let git = CommandInfo::new("git", None).expect("git isn't installed");
+
+        // If OVERRIDE_* is set, prefer that over the version that cargo built for us,
+        // this lets us test our shippable builds.
+        let cargo_dist_path = std::env::var(ENV_RUNTIME_CARGO_DIST_BIN)
+            .unwrap_or_else(|_| STATIC_CARGO_DIST_BIN.to_owned());
+        let cargo_dist = CommandInfo::new("cargo-dist", Some(&cargo_dist_path))
+            .expect("cargo-dist isn't built!?");
+        cargo_dist
+            .version()
+            .expect("couldn't parse cargo-dist version!?");
+        let shellcheck = CommandInfo::new("shellcheck", None);
+        let psanalyzer = CommandInfo::new_powershell_command("Invoke-ScriptAnalyzer");
+
+        Self {
+            git,
+            cargo_dist,
+            shellcheck,
+            psanalyzer,
+        }
+    }
+}
+
+impl ToolsImpl for Tools {
+    fn git(&self) -> &CommandInfo {
+        &self.git
+    }
+}
+impl Default for Tools {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+pub struct DistResult {
+    test_name: String,
+    shell_installer_path: Option<Utf8PathBuf>,
+    powershell_installer_path: Option<Utf8PathBuf>,
+    npm_installer_package_path: Option<Utf8PathBuf>,
+}
+
+impl TestContext<Tools> {
+    /// Run 'cargo dist build -aglobal' with the toml patched
+    /// and return paths to various files that were generated
+    pub fn cargo_dist_build_global(&self, test_name: &str, new_toml: String) -> Result<DistResult> {
+        eprintln!("\n=============== running test: {test_name} =================");
+        // patch the Cargo.toml
+        self.patch_cargo_toml(new_toml)?;
+
+        // If the cargo-dist target dir exists, delete it to avoid cross-contamination
+        let out_path = Utf8Path::new("target/distrib/");
+        if out_path.exists() {
+            LocalAsset::remove_dir_all(out_path)?;
+        }
+
+        // build installers
+        eprintln!("running cargo dist build -aglobal...");
+        self.tools
+            .cargo_dist
+            .output_checked(|cmd| cmd.arg("dist").arg("build").arg("-aglobal"))?;
+
+        self.load_dist_results(test_name)
+    }
+    /// Run 'cargo dist generate-ci' with the toml patched
+    /// and return the contents of .github/workflows/release.yml
+    pub fn cargo_dist_generate_ci(&self, test_name: &str, new_toml: String) -> Result<String> {
+        eprintln!("\n=============== running test: {test_name} =================");
+        // patch the Cargo.toml
+        self.patch_cargo_toml(new_toml)?;
+
+        let ci_path = Utf8Path::new(".github/workflows/release.yml");
+        // Delete ci.yml if it already exists
+        if ci_path.exists() {
+            LocalAsset::remove_file(ci_path)?;
+        }
+
+        // run generate-ci
+        eprintln!("running cargo dist build -aglobal...");
+        self.tools
+            .cargo_dist
+            .output_checked(|cmd| cmd.arg("dist").arg("generate-ci"))?;
+
+        // load the contents
+        let result = LocalAsset::load_string(ci_path)?;
+
+        Ok(result)
+    }
+
+    fn load_dist_results(&self, test_name: &str) -> Result<DistResult> {
+        // 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 npm_installer =
+            Utf8PathBuf::from(format!("target/distrib/{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),
+            npm_installer_package_path: npm_installer.exists().then_some(npm_installer),
+        })
+    }
+
+    fn patch_cargo_toml(&self, new_toml: String) -> Result<()> {
+        eprintln!("loading Cargo.toml...");
+        let toml_src = axoasset::SourceFile::load_local("Cargo.toml")?;
+        let mut toml = toml_src.deserialize_toml_edit()?;
+        eprintln!("editing Cargo.toml...");
+        let new_table_src = axoasset::SourceFile::new("new-Cargo.toml", new_toml);
+        let new_table = new_table_src.deserialize_toml_edit()?;
+
+        // Written slightly verbosely to make it easier to isolate which failed
+        eprintln!("{new_table}");
+        let old = &mut toml["workspace"]["metadata"]["dist"];
+        let new = &new_table["workspace"]["metadata"]["dist"];
+        *old = new.clone();
+        let toml_out = toml.to_string();
+        eprintln!("writing Cargo.toml...");
+        axoasset::LocalAsset::write_new(&toml_out, "Cargo.toml")?;
+
+        Ok(())
+    }
+}
+
+impl DistResult {
+    pub fn check_all(&self, ctx: &TestContext<Tools>, expected_bin_dir: &str) -> Result<()> {
+        // If we have shellcheck, check our shell script
+        self.shellcheck(ctx)?;
+
+        // If we have PsScriptAnalyzer, check our powershell script
+        self.psanalyzer(ctx)?;
+
+        // If we can, run the script in a temp HOME
+        self.runtest_shell_installer(ctx, expected_bin_dir)?;
+
+        // Now that all other checks have passed, it's safe to check snapshots
+        self.snapshot()?;
+
+        Ok(())
+    }
+
+    /// Run shellcheck on the shell scripts
+    pub fn shellcheck(&self, ctx: &TestContext<Tools>) -> Result<()> {
+        // Only do this if the script is available
+        let Some(script) = &self.shell_installer_path else {
+            return Ok(());
+        };
+        // Only do this if the tool is available
+        let Some(shellcheck) = &ctx.tools.shellcheck else {
+            return Ok(());
+        };
+        eprintln!("shellchecking {script}");
+        let output = shellcheck.output(|cmd| cmd.arg(script))?;
+
+        if !output.status.success() {
+            eprintln!("{}", String::from_utf8_lossy(&output.stdout));
+            return Err(miette!("shellcheck found issues"));
+        }
+        Ok(())
+    }
+
+    /// Run PSScriptAnalyzer on the powershell scripts
+    pub fn psanalyzer(&self, ctx: &TestContext<Tools>) -> Result<()> {
+        // Only do this if the script is available
+        let Some(script) = &self.powershell_installer_path else {
+            return Ok(());
+        };
+        // Only do this if the tool is available
+        let Some(psanalyzer) = &ctx.tools.psanalyzer else {
+            return Ok(());
+        };
+
+        eprintln!("PSScriptAnalyzing {script}");
+        let output = psanalyzer.output(|cmd| cmd.arg(script).arg("-EnableExit"))?;
+
+        if !output.status.success() {
+            eprintln!("{}", String::from_utf8_lossy(&output.stdout));
+            eprintln!("see https://learn.microsoft.com/en-ca/powershell/utility-modules/psscriptanalyzer/rules/readme\n");
+            return Err(miette!("PsScriptAnalyzer found issues"));
+        }
+        Ok(())
+    }
+
+    // Runs the installer script in a temp dir, attempting to set env vars to contain it to that dir
+    #[allow(unused_variables)]
+    pub fn runtest_shell_installer(
+        &self,
+        ctx: &TestContext<Tools>,
+        expected_bin_dir: &str,
+    ) -> Result<()> {
+        // Only do this on unix, and only do it if RUIN_MY_COMPUTER_WITH_INSTALLERS is set
+        #[cfg(target_family = "unix")]
+        if std::env::var(ENV_RUIN_ME)
+            .map(|s| !s.is_empty())
+            .unwrap_or(false)
+        {
+            let app_name = ctx.repo.app_name;
+            let test_name = &self.test_name;
+
+            // only do this if the script exists
+            let Some(shell_path) = &self.shell_installer_path else {
+                return Ok(());
+            };
+            eprintln!("running installer.sh...");
+            // Make installer.sh executable
+            use std::os::unix::prelude::PermissionsExt;
+            std::fs::set_permissions(shell_path, std::fs::Permissions::from_mode(0o755)).unwrap();
+            let script = CommandInfo::new_unchecked("installer.sh", Some(shell_path.as_str()));
+
+            // Create/clobber a temp dir in target
+            let cur_dir =
+                Utf8PathBuf::from_path_buf(std::env::current_dir().unwrap()).expect("non-utf8 cwd");
+            let parent = cur_dir.parent().unwrap();
+            let tempdir = parent.join(format!("{app_name}__{test_name}"));
+            if tempdir.exists() {
+                std::fs::remove_dir_all(&tempdir).unwrap();
+            }
+            std::fs::create_dir_all(&tempdir).unwrap();
+
+            // Run the installer script with:
+            //
+            // HOME="{tempdir}"            (for install-path=~/... and install-path=CARGO_HOME)
+            // MY_ENV_VAR=".{app_name}"    (for install-path=$MY_ENV_VAR/...)
+            // CARGO_HOME=null             (cargo test sets this so we have to clear it)
+            let app_home = tempdir.join(format!(".{app_name}"));
+            let _output = script.output_checked(|cmd| {
+                cmd.env("HOME", &tempdir)
+                    .env("MY_ENV_VAR", &app_home)
+                    .env_remove("CARGO_HOME")
+            })?;
+            // we could theoretically look at the above output and parse out the `source` line...
+
+            // Check that the script wrote files where we expected
+            let rcfile = tempdir.join(".profile");
+            let expected_bin_dir = Utf8PathBuf::from(expected_bin_dir);
+            let bin_dir = tempdir.join(&expected_bin_dir);
+            let env_dir = if expected_bin_dir
+                .components()
+                .any(|d| d.as_str() == ".cargo")
+            {
+                bin_dir.parent().unwrap()
+            } else {
+                &bin_dir
+            };
+            let env_script = env_dir.join("env");
+
+            assert!(bin_dir.exists(), "bin dir wasn't created");
+            assert!(rcfile.exists(), ".profile wasn't created");
+            assert!(env_script.exists(), "env script wasn't created");
+
+            // Check that all the binaries work
+            for bin_name in ctx.repo.bins {
+                let bin_path = bin_dir.join(bin_name);
+                assert!(bin_path.exists(), "bin wasn't created");
+
+                let bin =
+                    CommandInfo::new(bin_name, Some(bin_path.as_str())).expect("failed to run bin");
+                assert!(bin.version().is_some(), "failed to get app version");
+                eprintln!("installer.sh worked!");
+
+                // Check that sourcing the env script works (puts the right thing on path)
+                eprintln!("checking env script..");
+
+                // Make a test.sh script that sources the env script and then runs `which {bin_name}`
+                let test_script_path = tempdir.join("test.sh");
+                let test_script_text = format!(
+                    r#"#!/bin/sh
+
+                . {rcfile}
+                which {bin_name}
+                "#
+                );
+                LocalAsset::write_new(&test_script_text, &test_script_path)?;
+                std::fs::set_permissions(&test_script_path, std::fs::Permissions::from_mode(0o755))
+                    .unwrap();
+                let sh = CommandInfo::new_unchecked("test.sh", Some(test_script_path.as_str()));
+
+                // Run test.sh and check that the output matches
+                // NOTE: we only set HOME here to make sure that the early-bound vs late-bound env-var stuff works
+                // ($HOME should be kept as a variable, but $MY_ENV_VAR and $CARGO_HOME should be resolved permanently
+                // at install-time, so things should work if we don't set MY_ENV_VAR anymore)
+                let output = sh.output_checked(|cmd| cmd.env("HOME", &tempdir))?;
+                assert_eq!(
+                    String::from_utf8(output.stdout).unwrap().trim(),
+                    bin_path.as_str(),
+                    "bin path wasn't right"
+                );
+            }
+        }
+        Ok(())
+    }
+
+    // Run cargo-insta on everything we care to snapshot
+    pub fn snapshot(&self) -> Result<()> {
+        // We make a single uber-snapshot for both scripts to avoid the annoyances of having multiple snapshots
+        // in one test (necessitating rerunning it multiple times or passing special flags to get all the changes)
+        let mut snapshots = String::new();
+
+        Self::append_snapshot_file(
+            &mut snapshots,
+            "installer.sh",
+            self.shell_installer_path.as_deref(),
+        )?;
+        Self::append_snapshot_file(
+            &mut snapshots,
+            "installer.ps1",
+            self.powershell_installer_path.as_deref(),
+        )?;
+        Self::append_snapshot_tarball(
+            &mut snapshots,
+            "npm-package.tar.gz",
+            self.npm_installer_package_path.as_deref(),
+        )?;
+
+        let test_name = &self.test_name;
+        insta::assert_snapshot!(format!("{test_name}-installers"), &snapshots);
+        Ok(())
+    }
+
+    fn append_snapshot_tarball(
+        out: &mut String,
+        name: &str,
+        src_path: Option<&Utf8Path>,
+    ) -> Result<()> {
+        use std::io::Read;
+
+        // Skip snapshotting this file if absent
+        let Some(src_path) = src_path else {
+            return Ok(());
+        };
+
+        // We shove everything in a BTreeMap to keep ordering stable
+        let mut results = BTreeMap::new();
+
+        let file = LocalAsset::load_bytes(src_path)?;
+        let gz_decoder = flate2::read::GzDecoder::new(&file[..]);
+        let mut tar_decoder = tar::Archive::new(gz_decoder);
+        let entries = tar_decoder.entries().expect("couldn't read tar");
+        for entry in entries {
+            let mut entry = entry.expect("couldn't read tar entry");
+            if entry.header().entry_type() == tar::EntryType::Regular {
+                let path = entry
+                    .path()
+                    .expect("couldn't get tarred file's path")
+                    .to_string_lossy()
+                    .into_owned();
+                let mut val = String::new();
+                entry
+                    .read_to_string(&mut val)
+                    .expect("couldn't read tarred file to string");
+                results.insert(path, val);
+            }
+        }
+
+        for (path, val) in &results {
+            Self::append_snapshot_string(out, &format!("{name}/{path}"), val)?;
+        }
+        Ok(())
+    }
+
+    fn append_snapshot_file(
+        out: &mut String,
+        name: &str,
+        src_path: Option<&Utf8Path>,
+    ) -> Result<()> {
+        // Skip snapshotting this file if absent
+        let Some(src_path) = src_path else {
+            return Ok(());
+        };
+
+        let src = axoasset::LocalAsset::load_string(src_path)?;
+        Self::append_snapshot_string(out, name, &src)
+    }
+
+    fn append_snapshot_string(out: &mut String, name: &str, val: &str) -> Result<()> {
+        use std::fmt::Write;
+
+        writeln!(out, "\n\n================ {name} ================").unwrap();
+        writeln!(out, "{val}").unwrap();
+        Ok(())
+    }
+}
diff --git a/cargo-dist/tests/gallery/errors.rs b/cargo-dist/tests/gallery/errors.rs
new file mode 100644
index 00000000..00134a2f
--- /dev/null
+++ b/cargo-dist/tests/gallery/errors.rs
@@ -0,0 +1,1 @@
+pub type Result<T> = std::result::Result<T, miette::Report>;
diff --git a/cargo-dist/tests/gallery/mod.rs b/cargo-dist/tests/gallery/mod.rs
new file mode 100644
index 00000000..1ff4f060
--- /dev/null
+++ b/cargo-dist/tests/gallery/mod.rs
@@ -0,0 +1,26 @@
+mod command;
+mod dist;
+mod errors;
+mod repo;
+
+pub use dist::*;
+
+/// Taken from cargo-insta to avoid copy-paste errors
+///
+/// Gets the ~name of the function running this macro
+#[macro_export]
+macro_rules! _function_name {
+    () => {{
+        fn f() {}
+        fn type_name_of_val<T>(_: T) -> &'static str {
+            std::any::type_name::<T>()
+        }
+        let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
+        while let Some(rest) = name.strip_suffix("::{{closure}}") {
+            name = rest;
+        }
+        name.split_once("::")
+            .map(|(_module, func)| func)
+            .unwrap_or(name)
+    }};
+}
diff --git a/cargo-dist/tests/gallery/repo.rs b/cargo-dist/tests/gallery/repo.rs
new file mode 100644
index 00000000..9095fcf6
--- /dev/null
+++ b/cargo-dist/tests/gallery/repo.rs
@@ -0,0 +1,120 @@
+use std::sync::Mutex;
+
+use camino::Utf8PathBuf;
+use miette::IntoDiagnostic;
+
+use super::command::CommandInfo;
+use super::errors::Result;
+
+const TARGET_TEMP_DIR: &str = env!("CARGO_TARGET_TMPDIR");
+
+pub struct TestContextLock<ToolsImpl> {
+    repo: &'static Repo,
+    ctx: Mutex<Option<TestContext<ToolsImpl>>>,
+}
+
+pub struct TestContext<ToolsImpl> {
+    pub repo: &'static Repo,
+    pub tools: ToolsImpl,
+}
+
+pub struct Repo {
+    pub repo_owner: &'static str,
+    pub repo_name: &'static str,
+    pub commit_sha: &'static str,
+    pub app_name: &'static str,
+    pub bins: &'static [&'static str],
+}
+
+pub trait ToolsImpl: Default {
+    fn git(&self) -> &CommandInfo;
+}
+
+impl<Tools> TestContextLock<Tools>
+where
+    Tools: ToolsImpl,
+{
+    pub const fn new(repo: &'static Repo) -> Self {
+        Self {
+            repo,
+            ctx: Mutex::new(None),
+        }
+    }
+    pub fn run_test(&self, f: impl FnOnce(&TestContext<Tools>) -> Result<()>) -> Result<()> {
+        let maybe_guard = self.ctx.lock();
+        // It's fine for the mutex to be poisoned once the value is Some because none of the tests
+        // are allowed to mutate the TestContext. But if it's poisoned while None that means we
+        // encountered an error while setting up TestContext and should just abort everything
+        // instead of retrying over and over.
+        let mut guard = match maybe_guard {
+            Ok(guard) => guard,
+            Err(poison) => {
+                let guard = poison.into_inner();
+                if guard.is_none() {
+                    panic!("aborting all tests: failed fetch");
+                }
+                guard
+            }
+        };
+
+        if guard.is_none() {
+            // Intentionally unwrapping here to poison the mutex if we can't fetch
+            let ctx = self.init_context().unwrap();
+            *guard = Some(ctx);
+        }
+
+        let ctx = guard.as_ref().unwrap();
+
+        f(ctx)
+    }
+
+    fn init_context(&self) -> Result<TestContext<Tools>> {
+        let Repo {
+            repo_owner,
+            repo_name,
+            commit_sha,
+            ..
+        } = self.repo;
+        let repo_url = format!("https://github.com/{repo_owner}/{repo_name}");
+
+        // Get the tools we'll invoke
+        let tools = Tools::default();
+
+        // Clone the repo we're interested in and cd into it
+        Self::fetch_repo(tools.git(), repo_name, &repo_url, commit_sha)?;
+
+        // Run tests
+        let ctx = TestContext {
+            repo: self.repo,
+            tools,
+        };
+        Ok(ctx)
+    }
+
+    /// Fetch/update a repo to the given commit_sha
+    fn fetch_repo(
+        git: &CommandInfo,
+        repo_name: &str,
+        repo_url: &str,
+        commit_sha: &str,
+    ) -> Result<()> {
+        std::env::set_current_dir(TARGET_TEMP_DIR).into_diagnostic()?;
+        if Utf8PathBuf::from(repo_name).exists() {
+            eprintln!("repo already cloned, updating it...");
+            std::env::set_current_dir(repo_name).into_diagnostic()?;
+            git.output_checked(|c| c.arg("remote").arg("set-url").arg("origin").arg(repo_url))?;
+            git.output_checked(|c| c.arg("fetch").arg("origin").arg(commit_sha))?;
+            git.output_checked(|c| c.arg("reset").arg("--hard").arg("FETCH_HEAD"))?;
+        } else {
+            eprintln!("fetching {repo_url}");
+            axoasset::LocalAsset::create_dir(repo_name)?;
+            std::env::set_current_dir(repo_name).into_diagnostic()?;
+            git.output_checked(|c| c.arg("init"))?;
+            git.output_checked(|c| c.arg("remote").arg("add").arg("origin").arg(repo_url))?;
+            git.output_checked(|c| c.arg("fetch").arg("origin").arg(commit_sha))?;
+            git.output_checked(|c| c.arg("reset").arg("--hard").arg("FETCH_HEAD"))?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__basic-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__basic-installers.snap
new file mode 100644
index 00000000..035d4b35
--- /dev/null
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__basic-installers.snap
@@ -0,0 +1,1818 @@
+---
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
+---
+
+
+================ installer.sh ================
+#!/bin/sh
+# shellcheck shell=dash
+#
+# Licensed under the MIT license
+# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+# option. This file may not be copied, modified, or distributed
+# except according to those terms.
+
+if [ "$KSH_VERSION" = 'Version JM 93t+ 2010-03-05' ]; then
+    # The version of ksh93 that ships with many illumos systems does not
+    # support the "local" extension.  Print a message rather than fail in
+    # subtle ways later on:
+    echo 'this installer does not work with this ksh93 version; please try bash!' >&2
+    exit 1
+fi
+
+set -u
+
+APP_NAME="axolotlsay"
+APP_VERSION="0.1.0"
+ARTIFACT_DOWNLOAD_URL="${INSTALLER_DOWNLOAD_URL:-https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0}"
+PRINT_VERBOSE=${INSTALLER_PRINT_VERBOSE:-0}
+PRINT_QUIET=${INSTALLER_PRINT_QUIET:-0}
+NO_MODIFY_PATH=${INSTALLER_NO_MODIFY_PATH:-0}
+
+usage() {
+    # print help (this cat/EOF stuff is a "heredoc" string)
+    cat <<EOF
+axolotlsay-installer.sh
+
+The installer for axolotlsay 0.1.0
+
+This script detects what platform you're on and fetches an appropriate archive from
+https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
+then unpacks the binaries and installs them to \$CARGO_HOME/bin (\$HOME/.cargo/bin)
+
+It will then add that dir to PATH by adding the appropriate line to \$HOME/.profile
+
+USAGE:
+    axolotlsay-installer.sh [OPTIONS]
+
+OPTIONS:
+    -v, --verbose
+            Enable verbose output
+
+    -q, --quiet
+            Disable progress output
+
+        --no-modify-path
+            Don't configure the PATH environment variable
+
+    -h, --help
+            Print help information
+EOF
+}
+
+download_binary_and_run_installer() {
+    downloader --check
+    need_cmd uname
+    need_cmd mktemp
+    need_cmd chmod
+    need_cmd mkdir
+    need_cmd rm
+    need_cmd tar
+    need_cmd which
+    need_cmd grep
+    need_cmd cat
+
+    for arg in "$@"; do
+        case "$arg" in
+            --help)
+                usage
+                exit 0
+                ;;
+            --quiet)
+                PRINT_QUIET=1
+                ;;
+            --verbose)
+                PRINT_VERBOSE=1
+                ;;
+            --no-modify-path)
+                NO_MODIFY_PATH=1
+                ;;
+            *)
+                OPTIND=1
+                if [ "${arg%%--*}" = "" ]; then
+                    err "unknown option $arg"
+                    continue
+                fi
+                while getopts :hvq sub_arg "$arg"; do
+                    case "$sub_arg" in
+                        h)
+                            usage
+                            exit 0
+                            ;;
+                        v)
+                            # user wants to skip the prompt --
+                            # we don't need /dev/tty
+                            PRINT_VERBOSE=1
+                            ;;
+                        q)
+                            # user wants to skip the prompt --
+                            # we don't need /dev/tty
+                            PRINT_QUIET=1
+                            ;;
+                        *)
+                            err "unknown option -$OPTARG"
+                            ;;
+                        esac
+                done
+                ;;
+        esac
+    done
+
+    get_architecture || return 1
+    local _arch="$RETVAL"
+    assert_nz "$_arch" "arch"
+
+    local _bins
+    local _zip_ext
+    local _artifact_name
+
+    # Lookup what to download/unpack based on platform
+    case "$_arch" in 
+        "aarch64-apple-darwin")
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
+            _bins="axolotlsay"
+            ;;
+        "x86_64-apple-darwin")
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
+            _bins="axolotlsay"
+            ;;
+        "x86_64-unknown-linux-gnu")
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
+            _bins="axolotlsay"
+            ;;
+        *)
+            err "there isn't a package for $_arch"
+            ;;
+    esac
+
+    # download the archive
+    local _url="$ARTIFACT_DOWNLOAD_URL/$_artifact_name"
+    local _dir
+    if ! _dir="$(ensure mktemp -d)"; then
+        # Because the previous command ran in a subshell, we must manually
+        # propagate exit status.
+        exit 1
+    fi
+    local _file="$_dir/input$_zip_ext"
+
+    say "downloading $APP_NAME $APP_VERSION ${_arch}" 1>&2
+    say_verbose "  from $_url" 1>&2
+    say_verbose "  to $_file" 1>&2
+
+    ensure mkdir -p "$_dir"
+    
+    if ! downloader "$_url" "$_file"; then
+      say "failed to download $_url"
+      say "this may be a standard network error, but it may also indicate"
+      say "that $APP_NAME's release process is not working. When in doubt"
+      say "please feel free to open an issue!"
+      exit 1
+    fi
+
+    # unpack the archive
+    case "$_zip_ext" in
+        ".zip")
+            ensure unzip -q "$_file" -d "$_dir"
+            ;;
+
+        ".tar."*)
+            ensure tar xf "$_file" --strip-components 1 -C "$_dir"
+            ;;
+        *)
+            err "unknown archive format: $_zip_ext"
+            ;;
+    esac
+
+    install "$_dir" "$_bins" "$@"
+    local _retval=$?
+
+    ignore rm -rf "$_dir"
+
+    return "$_retval"
+}
+
+# See discussion of late-bound vs early-bound for why we use single-quotes with env vars
+# shellcheck disable=SC2016
+install() {
+    # This code needs to both compute certain paths for itself to write to, and
+    # also write them to shell/rc files so that they can look them up to e.g.
+    # add them to PATH. This requires an active distinction between paths
+    # and expressions that can compute them.
+    #
+    # The distinction lies in when we want env-vars to be evaluated. For instance
+    # if we determine that we want to install to $HOME/.myapp, which do we add
+    # to e.g. $HOME/.profile:
+    #
+    # * early-bound: export PATH="/home/myuser/.myapp:$PATH"
+    # * late-bound:  export PATH="$HOME/.myapp:$PATH"
+    #
+    # In this case most people would prefer the late-bound version, but in other
+    # cases the early-bound version might be a better idea. In particular when using
+    # other env-vars than $HOME, they are more likely to be only set temporarily
+    # for the duration of this install script, so it's more advisable to erase their
+    # existence with early-bounding.
+    #
+    # This distinction is handled by "double-quotes" (early) vs 'single-quotes' (late).
+    #
+    # This script has a few different variants, the most complex one being the
+    # CARGO_HOME version which attempts to install things to Cargo's bin dir,
+    # potentially setting up a minimal version if the user hasn't ever installed Cargo.
+    #
+    # In this case we need to:
+    #
+    # * Install to $HOME/.cargo/bin/
+    # * Create a shell script at $HOME/.cargo/env that:
+    #   * Checks if $HOME/.cargo/bin/ is on PATH
+    #   * and if not prepends it to PATH
+    # * Edits $HOME/.profile to run $HOME/.cargo/env (if the line doesn't exist)
+    #
+    # To do this we need these 4 values:
+
+    # The actual path we're going to install to
+    local _install_dir
+    # Path to the an shell script that adds install_dir to PATH
+    local _env_script_path
+    # Potentially-late-bound version of install_dir to write env_script
+    local _install_dir_expr
+    # Potentially-late-bound version of env_script_path to write to rcfiles like $HOME/.profile
+    local _env_script_path_expr
+
+
+    # first try CARGO_HOME, then fallback to HOME
+    if [ -n "${CARGO_HOME:-}" ]; then
+        _install_dir="$CARGO_HOME/bin"
+        _env_script_path="$CARGO_HOME/env"
+        # If CARGO_HOME was set but it ended up being the default $HOME-based path,
+        # then keep things late-bound. Otherwise bake the value for safety.
+        # This is what rustup does, and accurately reproducing it is useful.
+        if [ -n "${HOME:-}" ]; then
+            if [ "$HOME/.cargo/bin" = "$_install_dir" ]; then
+                _install_dir_expr='$HOME/.cargo/bin'
+                _env_script_path_expr='$HOME/.cargo/env'
+            else
+                _install_dir_expr="$_install_dir"
+                _env_script_path_expr="$_env_script_path"
+            fi
+        else
+            _install_dir_expr="$_install_dir"
+            _env_script_path_expr="$_env_script_path"
+        fi
+    elif [ -n "${HOME:-}" ]; then
+        _install_dir="$HOME/.cargo/bin"
+        _env_script_path="$HOME/.cargo/env"
+        _install_dir_expr='$HOME/.cargo/bin'
+        _env_script_path_expr='$HOME/.cargo/env'
+    else
+        err "could not find your CARGO_HOME or HOME dir to install binaries to"
+    fi
+
+    say "installing to $_install_dir"
+    ensure mkdir -p "$_install_dir"
+
+    # copy all the binaries to the install dir
+    local _src_dir="$1"
+    local _bins="$2"
+    for _bin_name in $_bins; do
+        local _bin="$_src_dir/$_bin_name"
+        ensure cp "$_bin" "$_install_dir"
+        # unzip seems to need this chmod
+        ensure chmod +x "$_install_dir/$_bin_name"
+        say "  $_bin_name"
+    done
+
+    say "everything's installed!"
+
+    if [ "0" = "$NO_MODIFY_PATH" ]; then
+        add_install_dir_to_path "$_install_dir_expr" "$_env_script_path" "$_env_script_path_expr"
+    fi
+}
+
+add_install_dir_to_path() {
+    # Edit rcfiles ($HOME/.profile) to add install_dir to $PATH
+    #
+    # We do this slightly indirectly by creating an "env" shell script which checks if install_dir
+    # is on $PATH already, and prepends it if not. The actual line we then add to rcfiles
+    # is to just source that script. This allows us to blast it into lots of different rcfiles and
+    # have it run multiple times without causing problems. It's also specifically compatible
+    # with the system rustup uses, so that we don't conflict with it.
+    local _install_dir_expr="$1"
+    local _env_script_path="$2"
+    local _env_script_path_expr="$3"
+    if [ -n "${HOME:-}" ]; then
+        local _rcfile="$HOME/.profile"
+        # `source x` is an alias for `. x`, and the latter is more portable/actually-posix.
+        # This apparently comes up a lot on freebsd. It's easy enough to always add
+        # the more robust line to rcfiles, but when telling the user to apply the change
+        # to their current shell ". x" is pretty easy to misread/miscopy, so we use the
+        # prettier "source x" line there. Hopefully people with Weird Shells are aware
+        # this is a thing and know to tweak it (or just restart their shell).
+        local _robust_line=". \"$_env_script_path_expr\""
+        local _pretty_line="source \"$_env_script_path_expr\""
+
+        # Add the env script if it doesn't already exist
+        if [ ! -f "$_env_script_path" ]; then 
+            say_verbose "creating $_env_script_path"
+            write_env_script "$_install_dir_expr" "$_env_script_path"
+        else
+            say_verbose "$_env_script_path already exists"
+        fi
+
+        # Check if the line is already in the rcfile
+        # grep: 0 if matched, 1 if no match, and 2 if an error occurred
+        #
+        # Ideally we could use quiet grep (-q), but that makes "match" and "error"
+        # have the same behaviour, when we want "no match" and "error" to be the same
+        # (on error we want to create the file, which >> conveniently does)
+        #
+        # We search for both kinds of line here just to do the right thing in more cases.
+        if ! grep -F "$_robust_line" "$_rcfile" > /dev/null 2>/dev/null && \
+           ! grep -F "$_pretty_line" "$_rcfile" > /dev/null 2>/dev/null
+        then 
+            # If the script now exists, add the line to source it to the rcfile
+            # (This will also create the rcfile if it doesn't exist)
+            if [ -f "$_env_script_path" ]; then 
+                say_verbose "adding $_robust_line to $_rcfile"
+                ensure echo "$_robust_line" >> "$_rcfile"
+                say ""
+                say "To add $_install_dir_expr to your PATH, either restart your shell or run:"
+                say ""
+                say "    $_pretty_line"
+            fi
+        else
+            say_verbose "$_install_dir already on PATH"
+        fi
+    fi
+}
+
+write_env_script() {
+    # write this env script to the given path (this cat/EOF stuff is a "heredoc" string)
+    local _install_dir_expr="$1"
+    local _env_script_path="$2"
+    ensure cat <<EOF > "$_env_script_path"
+#!/bin/sh
+# add binaries to PATH if they aren't added yet
+# affix colons on either side of \$PATH to simplify matching
+case ":\${PATH}:" in
+    *:"$_install_dir_expr":*)
+        ;;
+    *)
+        # Prepending path in case a system-installed binary needs to be overridden
+        export PATH="$_install_dir_expr:\$PATH"
+        ;;
+esac
+EOF
+}
+
+check_proc() {
+    # Check for /proc by looking for the /proc/self/exe link
+    # This is only run on Linux
+    if ! test -L /proc/self/exe ; then
+        err "fatal: Unable to find /proc/self/exe.  Is /proc mounted?  Installation cannot proceed without /proc."
+    fi
+}
+
+get_bitness() {
+    need_cmd head
+    # Architecture detection without dependencies beyond coreutils.
+    # ELF files start out "\x7fELF", and the following byte is
+    #   0x01 for 32-bit and
+    #   0x02 for 64-bit.
+    # The printf builtin on some shells like dash only supports octal
+    # escape sequences, so we use those.
+    local _current_exe_head
+    _current_exe_head=$(head -c 5 /proc/self/exe )
+    if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then
+        echo 32
+    elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then
+        echo 64
+    else
+        err "unknown platform bitness"
+    fi
+}
+
+is_host_amd64_elf() {
+    need_cmd head
+    need_cmd tail
+    # ELF e_machine detection without dependencies beyond coreutils.
+    # Two-byte field at offset 0x12 indicates the CPU,
+    # but we're interested in it being 0x3E to indicate amd64, or not that.
+    local _current_exe_machine
+    _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1)
+    [ "$_current_exe_machine" = "$(printf '\076')" ]
+}
+
+get_endianness() {
+    local cputype=$1
+    local suffix_eb=$2
+    local suffix_el=$3
+
+    # detect endianness without od/hexdump, like get_bitness() does.
+    need_cmd head
+    need_cmd tail
+
+    local _current_exe_endianness
+    _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)"
+    if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then
+        echo "${cputype}${suffix_el}"
+    elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then
+        echo "${cputype}${suffix_eb}"
+    else
+        err "unknown platform endianness"
+    fi
+}
+
+get_architecture() {
+    local _ostype
+    local _cputype
+    _ostype="$(uname -s)"
+    _cputype="$(uname -m)"
+    local _clibtype="gnu"
+
+    if [ "$_ostype" = Linux ]; then
+        if [ "$(uname -o)" = Android ]; then
+            _ostype=Android
+        fi
+        if ldd --version 2>&1 | grep -q 'musl'; then
+            _clibtype="musl"
+        fi
+    fi
+
+    if [ "$_ostype" = Darwin ] && [ "$_cputype" = i386 ]; then
+        # Darwin `uname -m` lies
+        if sysctl hw.optional.x86_64 | grep -q ': 1'; then
+            _cputype=x86_64
+        fi
+    fi
+
+    if [ "$_ostype" = SunOS ]; then
+        # Both Solaris and illumos presently announce as "SunOS" in "uname -s"
+        # so use "uname -o" to disambiguate.  We use the full path to the
+        # system uname in case the user has coreutils uname first in PATH,
+        # which has historically sometimes printed the wrong value here.
+        if [ "$(/usr/bin/uname -o)" = illumos ]; then
+            _ostype=illumos
+        fi
+
+        # illumos systems have multi-arch userlands, and "uname -m" reports the
+        # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86
+        # systems.  Check for the native (widest) instruction set on the
+        # running kernel:
+        if [ "$_cputype" = i86pc ]; then
+            _cputype="$(isainfo -n)"
+        fi
+    fi
+
+    case "$_ostype" in
+
+        Android)
+            _ostype=linux-android
+            ;;
+
+        Linux)
+            check_proc
+            _ostype=unknown-linux-$_clibtype
+            _bitness=$(get_bitness)
+            ;;
+
+        FreeBSD)
+            _ostype=unknown-freebsd
+            ;;
+
+        NetBSD)
+            _ostype=unknown-netbsd
+            ;;
+
+        DragonFly)
+            _ostype=unknown-dragonfly
+            ;;
+
+        Darwin)
+            _ostype=apple-darwin
+            ;;
+
+        illumos)
+            _ostype=unknown-illumos
+            ;;
+
+        MINGW* | MSYS* | CYGWIN* | Windows_NT)
+            _ostype=pc-windows-gnu
+            ;;
+
+        *)
+            err "unrecognized OS type: $_ostype"
+            ;;
+
+    esac
+
+    case "$_cputype" in
+
+        i386 | i486 | i686 | i786 | x86)
+            _cputype=i686
+            ;;
+
+        xscale | arm)
+            _cputype=arm
+            if [ "$_ostype" = "linux-android" ]; then
+                _ostype=linux-androideabi
+            fi
+            ;;
+
+        armv6l)
+            _cputype=arm
+            if [ "$_ostype" = "linux-android" ]; then
+                _ostype=linux-androideabi
+            else
+                _ostype="${_ostype}eabihf"
+            fi
+            ;;
+
+        armv7l | armv8l)
+            _cputype=armv7
+            if [ "$_ostype" = "linux-android" ]; then
+                _ostype=linux-androideabi
+            else
+                _ostype="${_ostype}eabihf"
+            fi
+            ;;
+
+        aarch64 | arm64)
+            _cputype=aarch64
+            ;;
+
+        x86_64 | x86-64 | x64 | amd64)
+            _cputype=x86_64
+            ;;
+
+        mips)
+            _cputype=$(get_endianness mips '' el)
+            ;;
+
+        mips64)
+            if [ "$_bitness" -eq 64 ]; then
+                # only n64 ABI is supported for now
+                _ostype="${_ostype}abi64"
+                _cputype=$(get_endianness mips64 '' el)
+            fi
+            ;;
+
+        ppc)
+            _cputype=powerpc
+            ;;
+
+        ppc64)
+            _cputype=powerpc64
+            ;;
+
+        ppc64le)
+            _cputype=powerpc64le
+            ;;
+
+        s390x)
+            _cputype=s390x
+            ;;
+        riscv64)
+            _cputype=riscv64gc
+            ;;
+        loongarch64)
+            _cputype=loongarch64
+            ;;
+        *)
+            err "unknown CPU type: $_cputype"
+
+    esac
+
+    # Detect 64-bit linux with 32-bit userland
+    if [ "${_ostype}" = unknown-linux-gnu ] && [ "${_bitness}" -eq 32 ]; then
+        case $_cputype in
+            x86_64)
+                # 32-bit executable for amd64 = x32
+                if is_host_amd64_elf; then {
+                    err "x32 linux unsupported"
+                }; else
+                    _cputype=i686
+                fi
+                ;;
+            mips64)
+                _cputype=$(get_endianness mips '' el)
+                ;;
+            powerpc64)
+                _cputype=powerpc
+                ;;
+            aarch64)
+                _cputype=armv7
+                if [ "$_ostype" = "linux-android" ]; then
+                    _ostype=linux-androideabi
+                else
+                    _ostype="${_ostype}eabihf"
+                fi
+                ;;
+            riscv64gc)
+                err "riscv64 with 32-bit userland unsupported"
+                ;;
+        esac
+    fi
+
+    # treat armv7 systems without neon as plain arm
+    if [ "$_ostype" = "unknown-linux-gnueabihf" ] && [ "$_cputype" = armv7 ]; then
+        if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then
+            # At least one processor does not have NEON.
+            _cputype=arm
+        fi
+    fi
+
+    _arch="${_cputype}-${_ostype}"
+
+    RETVAL="$_arch"
+}
+
+say() {
+    if [ "0" = "$PRINT_QUIET" ]; then
+        echo "$1"
+    fi
+}
+
+say_verbose() {
+    if [ "1" = "$PRINT_VERBOSE" ]; then
+        echo "$1"
+    fi
+}
+
+err() {
+    if [ "0" = "$PRINT_QUIET" ]; then
+        local red
+        local reset
+        red=$(tput setaf 1 2>/dev/null || echo '')
+        reset=$(tput sgr0 2>/dev/null || echo '')
+        say "${red}ERROR${reset}: $1" >&2
+    fi
+    exit 1
+}
+
+need_cmd() {
+    if ! check_cmd "$1"
+    then err "need '$1' (command not found)"
+    fi
+}
+
+check_cmd() {
+    command -v "$1" > /dev/null 2>&1
+    return $?
+}
+
+assert_nz() {
+    if [ -z "$1" ]; then err "assert_nz $2"; fi
+}
+
+# Run a command that should never fail. If the command fails execution
+# will immediately terminate with an error showing the failing
+# command.
+ensure() {
+    if ! "$@"; then err "command failed: $*"; fi
+}
+
+# This is just for indicating that commands' results are being
+# intentionally ignored. Usually, because it's being executed
+# as part of error handling.
+ignore() {
+    "$@"
+}
+
+# This wraps curl or wget. Try curl first, if not installed,
+# use wget instead.
+downloader() {
+    if check_cmd curl
+    then _dld=curl
+    elif check_cmd wget
+    then _dld=wget
+    else _dld='curl or wget' # to be used in error message of need_cmd
+    fi
+
+    if [ "$1" = --check ]
+    then need_cmd "$_dld"
+    elif [ "$_dld" = curl ]
+    then curl -sSfL "$1" -o "$2"
+    elif [ "$_dld" = wget ]
+    then wget "$1" -O "$2"
+    else err "Unknown downloader"   # should not reach here
+    fi
+}
+
+download_binary_and_run_installer "$@" || exit 1
+
+
+================ installer.ps1 ================
+# Licensed under the MIT license
+# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+# option. This file may not be copied, modified, or distributed
+# except according to those terms.
+
+<#
+.SYNOPSIS
+
+The installer for axolotlsay 0.1.0
+
+.DESCRIPTION
+
+This script detects what platform you're on and fetches an appropriate archive from
+https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
+then unpacks the binaries and installs them to $env:CARGO_HOME\bin ($HOME\.cargo\bin)
+
+It will then add that dir to PATH by editing your Environment.Path registry key
+
+.PARAMETER ArtifactDownloadUrl
+The URL of the directory where artifacts can be fetched from
+
+.PARAMETER NoModifyPath
+Don't add the install directory to PATH
+
+.PARAMETER Help
+Print help
+
+#>
+
+param (
+    [Parameter(HelpMessage = "The URL of the directory where artifacts can be fetched from")]
+    [string]$ArtifactDownloadUrl = 'https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0',
+    [Parameter(HelpMessage = "Don't add the install directory to PATH")]
+    [switch]$NoModifyPath,
+    [Parameter(HelpMessage = "Print Help")]
+    [switch]$Help
+)
+
+$app_name = 'axolotlsay'
+$app_version = '0.1.0'
+
+function Install-Binary($install_args) {
+  if ($Help) {
+    Get-Help $PSCommandPath -Detailed
+    Exit
+  }
+  $old_erroractionpreference = $ErrorActionPreference
+  $ErrorActionPreference = 'stop'
+
+  Initialize-Environment
+
+  # Platform info injected by cargo-dist
+  $platforms = @{
+    "x86_64-pc-windows-msvc" = @{
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
+      "bins" = "axolotlsay.exe"
+      "zip_ext" = ".tar.gz"
+    }
+  }
+
+  $fetched = Download "$ArtifactDownloadUrl" $platforms
+  # FIXME: add a flag that lets the user not do this step
+  Invoke-Installer $fetched "$install_args"
+
+  $ErrorActionPreference = $old_erroractionpreference
+}
+
+function Get-TargetTriple() {
+  try {
+    # NOTE: this might return X64 on ARM64 Windows, which is OK since emulation is available.
+    # It works correctly starting in PowerShell Core 7.3 and Windows PowerShell in Win 11 22H2.
+    # Ideally this would just be
+    #   [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
+    # but that gets a type from the wrong assembly on Windows PowerShell (i.e. not Core)
+    $a = [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime.InteropServices.RuntimeInformation")
+    $t = $a.GetType("System.Runtime.InteropServices.RuntimeInformation")
+    $p = $t.GetProperty("OSArchitecture")
+    # Possible OSArchitecture Values: https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.architecture
+    # Rust supported platforms: https://doc.rust-lang.org/stable/rustc/platform-support.html
+    switch ($p.GetValue($null).ToString())
+    {
+      "X86" { return "i686-pc-windows-msvc" }
+      "X64" { return "x86_64-pc-windows-msvc" }
+      "Arm" { return "thumbv7a-pc-windows-msvc" }
+      "Arm64" { return "aarch64-pc-windows-msvc" }
+    }
+  } catch {
+    # The above was added in .NET 4.7.1, so Windows PowerShell in versions of Windows
+    # prior to Windows 10 v1709 may not have this API.
+    Write-Verbose "Get-TargetTriple: Exception when trying to determine OS architecture."
+    Write-Verbose $_
+  }
+
+  # This is available in .NET 4.0. We already checked for PS 5, which requires .NET 4.5.
+  Write-Verbose("Get-TargetTriple: falling back to Is64BitOperatingSystem.")
+  if ([System.Environment]::Is64BitOperatingSystem) {
+    return "x86_64-pc-windows-msvc"
+  } else {
+    return "i686-pc-windows-msvc"
+  }
+}
+
+function Download($download_url, $platforms) {
+  $arch = Get-TargetTriple
+
+  if (-not $platforms.ContainsKey($arch)) {
+    # X64 is well-supported, including in emulation on ARM64
+    Write-Verbose "$arch is not availablem falling back to X64"
+    $arch = "x86_64-pc-windows-msvc"
+  }
+
+  if (-not $platforms.ContainsKey($arch)) {
+    # should not be possible, as currently we always produce X64 binaries.
+    $platforms_json = ConvertTo-Json $platforms
+    throw "ERROR: could not find binaries for this platform. Last platform tried: $arch platform info: $platforms_json"
+  }
+
+  # Lookup what we expect this platform to look like
+  $info = $platforms[$arch]
+  $zip_ext = $info["zip_ext"]
+  $bin_names = $info["bins"]
+  $artifact_name = $info["artifact_name"]
+
+  # Make a new temp dir to unpack things to
+  $tmp = New-Temp-Dir
+  $dir_path = "$tmp\$app_name$zip_ext"
+
+  # Download and unpack!
+  $url = "$download_url/$artifact_name"
+  Write-Information "Downloading $app_name $app_version ($arch)"
+  Write-Verbose "  from $url"
+  Write-Verbose "  to $dir_path"
+  $wc = New-Object Net.Webclient
+  $wc.downloadFile($url, $dir_path)
+
+  Write-Verbose "Unpacking to $tmp"
+
+  # Select the tool to unpack the files with.
+  #
+  # As of windows 10(?), powershell comes with tar preinstalled, but in practice
+  # it only seems to support .tar.gz, and not xz/zstd. Still, we should try to
+  # forward all tars to it in case the user has a machine that can handle it!
+  switch -Wildcard ($zip_ext) {
+    ".zip" {
+      Expand-Archive -Path $dir_path -DestinationPath "$tmp";
+      Break
+    }
+    ".tar.*" {
+      tar xf $dir_path --strip-components 1 -C "$tmp";
+      Break
+    }
+    Default {
+      throw "ERROR: unknown archive format $zip_ext"
+    }
+  }
+
+  # Let the next step know what to copy
+  $bin_paths = @()
+  foreach ($bin_name in $bin_names) {
+    Write-Verbose "  Unpacked $bin_name"
+    $bin_paths += "$tmp\$bin_name"
+  }
+  return $bin_paths
+}
+
+function Invoke-Installer($bin_paths) {
+
+  # first try CARGO_HOME, then fallback to HOME
+  # (for whatever reason $HOME is not a normal env var and doesn't need the $env: prefix)
+  $dest_dir = if (($base_dir = $env:CARGO_HOME)) {
+    Join-Path $base_dir "bin"
+  } elseif (($base_dir = $HOME)) {
+    Join-Path $base_dir ".cargo\bin"
+  } else {
+    throw "ERROR: could not find your HOME dir or CARGO_HOME to install binaries to"
+  }
+
+  $dest_dir = New-Item -Force -ItemType Directory -Path $dest_dir
+  Write-Information "Installing to $dest_dir"
+  # Just copy the binaries from the temp location to the install dir
+  foreach ($bin_path in $bin_paths) {
+    $installed_file = Split-Path -Path "$bin_path" -Leaf
+    Copy-Item "$bin_path" -Destination "$dest_dir"
+    Remove-Item "$bin_path" -Recurse -Force
+    Write-Information "  $installed_file"
+  }
+
+  Write-Information "Everything's installed!"
+  if (-not $NoModifyPath) {
+    if (Add-Path $dest_dir) {
+        Write-Information ""
+        Write-Information "$dest_dir was added to your PATH, you may need to restart your shell for that to take effect."
+    }
+  }
+}
+
+# Try to add the given path to PATH via the registry
+#
+# Returns true if the registry was modified, otherwise returns false
+# (indicating it was already on PATH)
+function Add-Path($OrigPathToAdd) {
+  $RegistryPath = "HKCU:\Environment"
+  $PropertyName = "Path"
+  $PathToAdd = $OrigPathToAdd
+
+  $Item = if (Test-Path $RegistryPath) {
+    # If the registry key exists, get it
+    Get-Item -Path $RegistryPath
+  } else {
+    # If the registry key doesn't exist, create it
+    Write-Verbose  "Creating $RegistryPath"
+    New-Item -Path $RegistryPath -Force
+  }
+
+  $OldPath = ""
+  try {
+    # Try to get the old PATH value. If that fails, assume we're making it from scratch.
+    # Otherwise assume there's already paths in here and use a ; separator
+    $OldPath = $Item | Get-ItemPropertyValue -Name $PropertyName
+    $PathToAdd = "$PathToAdd;"
+  } catch {
+    # We'll be creating the PATH from scratch
+    Write-Verbose "Adding $PropertyName Property to $RegistryPath"
+  }
+
+  # Check if the path is already there
+  #
+  # We don't want to incorrectly match "C:\blah\" to "C:\blah\blah\", so we include the semicolon
+  # delimiters when searching, ensuring exact matches. To avoid corner cases we add semicolons to
+  # both sides of the input, allowing us to pretend we're always in the middle of a list.
+  if (";$OldPath;" -like "*;$OrigPathToAdd;*") {
+    # Already on path, nothing to do
+    Write-Verbose "install dir already on PATH, all done!"
+    return $false
+  } else {
+    # Actually update PATH
+    Write-Verbose "Adding $OrigPathToAdd to your PATH"
+    $NewPath = $PathToAdd + $OldPath
+    # We use -Force here to make the value already existing not be an error
+    $Item | New-ItemProperty -Name $PropertyName -Value $NewPath -PropertyType String -Force | Out-Null
+    return $true
+  }
+}
+
+function Initialize-Environment() {
+  If (($PSVersionTable.PSVersion.Major) -lt 5) {
+    Write-Error "PowerShell 5 or later is required to install $app_name."
+    Write-Error "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/setup/installing-windows-powershell"
+    break
+  }
+
+  # show notification to change execution policy:
+  $allowedExecutionPolicy = @('Unrestricted', 'RemoteSigned', 'ByPass')
+  If ((Get-ExecutionPolicy).ToString() -notin $allowedExecutionPolicy) {
+    Write-Error "PowerShell requires an execution policy in [$($allowedExecutionPolicy -join ", ")] to run $app_name."
+    Write-Error "For example, to set the execution policy to 'RemoteSigned' please run :"
+    Write-Error "'Set-ExecutionPolicy RemoteSigned -scope CurrentUser'"
+    break
+  }
+
+  # GitHub requires TLS 1.2
+  If ([System.Enum]::GetNames([System.Net.SecurityProtocolType]) -notcontains 'Tls12') {
+    Write-Error "Installing $app_name requires at least .NET Framework 4.5"
+    Write-Error "Please download and install it first:"
+    Write-Error "https://www.microsoft.com/net/download"
+    break
+  }
+}
+
+function New-Temp-Dir() {
+  [CmdletBinding(SupportsShouldProcess)]
+  param()
+  $parent = [System.IO.Path]::GetTempPath()
+  [string] $name = [System.Guid]::NewGuid()
+  New-Item -ItemType Directory -Path (Join-Path $parent $name)
+}
+
+# PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
+$Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
+Install-Binary "$Args"
+
+
+================ npm-package.tar.gz/package/.gitignore ================
+/node_modules
+
+
+
+================ npm-package.tar.gz/package/CHANGELOG.md ================
+# Version 0.1.0
+
+```text
+         +------------------------+
+         | the initial release!!! |
+         +------------------------+
+        /
+≽(◕ ᴗ ◕)≼
+```
+
+
+================ npm-package.tar.gz/package/LICENSE-APACHE ================
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright 2023 Axo Developer Co.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+
+================ npm-package.tar.gz/package/LICENSE-MIT ================
+Copyright (c) 2023 Axo Developer Co.
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+
+
+================ npm-package.tar.gz/package/README.md ================
+# axolotlsay
+> 💬 a CLI for learning to distribute CLIs in rust
+
+
+## Usage
+
+```sh
+> axolotlsay "hello world"
+
+         +-------------+
+         | hello world |
+         +-------------+
+        /
+≽(◕ ᴗ ◕)≼
+```
+
+## License
+
+Licensed under either of
+
+* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0))
+* MIT license ([LICENSE-MIT](LICENSE-MIT) or [opensource.org/licenses/MIT](https://opensource.org/licenses/MIT))
+
+at your option.
+
+
+
+================ npm-package.tar.gz/package/binary.js ================
+const { Binary } = require("binary-install");
+const os = require("os");
+const cTable = require("console.table");
+const libc = require("detect-libc");
+const { configureProxy } = require("axios-proxy-builder");
+
+const error = (msg) => {
+  console.error(msg);
+  process.exit(1);
+};
+
+const { version } = require("./package.json");
+// These are injected by cargo-dist
+const name = "axolotlsay";
+const artifact_download_url = "https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0";
+
+// These are injected by cargo-dist
+const supportedPlatforms = {
+  "aarch64-apple-darwin": {
+    "artifact_name": "axolotlsay-aarch64-apple-darwin.tar.gz",
+    "bins": ["axolotlsay"],
+    "zip_ext": ".tar.gz"
+  },
+  "x86_64-apple-darwin": {
+    "artifact_name": "axolotlsay-x86_64-apple-darwin.tar.gz",
+    "bins": ["axolotlsay"],
+    "zip_ext": ".tar.gz"
+  },
+  "x86_64-pc-windows-msvc": {
+    "artifact_name": "axolotlsay-x86_64-pc-windows-msvc.tar.gz",
+    "bins": ["axolotlsay.exe"],
+    "zip_ext": ".tar.gz"
+  },
+  "x86_64-unknown-linux-gnu": {
+    "artifact_name": "axolotlsay-x86_64-unknown-linux-gnu.tar.gz",
+    "bins": ["axolotlsay"],
+    "zip_ext": ".tar.gz"
+  }
+};
+
+const getPlatform = () => {
+  const raw_os_type = os.type();
+  const raw_architecture = os.arch();
+
+  // We want to use rust-style target triples as the canonical key
+  // for a platform, so translate the "os" library's concepts into rust ones
+  let os_type = "";
+  switch (raw_os_type) {
+    case "Windows_NT":
+      os_type = "pc-windows-msvc";
+      break;
+    case "Darwin":
+      os_type = "apple-darwin";
+      break;
+    case "Linux":
+      os_type = "unknown-linux-gnu"
+      break;
+  }
+
+  let arch = "";
+  switch (raw_architecture) {
+    case "x64":
+      arch = "x86_64";
+      break;
+    case "arm64":
+      arch = "aarch64";
+      break;
+  }
+
+  // Assume the above succeeded and build a target triple to look things up with.
+  // If any of it failed, this lookup will fail and we'll handle it like normal.
+  let target_triple = `${arch}-${os_type}`;
+  let platform = supportedPlatforms[target_triple];
+
+  if (!platform) {
+    error(
+      `Platform with type "${raw_os_type}" and architecture "${raw_architecture}" is not supported by ${name}.\nYour system must be one of the following:\n\n${Object.keys(supportedPlatforms).join(",")}`
+    );
+  }
+
+  // These are both situation where you might toggle to unknown-linux-musl but we don't support that yet
+  if (raw_os_type === "Linux") {
+    if (libc.isNonGlibcLinuxSync()) {
+      error("This operating system does not support dynamic linking to glibc.");
+    } else {
+      let libc_version = libc.versionSync();
+      let split_libc_version = libc_version.split(".");
+      let libc_major_version = split_libc_version[0];
+      let libc_minor_version = split_libc_version[1];
+      let min_major_version = 2;
+      let min_minor_version = 17;
+      if (
+        libc_major_version < min_major_version ||
+        libc_minor_version < min_minor_version
+      ) {
+        error(
+          `This operating system needs glibc >= ${min_major_version}.${min_minor_version}, but only has ${libc_version} installed.`
+        );
+      }
+    }
+  }
+
+  return platform;
+};
+
+const getBinary = () => {
+  const platform = getPlatform();
+  const url = `${artifact_download_url}/${platform.artifact_name}`;
+
+  if (platform.bins.length > 1) {
+    // Not yet supported
+    error("this app has multiple binaries, which isn't yet implemented");
+  }
+  let binary = new Binary(platform.bins[0], url);
+
+  return binary;
+};
+
+const install = (suppressLogs) => {
+  const binary = getBinary();
+  const proxy = configureProxy(binary.url);
+
+  return binary.install(proxy, suppressLogs);
+};
+
+const run = () => {
+  const binary = getBinary();
+  binary.run();
+};
+
+module.exports = {
+  install,
+  run,
+  getBinary,
+};
+
+
+
+================ npm-package.tar.gz/package/install.js ================
+#!/usr/bin/env node
+
+const { install } = require("./binary");
+install(false);
+
+
+
+================ npm-package.tar.gz/package/npm-shrinkwrap.json ================
+{
+  "name": "axolotlsay",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "axolotlsay",
+      "version": "0.1.0",
+      "license": "MIT OR Apache-2.0",
+      "hasInstallScript": true,
+      "dependencies": {
+        "axios-proxy-builder": "^0.1.1",
+        "binary-install": "^1.0.6",
+        "console.table": "^0.10.0",
+        "detect-libc": "^2.0.0"
+      },
+      "bin": {
+        "rover": "run.js"
+      },
+      "devDependencies": {
+        "prettier": "2.8.4"
+      },
+      "engines": {
+        "node": ">=14",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/axios": {
+      "version": "0.26.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+      "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+      "dependencies": {
+        "follow-redirects": "^1.14.8"
+      }
+    },
+    "node_modules/axios-proxy-builder": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/axios-proxy-builder/-/axios-proxy-builder-0.1.2.tgz",
+      "integrity": "sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==",
+      "dependencies": {
+        "tunnel": "^0.0.6"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/binary-install": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.0.6.tgz",
+      "integrity": "sha512-h3K4jaC4jEauK3csXI9GxGBJldkpuJlHCIBv8i+XBNhPuxnlERnD1PWVczQYDqvhJfv0IHUbB3lhDrZUMHvSgw==",
+      "dependencies": {
+        "axios": "^0.26.1",
+        "rimraf": "^3.0.2",
+        "tar": "^6.1.11"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+      "optional": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "node_modules/console.table": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz",
+      "integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==",
+      "dependencies": {
+        "easy-table": "1.1.0"
+      },
+      "engines": {
+        "node": "> 0.10"
+      }
+    },
+    "node_modules/defaults": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+      "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+      "optional": true,
+      "dependencies": {
+        "clone": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
+      "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/easy-table": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz",
+      "integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==",
+      "optionalDependencies": {
+        "wcwidth": ">=1.0.1"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.4.tgz",
+      "integrity": "sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz",
+      "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/tar": {
+      "version": "6.1.13",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
+      "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==",
+      "dependencies": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^4.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/tunnel": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+      "engines": {
+        "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+      }
+    },
+    "node_modules/wcwidth": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+      "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+      "optional": true,
+      "dependencies": {
+        "defaults": "^1.0.3"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    }
+  }
+}
+
+
+
+================ npm-package.tar.gz/package/package.json ================
+{
+  "name": "axolotlsay",
+  "version": "0.1.0",
+  "description": "💬 a CLI for learning to distribute CLIs in rust",
+  "repository": "https://github.com/axodotdev/axolotlsay",
+  
+  
+  "license": "MIT OR Apache-2.0",
+  
+  "bin": {
+    "axolotlsay": "run.js"
+  },
+  "scripts": {
+    "postinstall": "node ./install.js",
+    "fmt": "prettier --write **/*.js",
+    "fmt:check": "prettier --check **/*.js"
+  },
+  "engines": {
+    "node": ">=14",
+    "npm": ">=6"
+  },
+  "volta": {
+    "node": "18.14.1",
+    "npm": "9.5.0"
+  },
+  "dependencies": {
+    "axios-proxy-builder": "^0.1.1",
+    "binary-install": "^1.0.6",
+    "console.table": "^0.10.0",
+    "detect-libc": "^2.0.0"
+  },
+  "devDependencies": {
+    "prettier": "2.8.4"
+  }
+}
+
+
+
+================ npm-package.tar.gz/package/run.js ================
+#!/usr/bin/env node
+
+const { run, install: maybeInstall } = require("./binary");
+maybeInstall(true).then(run);
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__basic-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_cargo_home-installers.snap
similarity index 98%
rename from cargo-dist/tests/snapshots/integration_tests__basic-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_cargo_home-installers.snap
index 138cfa2d..2b3a19d0 100644
--- a/cargo-dist/tests/snapshots/integration_tests__basic-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_cargo_home-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -758,9 +759,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -984,3 +985,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_env_no_subdir-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_no_subdir-installers.snap
similarity index 98%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_env_no_subdir-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_no_subdir-installers.snap
index d739bd18..81cb0e5b 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_env_no_subdir-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_no_subdir-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -244,8 +245,8 @@ install() {
     # Install to this subdir of the user's MY_ENV_VAR dir.
     # In this case we want to be early-bound, as the env-var can't be trusted longterm.
     if [ -n "${MY_ENV_VAR:-}" ]; then
-        _install_dir="$MY_ENV_VAR/"
-        _env_script_path="$MY_ENV_VAR//env"
+        _install_dir="$MY_ENV_VAR"
+        _env_script_path="$MY_ENV_VAR/env"
         _install_dir_expr="$_install_dir"
         _env_script_path_expr="$_env_script_path"
     else
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir-installers.snap
similarity index 98%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir-installers.snap
index 0fbaf1a9..92878920 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -37,7 +39,7 @@ The installer for axolotlsay 0.1.0
 
 This script detects what platform you're on and fetches an appropriate archive from
 https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to \$MY_ENV_VAR/bin/
+then unpacks the binaries and installs them to \$MY_ENV_VAR/bin
 
 It will then add that dir to PATH by adding the appropriate line to \$HOME/.profile
 
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -244,8 +245,8 @@ install() {
     # Install to this subdir of the user's MY_ENV_VAR dir.
     # In this case we want to be early-bound, as the env-var can't be trusted longterm.
     if [ -n "${MY_ENV_VAR:-}" ]; then
-        _install_dir="$MY_ENV_VAR/bin/"
-        _env_script_path="$MY_ENV_VAR/bin//env"
+        _install_dir="$MY_ENV_VAR/bin"
+        _env_script_path="$MY_ENV_VAR/bin/env"
         _install_dir_expr="$_install_dir"
         _env_script_path_expr="$_env_script_path"
     else
@@ -701,7 +702,7 @@ The installer for axolotlsay 0.1.0
 
 This script detects what platform you're on and fetches an appropriate archive from
 https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to $env:MY_ENV_VAR\bin/
+then unpacks the binaries and installs them to $env:MY_ENV_VAR\bin
 
 It will then add that dir to PATH by editing your Environment.Path registry key
 
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -856,7 +857,7 @@ function Invoke-Installer($bin_paths) {
 
   # Install to this subdir of the user's MY_ENV_VAR dir
   $dest_dir = if (($base_dir = $env:MY_ENV_VAR)) {
-    Join-Path $base_dir "bin/"
+    Join-Path $base_dir "bin"
   } else {
     throw "ERROR: could not find your MY_ENV_VAR dir to install binaries to"
   }
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir_space-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir_space-installers.snap
similarity index 98%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir_space-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir_space-installers.snap
index edae186d..36f22118 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir_space-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir_space-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir_space_deeper-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir_space_deeper-installers.snap
similarity index 98%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir_space_deeper-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir_space_deeper-installers.snap
index ce0efd27..75f5e232 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_env_subdir_space_deeper-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_env_subdir_space_deeper-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_deeper-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_deeper-installers.snap
similarity index 98%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_deeper-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_deeper-installers.snap
index 0f7b7122..a8a14d73 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_deeper-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_deeper-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_min-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_min-installers.snap
similarity index 97%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_min-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_min-installers.snap
index 37382306..6d1f5fe8 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_min-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_min-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -37,7 +39,7 @@ The installer for axolotlsay 0.1.0
 
 This script detects what platform you're on and fetches an appropriate archive from
 https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to \$HOME/.axolotlsay/
+then unpacks the binaries and installs them to \$HOME/.axolotlsay
 
 It will then add that dir to PATH by adding the appropriate line to \$HOME/.profile
 
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -244,10 +245,10 @@ install() {
     # Install to this subdir of the user's home dir
     # In this case we want to be late-bound, as $HOME is reliable/nice.
     if [ -n "${HOME:-}" ]; then
-        _install_dir="$HOME/.axolotlsay/"
-        _env_script_path="$HOME/.axolotlsay//env"
-        _install_dir_expr='$HOME/.axolotlsay/'
-        _env_script_path_expr='$HOME/.axolotlsay//env'
+        _install_dir="$HOME/.axolotlsay"
+        _env_script_path="$HOME/.axolotlsay/env"
+        _install_dir_expr='$HOME/.axolotlsay'
+        _env_script_path_expr='$HOME/.axolotlsay/env'
     else
         err "could not find your HOME dir to install binaries to"
     fi
@@ -701,7 +702,7 @@ The installer for axolotlsay 0.1.0
 
 This script detects what platform you're on and fetches an appropriate archive from
 https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to $HOME\.axolotlsay/
+then unpacks the binaries and installs them to $HOME\.axolotlsay
 
 It will then add that dir to PATH by editing your Environment.Path registry key
 
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -856,7 +857,7 @@ function Invoke-Installer($bin_paths) {
 
   # Install to this subdir of the user's home dir
   $dest_dir = if (($base_dir = $HOME)) {
-    Join-Path $base_dir ".axolotlsay/"
+    Join-Path $base_dir ".axolotlsay"
   } else {
     throw "ERROR: could not find your HOME dir to install binaries to"
   }
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_space-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_space-installers.snap
similarity index 98%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_space-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_space-installers.snap
index ad387390..cd360a59 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_space-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_space-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_space_deeper-installers.snap b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_space_deeper-installers.snap
similarity index 97%
rename from cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_space_deeper-installers.snap
rename to cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_space_deeper-installers.snap
index 9394f95b..d16e461c 100644
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_home_subdir_space_deeper-installers.snap
+++ b/cargo-dist/tests/gallery/snapshots/integration_tests__gallery__dist__install_path_home_subdir_space_deeper-installers.snap
@@ -1,7 +1,9 @@
 ---
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
+source: cargo-dist/tests/gallery/dist.rs
+expression: "&snapshots"
 ---
+
+
 ================ installer.sh ================
 #!/bin/sh
 # shellcheck shell=dash
@@ -37,7 +39,7 @@ The installer for axolotlsay 0.1.0
 
 This script detects what platform you're on and fetches an appropriate archive from
 https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to \$HOME/My Axolotlsay Documents/bin/
+then unpacks the binaries and installs them to \$HOME/My Axolotlsay Documents/bin
 
 It will then add that dir to PATH by adding the appropriate line to \$HOME/.profile
 
@@ -128,18 +130,18 @@ download_binary_and_run_installer() {
     # Lookup what to download/unpack based on platform
     case "$_arch" in 
         "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
+            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.gz"
+            _zip_ext=".tar.gz"
             _bins="axolotlsay"
             ;;
         *)
@@ -168,7 +170,6 @@ download_binary_and_run_installer() {
       say "this may be a standard network error, but it may also indicate"
       say "that $APP_NAME's release process is not working. When in doubt"
       say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
       exit 1
     fi
 
@@ -244,10 +245,10 @@ install() {
     # Install to this subdir of the user's home dir
     # In this case we want to be late-bound, as $HOME is reliable/nice.
     if [ -n "${HOME:-}" ]; then
-        _install_dir="$HOME/My Axolotlsay Documents/bin/"
-        _env_script_path="$HOME/My Axolotlsay Documents/bin//env"
-        _install_dir_expr='$HOME/My Axolotlsay Documents/bin/'
-        _env_script_path_expr='$HOME/My Axolotlsay Documents/bin//env'
+        _install_dir="$HOME/My Axolotlsay Documents/bin"
+        _env_script_path="$HOME/My Axolotlsay Documents/bin/env"
+        _install_dir_expr='$HOME/My Axolotlsay Documents/bin'
+        _env_script_path_expr='$HOME/My Axolotlsay Documents/bin/env'
     else
         err "could not find your HOME dir to install binaries to"
     fi
@@ -701,7 +702,7 @@ The installer for axolotlsay 0.1.0
 
 This script detects what platform you're on and fetches an appropriate archive from
 https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to $HOME\My Axolotlsay Documents/bin/
+then unpacks the binaries and installs them to $HOME\My Axolotlsay Documents/bin
 
 It will then add that dir to PATH by editing your Environment.Path registry key
 
@@ -741,9 +742,9 @@ function Install-Binary($install_args) {
   # Platform info injected by cargo-dist
   $platforms = @{
     "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
+      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.tar.gz"
       "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
+      "zip_ext" = ".tar.gz"
     }
   }
 
@@ -856,7 +857,7 @@ function Invoke-Installer($bin_paths) {
 
   # Install to this subdir of the user's home dir
   $dest_dir = if (($base_dir = $HOME)) {
-    Join-Path $base_dir "My Axolotlsay Documents/bin/"
+    Join-Path $base_dir "My Axolotlsay Documents/bin"
   } else {
     throw "ERROR: could not find your HOME dir to install binaries to"
   }
@@ -964,3 +965,4 @@ function New-Temp-Dir() {
 # PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
 $Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
 Install-Binary "$Args"
+
diff --git a/cargo-dist/tests/integration-tests.rs b/cargo-dist/tests/integration-tests.rs
index 528ae3cf..436f7f9f 100644
--- a/cargo-dist/tests/integration-tests.rs
+++ b/cargo-dist/tests/integration-tests.rs
@@ -1,186 +1,297 @@
-use camino::Utf8PathBuf;
-use miette::miette;
-use miette::Context;
-use miette::IntoDiagnostic;
-use std::process::Command;
-use std::sync::Mutex;
+//! Serious Integration Tests!
+//!
+//! These tests:
+//!
+//! * Fetch a specific commit of axolotlsay
+//! * Overlay a new value for [workspace.metadata.dist]
+//! * Run `cargo-dist dist build -aglobal` on it (building installers)
+//!     * set `OVERRIDE_CARGO_BIN_EXE_cargo-dist=path/to/some/cargo-dist` to not use the current checkout
+//! * Run shellcheck on installer.sh (only if detected on the system)
+//! * Run PSScriptAnalyzer on installer.ps1 (only if detected on the system)
+//! * Run installer.sh and check the results
+//!    * linux/macos only, must also set RUIN_MY_COMPUTER_WITH_INSTALLERS to opt in
+//!    * HOME, CARGO_HOME, and MY_ENV_VAR overriden to keep it scoped to a temp dir
+//!        * CARGO_HOME currently always deleted, should probably have a test where we set it
+//! * insta.rs snapshot the installers
+//!
+//! Also note that the "run installer.sh" step forces us to be coherent with the actual published
+//! releases. (i.e. axolotlsay 0.1.0 has .tar.gz archives, so we need to always set that!)
+//! In the future we may unblock that (and deepen the coverage of our integration-testing)
+//! by actually running `cargo dist build -alocal` and hosting the binaries on a local
+//! static file server.
+//!
+//! In the future we may also further generalize this into a "gallery" of test projects
+//! with support for testing other tools like oranda.
+
+mod gallery;
+use gallery::*;
+use insta::assert_snapshot;
 
 #[test]
 fn basic() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
-installers = ["shell", "powershell"]
+installers = ["shell", "powershell", "npm"]
 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"
+scope = "@axodotdev"
+
+"#))?;
+
+        results.check_all(ctx, ".cargo/bin/")?;
 
-"#))
+        Ok(())
+    })
+}
+
+#[test]
+fn basic_generate_github_ci() -> Result<(), miette::Report> {
+    let test_name = _function_name!();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
+
+        let results = ctx.cargo_dist_generate_ci(test_name, format!(r#"
+[workspace.metadata.dist]
+cargo-dist-version = "{dist_version}"   
+installers = ["shell", "powershell", "npm"]
+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"
+scope = "@axodotdev"
+
+"#))?;
+
+        insta::with_settings!({filters => vec![
+            (r"\d+\.\d+\.\d+(\-prerelease\d*)?(\.\d+)?", "1.0.0-FAKEVERSION"),
+        ]}, {
+            assert_snapshot!(format!("{test_name}-github-ci"), results);
+        });
+
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_cargo_home() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "CARGO_HOME"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
 
-"#))
+results.check_all(ctx, ".cargo/bin/")?;
+
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_home_subdir_min() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "~/.axolotlsay/"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
 
-"#))
+results.check_all(ctx, ".axolotlsay/")?;
+
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_home_subdir_deeper() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "~/.axolotlsay/bins"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
 
-"#))
+"#))?;
+
+results.check_all(ctx, ".axolotlsay/bins")?;
+
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_home_subdir_space() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "~/My Axolotlsay Documents"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
+
+results.check_all(ctx, "My Axolotlsay Documents/")?;
 
-"#))
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_home_subdir_space_deeper() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "~/My Axolotlsay Documents/bin/"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
+
+results.check_all(ctx, "My Axolotlsay Documents/bin/")?;
 
-"#))
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_env_no_subdir() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "$MY_ENV_VAR/"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
 
-"#))
+results.check_all(ctx, ".axolotlsay/")?;
+
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_env_subdir() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "$MY_ENV_VAR/bin/"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
+
+results.check_all(ctx, ".axolotlsay/bin/")?;
 
-"#))
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_env_subdir_space() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "$MY_ENV_VAR/My Axolotlsay Documents"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
+
+results.check_all(ctx, ".axolotlsay/My Axolotlsay Documents/")?;
 
-"#))
+        Ok(())
     })
 }
 
 #[test]
 fn install_path_env_subdir_space_deeper() -> Result<(), miette::Report> {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        let results = ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "$MY_ENV_VAR/My Axolotlsay Documents/bin"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#))?;
 
-"#))
+results.check_all(ctx, ".axolotlsay/My Axolotlsay Documents/bin/")?;
+
+        Ok(())
     })
 }
 
@@ -188,18 +299,22 @@ install-path = "$MY_ENV_VAR/My Axolotlsay Documents/bin"
 #[should_panic(expected = r#"install-path = "~/" is missing a subdirectory"#)]
 fn install_path_invalid() {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "~/"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
+
+"#)).unwrap();
 
-"#))
+        Ok(())
     }).unwrap();
 }
 
@@ -207,336 +322,21 @@ install-path = "~/"
 #[should_panic(expected = r#"install-path = "$MY_ENV" is missing a subdirectory"#)]
 fn env_path_invalid() {
     let test_name = _function_name!();
-    run_test(|ctx| {
-        let dist_version = ctx.tools.cargo_dist.version.as_ref().unwrap();
+    AXOLOTLSAY.run_test(|ctx| {
+        let dist_version = ctx.tools.cargo_dist.version().unwrap();
 
-        ctx.run(test_name, format!(r#"
+        ctx.cargo_dist_build_global(test_name, format!(r#"
 [workspace.metadata.dist]
 cargo-dist-version = "{dist_version}"   
 installers = ["shell", "powershell"]
 targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 ci = ["github"]
 install-path = "$MY_ENV"
+unix-archive = ".tar.gz"
+windows-archive = ".tar.gz"
 
-"#))
-    }).unwrap();
-}
-
-const STATIC_CARGO_DIST_BIN: &str = env!("CARGO_BIN_EXE_cargo-dist");
-const TARGET_DIR: &str = env!("CARGO_TARGET_TMPDIR");
-static TEST_CONTEXT: Mutex<Option<TestContext>> = Mutex::new(None);
+"#)).unwrap();
 
-struct TestContext {
-    tools: Tools,
-    app_name: String,
-}
-
-fn run_test(
-    f: impl FnOnce(&TestContext) -> Result<(), miette::Report>,
-) -> Result<(), miette::Report> {
-    let maybe_guard = TEST_CONTEXT.lock();
-    // It's fine for the mutex to be poisoned once the value is Some because none of the tests
-    // are allowed to mutate the TestContext. But if it's poisoned while None that means we
-    // encountered an error while setting up TestContext and should just abort everything
-    // instead of retrying over and over.
-    let mut guard = match maybe_guard {
-        Ok(guard) => guard,
-        Err(poison) => {
-            let guard = poison.into_inner();
-            if guard.is_none() {
-                panic!("aborting all tests: failed fetch");
-            }
-            guard
-        }
-    };
-    if guard.is_none() {
-        // Intentionally unwrapping here to poison the mutex if we can't fetch
-        let ctx = init_context().unwrap();
-        *guard = Some(ctx);
-    }
-
-    let ctx = guard.as_ref().unwrap();
-
-    f(ctx)
-}
-
-fn init_context() -> Result<TestContext, miette::Report> {
-    let repo_owner = "axodotdev";
-    let repo_name = "axolotlsay";
-    let repo_url = format!("https://github.com/{repo_owner}/{repo_name}");
-    let commit_sha = "6b8337fb742908e506296eab3371bb71b76283d7";
-    let app_name = repo_name;
-
-    // Get the tools we'll invoke
-    let tools = Tools::new();
-
-    // Clone the repo we're interested in and cd into it
-    fetch_repo(&tools.git, repo_name, &repo_url, commit_sha)?;
-
-    // Run tests
-    let ctx = TestContext {
-        tools,
-        app_name: app_name.to_owned(),
-    };
-    Ok(ctx)
-}
-
-impl TestContext {
-    fn run(&self, name: &str, new_toml: String) -> Result<(), miette::Report> {
-        eprintln!("\n=============== running test: {name} =================");
-        // patch the Cargo.toml
-        self.patch_cargo_toml(new_toml)?;
-
-        // build installers
-        eprintln!("running cargo dist build...");
-        self.tools
-            .cargo_dist
-            .run(|cmd| cmd.arg("dist").arg("build").arg("-aglobal"))?;
-
-        // read/analyze installers
-        eprintln!("loading results...");
-        let app_name = &self.app_name;
-        let powershell_path = format!("target/distrib/{app_name}-installer.ps1");
-        let shell_path = format!("target/distrib/{app_name}-installer.sh");
-
-        // If we have shellcheck, check our shell script
-        if let Some(shellcheck) = &self.tools.shellcheck {
-            eprintln!("shellchecking {shell_path}");
-            let output = shellcheck.output(|cmd| cmd.arg(&shell_path))?;
-
-            if !output.status.success() {
-                eprintln!("{}", String::from_utf8_lossy(&output.stdout));
-                return Err(miette!("shellcheck found issues"));
-            }
-        }
-
-        // If we have PsScriptAnalyzer, check our powershell script
-        if let Some(psanalyzer) = &self.tools.psanalyzer {
-            eprintln!("PsScriptAnalyzing {powershell_path}");
-            let output = psanalyzer.output(|cmd| cmd.arg(&powershell_path).arg("-EnableExit"))?;
-
-            if !output.status.success() {
-                eprintln!("{}", String::from_utf8_lossy(&output.stdout));
-                eprintln!("see https://learn.microsoft.com/en-ca/powershell/utility-modules/psscriptanalyzer/rules/readme\n");
-                return Err(miette!("PsScriptAnalyzer found issues"));
-            }
-        }
-
-        // Now that all other checks have passed, it's safe to check snapshots
-        //
-        // We make a single uber-snapshot for both scripts to avoid the annoyances of having multiple snapshots
-        // in one test (necessitating rerunning it multiple times or passing special flags to get all the changes)
-        let powershell_src = axoasset::SourceFile::load_local(&powershell_path)?;
-        let shell_src = axoasset::SourceFile::load_local(&shell_path)?;
-        let mut shell_snapshots = String::new();
-        shell_snapshots.push_str("================ installer.sh ================\n");
-        shell_snapshots.push_str(shell_src.contents());
-        shell_snapshots.push_str("\n\n\n================ installer.ps1 ================\n");
-        shell_snapshots.push_str(powershell_src.contents());
-
-        insta::assert_snapshot!(format!("{name}-installers"), &shell_snapshots);
-
-        eprintln!("ok!");
         Ok(())
-    }
-
-    fn patch_cargo_toml(&self, new_toml: String) -> Result<(), miette::Report> {
-        eprintln!("loading Cargo.toml...");
-        let toml_src = axoasset::SourceFile::load_local("Cargo.toml")?;
-        let mut toml = toml_src.deserialize_toml_edit()?;
-        eprintln!("editing Cargo.toml...");
-        let new_table_src = axoasset::SourceFile::new("new-Cargo.toml", new_toml);
-        let new_table = new_table_src.deserialize_toml_edit()?;
-
-        // Written slightly verbosely to make it easier to isolate which failed
-        eprintln!("{new_table}");
-        let old = &mut toml["workspace"]["metadata"]["dist"];
-        let new = &new_table["workspace"]["metadata"]["dist"];
-        *old = new.clone();
-        let toml_out = toml.to_string();
-        eprintln!("writing Cargo.toml...");
-        axoasset::LocalAsset::write_new(&toml_out, "Cargo.toml")?;
-
-        Ok(())
-    }
-}
-
-struct Tools {
-    git: CommandInfo,
-    cargo_dist: CommandInfo,
-    shellcheck: Option<CommandInfo>,
-    psanalyzer: Option<CommandInfo>,
-}
-
-impl Tools {
-    fn new() -> Self {
-        eprintln!("getting tools...");
-        let git = CommandInfo::new("git", None).expect("git isn't installed");
-
-        // If OVERRIDE_* is set, prefer that over the version that cargo built for us,
-        // this lets us test our shippable builds.
-        let cargo_dist_path = std::env::var("OVERRIDE_CARGO_BIN_EXE_cargo-dist")
-            .unwrap_or_else(|_| STATIC_CARGO_DIST_BIN.to_owned());
-        let cargo_dist = CommandInfo::new("cargo-dist", Some(&cargo_dist_path))
-            .expect("cargo-dist isn't built!?");
-        cargo_dist
-            .version
-            .as_ref()
-            .expect("couldn't parse cargo-dist version!?");
-        let shellcheck = CommandInfo::new("shellcheck", None);
-        let psanalyzer = CommandInfo::new_powershell_command("Invoke-ScriptAnalyzer");
-
-        Self {
-            git,
-            cargo_dist,
-            shellcheck,
-            psanalyzer,
-        }
-    }
-}
-
-struct CommandInfo {
-    name: String,
-    cmd: String,
-    args: Vec<String>,
-    version: Option<String>,
-}
-
-impl CommandInfo {
-    fn new(name: &str, path: Option<&str>) -> Option<Self> {
-        let cmd = path.unwrap_or(name).to_owned();
-        let output = Command::new(&cmd).arg("--version").output().ok()?;
-
-        Some(CommandInfo {
-            name: name.to_owned(),
-            cmd,
-            args: vec![],
-            version: parse_version(output),
-        })
-    }
-
-    fn new_powershell_command(name: &str) -> Option<Self> {
-        let output = Command::new("powershell")
-            .arg("-Command")
-            .arg("Get-Command")
-            .arg(name)
-            .output()
-            .ok()?;
-        if !output.status.success() {
-            return None;
-        }
-        Some(CommandInfo {
-            name: name.to_owned(),
-            cmd: "powershell".to_owned(),
-            args: vec!["-Command".to_owned(), name.to_owned()],
-            version: parse_version(output),
-        })
-    }
-
-    fn run(
-        &self,
-        builder: impl FnOnce(&mut Command) -> &mut Command,
-    ) -> Result<(), miette::Report> {
-        let mut command = Command::new(&self.cmd);
-        command.args(&self.args);
-        builder(&mut command);
-        let output = command
-            .output()
-            .into_diagnostic()
-            .wrap_err_with(|| format!("failed to run \"{}\"", pretty_cmd(&self.name, &command)))?;
-        if output.status.success() {
-            Ok(())
-        } else {
-            let mut out = String::new();
-            let stderr = String::from_utf8_lossy(&output.stderr);
-            let stdout = String::from_utf8_lossy(&output.stdout);
-            out.push_str("\nstdout:\n");
-            out.push_str(&stdout);
-            out.push_str("\nstderr:\n");
-            out.push_str(&stderr);
-            Err(miette!("{out}")).wrap_err_with(|| {
-                format!(
-                    "\"{}\" failed ({})",
-                    pretty_cmd(&self.name, &command),
-                    output.status
-                )
-            })
-        }
-    }
-
-    fn output(
-        &self,
-        builder: impl FnOnce(&mut Command) -> &mut Command,
-    ) -> Result<std::process::Output, miette::Report> {
-        let mut command = Command::new(&self.cmd);
-        command.args(&self.args);
-        builder(&mut command);
-        let output = command
-            .output()
-            .into_diagnostic()
-            .wrap_err_with(|| format!("failed to run \"{}\"", pretty_cmd(&self.name, &command)))?;
-        Ok(output)
-    }
-}
-
-fn parse_version(output: std::process::Output) -> Option<String> {
-    let version_bytes = output.stdout;
-    let version_full = String::from_utf8(version_bytes).ok()?;
-    let version_line = version_full.lines().next()?;
-    let version_suffix = version_line.split_once(' ')?.1.trim().to_owned();
-    Some(version_suffix)
-}
-
-fn fetch_repo(
-    git: &CommandInfo,
-    repo_name: &str,
-    repo_url: &str,
-    commit_sha: &str,
-) -> Result<(), miette::Report> {
-    std::env::set_current_dir(TARGET_DIR).into_diagnostic()?;
-    if Utf8PathBuf::from(repo_name).exists() {
-        eprintln!("repo already cloned, updating it...");
-        std::env::set_current_dir(repo_name).into_diagnostic()?;
-        git.run(|c| c.arg("remote").arg("set-url").arg("origin").arg(repo_url))?;
-        git.run(|c| c.arg("fetch").arg("origin").arg(commit_sha))?;
-        git.run(|c| c.arg("reset").arg("--hard").arg("FETCH_HEAD"))?;
-    } else {
-        eprintln!("fetching {repo_url}");
-        axoasset::LocalAsset::create_dir(repo_name)?;
-        std::env::set_current_dir(repo_name).into_diagnostic()?;
-        git.run(|c| c.arg("init"))?;
-        git.run(|c| c.arg("remote").arg("add").arg("origin").arg(repo_url))?;
-        git.run(|c| c.arg("fetch").arg("origin").arg(commit_sha))?;
-        git.run(|c| c.arg("reset").arg("--hard").arg("FETCH_HEAD"))?;
-    }
-
-    Ok(())
-}
-
-fn pretty_cmd(name: &str, cmd: &Command) -> String {
-    let mut out = String::new();
-    out.push_str(name);
-    for arg in cmd.get_args() {
-        out.push(' ');
-        out.push_str(&arg.to_string_lossy())
-    }
-    out
-}
-
-// Taken from cargo-insta to avoid copy-paste errors
-#[macro_export]
-macro_rules! _function_name {
-    () => {{
-        fn f() {}
-        fn type_name_of_val<T>(_: T) -> &'static str {
-            std::any::type_name::<T>()
-        }
-        let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
-        while let Some(rest) = name.strip_suffix("::{{closure}}") {
-            name = rest;
-        }
-        name.split_once("::")
-            .map(|(_module, func)| func)
-            .unwrap_or(name)
-    }};
+    }).unwrap();
 }
diff --git a/cargo-dist/tests/snapshots/integration_tests__basic_generate_github_ci-github-ci.snap b/cargo-dist/tests/snapshots/integration_tests__basic_generate_github_ci-github-ci.snap
new file mode 100644
index 00000000..ac65a7f7
--- /dev/null
+++ b/cargo-dist/tests/snapshots/integration_tests__basic_generate_github_ci-github-ci.snap
@@ -0,0 +1,151 @@
+---
+source: cargo-dist/tests/integration-tests.rs
+expression: results
+---
+# CI that:
+#
+# * checks for a Git Tag that looks like a release
+# * creates a Github Release™ and fills in its text
+# * builds artifacts with cargo-dist (executable-zips, installers)
+# * uploads those artifacts to the Github Release™
+#
+# Note that the Github Release™ will be created before the artifacts,
+# so there will be a few minutes where the release has no artifacts
+# and then they will slowly trickle in, possibly failing. To make
+# this more pleasant we mark the release as a "draft" until all
+# artifacts have been successfully uploaded. This allows you to
+# choose what to do with partial successes and avoids spamming
+# anyone with notifications before the release is actually ready.
+name: Release
+
+permissions:
+  contents: write
+
+# This task will run whenever you push a git tag that looks like a version
+# like "v1", "v1.0.0-FAKEVERSION", "v1.0.0-FAKEVERSION", "my-app-v1.0.0-FAKEVERSION", etc.
+# The version will be roughly parsed as ({PACKAGE_NAME}-)?v{VERSION}, where
+# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
+# must be a Cargo-style SemVer Version.
+#
+# If PACKAGE_NAME is specified, then we will create a Github Release™ 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 we will create a Github Release™ for all
+# (cargo-dist-able) packages in the workspace with that version (this is 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 Github Release™ for each one.
+#
+# If there's a prerelease-style suffix to the version then the Github Release™
+# will be marked as a prerelease.
+on:
+  push:
+    tags:
+      - '*-?v[0-9]+*'
+
+jobs:
+  # Create the Github Release™ so the packages have something to be uploaded to
+  create-release:
+    runs-on: ubuntu-latest
+    outputs:
+      has-releases: ${{ steps.create-release.outputs.has-releases }}
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: recursive
+      - name: Install cargo-dist
+        run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh
+      - id: create-release
+        run: |
+          cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json
+          echo "dist plan ran successfully"
+          cat dist-manifest.json
+
+          # Create the Github Release™ based on what cargo-dist thinks it should be
+          ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json)
+          IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json)
+          jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md
+          gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md
+          echo "created announcement!"
+
+          # Upload the manifest to the Github Release™
+          gh release upload ${{ github.ref_name }} dist-manifest.json
+          echo "uploaded manifest!"
+
+          # Disable all the upload-artifacts tasks if we have no actual releases
+          HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json)
+          echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT"
+
+  # Build and packages all the things
+  upload-artifacts:
+    # Let the initial task tell us to not run (currently very blunt)
+    needs: create-release
+    if: ${{ needs.create-release.outputs.has-releases == 'true' }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # For these target platforms
+        include:
+        - os: ubuntu-20.04
+          dist-args: --artifacts=global
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh
+        - os: macos-11
+          dist-args: --artifacts=local --target=aarch64-apple-darwin
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh
+        - os: macos-11
+          dist-args: --artifacts=local --target=x86_64-apple-darwin
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh
+        - os: windows-2019
+          dist-args: --artifacts=local --target=x86_64-pc-windows-msvc
+          install-dist: irm  https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.ps1 | iex
+        - os: ubuntu-20.04
+          dist-args: --artifacts=local --target=x86_64-unknown-linux-gnu
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-FAKEVERSION/cargo-dist-installer.sh | sh
+
+    runs-on: ${{ matrix.os }}
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: recursive
+      - name: Install cargo-dist
+        run: ${{ matrix.install-dist }}
+      - name: Run cargo-dist
+        # This logic is a bit janky because it's trying to be a polyglot between
+        # powershell and bash since this will run on windows, macos, and linux!
+        # The two platforms don't agree on how to talk about env vars but they
+        # do agree on 'cat' and '$()' so we use that to marshal values between commands.
+        run: |
+          # Actually do builds and make zips and whatnot
+          cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json
+          echo "dist ran successfully"
+          cat dist-manifest.json
+
+          # Parse out what we just built and upload it to the Github Release™
+          jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt
+          echo "uploading..."
+          cat uploads.txt
+          gh release upload ${{ github.ref_name }} $(cat uploads.txt)
+          echo "uploaded!"
+
+  # Mark the Github Release™ as a non-draft now that everything has succeeded!
+  publish-release:
+    # Only run after all the other tasks, but it's ok if upload-artifacts was skipped
+    needs: [create-release, upload-artifacts]
+    if: ${{ always() && needs.create-release.result == 'success' && (needs.upload-artifacts.result == 'skipped' || needs.upload-artifacts.result == 'success') }}
+    runs-on: ubuntu-latest
+    env:
+      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: recursive
+      - name: mark release as non-draft
+        run: |
+          gh release edit ${{ github.ref_name }} --draft=false
+
diff --git a/cargo-dist/tests/snapshots/integration_tests__install_path_cargo_home-installers.snap b/cargo-dist/tests/snapshots/integration_tests__install_path_cargo_home-installers.snap
deleted file mode 100644
index 138cfa2d..00000000
--- a/cargo-dist/tests/snapshots/integration_tests__install_path_cargo_home-installers.snap
+++ /dev/null
@@ -1,986 +0,0 @@
----
-source: cargo-dist/tests/integration-tests.rs
-expression: "&shell_snapshots"
----
-================ installer.sh ================
-#!/bin/sh
-# shellcheck shell=dash
-#
-# Licensed under the MIT license
-# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
-# option. This file may not be copied, modified, or distributed
-# except according to those terms.
-
-if [ "$KSH_VERSION" = 'Version JM 93t+ 2010-03-05' ]; then
-    # The version of ksh93 that ships with many illumos systems does not
-    # support the "local" extension.  Print a message rather than fail in
-    # subtle ways later on:
-    echo 'this installer does not work with this ksh93 version; please try bash!' >&2
-    exit 1
-fi
-
-set -u
-
-APP_NAME="axolotlsay"
-APP_VERSION="0.1.0"
-ARTIFACT_DOWNLOAD_URL="${INSTALLER_DOWNLOAD_URL:-https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0}"
-PRINT_VERBOSE=${INSTALLER_PRINT_VERBOSE:-0}
-PRINT_QUIET=${INSTALLER_PRINT_QUIET:-0}
-NO_MODIFY_PATH=${INSTALLER_NO_MODIFY_PATH:-0}
-
-usage() {
-    # print help (this cat/EOF stuff is a "heredoc" string)
-    cat <<EOF
-axolotlsay-installer.sh
-
-The installer for axolotlsay 0.1.0
-
-This script detects what platform you're on and fetches an appropriate archive from
-https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to \$CARGO_HOME/bin (\$HOME/.cargo/bin)
-
-It will then add that dir to PATH by adding the appropriate line to \$HOME/.profile
-
-USAGE:
-    axolotlsay-installer.sh [OPTIONS]
-
-OPTIONS:
-    -v, --verbose
-            Enable verbose output
-
-    -q, --quiet
-            Disable progress output
-
-        --no-modify-path
-            Don't configure the PATH environment variable
-
-    -h, --help
-            Print help information
-EOF
-}
-
-download_binary_and_run_installer() {
-    downloader --check
-    need_cmd uname
-    need_cmd mktemp
-    need_cmd chmod
-    need_cmd mkdir
-    need_cmd rm
-    need_cmd tar
-    need_cmd which
-    need_cmd grep
-    need_cmd cat
-
-    for arg in "$@"; do
-        case "$arg" in
-            --help)
-                usage
-                exit 0
-                ;;
-            --quiet)
-                PRINT_QUIET=1
-                ;;
-            --verbose)
-                PRINT_VERBOSE=1
-                ;;
-            --no-modify-path)
-                NO_MODIFY_PATH=1
-                ;;
-            *)
-                OPTIND=1
-                if [ "${arg%%--*}" = "" ]; then
-                    err "unknown option $arg"
-                    continue
-                fi
-                while getopts :hvq sub_arg "$arg"; do
-                    case "$sub_arg" in
-                        h)
-                            usage
-                            exit 0
-                            ;;
-                        v)
-                            # user wants to skip the prompt --
-                            # we don't need /dev/tty
-                            PRINT_VERBOSE=1
-                            ;;
-                        q)
-                            # user wants to skip the prompt --
-                            # we don't need /dev/tty
-                            PRINT_QUIET=1
-                            ;;
-                        *)
-                            err "unknown option -$OPTARG"
-                            ;;
-                        esac
-                done
-                ;;
-        esac
-    done
-
-    get_architecture || return 1
-    local _arch="$RETVAL"
-    assert_nz "$_arch" "arch"
-
-    local _bins
-    local _zip_ext
-    local _artifact_name
-
-    # Lookup what to download/unpack based on platform
-    case "$_arch" in 
-        "aarch64-apple-darwin")
-            _artifact_name="axolotlsay-aarch64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
-            _bins="axolotlsay"
-            ;;
-        "x86_64-apple-darwin")
-            _artifact_name="axolotlsay-x86_64-apple-darwin.tar.xz"
-            _zip_ext=".tar.xz"
-            _bins="axolotlsay"
-            ;;
-        "x86_64-unknown-linux-gnu")
-            _artifact_name="axolotlsay-x86_64-unknown-linux-gnu.tar.xz"
-            _zip_ext=".tar.xz"
-            _bins="axolotlsay"
-            ;;
-        *)
-            err "there isn't a package for $_arch"
-            ;;
-    esac
-
-    # download the archive
-    local _url="$ARTIFACT_DOWNLOAD_URL/$_artifact_name"
-    local _dir
-    if ! _dir="$(ensure mktemp -d)"; then
-        # Because the previous command ran in a subshell, we must manually
-        # propagate exit status.
-        exit 1
-    fi
-    local _file="$_dir/input$_zip_ext"
-
-    say "downloading $APP_NAME $APP_VERSION ${_arch}" 1>&2
-    say_verbose "  from $_url" 1>&2
-    say_verbose "  to $_file" 1>&2
-
-    ensure mkdir -p "$_dir"
-    
-    if ! downloader "$_url" "$_file"; then
-      say "failed to download $_url"
-      say "this may be a standard network error, but it may also indicate"
-      say "that $APP_NAME's release process is not working. When in doubt"
-      say "please feel free to open an issue!"
-      say_verbose "$REPO/issues/new/choose"
-      exit 1
-    fi
-
-    # unpack the archive
-    case "$_zip_ext" in
-        ".zip")
-            ensure unzip -q "$_file" -d "$_dir"
-            ;;
-
-        ".tar."*)
-            ensure tar xf "$_file" --strip-components 1 -C "$_dir"
-            ;;
-        *)
-            err "unknown archive format: $_zip_ext"
-            ;;
-    esac
-
-    install "$_dir" "$_bins" "$@"
-    local _retval=$?
-
-    ignore rm -rf "$_dir"
-
-    return "$_retval"
-}
-
-# See discussion of late-bound vs early-bound for why we use single-quotes with env vars
-# shellcheck disable=SC2016
-install() {
-    # This code needs to both compute certain paths for itself to write to, and
-    # also write them to shell/rc files so that they can look them up to e.g.
-    # add them to PATH. This requires an active distinction between paths
-    # and expressions that can compute them.
-    #
-    # The distinction lies in when we want env-vars to be evaluated. For instance
-    # if we determine that we want to install to $HOME/.myapp, which do we add
-    # to e.g. $HOME/.profile:
-    #
-    # * early-bound: export PATH="/home/myuser/.myapp:$PATH"
-    # * late-bound:  export PATH="$HOME/.myapp:$PATH"
-    #
-    # In this case most people would prefer the late-bound version, but in other
-    # cases the early-bound version might be a better idea. In particular when using
-    # other env-vars than $HOME, they are more likely to be only set temporarily
-    # for the duration of this install script, so it's more advisable to erase their
-    # existence with early-bounding.
-    #
-    # This distinction is handled by "double-quotes" (early) vs 'single-quotes' (late).
-    #
-    # This script has a few different variants, the most complex one being the
-    # CARGO_HOME version which attempts to install things to Cargo's bin dir,
-    # potentially setting up a minimal version if the user hasn't ever installed Cargo.
-    #
-    # In this case we need to:
-    #
-    # * Install to $HOME/.cargo/bin/
-    # * Create a shell script at $HOME/.cargo/env that:
-    #   * Checks if $HOME/.cargo/bin/ is on PATH
-    #   * and if not prepends it to PATH
-    # * Edits $HOME/.profile to run $HOME/.cargo/env (if the line doesn't exist)
-    #
-    # To do this we need these 4 values:
-
-    # The actual path we're going to install to
-    local _install_dir
-    # Path to the an shell script that adds install_dir to PATH
-    local _env_script_path
-    # Potentially-late-bound version of install_dir to write env_script
-    local _install_dir_expr
-    # Potentially-late-bound version of env_script_path to write to rcfiles like $HOME/.profile
-    local _env_script_path_expr
-
-
-    # first try CARGO_HOME, then fallback to HOME
-    if [ -n "${CARGO_HOME:-}" ]; then
-        _install_dir="$CARGO_HOME/bin"
-        _env_script_path="$CARGO_HOME/env"
-        # If CARGO_HOME was set but it ended up being the default $HOME-based path,
-        # then keep things late-bound. Otherwise bake the value for safety.
-        # This is what rustup does, and accurately reproducing it is useful.
-        if [ -n "${HOME:-}" ]; then
-            if [ "$HOME/.cargo/bin" = "$_install_dir" ]; then
-                _install_dir_expr='$HOME/.cargo/bin'
-                _env_script_path_expr='$HOME/.cargo/env'
-            else
-                _install_dir_expr="$_install_dir"
-                _env_script_path_expr="$_env_script_path"
-            fi
-        else
-            _install_dir_expr="$_install_dir"
-            _env_script_path_expr="$_env_script_path"
-        fi
-    elif [ -n "${HOME:-}" ]; then
-        _install_dir="$HOME/.cargo/bin"
-        _env_script_path="$HOME/.cargo/env"
-        _install_dir_expr='$HOME/.cargo/bin'
-        _env_script_path_expr='$HOME/.cargo/env'
-    else
-        err "could not find your CARGO_HOME or HOME dir to install binaries to"
-    fi
-
-    say "installing to $_install_dir"
-    ensure mkdir -p "$_install_dir"
-
-    # copy all the binaries to the install dir
-    local _src_dir="$1"
-    local _bins="$2"
-    for _bin_name in $_bins; do
-        local _bin="$_src_dir/$_bin_name"
-        ensure cp "$_bin" "$_install_dir"
-        # unzip seems to need this chmod
-        ensure chmod +x "$_install_dir/$_bin_name"
-        say "  $_bin_name"
-    done
-
-    say "everything's installed!"
-
-    if [ "0" = "$NO_MODIFY_PATH" ]; then
-        add_install_dir_to_path "$_install_dir_expr" "$_env_script_path" "$_env_script_path_expr"
-    fi
-}
-
-add_install_dir_to_path() {
-    # Edit rcfiles ($HOME/.profile) to add install_dir to $PATH
-    #
-    # We do this slightly indirectly by creating an "env" shell script which checks if install_dir
-    # is on $PATH already, and prepends it if not. The actual line we then add to rcfiles
-    # is to just source that script. This allows us to blast it into lots of different rcfiles and
-    # have it run multiple times without causing problems. It's also specifically compatible
-    # with the system rustup uses, so that we don't conflict with it.
-    local _install_dir_expr="$1"
-    local _env_script_path="$2"
-    local _env_script_path_expr="$3"
-    if [ -n "${HOME:-}" ]; then
-        local _rcfile="$HOME/.profile"
-        # `source x` is an alias for `. x`, and the latter is more portable/actually-posix.
-        # This apparently comes up a lot on freebsd. It's easy enough to always add
-        # the more robust line to rcfiles, but when telling the user to apply the change
-        # to their current shell ". x" is pretty easy to misread/miscopy, so we use the
-        # prettier "source x" line there. Hopefully people with Weird Shells are aware
-        # this is a thing and know to tweak it (or just restart their shell).
-        local _robust_line=". \"$_env_script_path_expr\""
-        local _pretty_line="source \"$_env_script_path_expr\""
-
-        # Add the env script if it doesn't already exist
-        if [ ! -f "$_env_script_path" ]; then 
-            say_verbose "creating $_env_script_path"
-            write_env_script "$_install_dir_expr" "$_env_script_path"
-        else
-            say_verbose "$_env_script_path already exists"
-        fi
-
-        # Check if the line is already in the rcfile
-        # grep: 0 if matched, 1 if no match, and 2 if an error occurred
-        #
-        # Ideally we could use quiet grep (-q), but that makes "match" and "error"
-        # have the same behaviour, when we want "no match" and "error" to be the same
-        # (on error we want to create the file, which >> conveniently does)
-        #
-        # We search for both kinds of line here just to do the right thing in more cases.
-        if ! grep -F "$_robust_line" "$_rcfile" > /dev/null 2>/dev/null && \
-           ! grep -F "$_pretty_line" "$_rcfile" > /dev/null 2>/dev/null
-        then 
-            # If the script now exists, add the line to source it to the rcfile
-            # (This will also create the rcfile if it doesn't exist)
-            if [ -f "$_env_script_path" ]; then 
-                say_verbose "adding $_robust_line to $_rcfile"
-                ensure echo "$_robust_line" >> "$_rcfile"
-                say ""
-                say "To add $_install_dir_expr to your PATH, either restart your shell or run:"
-                say ""
-                say "    $_pretty_line"
-            fi
-        else
-            say_verbose "$_install_dir already on PATH"
-        fi
-    fi
-}
-
-write_env_script() {
-    # write this env script to the given path (this cat/EOF stuff is a "heredoc" string)
-    local _install_dir_expr="$1"
-    local _env_script_path="$2"
-    ensure cat <<EOF > "$_env_script_path"
-#!/bin/sh
-# add binaries to PATH if they aren't added yet
-# affix colons on either side of \$PATH to simplify matching
-case ":\${PATH}:" in
-    *:"$_install_dir_expr":*)
-        ;;
-    *)
-        # Prepending path in case a system-installed binary needs to be overridden
-        export PATH="$_install_dir_expr:\$PATH"
-        ;;
-esac
-EOF
-}
-
-check_proc() {
-    # Check for /proc by looking for the /proc/self/exe link
-    # This is only run on Linux
-    if ! test -L /proc/self/exe ; then
-        err "fatal: Unable to find /proc/self/exe.  Is /proc mounted?  Installation cannot proceed without /proc."
-    fi
-}
-
-get_bitness() {
-    need_cmd head
-    # Architecture detection without dependencies beyond coreutils.
-    # ELF files start out "\x7fELF", and the following byte is
-    #   0x01 for 32-bit and
-    #   0x02 for 64-bit.
-    # The printf builtin on some shells like dash only supports octal
-    # escape sequences, so we use those.
-    local _current_exe_head
-    _current_exe_head=$(head -c 5 /proc/self/exe )
-    if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then
-        echo 32
-    elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then
-        echo 64
-    else
-        err "unknown platform bitness"
-    fi
-}
-
-is_host_amd64_elf() {
-    need_cmd head
-    need_cmd tail
-    # ELF e_machine detection without dependencies beyond coreutils.
-    # Two-byte field at offset 0x12 indicates the CPU,
-    # but we're interested in it being 0x3E to indicate amd64, or not that.
-    local _current_exe_machine
-    _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1)
-    [ "$_current_exe_machine" = "$(printf '\076')" ]
-}
-
-get_endianness() {
-    local cputype=$1
-    local suffix_eb=$2
-    local suffix_el=$3
-
-    # detect endianness without od/hexdump, like get_bitness() does.
-    need_cmd head
-    need_cmd tail
-
-    local _current_exe_endianness
-    _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)"
-    if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then
-        echo "${cputype}${suffix_el}"
-    elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then
-        echo "${cputype}${suffix_eb}"
-    else
-        err "unknown platform endianness"
-    fi
-}
-
-get_architecture() {
-    local _ostype
-    local _cputype
-    _ostype="$(uname -s)"
-    _cputype="$(uname -m)"
-    local _clibtype="gnu"
-
-    if [ "$_ostype" = Linux ]; then
-        if [ "$(uname -o)" = Android ]; then
-            _ostype=Android
-        fi
-        if ldd --version 2>&1 | grep -q 'musl'; then
-            _clibtype="musl"
-        fi
-    fi
-
-    if [ "$_ostype" = Darwin ] && [ "$_cputype" = i386 ]; then
-        # Darwin `uname -m` lies
-        if sysctl hw.optional.x86_64 | grep -q ': 1'; then
-            _cputype=x86_64
-        fi
-    fi
-
-    if [ "$_ostype" = SunOS ]; then
-        # Both Solaris and illumos presently announce as "SunOS" in "uname -s"
-        # so use "uname -o" to disambiguate.  We use the full path to the
-        # system uname in case the user has coreutils uname first in PATH,
-        # which has historically sometimes printed the wrong value here.
-        if [ "$(/usr/bin/uname -o)" = illumos ]; then
-            _ostype=illumos
-        fi
-
-        # illumos systems have multi-arch userlands, and "uname -m" reports the
-        # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86
-        # systems.  Check for the native (widest) instruction set on the
-        # running kernel:
-        if [ "$_cputype" = i86pc ]; then
-            _cputype="$(isainfo -n)"
-        fi
-    fi
-
-    case "$_ostype" in
-
-        Android)
-            _ostype=linux-android
-            ;;
-
-        Linux)
-            check_proc
-            _ostype=unknown-linux-$_clibtype
-            _bitness=$(get_bitness)
-            ;;
-
-        FreeBSD)
-            _ostype=unknown-freebsd
-            ;;
-
-        NetBSD)
-            _ostype=unknown-netbsd
-            ;;
-
-        DragonFly)
-            _ostype=unknown-dragonfly
-            ;;
-
-        Darwin)
-            _ostype=apple-darwin
-            ;;
-
-        illumos)
-            _ostype=unknown-illumos
-            ;;
-
-        MINGW* | MSYS* | CYGWIN* | Windows_NT)
-            _ostype=pc-windows-gnu
-            ;;
-
-        *)
-            err "unrecognized OS type: $_ostype"
-            ;;
-
-    esac
-
-    case "$_cputype" in
-
-        i386 | i486 | i686 | i786 | x86)
-            _cputype=i686
-            ;;
-
-        xscale | arm)
-            _cputype=arm
-            if [ "$_ostype" = "linux-android" ]; then
-                _ostype=linux-androideabi
-            fi
-            ;;
-
-        armv6l)
-            _cputype=arm
-            if [ "$_ostype" = "linux-android" ]; then
-                _ostype=linux-androideabi
-            else
-                _ostype="${_ostype}eabihf"
-            fi
-            ;;
-
-        armv7l | armv8l)
-            _cputype=armv7
-            if [ "$_ostype" = "linux-android" ]; then
-                _ostype=linux-androideabi
-            else
-                _ostype="${_ostype}eabihf"
-            fi
-            ;;
-
-        aarch64 | arm64)
-            _cputype=aarch64
-            ;;
-
-        x86_64 | x86-64 | x64 | amd64)
-            _cputype=x86_64
-            ;;
-
-        mips)
-            _cputype=$(get_endianness mips '' el)
-            ;;
-
-        mips64)
-            if [ "$_bitness" -eq 64 ]; then
-                # only n64 ABI is supported for now
-                _ostype="${_ostype}abi64"
-                _cputype=$(get_endianness mips64 '' el)
-            fi
-            ;;
-
-        ppc)
-            _cputype=powerpc
-            ;;
-
-        ppc64)
-            _cputype=powerpc64
-            ;;
-
-        ppc64le)
-            _cputype=powerpc64le
-            ;;
-
-        s390x)
-            _cputype=s390x
-            ;;
-        riscv64)
-            _cputype=riscv64gc
-            ;;
-        loongarch64)
-            _cputype=loongarch64
-            ;;
-        *)
-            err "unknown CPU type: $_cputype"
-
-    esac
-
-    # Detect 64-bit linux with 32-bit userland
-    if [ "${_ostype}" = unknown-linux-gnu ] && [ "${_bitness}" -eq 32 ]; then
-        case $_cputype in
-            x86_64)
-                # 32-bit executable for amd64 = x32
-                if is_host_amd64_elf; then {
-                    err "x32 linux unsupported"
-                }; else
-                    _cputype=i686
-                fi
-                ;;
-            mips64)
-                _cputype=$(get_endianness mips '' el)
-                ;;
-            powerpc64)
-                _cputype=powerpc
-                ;;
-            aarch64)
-                _cputype=armv7
-                if [ "$_ostype" = "linux-android" ]; then
-                    _ostype=linux-androideabi
-                else
-                    _ostype="${_ostype}eabihf"
-                fi
-                ;;
-            riscv64gc)
-                err "riscv64 with 32-bit userland unsupported"
-                ;;
-        esac
-    fi
-
-    # treat armv7 systems without neon as plain arm
-    if [ "$_ostype" = "unknown-linux-gnueabihf" ] && [ "$_cputype" = armv7 ]; then
-        if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then
-            # At least one processor does not have NEON.
-            _cputype=arm
-        fi
-    fi
-
-    _arch="${_cputype}-${_ostype}"
-
-    RETVAL="$_arch"
-}
-
-say() {
-    if [ "0" = "$PRINT_QUIET" ]; then
-        echo "$1"
-    fi
-}
-
-say_verbose() {
-    if [ "1" = "$PRINT_VERBOSE" ]; then
-        echo "$1"
-    fi
-}
-
-err() {
-    if [ "0" = "$PRINT_QUIET" ]; then
-        local red
-        local reset
-        red=$(tput setaf 1 2>/dev/null || echo '')
-        reset=$(tput sgr0 2>/dev/null || echo '')
-        say "${red}ERROR${reset}: $1" >&2
-    fi
-    exit 1
-}
-
-need_cmd() {
-    if ! check_cmd "$1"
-    then err "need '$1' (command not found)"
-    fi
-}
-
-check_cmd() {
-    command -v "$1" > /dev/null 2>&1
-    return $?
-}
-
-assert_nz() {
-    if [ -z "$1" ]; then err "assert_nz $2"; fi
-}
-
-# Run a command that should never fail. If the command fails execution
-# will immediately terminate with an error showing the failing
-# command.
-ensure() {
-    if ! "$@"; then err "command failed: $*"; fi
-}
-
-# This is just for indicating that commands' results are being
-# intentionally ignored. Usually, because it's being executed
-# as part of error handling.
-ignore() {
-    "$@"
-}
-
-# This wraps curl or wget. Try curl first, if not installed,
-# use wget instead.
-downloader() {
-    if check_cmd curl
-    then _dld=curl
-    elif check_cmd wget
-    then _dld=wget
-    else _dld='curl or wget' # to be used in error message of need_cmd
-    fi
-
-    if [ "$1" = --check ]
-    then need_cmd "$_dld"
-    elif [ "$_dld" = curl ]
-    then curl -sSfL "$1" -o "$2"
-    elif [ "$_dld" = wget ]
-    then wget "$1" -O "$2"
-    else err "Unknown downloader"   # should not reach here
-    fi
-}
-
-download_binary_and_run_installer "$@" || exit 1
-
-
-================ installer.ps1 ================
-# Licensed under the MIT license
-# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
-# option. This file may not be copied, modified, or distributed
-# except according to those terms.
-
-<#
-.SYNOPSIS
-
-The installer for axolotlsay 0.1.0
-
-.DESCRIPTION
-
-This script detects what platform you're on and fetches an appropriate archive from
-https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0
-then unpacks the binaries and installs them to $env:CARGO_HOME\bin ($HOME\.cargo\bin)
-
-It will then add that dir to PATH by editing your Environment.Path registry key
-
-.PARAMETER ArtifactDownloadUrl
-The URL of the directory where artifacts can be fetched from
-
-.PARAMETER NoModifyPath
-Don't add the install directory to PATH
-
-.PARAMETER Help
-Print help
-
-#>
-
-param (
-    [Parameter(HelpMessage = "The URL of the directory where artifacts can be fetched from")]
-    [string]$ArtifactDownloadUrl = 'https://github.com/axodotdev/axolotlsay/releases/download/v0.1.0',
-    [Parameter(HelpMessage = "Don't add the install directory to PATH")]
-    [switch]$NoModifyPath,
-    [Parameter(HelpMessage = "Print Help")]
-    [switch]$Help
-)
-
-$app_name = 'axolotlsay'
-$app_version = '0.1.0'
-
-function Install-Binary($install_args) {
-  if ($Help) {
-    Get-Help $PSCommandPath -Detailed
-    Exit
-  }
-  $old_erroractionpreference = $ErrorActionPreference
-  $ErrorActionPreference = 'stop'
-
-  Initialize-Environment
-
-  # Platform info injected by cargo-dist
-  $platforms = @{
-    "x86_64-pc-windows-msvc" = @{
-      "artifact_name" = "axolotlsay-x86_64-pc-windows-msvc.zip"
-      "bins" = "axolotlsay.exe"
-      "zip_ext" = ".zip"
-    }
-  }
-
-  $fetched = Download "$ArtifactDownloadUrl" $platforms
-  # FIXME: add a flag that lets the user not do this step
-  Invoke-Installer $fetched "$install_args"
-
-  $ErrorActionPreference = $old_erroractionpreference
-}
-
-function Get-TargetTriple() {
-  try {
-    # NOTE: this might return X64 on ARM64 Windows, which is OK since emulation is available.
-    # It works correctly starting in PowerShell Core 7.3 and Windows PowerShell in Win 11 22H2.
-    # Ideally this would just be
-    #   [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
-    # but that gets a type from the wrong assembly on Windows PowerShell (i.e. not Core)
-    $a = [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime.InteropServices.RuntimeInformation")
-    $t = $a.GetType("System.Runtime.InteropServices.RuntimeInformation")
-    $p = $t.GetProperty("OSArchitecture")
-    # Possible OSArchitecture Values: https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.architecture
-    # Rust supported platforms: https://doc.rust-lang.org/stable/rustc/platform-support.html
-    switch ($p.GetValue($null).ToString())
-    {
-      "X86" { return "i686-pc-windows-msvc" }
-      "X64" { return "x86_64-pc-windows-msvc" }
-      "Arm" { return "thumbv7a-pc-windows-msvc" }
-      "Arm64" { return "aarch64-pc-windows-msvc" }
-    }
-  } catch {
-    # The above was added in .NET 4.7.1, so Windows PowerShell in versions of Windows
-    # prior to Windows 10 v1709 may not have this API.
-    Write-Verbose "Get-TargetTriple: Exception when trying to determine OS architecture."
-    Write-Verbose $_
-  }
-
-  # This is available in .NET 4.0. We already checked for PS 5, which requires .NET 4.5.
-  Write-Verbose("Get-TargetTriple: falling back to Is64BitOperatingSystem.")
-  if ([System.Environment]::Is64BitOperatingSystem) {
-    return "x86_64-pc-windows-msvc"
-  } else {
-    return "i686-pc-windows-msvc"
-  }
-}
-
-function Download($download_url, $platforms) {
-  $arch = Get-TargetTriple
-
-  if (-not $platforms.ContainsKey($arch)) {
-    # X64 is well-supported, including in emulation on ARM64
-    Write-Verbose "$arch is not availablem falling back to X64"
-    $arch = "x86_64-pc-windows-msvc"
-  }
-
-  if (-not $platforms.ContainsKey($arch)) {
-    # should not be possible, as currently we always produce X64 binaries.
-    $platforms_json = ConvertTo-Json $platforms
-    throw "ERROR: could not find binaries for this platform. Last platform tried: $arch platform info: $platforms_json"
-  }
-
-  # Lookup what we expect this platform to look like
-  $info = $platforms[$arch]
-  $zip_ext = $info["zip_ext"]
-  $bin_names = $info["bins"]
-  $artifact_name = $info["artifact_name"]
-
-  # Make a new temp dir to unpack things to
-  $tmp = New-Temp-Dir
-  $dir_path = "$tmp\$app_name$zip_ext"
-
-  # Download and unpack!
-  $url = "$download_url/$artifact_name"
-  Write-Information "Downloading $app_name $app_version ($arch)"
-  Write-Verbose "  from $url"
-  Write-Verbose "  to $dir_path"
-  $wc = New-Object Net.Webclient
-  $wc.downloadFile($url, $dir_path)
-
-  Write-Verbose "Unpacking to $tmp"
-
-  # Select the tool to unpack the files with.
-  #
-  # As of windows 10(?), powershell comes with tar preinstalled, but in practice
-  # it only seems to support .tar.gz, and not xz/zstd. Still, we should try to
-  # forward all tars to it in case the user has a machine that can handle it!
-  switch -Wildcard ($zip_ext) {
-    ".zip" {
-      Expand-Archive -Path $dir_path -DestinationPath "$tmp";
-      Break
-    }
-    ".tar.*" {
-      tar xf $dir_path --strip-components 1 -C "$tmp";
-      Break
-    }
-    Default {
-      throw "ERROR: unknown archive format $zip_ext"
-    }
-  }
-
-  # Let the next step know what to copy
-  $bin_paths = @()
-  foreach ($bin_name in $bin_names) {
-    Write-Verbose "  Unpacked $bin_name"
-    $bin_paths += "$tmp\$bin_name"
-  }
-  return $bin_paths
-}
-
-function Invoke-Installer($bin_paths) {
-
-  # first try CARGO_HOME, then fallback to HOME
-  # (for whatever reason $HOME is not a normal env var and doesn't need the $env: prefix)
-  $dest_dir = if (($base_dir = $env:CARGO_HOME)) {
-    Join-Path $base_dir "bin"
-  } elseif (($base_dir = $HOME)) {
-    Join-Path $base_dir ".cargo\bin"
-  } else {
-    throw "ERROR: could not find your HOME dir or CARGO_HOME to install binaries to"
-  }
-
-  $dest_dir = New-Item -Force -ItemType Directory -Path $dest_dir
-  Write-Information "Installing to $dest_dir"
-  # Just copy the binaries from the temp location to the install dir
-  foreach ($bin_path in $bin_paths) {
-    $installed_file = Split-Path -Path "$bin_path" -Leaf
-    Copy-Item "$bin_path" -Destination "$dest_dir"
-    Remove-Item "$bin_path" -Recurse -Force
-    Write-Information "  $installed_file"
-  }
-
-  Write-Information "Everything's installed!"
-  if (-not $NoModifyPath) {
-    if (Add-Path $dest_dir) {
-        Write-Information ""
-        Write-Information "$dest_dir was added to your PATH, you may need to restart your shell for that to take effect."
-    }
-  }
-}
-
-# Try to add the given path to PATH via the registry
-#
-# Returns true if the registry was modified, otherwise returns false
-# (indicating it was already on PATH)
-function Add-Path($OrigPathToAdd) {
-  $RegistryPath = "HKCU:\Environment"
-  $PropertyName = "Path"
-  $PathToAdd = $OrigPathToAdd
-
-  $Item = if (Test-Path $RegistryPath) {
-    # If the registry key exists, get it
-    Get-Item -Path $RegistryPath
-  } else {
-    # If the registry key doesn't exist, create it
-    Write-Verbose  "Creating $RegistryPath"
-    New-Item -Path $RegistryPath -Force
-  }
-
-  $OldPath = ""
-  try {
-    # Try to get the old PATH value. If that fails, assume we're making it from scratch.
-    # Otherwise assume there's already paths in here and use a ; separator
-    $OldPath = $Item | Get-ItemPropertyValue -Name $PropertyName
-    $PathToAdd = "$PathToAdd;"
-  } catch {
-    # We'll be creating the PATH from scratch
-    Write-Verbose "Adding $PropertyName Property to $RegistryPath"
-  }
-
-  # Check if the path is already there
-  #
-  # We don't want to incorrectly match "C:\blah\" to "C:\blah\blah\", so we include the semicolon
-  # delimiters when searching, ensuring exact matches. To avoid corner cases we add semicolons to
-  # both sides of the input, allowing us to pretend we're always in the middle of a list.
-  if (";$OldPath;" -like "*;$OrigPathToAdd;*") {
-    # Already on path, nothing to do
-    Write-Verbose "install dir already on PATH, all done!"
-    return $false
-  } else {
-    # Actually update PATH
-    Write-Verbose "Adding $OrigPathToAdd to your PATH"
-    $NewPath = $PathToAdd + $OldPath
-    # We use -Force here to make the value already existing not be an error
-    $Item | New-ItemProperty -Name $PropertyName -Value $NewPath -PropertyType String -Force | Out-Null
-    return $true
-  }
-}
-
-function Initialize-Environment() {
-  If (($PSVersionTable.PSVersion.Major) -lt 5) {
-    Write-Error "PowerShell 5 or later is required to install $app_name."
-    Write-Error "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/setup/installing-windows-powershell"
-    break
-  }
-
-  # show notification to change execution policy:
-  $allowedExecutionPolicy = @('Unrestricted', 'RemoteSigned', 'ByPass')
-  If ((Get-ExecutionPolicy).ToString() -notin $allowedExecutionPolicy) {
-    Write-Error "PowerShell requires an execution policy in [$($allowedExecutionPolicy -join ", ")] to run $app_name."
-    Write-Error "For example, to set the execution policy to 'RemoteSigned' please run :"
-    Write-Error "'Set-ExecutionPolicy RemoteSigned -scope CurrentUser'"
-    break
-  }
-
-  # GitHub requires TLS 1.2
-  If ([System.Enum]::GetNames([System.Net.SecurityProtocolType]) -notcontains 'Tls12') {
-    Write-Error "Installing $app_name requires at least .NET Framework 4.5"
-    Write-Error "Please download and install it first:"
-    Write-Error "https://www.microsoft.com/net/download"
-    break
-  }
-}
-
-function New-Temp-Dir() {
-  [CmdletBinding(SupportsShouldProcess)]
-  param()
-  $parent = [System.IO.Path]::GetTempPath()
-  [string] $name = [System.Guid]::NewGuid()
-  New-Item -ItemType Directory -Path (Join-Path $parent $name)
-}
-
-# PSScriptAnalyzer doesn't like how we use our params as globals, this calms it
-$Null = $ArtifactDownloadUrl, $NoModifyPath, $Help
-Install-Binary "$Args"