From b84ee29627c995090de9f7b05f1e2b2462c2b3b3 Mon Sep 17 00:00:00 2001 From: "j.spijker@ultimaker.com" Date: Thu, 7 Jul 2022 05:38:08 +0200 Subject: 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 --- Ultimaker-Cura.spec.jinja | 186 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 2 deletions(-) (limited to 'Ultimaker-Cura.spec.jinja') diff --git a/Ultimaker-Cura.spec.jinja b/Ultimaker-Cura.spec.jinja index 494f5858d5..69ba80c5c4 100644 --- a/Ultimaker-Cura.spec.jinja +++ b/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 }}, -- cgit v1.2.3