220 lines
7.4 KiB
Python
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])
|