refactor Bazel variable export
Most of the variable export code for cc modules can be re-used for
exporting variables for java modules. Refactor this code into a more
composable structure for reuse.
Test: build/bazel/bp2build.sh
Test: manual comparison of
out/soong/soong_injection/cc_toolchain/constants.bzl
with previous output
Change-Id: Ie5a6fee08cc888b7dc69c3e324e5c3f8aa269a8f
diff --git a/android/config_bp2build.go b/android/config_bp2build.go
new file mode 100644
index 0000000..80b09fc
--- /dev/null
+++ b/android/config_bp2build.go
@@ -0,0 +1,435 @@
+// Copyright 2021 Google Inc. All rights reserved.
+//
+// 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
+//
+// http://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.
+
+package android
+
+import (
+ "fmt"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+
+ "android/soong/bazel"
+ "android/soong/starlark_fmt"
+
+ "github.com/google/blueprint"
+)
+
+// BazelVarExporter is a collection of configuration variables that can be exported for use in Bazel rules
+type BazelVarExporter interface {
+ // asBazel expands strings of configuration variables into their concrete values
+ asBazel(Config, ExportedStringVariables, ExportedStringListVariables, ExportedConfigDependingVariables) []bazelConstant
+}
+
+// ExportedVariables is a collection of interdependent configuration variables
+type ExportedVariables struct {
+ // Maps containing toolchain variables that are independent of the
+ // environment variables of the build.
+ exportedStringVars ExportedStringVariables
+ exportedStringListVars ExportedStringListVariables
+ exportedStringListDictVars ExportedStringListDictVariables
+
+ exportedVariableReferenceDictVars ExportedVariableReferenceDictVariables
+
+ /// Maps containing variables that are dependent on the build config.
+ exportedConfigDependingVars ExportedConfigDependingVariables
+
+ pctx PackageContext
+}
+
+// NewExportedVariables creats an empty ExportedVariables struct with non-nil maps
+func NewExportedVariables(pctx PackageContext) ExportedVariables {
+ return ExportedVariables{
+ exportedStringVars: ExportedStringVariables{},
+ exportedStringListVars: ExportedStringListVariables{},
+ exportedStringListDictVars: ExportedStringListDictVariables{},
+ exportedVariableReferenceDictVars: ExportedVariableReferenceDictVariables{},
+ exportedConfigDependingVars: ExportedConfigDependingVariables{},
+ pctx: pctx,
+ }
+}
+
+func (ev ExportedVariables) asBazel(config Config,
+ stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant {
+ ret := []bazelConstant{}
+ ret = append(ret, ev.exportedStringVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
+ ret = append(ret, ev.exportedStringListVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
+ ret = append(ret, ev.exportedStringListDictVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
+ // Note: ExportedVariableReferenceDictVars collections can only contain references to other variables and must be printed last
+ ret = append(ret, ev.exportedVariableReferenceDictVars.asBazel(config, stringVars, stringListVars, cfgDepVars)...)
+ return ret
+}
+
+// ExportStringStaticVariable declares a static string variable and exports it to
+// Bazel's toolchain.
+func (ev ExportedVariables) ExportStringStaticVariable(name string, value string) {
+ ev.pctx.StaticVariable(name, value)
+ ev.exportedStringVars.set(name, value)
+}
+
+// ExportStringListStaticVariable declares a static variable and exports it to
+// Bazel's toolchain.
+func (ev ExportedVariables) ExportStringListStaticVariable(name string, value []string) {
+ ev.pctx.StaticVariable(name, strings.Join(value, " "))
+ ev.exportedStringListVars.set(name, value)
+}
+
+// ExportVariableConfigMethod declares a variable whose value is evaluated at
+// runtime via a function with access to the Config and exports it to Bazel's
+// toolchain.
+func (ev ExportedVariables) ExportVariableConfigMethod(name string, method interface{}) blueprint.Variable {
+ ev.exportedConfigDependingVars.set(name, method)
+ return ev.pctx.VariableConfigMethod(name, method)
+}
+
+// ExportSourcePathVariable declares a static "source path" variable and exports
+// it to Bazel's toolchain.
+func (ev ExportedVariables) ExportSourcePathVariable(name string, value string) {
+ ev.pctx.SourcePathVariable(name, value)
+ ev.exportedStringVars.set(name, value)
+}
+
+// ExportString only exports a variable to Bazel, but does not declare it in Soong
+func (ev ExportedVariables) ExportString(name string, value string) {
+ ev.exportedStringVars.set(name, value)
+}
+
+// ExportStringList only exports a variable to Bazel, but does not declare it in Soong
+func (ev ExportedVariables) ExportStringList(name string, value []string) {
+ ev.exportedStringListVars.set(name, value)
+}
+
+// ExportStringListDict only exports a variable to Bazel, but does not declare it in Soong
+func (ev ExportedVariables) ExportStringListDict(name string, value map[string][]string) {
+ ev.exportedStringListDictVars.set(name, value)
+}
+
+// ExportVariableReferenceDict only exports a variable to Bazel, but does not declare it in Soong
+func (ev ExportedVariables) ExportVariableReferenceDict(name string, value map[string]string) {
+ ev.exportedVariableReferenceDictVars.set(name, value)
+}
+
+// ExportedConfigDependingVariables is a mapping of variable names to functions
+// of type func(config Config) string which return the runtime-evaluated string
+// value of a particular variable
+type ExportedConfigDependingVariables map[string]interface{}
+
+func (m ExportedConfigDependingVariables) set(k string, v interface{}) {
+ m[k] = v
+}
+
+// Ensure that string s has no invalid characters to be generated into the bzl file.
+func validateCharacters(s string) string {
+ for _, c := range []string{`\n`, `"`, `\`} {
+ if strings.Contains(s, c) {
+ panic(fmt.Errorf("%s contains illegal character %s", s, c))
+ }
+ }
+ return s
+}
+
+type bazelConstant struct {
+ variableName string
+ internalDefinition string
+ sortLast bool
+}
+
+// ExportedStringVariables is a mapping of variable names to string values
+type ExportedStringVariables map[string]string
+
+func (m ExportedStringVariables) set(k string, v string) {
+ m[k] = v
+}
+
+func (m ExportedStringVariables) asBazel(config Config,
+ stringVars ExportedStringVariables, stringListVars ExportedStringListVariables, cfgDepVars ExportedConfigDependingVariables) []bazelConstant {
+ ret := make([]bazelConstant, 0, len(m))
+ for k, variableValue := range m {
+ expandedVar, err := expandVar(config, variableValue, stringVars, stringListVars, cfgDepVars)
+ if err != nil {
+ panic(fmt.Errorf("error expanding config variable %s: %s", k, err))
+ }
+ if len(expandedVar) > 1 {
+ panic(fmt.Errorf("%s expands to more than one string value: %s", variableValue, expandedVar))
+ }
+ ret = append(ret, bazelConstant{
+ variableName: k,
+ internalDefinition: fmt.Sprintf(`"%s"`, validateCharacters(expandedVar[0])),
+ })
+ }
+ return ret
+}
+
+// ExportedStringListVariables is a mapping of variable names to a list of strings
+type ExportedStringListVariables map[string][]string
+
+func (m ExportedStringListVariables) set(k string, v []string) {
+ m[k] = v
+}
+
+func (m ExportedStringListVariables) asBazel(config Config,
+ stringScope ExportedStringVariables, stringListScope ExportedStringListVariables,
+ exportedVars ExportedConfigDependingVariables) []bazelConstant {
+ ret := make([]bazelConstant, 0, len(m))
+ // For each exported variable, recursively expand elements in the variableValue
+ // list to ensure that interpolated variables are expanded according to their values
+ // in the variable scope.
+ for k, variableValue := range m {
+ var expandedVars []string
+ for _, v := range variableValue {
+ expandedVar, err := expandVar(config, v, stringScope, stringListScope, exportedVars)
+ if err != nil {
+ panic(fmt.Errorf("Error expanding config variable %s=%s: %s", k, v, err))
+ }
+ expandedVars = append(expandedVars, expandedVar...)
+ }
+ // Assign the list as a bzl-private variable; this variable will be exported
+ // out through a constants struct later.
+ ret = append(ret, bazelConstant{
+ variableName: k,
+ internalDefinition: starlark_fmt.PrintStringList(expandedVars, 0),
+ })
+ }
+ return ret
+}
+
+// ExportedStringListDictVariables is a mapping from variable names to a
+// dictionary which maps keys to lists of strings
+type ExportedStringListDictVariables map[string]map[string][]string
+
+func (m ExportedStringListDictVariables) set(k string, v map[string][]string) {
+ m[k] = v
+}
+
+// Since dictionaries are not supported in Ninja, we do not expand variables for dictionaries
+func (m ExportedStringListDictVariables) asBazel(_ Config, _ ExportedStringVariables,
+ _ ExportedStringListVariables, _ ExportedConfigDependingVariables) []bazelConstant {
+ ret := make([]bazelConstant, 0, len(m))
+ for k, dict := range m {
+ ret = append(ret, bazelConstant{
+ variableName: k,
+ internalDefinition: starlark_fmt.PrintStringListDict(dict, 0),
+ })
+ }
+ return ret
+}
+
+// ExportedVariableReferenceDictVariables is a mapping from variable names to a
+// dictionary which references previously defined variables. This is used to
+// create a Starlark output such as:
+// string_var1 = "string1
+// var_ref_dict_var1 = {
+// "key1": string_var1
+// }
+// This type of variable collection must be expanded last so that it recognizes
+// previously defined variables.
+type ExportedVariableReferenceDictVariables map[string]map[string]string
+
+func (m ExportedVariableReferenceDictVariables) set(k string, v map[string]string) {
+ m[k] = v
+}
+
+func (m ExportedVariableReferenceDictVariables) asBazel(_ Config, _ ExportedStringVariables,
+ _ ExportedStringListVariables, _ ExportedConfigDependingVariables) []bazelConstant {
+ ret := make([]bazelConstant, 0, len(m))
+ for n, dict := range m {
+ for k, v := range dict {
+ matches, err := variableReference(v)
+ if err != nil {
+ panic(err)
+ } else if !matches.matches {
+ panic(fmt.Errorf("Expected a variable reference, got %q", v))
+ } else if len(matches.fullVariableReference) != len(v) {
+ panic(fmt.Errorf("Expected only a variable reference, got %q", v))
+ }
+ dict[k] = "_" + matches.variable
+ }
+ ret = append(ret, bazelConstant{
+ variableName: n,
+ internalDefinition: starlark_fmt.PrintDict(dict, 0),
+ sortLast: true,
+ })
+ }
+ return ret
+}
+
+// BazelToolchainVars expands an ExportedVariables collection and returns a string
+// of formatted Starlark variable definitions
+func BazelToolchainVars(config Config, exportedVars ExportedVariables) string {
+ results := exportedVars.asBazel(
+ config,
+ exportedVars.exportedStringVars,
+ exportedVars.exportedStringListVars,
+ exportedVars.exportedConfigDependingVars,
+ )
+
+ sort.Slice(results, func(i, j int) bool {
+ if results[i].sortLast != results[j].sortLast {
+ return !results[i].sortLast
+ }
+ return results[i].variableName < results[j].variableName
+ })
+
+ definitions := make([]string, 0, len(results))
+ constants := make([]string, 0, len(results))
+ for _, b := range results {
+ definitions = append(definitions,
+ fmt.Sprintf("_%s = %s", b.variableName, b.internalDefinition))
+ constants = append(constants,
+ fmt.Sprintf("%[1]s%[2]s = _%[2]s,", starlark_fmt.Indention(1), b.variableName))
+ }
+
+ // Build the exported constants struct.
+ ret := bazel.GeneratedBazelFileWarning
+ ret += "\n\n"
+ ret += strings.Join(definitions, "\n\n")
+ ret += "\n\n"
+ ret += "constants = struct(\n"
+ ret += strings.Join(constants, "\n")
+ ret += "\n)"
+
+ return ret
+}
+
+type match struct {
+ matches bool
+ fullVariableReference string
+ variable string
+}
+
+func variableReference(input string) (match, error) {
+ // e.g. "${ExternalCflags}"
+ r := regexp.MustCompile(`\${(?:config\.)?([a-zA-Z0-9_]+)}`)
+
+ matches := r.FindStringSubmatch(input)
+ if len(matches) == 0 {
+ return match{}, nil
+ }
+ if len(matches) != 2 {
+ return match{}, fmt.Errorf("Expected to only match 1 subexpression in %s, got %d", input, len(matches)-1)
+ }
+ return match{
+ matches: true,
+ fullVariableReference: matches[0],
+ // Index 1 of FindStringSubmatch contains the subexpression match
+ // (variable name) of the capture group.
+ variable: matches[1],
+ }, nil
+}
+
+// expandVar recursively expand interpolated variables in the exportedVars scope.
+//
+// We're using a string slice to track the seen variables to avoid
+// stackoverflow errors with infinite recursion. it's simpler to use a
+// string slice than to handle a pass-by-referenced map, which would make it
+// quite complex to track depth-first interpolations. It's also unlikely the
+// interpolation stacks are deep (n > 1).
+func expandVar(config Config, toExpand string, stringScope ExportedStringVariables,
+ stringListScope ExportedStringListVariables, exportedVars ExportedConfigDependingVariables) ([]string, error) {
+
+ // Internal recursive function.
+ var expandVarInternal func(string, map[string]bool) (string, error)
+ expandVarInternal = func(toExpand string, seenVars map[string]bool) (string, error) {
+ var ret string
+ remainingString := toExpand
+ for len(remainingString) > 0 {
+ matches, err := variableReference(remainingString)
+ if err != nil {
+ panic(err)
+ }
+ if !matches.matches {
+ return ret + remainingString, nil
+ }
+ matchIndex := strings.Index(remainingString, matches.fullVariableReference)
+ ret += remainingString[:matchIndex]
+ remainingString = remainingString[matchIndex+len(matches.fullVariableReference):]
+
+ variable := matches.variable
+ // toExpand contains a variable.
+ if _, ok := seenVars[variable]; ok {
+ return ret, fmt.Errorf(
+ "Unbounded recursive interpolation of variable: %s", variable)
+ }
+ // A map is passed-by-reference. Create a new map for
+ // this scope to prevent variables seen in one depth-first expansion
+ // to be also treated as "seen" in other depth-first traversals.
+ newSeenVars := map[string]bool{}
+ for k := range seenVars {
+ newSeenVars[k] = true
+ }
+ newSeenVars[variable] = true
+ if unexpandedVars, ok := stringListScope[variable]; ok {
+ expandedVars := []string{}
+ for _, unexpandedVar := range unexpandedVars {
+ expandedVar, err := expandVarInternal(unexpandedVar, newSeenVars)
+ if err != nil {
+ return ret, err
+ }
+ expandedVars = append(expandedVars, expandedVar)
+ }
+ ret += strings.Join(expandedVars, " ")
+ } else if unexpandedVar, ok := stringScope[variable]; ok {
+ expandedVar, err := expandVarInternal(unexpandedVar, newSeenVars)
+ if err != nil {
+ return ret, err
+ }
+ ret += expandedVar
+ } else if unevaluatedVar, ok := exportedVars[variable]; ok {
+ evalFunc := reflect.ValueOf(unevaluatedVar)
+ validateVariableMethod(variable, evalFunc)
+ evaluatedResult := evalFunc.Call([]reflect.Value{reflect.ValueOf(config)})
+ evaluatedValue := evaluatedResult[0].Interface().(string)
+ expandedVar, err := expandVarInternal(evaluatedValue, newSeenVars)
+ if err != nil {
+ return ret, err
+ }
+ ret += expandedVar
+ } else {
+ return "", fmt.Errorf("Unbound config variable %s", variable)
+ }
+ }
+ return ret, nil
+ }
+ var ret []string
+ for _, v := range strings.Split(toExpand, " ") {
+ val, err := expandVarInternal(v, map[string]bool{})
+ if err != nil {
+ return ret, err
+ }
+ ret = append(ret, val)
+ }
+
+ return ret, nil
+}
+
+func validateVariableMethod(name string, methodValue reflect.Value) {
+ methodType := methodValue.Type()
+ if methodType.Kind() != reflect.Func {
+ panic(fmt.Errorf("method given for variable %s is not a function",
+ name))
+ }
+ if n := methodType.NumIn(); n != 1 {
+ panic(fmt.Errorf("method for variable %s has %d inputs (should be 1)",
+ name, n))
+ }
+ if n := methodType.NumOut(); n != 1 {
+ panic(fmt.Errorf("method for variable %s has %d outputs (should be 1)",
+ name, n))
+ }
+ if kind := methodType.Out(0).Kind(); kind != reflect.String {
+ panic(fmt.Errorf("method for variable %s does not return a string",
+ name))
+ }
+}