blob: e71ab9082bbd8a32ce4aca44a9c67d1d03bbb837 [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.
package bazel2gn
import (
"errors"
"fmt"
"slices"
"strings"
"go.starlark.net/syntax"
)
const (
// Internal keyword arguments attached by `resolveLinuxResourceTargets` when
// a same-file binary target referenced by `linux_resource` produces a GN
// output file whose name differs from its target name. `$` is invalid in
// Starlark identifiers, so user `BUILD.bazel` files can never pass these.
internalBinOutputNameAttr = "$bin_output_name"
internalArch32BinOutputNameAttr = "$arch32_bin_output_name"
)
var (
allowedLinuxResourceAttrs = map[string]bool{
"name": true,
"src": true,
"dest": true,
"arch32_src": true,
"arch32_dest": true,
"testonly": true,
"visibility": true,
"tags": true,
"target_compatible_with": true,
internalBinOutputNameAttr: true,
internalArch32BinOutputNameAttr: true,
}
// Select conditions accepted on `arch32_src` (and `arch32_dest`) to guard
// 32-bit ARM Linux resources on arm64 Fuchsia builds.
arm64SelectConditions = map[string]bool{
"@platforms//cpu:arm64": true,
"@platforms//cpu:aarch64": true,
"//build/bazel/platforms:is_fuchsia_arm64": true,
}
osTargetConstraints = map[string]bool{
"@platforms//os:fuchsia": true,
"//build/bazel/platforms:linux_flavor_starnix": true,
"HOST_CONSTRAINTS": true,
"HOST_OS_CONSTRAINTS": true,
}
)
func isLinuxResourceBinaryRule(rule string) bool {
switch rule {
case "cc_binary", "fx_cc_binary", "fx_cc_test_binary", "rustc_binary", "rust_binary":
return true
default:
return false
}
}
func hasRawOutputNameAttr(rawAttrs []string) bool {
for _, raw := range rawAttrs {
if strings.HasPrefix(strings.TrimSpace(raw), "output_name") {
return true
}
}
return false
}
// resolveLinuxResourceTargets prepares a parsed `BUILD.bazel` file so Starnix
// `linux_resource()` targets and their same-file Linux binaries convert cleanly
// to GN:
//
// 1. Records `bin_output_name` / `arch32_bin_output_name` on each un-skipped
// `linux_resource()` whose `src` / `arch32_src` refers to an un-skipped
// binary target in the same file with a non-default GN output filename
// (for example, `rustc_binary` with `crate_name` / `binary_name`, or
// `fx_cc_binary(linkshared = True)`).
// 2. Adds `"//build/bazel/platforms:linux_flavor_starnix"` to
// `target_compatible_with` (wrapping the generated GN target in
// `if (is_linux && !is_host) { ... }`) on any un-skipped binary in the same
// file that is referenced by `linux_resource()` and has no other GN
// consumers in the file, so Linux-only `configs` / `sources` are not
// evaluated in Fuchsia or host toolchains.
func resolveLinuxResourceTargets(f *syntax.File) error {
gnBins, err := indexGNBinaries(f)
if err != nil {
return err
}
linuxResBins := map[string]bool{}
for _, stmt := range f.Stmts {
call, ok := callOfStmt(stmt)
if !ok {
continue
}
fn, ok := call.Fn.(*syntax.Ident)
if !ok || fn.Name != "linux_resource" {
continue
}
stmtSkipped, err := hasSkipAnnotation(stmt)
if err != nil {
return err
}
var extraArgs []syntax.Expr
for _, arg := range call.Args {
argSkipped, err := hasSkipAnnotation(arg)
if err != nil {
return err
}
if argSkipped {
continue
}
name, ok := attrName(arg)
if !ok {
continue
}
kv := arg.(*syntax.BinaryExpr)
switch name {
case "src":
if binTarget, ok := localTargetFromLinuxResourceExpr(kv, kv.Y); ok {
linuxResBins[binTarget] = true
if !stmtSkipped {
if gnOut, exists := gnBins[binTarget]; exists && gnOut.allowRenamed && gnOut.outFile != binTarget {
extraArgs = append(extraArgs, attrExpr(internalBinOutputNameAttr, stringLiteral(gnOut.outFile)))
}
}
}
case "arch32_src":
if binTarget, ok := localTargetFromArch32Expr(kv, kv.Y); ok {
linuxResBins[binTarget] = true
if !stmtSkipped {
if gnOut, exists := gnBins[binTarget]; exists && gnOut.allowRenamed && gnOut.outFile != binTarget {
extraArgs = append(extraArgs, attrExpr(internalArch32BinOutputNameAttr, stringLiteral(gnOut.outFile)))
}
}
}
}
}
if len(extraArgs) > 0 {
call.Args = append(call.Args, extraArgs...)
}
}
if len(linuxResBins) == 0 {
return nil
}
otherGNConsumers := map[string]bool{}
for _, stmt := range f.Stmts {
stmtSkipped, err := hasSkipAnnotation(stmt)
if err != nil {
return err
}
if stmtSkipped {
continue
}
if assign, ok := stmt.(*syntax.AssignStmt); ok {
walkLocalLabelRefs(assign.RHS, func(target string) {
otherGNConsumers[target] = true
})
continue
}
call, ok := callOfStmt(stmt)
if !ok {
continue
}
fn, ok := call.Fn.(*syntax.Ident)
if !ok {
continue
}
if fn.Name == "linux_resource" || fn.Name == "package" || bazelRuleToGNTemplate[fn.Name] == noGNEquivalent {
continue
}
var binName string
if isLinuxResourceBinaryRule(fn.Name) {
binName, _ = callStringAttr(call, "name")
}
for _, arg := range call.Args {
if skip, err := hasSkipAnnotation(arg); err != nil || skip {
continue
}
attr, ok := attrName(arg)
if !ok || attr == "name" || attr == "visibility" {
continue
}
kv := arg.(*syntax.BinaryExpr)
if (attr == "with_unit_tests" || attr == "with_host_unit_tests") && binName != "" {
if rhsIdent, ok := unwrapParenExpr(kv.Y).(*syntax.Ident); !ok || rhsIdent.Name != "False" {
otherGNConsumers[binName] = true
}
}
walkLocalLabelRefs(kv.Y, func(target string) {
otherGNConsumers[target] = true
})
}
}
for _, stmt := range f.Stmts {
stmtSkipped, err := hasSkipAnnotation(stmt)
if err != nil || stmtSkipped {
continue
}
call, ok := callOfStmt(stmt)
if !ok {
continue
}
fn, ok := call.Fn.(*syntax.Ident)
if !ok || !isLinuxResourceBinaryRule(fn.Name) {
continue
}
binName, ok := callStringAttr(call, "name")
if !ok || !linuxResBins[binName] || otherGNConsumers[binName] {
continue
}
ensureLinuxTargetCompatibleWith(call)
}
return nil
}
func stripLabelToolchain(label string) string {
if strings.HasSuffix(label, ")") {
if idx := strings.LastIndexByte(label, '('); idx >= 0 {
return label[:idx]
}
}
return label
}
func localTargetFromLinuxResourceExpr(owner syntax.Node, expr syntax.Expr) (string, bool) {
if _, hasRaw := overwrittenRawOnAny(owner, expr); hasRaw {
return "", false
}
if path, hasPath := overwrittenPathOnAny(owner, expr); hasPath {
if target, ok := strings.CutPrefix(stripLabelToolchain(path), ":"); ok && target != "" {
return target, true
}
return "", false
}
val, err := stringLiteralValue(expr)
if err != nil {
return "", false
}
target, ok := strings.CutPrefix(stripLabelToolchain(val), ":")
return target, ok && target != ""
}
func localTargetFromArch32Expr(owner syntax.Node, expr syntax.Expr) (string, bool) {
unwrapped := unwrapParenExpr(expr)
if isSelectCall(unwrapped) {
sel := unwrapped.(*syntax.CallExpr)
if len(sel.Args) == 0 {
return "", false
}
dict, ok := unwrapParenExpr(sel.Args[0]).(*syntax.DictExpr)
if !ok {
return "", false
}
for _, item := range dict.List {
entry, ok := item.(*syntax.DictEntry)
if !ok {
continue
}
key, err := stringLiteralValue(entry.Key)
if err != nil || !arm64SelectConditions[key] {
continue
}
return localTargetFromLinuxResourceExpr(entry, entry.Value)
}
return "", false
}
return localTargetFromLinuxResourceExpr(owner, unwrapped)
}
func walkLocalLabelRefs(root syntax.Node, visit func(target string)) {
if root == nil {
return
}
syntax.Walk(root, func(n syntax.Node) bool {
if n == nil {
return false
}
if skip, err := hasSkipAnnotation(n); err == nil && skip {
return false
}
lit, ok := n.(*syntax.Literal)
if !ok {
return true
}
label := ""
if overwritten, hasOverwrite := overwrittenPath(lit); hasOverwrite {
label = overwritten
} else if s, isStr := lit.Value.(string); isStr {
label = s
}
label = stripLabelToolchain(label)
if target, ok := strings.CutPrefix(label, ":"); ok && target != "" {
visit(target)
if base, hasTest := strings.CutSuffix(target, "_test"); hasTest && base != "" {
visit(base)
}
}
return true
})
}
func ensureLinuxTargetCompatibleWith(call *syntax.CallExpr) {
for _, arg := range call.Args {
if skip, err := hasSkipAnnotation(arg); err != nil || skip {
continue
}
attr, ok := attrName(arg)
if !ok || attr != "target_compatible_with" {
continue
}
kv := arg.(*syntax.BinaryExpr)
list, ok := unwrapParenExpr(kv.Y).(*syntax.ListExpr)
if !ok {
return
}
for _, elem := range list.List {
switch v := unwrapParenExpr(elem).(type) {
case *syntax.Literal:
if s, isStr := v.Value.(string); isStr && osTargetConstraints[s] {
return
}
case *syntax.Ident:
if osTargetConstraints[v.Name] {
return
}
}
}
list.List = append([]syntax.Expr{stringLiteral("//build/bazel/platforms:linux_flavor_starnix")}, slices.Clone(list.List)...)
return
}
call.Args = append(call.Args, attrExpr(
"target_compatible_with",
&syntax.ListExpr{List: []syntax.Expr{stringLiteral("//build/bazel/platforms:linux_flavor_starnix")}},
))
}
// linuxResourceToGN converts a Bazel `linux_resource()` rule call to a GN
// `linux_resource()` target (defined in //src/starnix/build/linux_resource.gni).
func linuxResourceToGN(call *syntax.CallExpr) ([]string, error) {
var name, binOutputName, arch32BinOutputName string
var srcArg, destArg, arch32SrcArg, arch32DestArg *syntax.BinaryExpr
var testonlyArg, tagsArg, visArg *syntax.BinaryExpr
var wrappingConditions []string
for _, arg := range call.Args {
binaryExpr, ok := arg.(*syntax.BinaryExpr)
if !ok || binaryExpr.Op != syntax.EQ {
return nil, fmt.Errorf("linux_resource only accepts keyword arguments, got %T", arg)
}
shouldSkip, err := hasSkipAnnotation(binaryExpr)
if err != nil {
return nil, fmt.Errorf("failed to check skip annotation: %v", err)
}
if shouldSkip {
continue
}
if hasRawAttributeAnnotation(binaryExpr) {
return nil, errors.New("found raw_attribute annotation on attribute, it should be above the target definition to take effect")
}
ident, ok := binaryExpr.X.(*syntax.Ident)
if !ok {
return nil, fmt.Errorf("expected identifier for argument name, got %T", binaryExpr.X)
}
if !allowedLinuxResourceAttrs[ident.Name] {
return nil, fmt.Errorf("bazel2gn doesn't support attribute %q of linux_resource; use # @bazel2gn:skip and write the GN copy by hand", ident.Name)
}
switch ident.Name {
case "name":
name, err = stringLiteralValue(binaryExpr.Y)
if err != nil {
return nil, fmt.Errorf("linux_resource name: %v", err)
}
case "src":
srcArg = binaryExpr
case "dest":
destArg = binaryExpr
case "arch32_src":
arch32SrcArg = binaryExpr
case "arch32_dest":
arch32DestArg = binaryExpr
case internalBinOutputNameAttr:
binOutputName, _ = stringLiteralValue(binaryExpr.Y)
case internalArch32BinOutputNameAttr:
arch32BinOutputName, _ = stringLiteralValue(binaryExpr.Y)
case "testonly":
testonlyArg = binaryExpr
case "tags":
tagsArg = binaryExpr
case "visibility":
visArg = binaryExpr
case "target_compatible_with":
wrappingConditions, err = targetCompatibleWithToGNConditions(binaryExpr.Y)
if err != nil {
return nil, fmt.Errorf("converting Bazel target_compatible_with to GN conditions: %v", err)
}
}
}
if name == "" {
return nil, errors.New("missing `name` attribute in linux_resource")
}
hasSrc := false
var srcGN, destVal string
if srcArg != nil && !isNoneIdent(srcArg.Y) {
var err error
srcGN, err = linuxResourceLabelToGN(srcArg, srcArg.Y, "src", name)
if err != nil {
return nil, err
}
hasSrc = true
}
if hasSrc {
if destArg == nil {
return nil, fmt.Errorf("missing `dest` attribute in linux_resource %q", name)
}
var err error
destVal, err = validateLinuxResourceDest(destArg.Y, "dest", name)
if err != nil {
return nil, err
}
} else if destArg != nil {
return nil, fmt.Errorf("`dest` attribute in linux_resource %q requires `src`", name)
}
hasArch32 := false
arch32Conditional := false
var arch32SrcGN, arch32DestVal string
if arch32SrcArg != nil && !isNoneIdent(arch32SrcArg.Y) {
unwrapped := unwrapParenExpr(arch32SrcArg.Y)
if isSelectCall(unwrapped) {
var err error
arch32SrcGN, hasArch32, err = parseArch32SelectLabelToGN(unwrapped.(*syntax.CallExpr), name)
if err != nil {
return nil, err
}
arch32Conditional = true
} else {
var err error
arch32SrcGN, err = linuxResourceLabelToGN(arch32SrcArg, unwrapped, "arch32_src", name)
if err != nil {
return nil, err
}
hasArch32 = true
}
}
if hasArch32 {
if arch32DestArg == nil {
return nil, fmt.Errorf("missing `arch32_dest` attribute in linux_resource %q when `arch32_src` is set", name)
}
var err error
arch32DestVal, err = parseArch32Dest(arch32DestArg.Y, name)
if err != nil {
return nil, err
}
} else if arch32DestArg != nil && arch32SrcArg == nil {
return nil, fmt.Errorf("`arch32_dest` attribute in linux_resource %q requires `arch32_src`", name)
}
if !hasSrc && !hasArch32 {
return nil, fmt.Errorf("linux_resource %q requires `src` or `arch32_src`", name)
}
ret := []string{fmt.Sprintf("linux_resource(%s) {", gnQuote(name))}
if testonlyArg != nil {
lines, err := attrAssignmentToGN(testonlyArg, "linux_resource", nil, false, false)
if err != nil {
return nil, fmt.Errorf("converting linux_resource testonly: %v", err)
}
ret = append(ret, indent(lines, 1)...)
}
if hasSrc {
ret = append(ret, indent([]string{fmt.Sprintf("src = %s", srcGN)}, 1)...)
if binOutputName != "" {
ret = append(ret, indent([]string{fmt.Sprintf("bin_output_name = %s", gnQuote(binOutputName))}, 1)...)
}
ret = append(ret, indent([]string{fmt.Sprintf("dest = %s", gnQuote(destVal))}, 1)...)
}
if hasArch32 {
arch32Lines := []string{fmt.Sprintf("arch32_src = %s", arch32SrcGN)}
if arch32BinOutputName != "" {
arch32Lines = append(arch32Lines, fmt.Sprintf("arch32_bin_output_name = %s", gnQuote(arch32BinOutputName)))
}
arch32Lines = append(arch32Lines, fmt.Sprintf("arch32_dest = %s", gnQuote(arch32DestVal)))
if arch32Conditional {
ret = append(ret, indent([]string{`if (target_cpu == "arm64") {`}, 1)...)
ret = append(ret, indent(arch32Lines, 2)...)
ret = append(ret, indent([]string{"}"}, 1)...)
} else {
ret = append(ret, indent(arch32Lines, 1)...)
}
}
if tagsArg != nil {
lines, err := attrAssignmentToGN(tagsArg, "linux_resource", nil, false, false)
if err != nil {
return nil, fmt.Errorf("converting linux_resource tags: %v", err)
}
ret = append(ret, indent(lines, 1)...)
}
if visArg != nil {
lines, err := attrAssignmentToGN(visArg, "linux_resource", nil, false, false)
if err != nil {
return nil, fmt.Errorf("converting linux_resource visibility: %v", err)
}
ret = append(ret, indent(lines, 1)...)
}
ret = append(ret, "}")
if len(wrappingConditions) > 0 {
ret = append([]string{
fmt.Sprintf("if (%s) {", strings.Join(wrappingConditions, " && ")),
}, indent(ret, 1)...)
ret = append(ret, "}")
}
return ret, nil
}
func isNoneIdent(expr syntax.Expr) bool {
ident, ok := unwrapParenExpr(expr).(*syntax.Ident)
return ok && ident.Name == "None"
}
func overwrittenPathOnAny(nodes ...syntax.Node) (string, bool) {
for _, n := range nodes {
if n == nil || n.Comments() == nil {
continue
}
for _, c := range n.Comments().Suffix {
if after, ok := strings.CutPrefix(c.Text, pathOverwriteAnnotationPrefix); ok {
return strings.TrimSpace(after), true
}
}
}
return "", false
}
func overwrittenRawOnAny(nodes ...syntax.Node) (string, bool) {
for _, n := range nodes {
if raw, ok := overwrittenRaw(n); ok {
return raw, true
}
}
return "", false
}
func linuxResourceLabelToGN(owner syntax.Node, expr syntax.Expr, attr, targetName string) (string, error) {
if raw, ok := overwrittenRawOnAny(owner, expr); ok {
return raw, nil
}
if path, ok := overwrittenPathOnAny(owner, expr); ok {
return gnQuote(path), nil
}
lit, ok := unwrapParenExpr(expr).(*syntax.Literal)
if !ok {
return "", fmt.Errorf("linux_resource %q %s must be a string literal label, got %T", targetName, attr, expr)
}
if _, isStr := lit.Value.(string); !isStr {
return "", fmt.Errorf("linux_resource %q %s must be a string literal label, got %s", targetName, attr, strings.TrimSpace(lit.Raw))
}
cloned := *lit
transformed, err := bazelDepToGN(&cloned)
if err != nil {
return "", fmt.Errorf("linux_resource %q %s: %v", targetName, attr, err)
}
return transformed.(*syntax.Literal).Raw, nil
}
func validateLinuxResourceDest(expr syntax.Expr, attr, targetName string) (string, error) {
dest, err := stringLiteralValue(expr)
if err != nil {
return "", fmt.Errorf("linux_resource %q %s: %v", targetName, attr, err)
}
if dest == "" {
return "", fmt.Errorf("linux_resource %q %s must not be empty", targetName, attr)
}
if strings.HasPrefix(dest, "/") {
return "", fmt.Errorf("linux_resource %q %s %q must not start with \"/\"", targetName, attr, dest)
}
return dest, nil
}
func parseArch32SelectLabelToGN(sel *syntax.CallExpr, targetName string) (string, bool, error) {
if len(sel.Args) != 1 {
return "", false, fmt.Errorf("linux_resource %q arch32_src select() must take a single dictionary argument", targetName)
}
dict, ok := unwrapParenExpr(sel.Args[0]).(*syntax.DictExpr)
if !ok {
return "", false, fmt.Errorf("linux_resource %q arch32_src select() argument must be a dictionary literal, got %T", targetName, sel.Args[0])
}
if len(dict.List) != 2 {
return "", false, fmt.Errorf("linux_resource %q arch32_src select() must have an arm64 branch and %q: None", targetName, defaultBazelSelectCondition)
}
var arm64Entry *syntax.DictEntry
hasDefaultNone := false
for _, item := range dict.List {
entry, ok := item.(*syntax.DictEntry)
if !ok {
return "", false, fmt.Errorf("unexpected dictionary entry %T in linux_resource %q arch32_src", item, targetName)
}
cond, err := stringLiteralValue(entry.Key)
if err != nil {
return "", false, fmt.Errorf("linux_resource %q arch32_src select() key: %v", targetName, err)
}
switch {
case cond == defaultBazelSelectCondition:
if !isNoneIdent(entry.Value) {
return "", false, fmt.Errorf("linux_resource %q arch32_src select() %q branch must be None", targetName, defaultBazelSelectCondition)
}
hasDefaultNone = true
case arm64SelectConditions[cond]:
if arm64Entry != nil {
return "", false, fmt.Errorf("linux_resource %q arch32_src select() has multiple arm64 branches", targetName)
}
arm64Entry = entry
default:
return "", false, fmt.Errorf("unsupported condition %q in linux_resource %q arch32_src select(); expected \"@platforms//cpu:arm64\" and %q: None", cond, targetName, defaultBazelSelectCondition)
}
}
if !hasDefaultNone || arm64Entry == nil {
return "", false, fmt.Errorf("linux_resource %q arch32_src select() must have an arm64 branch and %q: None", targetName, defaultBazelSelectCondition)
}
if isNoneIdent(arm64Entry.Value) {
return "", false, nil
}
gnLabel, err := linuxResourceLabelToGN(arm64Entry, arm64Entry.Value, "arch32_src", targetName)
if err != nil {
return "", false, err
}
return gnLabel, true, nil
}
func parseArch32Dest(expr syntax.Expr, targetName string) (string, error) {
unwrapped := unwrapParenExpr(expr)
if !isSelectCall(unwrapped) {
return validateLinuxResourceDest(unwrapped, "arch32_dest", targetName)
}
sel := unwrapped.(*syntax.CallExpr)
if len(sel.Args) != 1 {
return "", fmt.Errorf("linux_resource %q arch32_dest select() must take a single dictionary argument", targetName)
}
dict, ok := unwrapParenExpr(sel.Args[0]).(*syntax.DictExpr)
if !ok || len(dict.List) != 2 {
return "", fmt.Errorf("linux_resource %q arch32_dest select() must have an arm64 branch and %q", targetName, defaultBazelSelectCondition)
}
var arm64Dest string
hasDefault := false
for _, item := range dict.List {
entry, ok := item.(*syntax.DictEntry)
if !ok {
return "", fmt.Errorf("unexpected dictionary entry %T in linux_resource %q arch32_dest", item, targetName)
}
cond, err := stringLiteralValue(entry.Key)
if err != nil {
return "", fmt.Errorf("linux_resource %q arch32_dest select() key: %v", targetName, err)
}
switch {
case cond == defaultBazelSelectCondition:
if !isNoneIdent(entry.Value) {
defVal, err := stringLiteralValue(entry.Value)
if err != nil || defVal != "" {
return "", fmt.Errorf("linux_resource %q arch32_dest select() %q branch must be \"\" or None", targetName, defaultBazelSelectCondition)
}
}
hasDefault = true
case arm64SelectConditions[cond]:
if arm64Dest != "" {
return "", fmt.Errorf("linux_resource %q arch32_dest select() has multiple arm64 branches", targetName)
}
arm64Dest, err = validateLinuxResourceDest(entry.Value, "arch32_dest", targetName)
if err != nil {
return "", err
}
default:
return "", fmt.Errorf("unsupported condition %q in linux_resource %q arch32_dest select()", cond, targetName)
}
}
if !hasDefault || arm64Dest == "" {
return "", fmt.Errorf("linux_resource %q arch32_dest select() must have an arm64 branch and %q", targetName, defaultBazelSelectCondition)
}
return arm64Dest, nil
}