blob: 1adbb6d4d0071b394aa28faeaf48d4b7cea8e4ad [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2022 Project CHIP 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 os
import logging
import jinja2
from typing import Dict
from idl.matter_idl_types import Idl
from .filters import RegisterCommonFilters
class GeneratorStorage:
"""
Handles file operations for generator output. Specifically can create
required files for output.
Is overriden for unit tests.
"""
def __init__(self):
self._generated_paths = set()
@property
def generated_paths(self):
return self._generated_paths
def report_output_file(self, relative_path: str):
self._generated_paths.add(relative_path)
def get_existing_data(self, relative_path: str):
"""Gets the existing data at the given path.
If such data does not exist, will return None.
"""
raise NotImplementedError()
def write_new_data(self, relative_path: str, content: str):
"""Write new data to the given path."""
raise NotImplementedError()
class FileSystemGeneratorStorage(GeneratorStorage):
"""
A storage generator which will physically write files to disk into
a given output folder.
"""
def __init__(self, output_dir: str):
super().__init__()
self.output_dir = output_dir
def get_existing_data(self, relative_path: str):
"""Gets the existing data at the given path.
If such data does not exist, will return None.
"""
target = os.path.join(self.output_dir, relative_path)
if not os.path.exists(target):
return None
logging.info("Checking existing data in %s" % target)
with open(target, 'rt') as existing:
return existing.read()
def write_new_data(self, relative_path: str, content: str):
"""Write new data to the given path."""
target = os.path.join(self.output_dir, relative_path)
target_dir = os.path.dirname(target)
if not os.path.exists(target_dir):
logging.info("Creating output directory: %s" % target_dir)
os.makedirs(target_dir)
logging.info("Writing new data to: %s" % target)
with open(target, "wt") as out:
out.write(content)
class CodeGenerator:
"""
Defines the general interface for things that can generate code output.
A CodeGenerator takes a AST as input (a `Idl` type) and generates files
as output (like java/cpp/mm/other).
Its public interface surface is reasonably small:
'storage' init argument specifies where generated code goes
'idl' is the input AST to generate
'render' will perform a rendering of all files.
As special optimizations, CodeGenerators generally will try to read
existing data and will not re-write content if not changed (so that
write time of files do not change and rebuilds are not triggered).
"""
def __init__(self, storage: GeneratorStorage, idl: Idl):
"""
A code generator will render a parsed IDL (a AST) into a given storage.
"""
self.storage = storage
self.idl = idl
self.jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(searchpath=os.path.dirname(__file__)),
keep_trailing_newline=True)
self.dry_run = False
RegisterCommonFilters(self.jinja_env.filters)
def render(self, dry_run=False):
"""
Renders all required files given the idl contained in the code generator.
Reset the list of generated outputs.
Args:
dry_run: if true, outputs are not actually written to disk.
if false, outputs are actually written to disk.
"""
self.dry_run = dry_run
self.internal_render_all()
def internal_render_all(self):
"""This method is to be implemented by subclasses to run all generation
as needed.
"""
raise NotImplementedError("Method should be implemented by subclasses")
def internal_render_one_output(self, template_path: str, output_file_name: str, vars: Dict):
"""
Method to be called by subclasses to mark that a template is to be generated.
File will either actually do a jinja2 generation or just log things
if dry-run was requested during `render`.
NOTE: to make this method suitable for rebuilds, this file will NOT alter
the timestamp of the output file if the file content would not
change (i.e. no write will be invoked in that case.)
Args:
template_path - the path to the template to be loaded for file generation.
Template MUST be a jinja2 template.
output_file_name - File name that the template is to be generated to.
vars - variables used for template generation
"""
logging.info("File to be generated: %s" % output_file_name)
if self.dry_run:
return
rendered = self.jinja_env.get_template(template_path).render(vars)
# Report regardless if it has changed or not. This is because even if
# files are unchanged, validation of what the correct output is should
# still be done.
self.storage.report_output_file(output_file_name)
if rendered == self.storage.get_existing_data(output_file_name):
logging.info("File content not changed")
else:
self.storage.write_new_data(output_file_name, rendered)