Repository: beeware/briefcase
Pull request #971 (2022-11-14 22:37:58)

Sign frameworks in the canonical macOS app bundle location
The code-signing step currently signs mach-O binaries and Frameworks in the "Contents/Resources" folder of a macOS app bundle. Frameworks however are often (typically) found in the "Contents/Frameworks" folder.

The default Xcode and app templates used by briefcase currently don't bundle any Frameworks there. I'd argue that we should still code-sign anything found here because:

1. We might want to include Frameworks by default in the app bundle at some point.
2. Custom templates may include Frameworks and briefcase's code-signing step should still work with those, as long as they don't go against platform conventions.

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct





Patch:
diff --git a/changes/971.feature.rst b/changes/971.feature.rst
new file mode 100644
index 00000000..315d5fee
--- /dev/null
+++ b/changes/971.feature.rst
@@ -0,0 +1,1 @@
+Frameworks contained added to a macOS app bundle are now automatically code signed.
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index 28213707..beced44b 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -270,19 +270,23 @@ or
         """
         bundle_path = self.binary_path(app)
         resources_path = bundle_path / "Contents" / "Resources"
+        frameworks_path = bundle_path / "Contents" / "Frameworks"
 
-        # Sign all Mach-O executable objects
-        sign_targets = [
-            path
-            for path in resources_path.rglob("*")
-            if not path.is_dir() and is_mach_o_binary(path)
-        ]
+        sign_targets = []
+
+        for folder in (resources_path, frameworks_path):
+            # Sign all Mach-O executable objects
+            sign_targets.extend(
+                path
+                for path in folder.rglob("*")
+                if not path.is_dir() and is_mach_o_binary(path)
+            )
 
-        # Sign all embedded frameworks
-        sign_targets.extend(resources_path.rglob("*.framework"))
+            # Sign all embedded frameworks
+            sign_targets.extend(folder.rglob("*.framework"))
 
-        # Sign all embedded app objets
-        sign_targets.extend(resources_path.rglob("*.app"))
+            # Sign all embedded app objets
+            sign_targets.extend(folder.rglob("*.app"))
 
         # Sign the bundle path itself
         sign_targets.append(bundle_path)


Test patch:
diff --git a/tests/platforms/macOS/app/conftest.py b/tests/platforms/macOS/app/conftest.py
index dce51097..3804ed1a 100644
--- a/tests/platforms/macOS/app/conftest.py
+++ b/tests/platforms/macOS/app/conftest.py
@@ -9,6 +9,8 @@ def first_app_with_binaries(first_app_config, tmp_path):
     # Create some libraries that need to be signed.
     app_path = tmp_path / "base_path" / "macOS" / "app" / "First App" / "First App.app"
     lib_path = app_path / "Contents" / "Resources"
+    frameworks_path = app_path / "Contents" / "Frameworks"
+
     for lib in [
         "first_so.so",
         Path("subfolder") / "second_so.so",
@@ -31,8 +33,12 @@ def first_app_with_binaries(first_app_config, tmp_path):
         f.write(b"\xCA\xFE\xBA\xBEBinary content here")
 
     # An embedded framework
-    (lib_path / "Extras.framework" / "Resources").mkdir(parents=True, exist_ok=True)
-    with (lib_path / "Extras.framework" / "Resources" / "extras.dylib").open("wb") as f:
+    (frameworks_path / "Extras.framework" / "Resources").mkdir(
+        parents=True, exist_ok=True
+    )
+    with (frameworks_path / "Extras.framework" / "Resources" / "extras.dylib").open(
+        "wb"
+    ) as f:
         f.write(b"\xCA\xFE\xBA\xBEBinary content here")
 
     # Make sure there are some files in the bundle that *don't* need to be signed...
diff --git a/tests/platforms/macOS/app/test_signing.py b/tests/platforms/macOS/app/test_signing.py
index 16947fdb..f7082dcd 100644
--- a/tests/platforms/macOS/app/test_signing.py
+++ b/tests/platforms/macOS/app/test_signing.py
@@ -429,6 +429,7 @@ def test_sign_app(dummy_command, first_app_with_binaries, tmp_path):
     # * It traverses in "depth first" order
     app_path = tmp_path / "base_path" / "macOS" / "app" / "First App" / "First App.app"
     lib_path = app_path / "Contents" / "Resources"
+    frameworks_path = app_path / "Contents" / "Frameworks"
     dummy_command.tools.subprocess.run.assert_has_calls(
         [
             sign_call(tmp_path, lib_path / "subfolder" / "second_so.so"),
@@ -437,14 +438,15 @@ def test_sign_app(dummy_command, first_app_with_binaries, tmp_path):
             sign_call(tmp_path, lib_path / "other_binary"),
             sign_call(tmp_path, lib_path / "first_so.so"),
             sign_call(tmp_path, lib_path / "first_dylib.dylib"),
-            sign_call(
-                tmp_path, lib_path / "Extras.framework" / "Resources" / "extras.dylib"
-            ),
-            sign_call(tmp_path, lib_path / "Extras.framework"),
             sign_call(
                 tmp_path, lib_path / "Extras.app" / "Contents" / "MacOS" / "Extras"
             ),
             sign_call(tmp_path, lib_path / "Extras.app"),
+            sign_call(
+                tmp_path,
+                frameworks_path / "Extras.framework" / "Resources" / "extras.dylib",
+            ),
+            sign_call(tmp_path, frameworks_path / "Extras.framework"),
             sign_call(tmp_path, app_path),
         ],
         any_order=False,