File: pyedit-products/unzipped/build/build-app-exe/build4.0/build.py

#/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 <this script's dir>
	(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



[Home page] Books Code Blog Python Author Train Find ©M.Lutz