#!/usr/bin/env python3 """ Compile the application and create a release. """ import sys import os import shutil import subprocess import tempfile import time import datetime import json import re import getopt from PyInstaller.__main__ import run as run_pyinstaller BASE_DIR = os.path.split( os.path.abspath(__file__) )[ 0 ] MAIN_SCRIPT = "vasl_templates/main.py" APP_ICON = os.path.join( BASE_DIR, "vasl_templates/webapp/static/images/app.ico" ) # --------------------------------------------------------------------- def main( args ): #pylint: disable=too-many-locals """Main processing.""" # parse the command-line options output_fname = None no_loader = False work_dir = None cleanup = True opts,args = getopt.getopt( sys.argv[1:], "o:w:", ["output=","no-loader","work=","no-clean"] ) for opt, val in opts: if opt in ["-o","--output"]: output_fname = val.strip() elif opt in ["--no-loader"]: no_loader = True elif opt in ["-w","--work"]: work_dir = val.strip() elif opt in ["--no-clean"]: cleanup = False else: raise RuntimeError( "Unknown argument: {}".format( opt ) ) if not output_fname: raise RuntimeError( "No output file was specified." ) # figure out where to locate our work directories if work_dir: work_dir = os.path.abspath( 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 for extn,fmt in formats.items(): if output_fname.endswith( extn ): output_fmt = fmt output_fname2 = output_fname[:-len(extn)] break if not output_fmt: raise RuntimeError( "Unknown release archive format: {}".format( os.path.split(output_fname)[1] ) ) # configure pyinstaller # NOTE: Using UPX gave ~25% saving on Windows, but failed to run because of corrupt DLL's :-/ target_name = make_target_name( "vasl-templates" ) args = [ "--distpath", dist_dir, "--workpath", build_dir, "--specpath", build_dir, "--onefile", "--name", target_name, ] args.extend( [ "--add-data", os.path.join( BASE_DIR, "vassal-shim/release/vassal-shim.jar" + os.pathsep + "vasl_templates/webapp" ) ] ) # 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", os.path.join( BASE_DIR, 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", "vasl_templates/webapp/static" ) map_dir( "vasl_templates/webapp/templates", "vasl_templates/webapp/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 sysconfig dname = os.path.join( sysconfig.get_path("platlib") , "PyQt5/Qt5/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 start_time = time.time() os.chdir( BASE_DIR ) 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 cache files ignore = [ "__pycache__", "GPUCache" ] # ignore dot files ignore.extend( f for f in fnames if f.startswith(".") ) # ignore Python files ignore.extend( f for f in fnames if os.path.splitext(f)[1] == ".py" ) # ignore anything in .gitignore fname = os.path.join( dname, ".gitignore" ) if os.path.isfile( fname ): with open( fname, "r", encoding="utf-8" ) as fp: for line_buf in fp: 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 ) # set the build info build_info = { "timestamp": int( time.time() ), } build_info.update( get_git_info() ) dname = os.path.join( dist_dir, "config" ) fname = os.path.join( dname, "build-info.json" ) with open( fname, "w", encoding="utf-8" ) as fp: json.dump( build_info, fp ) # freeze the loader if no_loader: print( "Not including the loader." ) else: print( "--- BEGIN FREEZE LOADER ---" ) shutil.move( os.path.join( dist_dir, target_name ), os.path.join( dist_dir, make_target_name("vasl-templates-main") ) ) from loader.freeze import freeze_loader #pylint: disable=no-name-in-module freeze_loader( os.path.join( dist_dir, target_name ), build_dir, # nb: a "loader" sub-directory will be created and used False # nb: we will clean up, or not, everything ourself ) # 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 ) 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 ) 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) ) ) ) # --------------------------------------------------------------------- def get_git_info(): """Get the git branch/commit we're building from.""" # get the latest commit ID proc = subprocess.run( [ "git", "log" ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=True ) buf = proc.stdout.split( "\n" )[0] mo = re.search( r"^commit ([a-z0-9]+)$", buf ) last_commit_id = mo.group(1) # get the current git branch proc = subprocess.run( [ "git", "branch" ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", check=True ) lines = [ s for s in proc.stdout.split("\n") if s.startswith("* ") ] if len(lines) != 1: raise RuntimeError( "Can't parse git branch status." ) branch_name = lines[0][2:] if branch_name.startswith( "(HEAD detached at" ) and branch_name.endswith( ")" ): branch_name = branch_name[18:-1] return { "last_commit_id": last_commit_id, "branch_name": branch_name } def make_target_name( fname ): """Generate a target filename.""" return fname+".exe" if sys.platform == "win32" else fname # --------------------------------------------------------------------- if __name__ == "__main__": main( sys.argv[1:] )