blob: a3443e2ba0d81dcd32cddc983782e7e2f728a3cd [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Defines SHAC checks for the GN-to-Bazel migration.
These checks are only needed until the migration from GN to Bazel is complete.
"""
load(
"./common.star",
"cipd_platform_name",
"compiled_tool_path",
"get_fuchsia_dir",
"os_exec",
)
# LINT.IfChange(bazel2gn_output)
_BAZEL2GN_SENTINEL = "## BAZEL2GN SENTINEL - DO NOT EDIT BELOW THIS LINE ##"
_BATCH_ERROR_PREFIX = "* syncing directory "
_OUT_OF_SYNC_MESSAGE = "BUILD.gn is out of sync with BUILD.bazel"
# LINT.ThenChange(//build/tools/bazel2gn/cmd/main.go:bazel2gn_output)
# Changes to these files can change bazel2gn's output for any directory, so
# trigger a check of all bazel2gn-managed directories.
_BAZEL2GN_INPUTS_GLOB = [
"/build/tools/bazel2gn/**",
# Pins the gn and buildifier prebuilts.
"/manifests/prebuilts",
"/scripts/shac/bazel_migration.star",
]
# Patterns without slashes match at any level, including the root.
_BUILD_FILES_GLOB = ["BUILD.gn", "BUILD.bazel"]
def _enforce_bazel_build_file_for_new_fidl_library(ctx):
"""Checks that a BUILD.bazel file exists when a new FIDL library is created in GN."""
# Check when :
# 1. The file is a new BUILD.gn file added under //sdk/fidl/.
# 2. The BUILD.gn file contains at least one fidl() template.
for path, meta in ctx.scm.affected_files(glob = "BUILD.gn").items():
if meta.action != "A":
continue
# TODO(https://fxbug.dev/487883318): Extend the check to FIDL libraries outside //sdk/fidl directory.
if not path.startswith("sdk/fidl/"):
continue
# Read the BUILD.gn content
content = str(ctx.io.read_file(ctx.scm.root + "/" + path))
# Check if it contains at least one fidl() template
has_fidl = False
for line in content.splitlines():
# Ignore comments
hash_idx = line.find("#")
if hash_idx != -1:
line_code = line[:hash_idx]
else:
line_code = line
if ctx.re.allmatches(r"\bfidl\s*\(\s*[\"\']", line_code):
has_fidl = True
break
# Skip if the BUILD.gn file doesn't contain any fidl() template
if not has_fidl:
continue
# Check if BUILD.bazel exists in the same directory
bazel_path = "/".join(path.split("/")[:-1] + ["BUILD.bazel"])
if not ctx.scm.all_files(glob = bazel_path):
ctx.emit.finding(
level = "error",
filepath = path,
message = "BUILD.bazel file is missing in the same directory as the added FIDL BUILD.gn file.",
)
def _dirname(path):
return path.rpartition("/")[0] or "."
def _dirs_to_check(ctx):
"""Returns the bazel2gn-managed directories to check, or None for all.
Under `shac check --all`, every file is affected, including bazel2gn's
inputs, so all directories are checked. That only takes a few seconds.
"""
if ctx.scm.affected_files(glob = _BAZEL2GN_INPUTS_GLOB, include_deleted = True):
return None
gn_files = ctx.scm.all_files(glob = ["BUILD.gn"])
dirs = {}
# Include deleted files, e.g. a deleted BUILD.bazel next to a remaining
# bazel2gn-managed BUILD.gn must fail the check.
for path in ctx.scm.affected_files(glob = _BUILD_FILES_GLOB, include_deleted = True):
# Check the BUILD.gn in the same directory, which also covers affected
# BUILD.bazel files.
d = _dirname(path)
if d in dirs:
continue
gn_path = "BUILD.gn" if d == "." else d + "/BUILD.gn"
if gn_path in gn_files and _BAZEL2GN_SENTINEL in str(ctx.io.read_file(ctx.scm.root + "/" + gn_path)):
dirs[d] = True
return sorted(dirs.keys())
def _verify_bazel2gn(ctx):
"""Verifies BUILD.gn files managed by bazel2gn are in sync with BUILD.bazel."""
fuchsia_dir = get_fuchsia_dir(ctx)
dirs = _dirs_to_check(ctx)
if dirs == []:
return
platform = cipd_platform_name(ctx)
cmd = [
compiled_tool_path(ctx, "bazel2gn"),
"--check_only",
"--fuchsia_dir",
fuchsia_dir,
"--gn_bin",
"%s/prebuilt/third_party/gn/%s/gn" % (fuchsia_dir, platform),
"--buildifier_bin",
"%s/prebuilt/third_party/buildifier/%s/buildifier" % (fuchsia_dir, platform),
]
if dirs != None:
# Without --directory_list, bazel2gn checks all bazel2gn-managed
# directories.
cmd += ["--directory_list", ctx.io.tempfile("\n".join(dirs) + "\n")]
res = os_exec(ctx, cmd, ok_retcodes = (0, 1)).wait()
if res.retcode != 0:
_emit_bazel2gn_findings(ctx, res.stderr + "\n" + res.stdout)
def _emit_bazel2gn_findings(ctx, output):
"""Emits findings for a failed `bazel2gn --check_only` run."""
# Per-directory errors, keyed by directory.
errors = {}
for chunk in ("\n" + output).split("\n" + _BATCH_ERROR_PREFIX)[1:]:
d, _, err = chunk.partition(": ")
errors[d] = err.strip()
out_of_sync = sorted([d for d, err in errors.items() if err.startswith(_OUT_OF_SYNC_MESSAGE)])
fix_cmd = "fx bazel2gn " + " ".join(["-d " + d for d in out_of_sync])
for d in out_of_sync:
ctx.emit.finding(
level = "error",
filepath = "BUILD.gn" if d == "." else d + "/BUILD.gn",
message = (
"This file is out of sync with BUILD.bazel. Update BUILD.bazel " +
"instead of editing below the BAZEL2GN SENTINEL, then sync " +
"all out-of-sync directories with:\n %s\n\n%s"
) % (fix_cmd, errors[d][len(_OUT_OF_SYNC_MESSAGE):].strip()),
)
# Any other errors, e.g. a BUILD.bazel that fails to parse. A non-zero exit
# must never pass silently.
other_errors = [
_BATCH_ERROR_PREFIX + "%s: %s" % (d, err)
for d, err in errors.items()
if d not in out_of_sync
]
if other_errors or not out_of_sync:
ctx.emit.finding(
level = "error",
message = "bazel2gn failed:\n" + ("\n".join(other_errors) or output.strip()),
)
def register_bazel_migration_checks():
shac.register_check(shac.check(_enforce_bazel_build_file_for_new_fidl_library))
shac.register_check(shac.check(_verify_bazel2gn))