File: frigcal-products/unzipped/build/build-app-exe/build3.0/build.py

#/usr/bin/env python3
"""
===========================================================================
[Adapted from PPUS's pc-build/build.py  -- but here for Tkinter, not Kivy]

For Frigcal: there is no Kivy (only tkinter), PIL is unused (now),
there are supplemental console and GUI executables.  PPUS Kivy notes
here were retained because that may be a later context for Frigcal.

[Adapted from Mergeall's Windows-exe build.py]

Use PyInstaller to make a Windows|Linux|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 Mergeall's version, this 
script is coded to work portably on Windows, Linux, and macOS.

To use - on all three PC platforms:

- Ensure that app runs as source code; if needed: pip3 install kivy
- Edit paths in code here if needed (now mostly moot)
- Install PyInstaller in a console with: pip3 install pyinstaller
- Download and unzip ziptools in ~/Downloads from learning-python.com
- Run this script in its own folder (pc-build/) with: python3 build.py

Build tours (verify all copies along the way):
  On macOS: run pc-build/build.py in the app folder itself to make zip.
  The dev folder is ~/Desktop.DEV-BD/apps/PC-Phone-USB-Sync (not $C).
  UPDATE: now builds universal2 on macbook with py 3.10, not dev mbp;
  builds are run in ~/Desktop: copy app folder there, rename to P-P-U-S,
  run its pc-build/build.py, copy zip to SSD.

  On Windows: copy app folder from SSD to Desktop\temp, rename it as
  PC-Phone-USB-Sync, run its pc-build/build.py, copy zip back to SSD.
  UPDATE: build as user 'lutz', not 'me' ('me' is for installs/shots).

  On Linux: copy app folder from SSD to ~/ppus (optional?), rename it as
  PC-Phone-USB-Sync, run its pc-build/build.py, copy zip back to SSD.
  Made no-op virtual env to trim space (once), purged/reinstalled kivy;
  See ./HOW_TO_BULD_UBUNTU22_OCT23.txt for setup saga; latest build cmds:
  ~$ export PYTHONPATH=/home/me/ppus/lib/python3.8/site-packages
  ~/ppus/PC-Phone-USB-Sync/pc-build$ ~/ppus/bin/python3.8 build.py

  Copy 3 collected zips from SSD to macOS pc-builds/, copy all 3 zips to 
  _website/+downloads, run _website/_publish.sh to upload site with exes.

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, and 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: 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 paradigms well
- Require no Python or any other installs
- Are immune to more 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.


BUILD NOTES
===========

Need icon for both the froze app/exe, and window borders at 
runtime.  PyInstaller and Kivy automate some of this.  The 
former now builds a .ico or .icns from a .png (like iconify.py!),
thuogh this is not used on some platforms (see ahead_).

PyInstaller sets cwd to anything; main.py cd's to dir(sys.executable),
and copies data files to folder holding exe for . = exe's dir.

There are no included secondary frozen scripts in this build, and 
hence no relative-path launch issue for cd'ing to exe in the .py.

On Windows and Linux, this makes 64-bit executables only (32-bit has
largely faded).  Currently built on Windows 11 and Ubuntu only.

On macOS, this may make an Intel-only app, which requires rosetta2
to be installed on used for runs (right-click or Get Info on the app).
A universal binary would help, but this depends on PyInstaller, Kivy,
all libs, and the build machine, and building on a later macOS version
means earlier versions are not supported.  Currently built on Catalina.
macOS bluntly dropped 32-bit support altogether a few years ago...
UPDATE: now builds a universal2 binary with py3.10 on old mac book.

Mergeall's build.py has a note on the perils of symlinks in app builds. 
There are no symlinks in this app, but the required symlinks=True
in shutil.copytree() is in place here in any event.  UPDATE: there
are symlinks in the macOS --onedir build, but copies, ziptools, and 
most unzip tools all handle them properly.


RECENT CHANGES
==============

[feb22] Mod paths for new exe build on Windows 11, for Mergeall 3.3.
[feb22] Only provide a 64-bit exe on Windows (not 32-bit); see README.
[mar22] Need symlinks=True for extras shutil.copytree(); see above.

[oct22] Mergeall's GUI Help button now opens the online user guide, not 
the local copy--whose links may reference files absent in frozen packages.
The local copy may now not be very useful sans more includes (tbd).
Python-PC USC Sync always uses online resurces only.

[apr23] Adapted for PC-Phone USB Sync, and made portable to all PCs.


EDITORIAL
=========

Kivy, buildozer, and PyInstaller are the products of an amazing 
amount of effort, and Kivy may be the best game in town today 
for Python-coded apps (GUIs) that must run on Android.

But buildozer was a buggy slog, and didn't work at all for macOS
as advertised.  PyInstaller was just as buggy and sloggy.  And Kivy 
required dozens of coding workarounds across Android and PCs
(e.g., orange touch dots and DPI issues in Windows still unfixed).

Moreover, all the docs seem thin at the worst places.  Kivy's guides 
to building macOS/Windows/Linux exes with PyInstaller omit crucial 
details (e.g., packaging .kv files), and its resources are often 
misleading or just plain wrong (e.g., a .spec is optional on macOS;
Linux exes aren't covered and Windows docs don't apply; and file 
paths and POSIX do work on Android with All files access permission).

Kivy's docs may be dated, but it's rude to leave docs online that will 
send users down pointless and frustrating rabbit holes.  Especially docs
which end with terse statements that imply that tasks are somehow easy. 
Some of this may reflect project competition (see also the full absence
of tkinter support in Kivy's buildozer), but it's subpar.  Fix, please!
===========================================================================
"""


import os, sys, shutil
join, sep = os.path.join, os.path.sep

# run this script in its own dir (and mind the top-level name!)
assert os.getcwd().endswith(join(*'frigcal3.0/build/build-app-exe/build3.0'.split('/')))

# Python 3.X+ only
assert int(sys.version[0]) >= 3

# this script never used on Android
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

appname  = 'Frigcal'                     # spaces complicate windows zip cmd
exename  = appname                       # not: appname.replace(' ', '_')
homedir  = os.path.expanduser('~')
platform = 'Windows' if RunningOnWindows else 'macOS' if RunningOnMacOS else 'Linux'

force = len(sys.argv) > 1       # remake icon iff any arg (maybe: this is now cruft)
startdir = os.getcwd()          # this build script's dir: build3.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)

# [1.2.0] 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 not 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-')



#----------------------------------------------------------------------------
# Make exe's icon if one doesn't already exist
#----------------------------------------------------------------------------


# 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.


"""
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 force 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))
"""



#----------------------------------------------------------------------------
# First: copy source tree to temp folder to avoid accidental code loss
#----------------------------------------------------------------------------


print('TEMP COPY')
devfolder = 'frigcal3.0'    # dev, not dist; mind the dashes

# cp can't include itself
tempfolder = join(homedir, 'Desktop', 'temp-build-frigcal')
if os.path.exists(tempfolder):
    shutil.rmtree(FWP(tempfolder))    # rerun script if fails on macos...
os.mkdir(tempfolder)


# NEW: omit moots in the temp copy, so they're not added to built exes;
# else top-level android .buildozer is 4.86G, macos .DS_Store kills build,
# pc-build kills the build too (below), and _website adds 113M to the exes;
# (but need mergeall/ at runtime, and usbsync-pc/ at builtime and runtime);
#
# why does pyinstaller add these, and yet provide no good way to omit them?
# _website has just 1 .py script which is not imported anywhere in the app;
# --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', '_private_', 'build', '_old-screenshots', 'dependencies',
            'UserGuide.html', 'docetc',
            '.buildozer', '_dev-misc', 'bin', '.DS_Store', 'pc-build', '_website') 


# copy all to temp
shutil.copytree(FWP(join('..', '..', '..', '..', devfolder)),     # relative to build3.0
                FWP(tempfolder),                      # copies contents, not dir
                dirs_exist_ok=True,                   # else need new nested subdir
                symlinks=True,                        # copy symlinks verbatim
                ignore=skips)                         # skip android and build dirs, etc


# Windows only: post run1, copy .spec from tempfolder to pc-build/, add kivy deps lines
# macOS only: post run1, copy .spec from tempfolder to pc-build/, mod version# lines
# copy pc-build/ files up to main.py's level: else pc-build/ makes exe >2G, build fails!

specfile = '_%s--Frigcal.spec' % platform             # one per platform
hasspec = os.path.exists(specfile)                    # none on run1, or linux (so far)
if hasspec:
    # from cwd = dev's pcbuild/
    shutil.copy2(specfile, join(tempfolder, 'Frigcal.spec'))   # drop platform


if RunningOnWindows:
    shutil.copy2('_winver.txt', tempfolder)           # windows-only oddment

os.chdir(tempfolder)    # goto temp build dir for next steps (no FWP(): breaks!)



#--------------------------------------------------------------------------
# Build one-file main exe in temp dir's dist/ with pyinstaller
#
# pyinstall ignores icons on linux, though app may add to app bar;
# no need to exclude mergeall/ - it's run by exec() sans imports;
#
# nested mergeall/ scripts are run as source code via Python's exec()
# in threads an all PCs, not as separate frozen exes; 
#
# Android app runs nested mergeall/ scripts via exec() too in a thread 
# or service process, but is moot here;
#--------------------------------------------------------------------------



# BUILD TOOL


# pyinstaller may not be on PATH
if RunningOnWindows:
   #pyscripts = 'C:\\Python\\Scripts\\' 
    pydir = os.path.dirname(sys.executable)   # else py3.8's older pyinstaller [3.0]
    pyscripts = pydir + '\\Scripts\\'         # where py installed, not the default [feb22]
elif RunningOnLinux:
    pyscripts = '~/ppus/bin/'                 # temp try: virtualenv to cut exe size (40M vs 200M!)
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#)

    'Frigcal.spec' if hasspec else 'frigcal-main.py',
]
   
scriptfreeze = [
    # freeze these into command-line exes in main folder with --console
    # [3.0] drop searchcals: now in GUI, and can't find configs module
    # [3.0] configs module issue fixed for all tools: include searchcals.

    'pickcolor.py',                # GUI, but hardly worth a whle app on macOS
    'makenewcalendar.py',          # freeze these into exes in main folder 
    'searchcals.py',               # also ship source for their docs (only)
    'unicodemod.py',               # freezes require no Py install; source does
]

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

    'frigcal_configs.py',          # ship these in install folder='.'
    'frigcal_configs_base.py',
    'README.txt', 
    'README-3.0.txt',              # [3.0] supplemental doc
    'icons',                       # UPDATE SPEC FILES IS THIS LIST IS CHANGED
    'Calendars',
    'icalendar',
    'pytz',
    'terms-of-use.txt',            # legalese: caution, t-o-u, privacy
]

# the .kv must be in cwd for macOS --onedir only
"""
if RunningOnLinux:
    extradatas = extradatas[:-1]   # drop .kv file on Linux: must --add-data to _MEI*
if RunningOnWindows:
    extradatas = extradatas[:-1]   # drop .kv file on Windows: somehow in _MEI* auto
"""

extradatas = extradatas + scriptfreeze     # moot here: no command-line script exes



# BUILD COMMANDS


# ==> *NOTE* builds2+ on Windows and macOS use .spec files here, not these commands
# ==> specfiles generated in temp build tree on first run: copy to '.' and rname+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:

    """
    ----------------------------------------------------------
    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).

    [3.0] Spec file not used on Windows for Frigcal 3.0: it
    does not need Kivy deps/adds, and code need not be hidded.

    [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 only runs 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.
    ----------------------------------------------------------
    """

    # first build: create .spec file from args

    extraargs1 = (
        '   --splash icons/splash2.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: kivy dpi scaling bug workaround

    buildcmd1 = (
      # BAIL ON SPEC FILE FOR FRIGCAL 3.0 - no kivy deps or code hiding 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 from main.py
      ##'   --hidden-import PIL'          # for animated GIF, ref'd in .kv but not .py
        '   --exclude-module frigcal_configs'
        '   --exclude-module frigcal_configs_base'
        '   --icon icons/frigcal.ico'     # pyinstall/kivy auto, not iconify.py
       f'   {extraargs1}'
       f'   "%(target)s""')               # target not set till for loops ahead

    # builds 2+: use MANUALLY copied and edited spec file: required for kivy-deps
    buildcmd2 = (
       f'cmd /C ""{pyscripts}pyinstaller" --clean'
        '   "%(target)s""')

    buildcmd = buildcmd1 if not hasspec else buildcmd2



elif RunningOnLinux:

    """
    ----------------------------------------------------------------
    Run in ~/ppus virtualenv, but this was probably pointless once
    all undefs installed, and results are now the same either way.

    Linux does NOT require a .spec file for kivy-deps like Windows.
    In fact, the kivy-deps module doesn't exist for Linux at all,
    but kivy's docs don't say so, and omit Linux exes completely!
    https://kivy.org/doc/stable/gettingstarted/installation.html

    On Linux ONLY, Kivy does NOT look for .kv file in CWD, but 
    only in _MEI* unzip temp folder: use --add-data for it here.
    All other extras (including run-count and config-save files) 
    work fine in cwd after resetting it to the install folder.

    The only way to catch this was by running the exe from a 
    cmd line with -d to get extra debug info in ~/.kivy/logs (not
    build/warn*), else this fatal error was NEVER reported...
    UPDATE: Windows appears to locate+find .kv in the unzipped 
    exe auto, though macOS requires it in '.' in --onedir mode.

    Also required treesize.py to find and exclude large modules;.
    Running in a virtualenv helped with size some, but not much:
    once undef modules were installed, size wentfrom 40M back ~200M.

    Caveat: this makes a plain Linux exe which omits system libs 
    and may not work broadly.  Alt: use compileall.py and PyZipFile 
    to ship bytecode in a zipfile, with a simple .py to add the 
    zip to sys.path and import main.py to run.  This requires users
    to install Python and Kivy on their own machine, but seems
    much more likely to span hosts.  OTOH, bytecode=>code happens.

    The Linux exe IS now verified to work elsewhere, via online vm,
    and WSL2 on Windows, though only on Ubuntu 22 distros to date.

    [3.0] Wholly moot on Linux - no exe built, use source-code pkg.
    ----------------------------------------------------------------
    """

    extraargs = (
        '   --splash icons/Frigcal1024.png'  # shown during load, not supported on macos
       #'   --add-data pcphoneusbsync.kv:.'  # see above: a kivy linux req+convolution!
        '   --exclude-module opencv'         # else still ~140M after installing undefs
        '   --exclude-module numpy'          # back to 40M with module exludes
        '   --exclude-module enchant'        # maybe easier with a .spec, but extra cruft
        '   --exclude-module cv2')           # and this makes virtualenv pointless...

    buildcmd = (
       f'{pyscripts}pyinstaller'
        '   --onefile'                     # unzip to _MEIxxxxxx on start, extras in unzip dir
        '   --windowed'                    # no stdout/err console
       f'   --name "{exename}"'            # this instead of from main.py
      ##'   --hidden-import PIL'           # for animated GIF, ref'd in .kv but not .py
        '   --exclude-module frigcal_configs'
        '   --exclude-module frigcal_configs_base'
        '   --icon icons/Frigcal1024.png'  # icon not used on Linux: ignored, use linux.desktop
       f'   {extraargs}'
        '   "%(target)s"')                 # target not set till for loops ahead



elif RunningOnMacOS:

    """
    --------------------------------------------------------------------
    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
    ""

    UPDATE: now _does_ build universal2 (intel+arm/M) binary on catalina
    mac book (not dev mac book pro: py 3.8).  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

    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.

    [3.0] There is a wicked-long delay when first running tools exes,
    but PyInstaller doesn't support splash screens on macOS.  These 
    could be --onedir folders+apps, but that seems overkill; punt.
    --------------------------------------------------------------------
    """

    # files need  '.' else in subdir
    extraargs1 = ' '.join(
        '   --add-data %s:%s' % 
                  (data, (data if os.path.isdir(data) else '.')) for data in extradatas)

    buildcmd1 = (
       f'{pyscripts}pyi-makespec'    # not recognized: --clean
      ##'   --onefile'               # (unzip to _MEIxxxxxx on start, always mod ma/configs.py)
        '   --onedir'                # extras in app/Content/MacOS folder, no unzip on start
        '   --windowed'              # no console, make macos app-bundle folder, like --onedir?
       f'   --name "{exename}"'      # this instead of from main.py
      ##'   --hidden-import PIL'     # for animated GIF, ref'd in .kv but not .py
        '   --exclude-module frigcal_configs'
        '   --exclude-module frigcal_configs_base'
        '   --icon icons/frigcal.icns'            # iconify.py, not pyinstaller auto
        '   --target-architecture universal2'     # mb, py 3.10+, combine pillow wheels
       f'   {extraargs1}'
        '   %(target)s')                          # target not set till for loops ahead

    # builds 2+: use MANUALLY copied and edited spec file: required for kivy-deps
    buildcmd2 = (
       f'{pyscripts}pyinstaller --clean'
        '   "%(target)s"')

    buildcmd = buildcmd1 if not hasspec else buildcmd2



# BUILD RUNS


# make main app/exe with cmds above (or specfiles)
for target in guifreeze[:1]:
    print('\nBUILDING:', target)
    exitstat = os.system(buildcmd % vars())
    if exitstat:
        print('ERROR: build failed:', exitstat)
        sys.exit(exitstat)   # don't continue here


# make extra GUI standalones (if any)
"""MOOT
for target in guifreeze[1:]:
    print('\nBUILDING:', target)
    exitstat = os.system(
       f'%s{pyscripts}pyinstaller%s'
        '   --onefile'
        '   --windowed'
       f'   --icon icons{sep}{'Frigcal1024.png' if RunningOnWindows else 'frigcal.icns'}'
        '   --exclude-module frigcal_configs'
        '   --exclude-module frigcal_configs_base'
       f'   {target}%s' %
            (('cmd /S /C ""', '"', '"') if RunningOnWindows else ('', '', '')) )

    if exitstat:
        print('ERROR: build failed:', exitstat)
        sys.exit(exitstat)   # don't continue here
MOOT"""


# make extra cmdline exes
for target in scriptfreeze:
    print('\nBUILDING:', target)
    exitstat = os.system(
       f'%s{pyscripts}pyinstaller%s'
        '   --onefile'
        '   --console'
       f'   --icon icons{sep}Frigcal1024.png'
        '   --exclude-module frigcal_configs'         # NEEDED for Configs.icspath setting
        '   --exclude-module frigcal_configs_base'    # in both makenewcal and searchcals
       f'   {target}%s' %
            (('cmd /S /C ""', '"', '"') if RunningOnWindows else ('', '', '')) )

    if exitstat:
        print('ERROR: build failed:', exitstat)
        sys.exit(exitstat)   # don't continue here

    # no good way to do this... is it worth it???
    if RunningOnMacOS:
        shutil.move(join('dist', target.split('.')[0]), 
                    join('dist', 'Frigcal.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
#
# 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

#--------------------------------------------------------------------------
# Cleanup private bits - nothing here (except for calendards in Frigcal!)
#--------------------------------------------------------------------------

# 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 or RunningOnLinux:
    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 pc-build/ folder
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, 'dist'), thedir)
else:
    shutil.move(join(tempfolder, 'dist', appname) + '.app', thedir)


# forelorn hack 1 - linux exe doesn't have exe permission (why?)
# because it's a user error: use -permissions if extract with ziptools
# (py3 $Z/zip-extract.py PC-Phone\ USB\ Sync--Linux.zip . -permissions)
"""
if RunningOnLinux:
    os.chmod(join(thedir, appname), 0o775)
"""

# forelorn hack 2 - mod manifest on macos for version#
# version3# is supported by pyinstaller, but require .spec file
#
# Problem: this mod triggers a harsh message when the app is 
# unzipped and run - '"<appname>" is damaged and can't be opened.
# You should move it to the trash', with only "Move" and "Cancel" 
# options.  App still runs if remove quarantine with xattrs
# (xattr -r -d com.apple.quarantine PC-Phone\ USB\ Sync.app)
# but is scary enough to put off most users.  Sans this edit, the 
# message is much less severe, and offers a simple "Open" option.
# All on Catalina; Gatekeeper may grow more douchey later...
#
# this prompted using a .spec file on macOS too per rewrites above
"""
if RunningOnMacOS:
    plist = open(thedir + '/Contents/Info.plist', 'r').read()
    plist = plist.replace('<string>0.0.0</string>', '<string>1.0.0</string>')
    open(thedir + '/Contents/Info.plist', 'w').write(plist)
"""


# 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 build3.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