PageRenderTime 64ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 1ms

/mobileweb/templates/module/default/template/mobileweb/build.py.ejs

http://github.com/appcelerator/titanium_mobile
Python | 462 lines | 440 code | 18 blank | 4 comment | 25 complexity | 67b9b431921be4eb63bef19d1fd1c3ca 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. #
  3. # Appcelerator Titanium Module Packager
  4. #
  5. version = '<%- tisdkVersion %>'
  6. sdk_path = r'<%- tisdkPath %>'
  7. import os, sys, time, datetime, string, math, zipfile, codecs, re, shutil, subprocess, base64
  8. from datetime import date
  9. from xml.dom.minidom import parseString
  10. sys.path.append(os.path.join(sdk_path, "common"))
  11. import simplejson
  12. try:
  13. import markdown2 as markdown
  14. except ImportError:
  15. import markdown
  16. cwd = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
  17. os.chdir(cwd)
  18. ignoreFiles = ['.DS_Store','.cvsignore','.gitignore']
  19. ignoreDirs = ['.svn','_svn','.git','CVS','CVSROOT']
  20. required_manifest_keys = ['name','version','moduleid','description','copyright','license','copyright','platform','minsdk']
  21. manifest_defaults = {
  22. 'description':'My module',
  23. 'author': 'Your Name',
  24. 'license' : 'Specify your license',
  25. 'copyright' : 'Copyright (c) %s by Your Company' % str(date.today().year),
  26. }
  27. module_license_default = "TODO: place your license here and we'll include it in the module distribution"
  28. def getText(nodelist):
  29. rc = ''
  30. for node in nodelist:
  31. if node.nodeType == node.TEXT_NODE:
  32. rc = rc + node.data
  33. rc = rc.strip()
  34. if rc.lower() in ['true', 'yes', '1']:
  35. rc = 'true'
  36. elif rc in ['false', 'no', '0']:
  37. rc = 'false'
  38. return rc
  39. class Compiler(object):
  40. def __init__(self, deploytype):
  41. start_time = time.time()
  42. if not os.path.exists(sdk_path):
  43. print '[ERROR] Unable to find SDK path "%s"' % sdk_path
  44. sys.exit(1)
  45. print '[INFO] Titanium Mobile Web Module Compiler v%s' % version
  46. self.deploytype = deploytype
  47. self.module_path = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
  48. self.src_path = os.path.join(self.module_path, 'src')
  49. self.build_path = os.path.join(self.module_path, 'build')
  50. self.load_manifest()
  51. self.check_license()
  52. self.load_timodule_xml()
  53. self.check_main()
  54. self.modules_map = {}
  55. self.require_cache = {}
  56. self.parse_module(self.main, None)
  57. self.modules_to_cache = []
  58. for module in self.require_cache:
  59. if module != self.main and os.path.exists(os.path.join(self.build_path, module + '.js')):
  60. self.modules_to_cache.append(module)
  61. if 'precache' in self.timodule and 'requires' in self.timodule['precache'] and len(self.timodule['precache']['requires']):
  62. for req in self.timodule['precache']['requires']:
  63. self.modules_to_cache.append('commonjs:' + req)
  64. self.precache_images = []
  65. if 'precache' in self.timodule and 'images' in self.timodule['precache'] and len(self.timodule['precache']['images']):
  66. for img in self.timodule['precache']['images']:
  67. self.precache_images.append(img)
  68. if os.path.exists(self.build_path):
  69. shutil.rmtree(self.build_path, True)
  70. try:
  71. os.makedirs(self.build_path)
  72. except:
  73. pass
  74. self.copy(self.src_path, self.build_path)
  75. self.build_js()
  76. self.minify_js()
  77. self.package()
  78. total_time = round(time.time() - start_time)
  79. total_minutes = math.floor(total_time / 60)
  80. total_seconds = total_time % 60
  81. if total_minutes > 0:
  82. print '[INFO] Finished in %s minutes %s seconds' % (int(total_minutes), int(total_seconds))
  83. else:
  84. print '[INFO] Finished in %s seconds' % int(total_time)
  85. def load_manifest(self):
  86. self.manifest = {}
  87. manifest_file = os.path.join(self.module_path, 'manifest')
  88. if not os.path.exists(manifest_file):
  89. print '[ERROR] Unable to find manifest file'
  90. sys.exit(1)
  91. for line in open(manifest_file).readlines():
  92. line = line.strip()
  93. if line[0:1] == '#': continue
  94. if line.find(':') < 0: continue
  95. key,value = line.split(':')
  96. self.manifest[key.strip()] = value.strip()
  97. for key in required_manifest_keys:
  98. if not self.manifest.has_key(key):
  99. print '[ERROR] Missing required manifest key "%s"' % key
  100. sys.exit(1)
  101. if manifest_defaults.has_key(key):
  102. defvalue = manifest_defaults[key]
  103. curvalue = self.manifest[key]
  104. if curvalue == defvalue:
  105. print '[WARN] Please update the manifest key: "%s" to a non-default value' % key
  106. def check_license(self):
  107. license_file = os.path.join(cwd,'LICENSE')
  108. if not os.path.exists(license_file):
  109. license_file = os.path.join(cwd,'..','LICENSE')
  110. if os.path.exists(license_file):
  111. c = open(license_file).read()
  112. if c.find(module_license_default) != -1:
  113. print '[WARN] Please update the LICENSE file with your license text before distributing'
  114. def load_timodule_xml(self):
  115. global_settings = {}
  116. mobileweb_settings = {}
  117. timodule_file = os.path.join(self.module_path, 'timodule.xml')
  118. if not os.path.exists(timodule_file):
  119. print '[ERROR] Unable to find timodule.xml file'
  120. sys.exit(1)
  121. dom = parseString(codecs.open(timodule_file,'r','utf-8','replace').read().encode('utf-8'))
  122. root = dom.documentElement
  123. for node in root.childNodes:
  124. if node.nodeType == 1 and node.nodeName not in ['android', 'iphone']:
  125. if node.nodeName == 'mobileweb':
  126. for subnode in node.childNodes:
  127. if subnode.nodeType == 1:
  128. self.get_xml_children(mobileweb_settings[subnode.nodeName], subnode.childNodes)
  129. else:
  130. self.get_xml_children(global_settings[node.nodeName], node.childNodes)
  131. self.timodule = dict(global_settings.items() + mobileweb_settings.items())
  132. def check_main(self):
  133. self.main = self.timodule['main'] if 'main' in self.timodule else self.manifest['moduleid']
  134. if not os.path.exists(os.path.join(self.src_path, self.main + '.js')):
  135. print '[ERROR] Unable to find main module "%s"' % self.main
  136. sys.exit(1)
  137. def get_xml_children(self, dest, nodes):
  138. if len(nodes) > 1:
  139. dest = {}
  140. for child in nodes.childNodes:
  141. if child.nodeType == 1:
  142. self.get_xml_children(dest[child.nodeName], child.childNodes)
  143. else:
  144. dest = getText(child.childNodes)
  145. def compact_path(self, path):
  146. result = []
  147. path = path.replace('\\', '/').split('/');
  148. while len(path):
  149. segment = path[0]
  150. path = path[1:]
  151. if segment == '..' and len(result) and lastSegment != '..':
  152. result.pop()
  153. lastSegment = result[-1]
  154. elif segment != '.':
  155. lastSegment = segment
  156. result.append(segment)
  157. return '/'.join(result);
  158. def resolve(self, it, ref):
  159. parts = it.split('!')
  160. it = parts[-1]
  161. if it.startswith('url:'):
  162. it = it[4:]
  163. if it.startswith('/'):
  164. it = '.' + it
  165. parts = it.split('/')
  166. return [self.build_path, it]
  167. if it.find(':') != -1:
  168. return []
  169. if it.startswith('/') or (len(parts) == 1 and it.endswith('.js')):
  170. return [self.build_path, it]
  171. if it.startswith('.') and ref is not None:
  172. it = self.compact_path(ref + it)
  173. parts = it.split('/')
  174. return [self.build_path, it]
  175. def parse_module(self, module, ref):
  176. if module in self.require_cache or module == 'require':
  177. return
  178. parts = module.split('!')
  179. if len(parts) == 1:
  180. if module.startswith('.') and ref is not None:
  181. module = self.compact_path(ref + module)
  182. self.require_cache[module] = 1
  183. dep = self.resolve(module, ref)
  184. if not len(dep):
  185. return
  186. if len(parts) > 1:
  187. self.require_cache['url:' + parts[1]] = 1
  188. filename = dep[1]
  189. if not filename.endswith('.js'):
  190. filename += '.js'
  191. source = os.path.join(dep[0], filename)
  192. if not os.path.exists(source):
  193. return
  194. source = codecs.open(source, 'r', 'utf-8').read()
  195. pattern = re.compile('define\(\s*([\'\"][^\'\"]*[\'\"]\s*)?,?\s*(\[[^\]]+\])\s*?,?\s*(function|\{)')
  196. results = pattern.search(source)
  197. if results is None:
  198. self.modules_map[module] = []
  199. else:
  200. groups = results.groups()
  201. if groups is not None and len(groups):
  202. if groups[1] is None:
  203. self.modules_map[module] = []
  204. else:
  205. deps = self.parse_deps(groups[1])
  206. for i in range(0, len(deps)):
  207. dep = deps[i]
  208. parts = dep.split('!')
  209. ref = module.split('/')
  210. ref.pop()
  211. ref = '/'.join(ref) + '/'
  212. if dep.startswith('.'):
  213. deps[i] = self.compact_path(ref + dep)
  214. if len(parts) == 1:
  215. if dep.startswith('./'):
  216. parts = module.split('/')
  217. parts.pop()
  218. parts.append(dep)
  219. self.parse_module(self.compact_path('/'.join(parts)), ref)
  220. else:
  221. self.parse_module(dep, ref)
  222. else:
  223. self.modules_map[dep] = parts[0]
  224. self.parse_module(parts[0], module)
  225. if parts[0] == 'Ti/_/text':
  226. if dep.startswith('./'):
  227. parts = module.split('/')
  228. parts.pop()
  229. parts.append(dep)
  230. self.parse_module(self.compact_path('/'.join(parts)), ref)
  231. else:
  232. self.parse_module(dep, ref)
  233. self.modules_map[module] = deps
  234. def parse_deps(self, deps):
  235. found = []
  236. if len(deps) > 2:
  237. deps = deps[1:-1]
  238. deps = deps.split(',')
  239. for dep in deps:
  240. dep = dep.strip().split(' ')[0].strip()
  241. if dep.startswith('\'') or dep.startswith('"'):
  242. found.append(simplejson.loads(dep))
  243. return found
  244. def copy(self, src_path, dest_path):
  245. print '[INFO] Copying %s...' % src_path
  246. for root, dirs, files in os.walk(src_path):
  247. for name in ignoreDirs:
  248. if name in dirs:
  249. dirs.remove(name)
  250. for file in files:
  251. if file in ignoreFiles or file.startswith('._'):
  252. continue
  253. source = os.path.join(root, file)
  254. dest = os.path.expanduser(source.replace(src_path, dest_path, 1))
  255. dest_dir = os.path.expanduser(os.path.split(dest)[0])
  256. if not os.path.exists(dest_dir):
  257. os.makedirs(dest_dir)
  258. shutil.copy(source, dest)
  259. def build_js(self):
  260. main_file = os.path.join(self.build_path, self.main + '.js')
  261. tmp = main_file + '.tmp'
  262. js = codecs.open(tmp, 'w', encoding='utf-8')
  263. if len(self.modules_to_cache) > 0 or len(self.precache_images) > 0:
  264. js.write('require.cache({\n')
  265. first = True
  266. for x in self.modules_to_cache:
  267. if x == self.main:
  268. continue
  269. is_cjs = False
  270. if x.startswith('commonjs:'):
  271. is_cjs = True
  272. x = x[9:]
  273. dep = self.resolve(x, None)
  274. if not len(dep):
  275. continue
  276. if not first:
  277. js.write(',\n')
  278. first = False
  279. filename = dep[1]
  280. if not filename.endswith('.js'):
  281. filename += '.js'
  282. file_path = os.path.join(dep[0], filename)
  283. if x.startswith('url:'):
  284. source = file_path + '.uncompressed.js'
  285. if self.minify:
  286. os.rename(file_path, source)
  287. print '[INFO] Minifying include %s' % file_path
  288. p = subprocess.Popen('java -Xms256m -Xmx256m -jar "%s" --compilation_level SIMPLE_OPTIMIZATIONS --js "%s" --js_output_file "%s"' % (os.path.join(sdk_path, 'mobileweb', 'closureCompiler', 'compiler.jar'), source, file_path), shell=True, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
  289. stdout, stderr = p.communicate()
  290. if p.returncode != 0:
  291. print '[ERROR] Failed to minify "%s"' % file_path
  292. for line in stderr.split('\n'):
  293. if len(line):
  294. print '[ERROR] %s' % line
  295. print '[WARN] Leaving %s un-minified' % file_path
  296. os.remove(file_path)
  297. shutil.copy(source, file_path)
  298. js.write('"%s":"%s"' % (x, codecs.open(file_path, 'r', 'utf-8').read().strip().replace('\\', '\\\\').replace('\n', '\\n\\\n').replace('\"', '\\\"')))
  299. elif is_cjs:
  300. js.write('"%s":function(){\n/* %s */\ndefine(function(require, exports, module){\n%s\n});\n}' % (x, file_path.replace(self.build_path, ''), codecs.open(file_path, 'r', 'utf-8').read()))
  301. else:
  302. js.write('"%s":function(){\n/* %s */\n\n%s\n}' % (x, file_path.replace(self.build_path, ''), codecs.open(file_path, 'r', 'utf-8').read()))
  303. image_mime_types = {
  304. '.png': 'image/png',
  305. '.gif': 'image/gif',
  306. '.jpg': 'image/jpg',
  307. '.jpeg': 'image/jpg'
  308. }
  309. for x in self.precache_images:
  310. x = x.replace('\\', '/')
  311. y = x
  312. if y.startswith(os.sep):
  313. y = '.' + y
  314. img = os.path.join(self.module_path, os.sep.join(y.split('/')))
  315. if os.path.exists(img):
  316. fname, ext = os.path.splitext(img.lower())
  317. if ext in image_mime_types:
  318. if not first:
  319. js.write(',\n')
  320. first = False
  321. js.write('"url:%s":"data:%s;base64,%s"' % (x, image_mime_types[ext], base64.b64encode(open(img,'rb').read())))
  322. js.write('});\n')
  323. js.write(codecs.open(main_file, 'r', 'utf-8').read())
  324. js.close()
  325. os.remove(main_file)
  326. os.rename(tmp, main_file)
  327. def minify_js(self):
  328. subprocess.call('java -Xms256m -Xmx256m -cp "%s%s%s" -Djava.awt.headless=true minify "%s"' % (
  329. os.path.join(sdk_path, 'mobileweb', 'minify'),
  330. os.pathsep,
  331. os.path.join(sdk_path, 'mobileweb', 'closureCompiler', 'compiler.jar'),
  332. self.build_path
  333. ), shell=True)
  334. def generate_doc(self):
  335. docdir = os.path.join(self.module_path, 'documentation')
  336. if not os.path.exists(docdir):
  337. docdir = os.path.join(self.module_path, '..', 'documentation')
  338. if not os.path.exists(docdir):
  339. print '[WARN] Couldn\'t find documentation file at: %s' % docdir
  340. return None
  341. documentation = []
  342. for file in os.listdir(docdir):
  343. if file in ignoreFiles or file.startswith('._') or os.path.isdir(os.path.join(docdir, file)):
  344. continue
  345. md = open(os.path.join(docdir, file)).read()
  346. html = markdown.markdown(md)
  347. documentation.append({file:html});
  348. return documentation
  349. def zip_dir(self, zf, dir, basepath):
  350. for root, dirs, files in os.walk(dir):
  351. for name in ignoreDirs:
  352. if name in dirs:
  353. dirs.remove(name)
  354. for file in files:
  355. if file in ignoreFiles or file.startswith('._') or file.endswith('.uncompressed.js'):
  356. continue
  357. e = os.path.splitext(file)
  358. if len(e) == 2 and e[1] == '.pyc':
  359. continue
  360. from_ = os.path.join(root, file)
  361. to_ = from_.replace(dir, basepath, 1)
  362. zf.write(from_, to_)
  363. def package(self):
  364. name = self.manifest['name'].lower()
  365. moduleid = self.manifest['moduleid'].lower()
  366. version = self.manifest['version']
  367. install_path = 'modules/mobileweb/%s/%s' % (moduleid, version)
  368. zip_file = os.path.join(self.module_path, '%s-mobileweb-%s.zip' % (moduleid,version))
  369. if os.path.exists(zip_file):
  370. os.remove(zip_file)
  371. zf = zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED)
  372. zf.write(os.path.join(self.module_path, 'manifest'), '%s/manifest' % install_path)
  373. license_file = os.path.join(self.module_path, 'LICENSE')
  374. if not os.path.exists(license_file):
  375. license_file = os.path.join(self.module_path, '..', 'LICENSE')
  376. if os.path.exists(license_file):
  377. zf.write(license_file,'%s/LICENSE' % install_path)
  378. zf.writestr('%s/package.json' % install_path, simplejson.dumps({
  379. 'name': self.manifest['name'],
  380. 'description': self.manifest['description'],
  381. 'version': self.manifest['version'],
  382. 'directories': {
  383. 'lib': './src'
  384. },
  385. 'main': self.main
  386. }, indent=4, sort_keys=True))
  387. self.zip_dir(zf, 'build', '%s/src' % install_path)
  388. self.zip_dir(zf, 'example', '%s/example' % install_path)
  389. docs = self.generate_doc()
  390. if docs != None:
  391. for doc in docs:
  392. for file, html in doc.iteritems():
  393. filename = string.replace(file, '.md', '.html')
  394. zf.writestr('%s/documentation/%s' % (install_path, filename), html)
  395. zf.close()
  396. if __name__ == '__main__':
  397. if len(sys.argv) > 1 and sys.argv[1].lower() in ['help', '--help', '-h']:
  398. print 'Usage: %s [<deploytype>]' % os.path.basename(sys.argv[0])
  399. sys.exit(1)
  400. Compiler('production' if len(sys.argv) <= 1 else sys.argv[1].lower())
  401. sys.exit(0)