4560bb086e
Sometimes modules add arch-variant properties in load hooks, to disable modules by default on certain platforms for example. When changing the property to a Configurable property, these load hooks would also need to be changed in order to have a matching type for ExtendMatchingProperties. Since this can be kindof a pain to address everywhere, for now, special case the extension functions to promote non-configurable properties to configurable ones. We can remove this later when everything switches to configurable properties. Bug: 323382414 Test: go tests Change-Id: Iac96587dbd60ccdd6aa667dd69a71ad252abe589
554 lines
15 KiB
Go
554 lines
15 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"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type ConfigurableElements interface {
|
|
string | bool | []string
|
|
}
|
|
|
|
type ConfigurableEvaluator interface {
|
|
EvaluateConfiguration(condition ConfigurableCondition, property string) ConfigurableValue
|
|
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()
|
|
|
|
type ConfigurableCondition struct {
|
|
FunctionName string
|
|
Args []string
|
|
}
|
|
|
|
func (c *ConfigurableCondition) String() string {
|
|
var sb strings.Builder
|
|
sb.WriteString(c.FunctionName)
|
|
sb.WriteRune('(')
|
|
for i, arg := range c.Args {
|
|
sb.WriteString(strconv.Quote(arg))
|
|
if i < len(c.Args)-1 {
|
|
sb.WriteString(", ")
|
|
}
|
|
}
|
|
sb.WriteRune(')')
|
|
return sb.String()
|
|
}
|
|
|
|
type configurableValueType int
|
|
|
|
const (
|
|
configurableValueTypeString configurableValueType = iota
|
|
configurableValueTypeBool
|
|
configurableValueTypeUndefined
|
|
)
|
|
|
|
func (v *configurableValueType) patternType() configurablePatternType {
|
|
switch *v {
|
|
case configurableValueTypeString:
|
|
return configurablePatternTypeString
|
|
case configurableValueTypeBool:
|
|
return configurablePatternTypeBool
|
|
default:
|
|
panic("unimplemented")
|
|
}
|
|
}
|
|
|
|
func (v *configurableValueType) String() string {
|
|
switch *v {
|
|
case configurableValueTypeString:
|
|
return "string"
|
|
case configurableValueTypeBool:
|
|
return "bool"
|
|
case configurableValueTypeUndefined:
|
|
return "undefined"
|
|
default:
|
|
panic("unimplemented")
|
|
}
|
|
}
|
|
|
|
// ConfigurableValue represents the value of a certain condition being selected on.
|
|
// This type mostly exists to act as a sum type between string, bool, and undefined.
|
|
type ConfigurableValue struct {
|
|
typ configurableValueType
|
|
stringValue string
|
|
boolValue bool
|
|
}
|
|
|
|
func (c *ConfigurableValue) String() string {
|
|
switch c.typ {
|
|
case configurableValueTypeString:
|
|
return strconv.Quote(c.stringValue)
|
|
case configurableValueTypeBool:
|
|
if c.boolValue {
|
|
return "true"
|
|
} else {
|
|
return "false"
|
|
}
|
|
case configurableValueTypeUndefined:
|
|
return "undefined"
|
|
default:
|
|
panic("unimplemented")
|
|
}
|
|
}
|
|
|
|
func ConfigurableValueString(s string) ConfigurableValue {
|
|
return ConfigurableValue{
|
|
typ: configurableValueTypeString,
|
|
stringValue: s,
|
|
}
|
|
}
|
|
|
|
func ConfigurableValueBool(b bool) ConfigurableValue {
|
|
return ConfigurableValue{
|
|
typ: configurableValueTypeBool,
|
|
boolValue: b,
|
|
}
|
|
}
|
|
|
|
func ConfigurableValueUndefined() ConfigurableValue {
|
|
return ConfigurableValue{
|
|
typ: configurableValueTypeUndefined,
|
|
}
|
|
}
|
|
|
|
type configurablePatternType int
|
|
|
|
const (
|
|
configurablePatternTypeString configurablePatternType = iota
|
|
configurablePatternTypeBool
|
|
configurablePatternTypeDefault
|
|
)
|
|
|
|
func (v *configurablePatternType) String() string {
|
|
switch *v {
|
|
case configurablePatternTypeString:
|
|
return "string"
|
|
case configurablePatternTypeBool:
|
|
return "bool"
|
|
case configurablePatternTypeDefault:
|
|
return "default"
|
|
default:
|
|
panic("unimplemented")
|
|
}
|
|
}
|
|
|
|
type ConfigurablePattern struct {
|
|
typ configurablePatternType
|
|
stringValue string
|
|
boolValue bool
|
|
}
|
|
|
|
func NewStringConfigurablePattern(s string) ConfigurablePattern {
|
|
return ConfigurablePattern{
|
|
typ: configurablePatternTypeString,
|
|
stringValue: s,
|
|
}
|
|
}
|
|
|
|
func NewBoolConfigurablePattern(b bool) ConfigurablePattern {
|
|
return ConfigurablePattern{
|
|
typ: configurablePatternTypeBool,
|
|
boolValue: b,
|
|
}
|
|
}
|
|
|
|
func NewDefaultConfigurablePattern() ConfigurablePattern {
|
|
return ConfigurablePattern{
|
|
typ: configurablePatternTypeDefault,
|
|
}
|
|
}
|
|
|
|
func (p *ConfigurablePattern) matchesValue(v ConfigurableValue) bool {
|
|
if p.typ == configurablePatternTypeDefault {
|
|
return true
|
|
}
|
|
if v.typ == configurableValueTypeUndefined {
|
|
return false
|
|
}
|
|
if p.typ != v.typ.patternType() {
|
|
return false
|
|
}
|
|
switch p.typ {
|
|
case configurablePatternTypeString:
|
|
return p.stringValue == v.stringValue
|
|
case configurablePatternTypeBool:
|
|
return p.boolValue == v.boolValue
|
|
default:
|
|
panic("unimplemented")
|
|
}
|
|
}
|
|
|
|
func (p *ConfigurablePattern) matchesValueType(v ConfigurableValue) bool {
|
|
if p.typ == configurablePatternTypeDefault {
|
|
return true
|
|
}
|
|
if v.typ == configurableValueTypeUndefined {
|
|
return true
|
|
}
|
|
return p.typ == v.typ.patternType()
|
|
}
|
|
|
|
type ConfigurableCase[T ConfigurableElements] struct {
|
|
patterns []ConfigurablePattern
|
|
value *T
|
|
}
|
|
|
|
func (c *ConfigurableCase[T]) Clone() ConfigurableCase[T] {
|
|
return ConfigurableCase[T]{
|
|
patterns: slices.Clone(c.patterns),
|
|
value: copyConfiguredValue(c.value),
|
|
}
|
|
}
|
|
|
|
type configurableCaseReflection interface {
|
|
initialize(patterns []ConfigurablePattern, value interface{})
|
|
}
|
|
|
|
var _ configurableCaseReflection = &ConfigurableCase[string]{}
|
|
|
|
func NewConfigurableCase[T ConfigurableElements](patterns []ConfigurablePattern, value *T) ConfigurableCase[T] {
|
|
return ConfigurableCase[T]{
|
|
patterns: patterns,
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
func (c *ConfigurableCase[T]) initialize(patterns []ConfigurablePattern, value interface{}) {
|
|
c.patterns = patterns
|
|
c.value = value.(*T)
|
|
}
|
|
|
|
// for the given T, return the reflect.type of configurableCase[T]
|
|
func configurableCaseType(configuredType reflect.Type) reflect.Type {
|
|
// I don't think it's possible to do this generically with go's
|
|
// current reflection apis unfortunately
|
|
switch configuredType.Kind() {
|
|
case reflect.String:
|
|
return reflect.TypeOf(ConfigurableCase[string]{})
|
|
case reflect.Bool:
|
|
return reflect.TypeOf(ConfigurableCase[bool]{})
|
|
case reflect.Slice:
|
|
switch configuredType.Elem().Kind() {
|
|
case reflect.String:
|
|
return reflect.TypeOf(ConfigurableCase[[]string]{})
|
|
}
|
|
}
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// for the given T, return the reflect.type of Configurable[T]
|
|
func configurableType(configuredType reflect.Type) (reflect.Type, error) {
|
|
// I don't think it's possible to do this generically with go's
|
|
// current reflection apis unfortunately
|
|
switch configuredType.Kind() {
|
|
case reflect.String:
|
|
return reflect.TypeOf(Configurable[string]{}), nil
|
|
case reflect.Bool:
|
|
return reflect.TypeOf(Configurable[bool]{}), nil
|
|
case reflect.Slice:
|
|
switch configuredType.Elem().Kind() {
|
|
case reflect.String:
|
|
return reflect.TypeOf(Configurable[[]string]{}), nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("configurable structs can only contain strings, bools, or string slices, found %s", configuredType.String())
|
|
}
|
|
|
|
// 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
|
|
conditions []ConfigurableCondition
|
|
cases []ConfigurableCase[T]
|
|
appendWrapper *appendWrapper[T]
|
|
}
|
|
|
|
// Ignore the warning about the unused marker variable, it's used via reflection
|
|
var _ configurableMarker = Configurable[string]{}.marker
|
|
|
|
func NewConfigurable[T ConfigurableElements](conditions []ConfigurableCondition, cases []ConfigurableCase[T]) Configurable[T] {
|
|
for _, c := range cases {
|
|
if len(c.patterns) != len(conditions) {
|
|
panic(fmt.Sprintf("All configurables cases must have as many patterns as the configurable has conditions. Expected: %d, found: %d", len(conditions), len(c.patterns)))
|
|
}
|
|
}
|
|
return Configurable[T]{
|
|
conditions: conditions,
|
|
cases: cases,
|
|
appendWrapper: &appendWrapper[T]{},
|
|
}
|
|
}
|
|
|
|
// 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]
|
|
replace bool
|
|
}
|
|
|
|
// Get returns the final value for the configurable property.
|
|
// A configurable property may be unset, in which case Get will return nil.
|
|
func (c *Configurable[T]) Get(evaluator ConfigurableEvaluator) *T {
|
|
if c == nil || c.appendWrapper == nil {
|
|
return nil
|
|
}
|
|
if c.appendWrapper.replace {
|
|
return replaceConfiguredValues(
|
|
c.evaluateNonTransitive(evaluator),
|
|
c.appendWrapper.append.Get(evaluator),
|
|
)
|
|
} else {
|
|
return appendConfiguredValues(
|
|
c.evaluateNonTransitive(evaluator),
|
|
c.appendWrapper.append.Get(evaluator),
|
|
)
|
|
}
|
|
}
|
|
|
|
// GetOrDefault is the same as Get, but will return the provided default value if the property was unset.
|
|
func (c *Configurable[T]) GetOrDefault(evaluator ConfigurableEvaluator, defaultValue T) T {
|
|
result := c.Get(evaluator)
|
|
if result != nil {
|
|
return *result
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (c *Configurable[T]) evaluateNonTransitive(evaluator ConfigurableEvaluator) *T {
|
|
for i, case_ := range c.cases {
|
|
if len(c.conditions) != len(case_.patterns) {
|
|
evaluator.PropertyErrorf(c.propertyName, "Expected each case to have as many patterns as conditions. conditions: %d, len(cases[%d].patterns): %d", len(c.conditions), i, len(case_.patterns))
|
|
return nil
|
|
}
|
|
}
|
|
if len(c.conditions) == 0 {
|
|
if len(c.cases) == 0 {
|
|
return nil
|
|
} else if len(c.cases) == 1 {
|
|
return c.cases[0].value
|
|
} else {
|
|
evaluator.PropertyErrorf(c.propertyName, "Expected 0 or 1 branches in an unconfigured select, found %d", len(c.cases))
|
|
return nil
|
|
}
|
|
}
|
|
values := make([]ConfigurableValue, len(c.conditions))
|
|
for i, condition := range c.conditions {
|
|
values[i] = evaluator.EvaluateConfiguration(condition, c.propertyName)
|
|
}
|
|
foundMatch := false
|
|
var result *T
|
|
for _, case_ := range c.cases {
|
|
allMatch := true
|
|
for i, pat := range case_.patterns {
|
|
if !pat.matchesValueType(values[i]) {
|
|
evaluator.PropertyErrorf(c.propertyName, "Expected all branches of a select on condition %s to have type %s, found %s", c.conditions[i].String(), values[i].typ.String(), pat.typ.String())
|
|
return nil
|
|
}
|
|
if !pat.matchesValue(values[i]) {
|
|
allMatch = false
|
|
break
|
|
}
|
|
}
|
|
if allMatch && !foundMatch {
|
|
result = case_.value
|
|
foundMatch = true
|
|
}
|
|
}
|
|
if foundMatch {
|
|
return result
|
|
}
|
|
evaluator.PropertyErrorf(c.propertyName, "%s had value %s, which was not handled by the select statement", c.conditions, values)
|
|
return nil
|
|
}
|
|
|
|
func appendConfiguredValues[T ConfigurableElements](a, b *T) *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:
|
|
// Addition of bools will OR them together. This is inherited behavior
|
|
// from how proptools.ExtendBasicType works with non-configurable bools.
|
|
result := false
|
|
if a != nil {
|
|
result = result || *any(a).(*bool)
|
|
}
|
|
if b != nil {
|
|
result = result || *any(b).(*bool)
|
|
}
|
|
return any(&result).(*T)
|
|
default:
|
|
panic("Should be unreachable")
|
|
}
|
|
}
|
|
|
|
func replaceConfiguredValues[T ConfigurableElements](a, b *T) *T {
|
|
if b != nil {
|
|
return b
|
|
}
|
|
return a
|
|
}
|
|
|
|
// 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, replace bool)
|
|
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, conditions []ConfigurableCondition, cases any)
|
|
}
|
|
|
|
var _ configurableReflection = Configurable[string]{}
|
|
var _ configurablePtrReflection = &Configurable[string]{}
|
|
|
|
func (c *Configurable[T]) initialize(propertyName string, conditions []ConfigurableCondition, cases any) {
|
|
c.propertyName = propertyName
|
|
c.conditions = conditions
|
|
c.cases = cases.([]ConfigurableCase[T])
|
|
c.appendWrapper = &appendWrapper[T]{}
|
|
}
|
|
|
|
func (c Configurable[T]) setAppend(append any, replace bool) {
|
|
if c.appendWrapper.append.isEmpty() {
|
|
c.appendWrapper.append = append.(Configurable[T])
|
|
c.appendWrapper.replace = replace
|
|
} else {
|
|
c.appendWrapper.append.setAppend(append, replace)
|
|
}
|
|
}
|
|
|
|
func (c Configurable[T]) isEmpty() bool {
|
|
if c.appendWrapper != nil && !c.appendWrapper.append.isEmpty() {
|
|
return false
|
|
}
|
|
return 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()
|
|
inner.replace = c.appendWrapper.replace
|
|
}
|
|
}
|
|
|
|
conditionsCopy := make([]ConfigurableCondition, len(c.conditions))
|
|
copy(conditionsCopy, c.conditions)
|
|
|
|
casesCopy := make([]ConfigurableCase[T], len(c.cases))
|
|
for i, case_ := range c.cases {
|
|
casesCopy[i] = case_.Clone()
|
|
}
|
|
|
|
return &Configurable[T]{
|
|
propertyName: c.propertyName,
|
|
conditions: conditionsCopy,
|
|
cases: casesCopy,
|
|
appendWrapper: inner,
|
|
}
|
|
}
|
|
|
|
func copyConfiguredValue[T ConfigurableElements](t *T) *T {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
switch t2 := any(*t).(type) {
|
|
case []string:
|
|
result := any(slices.Clone(t2)).(T)
|
|
return &result
|
|
default:
|
|
return t
|
|
}
|
|
}
|