build.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. #!/usr/bin/python
  2. from browser.linkjs import link
  3. import argparse
  4. import os
  5. import shutil
  6. import stat
  7. import subprocess
  8. import sys
  9. import tempfile
  10. import zipfile
  11. root = os.path.dirname(os.path.abspath(__file__))
  12. src_dir = os.path.join(root, 'src')
  13. default_out_dir = os.path.join(root, '_out')
  14. default_out_js = os.path.join(default_out_dir, 'js')
  15. class Package( object ):
  16. bin_dir = os.path.join(root, 'bin')
  17. js_root = os.path.join(bin_dir, 'js')
  18. archive = os.path.join(bin_dir, 'compiled.zip')
  19. @staticmethod
  20. def pack():
  21. print('packing current js "%s" -> "%s"...' % (Package.js_root, Package.archive))
  22. file = zipfile.ZipFile(Package.archive, 'w')
  23. for f in os.listdir(Package.js_root):
  24. file.write(os.path.join(Package.js_root, f), f)
  25. @staticmethod
  26. def unpacked_bin():
  27. if not os.path.exists(Package.js_root):
  28. print('unpacking pre-compiled js "%s" -> "%s"...'
  29. % (Package.archive, Package.js_root))
  30. file = zipfile.ZipFile(Package.archive, 'r')
  31. file.extractall(Package.js_root)
  32. return Package.bin_dir
  33. def _remove_readonly(fn, path, excinfo):
  34. os.chmod(path, stat.S_IWRITE)
  35. fn(path)
  36. def norm_path(path):
  37. return os.path.normcase(os.path.normpath(os.path.realpath(os.path.abspath(path))))
  38. def is_parent_for(parent, child):
  39. parent = norm_path(parent)
  40. child = norm_path(child)
  41. while True:
  42. if parent == child:
  43. return True
  44. next = os.path.dirname(child)
  45. if next == child:
  46. return False
  47. child = next
  48. def cleanup(dir):
  49. if not os.path.exists(dir):
  50. return
  51. this_dir = os.path.dirname(__file__)
  52. if is_parent_for(dir, this_dir):
  53. raise Exception("cannot delete itself: %s" % this_dir)
  54. shutil.rmtree(dir, onerror=_remove_readonly)
  55. def copy(src, dst_dir):
  56. dst = os.path.join(dst_dir, os.path.basename(src))
  57. if os.path.exists(dst):
  58. os.chmod(dst, stat.S_IWRITE)
  59. print('%s -> %s' % (src, dst))
  60. shutil.copy(src, dst)
  61. def copytree(src, dst):
  62. cleanup(dst)
  63. print('%s -> %s' % (src, dst))
  64. shutil.copytree(src, dst)
  65. def run(cmd, env=None, cwd=None, print_output=False):
  66. p = subprocess.Popen(
  67. cmd,
  68. stdout=None if print_output else subprocess.PIPE,
  69. stderr=subprocess.STDOUT,
  70. #shell = True,
  71. env=env,
  72. cwd=cwd)
  73. result = None if print_output else p.stdout.read().decode()
  74. rc = p.wait()
  75. if rc:
  76. if result:
  77. print(result)
  78. print('"%s" failed with exit code %d' % (' '.join(cmd), rc))
  79. exit(rc)
  80. return result
  81. snapshot_root = os.path.join(root, 'snapshot')
  82. def make_js_search_dirs(bin):
  83. return [os.path.join(root, 'test'), src_dir, bin]
  84. def run_node(args, js_search_dirs, cwd=None):
  85. node_exe, path_separator = ('node.exe', ';') if os.name == 'nt' else ('node', ':')
  86. node_env = dict(list(os.environ.items())
  87. + [('NODE_PATH', path_separator.join(js_search_dirs))])
  88. run([node_exe] + args, node_env, cwd, print_output=True)
  89. def run_tests(bin, unit_test=None, code_test=None):
  90. print('run tests using "%s" ->' % bin)
  91. js_search_dirs = make_js_search_dirs(bin)
  92. if unit_test is None and code_test is None:
  93. unit_test = '*'
  94. code_test = '*'
  95. if unit_test:
  96. unit_tests = os.path.join(root, 'test', 'test_unit_run.js')
  97. args = [unit_tests]
  98. if unit_test != '*':
  99. args += [unit_test]
  100. run_node(args, js_search_dirs)
  101. if code_test:
  102. compile_tests = os.path.join(root, 'test', 'test_compile.js')
  103. args = [compile_tests]
  104. if code_test != '*':
  105. args += [code_test]
  106. run_node(args, js_search_dirs, cwd=os.path.join(root, 'test'))
  107. print('<-tests')
  108. def _run_compiler(bin_dir, sources, locale, out_dir, timing=False):
  109. if not os.path.exists(out_dir):
  110. os.makedirs(out_dir)
  111. include = ['src/ob', 'src/eberon', 'src/oberon']
  112. include += [os.path.join(i, locale) for i in include]
  113. args = [os.path.join(src_dir, 'oc_nodejs.js'),
  114. '--include=' + ';'.join([os.path.join(root, i) for i in include]),
  115. '--out-dir=%s' % out_dir,
  116. '--import-dir=js'
  117. ]
  118. if timing:
  119. args.append('--timing=true')
  120. args += sources
  121. run_node(args, [bin_dir, src_dir], cwd=root)
  122. all_oberon_sources = [
  123. 'ContextAssignment.ob',
  124. 'EberonSymbols.ob',
  125. 'EberonContextCase.ob', 'EberonContextExpression.ob',
  126. 'EberonContextIdentdef.ob', 'EberonContextIf.ob',
  127. 'EberonContextInPlace.ob', 'EberonContextProcedure',
  128. 'EberonContextType.ob', 'EberonContextVar.ob', 'EberonLanguageContext.ob',
  129. 'OberonContext.ob', 'OberonContextType.ob', 'OberonContextVar.ob',
  130. 'OberonSymbols.ob', 'Lexer.ob', 'Module.ob']
  131. def build_html(options):
  132. version = None
  133. if options.set_version:
  134. version = run(['git', 'log', '-1', '--format="%ci%n%H"'])
  135. out = options.out
  136. build_version = None
  137. build_version_path = os.path.join(out, 'version.txt')
  138. try:
  139. with open(build_version_path) as f:
  140. build_version = f.read()
  141. except:
  142. pass
  143. if (not build_version is None) and build_version == version:
  144. print("current html is up to date, do nothing")
  145. return
  146. if not os.path.exists(out):
  147. os.mkdir(out)
  148. link(['oc.js', 'oberon/oberon_grammar.js', 'eberon/eberon_grammar.js', 'test_unit.js'],
  149. os.path.join(out, 'oc.js'),
  150. ['src', Package.unpacked_bin(), 'test'],
  151. version)
  152. copy('browser/oberonjs.html', out)
  153. for d in ['codemirror', 'jslibs']:
  154. copytree(os.path.join('browser', d), os.path.join(out, d))
  155. if version is None:
  156. if os.path.exists(build_version_path):
  157. os.remove(build_version_path)
  158. else:
  159. with open(build_version_path, 'w') as f:
  160. f.write(version)
  161. def _recompile_with_replace(locale, skip_tests=False):
  162. bin_dir = Package.unpacked_bin()
  163. new_bin = tempfile.mkdtemp(dir=bin_dir)
  164. try:
  165. new_js = os.path.join(new_bin, 'js')
  166. print('recompile all oberon sources to "%s"...' % new_js)
  167. _run_compiler(bin_dir, all_oberon_sources, locale, new_js)
  168. if not skip_tests:
  169. run_tests(new_bin)
  170. print('replacing: "%s" -> "%s"...' % (new_js, Package.js_root))
  171. cleanup(Package.js_root)
  172. os.rename(new_js, Package.js_root)
  173. finally:
  174. shutil.rmtree(new_bin)
  175. print('OK!')
  176. def pre_commit_check(options):
  177. _recompile_with_replace(options.locale)
  178. Package.pack()
  179. class compile_target(object):
  180. name = 'compile'
  181. description = 'compile oberon source file'
  182. @staticmethod
  183. def setup_options(parser):
  184. parser.add_argument(
  185. 'files', nargs='+', metavar='FILE',
  186. help='oberon source file(s) to compile')
  187. parser.add_argument(
  188. '--out',
  189. help='output directory, default: "%(default)s"',
  190. default=default_out_js)
  191. def __init__(self, options):
  192. _run_compiler(
  193. Package.unpacked_bin(), options.files, options.locale, options.out)
  194. class recompile_target(object):
  195. name = 'recompile'
  196. description = 'recompile all oberon source files'
  197. @staticmethod
  198. def setup_options(parser):
  199. parser.add_argument(
  200. '--out',
  201. help='output directory, default: "%(default)s"',
  202. default=default_out_js)
  203. def __init__(self, options):
  204. _run_compiler(Package.unpacked_bin(), all_oberon_sources,
  205. options.locale, options.out)
  206. class self_recompile_target(object):
  207. name = 'self-recompile'
  208. description = 'compile and replace itself using current sources'
  209. @staticmethod
  210. def setup_options(parser):
  211. parser.add_argument('--skip-tests', action='store_true',
  212. help='do not run test after recompile')
  213. def __init__(self, options):
  214. _recompile_with_replace(options.locale, options.skip_tests)
  215. class html_target(object):
  216. name = 'html'
  217. description = 'build html page'
  218. @staticmethod
  219. def setup_options(parser):
  220. parser.add_argument('--out', help='output directory, default: "%(default)s"', default='_out')
  221. parser.add_argument('--set-version', action="store_true", help='include version in built html')
  222. def __init__(self, options):
  223. build_html(options)
  224. class tests_target(object):
  225. name = 'tests'
  226. description = 'run tests'
  227. @staticmethod
  228. def setup_options(parser):
  229. parser.add_argument('--unit', help='run specific unit test, use "*" to run all unit tests')
  230. parser.add_argument('--code', help='run specific code generator test, use "*" to run all generator tests')
  231. def __init__(self, options):
  232. run_tests(Package.unpacked_bin(), options.unit, options.code)
  233. class pre_commit_target(object):
  234. name = 'pre-commit'
  235. description = 'recompile oberon sources, run tests against just recompiled sources, pack compiled sources'
  236. @staticmethod
  237. def setup_options(parser):
  238. pass
  239. def __init__(self, options):
  240. pre_commit_check(options)
  241. targets = [compile_target, recompile_target, self_recompile_target, html_target, tests_target, pre_commit_target]
  242. if __name__ == '__main__':
  243. parser = argparse.ArgumentParser(description='Build tool')
  244. parser.add_argument('--locale', help='use specified localization subfolder, default is "%(default)s"', default='en')
  245. subparsers = parser.add_subparsers(help='targets')
  246. for t in targets:
  247. group = subparsers.add_parser(t.name, help=t.description)
  248. t.setup_options(group)
  249. group.set_defaults(func=t)
  250. args = parser.parse_args()
  251. args.func(args)