#/usr/bin/env python3 r""" ================================================================================ Run this script in its own folder with the Python to embed in the standalone: $ cd $C/pymailgui/PyMailGui-PP4E/PP4E/Gui/TextEditor/build/build-app-exe/build4.0 $ python3.12 build.py $ cd C:\Users\me\Desktop\MY-STUFF\Code\...\build4.0 (or \Users\me\Downloads\...) $ py -3.12 build.py On macOS, a .spec file must be moved after the first run and edited/maintained. Also on macOS, first do the following to pickup 3.12's pyinstaller (not 3.8's!): source ~/use312 AND ADD THIS TO ~/bash_profile else not propagated through os.system()... AND reinstall pyinstaller after this b/c was installed when PATH front = 3.8... (may have caused earlier issues, including --add-data fails?...) macOS universal2 issue (resolved): For universal2 on on macOS, also run the following to build a macOS fat binary for psutil?: rm /Users/me/Library/Caches/pip/wheels/39/e8/ef/2670...8b99/\ psutil-6.1.0-cp36-abi3-macosx_10_9_universal2.whl (or use pip --no-cache-dir) pips uninstall psutil python3.12 -m pip install --no-cache-dir --no-binary :all: psutil This didn't work either, because pip always selects/build for the host architecture only. Which is fine for users of a package, but arguably a bug for builders of packages: py2app and PyInstaller require all libs to be fat/universal2, so pip can't be used to install package components. => FIX IS AHEAD: grab x86 + arm64 wheels, 'fuse', pip force reinstall (like PIL in PPUS) History, latest first: - Adapted from Frigcal's build.py -- but here for PyEdit and suproxproxy - Adapted from PPUS's pc-build/build.py -- but here for Tkinter, not Kivy - Adapted from Mergeall's Windows-exe build.py FOR PyEdit: there is no Kivy (only tkinter), PIL is unused, psutil is required, and there are no supplemental console and GUI tool executables. There is an extra subprocproxy executable used by Run Code in frozen PyEdits sans user Pyton configs that NO LONGER embeds full Py stdlib (a py3.10+ sys.stdlib_module_names stdlib collection was abandoned due to its complexity and the startup times of a large frozen proxy; ahead). Use PyInstaller to make a Windows or macOS single-file executable or app from the app's main.py; copy extra data items to its folder; and zip the result for distribution. Unlike prior versions, this script is coded to work portably on Windows and macOS (not Linux). py2app was formerly used on macOS but adds an extra convolution. TO USE - on all three PC platforms: - Ensure that app runs as source code - Install dependency if needed: [pip3.12]|[py -3.12 -m pip] install psutil - Edit paths in code here if needed (now mostly moot) - Install PyInstaller in a console with: pip3.12 install pyinstaller - Download and unzip ziptools in ~/Downloads from learning-python.com - Run this script in its own folder (build40/) with: python3.12 build.py - The Python used to run it is crucial - this is what gets embedded HOW TO BUILD+RELEASE (verify all copies along the way): 1) Source: [python3.12 build.py] in build/build-source Builds a a zipfile containg full source, sans private items scubbed run on dev macbookpro with python3.12 (not macbook used for ppus) 2) macOS app: run build.py in its folder to make zip. Dev folder is: $C/pymailgui/PyMailGui-PP4E/PP4E/Gui/TextEditor/build/build-app-exe/build4.0/build.py though there is also an equiv symlink at $C/pymailgui/TextEditor. Builds a zipfile containing a --onedir universal2 (Intel+M) .app folder run on dev macbookpro with python3.12 (not macbook used for ppus) 3) Windows exe: either run out of synced $C (or copy $C dev folder from SSD to Desktop\temp and rename it as PyEdit); either way, run its build40/build.py, and copy resulting zip back to SSD. then to MBP. Builds a zipfile containing a --onefile folder with 64-bit .exe run on 2024 dell xps 14, user=me, [py -3.12 build.py] 4) THEN: copy Windows zips from SSD to macOS build40/; copy all 3 zips to $W/Programs/Current/COmplete/pyedit-products; unzip source and rename to unzipped/; copy __private__ screenshots to unzipped's docetc/4.0-screenshots; $W/_PUBLISH.py; upload pyedit-products/. x) (On Linux: punt - no longer building exes on Linux due to massive potential for library skew: use source-code package instead). On Windows only, also edit and run _winver-maker.py once (see its docs), and copy the .spec file made on first run from temp-build-ppus/ to file pc-build/_Windows--PC-Phone USB Sync.spec (no edit to add 3 required lines for Kivy dependencies). The .spec is auto used on all later runs instead of command-line args. Linux doesn't use the .spec; kivy-deps doesn't exist for Linux. macOS sprouted a .spec later (see ahead). Unlike py2app, there is no setup.py file for PyInstaller. There can be a .spec files made on first build (or pyi-makespec), and passed to pyinstaller instead of main.py; this is required here for Windows only (not Linux or macOS). Mergeall instead uses py2app on macOS, but this can expose its source code, and requires different and extra handling. UPDATE: Widows requires no .spec file for PyEdit. UPDATE: macOS now uses a .spec create on first run too, just to set its app version number via spec-file-only BUNDLE arg; copy from tempfolder to pc-build/_macOS--PC-Phone USB Sync.spec and edit there. Required because modding the app version number by post-build manifest edits triggers an harsh warning about damage/tampering with no option to open anyhow (even though the app works after killing its quarantine attrs), and PyInstaller has no other support for version# on macOS (see weird Windows-only _winver). Why freeze? Frozen executables/apps: - Set program icons automatically - Match the platform's idioms and paradigms well - Require no Python or any other installs - Are immune to mods in Python and other libs and tools - Don't require users to know how to launch scripts OTOH, they can still break on radical platform/hardware changes (see Linux's libc and Apple's M chips), and don't benefit from tool upgrades. APP DATA ACCESS PyInstaller unzips --onefile exes and their added data to a temporary folder, and builds an app folder on macOS. These convolute the executable's access to data items at runtime. See main.py's "if hasattr(sys, 'frozen')" for policies on exe data access implemented here; all data docs were moved there. ============================================================================= """ #============================================================================ # Preliminaries and globals #============================================================================ import os, sys, shutil join, sep = os.path.join, os.path.sep # this script never used on Android (nor Linux) RunningOnMacOS = sys.platform.startswith('darwin') # intel and apple m (rosetta|not) RunningOnWindows = sys.platform.startswith('win') # Windows py, may be run by Cygwin RunningOnLinux = sys.platform.startswith('linux') # native, Windows WSL, Android RUN_IN_WINDOWS_DOWNLOADS = False # to build in Downloads instead of MY-STUFF/Code ($C) # run this script in its own dir (and mind the top-level name!) if not RUN_IN_WINDOWS_DOWNLOADS: assert os.getcwd().endswith(join(*'TextEditor/build/build-app-exe/build4.0'.split('/'))) # Python 3.X+ only assert int(sys.version[0]) >= 3 # macOS and Windows only assert RunningOnMacOS or RunningOnWindows appname = 'PyEdit' # spaces complicate windows zip cmd exename = appname # not: appname.replace(' ', '_') homedir = os.path.expanduser('~') platform = 'Windows' if RunningOnWindows else 'macOS' if RunningOnMacOS else 'Linux' forceico = len(sys.argv) > 1 # remake icon iff any arg (maybe: this is now cruft) startdir = os.getcwd() # this build script's dir: build4.0 (run this here!) python = sys.executable # py running this script (may have embedded spaces!) def FWP(path): """ allow long paths on Windows: pyedit auto-save files see mergeall and ziptools for tons of details probably not required in this app, but... """ return path if not RunningOnWindows else '\\\\?\\' + os.path.abspath(path) # drop macOS AppleDouble files from exFAT removable-drive copies; # else, they wind up in _MEI* temp unzips and elsewhere, and can puzzle; # this will run just once per drive copy, but no other good place for it; dropped = False for fname in os.listdir('.'): if fname.startswith('._'): dropped = True print('Dropping macOS cruft:', fname) os.remove(fname) if dropped: input('-Drops pause (press Enter)-') #============================================================================ # Make exe's icon if one doesn't already exist #============================================================================ # now handle this once manually, iff needed # NEW: allow pyinstaller to build a .ico or .icns from .png using pillow; # this seems to work reasonably on all platforms, and obviates iconify.py # UPDATE: no it doesn't - pyinstaller's auto icons botched rounded corners; # go with custom .ico/.icns icons built by iconify.py before this script runs; # UPDATE: except the custom .ico fails on Windows (why?) - use rounded .png; # the icon is still also set in the .py, else the kivi icon is used (why?); # UPDATE: Linux taskbar icons need a .desktop file, provided with the zip. """SKIP print('ICONS') iconship = join('..', '..', 'icons') iconmake = join('..', '..', 'build-icons') iconnames = ['mergeall'] # step into icon build dir and make for iconname in iconnames: iconfile = iconname + '.ico' if forceico or not os.path.exists(iconship + sep + iconfile): os.chdir(iconmake) # requires a py with Pillow installed, pre sized/arranged images # os.system('%s iconify.py -win images-%s %s' % (python, iconname, iconname)) os.system('py -3.3 iconify.py -win images-%s %s' % (iconname, iconname)) os.chdir(startdir) shutil.move(join(iconmake, iconfile), join(iconship, iconfile)) SKIP""" #============================================================================ # First: copy source tree to temp folder to avoid accidental code loss #============================================================================ print('TEMP COPY') devfolder = 'TextEditor' # in dev tree, not temp/build/dist # cp can't include itself tempfolder = join(homedir, 'Desktop', 'temp-build-pyedit') if os.path.exists(tempfolder): shutil.rmtree(FWP(tempfolder)) # rerun script if fails on macos... os.mkdir(tempfolder) # Omit moots/privates in the temp copy, so they're not added to built exes; # why does pyinstaller add these, and yet provide no good way to omit them? # --exclude-modules and excludes in .spec don't help; filtering may work in # the .spec file (and it's easy with a temp copy here); but why not an arg? skips = shutil.ignore_patterns( '__private__', '__pycache__', '_private_', '_README.html', '.htaccess', '.nomedia' 'build', 'docetc', 'UserGuide.html', '.DS_Store') # setup source tree in tempfolder/TextEditor if not RUN_IN_WINDOWS_DOWNLOADS: # copy entire PP4E tree, with nested TextEditor, to temp root up6 = ['..'] * 6 shutil.copytree(FWP(join(*up6, 'PP4E')), # relative to dev's build4.0 FWP(join(tempfolder, 'PP4E')), # copies dir, not its contents dirs_exist_ok=True, # copy~tempfolder for contents symlinks=True, # copy symlinks verbatim ignore=skips) # skip privates build dirs, etc # move nested TextEditor up to temp root shutil.move(join(tempfolder, 'PP4E', 'Gui', 'TextEditor'), tempfolder) # move PP4E down to be nested in temp's TextEditor shutil.move(join(tempfolder, 'PP4E'), join(tempfolder, 'TextEditor')) else: # copy unzipped source package, PP4E already nested in it up3 = ['..'] * 3 shutil.copytree(FWP(join(*up3)), FWP(join(tempfolder, 'TextEditor')), dirs_exist_ok=True, symlinks=True, ignore=skips) # cull more personal stuff autosaves = join(tempfolder, 'TextEditor', '__pyedit-autosaves__') for cutme in os.listdir(autosaves): if cutme != 'README-autosaves.txt': print('Dropping autosave:', cutme) os.remove(join(autosaves, cutme)) # MANUAL SPECFILE STEP: on macOS only, after run1, copy PyEdit.spec from # tempfolder to build40/, rename it to _macOS--PyEdit.spec, and add the # following to the end of the "app = Bundle(" section at the very end: """STARTCOPY✂️✂️✂️✂️ # [ML] - wth isn't this a cmd arg? (becomes info.plist CFBundleShortVersionString) version='4.0.0', # [ML] - extras for the info.plist file inside .app (some auto-added by PI) info_plist=dict( ##CFBundleIdentifier = 'com.learning-python.pyedit', # PI auto ##NSAppleScriptEnabled = False, # moot # cosmetic stuff CFBundleGetInfoString = 'Edit text. Run code. Have fun.', # used? NSHumanReadableCopyright = '© 2000-2024 M. Lutz, all rights reserved.', # declare common associatable types (easier and user-initiated on Windows); # types for which PyEdit is suggested: can still be selected for others; # else not in file "Open With" menus till made the default via navigation; CFBundleDocumentTypes = [ dict( CFBundleTypeExtensions = ['txt', 'html', # bread and butter text 'py', 'pyw', # ditto: python source code 'xml', 'ics', # xml docs, calendar data 'css', 'cgi', 'js', # web styles, some scripts 'c', 'cxx', 'h', # c/c++ development: pysrc 'spec', 'htaccess', # pyinstaller, apache, self 'plist'], CFBundleTypeName = 'Text-or-code file', CFBundleTypeIconFile = 'pyedit.icns', CFBundleTypeRole = 'Editor' ) ] ) ENDCOPY✂️✂️✂️✂️""" specfile = '_%s--PyEdit.spec' % platform # one per platform, in dev tree hasspec = os.path.exists(specfile) # none on run1, or linux (so far) if hasspec: # from cwd=dev's build40/, to temp copy root shutil.copy2(specfile, join(tempfolder, 'TextEditor', 'PyEdit.spec')) # drop platform # from cwd=dev's build40/, to temp copy root if RunningOnWindows: shutil.copy2('_winver.txt', join(tempfolder, 'TextEditor')) # windows-only oddment # ==> was in startdir = dev tree's build40/ <== # now goto temp copy's TE dir for next steps os.chdir(join(tempfolder, 'TextEditor')) # no FWP(): breaks! #============================================================================ # Build frozen PyEdit (main GUI) in temp dir's dist/ with pyinstaller # # On of two frozen executables built - subproxproxy's build differs ahead. # PyInstaller ignores icons on linux, though app's code may add to app bar # (which is not moot in PyEdit 4.0 - no Linux exe). #============================================================================ # BUILD TOOL # pyinstaller may not be on PATH if RunningOnWindows: pydir = os.path.dirname(sys.executable) # else py3.8's older pyinstaller [Frigcal 3.0] pyscripts = pydir + '\\Scripts\\' # where py installed, not the default [feb22] elif RunningOnMacOS: pyscripts = '' # assume pyinstaller on system path, no venv needed # BUILD TARGETS guifreeze = [ # freeze this into app/executable with --windowed, and --onefile | --onedir # script name on Windows+macOS first runs, and Linux always (simple pyinstaller use) # else manually-edited .spec file on Windows+macOS for runs 2+ (kivy-deps, version#) 'PyEdit.spec' if hasspec else 'textEditor.py', ] scriptfreeze = [ # extra executable spawned by Run Code to exec window's py code; # most of py stdlib baked in, but can else set py+stdlib via configs; # appears alongside PyEdit.exe so no extra extras required here; 'subprocproxy.py' ] extradatas = [ # ship these with executable, access in .py by cwd uniformly # tbd: use a 'tools' folder to reduce some folder clutter? # nit: this exposes the .kv, but it's useless without the .py # file still needed if build() manually, unless its code in .py 'textConfig.py', # ship these in install folder='.' 'README.txt', 'icons', # UPDATE SPEC FILES IS THIS LIST IS CHANGED 'tools', 'terms-of-use.txt', # legalese: caution, t-o-u, privacy 'subprocproxy.py', # for src PyEdit or user Python; shows up sans this ] extradatas = extradatas + scriptfreeze # moot here: no command-line script exes # BUILD COMMANDS # ==> *NOTE* builds2+ on macOS use .spec files here, not these commands # ==> specfiles generated in temp build tree on first run: copy to '.' and rename+mod # ==> *MUST* make changes in both places - this scheme seems bizarre and error-prone! # ==> *HAVE* been burned by this multiple times, requiring restarts of build tours... if RunningOnWindows: r""" -------------------------------------------------------------------- Allow for embedded quotes in command line on Windows (not just '%s\\pyinstaller' or '"%s\\pyinstaller"'). Windows requires a .spec file to add kivy-deps bits; really, and only on Windows; exes won't work without this, despite the thin Kivy/PyInstaller docs. Windows adds an odd version file, and all data items are at the top-level of the install/unzip folder, and found via cwd reset in main.py - including the created run-counter and configs-save files. The .kv file loaded by Kivy used to be in added to the install folder too, but Windows exes still run if it's not there... oddly. Unlike Linux, the .kv doesn't have to be an --add-data. The Windows build still has a few "critical" errors that popup unreadable message boxes and might be fixed with extra dep installs, but are harmless in this app (but see Linux build: fixing undefs shot size up badly). [4.0] no such errors in 2024's PyEdit Windows build. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Frigcal 3.0] Spec file not used on Windows for Frigcal 3.0 (or PyEdit 4.0): do not need Kivy deps/adds, and code need not be hidden (source-code package is complete). [Frigcal 3.0] PyInstaller splash screen need not be 512x512 (this is for buildozer on Android) and can have transparency, but must be PNG else converted to it - hence animated GIFs don't work. For Frigcal, there are two N-second delays: unzip of the exe, and load of calendar files. The prior manual animation in 2.0 runs only for the second (the only delay in py2app 2.0), but splash screen spans both. Tool exes open in 1-3 secs: no need for splash on these. [PyEdit 4.0] Ditto on both, but no calendar-file load, and there is a seprate delay to start frozen code proxies. -------------------------------------------------------------------- """ # build 1: create .spec file from args (Windows .spec file NOT used in PyEdit) extraargs1 = ( f' --splash icons{sep}pyedit-splash-256-textdots.png' # shown during load, win+lin only, not rounded ' --version-file _winver.txt' # windows-only: via _winver-maker.py; convoluted! ' --hidden-import ctypes' # windows-only: tkinter dpi scaling bug workaround ) buildcmd1 = ( # BAIL ON SPEC FILE FOR PYEDIT 4.0 - no kivy deps or codehiding here #f'cmd /C ""{pyscripts}pyi-makespec"' f'cmd /C ""{pyscripts}pyinstaller"' ' --onefile' # unzip to _MEIxxxxxx on start, extras in unzip dir ' --windowed' # no stdout/err console f' --name "{exename}"' # this instead of post rename ' --exclude-module textConfig' f' --icon icons{sep}pyedit.ico' # pyinstall/kivy auto, not iconify.py f' {extraargs1}' f' --paths {join(tempfolder, 'TextEditor')} ' # for fixfrozenpaths (why?) f' "%(target)s""' # target not set till for loops ahead ) # builds 2+: NOT USED BY PYEDIT - manually copied and edited spec file for kivy-deps buildcmd2 = ( f'cmd /C ""{pyscripts}pyinstaller" --clean' ' "%(target)s""' ) buildcmd = buildcmd1 if not hasspec else buildcmd2 elif RunningOnMacOS: r""" -------------------------------------------------------------------- PyEdit 4.0 switched from py2app to pyinstaller for macOS, but uses the --onedir option to avoid unzip times of --onefile. Already builds a .app folder, so shipping as a busy dir won't confuse. [Frigcal 3.0] There is a wicked-long delay when first running tools exes (~8 secs?), but PyInstaller doesn't support splash screens on macOS. These could be --onedir folders+apps, but that seems overkill; punt. [PyEdit 4.0] psutil's pip install is installing JUST the Intel binaries on an Intel macbook, even when forcing it to build from source, and PyInstaller (and presumably py2app) require all libs to be fat universal2 binaries. So must either ship seperate apps for Intel and Apple M, or learn how to make a psutil universal2 locally with either X-code's cmake or psutil's setup.py... See also note on this at top of this file for failed tries. UPDATE: now universal2 via same fuse trick for PPUS PIL => ahead [PyEdit 4.0] Lots of new issues here. --add-data put .py files (like textConfig.py) in the app's MacOS folder instead of Resources despite same code storing in Resources for Frigcal; fixfrozenpaths is not being found despite an added --hidden-import; and --noconsole makes subprocprxy a .app instead of a simple exec; fixfrozenaths required new code for Content/{MacOS, Resources} split in the .app; pyinstaller was using py3.8 until set PATH and reinstalled pi (and this may have caused other some other issues above); [PyEdit 4.0] not using --add-data has big implications for import and resource paths in fixfrozenpaths.py on macOS; see that file for info. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Prior notes from Kivy-based PPUS app... Buildozer was a fail and spec file not required, despite kivy docs! Also no splash screen, unzip lag, dpi issue, or logging errors. Update: macOS now requires a .spec file like Windows, because there is no other way to set app version number (manifest edits trigger a big scary warning about tampering, with no option to run anyhow). Unlike Windows+Linux, macOS uses --onedir, because already builds an app-bundle folder, and --onefile takes ~12 seconds to load with no suport for a splash screen, versus 1~2 seconds for --onedir. This exposes the .kv file's code, but it's not worth much sans .py. On macOS, all data items are added to app folder's Contents/MacOS via --add-data, and located via a cwd reset in the .py. This includes the .kv file: --onedir exe won't run without the .kv in Contents/MacOS. On macOS only, run-counter and config-save files are made in ~/Library, not install (app own-folder access is gray, though mergeall/mergeall_configs.py edits are proved persistent). Currently builds an X86_64 Intel 64-bit exe only (run a 'file' cmd on the MacOS/ exe). To build a universal2 app exe that supports M chips natively too, use [target_arch='universal2'] in the spec file, and use [--target-architecture universal2] here. This failed on Py 3.8's struct, with the following msg; Py 3.9+ may work, but libs must support M chips too, and the project has no way to verify this today: "" PyInstaller.utils.osx.IncompatibleBinaryArchError: /Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/ lib-dynload/_struct.cpython-38-darwin.so is not a fat binary! ERROR: build failed: 256 "" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Prior notes from Kivy-based PPUS app (Pydit variant ahead)... UPDATE: now _does_ build universal2 (intel+arm/M) binary on catalina mac book pro (not mac book: use py3.12). To make this work, install ziptools, python 3.10 (or later), pyinstaller, kivy, and pillow; add to PATH /Users/mini/Library/Python/3.10/bin, set $Z in ~/.bash_profile; and fetch+combine pillow intel and arm binaries into a 'fat' universal2 lib with steps here (pillow does not ship a combo universal2, weirdly): https://pillow.readthedocs.io/en/stable/installation.html#basic-installation ==> PIL (Pillow) is no longer used by Frigcal, after MonthWidows cut ==> PIL is never used in PyEdit, so the Pillow steps below are moot ==> BUT PyEdit has same reqs for universal2 psutil: see ahead Summary: % cd pc-build % python3 -m pip download --only-binary=:all: --platform macosx_10_15_x86_64 Pillow % python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow % python3 -m pip install delocate >>> from delocate.fuse import fuse_wheels >>> fuse_wheels('Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl', 'Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl', 'Pillow-9.5.0-cp310-cp310-macosx_10_15_universal2.whl') % pip3 install --force-reinstall Pillow-9.5.0-cp310-cp310-macosx_10_15_universal2.whl % python3 build.py And set target_architecture here + .spec. Builds a universal2 with both x86_64 and arm64 exes in Content/MacOS (per % file xxx), which is 34m (vs 21) and works on intel mb+mbp. macOS app IS now verified to work on arm too, via online vm setup in AWS. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ WHAT TRIED+WORKED FOR PYEDIT'S PSUTIL UNIVERSAL2 DEP: - fail: source code was a no (better off pub two diff apps) - fail: universalPip (uPip) 0.1.1 onPyPI (had to fix main script error, then worse) - manual fusing worked: (at PyPI's psutil page, download latest: psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl) $ cd ~/Downloads $ python3.12 -m pip install delocate $ pyton3.12 >>> from delocate.fuse import fuse_wheels >>> fuse_wheels('psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl', ... 'psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl', ... 'psutil-6.1.0-cp36-abi3-macosx_10_15_universal2.whl') ^D $ python3.12 -m pip install \ --force-reinstall psutil-6.1.0-cp36-abi3-macosx_10_15_universal2.whl $ upip --checkU psutil (verify now fat lib binaries) $ file /Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/\ site-packages/psutil/_psutil_osx.abi3.so $ file /Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/\ site-packages/psutil/_psutil_posix.abi3.so (use "--target-architecture universal2" here AND for subprocproxy build ahead) # cd (delete _macOS--Pyedit.spec) $ python3.12 build.py (recopy temp's Pyedit.spec to $C, rename, add lines => per above) $ python3.12 build.py $ file PyEdit.app/Contents/MacOS/PyEdit $ file PyEdit.app/Contents/Resources/subprocproxy -------------------------------------------------------------------- """ # build 1: create .spec file from args (macOS .spec file IS used in PyEdit) # files need '.' else in subdir extraargs1 = ' '.join( ' --add-data %s:%s' % (data, (data if os.path.isdir(data) else '.')) for data in extradatas ) extraargs1 = '' # PUNT AND COPY MANUALLY AHEAD [4.0] buildcmd1 = ( f'{pyscripts}pyi-makespec' # not recognized: --clean ##' --onefile' # (unzip to _MEIxxxxxx on start, always mod ma/configs.py) ' --onedir' # extras in app/Content/Resources folder, no unzip on start ' --windowed' # no console, make macos app-bundle folder, != --onedir f' --name "{exename}"' # now this instead of post rename ' --exclude-module textConfig' ' --icon icons/pyedit.icns' # iconify.py, not pyinstaller auto ' --target-architecture universal2' # mbp, py 3.6+, combine psutil wheels (above) ' --hidden-import fixfrozenpaths' f' --paths {join(tempfolder, 'TextEditor')} ' # for fixfrozenpaths (why?) f' {extraargs1}' ' %(target)s' # target not set till for loops ahead ) # builds 2+: use MANUALLY copied and edited spec file for macOS version #, etc => ABOVE buildcmd2 = ( f'{pyscripts}pyinstaller --clean' ' "%(target)s"' ) buildcmd = buildcmd1 if not hasspec else buildcmd2 # BUILD RUNS # INFO: The loops here are overkill in PyEdit. The main GUI's build # command differs on Windows and macOS, but the proxy's does not (much). # make main app/exe with cmds above (or specfiles) for target in guifreeze[:1]: print('\nBUILDING:', target) exitstat = os.system(buildcmd % dict(target=target)) if exitstat: print('ERROR: build failed:', exitstat) sys.exit(exitstat) # don't continue here if RunningOnMacOS and not hasspec: print('Please copy the .spec file now, as covered in build.py at MANUAL SPECFILE STEP') sys.exit() #============================================================================ # Build frozen subprocproxy, used by Run Code till user configs local Python. # # Proxy's exe is copied to the dist folder to appear in app or exe folder, # where it will be located by Run Code via code in fixfrozenpaths.py. #============================================================================ #---------------------------------------------------------------------------------------- # [4.0] TBD: Won't a Run Code frozen subprocproxy take a long time to unzip? # In PE 3.0, macos py2app's included py ran spp.py source, but win+lin pyinstaller's exes # ran spp frozen (no python to launch). Per testing, it takes ~7 seconds for the first # code run on Windows but no more than a second thereafter and macos code runs are # immediate. Splash mght help on Windows for 4.0 but is not suported on macOS. # The proxy could also be a --onedir .app on macOS and folder on Windows. # # Update: the Run code pause with hook_os is always ~10 secs on a 5-year-old macbook # pro (+spinny wheel, and no splashscreen support on macOs); on Windows, the pause is # always ~3 seconds with hook_os (but there is at least a splashscreen). # # Either build with --onedir, or punt on stdlibs and requires env var settings # Update: sans hook_os, RC pause is always 1~2 secs on macOS, ~1 sec on Windows (+splash). # Update: on macos, --onedir makes 'spp/spp' in subdir which runs if mod paths in # both te.py and spp.py, but only produces output on first Run - just [eof] thereafter; # # PUNT: build spp sans hook_os, require env var settings for better stdlibs/extensions # and faster (immediate) startup for source-code proxy. After first run, PyEdit itself # opens in <=1 sec after run1 on macOS sans splash, and 1~2 secs on windows with splash. # Nothing more can be done about either platform (--onedir too confusing to users). #---------------------------------------------------------------------------------------- # code proxy: bake in stdlib here (only) via a generated pyinstaller hook file # [4.0] NO - ship minimal proxy and urge local-py config setting in popup/docs for target in scriptfreeze[:1]: print('\nBUILDING:', target) proxycmd = ( f'%s{pyscripts}pyinstaller%s' #####' --onedir' # punt on onedir and libs hook ####f' --additional-hooks-dir build{sep}build-app-exe{sep}build4.0' ' --onefile' ' --console' # -noconsole makes .app on macos f' --icon icons{sep}pyedit-subprocproxy.ico' ' --exclude-module textConfig' f' {f"--splash icons{sep}pyedit-splash-256-textdots.png" if RunningOnWindows else ""}' f' {"--target-architecture universal2" if RunningOnMacOS else ""}' f' {target}%s' % (('cmd /S /C ""', '"', '"') if RunningOnWindows else ('', '', '')) ) # unlike pyedit, no .spec file is used for the proxy on any platform #print('{proxycmd=}') exitstat = os.system(proxycmd) if exitstat: print('ERROR: build failed:', exitstat) sys.exit(exitstat) # don't continue here # no other good way to do this in --onedir mode used for macOS... # tetEditor.py looks for the proxy in install dirs from fixfrozenaths if RunningOnMacOS: shutil.move(join('dist', target.split('.')[0]), join('dist', 'PyEdit.app', 'Contents', 'Resources')) ##sys.exit() # to stop and inspect #============================================================================ # Use app exe (not main.py script) name in zipped result #============================================================================ # now done via pyinstaller --name """ exeext = '.exe' if RunningOnWindows else '' shutil.move(join('dist', 'main%s' % exeext), join('dist', '%s%s' % (exename, exeext))) """ #============================================================================ # Copy extras to exe's folder: the .py arranges to see these # # [PyEdit 4.0] bailed on --add-data, resources anually moved here. # # Older notes... # note: --add-data gets unzipped in a temp dir the user won't see, # though that's okay for items that will not change from run to run. # # for macos: since builds an app folder anyhow, use --onedir and # make data items --add-data to store alongside executable in app # folder, and cd to sys.executable dir on startup for cwd access. # # for windows and linux: store data items alongside single-file # --onefile executable in a manual zipfile, and cd to sys.executable # (install) dir on startup for cwd access (like Mergeall). # # See main.py's "if hasattr(sys, 'frozen')" for more details. #============================================================================ if RunningOnWindows or RunningOnLinux: # auto on macos with --add-data for name in extradatas: if os.path.isfile(name): shutil.copy2(name, 'dist') # [3.1] +file times (with data, mode bits) else: shutil.copytree(name, join('dist', name), symlinks=True) # [3.1] ok: files use copy2() # [3.3] symlinks=True # [4.0] --add-data is putting .pys in MacOS/, unlike in Frigcal with same code (3.8 issue?)... elif RunningOnMacOS: topath = join('dist', 'PyEdit.app', 'Contents', 'Resources') # relative to tempfolder root for name in extradatas: if os.path.isfile(name): shutil.copy2(name, topath) # relative to tempfolder root else: shutil.copytree(name, join(topath, name), symlinks=True) # [3.1] ok: files use copy2() #============================================================================ # Cleanup private bits - nothing here (except for calendars in Frigcal) #============================================================================ # [PyEdit 4.0] privates are filtered out earlier in this script """ MOOT IN PYEDIT # currently in tempfolder (copy) # drop personal calendar items: make new default on start, unless dir set in configs if RunningOnMacOS: prunee = join('dist', 'Frigcal.app', 'Contents', 'Resources', 'Calendars') # really else: prunee = join('dist', 'Calendars') for item in os.listdir(prunee): if item not in ('README.txt'): itempath = join(prunee, item) print('Removing', itempath) if os.path.isdir(itempath): shutil.rmtree(itempath) else: os.remove(itempath) """ #============================================================================ # Finale: move temp's dist/ to app's pc-build/ and zip exe/app folder # the exe or app is embedded in the resulting zipped download folder #============================================================================ # zip targets: folder and zip thedir = appname + ('.app' if RunningOnMacOS else '') # nested .exe on win thezip = appname + '--' + platform + '.zip' # zip command: use portable ziptools if RunningOnMacOS: zipper = join(os.environ['Z'], 'zip-create.py') # dev version elif RunningOnWindows: zipper = join(homedir, 'Downloads', 'ziptools', 'zip-create.py') # assume here! # allow spaces in app name, but not py (win) zipit = '%s %s "%s" "%s" -skipcruft' % (python, zipper, thezip, thedir) # move dist product folder to build script's build4/ folder in dev tree os.chdir(startdir) if os.path.exists(thezip): shutil.move(thezip, 'prev-' + thezip) # save previous version? if os.path.exists(thedir): shutil.rmtree(FWP(thedir)) # nuke unzipped version # move build to unzip name in pc-build/, folder+app in dist on macos if not RunningOnMacOS: shutil.move(join(tempfolder, 'TextEditor', 'dist'), thedir) else: shutil.move(join(tempfolder, 'TextEditor', 'dist', appname) + '.app', thedir) # zip the build/app folder os.system(zipit) # make download zipfile shutil.rmtree(FWP(thedir)) # no need to save raw dist here ##shutil.rmtree(FWP(tempfolder)) # rm entire temp build tree now? print('Done: see %s in build4.0/' % thezip) if RunningOnWindows and sys.stdin.isatty(): input('Press enter to close') # stay up if clicked (Windows) # +unzip exe folder, and move to ~/somewhere to make it permanent