blob: 3bb6b3dfe81c7c83e7f25a4f4a266b54d2b7d0bc [file] [log] [blame]
// Copyright 2024 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 main
import (
"cmp"
"encoding/json"
"flag"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"android/soong/cmd/release_config/release_config_proto"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
)
var verboseFlag bool
type StringList []string
func (l *StringList) Set(v string) error {
*l = append(*l, v)
return nil
}
func (l *StringList) String() string {
return fmt.Sprintf("%v", *l)
}
var releaseConfigMapPaths StringList
func DumpProtos(outDir string, message proto.Message) error {
basePath := filepath.Join(outDir, "all_release_configs")
writer := func(suffix string, marshal func() ([]byte, error)) error {
data, err := marshal()
if err != nil {
return err
}
return os.WriteFile(fmt.Sprintf("%s.%s", basePath, suffix), data, 0644)
}
err := writer("textproto", func() ([]byte, error) { return prototext.MarshalOptions{Multiline: true}.Marshal(message) })
if err != nil {
return err
}
err = writer("pb", func() ([]byte, error) { return proto.Marshal(message) })
if err != nil {
return err
}
return writer("json", func() ([]byte, error) { return json.MarshalIndent(message, "", " ") })
}
func LoadTextproto(path string, message proto.Message) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
ret := prototext.Unmarshal(data, message)
if verboseFlag {
debug, _ := prototext.Marshal(message)
fmt.Printf("%s: %s\n", path, debug)
}
return ret
}
func WalkTextprotoFiles(root string, subdir string, Func fs.WalkDirFunc) error {
path := filepath.Join(root, subdir)
if _, err := os.Stat(path); err != nil {
// Missing subdirs are not an error.
return nil
}
return filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(d.Name(), ".textproto") && d.Type().IsRegular() {
return Func(path, d, err)
}
return nil
})
}
type FlagValue struct {
// The path providing this value.
path string
// Protobuf
proto release_config_proto.FlagValue
}
func FlagValueFactory(protoPath string) (fv *FlagValue) {
fv = &FlagValue{path: protoPath}
if protoPath != "" {
LoadTextproto(protoPath, &fv.proto)
}
return fv
}
// One directory's contribution to the a release config.
type ReleaseConfigContribution struct {
// Paths to files providing this config.
path string
// The index of the config directory where this release config
// contribution was declared.
// Flag values cannot be set in a location with a lower index.
DeclarationIndex int
// Protobufs relevant to the config.
proto release_config_proto.ReleaseConfig
FlagValues []*FlagValue
}
// A single release_config_map.textproto and its associated data.
// Used primarily for debugging.
type ReleaseConfigMap struct {
// The path to this release_config_map file.
path string
// Data received
proto release_config_proto.ReleaseConfigMap
ReleaseConfigContributions map[string]*ReleaseConfigContribution
FlagDeclarations []release_config_proto.FlagDeclaration
}
// A generated release config.
type ReleaseConfig struct {
// the Name of the release config
Name string
// The index of the config directory where this release config was
// first declared.
// Flag values cannot be set in a location with a lower index.
DeclarationIndex int
// What contributes to this config.
Contributions []*ReleaseConfigContribution
// Aliases for this release
OtherNames []string
// The names of release configs that we inherit
InheritNames []string
// Unmarshalled flag artifacts
FlagArtifacts FlagArtifacts
// Generated release config
ReleaseConfigArtifact *release_config_proto.ReleaseConfigArtifact
// We have begun compiling this release config.
compileInProgress bool
}
type FlagArtifact struct {
FlagDeclaration *release_config_proto.FlagDeclaration
// The index of the config directory where this flag was declared.
// Flag values cannot be set in a location with a lower index.
DeclarationIndex int
Traces []*release_config_proto.Tracepoint
// Assigned value
Value *release_config_proto.Value
}
// Key is flag name.
type FlagArtifacts map[string]*FlagArtifact
type ReleaseConfigDirMap map[string]int
// The generated release configs.
type ReleaseConfigs struct {
// Ordered list of release config maps processed.
ReleaseConfigMaps []*ReleaseConfigMap
// Aliases
Aliases map[string]*string
// Dictionary of flag_name:FlagDeclaration, with no overrides applied.
FlagArtifacts FlagArtifacts
// Dictionary of name:ReleaseConfig
ReleaseConfigs map[string]*ReleaseConfig
// Generated release configs
Artifact release_config_proto.ReleaseConfigsArtifact
// The list of config directories used.
ConfigDirs []string
// A map from the config directory to its order in the list of config
// directories.
ConfigDirIndexes ReleaseConfigDirMap
}
func (src *FlagArtifact) Clone() *FlagArtifact {
value := &release_config_proto.Value{}
proto.Merge(value, src.Value)
return &FlagArtifact{
FlagDeclaration: src.FlagDeclaration,
Traces: src.Traces,
Value: value,
}
}
func (src FlagArtifacts) Clone() (dst FlagArtifacts) {
if dst == nil {
dst = make(FlagArtifacts)
}
for k, v := range src {
dst[k] = v.Clone()
}
return
}
func ReleaseConfigFactory(name string, index int) (c *ReleaseConfig) {
return &ReleaseConfig{Name: name, DeclarationIndex: index}
}
func ReleaseConfigsFactory() (c *ReleaseConfigs) {
return &ReleaseConfigs{
Aliases: make(map[string]*string),
FlagArtifacts: make(map[string]*FlagArtifact),
ReleaseConfigs: make(map[string]*ReleaseConfig),
ConfigDirs: []string{},
ConfigDirIndexes: make(ReleaseConfigDirMap),
}
}
func ReleaseConfigMapFactory(protoPath string) (m *ReleaseConfigMap) {
m = &ReleaseConfigMap{
path: protoPath,
ReleaseConfigContributions: make(map[string]*ReleaseConfigContribution),
}
if protoPath != "" {
LoadTextproto(protoPath, &m.proto)
}
return m
}
func FlagDeclarationFactory(protoPath string) (fd *release_config_proto.FlagDeclaration) {
fd = &release_config_proto.FlagDeclaration{}
if protoPath != "" {
LoadTextproto(protoPath, fd)
}
return fd
}
func (configs *ReleaseConfigs) LoadReleaseConfigMap(path string, ConfigDirIndex int) error {
m := ReleaseConfigMapFactory(path)
if m.proto.Origin == nil || *m.proto.Origin == "" {
return fmt.Errorf("Release config map %s lacks origin", path)
}
if m.proto.DefaultContainer == nil {
return fmt.Errorf("Release config map %s lacks default_container", path)
}
dir := filepath.Dir(path)
// Record any aliases, checking for duplicates.
for _, alias := range m.proto.Aliases {
name := *alias.Name
oldTarget, ok := configs.Aliases[name]
if ok {
if *oldTarget != *alias.Target {
return fmt.Errorf("Conflicting alias declarations: %s vs %s",
*oldTarget, *alias.Target)
}
}
configs.Aliases[name] = alias.Target
}
var err error
err = WalkTextprotoFiles(dir, "flag_declarations", func(path string, d fs.DirEntry, err error) error {
flagDeclaration := FlagDeclarationFactory(path)
// Container must be specified.
if flagDeclaration.Container == nil {
flagDeclaration.Container = m.proto.DefaultContainer
}
// TODO: drop flag_declaration.origin from the proto.
if flagDeclaration.Origin == nil {
flagDeclaration.Origin = m.proto.Origin
}
// There is always a default value.
if flagDeclaration.Value == nil {
flagDeclaration.Value = &release_config_proto.Value{Val: &release_config_proto.Value_UnspecifiedValue{true}}
}
m.FlagDeclarations = append(m.FlagDeclarations, *flagDeclaration)
name := *flagDeclaration.Name
if def, ok := configs.FlagArtifacts[name]; !ok {
configs.FlagArtifacts[name] = &FlagArtifact{FlagDeclaration: flagDeclaration, DeclarationIndex: ConfigDirIndex}
} else if !proto.Equal(def.FlagDeclaration, flagDeclaration) {
return fmt.Errorf("Duplicate definition of %s", *flagDeclaration.Name)
}
// Set the initial value in the flag artifact.
configs.FlagArtifacts[name].UpdateValue(
FlagValue{path: path, proto: release_config_proto.FlagValue{
Name: proto.String(name), Value: flagDeclaration.Value}})
return nil
})
if err != nil {
return err
}
err = WalkTextprotoFiles(dir, "release_configs", func(path string, d fs.DirEntry, err error) error {
releaseConfigContribution := &ReleaseConfigContribution{path: path, DeclarationIndex: ConfigDirIndex}
LoadTextproto(path, &releaseConfigContribution.proto)
name := *releaseConfigContribution.proto.Name
if fmt.Sprintf("%s.textproto", name) != filepath.Base(path) {
return fmt.Errorf("%s incorrectly declares release config %s", path, name)
}
if _, ok := configs.ReleaseConfigs[name]; !ok {
configs.ReleaseConfigs[name] = ReleaseConfigFactory(name, ConfigDirIndex)
}
config := configs.ReleaseConfigs[name]
config.InheritNames = append(config.InheritNames, releaseConfigContribution.proto.Inherits...)
// Only walk flag_values/{RELEASE} for defined releases.
err2 := WalkTextprotoFiles(dir, filepath.Join("flag_values", name), func(path string, d fs.DirEntry, err error) error {
flagValue := FlagValueFactory(path)
if fmt.Sprintf("%s.textproto", *flagValue.proto.Name) != filepath.Base(path) {
return fmt.Errorf("%s incorrectly sets value for flag %s", path, *flagValue.proto.Name)
}
releaseConfigContribution.FlagValues = append(releaseConfigContribution.FlagValues, flagValue)
return nil
})
if err2 != nil {
return err2
}
m.ReleaseConfigContributions[name] = releaseConfigContribution
config.Contributions = append(config.Contributions, releaseConfigContribution)
return nil
})
if err != nil {
return err
}
configs.ReleaseConfigMaps = append(configs.ReleaseConfigMaps, m)
return nil
}
func (configs *ReleaseConfigs) GetReleaseConfig(name string) (*ReleaseConfig, error) {
trace := []string{name}
for target, ok := configs.Aliases[name]; ok; target, ok = configs.Aliases[name] {
name = *target
trace = append(trace, name)
}
if config, ok := configs.ReleaseConfigs[name]; ok {
return config, nil
}
return nil, fmt.Errorf("Missing config %s. Trace=%v", name, trace)
}
func (configs *ReleaseConfigs) DumpMakefile(outDir, targetRelease string) error {
outFile := filepath.Join(outDir, "release_config.mk")
makeVars := make(map[string]string)
config, err := configs.GetReleaseConfig(targetRelease)
if err != nil {
return err
}
// Sort the flags by name first.
names := []string{}
for k, _ := range config.FlagArtifacts {
names = append(names, k)
}
slices.SortFunc(names, func(a, b string) int {
return cmp.Compare(a, b)
})
partitions := make(map[string][]string)
vNames := []string{}
addVar := func(name, suffix, value string) {
fullName := fmt.Sprintf("_ALL_RELEASE_FLAGS.%s.%s", name, suffix)
vNames = append(vNames, fullName)
makeVars[fullName] = value
}
for _, name := range names {
flag := config.FlagArtifacts[name]
decl := flag.FlagDeclaration
// cName := strings.ToLower(release_config_proto.Container_name[decl.GetContainer()])
cName := strings.ToLower(decl.Container.String())
if cName == strings.ToLower(release_config_proto.Container_ALL.String()) {
partitions["product"] = append(partitions["product"], name)
partitions["system"] = append(partitions["system"], name)
partitions["system_ext"] = append(partitions["system_ext"], name)
partitions["vendor"] = append(partitions["vendor"], name)
} else {
partitions[cName] = append(partitions[cName], name)
}
value := MarshalValue(flag.Value)
makeVars[name] = value
addVar(name, "PARTITIONS", cName)
addVar(name, "DEFAULT", MarshalValue(decl.Value))
addVar(name, "VALUE", value)
addVar(name, "DECLARED_IN", *flag.Traces[0].Source)
addVar(name, "SET_IN", *flag.Traces[len(flag.Traces)-1].Source)
addVar(name, "ORIGIN", *decl.Origin)
}
pNames := []string{}
for k, _ := range partitions {
pNames = append(pNames, k)
}
slices.SortFunc(pNames, func(a, b string) int {
return cmp.Compare(a, b)
})
// Now sort the make variables, and output them.
slices.SortFunc(vNames, func(a, b string) int {
return cmp.Compare(a, b)
})
// Write the flags as:
// _ALL_RELELASE_FLAGS
// _ALL_RELEASE_FLAGS.PARTITIONS.*
// all _ALL_RELEASE_FLAGS.*, sorted by name
// Final flag values, sorted by name.
data := fmt.Sprintf("_ALL_RELEASE_FLAGS :=$= %s\n", strings.Join(names, " "))
for _, pName := range pNames {
data += fmt.Sprintf("_ALL_RELEASE_FLAGS.PARTITIONS.%s :=$= %s\n", pName, strings.Join(partitions[pName], " "))
}
for _, vName := range vNames {
data += fmt.Sprintf("%s :=$= %s\n", vName, makeVars[vName])
}
data += "\n\n# Values for all build flags\n"
data += fmt.Sprintf("RELEASE_ACONFIG_VALUE_SETS :=$= %s\n",
strings.Join(config.ReleaseConfigArtifact.AconfigValueSets, " "))
for _, name := range names {
data += fmt.Sprintf("%s :=$= %s\n", name, makeVars[name])
}
return os.WriteFile(outFile, []byte(data), 0644)
}
func (configs *ReleaseConfigs) GenerateReleaseConfigs(targetRelease string) error {
otherNames := make(map[string][]string)
for aliasName, aliasTarget := range configs.Aliases {
if _, ok := configs.ReleaseConfigs[aliasName]; ok {
return fmt.Errorf("Alias %s is a declared release config", aliasName)
}
if _, ok := configs.ReleaseConfigs[*aliasTarget]; !ok {
if _, ok2 := configs.Aliases[*aliasTarget]; !ok2 {
return fmt.Errorf("Alias %s points to non-existing config %s", aliasName, *aliasTarget)
}
}
otherNames[*aliasTarget] = append(otherNames[*aliasTarget], aliasName)
}
for name, aliases := range otherNames {
configs.ReleaseConfigs[name].OtherNames = aliases
}
for _, config := range configs.ReleaseConfigs {
err := config.GenerateReleaseConfig(configs)
if err != nil {
return err
}
}
releaseConfig, err := configs.GetReleaseConfig(targetRelease)
if err != nil {
return err
}
configs.Artifact = release_config_proto.ReleaseConfigsArtifact{
ReleaseConfig: releaseConfig.ReleaseConfigArtifact,
OtherReleaseConfigs: func() []*release_config_proto.ReleaseConfigArtifact {
orc := []*release_config_proto.ReleaseConfigArtifact{}
for name, config := range configs.ReleaseConfigs {
if name != releaseConfig.Name {
orc = append(orc, config.ReleaseConfigArtifact)
}
}
return orc
}(),
}
return nil
}
func MarshalValue(value *release_config_proto.Value) string {
switch val := value.Val.(type) {
case *release_config_proto.Value_UnspecifiedValue:
// Value was never set.
return ""
case *release_config_proto.Value_StringValue:
return val.StringValue
case *release_config_proto.Value_BoolValue:
if val.BoolValue {
return "true"
}
// False ==> empty string
return ""
case *release_config_proto.Value_Obsolete:
return " #OBSOLETE"
default:
// Flagged as error elsewhere, so return empty string here.
return ""
}
}
func (fa *FlagArtifact) UpdateValue(flagValue FlagValue) error {
name := *flagValue.proto.Name
fa.Traces = append(fa.Traces, &release_config_proto.Tracepoint{Source: proto.String(flagValue.path), Value: flagValue.proto.Value})
if fa.Value.GetObsolete() {
return fmt.Errorf("Attempting to set obsolete flag %s. Trace=%v", name, fa.Traces)
}
switch val := flagValue.proto.Value.Val.(type) {
case *release_config_proto.Value_StringValue:
fa.Value = &release_config_proto.Value{Val: &release_config_proto.Value_StringValue{val.StringValue}}
case *release_config_proto.Value_BoolValue:
fa.Value = &release_config_proto.Value{Val: &release_config_proto.Value_BoolValue{val.BoolValue}}
case *release_config_proto.Value_Obsolete:
if !val.Obsolete {
return fmt.Errorf("%s: Cannot set obsolete=false. Trace=%v", name, fa.Traces)
}
fa.Value = &release_config_proto.Value{Val: &release_config_proto.Value_Obsolete{true}}
default:
return fmt.Errorf("Invalid type for flag_value: %T. Trace=%v", val, fa.Traces)
}
return nil
}
func (fa *FlagArtifact) Marshal() (*release_config_proto.FlagArtifact, error) {
return &release_config_proto.FlagArtifact{
FlagDeclaration: fa.FlagDeclaration,
Value: fa.Value,
Traces: fa.Traces,
}, nil
}
func (config *ReleaseConfig) GenerateReleaseConfig(configs *ReleaseConfigs) error {
if config.ReleaseConfigArtifact != nil {
return nil
}
if config.compileInProgress {
return fmt.Errorf("Loop detected for release config %s", config.Name)
}
config.compileInProgress = true
// Generate any configs we need to inherit. This will detect loops in
// the config.
contributionsToApply := []*ReleaseConfigContribution{}
myInherits := []string{}
myInheritsSet := make(map[string]bool)
for _, inherit := range config.InheritNames {
if _, ok := myInheritsSet[inherit]; ok {
continue
}
myInherits = append(myInherits, inherit)
myInheritsSet[inherit] = true
iConfig, err := configs.GetReleaseConfig(inherit)
if err != nil {
return err
}
iConfig.GenerateReleaseConfig(configs)
contributionsToApply = append(contributionsToApply, iConfig.Contributions...)
}
contributionsToApply = append(contributionsToApply, config.Contributions...)
myAconfigValueSets := []string{}
myFlags := configs.FlagArtifacts.Clone()
myDirsMap := make(map[int]bool)
for _, contrib := range contributionsToApply {
myAconfigValueSets = append(myAconfigValueSets, contrib.proto.AconfigValueSets...)
myDirsMap[contrib.DeclarationIndex] = true
for _, value := range contrib.FlagValues {
fa, ok := myFlags[*value.proto.Name]
if !ok {
return fmt.Errorf("Setting value for undefined flag %s in %s\n", *value.proto.Name, value.path)
}
myDirsMap[fa.DeclarationIndex] = true
if fa.DeclarationIndex > contrib.DeclarationIndex {
// Setting location is to the left of declaration.
return fmt.Errorf("Setting value for flag %s not allowed in %s\n", *value.proto.Name, value.path)
}
if err := fa.UpdateValue(*value); err != nil {
return err
}
}
}
directories := []string{}
for idx, confDir := range configs.ConfigDirs {
if _, ok := myDirsMap[idx]; ok {
directories = append(directories, confDir)
}
}
config.FlagArtifacts = myFlags
config.ReleaseConfigArtifact = &release_config_proto.ReleaseConfigArtifact{
Name: proto.String(config.Name),
OtherNames: config.OtherNames,
FlagArtifacts: func() []*release_config_proto.FlagArtifact {
ret := []*release_config_proto.FlagArtifact{}
for _, flag := range myFlags {
ret = append(ret, &release_config_proto.FlagArtifact{
FlagDeclaration: flag.FlagDeclaration,
Traces: flag.Traces,
Value: flag.Value,
})
}
return ret
}(),
AconfigValueSets: myAconfigValueSets,
Inherits: myInherits,
Directories: directories,
}
config.compileInProgress = false
return nil
}
func main() {
var targetRelease string
var outputDir string
outEnv := os.Getenv("OUT_DIR")
if outEnv == "" {
outEnv = "out"
}
defaultOutputDir := filepath.Join(outEnv, "soong", "release-config")
var defaultMapPaths StringList
defaultLocations := StringList{
"build/release/release_config_map.textproto",
"vendor/google_shared/build/release/release_config_map.textproto",
"vendor/google/release/release_config_map.textproto",
}
for _, path := range defaultLocations {
if _, err := os.Stat(path); err == nil {
defaultMapPaths = append(defaultMapPaths, path)
}
}
prodMaps := os.Getenv("PRODUCT_RELEASE_CONFIG_MAPS")
if prodMaps != "" {
defaultMapPaths = append(defaultMapPaths, strings.Split(prodMaps, " ")...)
}
flag.BoolVar(&verboseFlag, "debug", false, "print debugging information")
flag.Var(&releaseConfigMapPaths, "map", "path to a release_config_map.textproto. may be repeated")
flag.StringVar(&targetRelease, "release", "trunk_staging", "TARGET_RELEASE for this build")
flag.StringVar(&outputDir, "out_dir", defaultOutputDir, "basepath for the output. Multiple formats are created")
flag.Parse()
if len(releaseConfigMapPaths) == 0 {
releaseConfigMapPaths = defaultMapPaths
fmt.Printf("No --map argument provided. Using: --map %s\n", strings.Join(releaseConfigMapPaths, " --map "))
}
configs := ReleaseConfigsFactory()
for idx, releaseConfigMapPath := range releaseConfigMapPaths {
// Maintain an ordered list of release config directories.
configDir := filepath.Dir(releaseConfigMapPath)
configs.ConfigDirIndexes[configDir] = idx
configs.ConfigDirs = append(configs.ConfigDirs, configDir)
err := configs.LoadReleaseConfigMap(releaseConfigMapPath, idx)
if err != nil {
panic(err)
}
}
// Now that we have all of the release config maps, can meld them and generate the artifacts.
err := configs.GenerateReleaseConfigs(targetRelease)
if err != nil {
panic(err)
}
err = os.MkdirAll(outputDir, 0775)
if err != nil {
panic(err)
}
err = configs.DumpMakefile(outputDir, targetRelease)
if err != nil {
panic(err)
}
DumpProtos(outputDir, &configs.Artifact)
}