1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163

build / android / gyp / jinja_template.py [blame]

#!/usr/bin/env python3
#
# Copyright 2014 The Chromium Authors
# 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
import action_helpers  # build_utils adds //build to sys.path.
import zip_helpers

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:
  """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')
    with action_helpers.atomic_output(outputs_zip) as f:
      zip_helpers.zip_directory(f, temp_dir)


def _ParseVariables(variables_arg, error_func):
  variables = {}
  for v in action_helpers.parse_gn_list(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 = action_helpers.parse_gn_list(options.inputs)
  includes = action_helpers.parse_gn_list(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()