File: mergeall-products/unzipped/build/build-app-exe/macosx/build.py

#!/usr/bin/env python3
"""
=============================================================================
Make a Mac OS X App bundle (folder), using py2app.
Based on the more-complicated build.py of PyEdit.
Main file: run me in this folder to build the app.

To use, edit paths (see [feb22]) and install py2app in a console with:
	pip3 install py2app

An app allows text files to be associated (though irrelevant here);
makes the program immune from Python changes; requires no Python installs;
and better supports file drag-and-drops, program icons, and more (but also
does a woefully-good job of hiding user-config source files and auto-saves!).

Mac app also required... a mac-format .icns icon (the latter became an 
iconify.py extension), and fixfrozenpaths.py's context configurations.

Besides the executable, the app needs:
-mergeall_configs.py: must be source code, and user-visible and editable;
-icon for the exe/app (window borders/app bar for Windows/Linux);
-UserGuide.html and docetc items it uses at runtime;
-docetc image for the Help dialog
 
py2app sets the cwd to the bundle's Contents/Resource folder in all cases:
add '.' to sys.path, and use files copied to .=Resources, to which __file__
will refer.  Associations are irrelevant and unused here.

NOTE: it's assumed that the frozen app, plus the Python and Tk included 
in the app bundle are univeral binaries, supporting both 64- and 32-bit
machines.  This appears to be so, though the app's portability to older 
OS X versions remains to be shown (10.11 El Capitan is build host).

[3.1] Fixed to manually copy extra resources so their modtimes are
preserved; py2app's "--resources" option does not copy file modtimes, 
and these are especially important in the test/ folder in Mergeall.
Modtimes are retained correctly in PyInstaller exes and source code.

[feb22] Mod paths for new app build on Catalina (10.15), for Mergeall 3.3.

[feb22] Using stdout/in in Terminal for extra-scripts (as in 2017 builds)
today requires py2app 2.5 or earlier: see _scripts-require-py2app-0.25.txt.
Downgrading py2app is easier than switching to PyInstaller (for now).

[oct22] the GUI's 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).
=============================================================================
"""

import os, sys, shutil
join, sep = os.path.join, os.path.sep
force = len(sys.argv) > 1                 # remake icon if any arg
startdir = os.getcwd()                    # this build script's dir

# 'python3' fails in both IDLE and PyEdit RunCode (env's PATH not inherited?)
python = '/usr/local/bin/python3'  # sys.executable (else proxy in PyEdit?)

#----------------------------------------------------------------------------
# make app's icon if one doesn't already exist
#----------------------------------------------------------------------------

print('ICONS')
iconship = join('..', '..', '..', 'icons')
iconmake = join('..', '..', 'build-icons')
iconname = 'mergeall'
iconfile = iconname + '.icns'

# step into icon build dir and make
if force or not os.path.exists(iconship + sep + iconfile):
    os.chdir(iconmake)
   #os.system('iconutil -c icns %s.iconset' % iconname)                    # Apple util
   #os.system('./resize-on-mac.sh Pyedit1024new images-pyedit')            # just once
    os.system('%s iconify.py -mac images-pyedit %s' % (python, iconname))  # iconify 2.0
    os.chdir(startdir)
    shutil.move(join(iconmake, iconfile), join(iconship, iconfile))     # mv to ship

#----------------------------------------------------------------------------
# first, copy source tree to temp folder to avoid accidental code loss;
# [update - setup and teardown steps are now automated (run this script in 
# its dir), and Info.plist edits are now automatic by setup.py options;]
#----------------------------------------------------------------------------

# automated setup - run in this file's dir
print('TEMP COPY')
temp = '/Users/me/Desktop/tempsrc'                   # cp can't include self!  # blue=>me [feb22]
if os.path.exists(temp):
    shutil.rmtree(temp)
os.mkdir(temp)

# move all to temp, add setup.py
shutil.copytree('../../../../mergeall', temp+'/mergeall', symlinks=True) 
shutil.copy('setup.py', temp+'/mergeall')              # add setup script to root
os.chdir(temp+'/mergeall')                             # goto temp build dir
 
#----------------------------------------------------------------------------
# cp so use mergeall name for both the app-bundle folder and auto-menu title;
# this name must also be used in this project's setup.py file here;
# we can't rename these after the build, and the launcher is ingrained;
#----------------------------------------------------------------------------

# shutil.copy('textEditor.py', 'PyEdit.py')
# rename after launcher app built ahead

#----------------------------------------------------------------------------
# build the .app app-bundle folder in temp folder's ./dist, using this build 
# script folder's ./setup.py, and copying N mergeall folder extras to the 
# app's main Resources folder along with the generated program itself;
#
# this uses --resources to copy non-code extras, and -extras-scripts to
# also freeze related scripts shipped with mergeall, so that they can
# be run from the app's Content/MacOS folder without a ".py" extension 
# and without requiring a separate Python install (the app's main point).
# [3.1] UPDATE: --resources are now copied manually to retain modtimes
#
# Subtly, all mergeall spawners can run its source code.  The app's bundled
# Python has the union of the main and all --extra-scripts' dependencies.
# The frozen rollback.py, for example, still runs mergeall.py as source,
# because the app's Python running it has all of mergeall's requirements 
# "baked in."  Ditto for the GUI and console lauchers: they can use the
# app's Python to run mergeall's source, instead of running the frozen
# mergeall (which is relly just bootstrap code that routes the bundled
# Python to mergeall's source code anyhow).  Other source is included for
# its docs only, and may not work: run the frozen executable versions.
#----------------------------------------------------------------------------

extras = [                         # tbd: us a 'tools' folder?
    'mergeall_configs.py',         # ship these in Content/Resources='.'
    'README.txt',                  # [3.1] now copied manually to keep modtimes
    'icons',
    'docetc',
    'UserGuide.html',
    '__sloc__.py',                 # [3.1] include source-lines metric in app
    'test'                         # not scandir_defunct.py: now stubbed out
]

alsofreeze = [
    'mergeall.py',                 # freeze these into exec in Content/MacOS
    'diffall.py',                  # also ship source in Resources for their docs (only)
    'cpall.py',                    # freezes require no Py install; source does
    'rollback.py',
    'fix-fat-dst-modtimes.py',
    'nuke-cruft-files.py',
    'launch-mergeall-Console.py',
    'deltas.py',                     # new in 3.2 [feb22]
    'fix-nonportable-filenames.py'   # new in 3.2 [feb22]
]

exitstat = os.system(
    '%s setup.py py2app' 
    '   --excludes mergeall_configs'         # user-editable modules in Resources
    '   --iconfile icons/mergeall.icns'      # [3.1] now copies --resources manually 
    '   --extra-scripts %s'                  # [3.1] was '   --resources %s'
    % (python, ','.join(alsofreeze))         # [3.1] was ','.join(extras + alsofreeze))
)

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

#--------------------------------------------------------------------------
# [3.1] copy extras to exe's folder, retaining their original modtimes;
# not py2app's --resources: it fails to propagate files' modtimes;
# ziptools' __private__ folder copied here is deleted in a later step;
#--------------------------------------------------------------------------

resources = extras + alsofreeze
topath = join('dist', 'launch-mergeall-GUI.app', 'Contents', 'Resources')
assert os.path.isdir(topath)

for name in resources:
    if os.path.isfile(name):
         # copy data, mode bits, and file times, keep links
         shutil.copy2(name, topath, follow_symlinks=False)
    else:
         # copy dir tree and folder modtimes, uses copy2() for its files, keep links
         shutil.copytree(name, join(topath, name), symlinks=True) 

#----------------------------------------------------------------------------
# cleanup: move and zip the app folder for easy xfer and backup (it has
# _very_ many files: nearly 3K files+folders for the current PyEdit App);
# [update - teardown actions are now automated (but still no data to copy)]
# don't copy extras to Contents/Resources folder here: automatic via the 
# py2app args above;  fixfrozenpaths.py arranges to see these as needed;
# DON'T -skipcruft in the zip command: py2app makes a Resources/site.pyc!
#----------------------------------------------------------------------------

# zip command: use portable ziptools (vs: 'zip -r %s %s' % (thezip, thedir))
thedir = 'Mergeall.app'
thezip = thedir + '.zip'
code   = '~/MY-STUFF/Code/ziptools/link'                                   # add ~ [feb22]
zipit  = '%s %s/zip-create.py ../%s %s' % (python, code, thezip, thedir)

# move 'dist' product folder containing the .app to build folder (here)
os.chdir(startdir)
if os.path.exists('dist'):
    shutil.rmtree('dist')                      # nuke bogus retained temp?
shutil.move(temp+'/mergeall/dist', '.')
shutil.rmtree(temp)                            # rm temp build tree 

# clean up old app/folder and rename .app 
if os.path.exists(thezip):
    shutil.move(thezip, 'prev-'+thezip)        # save previous version
if os.path.exists(thedir):
    shutil.rmtree(thedir)                      # nuke unzipped version

os.chdir('dist')   
shutil.move('launch-mergeall-GUI.app', thedir)

# last-chance nested-system cleanup
shutil.rmtree(join('Mergeall.app', 'Contents', 'Resources', 
                   'test', 'ziptools', '__private__'))

# zip the nested app folder to .. (unzip to test)
os.system(zipit)                               # run zip in dist: has app dir
os.chdir('..')   
shutil.rmtree('dist')                          # rm dist: _very_ many files

print('Done: see ./%s' % thezip)

# +unzip app and copy it to /Applications to make it official



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