131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
# SPDX-FileCopyrightText: 2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
'''
|
|
Usage:
|
|
python discover_nodes.py
|
|
<sources/root>
|
|
<path/to/output.cc>
|
|
<generated_function_name>
|
|
<source>...
|
|
|
|
The goal is to make it easy for nodes to register themselves without having to have
|
|
a central place that registers all nodes manually. A node can use this mechanism by
|
|
invoking `NOD_REGISTER_NODE(register_function_name)`.
|
|
|
|
This scripts finds all those macro invocations generates code that calls the functions.
|
|
'''
|
|
__all__ = (
|
|
"main",
|
|
)
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
|
|
def filepath_is_older(filepath_test: str, filepath_compare: tuple[str, ...]) -> bool:
|
|
import stat
|
|
mtime = os.stat(filepath_test)[stat.ST_MTIME]
|
|
for filepath_other in filepath_compare:
|
|
if mtime < os.stat(filepath_other)[stat.ST_MTIME]:
|
|
return True
|
|
return False
|
|
|
|
|
|
def main() -> int:
|
|
# The build system requires the generated file to be touched if any files used to generate it are newer.
|
|
try:
|
|
sys.argv.remove("--use-makefile-workaround")
|
|
use_makefile_workaround = True
|
|
except ValueError:
|
|
use_makefile_workaround = False
|
|
|
|
# NOTE: avoid `pathlib`, pulls in many modules indirectly, path handling is simple enough.
|
|
source_root = sys.argv[1]
|
|
output_cc_file = sys.argv[2]
|
|
function_to_generate = sys.argv[3]
|
|
source_cc_files = [
|
|
os.path.join(source_root, path)
|
|
for path in sys.argv[4:]
|
|
if path.endswith(".cc")
|
|
]
|
|
|
|
macro_name = "NOD_REGISTER_NODE"
|
|
discover_suffix = "_discover"
|
|
|
|
include_lines: list[str] = []
|
|
decl_lines: list[str] = []
|
|
func_lines: list[str] = []
|
|
|
|
# Add forward declaration to avoid warning.
|
|
func_lines.append(f"void {function_to_generate}();")
|
|
func_lines.append(f"void {function_to_generate}()")
|
|
func_lines.append("{")
|
|
|
|
# Use a single regular expression to search for opening name-spaces, closing name-spaces
|
|
# and macro invocations. This makes it easy to iterate over the matches in order.
|
|
re_namespace_begin = r"^namespace ([\w:]+) \{"
|
|
re_namespace_end = r"^\} // namespace ([\w:]+)"
|
|
re_macro = r"MACRO\((\w+)\)".replace("MACRO", macro_name)
|
|
re_all = f"({re_namespace_begin})|({re_namespace_end})|({re_macro})"
|
|
re_all_compiled = re.compile(re_all, flags=re.MULTILINE)
|
|
|
|
for path in source_cc_files:
|
|
# Read the source code.
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
code = fh.read()
|
|
|
|
# Keeps track of the current name-space we're in.
|
|
namespace_parts: list[str] = []
|
|
|
|
for match in re_all_compiled.finditer(code):
|
|
if entered_namespace := match.group(2):
|
|
# Enter a (nested) name-space.
|
|
namespace_parts += entered_namespace.split("::")
|
|
elif exited_namespace := match.group(4):
|
|
# Exit a (nested) name-space.
|
|
del namespace_parts[-len(exited_namespace.split("::")):]
|
|
elif function_name := match.group(6):
|
|
# Macro invocation in the current name-space.
|
|
namespace_str = "::".join(namespace_parts)
|
|
# Add suffix so that this refers to the function created by the macro.
|
|
auto_run_name = function_name + discover_suffix
|
|
|
|
# Declare either outside of any named name-space or in a (nested) name-space.
|
|
# Can't declare it in an anonymous name-space because that would make the
|
|
# declared function static.
|
|
if namespace_str:
|
|
decl_lines.append(f"namespace {namespace_str} {{")
|
|
decl_lines.append(f"void {auto_run_name}();")
|
|
if namespace_str:
|
|
decl_lines.append("}")
|
|
|
|
# Call the function.
|
|
func_lines.append(f" {namespace_str}::{auto_run_name}();")
|
|
|
|
func_lines.append("}")
|
|
|
|
# Write the generated code if it changed. If the newly generated code is the same as before,
|
|
# don't overwrite the existing file to avoid unnecessary rebuilds.
|
|
try:
|
|
with open(output_cc_file, "r", encoding="utf-8") as fh:
|
|
old_generated_code = fh.read()
|
|
except Exception:
|
|
old_generated_code = ""
|
|
new_generated_code = "\n".join(include_lines + decl_lines + [""] + func_lines)
|
|
|
|
if old_generated_code != new_generated_code:
|
|
with open(output_cc_file, "w", encoding="utf-8") as fh:
|
|
fh.write(new_generated_code)
|
|
elif use_makefile_workaround and filepath_is_older(output_cc_file, (__file__, *source_cc_files)):
|
|
# If the generated file is older than this command, this file would be generated every time.
|
|
os.utime(output_cc_file)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|