| #!/usr/bin/env python3 | |
| # Copyright 2025 Google LLC | |
| # | |
| # 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 | |
| # | |
| # https://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. | |
| """Creates and validates the `build.json` file.""" | |
| import argparse | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| # The Bazel targets that define the embossc compiler sources. | |
| EMBOSSC_TARGETS = [ | |
| "//compiler/back_end/cpp:emboss_codegen_cpp", | |
| "//compiler/front_end:emboss_front_end", | |
| ] | |
| # The Bazel target for the C++ runtime sources. | |
| RUNTIME_CPP_TARGET = "//runtime/cpp:cpp_utils" | |
| def get_bazel_query_output(query): | |
| """Runs a Bazel query and returns the output as a list of strings.""" | |
| result = subprocess.run( | |
| ["bazel", "query", query], | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ) | |
| return result.stdout.strip().split("\n") | |
| def get_source_files_for_targets(targets): | |
| """Queries Bazel for all source files in the transitive dependencies.""" | |
| query = f"kind('source file', deps({'+'.join(targets)}))" | |
| output = get_bazel_query_output(query) | |
| return sorted( | |
| [ | |
| f.replace("//", "").replace(":", "/") | |
| for f in output | |
| if f.startswith("//") and not f.startswith("//external") | |
| ] | |
| ) | |
| def read_build_json(file_path): | |
| """Reads build.json, strips comments, and returns the parsed JSON data.""" | |
| with open(file_path, "r") as f: | |
| content = "".join(line for line in f if not line.strip().startswith("//")) | |
| return json.loads(content) | |
| def find_init_py_files(source_files): | |
| """Finds all __init__.py files in the directories of the source files.""" | |
| init_py_files = set() | |
| checked_dirs = set() | |
| for source_file in source_files: | |
| dir_path = os.path.dirname(source_file) | |
| while dir_path and dir_path not in checked_dirs: | |
| checked_dirs.add(dir_path) | |
| init_py = os.path.join(dir_path, "__init__.py") | |
| if os.path.exists(init_py): | |
| init_py_files.add(init_py) | |
| # Move to the parent directory | |
| parent_dir = os.path.dirname(dir_path) | |
| if parent_dir == dir_path: # Root directory | |
| break | |
| dir_path = parent_dir | |
| return sorted(list(init_py_files)) | |
| def generate_build_json(output_path): | |
| """Generates the build.json file from Bazel sources.""" | |
| print("Generating fresh source lists from Bazel...") | |
| embossc_sources = get_source_files_for_targets(EMBOSSC_TARGETS) | |
| embossc_sources.append("embossc") | |
| init_py_sources = find_init_py_files(embossc_sources) | |
| all_embossc_sources = sorted(list(set(embossc_sources + init_py_sources))) | |
| runtime_cpp_sources = get_source_files_for_targets([RUNTIME_CPP_TARGET]) | |
| build_data = { | |
| "embossc_sources": all_embossc_sources, | |
| "emboss_runtime_cpp_sources": runtime_cpp_sources, | |
| } | |
| # Use json.dumps to get a formatted string, then inject comments. | |
| json_string = json.dumps(build_data, indent=2) | |
| json_string = json_string.replace( | |
| '"embossc_sources":', | |
| '// A list of all source files required to build the Emboss compiler.\n "embossc_sources":', | |
| ) | |
| json_string = json_string.replace( | |
| '"emboss_runtime_cpp_sources":', | |
| '// A list of all source files required for the Emboss C++ runtime.\n "emboss_runtime_cpp_sources":', | |
| ) | |
| with open(output_path, "w") as f: | |
| f.write(json_string) | |
| f.write("\n") | |
| print(f"Successfully generated {output_path}") | |
| return 0 | |
| def validate_build_json(file_path): | |
| """Validates that the on-disk build.json file is up-to-date.""" | |
| print("Generating fresh source lists from Bazel for validation...") | |
| fresh_embossc_sources = get_source_files_for_targets(EMBOSSC_TARGETS) | |
| fresh_embossc_sources.append("embossc") | |
| init_py_sources = find_init_py_files(fresh_embossc_sources) | |
| all_fresh_embossc_sources = sorted( | |
| list(set(fresh_embossc_sources + init_py_sources)) | |
| ) | |
| fresh_runtime_cpp_sources = get_source_files_for_targets([RUNTIME_CPP_TARGET]) | |
| print(f"Reading existing sources from {file_path}...") | |
| on_disk_data = read_build_json(file_path) | |
| on_disk_embossc_sources = on_disk_data.get("embossc_sources", []) | |
| on_disk_runtime_cpp_sources = on_disk_data.get("emboss_runtime_cpp_sources", []) | |
| embossc_diff = set(all_fresh_embossc_sources) ^ set(on_disk_embossc_sources) | |
| runtime_diff = set(fresh_runtime_cpp_sources) ^ set(on_disk_runtime_cpp_sources) | |
| if not embossc_diff and not runtime_diff: | |
| print("build.json is up-to-date.") | |
| return 0 | |
| print("\nERROR: build.json is out of date!", file=sys.stderr) | |
| def print_diff(title, fresh_set, on_disk_set): | |
| added = sorted(list(fresh_set - on_disk_set)) | |
| removed = sorted(list(on_disk_set - fresh_set)) | |
| if added or removed: | |
| print(f" {title}:", file=sys.stderr) | |
| for f in added: | |
| print(f" + {f}", file=sys.stderr) | |
| for f in removed: | |
| print(f" - {f}", file=sys.stderr) | |
| print_diff( | |
| "embossc_sources", | |
| set(fresh_embossc_sources), | |
| set(on_disk_embossc_sources), | |
| ) | |
| print_diff( | |
| "emboss_runtime_cpp_sources", | |
| set(fresh_runtime_cpp_sources), | |
| set(on_disk_runtime_cpp_sources), | |
| ) | |
| print( | |
| "\nPlease run 'scripts/build_helpers/manage_build_json.py' to update.", | |
| file=sys.stderr, | |
| ) | |
| return 1 | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Manage the build.json file.") | |
| parser.add_argument( | |
| "--validate", | |
| action="store_true", | |
| help="Validate that build.json is up-to-date.", | |
| ) | |
| args = parser.parse_args() | |
| workspace_root = os.environ.get("BUILD_WORKSPACE_DIRECTORY", os.getcwd()) | |
| build_json_path = os.path.join(workspace_root, "build.json") | |
| if args.validate: | |
| return validate_build_json(build_json_path) | |
| else: | |
| return generate_build_json(build_json_path) | |
| if __name__ == "__main__": | |
| sys.exit(main()) |