Skip to content

Commit

Permalink
Merge pull request #1933 from freakboy3742/framework-filtering
Browse files Browse the repository at this point in the history
Modifications to support Python 3.13 support packages
  • Loading branch information
mhsmith authored Jul 29, 2024
2 parents cc5a10e + 93971ff commit 18d27ec
Show file tree
Hide file tree
Showing 19 changed files with 278 additions and 46 deletions.
1 change: 1 addition & 0 deletions changes/1933.bugfix.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Briefcase will no longer attempt to sign symlinks in macOS apps.
1 change: 1 addition & 0 deletions changes/1933.bugfix.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Briefcase is now able to remove symlinks to directories as part of the template cleanup.
1 change: 1 addition & 0 deletions changes/1933.bugfix.3.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If a macOS support package contains symlinks, those symlinks will be preserved when the support package is copied into the app bundle.
1 change: 1 addition & 0 deletions changes/1933.bugfix.4.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The log filter for iOS has been modified to capture logs generated when using PEP 730-style binary modules.
1 change: 1 addition & 0 deletions changes/1933.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
macOS app templates can now specify what part of the support package should be copied into the final application bundle.
2 changes: 1 addition & 1 deletion src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,7 @@ def cleanup_app_content(self, app: AppConfig):
# on the file system.
for path in self.bundle_path(app).glob(glob):
relative_path = path.relative_to(self.bundle_path(app))
if path.is_dir():
if path.is_dir() and not path.is_symlink():
self.logger.verbose(f"Removing directory {relative_path}")
self.tools.shutil.rmtree(path)
else:
Expand Down
3 changes: 2 additions & 1 deletion src/briefcase/platforms/iOS/xcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,8 @@ def run_app(
f'senderImagePath ENDSWITH "/{app.formal_name}"'
f' OR (processImagePath ENDSWITH "/{app.formal_name}"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down
12 changes: 9 additions & 3 deletions src/briefcase/platforms/macOS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ def __init__(self, *args, **kwargs):
# These are abstracted to enable testing without patching.
self.get_identities = get_identities

def entitlements_path(self, app: AppConfig):
def entitlements_path(self, app: AppConfig): # pragma: no-cover-if-is-windows
return self.bundle_path(app) / self.path_index(app, "entitlements_path")

def select_identity(
Expand Down Expand Up @@ -571,7 +571,11 @@ def sign_file(
else:
raise BriefcaseCommandError(f"Unable to code sign {path}.")

def sign_app(self, app: AppConfig, identity: SigningIdentity):
def sign_app(
self,
app: AppConfig,
identity: SigningIdentity,
): # pragma: no-cover-if-is-windows
"""Sign an entire app with a specific identity.
:param app: The app to sign
Expand All @@ -588,7 +592,9 @@ def sign_app(self, app: AppConfig, identity: SigningIdentity):
sign_targets.extend(
path
for path in folder.rglob("*")
if not path.is_dir() and is_mach_o_binary(path)
if not path.is_dir()
and not path.is_symlink()
and is_mach_o_binary(path)
)

# Sign all embedded frameworks
Expand Down
16 changes: 14 additions & 2 deletions src/briefcase/platforms/macOS/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def support_path(self, app: AppConfig, runtime=False) -> Path:
else:
return self.bundle_path(app) / "support"

def runtime_path(self, app: AppConfig) -> Path:
try:
return self.path_index(app, "runtime_path")
except KeyError:
return "python-stdlib"

def install_app_support_package(self, app: AppConfig):
"""Install the application support package.
Expand All @@ -61,9 +67,15 @@ def install_app_support_package(self, app: AppConfig):
self.tools.shutil.rmtree(runtime_support_path)
runtime_support_path.mkdir()

# The support package will contain a lot more content than is
# actually needed at runtime. Only copy the runtime_path into
# the support location nominated by the template. Also ensure
# that symlinks are preserved.
runtime_path = self.runtime_path(app)
self.tools.shutil.copytree(
self.support_path(app) / "python-stdlib",
runtime_support_path / "python-stdlib",
self.support_path(app) / runtime_path,
runtime_support_path / Path(runtime_path).name,
symlinks=True,
)

def install_app_resources(self, app: AppConfig):
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/platforms/macOS/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

MACOS_LOG_PREFIX_REGEX = re.compile(
r"\d{4}-\d{2}-\d{2} (?P<timestamp>\d{2}:\d{2}:\d{2}.\d{3}) Df (.*?)\[.*?:.*?\]"
r"(?P<subsystem>( \(libffi\.dylib\))|( \(_ctypes\.cpython-3\d{1,2}-.*?\.(so|dylib)\)))? (?P<content>.*)"
r"(?P<subsystem>( \(libffi\.dylib\))|( \(_ctypes(\.cpython-3\d{1,2}-.*?\.(so|dylib))?\)))? (?P<content>.*)"
)


Expand Down
79 changes: 77 additions & 2 deletions tests/commands/create/test_cleanup_app_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ def myapp_unrolled(myapp, support_path, app_packages_path_index):
create_file(support_path / "other/deep/b_file.doc", "wigs")
create_file(support_path / "other/deep/other.doc", "wigs")

# Create a symlink to a file
(support_path / "dir1/symlink.txt").symlink_to(support_path / "dir2/b_file.txt")

# Create a symlink to a directory
(support_path / "mirror").symlink_to(support_path / "dir1")

return myapp


Expand All @@ -38,6 +44,10 @@ def test_no_cleanup(create_command, myapp_unrolled, support_path, debug, capsys)
assert (support_path / "dir2/b_file.txt").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Symlinks still exist
assert (support_path / "dir1/symlink.txt").is_symlink()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
Expand All @@ -61,6 +71,10 @@ def test_dir_cleanup(create_command, myapp_unrolled, support_path, debug, capsys
assert (support_path / "dir2/b_file.txt").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Directory symlinks still exists, but the file symlink doesn't
assert not (support_path / "dir1/symlink.txt").exists()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
Expand All @@ -87,6 +101,10 @@ def test_file_cleanup(create_command, myapp_unrolled, support_path, debug, capsy
assert not (support_path / "dir1/__pycache__").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Symlinks still exist
assert (support_path / "dir1/symlink.txt").is_symlink()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
Expand Down Expand Up @@ -117,11 +135,15 @@ def test_all_files_in_dir_cleanup(
assert (support_path / "dir2/b_file.txt").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Directory symlinks still exist; file symlink doesn't
assert not (support_path / "dir1/symlink.txt").exists()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
assert output[-3] == "Removing unneeded app bundle content... done"
assert len(output) == (8 if debug else 4)
assert len(output) == (9 if debug else 4)


@pytest.mark.parametrize("debug", [True, False])
Expand All @@ -140,6 +162,10 @@ def test_dir_glob_cleanup(create_command, myapp_unrolled, support_path, debug, c
assert not (support_path / "dir2").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Directory symlinks still exist; file symlink doesn't
assert not (support_path / "dir1/symlink.txt").exists()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
Expand All @@ -166,11 +192,15 @@ def test_file_glob_cleanup(create_command, myapp_unrolled, support_path, debug,
assert (support_path / "dir2/b_file.txt").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Directory symlinks still exist; file symlink doesn't
assert not (support_path / "dir1/symlink.txt").exists()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
assert output[-3] == "Removing unneeded app bundle content... done"
assert len(output) == (7 if debug else 4)
assert len(output) == (8 if debug else 4)


@pytest.mark.parametrize("debug", [True, False])
Expand All @@ -193,13 +223,50 @@ def test_deep_glob_cleanup(create_command, myapp_unrolled, support_path, debug,
assert not (support_path / "other/deep/b_file.doc").exists()
assert (support_path / "other/deep/other.doc").exists()

# Symlinks still exist
assert (support_path / "dir1/symlink.txt").is_symlink()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
assert output[-3] == "Removing unneeded app bundle content... done"
assert len(output) == (8 if debug else 4)


@pytest.mark.parametrize("debug", [True, False])
def test_symlink_cleanup(create_command, myapp_unrolled, support_path, debug, capsys):
"""Symlinks can be cleaned up."""
if debug:
create_command.logger.verbosity = LogLevel.DEBUG

myapp_unrolled.cleanup_paths = [
"path/to/support/dir1/symlink.txt",
"path/to/support/mirror",
]

# Cleanup app content
create_command.cleanup_app_content(myapp_unrolled)

# Confirm none of the files (except __pycache__) have been removed
assert (support_path / "dir1/a_file1.txt").exists()
assert (support_path / "dir1/a_file2.doc").exists()
assert (support_path / "dir1/b_file.txt").exists()
assert not (support_path / "dir1/__pycache__").exists()
assert (support_path / "dir2/b_file.txt").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Directory symlinks still exist; file symlink doesn't
assert not (support_path / "dir1/symlink.txt").exists()
assert not (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
assert output[-3] == "Removing unneeded app bundle content... done"
assert len(output) == (7 if debug else 4)


@pytest.mark.parametrize("debug", [True, False])
def test_template_glob_cleanup(
create_command, myapp_unrolled, support_path, debug, capsys
Expand Down Expand Up @@ -228,6 +295,10 @@ def test_template_glob_cleanup(
assert (support_path / "dir2/b_file.txt").exists()
assert not (support_path / "other/deep/b_file.doc").exists()

# Symlinks still exist
assert (support_path / "dir1/symlink.txt").is_symlink()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
Expand Down Expand Up @@ -268,6 +339,10 @@ def test_non_existent_cleanup(
assert (support_path / "dir2/b_file.txt").exists()
assert (support_path / "other/deep/b_file.doc").exists()

# Symlinks still exist
assert (support_path / "dir1/symlink.txt").is_symlink()
assert (support_path / "mirror").is_symlink()

# Console output ends with the done message; the number of other messages depends on
# whether debug is enabled.
output = capsys.readouterr().out.split("\n")
Expand Down
30 changes: 20 additions & 10 deletions tests/platforms/iOS/xcode/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ def test_run_app_simulator_booted(run_command, first_app_config, tmp_path):
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -309,7 +310,8 @@ def test_run_app_simulator_booted_underscore(
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -438,7 +440,8 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path):
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -573,7 +576,8 @@ def test_run_app_simulator_shut_down(
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -714,7 +718,8 @@ def test_run_app_simulator_shutting_down(run_command, first_app_config, tmp_path
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -1090,7 +1095,8 @@ def test_run_app_simulator_launch_failure(run_command, first_app_config, tmp_pat
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -1210,7 +1216,8 @@ def test_run_app_simulator_no_pid(run_command, first_app_config, tmp_path):
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -1332,7 +1339,8 @@ def test_run_app_simulator_non_integer_pid(run_command, first_app_config, tmp_pa
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -1433,7 +1441,8 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path):
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down Expand Up @@ -1548,7 +1557,8 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p
'senderImagePath ENDSWITH "/First App"'
' OR (processImagePath ENDSWITH "/First App"'
' AND (senderImagePath ENDSWITH "-iphonesimulator.so"'
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"))',
' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"'
' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))',
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
Expand Down
6 changes: 5 additions & 1 deletion tests/platforms/macOS/app/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def first_app_templated(first_app_config, tmp_path):
"""
[paths]
app_packages_path="First App.app/Contents/Resources/app_packages"
support_path="First App.app/Contents/Resources/support"
support_path="First App.app/Contents/Frameworks"
runtime_path="Python.xcframework/macos-arm64_x86_64/Python.framework"
info_plist_path="First App.app/Contents/Info.plist"
entitlements_path="Entitlements.plist"
""",
Expand Down Expand Up @@ -119,4 +120,7 @@ def first_app_with_binaries(first_app_templated, first_app_config, tmp_path):
with (lib_path / "unknown.binary").open("wb") as f:
f.write(b"\xCA\xFE\xBA\xBEother")

# A symlink to a Mach-O executable binary file
(lib_path / "first_symlink.so").symlink_to(lib_path / "first_so.so")

return first_app_config
1 change: 1 addition & 0 deletions tests/platforms/macOS/app/package/test_notarize.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def test_notarize_app(
"First App.app/Contents/Resources/app_packages/first.other",
"First App.app/Contents/Resources/app_packages/first_dylib.dylib",
"First App.app/Contents/Resources/app_packages/first_so.so",
"First App.app/Contents/Resources/app_packages/first_symlink.so",
"First App.app/Contents/Resources/app_packages/other_binary",
"First App.app/Contents/Resources/app_packages/second.other",
"First App.app/Contents/Resources/app_packages/special.binary",
Expand Down
1 change: 1 addition & 0 deletions tests/platforms/macOS/app/package/test_package_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def test_package_zip(
"First App.app/Contents/Resources/app_packages/first.other",
"First App.app/Contents/Resources/app_packages/first_dylib.dylib",
"First App.app/Contents/Resources/app_packages/first_so.so",
"First App.app/Contents/Resources/app_packages/first_symlink.so",
"First App.app/Contents/Resources/app_packages/other_binary",
"First App.app/Contents/Resources/app_packages/second.other",
"First App.app/Contents/Resources/app_packages/special.binary",
Expand Down
Loading

0 comments on commit 18d27ec

Please sign in to comment.