dawn-cmake/generator/generator_lib.py

204 lines
7.3 KiB
Python

#!/usr/bin/env python2
# Copyright 2019 The Dawn Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse, json, os, re, sys
from collections import namedtuple
# The interface that must be implemented by generators.
class Generator:
def get_description(self):
return ""
def add_commandline_arguments(self, parser):
pass
def get_file_renders(self, args):
return []
def get_dependencies(self, args):
return []
FileRender = namedtuple('FileRender', ['template', 'output', 'params_dicts'])
# Try using an additional python path from the arguments if present. This
# isn't done through the regular argparse because PreprocessingLoader uses
# jinja2 in the global scope before "main" gets to run.
kExtraPythonPath = '--extra-python-path'
if kExtraPythonPath in sys.argv:
path = sys.argv[sys.argv.index(kExtraPythonPath) + 1]
sys.path.insert(1, path)
import jinja2
# A custom Jinja2 template loader that removes the extra indentation
# of the template blocks so that the output is correctly indented
class _PreprocessingLoader(jinja2.BaseLoader):
def __init__(self, path):
self.path = path
def get_source(self, environment, template):
path = os.path.join(self.path, template)
if not os.path.exists(path):
raise jinja2.TemplateNotFound(template)
mtime = os.path.getmtime(path)
with open(path) as f:
source = self.preprocess(f.read())
return source, path, lambda: mtime == os.path.getmtime(path)
blockstart = re.compile('{%-?\s*(if|for|block)[^}]*%}')
blockend = re.compile('{%-?\s*end(if|for|block)[^}]*%}')
def preprocess(self, source):
lines = source.split('\n')
# Compute the current indentation level of the template blocks and remove their indentation
result = []
indentation_level = 0
for line in lines:
# The capture in the regex adds one element per block start or end so we divide by two
# there is also an extra line chunk corresponding to the line end, so we substract it.
numends = (len(self.blockend.split(line)) - 1) // 2
indentation_level -= numends
result.append(self.remove_indentation(line, indentation_level))
numstarts = (len(self.blockstart.split(line)) - 1) // 2
indentation_level += numstarts
return '\n'.join(result) + '\n'
def remove_indentation(self, line, n):
for _ in range(n):
if line.startswith(' '):
line = line[4:]
elif line.startswith('\t'):
line = line[1:]
else:
assert(line.strip() == '')
return line
_FileOutput = namedtuple('FileOutput', ['name', 'content'])
def _do_renders(renders, template_dir):
loader = _PreprocessingLoader(template_dir)
env = jinja2.Environment(loader=loader, lstrip_blocks=True, trim_blocks=True, line_comment_prefix='//*')
def do_assert(expr):
assert expr
return ''
def debug(text):
print(text)
base_params = {
'enumerate': enumerate,
'format': format,
'len': len,
'debug': debug,
'assert': do_assert,
}
outputs = []
for render in renders:
params = {}
params.update(base_params)
for param_dict in render.params_dicts:
params.update(param_dict)
content = env.get_template(render.template).render(**params)
outputs.append(_FileOutput(render.output, content))
return outputs
# Compute the list of imported, non-system Python modules.
# It assumes that any path outside of Dawn's root directory is system.
def _compute_python_dependencies():
dawn_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
module_paths = (module.__file__ for module in sys.modules.values()
if module and hasattr(module, '__file__'))
paths = set()
for path in module_paths:
path = os.path.abspath(path)
if not path.startswith(dawn_root):
continue
if (path.endswith('.pyc')
or (path.endswith('c') and not os.path.splitext(path)[1])):
path = path[:-1]
paths.add(path)
return paths
def run_generator(generator):
parser = argparse.ArgumentParser(
description = generator.get_description(),
formatter_class = argparse.ArgumentDefaultsHelpFormatter
)
generator.add_commandline_arguments(parser);
parser.add_argument('-t', '--template-dir', default='templates', type=str, help='Directory with template files.')
parser.add_argument(kExtraPythonPath, default=None, type=str, help='Additional python path to set before loading Jinja2')
parser.add_argument('--output-json-tarball', default=None, type=str, help='Name of the "JSON tarball" to create (tar is too annoying to use in python).')
parser.add_argument('--depfile', default=None, type=str, help='Name of the Ninja depfile to create for the JSON tarball')
parser.add_argument('--expected-outputs-file', default=None, type=str, help="File to compare outputs with and fail if it doesn't match")
args = parser.parse_args()
renders = generator.get_file_renders(args);
# The caller wants to assert that the outputs are what it expects.
# Load the file and compare with our renders.
if args.expected_outputs_file != None:
with open(args.expected_outputs_file) as f:
expected = set([line.strip() for line in f.readlines()])
actual = set()
actual.update([render.output for render in renders])
if actual != expected:
print("Wrong expected outputs, caller expected:\n " + repr(list(expected)))
print("Actual output:\n " + repr(list(actual)))
return 1
# Add a any extra Python path before importing Jinja2 so invokers can point
# to a checkout of Jinja2 and note require it to be installed on the system
if args.extra_python_path != None:
sys.path.insert(1, args.extra_python_path)
import jinja2
outputs = _do_renders(renders, args.template_dir)
# Output the tarball and its depfile
if args.output_json_tarball != None:
json_root = {}
for output in outputs:
json_root[output.name] = output.content
with open(args.output_json_tarball, 'w') as f:
f.write(json.dumps(json_root))
# Output a list of all dependencies for the tarball for Ninja.
if args.depfile != None:
dependencies = generator.get_dependencies(args)
dependencies += [args.template_dir + os.path.sep + render.template for render in renders]
dependencies += _compute_python_dependencies()
with open(args.depfile, 'w') as f:
f.write(args.output_json_tarball + ": " + " ".join(dependencies))