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

#!python3
"""
===========================================================================
Main file: make a Windows single-file executable with PyInstaller,
and manually copy some data items to its folder.

This allows text files to be associated to open in PyEdit on clicks,
sets program icons automatically, and avoids Python install and morph.

There is no setup.py file for PyIstaller.
Didn't get cx_freeze or py2exe to work, but stopped short...

Moves PP4E pkg root (less TextEditor) to be nested here, not above.
Need textConfig to be source code, and user-visible/editable.
Need icon for both the exe, and window borders in Tk runtime.
PyInstaller sets cwd to anything; uses dir of sys.argv[0],
and copies files to folder holding exe for .=exe's dir.

Python recoding of an original DOS batch-file version; now based on 
a Linux PyInstaller version (with icon code fron a Mac OS X version): 
Python portability makes this nearly the same everywhere.  Could also
neutralize slash diffs with join() and sep, but other parts vary too.

UPDATE: on Windows, make both 64- and 32-bit executables.  The 32-bit
build machine varies from the 64-bit main machine; specialize paths,
and quote to allow for embedded spaces in Python dirs on 32-bit host.
The 32-bit Windows 8 box PyInstaller required runnning as admin, and
upgrading pip and setuptools to fix a Windows path-quoting bug...

*NOTE*: change "bitsize" in this script from 64 to 32 for build machine.
===========================================================================
"""

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
python = sys.executable                   # may have embedded spaces!

bitsize = 64  # or 32
mainhost = (bitsize == 64)

def FWP(path):
    # allow long paths on Windows: auto-save files
    # see mergeall and ziptools for tons of details
    return '\\\\?\\' + os.path.abspath(path)

#----------------------------------------------------------------------------
# force all stdlib mods to be baked-in to proxy exe's python, for Run Code
#----------------------------------------------------------------------------

if mainhost:
    # make hook file (no need to remake on 32-bit m/c)
    exitstat = os.system('"%s" include-full-stdlib.py' % python)
    assert exitstat == 0

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

iconship  = join('..', '..', '..', 'icons')
iconmake  = join('..', '..', 'build-icons')
iconnames = ['pyedit',
             'pyedit-window-main',
             'pyedit-window-popup',
             'pyedit-subprocproxy']

# 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 PP4E, move its TextEditor to root, and nest PP4E inside it;
# [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
temp = r'C:\Users\mark\Desktop\tempsrc'                # cp can't include self!
if os.path.exists(temp):                               # same dir on 64/32 m/c
    shutil.rmtree(FWP(temp))
os.mkdir(temp)

# move all to temp, nest PP4E in TextEditor
shutil.copytree(FWP(r'..\..\..\..\..\..\PP4E'), FWP(temp+r'\PP4E'), symlinks=True) 
shutil.move(temp+r'\PP4E\Gui\TextEditor', temp)        # move nested up to temp root
shutil.move(temp+r'\PP4E', temp+r'\TextEditor')        # move PP4E down to nested
#shutil.copy('setup.py', temp+r'\TextEditor')          # not for pyinstaller
os.chdir(temp+r'\TextEditor')                          # goto temp build dir

#--------------------------------------------------------------------------
# build one-file exe in .\dist (now actually _two_ exes, per below)
#
# ABOUT THE EXTRA EXECUTABLE:
#
# A PyInstaller one-file build is essentially a self-extracting bundle,
# which unpacks itself into a temp dir, named by sys._MEIPASS; the temp
# dir is deleted on (normal) program exit.  Hence the start-up delay,
# and need to copy user-visible data items to the exe's permanent folder.
#
# Unlike both source code and py2app Mac bundles, with frozen exes there
# is no Python executable, just a Python binary lib, and sys.executable is
# not Python (it's the exe itself).  Thus, to spawn the proxy Python script
# from a frozen Windows/Linux executable, the script must either be frozen
# too, or be somehow started by a new instance of the frozen PyEdit (e.g.,
# with a special cmdline arg and import or exec); chose former scheme here.
#
# This also makes it impossible to run arbitrary Python script processes
# from a frozen executable, unless they are run by exec() as in the Capture
# Run-Code mode's scheme -- there may be no installed Python, the scripts
# can't be expected to be frozen too, and requiring an extra Python install
# would negate a prime benefit of using frozen exes in the first place.
#
# This was addressed by forcing as many standard libs into the freeze as
# possible, and allowing users to configure a locally-installed Python and
# module import-path extensions in their textConfig.py file.  In Capture
# mode, the proxy is launched as source if a Python is configured, or as
# a frozen exe otherwise, and both versions are shipped in the zipfile.
#
# In more detail: for the proxy only, and using PyInstaller only, force the
# inclusion of the full stdlib (at freeze time) by generating a "hook" file
# having all stdlib module names, sourced on import of "os" in subprocproxy.py
# at exe build time.  See the hook maker, include-full-stdlib.py, for more.
# This mimics a generated setup.py scheme used in py2app Mac builds, though
# there the stdlib is forced into the bundle's Python exeuctable.
#
# Note that this adds the full stdlib to *only* the subprocproxy's exe:
# there's no need to bundle this with PyEdit.exe, and limiting it to the
# proxy means that the extra load time hit is incurred for Run Code only,
# not when starting the editor in general.
#
# The frozen proxy also required custom code exception-text parsing (there
# is one less output line), special arguments (shell=True) on Windows to
# suppress console (Command Prompt) popup in some frozen exe contexts,
# tricks to clean up the temp unzip file when the process crashes, and
# platform-specific schemes to forcibly kill the process on user request.
#
# LESSON: frozen exes don't play well with subprocess or multiprocessing,
# seem painfully-reminiscent of C development and makefiles, and require
# code changes and extra tasks in nontrivial use-cases.  OTOH, they also
# are a valiant effort to promote scripts to first-class program status. 
#
# Caveats: --one-file builds incur a speed hit at startup to unpack to
# a temp dir, and may leave substantially-large garbage in the temp dir.
# An alternative --one-dir build was explored (see unused-defunct/) but
# abandoned. because no good way was found to move the many included
# dependencies to a subfolder (DLLs seemed problematic).  Instead, PyEdit
# now attempts to reap temp dir trash when run in frozen mode on Windows.
#
# Issues summary:
# - py2exe and cx_freeze had issues early on
# - multiprocessing requires a patch, subprocessing may require args
# - must adjust cwd and import paths for runtime components use
# - must exclude and ship user-editable source-code config module
# - must include extras like help files and window icons
# - no standalone py is shipped, and Run Code cannot assume one is present
# - frozen proxy does not include full stlib: must force for Run Code 
# - mode --one-file unzips to temp folder, leaving garbage to accumulate: prune
# - mode --one-file takes time to unpack itself to temp folder on startup: punt
# - only subprocproxy needs full stdlib (could share in --one-dir builds)
# - build pyedit and proxy separately: modes equivalent, other needs differ
# - frozen exe streams don't respect unbuffered and IO encoding env vars
# - killing the process on request was complex, nonportable, and painful
#--------------------------------------------------------------------------

# may not be on PATH
if bitsize == 64:
    pyscripts = r'C:\Users\mark\AppData\Local\Programs\Python\Python35\Scripts'
else:
    pyscripts = r'c:\Program Files\Python 3.5\Scripts'

# pyedit: allow embedded quotes; not just '%s\\pyinstaller' or '"%s\\pyinstaller"'
exitstat = os.system(
    'cmd /C ""%s\\pyinstaller"'
    '   --onefile'
    '   --windowed'
    '   --icon icons\\pyedit.ico'
    '   --exclude-module textConfig'
    '   textEditor.py"' % pyscripts)         # or list subprocproxy.py here too

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

# code proxy: bake in stdlib here (only) via a generated pyinstaller hook file
exitstat = os.system(
    'cmd /S /C ""%s\\pyinstaller"'
    '   --onefile'
    '   --noconsole'
    '   --icon icons\\pyedit-subprocproxy.ico'
    '   --exclude-module textConfig'
    '   --additional-hooks-dir build\\build-app-exe\\windows'
    '   subprocproxy.py"' % pyscripts)

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

#--------------------------------------------------------------------------
# use exe (not script) name
#--------------------------------------------------------------------------

shutil.move(r'dist\textEditor.exe', r'dist\PyEdit.exe')

#--------------------------------------------------------------------------
# copy extras to exe's folder: textEditor.py arranges to see these;
# not --add-data: it gets unzipped in a Temp dir the user won't see...
#--------------------------------------------------------------------------

extras = ['textConfig.py', 
          'README.txt', 
          'icons',
          'tools',
          'docetc', 
          'UserGuide.html',
          'subprocproxy.py']       # proxy: ship frozen AND source versions

for name in extras:
    if os.path.isfile(name):
         shutil.copy(name, 'dist')
    else:
         shutil.copytree(name, join('dist', name))

#----------------------------------------------------------------------------
# cleanup: move and zip the exe folder for easy xfer (just a few files here);
# [update - teardown actions are now automated (but still no data to copy)]
#----------------------------------------------------------------------------

# zip command: use portable ziptools
thedir = 'PyEdit' + ('-64bit' if bitsize == 64 else '-32bit')
thezip = thedir + '.zip'
if bitsize == 64:
    code = r'C:\MY-STUFF\Code\mergeall\test\ziptools'
else:
    code = r'D:\MY-STUFF\Code\mergeall\test\ziptools'

# allow spaces
zipit = r'"%s" %s\zip-create.py %s %s -skipcruft' % (python, code, thezip, thedir)

# move dist product folder here
os.chdir(startdir)
if os.path.exists('dist'):
    shutil.rmtree('dist')                          # nuke bogus retained temp?
shutil.move(FWP(temp+r'\TextEditor\dist'), '.')
shutil.rmtree(temp)                                # rm temp build tree 

# zip the exe=dist folder - unzip to test and use here or elsewhere
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

os.rename('dist', thedir)                          # rename for unzip name
os.system(zipit) 
shutil.rmtree(FWP(thedir))                         # no need to save raw dist

print('Done: see .\%s' % thezip)
if sys.stdin.isatty():
   input('Press enter to close')                   # stay up if clicked (Win)

# +unzip exe folder, and move to C:\Programs (?) to make it permanent



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