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