blob: 4e3555fa4e336f91706147d14196e6de0d7a6d26 [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 ##"
# LINT.ThenChange(//build/tools/bazel2gn/sentinel.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
# Include deleted files, e.g. a deleted BUILD.bazel next to a remaining
# bazel2gn-managed BUILD.gn must fail the check.
affected_build_files = ctx.scm.affected_files(glob = _BUILD_FILES_GLOB, include_deleted = True)
if not affected_build_files:
return []
gn_files = ctx.scm.all_files(glob = ["BUILD.gn"])
dirs = {}
for path in affected_build_files:
# 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",
"--json",
"--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:
return
# bazel2gn prints no diagnostics if it fails before syncing any directory,
# e.g. due to a bad flag. A non-zero exit must never pass silently.
diagnostics = json.decode(res.stdout, default = None)
if not diagnostics:
error_msg = res.stderr.strip() or res.stdout.strip() or "no error message"
ctx.emit.finding(
level = "error",
message = "bazel2gn failed: " + error_msg,
)
return
_emit_bazel2gn_findings(ctx, diagnostics)
# LINT.IfChange(bazel2gn_json)
def _emit_bazel2gn_findings(ctx, diagnostics):
"""Emits findings for the diagnostics printed by `bazel2gn --json`."""
out_of_sync_dirs = [diag["directory"] for diag in diagnostics if diag.get("out_of_sync")]
fix_cmd = "fx bazel2gn " + " ".join(["-d " + d for d in out_of_sync_dirs])
for diag in diagnostics:
if diag.get("out_of_sync"):
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, diag["message"])
else:
# The recipe that runs shac only shows the first line of each
# finding in the build summary, so the error must come first.
message = "bazel2gn failed: " + diag["message"]
filepath = diag.get("file", "")
line = diag.get("line", 0)
# Paths are relative to the Fuchsia root, which may not be the root of
# this repository (e.g. for vendor/google), and shac rejects findings
# on files it doesn't know about.
if filepath and not ctx.scm.all_files(glob = "/" + filepath):
message = "%s:%s: %s" % (filepath, line, message) if line else "%s: %s" % (filepath, message)
filepath, line = "", 0
ctx.emit.finding(level = "error", filepath = filepath, line = line, message = message)
# LINT.ThenChange(//build/tools/bazel2gn/cmd/main.go:bazel2gn_json)
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))