Skip to content
Merged
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
1 change: 1 addition & 0 deletions changes/2957.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
You can now set the `BRIEFCASE_ALLOW_EMULATION` environment variable to allow Briefcase to run when using an x86_64 Python executable on ARM64 hardware.
12 changes: 12 additions & 0 deletions docs/en/reference/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ There are three restrictions on this path specification:
3. It *must not* be on a network drive.

The second two restrictions both exist because some of the tools that Briefcase uses (in particular, the Android SDK) do not work in these locations.

### `BRIEFCASE_ALLOW_EMULATION`

/// warning | Do not use in production

It is *highly* inadvisable to generate production apps while this environment variable is set.

///

Both macOS and Windows provide mechanisms for running x86_64 binaries on ARM64 hardware. These tools can make it very difficult to generate a packaged application, as it can be difficult to ensure a packaged application contains code matching the right architecture when the platform provides misleading answers about the current platform. As a result, Briefcase will raise an error if it detects that the version of Python that is being used doesn't match the underlying hardware architecture.

However, it is possible to disable this check by setting the `BRIEFCASE_ALLOW_EMULATION` environment variable. This will cause Briefcase to output a warning if it appears that Python is running in emulation mode, but it will allow Briefcase commands to run.
26 changes: 10 additions & 16 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,26 +122,22 @@ def support_package_url(self, support_revision: str) -> str:
f"{self.support_package_filename(support_revision)}"
)

def stub_binary_filename(self, support_revision: str, is_console_app: bool) -> str:
def stub_binary_filename(
self,
support_revision: str,
app: FinalizedAppConfig,
) -> str:
"""The filename for the stub binary."""
stub_type = "Console" if is_console_app else "GUI"
win_suffix = (
f"-{self.tools.host_arch.lower()}"
if self.tools.host_os == "Windows"
else ""
)
return (
f"{stub_type}-Stub-{self.python_version_tag}{win_suffix}"
f"-b{support_revision}.zip"
)
stub_type = "Console" if app.console_app else "GUI"
return f"{stub_type}-Stub-{self.python_version_tag}-b{support_revision}.zip"

def stub_binary_url(self, support_revision: str, is_console_app: bool) -> str:
def stub_binary_url(self, support_revision: str, app: FinalizedAppConfig) -> str:
"""The URL of the stub binary to use for apps of this type."""
return (
"https://briefcase-support.s3.amazonaws.com/python/"
f"{self.python_version_tag}/"
f"{self.platform}/"
f"{self.stub_binary_filename(support_revision, is_console_app)}"
f"{self.stub_binary_filename(support_revision, app)}"
)

def icon_targets(self, app: FinalizedAppConfig):
Expand Down Expand Up @@ -492,9 +488,7 @@ def _download_stub_binary(self, app: FinalizedAppConfig) -> Path:
except AttributeError:
stub_binary_revision = self.stub_binary_revision(app)

stub_binary_url = self.stub_binary_url(
stub_binary_revision, app.console_app
)
stub_binary_url = self.stub_binary_url(stub_binary_revision, app=app)
custom_stub_binary = False
self.console.info(f"Using stub binary {stub_binary_url}")

Expand Down
23 changes: 17 additions & 6 deletions src/briefcase/platforms/macOS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,23 @@ def verify_tools(self):
self.tools.platform.machine() == "x86_64"
and "ARM64" in self.tools.platform.version()
):
raise BriefcaseCommandError(
"The Python interpreter that is being used to run Briefcase has been "
"compiled for x86_64, and is running in emulation mode on Apple "
"Silicon hardware. You must use a Python interpreter that has been "
"compiled for Apple Silicon, or is a Universal binary."
)
if bool(self.tools.os.getenv("BRIEFCASE_ALLOW_EMULATION", "")):
self.console.warning_banner(
"Running in CPU emulation mode",
(
"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. This configuration should not be used "
"for production apps."
),
)
else:
raise BriefcaseCommandError(
"The Python interpreter that is being used to run Briefcase has "
"been compiled for x86_64, and is running in emulation mode on "
"Apple Silicon hardware. You must use a Python interpreter that "
"has been compiled for Apple Silicon, or is a Universal binary."
)

super().verify_tools()

Expand Down
35 changes: 29 additions & 6 deletions src/briefcase/platforms/windows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,23 @@ def verify_host(self):
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 bool(self.tools.os.getenv("BRIEFCASE_ALLOW_EMULATION", "")):
self.console.warning_banner(
"Running in CPU emulation mode",
(
"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. This configuration should not be used "
"for production apps."
),
)
else:
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()):
Expand Down Expand Up @@ -148,6 +159,18 @@ def target_windows_build(self, app: FinalizedAppConfig) -> int | None:


class WindowsCreateCommand(CreateCommand):
def stub_binary_filename(
self,
support_revision: str,
app: FinalizedAppConfig,
) -> str:
"""The filename for the stub binary."""
stub_type = "Console" if app.console_app else "GUI"
return (
f"{stub_type}-Stub-{self.python_version_tag}-{self.tools.host_arch.lower()}"
f"-b{support_revision}.zip"
)

def support_package_filename(self, support_revision):
arch = self.tools.host_arch.lower()
return f"python-{self.python_version_tag}.{support_revision}-embed-{arch}.zip"
Expand Down
4 changes: 2 additions & 2 deletions src/briefcase/platforms/windows/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
PublishCommand,
UpdateCommand,
)
from briefcase.config import BaseConfig
from briefcase.config import FinalizedAppConfig
from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.rcedit import RCEdit
from briefcase.integrations.windows_sdk import WindowsSDK
Expand Down Expand Up @@ -59,7 +59,7 @@ def verify_tools(self):
with suppress(BriefcaseCommandError):
WindowsSDK.verify(tools=self.tools)

def build_app(self, app: BaseConfig, **kwargs):
def build_app(self, app: FinalizedAppConfig, **kwargs):
"""Build the application.

:param app: The config object for the app
Expand Down
20 changes: 20 additions & 0 deletions tests/platforms/macOS/test_macOSMixin_verify_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,23 @@ def test_verify_macos_cpu_arch(dummy_command):
),
):
dummy_command.verify_tools()


def test_verify_macos_cpu_arch_warning(monkeypatch, dummy_command, capsys):
"""Rosetta emulation is allowed if you opt in, with a warning."""
# Set the environment variable to allow emulation
monkeypatch.setenv("BRIEFCASE_ALLOW_EMULATION", "1")

# Create a Mock object for the platform module
dummy_command.tools.platform = MagicMock(spec_set=platform)

# Simulate that Mock platform is running on Apple Silicon with an x86_64 Python interpreter
dummy_command.tools.platform.machine = MagicMock(return_value="x86_64")
dummy_command.tools.platform.version = MagicMock(return_value="ARM64")

dummy_command.verify_tools()

# A warning was raised, but the app can continue.
stdout, stderr = capsys.readouterr()
assert "Running in CPU emulation mode" in stdout
assert stderr == ""
5 changes: 3 additions & 2 deletions tests/platforms/macOS/xcode/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
def first_app_generated(first_app_config, tmp_path):
# Create the briefcase.toml file
create_file(
tmp_path / "base_path/macos/xcode/First App/briefcase.toml",
tmp_path / "base_path/build/first-app/macos/xcode/briefcase.toml",
"""
[paths]
app_packages_path="app_packages"
support_path="support"
info_plist_path="Info.plist"
""",
)
(tmp_path / "base_path/build/first-app/macos/xcode/support").mkdir(parents=True)

create_plist_file(
tmp_path / "base_path/macos/xcode/First App/Info.plist",
tmp_path / "base_path/build/first-app/macos/xcode/First App/Info.plist",
{
"MainModule": "first_app",
},
Expand Down
57 changes: 54 additions & 3 deletions tests/platforms/windows/app/create/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,29 @@ def test_verify_windows_cpu_arch(create_command):
create_command.verify_host()


def test_verify_windows_cpu_arch_warning(monkeypatch, create_command, capsys):
"""User can opt into emuluation mode, but they will get a warning."""
# Set the environment variable to allow emulation
monkeypatch.setenv("BRIEFCASE_ALLOW_EMULATION", "1")

# 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)"
)

create_command.verify_host()

# A warning was raised, but the app can continue.
stdout, stderr = capsys.readouterr()
assert "Running in CPU emulation mode" in stdout
assert stderr == ""


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 @@ -196,6 +219,36 @@ def test_installer_images(create_command, first_app_config, tmp_path):
}


@pytest.mark.parametrize(
("console_app", "arch", "revision", "expected_binary"),
[
(False, "AMD64", "42", "GUI-Stub-3.X-amd64-b42.zip"),
(True, "AMD64", "42", "Console-Stub-3.X-amd64-b42.zip"),
(False, "arm64", "37", "GUI-Stub-3.X-arm64-b37.zip"),
(True, "arm64", "37", "Console-Stub-3.X-arm64-b37.zip"),
],
)
def test_stub_binary_filename(
create_command,
first_app_config,
console_app,
arch,
revision,
expected_binary,
):
"""A valid support package URL is created for a support revision."""
first_app_config.console_app = console_app
create_command.tools.host_arch = arch

create_command.tools.sys = MagicMock(spec=sys)
create_command.tools.sys.version_info = ("3", "X", "Y")

assert (
create_command.stub_binary_filename(revision, first_app_config)
== expected_binary
)


@pytest.mark.parametrize(
("revision", "micro"),
[
Expand All @@ -209,9 +262,7 @@ def test_installer_images(create_command, first_app_config, tmp_path):
("0rc1", "0"),
],
)
def test_support_package_url(
create_command, revision, micro, first_app_config, tmp_path
):
def test_support_package_url(create_command, revision, micro):
"""A valid support package URL is created for a support revision."""
expected_link = (
f"https://www.python.org/ftp/python"
Expand Down