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/2429.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Wheels that contain Linux `.so` are now usable on macOS.
26 changes: 1 addition & 25 deletions src/briefcase/platforms/macOS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
)
from briefcase.integrations.xcode import XcodeCliTools, get_identities
from briefcase.platforms.macOS.filters import macOS_log_clean_filter
from briefcase.platforms.macOS.utils import AppPackagesMergeMixin
from briefcase.platforms.macOS.utils import AppPackagesMergeMixin, is_mach_o_binary

try:
import dmgbuild
Expand Down Expand Up @@ -554,30 +554,6 @@ def run_gui_app(
self.tools.os.kill(app_pid, SIGTERM)


def is_mach_o_binary(path): # pragma: no-cover-if-is-windows
"""Determine if the file at the given path is a Mach-O binary.

:param path: The path to check
:returns: True if the file at the given location is a Mach-O binary.
"""
# A binary is any file that is executable, or has a suffix from a known list
if os.access(path, os.X_OK) or path.suffix.lower() in {".dylib", ".o", ".so", ""}:
# File is a binary; read the file magic to determine if it's Mach-O.
with path.open("rb") as f:
magic = f.read(4)
return magic in (
b"\xca\xfe\xba\xbe",
b"\xcf\xfa\xed\xfe",
b"\xce\xfa\xed\xfe",
b"\xbe\xba\xfe\xca",
b"\xfe\xed\xfa\xcf",
b"\xfe\xed\xfa\xce",
)
else:
# Not a binary
return False


class macOSSigningMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
29 changes: 27 additions & 2 deletions src/briefcase/platforms/macOS/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import concurrent
import email
import hashlib
import os
import pathlib
import plistlib
import subprocess
Expand All @@ -13,6 +14,30 @@
CORETYPES_PATH = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Info.plist"


def is_mach_o_binary(path: Path): # pragma: no-cover-if-is-windows
"""Determine if the file at the given path is a Mach-O binary.

:param path: The path to check
:returns: True if the file at the given location is a Mach-O binary.
"""
# A binary is any file that is executable, or has a suffix from a known list
if os.access(path, os.X_OK) or path.suffix.lower() in {".dylib", ".o", ".so", ""}:
# File is a binary; read the file magic to determine if it's Mach-O.
with path.open("rb") as f:
magic = f.read(4)
return magic in (
b"\xca\xfe\xba\xbe",
b"\xcf\xfa\xed\xfe",
b"\xce\xfa\xed\xfe",
b"\xbe\xba\xfe\xca",
b"\xfe\xed\xfa\xcf",
b"\xfe\xed\xfa\xce",
)
else:
# Not a binary
return False


def sha256_file_digest(path: Path) -> str:
"""Compute a sha256 checksum digest of a file.

Expand Down Expand Up @@ -169,7 +194,7 @@ def thin_app_packages(
"""Ensure that all the dylibs in a given app_packages folder are thin."""
dylibs = []
for source_path in app_packages.glob("**/*"):
if source_path.suffix in {".so", ".dylib"}:
if not source_path.is_dir() and is_mach_o_binary(source_path):
dylibs.append(source_path)

# Call lipo on each dylib that was found to ensure it is thin.
Expand Down Expand Up @@ -229,7 +254,7 @@ def merge_app_packages(
if source_path.is_dir():
target_path.mkdir(exist_ok=True)
else:
if source_path.suffix in {".so", ".dylib"}:
if is_mach_o_binary(source_path):
# Dynamic libraries need to be merged; do this in a second pass.
dylibs.add(relative_path)
elif target_path.exists():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ def test_merge(dummy_command, pre_existing, tmp_path):
("second/other.py", "# other python"),
("second/different.py", "# different python"),
("second/some-binary", "# A file with executable permissions", 0o755),
("second/sub1/module1.dylib", "dylib-gothic"),
("second/sub1/module2.so", "dylib-gothic"),
("second/sub1/module3.dylib", "dylib-gothic"),
("second/sub1/module1.dylib", b"\xca\xfe\xba\xbedylib-gothic"),
("second/sub1/module2.so", b"\xca\xfe\xba\xbedylib-gothic"),
("second/sub1/module3.dylib", b"\xca\xfe\xba\xbedylib-gothic"),
("second/sub1/module5.so", "\x7fELFso-elf"),
],
)

Expand All @@ -46,9 +47,10 @@ def test_merge(dummy_command, pre_existing, tmp_path):
tag="macOS_11_0_modern",
extra_content=[
("second/different.py", "# I need to be different"),
("second/sub1/module1.dylib", "dylib-modern"),
("second/sub1/module2.so", "dylib-modern"),
("second/sub1/module4.dylib", "dylib-modern"),
("second/sub1/module1.dylib", b"\xca\xfe\xba\xbedylib-modern"),
("second/sub1/module2.so", b"\xca\xfe\xba\xbedylib-modern"),
("second/sub1/module4.dylib", b"\xca\xfe\xba\xbedylib-modern"),
("second/sub1/module5.so", "\x7fELFso-elf"),
("second/sub2/extra.py", "# extra python"),
],
)
Expand All @@ -58,7 +60,7 @@ def lipo(cmd, **kwargs):
if cmd[0] != "lipo":
pytest.fail(f"Subprocess called {cmd[0]}, not lipo")

create_file(cmd[3], "dylib-merged")
create_file(cmd[3], b"\xca\xfe\xba\xbedylib-merged", mode="wb")

dummy_command.tools.subprocess.run.side_effect = lipo

Expand Down Expand Up @@ -116,10 +118,11 @@ def lipo(cmd, **kwargs):
(Path("second/some-binary"), "# A file with executable permissions"),
(Path("second/other.py"), "# other python"),
(Path("second/sub1"), None),
(Path("second/sub1/module1.dylib"), "dylib-merged"),
(Path("second/sub1/module2.so"), "dylib-merged"),
(Path("second/sub1/module3.dylib"), "dylib-merged"),
(Path("second/sub1/module4.dylib"), "dylib-merged"),
(Path("second/sub1/module1.dylib"), b"\xca\xfe\xba\xbedylib-merged"),
(Path("second/sub1/module2.so"), b"\xca\xfe\xba\xbedylib-merged"),
(Path("second/sub1/module3.dylib"), b"\xca\xfe\xba\xbedylib-merged"),
(Path("second/sub1/module4.dylib"), b"\xca\xfe\xba\xbedylib-merged"),
(Path("second/sub1/module5.so"), "\x7fELFso-elf"),
(Path("second/sub2"), None),
(Path("second/sub2/extra.py"), "# extra python"),
(Path("second-2.3.4.dist-info"), None),
Expand Down Expand Up @@ -168,7 +171,7 @@ def test_merge_problem(dummy_command, tmp_path):
"2.3.4",
tag="macOS_11_0_gothic",
extra_content=[
("second/sub1/module1.dylib", "dylib-gothic"),
("second/sub1/module1.dylib", b"\xca\xfe\xba\xbedylib-gothic"),
],
)
# Create 2 packages in the "modern" architecture app package sources
Expand All @@ -182,7 +185,7 @@ def test_merge_problem(dummy_command, tmp_path):
"2.3.4",
tag="macOS_11_0_modern",
extra_content=[
("second/sub1/module1.dylib", "dylib-modern"),
("second/sub1/module1.dylib", b"\xca\xfe\xba\xbedylib-modern"),
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def test_thin_app_packages(dummy_command, tmp_path):
tag="macOS_11_0_gothic",
extra_content=[
("pkg/other.py", "# other python"),
("pkg/sub1/module1.dylib", "dylib-fat"),
("pkg/sub1/module2.so", "dylib-fat"),
("pkg/sub2/module3.dylib", "dylib-fat"),
("pkg/sub1/module1.dylib", b"\xca\xfe\xba\xbedylib-fat"),
("pkg/sub1/module2.so", b"\xca\xfe\xba\xbedylib-fat"),
("pkg/sub2/module3.dylib", b"\xca\xfe\xba\xbedylib-fat"),
],
)

Expand All @@ -32,17 +32,30 @@ def test_thin_app_packages(dummy_command, tmp_path):

# Mock the effect of calling lipo -thin
def thin_dylib(*args, **kwargs):
create_file(args[0][args[0].index("-output") + 1], "dylib-thin")
create_file(
args[0][args[0].index("-output") + 1],
b"\xca\xfe\xba\xbedylib-thin",
mode="wb",
)

dummy_command.tools.subprocess.run.side_effect = thin_dylib

# Thin the app_packages folder to gothic dylibs
dummy_command.thin_app_packages(app_packages, arch="gothic")

# All libraries have been thinned
assert file_content(app_packages / "pkg/sub1/module1.dylib") == "dylib-thin"
assert file_content(app_packages / "pkg/sub1/module2.so") == "dylib-thin"
assert file_content(app_packages / "pkg/sub2/module3.dylib") == "dylib-thin"
assert (
file_content(app_packages / "pkg/sub1/module1.dylib")
== b"\xca\xfe\xba\xbedylib-thin"
)
assert (
file_content(app_packages / "pkg/sub1/module2.so")
== b"\xca\xfe\xba\xbedylib-thin"
)
assert (
file_content(app_packages / "pkg/sub2/module3.dylib")
== b"\xca\xfe\xba\xbedylib-thin"
)


def test_thin_app_packages_problem(dummy_command, tmp_path):
Expand All @@ -57,9 +70,9 @@ def test_thin_app_packages_problem(dummy_command, tmp_path):
tag="macOS_11_0_gothic",
extra_content=[
("pkg/other.py", "# other python"),
("pkg/sub1/module1.dylib", "dylib-fat"),
("pkg/sub1/module2.so", "dylib-fat"),
("pkg/sub2/module3.dylib", "dylib-fat"),
("pkg/sub1/module1.dylib", b"\xca\xfe\xba\xbedylib-fat"),
("pkg/sub1/module2.so", b"\xca\xfe\xba\xbedylib-fat"),
("pkg/sub2/module3.dylib", b"\xca\xfe\xba\xbedylib-fat"),
],
)

Expand Down
13 changes: 9 additions & 4 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ def create_installed_package(
except ValueError:
filename, content = entry
chmod = None
create_file(path / filename, content=content, chmod=chmod)
mode = "wb" if isinstance(content, bytes) else "w"
create_file(path / filename, content=content, mode=mode, chmod=chmod)


def create_wheel(
Expand Down Expand Up @@ -318,12 +319,16 @@ def create_wheel(
return wheel_filename


def file_content(path: Path) -> str | None:
def file_content(path: Path) -> str | bytes | None:
"""Return the content of a file, or None if the path is a directory."""
if path.is_dir():
return None
with path.open(encoding="utf-8") as f:
return f.read()
try:
with path.open(encoding="utf-8") as f:
return f.read()
except UnicodeDecodeError:
with path.open(mode="rb") as f:
return f.read()


def assert_url_resolvable(url: str):
Expand Down
Loading