Repository: pyinstaller/pyinstaller

Issue:
Increase `codesign` timeout
The timeout for the `codesign` command in PyInstaller on macOS is set to 60 seconds. While this might seem like a long time, for our fat application being build on GitHub Actions, this is quite often not long enough.
Especially when it's busy at GitHub Actions, runners can get limited resources and it will take longer to complete such system calls.

Could this be extended to something longer? Or make it configurable?

Example: https://github.com/sabnzbd/sabnzbd/runs/5408721960?check_suite_focus=true#step:9:194
Here we let PyInstaller sign using our real identity, but I also have plenty of runs where the ad-hoc signing and/or the signature-removal failed due to the timeout being reached.


Discussion:
Hmm, we *could* make the timeout proportional to the size of the executable...
Can you give us some numbers? i.e. How big is this executable and how long does it need to sign?
This is similar to the report we had in #6536. I think we should have a longer timeout (e.g., 10 minutes) at least for bundle code-signing and for running that PKG embedding objcopy call on linux. Which means we'll need to add a timeout argument to `exec_command` (and higher-level uses of it, if applicable) - the current hard-coded default is [here](https://github.com/pyinstaller/pyinstaller/blob/56b2dd51d30103e9200496c0ec5b5a11e7bf546e/PyInstaller/compat.py#L350). EDIT: both operations actually use `exec_command_all`, with hard-coded default [here](https://github.com/pyinstaller/pyinstaller/blob/56b2dd51d30103e9200496c0ec5b5a11e7bf546e/PyInstaller/compat.py#L489).

But perhaps we should also make both the "short" (default 1-minute) timeout and "long" timeout value a variable in `PyInstaller.compat`, so that they can be overridden at the start of the spec file (or within custom build system that integrates PyInstaller), if necessary.
> Which means we'll need to add a timeout argument to exec_command (and higher-level uses of it, if applicable)

I'd rather just let them use `subprocess.run()` directly. Those `exec_*()` functions are all cases of [xkcd 927](https://xkcd.com/927/) to me.
> Can you give us some numbers? i.e. How big is this executable and how long does it need to sign?

FWIW: uncompressed it's 70MB with a couple of sub-binaries that we include and of course all the Python `*.so` files. It's `universal2` now, so very fat, and needing signatures for multiple architectures.
Sometimes the signing only takes 20 seconds, it really seems to depend on the resources the GitHub Actions runner has at the moment.

So a configurable option would be great.
@rokm Do you think that there really is any need for `objdump` to have a timeout? I can't say that I've ever seen it stall.
> @rokm Do you think that there really is any need for `objdump` to have a timeout? I can't say that I've ever seen it stall.

I wonder the same about `codesign`, why would it stall? It fails or succeeds, or have you seen otherwise? 
Neither command really needs a timeout, as far as I'm concerned. 

The current situation came about with global introduction of `exec_command_*` timeouts in 0bd3ec8a4bbe24bbc7e4cea27bc9561569c2d79b, which aimed to remedy the CI pipelines getting stuck sometimes (whatever the reason for those really was).
So perhaps the fastest and least painful fix for this would indeed be to migrate these two commands to use `subprocess.run` directly. I'd personally prefer to have the codebase consistently use either `subprocess.run` or the `exec_*` functions, but purging the latter from the codebase will probably be quite a hassle...

Resolving patch:
diff --git a/PyInstaller/building/api.py b/PyInstaller/building/api.py
--- a/PyInstaller/building/api.py
+++ b/PyInstaller/building/api.py
@@ -16,6 +16,7 @@
 """
 
 import os
+import subprocess
 import time
 import pprint
 import shutil
@@ -29,7 +30,7 @@
     _check_guts_toc, _make_clean_directory, _rmtree, add_suffix_to_extension, checkCache, get_code_object,
     strip_paths_in_code
 )
-from PyInstaller.compat import (exec_command_all, is_cygwin, is_darwin, is_linux, is_win)
+from PyInstaller.compat import (is_cygwin, is_darwin, is_linux, is_win)
 from PyInstaller.depend import bindepend
 from PyInstaller.depend.analysis import get_bootstrap_modules
 from PyInstaller.depend.utils import is_path_to_egg
@@ -721,16 +722,11 @@ def assemble(self):
         if is_linux:
             # Linux: append data into custom ELF section using objcopy.
             logger.info("Appending %s to custom ELF section in EXE", append_type)
-            retcode, stdout, stderr = exec_command_all(
-                'objcopy', '--add-section', 'pydata=%s' % append_file, build_name
-            )
-            logger.debug("objcopy returned %i", retcode)
-            if stdout:
-                logger.debug(stdout)
-            if stderr:
-                logger.debug(stderr)
-            if retcode != 0:
-                raise SystemError("objcopy Failure: %s" % stderr)
+            cmd = ['objcopy', '--add-section', f'pydata={append_file}', build_name]
+            p = subprocess.run(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, universal_newlines=True)
+            if p.returncode:
+                raise SystemError(f"objcopy Failure: {p.returncode} {p.stdout}")
+
         elif is_darwin:
             # macOS: remove signature, append data, and fix-up headers so that the appended data appears to be part of
             # the executable (which is required by strict validation during code-signing).
diff --git a/PyInstaller/utils/osx.py b/PyInstaller/utils/osx.py
--- a/PyInstaller/utils/osx.py
+++ b/PyInstaller/utils/osx.py
@@ -14,13 +14,14 @@
 
 import math
 import os
+import subprocess
 import shutil
 
 from macholib.mach_o import LC_BUILD_VERSION, LC_CODE_SIGNATURE, LC_SEGMENT_64, LC_SYMTAB, LC_VERSION_MIN_MACOSX
 from macholib.MachO import MachO
 
 import PyInstaller.log as logging
-from PyInstaller.compat import base_prefix, exec_command_all
+from PyInstaller.compat import base_prefix
 
 logger = logging.getLogger(__name__)
 
@@ -292,12 +293,9 @@ def convert_binary_to_thin_arch(filename, thin_arch):
     Convert the given fat binary into thin one with the specified target architecture.
     """
     cmd_args = ['lipo', '-thin', thin_arch, filename, '-output', filename]
-    retcode, stdout, stderr = exec_command_all(*cmd_args)
-    if retcode != 0:
-        logger.warning(
-            "lipo command (%r) failed with error code %d!\nstdout: %r\nstderr: %r", cmd_args, retcode, stdout, stderr
-        )
-        raise SystemError("lipo failure!")
+    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
+    if p.returncode:
+        raise SystemError(f"lipo command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}")
 
 
 def binary_to_target_arch(filename, target_arch, display_name=None):
@@ -334,14 +332,9 @@ def remove_signature_from_binary(filename):
     """
     logger.debug("Removing signature from file %r", filename)
     cmd_args = ['codesign', '--remove', '--all-architectures', filename]
-    retcode, stdout, stderr = exec_command_all(*cmd_args)
-    if retcode != 0:
-        logger.warning(
-            "codesign command (%r) failed with error code %d!\n"
-            "stdout: %r\n"
-            "stderr: %r", cmd_args, retcode, stdout, stderr
-        )
-        raise SystemError("codesign failure!")
+    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
+    if p.returncode:
+        raise SystemError(f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}")
 
 
 def sign_binary(filename, identity=None, entitlements_file=None, deep=False):
@@ -361,11 +354,6 @@ def sign_binary(filename, identity=None, entitlements_file=None, deep=False):
 
     logger.debug("Signing file %r", filename)
     cmd_args = ['codesign', '-s', identity, '--force', '--all-architectures', '--timestamp', *extra_args, filename]
-    retcode, stdout, stderr = exec_command_all(*cmd_args)
-    if retcode != 0:
-        logger.warning(
-            "codesign command (%r) failed with error code %d!\n"
-            "stdout: %r\n"
-            "stderr: %r", cmd_args, retcode, stdout, stderr
-        )
-        raise SystemError("codesign failure!")
+    p = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
+    if p.returncode:
+        raise SystemError(f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}")


Test patch: