Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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" ]
1 change: 1 addition & 0 deletions changes/1887.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Briefcase can now build Windows apps for ARM64 devices.
4 changes: 2 additions & 2 deletions docs/en/reference/platforms/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
<td></td>
<td></td>
<td colspan="2">{{ ci_tested }}</td>
<td colspan="2"></td>
<td colspan="2">{{ ci_tested }}</td>
<td></td>
<td></td>
<td></td>
Expand All @@ -165,7 +165,7 @@
<td></td>
<td></td>
<td colspan="2">{{ ci_tested }}</td>
<td colspan="2"></td>
<td colspan="2">{{ ci_tested }}</td>
<td></td>
<td></td>
<td></td>
Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/platforms/windows/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<td></td>
<td></td>
<td colspan="2">{{ ci_tested }}</td>
<td colspan="2"></td>
<td colspan="2">{{ ci_tested }}</td>
<td></td>
<td></td>
<td></td>
Expand Down
3 changes: 2 additions & 1 deletion docs/en/reference/platforms/windows/visualstudio.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also a mention of this in integrations/visualstudio.py, as part of the error message that is displayed if VSCode can't be found.


## Application configuration

Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,4 @@ Xauth
Xcode
Xr
Xs
MSVC
10 changes: 9 additions & 1 deletion src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
32 changes: 31 additions & 1 deletion src/briefcase/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Comment on lines +221 to +238
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clever - but I've found an even easier way that doesn't need system calls:

Suggested change
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 os.getenv("PROCESSOR_ARCHITECTURE", arch)

AFAICT, PROCESSOR_ARCHITECTURE is defined on all Win10 and 11 machines, and returns ARM64/AMD64 as appropriate.

return arch

@cached_property
def system_encoding(self) -> str:
"""The character encoding for the system's locale.
Expand Down
31 changes: 22 additions & 9 deletions src/briefcase/platforms/windows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,36 @@ 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"
Comment thread
freakboy3742 marked this conversation as resolved.
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"):
Comment thread
freakboy3742 marked this conversation as resolved.
if all(app.external_package_path for app in self.apps.values()):
if not self.is_clone:
self.console.warning(f"""
*************************************************************************
** 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.

*************************************************************************
""")
Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion src/briefcase/platforms/windows/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@

class WindowsAppMixin(WindowsMixin):
output_format = "app"
packaging_root = Path("src")
supports_external_packaging = True

@property
def packaging_root(self):
Comment thread
freakboy3742 marked this conversation as resolved.
return Path("src")

def project_path(self, app):
return self.bundle_path(app)

Expand Down
6 changes: 5 additions & 1 deletion src/briefcase/platforms/windows/visualstudio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions tests/integrations/base/test_ToolCache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 31 additions & 4 deletions tests/platforms/windows/app/create/test_create.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import platform
import sys
from unittest.mock import MagicMock

import pytest
from packaging.version import Version
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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()) == [
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions tests/platforms/windows/visualstudio/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading