Skip to content

Improving build error messages when build fails due to missing wheels #2303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 13 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/2230.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added hints to error messages--suggesting that the user checks for missing wheels--that appear when build iOS or build macOS fails.
4 changes: 4 additions & 0 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ def _install_app_requirements(
progress_message: str = "Installing app requirements...",
pip_args: list[str] | None = None,
pip_kwargs: dict[str, str] | None = None,
install_hint: str = "",
):
"""Install requirements for the app with pip.

Expand All @@ -645,6 +646,8 @@ def _install_app_requirements(
:param pip_args: Any additional command line arguments to use when invoking pip.
:param pip_kwargs: Any additional keyword arguments to pass to the subprocess
when invoking pip.
:param install_hint: Additional hint information to provide in the exception
message if the pip install call fails.
"""
# Clear existing dependency directory
if app_packages_path.is_dir():
Expand All @@ -661,6 +664,7 @@ def _install_app_requirements(
([] if pip_args is None else pip_args)
+ self._pip_requires(app, requires)
),
install_hint=install_hint,
**(pip_kwargs if pip_kwargs else {}),
)
else:
Expand Down
12 changes: 11 additions & 1 deletion src/briefcase/platforms/iOS/xcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,13 @@ def _install_app_requirements(
pip_kwargs={
"env": {
"PYTHONPATH": str(device_platform_site),
},
}
},
install_hint="""

It may also indicate that an `iphonesimulator` wheel could be found, but an
`iphoneos` wheel could not be found.
""",
)

# Perform a second install pass targeting the "iphonesimulator" platform for the
Expand All @@ -392,6 +397,11 @@ def _install_app_requirements(
"PYTHONPATH": str(simulator_platform_site),
},
},
install_hint="""

It may also indicate that an `iphoneos` wheel could be found, but an
`iphonesimulator` wheel could not be found.
""",
)


Expand Down
5 changes: 5 additions & 0 deletions src/briefcase/platforms/macOS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ def _install_app_requirements(
"--platform",
f"macosx_{macOS_min_tag}_{self.tools.host_arch}",
],
install_hint=f"""

If an {self.tools.host_arch} wheel has not been published for one or more of your requirements,
you must compile those wheels yourself.
""",
)

# Find all the packages with binary components.
Expand Down
63 changes: 62 additions & 1 deletion tests/platforms/iOS/xcode/test_create.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import shutil
import sys
from pathlib import Path
from subprocess import CalledProcessError
from unittest.mock import MagicMock, call

import pytest

from briefcase.console import Console
from briefcase.exceptions import BriefcaseCommandError, UnsupportedHostError
from briefcase.exceptions import (
BriefcaseCommandError,
RequirementsInstallError,
UnsupportedHostError,
)
from briefcase.integrations.subprocess import Subprocess
from briefcase.platforms.iOS.xcode import iOSXcodeCreateCommand

Expand Down Expand Up @@ -438,3 +444,58 @@ def test_permissions_context(create_command, first_app, permissions, info, conte
x_permissions = create_command._x_permissions(first_app)
# Check that the final platform permissions are rendered as expected.
assert context == create_command.permissions_context(first_app, x_permissions)


def test_install_app_requirements_error_adds_install_hint_missing_iphoneos_wheel(
create_command, first_app_generated
):
"""Install_hint (mentioning a missing iphoneos wheel) is added when RequirementsInstallError is raised
by _install_app_requirements in the iOS create command."""
first_app_generated.requires = ["package-one", "package_two", "package_three"]

# Mock app_context for the generated app to simulate pip failure
mock_app_context = MagicMock(spec=Subprocess)
mock_app_context.run.side_effect = CalledProcessError(returncode=1, cmd="pip")
create_command.tools[first_app_generated].app_context = mock_app_context

# Check that _install_app_requirements raises a RequirementsInstallError with an install hint
with pytest.raises(
RequirementsInstallError, match="`iphoneos` wheel could not be found"
):
create_command._install_app_requirements(
app=first_app_generated,
requires=first_app_generated.requires,
app_packages_path=Path("/test/path"),
)

# Ensure the mocked subprocess was called as expected
mock_app_context.run.assert_called_once()


def test_install_app_requirements_error_adds_install_hint_missing_iphonesimulator_wheel(
create_command, first_app_generated
):
"""Install_hint (mentioning a missing iphonesimulator wheel) is added when RequirementsInstallError is raised
by _install_app_requirements in the iOS create command."""
first_app_generated.requires = ["package-one", "package_two", "package_three"]

# Mock app_context for the generated app to simulate pip failure
mock_app_context = MagicMock(spec=Subprocess)
mock_app_context.run.side_effect = [
None,
CalledProcessError(returncode=1, cmd="pip"),
]
create_command.tools[first_app_generated].app_context = mock_app_context

# Check that _install_app_requirements raises a RequirementsInstallError with an install hint
with pytest.raises(
RequirementsInstallError, match="`iphonesimulator` wheel could not be found"
):
create_command._install_app_requirements(
app=first_app_generated,
requires=first_app_generated.requires,
app_packages_path=Path("/test/path"),
)

# Ensure the mocked subprocess was called as expected
assert mock_app_context.run.call_count == 2
68 changes: 67 additions & 1 deletion tests/platforms/macOS/app/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import subprocess
import sys
import time
from pathlib import Path
from subprocess import CalledProcessError
from unittest import mock

import pytest

from briefcase.console import Console
from briefcase.exceptions import BriefcaseCommandError
from briefcase.exceptions import BriefcaseCommandError, RequirementsInstallError
from briefcase.integrations.subprocess import Subprocess
from briefcase.platforms.macOS.app import macOSAppCreateCommand

Expand Down Expand Up @@ -1012,3 +1014,67 @@ def test_install_support_package(

# The legacy content has been purged
assert not (runtime_support_path / "python-stdlib/old-Python").exists()


def test_install_app_requirements_error_adds_install_hint_missing_x86_64_wheel(
create_command, first_app_templated
):
"""Install_hint (mentioning a missing x86_64 wheel) is added when RequirementsInstallError is raised
by _install_app_requirements in the macOS create command."""

create_command.tools.host_arch = "x86_64"
first_app_templated.requires = ["package-one", "package_two", "packagethree"]

# Mock app_context for the generated app to simulate pip failure
mock_app_context = mock.MagicMock(spec=Subprocess)
mock_app_context.run.side_effect = CalledProcessError(returncode=1, cmd="pip")
create_command.tools[first_app_templated].app_context = mock_app_context

# Check that _install_app_requirements raises a RequirementsInstallError with an install hint
with pytest.raises(
RequirementsInstallError,
match="x86_64 wheel has not been published for one or more of your requirements",
):
create_command._install_app_requirements(
app=first_app_templated,
requires=first_app_templated.requires,
app_packages_path=Path("/test/path"),
)

# Ensure the mocked subprocess was called as expected
mock_app_context.run.assert_called_once()


def test_install_app_requirements_error_adds_install_hint_missing_arm64_wheel(
create_command, first_app_templated
):
"""Install_hint (mentioning a missing arm64 wheel) is added when RequirementsInstallError is raised
by _install_app_requirements in the macOS create command."""

create_command.tools.host_arch = "x86_64"
first_app_templated.requires = ["package-one", "package_two", "packagethree"]

# Fake a found binary package (so second install is triggered)
create_command.find_binary_packages = mock.Mock(
return_value=[("package-one", "1.0")]
)

# First call (host arch x86_64) succeeds, second (other arch arm64) fails
create_command.tools[first_app_templated].app_context.run.side_effect = [
None,
CalledProcessError(returncode=1, cmd="pip"),
]

# Check that _install_app_requirements raises a RequirementsInstallError with an install hint
with pytest.raises(
RequirementsInstallError,
match="arm64 wheel has not been published for one or more of your requirements",
):
create_command._install_app_requirements(
app=first_app_templated,
requires=first_app_templated.requires,
app_packages_path=Path("/test/path"),
)

# Ensure the mocked subprocess was called as expected
assert create_command.tools[first_app_templated].app_context.run.call_count == 2