#!python3 r""" =========================================================================== Main file: make a Windows single-file executable with PyInstaller, and manually copy some data items to its folder. Run me in my folder. This 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... 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 for build machine. NOV21 UPDATE: fix paths (e.g., \MY-STUFF, \Users\*) for builds on new Windows 10 machine, and change "/python3.5/" => "/python3.8/". All else worked as is on 10 with PyInstaller 4.7. Apps were rebuilt to pick up a newer Python's (3.8 on Win) support for newer openSSL; else some email servers now refuse requests that have worked with the older app (and its Python 3.5) since 2017. Freezing doesn't ensure longevity... ==> NOT USED: pyinstaller 4.7 can't find basic top-level imports... getting to work before was like root canal (and partly luck); PUNT: it's just POP, it's just plain text, and needs launcher rewrite for android =========================================================================== """ 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 = ['pymailgui'] # 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! 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'..\..\..\..\pymailgui'), FWP(temp+r'\pymailgui'), symlinks=True) #shutil.copy('setup.py', temp+r'\TextEditor') # not for pyinstaller os.chdir(temp+r'\pymailgui') # goto temp build dir #---------------------------------------------------------------------------- # drop big and irrelevant folders, plus any personal email stuff; # some of these would be cut later when tidying the PyMailGui folder, # but drop here anyhow in case the build code ahead ever changes; #---------------------------------------------------------------------------- # 1) strip the 400M+ nested PyEdit build dirs pyeditbuild = 'PyMailGui-PP4E/PP4E/Gui/TextEditor/build' # '/' ok on Windows shutil.rmtree(pyeditbuild) os.mkdir(pyeditbuild) dummy = open(join(pyeditbuild, 'README.txt'), 'w') dummy.write('Content removed: see PyEdit for its build scripts.\n') dummy.close() # else in-use on rmtree non Win (Mac?) # 2) pyedit auto-saves; pass on reinstating zipped examples in docetc/examples autosave_pmg = 'PyMailGui-PP4E/__pyedit-autosaves__' autosave_pe = 'PyMailGui-PP4E/PP4E/Gui/TextEditor/__pyedit-autosaves__' for asave in (autosave_pmg, autosave_pe): for item in os.listdir(asave): if item != 'README-autosaves.txt': itempath = join(asave, item) print('Removing', itempath) os.remove(itempath) # 3) TempParts; pass on reinstating zipped example in docetc/examples tempparts = 'PyMailGui-PP4E/TempParts' for item in os.listdir(tempparts): if item not in ['README-TempParts.txt', 'DSC00565.JPG', 'DSC03890.JPG']: itempath = join(tempparts, item) print('Removing', itempath) os.remove(itempath) # 4) sent mail; very dangerous... print('Replacing sentmail') sentmail = 'PyMailGui-PP4E/sentmail.txt' os.remove(sentmail) os.rename('PyMailGui-PP4E/sentmail-save.txt', sentmail) #-------------------------------------------------------------------------- # build one-file main exe in .\dist for launcher and main program; # no need for extra builds for standalone scripts here; # can't assume a local Python install to run the main program; # # exclude Launch_PyEdit.pyw here: getting PyEdit to build alone was # a herculean task, and building it nested in PyMailGUI would be # worse: users can fetch PyEdit standalone or in PyMailGUI source; # # 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... #-------------------------------------------------------------------------- # pyinstaller may not be on PATH if bitsize == 64: #pyscripts = r'C:\Users\lutz\AppData\Local\Programs\Python\Python35\Scripts' pyscripts = r'C:\Python\Scripts' else: pyscripts = r'C:\Program Files\Python 3.8\Scripts' # 32 bits is now defunct # build launcher that spawns PyMailGui for a selected account # allow for embedded quotes (not just '%s\\pyinstaller' or '"%s\\pyinstaller"') target = 'Launch_PyMailGUI.py' print('\nBUILDING:', target) exitstat = os.system( 'cmd /C ""%s\\pyinstaller"' ' --onefile' ' --windowed' # tbd: or --console? ' --icon icons\\pymailgui.ico' ' --hidden-import PyMailGui-PP4E.PP4E.Gui.Tools.guimaker' ' %s"' % (pyscripts, target)) if exitstat: print('ERROR: build failed:', exitstat) sys.exit(exitstat) # don't continue here # build nested pkg's main program (its subfolder is too ingrained to move) target = 'PyMailGui.py' print('\nBUILDING:', 'PyMailGui-PP4E\PyMailGui.py') os.chdir('PyMailGui-PP4E') exitstat = os.system( 'cmd /C ""%s\\pyinstaller"' ' --onefile' ' --windowed' # tbd: or --console? ' --icon ..\\icons\\pymailgui.ico' ' --exclude-module mailconfig' ' --exclude-module textConfig' ' %s"' % (pyscripts, target)) os.chdir('..') # leave the exe in nested dist/ for now if exitstat: print('ERROR: build failed:', exitstat) sys.exit(exitstat) # don't continue here #-------------------------------------------------------------------------- # use exe (not script) name [not here] #-------------------------------------------------------------------------- # 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... #-------------------------------------------------------------------------- print('Copying extras to root') extras = [ # tbd: use a 'tools' folder? 'MailConfigs', # ship these in install folder='.' 'README.txt', 'icons', 'docetc', 'UserGuide.html', 'PyMailGui-PP4E' # but this will be stripped later #'Launch_PyEdit.pyw' # runs as source if full PyMailGUI pkg included ] # not used here: extras = extras + scriptfreeze for name in extras: if os.path.isfile(name): shutil.copy(name, 'dist') else: shutil.copytree(name, join('dist', name)) #---------------------------------------------------------------------------- # duplicate nested PyEdit's source dir items to a subfolder in dist root; # these items are special-cased by access code in PyEdit when embedded: # pyedit's own fixfrozenpaths.py uses this as its install dir if present; # the subprocproxy exe is copied from PyEdit's build tree, so it will have # the full py standard lib baked in (this means PyEdit must be built first!); # # UPDATE: we're punting on the subprocproxy, and disabling Run Code's # Capture mode altogether when it's being used in a PyInstaller frozen # executable other than PyEdit's own Pyinstaller exe. This turned out # to be too much trouble for too little reward -- running code in an # email client is probably academic novelty at best. Capture mode still # works in source-code packages, and PyEdit apps and executables. In # the Mac PyMailGUI app, it works but has minimal std libs baked in # (but it was later disabled in PyMailGUI there too for consistency). #---------------------------------------------------------------------------- print('Duplicating PyEdit data to PyMailGui-PP4E\__pyedit-component-data__') os.chdir(join('dist', 'PyMailGui-PP4E')) componentdir = '__pyedit-component-data__' pyeditdir = r'PP4E\Gui\TextEditor' os.mkdir(componentdir) items = ('UserGuide.html', 'README.txt', join('icons', 'pyedit-window-main.gif'), join('icons', 'pyedit256.png')) for item in items: source = join(pyeditdir, item) if os.sep not in item: dest = componentdir else: dest = join(componentdir, os.path.dirname(item)) if not os.path.exists(dest): os.makedirs(dest) # or exists_ok=True shutil.copy(source, dest) # plus docetc folder for PyEdit's UseGuide.html images (adds 32M; blah) shutil.copytree(join(pyeditdir, 'docetc'), join(componentdir, 'docetc')) # back to temp dir os.chdir(join('..', '..')) r""" ............................................................................... Nope - see above # and now for the proxy exe (it's already been nuked from temp dir...); # the pyedit build must be run and unzipped for this to work -- quite # a hack (?), but it avoids redoing the proxy's stdlib shenanigans here; pyeditproxy= (r'C:\Users\lutz\Desktop' r'\MY-STUFF\Code\pymailgui\PyMailGui-PP4E\PP4E\Gui\TextEditor' r'\build\build-app-exe\windows\PyEdit-%dbit\subprocproxy.exe' % bitsize) if not os.path.exists(pyeditproxy): print('Error: PyEdit\'s proxy is not built or unzipped') sys.exit(1) else: shutil.copy(pyeditproxy, componentdir) ............................................................................... """ #---------------------------------------------------------------------------- # move the PyMailGui exe up and tidy up its folder; unlike the Mac app, # we don't have a Python executable running the launcher that embeds the # full package that PyMailGui is a part of (PyMailGui-PP4E), so we can't # simply spawn PyMailGui's source code with such a Python; instead, # freeze PyMailGui separately, drop its source, and spawn its executable; # CAUTION: the border/bar icons listed here are all that make the cut # (even though the code would use any .ico and .gif placed in the folder); #---------------------------------------------------------------------------- # move up frozen exe in nested dist os.chdir(join('dist', 'PyMailGui-PP4E')) shutil.move(join('dist', 'PyMailGui.exe'), '.') # nested build's dist manifest = ('__pyedit-component-data__', 'PyMailGui.exe', 'mailconfig.py', 'textConfig.py', 'Drafts', 'media', 'SavedMail', 'TempParts', 'sentmail.txt', '__pyedit-autosaves__', #'pymailgui.gif', 'pymailgui.ico', 'pymailguiplainset.ico', 'pymailgui.gif', 'README.txt', 'PyMailGuiHelp.html') # drop nonessentials in package folder for item in os.listdir('.'): if item not in manifest: print('Pruning', item) if os.path.isdir(item): shutil.rmtree(item) else: os.remove(item) # back to temp dir os.chdir(join('..', '..')) #---------------------------------------------------------------------------- # 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 = 'PyMailGUI' + ('-64bit' if bitsize == 64 else '-32bit') thezip = thedir + '.zip' if bitsize == 64: code = r'C:\Users\lutz\Desktop\MY-STUFF\Code\mergeall\test\ziptools' else: code = r'D:\MY-STUFF\Code\mergeall\test\ziptools' # 32 bits now defunct # 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'\pymailgui\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