import glob import os import shutil import subprocess from pathlib import Path from typing import List, Tuple from PyInstaller.building.api import PYZ, EXE, COLLECT from PyInstaller.building.build_main import Analysis from PyInstaller.building.datastruct import TOC from corrscope import version as v block_cipher = None def keep(dir, wildcard): includes = glob.glob(f"{dir}/{wildcard}") assert includes, f"{dir}, {wildcard} has no matches" return [(include, dir) for include in includes] InFile = str OutFolder = str datas: List[Tuple[InFile, OutFolder]] = [] version = v.pyinstaller_write_version() datas += [(str(v.version_txt), ".")] app_name = "corrscope" app_name_version = f"{app_name}-{version}" a = Analysis( ["corrscope/__main__.py"], pathex=["."], binaries=[], datas=datas, hiddenimports=["corrscope.gui.__init__"], hookspath=[], runtime_hooks=[], excludes=["FixTk", "tcl", "tk", "_tkinter", "tkinter", "Tkinter"], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, ) # Some dirs are included by PyInstaller hooks and must be removed after the fact. _path_excludes = ( # Matplotlib # Deleting mpl-data/fonts and rendering with an unrecognized font causes matplotlib # to enter an infinite recursive loop of findfont(...DejaVu Sans) and crash. # So don't delete the default fonts. """ mpl-data/images mpl-data/sample_data """ # PyQt """ Qt5DBus.dll Qt5Network.dll Qt5Quick.dll Qt5Qml.dll Qt5Svg.dll Qt5WebSockets.dll """ # QML file list taken from https://github.com/pyinstaller/pyinstaller/blob/0f31b35fe96de59e1a6faf692340a9ef93492472/PyInstaller/hooks/hook-PyQt5.py#L55 """ libEGL.dll libGLESv2.dll d3dcompiler_ opengl32sw.dll """ ).split() path_excludes = {s.lower() for s in _path_excludes} def path_contains(path: str) -> bool: path = path.replace("\\", "/").lower() ret = any(x in path for x in path_excludes) return ret # A TOC appears to be a list of tuples of the form (name, path, typecode). # In fact, it's an ordered set, not a list. # A TOC contains no duplicates, where uniqueness is based on name only. def strip(arr: TOC): arr[:] = [ (name, path, typecode) for (name, path, typecode) in arr if not path_contains(path) ] strip(a.datas) strip(a.binaries) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name=app_name, debug=False, bootloader_ignore_signals=False, strip=False, upx=True, console=True, ) class ZipCollect(COLLECT): name: str # dist/dir-name, != __init__(name=) def assemble(self): ret = super().assemble() if shutil.which("7z"): cmd_7z = "7z a -mx=3".split() # Don't use Path.with_suffix(), it removes trailing semver components. out_name = str(Path(self.name).with_name(app_name_version)) + ".7z" in_files = f"{self.name}/*" # asterisk removes root directory from archive subprocess.run(cmd_7z + [out_name, in_files], check=True) assert os.path.getsize(out_name) > 2 ** 20 return ret coll = ZipCollect( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, name=app_name )