PageRenderTime 65ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/support/android/builder.py

http://github.com/appcelerator/titanium_mobile
Python | 2648 lines | 2435 code | 132 blank | 81 comment | 233 complexity | cac4effa378a19aae6a2aed15fafadbf MD5 | raw file
Possible License(s): MIT, JSON, Apache-2.0, 0BSD, CC-BY-SA-3.0, BSD-2-Clause, MPL-2.0-no-copyleft-exception, BSD-3-Clause, CC-BY-3.0, Unlicense
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Appcelerator Titanium Mobile
  5. # Copyright (c) 2011-2012 by Appcelerator, Inc. All Rights Reserved.
  6. # Licensed under the terms of the Apache Public License
  7. # Please see the LICENSE included with this distribution for details.
  8. #
  9. # General builder script for staging, packaging, deploying,
  10. # and debugging Titanium Mobile applications on Android
  11. #
  12. import os, sys, subprocess, shutil, time, signal, string, platform, re, glob, hashlib, imp, inspect
  13. import run, avd, prereq, zipfile, tempfile, fnmatch, codecs, traceback, sgmllib
  14. from os.path import splitext
  15. from compiler import Compiler
  16. from os.path import join, splitext, split, exists
  17. from shutil import copyfile
  18. from xml.dom.minidom import parseString
  19. from tilogger import *
  20. from datetime import datetime, timedelta
  21. reload(sys) # this is required to prevent the following error: "AttributeError: 'module' object has no attribute 'setdefaultencoding'"
  22. sys.setdefaultencoding("utf_8") # Fix umlaut issues
  23. template_dir = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
  24. top_support_dir = os.path.dirname(template_dir)
  25. sys.path.append(top_support_dir)
  26. sys.path.append(os.path.join(top_support_dir, 'common'))
  27. sys.path.append(os.path.join(top_support_dir, 'module'))
  28. import simplejson, java
  29. from mako.template import Template
  30. from tiapp import *
  31. from android import Android
  32. from androidsdk import AndroidSDK
  33. from deltafy import Deltafy, Delta
  34. from css import csscompiler
  35. from module import ModuleDetector
  36. import localecompiler
  37. import fastdev
  38. import requireIndex
  39. resourceFiles = ['strings.xml', 'attrs.xml', 'styles.xml', 'bools.xml', 'colors.xml',
  40. 'dimens.xml', 'ids.xml', 'integers.xml', 'arrays.xml']
  41. ignoreFiles = ['.gitignore', '.cvsignore', '.DS_Store'];
  42. ignoreDirs = ['.git','.svn','_svn', 'CVS'];
  43. android_avd_hw = {'hw.camera': 'yes', 'hw.gps':'yes'}
  44. res_skips = ['style']
  45. log = None
  46. # Copied from frameworks/base/tools/aapt/Package.cpp
  47. uncompressed_types = [
  48. ".jpg", ".jpeg", ".png", ".gif",
  49. ".wav", ".mp2", ".mp3", ".ogg", ".aac",
  50. ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
  51. ".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
  52. ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
  53. ".amr", ".awb", ".wma", ".wmv"
  54. ]
  55. # Java keywords to reference in case app id contains java keyword
  56. java_keywords = [
  57. "abstract", "continue", "for", "new", "switch",
  58. "assert", "default", "goto", "package", "synchronized",
  59. "boolean", "do", "if", "private", "this",
  60. "break", "double", "implements", "protected", "throw",
  61. "byte", "else", "import", "public", "throws",
  62. "case", "enum", "instanceof", "return", "transient",
  63. "catch", "extends", "int", "short", "try",
  64. "char", "final", "interface", "static", "void",
  65. "class", "finally", "long", "strictfp", "volatile",
  66. "const", "float", "native", "super", "while",
  67. "true", "false", "null"
  68. ]
  69. MIN_API_LEVEL = 10
  70. HONEYCOMB_MR2_LEVEL = 13
  71. KNOWN_ABIS = ("armeabi-v7a", "x86")
  72. # Used only to find <script> tags in HTML files
  73. # so we can be sure to package referenced JS files
  74. # even when compiling for production. (See
  75. # Builder.package_and_deploy later in this file.)
  76. class HTMLParser(sgmllib.SGMLParser):
  77. def parse(self, html_source):
  78. self.feed(html_source)
  79. self.close()
  80. def __init__(self, verbose=0):
  81. sgmllib.SGMLParser.__init__(self, verbose)
  82. self.referenced_js_files = []
  83. def start_script(self, attributes):
  84. for name, value in attributes:
  85. if value and name.lower() == "src":
  86. self.referenced_js_files.append(value.lower())
  87. def get_referenced_js_files(self):
  88. return self.referenced_js_files
  89. def launch_logcat():
  90. valid_device_switches = ('-e', '-d', '-s')
  91. device_id = None
  92. android_sdk_location = None
  93. adb_location = None
  94. logcat_process = None
  95. device_switch = None # e.g., -e or -d or -s
  96. def show_usage():
  97. print >> sys.stderr, ""
  98. print >> sys.stderr, "%s devicelog <sdk_dir> <device_switch> [device_serial_number]" % os.path.basename(sys.argv[0])
  99. print >> sys.stderr, ""
  100. print >> sys.stderr, "The <device_switch> can be -e, -d -s. If -s, also pass serial number."
  101. sys.exit(1)
  102. if len(sys.argv) < 3:
  103. print >> sys.stderr, "Missing Android SDK location."
  104. show_usage()
  105. else:
  106. android_sdk_location = os.path.abspath(os.path.expanduser(sys.argv[2]))
  107. adb_location = AndroidSDK(android_sdk_location).get_adb()
  108. if len(sys.argv) < 4:
  109. print >> sys.stderr, "Missing device/emulator switch (e.g., -e, -d, -s)."
  110. show_usage()
  111. device_switch = sys.argv[3]
  112. if device_switch not in valid_device_switches:
  113. print >> sys.stderr, "Unknown device type switch: %s" % device_switch
  114. show_usage()
  115. if device_switch == "-s":
  116. if len(sys.argv) < 5:
  117. print >> sys.stderr, "Must specify serial number when using -s."
  118. show_usage()
  119. else:
  120. device_id = sys.argv[4]
  121. # For killing the logcat process if our process gets killed.
  122. def signal_handler(signum, frame):
  123. print "[DEBUG] Signal %s received. Terminating the logcat process." % signum
  124. if logcat_process is not None:
  125. if platform.system() == "Windows":
  126. os.system("taskkill /F /T /PID %i" % logcat_process.pid)
  127. else:
  128. os.kill(logcat_process.pid, signal.SIGTERM)
  129. # make sure adb is running on windows, else XP can lockup the python
  130. # process when adb runs first time
  131. if platform.system() == "Windows":
  132. run.run([adb_location, "start-server"], True, ignore_output=True)
  133. logcat_cmd = [adb_location, device_switch]
  134. if device_id:
  135. logcat_cmd.append(device_id)
  136. logcat_cmd.extend(["logcat", "-s", "*:d,*,TiAPI:V"])
  137. logcat_process = subprocess.Popen(logcat_cmd)
  138. if platform.system() != "Windows":
  139. signal.signal(signal.SIGHUP, signal_handler)
  140. signal.signal(signal.SIGQUIT, signal_handler)
  141. signal.signal(signal.SIGINT, signal_handler)
  142. signal.signal(signal.SIGABRT, signal_handler)
  143. signal.signal(signal.SIGTERM, signal_handler)
  144. # In case it's gonna exit early (like if the command line
  145. # was wrong or something) give it a chance to do so before we start
  146. # waiting on it.
  147. time.sleep(1)
  148. return_code = logcat_process.poll()
  149. if return_code:
  150. signal_handler(signal.SIGQUIT, None)
  151. sys.exit(return_code)
  152. # Now wait for it.
  153. try:
  154. return_code = logcat_process.wait()
  155. except OSError:
  156. signal_handler(signal.SIGQUIT, None)
  157. sys.exit(return_code)
  158. sys.exit(return_code)
  159. def render_template_with_tiapp(template_text, tiapp_obj):
  160. t = Template(template_text)
  161. return t.render(tiapp=tiapp_obj)
  162. def remove_ignored_dirs(dirs):
  163. for d in dirs:
  164. if d in ignoreDirs:
  165. dirs.remove(d)
  166. # ZipFile.extractall introduced in Python 2.6, so this is workaround for earlier
  167. # versions
  168. def zip_extractall(zfile, target_dir):
  169. file_infos = zfile.infolist()
  170. for info in file_infos:
  171. if info.file_size > 0:
  172. file_path = os.path.join(target_dir, os.path.normpath(info.filename))
  173. parent_path = os.path.dirname(file_path)
  174. if not os.path.exists(parent_path):
  175. os.makedirs(parent_path)
  176. out_file = open(file_path, "wb")
  177. out_file.write(zfile.read(info.filename))
  178. out_file.close()
  179. def dequote(s):
  180. if s[0:1] == '"':
  181. return s[1:-1]
  182. return s
  183. def pipe(args1,args2):
  184. p1 = subprocess.Popen(args1, stdout=subprocess.PIPE)
  185. p2 = subprocess.Popen(args2, stdin=p1.stdout, stdout=subprocess.PIPE)
  186. return p2.communicate()[0]
  187. def read_properties(propFile, separator=":= "):
  188. propDict = dict()
  189. for propLine in propFile:
  190. propDef = propLine.strip()
  191. if len(propDef) == 0:
  192. continue
  193. if propDef[0] in ( '!', '#' ):
  194. continue
  195. punctuation= [ propDef.find(c) for c in separator ] + [ len(propDef) ]
  196. found= min( [ pos for pos in punctuation if pos != -1 ] )
  197. name= propDef[:found].rstrip()
  198. value= propDef[found:].lstrip(separator).rstrip()
  199. propDict[name]= value
  200. propFile.close()
  201. return propDict
  202. def info(msg):
  203. log.info(msg)
  204. def debug(msg):
  205. log.debug(msg)
  206. def warn(msg):
  207. log.warn(msg)
  208. def trace(msg):
  209. log.trace(msg)
  210. def error(msg):
  211. log.error(msg)
  212. def copy_all(source_folder, dest_folder, mergeXMLResources=False, ignore_dirs=[], ignore_files=[], ignore_exts=[], one_time_msg=""):
  213. msg_shown = False
  214. for root, dirs, files in os.walk(source_folder, True, None, True):
  215. for d in dirs:
  216. if d in ignore_dirs:
  217. dirs.remove(d)
  218. for f in files:
  219. if f in ignore_files:
  220. continue
  221. ext = os.path.splitext(f)[1]
  222. if ext in ignore_exts:
  223. continue
  224. if one_time_msg and not msg_shown:
  225. info(one_time_msg)
  226. msg_shown = True
  227. from_ = os.path.join(root, f)
  228. to_ = from_.replace(source_folder, dest_folder, 1)
  229. to_directory = os.path.split(to_)[0]
  230. if not os.path.exists(to_directory):
  231. os.makedirs(to_directory)
  232. shutil.copyfile(from_, to_)
  233. #
  234. # Merge the xml resource files in res/values/ if there are multiple files with the same name.
  235. # (TIMOB-12663)
  236. #
  237. elif mergeXMLResources and os.path.isfile(to_) and f in resourceFiles:
  238. sfile = open(from_, 'r')
  239. dfile = open(to_, 'r')
  240. scontent = sfile.read()
  241. dcontent = dfile.read()
  242. sfile.close()
  243. dfile.close()
  244. sindex = scontent.find('</resources>')
  245. dindex = dcontent.find('>', dcontent.find('<resources')) + 1
  246. content_to_write = scontent[:sindex] + dcontent[dindex:]
  247. wfile = open(to_, 'w')
  248. wfile.write(content_to_write)
  249. wfile.close()
  250. else:
  251. shutil.copyfile(from_, to_)
  252. def remove_orphaned_files(source_folder, target_folder, ignore=[]):
  253. is_res = source_folder.endswith('Resources') or source_folder.endswith('Resources' + os.sep)
  254. for root, dirs, files in os.walk(target_folder):
  255. for f in files:
  256. if f in ignore:
  257. continue
  258. full = os.path.join(root, f)
  259. rel = full.replace(target_folder, '')
  260. if rel[0] == os.sep:
  261. rel = rel[1:]
  262. is_orphan = False
  263. if not os.path.exists(os.path.join(source_folder, rel)):
  264. is_orphan = True
  265. # But it could be under android/... too (platform-specific)
  266. if is_orphan and is_res:
  267. if os.path.exists(os.path.join(source_folder, 'android', rel)):
  268. is_orphan = False
  269. if is_orphan:
  270. os.remove(full)
  271. def is_resource_drawable(path):
  272. if re.search("android/images/(high|medium|low|res-[^/]+)/", path.replace(os.sep, "/")):
  273. return True
  274. else:
  275. return False
  276. def resource_drawable_folder(path):
  277. if not is_resource_drawable(path):
  278. return None
  279. else:
  280. pattern = r'/android/images/(high|medium|low|res-[^/]+)/'
  281. match = re.search(pattern, path.replace(os.sep, "/"))
  282. if not match.groups():
  283. return None
  284. folder = match.groups()[0]
  285. if re.match('high|medium|low', folder):
  286. return 'drawable-%sdpi' % folder[0]
  287. else:
  288. return 'drawable-%s' % folder.replace('res-', '')
  289. def remove_duplicate_nodes_in_res_file(full_path, node_names_to_check):
  290. f = open(full_path, 'r')
  291. contents = f.read()
  292. f.close()
  293. doc = parseString(contents)
  294. resources_node = doc.getElementsByTagName('resources')[0]
  295. made_change = False
  296. for node_name in node_names_to_check:
  297. nodes = doc.getElementsByTagName(node_name)
  298. if len(nodes) == 0:
  299. continue
  300. name_list = [] #keeps track of the name attribute for the node we are checking
  301. for node in nodes:
  302. # Only check for the children of the "resources" node
  303. if node.parentNode != resources_node:
  304. continue
  305. name = node.getAttribute('name')
  306. # Remove the node with the duplicate names
  307. if name in name_list:
  308. resources_node.removeChild(node)
  309. made_change = True
  310. debug('Removed duplicate node [%s] from %s' %(name, full_path))
  311. else:
  312. name_list.append(name)
  313. if made_change:
  314. new_contents = doc.toxml()
  315. f = codecs.open(full_path, 'w')
  316. f.write(new_contents)
  317. f.close()
  318. class Builder(object):
  319. def __init__(self, name, sdk, project_dir, support_dir, app_id, is_emulator):
  320. self.top_dir = project_dir
  321. self.project_tiappxml = os.path.join(self.top_dir,'tiapp.xml')
  322. self.project_dir = os.path.join(project_dir,'build','android')
  323. self.res_dir = os.path.join(self.project_dir,'res')
  324. self.platform_dir = os.path.join(project_dir, 'platform', 'android')
  325. self.project_src_dir = os.path.join(self.project_dir, 'src')
  326. self.project_gen_dir = os.path.join(self.project_dir, 'gen')
  327. self.name = name
  328. self.app_id = app_id
  329. self.support_dir = support_dir
  330. self.compiled_files = []
  331. self.force_rebuild = False
  332. self.debugger_host = None
  333. self.debugger_port = -1
  334. self.profiler_host = None
  335. self.profiler_port = -1
  336. self.fastdev_port = -1
  337. self.fastdev = False
  338. self.compile_js = False
  339. self.tool_api_level = MIN_API_LEVEL
  340. self.abis = list(KNOWN_ABIS)
  341. # don't build if a java keyword in the app id would cause the build to fail
  342. tok = self.app_id.split('.')
  343. for token in tok:
  344. if token in java_keywords:
  345. error("Do not use java keywords for project app id, such as " + token)
  346. sys.exit(1)
  347. tool_api_level_explicit = False
  348. temp_tiapp = TiAppXML(self.project_tiappxml)
  349. if temp_tiapp and temp_tiapp.android:
  350. if 'tool-api-level' in temp_tiapp.android:
  351. self.tool_api_level = int(temp_tiapp.android['tool-api-level'])
  352. tool_api_level_explicit = True
  353. if 'abi' in temp_tiapp.android and temp_tiapp.android['abi'] != 'all':
  354. tiapp_abis = [abi.strip() for abi in temp_tiapp.android['abi'].split(",")]
  355. to_remove = [bad_abi for bad_abi in tiapp_abis if bad_abi not in KNOWN_ABIS]
  356. if to_remove:
  357. warn("The following ABIs listed in the Android <abi> section of tiapp.xml are unknown and will be ignored: %s." % ", ".join(to_remove))
  358. tiapp_abis = [abi for abi in tiapp_abis if abi not in to_remove]
  359. self.abis = tiapp_abis
  360. if not self.abis:
  361. warn("Android <abi> tiapp.xml section does not specify any valid ABIs. Defaulting to '%s'." %
  362. ",".join(KNOWN_ABIS))
  363. self.abis = list(KNOWN_ABIS)
  364. self.sdk = AndroidSDK(sdk, self.tool_api_level)
  365. # If the tool-api-level was not explicitly set in the tiapp.xml, but
  366. # <uses-sdk android:targetSdkVersion> *is* set, try to match the target version.
  367. if (not tool_api_level_explicit and temp_tiapp and temp_tiapp.android_manifest
  368. and "manifest" in temp_tiapp.android_manifest):
  369. self.check_target_api_version(temp_tiapp.android_manifest["manifest"])
  370. self.tiappxml = temp_tiapp
  371. json_contents = open(os.path.join(template_dir,'dependency.json')).read()
  372. self.depends_map = simplejson.loads(json_contents)
  373. # favor the ANDROID_SDK_HOME environment variable if used
  374. if os.environ.has_key('ANDROID_SDK_HOME') and os.path.exists(os.environ['ANDROID_SDK_HOME']):
  375. self.home_dir = os.path.join(os.environ['ANDROID_SDK_HOME'], '.titanium')
  376. self.android_home_dir = os.path.join(os.environ['ANDROID_SDK_HOME'], '.android')
  377. # we place some files in the users home
  378. elif platform.system() == "Windows":
  379. self.home_dir = os.path.join(os.environ['USERPROFILE'], '.titanium')
  380. self.android_home_dir = os.path.join(os.environ['USERPROFILE'], '.android')
  381. else:
  382. self.home_dir = os.path.join(os.path.expanduser('~'), '.titanium')
  383. self.android_home_dir = os.path.join(os.path.expanduser('~'), '.android')
  384. if not os.path.exists(self.home_dir):
  385. os.makedirs(self.home_dir)
  386. self.sdcard = os.path.join(self.home_dir,'android2.sdcard')
  387. self.classname = Android.strip_classname(self.name)
  388. if not is_emulator:
  389. self.set_java_commands()
  390. # start in 1.4, you no longer need the build/android directory
  391. # if missing, we'll create it on the fly
  392. if not os.path.exists(self.project_dir) or not os.path.exists(os.path.join(self.project_dir,'AndroidManifest.xml')):
  393. android_creator = Android(name, app_id, self.sdk, None, self.java)
  394. parent_dir = os.path.dirname(self.top_dir)
  395. if os.path.exists(self.top_dir):
  396. android_creator.create(parent_dir, project_dir=self.top_dir, build_time=True)
  397. else:
  398. android_creator.create(parent_dir)
  399. self.force_rebuild = True
  400. sys.stdout.flush()
  401. def check_target_api_version(self, manifest_elements):
  402. pattern = r'android:targetSdkVersion=\"(\d+)\"'
  403. for el in manifest_elements:
  404. if el.nodeName == "uses-sdk":
  405. xml = el.toxml()
  406. matches = re.findall(pattern, xml)
  407. if matches:
  408. new_level = self.sdk.try_best_match_api_level(int(matches[0]))
  409. if new_level != self.tool_api_level:
  410. self.tool_api_level = new_level
  411. break
  412. def set_java_commands(self):
  413. commands = java.find_java_commands()
  414. to_check = ("java", "javac", "keytool", "jarsigner")
  415. found = True
  416. for check in to_check:
  417. if not commands[check]:
  418. found = False
  419. error("Required Java tool '%s' not located." % check)
  420. if not found:
  421. error("One or more required files not found - please check your JAVA_HOME environment variable")
  422. sys.exit(1)
  423. self.jarsigner = commands["jarsigner"]
  424. self.keytool = commands["keytool"]
  425. self.javac = commands["javac"]
  426. self.java = commands["java"]
  427. if not commands["environ_java_home"] and commands["java_home"]:
  428. os.environ["JAVA_HOME"] = commands["java_home"]
  429. def wait_for_home(self, type):
  430. max_wait = 20
  431. attempts = 0
  432. while True:
  433. processes = self.sdk.list_processes(['-%s' % type])
  434. found_home = False
  435. for process in processes:
  436. if process["name"] == "android.process.acore":
  437. found_home = True
  438. break
  439. if found_home:
  440. break
  441. attempts += 1
  442. if attempts == max_wait:
  443. error("Timed out waiting for android.process.acore")
  444. return False
  445. time.sleep(1)
  446. return True
  447. def wait_for_device(self, type):
  448. debug("Waiting for device to be ready ...")
  449. t = time.time()
  450. max_wait = 30
  451. max_zero = 10
  452. attempts = 0
  453. zero_attempts = 0
  454. timed_out = True
  455. no_devices = False
  456. while True:
  457. devices = self.sdk.list_devices()
  458. trace("adb devices returned %s devices/emulators" % len(devices))
  459. if len(devices) > 0:
  460. found = False
  461. for device in devices:
  462. if type == "e" and device.is_emulator() and not device.is_offline(): found = True
  463. elif type == "d" and device.is_device(): found = True
  464. if found:
  465. timed_out = False
  466. break
  467. else: zero_attempts += 1
  468. try: time.sleep(5) # for some reason KeyboardInterrupts get caught here from time to time
  469. except KeyboardInterrupt: pass
  470. attempts += 1
  471. if attempts == max_wait:
  472. break
  473. elif zero_attempts == max_zero:
  474. no_devices = True
  475. break
  476. if timed_out:
  477. if type == "e":
  478. device = "emulator"
  479. extra_message = "you may need to close the emulator and try again"
  480. else:
  481. device = "device"
  482. extra_message = "you may try reconnecting the USB cable"
  483. error("Timed out waiting for %s to be ready, %s" % (device, extra_message))
  484. if no_devices:
  485. sys.exit(1)
  486. return False
  487. debug("Device connected... (waited %d seconds)" % (attempts*5))
  488. duration = time.time() - t
  489. debug("waited %f seconds on emulator to get ready" % duration)
  490. if duration > 1.0:
  491. info("Waiting for the Android Emulator to become available")
  492. return self.wait_for_home(type)
  493. #time.sleep(20) # give it a little more time to get installed
  494. return True
  495. def create_avd(self, avd_id, avd_skin, avd_abi):
  496. # Sanity check the AVD to see if the ABI is available, or
  497. # necessary.
  498. available_avds = avd.get_avds(self.sdk)
  499. multiple_abis = False
  500. for device in available_avds:
  501. if device['id'] == avd_id:
  502. default_abi = device['abis'][0]
  503. multiple_abis = ( len(device['abis']) != 1 )
  504. if avd_abi is None:
  505. avd_abi = default_abi
  506. elif avd_abi not in device['abis']:
  507. warn("ABI %s not supported for AVD ID %s: Using default ABI %s" % (avd_abi, avd_id, default_abi))
  508. avd_abi = default_abi
  509. break
  510. if multiple_abis:
  511. name = "titanium_%s_%s_%s" % (avd_id, avd_skin, avd_abi)
  512. else:
  513. name = "titanium_%s_%s" % (avd_id, avd_skin)
  514. name = name.replace(' ', '_')
  515. if not os.path.exists(self.home_dir):
  516. os.makedirs(self.home_dir)
  517. avd_path = os.path.join(self.android_home_dir, 'avd')
  518. my_avd = os.path.join(avd_path,"%s.avd" % name)
  519. own_sdcard = os.path.join(self.home_dir, '%s.sdcard' % name)
  520. if not os.path.exists(my_avd) or os.path.exists(own_sdcard):
  521. # starting with 1.7.2, when we create a new avd, give it its own
  522. # SDCard as well.
  523. self.sdcard = own_sdcard
  524. if not os.path.exists(self.sdcard):
  525. info("Creating 64M SD card for use in Android emulator")
  526. run.run([self.sdk.get_mksdcard(), '64M', self.sdcard])
  527. if not os.path.exists(my_avd):
  528. if multiple_abis:
  529. info("Creating new Android Virtual Device (%s %s %s)" % (avd_id,avd_skin,avd_abi))
  530. else:
  531. info("Creating new Android Virtual Device (%s %s)" % (avd_id,avd_skin))
  532. inputgen = os.path.join(template_dir,'input.py')
  533. abi_args = []
  534. if multiple_abis:
  535. abi_args = ['-b', avd_abi]
  536. 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)
  537. inifile = os.path.join(my_avd,'config.ini')
  538. inifilec = open(inifile,'r').read()
  539. inifiledata = open(inifile,'w')
  540. inifiledata.write(inifilec)
  541. # TODO - Document options
  542. for hw_option in android_avd_hw.keys():
  543. inifiledata.write("%s=%s\n" % (hw_option, android_avd_hw[hw_option]))
  544. inifiledata.close()
  545. return name
  546. def run_emulator(self, avd_id, avd_skin, avd_name, avd_abi, add_args):
  547. info("Launching Android emulator...one moment")
  548. debug("From: " + self.sdk.get_emulator())
  549. debug("SDCard: " + self.sdcard)
  550. if avd_name is None:
  551. debug("AVD ID: " + avd_id)
  552. debug("AVD Skin: " + avd_skin)
  553. else:
  554. debug("AVD Name: " + avd_name)
  555. if avd_abi is not None:
  556. debug("AVD ABI: " + avd_abi)
  557. debug("SDK: " + sdk_dir)
  558. # make sure adb is running on windows, else XP can lockup the python
  559. # process when adb runs first time
  560. if platform.system() == "Windows":
  561. run.run([self.sdk.get_adb(), "start-server"], True, ignore_output=True)
  562. devices = self.sdk.list_devices()
  563. for device in devices:
  564. if device.is_emulator() and device.get_port() == 5560:
  565. info("Emulator is running.")
  566. sys.exit()
  567. # this will create an AVD on demand or re-use existing one if already created
  568. if avd_name == None:
  569. avd_name = self.create_avd(avd_id, avd_skin, avd_abi)
  570. # start the emulator
  571. emulator_cmd = [
  572. self.sdk.get_emulator(),
  573. '-avd',
  574. avd_name,
  575. '-port',
  576. '5560',
  577. '-sdcard',
  578. self.get_sdcard_path(),
  579. '-logcat',
  580. '*:d,*,TiAPI:V',
  581. '-no-boot-anim',
  582. '-partition-size',
  583. '128' # in between nexusone and droid
  584. ]
  585. if add_args:
  586. emulator_cmd.extend([arg.strip() for arg in add_args if len(arg.strip()) > 0])
  587. debug(' '.join(emulator_cmd))
  588. p = subprocess.Popen(emulator_cmd)
  589. def handler(signum, frame):
  590. debug("signal caught: %d" % signum)
  591. if not p == None:
  592. debug("calling emulator kill on %d" % p.pid)
  593. if platform.system() == "Windows":
  594. os.system("taskkill /F /T /PID %i" % p.pid)
  595. else:
  596. os.kill(p.pid, signal.SIGTERM)
  597. if platform.system() != "Windows":
  598. signal.signal(signal.SIGHUP, handler)
  599. signal.signal(signal.SIGQUIT, handler)
  600. signal.signal(signal.SIGINT, handler)
  601. signal.signal(signal.SIGABRT, handler)
  602. signal.signal(signal.SIGTERM, handler)
  603. # give it some time to exit prematurely
  604. time.sleep(1)
  605. rc = p.poll()
  606. if rc != None:
  607. handler(3,None)
  608. sys.exit(rc)
  609. # wait for the emulator to finish
  610. try:
  611. rc = p.wait()
  612. except OSError:
  613. handler(3,None)
  614. info("Android Emulator has exited")
  615. sys.exit(rc)
  616. def check_file_exists(self, path):
  617. output = self.run_adb('shell', 'ls', path)
  618. if output != None:
  619. if output.find("No such file or directory") == -1 \
  620. and output.find("error: device offline") == -1:
  621. return True
  622. return False
  623. def is_app_installed(self):
  624. return self.check_file_exists('/data/app/%s*.apk' % self.app_id)
  625. def get_sdcard_path(self):
  626. # We need to surround the sd card path in quotes for windows to account for spaces in path
  627. if platform.system() == "Windows":
  628. return '"' + self.sdcard + '"'
  629. return self.sdcard
  630. def are_resources_installed(self):
  631. return self.check_file_exists(self.sdcard_resources+'/app.js')
  632. def include_path(self, path, isfile):
  633. if not isfile and os.path.basename(path) in ignoreDirs: return False
  634. elif isfile and os.path.basename(path) in ignoreFiles: return False
  635. return True
  636. def warn_dupe_drawable_folders(self):
  637. tocheck = ('high', 'medium', 'low')
  638. image_parent = os.path.join(self.top_dir, 'Resources', 'android', 'images')
  639. for check in tocheck:
  640. if os.path.exists(os.path.join(image_parent, check)) and os.path.exists(os.path.join(image_parent, 'res-%sdpi' % check[0])):
  641. 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]))
  642. def copy_module_platform_folders(self):
  643. for module in self.modules:
  644. platform_folder = os.path.join(module.path, 'platform', 'android')
  645. if os.path.exists(platform_folder):
  646. copy_all(platform_folder, self.project_dir, True, one_time_msg="Copying platform-specific files for '%s' module" % module.manifest.name)
  647. def copy_commonjs_modules(self):
  648. info('Copying CommonJS modules...')
  649. for module in self.modules:
  650. if module.js is None:
  651. continue
  652. module_name = os.path.basename(module.js)
  653. self.non_orphans.append(module_name)
  654. shutil.copy(module.js, self.assets_resources_dir)
  655. def copy_project_platform_folder(self, ignore_dirs=[], ignore_files=[]):
  656. if not os.path.exists(self.platform_dir):
  657. return
  658. copy_all(self.platform_dir, self.project_dir, True, ignore_dirs, ignore_files, one_time_msg="Copying platform-specific files ...")
  659. def copy_resource_drawables(self):
  660. debug('Processing Android resource drawables')
  661. def make_resource_drawable_filename(orig):
  662. normalized = orig.replace(os.sep, "/")
  663. matches = re.search("/android/images/(high|medium|low|res-[^/]+)/(?P<chopped>.*$)", normalized)
  664. if matches and matches.groupdict() and 'chopped' in matches.groupdict():
  665. chopped = matches.groupdict()['chopped'].lower()
  666. for_hash = chopped
  667. if for_hash.endswith('.9.png'):
  668. for_hash = for_hash[:-6] + '.png'
  669. extension = ""
  670. without_extension = chopped
  671. if re.search("\\..*$", chopped):
  672. if chopped.endswith('.9.png'):
  673. extension = '9.png'
  674. without_extension = chopped[:-6]
  675. else:
  676. extension = chopped.split(".")[-1]
  677. without_extension = chopped[:-(len(extension)+1)]
  678. cleaned_without_extension = re.sub(r'[^a-z0-9_]', '_', without_extension)
  679. cleaned_extension = re.sub(r'[^a-z0-9\._]', '_', extension)
  680. result = cleaned_without_extension[:80] + "_" + hashlib.md5(for_hash).hexdigest()[:10]
  681. if extension:
  682. result += "." + extension
  683. return result
  684. else:
  685. trace("Regexp for resource drawable file %s failed" % orig)
  686. return None
  687. def delete_resource_drawable(orig):
  688. folder = resource_drawable_folder(orig)
  689. res_file = os.path.join(self.res_dir, folder, make_resource_drawable_filename(orig))
  690. if os.path.exists(res_file):
  691. try:
  692. trace("DELETING FILE: %s" % res_file)
  693. os.remove(res_file)
  694. except:
  695. warn('Unable to delete %s: %s. Execution will continue.' % (res_file, sys.exc_info()[0]))
  696. def copy_resource_drawable(orig):
  697. partial_folder = resource_drawable_folder(orig)
  698. if not partial_folder:
  699. trace("Could not copy %s; resource folder not determined" % orig)
  700. return
  701. dest_folder = os.path.join(self.res_dir, partial_folder)
  702. dest_filename = make_resource_drawable_filename(orig)
  703. if dest_filename is None:
  704. return
  705. dest = os.path.join(dest_folder, dest_filename)
  706. if not os.path.exists(dest_folder):
  707. os.makedirs(dest_folder)
  708. trace("COPYING FILE: %s => %s" % (orig, dest))
  709. shutil.copy(orig, dest)
  710. fileset = []
  711. if self.force_rebuild or self.deploy_type == 'production' or \
  712. (self.js_changed and not self.fastdev):
  713. for root, dirs, files in os.walk(os.path.join(self.top_dir, "Resources")):
  714. remove_ignored_dirs(dirs)
  715. for f in files:
  716. if f in ignoreFiles:
  717. continue
  718. path = os.path.join(root, f)
  719. if is_resource_drawable(path) and f != 'default.png':
  720. fileset.append(path)
  721. else:
  722. if self.project_deltas:
  723. for delta in self.project_deltas:
  724. path = delta.get_path()
  725. if is_resource_drawable(path):
  726. if delta.get_status() == Delta.DELETED:
  727. delete_resource_drawable(path)
  728. else:
  729. fileset.append(path)
  730. if len(fileset) == 0:
  731. return False
  732. for f in fileset:
  733. copy_resource_drawable(f)
  734. return True
  735. def copy_project_resources(self):
  736. info("Copying project resources..")
  737. def validate_filenames(topdir):
  738. for root, dirs, files in os.walk(topdir):
  739. remove_ignored_dirs(dirs)
  740. for d in dirs:
  741. if d == "iphone" or d == "mobileweb":
  742. dirs.remove(d)
  743. for filename in files:
  744. if filename.startswith("_"):
  745. error("%s is an invalid filename. Android will not package assets whose filenames start with underscores. Fix and rebuild." % os.path.join(root, filename))
  746. sys.exit(1)
  747. resources_dir = os.path.join(self.top_dir, 'Resources')
  748. validate_filenames(resources_dir)
  749. android_resources_dir = os.path.join(resources_dir, 'android')
  750. self.project_deltafy = Deltafy(resources_dir, include_callback=self.include_path)
  751. self.project_deltas = self.project_deltafy.scan()
  752. self.js_changed = False
  753. tiapp_delta = self.project_deltafy.scan_single_file(self.project_tiappxml)
  754. self.tiapp_changed = tiapp_delta is not None
  755. full_copy = not os.path.exists(self.assets_resources_dir)
  756. if self.tiapp_changed or self.force_rebuild or full_copy:
  757. info("Detected change in tiapp.xml, or assets deleted. Forcing full re-build...")
  758. # force a clean scan/copy when the tiapp.xml has changed
  759. self.project_deltafy.clear_state()
  760. self.project_deltas = self.project_deltafy.scan()
  761. # rescan tiapp.xml so it doesn't show up as created next time around
  762. self.project_deltafy.scan_single_file(self.project_tiappxml)
  763. if self.tiapp_changed:
  764. for root, dirs, files in os.walk(self.project_gen_dir, topdown=False):
  765. for name in files:
  766. os.remove(os.path.join(root, name))
  767. for name in dirs:
  768. os.rmdir(os.path.join(root, name))
  769. def strip_slash(s):
  770. if s[0:1]=='/' or s[0:1]=='\\': return s[1:]
  771. return s
  772. def make_relative(path, relative_to, prefix=None):
  773. relative_path = strip_slash(path[len(relative_to):])
  774. if prefix is not None:
  775. return os.path.join(prefix, relative_path)
  776. return relative_path
  777. for delta in self.project_deltas:
  778. path = delta.get_path()
  779. if re.search("android/images/(high|medium|low|res-[^/]+)/", path.replace(os.sep, "/")):
  780. continue # density images are handled later
  781. if delta.get_status() == Delta.DELETED and path.startswith(android_resources_dir):
  782. shared_path = path.replace(android_resources_dir, resources_dir, 1)
  783. if os.path.exists(shared_path):
  784. dest = make_relative(shared_path, resources_dir, self.assets_resources_dir)
  785. trace("COPYING FILE: %s => %s (platform-specific file was removed)" % (shared_path, dest))
  786. shutil.copy(shared_path, dest)
  787. if delta.get_status() != Delta.DELETED:
  788. if path.startswith(android_resources_dir):
  789. dest = make_relative(path, android_resources_dir, self.assets_resources_dir)
  790. else:
  791. # don't copy it if there is an android-specific file
  792. if os.path.exists(path.replace(resources_dir, android_resources_dir, 1)):
  793. continue
  794. dest = make_relative(path, resources_dir, self.assets_resources_dir)
  795. 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")):
  796. continue
  797. parent = os.path.dirname(dest)
  798. if not os.path.exists(parent):
  799. os.makedirs(parent)
  800. trace("COPYING %s FILE: %s => %s" % (delta.get_status_str(), path, dest))
  801. shutil.copy(path, dest)
  802. if (path.startswith(resources_dir) or path.startswith(android_resources_dir)) and path.endswith(".js"):
  803. self.js_changed = True
  804. # copy to the sdcard in development mode
  805. if self.sdcard_copy and self.app_installed and (self.deploy_type == 'development' or self.deploy_type == 'test'):
  806. if path.startswith(android_resources_dir):
  807. relative_path = make_relative(delta.get_path(), android_resources_dir)
  808. else:
  809. relative_path = make_relative(delta.get_path(), resources_dir)
  810. relative_path = relative_path.replace("\\", "/")
  811. self.run_adb('push', delta.get_path(), "%s/%s" % (self.sdcard_resources, relative_path))
  812. if os.environ.has_key('LIVEVIEW'):
  813. debug("LiveView enabled")
  814. appjs = os.path.join(self.assets_resources_dir, 'app.js')
  815. _appjs = os.path.join(self.assets_resources_dir, '_app.js')
  816. liveviewjs = os.path.join(tempfile.gettempdir(), 'liveview.js')
  817. self.non_orphans.append('_app.js')
  818. if not os.path.exists(appjs):
  819. debug('app.js not found: %s' % appjs)
  820. if not os.path.exists(liveviewjs):
  821. debug('liveviewjs.js not found: %s' % liveviewjs)
  822. if os.path.exists(appjs) and os.path.exists(liveviewjs):
  823. trace("COPYING %s => %s" % (appjs, _appjs))
  824. shutil.copy(appjs, _appjs)
  825. trace("COPYING %s => %s" % (liveviewjs, appjs))
  826. shutil.copy(liveviewjs, appjs)
  827. else:
  828. debug('LiveView not enabled')
  829. index_json_path = os.path.join(self.assets_dir, "index.json")
  830. if len(self.project_deltas) > 0 or not os.path.exists(index_json_path):
  831. requireIndex.generateJSON(self.assets_dir, index_json_path)
  832. def check_permissions_mapping(self, key, permissions_mapping, permissions_list):
  833. try:
  834. perms = permissions_mapping[key]
  835. if perms:
  836. for perm in perms:
  837. try:
  838. permissions_list.index(perm)
  839. except:
  840. permissions_list.append(perm)
  841. except:
  842. pass
  843. def generate_android_manifest(self,compiler):
  844. self.generate_localizations()
  845. self.remove_duplicate_res()
  846. # NOTE: these are built-in permissions we need -- we probably need to refine when these are needed too
  847. permissions_required = ['INTERNET','ACCESS_WIFI_STATE','ACCESS_NETWORK_STATE', 'WRITE_EXTERNAL_STORAGE']
  848. GEO_PERMISSION = [ 'ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION']
  849. CONTACTS_READ_PERMISSION = ['READ_CONTACTS']
  850. CONTACTS_PERMISSION = ['READ_CONTACTS', 'WRITE_CONTACTS']
  851. CALENDAR_PERMISSION = ['READ_CALENDAR', 'WRITE_CALENDAR']
  852. VIBRATE_PERMISSION = ['VIBRATE']
  853. CAMERA_PERMISSION = ['CAMERA']
  854. WALLPAPER_PERMISSION = ['SET_WALLPAPER']
  855. # Enable mock location if in development or test mode.
  856. if self.deploy_type == 'development' or self.deploy_type == 'test':
  857. GEO_PERMISSION.append('ACCESS_MOCK_LOCATION')
  858. # this is our module to permission(s) trigger - for each module on the left, require the permission(s) on the right
  859. permissions_module_mapping = {
  860. # GEO
  861. 'geolocation' : GEO_PERMISSION
  862. }
  863. # this is our module method to permission(s) trigger - for each method on the left, require the permission(s) on the right
  864. permissions_method_mapping = {
  865. # MAP
  866. 'Map.createView' : GEO_PERMISSION,
  867. # MEDIA
  868. 'Media.vibrate' : VIBRATE_PERMISSION,
  869. 'Media.showCamera' : CAMERA_PERMISSION,
  870. # CONTACTS
  871. 'Contacts.createPerson' : CONTACTS_PERMISSION,
  872. 'Contacts.removePerson' : CONTACTS_PERMISSION,
  873. 'Contacts.getAllContacts' : CONTACTS_READ_PERMISSION,
  874. 'Contacts.showContactPicker' : CONTACTS_READ_PERMISSION,
  875. 'Contacts.showContacts' : CONTACTS_READ_PERMISSION,
  876. 'Contacts.getPersonByID' : CONTACTS_READ_PERMISSION,
  877. 'Contacts.getPeopleWithName' : CONTACTS_READ_PERMISSION,
  878. 'Contacts.getAllPeople' : CONTACTS_READ_PERMISSION,
  879. 'Contacts.getAllGroups' : CONTACTS_READ_PERMISSION,
  880. 'Contacts.getGroupByID' : CONTACTS_READ_PERMISSION,
  881. # Old CALENDAR
  882. 'Android.Calendar.getAllAlerts' : CALENDAR_PERMISSION,
  883. 'Android.Calendar.getAllCalendars' : CALENDAR_PERMISSION,
  884. 'Android.Calendar.getCalendarById' : CALENDAR_PERMISSION,
  885. 'Android.Calendar.getSelectableCalendars' : CALENDAR_PERMISSION,
  886. # CALENDAR
  887. 'Calendar.getAllAlerts' : CALENDAR_PERMISSION,
  888. 'Calendar.getAllCalendars' : CALENDAR_PERMISSION,
  889. 'Calendar.getCalendarById' : CALENDAR_PERMISSION,
  890. 'Calendar.getSelectableCalendars' : CALENDAR_PERMISSION,
  891. # WALLPAPER
  892. 'Media.Android.setSystemWallpaper' : WALLPAPER_PERMISSION,
  893. }
  894. VIDEO_ACTIVITY = """<activity
  895. android:name="ti.modules.titanium.media.TiVideoActivity"
  896. android:configChanges="keyboardHidden|orientation"
  897. android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
  898. android:launchMode="singleTask"
  899. />"""
  900. MAP_ACTIVITY = """<activity
  901. android:name="ti.modules.titanium.map.TiMapActivity"
  902. android:configChanges="keyboardHidden|orientation"
  903. android:launchMode="singleTask"
  904. />
  905. <uses-library android:name="com.google.android.maps" />"""
  906. CAMERA_ACTIVITY = """<activity
  907. android:name="ti.modules.titanium.media.TiCameraActivity"
  908. android:configChanges="keyboardHidden|orientation"
  909. android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
  910. />"""
  911. activity_mapping = {
  912. # MEDIA
  913. 'Media.createVideoPlayer' : VIDEO_ACTIVITY,
  914. 'Media.showCamera' : CAMERA_ACTIVITY,
  915. # MAPS
  916. 'Map.createView' : MAP_ACTIVITY,
  917. }
  918. # this is a map of our APIs to ones that require Google APIs to be available on the device
  919. google_apis = {
  920. "Map.createView" : True
  921. }
  922. activities = []
  923. # figure out which permissions we need based on the used module
  924. for mod in compiler.modules:
  925. self.check_permissions_mapping(mod, permissions_module_mapping, permissions_required)
  926. # figure out which permissions we need based on the used module methods
  927. for mn in compiler.module_methods:
  928. self.check_permissions_mapping(mn, permissions_method_mapping, permissions_required)
  929. try:
  930. mappings = activity_mapping[mn]
  931. try:
  932. if google_apis[mn] and not self.google_apis_supported:
  933. 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']))
  934. continue
  935. except:
  936. pass
  937. try:
  938. activities.index(mappings)
  939. except:
  940. activities.append(mappings)
  941. except:
  942. pass
  943. # Javascript-based activities defined in tiapp.xml
  944. if self.tiapp and self.tiapp.android and 'activities' in self.tiapp.android:
  945. tiapp_activities = self.tiapp.android['activities']
  946. for key in tiapp_activities:
  947. activity = tiapp_activities[key]
  948. if not 'url' in activity:
  949. continue
  950. activity_name = self.app_id + '.' + activity['classname']
  951. activity_str = '<activity \n\t\t\tandroid:name="%s"' % activity_name
  952. for subkey in activity:
  953. if subkey not in ('nodes', 'name', 'url', 'options', 'classname', 'android:name'):
  954. activity_str += '\n\t\t\t%s="%s"' % (subkey, activity[subkey])
  955. if 'android:config' not in activity:
  956. activity_str += '\n\t\t\tandroid:configChanges="keyboardHidden|orientation"'
  957. if 'nodes' in activity:
  958. activity_str += '>'
  959. for node in activity['nodes']:
  960. activity_str += '\n\t\t\t\t' + node.toxml()
  961. activities.append(activity_str + '\n\t\t</activity>\n')
  962. else:
  963. activities.append(activity_str + '\n\t\t/>\n')
  964. activities = set(activities)
  965. services = []
  966. # Javascript-based services defined in tiapp.xml
  967. if self.tiapp and self.tiapp.android and 'services' in self.tiapp.android:
  968. tiapp_services = self.tiapp.android['services']
  969. for key in tiapp_services:
  970. service = tiapp_services[key]
  971. if not 'url' in service:
  972. continue
  973. service_name = self.app_id + '.' + service['classname']
  974. service_str = '<service \n\t\t\tandroid:name="%s"' % service_name
  975. for subkey in service:
  976. if subkey not in ('nodes', 'service_type', 'type', 'name', 'url', 'options', 'classname', 'android:name'):
  977. service_str += '\n\t\t\t%s="%s"' % (subkey, service[subkey])
  978. if 'nodes' in service:
  979. service_str += '>'
  980. for node in service['nodes']:
  981. service_str += '\n\t\t\t\t' + node.toxml()
  982. services.append(service_str + '\n\t\t</service>\n')
  983. else:
  984. services.append(service_str + '\n\t\t/>\n')
  985. self.use_maps = False
  986. self.res_changed = False
  987. icon_name = self.tiapp.properties['icon']
  988. icon_path = os.path.join(self.assets_resources_dir, icon_name)
  989. icon_ext = os.path.splitext(icon_path)[1]
  990. res_drawable_dest = os.path.join(self.project_dir, 'res', 'drawable')
  991. if not os.path.exists(res_drawable_dest):
  992. os.makedirs(res_drawable_dest)
  993. default_icon = os.path.join(self.support_resources_dir, 'default.png')
  994. dest_icon = os.path.join(res_drawable_dest, 'appicon%s' % icon_ext)
  995. if Deltafy.needs_update(icon_path, dest_icon):
  996. self.res_changed = True
  997. debug("copying app icon: %s" % icon_path)
  998. shutil.copy(icon_path, dest_icon)
  999. elif Deltafy.needs_update(default_icon, dest_icon):
  1000. self.res_changed = True
  1001. debug("copying default app icon")
  1002. shutil.copy(default_icon, dest_icon)
  1003. # make our Titanium theme for our icon
  1004. res_values_dir = os.path.join(self.project_dir, 'res','values')
  1005. if not os.path.exists(res_values_dir):
  1006. os.makedirs(res_values_dir)
  1007. theme_xml = os.path.join(res_values_dir,'theme.xml')
  1008. if not os.path.exists(theme_xml):
  1009. self.res_changed = True
  1010. debug('generating theme.xml')
  1011. theme_file = open(theme_xml, 'w')
  1012. theme_flags = "Theme"
  1013. # We need to treat the default values for fulscreen and
  1014. # navbar-hidden the same as android.py does -- false for both.
  1015. theme_fullscreen = False
  1016. theme_navbarhidden = False
  1017. if (self.tiapp.properties.get("fullscreen") == "true" or
  1018. self.tiapp.properties.get("statusbar-hidden") == "true"):
  1019. theme_fullscreen = True
  1020. elif self.tiapp.properties.get("navbar-hidden") == "true":
  1021. theme_navbarhidden = True
  1022. if theme_fullscreen:
  1023. theme_flags += ".NoTitleBar.Fullscreen"
  1024. elif theme_navbarhidden:
  1025. theme_flags += ".NoTitleBar"
  1026. # Wait, one exception. If you want the notification area (very
  1027. # top of screen) hidden, but want the title bar in the app,
  1028. # there's no theme for that. So we have to use the default theme (no flags)
  1029. # and when the application code starts running, the adjustments are then made.
  1030. # Only do this when the properties are explicitly set, so as to avoid changing
  1031. # old default behavior.
  1032. if theme_flags.endswith('.Fullscreen') and \
  1033. self.tiapp.properties.get("navbar-hidden") == 'false' and \
  1034. ('fullscreen' in self.tiapp.explicit_properties or \
  1035. 'statusbar-hidden' in self.tiapp.explicit_properties) and \
  1036. 'navbar-hidden' in self.tiapp.explicit_properties:
  1037. theme_flags = 'Theme'
  1038. TITANIUM_THEME="""<?xml version="1.0" encoding="utf-8"?>
  1039. <resources>
  1040. <style name="Theme.Titanium" parent="android:%s">
  1041. <item name="android:windowBackground">@drawable/background</item>
  1042. </style>
  1043. </resources>
  1044. """ % theme_flags
  1045. theme_file.write(TITANIUM_THEME)
  1046. theme_file.close()
  1047. # create our background image which acts as splash screen during load
  1048. resources_dir = os.path.join(self.top_dir, 'Resources')
  1049. android_images_dir = os.path.join(resources_dir, 'android', 'images')
  1050. # look for density-specific default.png's first
  1051. if os.path.exists(android_images_dir):
  1052. pattern = r'/android/images/(high|medium|low|res-[^/]+)/default.png'
  1053. for root, dirs, files in os.walk(android_images_dir):
  1054. remove_ignored_dirs(dirs)
  1055. for f in files:
  1056. if f in ignoreFiles:
  1057. continue
  1058. path = os.path.join(root, f)
  1059. if re.search(pattern, path.replace(os.sep, "/")):
  1060. res_folder = resource_drawable_folder(path)
  1061. debug('found %s splash screen at %s' % (res_folder, path))
  1062. dest_path = os.path.join(self.res_dir, res_folder)
  1063. dest_file = os.path.join(dest_path, 'background.png')
  1064. if not os.path.exists(dest_path):
  1065. os.makedirs(dest_path)
  1066. if Deltafy.needs_update(path, dest_file):
  1067. self.res_changed = True
  1068. debug('copying %s splash screen to %s' % (path, dest_file))
  1069. shutil.copy(path, dest_file)
  1070. default_png = os.path.join(self.assets_resources_dir, 'default.png')
  1071. support_default_png = os.path.join(self.support_resources_dir, 'default.png')
  1072. background_png = os.path.join(self.project_dir, 'res','drawable','background.png')
  1073. if os.path.exists(default_png) and Deltafy.needs_update(default_png, background_png):
  1074. self.res_changed = True
  1075. debug("found splash screen at %s" % os.path.abspath(default_png))
  1076. shutil.copy(default_png, background_png)
  1077. elif Deltafy.needs_update(support_default_png, background_png):
  1078. self.res_changed = True
  1079. debug("copying default splash screen")
  1080. shutil.copy(support_default_png, background_png)
  1081. android_manifest = os.path.join(self.project_dir, 'AndroidManifest.xml')
  1082. android_manifest_to_read = android_manifest
  1083. # NOTE: allow the user to use their own custom AndroidManifest if they put a file named
  1084. # AndroidManifest.xml in platform/android, in which case all bets are off
  1085. is_custom = False
  1086. # Catch people who may have it in project root (un-released 1.4.x android_native_refactor branch users)
  1087. if os.path.exists(os.path.join(self.top_dir, 'AndroidManifest.xml')):
  1088. warn('AndroidManifest.xml file in the project root is ignored. Move it to platform/android if you want it to be your custom manifest.')
  1089. android_custom_manifest = os.path.join(self.project_dir, 'AndroidManifest.custom.xml')
  1090. if not os.path.exists(android_custom_manifest):
  1091. android_custom_manifest = os.path.join(self.platform_dir, 'AndroidManifest.xml')
  1092. else:
  1093. 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')
  1094. if os.path.exists(android_custom_manifest):
  1095. android_manifest_to_read = android_custom_manifest
  1096. is_custom = True
  1097. info("Detected custom ApplicationManifest.xml -- no Titanium version migration supported")
  1098. default_manifest_contents = self.android.render_android_manifest()
  1099. if self.sdk.api_level >= HONEYCOMB_MR2_LEVEL:
  1100. # Need to add "screenSize" in our default "configChanges" attribute on
  1101. # <activity> elements, else changes in orientation will cause the app
  1102. # to restart. cf. TIMOB-10863.
  1103. default_manifest_contents = default_manifest_contents.replace('|orientation"', '|orientation|screenSize"')
  1104. debug("Added 'screenSize' to <activity android:configChanges> because targeted api level %s is >= %s" % (self.sdk.api_level, HONEYCOMB_MR2_LEVEL))
  1105. custom_manifest_contents = None
  1106. if is_custom:
  1107. custom_manifest_contents = open(android_manifest_to_read,'r').read()
  1108. manifest_xml = ''
  1109. def get_manifest_xml(tiapp, template_obj=None):
  1110. xml = ''
  1111. if 'manifest' in tiapp.android_manifest:
  1112. for manifest_el in tiapp.android_manifest['manifest']:
  1113. # since we already track permissions in another way, go ahead and us e that
  1114. if manifest_el.nodeName == 'uses-permission' and manifest_el.hasAttribute('android:name'):
  1115. if manifest_el.getAttribute('android:name').split('.')[-1] not in permissions_required:
  1116. perm_val = manifest_el.getAttribute('android:name')
  1117. if template_obj is not None and "${" in perm_val:
  1118. perm_val = render_template_with_tiapp(perm_val, template_obj)
  1119. permissions_required.append(perm_val)
  1120. elif manifest_el.nodeName not in ('supports-screens', 'uses-sdk'):
  1121. this_xml = manifest_el.toprettyxml()
  1122. if template_obj is not None and "${" in this_xml:
  1123. this_xml = render_template_with_tiapp(this_xml, template_obj)
  1124. xml += this_xml
  1125. return xml
  1126. application_xml = ''
  1127. def get_application_xml(tiapp, template_obj=None):
  1128. xml = ''
  1129. if 'application' in tiapp.android_manifest:
  1130. for app_el in tiapp.android_manifest['application']:
  1131. this_xml = app_el.toxml()
  1132. if template_obj is not None and "${" in this_xml:
  1133. this_xml = render_template_with_tiapp(this_xml, template_obj)
  1134. xml += this_xml
  1135. return xml
  1136. # add manifest / application entries from tiapp.xml
  1137. manifest_xml += get_manifest_xml(self.tiapp)
  1138. application_xml += get_application_xml(self.tiapp)
  1139. # add manifest / application entries from modules
  1140. for module in self.modules:
  1141. if module.xml == None: continue
  1142. manifest_xml += get_manifest_xml(module.xml, self.tiapp)
  1143. application_xml += get_application_xml(module.xml, self.tiapp)
  1144. # build the permissions XML based on the permissions detected
  1145. permissions_required = set(permissions_required)
  1146. permissions_required_xml = ""
  1147. for p in permissions_required:
  1148. if '.' not in p:
  1149. permissions_required_xml+="<uses-permission android:name=\"android.permission.%s\"/>\n\t" % p
  1150. else:
  1151. permissions_required_xml+="<uses-permission android:name=\"%s\"/>\n\t" % p
  1152. def fill_manifest(manifest_source):
  1153. ti_activities = '<!-- TI_ACTIVITIES -->'
  1154. ti_permissions = '<!-- TI_PERMISSIONS -->'
  1155. ti_manifest = '<!-- TI_MANIFEST -->'
  1156. ti_application = '<!-- TI_APPLICATION -->'
  1157. ti_services = '<!-- TI_SERVICES -->'
  1158. manifest_source = manifest_source.replace(ti_activities,"\n\n\t\t".join(activities))
  1159. manifest_source = manifest_source.replace(ti_services,"\n\n\t\t".join(services))
  1160. manifest_source = manifest_source.replace(ti_permissions,permissions_required_xml)
  1161. if len(manifest_xml) > 0:
  1162. manifest_source = manifest_source.replace(ti_manifest, manifest_xml)
  1163. if len(application_xml) > 0:
  1164. manifest_source = manifest_source.replace(ti_application, application_xml)
  1165. return manifest_source
  1166. default_manifest_contents = fill_manifest(default_manifest_contents)
  1167. # if a custom uses-sdk or supports-screens has been specified via tiapp.xml
  1168. # <android><manifest>..., we need to replace the ones in the generated
  1169. # default manifest
  1170. supports_screens_node = None
  1171. uses_sdk_node = None
  1172. if 'manifest' in self.tiapp.android_manifest:
  1173. for node in self.tiapp.android_manifest['manifest']:
  1174. if node.nodeName == 'uses-sdk':
  1175. uses_sdk_node = node
  1176. elif node.nodeName == 'supports-screens':
  1177. supports_screens_node = node
  1178. 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):
  1179. dom = parseString(default_manifest_contents)
  1180. def replace_node(olddom, newnode):
  1181. nodes = olddom.getElementsByTagName(newnode.nodeName)
  1182. retval = False
  1183. if nodes:
  1184. olddom.documentElement.replaceChild(newnode, nodes[0])
  1185. retval = True
  1186. return retval
  1187. if supports_screens_node:
  1188. if not replace_node(dom, supports_screens_node):
  1189. dom.documentElement.insertBefore(supports_screens_node, dom.documentElement.firstChild.nextSibling)
  1190. if uses_sdk_node:
  1191. replace_node(dom, uses_sdk_node)
  1192. def set_attrs(element, new_attr_set):
  1193. for k in new_attr_set.keys():
  1194. if element.hasAttribute(k):
  1195. element.removeAttribute(k)
  1196. element.setAttribute(k, new_attr_set.get(k).value)
  1197. if 'manifest-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['manifest-attributes'].length:
  1198. set_attrs(dom.documentElement, self.tiapp.android_manifest['manifest-attributes'])
  1199. if 'application-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['application-attributes'].length:
  1200. set_attrs(dom.getElementsByTagName('application')[0], self.tiapp.android_manifest['application-attributes'])
  1201. default_manifest_contents = dom.toxml()
  1202. if application_xml:
  1203. # If the tiapp.xml <manifest><application> section was not empty, it could be
  1204. # that user put in <activity> entries that duplicate our own,
  1205. # such as if they want a custom theme on TiActivity. So we should delete any dupes.
  1206. dom = parseString(default_manifest_contents)
  1207. package_name = dom.documentElement.getAttribute('package')
  1208. manifest_activities = dom.getElementsByTagName('activity')
  1209. activity_names = []
  1210. nodes_to_delete = []
  1211. for manifest_activity in manifest_activities:
  1212. if manifest_activity.hasAttribute('android:name'):
  1213. activity_name = manifest_activity.getAttribute('android:name')
  1214. if activity_name.startswith('.'):
  1215. activity_name = package_name + activity_name
  1216. if activity_name in activity_names:
  1217. nodes_to_delete.append(manifest_activity)
  1218. else:
  1219. activity_names.append(activity_name)
  1220. if nodes_to_delete:
  1221. for node_to_delete in nodes_to_delete:
  1222. node_to_delete.parentNode.removeChild(node_to_delete)
  1223. default_manifest_contents = dom.toxml()
  1224. if custom_manifest_contents:
  1225. custom_manifest_contents = fill_manifest(custom_manifest_contents)
  1226. new_manifest_contents = None
  1227. android_manifest_gen = android_manifest + '.gen'
  1228. if custom_manifest_contents:
  1229. new_manifest_contents = custom_manifest_contents
  1230. # Write the would-be default as well so user can see
  1231. # some of the auto-gen'd insides of it if they need/want.
  1232. amf = open(android_manifest + '.gen', 'w')
  1233. amf.write(default_manifest_contents)
  1234. amf.close()
  1235. else:
  1236. new_manifest_contents = default_manifest_contents
  1237. if os.path.exists(android_manifest_gen):
  1238. os.remove(android_manifest_gen)
  1239. manifest_changed = False
  1240. old_contents = None
  1241. if os.path.exists(android_manifest):
  1242. old_contents = open(android_manifest, 'r').read()
  1243. if new_manifest_contents != old_contents:
  1244. trace("Writing out AndroidManifest.xml")
  1245. amf = open(android_manifest,'w')
  1246. amf.write(new_manifest_contents)
  1247. amf.close()
  1248. manifest_changed = True
  1249. if self.res_changed or manifest_changed:
  1250. res_dir = os.path.join(self.project_dir, 'res')
  1251. output = run.run([self.aapt, 'package', '-m',
  1252. '-J', self.project_gen_dir,
  1253. '-M', android_manifest,
  1254. '-S', res_dir,
  1255. '-I', self.android_jar], warning_regex=r'skipping')
  1256. r_file = os.path.join(self.project_gen_dir, self.app_id.replace('.', os.sep), 'R.java')
  1257. if not os.path.exists(r_file) or (self.res_changed and output == None):
  1258. error("Error generating R.java from manifest")
  1259. sys.exit(1)
  1260. return manifest_changed
  1261. def generate_stylesheet(self):
  1262. update_stylesheet = False
  1263. resources_dir = os.path.join(self.top_dir, 'Resources')
  1264. project_gen_pkg_dir = os.path.join(self.project_gen_dir, self.app_id.replace('.', os.sep))
  1265. app_stylesheet = os.path.join(project_gen_pkg_dir, 'ApplicationStylesheet.java')
  1266. if not os.path.exists(app_stylesheet):
  1267. update_stylesheet = True
  1268. else:
  1269. for root, dirs, files in os.walk(resources_dir, True, None, True):
  1270. remove_ignored_dirs(dirs)
  1271. for f in files:
  1272. if f in ignoreFiles:
  1273. continue
  1274. if f.endswith(".jss"):
  1275. absolute_path = os.path.join(root, f)
  1276. if Deltafy.needs_update(absolute_path, app_stylesheet):
  1277. update_stylesheet = True
  1278. break
  1279. if not update_stylesheet:
  1280. return
  1281. cssc = csscompiler.CSSCompiler(resources_dir, 'android', self.app_id)
  1282. if not os.path.exists(project_gen_pkg_dir):
  1283. os.makedirs(project_gen_pkg_dir)
  1284. debug("app stylesheet => %s" % app_stylesheet)
  1285. asf = codecs.open(app_stylesheet, 'w', 'utf-8')
  1286. asf.write(cssc.code)
  1287. asf.close()
  1288. def generate_localizations(self):
  1289. # compile localization files
  1290. localecompiler.LocaleCompiler(self.name,self.top_dir,'android',sys.argv[1]).compile()
  1291. # fix un-escaped single-quotes and full-quotes
  1292. # remove duplicate strings since we merge strings.xml from /i18n/ and /platform/android/res/values (TIMOB-12663)
  1293. offending_pattern = '[^\\\\][\'"]'
  1294. for root, dirs, files in os.walk(self.res_dir):
  1295. remove_ignored_dirs(dirs)
  1296. for filename in files:
  1297. if filename in ignoreFiles or not filename.endswith('.xml'):
  1298. continue
  1299. string_name_list = [] #keeps track of the string names
  1300. full_path = os.path.join(root, filename)
  1301. f = codecs.open(full_path, 'r', 'utf-8')
  1302. contents = f.read()
  1303. f.close()
  1304. if not re.search(r"<string ", contents):
  1305. continue
  1306. doc = parseString(contents.encode("utf-8"))
  1307. string_nodes = doc.getElementsByTagName('string')
  1308. resources_node = doc.getElementsByTagName('resources')[0]
  1309. if len(string_nodes) == 0:
  1310. continue
  1311. made_change = False
  1312. for string_node in string_nodes:
  1313. name = string_node.getAttribute('name')
  1314. # Remove the string node with the duplicate names
  1315. if name in string_name_list:
  1316. resources_node.removeChild(string_node)
  1317. made_change = True
  1318. debug('Removed duplicate string [%s] from %s' %(name, full_path))
  1319. else:
  1320. string_name_list.append(name)
  1321. if not string_node.hasChildNodes():
  1322. continue
  1323. string_child = string_node.firstChild
  1324. if string_child.nodeType == string_child.CDATA_SECTION_NODE or string_child.nodeType == string_child.TEXT_NODE:
  1325. string_value = string_child.nodeValue
  1326. if not re.search(offending_pattern, string_value):
  1327. continue
  1328. offenders = re.findall(offending_pattern, string_value)
  1329. if offenders:
  1330. for offender in offenders:
  1331. string_value = string_value.replace(offender, offender[0] + "\\" + offender[-1:])
  1332. made_change = True
  1333. string_child.nodeValue = string_value
  1334. if made_change:
  1335. new_contents = doc.toxml()
  1336. f = codecs.open(full_path, 'w', 'utf-8')
  1337. f.write(new_contents)
  1338. f.close()
  1339. def remove_duplicate_res(self):
  1340. for root, dirs, files in os.walk(self.res_dir):
  1341. remove_ignored_dirs(dirs)
  1342. for filename in files:
  1343. if not (filename in resourceFiles):
  1344. continue
  1345. full_path = os.path.join(root, filename)
  1346. node_names_to_check = ["string", "bool", "color", "dimen", "item", "integer",
  1347. "array", "integer-array", "string-array", "declare-styleable", "attr", "style"]
  1348. # "strings.xml" is checked in generate_localizations()
  1349. if filename != "strings.xml":
  1350. remove_duplicate_nodes_in_res_file(full_path, node_names_to_check)
  1351. def recurse(self, paths, file_glob=None):
  1352. if paths == None: yield None
  1353. if not isinstance(paths, list): paths = [paths]
  1354. for path in paths:
  1355. for root, dirs, files in os.walk(path):
  1356. remove_ignored_dirs(dirs)
  1357. for filename in files:
  1358. if filename in ignoreFiles:
  1359. continue
  1360. if file_glob != None:
  1361. if not fnmatch.fnmatch(filename, file_glob): continue
  1362. yield os.path.join(root, filename)
  1363. def generate_aidl(self):
  1364. # support for android remote interfaces in platform/android/src
  1365. framework_aidl = self.sdk.platform_path('framework.aidl')
  1366. aidl_args = [self.sdk.get_aidl(), '-p' + framework_aidl, '-I' + self.project_src_dir, '-o' + self.project_gen_dir]
  1367. for aidl_file in self.recurse(self.project_src_dir, '*.aidl'):
  1368. run.run(aidl_args + [aidl_file])
  1369. def build_generated_classes(self):
  1370. src_list = []
  1371. self.module_jars = []
  1372. classpath = os.pathsep.join([self.android_jar, os.pathsep.join(self.android_jars)])
  1373. project_module_dir = os.path.join(self.top_dir,'modules','android')
  1374. for module in self.modules:
  1375. if module.jar == None: continue
  1376. self.module_jars.append(module.jar)
  1377. classpath = os.pathsep.join([classpath, module.jar])
  1378. module_lib = module.get_resource('lib')
  1379. for jar in glob.glob(os.path.join(module_lib, '*.jar')):
  1380. self.module_jars.append(jar)
  1381. classpath = os.pathsep.join([classpath, jar])
  1382. if len(self.module_jars) > 0:
  1383. # kroll-apt.jar is needed for modules
  1384. classpath = os.pathsep.join([classpath, self.kroll_apt_jar])
  1385. classpath = os.pathsep.join([classpath, os.path.join(self.support_dir, 'lib', 'titanium-verify.jar')])
  1386. if self.deploy_type != 'production':
  1387. classpath = os.pathsep.join([classpath, os.path.join(self.support_dir, 'lib', 'titanium-debug.jar')])
  1388. classpath = os.pathsep.join([classpath, os.path.join(self.support_dir, 'lib', 'titanium-profiler.jar')])
  1389. for java_file in self.recurse([self.project_src_dir, self.project_gen_dir], '*.java'):
  1390. if self.project_src_dir in java_file:
  1391. relative_path = java_file[len(self.project_src_dir)+1:]
  1392. else:
  1393. relative_path = java_file[len(self.project_gen_dir)+1:]
  1394. class_file = os.path.join(self.classes_dir, relative_path.replace('.java', '.class'))
  1395. if Deltafy.needs_update(java_file, class_file) > 0:
  1396. # the file list file still needs each file escaped apparently
  1397. debug("adding %s to javac build list" % java_file)
  1398. src_list.append('"%s"' % java_file.replace("\\", "\\\\"))
  1399. if len(src_list) == 0:
  1400. # No sources are older than their classfile counterparts, we can skip javac / dex
  1401. return False
  1402. debug("Building Java Sources: " + " ".join(src_list))
  1403. javac_command = [self.javac, '-encoding', 'utf8',
  1404. '-classpath', classpath, '-d', self.classes_dir, '-proc:none',
  1405. '-sourcepath', self.project_src_dir,
  1406. '-sourcepath', self.project_gen_dir, '-target', '1.6', '-source', '1.6']
  1407. (src_list_osfile, src_list_filename) = tempfile.mkstemp()
  1408. src_list_file = os.fdopen(src_list_osfile, 'w')
  1409. src_list_file.write("\n".join(src_list))
  1410. src_list_file.close()
  1411. javac_command.append('@' + src_list_filename)
  1412. (out, err, javac_process) = run.run(javac_command, ignore_error=True, return_error=True, return_process=True)
  1413. os.remove(src_list_filename)
  1414. if javac_process.returncode != 0:
  1415. error("Error(s) compiling generated Java code")
  1416. error(str(err))
  1417. sys.exit(1)
  1418. return True
  1419. def create_unsigned_apk(self, resources_zip_file, webview_js_files=None):
  1420. unsigned_apk = os.path.join(self.project_dir, 'bin', 'app-unsigned.apk')
  1421. self.apk_updated = False
  1422. apk_modified = None
  1423. if os.path.exists(unsigned_apk):
  1424. apk_modified = Deltafy.get_modified_datetime(unsigned_apk)
  1425. debug("creating unsigned apk: " + unsigned_apk)
  1426. # copy existing resources into the APK
  1427. apk_zip = zipfile.ZipFile(unsigned_apk, 'w', zipfile.ZIP_DEFLATED)
  1428. def skip_jar_path(path):
  1429. ext = os.path.splitext(path)[1]
  1430. if path.endswith('/'): return True
  1431. if path.startswith('META-INF/'): return True
  1432. if path.split('/')[-1].startswith('.'): return True
  1433. if ext == '.class': return True
  1434. if 'org/appcelerator/titanium/bindings' in path and ext == '.json': return True
  1435. if 'tiapp' in path and ext =='.xml': return True
  1436. def skip_js_file(path):
  1437. return self.compile_js is True and \
  1438. os.path.splitext(path)[1] == '.js' and \
  1439. os.path.join(self.project_dir, "bin", path) not in webview_js_files
  1440. def compression_type(path):
  1441. ext = os.path.splitext(path)[1]
  1442. if ext in uncompressed_types:
  1443. return zipfile.ZIP_STORED
  1444. return zipfile.ZIP_DEFLATED
  1445. def zipinfo(path):
  1446. info = zipfile.ZipInfo(path)
  1447. info.compress_type = compression_type(path)
  1448. return info
  1449. def is_modified(path):
  1450. return apk_modified is None or Deltafy.needs_update_timestamp(path, apk_modified)
  1451. def zip_contains(zip, entry):
  1452. try:
  1453. zip.getinfo(entry)
  1454. except:
  1455. return False
  1456. return True
  1457. if is_modified(resources_zip_file):
  1458. self.apk_updated = True
  1459. resources_zip = zipfile.ZipFile(resources_zip_file)
  1460. for path in resources_zip.namelist():
  1461. if skip_jar_path(path) or skip_js_file(path): continue
  1462. debug("from resource zip => " + path)
  1463. apk_zip.writestr(zipinfo(path), resources_zip.read(path))
  1464. resources_zip.close()
  1465. # add classes.dex
  1466. if is_modified(self.classes_dex) or not zip_contains(apk_zip, 'classes.dex'):
  1467. apk_zip.write(self.classes_dex, 'classes.dex')
  1468. # add all resource files from the project
  1469. for root, dirs, files in os.walk(self.project_src_dir, True, None, True):
  1470. remove_ignored_dirs(dirs)
  1471. for f in files:
  1472. if f in ignoreFiles:
  1473. continue
  1474. if os.path.splitext(f)[1] != '.java':
  1475. absolute_path = os.path.join(root, f)
  1476. relative_path = os.path.join(root[len(self.project_src_dir)+1:], f)
  1477. if is_modified(absolute_path) or not zip_contains(apk_zip, relative_path):
  1478. self.apk_updated = True
  1479. debug("resource file => " + relative_path)
  1480. apk_zip.write(os.path.join(root, f), relative_path, compression_type(f))
  1481. def add_resource_jar(jar_file):
  1482. jar = zipfile.ZipFile(jar_file)
  1483. for path in jar.namelist():
  1484. if skip_jar_path(path): continue
  1485. debug("from JAR %s => %s" % (jar_file, path))
  1486. apk_zip.writestr(zipinfo(path), jar.read(path))
  1487. jar.close()
  1488. for jar_file in self.module_jars:
  1489. add_resource_jar(jar_file)
  1490. for jar_file in self.android_jars:
  1491. add_resource_jar(jar_file)
  1492. def add_native_libs(libs_dir, exclude=[]):
  1493. if os.path.exists(libs_dir):
  1494. for abi_dir in os.listdir(libs_dir):
  1495. if abi_dir not in self.abis:
  1496. continue
  1497. libs_abi_dir = os.path.join(libs_dir, abi_dir)
  1498. if not os.path.isdir(libs_abi_dir): continue
  1499. for file in os.listdir(libs_abi_dir):
  1500. if file.endswith('.so') and file not in exclude:
  1501. native_lib = os.path.join(libs_abi_dir, file)
  1502. path_in_zip = '/'.join(['lib', abi_dir, file])
  1503. if is_modified(native_lib) or not zip_contains(apk_zip, path_in_zip):
  1504. self.apk_updated = True
  1505. debug("installing native lib: %s" % native_lib)
  1506. apk_zip.write(native_lib, path_in_zip)
  1507. # add module native libraries
  1508. for module in self.modules:
  1509. exclude_libs = []
  1510. add_native_libs(module.get_resource('libs'), exclude_libs)
  1511. # add any native libraries : libs/**/*.so -> lib/**/*.so
  1512. add_native_libs(os.path.join(self.project_dir, 'libs'))
  1513. # add sdk runtime native libraries
  1514. debug("installing native SDK libs")
  1515. sdk_native_libs = os.path.join(template_dir, 'native', 'libs')
  1516. for abi in self.abis:
  1517. lib_source_dir = os.path.join(sdk_native_libs, abi)
  1518. lib_dest_dir = 'lib/%s/' % abi
  1519. # libtiverify is always included
  1520. apk_zip.write(os.path.join(lib_source_dir, 'libtiverify.so'), lib_dest_dir + 'libtiverify.so')
  1521. # profiler
  1522. apk_zip.write(os.path.join(lib_source_dir, 'libtiprofiler.so'), lib_dest_dir + 'libtiprofiler.so')
  1523. for fname in ('libkroll-v8.so', 'libstlport_shared.so', 'libc++_shared.so'):
  1524. if os.path.exists(os.path.join(lib_source_dir, fname))
  1525. apk_zip.write(os.path.join(lib_source_dir, fname), lib_dest_dir + fname)
  1526. self.apk_updated = True
  1527. apk_zip.close()
  1528. return unsigned_apk
  1529. def run_adb(self, *args):
  1530. command = [self.sdk.get_adb()]
  1531. command.extend(self.device_args)
  1532. command.extend(args)
  1533. return run.run(command)
  1534. def get_sigalg(self):
  1535. output = run.run([self.keytool,
  1536. '-v',
  1537. '-list',
  1538. '-keystore', self.keystore,
  1539. '-storepass', self.keystore_pass,
  1540. '-alias', self.keystore_alias
  1541. ], protect_arg_positions=(6,))
  1542. # If the keytool encounters an error, that means some of the provided
  1543. # keychain info is invalid and we should bail anyway
  1544. run.check_output_for_error(output, r'RuntimeException: (.*)', True)
  1545. run.check_output_for_error(output, r'^keytool: (.*)', True)
  1546. match = re.search(r'Signature algorithm name: (.*)', output)
  1547. if match is not None:
  1548. return match.group(1)
  1549. # Return the default:
  1550. return "MD5withRSA"
  1551. def package_and_deploy(self):
  1552. # If in production mode and compiling JS, we do not package the JS
  1553. # files as assets (we protect them from prying eyes). But if a JS
  1554. # file is referenced in an html <script> tag, we DO need to package it.
  1555. def get_js_referenced_in_html():
  1556. js_files = []
  1557. for root, dirs, files in os.walk(self.assets_dir):
  1558. for one_file in files:
  1559. if one_file.lower().endswith(".html"):
  1560. full_path = os.path.join(root, one_file)
  1561. html_source = None
  1562. file_stream = None
  1563. try:
  1564. file_stream = open(full_path, "r")
  1565. html_source = file_stream.read()
  1566. except:
  1567. error("Unable to read html file '%s'" % full_path)
  1568. finally:
  1569. file_stream.close()
  1570. if html_source:
  1571. parser = HTMLParser()
  1572. parser.parse(html_source)
  1573. relative_js_files = parser.get_referenced_js_files()
  1574. if relative_js_files:
  1575. for one_rel_js_file in relative_js_files:
  1576. if one_rel_js_file.startswith("http:") or one_rel_js_file.startswith("https:"):
  1577. continue
  1578. if one_rel_js_file.startswith("app://"):
  1579. one_rel_js_file = one_rel_js_file[6:]
  1580. js_files.append(os.path.abspath(os.path.join(os.path.dirname(full_path), one_rel_js_file)))
  1581. return js_files
  1582. ap_ = os.path.join(self.project_dir, 'bin', 'app.ap_')
  1583. # This is only to check if this has been overridden in production
  1584. has_compile_js = self.tiappxml.has_app_property("ti.android.compilejs")
  1585. compile_js = not has_compile_js or (has_compile_js and \
  1586. self.tiappxml.to_bool(self.tiappxml.get_app_property('ti.android.compilejs')))
  1587. # JS files referenced in html files and thus likely needed for webviews.
  1588. webview_js_files = []
  1589. pkg_assets_dir = self.assets_dir
  1590. if self.deploy_type == "test":
  1591. compile_js = False
  1592. if compile_js and os.environ.has_key('SKIP_JS_MINIFY'):
  1593. compile_js = False
  1594. info("Disabling JavaScript minification")
  1595. if self.deploy_type == "production" and compile_js:
  1596. webview_js_files = get_js_referenced_in_html()
  1597. non_js_assets = os.path.join(self.project_dir, 'bin', 'non-js-assets')
  1598. if not os.path.exists(non_js_assets):
  1599. os.mkdir(non_js_assets)
  1600. copy_all(self.assets_dir, non_js_assets, ignore_exts=['.js'])
  1601. # if we have any js files referenced in html, we *do* need
  1602. # to package them as if they are non-js assets.
  1603. if webview_js_files:
  1604. for one_js_file in webview_js_files:
  1605. if os.path.exists(one_js_file):
  1606. dest_file = one_js_file.replace(self.assets_dir, non_js_assets, 1)
  1607. if not os.path.exists(os.path.dirname(dest_file)):
  1608. os.makedirs(os.path.dirname(dest_file))
  1609. shutil.copyfile(one_js_file, dest_file)
  1610. pkg_assets_dir = non_js_assets
  1611. run.run([self.aapt, 'package', '-f', '-M', 'AndroidManifest.xml', '-A', pkg_assets_dir,
  1612. '-S', 'res', '-I', self.android_jar, '-I', self.titanium_jar, '-F', ap_], warning_regex=r'skipping')
  1613. unsigned_apk = self.create_unsigned_apk(ap_, webview_js_files)
  1614. if self.dist_dir:
  1615. app_apk = os.path.join(self.dist_dir, self.name + '.apk')
  1616. else:
  1617. app_apk = os.path.join(self.project_dir, 'bin', 'app.apk')
  1618. output = run.run([self.jarsigner,
  1619. '-sigalg', self.get_sigalg(),
  1620. '-digestalg', 'SHA1',
  1621. '-storepass', self.keystore_pass,
  1622. '-keystore', self.keystore,
  1623. '-signedjar', app_apk,
  1624. unsigned_apk,
  1625. self.keystore_alias], protect_arg_positions=(6,))
  1626. run.check_output_for_error(output, r'RuntimeException: (.*)', True)
  1627. run.check_output_for_error(output, r'^jarsigner: (.*)', True)
  1628. # TODO Document Exit message
  1629. #success = re.findall(r'RuntimeException: (.*)', output)
  1630. #if len(success) > 0:
  1631. # error(success[0])
  1632. # sys.exit(1)
  1633. # zipalign to align byte boundaries
  1634. zipalign = self.sdk.get_zipalign()
  1635. if os.path.exists(app_apk+'z'):
  1636. os.remove(app_apk+'z')
  1637. ALIGN_32_BIT = 4
  1638. output = run.run([zipalign, '-v', str(ALIGN_32_BIT), app_apk, app_apk+'z'])
  1639. # TODO - Document Exit message
  1640. if output == None:
  1641. error("System Error while compiling Android classes.dex")
  1642. sys.exit(1)
  1643. else:
  1644. os.unlink(app_apk)
  1645. os.rename(app_apk+'z',app_apk)
  1646. if self.dist_dir:
  1647. self.post_build()
  1648. sys.exit()
  1649. if self.build_only:
  1650. return (False, False)
  1651. out = self.run_adb('get-state')
  1652. #out = subprocess.Popen([self.sdk.get_adb(), self.device_type_arg, 'get-state'], stderr=subprocess.PIPE, stdout=subprocess.PIPE).communicate()[0]
  1653. out = str(out).strip()
  1654. # try a few times as sometimes it fails waiting on boot
  1655. attempts = 0
  1656. launched = False
  1657. launch_failed = False
  1658. while attempts < 5:
  1659. try:
  1660. if self.install:
  1661. self.wait_for_device('d')
  1662. info("Installing application on device")
  1663. else:
  1664. self.wait_for_device('e')
  1665. info("Installing application on emulator")
  1666. output = self.run_adb('install', '-r', app_apk)
  1667. #output = run.run(cmd)
  1668. if output == None:
  1669. launch_failed = True
  1670. elif "Failure" in output:
  1671. error("Failed installing %s: %s" % (self.app_id, output))
  1672. launch_failed = True
  1673. elif not self.install:
  1674. launched = True
  1675. break
  1676. except Exception, e:
  1677. error(e)
  1678. time.sleep(3)
  1679. attempts+=1
  1680. return (launched, launch_failed)
  1681. def run_app(self):
  1682. info("Launching application ... %s" % self.name)
  1683. output = self.run_adb('shell', 'am', 'start',
  1684. '-a', 'android.intent.action.MAIN',
  1685. '-c','android.intent.category.LAUNCHER',
  1686. '-n', '%s/.%sActivity' % (self.app_id , self.classname),
  1687. '-f', '0x10200000')
  1688. trace("Launch output: %s" % output)
  1689. def wait_for_sdcard(self):
  1690. # Quick check: the existence of /sdcard/Android,
  1691. # which really should be there on all phones and emulators.
  1692. output = self.run_adb('shell', 'cd /sdcard/Android && echo SDCARD READY')
  1693. if 'SDCARD READY' in output:
  1694. return True
  1695. # Our old way of checking in case the above
  1696. # didn't succeed:
  1697. mount_points_check = ['/sdcard', '/mnt/sdcard']
  1698. # Check the symlink that is typically in root.
  1699. # If you find it, add its target to the mount points to check.
  1700. output = self.run_adb('shell', 'ls', '-l', '/sdcard')
  1701. if output:
  1702. target_pattern = r"\-\> (\S+)\s*$"
  1703. mount_points_check.extend(re.findall(target_pattern, output))
  1704. info("Waiting for SDCard to become available..")
  1705. waited = 0
  1706. max_wait = 60
  1707. while waited < max_wait:
  1708. output = self.run_adb('shell', 'mount')
  1709. if output != None:
  1710. mount_points = output.splitlines()
  1711. for mount_point in mount_points:
  1712. tokens = mount_point.split()
  1713. if len(tokens) < 2: continue
  1714. mount_path = tokens[1]
  1715. if mount_path in mount_points_check:
  1716. return True
  1717. else:
  1718. error("Error checking for SDCard using 'mount'")
  1719. return False
  1720. time.sleep(1)
  1721. waited += 1
  1722. error("Timed out waiting for SDCard to become available (%ds)" % max_wait)
  1723. return False
  1724. def push_deploy_json(self):
  1725. deploy_data = {
  1726. "debuggerEnabled": self.debugger_host != None,
  1727. "debuggerPort": self.debugger_port,
  1728. "profilerEnabled": self.profiler_host != None,
  1729. "profilerPort": self.profiler_port,
  1730. "fastdevPort": self.fastdev_port
  1731. }
  1732. deploy_json = os.path.join(self.project_dir, 'bin', 'deploy.json')
  1733. open(deploy_json, 'w+').write(simplejson.dumps(deploy_data))
  1734. sdcard_available = self.wait_for_sdcard()
  1735. if sdcard_available:
  1736. self.run_adb('shell', 'mkdir /sdcard/%s || echo' % self.app_id)
  1737. self.run_adb('push', deploy_json, '/sdcard/%s/deploy.json' % self.app_id)
  1738. os.unlink(deploy_json)
  1739. def verify_fastdev(self):
  1740. lock_file = os.path.join(self.top_dir, '.fastdev.lock')
  1741. if not fastdev.is_running(self.top_dir):
  1742. if os.path.exists(lock_file):
  1743. os.unlink(lock_file)
  1744. return False
  1745. else:
  1746. data = simplejson.loads(open(lock_file, 'r').read())
  1747. self.fastdev_port = data["port"]
  1748. return True
  1749. def fastdev_kill_app(self):
  1750. lock_file = os.path.join(self.top_dir, ".fastdev.lock")
  1751. if os.path.exists(lock_file):
  1752. class Options(object): pass
  1753. options = Options()
  1754. options.lock_file = lock_file
  1755. try:
  1756. return fastdev.kill_app(self.top_dir, options)
  1757. except Exception, e:
  1758. return False
  1759. def merge_internal_module_resources(self):
  1760. if not self.android_jars:
  1761. return
  1762. for jar in self.android_jars:
  1763. if not os.path.exists(jar):
  1764. continue
  1765. res_zip = jar[:-4] + '.res.zip'
  1766. if not os.path.exists(res_zip):
  1767. continue
  1768. res_zip_file = zipfile.ZipFile(res_zip, "r")
  1769. try:
  1770. zip_extractall(res_zip_file, self.project_dir)
  1771. except:
  1772. raise
  1773. finally:
  1774. res_zip_file.close()
  1775. 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):
  1776. deploy_type = 'development'
  1777. self.build_only = build_only
  1778. self.device_args = device_args
  1779. self.postbuild_modules = []
  1780. self.finalize_modules = []
  1781. self.non_orphans = []
  1782. if install:
  1783. if self.device_args == None:
  1784. self.device_args = ['-d']
  1785. if keystore == None:
  1786. deploy_type = 'test'
  1787. else:
  1788. deploy_type = 'production'
  1789. if self.device_args == None:
  1790. self.device_args = ['-e']
  1791. self.deploy_type = deploy_type
  1792. (java_failed, java_status) = prereq.check_java()
  1793. if java_failed:
  1794. error(java_status)
  1795. sys.exit(1)
  1796. # attempt to load any compiler plugins
  1797. if len(self.tiappxml.properties['plugins']) > 0:
  1798. titanium_dir = os.path.abspath(os.path.join(template_dir,'..','..','..','..'))
  1799. local_compiler_dir = os.path.abspath(os.path.join(self.top_dir,'plugins'))
  1800. tp_compiler_dir = os.path.abspath(os.path.join(titanium_dir,'plugins'))
  1801. if not os.path.exists(tp_compiler_dir) and not os.path.exists(local_compiler_dir):
  1802. error("Build Failed (Missing plugins directory)")
  1803. sys.exit(1)
  1804. compiler_config = {
  1805. 'platform':'android',
  1806. 'tiapp':self.tiappxml,
  1807. 'project_dir':self.top_dir,
  1808. 'titanium_dir':titanium_dir,
  1809. 'appid':self.app_id,
  1810. 'template_dir':template_dir,
  1811. 'project_name':self.name,
  1812. 'command':self.command,
  1813. 'build_dir':self.project_dir,
  1814. 'app_name':self.name,
  1815. 'android_builder':self,
  1816. 'deploy_type':deploy_type,
  1817. 'dist_dir':dist_dir,
  1818. 'logger':log
  1819. }
  1820. for plugin in self.tiappxml.properties['plugins']:
  1821. local_plugin_file = os.path.join(local_compiler_dir,plugin['name'],'plugin.py')
  1822. plugin_file = os.path.join(tp_compiler_dir,plugin['name'],plugin['version'],'plugin.py')
  1823. info("plugin=%s" % plugin_file)
  1824. if not os.path.exists(local_plugin_file) and not os.path.exists(plugin_file):
  1825. error("Build Failed (Missing plugin for %s)" % plugin['name'])
  1826. sys.exit(1)
  1827. info("Detected compiler plugin: %s/%s" % (plugin['name'],plugin['version']))
  1828. code_path = plugin_file
  1829. if os.path.exists(local_plugin_file):
  1830. code_path = local_plugin_file
  1831. compiler_config['plugin']=plugin
  1832. fin = open(code_path, 'rb')
  1833. m = hashlib.md5()
  1834. m.update(open(code_path,'rb').read())
  1835. code_hash = m.hexdigest()
  1836. p = imp.load_source(code_hash, code_path, fin)
  1837. module_functions = dict(inspect.getmembers(p, inspect.isfunction))
  1838. if module_functions.has_key('postbuild'):
  1839. debug("plugin contains a postbuild function. Will execute after project is built and packaged")
  1840. self.postbuild_modules.append((plugin['name'], p))
  1841. if module_functions.has_key('finalize'):
  1842. debug("plugin contains a finalize function. Will execute before script exits")
  1843. self.finalize_modules.append((plugin['name'], p))
  1844. p.compile(compiler_config)
  1845. fin.close()
  1846. # in Windows, if the adb server isn't running, calling "adb devices"
  1847. # will fork off a new adb server, and cause a lock-up when we
  1848. # try to pipe the process' stdout/stderr. the workaround is
  1849. # to simply call adb start-server here, and not care about
  1850. # the return code / pipes. (this is harmless if adb is already running)
  1851. # -- thanks to Bill Dawson for the workaround
  1852. if platform.system() == "Windows" and not build_only:
  1853. run.run([self.sdk.get_adb(), "start-server"], True, ignore_output=True)
  1854. ti_version_file = os.path.join(self.support_dir, '..', 'version.txt')
  1855. if os.path.exists(ti_version_file):
  1856. ti_version_info = read_properties(open(ti_version_file, 'r'), '=')
  1857. if not ti_version_info is None and 'version' in ti_version_info:
  1858. ti_version_string = 'Titanium SDK version: %s' % ti_version_info['version']
  1859. if 'timestamp' in ti_version_info or 'githash' in ti_version_info:
  1860. ti_version_string += ' ('
  1861. if 'timestamp' in ti_version_info:
  1862. ti_version_string += '%s' % ti_version_info['timestamp']
  1863. if 'githash' in ti_version_info:
  1864. ti_version_string += ' %s' % ti_version_info['githash']
  1865. ti_version_string += ')'
  1866. info(ti_version_string)
  1867. if not build_only:
  1868. if deploy_type == 'development':
  1869. self.wait_for_device('e')
  1870. elif deploy_type == 'test':
  1871. self.wait_for_device('d')
  1872. self.install = install
  1873. self.dist_dir = dist_dir
  1874. self.aapt = self.sdk.get_aapt()
  1875. self.android_jar = self.sdk.get_android_jar()
  1876. self.titanium_jar = os.path.join(self.support_dir,'titanium.jar')
  1877. self.kroll_apt_jar = os.path.join(self.support_dir, 'kroll-apt.jar')
  1878. dx = self.sdk.get_dx()
  1879. self.apkbuilder = self.sdk.get_apkbuilder()
  1880. self.sdcard_resources = '/sdcard/Ti.debug/%s/Resources' % self.app_id
  1881. self.resources_installed = False
  1882. if deploy_type == "production":
  1883. self.app_installed = False
  1884. else:
  1885. self.app_installed = not build_only and self.is_app_installed()
  1886. debug("%s installed? %s" % (self.app_id, self.app_installed))
  1887. #self.resources_installed = not build_only and self.are_resources_installed()
  1888. #debug("%s resources installed? %s" % (self.app_id, self.resources_installed))
  1889. if keystore == None:
  1890. keystore = os.path.join(self.support_dir,'dev_keystore')
  1891. self.keystore = keystore
  1892. self.keystore_pass = keystore_pass
  1893. self.keystore_alias = keystore_alias
  1894. curdir = os.getcwd()
  1895. self.support_resources_dir = os.path.join(self.support_dir, 'resources')
  1896. try:
  1897. os.chdir(self.project_dir)
  1898. self.android = Android(self.name, self.app_id, self.sdk, deploy_type, self.java)
  1899. if not os.path.exists('bin'):
  1900. os.makedirs('bin')
  1901. resources_dir = os.path.join(self.top_dir,'Resources')
  1902. self.assets_dir = os.path.join(self.project_dir,'bin','assets')
  1903. self.assets_resources_dir = os.path.join(self.assets_dir,'Resources')
  1904. if not os.path.exists(self.assets_resources_dir):
  1905. os.makedirs(self.assets_resources_dir)
  1906. shutil.copy(self.project_tiappxml, self.assets_dir)
  1907. finalxml = os.path.join(self.assets_dir,'tiapp.xml')
  1908. self.tiapp = TiAppXML(finalxml)
  1909. self.tiapp.setDeployType(deploy_type)
  1910. self.sdcard_copy = False
  1911. sdcard_property = "ti.android.loadfromsdcard"
  1912. if self.tiapp.has_app_property(sdcard_property):
  1913. self.sdcard_copy = self.tiapp.to_bool(self.tiapp.get_app_property(sdcard_property))
  1914. fastdev_property = "ti.android.fastdev"
  1915. fastdev_enabled = (self.deploy_type == 'development' and not self.build_only)
  1916. if self.tiapp.has_app_property(fastdev_property) and self.deploy_type == 'development':
  1917. fastdev_enabled = self.tiapp.to_bool(self.tiapp.get_app_property(fastdev_property))
  1918. if fastdev_enabled:
  1919. if self.verify_fastdev():
  1920. info("Fastdev server running, deploying in Fastdev mode")
  1921. self.fastdev = True
  1922. else:
  1923. warn("Fastdev enabled, but server isn't running, deploying normally")
  1924. self.classes_dir = os.path.join(self.project_dir, 'bin', 'classes')
  1925. if not os.path.exists(self.classes_dir):
  1926. os.makedirs(self.classes_dir)
  1927. if (not debugger_host is None) and len(debugger_host) > 0:
  1928. hostport = debugger_host.split(":")
  1929. self.debugger_host = hostport[0]
  1930. self.debugger_port = int(hostport[1])
  1931. debugger_enabled = self.debugger_host != None and len(self.debugger_host) > 0
  1932. if (not profiler_host is None) and len(profiler_host) > 0:
  1933. hostport = profiler_host.split(":")
  1934. self.profiler_host = hostport[0]
  1935. self.profiler_port = int(hostport[1])
  1936. profiler_enabled = self.profiler_host != None and len(self.profiler_host) > 0
  1937. # Detect which modules are being used.
  1938. # We need to know this info in a few places, so the info is saved
  1939. # in self.missing_modules and self.modules
  1940. detector = ModuleDetector(self.top_dir)
  1941. self.missing_modules, self.modules = detector.find_app_modules(self.tiapp, 'android', deploy_type)
  1942. self.copy_commonjs_modules()
  1943. self.copy_project_resources()
  1944. last_build_info = None
  1945. built_all_modules = False
  1946. build_info_path = os.path.join(self.project_dir, 'bin', 'build_info.json')
  1947. if os.path.exists(build_info_path):
  1948. last_build_info = simplejson.loads(open(build_info_path, 'r').read())
  1949. built_all_modules = last_build_info["include_all_modules"]
  1950. if self.tiapp.has_app_property("ti.android.compilejs"):
  1951. if self.tiapp.to_bool(self.tiapp.get_app_property('ti.android.compilejs')):
  1952. self.compile_js = True
  1953. elif self.tiapp.has_app_property('ti.deploytype'):
  1954. if self.tiapp.get_app_property('ti.deploytype') == 'production':
  1955. self.compile_js = True
  1956. if self.compile_js and os.environ.has_key('SKIP_JS_MINIFY'):
  1957. self.compile_js = False
  1958. info("Disabling JavaScript minification")
  1959. include_all_ti_modules = self.fastdev
  1960. if (self.tiapp.has_app_property('ti.android.include_all_modules')):
  1961. if self.tiapp.to_bool(self.tiapp.get_app_property('ti.android.include_all_modules')):
  1962. include_all_ti_modules = True
  1963. if self.tiapp_changed or (self.js_changed and not self.fastdev) or \
  1964. self.force_rebuild or self.deploy_type == "production" or \
  1965. (self.fastdev and not built_all_modules) or \
  1966. (not self.fastdev and built_all_modules):
  1967. self.android.config['compile_js'] = self.compile_js
  1968. trace("Generating Java Classes")
  1969. self.android.create(os.path.abspath(os.path.join(self.top_dir,'..')),
  1970. True, project_dir = self.top_dir, include_all_ti_modules=include_all_ti_modules)
  1971. open(build_info_path, 'w').write(simplejson.dumps({
  1972. "include_all_modules": include_all_ti_modules
  1973. }))
  1974. else:
  1975. info("Tiapp.xml unchanged, skipping class generation")
  1976. # compile resources
  1977. full_resource_dir = os.path.join(self.project_dir, self.assets_resources_dir)
  1978. compiler = Compiler(self.tiapp,
  1979. full_resource_dir,
  1980. self.java,
  1981. self.classes_dir,
  1982. self.project_gen_dir,
  1983. self.project_dir,
  1984. include_all_modules=include_all_ti_modules)
  1985. compiler.compile(compile_bytecode=self.compile_js, external_modules=self.modules)
  1986. self.compiled_files = compiler.compiled_files
  1987. self.android_jars = compiler.jar_libraries
  1988. self.merge_internal_module_resources()
  1989. if not os.path.exists(self.assets_dir):
  1990. os.makedirs(self.assets_dir)
  1991. self.resource_drawables_changed = self.copy_resource_drawables()
  1992. self.warn_dupe_drawable_folders()
  1993. self.copy_module_platform_folders()
  1994. special_resources_dir = os.path.join(self.top_dir,'platform','android')
  1995. if os.path.exists(special_resources_dir):
  1996. debug("found special platform files dir = %s" % special_resources_dir)
  1997. ignore_files = ignoreFiles
  1998. ignore_files.extend(['AndroidManifest.xml']) # don't want to overwrite build/android/AndroidManifest.xml yet
  1999. self.copy_project_platform_folder(ignoreDirs, ignore_files)
  2000. self.generate_stylesheet()
  2001. self.generate_aidl()
  2002. self.manifest_changed = self.generate_android_manifest(compiler)
  2003. my_avd = None
  2004. self.google_apis_supported = False
  2005. # find the AVD we've selected and determine if we support Google APIs
  2006. if avd_id is not None:
  2007. for avd_props in avd.get_avds(self.sdk):
  2008. if avd_props['id'] == avd_id:
  2009. my_avd = avd_props
  2010. self.google_apis_supported = (my_avd['name'].find('Google')!=-1 or my_avd['name'].find('APIs')!=-1)
  2011. break
  2012. if build_only or avd_id is None:
  2013. self.google_apis_supported = True
  2014. remove_orphaned_files(resources_dir, self.assets_resources_dir, self.non_orphans)
  2015. generated_classes_built = self.build_generated_classes()
  2016. # TODO: enable for "test" / device mode for debugger / fastdev
  2017. if not self.build_only and (self.deploy_type == "development" or self.deploy_type == "test"):
  2018. self.push_deploy_json()
  2019. self.classes_dex = os.path.join(self.project_dir, 'bin', 'classes.dex')
  2020. def jar_includer(path, isfile):
  2021. if isfile and path.endswith(".jar"): return True
  2022. return False
  2023. support_deltafy = Deltafy(self.support_dir, jar_includer)
  2024. self.support_deltas = support_deltafy.scan()
  2025. dex_built = False
  2026. if len(self.support_deltas) > 0 or generated_classes_built or self.deploy_type == "production":
  2027. # the dx.bat that ships with android in windows doesn't allow command line
  2028. # overriding of the java heap space, so we call the jar directly
  2029. if platform.system() == 'Windows':
  2030. dex_args = [self.java, '-Xmx1024M', '-Djava.ext.dirs=%s' % self.sdk.get_platform_tools_dir(), '-jar', self.sdk.get_dx_jar()]
  2031. else:
  2032. dex_args = [dx, '-JXmx1536M', '-JXX:-UseGCOverheadLimit']
  2033. # Look for New Relic module
  2034. newrelic_module = None
  2035. for module in self.modules:
  2036. if module.path.find("newrelic") > 0:
  2037. newrelic_module = module
  2038. break
  2039. # If New Relic is present, add its Java agent to the dex arguments.
  2040. if newrelic_module:
  2041. info("Adding New Relic support.")
  2042. # Copy the dexer java agent jar to a tempfile. Eliminates white space from
  2043. # the module path which causes problems with the dex -Jjavaagent argument.
  2044. temp_jar = tempfile.NamedTemporaryFile(suffix='.jar', delete=True)
  2045. shutil.copyfile(os.path.join(newrelic_module.path, 'class.rewriter.jar'), temp_jar.name)
  2046. dex_args += ['-Jjavaagent:' + os.path.join(temp_jar.name)]
  2047. dex_args += ['--dex', '--output='+self.classes_dex, self.classes_dir]
  2048. dex_args += self.android_jars
  2049. dex_args += self.module_jars
  2050. dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-verify.jar'))
  2051. if self.deploy_type != 'production':
  2052. dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-debug.jar'))
  2053. dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-profiler.jar'))
  2054. # the verifier depends on Ti.Network classes, so we may need to inject it
  2055. has_network_jar = False
  2056. for jar in self.android_jars:
  2057. if jar.endswith('titanium-network.jar'):
  2058. has_network_jar = True
  2059. break
  2060. if not has_network_jar:
  2061. dex_args.append(os.path.join(self.support_dir, 'modules', 'titanium-network.jar'))
  2062. info("Compiling Android Resources... This could take some time")
  2063. # TODO - Document Exit message
  2064. run_result = run.run(dex_args, warning_regex=r'warning: ')
  2065. if (run_result == None):
  2066. dex_built = False
  2067. error("System Error while compiling Android classes.dex")
  2068. sys.exit(1)
  2069. else:
  2070. dex_built = True
  2071. debug("Android classes.dex built")
  2072. if dex_built or generated_classes_built or self.tiapp_changed or self.manifest_changed or not self.app_installed or not self.fastdev:
  2073. # metadata has changed, we need to do a full re-deploy
  2074. launched, launch_failed = self.package_and_deploy()
  2075. if launched:
  2076. self.run_app()
  2077. info("Deployed %s ... Application should be running." % self.name)
  2078. elif launch_failed==False and not build_only:
  2079. info("Application installed. Launch from drawer on Home Screen")
  2080. elif not build_only:
  2081. # Relaunch app if nothing was built
  2082. info("Re-launching application ... %s" % self.name)
  2083. relaunched = False
  2084. killed = False
  2085. if self.fastdev:
  2086. killed = self.fastdev_kill_app()
  2087. if not killed:
  2088. processes = self.run_adb('shell', 'ps')
  2089. for line in processes.splitlines():
  2090. columns = line.split()
  2091. if len(columns) > 1:
  2092. pid = columns[1]
  2093. id = columns[len(columns)-1]
  2094. if id == self.app_id:
  2095. self.run_adb('shell', 'kill', pid)
  2096. relaunched = True
  2097. self.run_app()
  2098. if relaunched:
  2099. info("Relaunched %s ... Application should be running." % self.name)
  2100. self.post_build()
  2101. # Enable port forwarding for debugger if application
  2102. # acts as the server.
  2103. if debugger_enabled:
  2104. info('Forwarding host port %s to device for debugging.' % self.debugger_port)
  2105. forwardPort = 'tcp:%s' % self.debugger_port
  2106. self.sdk.run_adb(['forward', forwardPort, forwardPort])
  2107. # Enable port forwarding for profiler
  2108. if profiler_enabled:
  2109. info('Forwarding host port %s to device for profiling.' % self.profiler_port)
  2110. forwardPort = 'tcp:%s' % self.profiler_port
  2111. self.sdk.run_adb(['forward', forwardPort, forwardPort])
  2112. #intermediary code for on-device debugging (later)
  2113. #if debugger_host != None:
  2114. #import debugger
  2115. #debug("connecting to debugger: %s, debugger=%s" % (debugger_host, str(debugger)))
  2116. #debugger.run(debugger_host, '127.0.0.1:5999')
  2117. finally:
  2118. os.chdir(curdir)
  2119. sys.stdout.flush()
  2120. def post_build(self):
  2121. try:
  2122. if self.postbuild_modules:
  2123. for p in self.postbuild_modules:
  2124. info("Running postbuild function in %s plugin" % p[0])
  2125. p[1].postbuild()
  2126. except Exception,e:
  2127. error("Error performing post-build steps: %s" % e)
  2128. def finalize(self):
  2129. try:
  2130. if self.finalize_modules:
  2131. for p in self.finalize_modules:
  2132. info("Running finalize function in %s plugin" % p[0])
  2133. p[1].finalize()
  2134. except Exception,e:
  2135. error("Error performing finalize steps: %s" % e)
  2136. if __name__ == "__main__":
  2137. def usage():
  2138. 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])
  2139. print
  2140. print "available commands: "
  2141. print
  2142. print " emulator build and run the emulator"
  2143. print " simulator build and run the app on the simulator"
  2144. print " install build and install the app on the device"
  2145. print " distribute build final distribution package for upload to marketplace"
  2146. print " run build and run the project using values from tiapp.xml"
  2147. print " run-emulator run the emulator with a default AVD ID and skin"
  2148. sys.exit(1)
  2149. argc = len(sys.argv)
  2150. if argc < 2:
  2151. usage()
  2152. command = sys.argv[1]
  2153. if command == 'logcat':
  2154. launch_logcat()
  2155. template_dir = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
  2156. get_values_from_tiapp = False
  2157. is_emulator = False
  2158. if command == 'run':
  2159. if argc < 4:
  2160. print 'Usage: %s run <project_dir> <android_sdk>' % sys.argv[0]
  2161. sys.exit(1)
  2162. get_values_from_tiapp = True
  2163. project_dir = sys.argv[2]
  2164. sdk_dir = sys.argv[3]
  2165. avd_id = "7"
  2166. elif command == 'run-emulator':
  2167. if argc < 4:
  2168. print 'Usage: %s run-emulator <project_dir> <android_sdk>' % sys.argv[0]
  2169. sys.exit(1)
  2170. get_values_from_tiapp = True
  2171. project_dir = sys.argv[2]
  2172. sdk_dir = sys.argv[3]
  2173. # sensible defaults?
  2174. avd_id = "7"
  2175. avd_skin = "HVGA"
  2176. else:
  2177. if command == 'emulator':
  2178. is_emulator = True
  2179. if argc < 6 or command == '--help' or (command=='distribute' and argc < 10):
  2180. usage()
  2181. if get_values_from_tiapp:
  2182. tiappxml = TiAppXML(os.path.join(project_dir, 'tiapp.xml'))
  2183. app_id = tiappxml.properties['id']
  2184. project_name = tiappxml.properties['name']
  2185. else:
  2186. project_name = dequote(sys.argv[2])
  2187. sdk_dir = os.path.abspath(os.path.expanduser(dequote(sys.argv[3])))
  2188. project_dir = os.path.abspath(os.path.expanduser(dequote(sys.argv[4])))
  2189. app_id = dequote(sys.argv[5])
  2190. log = TiLogger(os.path.join(os.path.abspath(os.path.expanduser(dequote(project_dir))), 'build.log'))
  2191. log.debug(" ".join(sys.argv))
  2192. builder = Builder(project_name,sdk_dir,project_dir,template_dir,app_id,is_emulator)
  2193. builder.command = command
  2194. try:
  2195. if command == 'run-emulator':
  2196. builder.run_emulator(avd_id, avd_skin, None, None, [])
  2197. elif command == 'run':
  2198. builder.build_and_run(False, avd_id)
  2199. elif command == 'emulator':
  2200. avd_id = dequote(sys.argv[6])
  2201. add_args = None
  2202. avd_abi = None
  2203. avd_skin = None
  2204. avd_name = None
  2205. if avd_id.isdigit():
  2206. avd_name = None
  2207. avd_skin = dequote(sys.argv[7])
  2208. if argc > 8:
  2209. # The first of the remaining args
  2210. # could either be an abi or an additional argument for
  2211. # the emulator. Compare to known abis.
  2212. next_index = 8
  2213. test_arg = sys.argv[next_index]
  2214. if test_arg in KNOWN_ABIS:
  2215. avd_abi = test_arg
  2216. next_index += 1
  2217. # Whatever remains (if anything) is an additional
  2218. # argument to pass to the emulator.
  2219. if argc > next_index:
  2220. add_args = sys.argv[next_index:]
  2221. else:
  2222. avd_name = sys.argv[6]
  2223. # If the avd is known by name, then the skin and abi shouldn't be passed,
  2224. # because the avd already has the skin and abi "in it".
  2225. avd_id = None
  2226. avd_skin = None
  2227. avd_abi = None
  2228. if argc > 7:
  2229. add_args = sys.argv[7:]
  2230. builder.run_emulator(avd_id, avd_skin, avd_name, avd_abi, add_args)
  2231. elif command == 'simulator':
  2232. info("Building %s for Android ... one moment" % project_name)
  2233. avd_id = dequote(sys.argv[6])
  2234. debugger_host = None
  2235. profiler_host = None
  2236. if len(sys.argv) > 9 and sys.argv[9] == 'profiler':
  2237. profiler_host = dequote(sys.argv[8])
  2238. elif len(sys.argv) > 8:
  2239. debugger_host = dequote(sys.argv[8])
  2240. builder.build_and_run(False, avd_id, debugger_host=debugger_host, profiler_host=profiler_host)
  2241. elif command == 'install':
  2242. avd_id = dequote(sys.argv[6])
  2243. device_args = ['-d']
  2244. # We have to be careful here because Windows can't handle an empty argument
  2245. # on the command line, so if a device serial number is not passed in, but
  2246. # a debugger_host (the argument after device serial number) _is_ passed in,
  2247. # to Windows it just looks like a serial number is passed in (the debugger_host
  2248. # argument shifts left to take over the empty argument.)
  2249. debugger_host = None
  2250. profiler_host = None
  2251. if len(sys.argv) >= 10 and sys.argv[9] == 'profiler':
  2252. profiler_host = dequote(sys.argv[8])
  2253. if len(sys.argv[7]) > 0:
  2254. device_args = ['-s', sys.argv[7]]
  2255. elif len(sys.argv) >= 9 and len(sys.argv[8]) > 0:
  2256. debugger_host = dequote(sys.argv[8])
  2257. if len(sys.argv[7]) > 0:
  2258. device_args = ['-s', sys.argv[7]]
  2259. elif len(sys.argv) >= 8 and len(sys.argv[7]) > 0:
  2260. arg7 = dequote(sys.argv[7])
  2261. if 'adb:' in arg7:
  2262. debugger_host = arg7
  2263. else:
  2264. device_args = ['-s', arg7]
  2265. builder.build_and_run(True, avd_id, device_args=device_args, debugger_host=debugger_host, profiler_host=profiler_host)
  2266. elif command == 'distribute':
  2267. key = os.path.abspath(os.path.expanduser(dequote(sys.argv[6])))
  2268. password = dequote(sys.argv[7])
  2269. alias = dequote(sys.argv[8])
  2270. output_dir = dequote(sys.argv[9])
  2271. builder.build_and_run(True, None, key, password, alias, output_dir)
  2272. elif command == 'build':
  2273. builder.build_and_run(False, 1, build_only=True)
  2274. else:
  2275. error("Unknown command: %s" % command)
  2276. usage()
  2277. except SystemExit, n:
  2278. sys.exit(n)
  2279. except:
  2280. e = traceback.format_exc()
  2281. error("Exception occured while building Android project:")
  2282. for line in e.splitlines():
  2283. error(line)
  2284. sys.exit(1)
  2285. finally:
  2286. # Don't run plugin finalizer functions if all we were doing is
  2287. # starting up the emulator.
  2288. if builder and command not in ("emulator", "run-emulator"):
  2289. builder.finalize()