Repository: axodotdev/cargo-dist
Pull request #168 (2023-02-26 19:44:12)

PowerShell installer: use manifest to select files to download.
This does not implement actually building something other than X64.

This checks for architects that matches the current operating system's architecture. It falls back to X64 if none are available.

Related issue: #85

AustinWise: @Gankra What do you think of using the `dist-manfiest.json` to select which files to download and install?

With regards to supporting archives other than zip (#17), the script stops executing after download if it encounters something other than a .zip file. The download path is printed, so the user can find the downloaded file easily.

I'm leaving this as draft until I remove the part in `install.rs` that generates the hard-coded asset list.
Gankra: Hey! Sorry for the delay, was focusing on the last bits of cutting 0.0.3.

> What do you think of using the dist-manfiest.json to select which files to download and install?

I am... *mixed* on it. In principle it's a good idea, but also in principle it's unnecessary and pushing complexity from the extremely maintainable/safe Rust code to the more hairy/dangerous shell scripts. Do you have a usecase in mind that makes it important/useful to do this kind of late-bound analysis?
AustinWise: > Do you have a usecase in mind that makes it important/useful to do this kind of late-bound analysis?

Not really. There would certainly be fewer things that could go wrong if the list of available artifacts and assets is hard-coded into the script.

Do you still think it is a good idea to have the script detect the OS architecture? I can roll-back my changes to use dist-manifest.json and instead include the architecture in the platform info injected by cargo-dist.
Gankra: yeah #85 would be great still! Sorry I didn't realize you had actually impl'd the target detection stuff, I'll review that part now!
Gankra: Out of morbid curiousity I checked out what powershell on macos does (yeah it's a thing) and then hacked up a version that should work on all platforms:

```ps1
function Get-TargetTriple() {
  try {
    Write-Verbose "test"
    # 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)

    # Possible OSPlatform Values: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.osplatform?source=recommendations&view=net-7.0
    # 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

    $a = [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime.InteropServices.RuntimeInformation")
    $t = $a.GetType("System.Runtime.InteropServices.RuntimeInformation")
    $raw_arch = $t.GetProperty("OSArchitecture")
    $arch = $null
    switch ($raw_arch.GetValue($null).ToString())
    {
      "X86" { $arch = "i686" }
      "X64" { $arch = "x86_64" }
      "Arm" { $arch = "thumbv7a" }
      "Arm64" { $arch = "aarch64" }
    }
    Write-Verbose "arch is $arch"

    $windows_os = [System.Runtime.InteropServices.OSPlatform]::Create("WINDOWS")
    $mac_os = [System.Runtime.InteropServices.OSPlatform]::Create("OSX")
    $linux_os = [System.Runtime.InteropServices.OSPlatform]::Create("FREEBSD")
    $freebsd_os = [System.Runtime.InteropServices.OSPlatform]::Create("LINUX")
    $os = $null
    if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($windows_os)) {
      $os = "pc-windows-msvc"
    } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($mac_os)) {
      $os = "apple-darwin"
    } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($linux_os)) {
      $os = "unknown-linux-gnu"
    } elseif ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($freebsd_os)) {
      $os = "unknown-freebsd"
    }
    Write-Verbose "os is $os"

    if ($os && $arch) {
      return "$arch-$os"
    } else {
      Write-Verbose "failed to get os and arch"
    }
  } 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"
  }
}
```

(probably this can be cleaned up)

```
PS /Users/ABeingessner/dev/tmp> ./tmp.ps1
VERBOSE: test
VERBOSE: System.Runtime.InteropServices.RuntimeInformation
VERBOSE: arch is x86_64
VERBOSE: os is apple-darwin
x86_64-apple-darwin
```
Gankra: Doing a bit more research into aarch64 windows, and I'm seeing a lot of notes of "eh fuck it, just use x64, arm64 windows has a rosetta-like emulation layer". Do you think we should add the same kind of installer fallback that was added in #165?
AustinWise: > Out of morbid curiousity I checked out what powershell on macos does (yeah it's a thing) and then hacked up a version that should work on all platforms:

It is a fair question whether or not the PowerShell script should support non-Windows platforms, given that it can run on several platforms now. Espeially since it appears you could [set PowerShell as your logon shell](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh?view=powershell-7.3#-login---l) on Unix-like systems!

One argument against that is you would have to implement all the logic to support Unix-like systems twice: once in the shell script and once in the PowerShell script. To avoid that maintenance burden, the PowerShell script could detect when it is being run on non-Windows:

```powershell
if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
    throw "This script is only supported on Windows. Please run this instead:\n curl --proto '=https' --tlsv1.2 -LsSf https://github.com/{{REPO}}/releases/download/{{VERSION}}/{{CRATE}}-{{VERSION}-installer.sh | sh";
}
```

`cargo-dist` could fill in all the right values to make that *just work*. It could even run the `curl | sh` pipeline behind the scenes, but that might be excessively clever.
AustinWise: > Doing a bit more research into arm64 windows, and I'm seeing a lot of notes of "eh fuck it, just use x64; arm64 windows has a rosetta-like emulation layer". Do you think we should add the same kind of installer fallback that was added in #165?

After fiddling with my Windows Dev Kit 2023 ARM64 I have come to a similar conclusion for Windows. It makes sense to try to download platform-specific binaries if they are available, but otherwise fallback to X64. That is the WIP state of this PR and I have updated #85 to reflect that this simple approach should be good enough.
AustinWise: This is now much-simplified from the original PR and now ready for more review.
Gankra: taking today off, will review on thursday, but i wanted to leave you some
loose notes on installer stuff:

* powershell installers will need to grow support for most of the linux-y
functionality anyway once we make zip style configurable. this doesn't need
to happen in your PR, i'm just keeping an eye out for the future. also i am
told that windows 10 now ships with tar out of the box which makes that
simple, although i need to look into how new that is.

* i'm going to refactor the shell/powershell code on the rust side to have
them share more code. this can happen before or after your pr, whichever
you prefer. i intentionally kept them separate in case they needed to
wildly diverge but i'm now seeing they are only converging more. this work
will also power future feature work on npm-based installers.

* i'm going to add some more general logic for "fallback" platforms so it's
easy for us to plug in more compatibility cases like x64 on arm64
(mac/windows), or x86 on x64 (windows only at this point..?)

On Wed, Mar 1, 2023 at 3:01 AM Austin Wise ***@***.***> wrote:

> This is now much-simplified from the original PR and now ready for more
> review.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/axodotdev/cargo-dist/pull/168#issuecomment-1449515595>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAIVRYEDNNNG4OYG7LUOXLTWZ364DANCNFSM6AAAAAAVIUOPP4>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 18361c72..9bfcee54 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -54,7 +54,7 @@ jobs:
       - name: Install Rust
         run: rustup update 1.67.1 --no-self-update && rustup default 1.67.1
       - name: Install cargo-dist
-        run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.3-prerelease12/cargo-dist-v0.0.3-prerelease12-installer.sh | sh
+        run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.4-prerelease.1/cargo-dist-v0.0.4-prerelease.1-installer.sh | sh
       - id: create-release
         run: |
           cargo dist manifest --tag=${{ github.ref_name }} --artifacts=all --no-local-paths --output-format=json > dist-manifest.json
@@ -87,16 +87,16 @@ jobs:
         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/v0.0.3-prerelease12/cargo-dist-v0.0.3-prerelease12-installer.sh | sh
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.4-prerelease.1/cargo-dist-v0.0.4-prerelease.1-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/v0.0.3-prerelease12/cargo-dist-v0.0.3-prerelease12-installer.sh | sh
+          dist-args: --artifacts=local --target=aarch64-apple-darwin --target=x86_64-apple-darwin
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.4-prerelease.1/cargo-dist-v0.0.4-prerelease.1-installer.sh | sh
         - 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/v0.0.3-prerelease12/cargo-dist-v0.0.3-prerelease12-installer.sh | sh
+          install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.4-prerelease.1/cargo-dist-v0.0.4-prerelease.1-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/v0.0.3-prerelease12/cargo-dist-v0.0.3-prerelease12-installer.ps1 | iex
+          install-dist: irm  https://github.com/axodotdev/cargo-dist/releases/download/v0.0.4-prerelease.1/cargo-dist-v0.0.4-prerelease.1-installer.ps1 | iex
 
     runs-on: ${{ matrix.os }}
     env:
diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
index f26aa452..357b814e 100644
--- a/.github/workflows/web.yml
+++ b/.github/workflows/web.yml
@@ -15,6 +15,7 @@ jobs:
     - uses: actions/checkout@v2
       with:
         fetch-depth: 0
+        ref: '5a5056d585a38e1bd1a7e5254073dc3b1e7470d2'
     - uses: actions-rs/toolchain@v1
       with:
         toolchain: stable
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e9b6c57..2ae96825 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# Version 0.0.4 (under development)
+
+* aarch64-apple-darwin ("apple silicon"/"arm64 macos") is now properly supported, and can be cross-compiled from x64 macos (and x64 can be crossed from arm64)
+    * if you have rustup installed we will `rustup target add` before attempting the build, as this is the only requirement (thanks for making it easy, Apple!)
+    * add this target to your Cargo.toml before you `cargo dist regenerate-ci` to make sure the CI knows to build it!
+    * currently both mac builds will be multiplexed onto the same runner. this will increase latency of your releases but should reduce the total resource usage of your CI (by avoiding fixed overheads). We might make this configurable in the future, but if you care about Universal MacOS binaries which staple x64 and arm64 together, your build will end up looking like this anyway (not yet implemented).
+
+
 # Version 0.0.3 (2023-02-27)
 
 A major overhaul has been done to the design to rationalize some improperly defined features/behaviours. When you update to this version **we recommend following these MIGRATION INSTRUCTIONS**:
diff --git a/Cargo.lock b/Cargo.lock
index d8b8bdbd..1dd8bc45 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -146,7 +146,7 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist"
-version = "0.0.3"
+version = "0.0.4-prerelease.2"
 dependencies = [
  "camino",
  "cargo-dist-schema",
@@ -174,7 +174,7 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist-schema"
-version = "0.0.3"
+version = "0.0.4-prerelease.2"
 dependencies = [
  "camino",
  "insta",
diff --git a/Cargo.toml b/Cargo.toml
index b7d463f8..d501bd56 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ tag-name = "v{{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.0.3-prerelease12"
+cargo-dist-version = "0.0.4-prerelease.1"
 # The preferred Rust toolchain to use in CI (rustup toolchain syntax)
 rust-toolchain-version = "1.67.1"
 # CI backends to support (see 'cargo dist generate-ci')
@@ -19,7 +19,7 @@ ci = ["github"]
 # The installers to generate for each app
 installers = ["shell", "powershell"]
 # Target platforms to build apps for (Rust target-triple syntax)
-targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc"]
+targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-apple-darwin"]
 
 # The profile that 'cargo dist' will build with
 [profile.dist]
diff --git a/book/src/way-too-quickstart.md b/book/src/way-too-quickstart.md
index 43464325..cc5be056 100644
--- a/book/src/way-too-quickstart.md
+++ b/book/src/way-too-quickstart.md
@@ -50,7 +50,7 @@ git commit -am "wow shiny new cargo-dist CI!"
 cargo release 0.1.0
 ```
 
-(I left off the --execute flag from `cargo-release` so you won't accidentally break anything if you really did just copy paste that 😇)
+(I left off the `--execute` flag from `cargo-release` so you won't accidentally break anything if you really did just copy paste that 😇)
 
 For more details on using cargo-release with cargo-dist, see [the guide for that][cargo-release-guide].
 
@@ -58,4 +58,4 @@ For more details on using cargo-release with cargo-dist, see [the guide for that
 [cargo-release]: https://github.com/crate-ci/cargo-release
 [guide]: ./guide.md
 [install]: ./install.md
-[cargo-release-guide]: ./cargo-release-guide.md
\ No newline at end of file
+[cargo-release-guide]: ./cargo-release-guide.md
diff --git a/cargo-dist-schema/Cargo.toml b/cargo-dist-schema/Cargo.toml
index f7c971a0..5af1681c 100644
--- a/cargo-dist-schema/Cargo.toml
+++ b/cargo-dist-schema/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist-schema"
 description = "Schema information for cargo-dist's dist-manifest.json"
-version = "0.0.3"
+version = "0.0.4-prerelease.2"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
diff --git a/cargo-dist/Cargo.toml b/cargo-dist/Cargo.toml
index 9429ea06..31a0965f 100644
--- a/cargo-dist/Cargo.toml
+++ b/cargo-dist/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist"
 description = "Shippable application packaging for Rust"
-version = "0.0.3"
+version = "0.0.4-prerelease.2"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
@@ -32,7 +32,7 @@ console = { version = "0.15.1", optional = true }
 clap-cargo = { version = "0.10.0", optional = true }
 
 # Features used by the cli and library
-cargo-dist-schema = { version = "=0.0.3", path = "../cargo-dist-schema" }
+cargo-dist-schema = { version = "=0.0.4-prerelease.2", path = "../cargo-dist-schema" }
 miette = { version = "5.3.0" }
 thiserror = "1.0.35"
 tracing = { version = "0.1.36", features = ["log"] }
diff --git a/cargo-dist/src/installer.ps1 b/cargo-dist/src/installer.ps1
index 38163bb2..b0255129 100644
--- a/cargo-dist/src/installer.ps1
+++ b/cargo-dist/src/installer.ps1
@@ -35,12 +35,58 @@ function Install-Binary($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) {
   # FIXME: make this something we lookup based on the current machine
-  $arch = "x86_64-pc-windows-msvc"
+  $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
-  # FIXME: produce a nice error if $arch isn't found (or do fallback guesses?)
   $info = $platforms[$arch]
   $zip_ext = $info["zip_ext"]
   $bin_names = $info["bins"]
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index eaaeb86b..5639b139 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -217,6 +217,7 @@ fn manifest_artifact(
 fn run_build_step(dist_graph: &DistGraph, target: &BuildStep) -> Result<()> {
     match target {
         BuildStep::Cargo(target) => build_cargo_target(dist_graph, target),
+        BuildStep::Rustup(cmd) => rustup_toolchain(dist_graph, cmd),
         BuildStep::CopyFile(CopyFileStep {
             src_path,
             dest_path,
@@ -376,6 +377,23 @@ fn build_cargo_target(dist_graph: &DistGraph, target: &CargoBuildStep) -> Result
     Ok(())
 }
 
+/// Build a cargo target
+fn rustup_toolchain(_dist_graph: &DistGraph, cmd: &RustupStep) -> Result<()> {
+    eprintln!("running rustup to ensure you have {} installed", cmd.target);
+    let status = Command::new(&cmd.rustup.cmd)
+        .arg("target")
+        .arg("add")
+        .arg(&cmd.target)
+        .status()
+        .into_diagnostic()
+        .wrap_err("Failed to install rustup toolchain")?;
+
+    if !status.success() {
+        return Err(miette!("Failed to install rustup toolchain"));
+    }
+    Ok(())
+}
+
 /// Initialize the dir for an artifact (and delete the old artifact file).
 fn init_artifact_dir(_dist: &DistGraph, artifact: &Artifact) -> Result<()> {
     // Delete any existing bundle
diff --git a/cargo-dist/src/main.rs b/cargo-dist/src/main.rs
index b52f045e..d4d6cb21 100644
--- a/cargo-dist/src/main.rs
+++ b/cargo-dist/src/main.rs
@@ -284,17 +284,11 @@ fn cmd_init(cli: &Cli, _args: &InitArgs) -> Result<(), miette::Report> {
 }
 
 fn cmd_generate_ci(cli: &Cli, _args: &GenerateCiArgs) -> Result<(), miette::Report> {
-    // This command is more automagic, so provide default targets if none are chosen
-    let targets = if cli.target.is_empty() {
-        default_desktop_targets()
-    } else {
-        cli.target.clone()
-    };
     let config = cargo_dist::Config {
         needs_coherent_announcement_tag: false,
         artifact_mode: cargo_dist::ArtifactMode::All,
         no_local_paths: cli.no_local_paths,
-        targets,
+        targets: cli.target.clone(),
         ci: cli.ci.iter().map(|ci| ci.to_lib()).collect(),
         installers: cli.installer.iter().map(|ins| ins.to_lib()).collect(),
         announcement_tag: cli.tag.clone(),
@@ -305,13 +299,15 @@ fn cmd_generate_ci(cli: &Cli, _args: &GenerateCiArgs) -> Result<(), miette::Repo
 
 fn default_desktop_targets() -> Vec<String> {
     vec![
+        // Everyone can build x64!
         "x86_64-unknown-linux-gnu".to_owned(),
         "x86_64-apple-darwin".to_owned(),
         "x86_64-pc-windows-msvc".to_owned(),
-        // cross-compiles not yet supported
+        // Apple is really easy to cross from Apple
+        "aarch64-apple-darwin".to_owned(),
+        // other cross-compiles not yet supported
         // "aarch64-gnu-unknown-linux".to_owned(),
         // "aarch64-pc-windows-msvc".to_owned(),
-        // "aarch64-apple-darwin".to_owned(),
     ]
 }
 
diff --git a/cargo-dist/src/tasks.rs b/cargo-dist/src/tasks.rs
index 25df4aa0..337b728b 100644
--- a/cargo-dist/src/tasks.rs
+++ b/cargo-dist/src/tasks.rs
@@ -339,6 +339,9 @@ pub struct DistGraph {
     /// The desired rust toolchain for handling this project
     pub desired_rust_toolchain: Option<String>,
     /// The desired ci backends for this project
+    pub tools: Tools,
+    /// The target triple of the current system
+    pub host_target: String,
 
     /// Styles of CI we want to support
     pub ci_style: Vec<CiStyle>,
@@ -369,6 +372,22 @@ pub struct DistGraph {
     pub releases: Vec<Release>,
 }
 
+/// Various tools we have found installed on the system
+#[derive(Debug, Clone, Default)]
+pub struct Tools {
+    /// rustup, useful for getting specific toolchains
+    pub rustup: Option<Tool>,
+}
+
+/// A tool we have found installed on the system
+#[derive(Debug, Clone, Default)]
+pub struct Tool {
+    /// The string to pass to Command::new
+    pub cmd: String,
+    /// The version the tool reported (in case useful)
+    pub version: String,
+}
+
 /// A binary we want to build (specific to a Variant)
 #[derive(Debug)]
 pub struct Binary {
@@ -399,6 +418,8 @@ pub struct Binary {
 pub enum BuildStep {
     /// Do a cargo build (and copy the outputs to various locations)
     Cargo(CargoBuildStep),
+    /// Run rustup to get a toolchain
+    Rustup(RustupStep),
     /// Copy a file
     CopyFile(CopyFileStep),
     /// Copy a dir
@@ -428,6 +449,15 @@ pub struct CargoBuildStep {
     pub expected_binaries: Vec<BinaryIdx>,
 }
 
+/// A cargo build (and copy the outputs to various locations)
+#[derive(Debug)]
+pub struct RustupStep {
+    /// The rustup to invoke (mostly here to prove you Have rustup)
+    pub rustup: Tool,
+    /// The target to install
+    pub target: String,
+}
+
 /// zip/tarball some directory
 #[derive(Debug)]
 pub struct ZipDirStep {
@@ -805,6 +835,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         cargo: String,
         workspace: &'pkg_graph WorkspaceInfo<'pkg_graph>,
         artifact_mode: ArtifactMode,
+        host_target: String,
     ) -> Self {
         let target_dir = workspace.info.target_directory().to_owned();
         let workspace_dir = workspace.info.root().to_owned();
@@ -819,6 +850,8 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 dist_dir,
                 desired_cargo_dist_version: workspace.desired_cargo_dist_version.clone(),
                 desired_rust_toolchain: workspace.desired_rust_toolchain.clone(),
+                tools: Tools::default(),
+                host_target,
                 announcement_tag: None,
                 announcement_is_prerelease: false,
                 announcement_changelog: None,
@@ -1248,7 +1281,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
 
         let mut build_steps = vec![];
         let cargo_builds = self.compute_cargo_builds();
-        build_steps.extend(cargo_builds.into_iter().map(BuildStep::Cargo));
+        build_steps.extend(cargo_builds.into_iter());
 
         for artifact in &self.inner.artifacts {
             match &artifact.kind {
@@ -1301,7 +1334,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         self.inner.build_steps = build_steps;
     }
 
-    fn compute_cargo_builds(&mut self) -> Vec<CargoBuildStep> {
+    fn compute_cargo_builds(&mut self) -> Vec<BuildStep> {
         // For now we can be really simplistic and just do a workspace build for every
         // target-triple we have a binary-that-needs-a-real-build for.
         let mut targets = SortedMap::<TargetTriple, Vec<BinaryIdx>>::new();
@@ -1332,7 +1365,22 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 rustflags.push_str(" -Ctarget-feature=+crt-static");
             }
 
-            builds.push(CargoBuildStep {
+            // If we're trying to cross-compile on macOS, ensure the rustup toolchain
+            // is setup!
+            if target.ends_with("apple-darwin")
+                && self.inner.host_target.ends_with("apple-darwin")
+                && target != self.inner.host_target
+            {
+                if let Some(rustup) = self.inner.tools.rustup.clone() {
+                    builds.push(BuildStep::Rustup(RustupStep {
+                        rustup,
+                        target: target.clone(),
+                    }));
+                } else {
+                    warn!("You're trying to cross-compile on macOS, but I can't find rustup to ensure you have the rust toolchains for it!")
+                }
+            }
+            builds.push(BuildStep::Cargo(CargoBuildStep {
                 target_triple: target.clone(),
                 // Just build the whole workspace for now
                 package: CargoTargetPackages::Workspace,
@@ -1344,7 +1392,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 rustflags,
                 profile: String::from(PROFILE_DIST),
                 expected_binaries: binaries,
-            });
+            }));
         }
         builds
     }
@@ -1522,9 +1570,6 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         self.inner.announcement_github_body = Some(gh_body);
     }
 
-    fn cargo(&self) -> &str {
-        &self.inner.cargo
-    }
     fn workspace(&self) -> &'pkg_graph WorkspaceInfo<'pkg_graph> {
         self.workspace
     }
@@ -1595,7 +1640,9 @@ pub fn gather_work(cfg: &Config) -> Result<DistGraph> {
     let cargo = cargo()?;
     let package_graph = package_graph(&cargo)?;
     let workspace = workspace_info(&package_graph)?;
-    let mut graph = DistGraphBuilder::new(cargo, &workspace, cfg.artifact_mode);
+    let host_target = get_host_target(&cargo)?;
+    let mut graph = DistGraphBuilder::new(cargo, &workspace, cfg.artifact_mode, host_target);
+    graph.inner.tools = tool_info();
 
     // First thing's first: if they gave us an announcement tag then we should try to parse it
     let mut announcing_package = None;
@@ -1643,7 +1690,7 @@ pub fn gather_work(cfg: &Config) -> Result<DistGraph> {
     }
 
     // If no targets were specified, just use the host target
-    let host_target_triple = [get_host_target(graph.cargo())?];
+    let host_target_triple = [graph.inner.host_target.clone()];
     // If all targets specified, union together the targets our packages support
     // Note that this uses BTreeSet as an intermediate to make the order stable
     let all_target_triples = graph
@@ -2332,3 +2379,19 @@ fn try_extract_changelog_normalized(
 
     Some((title.trim().to_string(), release_notes.notes.to_string()))
 }
+
+fn tool_info() -> Tools {
+    Tools {
+        rustup: find_tool("rustup"),
+    }
+}
+
+fn find_tool(name: &str) -> Option<Tool> {
+    let output = Command::new(name).arg("-V").output().ok()?;
+    let string_output = String::from_utf8(output.stdout).ok()?;
+    let version = string_output.lines().next()?;
+    Some(Tool {
+        cmd: name.to_owned(),
+        version: version.to_owned(),
+    })
+}


Test patch:
diff --git a/cargo-dist/tests/cli-tests.rs b/cargo-dist/tests/cli-tests.rs
index 017d1f1f..3dd40798 100644
--- a/cargo-dist/tests/cli-tests.rs
+++ b/cargo-dist/tests/cli-tests.rs
@@ -32,10 +32,12 @@ fn test_version() {
     let last = ver_parts.next().unwrap();
     if let Some((last, _prerelease)) = last.split_once('-') {
         last.parse::<u8>().unwrap();
+        if let Some(build) = ver_parts.next() {
+            build.parse::<u8>().unwrap();
+        }
     } else {
         last.parse::<u8>().unwrap();
     }
-
     assert!(ver_parts.next().is_none());
 }
 
@@ -82,7 +84,7 @@ fn test_manifest() {
 
     // We don't want this to churn every time we do a version bump
     insta::with_settings!({filters => vec![
-        (r"\d+\.\d+\.\d+(\-prerelease\d+)?", "1.0.0-FAKEVERSION"),
+        (r"\d+\.\d+\.\d+(\-prerelease\d*)?(\.\d+)?", "1.0.0-FAKEVERSION"),
         (r#""announcement_tag": .*"#, r#""announcement_tag": "CENSORED","#),
         (r#""announcement_title": .*"#, r#""announcement_title": "CENSORED""#),
         (r#""announcement_changelog": .*"#, r#""announcement_changelog": "CENSORED""#),
@@ -113,7 +115,7 @@ fn test_lib_manifest() {
 
     // We don't want this to churn every time we do a version bump
     insta::with_settings!({filters => vec![
-        (r"\d+\.\d+\.\d+(\-prerelease\d+)?", "1.0.0-FAKEVERSION"),
+        (r"\d+\.\d+\.\d+(\-prerelease\d*)?(\.\d+)?", "1.0.0-FAKEVERSION"),
         (r#""announcement_tag": .*"#, r#""announcement_tag": "CENSORED","#),
         (r#""announcement_title": .*"#, r#""announcement_title": "CENSORED""#),
         (r#""announcement_changelog": .*"#, r#""announcement_changelog": "CENSORED""#),
@@ -142,7 +144,7 @@ fn test_error_manifest() {
 
     // We don't want this to churn every time we do a version bump
     insta::with_settings!({filters => vec![
-        (r"\d+\.\d+\.\d+(\-prerelease\d+)?", "1.0.0-FAKEVERSION"),
+        (r"\d+\.\d+\.\d+(\-prerelease\d*)?(\.\d+)?", "1.0.0-FAKEVERSION"),
         (r#""announcement_tag": .*"#, r#""announcement_tag": "CENSORED","#),
         (r#""announcement_title": .*"#, r#""announcement_title": "CENSORED""#),
         (r#""announcement_changelog": .*"#, r#""announcement_changelog": "CENSORED""#),
diff --git a/cargo-dist/tests/snapshots/cli_tests__manifest.snap b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
index 1a6c255f..4bb91ed9 100644
--- a/cargo-dist/tests/snapshots/cli_tests__manifest.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
@@ -17,6 +17,7 @@ stdout:
       "artifacts": [
         "cargo-dist-v1.0.0-FAKEVERSION-installer.sh",
         "cargo-dist-v1.0.0-FAKEVERSION-installer.ps1",
+        "cargo-dist-v1.0.0-FAKEVERSION-aarch64-apple-darwin.tar.xz",
         "cargo-dist-v1.0.0-FAKEVERSION-x86_64-apple-darwin.tar.xz",
         "cargo-dist-v1.0.0-FAKEVERSION-x86_64-pc-windows-msvc.zip",
         "cargo-dist-v1.0.0-FAKEVERSION-x86_64-unknown-linux-gnu.tar.xz"
@@ -24,6 +25,40 @@ stdout:
     }
   ],
   "artifacts": {
+    "cargo-dist-v1.0.0-FAKEVERSION-aarch64-apple-darwin.tar.xz": {
+      "name": "cargo-dist-v1.0.0-FAKEVERSION-aarch64-apple-darwin.tar.xz",
+      "kind": "executable-zip",
+      "target_triples": [
+        "aarch64-apple-darwin"
+      ],
+      "assets": [
+        {
+          "name": "CHANGELOG.md",
+          "path": "CHANGELOG.md",
+          "kind": "changelog"
+        },
+        {
+          "name": "LICENSE-APACHE",
+          "path": "LICENSE-APACHE",
+          "kind": "license"
+        },
+        {
+          "name": "LICENSE-MIT",
+          "path": "LICENSE-MIT",
+          "kind": "license"
+        },
+        {
+          "name": "README.md",
+          "path": "README.md",
+          "kind": "readme"
+        },
+        {
+          "name": "cargo-dist",
+          "path": "cargo-dist",
+          "kind": "executable"
+        }
+      ]
+    },
     "cargo-dist-v1.0.0-FAKEVERSION-installer.ps1": {
       "name": "cargo-dist-v1.0.0-FAKEVERSION-installer.ps1",
       "kind": "installer",
@@ -37,6 +72,7 @@ stdout:
       "name": "cargo-dist-v1.0.0-FAKEVERSION-installer.sh",
       "kind": "installer",
       "target_triples": [
+        "aarch64-apple-darwin",
         "x86_64-apple-darwin",
         "x86_64-unknown-linux-gnu"
       ],