Repository: pyinstaller/pyinstaller
Pull request: Clean up UPX handling
In the UPX availability check, do not attempt to parse the UPX version; just display the first line of the UPX output for debugging purposes. Closes #7702.

When disabling UPX on platforms with known compatibility issues (non-Windows), do so in the initial setup phase instead of during the build, right before calling the `upx`. Also display an INFO message informing the user that while UPX is available, it will not be used. 

Do not set default UPX parameters via the `UPX` environment variable; put those parameters on the command-line. This prevents us from having two different places in which parameters are configured, when we are only calling UPX in one place.

Lastly, for development purposes, allow UPX to be enabled on platforms with known compatibility issues, via an environment variable. 

Problem statement:
UPX version string parsing failure
Seems like you fail to parse the UPX version string correctly:
```
188 INFO: PyInstaller: 5.12.0
188 INFO: Python: 3.11.3
189 INFO: Platform: Linux-6.3.1-arch2-1-x86_64-with-glibc2.37
194 INFO: An exception occurred when testing for UPX:
194 INFO:   ValueError("invalid literal for int() with base 10: 'v4'")
194 INFO: UPX is not available.
```

That's what `upx --version` spews out:
```
upx v4.0.2
UCL data compression library 1.03
zlib data compression library 1.2.13.1-motley
LZMA SDK version 4.43
doctest C++ testing framework version 2.4.9
Copyright (C) 1996-2023 Markus Franz Xaver Johannes Oberhumer
Copyright (C) 1996-2023 Laszlo Molnar
Copyright (C) 2000-2023 John F. Reiser
Copyright (C) 2002-2023 Jens Medoch
Copyright (C) 1995-2022 Jean-loup Gailly and Mark Adler
Copyright (C) 1999-2006 Igor Pavlov
Copyright (C) 2016-2021 Viktor Kirilov
UPX comes with ABSOLUTELY NO WARRANTY; for details type 'upx -L'.
```

Hints from issue discussion:
Hmm, will look at it. Not that it makes much difference in the great scheme of things, because UPX is not used on linux even if it is available.

Commit message: building: utils: fix command info notice in checkCache

The informative notice about external command (upx or strip) being
ran in checkCache seems to have lost spaces between arguments
during cleanup in b87d426.

Patch to review:
diff --git a/PyInstaller/building/utils.py b/PyInstaller/building/utils.py
index 4683ae3df49..79d7d2747a3 100644
--- a/PyInstaller/building/utils.py
+++ b/PyInstaller/building/utils.py
@@ -272,7 +272,7 @@ def checkCache(
             bestopt = "--best"
             # FIXME: Linux builds of UPX do not seem to contain LZMA (they assert out).
             # A better configure-time check is due.
-            if CONF["hasUPX"] >= (3,) and os.name == "nt":
+            if CONF["hasUPX"] and os.name == "nt":
                 bestopt = "--lzma"
 
             upx_executable = "upx"
@@ -348,7 +348,7 @@ def checkCache(
                                     raise
 
     if cmd:
-        logger.info("Executing - " + "".join(cmd))
+        logger.info("Executing: %s", " ".join(cmd))
         subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
 
     # update cache index
diff --git a/PyInstaller/configure.py b/PyInstaller/configure.py
index 1417db96534..723e014b87e 100644
--- a/PyInstaller/configure.py
+++ b/PyInstaller/configure.py
@@ -13,59 +13,52 @@
 """
 
 import os
+import subprocess
 
 from PyInstaller import compat
 from PyInstaller import log as logging
-from PyInstaller.compat import is_darwin, is_win
 
 logger = logging.getLogger(__name__)
 
 
-def test_UPX(config, upx_dir):
-    logger.debug('Testing for UPX ...')
-    cmd = "upx"
+def _check_upx_availability(upx_dir):
+    logger.debug('Testing UPX availability ...')
+
+    upx_exe = "upx"
     if upx_dir:
-        cmd = os.path.normpath(os.path.join(upx_dir, cmd))
+        upx_exe = os.path.normpath(os.path.join(upx_dir, upx_exe))
 
-    hasUPX = 0
+    # Check if we can call `upx -V`.
     try:
-        vers = compat.exec_command(cmd, '-V', raise_enoent=True).strip().splitlines()
-        if vers:
-            v = vers[0].split()[1]
-            try:
-                # v = "3.96-git-d7ba31cab8ce"
-                v = v.split("-")[0]
-            except Exception:
-                pass
-            hasUPX = tuple(map(int, v.split(".")))
-            if is_win and hasUPX < (1, 92):
-                logger.error('UPX is too old! Python 2.4 under Windows requires UPX 1.92+.')
-                hasUPX = 0
-    except Exception as e:
-        if isinstance(e, OSError) and e.errno == 2:
-            # No such file or directory
-            pass
-        else:
-            logger.info('An exception occurred when testing for UPX:')
-            logger.info('  %r', e)
-    if hasUPX:
-        is_available = 'available'
-    else:
-        is_available = 'not available'
-    logger.info('UPX is %s.', is_available)
-    config['hasUPX'] = hasUPX
-    config['upx_dir'] = upx_dir
+        output = subprocess.check_output(
+            [upx_exe, '-V'],
+            stdin=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+            universal_newlines=True,
+        )
+    except Exception:
+        logger.debug('UPX is not available.')
+        return False
+
+    # Read the first line to display version string
+    try:
+        version_string = output.splitlines()[0]
+    except IndexError:
+        version_string = 'version string unavailable'
+
+    logger.debug('UPX is available: %s', version_string)
+    return True
 
 
-def _get_pyinst_cache_dir():
+def _get_pyinstaller_cache_dir():
     old_cache_dir = None
     if compat.getenv('PYINSTALLER_CONFIG_DIR'):
         cache_dir = compat.getenv('PYINSTALLER_CONFIG_DIR')
-    elif is_win:
+    elif compat.is_win:
         cache_dir = compat.getenv('LOCALAPPDATA')
         if not cache_dir:
             cache_dir = os.path.expanduser('~\\Application Data')
-    elif is_darwin:
+    elif compat.is_darwin:
         cache_dir = os.path.expanduser('~/Library/Application Support')
     else:
         # According to XDG specification: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
@@ -89,7 +82,9 @@ def _get_pyinst_cache_dir():
 
 def get_config(upx_dir, **kw):
     config = {}
-    test_UPX(config, upx_dir)
-    config['cachedir'] = _get_pyinst_cache_dir()
+
+    config['cachedir'] = _get_pyinstaller_cache_dir()
+    config['upx_dir'] = upx_dir
+    config['hasUPX'] = _check_upx_availability(upx_dir)
 
     return config


Code review comments:
diff_hunk: @@ -80,11 +80,24 @@ def _get_pyinstaller_cache_dir():
     return cache_dir
 
 
-def get_config(upx_dir, **kw):
+def get_config(upx_dir=None):
     config = {}
 
     config['cachedir'] = _get_pyinstaller_cache_dir()
     config['upx_dir'] = upx_dir
-    config['hasUPX'] = _check_upx_availability(upx_dir)
+
+    # Disable UPX on non-Windows. Using UPX (3.96) on modern Linux shared libraries (for example, the python3.x.so
+    # shared library) seems to result in segmentation fault when they are dlopen'd. This happens in recent versions
+    # of Fedora and Ubuntu linux, as well as in Alpine containers. On macOS, UPX (3.96) fails with
+    # UnknownExecutableFormatException on most .dylibs (and interferes with code signature on other occasions). And
+    # even when it would succeed, compressed libraries cannot be (re)signed due to failed strict validation.
+    upx_available = _check_upx_availability(upx_dir)
+    if upx_available:
+        if compat.is_win or compat.is_cygwin:
+            logger.info("UPX is available and will be used if enabled on build targets.")
+        else:
+            upx_available = False
+            logger.info("UPX is available but is disabled on this platform due to known compatibility problems.")
path: PyInstaller/configure.py
text: @user1:
```suggestion
            logger.info("UPX is available but is disabled on non-Windows due to known compatibility problems.")
```
?