diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 263dd3fa4a..3f851dc384 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -235,7 +235,7 @@ jobs:
fail-fast: false
matrix:
framework: [ "toga", "pyside6", "pygame", "console" ]
- runner-os: [ "macos-15-intel", "macos-latest", "ubuntu-24.04", "ubuntu-24.04-arm", "windows-latest" ]
+ runner-os: [ "macos-15-intel", "macos-latest", "ubuntu-24.04", "ubuntu-24.04-arm", "windows-latest", "windows-11-arm"]
verify-apps:
name: Build app
@@ -250,4 +250,4 @@ jobs:
fail-fast: false
matrix:
framework: [ "toga", "pyside6", "pygame", "console" ]
- runner-os: [ "macos-15-intel", "macos-15", "ubuntu-24.04", "ubuntu-24.04-arm", "windows-latest" ]
+ runner-os: [ "macos-15-intel", "macos-15", "ubuntu-24.04", "ubuntu-24.04-arm", "windows-latest", "windows-11-arm" ]
diff --git a/changes/1887.feature.md b/changes/1887.feature.md
new file mode 100644
index 0000000000..76117f8715
--- /dev/null
+++ b/changes/1887.feature.md
@@ -0,0 +1 @@
+Briefcase can now build Windows apps for ARM64 devices.
diff --git a/docs/en/reference/platforms/index.md b/docs/en/reference/platforms/index.md
index b3aa540f32..439ad98711 100644
--- a/docs/en/reference/platforms/index.md
+++ b/docs/en/reference/platforms/index.md
@@ -153,7 +153,7 @@
|
|
{{ ci_tested }} |
- |
+{{ ci_tested }} |
|
|
|
@@ -165,7 +165,7 @@
|
|
{{ ci_tested }} |
- |
+{{ ci_tested }} |
|
|
|
diff --git a/docs/en/reference/platforms/windows/index.md b/docs/en/reference/platforms/windows/index.md
index 8c3e0f3abc..aac47285eb 100644
--- a/docs/en/reference/platforms/windows/index.md
+++ b/docs/en/reference/platforms/windows/index.md
@@ -41,7 +41,7 @@
|
|
{{ ci_tested }} |
- |
+{{ ci_tested }} |
|
|
|
diff --git a/docs/en/reference/platforms/windows/visualstudio.md b/docs/en/reference/platforms/windows/visualstudio.md
index 2d92bf0ba6..29cf43c432 100644
--- a/docs/en/reference/platforms/windows/visualstudio.md
+++ b/docs/en/reference/platforms/windows/visualstudio.md
@@ -60,7 +60,7 @@ All Windows apps, regardless of output format, use the same icon formats, have t
## Pre-requisites
-Building the Visual Studio project requires that you install Visual Studio 2022 or later. Visual Studio 2022 Community Edition [can be downloaded for free from Microsoft](https://visualstudio.microsoft.com/vs/community/). You can also use the Professional or Enterprise versions if you have them.
+Building the Visual Studio project requires that you install Visual Studio. Visual Studio Community Edition [can be downloaded for free from Microsoft](https://visualstudio.microsoft.com/vs/community/). You can also use the Professional or Enterprise versions if you have them.
Briefcase will auto-detect the location of your Visual Studio installation, provided one of the following three things are true:
@@ -75,6 +75,7 @@ When you install Visual Studio, there are many optional components. You should e
- Desktop Development with C++
- All default packages
- C++/CLI support for v143 build tools
+ - MSVC v143 VS 2022 C++ ARM64/x64 build tools
## Application configuration
diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist
index 8935009eaf..3c4387b701 100644
--- a/docs/spelling_wordlist
+++ b/docs/spelling_wordlist
@@ -237,3 +237,4 @@ Xauth
Xcode
Xr
Xs
+MSVC
diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py
index 937bbf8f04..d0e9244749 100644
--- a/src/briefcase/commands/create.py
+++ b/src/briefcase/commands/create.py
@@ -123,7 +123,15 @@ def support_package_url(self, support_revision: str) -> str:
def stub_binary_filename(self, support_revision: str, is_console_app: bool) -> str:
"""The filename for the stub binary."""
stub_type = "Console" if is_console_app else "GUI"
- return f"{stub_type}-Stub-{self.python_version_tag}-b{support_revision}.zip"
+ win_suffix = (
+ f"-{self.tools.host_arch.lower()}"
+ if self.tools.host_os == "Windows"
+ else ""
+ )
+ return (
+ f"{stub_type}-Stub-{self.python_version_tag}-b{support_revision}"
+ f"{win_suffix}.zip"
+ )
def stub_binary_url(self, support_revision: str, is_console_app: bool) -> str:
"""The URL of the stub binary to use for apps of this type."""
diff --git a/src/briefcase/integrations/base.py b/src/briefcase/integrations/base.py
index 2b412bf586..753ff9f817 100644
--- a/src/briefcase/integrations/base.py
+++ b/src/briefcase/integrations/base.py
@@ -195,8 +195,8 @@ def __init__(
self.base_path = Path(base_path)
self.home_path = Path(os.path.expanduser(home_path or Path.home()))
- self.host_arch = self.platform.machine()
self.host_os = self.platform.system()
+ self.host_arch = self._get_host_arch()
# Python is 32bit if its pointers can only address with 32 bits or fewer
self.is_32bit_python = self.sys.maxsize <= 2**32
@@ -208,6 +208,36 @@ def __init__(
)
)
+ def _get_host_arch(self) -> str:
+ arch = self.platform.machine()
+ # On Windows with Python < 3.12, ``platform.machine()`` returns the
+ # emulated architecture (e.g. "AMD64") when an x86-64 Python interpreter
+ # is running under ARM64. ``IsWow64Process2()`` exposes the
+ # native machine type.
+ if (
+ arch == "AMD64"
+ and self.host_os == "Windows"
+ and self.sys.version_info < (3, 12)
+ and self.sys.getwindowsversion().build >= 16299 # Windows 10 1709
+ ):
+ import ctypes
+ from ctypes import wintypes
+
+ IMAGE_FILE_MACHINE_ARM64 = 0xAA64
+ kernel32 = ctypes.windll.kernel32
+ process_machine = ctypes.c_ushort(0)
+ native_machine = ctypes.c_ushort(0)
+ if (
+ kernel32.IsWow64Process2(
+ wintypes.HANDLE(kernel32.GetCurrentProcess()),
+ ctypes.byref(process_machine),
+ ctypes.byref(native_machine),
+ )
+ and native_machine.value == IMAGE_FILE_MACHINE_ARM64
+ ):
+ return "ARM64"
+ return arch
+
@cached_property
def system_encoding(self) -> str:
"""The character encoding for the system's locale.
diff --git a/src/briefcase/integrations/visualstudio.py b/src/briefcase/integrations/visualstudio.py
index ace35185a4..e42d13f961 100644
--- a/src/briefcase/integrations/visualstudio.py
+++ b/src/briefcase/integrations/visualstudio.py
@@ -18,7 +18,7 @@ class VisualStudio(Tool):
- Default packages
* Desktop Development with C++
- Default packages; plus
- - C++/CLI support for v143 build tools
+ - MSVC v143 VS 2022 C++ ARM64/x64 build tools
"""
def __init__(
diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py
index 9ae3cd5ff0..ffa6370051 100644
--- a/src/briefcase/platforms/windows/__init__.py
+++ b/src/briefcase/platforms/windows/__init__.py
@@ -88,12 +88,24 @@ def distribution_path(self, app):
suffix = "zip" if app.packaging_format == "zip" else "msi"
return self.dist_path / f"{app.formal_name}-{app.version}.{suffix}"
+ @property
+ def vscode_platform(self):
+ return "ARM64" if self.tools.host_arch == "ARM64" else "x64"
+
def verify_host(self):
super().verify_host()
- # The stub app only supports x86-64 right now, and our VisualStudio and WiX code
- # is the same (#1887). However, we can package an external x86-64 app on any
- # build machine.
- if self.tools.host_arch != "AMD64":
+ if (
+ self.tools.host_arch == "ARM64"
+ and "AMD64" in self.tools.platform.python_compiler()
+ ):
+ raise UnsupportedHostError(
+ "The Python interpreter that is being used to run Briefcase has been "
+ "compiled for x86_64, and is running in emulation mode on ARM64 "
+ "hardware. You must use a Python interpreter that has been "
+ "compiled for ARM64."
+ )
+
+ if self.tools.host_arch not in ("AMD64", "ARM64"):
if all(app.external_package_path for app in self.apps.values()):
if not self.is_clone:
self.console.warning(f"""
@@ -101,11 +113,11 @@ def verify_host(self):
** WARNING: Possible architecture mismatch **
*************************************************************************
-The build machine is {self.tools.host_arch}, but Briefcase on Windows currently only
-supports x86-64 installers.
+The build machine is {self.tools.host_arch}, but Briefcase on Windows only
+supports x86-64 and ARM64 installers.
You are responsible for ensuring that the content of external_package_path
-is compatible with x86-64.
+is compatible with supported platforms.
*************************************************************************
""")
@@ -127,7 +139,8 @@ def verify_host(self):
class WindowsCreateCommand(CreateCommand):
def support_package_filename(self, support_revision):
- return f"python-{self.python_version_tag}.{support_revision}-embed-amd64.zip"
+ arch = self.tools.host_arch.lower()
+ return f"python-{self.python_version_tag}.{support_revision}-embed-{arch}.zip"
def support_package_url(self, support_revision):
micro = re.match(r"\d+", str(support_revision)).group(0)
@@ -594,7 +607,7 @@ def _package_msi(self, app):
"-ext",
self.tools.wix.ext_path("UI"),
"-arch",
- "x64", # Default is x86, regardless of the build machine.
+ self.vscode_platform.lower(),
f"{app.app_name}.wxs",
"-loc",
"unicode.wxl",
diff --git a/src/briefcase/platforms/windows/app.py b/src/briefcase/platforms/windows/app.py
index 06dbd079fa..0559bdae95 100644
--- a/src/briefcase/platforms/windows/app.py
+++ b/src/briefcase/platforms/windows/app.py
@@ -23,9 +23,12 @@
class WindowsAppMixin(WindowsMixin):
output_format = "app"
- packaging_root = Path("src")
supports_external_packaging = True
+ @property
+ def packaging_root(self):
+ return Path("src")
+
def project_path(self, app):
return self.bundle_path(app)
diff --git a/src/briefcase/platforms/windows/visualstudio.py b/src/briefcase/platforms/windows/visualstudio.py
index b7275fe3b2..5e61a46357 100644
--- a/src/briefcase/platforms/windows/visualstudio.py
+++ b/src/briefcase/platforms/windows/visualstudio.py
@@ -21,7 +21,10 @@
class WindowsVisualStudioMixin(WindowsMixin):
output_format = "VisualStudio"
- packaging_root = Path("x64/Release")
+
+ @property
+ def packaging_root(self):
+ return Path(f"{self.vscode_platform}/Release")
def project_path(self, app):
return self.bundle_path(app) / f"{app.formal_name}.sln"
@@ -65,6 +68,7 @@ def build_app(self, app: BaseConfig, **kwargs):
"-property:RestorePackagesConfig=true",
f"-target:{app.formal_name}",
"-property:Configuration=Release",
+ f"-property:Platform={self.vscode_platform}",
(
"-verbosity:detailed"
if self.console.is_deep_debug
diff --git a/tests/integrations/base/test_ToolCache.py b/tests/integrations/base/test_ToolCache.py
index 07f4fb3cba..feec097739 100644
--- a/tests/integrations/base/test_ToolCache.py
+++ b/tests/integrations/base/test_ToolCache.py
@@ -109,6 +109,72 @@ def test_host_arch_and_os(simple_tools):
assert simple_tools.host_os == platform.system()
+def test_get_host_arch_windows_arm64(dummy_console, monkeypatch, tmp_path):
+ """ARM64 is detected on Windows with a pre-3.12 Python x86_64 interpreter."""
+ mock_ctypes = MagicMock()
+ mock_ctypes.c_ushort.return_value.value = 0xAA64 # IMAGE_FILE_MACHINE_ARM64
+
+ mock_platform = MagicMock()
+ mock_platform.machine.return_value = "AMD64"
+ mock_platform.system.return_value = "Windows"
+
+ mock_sys = MagicMock()
+ mock_sys.version_info = (3, 11, 0)
+ mock_sys.getwindowsversion.return_value.build = 16299
+ mock_sys.maxsize = 2**64
+
+ monkeypatch.setattr(ToolCache, "platform", mock_platform)
+ monkeypatch.setattr(ToolCache, "sys", mock_sys)
+ monkeypatch.setitem(sys.modules, "ctypes", mock_ctypes)
+
+ tools = ToolCache(console=dummy_console, base_path=tmp_path)
+
+ assert tools.host_arch == "ARM64"
+
+
+def test_get_host_arch_windows_not_arm64(dummy_console, monkeypatch, tmp_path):
+ """AMD64 is returned when IsWow64Process2 reports the native machine is not
+ ARM64."""
+ mock_ctypes = MagicMock()
+ mock_ctypes.windll.kernel32.IsWow64Process2.return_value = 0
+
+ mock_platform = MagicMock()
+ mock_platform.machine.return_value = "AMD64"
+ mock_platform.system.return_value = "Windows"
+
+ mock_sys = MagicMock()
+ mock_sys.version_info = (3, 11, 0)
+ mock_sys.getwindowsversion.return_value.build = 16299
+ mock_sys.maxsize = 2**64
+
+ monkeypatch.setattr(ToolCache, "platform", mock_platform)
+ monkeypatch.setattr(ToolCache, "sys", mock_sys)
+ monkeypatch.setitem(sys.modules, "ctypes", mock_ctypes)
+
+ tools = ToolCache(console=dummy_console, base_path=tmp_path)
+
+ assert tools.host_arch == "AMD64"
+
+
+def test_get_host_arch_windows_python312(dummy_console, monkeypatch, tmp_path):
+ """On Python 3.12+, IsWow64Process2 is not called; platform.machine() is used
+ directly."""
+ mock_platform = MagicMock()
+ mock_platform.machine.return_value = "AMD64"
+ mock_platform.system.return_value = "Windows"
+
+ mock_sys = MagicMock()
+ mock_sys.version_info = (3, 12, 0)
+ mock_sys.maxsize = 2**64
+
+ monkeypatch.setattr(ToolCache, "platform", mock_platform)
+ monkeypatch.setattr(ToolCache, "sys", mock_sys)
+
+ tools = ToolCache(console=dummy_console, base_path=tmp_path)
+
+ assert tools.host_arch == "AMD64"
+
+
def test_base_path_is_path(dummy_console, simple_tools):
"""Base path is always a Path."""
# The BaseCommand tests have much more extensive tests for this path.
diff --git a/tests/platforms/windows/app/create/test_create.py b/tests/platforms/windows/app/create/test_create.py
index 61afc1fe80..61b036e60e 100644
--- a/tests/platforms/windows/app/create/test_create.py
+++ b/tests/platforms/windows/app/create/test_create.py
@@ -1,4 +1,6 @@
+import platform
import sys
+from unittest.mock import MagicMock
import pytest
from packaging.version import Version
@@ -28,9 +30,9 @@ def test_unsupported_host_os(create_command, host_os):
create_command()
-@pytest.mark.parametrize("host_arch", ["i686", "ARM64", "wonky"])
+@pytest.mark.parametrize("host_arch", ["i686", "wonky"])
def test_unsupported_arch(create_command, host_arch, first_app_config):
- """Internal apps can only be developed on x86-64."""
+ """Internal apps can only be developed on x86-64 and ARM64."""
create_command.tools.host_os = "Windows"
create_command.tools.host_arch = host_arch
create_command.apps["first"] = first_app_config
@@ -42,7 +44,7 @@ def test_unsupported_arch(create_command, host_arch, first_app_config):
create_command.verify_host()
-@pytest.mark.parametrize("host_arch", ["i686", "ARM64", "wonky"])
+@pytest.mark.parametrize("host_arch", ["i686", "wonky"])
def test_unsupported_arch_external(create_command, host_arch, first_app_config, capsys):
"""External apps can be built on a different architecture, with a warning."""
create_command.tools.host_os = "Windows"
@@ -82,6 +84,31 @@ def test_unsupported_32bit_python(create_command):
create_command()
+def test_verify_windows_cpu_arch(create_command):
+ """Running through x86_64 emulation on Windows ARM64 will raise an error."""
+
+ # Create a Mock object for the platform module
+ create_command.tools.platform = MagicMock(spec_set=platform)
+
+ # Simulate that Mock platform is running on Windows ARM64 with an x86_64 Python interpreter
+ create_command.tools.host_os = "Windows"
+ create_command.tools.host_arch = "ARM64"
+ create_command.tools.platform.python_compiler = MagicMock(
+ return_value="MSV v.1950 64 bit (AMD64)"
+ )
+
+ with pytest.raises(
+ UnsupportedHostError,
+ match=(
+ r"The Python interpreter that is being used to run Briefcase has been "
+ r"compiled for x86_64, and is running in emulation mode on ARM64 "
+ r"hardware. You must use a Python interpreter that has been "
+ r"compiled for ARM64."
+ ),
+ ):
+ create_command.verify_host()
+
+
def test_context(create_command, first_app_config):
context = create_command.output_format_template_context(first_app_config)
assert sorted(context.keys()) == [
@@ -162,7 +189,7 @@ def test_support_package_url(
expected_link = (
f"https://www.python.org/ftp/python"
f"/{sys.version_info.major}.{sys.version_info.minor}.{micro}"
- f"/python-{sys.version_info.major}.{sys.version_info.minor}.{revision}-embed-amd64.zip"
+ f"/python-{sys.version_info.major}.{sys.version_info.minor}.{revision}-embed-{create_command.tools.host_arch.lower()}.zip"
)
assert create_command.support_package_url(revision) == expected_link
diff --git a/tests/platforms/windows/visualstudio/test_build.py b/tests/platforms/windows/visualstudio/test_build.py
index 7c6681ed1f..7ca0327a3d 100644
--- a/tests/platforms/windows/visualstudio/test_build.py
+++ b/tests/platforms/windows/visualstudio/test_build.py
@@ -65,6 +65,7 @@ def test_build_app(build_command, first_app_config, tool_debug_mode, tmp_path):
"-property:RestorePackagesConfig=true",
"-target:First App",
"-property:Configuration=Release",
+ f"-property:Platform={build_command.vscode_platform}",
"-verbosity:detailed" if tool_debug_mode else "-verbosity:normal",
],
check=True,