blob: 3d356c10e0b8e3d2bea2e011d7ca54e201254d4b [file] [log] [blame]
#!/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())