blob: e60370829f9be1b328b3130b18522e670866ec9f [file] [log] [blame]
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Builds applications in release mode:
- Concatenates autostart modules, application modules' module.json descriptors,
and the application loader into a single script.
- Builds app.html referencing the application script.
"""
from io import StringIO
from os import path
from os.path import join
import copy
import os
import re
import shutil
import sys
import subprocess
from modular_build import read_file, write_file, bail_error
import modular_build
import rjsmin
import special_case_namespaces
try:
import simplejson as json
except ImportError:
import json
try:
original_sys_path = sys.path
sys.path = sys.path + [path.join(os.path.dirname(os.path.realpath(__file__)), '..')]
import devtools_paths
finally:
sys.path = original_sys_path
ROLLUP_ARGS = ['--no-treeshake', '--format', 'iife', '--context', 'self']
def main(argv):
try:
input_path_flag_index = argv.index('--input_path')
input_path = argv[input_path_flag_index + 1]
output_path_flag_index = argv.index('--output_path')
output_path = argv[output_path_flag_index + 1]
application_names = argv[1:input_path_flag_index]
except:
print('Usage: %s app_1 app_2 ... app_N --input_path <input_path> --output_path <output_path>' % argv[0])
raise
loader = modular_build.DescriptorLoader(input_path)
for app in application_names:
descriptors = loader.load_application(app)
builder = ReleaseBuilder(app, descriptors, input_path, output_path)
builder.build_app()
def resource_source_url(url):
return '\n/*# sourceURL=' + url + ' */'
def minify_js(javascript):
return rjsmin.jsmin(javascript)
def concatenated_module_filename(module_name, output_dir):
return join(output_dir, module_name + '/' + module_name + '_module.js')
def symlink_or_copy_file(src, dest, safe=False):
if safe and path.exists(dest):
os.remove(dest)
if hasattr(os, 'symlink'):
os.symlink(src, dest)
else:
shutil.copy(src, dest)
def symlink_or_copy_dir(src, dest):
if path.exists(dest):
shutil.rmtree(dest)
for src_dir, dirs, files in os.walk(src):
subpath = path.relpath(src_dir, src)
dest_dir = path.normpath(join(dest, subpath))
os.mkdir(dest_dir)
for name in files:
src_name = join(os.getcwd(), src_dir, name)
dest_name = join(dest_dir, name)
symlink_or_copy_file(src_name, dest_name)
# Outputs:
# <app_name>.html
# <app_name>.js
# <module_name>_module.js
class ReleaseBuilder(object):
def __init__(self, application_name, descriptors, application_dir, output_dir):
self.application_name = application_name
self.descriptors = descriptors
self.application_dir = application_dir
self.output_dir = output_dir
self._special_case_namespaces = special_case_namespaces.special_case_namespaces
def app_file(self, extension):
return self.application_name + '.' + extension
def autorun_resource_names(self):
result = []
for module in self.descriptors.sorted_modules():
if self.descriptors.application[module].get('type') != 'autostart':
continue
resources = self.descriptors.modules[module].get('resources')
if not resources:
continue
for resource_name in resources:
result.append(path.join(module, resource_name))
return result
def build_app(self):
if self.descriptors.has_html:
self._build_html()
self._build_app_script()
for module in filter(lambda desc: (not desc.get('type') or desc.get('type') == 'remote'),
self.descriptors.application.values()):
self._concatenate_dynamic_module(module['name'])
def _write_include_tags(self, descriptors, output):
if descriptors.extends:
self._write_include_tags(descriptors.extends, output)
output.write(self._generate_include_tag(descriptors.application_name + '.js'))
def _build_html(self):
html_name = self.app_file('html')
output = StringIO()
with open(join(self.application_dir, html_name), 'r') as app_input_html:
for line in app_input_html:
if ('<script ' in line and 'type="module"' not in line) or '<link ' in line:
continue
if '</head>' in line:
self._write_include_tags(self.descriptors, output)
js_file = join(self.application_dir, self.app_file('js'))
if path.exists(js_file):
output.write(' <script type="module">%s</script>\n' % minify_js(read_file(js_file)))
output.write(line)
write_file(join(self.output_dir, html_name), output.getvalue())
output.close()
def _build_app_script(self):
script_name = self.app_file('js')
output = StringIO()
self._concatenate_application_script(output)
write_file(join(self.output_dir, script_name), minify_js(output.getvalue()))
output.close()
def _generate_include_tag(self, resource_path):
if resource_path.endswith('.js'):
return ' <script defer src="%s"></script>\n' % resource_path
else:
assert resource_path
def _release_module_descriptors(self):
module_descriptors = self.descriptors.modules
result = []
for name in module_descriptors:
module = copy.copy(module_descriptors[name])
module_type = self.descriptors.application[name].get('type')
# Clear scripts, as they are not used at runtime
# (only the fact of their presence is important).
resources = module.get('resources', None)
if module.get('scripts') or resources:
if module_type == 'autostart':
# Autostart modules are already baked in.
del module['scripts']
else:
# Non-autostart modules are vulcanized.
module['scripts'] = [name + '_module.js']
module['modules'] = module.get('modules', [])
# Resources are already baked into scripts.
if resources is not None:
del module['resources']
result.append(module)
return json.dumps(result)
def _write_module_resources(self, resource_names, output):
for resource_name in resource_names:
resource_name = path.normpath(resource_name).replace('\\', '/')
output.write('Root.Runtime.cachedResources["%s"] = "' % resource_name)
resource_content = read_file(path.join(self.application_dir, resource_name))
resource_content += resource_source_url(resource_name)
resource_content = resource_content.replace('\\', '\\\\')
resource_content = resource_content.replace('\n', '\\n')
resource_content = resource_content.replace('"', '\\"')
output.write(resource_content)
output.write('";\n')
def _concatenate_autostart_modules(self, output):
non_autostart = set()
sorted_module_names = self.descriptors.sorted_modules()
for name in sorted_module_names:
desc = self.descriptors.modules[name]
name = desc['name']
type = self.descriptors.application[name].get('type')
if type == 'autostart':
deps = set(desc.get('dependencies', []))
non_autostart_deps = deps & non_autostart
if len(non_autostart_deps):
bail_error(
'Non-autostart dependencies specified for the autostarted module "%s": %s' % (name, non_autostart_deps))
namespace = self._map_module_to_namespace(name)
output.write('\n/* Module %s */\n' % name)
output.write('\nself[\'%s\'] = self[\'%s\'] || {};\n' % (namespace, namespace))
modular_build.concatenate_scripts(desc.get('scripts'), join(self.application_dir, name), self.output_dir, output)
else:
non_autostart.add(name)
def _map_module_to_namespace(self, module):
camel_case_namespace = "".join(map(lambda x: x[0].upper() + x[1:], module.split('_')))
return self._special_case_namespaces.get(module, camel_case_namespace)
def _concatenate_application_script(self, output):
if not self.descriptors.extends:
runtime_contents = read_file(join(self.application_dir, 'Runtime.js'))
output.write('/* Runtime.js */\n')
output.write(runtime_contents)
output.write('Root.allDescriptors.push(...%s);' % self._release_module_descriptors())
output.write('/* Application descriptor %s */\n' % self.app_file('json'))
output.write('Root.applicationDescriptor = %s;' % self.descriptors.application_json())
else:
output.write('/* Additional descriptors */\n')
output.write('Root.allDescriptors.push(...%s);' % self._release_module_descriptors())
output.write('/* Additional descriptors %s */\n' % self.app_file('json'))
output.write('Root.applicationDescriptor.modules.push(...%s);' % json.dumps(list(self.descriptors.application.values())))
output.write('\n/* Autostart modules */\n')
if (self.descriptors.worker):
self._rollup_worker(output)
else:
self._concatenate_autostart_modules(output)
output.write(';\n/* Autostart resources */\n')
self._write_module_resources(self.autorun_resource_names(), output)
if not self.descriptors.has_html and not self.descriptors.worker:
js_file = join(self.application_dir, self.app_file('js'))
if path.exists(js_file):
output.write(';\n/* Autostart script for worker */\n')
output.write(read_file(js_file))
def _rollup_worker(self, output):
js_entrypoint = join(self.application_dir, self.app_file('unbundled.js'))
rollup_process = subprocess.Popen(
[devtools_paths.node_path(), devtools_paths.rollup_path()] + ROLLUP_ARGS + ['--input', js_entrypoint],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, error = rollup_process.communicate()
if rollup_process.returncode != 0:
print('Error while running rollup:')
print(error)
sys.exit(1)
output.write(minify_js(out))
def _concatenate_dynamic_module(self, module_name):
module = self.descriptors.modules[module_name]
scripts = module.get('scripts')
resources = self.descriptors.module_resources(module_name)
module_dir = join(self.application_dir, module_name)
output = StringIO()
if scripts:
modular_build.concatenate_scripts(scripts, module_dir, self.output_dir, output)
if resources:
self._write_module_resources(resources, output)
output_file_path = concatenated_module_filename(module_name, self.output_dir)
write_file(output_file_path, minify_js(output.getvalue()))
output.close()
if __name__ == '__main__':
sys.exit(main(sys.argv))