#!/usr/bin/env python2.7 from __future__ import print_function from os.path import dirname, join, isfile, realpath, relpath, split, exists from os import makedirs import os import tarfile import time import subprocess import shutil from zipfile import ZipFile import sys import re import shlex from fnmatch import fnmatch import jinja2 if os.name == 'nt': ANDROID = 'android.bat' ANT = 'ant.bat' else: ANDROID = 'android' ANT = 'ant' curdir = dirname(__file__) # Try to find a host version of Python that matches our ARM version. PYTHON = join(curdir, 'python-install', 'bin', 'python.host') BLACKLIST_PATTERNS = [ # code versionning '^*.hg/*', '^*.git/*', '^*.bzr/*', '^*.svn/*', # pyc/py '*.pyc', '*.py', # temp files '~', '*.bak', '*.swp', ] WHITELIST_PATTERNS = [] python_files = [] environment = jinja2.Environment(loader=jinja2.FileSystemLoader( join(curdir, 'templates'))) def render(template, dest, **kwargs): '''Using jinja2, render `template` to the filename `dest`, supplying the keyword arguments as template parameters. ''' dest_dir = dirname(dest) if dest_dir and not exists(dest_dir): makedirs(dest_dir) template = environment.get_template(template) text = template.render(**kwargs) f = open(dest, 'wb') f.write(text.encode('utf-8')) f.close() def is_whitelist(name): return match_filename(WHITELIST_PATTERNS, name) def is_blacklist(name): if is_whitelist(name): return False return match_filename(BLACKLIST_PATTERNS, name) def match_filename(pattern_list, name): for pattern in pattern_list: if pattern.startswith('^'): pattern = pattern[1:] else: pattern = '*/' + pattern if fnmatch(name, pattern): return True def listfiles(d): basedir = d subdirlist = [] for item in os.listdir(d): fn = join(d, item) if isfile(fn): yield fn else: subdirlist.append(join(basedir, item)) for subdir in subdirlist: for fn in listfiles(subdir): yield fn def make_python_zip(): ''' Search for all the python related files, and construct the pythonXX.zip According to # http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html site-packages, config and lib-dynload will be not included. ''' if not exists('private'): print('No compiled python is present to zip, skipping.') print('this should only be the case if you are using the CrystaX python') return global python_files d = realpath(join('private', 'lib', 'python2.7')) def select(fn): if is_blacklist(fn): return False fn = realpath(fn) assert(fn.startswith(d)) fn = fn[len(d):] if (fn.startswith('/site-packages/') or fn.startswith('/config/') or fn.startswith('/lib-dynload/') or fn.startswith('/libpymodules.so')): return False return fn # get a list of all python file python_files = [x for x in listfiles(d) if select(x)] # create the final zipfile zfn = join('private', 'lib', 'python27.zip') zf = ZipFile(zfn, 'w') # put all the python files in it for fn in python_files: afn = fn[len(d):] zf.write(fn, afn) zf.close() def make_tar(tfn, source_dirs, ignore_path=[]): ''' Make a zip file `fn` from the contents of source_dis. ''' # selector function def select(fn): rfn = realpath(fn) for p in ignore_path: if p.endswith('/'): p = p[:-1] if rfn.startswith(p): return False if rfn in python_files: return False return not is_blacklist(fn) # get the files and relpath file of all the directory we asked for files = [] for sd in source_dirs: sd = realpath(sd) compile_dir(sd) files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd) if select(x)] # create tar.gz of thoses files tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT) dirs = [] for fn, afn in files: # print('%s: %s' % (tfn, fn)) dn = dirname(afn) if dn not in dirs: # create every dirs first if not exist yet d = '' for component in split(dn): d = join(d, component) if d.startswith('/'): d = d[1:] if d == '' or d in dirs: continue dirs.append(d) tinfo = tarfile.TarInfo(d) tinfo.type = tarfile.DIRTYPE tf.addfile(tinfo) # put the file tf.add(fn, afn) tf.close() def compile_dir(dfn): ''' Compile *.py in directory `dfn` to *.pyo ''' subprocess.call([PYTHON, '-OO', '-m', 'compileall', '-f', dfn]) # import compileall # compileall.compile_dir(dfn, quiet=1) def make_package(args): if exists('res'): shutil.rmtree('res') os.mkdir('res') if exists('assets'): shutil.rmtree('assets') os.mkdir('assets') shutil.copytree( os.path.join('templates', 'drawable', 'drawable-xxhdpi'), os.path.join('res', 'drawable') ) if args.icon: os.unlink(os.path.join('res', 'drawable', 'icon.png')) shutil.copy( args.icon or default_icon, os.path.join('res', 'drawable', 'icon.png') ) # In order to speedup import and initial depack, # construct a python27.zip make_python_zip() # Package up the private and public data. # AND: Just private for now tar_dirs = [args.private] if exists('private'): tar_dirs.append('private') if exists('crystax_python'): tar_dirs.append('crystax_python') if args.private: make_tar('assets/private.mp3', tar_dirs, args.ignore_path) else: make_tar('assets/private.mp3', ['private']) # Prepare some variables for templating process # If extra Java jars were requested, copy them into the libs directory if args.add_jar: for jarname in args.add_jar: if not exists(jarname): print('Requested jar does not exist: {}'.format(jarname)) sys.exit(-1) shutil.copy(jarname, 'libs') versioned_name = (args.name.replace(' ', '').replace('\'', '') + '-' + args.version) version_code = 0 if not args.numeric_version: for i in args.version.split('.'): version_code *= 100 version_code += int(i) args.numeric_version = str(version_code) if args.intent_filters: with open(args.intent_filters) as fd: args.intent_filters = fd.read() if args.extra_source_dirs: esd = [] for spec in args.extra_source_dirs: if ':' in spec: specdir, specincludes = spec.split(':') else: specdir = spec specincludes = '**' esd.append((realpath(specdir), specincludes)) args.extra_source_dirs = esd else: args.extra_source_dirs = [] service = False service_main = join(realpath(args.private), 'service', 'main.py') render( 'BroadcastLauncher.tmpl.java', os.path.join( 'src', args.package.replace('.', os.path.sep), 'BroadcastLauncher.java' ), args=args ) render( 'AndroidManifest.tmpl.xml', 'AndroidManifest.xml', args=args ) render( 'Launcher.tmpl.java', os.path.join( 'src', args.package.replace('.', os.path.sep), 'Launcher.java' ), args=args ) render( 'build.tmpl.xml', 'build.xml', args=args, versioned_name=versioned_name ) render( 'strings.tmpl.xml', 'res/values/strings.xml', args=args ) def parse_args(args=None): global BLACKLIST_PATTERNS, WHITELIST_PATTERNS default_android_api = 12 import argparse ap = argparse.ArgumentParser(description='''\ Package a Python application for Android. For this to work, Java and Ant need to be in your path, as does the tools directory of the Android SDK. ''') ap.add_argument('--private', dest='private', help='the dir of user files', required=True) ap.add_argument('--package', dest='package', help=('The name of the java package the project will be' ' packaged under.'), required=True) ap.add_argument('--name', dest='name', help=('The human-readable name of the project.'), required=False) ap.add_argument('--numeric-version', dest='numeric_version', help=('The numeric version number of the project. If not ' 'given, this is automatically computed from the ' 'version.')) ap.add_argument('--version', dest='version', help=('The version number of the project. This should ' 'consist of numbers and dots, and should have the ' 'same number of groups of numbers as previous ' 'versions.'), required=False) ap.add_argument('--orientation', dest='orientation', default='portrait', help=('The orientation that the game will display in. ' 'Usually one of "landscape", "portrait" or ' '"sensor"')) ap.add_argument('--icon', dest='icon', help='A png file to use as the icon for the application.') ap.add_argument('--permission', dest='permissions', action='append', help='The permissions to give this app.') ap.add_argument('--meta-data', dest='meta_data', action='append', help='Custom key=value to add in application metadata') ap.add_argument('--wakelock', dest='wakelock', action='store_true', help=('Indicate if the application needs the device ' 'to stay on')) ap.add_argument('--window', dest='window', action='store_false', help='Indicate if the application will be windowed') ap.add_argument('--blacklist', dest='blacklist', default=join(curdir, 'blacklist.txt'), help=('Use a blacklist file to match unwanted file in ' 'the final APK')) ap.add_argument('--whitelist', dest='whitelist', default=join(curdir, 'whitelist.txt'), help=('Use a whitelist file to prevent blacklisting of ' 'file in the final APK')) ap.add_argument('--add-jar', dest='add_jar', action='append', help=('Add a Java .jar to the libs, so you can access its ' 'classes with pyjnius. You can specify this ' 'argument more than once to include multiple jars')) ap.add_argument('--sdk', dest='sdk_version', default=-1, type=int, help=('Android SDK version to use. Default to ' 'the value of minsdk')) ap.add_argument('--minsdk', dest='min_sdk_version', default=default_android_api, type=int, help=('Minimum Android SDK version to use. Default to ' 'the value of ANDROIDAPI, or {} if not set' .format(default_android_api))) ap.add_argument('--intent-filters', dest='intent_filters', help=('Add intent-filters xml rules to the ' 'AndroidManifest.xml file. The argument is a ' 'filename containing xml. The filename should be ' 'located relative to the python-for-android ' 'directory')) ap.add_argument('--service', dest='services', action='append', help='Declare a new service entrypoint: ' 'NAME:PATH_TO_PY[:foreground]') ap.add_argument('--add-source', dest='extra_source_dirs', action='append', help='Include additional source dirs in Java build') ap.add_argument('--sign', action='store_true') # Just ignore def _read_configuration(): # search for a .p4a configuration file in the current directory if not exists(".p4a"): return print("Reading .p4a configuration") with open(".p4a") as fd: lines = fd.readlines() lines = [shlex.split(line) for line in lines if not line.startswith("#")] for line in lines: for arg in line: sys.argv.append(arg) _read_configuration() if args is None: args = sys.argv[1:] args = ap.parse_args(args) args.ignore_path = [] if args.sdk_version == -1: args.sdk_version = args.min_sdk_version if args.permissions is None: args.permissions = [] if args.meta_data is None: args.meta_data = [] if args.services is None: args.services = [] if args.blacklist: with open(args.blacklist) as fd: patterns = [x.strip() for x in fd.read().splitlines() if x.strip() and not x.strip().startswith('#')] BLACKLIST_PATTERNS += patterns if args.whitelist: with open(args.whitelist) as fd: patterns = [x.strip() for x in fd.read().splitlines() if x.strip() and not x.strip().startswith('#')] WHITELIST_PATTERNS += patterns make_package(args) return args if __name__ == "__main__": parse_args()