Browse Source

ducktype the PyInstaller BUNDLE

Should be an acceptable workaround for pyinstaller/pyinstaller#6612

The `assemble` method is probably to0 specific to contribute with
a PR against PyInstaller.

Contributes to CURA-9365
j.spijker@ultimaker.com 2 years ago
parent
commit
b84ee29627
2 changed files with 185 additions and 6 deletions
  1. 184 2
      Ultimaker-Cura.spec.jinja
  2. 1 4
      requirements-dev.txt

+ 184 - 2
Ultimaker-Cura.spec.jinja

@@ -1,6 +1,7 @@
 # -*- mode: python ; coding: utf-8 -*-
-from PyInstaller.utils.hooks import collect_all
 import os
+from PyInstaller.utils.hooks import collect_all
+
 
 datas = {{ datas }}
 binaries = {{ binaries }}
@@ -60,7 +61,188 @@ coll = COLLECT(
     name=r'{{ name }}'
 )
 
-{% if macos == true %}app = BUNDLE(
+{% if macos == true %}
+# PyInstaller seems to copy everything in the resource folder for the MacOS, this causes issues with codesigning and notarizing
+# The folder structure should adhere to the one specified in Table 2-5
+# https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1
+# The class below is basically ducktyping the BUNDLE class of PyInstaller and using our own `assemble` method for more fine-grain and specific
+# control. Some code of the method below is copied from:
+# https://github.com/pyinstaller/pyinstaller/blob/22d1d2a5378228744cc95f14904dae1664df32c4/PyInstaller/building/osx.py#L115
+#-----------------------------------------------------------------------------
+# Copyright (c) 2005-2022, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License (version 2
+# or later) with exception for distributing the bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#
+# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
+#-----------------------------------------------------------------------------
+
+import plistlib
+import shutil
+import PyInstaller.utils.osx as osxutils
+from pathlib import Path
+from PyInstaller.building.osx import BUNDLE
+from PyInstaller.building.utils import (_check_path_overlap, _rmtree, add_suffix_to_extension, checkCache)
+from PyInstaller.building.datastruct import logger
+from PyInstaller.building.icon import normalize_icon_type
+
+
+class UMBUNDLE(BUNDLE):
+    def assemble(self):
+        from PyInstaller.config import CONF
+
+        if _check_path_overlap(self.name) and os.path.isdir(self.name):
+            _rmtree(self.name)
+        logger.info("Building BUNDLE %s", self.tocbasename)
+
+        # Create a minimal Mac bundle structure.
+        macos_path = Path(self.name, "Contents", "MacOS")
+        resources_path = Path(self.name, "Contents", "Resources")
+        frameworks_path = Path(self.name, "Contents", "Frameworks")
+        os.makedirs(macos_path)
+        os.makedirs(resources_path)
+        os.makedirs(frameworks_path)
+
+        # Makes sure the icon exists and attempts to convert to the proper format if applicable
+        self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
+
+        # Ensure icon path is absolute
+        self.icon = os.path.abspath(self.icon)
+
+        # Copy icns icon to Resources directory.
+        shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources'))
+
+        # Key/values for a minimal Info.plist file
+        info_plist_dict = {
+            "CFBundleDisplayName": self.appname,
+            "CFBundleName": self.appname,
+
+            # Required by 'codesign' utility.
+            # The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
+            # purposes. It even identifies the APP for access to restricted OS X areas like Keychain.
+            #
+            # The identifier used for signing must be globally unique. The usual form for this identifier is a
+            # hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
+            # name, followed by the department within the company, and ending with the product name. Usually in the
+            # form: com.mycompany.department.appname
+            # CLI option --osx-bundle-identifier sets this value.
+            "CFBundleIdentifier": self.bundle_identifier,
+            "CFBundleExecutable": os.path.basename(self.exename),
+            "CFBundleIconFile": os.path.basename(self.icon),
+            "CFBundleInfoDictionaryVersion": "6.0",
+            "CFBundlePackageType": "APPL",
+            "CFBundleShortVersionString": self.version,
+        }
+
+        # Set some default values. But they still can be overwritten by the user.
+        if self.console:
+            # Setting EXE console=True implies LSBackgroundOnly=True.
+            info_plist_dict['LSBackgroundOnly'] = True
+        else:
+            # Let's use high resolution by default.
+            info_plist_dict['NSHighResolutionCapable'] = True
+
+        # Merge info_plist settings from spec file
+        if isinstance(self.info_plist, dict) and self.info_plist:
+            info_plist_dict.update(self.info_plist)
+
+        plist_filename = os.path.join(self.name, "Contents", "Info.plist")
+        with open(plist_filename, "wb") as plist_fh:
+            plistlib.dump(info_plist_dict, plist_fh)
+
+        links = []
+        _QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PyQt6', 'PySide6'}
+        for inm, fnm, typ in self.toc:
+            # Adjust name for extensions, if applicable
+            inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
+            inm = Path(inm)
+            fnm = Path(fnm)
+            # Copy files from cache. This ensures that are used files with relative paths to dynamic library
+            # dependencies (@executable_path)
+            if typ in ('EXTENSION', 'BINARY') or (typ == 'DATA' and inm.suffix == '.so'):
+                if any(['.' in p for p in inm.parent.parts]):
+                    inm = Path(inm.name)
+                fnm = Path(checkCache(
+                    str(fnm),
+                    strip = self.strip,
+                    upx = self.upx,
+                    upx_exclude = self.upx_exclude,
+                    dist_nm = str(inm),
+                    target_arch = self.target_arch,
+                    codesign_identity = self.codesign_identity,
+                    entitlements_file = self.entitlements_file,
+                    strict_arch_validation = (typ == 'EXTENSION'),
+                ))
+                frame_dst = frameworks_path.joinpath(inm)
+                if not frame_dst.exists():
+                    if frame_dst.is_dir():
+                        os.makedirs(frame_dst, exist_ok = True)
+                    else:
+                        os.makedirs(frame_dst.parent, exist_ok = True)
+                shutil.copy(fnm, frame_dst, follow_symlinks = True)
+                macos_dst = macos_path.joinpath(inm)
+                if not macos_dst.exists():
+                    if macos_dst.is_dir():
+                        os.makedirs(macos_dst, exist_ok = True)
+                    else:
+                        os.makedirs(macos_dst.parent, exist_ok = True)
+
+                    # Create relative symlink to the framework
+                    symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Frameworks").joinpath(
+                        frame_dst.relative_to(frameworks_path))
+                    try:
+                        macos_dst.symlink_to(symlink_to)
+                    except FileExistsError:
+                        pass
+            else:
+                if typ == 'DATA':
+                    if any(['.' in p for p in inm.parent.parts]) or inm.suffix == '.so':
+                        # Skip info dist egg and some not needed folders in tcl and tk, since they all contain dots in their files
+                        logger.warning(f"Skipping DATA file {inm}")
+                        continue
+                    res_dst = resources_path.joinpath(inm)
+                    if not res_dst.exists():
+                        if res_dst.is_dir():
+                            os.makedirs(res_dst, exist_ok = True)
+                        else:
+                            os.makedirs(res_dst.parent, exist_ok = True)
+                    shutil.copy(fnm, res_dst, follow_symlinks = True)
+                    macos_dst = macos_path.joinpath(inm)
+                    if not macos_dst.exists():
+                        if macos_dst.is_dir():
+                            os.makedirs(macos_dst, exist_ok = True)
+                        else:
+                            os.makedirs(macos_dst.parent, exist_ok = True)
+
+                        # Create relative symlink to the resource
+                        symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Resources").joinpath(
+                            res_dst.relative_to(resources_path))
+                        try:
+                            macos_dst.symlink_to(symlink_to)
+                        except FileExistsError:
+                            pass
+                else:
+                    macos_dst = macos_path.joinpath(inm)
+                    if not macos_dst.exists():
+                        if macos_dst.is_dir():
+                            os.makedirs(macos_dst, exist_ok = True)
+                        else:
+                            os.makedirs(macos_dst.parent, exist_ok = True)
+                        shutil.copy(fnm, macos_dst, follow_symlinks = True)
+
+        # Sign the bundle
+        logger.info('Signing the BUNDLE...')
+        try:
+            osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep = True)
+        except Exception as e:
+            logger.warning(f"Error while signing the bundle: {e}")
+            logger.warning("You will need to sign the bundle manually!")
+
+        logger.info(f"Building BUNDLE {self.tocbasename} completed successfully.")
+
+app = UMBUNDLE(
     coll,
     name='{{ name }}.app',
     icon={{ icon }},

+ 1 - 4
requirements-dev.txt

@@ -1,7 +1,4 @@
 pytest
-
-# Fix for signing the MacOS App
-git+https://github.com/jellespijker/pyinstaller@binaries_in_frameworks#egg=pyinstaller
-#pyinstaller
+pyinstaller
 pyinstaller-hooks-contrib
 sip==6.5.1