Repository: beeware/briefcase

Issue:
Adhoc signing happens too often
Adhoc signing of every .so or .dylib in the app takes at least 7 seconds on my machine for a minimal app, and 15 seconds for [this app](https://github.com/mhsmith/HackdaysAIImageGenerator). So it's annoying that it happens every time I run `briefcase run -u`, and unnecessary because that command isn't capable of changing any of the signed files.

Similarly, `briefcase package -u` will adhoc sign the app, and then properly sign it all over again.

Discussion:
@freakboy3742: Definitely agreed that some optimisation is called for here. 

Some notes for potential implementors:
1. We can't completely drop signing on `briefcase run -u` - while we don't need to resign the app_packages, the signing of the app bundle as a whole is needed, because the app content has changed. 
2. Build currently automatically does an automatic adhoc sign to ensure that `run` has a binary that can run locally. However, when invoking `package`, that signing during `build` is redundant. The good news is that the `kwargs` passed to `build_app` will have `identity` and `adhoc_sign` provided as arguments if it is being invoked transitively from a package command; this could be used as a signal to ignore the signing pass on build.
3. There is some crossover with #865; anyone tackling this issue may want to look into that one as well.
4. There is also potentially some crossover with #807. Part of the underlying issue here is that the "dirty" state of the app includes whether the file has been updated since it was last signed. Updating a single binary file should only require that single file to be re-signed; the current process does a complete re-sign, regardless of whether a file has been altered. Addressing #807 is a much bigger change; we can make significant improvements to workflows without introducing a full "dirty state" tracking system.

@sjirwin: Working on this one

Solution patch:
diff --git a/changes/1099.bugfix.rst b/changes/1099.bugfix.rst
new file mode 100644
index 000000000..4886c19d9
--- /dev/null
+++ b/changes/1099.bugfix.rst
@@ -0,0 +1 @@
+A redundant signing phase has been removed when packaging an apps for macOS.
diff --git a/src/briefcase/platforms/macOS/app.py b/src/briefcase/platforms/macOS/app.py
index d4da66510..bf08e9124 100644
--- a/src/briefcase/platforms/macOS/app.py
+++ b/src/briefcase/platforms/macOS/app.py
@@ -127,9 +127,14 @@ def build_app(self, app: AppConfig, **kwargs):
         # macOS apps don't have anything to compile, but they do need to be
         # signed to be able to execute on Apple Silicon hardware - even if it's only an
         # ad-hoc signing identity. Apply an ad-hoc signing identity to the
-        # app bundle.
-        self.console.info("Ad-hoc signing app...", prefix=app.app_name)
-        self.sign_app(app=app, identity=SigningIdentity())
+        # app bundle. However, the app only needs to be signed once. If the build is
+        # being done as part of a package command, the signing will happen during
+        # packaging. The two case can be distinguished because "adhoc_sign" will
+        # be absent from kwargs if only doing a build command, but will be present if
+        # the build is being done as part of a package command.
+        if "adhoc_sign" not in kwargs:
+            self.console.info("Ad-hoc signing app...", prefix=app.app_name)
+            self.sign_app(app=app, identity=SigningIdentity())
 
 
 class macOSAppRunCommand(macOSRunMixin, macOSAppMixin, RunCommand):


Test patch:
diff --git a/tests/platforms/macOS/app/test_build.py b/tests/platforms/macOS/app/test_build.py
index 825f3f51f..6e6baea1d 100644
--- a/tests/platforms/macOS/app/test_build.py
+++ b/tests/platforms/macOS/app/test_build.py
@@ -22,6 +22,9 @@ def build_command(tmp_path):
     return command
 
 
+@pytest.mark.parametrize(
+    "extra_args", [{}, {"adhoc_sign": True}, {"adhoc_sign": False}]
+)
 @pytest.mark.parametrize("universal_build", [True, False])
 @pytest.mark.parametrize("pre_existing", [True, False])
 @pytest.mark.parametrize("console_app", [True, False])
@@ -31,6 +34,7 @@ def test_build_app(
     console_app,
     universal_build,
     pre_existing,
+    extra_args,
     tmp_path,
 ):
     """A macOS App is ad-hoc signed as part of the build process."""
@@ -49,7 +53,9 @@ def test_build_app(
     build_command.ensure_thin_binary = mock.Mock()
 
     # Build the app
-    build_command.build_app(first_app_with_binaries, test_mode=False)
+    base_args = {"test_mode": False}
+    kwargs = base_args | extra_args
+    build_command.build_app(first_app_with_binaries, **kwargs)
 
     # The stub binary has been renamed
     assert not (exec_path / "Stub").is_file()
@@ -64,11 +70,17 @@ def test_build_app(
             arch="gothic",
         )
 
-    # A request has been made to sign the app
-    build_command.sign_app.assert_called_once_with(
-        app=first_app_with_binaries,
-        identity=SigningIdentity(),
-    )
+    # Verify that a request has been made to sign the app, but only when it is as part of a direct build command
+    if "adhoc_sign" in kwargs:
+        # build command was called as part of package command.
+        # expect no signing now since it will happen during packaging
+        build_command.sign_app.assert_not_called()
+    else:
+        # build command was called directly so expect signing
+        build_command.sign_app.assert_called_once_with(
+            app=first_app_with_binaries,
+            identity=SigningIdentity(),
+        )
 
     # No request to select a signing identity was made
     build_command.select_identity.assert_not_called()