From 7eb24b3dc0ac3f698c8f8a1645a8401be33cb624 Mon Sep 17 00:00:00 2001 From: Zeid Zabaneh Date: Mon, 31 Jul 2023 10:22:00 -0400 Subject: [PATCH] gui: add splash screen hook (bug 1845573) - create new BUNDLE_WITH_TK class - add required tcl/tk files to "lib" directory in bundle - force resigning of bundle after it is assembled - add splash runtime hook that shows a loading message --- gui/gui.spec | 7 +- gui/splash_hook.py | 12 ++++ mozregression/pyinstaller.py | 133 +++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 gui/splash_hook.py create mode 100644 mozregression/pyinstaller.py diff --git a/gui/gui.spec b/gui/gui.spec index 4e591a35b..81c436768 100644 --- a/gui/gui.spec +++ b/gui/gui.spec @@ -1,7 +1,10 @@ # -*- mode: python -*- import sys + from PyInstaller.utils.hooks import collect_all, collect_submodules +from mozregression.pyinstaller import BUNDLE_WITH_TK + IS_MAC = sys.platform == "darwin" block_cipher = None @@ -19,7 +22,7 @@ a = Analysis( datas=datas, hiddenimports=hiddenimports, hookspath=[], - runtime_hooks=[], + runtime_hooks=["splash_hook.py"], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, @@ -64,7 +67,7 @@ if IS_MAC: console=False, target_arch="universal2", ) - app = BUNDLE( + app = BUNDLE_WITH_TK( exe, strip=False, upx=False, diff --git a/gui/splash_hook.py b/gui/splash_hook.py new file mode 100644 index 000000000..f7372d5c0 --- /dev/null +++ b/gui/splash_hook.py @@ -0,0 +1,12 @@ +import tkinter +from mozregression import version + +win = tkinter.Tk() +tkinter.Label(win, text=f"mozregression {version} starting, please wait...").pack(pady=20, padx=20) + +# Though the window is destroyed after 1 second, it persists until the +# bootloader is finished. This is hacky but seems to work. +win.after(1000, lambda: win.destroy()) +win.overrideredirect(True) +win.eval("tk::PlaceWindow . center") +win.mainloop() diff --git a/mozregression/pyinstaller.py b/mozregression/pyinstaller.py new file mode 100644 index 000000000..737d303df --- /dev/null +++ b/mozregression/pyinstaller.py @@ -0,0 +1,133 @@ +"""PyInstaller helper module to enable packaging tcl/tk with app bundles.""" + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +# ---------------------------------------------------------------------------- +# Copyright (c) 2005-2023, 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) +# ---------------------------------------------------------------------------- + +# Some code in this module is copied and modified from the original version. +# The original version can be found at https://github.com/pyinstaller/pyinstaller. +# See also: https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt + +import importlib +import os +import shutil +from pathlib import Path + +import PyInstaller.log as logging +from PyInstaller.building.datastruct import Tree +from PyInstaller.building.osx import BUNDLE +from PyInstaller.compat import exec_command_all, is_darwin +from PyInstaller.utils.hooks.tcl_tk import ( + _collect_tcl_modules, + _find_tcl_tk, + find_tcl_tk_shared_libs, +) + +logger = logging.getLogger(__name__) + + +class BUNDLE_WITH_TK(BUNDLE): + """A BUNDLE class that incorporates tcl/tk libraries.""" + + def __init__(self, *args, **kwargs): + if not is_darwin: + # This class can only be used on macOS. + return + super().__init__(*args, **kwargs) + + def assemble(self): + super().assemble() + self.post_assemble() + self.resign_binary() + + def post_assemble(self): + """Collect tcl/tk library/module files and add them to the bundle.""" + + # These files will not be needed (originally excluded by PyInstaller). + EXCLUDES = ["demos", "*.lib", "*Config.sh"] + + # Discover location of tcl/tk dynamic library files (i.e., dylib). + _tkinter_module = importlib.import_module("_tkinter") + _tkinter_file = _tkinter_module.__file__ + tcl_lib, tk_lib = find_tcl_tk_shared_libs(_tkinter_file) + + # Determine full path/location of tcl/tk libraries. + tcl_root, tk_root = _find_tcl_tk(_tkinter_file) + + # Determine original prefixes to put all the library files in. + tcl_prefix, tk_prefix = Path(tcl_root).parts[-1], Path(tk_root).parts[-1] + + # Create Tree objects with all the files that need to be included in the bundle. + # Tree objects are a way of creating a table of contents describing everything in + # the provided root directory. + tcltree = Tree(tcl_root, prefix=tcl_prefix, excludes=EXCLUDES) + tktree = Tree(tk_root, prefix=tk_prefix, excludes=EXCLUDES) + tclmodulestree = _collect_tcl_modules(tcl_root) + tcl_tk_files = tcltree + tktree + tclmodulestree + + # Use Tree object to list out files that will be copied. + links = [(inm, fnm) for inm, fnm, typ in tcl_tk_files] + + # Append dynamic library files. + links.append((tcl_lib[0], tcl_lib[1])) + links.append((tk_lib[0], tk_lib[1])) + + # Create "lib" directory in the .app bundle. + lib_dir = os.path.join(self.name, "Contents", "lib") + os.makedirs(lib_dir) + + # Iterate over all files and copy them into "lib" directory. + # The rest of the code in this method is copied and adapted from the + # PyInstaller.building.osx module. + for inm, fnm in links: + tofnm = os.path.join(lib_dir, inm) + todir = os.path.dirname(tofnm) + if not os.path.exists(todir): + os.makedirs(todir) + if os.path.isdir(fnm): + shutil.copytree(fnm, tofnm) + else: + shutil.copy(fnm, tofnm) + + def resign_binary(self): + """Force resigning of the .app bundle.""" + # The code in this method is copied and modified from the original code + # in PyInstaller.utils.osx.sign_binary. It was modified to allow force + # signing of the bundle, and assumes the ad-hoc identity. + args = [] + if self.entitlements_file: + args.append("--entitlements") + args.append(self.entitlements_file) + args.append("--deep") + args.append("--force") + cmd = [ + "codesign", + "-s", + "-", + "--force", + "--all-architectures", + "--timestamp", + *args, + self.name, + ] + retcode, stdout, stderr = exec_command_all(*cmd) + if retcode != 0: + logger.warning( + "codesign command (%r) failed with error code %d!\n" "stdout: %r\n" "stderr: %r", + cmd, + retcode, + stdout, + stderr, + ) + raise SystemError("codesign failure!")