| #!/usr/bin/env python3 |
| # |
| # Copyright 2014 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. |
| |
| """Renders one or more template files using the Jinja template engine.""" |
| |
| import codecs |
| import argparse |
| import os |
| import sys |
| |
| from util import build_utils |
| from util import resource_utils |
| |
| sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) |
| from pylib.constants import host_paths |
| |
| # Import jinja2 from third_party/jinja2 |
| sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party')) |
| import jinja2 # pylint: disable=F0401 |
| |
| |
| class _RecordingFileSystemLoader(jinja2.FileSystemLoader): |
| def __init__(self, searchpath): |
| jinja2.FileSystemLoader.__init__(self, searchpath) |
| self.loaded_templates = set() |
| |
| def get_source(self, environment, template): |
| contents, filename, uptodate = jinja2.FileSystemLoader.get_source( |
| self, environment, template) |
| self.loaded_templates.add(os.path.relpath(filename)) |
| return contents, filename, uptodate |
| |
| |
| class JinjaProcessor(object): |
| """Allows easy rendering of jinja templates with input file tracking.""" |
| def __init__(self, loader_base_dir, variables=None): |
| self.loader_base_dir = loader_base_dir |
| self.variables = variables or {} |
| self.loader = _RecordingFileSystemLoader(loader_base_dir) |
| self.env = jinja2.Environment(loader=self.loader) |
| self.env.undefined = jinja2.StrictUndefined |
| self.env.line_comment_prefix = '##' |
| self.env.trim_blocks = True |
| self.env.lstrip_blocks = True |
| self._template_cache = {} # Map of path -> Template |
| |
| def Render(self, input_filename, variables=None): |
| input_rel_path = os.path.relpath(input_filename, self.loader_base_dir) |
| template = self._template_cache.get(input_rel_path) |
| if not template: |
| template = self.env.get_template(input_rel_path) |
| self._template_cache[input_rel_path] = template |
| return template.render(variables or self.variables) |
| |
| def GetLoadedTemplates(self): |
| return list(self.loader.loaded_templates) |
| |
| |
| def _ProcessFile(processor, input_filename, output_filename): |
| output = processor.Render(input_filename) |
| |
| # If |output| is same with the file content, we skip update and |
| # ninja's restat will avoid rebuilding things that depend on it. |
| if os.path.isfile(output_filename): |
| with codecs.open(output_filename, 'r', 'utf-8') as f: |
| if f.read() == output: |
| return |
| |
| with codecs.open(output_filename, 'w', 'utf-8') as output_file: |
| output_file.write(output) |
| |
| |
| def _ProcessFiles(processor, input_filenames, inputs_base_dir, outputs_zip): |
| with build_utils.TempDir() as temp_dir: |
| path_info = resource_utils.ResourceInfoFile() |
| for input_filename in input_filenames: |
| relpath = os.path.relpath(os.path.abspath(input_filename), |
| os.path.abspath(inputs_base_dir)) |
| if relpath.startswith(os.pardir): |
| raise Exception('input file %s is not contained in inputs base dir %s' |
| % (input_filename, inputs_base_dir)) |
| |
| output_filename = os.path.join(temp_dir, relpath) |
| parent_dir = os.path.dirname(output_filename) |
| build_utils.MakeDirectory(parent_dir) |
| _ProcessFile(processor, input_filename, output_filename) |
| path_info.AddMapping(relpath, input_filename) |
| |
| path_info.Write(outputs_zip + '.info') |
| build_utils.ZipDir(outputs_zip, temp_dir) |
| |
| |
| def _ParseVariables(variables_arg, error_func): |
| variables = {} |
| for v in build_utils.ParseGnList(variables_arg): |
| if '=' not in v: |
| error_func('--variables argument must contain "=": ' + v) |
| name, _, value = v.partition('=') |
| variables[name] = value |
| return variables |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--inputs', required=True, |
| help='GN-list of template files to process.') |
| parser.add_argument('--includes', default='', |
| help="GN-list of files that get {% include %}'ed.") |
| parser.add_argument('--output', help='The output file to generate. Valid ' |
| 'only if there is a single input.') |
| parser.add_argument('--outputs-zip', help='A zip file for the processed ' |
| 'templates. Required if there are multiple inputs.') |
| parser.add_argument('--inputs-base-dir', help='A common ancestor directory ' |
| 'of the inputs. Each output\'s path in the output zip ' |
| 'will match the relative path from INPUTS_BASE_DIR to ' |
| 'the input. Required if --output-zip is given.') |
| parser.add_argument('--loader-base-dir', help='Base path used by the ' |
| 'template loader. Must be a common ancestor directory of ' |
| 'the inputs. Defaults to DIR_SOURCE_ROOT.', |
| default=host_paths.DIR_SOURCE_ROOT) |
| parser.add_argument('--variables', help='Variables to be made available in ' |
| 'the template processing environment, as a GYP list ' |
| '(e.g. --variables "channel=beta mstone=39")', default='') |
| parser.add_argument('--check-includes', action='store_true', |
| help='Enable inputs and includes checks.') |
| options = parser.parse_args() |
| |
| inputs = build_utils.ParseGnList(options.inputs) |
| includes = build_utils.ParseGnList(options.includes) |
| |
| if (options.output is None) == (options.outputs_zip is None): |
| parser.error('Exactly one of --output and --output-zip must be given') |
| if options.output and len(inputs) != 1: |
| parser.error('--output cannot be used with multiple inputs') |
| if options.outputs_zip and not options.inputs_base_dir: |
| parser.error('--inputs-base-dir must be given when --output-zip is used') |
| |
| variables = _ParseVariables(options.variables, parser.error) |
| processor = JinjaProcessor(options.loader_base_dir, variables=variables) |
| |
| if options.output: |
| _ProcessFile(processor, inputs[0], options.output) |
| else: |
| _ProcessFiles(processor, inputs, options.inputs_base_dir, |
| options.outputs_zip) |
| |
| if options.check_includes: |
| all_inputs = set(processor.GetLoadedTemplates()) |
| all_inputs.difference_update(inputs) |
| all_inputs.difference_update(includes) |
| if all_inputs: |
| raise Exception('Found files not listed via --includes:\n' + |
| '\n'.join(sorted(all_inputs))) |
| |
| |
| if __name__ == '__main__': |
| main() |