220 lines
7.4 KiB
Python

# based on https://github.com/hgomersall/scons-jinja
from SCons.Script import *
import os
import pathlib
from spp import get_spp
try:
import jinja2
from jinja2.utils import open_if_exists
except ImportError:
jinja2 = None
print('No Jinja :(')
spp = get_spp()
def available(**kwargs) -> bool:
return jinja2 is not None
def post_environment(**kwargs) -> None:
env: Environment = spp.globals['env']
env.SetDefault(JINJA_CONTEXT={})
env.SetDefault(JINJA_ENVIRONMENT_VARS={})
env.SetDefault(JINJA_FILTERS={'load_config': _jinja_load_config})
env.SetDefault(JINJA_GLOBALS={
'file_size': lambda *args: _file_size(env, *args),
'file_content_hex': lambda *args: _file_content_hex(env, *args)
})
env.SetDefault(JINJA_TEMPLATE_SEARCHPATH=['data/jinja'])
env.SetDefault(JINJA_CONFIG_SEARCHPATH=[env.Dir('#data/config')])
env.SetDefault(JINJA_FILE_SEARCHPATH=[env.Dir('#')])
env['BUILDERS']['Jinja'] = Builder(
action=render_jinja_template
)
scanner = env.Scanner(function=jinja_scanner,
skeys=['.jinja'])
env.Append(SCANNERS=scanner)
env.AddMethod(_wrap_jinja(env.Jinja), 'Jinja')
class FileSystemLoaderRecorder(jinja2.FileSystemLoader):
""" A wrapper around FileSystemLoader that records files as they are
loaded. These are contained within loaded_filenames set attribute.
"""
def __init__(self, searchpath, encoding='utf-8'):
self.loaded_filenames = set()
super(FileSystemLoaderRecorder, self).__init__(searchpath, encoding)
def get_source(self, environment, template):
"""Overwritten FileSystemLoader.get_source method that extracts the
filename that is used to load each filename and adds it to
self.loaded_filenames.
"""
for searchpath in self.searchpath:
filename = os.path.join(searchpath, template)
f = open_if_exists(filename)
if f is None:
continue
try:
contents = f.read().decode(self.encoding)
finally:
f.close()
self.loaded_filenames.add(filename)
return super(FileSystemLoaderRecorder, self).get_source(
environment, template)
# If the template isn't found, then we have to drop out.
raise jinja2.TemplateNotFound(template)
def jinja_scanner(node, env, path):
# Instantiate the file as necessary
node.get_text_contents()
template_dir, filename = os.path.split(str(node))
template_search_path = ([template_dir] +
env.subst(env['JINJA_TEMPLATE_SEARCHPATH']))
template_loader = FileSystemLoaderRecorder(template_search_path)
jinja_env = jinja2.Environment(loader=template_loader,
extensions=['jinja2.ext.do'], **env['JINJA_ENVIRONMENT_VARS'])
jinja_env.filters.update(env['JINJA_FILTERS'])
jinja_env.globals.update(env['JINJA_GLOBALS'])
try:
template = jinja_env.get_template(filename)
except jinja2.TemplateNotFound as e:
env.Error(f'Missing template: {os.path.join(template_dir, str(e))}')
# We need to render the template to do all the necessary loading.
#
# It's necessary to respond to missing templates by grabbing
# the content as the exception is raised. This makes sure of the
# existence of the file upon which the current scanned node depends.
#
# I suspect that this is pretty inefficient, but it does
# work reliably.
context = env['JINJA_CONTEXT']
last_missing_file = ''
while True:
try:
template.render(**context)
except jinja2.TemplateNotFound as e:
if last_missing_file == str(e):
# We've already been round once for this file,
# so need to raise
env.Error(f'Missing template: {os.path.join(template_dir, str(e))}')
last_missing_file = str(e)
# Find where the template came from (using the same ordering
# as Jinja uses).
for searchpath in template_search_path:
filename = os.path.join(searchpath, last_missing_file)
if os.path.exists(filename):
continue
else:
env.File(filename).get_text_contents()
continue
break
# Get all the files that were loaded. The set includes the current node,
# so we remove that.
found_nodes_names = list(template_loader.loaded_filenames)
try:
found_nodes_names.remove(str(node))
except ValueError as e:
env.Error(f'Missing template node: {str(node)}')
return [env.File(f) for f in found_nodes_names]
def render_jinja_template(target, source, env):
output_str = ''
if not source:
source = [f'{target}.jinja']
for template_file in source:
template_dir, filename = os.path.split(str(template_file))
template_search_path = ([template_dir] +
env.subst(env['JINJA_TEMPLATE_SEARCHPATH']))
template_loader = FileSystemLoaderRecorder(template_search_path)
jinja_env = jinja2.Environment(loader=template_loader,
extensions=['jinja2.ext.do'], **env['JINJA_ENVIRONMENT_VARS'])
jinja_env.filters.update(env['JINJA_FILTERS'])
jinja_env.globals.update(env['JINJA_GLOBALS'])
jinja_env.filters.update(env['JINJA_FILTERS'])
template = jinja_env.get_template(filename)
context = env['JINJA_CONTEXT']
template.render(**context)
output_str += template.render(**context)
with open(str(target[0]), 'w') as target_file:
target_file.write(output_str)
return None
def _jinja_load_config(env, config_name):
searched_paths = []
for scons_path in env['JINJA_CONFIG_SEARCHPATH']:
if hasattr(scons_path, 'abspath'):
scons_path = scons_path.abspath
path = pathlib.Path(scons_path) / f'{config_name}.yml'
if path.exists():
with path.open('r') as file:
import yaml
return yaml.safe_load(file)
searched_paths.append(f'\n{path}')
joined_paths = ''.join(searched_paths)
raise Exception(f'Could not find Jinja config file "{config_name}.yml". Searched: {joined_paths}')
def _wrap_jinja(orig_jinja):
def _wrapped(env, target, **kwargs):
if 'source' not in kwargs:
kwargs['source'] = f'{target}.jinja'
target = orig_jinja(target=target, **kwargs)
if 'depends' in kwargs:
for dependency in kwargs['depends']:
env.Depends(target, dependency)
return target
return _wrapped
def _find_file(env, fname):
for path in env['JINJA_FILE_SEARCHPATH']:
fullpath = os.path.join(path.abspath, fname)
if os.path.exists(fullpath):
return env.File(fullpath)
return None
def _file_size(env, fname: str) -> int:
file = _find_file(env, fname)
if not file:
env.Error(f'File does not exist: {fname}. Searched in: {[d.abspath for d in env["JINJA_FILE_SEARCHPATH"]]}')
return file.get_size()
def _file_content_hex(env, fname: str) -> str:
file = _find_file(env, fname)
if not file:
env.Error(f'File does not exist: {fname}. Searched in: {[d.abspath for d in env["JINJA_FILE_SEARCHPATH"]]}')
bytes = file.get_contents()
return ','.join([hex(byte) for byte in bytes])