File: mergeall-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 sets program icons automatically, and avoids Python install and morph.

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

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

Need config files 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.
Need exes for included scripts: there may be no Python install.

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 as needed.

UPDATE: 32-bit builds are officially dropped (but kept as a prev- pkg).

----
UPDATE [3.3]: on Windows 11 (March 2022), symlinks are now created 
correctly when unzipping with ziptools, even sans admin permission.
This "fix" in turn requires the shutil.copytree() call for extras here 
to use symlinks=True, else the False default makes the build bomb on 
permission errors when trying to follow symlinks sans admin... even 
though a True copies symlinks correctly and without error sans admin
(yes, Windows 11 now creates but doesn't follow!).  Windows-Explorer 
unzips make stub files instead, so prior builds finished without error, 
and without the symlinks=True; this flag is needed only for ziptools' 
correct unzips of symlinks, and only on Windows 11 which "fixes" this.

To build today: unzip the source with ZIPTOOLS to create its symlinks, 
and use symlinks=True here to copy them verbatim into the shipped 
package (the package zip here already uses ziptools too).  When the 
shipped package is later unzipped, ziptools will remake the symlinks 
correctly; Explorer will simply substitute stub files; and other zip
tools may copy, stub, or skip links.  There are just a few symlinks in 
test/ and none are crucial or core, so any loss in transit is trivial.

ALSO added symlinks=True to the Linux build.py's extras copy; its 
former False default follow didn't kill its build due to permissions
like Windows, but following unnormalized Unicode-variant link paths 
caused its build to abort anyhow (see the Linux build.py).  macos's 
build.py already had the flag, and thus always copied symlinks verbatim.

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

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

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

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

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;
# setup and teardown steps are now automated (run this script in its dir);
#----------------------------------------------------------------------------

# automated setup - run in this file's dir
print('TEMP COPY')
temp = r'C:\Users\lutz\Desktop\tempsrc'                # cp can't include self! [feb22]
if os.path.exists(temp):                               # same dir on 64/32 m/c
    shutil.rmtree(FWP(temp))
os.mkdir(temp)

# move all to temp
shutil.copytree(FWP(r'..\..\..\..\mergeall'), FWP(temp+r'\mergeall'), symlinks=True) 
#shutil.copy('setup.py', temp+r'\TextEditor')          # not for pyinstaller
os.chdir(temp+r'\mergeall')                            # goto temp build dir

#--------------------------------------------------------------------------
# build one-file main exe in .\dist, plus extras for standalone scripts
# can't assume a local Python install to run the included scripts;
# pyinstall ignores icons on linux, though mergeall adds to app bar;
#
# UPDATE: PyInstaller's "multipackage" feature (1 bundle, N scripts)
# is currently broken, so make (large) standalone exes for all; blah.
# This seems a deal-breaker, but py2exe is languishing too, and...
#
# Unlike the Mac app, there is no bundled Python executable with the
# sum of all the frozen script's depedencies, so all mergeall spawners
# must run its frozen executable, not source code.  This includes both
# the GUI and console lunchers, as well as the frozen rollback.py. 
#--------------------------------------------------------------------------

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

guifreeze = [
    'launch-mergeall-GUI.pyw',
]
   
scriptfreeze = [
    'mergeall.py',                 # freeze these into exes in main folder
    'diffall.py',                  # also ship source 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]
]

# allow for embedded quotes (not just '%s\\pyinstaller' or '"%s\\pyinstaller"')
for target in guifreeze:
    print('\nBUILDING:', target)
    exitstat = os.system(
        'cmd /C ""%s\\pyinstaller"'
        '   --onefile'
        '   --windowed'
        '   --icon icons\\mergeall.ico'
        '   --exclude-module mergeall_configs'
        '   %s"' % (pyscripts, target))  

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

for target in scriptfreeze:
    print('\nBUILDING:', target)
    exitstat = os.system(
        'cmd /S /C ""%s\\pyinstaller"'
        '   --onefile'
        '   --console'
        '   --icon icons\\mergeall.ico'
        '   --exclude-module mergeall_configs'
        '   %s"' % (pyscripts, target))

    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: source code arranges to see these;
# not --add-data: it gets unzipped in a Temp dir the user won't see...
#--------------------------------------------------------------------------

extras = [                         # tbd: use a 'tools' folder?
    'mergeall_configs.py',         # ship these in install folder='.'
    'README.txt', 
    'icons',
    'docetc',
    'UserGuide.html',
    'test'                         # not scandir_defunct.py: now stubbed out
]

extras = extras + scriptfreeze

for name in extras:
    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
# nested system cleanup
shutil.rmtree(join('dist', 'test', 'ziptools', '__private__'))

#----------------------------------------------------------------------------
# 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 = 'Mergeall' + ('-64bit' if bitsize == 64 else '-32bit')
thezip = thedir + '.zip'
if bitsize == 64:
   #code = r'G:\MY-STUFF\Code\mergeall\test\ziptools'
    code = r'C:\Users\lutz\Desktop\temp\mergeall\test\ziptools'   # [feb22] 
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'\mergeall\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:\Users\you (?) to make it permanent



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