platform_build_blueprint/proptools/configurable.go
Cole Faust 6437d4e737 Select statements
Select statements are a new blueprint feature inspired by bazel's select
statements. They are essentially alternative syntax for soong config
variables that require less boilerplate. In addition, they support
making decisions based on a module's variant, which will eliminate
the need for manual property struct manipulation, such as the arch
mutator's arch: and target: properties.

In order to support decisions based on the variant, select statements
cannot be evaluated as soon as they're parsed. Instead, they must be
stored in the property struct unevaluated. This means that individual
properties need to change their type from say, string, to
Configurable[string]. Currently, only configurable strings, bools, and
string slices are supported, but more types can be added later.
The module implementation must call my_property.Evaluate(ctx) in order
to get the final, resolved value of the select statement.

Bug: 323382414
Test: go tests
Change-Id: I62f8721d7f0ac3d1df4a06d7eaa260a5aa7fcba3
2024-03-06 15:00:39 -08:00

281 lines
7.8 KiB
Go

// Copyright 2023 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 proptools
import (
"fmt"
"reflect"
"slices"
"github.com/google/blueprint/parser"
)
const default_select_branch_name = "__soong_conditions_default__"
type ConfigurableElements interface {
string | bool | []string
}
type ConfigurableEvaluator interface {
EvaluateConfiguration(parser.SelectType, string) (string, bool)
PropertyErrorf(property, fmt string, args ...interface{})
}
// configurableMarker is just so that reflection can check type of the first field of
// the struct to determine if it is a configurable struct.
type configurableMarker bool
var configurableMarkerType reflect.Type = reflect.TypeOf((*configurableMarker)(nil)).Elem()
// Configurable can wrap the type of a blueprint property,
// in order to allow select statements to be used in bp files
// for that property. For example, for the property struct:
//
// my_props {
// Property_a: string,
// Property_b: Configurable[string],
// }
//
// property_b can then use select statements:
//
// my_module {
// property_a: "foo"
// property_b: select soong_config_variable: "my_namespace" "my_variable" {
// "value_1": "bar",
// "value_2": "baz",
// default: "qux",
// }
// }
//
// The configurable property holds all the branches of the select
// statement in the bp file. To extract the final value, you must
// call Evaluate() on the configurable property.
//
// All configurable properties support being unset, so there is
// no need to use a pointer type like Configurable[*string].
type Configurable[T ConfigurableElements] struct {
marker configurableMarker
propertyName string
typ parser.SelectType
condition string
cases map[string]T
appendWrapper *appendWrapper[T]
}
// Ignore the warning about the unused marker variable, it's used via reflection
var _ configurableMarker = Configurable[string]{}.marker
// appendWrapper exists so that we can set the value of append
// from a non-pointer method receiver. (setAppend)
type appendWrapper[T ConfigurableElements] struct {
append Configurable[T]
}
func (c *Configurable[T]) GetType() parser.SelectType {
return c.typ
}
func (c *Configurable[T]) GetCondition() string {
return c.condition
}
// Evaluate returns the final value for the configurable property.
// A configurable property may be unset, in which case Evaluate will return nil.
func (c *Configurable[T]) Evaluate(evaluator ConfigurableEvaluator) *T {
if c == nil || c.appendWrapper == nil {
return nil
}
return mergeConfiguredValues(
c.evaluateNonTransitive(evaluator),
c.appendWrapper.append.Evaluate(evaluator),
c.propertyName,
evaluator,
)
}
func (c *Configurable[T]) evaluateNonTransitive(evaluator ConfigurableEvaluator) *T {
if c.typ == parser.SelectTypeUnconfigured {
if len(c.cases) == 0 {
return nil
} else if len(c.cases) != 1 {
panic(fmt.Sprintf("Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases)))
}
result, ok := c.cases[default_select_branch_name]
if !ok {
actual := ""
for k := range c.cases {
actual = k
}
panic(fmt.Sprintf("Expected the single branch of an unconfigured select to be %q, got %q", default_select_branch_name, actual))
}
return &result
}
val, defined := evaluator.EvaluateConfiguration(c.typ, c.condition)
if !defined {
if result, ok := c.cases[default_select_branch_name]; ok {
return &result
}
evaluator.PropertyErrorf(c.propertyName, "%s %q was not defined", c.typ.String(), c.condition)
return nil
}
if val == default_select_branch_name {
panic("Evaluator cannot return the default branch")
}
if result, ok := c.cases[val]; ok {
return &result
}
if result, ok := c.cases[default_select_branch_name]; ok {
return &result
}
evaluator.PropertyErrorf(c.propertyName, "%s %q had value %q, which was not handled by the select statement", c.typ.String(), c.condition, val)
return nil
}
func mergeConfiguredValues[T ConfigurableElements](a, b *T, propertyName string, evalutor ConfigurableEvaluator) *T {
if a == nil && b == nil {
return nil
}
switch any(a).(type) {
case *[]string:
var a2 []string
var b2 []string
if a != nil {
a2 = *any(a).(*[]string)
}
if b != nil {
b2 = *any(b).(*[]string)
}
result := make([]string, len(a2)+len(b2))
idx := 0
for i := 0; i < len(a2); i++ {
result[idx] = a2[i]
idx += 1
}
for i := 0; i < len(b2); i++ {
result[idx] = b2[i]
idx += 1
}
return any(&result).(*T)
case *string:
a := String(any(a).(*string))
b := String(any(b).(*string))
result := a + b
return any(&result).(*T)
case *bool:
numNonNil := 0
var nonNil *T
if a != nil {
numNonNil += 1
nonNil = a
}
if b != nil {
numNonNil += 1
nonNil = b
}
if numNonNil == 1 {
return nonNil
} else {
evalutor.PropertyErrorf(propertyName, "Cannot append bools")
return nil
}
default:
panic("Should be unreachable")
}
}
// configurableReflection is an interface that exposes some methods that are
// helpful when working with reflect.Values of Configurable objects, used by
// the property unpacking code. You can't call unexported methods from reflection,
// (at least without unsafe pointer trickery) so this is the next best thing.
type configurableReflection interface {
setAppend(append any)
configuredType() reflect.Type
cloneToReflectValuePtr() reflect.Value
isEmpty() bool
}
// Same as configurableReflection, but since initialize needs to take a pointer
// to a Configurable, it was broken out into a separate interface.
type configurablePtrReflection interface {
initialize(propertyName string, typ parser.SelectType, condition string, cases any)
}
var _ configurableReflection = Configurable[string]{}
var _ configurablePtrReflection = &Configurable[string]{}
func (c *Configurable[T]) initialize(propertyName string, typ parser.SelectType, condition string, cases any) {
c.propertyName = propertyName
c.typ = typ
c.condition = condition
c.cases = cases.(map[string]T)
c.appendWrapper = &appendWrapper[T]{}
}
func (c Configurable[T]) setAppend(append any) {
if c.appendWrapper.append.isEmpty() {
c.appendWrapper.append = append.(Configurable[T])
} else {
c.appendWrapper.append.setAppend(append)
}
}
func (c Configurable[T]) isEmpty() bool {
if c.appendWrapper != nil && !c.appendWrapper.append.isEmpty() {
return false
}
return c.typ == parser.SelectTypeUnconfigured && len(c.cases) == 0
}
func (c Configurable[T]) configuredType() reflect.Type {
return reflect.TypeOf((*T)(nil)).Elem()
}
func (c Configurable[T]) cloneToReflectValuePtr() reflect.Value {
return reflect.ValueOf(c.clone())
}
func (c *Configurable[T]) clone() *Configurable[T] {
if c == nil {
return nil
}
var inner *appendWrapper[T]
if c.appendWrapper != nil {
inner = &appendWrapper[T]{}
if !c.appendWrapper.append.isEmpty() {
inner.append = *c.appendWrapper.append.clone()
}
}
casesCopy := make(map[string]T, len(c.cases))
for k, v := range c.cases {
casesCopy[k] = copyConfiguredValue(v)
}
return &Configurable[T]{
propertyName: c.propertyName,
typ: c.typ,
condition: c.condition,
cases: casesCopy,
appendWrapper: inner,
}
}
func copyConfiguredValue[T ConfigurableElements](t T) T {
switch t2 := any(t).(type) {
case []string:
return any(slices.Clone(t2)).(T)
default:
return t
}
}