Repository: beeware/briefcase
Pull request #1360 (2023-07-14 00:26:25)

Implement architecture and bitness checks
## Summary
- Only download Android SDK when known to work on host platform
- Only download JDK when known to work on host platform
- Only download a Standalone Python known to work on host platform
- Only build Windows Apps on wholly 64bit platforms
- Fix i386 support for AppImages
- (Attempt to) Build x86-64 AppImages on Apple Silicon
- Ensure Linux System builds are actually compatible with the target platform

## Changes
- Prevent Briefcase from attempting to run in known unsupported configurations
- Android AAB/APK
  - Java
    - Error if JDK is not supported and `JAVA_HOME` is not set
    - Adoptium provides JDK for:
      - Linux: `arm`, `aarch64`, `x86_64`
        - The `arm` release is for the `armv7hf`ABI...so `armv6l` (e.g. RPi Zero) is not supported
      - macOS: `x86_64`, `arm64`
      - Windows: `AMD64` (and technically `x86`)
  - Android SDK
    - Error if Android SDK is not supported and `ANDROID_HOME` is not set
    - The `sdkmanager` is a lot of fun 🙃 
      - A key prerequisite for Briefcase to install the Android SDK using `sdkmanager` is its support to install `emulator` as a dependency for `build-tools`
      - However, the `sdkmanager` can only download `emulator` package for:
        - Linux: `x86_64`
          - The emulator [can run](https://developer.android.com/studio/releases/emulator#emulator_for_arm64_hosts) on `aarch64`....Google even [builds](https://ci.android.com/builds/branches/aosp-emu-master-dev/grid) it in their CI....but `sdkmanager` won't fetch it for you...
        - macOS: `x86_64`, `arm64`
        - Windows: `AMD64`
          - It's unclear if running the emulator on Windows on ARM is possible, but there is "[Windows Subsystem for Android](https://learn.microsoft.com/en-us/windows/android/wsa/)"....and it definitely looks like arbitrary Android apps can [run in it](https://www.androidpolice.com/set-up-wsa-windows-11-android-apps/) via `adb`
- iOS App
  - Assume platform is supported since macOS is only practically available for `x86_64` and `arm64`
- Linux AppImage
  - Error if LinuxDeploy is not supported
  - LinuxDeploy is available for `i686` and `x86_64`
- Linux Flatpak
  - ~~TODO: decide if error if Flatpak runtime support is scant~~
  - The existing error that a runtime is not available is pretty clear about the problem...so, leaving as is.
  - Flatpak runtime support is only practical for `x86_64` and `aarch64`
  - Although, the Flatpak tools all "work" on the other platforms
- Linux System
  - ~~TODO: decide if error when Python bitness doesn't match target OS bitness~~
  - It's not exactly straightforward to determine if Python has a different bitness than the OS. However, `pip` detects this and installs 32bit wheels and the underlying packaging tools seem to create 32bit packages as well.
  - Any platform is supported by virtue of the fact the platform exists
- macOS App
  - Assume platform is supported since macOS is only practically available for `x86_64` and `arm64`
- Web
  - Assume platform is supposed since web is practically platform-agnostic
- Window App
  - Error if Python was built targeting 32bit
  - Error for unsupported platforms
  - The Briefcase template only supports `AMD64`
  - `ARM64` support would ideally be added at some point, though
    - `RCEdit` is only available for `AMD64`...although Windows on ARM can run `x86_64` binaries....so, I guess the real question is whether `RCEdit` can edit `ARM64` binaries....
  - `i686` support is unnecessary as 32bit Windows continues to disappear

## Notes
- It's a bit annoying that `sdkmanager` considers `emulator` a dependency of `build-tools`
  - This effectively breaks Android support on any platform that isn't a "normal PC or macbook"
  - Maybe it's possible to spoof `emulator` or avoid the dependency another way...
    - Putting a proper `android_sdk/emulator/package.xml` file is sufficient to spoof this for `sdkmanager`
    - A `android_sdk/emulator/emulator.exe` file is sufficient to spoof this for Briefcase....however, Briefcase depends on this file being an executable later on...

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct


freakboy3742: > * macOS
>   
>   * Supported arches: `x86-64`, `arm64`
>     
>     * Modern macOS is only x86-64 and arm64...so probably not really effective checking this
>   * Supported Python: 64bit
>     
>     * I don't know when macOS dropped 32bit support but Python did with 3.7
>     * Although, maybe "universal2" installers include 32bit support...kinda doubt it tbh

FWIW: The last macOS release to support 32 bit apps was Mojave (10.14), which had EOL almost exactly 2 years ago; even that release had copious warnings whenever you tried to run a 32 bit app.

Universal2 doesn't (and can't) include 32 bit support. Python packaging theoretically supports:
 * "universal", supporting x86_64, i386, ppc64 and ppc
 * "intel", "fat" and "fat32" supporting i386 and x86_64

However, in practice, I don't think I've ever seen one of those tags in use.

tl;dr - 32-bit macOS effectively doesn't exist at this point.




rmartin16: To help me keep this straight in my head, I put it in a spreadsheet instead. I believe this represents our current situation....although, I'm considering it up for debate based on what the team .

A goal I think I'd like to achieve for this PR is strictly enforcing the supported platforms....instead of letting (for instance) the JDK download fail because there isn't a arm64 version for download (although, msft does have one for download), Briefcase would provide a relevant error message to the user.

So, first I would like to confirm we align on this goal of defined error states vs "if it works, then go ahead". So, practically, I think this means committing to an actual definition of what Briefcase supports.

A couple of things to point out:
- Do we really need all these arm versions?
  - maybe...
    - Pi zero will report `armv6l`
    - Pi 3B+ will report `armv7l`
    - Pi 4B with 32bit OS will report `armv7l` or `armv8l` (need to confirm)
      - Older Linux kernels jury-rigged Pi support by hardcoding some broadcom chip so `armv7l` would always be reported by certain tools. need to confirm if this is still the case.
    - Pi 4B with 64bit OS will report `aarch64`
  - This good news is this is only really relevant to Linux System packages and we don't have to include a Python support package for those apps
    - So, this avoids all the complications of the ARM ABIs prior to `aarch64`
- What about 32bit Python on 64bit OS?
  - On Windows, this will be an error
  - On macOS, this is moot...so a check probably isn't even necessary
  - On Linux...it'll probably be necessary to confirm Python's bitness matches that of the OS
    - Needs more testing....although, it's also a bit difficult to get 32bit Python running on 64bit Linux nowadays
- Flatpak i686 and arm support
  - While runtimes aren't generally available, if one was found, this would all work..
  - So, we could just show a warning to users....or simply enforce that only `x86_64` and `aarch64` are supported
- The `build` directory doesn't really support targeting different arches
  - I'm unlikely to address this in this PR...but calling it out here
  - For instance, it's currently possible to target both `debian:bullseye` and `i386/debian:bullseye`....but they will use the same `build` sub-directory.
  - This obviously isn't ideal...and will definitely be exacerbated once explicitly targeting other architectures is supported

### A prettier [Google Sheets version](https://docs.google.com/spreadsheets/d/16L0Iz4FtAHTjqzfVmFAgObDfhga00OCBC9PBEZ-VGTw/edit?usp=sharing)

Platform | Format | Host OS | Host Arch | Supported? | Notes
-- | -- | -- | -- | -- | --
Android | Gradle | Linux | i686 | ❌ |  
  |   |   | x86_64 | ✅ |  
  |   |   | armv6l | 🟨 | Requires user-installed Android SDK
  |   |   | armv7l | 🟨 | Requires user-installed Android SDK
  |   |   | armv8l | 🟨 | Requires user-installed Android SDK
  |   |   | aarch64 | 🟨 | Requires user-installed Android SDK
  |   | macOS | x86_64 | ✅ |  
  |   |   | arm64 | ✅ |  
  |   | Windows | x86_64 | ✅ |  
  |   |   | arm64 | 🟨 | Requires user-installed Android SDK and JDK
iOS | Xcode | macOS | x86_64 | ✅ |  
  |   |   | arm64 | ✅ |  
Linux | AppImage | Linux | i686 | ✅ |  
  |   |   | x86_64 | ✅ |  
  |   |   | armv6l | ❌ | Support from LinuxDeploy is unlikely
  |   |   | armv7l | ❌ | Support from LinuxDeploy is unlikely
  |   |   | armv8l | ❌ | Support from LinuxDeploy is unlikely
  |   |   | aarch64 | 🟠 | Pending aarch64 release from LinuxDeploy
  |   | macOS | x86_64 | ✅ | Requires Docker on host
  |   |   | arm64 | 🟠 | Requires Docker on host; pending aarch64 release from LinuxDeploy
  | Flatpak | Linux | i686 | ❌ | Runtime support was dropped
  |   |   | x86_64 | ✅ |  
  |   |   | armv6l | ❌ | Runtime support was dropped
  |   |   | armv7l | ❌ | Runtime support was dropped
  |   |   | armv8l | ❌ | Runtime support was dropped
  |   |   | aarch64 | ✅ |  
  | System | Linux | i686 | ✅ |  
  |   |   | x86_64 | ✅ |  
  |   |   | armv6l | ✅ |  
  |   |   | armv7l | ✅ |  
  |   |   | armv8l | ✅ |  
  |   |   | aarch64 | ✅ |  
  |   | macOS | x86_64 | ✅ |  
  |   |   | arm64 | ✅ |  
macOS | App | macOS | x86_64 | ✅ |  
  |   |   | arm64 | ✅ |  
  | Xcode | macOS | x86_64 | ✅ |  
  |   |   | arm64 | ✅ |  
Web | Static | Linux | Any | ✅ |  
  |   | macOS | Any | ✅ |  
  |   | Windows | Any | ✅ |  
Windows | App | Windows | x86_64 | ✅ |  
  |   |   | arm64 | ⚫ | The stub app in the template needs to be built for arm64
  | VisualStudio | Windows | x86_64 | ✅ |  
  |   |   | arm64 | ⚫ | The VS project needs arm64 added as a compile target

Legend
Symbol | Description
-- | --
✅ | Supported |  
🟨 | Supported with user prerequisites |  
⚫ | Requires BeeWare development for support |  
🟠 | Requires upstream development to add support |
❌ | Support not possible and/or desired |  
rmartin16: After experimenting a bit more, I think I've landed on an implementation that won't require defining some exhaustive set of compatible platforms.

There are two reasons a user's platform may not be compatible:
- Briefcase, itself, does not support the platform
- An underlying tool does not support the platform...or Briefcase's installation method doesn't support the platform

So, given these known and definable limitations, I'll just update Briefcase to:
- prevent commands from running if Briefcase doesn't work on the platform
  - e.g. Windows commands on a arch other than `AMD64`
- return a proper error if Briefcase cannot install a usable version of a tool
  - e.g. JDK can't be installed on `armv6l`....so instead of erroring on the download, tell the user to set `JAVA_HOME`
  - (although, i'ts actually a bit more complicated for JDK here....but I'll omit the details for now)
rmartin16: @freakboy3742, couple questions when you get a break from pycon...

As it relates to using LinuxDeploy, was Briefcase able to create an AppImage on macOS for Apple Silicon? It seems this would try to download LinuxDeploy using `arm64` as the architecture...which isn't going to work.

```python
class LinuxDeploy(LinuxDeployBase, ManagedTool):
    @property
    def file_name(self) -> str:
        return f"linuxdeploy-{self.tools.host_arch}.AppImage"

    @property
    def download_url(self) -> str:
        return (
            "https://github.com/linuxdeploy/linuxdeploy/"
            f"releases/download/continuous/{self.file_name}"
        )
```

Also, this makes me wonder how Docker behaves on macOS for Apple Silicon. Is the architecture inside the Docker container `x86_64`? or `aarch64`?

Docker docs say they recommend rosetta...so, my presumption is the Linux VM that Docker Desktop runs the containers in is running `x86_64` in emulation.
rmartin16: I'm implemented all the checks I've identified as necessary and described in the PR description...save one.

The are issues when running a 32bit Python and/or a 32bit Linux on 64bit hardware. It is probably pretty uncommon nowadays for x86-64 hardware to be running a 32bit Linux...if only because most of the big distros have dropped 32bit support. However, this is more common on ARM hardware. Even now, the Raspberry Pi Imager software for imaging SD cards puts the 32bit version front and center in the UI with "recommended" tagged on it.

AppImage (in Docker anyway) and Flatpak seem to be ok since they are built in sandboxes.

However, the System packages have most of the issues since they can become a mix of 32 and 64 bit. I'm gonna spend some more time trying to tweak some of these trouble spots to better source the information.

Moreover, though, I'm not sure there's a reliable way (or at least straightforward way) to detect this situation. While it's pretty straightforward to determine the bitness of Python....it can be tricky to know the bitness of the OS or hardware in an arbitrary environment. As an example, here's `systemd` trying to [figure](https://github.com/systemd/systemd/blob/db58f6a9338d30935aa219bec9a8a853cc807756/src/basic/architecture.c) it out the arch.
rmartin16: > However, the System packages have most of the issues since they can become a mix of 32 and 64 bit. I'm gonna spend some more time trying to tweak some of these trouble spots to better source the information.

After spending a lot of time trying to understand how the distros internalize architectures, I've landed on an implementation that at least appears to work (although, it should be no worse there where we started)...I am, at least, quite confident that the strategy for Debian is reliable.

Instead of using `platform.machine()` to determine the ABI tag for system packages, Briefcase now defers to the build environment for this information:
- Debian: `dpkg --print-architecture`
- RHEL: `rpm --eval '%_target_cpu`
- Arch: `uname -m`

Of note, `platform.machine()` is effectively the same as `uname -m`.

Additionally, this approach basically does all the groundwork to eventually implement #1128.
freakboy3742: > @freakboy3742, couple questions when you get a break from pycon...
> 
> As it relates to using LinuxDeploy, was Briefcase able to create an AppImage on macOS for Apple Silicon? It seems this would try to download LinuxDeploy using `arm64` as the architecture...which isn't going to work.

Yeah - it currently fails because of platform/architecture confusion. I did some experimentation a while back with x86 on ARM, and got all sorts of segfaults and failures (e.g., Java didn't like running in an emulated container). However, that was pre-Ventura, and Java 8 (i.e., Java from a pre-M1 world).

> Also, this makes me wonder how Docker behaves on macOS for Apple Silicon. Is the architecture inside the Docker container `x86_64`? or `aarch64`?
>
> Docker docs say they recommend rosetta...so, my presumption is the Linux VM that Docker Desktop runs the containers in is running `x86_64` in emulation.

```
rkm@eunectes ~ % docker run --rm -it --platform linux/amd64 debian:bookworm /bin/bash
root@38ab78842616:/# uname -a
Linux 38ab78842616 5.15.49-linuxkit-pr #1 SMP PREEMPT Thu May 25 07:27:39 UTC 2023 x86_64 GNU/Linux
root@38ab78842616:/# apt-get update
...
root@38ab78842616:/# apt-get install python3
...
root@38ab78842616:/# python3
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import platform
>>> platform.platform()
'Linux-5.15.49-linuxkit-pr-x86_64-with-glibc2.36'
>>> platform.architecture()
('64bit', '')
>>> 
>>> platform.machine()
'x86_64'
>>> platform.processor()
''
>>> platform.system()
'Linux'
```

I'm not 100% sure what's going on behind the scenes, but all the docker binaries present as being M1 architecture in the Apple system monitor (rather than reporting as Intel). However, that's not reporting binaries *inside* the container, so I'm guessing there's some weird hypervisor/emulation stuff going on internal to the Docker binary.

If I try to run docker *without* the `--platform linux/arm64`, I get:
```
rkm@eunectes ~ % docker run --rm -it debian:bookworm /bin/bash 
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
```
This appears to be because the previous (successful) command has cached a debian:bookworm image; if I purge the image, or I explicitly ask for ARM:
rkm@eunectes ~ % docker run --rm -it --platform linux/arm64/v8 debian:bookworm /bin/bash
Unable to find image 'debian:bookworm' locally
bookworm: Pulling from library/debian
42cbebf8bc11: Pull complete 
Digest: sha256:3d868b5eb908155f3784317b3dda2941df87bbbbaa4608f84881de66d9bb297b
Status: Downloaded newer image for debian:bookworm
root@206e36697551:/# uname -a
Linux 206e36697551 5.15.49-linuxkit-pr #1 SMP PREEMPT Thu May 25 07:27:39 UTC 2023 aarch64 GNU/Linux
root@206e36697551:/# apt-get install python3
...
root@206e36697551:/# apt-get install python3
...
root@206e36697551:/# python3
Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import platform
>>> platform.platform()
'Linux-5.15.49-linuxkit-pr-aarch64-with-glibc2.36'
>>> platform.architecture()
('64bit', '')
>>> platform.machine()
'aarch64'
>>> platform.processor()
''
>>> platform.system()
'Linux'
```

So - you can run both ARM and x86_64 docker, as long as you're explicit about platform (or there's no pre-existing cached image).

Happy to run other tests if you want; just let me know what you need.
rmartin16: > WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested

Ah, ok; so, Docker is requesting the `arm64` architecture by default for multi-architecture images. That's important information....but also makes sense. Thanks.

For AppImages, though, that won't play too big a role...now that I think about it.

First, this PR is currently limiting successful LinuxDeploy verification to `x86_64` and `i686`....but it's forcing the use of `x86_64` version of LinuxDeploy for macOS (ignoring whatever the actual arch is).

Second, manylinux doesn't use multi-architecture images....you have to explicitly request a platform in the image name:
```python
context["manylinux_image"] = f"{app.manylinux}_{self.tools.host_arch}:{tag}"
```

So, I'll also need to update this to always be `x86_64` for macOS. That should allow building x86-64 AppImages on Apple Silicon....as long as the emulation doesn't cause too many problems, that is....
freakboy3742: > > WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
> 
> Ah, ok; so, Docker is requesting the `arm64` architecture by default for multi-architecture images. That's important information....but also makes sense. Thanks.

This was testing with an image that has both ARM64 and AMD64 variants; I haven't tested what happens if you request an image that *only* has an AMD64 or ARM64 variant (mostly because I don't know of one off the top of my head)

> All that said, Docker says running x86-64 images on Apple silicon is "[best effort](https://docs.docker.com/desktop/troubleshoot/known-issues/)"...

Yeah - it definitely seems *more* stable under Ventura than it was under Monterey... but that's not saying much :-)
rmartin16: > This was testing with an image that has both ARM64 and AMD64 variants; I haven't tested what happens if you request an image that _only_ has an AMD64 or ARM64 variant (mostly because I don't know of one off the top of my head)

Right; I'm also finding a bunch of blog posts about Docker on Apple silicon that I'm searching from the perspective which arch Docker is requesting.

That said, `archlinux` publishes images that are only `amd64` and openSUSE provides `opensuse/arm64v8` which is only `arm64`.

I'm pretty sure Docker just throws up that warning about mismatched architectures and tries to run incompatible code with an eventual "exec error".

Patch:
diff --git a/changes/1360.feature.rst b/changes/1360.feature.rst
new file mode 100644
index 00000000..e328b5e8
--- /dev/null
+++ b/changes/1360.feature.rst
@@ -0,0 +1,1 @@
+Support for less common environments, such as Linux on ARM, is improved and error messages for unsupported platforms are more accurate.
diff --git a/docs/conf.py b/docs/conf.py
index 8272744c..9b7d029c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -97,6 +97,16 @@ pygments_style = "sphinx"
 
 intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
 
+# API status indicators.
+rst_prolog = """
+.. role:: full
+.. role:: yes
+.. role:: ymmv
+.. |f| replace:: :full:`●`
+.. |y| replace:: :yes:`○`
+.. |v| replace:: :ymmv:`△`
+"""
+
 # -- Options for link checking -------------------------------------------------
 
 linkcheck_anchors_ignore = [
@@ -310,6 +320,17 @@ spelling_lang = "en_US"
 # Location of word list.
 spelling_word_list_filename = "spelling_wordlist"
 
+# -- Options for link check -------------------------------------------
+
+linkcheck_ignore = [
+    r"./android.html",
+    r"./iOS.html",
+    r"./linux/system.html",
+    r"./macOS/app.html",
+    r"./web.html",
+    r"./windows/app.html",
+]
+
 # -- Options for Todos -------------------------------------------
 
 # If this is True, todo and todolist produce output, else they produce nothing. The default is False.
diff --git a/docs/reference/platforms/android.rst b/docs/reference/platforms/android.rst
index d08ed8c8..cea9a020 100644
--- a/docs/reference/platforms/android.rst
+++ b/docs/reference/platforms/android.rst
@@ -2,6 +2,17 @@
 Android
 =======
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+| |f|    | |y|   |     | |f|    |       | |v| | |f|    | |v| | |v|   |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
+
 When generating an Android project, Briefcase produces a Gradle project.
 
 Gradle requires an install of the Android SDK and a Java 17 JDK.
diff --git a/docs/reference/platforms/iOS.rst b/docs/reference/platforms/iOS.rst
index 009c060b..2008d7ae 100644
--- a/docs/reference/platforms/iOS.rst
+++ b/docs/reference/platforms/iOS.rst
@@ -2,6 +2,16 @@
 iOS
 ===
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+| |f|    | |y|   |     |        |       |     |        |     |       |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 When generating an iOS project, Briefcase produces an Xcode project.
 
 Icon format
diff --git a/docs/reference/platforms/index.rst b/docs/reference/platforms/index.rst
index 929d02e4..b1a2c472 100644
--- a/docs/reference/platforms/index.rst
+++ b/docs/reference/platforms/index.rst
@@ -3,7 +3,7 @@ Platform support
 ================
 
 .. toctree::
-   :maxdepth: 2
+   :hidden:
 
    macOS/index
    windows/index
@@ -11,3 +11,63 @@ Platform support
    iOS
    android
    web
+
+.. _platform-support-key:
+
+Key
+---
+
++-----+-------------------------------------+
+| |f| | Supported and tested in CI          |
++-----+-------------------------------------+
+| |y| | Supported and tested by maintainers |
++-----+-------------------------------------+
+| |v| | Supported but not tested regularly  |
++-----+-------------------------------------+
+
+
+.. |Gradle| replace:: **Gradle**
+.. _Gradle: ./android.html
+
+.. |iOS| replace:: **Xcode**
+.. _iOS: ./iOS.html
+
+.. |System| replace:: **Native System Packages**
+.. _System: ./linux/system.html
+
+.. |macOSApp| replace:: **.app bundle**
+.. _macOSApp: ./macOS/app.html
+
+.. |windowsApp| replace:: **Windows app**
+.. _windowsApp: ./windows/app.html
+
+.. |Web| replace:: **Static**
+.. _Web: ./web.html
+
++---------+--------------------------------------+--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Target App Format                              | Host System                                                        |
++                                                +--------+-------+---------+--------+---+-----+--------+-----+-------+
+|                                                | macOS          | Windows              | Linux                      |
++                                                +--------+-------+-----+--------+-------+-----+--------+-----+-------+
+|                                                | x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++=========+======================================+========+=======+=====+========+=======+=====+========+=====+=======+
+| Android | |Gradle|_                            | |f|    | |y|   |     | |f|    |       | |v| | |f|    | |v| | |v|   |
++---------+--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| iOS     | |iOS|_                               | |f|    | |y|   |     |        |       |     |        |     |       |
++---------+--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| Linux   | :doc:`./linux/appimage`              | |f|    |       |     |        |       | |v| | |f|    |     |       |
++         +--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+|         | :doc:`./linux/flatpak`               |        |       |     |        |       | |v| | |f|    | |v| | |v|   |
++         +--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+|         | |System|_                            | |y|    | |y|   |     |        |       | |v| | |f|    | |v| | |v|   |
++---------+--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| macOS   | |macOSApp|_                          | |f|    | |y|   |     |        |       |     |        |     |       |
++         +--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+|         | :doc:`Xcode project <./macOS/xcode>` | |f|    | |y|   |     |        |       |     |        |     |       |
++---------+--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| Web     | |web|_                               | |f|    | |y|   | |v| | |f|    | |v|   | |v| | |f|    | |v| | |v|   |
++---------+--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| Windows | |windowsApp|_                        |        |       |     | |f|    |       |     |        |     |       |
++         +--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
+|         | :doc:`./windows/visualstudio`        |        |       |     | |f|    |       |     |        |     |       |
++---------+--------------------------------------+--------+-------+-----+--------+-------+-----+--------+-----+-------+
diff --git a/docs/reference/platforms/linux/appimage.rst b/docs/reference/platforms/linux/appimage.rst
index 0c949af2..0bf5d5e4 100644
--- a/docs/reference/platforms/linux/appimage.rst
+++ b/docs/reference/platforms/linux/appimage.rst
@@ -2,6 +2,16 @@
 AppImage
 ========
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+| |f|    |       |     |        |       | |v| | |f|    |     |       |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 `AppImage <https://appimage.org>`__ provides a way for developers to provide
 "native" binaries for Linux users. It allow packaging applications for any
 common Linux based operating system, including Ubuntu, Debian, Fedora, and more.
diff --git a/docs/reference/platforms/linux/flatpak.rst b/docs/reference/platforms/linux/flatpak.rst
index ee025f1f..e4058cf1 100644
--- a/docs/reference/platforms/linux/flatpak.rst
+++ b/docs/reference/platforms/linux/flatpak.rst
@@ -2,6 +2,16 @@
 Flatpak
 =======
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+|        |       |     |        |       | |v| | |f|    | |v| | |v|   |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 `Flatpak <https://flatpak.org>`__ provides a way for developers to distribute
 apps to Linux users in a format that is independent of the specific distribution
 used by the end-user. It allow packaging applications for use on any common
diff --git a/docs/reference/platforms/linux/system.rst b/docs/reference/platforms/linux/system.rst
index 28def691..b372524b 100644
--- a/docs/reference/platforms/linux/system.rst
+++ b/docs/reference/platforms/linux/system.rst
@@ -2,6 +2,16 @@
 Native System Packages
 ======================
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+| |y|    | |y|   |     |        |       | |v| | |f|    | |v| | |v|   |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 All modern Linux distributions have a native format for distributing packages
 that are integrated into their overall operating system:
 
@@ -12,17 +22,10 @@ that are integrated into their overall operating system:
 The Briefcase ``system`` backend provides a way to build your app in these
 system package formats.
 
-.. note:: Not all Linux distributions are currently supported!
-
-    At present, Briefcase only has full support for Debian-based distributions.
-    It should be possible to build and run an application on other Linux
-    distributions; however, it won't be possible to package the app for
-    distribution on those platforms. We intend to add support for other
-    packaging formats - patches are welcome.
+.. admonition:: Not all Linux distributions are supported!
 
-    In addition, Briefcase cannot reliably identify *every* Linux vendor. If
-    your Linux distribution isn't being identified (or isn't being identified
-    correctly), please `open a ticket
+    Briefcase cannot reliably identify *every* Linux vendor. If your Linux distribution
+    isn't being identified (or isn't being identified correctly), please `open a ticket
     <https://github.com/beeware/briefcase/issues>`__ with the contents of your
     ``/etc/os-release`` file.
 
@@ -108,13 +111,13 @@ supported by the vendor, and system Python is Python 3.8 or later.
 
 The following Linux vendors are known to work as Docker targets:
 
-  * Debian (e.g., ``debian:bullseye`` or ``debian:11``)
-  * Ubuntu (e.g., ``ubuntu:jammy`` or ``ubuntu:22.04``)
-  * Fedora (e.g, ``fedora:37``)
-  * AlmaLinux (e.g., ``almalinux:9``)
-  * Red Hat Enterprise Linux (e.g., ``redhat/ubi9:9``)
-  * Arch Linux (e.g., ``archlinux:latest``)
-  * Manjaro Linux (e.g., ``manjarolinux/base:latest``)
+* Debian (e.g., ``debian:bullseye`` or ``debian:11``)
+* Ubuntu (e.g., ``ubuntu:jammy`` or ``ubuntu:22.04``)
+* Fedora (e.g, ``fedora:37``)
+* AlmaLinux (e.g., ``almalinux:9``)
+* Red Hat Enterprise Linux (e.g., ``redhat/ubi9:9``)
+* Arch Linux (e.g., ``archlinux:latest``)
+* Manjaro Linux (e.g., ``manjarolinux/base:latest``)
 
 Application configuration
 =========================
diff --git a/docs/reference/platforms/macOS/app.rst b/docs/reference/platforms/macOS/app.rst
index e547187a..6d866ece 100644
--- a/docs/reference/platforms/macOS/app.rst
+++ b/docs/reference/platforms/macOS/app.rst
@@ -2,6 +2,16 @@
 .app bundle
 ===========
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+| |f|    | |y|   |     |        |       |     |        |     |       |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 A macOS ``.app`` bundle is a collection of directory with a specific layout,
 and with some key metadata. If this structure and metadata exists, macOS treats
 the folder as an executable file, giving it an icon.
diff --git a/docs/reference/platforms/macOS/xcode.rst b/docs/reference/platforms/macOS/xcode.rst
index 2820184e..77c4e27b 100644
--- a/docs/reference/platforms/macOS/xcode.rst
+++ b/docs/reference/platforms/macOS/xcode.rst
@@ -1,6 +1,16 @@
-=============
-Xcode project
-=============
+===================
+macOS Xcode project
+===================
+
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+| |f|    | |y|   |     |        |       |     |        |     |       |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
 
 Briefcase supports creating a full Xcode project for a macOS app. This project
 can then be used to build an actual app bundle, with the ``briefcase build``
diff --git a/docs/reference/platforms/web.rst b/docs/reference/platforms/web.rst
index 48f58c61..65330418 100644
--- a/docs/reference/platforms/web.rst
+++ b/docs/reference/platforms/web.rst
@@ -2,6 +2,16 @@
 Web
 ===
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+| |f|    | |y|   | |v| | |f|    | |v|   | |v| | |f|    | |v| | |v|   |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 When generating a web project, Briefcase produces a static folder of HTML, CSS
 and JavaScript resources that can be deployed as a web site. The static web site
 is packaged as a ``.zip`` file for distribution.
diff --git a/docs/reference/platforms/windows/app.rst b/docs/reference/platforms/windows/app.rst
index c9696e2a..d1aee4d8 100644
--- a/docs/reference/platforms/windows/app.rst
+++ b/docs/reference/platforms/windows/app.rst
@@ -2,6 +2,16 @@
 Windows App
 ===========
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+|        |       |     | |f|    |       |     |        |     |       |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 A Windows app is a stub binary, allow with a collection of folders that contain
 the Python code for the app and the Python runtime libraries.
 
diff --git a/docs/reference/platforms/windows/visualstudio.rst b/docs/reference/platforms/windows/visualstudio.rst
index ae1d4609..912d51c8 100644
--- a/docs/reference/platforms/windows/visualstudio.rst
+++ b/docs/reference/platforms/windows/visualstudio.rst
@@ -2,6 +2,16 @@
 Visual Studio project
 =====================
 
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| Host Platform Support (:ref:`platform-support-key`)                |
++--------+-------+---------+--------+---+-----+--------+-----+-------+
+| macOS          | Windows              | Linux                      |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+| x86‑64 | arm64 | x86 | x86‑64 | arm64 | x86 | x86‑64 | arm | arm64 |
++========+=======+=====+========+=======+=====+========+=====+=======+
+|        |       |     | |f|    |       |     |        |     |       |
++--------+-------+-----+--------+-------+-----+--------+-----+-------+
+
 Briefcase supports creating a full Visual Studio project for a Windows App. This
 project can then be used to build the stub app binary with the ``briefcase
 build`` command, or directly from Visual Studio.
diff --git a/setup.cfg b/setup.cfg
index b9744a11..f61cbeca 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -94,7 +94,7 @@ dev =
     pytest == 7.4.0
     pytest-xdist == 3.3.1
     setuptools_scm[toml] == 7.1.0
-    tox == 4.10.0
+    tox == 4.11.0
 docs =
     furo == 2023.8.19
     pyenchant == 3.2.2
diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py
index d04e5c66..3a165c7c 100644
--- a/src/briefcase/commands/create.py
+++ b/src/briefcase/commands/create.py
@@ -344,6 +344,7 @@ class CreateCommand(BaseCommand):
                             python_version_tag=self.python_version_tag,
                             platform=self.platform,
                             host_arch=self.tools.host_arch,
+                            is_32bit=self.tools.is_32bit_python,
                         )
 
                 support_package_url = self.support_package_url(support_revision)
@@ -384,6 +385,7 @@ class CreateCommand(BaseCommand):
                     python_version_tag=self.python_version_tag,
                     platform=self.platform,
                     host_arch=self.tools.host_arch,
+                    is_32bit=self.tools.is_32bit_python,
                 ) from e
 
     def _write_requirements_file(
diff --git a/src/briefcase/exceptions.py b/src/briefcase/exceptions.py
index 005f7809..bb12101b 100644
--- a/src/briefcase/exceptions.py
+++ b/src/briefcase/exceptions.py
@@ -103,6 +103,18 @@ class MissingToolError(BriefcaseCommandError):
         super().__init__(msg=f"Unable to locate {tool!r}. Has it been installed?")
 
 
+class IncompatibleToolError(BriefcaseCommandError):
+    def __init__(self, tool: str, env_var: str):
+        self.tool = tool
+        super().__init__(
+            msg=f"""\
+Briefcase cannot install {tool} on this machine.
+
+Install {tool} manually and specify the installation directory in the {env_var} environment variable.
+            """
+        )
+
+
 class NonManagedToolError(BriefcaseCommandError):
     def __init__(self, tool):
         self.tool = tool
@@ -153,16 +165,18 @@ class MissingAppMetadata(BriefcaseCommandError):
 
 
 class MissingSupportPackage(BriefcaseCommandError):
-    def __init__(self, python_version_tag, platform, host_arch):
+    def __init__(self, python_version_tag, platform, host_arch, is_32bit):
         self.python_version_tag = python_version_tag
         self.platform = platform
         self.host_arch = host_arch
+        self.is_32bit = is_32bit
+        platform_name = f"{'32 bit ' if is_32bit else ''}{platform}"
         super().__init__(
             f"""\
-Unable to download {self.platform} support package for Python {self.python_version_tag} on {self.host_arch}.
+Unable to download {platform_name} support package for Python {self.python_version_tag} on {self.host_arch}.
 
-This is likely because either Python {self.python_version_tag} and/or {self.host_arch}
-is not yet supported on {self.platform}. You will need to:
+This is likely because either Python {self.python_version_tag} and/or {self.host_arch} is not yet
+supported on {platform_name}. You will need to:
     * Use an older version of Python; or
     * Compile your own custom support package.
 """
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index 12e4d541..ef3d7813 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -15,6 +15,7 @@ from briefcase.config import PEP508_NAME_RE
 from briefcase.console import InputDisabled, select_option
 from briefcase.exceptions import (
     BriefcaseCommandError,
+    IncompatibleToolError,
     InvalidDeviceError,
     MissingToolError,
 )
@@ -62,12 +63,30 @@ class AndroidSDK(ManagedTool):
 
     @property
     def cmdline_tools_url(self) -> str:
-        """The Android SDK Command-Line Tools URL appropriate to the current OS."""
-        platform_name = self.tools.host_os.lower()
-        if self.tools.host_os.lower() == "darwin":
-            platform_name = "mac"
-        elif self.tools.host_os.lower() == "windows":  # pragma: no branch
-            platform_name = "win"
+        """The Android SDK Command-Line Tools URL appropriate for the current machine.
+
+        The SDK largely only supports typical development environments; if a machine is
+        using an unsupported architecture, `sdkmanager` will error while installing the
+        emulator as a dependency of the build-tools. However, for some of the platforms
+        that are unsupported by sdkmanager, users can set up their own SDK install.
+        """
+        try:
+            platform_name = {
+                "Darwin": {
+                    "arm64": "mac",
+                    "x86_64": "mac",
+                },
+                "Linux": {
+                    "x86_64": "linux",
+                },
+                "Windows": {
+                    "AMD64": "win",
+                },
+            }[self.tools.host_os][self.tools.host_arch]
+        except KeyError as e:
+            raise IncompatibleToolError(
+                tool=self.full_name, env_var="ANDROID_HOME"
+            ) from e
 
         return (
             f"https://dl.google.com/android/repository/"
diff --git a/src/briefcase/integrations/base.py b/src/briefcase/integrations/base.py
index e054c02a..472c4374 100644
--- a/src/briefcase/integrations/base.py
+++ b/src/briefcase/integrations/base.py
@@ -195,6 +195,8 @@ class ToolCache(Mapping):
 
         self.host_arch = self.platform.machine()
         self.host_os = self.platform.system()
+        # Python is 32bit if its pointers can only address with 32 bits or fewer
+        self.is_32bit_python = self.sys.maxsize <= 2**32
 
         self.app_tools: DefaultDict[AppConfig, ToolCache] = defaultdict(
             lambda: ToolCache(
diff --git a/src/briefcase/integrations/java.py b/src/briefcase/integrations/java.py
index 8ce7d775..a9e3b4cd 100644
--- a/src/briefcase/integrations/java.py
+++ b/src/briefcase/integrations/java.py
@@ -5,7 +5,11 @@ import shutil
 import subprocess
 from pathlib import Path
 
-from briefcase.exceptions import BriefcaseCommandError, MissingToolError
+from briefcase.exceptions import (
+    BriefcaseCommandError,
+    IncompatibleToolError,
+    MissingToolError,
+)
 from briefcase.integrations.base import ManagedTool, ToolCache
 
 
@@ -25,23 +29,30 @@ class JDK(ManagedTool):
 
     @property
     def OpenJDK_download_url(self):
-        arch = {
-            "x86_64": "x64",  # Linux\macOS x86-64
-            "aarch64": "aarch64",  # Linux arm64
-            "armv6l": "arm",  # Linux arm
-            "arm64": "aarch64",  # macOS arm64
-            "AMD64": "x64",  # Windows x86-64
-        }.get(self.tools.host_arch)
-
-        platform = {
-            "Darwin": "mac",
-            "Windows": "windows",
-            "Linux": "linux",
-        }.get(self.tools.host_os)
-
-        extension = {
-            "Windows": "zip",
-        }.get(self.tools.host_os, "tar.gz")
+        """The OpenJDK download URL appropriate for the current machine."""
+        system_arch = self.tools.host_arch
+        # use a 32bit JDK if using 32bit Python on 64bit hardware
+        if self.tools.is_32bit_python and self.tools.host_arch == "aarch64":
+            system_arch = "armv7l"
+
+        try:
+            platform, arch, extension = {
+                "Darwin": {
+                    "arm64": ("mac", "aarch64", "tar.gz"),
+                    "x86_64": ("mac", "x64", "tar.gz"),
+                },
+                "Linux": {
+                    "armv7l": ("linux", "arm", "tar.gz"),
+                    "armv8l": ("linux", "arm", "tar.gz"),
+                    "aarch64": ("linux", "aarch64", "tar.gz"),
+                    "x86_64": ("linux", "x64", "tar.gz"),
+                },
+                "Windows": {
+                    "AMD64": ("windows", "x64", "zip"),
+                },
+            }[self.tools.host_os][system_arch]
+        except KeyError as e:
+            raise IncompatibleToolError(tool=self.full_name, env_var="JAVA_HOME") from e
 
         return (
             f"https://github.com/adoptium/temurin{self.JDK_MAJOR_VER}-binaries/"
diff --git a/src/briefcase/integrations/linuxdeploy.py b/src/briefcase/integrations/linuxdeploy.py
index 9d5d9f5f..b00e0708 100644
--- a/src/briefcase/integrations/linuxdeploy.py
+++ b/src/briefcase/integrations/linuxdeploy.py
@@ -11,6 +11,7 @@ from briefcase.exceptions import (
     BriefcaseCommandError,
     CorruptToolError,
     MissingToolError,
+    UnsupportedHostError,
 )
 from briefcase.integrations.base import ManagedTool, Tool, ToolCache
 
@@ -48,6 +49,19 @@ class LinuxDeployBase(ABC):
     def file_path(self) -> Path:
         """The folder on the local filesystem that contains the file_name."""
 
+    @classmethod
+    def arch(cls, host_arch: str) -> str:
+        """The architecture defined (and supported) by linuxdeploy for AppImages."""
+        try:
+            return {
+                "x86_64": "x86_64",
+                "i686": "i386",
+            }[host_arch]
+        except KeyError as e:
+            raise UnsupportedHostError(
+                f"Linux AppImages cannot be built on {host_arch}."
+            ) from e
+
     def exists(self) -> bool:
         return (self.file_path / self.file_name).is_file()
 
@@ -208,7 +222,7 @@ class LinuxDeployQtPlugin(LinuxDeployPluginBase, ManagedTool):
 
     @property
     def file_name(self) -> str:
-        return f"linuxdeploy-plugin-qt-{self.tools.host_arch}.AppImage"
+        return f"linuxdeploy-plugin-qt-{self.arch(self.tools.host_arch)}.AppImage"
 
     @property
     def download_url(self) -> str:
@@ -319,7 +333,7 @@ class LinuxDeploy(LinuxDeployBase, ManagedTool):
 
     @property
     def file_name(self) -> str:
-        return f"linuxdeploy-{self.tools.host_arch}.AppImage"
+        return f"linuxdeploy-{self.arch(self.tools.host_arch)}.AppImage"
 
     @property
     def download_url(self) -> str:
diff --git a/src/briefcase/platforms/linux/__init__.py b/src/briefcase/platforms/linux/__init__.py
index 1a2efadd..12a1338a 100644
--- a/src/briefcase/platforms/linux/__init__.py
+++ b/src/briefcase/platforms/linux/__init__.py
@@ -66,10 +66,17 @@ class LinuxMixin:
         System packages don't use a support package; this is defined by
         the template, so this method won't be invoked
         """
+        python_download_arch = self.tools.host_arch
+        # use a 32bit Python if using 32bit Python on 64bit hardware
+        if self.tools.is_32bit_python and self.tools.host_arch == "aarch64":
+            python_download_arch = "armv7"
+        elif self.tools.is_32bit_python and self.tools.host_arch == "x86_64":
+            python_download_arch = "i686"
+
         version, datestamp = support_revision.split("+")
         return (
             "https://github.com/indygreg/python-build-standalone/releases/download/"
-            f"{datestamp}/cpython-{support_revision}-{self.tools.host_arch}-unknown-linux-gnu-install_only.tar.gz"
+            f"{datestamp}/cpython-{support_revision}-{python_download_arch}-unknown-linux-gnu-install_only.tar.gz"
         )
 
     def vendor_details(self, freedesktop_info):
diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py
index d29575b0..55f47acb 100644
--- a/src/briefcase/platforms/linux/appimage.py
+++ b/src/briefcase/platforms/linux/appimage.py
@@ -44,7 +44,8 @@ class LinuxAppImagePassiveMixin(LinuxMixin):
 
     def binary_name(self, app):
         safe_name = app.formal_name.replace(" ", "_")
-        return f"{safe_name}-{app.version}-{self.tools.host_arch}.AppImage"
+        arch = LinuxDeploy.arch(self.tools.host_arch)
+        return f"{safe_name}-{app.version}-{arch}.AppImage"
 
     def binary_path(self, app):
         return self.bundle_path(app) / self.binary_name(app)
@@ -52,6 +53,11 @@ class LinuxAppImagePassiveMixin(LinuxMixin):
     def distribution_path(self, app):
         return self.dist_path / self.binary_name(app)
 
+    def verify_tools(self):
+        """Verify the AppImage LinuxDeploy tool and its plugins exist."""
+        super().verify_tools()
+        LinuxDeploy.verify(tools=self.tools)
+
     def add_options(self, parser):
         super().add_options(parser)
         parser.add_argument(
@@ -154,7 +160,11 @@ class LinuxAppImageCreateCommand(
         # Add the manylinux tag to the template context.
         try:
             tag = getattr(app, "manylinux_image_tag", "latest")
-            context["manylinux_image"] = f"{app.manylinux}_{self.tools.host_arch}:{tag}"
+            manylinux_arch = {
+                "x86_64": "x86_64",
+                "i386": "i686",
+            }[LinuxDeploy.arch(self.tools.host_arch)]
+            context["manylinux_image"] = f"{app.manylinux}_{manylinux_arch}:{tag}"
             if app.manylinux in {"manylinux1", "manylinux2010", "manylinux2014"}:
                 context["vendor_base"] = "centos"
             elif app.manylinux == "manylinux_2_24":
@@ -213,11 +223,6 @@ class LinuxAppImageOpenCommand(LinuxAppImageMostlyPassiveMixin, DockerOpenComman
 class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
     description = "Build a Linux AppImage."
 
-    def verify_tools(self):
-        """Verify the AppImage linuxdeploy tool and plugins exist."""
-        super().verify_tools()
-        LinuxDeploy.verify(tools=self.tools)
-
     def build_app(self, app: AppConfig, **kwargs):  # pragma: no-cover-if-is-windows
         """Build an application.
 
diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py
index 2a727084..32629a86 100644
--- a/src/briefcase/platforms/linux/system.py
+++ b/src/briefcase/platforms/linux/system.py
@@ -54,15 +54,6 @@ class LinuxSystemPassiveMixin(LinuxMixin):
 
         return options
 
-    @property
-    def linux_arch(self):
-        # Linux uses different architecture identifiers for some platforms
-        return {
-            "x86_64": "amd64",
-            "aarch64": "arm64",
-            "armv6l": "armhf",
-        }.get(self.tools.host_arch, self.tools.host_arch)
-
     def build_path(self, app):
         # Override the default build path to use the vendor name,
         # rather than "linux"
@@ -85,31 +76,6 @@ class LinuxSystemPassiveMixin(LinuxMixin):
         else:
             return f"el{app.target_codename}"
 
-    def distribution_filename(self, app):
-        if app.packaging_format == "deb":
-            return (
-                f"{app.app_name}_{app.version}-{getattr(app, 'revision', 1)}"
-                f"~{app.target_vendor}-{app.target_codename}_{self.linux_arch}.deb"
-            )
-        elif app.packaging_format == "rpm":
-            return (
-                f"{app.app_name}-{app.version}-{getattr(app, 'revision', 1)}"
-                f".{self.rpm_tag(app)}.{self.tools.host_arch}.rpm"
-            )
-        elif app.packaging_format == "pkg":
-            return (
-                f"{app.app_name}-{app.version}-{getattr(app, 'revision', 1)}"
-                f"-{self.tools.host_arch}.pkg.tar.zst"
-            )
-        else:
-            raise BriefcaseCommandError(
-                "Briefcase doesn't currently know how to build system packages in "
-                f"{app.packaging_format.upper()} format."
-            )
-
-    def distribution_path(self, app):
-        return self.dist_path / self.distribution_filename(app)
-
     def target_glibc_version(self, app):
         target_glibc = self.tools.os.confstr("CS_GNU_LIBC_VERSION").split()[1]
         return target_glibc
@@ -235,7 +201,7 @@ class LinuxSystemMostlyPassiveMixin(LinuxSystemPassiveMixin):
         # what "use docker" means in terms of target_image.
         return bool(self.target_image)
 
-    def app_python_version_tag(self, app):
+    def app_python_version_tag(self, app: AppConfig):
         if self.use_docker:
             # If we're running in Docker, we can't know the Python3 version
             # before rolling out the template; so we fall back to "3". Later,
@@ -247,6 +213,75 @@ class LinuxSystemMostlyPassiveMixin(LinuxSystemPassiveMixin):
             python_version_tag = super().app_python_version_tag(app)
         return python_version_tag
 
+    def _build_env_abi(self, app: AppConfig):
+        """Retrieves the ABI the packaging system is targeting in the build env.
+
+        Each packaging system uses different values to identify the exact ABI that
+        describes the target environment...so just defer to the packaging system.
+        """
+        command = {
+            "deb": ["dpkg", "--print-architecture"],
+            "rpm": ["rpm", "--eval", "%_target_cpu"],
+            "pkg": ["pacman-conf", "Architecture"],
+        }[app.packaging_format]
+        try:
+            return (
+                self.tools[app].app_context.check_output(command).split("\n")[0].strip()
+            )
+        except (OSError, subprocess.CalledProcessError) as e:
+            raise BriefcaseCommandError(
+                "Failed to determine build environment's ABI for packaging."
+            ) from e
+
+    def deb_abi(self, app: AppConfig) -> str:
+        """The default ABI for dpkg packaging for the target environment."""
+        try:
+            return self._deb_abi
+        except AttributeError:
+            self._deb_abi = self._build_env_abi(app)
+            return self._deb_abi
+
+    def rpm_abi(self, app: AppConfig) -> str:
+        """The default ABI for rpm packaging for the target environment."""
+        try:
+            return self._rpm_abi
+        except AttributeError:
+            self._rpm_abi = self._build_env_abi(app)
+            return self._rpm_abi
+
+    def pkg_abi(self, app: AppConfig) -> str:
+        """The default ABI for pacman packaging for the target environment."""
+        try:
+            return self._pkg_abi
+        except AttributeError:
+            self._pkg_abi = self._build_env_abi(app)
+            return self._pkg_abi
+
+    def distribution_filename(self, app: AppConfig) -> str:
+        if app.packaging_format == "deb":
+            return (
+                f"{app.app_name}_{app.version}-{getattr(app, 'revision', 1)}"
+                f"~{app.target_vendor}-{app.target_codename}_{self.deb_abi(app)}.deb"
+            )
+        elif app.packaging_format == "rpm":
+            return (
+                f"{app.app_name}-{app.version}-{getattr(app, 'revision', 1)}"
+                f".{self.rpm_tag(app)}.{self.rpm_abi(app)}.rpm"
+            )
+        elif app.packaging_format == "pkg":
+            return (
+                f"{app.app_name}-{app.version}-{getattr(app, 'revision', 1)}"
+                f"-{self.pkg_abi(app)}.pkg.tar.zst"
+            )
+        else:
+            raise BriefcaseCommandError(
+                "Briefcase doesn't currently know how to build system packages in "
+                f"{app.packaging_format.upper()} format."
+            )
+
+    def distribution_path(self, app: AppConfig):
+        return self.dist_path / self.distribution_filename(app)
+
     def target_glibc_version(self, app):
         """Determine the glibc version.
 
@@ -289,7 +324,7 @@ class LinuxSystemMostlyPassiveMixin(LinuxSystemPassiveMixin):
 
         return target_glibc
 
-    def platform_freedesktop_info(self, app):
+    def platform_freedesktop_info(self, app: AppConfig):
         if self.use_docker:
             # Preserve the target image on the command line as the app's target
             app.target_image = self.target_image
@@ -308,7 +343,7 @@ class LinuxSystemMostlyPassiveMixin(LinuxSystemPassiveMixin):
 
         return freedesktop_info
 
-    def docker_image_tag(self, app):
+    def docker_image_tag(self, app: AppConfig):
         """The Docker image tag for an app."""
         return f"briefcase/{app.bundle_identifier.lower()}:{app.target_vendor}-{app.target_codename}"
 
@@ -339,7 +374,7 @@ class LinuxSystemMostlyPassiveMixin(LinuxSystemPassiveMixin):
         super().clone_options(command)
         self.target_image = command.target_image
 
-    def verify_python(self, app):
+    def verify_python(self, app: AppConfig):
         """Verify that the version of Python being used to build the app in Docker is
         compatible with the version being used to run Briefcase.
 
@@ -487,7 +522,7 @@ class LinuxSystemMostlyPassiveMixin(LinuxSystemPassiveMixin):
             )
             return
 
-        # Run a check for each packages listed in the app's system_requires,
+        # Run a check for each package listed in the app's system_requires,
         # plus the baseline system packages that are required.
         missing = []
         for package in base_system_packages + getattr(app, "system_requires", []):
@@ -880,15 +915,15 @@ class LinuxSystemPackageCommand(LinuxSystemMixin, PackageCommand):
                 f.write(
                     "\n".join(
                         [
-                            f"Package: { app.app_name }",
-                            f"Version: { app.version }",
-                            f"Architecture: { self.linux_arch }",
-                            f"Maintainer: { app.author } <{ app.author_email }>",
-                            f"Homepage: { app.url }",
-                            f"Description: { app.description }",
-                            f" { debian_multiline_description(app.long_description) }",
-                            f"Depends: { system_runtime_requires }",
-                            f"Section: { getattr(app, 'system_section', 'utils') }",
+                            f"Package: {app.app_name}",
+                            f"Version: {app.version}",
+                            f"Architecture: {self.deb_abi(app)}",
+                            f"Maintainer: {app.author } <{app.author_email}>",
+                            f"Homepage: {app.url}",
+                            f"Description: {app.description}",
+                            f" {debian_multiline_description(app.long_description)}",
+                            f"Depends: {system_runtime_requires}",
+                            f"Section: {getattr(app, 'system_section', 'utils')}",
                             "Priority: optional\n",
                         ]
                     )
@@ -994,7 +1029,7 @@ class LinuxSystemPackageCommand(LinuxSystemMixin, PackageCommand):
                         ]
                         + [
                             "",
-                            f"ExclusiveArch:  {self.tools.host_arch}",
+                            f"ExclusiveArch:  {self.rpm_abi(app)}",
                             "",
                             "%description",
                             app.long_description,
@@ -1072,7 +1107,7 @@ with details about the release.
         self.tools.shutil.move(
             rpmbuild_path
             / "RPMS"
-            / self.tools.host_arch
+            / self.rpm_abi(app)
             / self.distribution_filename(app),
             self.distribution_path(app),
         )
@@ -1140,7 +1175,7 @@ with details about the release.
                             f"pkgver={app.version}",
                             f"pkgrel={getattr(app, 'revision', 1)}",
                             f'pkgdesc="{app.description}"',
-                            f"arch=('{self.tools.host_arch}')",
+                            f"arch=('{self.pkg_abi(app)}')",
                             f'url="{app.url}"',
                             f"license=('{app.license}')",
                             f"depends=({system_runtime_requires})",
@@ -1162,6 +1197,7 @@ with details about the release.
                     [
                         "makepkg",
                     ],
+                    env={"PKGEXT": ".pkg.tar.zst"},
                     check=True,
                     cwd=pkgbuild_path,
                 )
diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py
index fbd2c18f..c205efd5 100644
--- a/src/briefcase/platforms/windows/__init__.py
+++ b/src/briefcase/platforms/windows/__init__.py
@@ -8,7 +8,7 @@ from zipfile import ZIP_DEFLATED, ZipFile
 
 from briefcase.commands import CreateCommand, PackageCommand, RunCommand
 from briefcase.config import AppConfig, parsed_version
-from briefcase.exceptions import BriefcaseCommandError
+from briefcase.exceptions import BriefcaseCommandError, UnsupportedHostError
 from briefcase.integrations.windows_sdk import WindowsSDK
 from briefcase.integrations.wix import WiX
 
@@ -27,6 +27,23 @@ class WindowsMixin:
         suffix = "zip" if app.packaging_format == "zip" else "msi"
         return self.dist_path / f"{app.formal_name}-{app.version}.{suffix}"
 
+    def verify_host(self):
+        super().verify_host()
+        # the stub app only supports x86-64 right now
+        if self.tools.host_arch != "AMD64":
+            raise UnsupportedHostError(
+                f"Windows applications cannot be built on an {self.tools.host_arch} machine."
+            )
+        # 64bit Python is required to ensure 64bit wheels are installed/created for the app
+        if self.tools.is_32bit_python:
+            raise UnsupportedHostError(
+                """\
+Windows applications cannot be built using a 32bit version of Python.
+
+Install a 64bit version of Python and run Briefcase again.
+"""
+            )
+
 
 class WindowsCreateCommand(CreateCommand):
     def support_package_filename(self, support_revision):
@@ -34,8 +51,9 @@ class WindowsCreateCommand(CreateCommand):
 
     def support_package_url(self, support_revision):
         return (
-            f"https://www.python.org/ftp/python/{self.python_version_tag}.{support_revision}/"
-            + self.support_package_filename(support_revision)
+            f"https://www.python.org/ftp/python/"
+            f"{self.python_version_tag}.{support_revision}/"
+            f"{self.support_package_filename(support_revision)}"
         )
 
     def output_format_template_context(self, app: AppConfig):


Test patch:
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_properties.py b/tests/integrations/android_sdk/AndroidSDK/test_properties.py
index 249c077f..c37e6e74 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_properties.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_properties.py
@@ -9,17 +9,18 @@ from ..conftest import SDK_MGR_DL_VER, SDK_MGR_VER
 
 
 @pytest.mark.parametrize(
-    "host_os, name",
+    "host_os, host_arch, name",
     [
-        ("windows", "win"),
-        ("Windows", "win"),
-        ("darwin", "mac"),
-        ("Darwin", "mac"),
+        ("Darwin", "arm64", "mac"),
+        ("Darwin", "x86_64", "mac"),
+        ("Linux", "x86_64", "linux"),
+        ("Windows", "AMD64", "win"),
     ],
 )
-def test_cmdline_tools_url(mock_tools, android_sdk, host_os, name):
+def test_cmdline_tools_url(mock_tools, android_sdk, host_os, host_arch, name):
     """Validate that the SDK URL is computed using `host_os`."""
     mock_tools.host_os = host_os
+    mock_tools.host_arch = host_arch
 
     assert android_sdk.cmdline_tools_url == (
         f"https://dl.google.com/android/repository/commandlinetools-{name}-{SDK_MGR_DL_VER}_latest.zip"
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
index 11d6768b..84c1ccb4 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
@@ -7,6 +7,7 @@ import pytest
 
 from briefcase.exceptions import (
     BriefcaseCommandError,
+    IncompatibleToolError,
     MissingToolError,
     NetworkFailure,
     UnsupportedHostError,
@@ -85,6 +86,48 @@ def test_unsupported_os(mock_tools):
         AndroidSDK.verify(mock_tools)
 
 
+def test_unsupported_arch(mock_tools):
+    """When the architecture is not supported, an error is raised."""
+    mock_tools.host_arch = "IA-64"
+
+    with pytest.raises(
+        IncompatibleToolError,
+        match="Briefcase cannot install Android SDK on this machine.",
+    ):
+        AndroidSDK.verify(mock_tools)
+
+
+@pytest.mark.parametrize(
+    "host_os, host_arch",
+    [
+        ("Darwin", "arm64"),
+        ("Darwin", "x86_64"),
+        ("Linux", "x86_64"),
+        ("Windows", "AMD64"),
+    ],
+)
+def test_supported_os_arch(mock_tools, host_os, host_arch, tmp_path):
+    """The supported OS/arch combinations do not error."""
+    mock_tools.host_os = host_os
+    mock_tools.host_arch = host_arch
+
+    # Create `sdkmanager` and the license file.
+    android_sdk_root_path = tmp_path / "tools" / "android_sdk"
+    tools_bin = android_sdk_root_path / "cmdline-tools" / "9.0" / "bin"
+    tools_bin.mkdir(parents=True, mode=0o755)
+    if host_os == "Windows":
+        sdk_manager = tools_bin / "sdkmanager.bat"
+        sdk_manager.touch()
+    else:
+        sdk_manager = tools_bin / "sdkmanager"
+        sdk_manager.touch(mode=0o755)
+
+    # Pre-accept the license
+    accept_license(android_sdk_root_path)()
+
+    assert isinstance(AndroidSDK.verify(mock_tools), AndroidSDK)
+
+
 def test_succeeds_immediately_in_happy_path(mock_tools, tmp_path):
     """If verify is invoked on a path containing an Android SDK, it does nothing."""
     # If `sdkmanager` exists and has the right permissions, and `android-sdk-license`
diff --git a/tests/integrations/base/test_ToolCache.py b/tests/integrations/base/test_ToolCache.py
index 0d174bdc..ac6dddb6 100644
--- a/tests/integrations/base/test_ToolCache.py
+++ b/tests/integrations/base/test_ToolCache.py
@@ -165,3 +165,17 @@ def test_windows_home_path(home_path, expected_path, tmp_path):
         home_path=home_path,
     )
     assert tools.home_path == expected_path
+
+
+@pytest.mark.parametrize("maxsize, is_32bit", [(2**32, True), (2**64, False)])
+def test_is_32bit_python(maxsize, is_32bit, monkeypatch, tmp_path):
+    """Whether Python is 32bits is sensitive to `sys.maxsize`."""
+    monkeypatch.setattr(sys, "maxsize", maxsize)
+
+    tools = ToolCache(
+        logger=Log(),
+        console=Console(),
+        base_path=tmp_path,
+    )
+
+    assert tools.is_32bit_python is is_32bit
diff --git a/tests/integrations/java/test_JDK__verify.py b/tests/integrations/java/test_JDK__verify.py
index 42a41c33..b3817510 100644
--- a/tests/integrations/java/test_JDK__verify.py
+++ b/tests/integrations/java/test_JDK__verify.py
@@ -8,6 +8,7 @@ import pytest
 
 from briefcase.exceptions import (
     BriefcaseCommandError,
+    IncompatibleToolError,
     MissingToolError,
     NetworkFailure,
     UnsupportedHostError,
@@ -40,6 +41,17 @@ def test_unsupported_os(mock_tools):
         JDK.verify(mock_tools)
 
 
+def test_unsupported_arch(mock_tools):
+    """When the architecture is not supported, an error is raised."""
+    mock_tools.host_arch = "IA-64"
+
+    with pytest.raises(
+        IncompatibleToolError,
+        match="Briefcase cannot install Java JDK on this machine.",
+    ):
+        JDK.verify(mock_tools)
+
+
 def test_macos_tool_java_home(mock_tools, capsys):
     """On macOS, the /usr/libexec/java_home utility is checked."""
     # Mock being on macOS
@@ -388,7 +400,7 @@ def test_unparseable_javac_version(mock_tools, host_os, java_home, tmp_path, cap
 
 
 @pytest.mark.parametrize(
-    "host_os, host_arch, jdk_url, jhome",
+    "host_os, host_arch, jdk_url, jhome, is_32bit",
     [
         (
             "Darwin",
@@ -396,13 +408,15 @@ def test_unparseable_javac_version(mock_tools, host_os, java_home, tmp_path, cap
             "https://github.com/adoptium/temurin17-binaries/releases/download/"
             f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_x64_mac_hotspot_{JDK_RELEASE}_7.tar.gz",
             "java17/Contents/Home",
+            False,
         ),
         (
             "Darwin",
-            "aarch64",
+            "arm64",
             "https://github.com/adoptium/temurin17-binaries/releases/download/"
             f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_aarch64_mac_hotspot_{JDK_RELEASE}_7.tar.gz",
             "java17/Contents/Home",
+            False,
         ),
         (
             "Linux",
@@ -410,6 +424,7 @@ def test_unparseable_javac_version(mock_tools, host_os, java_home, tmp_path, cap
             "https://github.com/adoptium/temurin17-binaries/releases/download/"
             f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_x64_linux_hotspot_{JDK_RELEASE}_7.tar.gz",
             "java17",
+            False,
         ),
         (
             "Linux",
@@ -417,13 +432,31 @@ def test_unparseable_javac_version(mock_tools, host_os, java_home, tmp_path, cap
             "https://github.com/adoptium/temurin17-binaries/releases/download/"
             f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_aarch64_linux_hotspot_{JDK_RELEASE}_7.tar.gz",
             "java17",
+            False,
+        ),
+        (
+            "Linux",
+            "aarch64",
+            "https://github.com/adoptium/temurin17-binaries/releases/download/"
+            f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_arm_linux_hotspot_{JDK_RELEASE}_7.tar.gz",
+            "java17",
+            True,
+        ),
+        (
+            "Linux",
+            "armv7l",
+            "https://github.com/adoptium/temurin17-binaries/releases/download/"
+            f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_arm_linux_hotspot_{JDK_RELEASE}_7.tar.gz",
+            "java17",
+            False,
         ),
         (
             "Linux",
-            "armv6l",
+            "armv8l",
             "https://github.com/adoptium/temurin17-binaries/releases/download/"
             f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_arm_linux_hotspot_{JDK_RELEASE}_7.tar.gz",
             "java17",
+            False,
         ),
         (
             "Windows",
@@ -431,6 +464,7 @@ def test_unparseable_javac_version(mock_tools, host_os, java_home, tmp_path, cap
             "https://github.com/adoptium/temurin17-binaries/releases/download/"
             f"jdk-{JDK_RELEASE}+7/OpenJDK17U-jdk_x64_windows_hotspot_{JDK_RELEASE}_7.zip",
             "java17",
+            False,
         ),
     ],
 )
@@ -442,11 +476,13 @@ def test_successful_jdk_download(
     host_arch,
     jdk_url,
     jhome,
+    is_32bit,
 ):
     """If needed, a JDK can be downloaded."""
     # Mock host OS and arch
     mock_tools.host_os = host_os
     mock_tools.host_arch = host_arch
+    mock_tools.is_32bit_python = is_32bit
 
     # Mock a JAVA_HOME that won't exist
     # This is only needed to make macOS *not* run /usr/libexec/java_home
diff --git a/tests/integrations/linuxdeploy/conftest.py b/tests/integrations/linuxdeploy/conftest.py
index d95fbb5d..be9ee010 100644
--- a/tests/integrations/linuxdeploy/conftest.py
+++ b/tests/integrations/linuxdeploy/conftest.py
@@ -12,7 +12,7 @@ from briefcase.integrations.subprocess import Subprocess
 @pytest.fixture
 def mock_tools(tmp_path, mock_tools) -> ToolCache:
     mock_tools.host_os = "Linux"
-    mock_tools.host_arch = "wonky"
+    mock_tools.host_arch = "i686"
 
     # Mock default tools
     mock_tools.subprocess = MagicMock(spec_set=Subprocess)
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py b/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py
index 756be873..5199e77a 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py
@@ -11,7 +11,7 @@ from tests.integrations.linuxdeploy.utils import create_mock_appimage
 
 def test_patch_linuxdeploy_elf_header_unpatched(linuxdeploy, tmp_path):
     """If the linuxdeploy tool/plugin is not patched, patch it."""
-    appimage_path = tmp_path / "tools" / "linuxdeploy-wonky.AppImage"
+    appimage_path = tmp_path / "tools" / "linuxdeploy-i386.AppImage"
 
     # Mock an unpatched linuxdeploy AppImage
     pre_patch_header = create_mock_appimage(
@@ -32,7 +32,7 @@ def test_patch_linuxdeploy_elf_header_unpatched(linuxdeploy, tmp_path):
 
 def test_patch_linuxdeploy_elf_header_already_patched(linuxdeploy, tmp_path):
     """If linuxdeploy is already patched, don't patch it."""
-    appimage_path = tmp_path / "tools" / "linuxdeploy-wonky.AppImage"
+    appimage_path = tmp_path / "tools" / "linuxdeploy-i386.AppImage"
 
     # Mock a patched linuxdeploy AppImage
     pre_patch_header = create_mock_appimage(
@@ -53,7 +53,7 @@ def test_patch_linuxdeploy_elf_header_already_patched(linuxdeploy, tmp_path):
 
 def test_patch_linuxdeploy_elf_header_bad_appimage(linuxdeploy, tmp_path):
     """If linuxdeploy does not have a valid header, raise an error."""
-    appimage_path = tmp_path / "tools" / "linuxdeploy-wonky.AppImage"
+    appimage_path = tmp_path / "tools" / "linuxdeploy-i386.AppImage"
 
     # Mock a bad linuxdeploy AppImage
     create_mock_appimage(appimage_path=appimage_path, mock_appimage_kind="corrupt")
@@ -65,7 +65,7 @@ def test_patch_linuxdeploy_elf_header_bad_appimage(linuxdeploy, tmp_path):
 
 def test_patch_linuxdeploy_elf_header_empty_appimage(linuxdeploy, tmp_path):
     """If file is empty, raise an error."""
-    appimage_path = tmp_path / "tools" / "linuxdeploy-wonky.AppImage"
+    appimage_path = tmp_path / "tools" / "linuxdeploy-i386.AppImage"
 
     # Mock a bad linuxdeploy AppImage
     create_mock_appimage(appimage_path=appimage_path, mock_appimage_kind="empty")
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py b/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py
index 4548b541..ee406812 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py
@@ -6,7 +6,7 @@ from tests.integrations.linuxdeploy.utils import side_effect_create_mock_appimag
 
 def test_upgrade_exists(linuxdeploy, mock_tools, tmp_path):
     """If linuxdeploy already exists, upgrading deletes first."""
-    appimage_path = tmp_path / "tools" / "linuxdeploy-wonky.AppImage"
+    appimage_path = tmp_path / "tools" / "linuxdeploy-i386.AppImage"
 
     # Mock the existence of an install
     appimage_path.touch()
@@ -24,7 +24,7 @@ def test_upgrade_exists(linuxdeploy, mock_tools, tmp_path):
 
     # A download is invoked
     mock_tools.download.file.assert_called_with(
-        url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-wonky.AppImage",
+        url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-i386.AppImage",
         download_path=tmp_path / "tools",
         role="linuxdeploy",
     )
@@ -45,7 +45,7 @@ def test_upgrade_does_not_exist(linuxdeploy, mock_tools):
 def test_upgrade_linuxdeploy_download_failure(linuxdeploy, mock_tools, tmp_path):
     """If linuxdeploy doesn't exist, but a download failure occurs, an error is
     raised."""
-    appimage_path = tmp_path / "tools" / "linuxdeploy-wonky.AppImage"
+    appimage_path = tmp_path / "tools" / "linuxdeploy-i386.AppImage"
 
     # Mock the existence of an install
     appimage_path.touch()
@@ -61,7 +61,7 @@ def test_upgrade_linuxdeploy_download_failure(linuxdeploy, mock_tools, tmp_path)
 
     # A download was invoked
     mock_tools.download.file.assert_called_with(
-        url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-wonky.AppImage",
+        url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-i386.AppImage",
         download_path=tmp_path / "tools",
         role="linuxdeploy",
     )
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeployQtPlugin__properties.py b/tests/integrations/linuxdeploy/test_LinuxDeployQtPlugin__properties.py
index 13e2bce1..4f523e70 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeployQtPlugin__properties.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeployQtPlugin__properties.py
@@ -16,9 +16,24 @@ def test_file_path(mock_tools, linuxdeploy_qt_plugin):
     )
 
 
-def test_file_name(linuxdeploy_qt_plugin):
+@pytest.mark.parametrize(
+    "host_os, host_arch, linuxdeploy_arch",
+    [
+        ("Linux", "x86_64", "x86_64"),
+        ("Linux", "i686", "i386"),
+        ("Darwin", "x86_64", "x86_64"),
+    ],
+)
+def test_file_name(mock_tools, host_os, host_arch, linuxdeploy_arch):
     """Linuxdeploy Qt plugin filename is architecture dependent."""
-    assert linuxdeploy_qt_plugin.file_name == "linuxdeploy-plugin-qt-wonky.AppImage"
+    mock_tools.host_os = host_os
+    mock_tools.host_arch = host_arch
+
+    linuxdeploy_qt_plugin = LinuxDeployQtPlugin(mock_tools)
+    assert (
+        linuxdeploy_qt_plugin.file_name
+        == f"linuxdeploy-plugin-qt-{linuxdeploy_arch}.AppImage"
+    )
 
 
 def test_plugin_id(linuxdeploy_qt_plugin):
@@ -26,9 +41,22 @@ def test_plugin_id(linuxdeploy_qt_plugin):
     assert linuxdeploy_qt_plugin.plugin_id == "qt"
 
 
-def test_download_url(linuxdeploy_qt_plugin):
+@pytest.mark.parametrize(
+    "host_os, host_arch, linuxdeploy_arch",
+    [
+        ("Linux", "x86_64", "x86_64"),
+        ("Linux", "i686", "i386"),
+        ("Darwin", "x86_64", "x86_64"),
+    ],
+)
+def test_download_url(mock_tools, host_os, host_arch, linuxdeploy_arch):
     """Linuxdeploy Qt plugin download URL is architecture dependent."""
+    mock_tools.host_os = host_os
+    mock_tools.host_arch = host_arch
+
+    linuxdeploy_qt_plugin = LinuxDeployQtPlugin(mock_tools)
+
     assert linuxdeploy_qt_plugin.download_url == (
         "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/"
-        "releases/download/continuous/linuxdeploy-plugin-qt-wonky.AppImage"
+        f"releases/download/continuous/linuxdeploy-plugin-qt-{linuxdeploy_arch}.AppImage"
     )
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeploy__is_elf_file.py b/tests/integrations/linuxdeploy/test_LinuxDeploy__is_elf_file.py
index 2971f21c..eb238382 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeploy__is_elf_file.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeploy__is_elf_file.py
@@ -3,16 +3,14 @@ from tests.integrations.linuxdeploy.utils import create_mock_appimage
 
 def test_is_elf_header_positive_detection(linuxdeploy, tmp_path):
     """File with an ELF Header is identified as an ELF file."""
-    create_mock_appimage(
-        appimage_path=tmp_path / "tools" / "linuxdeploy-wonky.AppImage"
-    )
+    create_mock_appimage(appimage_path=tmp_path / "tools" / "linuxdeploy-i386.AppImage")
     assert linuxdeploy.is_elf_file() is True
 
 
 def test_is_elf_header_negative_detection(linuxdeploy, tmp_path):
     """File without an ELF Header is not identified as an ELF file."""
     create_mock_appimage(
-        appimage_path=tmp_path / "tools" / "linuxdeploy-wonky.AppImage",
+        appimage_path=tmp_path / "tools" / "linuxdeploy-i386.AppImage",
         mock_appimage_kind="corrupt",
     )
     assert linuxdeploy.is_elf_file() is False
@@ -21,7 +19,7 @@ def test_is_elf_header_negative_detection(linuxdeploy, tmp_path):
 def test_is_elf_header_empty_file(linuxdeploy, tmp_path):
     """Empty file is not identified as an ELF file without any errors."""
     create_mock_appimage(
-        appimage_path=tmp_path / "tools" / "linuxdeploy-wonky.AppImage",
+        appimage_path=tmp_path / "tools" / "linuxdeploy-i386.AppImage",
         mock_appimage_kind="empty",
     )
     assert linuxdeploy.is_elf_file() is False
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeploy__properties.py b/tests/integrations/linuxdeploy/test_LinuxDeploy__properties.py
index c5d17c8a..bfbe57bc 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeploy__properties.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeploy__properties.py
@@ -1,4 +1,11 @@
-from briefcase.integrations.linuxdeploy import LinuxDeployGtkPlugin, LinuxDeployQtPlugin
+import pytest
+
+from briefcase.exceptions import UnsupportedHostError
+from briefcase.integrations.linuxdeploy import (
+    LinuxDeploy,
+    LinuxDeployGtkPlugin,
+    LinuxDeployQtPlugin,
+)
 
 
 def test_managed_install(linuxdeploy):
@@ -11,16 +18,53 @@ def test_file_path(linuxdeploy, mock_tools):
     assert linuxdeploy.file_path == mock_tools.base_path
 
 
-def test_file_name(linuxdeploy):
+@pytest.mark.parametrize(
+    "host_os, host_arch, linuxdeploy_arch",
+    [
+        ("Linux", "x86_64", "x86_64"),
+        ("Linux", "i686", "i386"),
+        ("Darwin", "x86_64", "x86_64"),
+    ],
+)
+def test_file_name(mock_tools, host_os, host_arch, linuxdeploy_arch):
     """Linuxdeploy filename is architecture dependent."""
-    assert linuxdeploy.file_name == "linuxdeploy-wonky.AppImage"
+    mock_tools.host_os = host_os
+    mock_tools.host_arch = host_arch
+
+    linuxdeploy = LinuxDeploy(mock_tools)
+
+    assert linuxdeploy.file_name == f"linuxdeploy-{linuxdeploy_arch}.AppImage"
+
 
+def test_file_name_unsupported_arch(mock_tools):
+    """LinuxDeploy cannot be verified for an unsupported architecture."""
+    mock_tools.host_arch = "IA-64"
 
-def test_download_url(linuxdeploy):
+    with pytest.raises(
+        UnsupportedHostError,
+        match="Linux AppImages cannot be built on IA-64.",
+    ):
+        _ = LinuxDeploy(mock_tools).file_name
+
+
+@pytest.mark.parametrize(
+    "host_os, host_arch, linuxdeploy_arch",
+    [
+        ("Linux", "x86_64", "x86_64"),
+        ("Linux", "i686", "i386"),
+        ("Darwin", "x86_64", "x86_64"),
+    ],
+)
+def test_download_url(mock_tools, host_os, host_arch, linuxdeploy_arch):
     """Linuxdeploy download URL is architecture dependent."""
+    mock_tools.host_os = host_os
+    mock_tools.host_arch = host_arch
+
+    linuxdeploy = LinuxDeploy(mock_tools)
+
     assert linuxdeploy.download_url == (
         "https://github.com/linuxdeploy/linuxdeploy/"
-        "releases/download/continuous/linuxdeploy-wonky.AppImage"
+        f"releases/download/continuous/linuxdeploy-{linuxdeploy_arch}.AppImage"
     )
 
 
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py b/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py
index db17c3f8..f3de248d 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py
@@ -57,7 +57,7 @@ def test_qt_plugin(linuxdeploy, mock_tools, tmp_path):
         / "tools"
         / "linuxdeploy_plugins"
         / "qt"
-        / "linuxdeploy-plugin-qt-wonky.AppImage"
+        / "linuxdeploy-plugin-qt-i386.AppImage"
     )
 
     plugins = linuxdeploy.verify_plugins(["qt"], bundle_path=tmp_path / "bundle")
@@ -68,7 +68,7 @@ def test_qt_plugin(linuxdeploy, mock_tools, tmp_path):
     mock_tools.download.file.assert_called_with(
         url=(
             "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/"
-            "releases/download/continuous/linuxdeploy-plugin-qt-wonky.AppImage"
+            "releases/download/continuous/linuxdeploy-plugin-qt-i386.AppImage"
         ),
         download_path=tmp_path / "tools" / "linuxdeploy_plugins" / "qt",
         role="linuxdeploy Qt plugin",
@@ -84,12 +84,12 @@ def test_custom_url_plugin(linuxdeploy, mock_tools, tmp_path):
         / "tools"
         / "linuxdeploy_plugins"
         / "sometool"
-        / "f3355f8e631ffc1abbb7afd37b36315f7846182ca2276c481fb9a43a7f4d239f"
-        / "linuxdeploy-plugin-sometool-wonky.AppImage"
+        / "bbf1b5dc4c3d2069dc5b916f73e9d6f5ad24603576298509367878e393c6f8f5"
+        / "linuxdeploy-plugin-sometool-i386.AppImage"
     )
 
     plugins = linuxdeploy.verify_plugins(
-        ["https://example.com/path/to/linuxdeploy-plugin-sometool-wonky.AppImage"],
+        ["https://example.com/path/to/linuxdeploy-plugin-sometool-i386.AppImage"],
         bundle_path=tmp_path / "bundle",
     )
 
@@ -97,12 +97,12 @@ def test_custom_url_plugin(linuxdeploy, mock_tools, tmp_path):
     assert isinstance(plugins["sometool"], LinuxDeployURLPlugin)
 
     mock_tools.download.file.assert_called_with(
-        url="https://example.com/path/to/linuxdeploy-plugin-sometool-wonky.AppImage",
+        url="https://example.com/path/to/linuxdeploy-plugin-sometool-i386.AppImage",
         download_path=tmp_path
         / "tools"
         / "linuxdeploy_plugins"
         / "sometool"
-        / "f3355f8e631ffc1abbb7afd37b36315f7846182ca2276c481fb9a43a7f4d239f",
+        / "bbf1b5dc4c3d2069dc5b916f73e9d6f5ad24603576298509367878e393c6f8f5",
         role="user-provided linuxdeploy plugin from URL",
     )
 
@@ -115,9 +115,7 @@ def test_custom_local_file_plugin(linuxdeploy, mock_tools, tmp_path):
     """A Custom local file plugin can be verified."""
 
     # Create a local file
-    plugin_path = (
-        tmp_path / "path" / "to" / "linuxdeploy-plugin-sometool-wonky.AppImage"
-    )
+    plugin_path = tmp_path / "path" / "to" / "linuxdeploy-plugin-sometool-i386.AppImage"
     create_mock_appimage(plugin_path)
 
     plugins = linuxdeploy.verify_plugins(
@@ -131,7 +129,7 @@ def test_custom_local_file_plugin(linuxdeploy, mock_tools, tmp_path):
     # No download happened
     mock_tools.download.file.assert_not_called()
     # But a copy happened
-    assert (tmp_path / "bundle" / "linuxdeploy-plugin-sometool-wonky.AppImage").exists()
+    assert (tmp_path / "bundle" / "linuxdeploy-plugin-sometool-i386.AppImage").exists()
 
 
 @pytest.mark.parametrize(
@@ -214,7 +212,7 @@ def test_complex_plugin_config(linuxdeploy, mock_tools, tmp_path):
                 / "tools"
                 / "linuxdeploy_plugins"
                 / "qt"
-                / "linuxdeploy-plugin-qt-wonky.AppImage"
+                / "linuxdeploy-plugin-qt-i386.AppImage"
             )(url, download_path, role)
         elif "linuxdeploy_plugins/network" in str(download_path):
             return side_effect_create_mock_tool(
@@ -232,7 +230,7 @@ def test_complex_plugin_config(linuxdeploy, mock_tools, tmp_path):
 
     # Local file tool is a local file.
     local_plugin_path = (
-        tmp_path / "path" / "to" / "linuxdeploy-plugin-sometool-wonky.AppImage"
+        tmp_path / "path" / "to" / "linuxdeploy-plugin-sometool-i386.AppImage"
     )
     create_mock_appimage(local_plugin_path)
 
@@ -260,7 +258,7 @@ def test_complex_plugin_config(linuxdeploy, mock_tools, tmp_path):
     # Local file plugin is as expected
     assert isinstance(plugins["sometool"], LinuxDeployLocalFilePlugin)
     assert plugins["sometool"].env == {"QUALITY": "really nice"}
-    assert (tmp_path / "bundle" / "linuxdeploy-plugin-sometool-wonky.AppImage").exists()
+    assert (tmp_path / "bundle" / "linuxdeploy-plugin-sometool-i386.AppImage").exists()
 
     # URL plugin is as expected
     assert isinstance(plugins["network"], LinuxDeployURLPlugin)
diff --git a/tests/platforms/linux/appimage/test_build.py b/tests/platforms/linux/appimage/test_build.py
index 22151729..88096f16 100644
--- a/tests/platforms/linux/appimage/test_build.py
+++ b/tests/platforms/linux/appimage/test_build.py
@@ -56,7 +56,7 @@ def build_command(tmp_path, first_app_config):
         apps={"first": first_app_config},
     )
     command.tools.host_os = "Linux"
-    command.tools.host_arch = "wonky"
+    command.tools.host_arch = "x86_64"
     command.use_docker = False
     command._briefcase_toml[first_app_config] = {
         "paths": {
@@ -138,7 +138,7 @@ def test_verify_tools_download_failure(build_command):
 
     # The download was attempted
     build_command.tools.download.file.assert_called_with(
-        url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-wonky.AppImage",
+        url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage",
         download_path=build_command.tools.base_path,
         role="linuxdeploy",
     )
@@ -175,14 +175,14 @@ def test_build_appimage(build_command, first_app, debug_mode, tmp_path, sub_stre
         "LINUXDEPLOY_OUTPUT_VERSION": "0.0.1",
         "DISABLE_COPYRIGHT_FILES_DEPLOYMENT": "1",
         "APPIMAGE_EXTRACT_AND_RUN": "1",
-        "ARCH": "wonky",
+        "ARCH": "x86_64",
     }
     if debug_mode:
         expected_env["DEBUG"] = "1"
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(
-                tmp_path / "briefcase" / "tools" / "linuxdeploy-wonky.AppImage"
+                tmp_path / "briefcase" / "tools" / "linuxdeploy-x86_64.AppImage"
             ),
             "--appdir",
             os.fsdecode(app_dir),
@@ -212,7 +212,7 @@ def test_build_appimage(build_command, first_app, debug_mode, tmp_path, sub_stre
         / "first-app"
         / "linux"
         / "appimage"
-        / "First_App-0.0.1-wonky.AppImage",
+        / "First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -262,7 +262,7 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path, sub_stre
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(
-                tmp_path / "briefcase" / "tools" / "linuxdeploy-wonky.AppImage"
+                tmp_path / "briefcase" / "tools" / "linuxdeploy-x86_64.AppImage"
             ),
             "--appdir",
             os.fsdecode(app_dir),
@@ -288,7 +288,7 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path, sub_stre
             "LINUXDEPLOY_OUTPUT_VERSION": "0.0.1",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT": "1",
             "APPIMAGE_EXTRACT_AND_RUN": "1",
-            "ARCH": "wonky",
+            "ARCH": "x86_64",
         },
         cwd=os.fsdecode(
             tmp_path / "base_path" / "build" / "first-app" / "linux" / "appimage"
@@ -314,7 +314,7 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path, sub_stre
         / "first-app"
         / "linux"
         / "appimage"
-        / "First_App-0.0.1-wonky.AppImage",
+        / "First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -345,7 +345,7 @@ def test_build_failure(build_command, first_app, tmp_path, sub_stream_kw):
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(
-                tmp_path / "briefcase" / "tools" / "linuxdeploy-wonky.AppImage"
+                tmp_path / "briefcase" / "tools" / "linuxdeploy-x86_64.AppImage"
             ),
             "--appdir",
             os.fsdecode(app_dir),
@@ -366,7 +366,7 @@ def test_build_failure(build_command, first_app, tmp_path, sub_stream_kw):
             "LINUXDEPLOY_OUTPUT_VERSION": "0.0.1",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT": "1",
             "APPIMAGE_EXTRACT_AND_RUN": "1",
-            "ARCH": "wonky",
+            "ARCH": "x86_64",
         },
         cwd=os.fsdecode(
             tmp_path / "base_path" / "build" / "first-app" / "linux" / "appimage"
@@ -432,11 +432,11 @@ def test_build_appimage_in_docker(build_command, first_app, tmp_path, sub_stream
             "--env",
             "APPIMAGE_EXTRACT_AND_RUN=1",
             "--env",
-            "ARCH=wonky",
+            "ARCH=x86_64",
             "--workdir",
             "/app",
             f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
-            "/briefcase/tools/linuxdeploy-wonky.AppImage",
+            "/briefcase/tools/linuxdeploy-x86_64.AppImage",
             "--appdir",
             "/app/First App.AppDir",
             "--desktop-file",
@@ -461,7 +461,7 @@ def test_build_appimage_in_docker(build_command, first_app, tmp_path, sub_stream
         / "first-app"
         / "linux"
         / "appimage"
-        / "First_App-0.0.1-wonky.AppImage",
+        / "First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -557,11 +557,11 @@ def test_build_appimage_with_plugins_in_docker(
             "--env",
             "APPIMAGE_EXTRACT_AND_RUN=1",
             "--env",
-            "ARCH=wonky",
+            "ARCH=x86_64",
             "--workdir",
             "/app",
             f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
-            "/briefcase/tools/linuxdeploy-wonky.AppImage",
+            "/briefcase/tools/linuxdeploy-x86_64.AppImage",
             "--appdir",
             "/app/First App.AppDir",
             "--desktop-file",
@@ -601,7 +601,7 @@ def test_build_appimage_with_plugins_in_docker(
         / "first-app"
         / "linux"
         / "appimage"
-        / "First_App-0.0.1-wonky.AppImage",
+        / "First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
 
@@ -666,7 +666,7 @@ def test_build_appimage_with_support_package_update(
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(
-                tmp_path / "briefcase" / "tools" / "linuxdeploy-wonky.AppImage"
+                tmp_path / "briefcase" / "tools" / "linuxdeploy-x86_64.AppImage"
             ),
             "--appdir",
             os.fsdecode(app_dir),
@@ -687,7 +687,7 @@ def test_build_appimage_with_support_package_update(
             "LINUXDEPLOY_OUTPUT_VERSION": "0.0.1",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT": "1",
             "APPIMAGE_EXTRACT_AND_RUN": "1",
-            "ARCH": "wonky",
+            "ARCH": "x86_64",
         },
         cwd=os.fsdecode(
             tmp_path / "base_path" / "build" / "first-app" / "linux" / "appimage"
@@ -702,6 +702,6 @@ def test_build_appimage_with_support_package_update(
         / "first-app"
         / "linux"
         / "appimage"
-        / "First_App-0.0.1-wonky.AppImage",
+        / "First_App-0.0.1-x86_64.AppImage",
         0o755,
     )
diff --git a/tests/platforms/linux/appimage/test_create.py b/tests/platforms/linux/appimage/test_create.py
index 94d40b93..d008b9e6 100644
--- a/tests/platforms/linux/appimage/test_create.py
+++ b/tests/platforms/linux/appimage/test_create.py
@@ -9,12 +9,14 @@ from briefcase.platforms.linux.appimage import LinuxAppImageCreateCommand
 
 @pytest.fixture
 def create_command(first_app_config, tmp_path):
-    return LinuxAppImageCreateCommand(
+    command = LinuxAppImageCreateCommand(
         logger=Log(),
         console=Console(),
         base_path=tmp_path / "base_path",
         data_path=tmp_path / "briefcase",
     )
+    command.tools.host_arch = "x86_64"
+    return command
 
 
 def test_default_options(create_command):
@@ -82,15 +84,16 @@ def test_finalize_nodocker(create_command, first_app_config, capsys):
 
 
 @pytest.mark.parametrize(
-    "manylinux, tag, host_arch, is_user_mapped, context",
+    "manylinux, tag, host_os, host_arch, is_user_mapped, context",
     [
         # Fallback.
-        (None, None, "x86_64", False, {"use_non_root_user": True}),
-        # x86_64 architecture, all versions
+        (None, None, "Linux", "x86_64", False, {"use_non_root_user": True}),
+        # Linux on x86_64 architecture, all versions
         # Explicit tag
         (
             "manylinux1",
             "2023-03-05-271004f",
+            "Linux",
             "x86_64",
             True,
             {
@@ -103,6 +106,7 @@ def test_finalize_nodocker(create_command, first_app_config, capsys):
         (
             "manylinux2010",
             "latest",
+            "Linux",
             "x86_64",
             False,
             {
@@ -115,6 +119,7 @@ def test_finalize_nodocker(create_command, first_app_config, capsys):
         (
             "manylinux2014",
             None,
+            "Linux",
             "x86_64",
             True,
             {
@@ -126,6 +131,7 @@ def test_finalize_nodocker(create_command, first_app_config, capsys):
         (
             "manylinux_2_24",
             None,
+            "Linux",
             "x86_64",
             True,
             {
@@ -137,6 +143,7 @@ def test_finalize_nodocker(create_command, first_app_config, capsys):
         (
             "manylinux_2_28",
             None,
+            "Linux",
             "x86_64",
             False,
             {
@@ -145,14 +152,28 @@ def test_finalize_nodocker(create_command, first_app_config, capsys):
                 "use_non_root_user": True,
             },
         ),
-        # non x86 architecture
+        # Linux on i686 hardware
+        (
+            "manylinux_2_28",
+            None,
+            "Linux",
+            "i686",
+            False,
+            {
+                "manylinux_image": "manylinux_2_28_i686:latest",
+                "vendor_base": "almalinux",
+                "use_non_root_user": True,
+            },
+        ),
+        # macOS on x86_64
         (
             "manylinux2014",
             None,
-            "aarch64",
+            "Darwin",
+            "x86_64",
             True,
             {
-                "manylinux_image": "manylinux2014_aarch64:latest",
+                "manylinux_image": "manylinux2014_x86_64:latest",
                 "vendor_base": "centos",
                 "use_non_root_user": False,
             },
@@ -164,6 +185,7 @@ def test_output_format_template_context(
     first_app_config,
     manylinux,
     tag,
+    host_os,
     host_arch,
     is_user_mapped,
     context,
@@ -178,6 +200,7 @@ def test_output_format_template_context(
     if tag:
         first_app_config.manylinux_image_tag = tag
 
+    create_command.tools.host_os = host_os
     create_command.tools.host_arch = host_arch
 
     assert create_command.output_format_template_context(first_app_config) == context
diff --git a/tests/platforms/linux/appimage/test_mixin.py b/tests/platforms/linux/appimage/test_mixin.py
index ec491486..73f23354 100644
--- a/tests/platforms/linux/appimage/test_mixin.py
+++ b/tests/platforms/linux/appimage/test_mixin.py
@@ -6,6 +6,7 @@ import pytest
 import briefcase.platforms.linux.appimage
 from briefcase.console import Console, Log
 from briefcase.integrations.docker import Docker, DockerAppContext
+from briefcase.integrations.linuxdeploy import LinuxDeploy
 from briefcase.integrations.subprocess import Subprocess
 from briefcase.platforms.linux.appimage import (
     LinuxAppImageBuildCommand,
@@ -15,12 +16,15 @@ from briefcase.platforms.linux.appimage import (
 
 @pytest.fixture
 def create_command(tmp_path):
-    return LinuxAppImageCreateCommand(
+    command = LinuxAppImageCreateCommand(
         logger=Log(),
         console=Console(),
         base_path=tmp_path / "base_path",
         data_path=tmp_path / "briefcase",
     )
+    # Mock verified linuxdeploy
+    command.tools.linuxdeploy = LinuxDeploy(tools=command.tools)
+    return command
 
 
 def test_binary_path(create_command, first_app_config, tmp_path):
diff --git a/tests/platforms/linux/appimage/test_package.py b/tests/platforms/linux/appimage/test_package.py
index b46af82d..f837d0c8 100644
--- a/tests/platforms/linux/appimage/test_package.py
+++ b/tests/platforms/linux/appimage/test_package.py
@@ -15,7 +15,7 @@ def package_command(tmp_path, first_app_config):
         data_path=tmp_path / "briefcase",
     )
     # Mock the host architecture to something repeatable
-    command.tools.host_arch = "wonky"
+    command.tools.host_arch = "x86_64"
 
     # Ensure the dist folder exists
     (tmp_path / "base_path" / "dist").mkdir(parents=True)
@@ -34,7 +34,7 @@ def test_package_app(package_command, first_app_config, tmp_path):
         / "first-app"
         / "linux"
         / "appimage"
-        / "First_App-0.0.1-wonky.AppImage",
+        / "First_App-0.0.1-x86_64.AppImage",
         "AppImage",
     )
 
@@ -42,4 +42,6 @@ def test_package_app(package_command, first_app_config, tmp_path):
     package_command.package_app(first_app_config)
 
     # The binary has been copied to the dist folder
-    assert (tmp_path / "base_path" / "dist" / "First_App-0.0.1-wonky.AppImage").exists()
+    assert (
+        tmp_path / "base_path" / "dist" / "First_App-0.0.1-x86_64.AppImage"
+    ).exists()
diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py
index 83480be3..c2737537 100644
--- a/tests/platforms/linux/appimage/test_run.py
+++ b/tests/platforms/linux/appimage/test_run.py
@@ -21,7 +21,7 @@ def run_command(tmp_path):
     command.tools.home_path = tmp_path / "home"
 
     # Set the host architecture for test purposes.
-    command.tools.host_arch = "wonky"
+    command.tools.host_arch = "x86_64"
 
     command.tools.subprocess = mock.MagicMock(spec_set=Subprocess)
 
@@ -63,7 +63,7 @@ def test_run_app(run_command, first_app_config, tmp_path):
                 / "first-app"
                 / "linux"
                 / "appimage"
-                / "First_App-0.0.1-wonky.AppImage"
+                / "First_App-0.0.1-x86_64.AppImage"
             )
         ],
         cwd=tmp_path / "home",
@@ -104,7 +104,7 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path):
                 / "first-app"
                 / "linux"
                 / "appimage"
-                / "First_App-0.0.1-wonky.AppImage"
+                / "First_App-0.0.1-x86_64.AppImage"
             ),
             "foo",
             "--bar",
@@ -141,7 +141,7 @@ def test_run_app_failed(run_command, first_app_config, tmp_path):
                 / "first-app"
                 / "linux"
                 / "appimage"
-                / "First_App-0.0.1-wonky.AppImage"
+                / "First_App-0.0.1-x86_64.AppImage"
             )
         ],
         cwd=tmp_path / "home",
@@ -173,7 +173,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
                 / "first-app"
                 / "linux"
                 / "appimage"
-                / "First_App-0.0.1-wonky.AppImage"
+                / "First_App-0.0.1-x86_64.AppImage"
             )
         ],
         cwd=tmp_path / "home",
@@ -215,7 +215,7 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path):
                 / "first-app"
                 / "linux"
                 / "appimage"
-                / "First_App-0.0.1-wonky.AppImage"
+                / "First_App-0.0.1-x86_64.AppImage"
             ),
             "foo",
             "--bar",
diff --git a/tests/platforms/linux/system/test_mixin__properties.py b/tests/platforms/linux/system/test_mixin__properties.py
index 35f40ee7..fae88c34 100644
--- a/tests/platforms/linux/system/test_mixin__properties.py
+++ b/tests/platforms/linux/system/test_mixin__properties.py
@@ -1,3 +1,5 @@
+from unittest.mock import MagicMock
+
 import pytest
 
 from briefcase.console import Console, Log
@@ -5,20 +7,6 @@ from briefcase.exceptions import BriefcaseCommandError
 from briefcase.platforms.linux.system import LinuxSystemBuildCommand
 
 
-@pytest.mark.parametrize(
-    "host_arch, linux_arch",
-    [
-        ("x86_64", "amd64"),
-        ("aarch64", "arm64"),
-        ("armv6l", "armhf"),
-    ],
-)
-def test_linux_arch(create_command, host_arch, linux_arch):
-    """Host architectures are transformed to Linux-accepted values."""
-    create_command.tools.host_arch = host_arch
-    assert create_command.linux_arch == linux_arch
-
-
 @pytest.mark.parametrize(
     "vendor, codename",
     [
@@ -93,14 +81,22 @@ def test_binary_path(create_command, first_app_config, tmp_path):
 
 @pytest.mark.parametrize(
     "packaging_format, filename",
-    [("deb", "first-app_0.0.1-1~somevendor-surprising_amd64.deb")],
+    [
+        ("deb", "first-app_0.0.1-1~somevendor-surprising_wonky.deb"),
+        ("rpm", "first-app-0.0.1-1.elsurprising.wonky.rpm"),
+        ("pkg", "first-app-0.0.1-1-wonky.pkg.tar.zst"),
+    ],
 )
 def test_distribution_path(
-    create_command, first_app_config, packaging_format, filename, tmp_path
+    create_command,
+    first_app_config,
+    packaging_format,
+    filename,
+    tmp_path,
 ):
     """The distribution path contains vendor details."""
-    # Force the architecture to x86_64 for test purposes.
-    create_command.tools.host_arch = "x86_64"
+    # Mock return value for ABI from packaging system
+    create_command._build_env_abi = MagicMock(return_value="wonky")
 
     # Force a dummy vendor:codename for test purposes.
     first_app_config.target_vendor = "somevendor"
diff --git a/tests/platforms/linux/system/test_package.py b/tests/platforms/linux/system/test_package.py
index 1af710bb..6c45635c 100644
--- a/tests/platforms/linux/system/test_package.py
+++ b/tests/platforms/linux/system/test_package.py
@@ -1,9 +1,11 @@
+import subprocess
 from unittest import mock
 
 import pytest
 
 from briefcase.console import Console, Log
 from briefcase.exceptions import BriefcaseCommandError
+from briefcase.integrations.subprocess import Subprocess
 from briefcase.platforms.linux.system import LinuxSystemPackageCommand
 
 
@@ -17,9 +19,6 @@ def package_command(monkeypatch, first_app, tmp_path):
     )
     command.tools.home_path = tmp_path / "home"
 
-    # Set the host architecture for test purposes.
-    command.tools.host_arch = "wonky"
-
     # Run outside docker for these tests.
     command.target_image = None
 
@@ -36,7 +35,7 @@ def package_command(monkeypatch, first_app, tmp_path):
 
 
 def test_formats(package_command):
-    "The supported packaging formats are as expected."
+    """The supported packaging formats are as expected."""
     assert package_command.packaging_formats == ["deb", "rpm", "pkg", "system"]
 
 
@@ -67,12 +66,25 @@ def test_formats(package_command):
     ],
 )
 def test_distribution_path(
-    package_command, first_app, format, vendor, codename, revision, filename, tmp_path
+    package_command,
+    first_app,
+    format,
+    vendor,
+    codename,
+    revision,
+    filename,
+    tmp_path,
 ):
     first_app.packaging_format = format
     first_app.target_vendor = vendor
     first_app.target_codename = codename
 
+    # Mock return value for ABI from packaging system
+    package_command.tools[first_app].app_context = mock.MagicMock(spec_set=Subprocess)
+    package_command.tools[first_app].app_context.check_output = mock.MagicMock(
+        return_value="wonky"
+    )
+
     if revision:
         first_app.revision = revision
 
@@ -81,6 +93,33 @@ def test_distribution_path(
         == tmp_path / "base_path" / "dist" / filename
     )
 
+    # Confirm ABI was requested from build env
+    package_command.tools[first_app].app_context.check_output.assert_called_with(
+        {
+            "deb": ["dpkg", "--print-architecture"],
+            "rpm": ["rpm", "--eval", "%_target_cpu"],
+            "pkg": ["pacman-conf", "Architecture"],
+        }[format]
+    )
+
+
+@pytest.mark.parametrize("format", ["rpm", "deb", "pkg"])
+def test_build_env_abi_failure(package_command, first_app, format):
+    """If the subprocess to get the build ABI fails, an error is raised."""
+    first_app.packaging_format = format
+
+    # Mock return value for ABI from packaging system
+    package_command.tools[first_app].app_context = mock.MagicMock(spec_set=Subprocess)
+    package_command.tools[first_app].app_context.check_output = mock.MagicMock(
+        side_effect=subprocess.CalledProcessError(returncode=1, cmd="pkg -arch")
+    )
+
+    with pytest.raises(
+        BriefcaseCommandError,
+        match="Failed to determine build environment's ABI for packaging.",
+    ):
+        getattr(package_command, f"{format}_abi")(first_app)
+
 
 @pytest.mark.parametrize(
     "base_vendor, input_format, output_format",
@@ -102,9 +141,13 @@ def test_distribution_path(
     ],
 )
 def test_adjust_packaging_format(
-    package_command, first_app, base_vendor, input_format, output_format
+    package_command,
+    first_app,
+    base_vendor,
+    input_format,
+    output_format,
 ):
-    "The packaging format can be adjusted based on host system knowledge"
+    """The packaging format can be adjusted based on host system knowledge."""
     first_app.target_vendor_base = base_vendor
     first_app.packaging_format = input_format
 
@@ -171,7 +214,7 @@ def test_package_pkg_app(package_command, first_app):
 
 
 def test_package_unknown_format(package_command, first_app):
-    "Unknown/unsupported packaging formats raise an error"
+    """Unknown/unsupported packaging formats raise an error."""
     # Set the packaging format
     first_app.packaging_format = "unknown"
 
diff --git a/tests/platforms/linux/system/test_package__deb.py b/tests/platforms/linux/system/test_package__deb.py
index 6d32d4d5..a12aef3e 100644
--- a/tests/platforms/linux/system/test_package__deb.py
+++ b/tests/platforms/linux/system/test_package__deb.py
@@ -27,8 +27,8 @@ def package_command(first_app, tmp_path):
     )
     command.tools.home_path = tmp_path / "home"
 
-    # Set the host architecture for test purposes.
-    command.tools.host_arch = "wonky"
+    # Mock ABI from packaging system
+    command._deb_abi = "wonky"
 
     # Mock the app context
     command.tools.app_tools[first_app].app_context = mock.MagicMock()
@@ -121,7 +121,7 @@ def test_verify_docker(monkeypatch, package_command, first_app_deb):
     dpkg_deb.exists.assert_not_called()
 
 
-@pytest.mark.skipif(sys.platform == "win32", reason="Can't build PKGs on Windows")
+@pytest.mark.skipif(sys.platform == "win32", reason="Can't build debs on Windows")
 def test_deb_package(package_command, first_app_deb, tmp_path):
     """A deb app can be packaged."""
     bundle_path = (
@@ -340,7 +340,7 @@ def test_deb_package_extra_requirements(package_command, first_app_deb, tmp_path
 
 
 def test_deb_package_failure(package_command, first_app_deb, tmp_path):
-    """If an packaging doesn't succeed, an error is raised."""
+    """If a packaging doesn't succeed, an error is raised."""
     bundle_path = (
         tmp_path / "base_path" / "build" / "first-app" / "somevendor" / "surprising"
     )
diff --git a/tests/platforms/linux/system/test_package__pkg.py b/tests/platforms/linux/system/test_package__pkg.py
index 9954754f..22a8904b 100644
--- a/tests/platforms/linux/system/test_package__pkg.py
+++ b/tests/platforms/linux/system/test_package__pkg.py
@@ -25,8 +25,8 @@ def package_command(first_app, tmp_path):
     )
     command.tools.home_path = tmp_path / "home"
 
-    # Set the host architecture for test purposes.
-    command.tools.host_arch = "wonky"
+    # Mock ABI from packaging system
+    command._pkg_abi = "wonky"
 
     # Mock the app context
     command.tools.app_tools[first_app].app_context = mock.MagicMock()
@@ -220,6 +220,7 @@ def test_pkg_package(package_command, first_app_pkg, tmp_path):
         ],
         check=True,
         cwd=(bundle_path / "pkgbuild"),
+        env={"PKGEXT": ".pkg.tar.zst"},
     )
 
     # The pkg was moved into the final location
@@ -317,6 +318,7 @@ def test_pkg_re_package(package_command, first_app_pkg, tmp_path):
         ],
         check=True,
         cwd=(bundle_path / "pkgbuild"),
+        env={"PKGEXT": ".pkg.tar.zst"},
     )
 
     # The pkg was moved into the final location
@@ -405,6 +407,7 @@ def test_pkg_package_extra_requirements(package_command, first_app_pkg, tmp_path
         ],
         check=True,
         cwd=(bundle_path / "pkgbuild"),
+        env={"PKGEXT": ".pkg.tar.zst"},
     )
 
     # The pkg was moved into the final location
@@ -454,6 +457,7 @@ def test_pkg_package_failure(package_command, first_app_pkg, tmp_path):
         ],
         check=True,
         cwd=(bundle_path / "pkgbuild"),
+        env={"PKGEXT": ".pkg.tar.zst"},
     )
 
     # The pkg wasn't built, so it wasn't moved.
@@ -462,7 +466,7 @@ def test_pkg_package_failure(package_command, first_app_pkg, tmp_path):
 
 @pytest.mark.skipif(sys.platform == "win32", reason="Can't build PKGs on Windows")
 def test_no_changelog(package_command, first_app_pkg, tmp_path):
-    """If an packaging doesn't succeed, an error is raised."""
+    """If a packaging doesn't succeed, an error is raised."""
     bundle_path = (
         tmp_path / "base_path" / "build" / "first-app" / "somevendor" / "surprising"
     )
diff --git a/tests/platforms/linux/system/test_package__rpm.py b/tests/platforms/linux/system/test_package__rpm.py
index 3a580d8a..cf7f1ea5 100644
--- a/tests/platforms/linux/system/test_package__rpm.py
+++ b/tests/platforms/linux/system/test_package__rpm.py
@@ -25,8 +25,8 @@ def package_command(first_app, tmp_path):
     )
     command.tools.home_path = tmp_path / "home"
 
-    # Set the host architecture for test purposes.
-    command.tools.host_arch = "wonky"
+    # Mock ABI from packaging system
+    command._rpm_abi = "wonky"
 
     # Mock the app context
     command.tools.app_tools[first_app].app_context = mock.MagicMock()
diff --git a/tests/platforms/linux/test_DockerOpenCommand.py b/tests/platforms/linux/test_DockerOpenCommand.py
index a3fae7f3..11cbb775 100644
--- a/tests/platforms/linux/test_DockerOpenCommand.py
+++ b/tests/platforms/linux/test_DockerOpenCommand.py
@@ -22,10 +22,19 @@ def open_command(tmp_path):
     )
     command.tools.os = MagicMock(spec_set=os)
 
+    # Mock x86_64 for linuxdeploy verification
+    command.tools.host_arch = "x86_64"
+
     # Store the underlying subprocess instance
     command._subprocess = MagicMock(spec_set=Subprocess)
     command.tools.subprocess = command._subprocess
 
+    # Mock existence of linuxdeploy in tools
+    create_file(
+        command.tools.base_path / "linuxdeploy-x86_64.AppImage",
+        content="",
+    )
+
     return command
 
 
@@ -121,7 +130,7 @@ def test_open_no_docker_linux(open_command, first_app_config, tmp_path):
 
 @pytest.mark.skipif(sys.platform != "darwin", reason="macOS specific test")
 def test_open_no_docker_macOS(open_command, first_app_config, tmp_path):
-    """On macOS, Open runs `open` on the project folder if we specify --no- docker."""
+    """On macOS, Open runs `open` on the project folder if we specify --no-docker."""
     # Create the desktop file that would be in the project folder.
     create_file(
         open_command.project_path(first_app_config)
diff --git a/tests/platforms/linux/test_LinuxMixin__support_package_url.py b/tests/platforms/linux/test_LinuxMixin__support_package_url.py
index 351c2999..93b64b19 100644
--- a/tests/platforms/linux/test_LinuxMixin__support_package_url.py
+++ b/tests/platforms/linux/test_LinuxMixin__support_package_url.py
@@ -2,34 +2,63 @@ import pytest
 
 
 @pytest.mark.parametrize(
-    "support_revision, host_arch, url",
+    "support_revision, host_arch, is_32bit_python, url",
     [
         (
             "3.10.9+20230116",
             "x86_64",
+            False,
             "20230116/cpython-3.10.9+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz",
         ),
         (
             "3.11.1+20230116",
             "aarch64",
+            False,
             "20230116/cpython-3.11.1+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz",
         ),
+        (
+            "3.11.1+20230116",
+            "aarch64",
+            True,
+            "20230116/cpython-3.11.1+20230116-armv7-unknown-linux-gnu-install_only.tar.gz",
+        ),
+        (
+            "3.11.1+20230116",
+            "armv7l",
+            True,
+            "20230116/cpython-3.11.1+20230116-armv7l-unknown-linux-gnu-install_only.tar.gz",
+        ),
         (
             "3.8.16+20221220",
             "x86_64",
+            False,
             "20221220/cpython-3.8.16+20221220-x86_64-unknown-linux-gnu-install_only.tar.gz",
         ),
+        (
+            "3.8.16+20221220",
+            "x86_64",
+            True,
+            "20221220/cpython-3.8.16+20221220-i686-unknown-linux-gnu-install_only.tar.gz",
+        ),
+        (
+            "3.8.16+20221220",
+            "i686",
+            True,
+            "20221220/cpython-3.8.16+20221220-i686-unknown-linux-gnu-install_only.tar.gz",
+        ),
     ],
 )
 def test_support_package_url(
     linux_mixin,
     host_arch,
     support_revision,
+    is_32bit_python,
     url,
 ):
     """The support package URL is customized."""
     # Set up the host architecture
     linux_mixin.tools.host_arch = host_arch
+    linux_mixin.tools.is_32bit_python = is_32bit_python
 
     assert linux_mixin.support_package_url(support_revision) == (
         "https://github.com/indygreg/python-build-standalone/releases/download/" + url
diff --git a/tests/platforms/windows/app/test_build.py b/tests/platforms/windows/app/test_build.py
index 2d6ac253..b096b3b8 100644
--- a/tests/platforms/windows/app/test_build.py
+++ b/tests/platforms/windows/app/test_build.py
@@ -27,6 +27,7 @@ def build_command(tmp_path):
         data_path=tmp_path / "briefcase",
     )
     command.tools.host_os = "Windows"
+    command.tools.host_arch = "AMD64"
     command.tools.subprocess = mock.MagicMock(spec_set=Subprocess)
     command.tools.shutil = mock.MagicMock(spec_set=shutil)
     command.tools.download = mock.MagicMock()
diff --git a/tests/platforms/windows/app/test_create.py b/tests/platforms/windows/app/test_create.py
index ded0a154..bb29009e 100644
--- a/tests/platforms/windows/app/test_create.py
+++ b/tests/platforms/windows/app/test_create.py
@@ -29,6 +29,40 @@ def test_unsupported_host_os(create_command, host_os):
         create_command()
 
 
+@pytest.mark.parametrize("host_arch", ["i686", "ARM64", "wonky"])
+def test_unsupported_arch(create_command, host_arch):
+    """Windows commands can only run on x86-64."""
+    create_command.tools.host_os = "Windows"
+    create_command.tools.host_arch = host_arch
+
+    with pytest.raises(
+        UnsupportedHostError,
+        match=f"Windows applications cannot be built on an {host_arch} machine.",
+    ):
+        create_command()
+
+
+def test_supported_arch(create_command):
+    """Windows command are allowed to run on x86-64."""
+    create_command.tools.host_os = "Windows"
+    create_command.tools.host_arch = "AMD64"
+
+    create_command()
+
+
+def test_unsupported_32bit_python(create_command):
+    """Windows commands cannot run with 32bit Python."""
+    create_command.tools.host_os = "Windows"
+    create_command.tools.host_arch = "AMD64"
+    create_command.tools.is_32bit_python = True
+
+    with pytest.raises(
+        UnsupportedHostError,
+        match="Windows applications cannot be built using a 32bit version of Python",
+    ):
+        create_command()
+
+
 @pytest.mark.parametrize(
     "version, version_triple",
     [