Use pyinstaller to freeze the application.

master
Pacman Ghost 6 years ago
parent b524af57c2
commit 8a3acb19c6
  1. 145
      _freeze.py
  2. 2
      setup.py
  3. 7
      vasl_templates/main_window.py
  4. 11
      vasl_templates/webapp/__init__.py
  5. 6
      vasl_templates/webapp/config/constants.py

@ -4,16 +4,16 @@
import sys
import os
import shutil
import glob
import tempfile
import time
import datetime
import getopt
from cx_Freeze import setup, Executable
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION, APP_DESCRIPTION
from PyInstaller.__main__ import run as run_pyinstaller
BASE_DIR = os.path.split( os.path.abspath(__file__) )[ 0 ]
BUILD_DIR = os.path.join( BASE_DIR, "build" )
MAIN_ENTRY_POINT = "vasl_templates/main.py"
MAIN_SCRIPT = "vasl_templates/main.py"
APP_ICON = os.path.join( BASE_DIR, "vasl_templates/webapp/static/images/app.ico" )
TARGET_NAMES = {
@ -23,24 +23,16 @@ DEFAULT_TARGET_NAME = "vasl-templates"
# ---------------------------------------------------------------------
def get_extra_files():
"""Get the extra files to include in the release."""
def globfiles( fspec ): #pylint: disable=missing-docstring,unused-variable
fnames = glob.glob( fspec )
return zip( fnames, fnames )
extra_files = []
extra_files.append( "LICENSE.txt" )
return extra_files
# ---------------------------------------------------------------------
# parse the command-line options
output_fname = None
work_dir = None
cleanup = True
opts,args = getopt.getopt( sys.argv[1:], "o:", ["output=","noclean"] )
opts,args = getopt.getopt( sys.argv[1:], "o:w:", ["output=","work=","noclean"] )
for opt,val in opts:
if opt in ["-o","--output"]:
output_fname = val.strip()
elif opt in ["-w","--work"]:
work_dir = val.strip()
elif opt in ["--noclean"]:
cleanup = False
else:
@ -48,6 +40,18 @@ for opt,val in opts:
if not output_fname:
raise RuntimeError( "No output file was specified." )
# figure out where to locate our work directories
if work_dir:
build_dir = os.path.join( work_dir, "build" )
if os.path.isdir( build_dir ):
shutil.rmtree( build_dir )
dist_dir = os.path.join( work_dir, "dist" )
if os.path.isdir( dist_dir ):
shutil.rmtree( dist_dir )
else:
build_dir = tempfile.mkdtemp()
dist_dir = tempfile.mkdtemp()
# figure out the format of the release archive
formats = { ".zip": "zip", ".tar.gz": "gztar", ".tar.bz": "bztar", ".tar": "tar" }
output_fmt = None
@ -59,54 +63,68 @@ for extn,fmt in formats.items():
if not output_fmt:
raise RuntimeError( "Unknown release archive format: {}".format( os.path.split(output_fname)[1] ) )
# initialize the build options
build_options = {
"packages": [ "os", "asyncio", "jinja2" ],
"excludes": [ "tkinter" ],
"include_files": get_extra_files(),
}
# configure pyinstaller
# NOTE: Using UPX gave ~25% saving on Windows, but failed to run because of corrupt DLL's :-/
# NOTE: Setting --specpath breaks the build - it's being used as the project root...? (!)
target_name = TARGET_NAMES.get( sys.platform, DEFAULT_TARGET_NAME )
args = [
"--distpath", dist_dir,
"--workpath", build_dir,
"--onefile",
"--name", target_name,
]
# NOTE: We also need to include the config/ and data/ subdirectories, but we would like to
# make them available to the user, so we include them ourself in the final release archive.
def map_dir( src, dest ): #pylint: disable=missing-docstring
args.extend( [ "--add-data", src + os.pathsep + dest ] )
map_dir( "vasl_templates/ui", "vasl_templates/ui" )
map_dir( "vasl_templates/resources", "vasl_templates/resources" )
map_dir( "vasl_templates/webapp/static", "static" )
map_dir( "vasl_templates/webapp/templates", "templates" )
if sys.platform == "win32":
args.append( "--noconsole" )
args.extend( [ "--icon", APP_ICON ] )
# NOTE: These files are not always required but it's probably safer to always include them.
import distutils.sysconfig #pylint: disable=import-error
dname = os.path.join( distutils.sysconfig.get_python_lib() , "PyQt5/Qt/bin" )
args.extend( [ "--add-binary", os.path.join(dname,"libEGL.dll") + os.pathsep + "PyQt5/Qt/bin" ] )
args.extend( [ "--add-binary", os.path.join(dname,"libGLESv2.dll") + os.pathsep + "PyQt5/Qt/bin" ] )
args.append( MAIN_SCRIPT )
# freeze the application
# NOTE: It would be nice to be able to use py2exe to compile this for Windows (since it produces
# a single EXE instead of the morass of files cx-freeze generates) but py2exe only works up to
# Python 3.4, since the byte code format changed after that.
target = Executable(
MAIN_ENTRY_POINT,
base = "Win32GUI" if sys.platform == "win32" else None,
targetName = TARGET_NAMES.get( sys.platform, DEFAULT_TARGET_NAME ),
icon = APP_ICON
)
if os.path.isdir( BUILD_DIR ):
shutil.rmtree( BUILD_DIR )
start_time = time.time()
os.chdir( BASE_DIR )
del sys.argv[1:]
sys.argv.append( "build" )
# nb: cx-freeze doesn't report compile errors or anything like that :-/
setup(
name = APP_NAME,
version = APP_VERSION,
description = APP_DESCRIPTION,
options = {
"build_exe": build_options
},
executables = [ target ]
)
print()
# locate the release files
files = os.listdir( BUILD_DIR )
if len(files) != 1:
raise RuntimeError( "Unexpected freeze output." )
dname = os.path.join( BUILD_DIR, files[0] )
os.chdir( dname )
# remove some unwanted files
for fname in ["debug.cfg","logging.cfg"]:
fname = os.path.join( "lib/vasl_templates/webapp/config", fname )
run_pyinstaller( args ) # nb: this doesn't return any indication if it worked or not :-/
# add extra files to the distribution
def ignore_files( dname, fnames ): #pylint: disable=redefined-outer-name
"""Return files to ignore during copytree()."""
# ignore Python cached files
ignore = [ "__pycache__" ]
# ignore dot files
ignore.extend( f for f in fnames if f.startswith(".") )
# ignore anything in .gitignore
fname = os.path.join( dname, ".gitignore" )
if os.path.isfile( fname ):
os.unlink( fname )
for line_buf in open(fname,"r"):
line_buf = line_buf.strip()
if not line_buf or line_buf.startswith("#"):
continue
ignore.append( line_buf ) # nb: we assume normal filenames i.e. no globbing
return ignore
shutil.copy( "LICENSE.txt", dist_dir )
shutil.copytree( "vasl_templates/webapp/data", os.path.join(dist_dir,"data") )
shutil.copytree( "vasl_templates/webapp/config", os.path.join(dist_dir,"config"), ignore=ignore_files )
# copy the examples
dname = os.path.join( dist_dir, "examples" )
os.makedirs( dname )
fnames = [ f for f in os.listdir("examples") if os.path.splitext(f)[1] in (".json",".png") ]
for f in fnames:
shutil.copy( os.path.join("examples",f), dname )
# create the release archive
os.chdir( dist_dir )
print()
print( "Generating release archive: {}".format( output_fname ) )
shutil.make_archive( output_fname2, output_fmt )
file_size = os.path.getsize( output_fname )
@ -115,4 +133,11 @@ print( "- Done: {0:.1f} MB".format( float(file_size) / 1024 / 1024 ) )
# clean up
if cleanup:
os.chdir( BASE_DIR ) # so we can delete the build directory :-/
shutil.rmtree( BUILD_DIR )
os.unlink( target_name + ".spec" )
shutil.rmtree( build_dir )
shutil.rmtree( dist_dir )
# log the elapsed time
elapsed_time = time.time() - start_time
print()
print( "Elapsed time: {}".format( datetime.timedelta( seconds=int(elapsed_time) ) ) )

@ -31,7 +31,7 @@ setup(
"lxml==4.2.4",
"pylint==1.9.2",
"pytest-pylint==0.9.0",
"cx-Freeze==5.1.1",
"PyInstaller==3.4",
],
},
include_package_data = True,

@ -1,5 +1,6 @@
""" Main application window. """
import sys
import os
import re
import json
@ -54,8 +55,12 @@ class MainWindow( QWidget ):
# initialize the main window
self.setWindowTitle( APP_NAME )
if getattr( sys, "frozen", False ):
dname = sys._MEIPASS #pylint: disable=no-member,protected-access
else:
dname = os.path.join( os.path.split(__file__)[0], "webapp" )
self.setWindowIcon( QIcon(
os.path.join( os.path.split(__file__)[0], "webapp/static/images/app.ico" )
os.path.join( dname, "static/images/app.ico" )
) )
# create the menu

@ -1,5 +1,6 @@
""" Initialize the package. """
import sys
import os
import configparser
import logging
@ -28,7 +29,15 @@ def load_debug_config( fname ):
# ---------------------------------------------------------------------
# initialize Flask
app = Flask( __name__ )
if getattr( sys, "frozen", False ):
# NOTE: The support directories must have been set up by pyinstaller (via --add-data).
meipass = sys._MEIPASS #pylint: disable=no-member,protected-access
app = Flask( __name__,
template_folder = os.path.join( meipass, "templates" ),
static_folder = os.path.join( meipass, "static" )
)
else:
app = Flask( __name__ )
# load the application configuration
config_dir = os.path.join( BASE_DIR, "config" )

@ -1,10 +1,14 @@
""" Application constants. """
import sys
import os
APP_NAME = "VASL Templates"
APP_VERSION = "v0.4" # nb: also update setup.py
APP_DESCRIPTION = "Generate HTML for use in VASL scenarios."
BASE_DIR = os.path.abspath( os.path.join( os.path.split(__file__)[0], ".." ) )
if getattr( sys, "frozen", False ):
BASE_DIR = os.path.split( sys.executable )[0]
else:
BASE_DIR = os.path.abspath( os.path.join( os.path.split(__file__)[0], ".." ) )
DATA_DIR = os.path.join( BASE_DIR, "data" )

Loading…
Cancel
Save