anvil-build/anvil/rules/closure_js_rules.py

445 lines
14 KiB
Python

# Copyright 2012 Google Inc. All Rights Reserved.
"""Closure compiler rules for the build system.
Contains the following rules:
closure_js_lint
closure_js_fixstyle
closure_js_library
Assumes Closure Linter is installed and on the path.
"""
__author__ = 'benvanik@google.com (Ben Vanik)'
import io
import os
import re
from anvil.context import RuleContext
from anvil.rule import Rule, build_rule
from anvil.task import (Task, ExecutableTask, JavaExecutableTask,
WriteFileTask)
import anvil.util
@build_rule('closure_js_lint')
class ClosureJsLintRule(Rule):
"""A set of linted JS files.
Passes all source files through the Closure style linter.
Similar to file_set, all source files are globbed together and de-duplicated
before being passed on as outputs. If a src_filter is provided then it is used
to filter all sources.
Inputs:
srcs: Source JS file paths.
namespaces: A list of Closurized namespaces.
Outputs:
All of the source file paths, passed-through unmodified.
"""
def __init__(self, name, namespaces, *args, **kwargs):
"""Initializes a Closure JS lint rule.
Args:
name: Rule name.
namespaces: A list of Closurized namespaces.
"""
super(ClosureJsLintRule, self).__init__(name, *args, **kwargs)
self.src_filter = '*.js'
self._command = 'gjslint'
self._extra_args = ['--nobeep',]
self.namespaces = []
self.namespaces.extend(namespaces)
class _Context(RuleContext):
def begin(self):
super(ClosureJsLintRule._Context, self).begin()
self._append_output_paths(self.src_paths)
namespaces = ','.join(self.rule.namespaces)
args = [
'--strict',
'--jslint_error=all',
'--closurized_namespaces=%s' % (namespaces),
]
# TODO(benvanik): only changed paths
# Exclude any path containing build-*
for src_path in self.src_paths:
if (src_path.find('build-out%s' % path.sep) == -1 and
src_path.find('build-gen%s' % path.sep) == -1):
args.append(src_path)
d = self._run_task_async(ExecutableTask(
self.build_env, self.rule._command, args))
# TODO(benvanik): pull out errors?
self._chain(d)
@build_rule('closure_js_fixstyle')
class ClosureJsFixStyleRule(ClosureJsLintRule):
"""A set of style-corrected JS files.
Passes all source files through the Closure style fixer.
Similar to file_set, all source files are globbed together and de-duplicated
before being passed on as outputs. If a src_filter is provided then it is used
to filter all sources.
This rule is special in that it fixes the style of the source files in-place,
overwriting them in their source path.
Inputs:
srcs: Source JS file paths.
namespaces: A list of Closurized namespaces.
Outputs:
All of the source file paths, passed-through post-correction.
"""
def __init__(self, name, namespaces, *args, **kwargs):
"""Initializes a Closure JS style fixing rule.
Args:
name: Rule name.
namespaces: A list of Closurized namespaces.
"""
super(ClosureJsFixStyleRule, self).__init__(name, namespaces,
*args, **kwargs)
self._command = 'fixjsstyle'
self._extra_args = []
# TODO(benvanik): support non-closure code
# TODO(benvanik): support AMD modules
@build_rule('closure_js_library')
class ClosureJsLibraryRule(Rule):
"""A Closure compiler JavaScript library.
Uses the Closure compiler to build a library from the given source files.
Processes input files using the Closure compiler in the given mode.
goog.provide and goog.require are used to order the files when concatenated.
In SIMPLE and ADVANCED modes dependencies are used to remove dead code.
If UNCOMPILED mode is used, only a -deps.js file is generated. This is a fast
operation and can be used when interactively running code in a browser in
uncompiled mode.
A compiler JAR must be provided.
Inputs:
srcs: All source JS files.
mode: Compilation mode, one of ['UNCOMPILED', 'SIMPLE', 'ADVANCED'].
compiler_jar: Path to a compiler .jar file.
entry_points: A list of entry points, such as 'myapp.start'.
pretty_print: True to pretty print the output.
debug: True to enable Closure DEBUG consts.
compiler_flags: A list of string compiler flags.
externs: Additional extern .js files.
wrap_with_global: Wrap all output in a closure and call with the given
global object.
Example - 'global' -> (function(){...code...}).call(global);
out: Optional output name. If none is provided than the rule name will be
used.
Outputs:
A single compiled JS file. If no out is specified a file with the name of
the rule will be created.
"""
def __init__(self, name, mode, compiler_jar, entry_points,
pretty_print=False, debug=False,
compiler_flags=None, externs=None, wrap_with_global=None, out=None,
*args, **kwargs):
"""Initializes a Closure JS library rule.
Args:
name: Rule name.
mode: Compilation mode, one of ['UNCOMPILED', 'SIMPLE', 'ADVANCED'].
compiler_jar: Path to a compiler .jar file.
entry_points: A list of entry points, such as 'myapp.start'.
pretty_print: True to pretty print the output.
debug: True to enable Closure DEBUG consts.
compiler_flags: A list of string compiler flags.
externs: Additional extern .js files.
wrap_with_global: Wrap all output in a closure and call with the given
global object.
Example - 'global' -> (function(){...code...}).call(global);
out: Optional output name.
"""
super(ClosureJsLibraryRule, self).__init__(name, *args, **kwargs)
self.src_filter = '*.js'
self.mode = mode
self.compiler_jar = compiler_jar
self._append_dependent_paths([self.compiler_jar])
self.pretty_print = pretty_print
self.debug = debug
self.entry_points = []
if isinstance(entry_points, str):
self.entry_points.append(entry_points)
elif entry_points:
self.entry_points.extend(entry_points)
self.compiler_flags = []
if compiler_flags:
self.compiler_flags.extend(compiler_flags)
self.externs = []
if externs:
self.externs.extend(externs)
self._append_dependent_paths(self.externs)
self.wrap_with_global = wrap_with_global
self.out = out
class _Context(RuleContext):
def begin(self):
super(ClosureJsLibraryRule._Context, self).begin()
jar_path = self._resolve_input_files([self.rule.compiler_jar])[0]
args = [
'--process_closure_primitives',
'--generate_exports',
'--summary_detail_level=3',
'--warning_level=VERBOSE',
]
args.extend(self.rule.compiler_flags)
compiling = False
if self.rule.mode == 'SIMPLE':
compiling = True
args.append('--compilation_level=SIMPLE_OPTIMIZATIONS')
elif self.rule.mode == 'ADVANCED':
compiling = True
args.append('--compilation_level=ADVANCED_OPTIMIZATIONS')
if self.rule.pretty_print:
args.append('--formatting=PRETTY_PRINT')
args.append('--formatting=PRINT_INPUT_DELIMITER')
if not self.rule.debug:
args.append('--define=goog.DEBUG=false')
args.append('--define=goog.asserts.ENABLE_ASSERTS=false')
if self.rule.wrap_with_global:
args.append('--output_wrapper="(function(){%%output%%}).call(%s);"' % (
self.rule.wrap_with_global))
extern_paths = self._resolve_input_files(self.rule.externs)
for extern_path in extern_paths:
args.append('--externs=%s' % (extern_path))
for entry_point in self.rule.entry_points:
args.append('--closure_entry_point=%s' % (entry_point))
if compiling:
# Compiling - will generate the main js library
output_path = self._get_out_path(name=self.rule.out, suffix='.js')
self._ensure_output_exists(os.path.dirname(output_path))
self._append_output_paths([output_path])
args.append('--js_output_file=%s' % (output_path))
else:
# Uncompiled - will generate a deps.js file
deps_js_path = self._get_out_path(name=self.rule.out, suffix='-deps.js')
self._ensure_output_exists(os.path.dirname(deps_js_path))
self._append_output_paths([deps_js_path])
# Issue dependency scanning to build the deps graph
d = self._run_task_async(_ScanJsDependenciesTask(
self.build_env, self.src_paths))
# Launch the compilation and deps.js gen tasks when scanning completes
def _deps_scanned(dep_graph):
ds = []
# Grab all used files
used_paths = dep_graph.get_transitive_closure(self.rule.entry_points)
# Generate/write JS deps
if not compiling:
ds.append(self._run_task_async(WriteFileTask(
self.build_env, dep_graph.get_deps_js(), deps_js_path)))
# Compile main lib
if compiling:
for src_path in used_paths:
args.append('--js=%s' % (src_path))
ds.append(self._run_task_async(JavaExecutableTask(
self.build_env, jar_path, args)))
# TODO(benvanik): pull out (stdout, stderr) from result and the
# exception to get better error logging
else:
# Uncompiled - pass along all inputs as outputs
self._append_output_paths(used_paths)
self._chain(ds)
# Kickoff chain
d.add_callback_fn(_deps_scanned)
self._chain_errback(d)
class _ScanJsDependenciesTask(Task):
def __init__(self, build_env, src_paths, *args, **kwargs):
super(_ScanJsDependenciesTask, self).__init__(build_env, *args, **kwargs)
self.src_paths = src_paths
def execute(self):
deps_graph = JsDependencyGraph(self.build_env, self.src_paths)
return deps_graph
class JsDependencyFile(object):
"""A single source JS file.
Parses the file and finds all goog.provide and goog.require statements.
"""
_PROVIDEREQURE_REGEX = re.compile(
'goog\.(provide|require)\(\s*[\'"](.+)[\'"]\s*\)')
_GOOG_BASE_LINE = (
'var goog = goog || {}; // Identifies this file as the Closure base.\n')
def __init__(self, src_path):
"""Initializes a JS dependency file.
Args:
src_path: Source JS file path.
"""
self.src_path = src_path
self.provides = []
self.requires = []
self.is_base_js = False
self.is_css_rename_map = False
with io.open(self.src_path, 'rb') as f:
self._scan(f)
def _scan(self, f):
"""Scans the given file for provides/requires and returns the results.
Args:
f: Input file.
"""
provides = set()
requires = set()
for line in f.readlines():
match = self._PROVIDEREQURE_REGEX.match(line)
if match:
if match.group(1) == 'provide':
provides.add(str(match.group(2)))
else:
requires.add(str(match.group(2)))
elif line == self._GOOG_BASE_LINE:
provides.add('goog')
self.is_base_js = True
elif line.startswith('goog.setCssNameMapping('):
self.is_css_rename_map = True
self.provides = list(provides)
self.provides.sort()
self.requires = list(requires)
self.requires.sort()
class JsDependencyGraph(object):
"""Represents a JS dependency graph.
This scans all source JavaScript files to identify their dependencies.
The result is a queryable list
"""
def __init__(self, build_env, src_paths, *args, **kwargs):
"""Initializes a JS dependency graph.
Args:
build_env: BuildEnvironment.
src_paths: A list of source JS paths.
"""
self.build_env = build_env
self.src_paths = list(src_paths)
self.dep_files = {}
self._provide_map = {}
self.base_dep_file = None
self.base_js_path = None
# Scan all files
for src_path in self.src_paths:
dep_file = JsDependencyFile(src_path)
self.dep_files[src_path] = dep_file
if dep_file.is_base_js:
self.base_dep_file = dep_file
self.base_js_path = os.path.dirname(dep_file.src_path)
for provide in dep_file.provides:
assert not provide in self._provide_map
self._provide_map[provide] = dep_file
def get_deps_js(self):
"""Generates the contents of a deps.js file from the dependency graph.
Returns:
A string containing all of the lines of a deps.js file.
"""
# Path that all dependencies will be relative from
base_path = self.build_env.root_path
if self.base_js_path:
base_path = self.base_js_path
lines = [
'// Automatically generated by anvil-build - do not modify',
'',
]
# Write in path order (to make the file easier to debug)
src_paths = self.dep_files.keys()
src_paths.sort()
for src_path in src_paths:
dep_file = self.dep_files[src_path]
rel_path = os.path.relpath(dep_file.src_path, base_path)
rel_path = anvil.util.strip_build_paths(rel_path)
lines.append('goog.addDependency(\'%s\', %s, %s);' % (
anvil.util.ensure_forwardslashes(rel_path),
dep_file.provides, dep_file.requires))
return u'\n'.join(lines)
def get_transitive_closure(self, entry_points):
"""Identifies the transitive closure of the dependency graph for the given
entry points.
Args:
entry_points: Closure entry points, such as 'my.start'.
Returns:
A list of all file paths required by the given entry points.
The files are sorted in proper dependency order.
"""
deps_list = []
# Always base.js first
if self.base_dep_file:
deps_list.append(self.base_dep_file.src_path)
# Followed by all files in the transitive closure, in order
for entry_point in entry_points:
self._add_dependencies(deps_list, entry_point)
# And finally, any files that look special
for dep_file in self.dep_files.values():
if dep_file.is_css_rename_map:
deps_list.append(dep_file.src_path)
return deps_list
def _add_dependencies(self, deps_list, namespace):
if not namespace in self._provide_map:
print 'Namespace %s not provided' % (namespace)
assert namespace in self._provide_map
dep_file = self._provide_map[namespace]
if dep_file.src_path in deps_list:
return
for require in dep_file.requires:
self._add_dependencies(deps_list, require)
deps_list.append(dep_file.src_path)