Skip to content

Instantly share code, notes, and snippets.

@isis
Created July 29, 2013 12:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save isis/6103982 to your computer and use it in GitHub Desktop.
Save isis/6103982 to your computer and use it in GitHub Desktop.
remove x86 support from Titanium SDK 3.1.1 GA android/builder.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Appcelerator Titanium Mobile
# Copyright (c) 2011-2012 by Appcelerator, Inc. All Rights Reserved.
# Licensed under the terms of the Apache Public License
# Please see the LICENSE included with this distribution for details.
#
# General builder script for staging, packaging, deploying,
# and debugging Titanium Mobile applications on Android
#
import os, sys, subprocess, shutil, time, signal, string, platform, re, glob, hashlib, imp, inspect
import run, avd, prereq, zipfile, tempfile, fnmatch, codecs, traceback, sgmllib
from os.path import splitext
from compiler import Compiler
from os.path import join, splitext, split, exists
from shutil import copyfile
from xml.dom.minidom import parseString
from tilogger import *
from datetime import datetime, timedelta
template_dir = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
top_support_dir = os.path.dirname(template_dir)
sys.path.append(top_support_dir)
sys.path.append(os.path.join(top_support_dir, 'common'))
sys.path.append(os.path.join(top_support_dir, 'module'))
import simplejson, java
from mako.template import Template
from tiapp import *
from android import Android
from androidsdk import AndroidSDK
from deltafy import Deltafy, Delta
from css import csscompiler
from module import ModuleDetector
import localecompiler
import fastdev
import requireIndex
resourceFiles = ['strings.xml', 'attrs.xml', 'styles.xml', 'bools.xml', 'colors.xml',
'dimens.xml', 'ids.xml', 'integers.xml', 'arrays.xml']
ignoreFiles = ['.gitignore', '.cvsignore', '.DS_Store'];
ignoreDirs = ['.git','.svn','_svn', 'CVS'];
android_avd_hw = {'hw.camera': 'yes', 'hw.gps':'yes'}
res_skips = ['style']
log = None
# Copied from frameworks/base/tools/aapt/Package.cpp
uncompressed_types = [
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv"
]
# Java keywords to reference in case app id contains java keyword
java_keywords = [
"abstract", "continue", "for", "new", "switch",
"assert", "default", "goto", "package", "synchronized",
"boolean", "do", "if", "private", "this",
"break", "double", "implements", "protected", "throw",
"byte", "else", "import", "public", "throws",
"case", "enum", "instanceof", "return", "transient",
"catch", "extends", "int", "short", "try",
"char", "final", "interface", "static", "void",
"class", "finally", "long", "strictfp", "volatile",
"const", "float", "native", "super", "while",
"true", "false", "null"
]
MIN_API_LEVEL = 10
HONEYCOMB_MR2_LEVEL = 13
# remove x86 support
#KNOWN_ABIS = ("armeabi", "armeabi-v7a", "x86")
KNOWN_ABIS = ("armeabi", "armeabi-v7a")
# Used only to find <script> tags in HTML files
# so we can be sure to package referenced JS files
# even when compiling for production. (See
# Builder.package_and_deploy later in this file.)
class HTMLParser(sgmllib.SGMLParser):
def parse(self, html_source):
self.feed(html_source)
self.close()
def __init__(self, verbose=0):
sgmllib.SGMLParser.__init__(self, verbose)
self.referenced_js_files = []
def start_script(self, attributes):
for name, value in attributes:
if value and name.lower() == "src":
self.referenced_js_files.append(value.lower())
def get_referenced_js_files(self):
return self.referenced_js_files
def launch_logcat():
valid_device_switches = ('-e', '-d', '-s')
device_id = None
android_sdk_location = None
adb_location = None
logcat_process = None
device_switch = None # e.g., -e or -d or -s
def show_usage():
print >> sys.stderr, ""
print >> sys.stderr, "%s devicelog <sdk_dir> <device_switch> [device_serial_number]" % os.path.basename(sys.argv[0])
print >> sys.stderr, ""
print >> sys.stderr, "The <device_switch> can be -e, -d -s. If -s, also pass serial number."
sys.exit(1)
if len(sys.argv) < 3:
print >> sys.stderr, "Missing Android SDK location."
show_usage()
else:
android_sdk_location = os.path.abspath(os.path.expanduser(sys.argv[2]))
adb_location = AndroidSDK(android_sdk_location).get_adb()
if len(sys.argv) < 4:
print >> sys.stderr, "Missing device/emulator switch (e.g., -e, -d, -s)."
show_usage()
device_switch = sys.argv[3]
if device_switch not in valid_device_switches:
print >> sys.stderr, "Unknown device type switch: %s" % device_switch
show_usage()
if device_switch == "-s":
if len(sys.argv) < 5:
print >> sys.stderr, "Must specify serial number when using -s."
show_usage()
else:
device_id = sys.argv[4]
# For killing the logcat process if our process gets killed.
def signal_handler(signum, frame):
print "[DEBUG] Signal %s received. Terminating the logcat process." % signum
if logcat_process is not None:
if platform.system() == "Windows":
os.system("taskkill /F /T /PID %i" % logcat_process.pid)
else:
os.kill(logcat_process.pid, signal.SIGTERM)
# make sure adb is running on windows, else XP can lockup the python
# process when adb runs first time
if platform.system() == "Windows":
run.run([adb_location, "start-server"], True, ignore_output=True)
logcat_cmd = [adb_location, device_switch]
if device_id:
logcat_cmd.append(device_id)
logcat_cmd.extend(["logcat", "-s", "*:d,*,TiAPI:V"])
logcat_process = subprocess.Popen(logcat_cmd)
if platform.system() != "Windows":
signal.signal(signal.SIGHUP, signal_handler)
signal.signal(signal.SIGQUIT, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGABRT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# In case it's gonna exit early (like if the command line
# was wrong or something) give it a chance to do so before we start
# waiting on it.
time.sleep(1)
return_code = logcat_process.poll()
if return_code:
signal_handler(signal.SIGQUIT, None)
sys.exit(return_code)
# Now wait for it.
try:
return_code = logcat_process.wait()
except OSError:
signal_handler(signal.SIGQUIT, None)
sys.exit(return_code)
sys.exit(return_code)
def render_template_with_tiapp(template_text, tiapp_obj):
t = Template(template_text)
return t.render(tiapp=tiapp_obj)
def remove_ignored_dirs(dirs):
for d in dirs:
if d in ignoreDirs:
dirs.remove(d)
# ZipFile.extractall introduced in Python 2.6, so this is workaround for earlier
# versions
def zip_extractall(zfile, target_dir):
file_infos = zfile.infolist()
for info in file_infos:
if info.file_size > 0:
file_path = os.path.join(target_dir, os.path.normpath(info.filename))
parent_path = os.path.dirname(file_path)
if not os.path.exists(parent_path):
os.makedirs(parent_path)
out_file = open(file_path, "wb")
out_file.write(zfile.read(info.filename))
out_file.close()
def dequote(s):
if s[0:1] == '"':
return s[1:-1]
return s
def pipe(args1,args2):
p1 = subprocess.Popen(args1, stdout=subprocess.PIPE)
p2 = subprocess.Popen(args2, stdin=p1.stdout, stdout=subprocess.PIPE)
return p2.communicate()[0]
def read_properties(propFile, separator=":= "):
propDict = dict()
for propLine in propFile:
propDef = propLine.strip()
if len(propDef) == 0:
continue
if propDef[0] in ( '!', '#' ):
continue
punctuation= [ propDef.find(c) for c in separator ] + [ len(propDef) ]
found= min( [ pos for pos in punctuation if pos != -1 ] )
name= propDef[:found].rstrip()
value= propDef[found:].lstrip(separator).rstrip()
propDict[name]= value
propFile.close()
return propDict
def info(msg):
log.info(msg)
def debug(msg):
log.debug(msg)
def warn(msg):
log.warn(msg)
def trace(msg):
log.trace(msg)
def error(msg):
log.error(msg)
def copy_all(source_folder, dest_folder, mergeXMLResources=False, ignore_dirs=[], ignore_files=[], ignore_exts=[], one_time_msg=""):
msg_shown = False
for root, dirs, files in os.walk(source_folder, True, None, True):
for d in dirs:
if d in ignore_dirs:
dirs.remove(d)
for f in files:
if f in ignore_files:
continue
ext = os.path.splitext(f)[1]
if ext in ignore_exts:
continue
if one_time_msg and not msg_shown:
info(one_time_msg)
msg_shown = True
from_ = os.path.join(root, f)
to_ = from_.replace(source_folder, dest_folder, 1)
to_directory = os.path.split(to_)[0]
if not os.path.exists(to_directory):
os.makedirs(to_directory)
shutil.copyfile(from_, to_)
#
# Merge the xml resource files in res/values/ if there are multiple files with the same name.
# (TIMOB-12663)
#
elif mergeXMLResources and os.path.isfile(to_) and f in resourceFiles:
sfile = open(from_, 'r')
dfile = open(to_, 'r')
scontent = sfile.read()
dcontent = dfile.read()
sfile.close()
dfile.close()
sindex = scontent.find('</resources>')
dindex = dcontent.find('>', dcontent.find('<resources')) + 1
content_to_write = scontent[:sindex] + dcontent[dindex:]
wfile = open(to_, 'w')
wfile.write(content_to_write)
wfile.close()
else:
shutil.copyfile(from_, to_)
def remove_orphaned_files(source_folder, target_folder, ignore=[]):
is_res = source_folder.endswith('Resources') or source_folder.endswith('Resources' + os.sep)
for root, dirs, files in os.walk(target_folder):
for f in files:
if f in ignore:
continue
full = os.path.join(root, f)
rel = full.replace(target_folder, '')
if rel[0] == os.sep:
rel = rel[1:]
is_orphan = False
if not os.path.exists(os.path.join(source_folder, rel)):
is_orphan = True
# But it could be under android/... too (platform-specific)
if is_orphan and is_res:
if os.path.exists(os.path.join(source_folder, 'android', rel)):
is_orphan = False
if is_orphan:
os.remove(full)
def is_resource_drawable(path):
if re.search("android/images/(high|medium|low|res-[^/]+)/", path.replace(os.sep, "/")):
return True
else:
return False
def resource_drawable_folder(path):
if not is_resource_drawable(path):
return None
else:
pattern = r'/android/images/(high|medium|low|res-[^/]+)/'
match = re.search(pattern, path.replace(os.sep, "/"))
if not match.groups():
return None
folder = match.groups()[0]
if re.match('high|medium|low', folder):
return 'drawable-%sdpi' % folder[0]
else:
return 'drawable-%s' % folder.replace('res-', '')
def remove_duplicate_nodes_in_res_file(full_path, node_names_to_check):
f = open(full_path, 'r')
contents = f.read()
f.close()
doc = parseString(contents)
resources_node = doc.getElementsByTagName('resources')[0]
made_change = False
for node_name in node_names_to_check:
nodes = doc.getElementsByTagName(node_name)
if len(nodes) == 0:
continue
name_list = [] #keeps track of the name attribute for the node we are checking
for node in nodes:
# Only check for the children of the "resources" node
if node.parentNode != resources_node:
continue
name = node.getAttribute('name')
# Remove the node with the duplicate names
if name in name_list:
resources_node.removeChild(node)
made_change = True
debug('Removed duplicate node [%s] from %s' %(name, full_path))
else:
name_list.append(name)
if made_change:
new_contents = doc.toxml()
f = codecs.open(full_path, 'w')
f.write(new_contents)
f.close()
class Builder(object):
def __init__(self, name, sdk, project_dir, support_dir, app_id, is_emulator):
self.top_dir = project_dir
self.project_tiappxml = os.path.join(self.top_dir,'tiapp.xml')
self.project_dir = os.path.join(project_dir,'build','android')
self.res_dir = os.path.join(self.project_dir,'res')
self.platform_dir = os.path.join(project_dir, 'platform', 'android')
self.project_src_dir = os.path.join(self.project_dir, 'src')
self.project_gen_dir = os.path.join(self.project_dir, 'gen')
self.name = name
self.app_id = app_id
self.support_dir = support_dir
self.compiled_files = []
self.force_rebuild = False
self.debugger_host = None
self.debugger_port = -1
self.profiler_host = None
self.profiler_port = -1
self.fastdev_port = -1
self.fastdev = False
self.compile_js = False
self.tool_api_level = MIN_API_LEVEL
self.abis = list(KNOWN_ABIS)
# don't build if a java keyword in the app id would cause the build to fail
tok = self.app_id.split('.')
for token in tok:
if token in java_keywords:
error("Do not use java keywords for project app id, such as " + token)
sys.exit(1)
tool_api_level_explicit = False
temp_tiapp = TiAppXML(self.project_tiappxml)
if temp_tiapp and temp_tiapp.android:
if 'tool-api-level' in temp_tiapp.android:
self.tool_api_level = int(temp_tiapp.android['tool-api-level'])
tool_api_level_explicit = True
if 'abi' in temp_tiapp.android and temp_tiapp.android['abi'] != 'all':
tiapp_abis = [abi.strip() for abi in temp_tiapp.android['abi'].split(",")]
to_remove = [bad_abi for bad_abi in tiapp_abis if bad_abi not in KNOWN_ABIS]
if to_remove:
warn("The following ABIs listed in the Android <abi> section of tiapp.xml are unknown and will be ignored: %s." % ", ".join(to_remove))
tiapp_abis = [abi for abi in tiapp_abis if abi not in to_remove]
self.abis = tiapp_abis
if not self.abis:
warn("Android <abi> tiapp.xml section does not specify any valid ABIs. Defaulting to '%s'." %
",".join(KNOWN_ABIS))
self.abis = list(KNOWN_ABIS)
self.sdk = AndroidSDK(sdk, self.tool_api_level)
# If the tool-api-level was not explicitly set in the tiapp.xml, but
# <uses-sdk android:targetSdkVersion> *is* set, try to match the target version.
if (not tool_api_level_explicit and temp_tiapp and temp_tiapp.android_manifest
and "manifest" in temp_tiapp.android_manifest):
self.check_target_api_version(temp_tiapp.android_manifest["manifest"])
self.tiappxml = temp_tiapp
json_contents = open(os.path.join(template_dir,'dependency.json')).read()
self.depends_map = simplejson.loads(json_contents)
# favor the ANDROID_SDK_HOME environment variable if used
if os.environ.has_key('ANDROID_SDK_HOME') and os.path.exists(os.environ['ANDROID_SDK_HOME']):
self.home_dir = os.path.join(os.environ['ANDROID_SDK_HOME'], '.titanium')
self.android_home_dir = os.path.join(os.environ['ANDROID_SDK_HOME'], '.android')
# we place some files in the users home
elif platform.system() == "Windows":
self.home_dir = os.path.join(os.environ['USERPROFILE'], '.titanium')
self.android_home_dir = os.path.join(os.environ['USERPROFILE'], '.android')
else:
self.home_dir = os.path.join(os.path.expanduser('~'), '.titanium')
self.android_home_dir = os.path.join(os.path.expanduser('~'), '.android')
if not os.path.exists(self.home_dir):
os.makedirs(self.home_dir)
self.sdcard = os.path.join(self.home_dir,'android2.sdcard')
self.classname = Android.strip_classname(self.name)
if not is_emulator:
self.set_java_commands()
# start in 1.4, you no longer need the build/android directory
# if missing, we'll create it on the fly
if not os.path.exists(self.project_dir) or not os.path.exists(os.path.join(self.project_dir,'AndroidManifest.xml')):
android_creator = Android(name, app_id, self.sdk, None, self.java)
parent_dir = os.path.dirname(self.top_dir)
if os.path.exists(self.top_dir):
android_creator.create(parent_dir, project_dir=self.top_dir, build_time=True)
else:
android_creator.create(parent_dir)
self.force_rebuild = True
sys.stdout.flush()
def check_target_api_version(self, manifest_elements):
pattern = r'android:targetSdkVersion=\"(\d+)\"'
for el in manifest_elements:
if el.nodeName == "uses-sdk":
xml = el.toxml()
matches = re.findall(pattern, xml)
if matches:
new_level = self.sdk.try_best_match_api_level(int(matches[0]))
if new_level != self.tool_api_level:
self.tool_api_level = new_level
break
def set_java_commands(self):
commands = java.find_java_commands()
to_check = ("java", "javac", "keytool", "jarsigner")
found = True
for check in to_check:
if not commands[check]:
found = False
error("Required Java tool '%s' not located." % check)
if not found:
error("One or more required files not found - please check your JAVA_HOME environment variable")
sys.exit(1)
self.jarsigner = commands["jarsigner"]
self.keytool = commands["keytool"]
self.javac = commands["javac"]
self.java = commands["java"]
if not commands["environ_java_home"] and commands["java_home"]:
os.environ["JAVA_HOME"] = commands["java_home"]
def wait_for_home(self, type):
max_wait = 20
attempts = 0
while True:
processes = self.sdk.list_processes(['-%s' % type])
found_home = False
for process in processes:
if process["name"] == "android.process.acore":
found_home = True
break
if found_home:
break
attempts += 1
if attempts == max_wait:
error("Timed out waiting for android.process.acore")
return False
time.sleep(1)
return True
def wait_for_device(self, type):
debug("Waiting for device to be ready ...")
t = time.time()
max_wait = 30
max_zero = 10
attempts = 0
zero_attempts = 0
timed_out = True
no_devices = False
while True:
devices = self.sdk.list_devices()
trace("adb devices returned %s devices/emulators" % len(devices))
if len(devices) > 0:
found = False
for device in devices:
if type == "e" and device.is_emulator() and not device.is_offline(): found = True
elif type == "d" and device.is_device(): found = True
if found:
timed_out = False
break
else: zero_attempts += 1
try: time.sleep(5) # for some reason KeyboardInterrupts get caught here from time to time
except KeyboardInterrupt: pass
attempts += 1
if attempts == max_wait:
break
elif zero_attempts == max_zero:
no_devices = True
break
if timed_out:
if type == "e":
device = "emulator"
extra_message = "you may need to close the emulator and try again"
else:
device = "device"
extra_message = "you may try reconnecting the USB cable"
error("Timed out waiting for %s to be ready, %s" % (device, extra_message))
if no_devices:
sys.exit(1)
return False
debug("Device connected... (waited %d seconds)" % (attempts*5))
duration = time.time() - t
debug("waited %f seconds on emulator to get ready" % duration)
if duration > 1.0:
info("Waiting for the Android Emulator to become available")
return self.wait_for_home(type)
#time.sleep(20) # give it a little more time to get installed
return True
def create_avd(self, avd_id, avd_skin, avd_abi):
# Sanity check the AVD to see if the ABI is available, or
# necessary.
available_avds = avd.get_avds(self.sdk)
multiple_abis = False
for device in available_avds:
if device['id'] == avd_id:
default_abi = device['abis'][0]
multiple_abis = ( len(device['abis']) != 1 )
if avd_abi is None:
avd_abi = default_abi
elif avd_abi not in device['abis']:
warn("ABI %s not supported for AVD ID %s: Using default ABI %s" % (avd_abi, avd_id, default_abi))
avd_abi = default_abi
break
if multiple_abis:
name = "titanium_%s_%s_%s" % (avd_id, avd_skin, avd_abi)
else:
name = "titanium_%s_%s" % (avd_id, avd_skin)
name = name.replace(' ', '_')
if not os.path.exists(self.home_dir):
os.makedirs(self.home_dir)
avd_path = os.path.join(self.android_home_dir, 'avd')
my_avd = os.path.join(avd_path,"%s.avd" % name)
own_sdcard = os.path.join(self.home_dir, '%s.sdcard' % name)
if not os.path.exists(my_avd) or os.path.exists(own_sdcard):
# starting with 1.7.2, when we create a new avd, give it its own
# SDCard as well.
self.sdcard = own_sdcard
if not os.path.exists(self.sdcard):
info("Creating 64M SD card for use in Android emulator")
run.run([self.sdk.get_mksdcard(), '64M', self.sdcard])
if not os.path.exists(my_avd):
if multiple_abis:
info("Creating new Android Virtual Device (%s %s %s)" % (avd_id,avd_skin,avd_abi))
else:
info("Creating new Android Virtual Device (%s %s)" % (avd_id,avd_skin))
inputgen = os.path.join(template_dir,'input.py')
abi_args = []
if multiple_abis:
abi_args = ['-b', avd_abi]
pipe([sys.executable, inputgen], [self.sdk.get_android(), '--verbose', 'create', 'avd', '--name', name, '--target', avd_id, '-s', avd_skin, '--force', '--sdcard', self.sdcard] + abi_args)
inifile = os.path.join(my_avd,'config.ini')
inifilec = open(inifile,'r').read()
inifiledata = open(inifile,'w')
inifiledata.write(inifilec)
# TODO - Document options
for hw_option in android_avd_hw.keys():
inifiledata.write("%s=%s\n" % (hw_option, android_avd_hw[hw_option]))
inifiledata.close()
return name
def run_emulator(self, avd_id, avd_skin, avd_name, avd_abi, add_args):
info("Launching Android emulator...one moment")
debug("From: " + self.sdk.get_emulator())
debug("SDCard: " + self.sdcard)
if avd_name is None:
debug("AVD ID: " + avd_id)
debug("AVD Skin: " + avd_skin)
else:
debug("AVD Name: " + avd_name)
if avd_abi is not None:
debug("AVD ABI: " + avd_abi)
debug("SDK: " + sdk_dir)
# make sure adb is running on windows, else XP can lockup the python
# process when adb runs first time
if platform.system() == "Windows":
run.run([self.sdk.get_adb(), "start-server"], True, ignore_output=True)
devices = self.sdk.list_devices()
for device in devices:
if device.is_emulator() and device.get_port() == 5560:
info("Emulator is running.")
sys.exit()
# this will create an AVD on demand or re-use existing one if already created
if avd_name == None:
avd_name = self.create_avd(avd_id, avd_skin, avd_abi)
# start the emulator
emulator_cmd = [
self.sdk.get_emulator(),
'-avd',
avd_name,
'-port',
'5560',
'-sdcard',
self.get_sdcard_path(),
'-logcat',
'*:d,*,TiAPI:V',
'-no-boot-anim',
'-partition-size',
'128' # in between nexusone and droid
]
if add_args:
emulator_cmd.extend([arg.strip() for arg in add_args if len(arg.strip()) > 0])
debug(' '.join(emulator_cmd))
p = subprocess.Popen(emulator_cmd)
def handler(signum, frame):
debug("signal caught: %d" % signum)
if not p == None:
debug("calling emulator kill on %d" % p.pid)
if platform.system() == "Windows":
os.system("taskkill /F /T /PID %i" % p.pid)
else:
os.kill(p.pid, signal.SIGTERM)
if platform.system() != "Windows":
signal.signal(signal.SIGHUP, handler)
signal.signal(signal.SIGQUIT, handler)
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGABRT, handler)
signal.signal(signal.SIGTERM, handler)
# give it some time to exit prematurely
time.sleep(1)
rc = p.poll()
if rc != None:
handler(3,None)
sys.exit(rc)
# wait for the emulator to finish
try:
rc = p.wait()
except OSError:
handler(3,None)
info("Android Emulator has exited")
sys.exit(rc)
def check_file_exists(self, path):
output = self.run_adb('shell', 'ls', path)
if output != None:
if output.find("No such file or directory") == -1 \
and output.find("error: device offline") == -1:
return True
return False
def is_app_installed(self):
return self.check_file_exists('/data/app/%s*.apk' % self.app_id)
def get_sdcard_path(self):
# We need to surround the sd card path in quotes for windows to account for spaces in path
if platform.system() == "Windows":
return '"' + self.sdcard + '"'
return self.sdcard
def are_resources_installed(self):
return self.check_file_exists(self.sdcard_resources+'/app.js')
def include_path(self, path, isfile):
if not isfile and os.path.basename(path) in ignoreDirs: return False
elif isfile and os.path.basename(path) in ignoreFiles: return False
return True
def warn_dupe_drawable_folders(self):
tocheck = ('high', 'medium', 'low')
image_parent = os.path.join(self.top_dir, 'Resources', 'android', 'images')
for check in tocheck:
if os.path.exists(os.path.join(image_parent, check)) and os.path.exists(os.path.join(image_parent, 'res-%sdpi' % check[0])):
warn('You have both an android/images/%s folder and an android/images/res-%sdpi folder. Files from both of these folders will end up in res/drawable-%sdpi. If two files are named the same, there is no guarantee which one will be copied last and therefore be the one the application uses. You should use just one of these folders to avoid conflicts.' % (check, check[0], check[0]))
def copy_module_platform_folders(self):
for module in self.modules:
platform_folder = os.path.join(module.path, 'platform', 'android')
if os.path.exists(platform_folder):
copy_all(platform_folder, self.project_dir, True, one_time_msg="Copying platform-specific files for '%s' module" % module.manifest.name)
def copy_commonjs_modules(self):
info('Copying CommonJS modules...')
for module in self.modules:
if module.js is None:
continue
module_name = os.path.basename(module.js)
self.non_orphans.append(module_name)
shutil.copy(module.js, self.assets_resources_dir)
def copy_project_platform_folder(self, ignore_dirs=[], ignore_files=[]):
if not os.path.exists(self.platform_dir):
return
copy_all(self.platform_dir, self.project_dir, True, ignore_dirs, ignore_files, one_time_msg="Copying platform-specific files ...")
def copy_resource_drawables(self):
debug('Processing Android resource drawables')
def make_resource_drawable_filename(orig):
normalized = orig.replace(os.sep, "/")
matches = re.search("/android/images/(high|medium|low|res-[^/]+)/(?P<chopped>.*$)", normalized)
if matches and matches.groupdict() and 'chopped' in matches.groupdict():
chopped = matches.groupdict()['chopped'].lower()
for_hash = chopped
if for_hash.endswith('.9.png'):
for_hash = for_hash[:-6] + '.png'
extension = ""
without_extension = chopped
if re.search("\\..*$", chopped):
if chopped.endswith('.9.png'):
extension = '9.png'
without_extension = chopped[:-6]
else:
extension = chopped.split(".")[-1]
without_extension = chopped[:-(len(extension)+1)]
cleaned_without_extension = re.sub(r'[^a-z0-9_]', '_', without_extension)
cleaned_extension = re.sub(r'[^a-z0-9\._]', '_', extension)
result = cleaned_without_extension[:80] + "_" + hashlib.md5(for_hash).hexdigest()[:10]
if extension:
result += "." + extension
return result
else:
trace("Regexp for resource drawable file %s failed" % orig)
return None
def delete_resource_drawable(orig):
folder = resource_drawable_folder(orig)
res_file = os.path.join(self.res_dir, folder, make_resource_drawable_filename(orig))
if os.path.exists(res_file):
try:
trace("DELETING FILE: %s" % res_file)
os.remove(res_file)
except:
warn('Unable to delete %s: %s. Execution will continue.' % (res_file, sys.exc_info()[0]))
def copy_resource_drawable(orig):
partial_folder = resource_drawable_folder(orig)
if not partial_folder:
trace("Could not copy %s; resource folder not determined" % orig)
return
dest_folder = os.path.join(self.res_dir, partial_folder)
dest_filename = make_resource_drawable_filename(orig)
if dest_filename is None:
return
dest = os.path.join(dest_folder, dest_filename)
if not os.path.exists(dest_folder):
os.makedirs(dest_folder)
trace("COPYING FILE: %s => %s" % (orig, dest))
shutil.copy(orig, dest)
fileset = []
if self.force_rebuild or self.deploy_type == 'production' or \
(self.js_changed and not self.fastdev):
for root, dirs, files in os.walk(os.path.join(self.top_dir, "Resources")):
remove_ignored_dirs(dirs)
for f in files:
if f in ignoreFiles:
continue
path = os.path.join(root, f)
if is_resource_drawable(path) and f != 'default.png':
fileset.append(path)
else:
if self.project_deltas:
for delta in self.project_deltas:
path = delta.get_path()
if is_resource_drawable(path):
if delta.get_status() == Delta.DELETED:
delete_resource_drawable(path)
else:
fileset.append(path)
if len(fileset) == 0:
return False
for f in fileset:
copy_resource_drawable(f)
return True
def copy_project_resources(self):
info("Copying project resources..")
def validate_filenames(topdir):
for root, dirs, files in os.walk(topdir):
remove_ignored_dirs(dirs)
for d in dirs:
if d == "iphone" or d == "mobileweb":
dirs.remove(d)
for filename in files:
if filename.startswith("_"):
error("%s is an invalid filename. Android will not package assets whose filenames start with underscores. Fix and rebuild." % os.path.join(root, filename))
sys.exit(1)
resources_dir = os.path.join(self.top_dir, 'Resources')
validate_filenames(resources_dir)
android_resources_dir = os.path.join(resources_dir, 'android')
self.project_deltafy = Deltafy(resources_dir, include_callback=self.include_path)
self.project_deltas = self.project_deltafy.scan()
self.js_changed = False
tiapp_delta = self.project_deltafy.scan_single_file(self.project_tiappxml)
self.tiapp_changed = tiapp_delta is not None
full_copy = not os.path.exists(self.assets_resources_dir)
if self.tiapp_changed or self.force_rebuild or full_copy:
info("Detected change in tiapp.xml, or assets deleted. Forcing full re-build...")
# force a clean scan/copy when the tiapp.xml has changed
self.project_deltafy.clear_state()
self.project_deltas = self.project_deltafy.scan()
# rescan tiapp.xml so it doesn't show up as created next time around
self.project_deltafy.scan_single_file(self.project_tiappxml)
if self.tiapp_changed:
for root, dirs, files in os.walk(self.project_gen_dir, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
def strip_slash(s):
if s[0:1]=='/' or s[0:1]=='\\': return s[1:]
return s
def make_relative(path, relative_to, prefix=None):
relative_path = strip_slash(path[len(relative_to):])
if prefix is not None:
return os.path.join(prefix, relative_path)
return relative_path
for delta in self.project_deltas:
path = delta.get_path()
if re.search("android/images/(high|medium|low|res-[^/]+)/", path.replace(os.sep, "/")):
continue # density images are handled later
if delta.get_status() == Delta.DELETED and path.startswith(android_resources_dir):
shared_path = path.replace(android_resources_dir, resources_dir, 1)
if os.path.exists(shared_path):
dest = make_relative(shared_path, resources_dir, self.assets_resources_dir)
trace("COPYING FILE: %s => %s (platform-specific file was removed)" % (shared_path, dest))
shutil.copy(shared_path, dest)
if delta.get_status() != Delta.DELETED:
if path.startswith(android_resources_dir):
dest = make_relative(path, android_resources_dir, self.assets_resources_dir)
else:
# don't copy it if there is an android-specific file
if os.path.exists(path.replace(resources_dir, android_resources_dir, 1)):
continue
dest = make_relative(path, resources_dir, self.assets_resources_dir)
if path.startswith(os.path.join(resources_dir, "iphone")) or path.startswith(os.path.join(resources_dir, "mobileweb")) or path.startswith(os.path.join(resources_dir, "blackberry")):
continue
parent = os.path.dirname(dest)
if not os.path.exists(parent):
os.makedirs(parent)
trace("COPYING %s FILE: %s => %s" % (delta.get_status_str(), path, dest))
shutil.copy(path, dest)
if (path.startswith(resources_dir) or path.startswith(android_resources_dir)) and path.endswith(".js"):
self.js_changed = True
# copy to the sdcard in development mode
if self.sdcard_copy and self.app_installed and (self.deploy_type == 'development' or self.deploy_type == 'test'):
if path.startswith(android_resources_dir):
relative_path = make_relative(delta.get_path(), android_resources_dir)
else:
relative_path = make_relative(delta.get_path(), resources_dir)
relative_path = relative_path.replace("\\", "/")
self.run_adb('push', delta.get_path(), "%s/%s" % (self.sdcard_resources, relative_path))
if os.environ.has_key('LIVEVIEW'):
debug("LiveView enabled")
appjs = os.path.join(self.assets_resources_dir, 'app.js')
_appjs = os.path.join(self.assets_resources_dir, '_app.js')
liveviewjs = os.path.join(tempfile.gettempdir(), 'liveview.js')
self.non_orphans.append('_app.js')
if not os.path.exists(appjs):
debug('app.js not found: %s' % appjs)
if not os.path.exists(liveviewjs):
debug('liveviewjs.js not found: %s' % liveviewjs)
if os.path.exists(appjs) and os.path.exists(liveviewjs):
trace("COPYING %s => %s" % (appjs, _appjs))
shutil.copy(appjs, _appjs)
trace("COPYING %s => %s" % (liveviewjs, appjs))
shutil.copy(liveviewjs, appjs)
else:
debug('LiveView not enabled')
index_json_path = os.path.join(self.assets_dir, "index.json")
if len(self.project_deltas) > 0 or not os.path.exists(index_json_path):
requireIndex.generateJSON(self.assets_dir, index_json_path)
def check_permissions_mapping(self, key, permissions_mapping, permissions_list):
try:
perms = permissions_mapping[key]
if perms:
for perm in perms:
try:
permissions_list.index(perm)
except:
permissions_list.append(perm)
except:
pass
def generate_android_manifest(self,compiler):
self.generate_localizations()
self.remove_duplicate_res()
# NOTE: these are built-in permissions we need -- we probably need to refine when these are needed too
permissions_required = ['INTERNET','ACCESS_WIFI_STATE','ACCESS_NETWORK_STATE', 'WRITE_EXTERNAL_STORAGE']
GEO_PERMISSION = [ 'ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION']
CONTACTS_READ_PERMISSION = ['READ_CONTACTS']
CONTACTS_PERMISSION = ['READ_CONTACTS', 'WRITE_CONTACTS']
CALENDAR_PERMISSION = ['READ_CALENDAR', 'WRITE_CALENDAR']
VIBRATE_PERMISSION = ['VIBRATE']
CAMERA_PERMISSION = ['CAMERA']
WALLPAPER_PERMISSION = ['SET_WALLPAPER']
# Enable mock location if in development or test mode.
if self.deploy_type == 'development' or self.deploy_type == 'test':
GEO_PERMISSION.append('ACCESS_MOCK_LOCATION')
# this is our module to permission(s) trigger - for each module on the left, require the permission(s) on the right
permissions_module_mapping = {
# GEO
'geolocation' : GEO_PERMISSION
}
# this is our module method to permission(s) trigger - for each method on the left, require the permission(s) on the right
permissions_method_mapping = {
# MAP
'Map.createView' : GEO_PERMISSION,
# MEDIA
'Media.vibrate' : VIBRATE_PERMISSION,
'Media.showCamera' : CAMERA_PERMISSION,
# CONTACTS
'Contacts.createPerson' : CONTACTS_PERMISSION,
'Contacts.removePerson' : CONTACTS_PERMISSION,
'Contacts.getAllContacts' : CONTACTS_READ_PERMISSION,
'Contacts.showContactPicker' : CONTACTS_READ_PERMISSION,
'Contacts.showContacts' : CONTACTS_READ_PERMISSION,
'Contacts.getPersonByID' : CONTACTS_READ_PERMISSION,
'Contacts.getPeopleWithName' : CONTACTS_READ_PERMISSION,
'Contacts.getAllPeople' : CONTACTS_READ_PERMISSION,
'Contacts.getAllGroups' : CONTACTS_READ_PERMISSION,
'Contacts.getGroupByID' : CONTACTS_READ_PERMISSION,
# Old CALENDAR
'Android.Calendar.getAllAlerts' : CALENDAR_PERMISSION,
'Android.Calendar.getAllCalendars' : CALENDAR_PERMISSION,
'Android.Calendar.getCalendarById' : CALENDAR_PERMISSION,
'Android.Calendar.getSelectableCalendars' : CALENDAR_PERMISSION,
# CALENDAR
'Calendar.getAllAlerts' : CALENDAR_PERMISSION,
'Calendar.getAllCalendars' : CALENDAR_PERMISSION,
'Calendar.getCalendarById' : CALENDAR_PERMISSION,
'Calendar.getSelectableCalendars' : CALENDAR_PERMISSION,
# WALLPAPER
'Media.Android.setSystemWallpaper' : WALLPAPER_PERMISSION,
}
VIDEO_ACTIVITY = """<activity
android:name="ti.modules.titanium.media.TiVideoActivity"
android:configChanges="keyboardHidden|orientation"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
/>"""
MAP_ACTIVITY = """<activity
android:name="ti.modules.titanium.map.TiMapActivity"
android:configChanges="keyboardHidden|orientation"
android:launchMode="singleTask"
/>
<uses-library android:name="com.google.android.maps" />"""
CAMERA_ACTIVITY = """<activity
android:name="ti.modules.titanium.media.TiCameraActivity"
android:configChanges="keyboardHidden|orientation"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
/>"""
activity_mapping = {
# MEDIA
'Media.createVideoPlayer' : VIDEO_ACTIVITY,
'Media.showCamera' : CAMERA_ACTIVITY,
# MAPS
'Map.createView' : MAP_ACTIVITY,
}
# this is a map of our APIs to ones that require Google APIs to be available on the device
google_apis = {
"Map.createView" : True
}
activities = []
# figure out which permissions we need based on the used module
for mod in compiler.modules:
self.check_permissions_mapping(mod, permissions_module_mapping, permissions_required)
# figure out which permissions we need based on the used module methods
for mn in compiler.module_methods:
self.check_permissions_mapping(mn, permissions_method_mapping, permissions_required)
try:
mappings = activity_mapping[mn]
try:
if google_apis[mn] and not self.google_apis_supported:
warn("Google APIs detected but a device has been selected that doesn't support them. The API call to Titanium.%s will fail using '%s'" % (mn,my_avd['name']))
continue
except:
pass
try:
activities.index(mappings)
except:
activities.append(mappings)
except:
pass
# Javascript-based activities defined in tiapp.xml
if self.tiapp and self.tiapp.android and 'activities' in self.tiapp.android:
tiapp_activities = self.tiapp.android['activities']
for key in tiapp_activities:
activity = tiapp_activities[key]
if not 'url' in activity:
continue
activity_name = self.app_id + '.' + activity['classname']
activity_str = '<activity \n\t\t\tandroid:name="%s"' % activity_name
for subkey in activity:
if subkey not in ('nodes', 'name', 'url', 'options', 'classname', 'android:name'):
activity_str += '\n\t\t\t%s="%s"' % (subkey, activity[subkey])
if 'android:config' not in activity:
activity_str += '\n\t\t\tandroid:configChanges="keyboardHidden|orientation"'
if 'nodes' in activity:
activity_str += '>'
for node in activity['nodes']:
activity_str += '\n\t\t\t\t' + node.toxml()
activities.append(activity_str + '\n\t\t</activity>\n')
else:
activities.append(activity_str + '\n\t\t/>\n')
activities = set(activities)
services = []
# Javascript-based services defined in tiapp.xml
if self.tiapp and self.tiapp.android and 'services' in self.tiapp.android:
tiapp_services = self.tiapp.android['services']
for key in tiapp_services:
service = tiapp_services[key]
if not 'url' in service:
continue
service_name = self.app_id + '.' + service['classname']
service_str = '<service \n\t\t\tandroid:name="%s"' % service_name
for subkey in service:
if subkey not in ('nodes', 'service_type', 'type', 'name', 'url', 'options', 'classname', 'android:name'):
service_str += '\n\t\t\t%s="%s"' % (subkey, service[subkey])
if 'nodes' in service:
service_str += '>'
for node in service['nodes']:
service_str += '\n\t\t\t\t' + node.toxml()
services.append(service_str + '\n\t\t</service>\n')
else:
services.append(service_str + '\n\t\t/>\n')
self.use_maps = False
self.res_changed = False
icon_name = self.tiapp.properties['icon']
icon_path = os.path.join(self.assets_resources_dir, icon_name)
icon_ext = os.path.splitext(icon_path)[1]
res_drawable_dest = os.path.join(self.project_dir, 'res', 'drawable')
if not os.path.exists(res_drawable_dest):
os.makedirs(res_drawable_dest)
default_icon = os.path.join(self.support_resources_dir, 'default.png')
dest_icon = os.path.join(res_drawable_dest, 'appicon%s' % icon_ext)
if Deltafy.needs_update(icon_path, dest_icon):
self.res_changed = True
debug("copying app icon: %s" % icon_path)
shutil.copy(icon_path, dest_icon)
elif Deltafy.needs_update(default_icon, dest_icon):
self.res_changed = True
debug("copying default app icon")
shutil.copy(default_icon, dest_icon)
# make our Titanium theme for our icon
res_values_dir = os.path.join(self.project_dir, 'res','values')
if not os.path.exists(res_values_dir):
os.makedirs(res_values_dir)
theme_xml = os.path.join(res_values_dir,'theme.xml')
if not os.path.exists(theme_xml):
self.res_changed = True
debug('generating theme.xml')
theme_file = open(theme_xml, 'w')
theme_flags = "Theme"
# We need to treat the default values for fulscreen and
# navbar-hidden the same as android.py does -- false for both.
theme_fullscreen = False
theme_navbarhidden = False
if (self.tiapp.properties.get("fullscreen") == "true" or
self.tiapp.properties.get("statusbar-hidden") == "true"):
theme_fullscreen = True
elif self.tiapp.properties.get("navbar-hidden") == "true":
theme_navbarhidden = True
if theme_fullscreen:
theme_flags += ".NoTitleBar.Fullscreen"
elif theme_navbarhidden:
theme_flags += ".NoTitleBar"
# Wait, one exception. If you want the notification area (very
# top of screen) hidden, but want the title bar in the app,
# there's no theme for that. So we have to use the default theme (no flags)
# and when the application code starts running, the adjustments are then made.
# Only do this when the properties are explicitly set, so as to avoid changing
# old default behavior.
if theme_flags.endswith('.Fullscreen') and \
self.tiapp.properties.get("navbar-hidden") == 'false' and \
('fullscreen' in self.tiapp.explicit_properties or \
'statusbar-hidden' in self.tiapp.explicit_properties) and \
'navbar-hidden' in self.tiapp.explicit_properties:
theme_flags = 'Theme'
TITANIUM_THEME="""<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Titanium" parent="android:%s">
<item name="android:windowBackground">@drawable/background</item>
</style>
</resources>
""" % theme_flags
theme_file.write(TITANIUM_THEME)
theme_file.close()
# create our background image which acts as splash screen during load
resources_dir = os.path.join(self.top_dir, 'Resources')
android_images_dir = os.path.join(resources_dir, 'android', 'images')
# look for density-specific default.png's first
if os.path.exists(android_images_dir):
pattern = r'/android/images/(high|medium|low|res-[^/]+)/default.png'
for root, dirs, files in os.walk(android_images_dir):
remove_ignored_dirs(dirs)
for f in files:
if f in ignoreFiles:
continue
path = os.path.join(root, f)
if re.search(pattern, path.replace(os.sep, "/")):
res_folder = resource_drawable_folder(path)
debug('found %s splash screen at %s' % (res_folder, path))
dest_path = os.path.join(self.res_dir, res_folder)
dest_file = os.path.join(dest_path, 'background.png')
if not os.path.exists(dest_path):
os.makedirs(dest_path)
if Deltafy.needs_update(path, dest_file):
self.res_changed = True
debug('copying %s splash screen to %s' % (path, dest_file))
shutil.copy(path, dest_file)
default_png = os.path.join(self.assets_resources_dir, 'default.png')
support_default_png = os.path.join(self.support_resources_dir, 'default.png')
background_png = os.path.join(self.project_dir, 'res','drawable','background.png')
if os.path.exists(default_png) and Deltafy.needs_update(default_png, background_png):
self.res_changed = True
debug("found splash screen at %s" % os.path.abspath(default_png))
shutil.copy(default_png, background_png)
elif Deltafy.needs_update(support_default_png, background_png):
self.res_changed = True
debug("copying default splash screen")
shutil.copy(support_default_png, background_png)
android_manifest = os.path.join(self.project_dir, 'AndroidManifest.xml')
android_manifest_to_read = android_manifest
# NOTE: allow the user to use their own custom AndroidManifest if they put a file named
# AndroidManifest.xml in platform/android, in which case all bets are off
is_custom = False
# Catch people who may have it in project root (un-released 1.4.x android_native_refactor branch users)
if os.path.exists(os.path.join(self.top_dir, 'AndroidManifest.xml')):
warn('AndroidManifest.xml file in the project root is ignored. Move it to platform/android if you want it to be your custom manifest.')
android_custom_manifest = os.path.join(self.project_dir, 'AndroidManifest.custom.xml')
if not os.path.exists(android_custom_manifest):
android_custom_manifest = os.path.join(self.platform_dir, 'AndroidManifest.xml')
else:
warn('Use of AndroidManifest.custom.xml is deprecated. Please put your custom manifest as "AndroidManifest.xml" in the "platform/android" directory if you do not need to compile for versions < 1.5')
if os.path.exists(android_custom_manifest):
android_manifest_to_read = android_custom_manifest
is_custom = True
info("Detected custom ApplicationManifest.xml -- no Titanium version migration supported")
default_manifest_contents = self.android.render_android_manifest()
if self.sdk.api_level >= HONEYCOMB_MR2_LEVEL:
# Need to add "screenSize" in our default "configChanges" attribute on
# <activity> elements, else changes in orientation will cause the app
# to restart. cf. TIMOB-10863.
default_manifest_contents = default_manifest_contents.replace('|orientation"', '|orientation|screenSize"')
debug("Added 'screenSize' to <activity android:configChanges> because targeted api level %s is >= %s" % (self.sdk.api_level, HONEYCOMB_MR2_LEVEL))
custom_manifest_contents = None
if is_custom:
custom_manifest_contents = open(android_manifest_to_read,'r').read()
manifest_xml = ''
def get_manifest_xml(tiapp, template_obj=None):
xml = ''
if 'manifest' in tiapp.android_manifest:
for manifest_el in tiapp.android_manifest['manifest']:
# since we already track permissions in another way, go ahead and us e that
if manifest_el.nodeName == 'uses-permission' and manifest_el.hasAttribute('android:name'):
if manifest_el.getAttribute('android:name').split('.')[-1] not in permissions_required:
perm_val = manifest_el.getAttribute('android:name')
if template_obj is not None and "${" in perm_val:
perm_val = render_template_with_tiapp(perm_val, template_obj)
permissions_required.append(perm_val)
elif manifest_el.nodeName not in ('supports-screens', 'uses-sdk'):
this_xml = manifest_el.toprettyxml()
if template_obj is not None and "${" in this_xml:
this_xml = render_template_with_tiapp(this_xml, template_obj)
xml += this_xml
return xml
application_xml = ''
def get_application_xml(tiapp, template_obj=None):
xml = ''
if 'application' in tiapp.android_manifest:
for app_el in tiapp.android_manifest['application']:
this_xml = app_el.toxml()
if template_obj is not None and "${" in this_xml:
this_xml = render_template_with_tiapp(this_xml, template_obj)
xml += this_xml
return xml
# add manifest / application entries from tiapp.xml
manifest_xml += get_manifest_xml(self.tiapp)
application_xml += get_application_xml(self.tiapp)
# add manifest / application entries from modules
for module in self.modules:
if module.xml == None: continue
manifest_xml += get_manifest_xml(module.xml, self.tiapp)
application_xml += get_application_xml(module.xml, self.tiapp)
# build the permissions XML based on the permissions detected
permissions_required = set(permissions_required)
permissions_required_xml = ""
for p in permissions_required:
if '.' not in p:
permissions_required_xml+="<uses-permission android:name=\"android.permission.%s\"/>\n\t" % p
else:
permissions_required_xml+="<uses-permission android:name=\"%s\"/>\n\t" % p
def fill_manifest(manifest_source):
ti_activities = '<!-- TI_ACTIVITIES -->'
ti_permissions = '<!-- TI_PERMISSIONS -->'
ti_manifest = '<!-- TI_MANIFEST -->'
ti_application = '<!-- TI_APPLICATION -->'
ti_services = '<!-- TI_SERVICES -->'
manifest_source = manifest_source.replace(ti_activities,"\n\n\t\t".join(activities))
manifest_source = manifest_source.replace(ti_services,"\n\n\t\t".join(services))
manifest_source = manifest_source.replace(ti_permissions,permissions_required_xml)
if len(manifest_xml) > 0:
manifest_source = manifest_source.replace(ti_manifest, manifest_xml)
if len(application_xml) > 0:
manifest_source = manifest_source.replace(ti_application, application_xml)
return manifest_source
default_manifest_contents = fill_manifest(default_manifest_contents)
# if a custom uses-sdk or supports-screens has been specified via tiapp.xml
# <android><manifest>..., we need to replace the ones in the generated
# default manifest
supports_screens_node = None
uses_sdk_node = None
if 'manifest' in self.tiapp.android_manifest:
for node in self.tiapp.android_manifest['manifest']:
if node.nodeName == 'uses-sdk':
uses_sdk_node = node
elif node.nodeName == 'supports-screens':
supports_screens_node = node
if supports_screens_node or uses_sdk_node or ('manifest-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['manifest-attributes'].length) or ('application-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['application-attributes'].length):
dom = parseString(default_manifest_contents)
def replace_node(olddom, newnode):
nodes = olddom.getElementsByTagName(newnode.nodeName)
retval = False
if nodes:
olddom.documentElement.replaceChild(newnode, nodes[0])
retval = True
return retval
if supports_screens_node:
if not replace_node(dom, supports_screens_node):
dom.documentElement.insertBefore(supports_screens_node, dom.documentElement.firstChild.nextSibling)
if uses_sdk_node:
replace_node(dom, uses_sdk_node)
def set_attrs(element, new_attr_set):
for k in new_attr_set.keys():
if element.hasAttribute(k):
element.removeAttribute(k)
element.setAttribute(k, new_attr_set.get(k).value)
if 'manifest-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['manifest-attributes'].length:
set_attrs(dom.documentElement, self.tiapp.android_manifest['manifest-attributes'])
if 'application-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['application-attributes'].length:
set_attrs(dom.getElementsByTagName('application')[0], self.tiapp.android_manifest['application-attributes'])
default_manifest_contents = dom.toxml()
if application_xml:
# If the tiapp.xml <manifest><application> section was not empty, it could be
# that user put in <activity> entries that duplicate our own,
# such as if they want a custom theme on TiActivity. So we should delete any dupes.
dom = parseString(default_manifest_contents)
package_name = dom.documentElement.getAttribute('package')
manifest_activities = dom.getElementsByTagName('activity')
activity_names = []
nodes_to_delete = []
for manifest_activity in manifest_activities:
if manifest_activity.hasAttribute('android:name'):
activity_name = manifest_activity.getAttribute('android:name')
if activity_name.startswith('.'):
activity_name = package_name + activity_name
if activity_name in activity_names:
nodes_to_delete.append(manifest_activity)
else:
activity_names.append(activity_name)
if nodes_to_delete:
for node_to_delete in nodes_to_delete:
node_to_delete.parentNode.removeChild(node_to_delete)
default_manifest_contents = dom.toxml()
if custom_manifest_contents:
custom_manifest_contents = fill_manifest(custom_manifest_contents)
new_manifest_contents = None
android_manifest_gen = android_manifest + '.gen'
if custom_manifest_contents:
new_manifest_contents = custom_manifest_contents
# Write the would-be default as well so user can see
# some of the auto-gen'd insides of it if they need/want.
amf = open(android_manifest + '.gen', 'w')
amf.write(default_manifest_contents)
amf.close()
else:
new_manifest_contents = default_manifest_contents
if os.path.exists(android_manifest_gen):
os.remove(android_manifest_gen)
manifest_changed = False
old_contents = None
if os.path.exists(android_manifest):
old_contents = open(android_manifest, 'r').read()
if new_manifest_contents != old_contents:
trace("Writing out AndroidManifest.xml")
amf = open(android_manifest,'w')
amf.write(new_manifest_contents)
amf.close()
manifest_changed = True
if self.res_changed or manifest_changed:
res_dir = os.path.join(self.project_dir, 'res')
output = run.run([self.aapt, 'package', '-m',
'-J', self.project_gen_dir,
'-M', android_manifest,
'-S', res_dir,
'-I', self.android_jar], warning_regex=r'skipping')
r_file = os.path.join(self.project_gen_dir, self.app_id.replace('.', os.sep), 'R.java')
if not os.path.exists(r_file) or (self.res_changed and output == None):
error("Error generating R.java from manifest")
sys.exit(1)
return manifest_changed
def generate_stylesheet(self):
update_stylesheet = False
resources_dir = os.path.join(self.top_dir, 'Resources')
project_gen_pkg_dir = os.path.join(self.project_gen_dir, self.app_id.replace('.', os.sep))
app_stylesheet = os.path.join(project_gen_pkg_dir, 'ApplicationStylesheet.java')
if not os.path.exists(app_stylesheet):
update_stylesheet = True
else:
for root, dirs, files in os.walk(resources_dir, True, None, True):
remove_ignored_dirs(dirs)
for f in files:
if f in ignoreFiles:
continue
if f.endswith(".jss"):
absolute_path = os.path.join(root, f)
if Deltafy.needs_update(absolute_path, app_stylesheet):
update_stylesheet = True
break
if not update_stylesheet:
return
cssc = csscompiler.CSSCompiler(resources_dir, 'android', self.app_id)
if not os.path.exists(project_gen_pkg_dir):
os.makedirs(project_gen_pkg_dir)
debug("app stylesheet => %s" % app_stylesheet)
asf = codecs.open(app_stylesheet, 'w', 'utf-8')
asf.write(cssc.code)
asf.close()
def generate_localizations(self):
# compile localization files
localecompiler.LocaleCompiler(self.name,self.top_dir,'android',sys.argv[1]).compile()
# fix un-escaped single-quotes and full-quotes
# remove duplicate strings since we merge strings.xml from /i18n/ and /platform/android/res/values (TIMOB-12663)
offending_pattern = '[^\\\\][\'"]'
for root, dirs, files in os.walk(self.res_dir):
remove_ignored_dirs(dirs)
for filename in files:
if filename in ignoreFiles or not filename.endswith('.xml'):
continue
string_name_list = [] #keeps track of the string names
full_path = os.path.join(root, filename)
f = codecs.open(full_path, 'r', 'utf-8')
contents = f.read()
f.close()
if not re.search(r"<string ", contents):
continue
doc = parseString(contents.encode("utf-8"))
string_nodes = doc.getElementsByTagName('string')
resources_node = doc.getElementsByTagName('resources')[0]
if len(string_nodes) == 0:
continue
made_change = False
for string_node in string_nodes:
name = string_node.getAttribute('name')
# Remove the string node with the duplicate names
if name in string_name_list:
resources_node.removeChild(string_node)
made_change = True
debug('Removed duplicate string [%s] from %s' %(name, full_path))
else:
string_name_list.append(name)
if not string_node.hasChildNodes():
continue
string_child = string_node.firstChild
if string_child.nodeType == string_child.CDATA_SECTION_NODE or string_child.nodeType == string_child.TEXT_NODE:
string_value = string_child.nodeValue
if not re.search(offending_pattern, string_value):
continue
offenders = re.findall(offending_pattern, string_value)
if offenders:
for offender in offenders:
string_value = string_value.replace(offender, offender[0] + "\\" + offender[-1:])
made_change = True
string_child.nodeValue = string_value
if made_change:
new_contents = doc.toxml()
f = codecs.open(full_path, 'w', 'utf-8')
f.write(new_contents)
f.close()
def remove_duplicate_res(self):
for root, dirs, files in os.walk(self.res_dir):
remove_ignored_dirs(dirs)
for filename in files:
if not (filename in resourceFiles):
continue
full_path = os.path.join(root, filename)
node_names_to_check = ["string", "bool", "color", "dimen", "item", "integer",
"array", "integer-array", "string-array", "declare-styleable", "attr", "style"]
# "strings.xml" is checked in generate_localizations()
if filename != "strings.xml":
remove_duplicate_nodes_in_res_file(full_path, node_names_to_check)
def recurse(self, paths, file_glob=None):
if paths == None: yield None
if not isinstance(paths, list): paths = [paths]
for path in paths:
for root, dirs, files in os.walk(path):
remove_ignored_dirs(dirs)
for filename in files:
if filename in ignoreFiles:
continue
if file_glob != None:
if not fnmatch.fnmatch(filename, file_glob): continue
yield os.path.join(root, filename)
def generate_aidl(self):
# support for android remote interfaces in platform/android/src
framework_aidl = self.sdk.platform_path('framework.aidl')
aidl_args = [self.sdk.get_aidl(), '-p' + framework_aidl, '-I' + self.project_src_dir, '-o' + self.project_gen_dir]
for aidl_file in self.recurse(self.project_src_dir, '*.aidl'):
run.run(aidl_args + [aidl_file])
def build_generated_classes(self):
src_list = []
self.module_jars = []
classpath = os.pathsep.join([self.android_jar, os.pathsep.join(self.android_jars)])
project_module_dir = os.path.join(self.top_dir,'modules','android')
for module in self.modules:
if module.jar == None: continue
self.module_jars.append(module.jar)
classpath = os.pathsep.join([classpath, module.jar])
module_lib = module.get_resource('lib')
for jar in glob.glob(os.path.join(module_lib, '*.jar')):
self.module_jars.append(jar)
classpath = os.pathsep.join([classpath, jar])
if len(self.module_jars) > 0:
# kroll-apt.jar is needed for modules
classpath = os.pathsep.join([classpath, self.kroll_apt_jar])
classpath = os.pathsep.join([classpath, os.path.join(self.support_dir, 'lib', 'titanium-verify.jar')])
if self.deploy_type != 'production':
classpath = os.pathsep.join([classpath, os.path.join(self.support_dir, 'lib', 'titanium-debug.jar')])
classpath = os.pathsep.join([classpath, os.path.join(self.support_dir, 'lib', 'titanium-profiler.jar')])
for java_file in self.recurse([self.project_src_dir, self.project_gen_dir], '*.java'):
if self.project_src_dir in java_file:
relative_path = java_file[len(self.project_src_dir)+1:]
else:
relative_path = java_file[len(self.project_gen_dir)+1:]
class_file = os.path.join(self.classes_dir, relative_path.replace('.java', '.class'))
if Deltafy.needs_update(java_file, class_file) > 0:
# the file list file still needs each file escaped apparently
debug("adding %s to javac build list" % java_file)
src_list.append('"%s"' % java_file.replace("\\", "\\\\"))
if len(src_list) == 0:
# No sources are older than their classfile counterparts, we can skip javac / dex
return False
debug("Building Java Sources: " + " ".join(src_list))
javac_command = [self.javac, '-encoding', 'utf8',
'-classpath', classpath, '-d', self.classes_dir, '-proc:none',
'-sourcepath', self.project_src_dir,
'-sourcepath', self.project_gen_dir, '-target', '1.6', '-source', '1.6']
(src_list_osfile, src_list_filename) = tempfile.mkstemp()
src_list_file = os.fdopen(src_list_osfile, 'w')
src_list_file.write("\n".join(src_list))
src_list_file.close()
javac_command.append('@' + src_list_filename)
(out, err, javac_process) = run.run(javac_command, ignore_error=True, return_error=True, return_process=True)
os.remove(src_list_filename)
if javac_process.returncode != 0:
error("Error(s) compiling generated Java code")
error(str(err))
sys.exit(1)
return True
def create_unsigned_apk(self, resources_zip_file, webview_js_files=None):
unsigned_apk = os.path.join(self.project_dir, 'bin', 'app-unsigned.apk')
self.apk_updated = False
apk_modified = None
if os.path.exists(unsigned_apk):
apk_modified = Deltafy.get_modified_datetime(unsigned_apk)
debug("creating unsigned apk: " + unsigned_apk)
# copy existing resources into the APK
apk_zip = zipfile.ZipFile(unsigned_apk, 'w', zipfile.ZIP_DEFLATED)
def skip_jar_path(path):
ext = os.path.splitext(path)[1]
if path.endswith('/'): return True
if path.startswith('META-INF/'): return True
if path.split('/')[-1].startswith('.'): return True
if ext == '.class': return True
if 'org/appcelerator/titanium/bindings' in path and ext == '.json': return True
def skip_js_file(path):
return self.compile_js is True and \
os.path.splitext(path)[1] == '.js' and \
os.path.join(self.project_dir, "bin", path) not in webview_js_files
def compression_type(path):
ext = os.path.splitext(path)[1]
if ext in uncompressed_types:
return zipfile.ZIP_STORED
return zipfile.ZIP_DEFLATED
def zipinfo(path):
info = zipfile.ZipInfo(path)
info.compress_type = compression_type(path)
return info
def is_modified(path):
return apk_modified is None or Deltafy.needs_update_timestamp(path, apk_modified)
def zip_contains(zip, entry):
try:
zip.getinfo(entry)
except:
return False
return True
if is_modified(resources_zip_file):
self.apk_updated = True
resources_zip = zipfile.ZipFile(resources_zip_file)
for path in resources_zip.namelist():
if skip_jar_path(path) or skip_js_file(path): continue
debug("from resource zip => " + path)
apk_zip.writestr(zipinfo(path), resources_zip.read(path))
resources_zip.close()
# add classes.dex
if is_modified(self.classes_dex) or not zip_contains(apk_zip, 'classes.dex'):
apk_zip.write(self.classes_dex, 'classes.dex')
# add all resource files from the project
for root, dirs, files in os.walk(self.project_src_dir, True, None, True):
remove_ignored_dirs(dirs)
for f in files:
if f in ignoreFiles:
continue
if os.path.splitext(f)[1] != '.java':
absolute_path = os.path.join(root, f)
relative_path = os.path.join(root[len(self.project_src_dir)+1:], f)
if is_modified(absolute_path) or not zip_contains(apk_zip, relative_path):
self.apk_updated = True
debug("resource file => " + relative_path)
apk_zip.write(os.path.join(root, f), relative_path, compression_type(f))
def add_resource_jar(jar_file):
jar = zipfile.ZipFile(jar_file)
for path in jar.namelist():
if skip_jar_path(path): continue
debug("from JAR %s => %s" % (jar_file, path))
apk_zip.writestr(zipinfo(path), jar.read(path))
jar.close()
for jar_file in self.module_jars:
add_resource_jar(jar_file)
for jar_file in self.android_jars:
add_resource_jar(jar_file)
def add_native_libs(libs_dir, exclude=[]):
if os.path.exists(libs_dir):
for abi_dir in os.listdir(libs_dir):
if abi_dir not in self.abis:
continue
libs_abi_dir = os.path.join(libs_dir, abi_dir)
if not os.path.isdir(libs_abi_dir): continue
for file in os.listdir(libs_abi_dir):
if file.endswith('.so') and file not in exclude:
native_lib = os.path.join(libs_abi_dir, file)
path_in_zip = '/'.join(['lib', abi_dir, file])
if is_modified(native_lib) or not zip_contains(apk_zip, path_in_zip):
self.apk_updated = True
debug("installing native lib: %s" % native_lib)
apk_zip.write(native_lib, path_in_zip)
# add module native libraries
for module in self.modules:
exclude_libs = []
add_native_libs(module.get_resource('libs'), exclude_libs)
# add any native libraries : libs/**/*.so -> lib/**/*.so
add_native_libs(os.path.join(self.project_dir, 'libs'))
# add sdk runtime native libraries
debug("installing native SDK libs")
sdk_native_libs = os.path.join(template_dir, 'native', 'libs')
for abi in self.abis:
lib_source_dir = os.path.join(sdk_native_libs, abi)
lib_dest_dir = 'lib/%s/' % abi
if abi == 'x86' and ((not os.path.exists(lib_source_dir)) or self.deploy_type == 'production'):
# x86 only in non-production builds for now.
continue
# libtiverify is always included
apk_zip.write(os.path.join(lib_source_dir, 'libtiverify.so'), lib_dest_dir + 'libtiverify.so')
# profiler
apk_zip.write(os.path.join(lib_source_dir, 'libtiprofiler.so'), lib_dest_dir + 'libtiprofiler.so')
for fname in ('libkroll-v8.so', 'libstlport_shared.so'):
apk_zip.write(os.path.join(lib_source_dir, fname), lib_dest_dir + fname)
self.apk_updated = True
apk_zip.close()
return unsigned_apk
def run_adb(self, *args):
command = [self.sdk.get_adb()]
command.extend(self.device_args)
command.extend(args)
return run.run(command)
def get_sigalg(self):
output = run.run([self.keytool,
'-v',
'-list',
'-keystore', self.keystore,
'-storepass', self.keystore_pass,
'-alias', self.keystore_alias
], protect_arg_positions=(6,))
# If the keytool encounters an error, that means some of the provided
# keychain info is invalid and we should bail anyway
run.check_output_for_error(output, r'RuntimeException: (.*)', True)
run.check_output_for_error(output, r'^keytool: (.*)', True)
match = re.search(r'Signature algorithm name: (.*)', output)
if match is not None:
return match.group(1)
# Return the default:
return "MD5withRSA"
def package_and_deploy(self):
# If in production mode and compiling JS, we do not package the JS
# files as assets (we protect them from prying eyes). But if a JS
# file is referenced in an html <script> tag, we DO need to package it.
def get_js_referenced_in_html():
js_files = []
for root, dirs, files in os.walk(self.assets_dir):
for one_file in files:
if one_file.lower().endswith(".html"):
full_path = os.path.join(root, one_file)
html_source = None
file_stream = None
try:
file_stream = open(full_path, "r")
html_source = file_stream.read()
except:
error("Unable to read html file '%s'" % full_path)
finally:
file_stream.close()
if html_source:
parser = HTMLParser()
parser.parse(html_source)
relative_js_files = parser.get_referenced_js_files()
if relative_js_files:
for one_rel_js_file in relative_js_files:
if one_rel_js_file.startswith("http:") or one_rel_js_file.startswith("https:"):
continue
if one_rel_js_file.startswith("app://"):
one_rel_js_file = one_rel_js_file[6:]
js_files.append(os.path.abspath(os.path.join(os.path.dirname(full_path), one_rel_js_file)))
return js_files
ap_ = os.path.join(self.project_dir, 'bin', 'app.ap_')
# This is only to check if this has been overridden in production
has_compile_js = self.tiappxml.has_app_property("ti.android.compilejs")
compile_js = not has_compile_js or (has_compile_js and \
self.tiappxml.to_bool(self.tiappxml.get_app_property('ti.android.compilejs')))
# JS files referenced in html files and thus likely needed for webviews.
webview_js_files = []
pkg_assets_dir = self.assets_dir
if self.deploy_type == "test":
compile_js = False
if compile_js and os.environ.has_key('SKIP_JS_MINIFY'):
compile_js = False
info("Disabling JavaScript minification")
if self.deploy_type == "production" and compile_js:
webview_js_files = get_js_referenced_in_html()
non_js_assets = os.path.join(self.project_dir, 'bin', 'non-js-assets')
if not os.path.exists(non_js_assets):
os.mkdir(non_js_assets)
copy_all(self.assets_dir, non_js_assets, ignore_exts=['.js'])
# if we have any js files referenced in html, we *do* need
# to package them as if they are non-js assets.
if webview_js_files:
for one_js_file in webview_js_files:
if os.path.exists(one_js_file):
dest_file = one_js_file.replace(self.assets_dir, non_js_assets, 1)
if not os.path.exists(os.path.dirname(dest_file)):
os.makedirs(os.path.dirname(dest_file))
shutil.copyfile(one_js_file, dest_file)
pkg_assets_dir = non_js_assets
run.run([self.aapt, 'package', '-f', '-M', 'AndroidManifest.xml', '-A', pkg_assets_dir,
'-S', 'res', '-I', self.android_jar, '-I', self.titanium_jar, '-F', ap_], warning_regex=r'skipping')
unsigned_apk = self.create_unsigned_apk(ap_, webview_js_files)
if self.dist_dir:
app_apk = os.path.join(self.dist_dir, self.name + '.apk')
else:
app_apk = os.path.join(self.project_dir, 'bin', 'app.apk')
output = run.run([self.jarsigner,
'-sigalg', self.get_sigalg(),
'-digestalg', 'SHA1',
'-storepass', self.keystore_pass,
'-keystore', self.keystore,
'-signedjar', app_apk,
unsigned_apk,
self.keystore_alias], protect_arg_positions=(6,))
run.check_output_for_error(output, r'RuntimeException: (.*)', True)
run.check_output_for_error(output, r'^jarsigner: (.*)', True)
# TODO Document Exit message
#success = re.findall(r'RuntimeException: (.*)', output)
#if len(success) > 0:
# error(success[0])
# sys.exit(1)
# zipalign to align byte boundaries
zipalign = self.sdk.get_zipalign()
if os.path.exists(app_apk+'z'):
os.remove(app_apk+'z')
ALIGN_32_BIT = 4
output = run.run([zipalign, '-v', str(ALIGN_32_BIT), app_apk, app_apk+'z'])
# TODO - Document Exit message
if output == None:
error("System Error while compiling Android classes.dex")
sys.exit(1)
else:
os.unlink(app_apk)
os.rename(app_apk+'z',app_apk)
if self.dist_dir:
self.post_build()
sys.exit()
if self.build_only:
return (False, False)
out = self.run_adb('get-state')
#out = subprocess.Popen([self.sdk.get_adb(), self.device_type_arg, 'get-state'], stderr=subprocess.PIPE, stdout=subprocess.PIPE).communicate()[0]
out = str(out).strip()
# try a few times as sometimes it fails waiting on boot
attempts = 0
launched = False
launch_failed = False
while attempts < 5:
try:
if self.install:
self.wait_for_device('d')
info("Installing application on device")
else:
self.wait_for_device('e')
info("Installing application on emulator")
output = self.run_adb('install', '-r', app_apk)
#output = run.run(cmd)
if output == None:
launch_failed = True
elif "Failure" in output:
error("Failed installing %s: %s" % (self.app_id, output))
launch_failed = True
elif not self.install:
launched = True
break
except Exception, e:
error(e)
time.sleep(3)
attempts+=1
return (launched, launch_failed)
def run_app(self):
info("Launching application ... %s" % self.name)
output = self.run_adb('shell', 'am', 'start',
'-a', 'android.intent.action.MAIN',
'-c','android.intent.category.LAUNCHER',
'-n', '%s/.%sActivity' % (self.app_id , self.classname),
'-f', '0x10200000')
trace("Launch output: %s" % output)
def wait_for_sdcard(self):
# Quick check: the existence of /sdcard/Android,
# which really should be there on all phones and emulators.
output = self.run_adb('shell', 'cd /sdcard/Android && echo SDCARD READY')
if 'SDCARD READY' in output:
return True
# Our old way of checking in case the above
# didn't succeed:
mount_points_check = ['/sdcard', '/mnt/sdcard']
# Check the symlink that is typically in root.
# If you find it, add its target to the mount points to check.
output = self.run_adb('shell', 'ls', '-l', '/sdcard')
if output:
target_pattern = r"\-\> (\S+)\s*$"
mount_points_check.extend(re.findall(target_pattern, output))
info("Waiting for SDCard to become available..")
waited = 0
max_wait = 60
while waited < max_wait:
output = self.run_adb('shell', 'mount')
if output != None:
mount_points = output.splitlines()
for mount_point in mount_points:
tokens = mount_point.split()
if len(tokens) < 2: continue
mount_path = tokens[1]
if mount_path in mount_points_check:
return True
else:
error("Error checking for SDCard using 'mount'")
return False
time.sleep(1)
waited += 1
error("Timed out waiting for SDCard to become available (%ds)" % max_wait)
return False
def push_deploy_json(self):
deploy_data = {
"debuggerEnabled": self.debugger_host != None,
"debuggerPort": self.debugger_port,
"profilerEnabled": self.profiler_host != None,
"profilerPort": self.profiler_port,
"fastdevPort": self.fastdev_port
}
deploy_json = os.path.join(self.project_dir, 'bin', 'deploy.json')
open(deploy_json, 'w+').write(simplejson.dumps(deploy_data))
sdcard_available = self.wait_for_sdcard()
if sdcard_available:
self.run_adb('shell', 'mkdir /sdcard/%s || echo' % self.app_id)
self.run_adb('push', deploy_json, '/sdcard/%s/deploy.json' % self.app_id)
os.unlink(deploy_json)
def verify_fastdev(self):
lock_file = os.path.join(self.top_dir, '.fastdev.lock')
if not fastdev.is_running(self.top_dir):
if os.path.exists(lock_file):
os.unlink(lock_file)
return False
else:
data = simplejson.loads(open(lock_file, 'r').read())
self.fastdev_port = data["port"]
return True
def fastdev_kill_app(self):
lock_file = os.path.join(self.top_dir, ".fastdev.lock")
if os.path.exists(lock_file):
class Options(object): pass
options = Options()
options.lock_file = lock_file
try:
return fastdev.kill_app(self.top_dir, options)
except Exception, e:
return False
def merge_internal_module_resources(self):
if not self.android_jars:
return
for jar in self.android_jars:
if not os.path.exists(jar):
continue
res_zip = jar[:-4] + '.res.zip'
if not os.path.exists(res_zip):
continue
res_zip_file = zipfile.ZipFile(res_zip, "r")
try:
zip_extractall(res_zip_file, self.project_dir)
except:
raise
finally:
res_zip_file.close()
def build_and_run(self, install, avd_id, keystore=None, keystore_pass='tirocks', keystore_alias='tidev', dist_dir=None, build_only=False, device_args=None, debugger_host=None, profiler_host=None):
deploy_type = 'development'
self.build_only = build_only
self.device_args = device_args
self.postbuild_modules = []
self.finalize_modules = []
self.non_orphans = []
if install:
if self.device_args == None:
self.device_args = ['-d']
if keystore == None:
deploy_type = 'test'
else:
deploy_type = 'production'
if self.device_args == None:
self.device_args = ['-e']
self.deploy_type = deploy_type
(java_failed, java_status) = prereq.check_java()
if java_failed:
error(java_status)
sys.exit(1)
# attempt to load any compiler plugins
if len(self.tiappxml.properties['plugins']) > 0:
titanium_dir = os.path.abspath(os.path.join(template_dir,'..','..','..','..'))
local_compiler_dir = os.path.abspath(os.path.join(self.top_dir,'plugins'))
tp_compiler_dir = os.path.abspath(os.path.join(titanium_dir,'plugins'))
if not os.path.exists(tp_compiler_dir) and not os.path.exists(local_compiler_dir):
error("Build Failed (Missing plugins directory)")
sys.exit(1)
compiler_config = {
'platform':'android',
'tiapp':self.tiappxml,
'project_dir':self.top_dir,
'titanium_dir':titanium_dir,
'appid':self.app_id,
'template_dir':template_dir,
'project_name':self.name,
'command':self.command,
'build_dir':self.project_dir,
'app_name':self.name,
'android_builder':self,
'deploy_type':deploy_type,
'dist_dir':dist_dir,
'logger':log
}
for plugin in self.tiappxml.properties['plugins']:
local_plugin_file = os.path.join(local_compiler_dir,plugin['name'],'plugin.py')
plugin_file = os.path.join(tp_compiler_dir,plugin['name'],plugin['version'],'plugin.py')
info("plugin=%s" % plugin_file)
if not os.path.exists(local_plugin_file) and not os.path.exists(plugin_file):
error("Build Failed (Missing plugin for %s)" % plugin['name'])
sys.exit(1)
info("Detected compiler plugin: %s/%s" % (plugin['name'],plugin['version']))
code_path = plugin_file
if os.path.exists(local_plugin_file):
code_path = local_plugin_file
compiler_config['plugin']=plugin
fin = open(code_path, 'rb')
m = hashlib.md5()
m.update(open(code_path,'rb').read())
code_hash = m.hexdigest()
p = imp.load_source(code_hash, code_path, fin)
module_functions = dict(inspect.getmembers(p, inspect.isfunction))
if module_functions.has_key('postbuild'):
debug("plugin contains a postbuild function. Will execute after project is built and packaged")
self.postbuild_modules.append((plugin['name'], p))
if module_functions.has_key('finalize'):
debug("plugin contains a finalize function. Will execute before script exits")
self.finalize_modules.append((plugin['name'], p))
p.compile(compiler_config)
fin.close()
# in Windows, if the adb server isn't running, calling "adb devices"
# will fork off a new adb server, and cause a lock-up when we
# try to pipe the process' stdout/stderr. the workaround is
# to simply call adb start-server here, and not care about
# the return code / pipes. (this is harmless if adb is already running)
# -- thanks to Bill Dawson for the workaround
if platform.system() == "Windows" and not build_only:
run.run([self.sdk.get_adb(), "start-server"], True, ignore_output=True)
ti_version_file = os.path.join(self.support_dir, '..', 'version.txt')
if os.path.exists(ti_version_file):
ti_version_info = read_properties(open(ti_version_file, 'r'), '=')
if not ti_version_info is None and 'version' in ti_version_info:
ti_version_string = 'Titanium SDK version: %s' % ti_version_info['version']
if 'timestamp' in ti_version_info or 'githash' in ti_version_info:
ti_version_string += ' ('
if 'timestamp' in ti_version_info:
ti_version_string += '%s' % ti_version_info['timestamp']
if 'githash' in ti_version_info:
ti_version_string += ' %s' % ti_version_info['githash']
ti_version_string += ')'
info(ti_version_string)
if not build_only:
if deploy_type == 'development':
self.wait_for_device('e')
elif deploy_type == 'test':
self.wait_for_device('d')
self.install = install
self.dist_dir = dist_dir
self.aapt = self.sdk.get_aapt()
self.android_jar = self.sdk.get_android_jar()
self.titanium_jar = os.path.join(self.support_dir,'titanium.jar')
self.kroll_apt_jar = os.path.join(self.support_dir, 'kroll-apt.jar')
dx = self.sdk.get_dx()
self.apkbuilder = self.sdk.get_apkbuilder()
self.sdcard_resources = '/sdcard/Ti.debug/%s/Resources' % self.app_id
self.resources_installed = False
if deploy_type == "production":
self.app_installed = False
else:
self.app_installed = not build_only and self.is_app_installed()
debug("%s installed? %s" % (self.app_id, self.app_installed))
#self.resources_installed = not build_only and self.are_resources_installed()
#debug("%s resources installed? %s" % (self.app_id, self.resources_installed))
if keystore == None:
keystore = os.path.join(self.support_dir,'dev_keystore')
self.keystore = keystore
self.keystore_pass = keystore_pass
self.keystore_alias = keystore_alias
curdir = os.getcwd()
self.support_resources_dir = os.path.join(self.support_dir, 'resources')
try:
os.chdir(self.project_dir)
self.android = Android(self.name, self.app_id, self.sdk, deploy_type, self.java)
if not os.path.exists('bin'):
os.makedirs('bin')
resources_dir = os.path.join(self.top_dir,'Resources')
self.assets_dir = os.path.join(self.project_dir,'bin','assets')
self.assets_resources_dir = os.path.join(self.assets_dir,'Resources')
if not os.path.exists(self.assets_resources_dir):
os.makedirs(self.assets_resources_dir)
shutil.copy(self.project_tiappxml, self.assets_dir)
finalxml = os.path.join(self.assets_dir,'tiapp.xml')
self.tiapp = TiAppXML(finalxml)
self.tiapp.setDeployType(deploy_type)
self.sdcard_copy = False
sdcard_property = "ti.android.loadfromsdcard"
if self.tiapp.has_app_property(sdcard_property):
self.sdcard_copy = self.tiapp.to_bool(self.tiapp.get_app_property(sdcard_property))
fastdev_property = "ti.android.fastdev"
fastdev_enabled = (self.deploy_type == 'development' and not self.build_only)
if self.tiapp.has_app_property(fastdev_property):
fastdev_enabled = self.tiapp.to_bool(self.tiapp.get_app_property(fastdev_property))
if fastdev_enabled:
if self.verify_fastdev():
info("Fastdev server running, deploying in Fastdev mode")
self.fastdev = True
else:
warn("Fastdev enabled, but server isn't running, deploying normally")
self.classes_dir = os.path.join(self.project_dir, 'bin', 'classes')
if not os.path.exists(self.classes_dir):
os.makedirs(self.classes_dir)
if (not debugger_host is None) and len(debugger_host) > 0:
hostport = debugger_host.split(":")
self.debugger_host = hostport[0]
self.debugger_port = int(hostport[1])
debugger_enabled = self.debugger_host != None and len(self.debugger_host) > 0
if (not profiler_host is None) and len(profiler_host) > 0:
hostport = profiler_host.split(":")
self.profiler_host = hostport[0]
self.profiler_port = int(hostport[1])
profiler_enabled = self.profiler_host != None and len(self.profiler_host) > 0
# Detect which modules are being used.
# We need to know this info in a few places, so the info is saved
# in self.missing_modules and self.modules
detector = ModuleDetector(self.top_dir)
self.missing_modules, self.modules = detector.find_app_modules(self.tiapp, 'android', deploy_type)
self.copy_commonjs_modules()
self.copy_project_resources()
last_build_info = None
built_all_modules = False
build_info_path = os.path.join(self.project_dir, 'bin', 'build_info.json')
if os.path.exists(build_info_path):
last_build_info = simplejson.loads(open(build_info_path, 'r').read())
built_all_modules = last_build_info["include_all_modules"]
if self.tiapp.has_app_property("ti.android.compilejs"):
if self.tiapp.to_bool(self.tiapp.get_app_property('ti.android.compilejs')):
self.compile_js = True
elif self.tiapp.has_app_property('ti.deploytype'):
if self.tiapp.get_app_property('ti.deploytype') == 'production':
self.compile_js = True
if self.compile_js and os.environ.has_key('SKIP_JS_MINIFY'):
self.compile_js = False
info("Disabling JavaScript minification")
include_all_ti_modules = self.fastdev
if (self.tiapp.has_app_property('ti.android.include_all_modules')):
if self.tiapp.to_bool(self.tiapp.get_app_property('ti.android.include_all_modules')):
include_all_ti_modules = True
if self.tiapp_changed or (self.js_changed and not self.fastdev) or \
self.force_rebuild or self.deploy_type == "production" or \
(self.fastdev and not built_all_modules) or \
(not self.fastdev and built_all_modules):
self.android.config['compile_js'] = self.compile_js
trace("Generating Java Classes")
self.android.create(os.path.abspath(os.path.join(self.top_dir,'..')),
True, project_dir = self.top_dir, include_all_ti_modules=include_all_ti_modules)
open(build_info_path, 'w').write(simplejson.dumps({
"include_all_modules": include_all_ti_modules
}))
else:
info("Tiapp.xml unchanged, skipping class generation")
# compile resources
full_resource_dir = os.path.join(self.project_dir, self.assets_resources_dir)
compiler = Compiler(self.tiapp,
full_resource_dir,
self.java,
self.classes_dir,
self.project_gen_dir,
self.project_dir,
include_all_modules=include_all_ti_modules)
compiler.compile(compile_bytecode=self.compile_js, external_modules=self.modules)
self.compiled_files = compiler.compiled_files
self.android_jars = compiler.jar_libraries
self.merge_internal_module_resources()
if not os.path.exists(self.assets_dir):
os.makedirs(self.assets_dir)
self.resource_drawables_changed = self.copy_resource_drawables()
self.warn_dupe_drawable_folders()
self.copy_module_platform_folders()
special_resources_dir = os.path.join(self.top_dir,'platform','android')
if os.path.exists(special_resources_dir):
debug("found special platform files dir = %s" % special_resources_dir)
ignore_files = ignoreFiles
ignore_files.extend(['AndroidManifest.xml']) # don't want to overwrite build/android/AndroidManifest.xml yet
self.copy_project_platform_folder(ignoreDirs, ignore_files)
self.generate_stylesheet()
self.generate_aidl()
self.manifest_changed = self.generate_android_manifest(compiler)
my_avd = None
self.google_apis_supported = False
# find the AVD we've selected and determine if we support Google APIs
if avd_id is not None:
for avd_props in avd.get_avds(self.sdk):
if avd_props['id'] == avd_id:
my_avd = avd_props
self.google_apis_supported = (my_avd['name'].find('Google')!=-1 or my_avd['name'].find('APIs')!=-1)
break
if build_only or avd_id is None:
self.google_apis_supported = True
remove_orphaned_files(resources_dir, self.assets_resources_dir, self.non_orphans)
generated_classes_built = self.build_generated_classes()
# TODO: enable for "test" / device mode for debugger / fastdev
if not self.build_only and (self.deploy_type == "development" or self.deploy_type == "test"):
self.push_deploy_json()
self.classes_dex = os.path.join(self.project_dir, 'bin', 'classes.dex')
def jar_includer(path, isfile):
if isfile and path.endswith(".jar"): return True
return False
support_deltafy = Deltafy(self.support_dir, jar_includer)
self.support_deltas = support_deltafy.scan()
dex_built = False
if len(self.support_deltas) > 0 or generated_classes_built or self.deploy_type == "production":
# the dx.bat that ships with android in windows doesn't allow command line
# overriding of the java heap space, so we call the jar directly
if platform.system() == 'Windows':
dex_args = [self.java, '-Xmx1024M', '-Djava.ext.dirs=%s' % self.sdk.get_platform_tools_dir(), '-jar', self.sdk.get_dx_jar()]
else:
dex_args = [dx, '-JXmx1536M', '-JXX:-UseGCOverheadLimit']
dex_args += ['--dex', '--output='+self.classes_dex, self.classes_dir]
dex_args += self.android_jars
dex_args += self.module_jars
dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-verify.jar'))
if self.deploy_type != 'production':
dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-debug.jar'))
dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-profiler.jar'))
# the verifier depends on Ti.Network classes, so we may need to inject it
has_network_jar = False
for jar in self.android_jars:
if jar.endswith('titanium-network.jar'):
has_network_jar = True
break
if not has_network_jar:
dex_args.append(os.path.join(self.support_dir, 'modules', 'titanium-network.jar'))
info("Compiling Android Resources... This could take some time")
# TODO - Document Exit message
run_result = run.run(dex_args, warning_regex=r'warning: ')
if (run_result == None):
dex_built = False
error("System Error while compiling Android classes.dex")
sys.exit(1)
else:
dex_built = True
debug("Android classes.dex built")
if dex_built or generated_classes_built or self.tiapp_changed or self.manifest_changed or not self.app_installed or not self.fastdev:
# metadata has changed, we need to do a full re-deploy
launched, launch_failed = self.package_and_deploy()
if launched:
self.run_app()
info("Deployed %s ... Application should be running." % self.name)
elif launch_failed==False and not build_only:
info("Application installed. Launch from drawer on Home Screen")
elif not build_only:
# Relaunch app if nothing was built
info("Re-launching application ... %s" % self.name)
relaunched = False
killed = False
if self.fastdev:
killed = self.fastdev_kill_app()
if not killed:
processes = self.run_adb('shell', 'ps')
for line in processes.splitlines():
columns = line.split()
if len(columns) > 1:
pid = columns[1]
id = columns[len(columns)-1]
if id == self.app_id:
self.run_adb('shell', 'kill', pid)
relaunched = True
self.run_app()
if relaunched:
info("Relaunched %s ... Application should be running." % self.name)
self.post_build()
# Enable port forwarding for debugger if application
# acts as the server.
if debugger_enabled:
info('Forwarding host port %s to device for debugging.' % self.debugger_port)
forwardPort = 'tcp:%s' % self.debugger_port
self.sdk.run_adb(['forward', forwardPort, forwardPort])
# Enable port forwarding for profiler
if profiler_enabled:
info('Forwarding host port %s to device for profiling.' % self.profiler_port)
forwardPort = 'tcp:%s' % self.profiler_port
self.sdk.run_adb(['forward', forwardPort, forwardPort])
#intermediary code for on-device debugging (later)
#if debugger_host != None:
#import debugger
#debug("connecting to debugger: %s, debugger=%s" % (debugger_host, str(debugger)))
#debugger.run(debugger_host, '127.0.0.1:5999')
finally:
os.chdir(curdir)
sys.stdout.flush()
def post_build(self):
try:
if self.postbuild_modules:
for p in self.postbuild_modules:
info("Running postbuild function in %s plugin" % p[0])
p[1].postbuild()
except Exception,e:
error("Error performing post-build steps: %s" % e)
def finalize(self):
try:
if self.finalize_modules:
for p in self.finalize_modules:
info("Running finalize function in %s plugin" % p[0])
p[1].finalize()
except Exception,e:
error("Error performing finalize steps: %s" % e)
if __name__ == "__main__":
def usage():
print "%s <command> <project_name> <sdk_dir> <project_dir> <app_id> [key] [password] [alias] [dir] [avdid] [avdskin] [avdabi] [emulator options]" % os.path.basename(sys.argv[0])
print
print "available commands: "
print
print " emulator build and run the emulator"
print " simulator build and run the app on the simulator"
print " install build and install the app on the device"
print " distribute build final distribution package for upload to marketplace"
print " run build and run the project using values from tiapp.xml"
print " run-emulator run the emulator with a default AVD ID and skin"
sys.exit(1)
argc = len(sys.argv)
if argc < 2:
usage()
command = sys.argv[1]
if command == 'logcat':
launch_logcat()
template_dir = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
get_values_from_tiapp = False
is_emulator = False
if command == 'run':
if argc < 4:
print 'Usage: %s run <project_dir> <android_sdk>' % sys.argv[0]
sys.exit(1)
get_values_from_tiapp = True
project_dir = sys.argv[2]
sdk_dir = sys.argv[3]
avd_id = "7"
elif command == 'run-emulator':
if argc < 4:
print 'Usage: %s run-emulator <project_dir> <android_sdk>' % sys.argv[0]
sys.exit(1)
get_values_from_tiapp = True
project_dir = sys.argv[2]
sdk_dir = sys.argv[3]
# sensible defaults?
avd_id = "7"
avd_skin = "HVGA"
else:
if command == 'emulator':
is_emulator = True
if argc < 6 or command == '--help' or (command=='distribute' and argc < 10):
usage()
if get_values_from_tiapp:
tiappxml = TiAppXML(os.path.join(project_dir, 'tiapp.xml'))
app_id = tiappxml.properties['id']
project_name = tiappxml.properties['name']
else:
project_name = dequote(sys.argv[2])
sdk_dir = os.path.abspath(os.path.expanduser(dequote(sys.argv[3])))
project_dir = os.path.abspath(os.path.expanduser(dequote(sys.argv[4])))
app_id = dequote(sys.argv[5])
log = TiLogger(os.path.join(os.path.abspath(os.path.expanduser(dequote(project_dir))), 'build.log'))
log.debug(" ".join(sys.argv))
builder = Builder(project_name,sdk_dir,project_dir,template_dir,app_id,is_emulator)
builder.command = command
try:
if command == 'run-emulator':
builder.run_emulator(avd_id, avd_skin, None, None, [])
elif command == 'run':
builder.build_and_run(False, avd_id)
elif command == 'emulator':
avd_id = dequote(sys.argv[6])
add_args = None
avd_abi = None
avd_skin = None
avd_name = None
if avd_id.isdigit():
avd_name = None
avd_skin = dequote(sys.argv[7])
if argc > 8:
# The first of the remaining args
# could either be an abi or an additional argument for
# the emulator. Compare to known abis.
next_index = 8
test_arg = sys.argv[next_index]
if test_arg in KNOWN_ABIS:
avd_abi = test_arg
next_index += 1
# Whatever remains (if anything) is an additional
# argument to pass to the emulator.
if argc > next_index:
add_args = sys.argv[next_index:]
else:
avd_name = sys.argv[6]
# If the avd is known by name, then the skin and abi shouldn't be passed,
# because the avd already has the skin and abi "in it".
avd_id = None
avd_skin = None
avd_abi = None
if argc > 7:
add_args = sys.argv[7:]
builder.run_emulator(avd_id, avd_skin, avd_name, avd_abi, add_args)
elif command == 'simulator':
info("Building %s for Android ... one moment" % project_name)
avd_id = dequote(sys.argv[6])
debugger_host = None
profiler_host = None
if len(sys.argv) > 9 and sys.argv[9] == 'profiler':
profiler_host = dequote(sys.argv[8])
elif len(sys.argv) > 8:
debugger_host = dequote(sys.argv[8])
builder.build_and_run(False, avd_id, debugger_host=debugger_host, profiler_host=profiler_host)
elif command == 'install':
avd_id = dequote(sys.argv[6])
device_args = ['-d']
# We have to be careful here because Windows can't handle an empty argument
# on the command line, so if a device serial number is not passed in, but
# a debugger_host (the argument after device serial number) _is_ passed in,
# to Windows it just looks like a serial number is passed in (the debugger_host
# argument shifts left to take over the empty argument.)
debugger_host = None
profiler_host = None
if len(sys.argv) >= 10 and sys.argv[9] == 'profiler':
profiler_host = dequote(sys.argv[8])
if len(sys.argv[7]) > 0:
device_args = ['-s', sys.argv[7]]
elif len(sys.argv) >= 9 and len(sys.argv[8]) > 0:
debugger_host = dequote(sys.argv[8])
if len(sys.argv[7]) > 0:
device_args = ['-s', sys.argv[7]]
elif len(sys.argv) >= 8 and len(sys.argv[7]) > 0:
arg7 = dequote(sys.argv[7])
if 'adb:' in arg7:
debugger_host = arg7
else:
device_args = ['-s', arg7]
builder.build_and_run(True, avd_id, device_args=device_args, debugger_host=debugger_host, profiler_host=profiler_host)
elif command == 'distribute':
key = os.path.abspath(os.path.expanduser(dequote(sys.argv[6])))
password = dequote(sys.argv[7])
alias = dequote(sys.argv[8])
output_dir = dequote(sys.argv[9])
builder.build_and_run(True, None, key, password, alias, output_dir)
elif command == 'build':
builder.build_and_run(False, 1, build_only=True)
else:
error("Unknown command: %s" % command)
usage()
except SystemExit, n:
sys.exit(n)
except:
e = traceback.format_exc()
error("Exception occured while building Android project:")
for line in e.splitlines():
error(line)
sys.exit(1)
finally:
# Don't run plugin finalizer functions if all we were doing is
# starting up the emulator.
if builder and command not in ("emulator", "run-emulator"):
builder.finalize()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment