1#!/usr/bin/env python3 2# 3# Copyright 2014 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Renders one or more template files using the Jinja template engine.""" 8 9import codecs 10import argparse 11import os 12import sys 13 14from util import build_utils 15from util import resource_utils 16import action_helpers # build_utils adds //build to sys.path. 17import zip_helpers 18 19sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) 20from pylib.constants import host_paths 21 22# Import jinja2 from third_party/jinja2 23sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party')) 24import jinja2 # pylint: disable=F0401 25 26 27class _RecordingFileSystemLoader(jinja2.FileSystemLoader): 28 def __init__(self, searchpath): 29 jinja2.FileSystemLoader.__init__(self, searchpath) 30 self.loaded_templates = set() 31 32 def get_source(self, environment, template): 33 contents, filename, uptodate = jinja2.FileSystemLoader.get_source( 34 self, environment, template) 35 self.loaded_templates.add(os.path.relpath(filename)) 36 return contents, filename, uptodate 37 38 39class JinjaProcessor: 40 """Allows easy rendering of jinja templates with input file tracking.""" 41 def __init__(self, loader_base_dir, variables=None): 42 self.loader_base_dir = loader_base_dir 43 self.variables = variables or {} 44 self.loader = _RecordingFileSystemLoader(loader_base_dir) 45 self.env = jinja2.Environment(loader=self.loader) 46 self.env.undefined = jinja2.StrictUndefined 47 self.env.line_comment_prefix = '##' 48 self.env.trim_blocks = True 49 self.env.lstrip_blocks = True 50 self._template_cache = {} # Map of path -> Template 51 52 def Render(self, input_filename, variables=None): 53 input_rel_path = os.path.relpath(input_filename, self.loader_base_dir) 54 template = self._template_cache.get(input_rel_path) 55 if not template: 56 template = self.env.get_template(input_rel_path) 57 self._template_cache[input_rel_path] = template 58 return template.render(variables or self.variables) 59 60 def GetLoadedTemplates(self): 61 return list(self.loader.loaded_templates) 62 63 64def _ProcessFile(processor, input_filename, output_filename): 65 output = processor.Render(input_filename) 66 67 # If |output| is same with the file content, we skip update and 68 # ninja's restat will avoid rebuilding things that depend on it. 69 if os.path.isfile(output_filename): 70 with codecs.open(output_filename, 'r', 'utf-8') as f: 71 if f.read() == output: 72 return 73 74 with codecs.open(output_filename, 'w', 'utf-8') as output_file: 75 output_file.write(output) 76 77 78def _ProcessFiles(processor, input_filenames, inputs_base_dir, outputs_zip): 79 with build_utils.TempDir() as temp_dir: 80 path_info = resource_utils.ResourceInfoFile() 81 for input_filename in input_filenames: 82 relpath = os.path.relpath(os.path.abspath(input_filename), 83 os.path.abspath(inputs_base_dir)) 84 if relpath.startswith(os.pardir): 85 raise Exception('input file %s is not contained in inputs base dir %s' 86 % (input_filename, inputs_base_dir)) 87 88 output_filename = os.path.join(temp_dir, relpath) 89 parent_dir = os.path.dirname(output_filename) 90 build_utils.MakeDirectory(parent_dir) 91 _ProcessFile(processor, input_filename, output_filename) 92 path_info.AddMapping(relpath, input_filename) 93 94 path_info.Write(outputs_zip + '.info') 95 with action_helpers.atomic_output(outputs_zip) as f: 96 zip_helpers.zip_directory(f, temp_dir) 97 98 99def _ParseVariables(variables_arg, error_func): 100 variables = {} 101 for v in action_helpers.parse_gn_list(variables_arg): 102 if '=' not in v: 103 error_func('--variables argument must contain "=": ' + v) 104 name, _, value = v.partition('=') 105 variables[name] = value 106 return variables 107 108 109def main(): 110 parser = argparse.ArgumentParser() 111 parser.add_argument('--inputs', required=True, 112 help='GN-list of template files to process.') 113 parser.add_argument('--includes', default='', 114 help="GN-list of files that get {% include %}'ed.") 115 parser.add_argument('--output', help='The output file to generate. Valid ' 116 'only if there is a single input.') 117 parser.add_argument('--outputs-zip', help='A zip file for the processed ' 118 'templates. Required if there are multiple inputs.') 119 parser.add_argument('--inputs-base-dir', help='A common ancestor directory ' 120 'of the inputs. Each output\'s path in the output zip ' 121 'will match the relative path from INPUTS_BASE_DIR to ' 122 'the input. Required if --output-zip is given.') 123 parser.add_argument('--loader-base-dir', help='Base path used by the ' 124 'template loader. Must be a common ancestor directory of ' 125 'the inputs. Defaults to DIR_SOURCE_ROOT.', 126 default=host_paths.DIR_SOURCE_ROOT) 127 parser.add_argument('--variables', help='Variables to be made available in ' 128 'the template processing environment, as a GYP list ' 129 '(e.g. --variables "channel=beta mstone=39")', default='') 130 parser.add_argument('--check-includes', action='store_true', 131 help='Enable inputs and includes checks.') 132 options = parser.parse_args() 133 134 inputs = action_helpers.parse_gn_list(options.inputs) 135 includes = action_helpers.parse_gn_list(options.includes) 136 137 if (options.output is None) == (options.outputs_zip is None): 138 parser.error('Exactly one of --output and --output-zip must be given') 139 if options.output and len(inputs) != 1: 140 parser.error('--output cannot be used with multiple inputs') 141 if options.outputs_zip and not options.inputs_base_dir: 142 parser.error('--inputs-base-dir must be given when --output-zip is used') 143 144 variables = _ParseVariables(options.variables, parser.error) 145 processor = JinjaProcessor(options.loader_base_dir, variables=variables) 146 147 if options.output: 148 _ProcessFile(processor, inputs[0], options.output) 149 else: 150 _ProcessFiles(processor, inputs, options.inputs_base_dir, 151 options.outputs_zip) 152 153 if options.check_includes: 154 all_inputs = set(processor.GetLoadedTemplates()) 155 all_inputs.difference_update(inputs) 156 all_inputs.difference_update(includes) 157 if all_inputs: 158 raise Exception('Found files not listed via --includes:\n' + 159 '\n'.join(sorted(all_inputs))) 160 161 162if __name__ == '__main__': 163 main() 164