diff options
-rw-r--r-- | Android.bp | 10 | ||||
-rw-r--r-- | bootstrap/command.go | 32 | ||||
-rw-r--r-- | bootstrap/glob.go | 6 | ||||
-rw-r--r-- | context.go | 231 | ||||
-rw-r--r-- | context_test.go | 74 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | optional/optional.go | 58 | ||||
-rw-r--r-- | parser/parser.go | 1 | ||||
-rw-r--r-- | proptools/configurable.go | 114 | ||||
-rw-r--r-- | proptools/hash_provider.go | 45 | ||||
-rw-r--r-- | proptools/hash_provider_test.go | 84 | ||||
-rw-r--r-- | proptools/utils.go | 42 | ||||
-rw-r--r-- | provider.go | 2 | ||||
-rw-r--r-- | transition_test.go | 32 |
14 files changed, 468 insertions, 265 deletions
@@ -116,6 +116,7 @@ bootstrap_go_package { pkgPath: "github.com/google/blueprint/proptools", deps: [ "blueprint-parser", + "blueprint-optional", ], srcs: [ "proptools/clone.go", @@ -128,6 +129,7 @@ bootstrap_go_package { "proptools/tag.go", "proptools/typeequal.go", "proptools/unpack.go", + "proptools/utils.go", ], testSrcs: [ "proptools/clone_test.go", @@ -142,6 +144,14 @@ bootstrap_go_package { } bootstrap_go_package { + name: "blueprint-optional", + pkgPath: "github.com/google/blueprint/optional", + srcs: [ + "optional/optional.go", + ], +} + +bootstrap_go_package { name: "blueprint-bootstrap", deps: [ "blueprint", diff --git a/bootstrap/command.go b/bootstrap/command.go index 580907c..3071e3e 100644 --- a/bootstrap/command.go +++ b/bootstrap/command.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "os" - "path/filepath" "runtime" "runtime/debug" "runtime/pprof" @@ -28,6 +27,7 @@ import ( "strings" "github.com/google/blueprint" + "github.com/google/blueprint/proptools" ) type Args struct { @@ -63,7 +63,7 @@ func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, conf } if args.Cpuprofile != "" { - f, err := os.Create(joinPath(ctx.SrcDir(), args.Cpuprofile)) + f, err := os.Create(blueprint.JoinPath(ctx.SrcDir(), args.Cpuprofile)) if err != nil { return nil, fmt.Errorf("error opening cpuprofile: %s", err) } @@ -73,7 +73,7 @@ func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, conf } if args.TraceFile != "" { - f, err := os.Create(joinPath(ctx.SrcDir(), args.TraceFile)) + f, err := os.Create(blueprint.JoinPath(ctx.SrcDir(), args.TraceFile)) if err != nil { return nil, fmt.Errorf("error opening trace: %s", err) } @@ -102,7 +102,6 @@ func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, conf ctx.RegisterBottomUpMutator("bootstrap_plugin_deps", pluginDeps) ctx.RegisterSingletonType("bootstrap", newSingletonFactory(), false) RegisterGoModuleTypes(ctx) - blueprint.RegisterPackageIncludesModuleType(ctx) ctx.BeginEvent("parse_bp") if blueprintFiles, errs := ctx.ParseFileList(".", filesToParse, config); len(errs) > 0 { @@ -151,7 +150,6 @@ func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, conf providersValidationChan <- nil } - const outFilePermissions = 0666 var out blueprint.StringWriterWriter var f *os.File var buf *bufio.Writer @@ -159,12 +157,12 @@ func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, conf ctx.BeginEvent("write_files") defer ctx.EndEvent("write_files") if args.EmptyNinjaFile { - if err := os.WriteFile(joinPath(ctx.SrcDir(), args.OutFile), []byte(nil), outFilePermissions); err != nil { + if err := os.WriteFile(blueprint.JoinPath(ctx.SrcDir(), args.OutFile), []byte(nil), blueprint.OutFilePermissions); err != nil { return nil, fmt.Errorf("error writing empty Ninja file: %s", err) } out = io.Discard.(blueprint.StringWriterWriter) } else { - f, err := os.OpenFile(joinPath(ctx.SrcDir(), args.OutFile), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, outFilePermissions) + f, err := os.OpenFile(blueprint.JoinPath(ctx.SrcDir(), args.OutFile), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, blueprint.OutFilePermissions) if err != nil { return nil, fmt.Errorf("error opening Ninja file: %s", err) } @@ -173,7 +171,7 @@ func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, conf out = buf } - if err := ctx.WriteBuildFile(out); err != nil { + if err := ctx.WriteBuildFile(out, !strings.Contains(args.OutFile, "bootstrap.ninja") && !args.EmptyNinjaFile, args.OutFile); err != nil { return nil, fmt.Errorf("error writing Ninja file contents: %s", err) } @@ -191,18 +189,11 @@ func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, conf providerValidationErrors := <-providersValidationChan if providerValidationErrors != nil { - var sb strings.Builder - for i, err := range providerValidationErrors { - if i != 0 { - sb.WriteString("\n") - } - sb.WriteString(err.Error()) - } - return nil, errors.New(sb.String()) + return nil, proptools.MergeErrors(providerValidationErrors) } if args.Memprofile != "" { - f, err := os.Create(joinPath(ctx.SrcDir(), args.Memprofile)) + f, err := os.Create(blueprint.JoinPath(ctx.SrcDir(), args.Memprofile)) if err != nil { return nil, fmt.Errorf("error opening memprofile: %s", err) } @@ -230,10 +221,3 @@ func fatalErrors(errs []error) error { return errors.New("fatal errors encountered") } - -func joinPath(base, path string) string { - if filepath.IsAbs(path) { - return path - } - return filepath.Join(base, path) -} diff --git a/bootstrap/glob.go b/bootstrap/glob.go index b2c3a2d..4611cef 100644 --- a/bootstrap/glob.go +++ b/bootstrap/glob.go @@ -196,7 +196,7 @@ func (s *GlobSingleton) GenerateBuildActions(ctx blueprint.SingletonContext) { // We don't need to write the depfile because we're guaranteed that ninja // will run the command at least once (to record it into the ninja_log), so // the depfile will be loaded from that execution. - absoluteFileListFile := joinPath(s.SrcDir, fileListFile) + absoluteFileListFile := blueprint.JoinPath(s.SrcDir, fileListFile) err := pathtools.WriteFileIfChanged(absoluteFileListFile, globs.FileList(), 0666) if err != nil { panic(fmt.Errorf("error writing %s: %s", fileListFile, err)) @@ -217,7 +217,7 @@ func WriteBuildGlobsNinjaFile(glob *GlobSingleton, config interface{}) error { } const outFilePermissions = 0666 - err := ioutil.WriteFile(joinPath(glob.SrcDir, glob.GlobFile), buffer, outFilePermissions) + err := ioutil.WriteFile(blueprint.JoinPath(glob.SrcDir, glob.GlobFile), buffer, outFilePermissions) if err != nil { return fmt.Errorf("error writing %s: %s", glob.GlobFile, err) } @@ -251,7 +251,7 @@ func generateGlobNinjaFile(glob *GlobSingleton, config interface{}) ([]byte, []e } buf := bytes.NewBuffer(nil) - err := ctx.WriteBuildFile(buf) + err := ctx.WriteBuildFile(buf, false, "") if err != nil { return nil, []error{err} } @@ -15,6 +15,7 @@ package blueprint import ( + "bufio" "bytes" "cmp" "context" @@ -50,6 +51,8 @@ var ErrBuildActionsNotReady = errors.New("build actions are not ready") const maxErrors = 10 const MockModuleListFile = "bplist" +const OutFilePermissions = 0666 + // A Context contains all the state needed to parse a set of Blueprints files // and generate a Ninja file. The process of generating a Ninja file proceeds // through a series of four phases. Each phase corresponds with a some methods @@ -809,43 +812,10 @@ type shouldVisitFileInfo struct { // This should be processed before adding any modules to the build graph func shouldVisitFile(c *Context, file *parser.File) shouldVisitFileInfo { skippedModules := []string{} - var blueprintPackageIncludes *PackageIncludes for _, def := range file.Defs { switch def := def.(type) { case *parser.Module: skippedModules = append(skippedModules, def.Name()) - if def.Type != "blueprint_package_includes" { - continue - } - module, errs := processModuleDef(def, file.Name, c.moduleFactories, nil, c.ignoreUnknownModuleTypes) - if len(errs) > 0 { - // This file contains errors in blueprint_package_includes - // Visit anyways so that we can report errors on other modules in the file - return shouldVisitFileInfo{ - shouldVisitFile: true, - errs: errs, - } - } - logicModule, _ := c.cloneLogicModule(module) - blueprintPackageIncludes = logicModule.(*PackageIncludes) - } - } - - if blueprintPackageIncludes != nil { - packageMatches, err := blueprintPackageIncludes.matchesIncludeTags(c) - if err != nil { - return shouldVisitFileInfo{ - errs: []error{err}, - } - } else if !packageMatches { - return shouldVisitFileInfo{ - shouldVisitFile: false, - skippedModules: skippedModules, - reasonForSkip: fmt.Sprintf( - "module is defined in %q which contains a blueprint_package_includes module with unsatisfied tags", - file.Name, - ), - } } } @@ -1877,7 +1847,7 @@ func (c *Context) findReverseDependency(module *moduleInfo, config any, destName // modify the requested variation. It finds a variant that existed before the TransitionMutator ran that is // a subset of the requested variant to use as the module context for IncomingTransition. func (c *Context) applyTransitions(config any, module *moduleInfo, group *moduleGroup, variant variationMap, - requestedVariations []Variation) { + requestedVariations []Variation) variationMap { for _, transitionMutator := range c.transitionMutators { // Apply the outgoing transition if it was not explicitly requested. explicitlyRequested := slices.ContainsFunc(requestedVariations, func(variation Variation) bool { @@ -1903,6 +1873,9 @@ func (c *Context) applyTransitions(config any, module *moduleInfo, group *module } finalVariation := transitionMutator.mutator.IncomingTransition(ctx, outgoingVariation) + if variant == nil { + variant = make(variationMap) + } variant[transitionMutator.name] = finalVariation appliedIncomingTransition = true break @@ -1914,6 +1887,8 @@ func (c *Context) applyTransitions(config any, module *moduleInfo, group *module delete(variant, transitionMutator.name) } } + + return variant } func (c *Context) findVariant(module *moduleInfo, config any, @@ -1940,7 +1915,7 @@ func (c *Context) findVariant(module *moduleInfo, config any, newVariant[v.Mutator] = v.Variation } - c.applyTransitions(config, module, possibleDeps, newVariant, requestedVariations) + newVariant = c.applyTransitions(config, module, possibleDeps, newVariant, requestedVariations) check := func(variant variationMap) bool { if far { @@ -4135,23 +4110,50 @@ func (c *Context) VerifyProvidersWereUnchanged() []error { if !c.buildActionsReady { return []error{ErrBuildActionsNotReady} } - var errors []error - for _, m := range c.modulesSorted { - for i, provider := range m.providers { - if provider != nil { - hash, err := proptools.HashProvider(provider) - if err != nil { - errors = append(errors, fmt.Errorf("provider %q on module %q was modified after being set, and no longer hashable afterwards: %s", providerRegistry[i].typ, m.Name(), err.Error())) - continue - } - if provider != nil && m.providerInitialValueHashes[i] != hash { - errors = append(errors, fmt.Errorf("provider %q on module %q was modified after being set", providerRegistry[i].typ, m.Name())) + toProcess := make(chan *moduleInfo) + errorCh := make(chan []error) + var wg sync.WaitGroup + go func() { + for _, m := range c.modulesSorted { + toProcess <- m + } + close(toProcess) + }() + for i := 0; i < 1000; i++ { + wg.Add(1) + go func() { + var errors []error + for m := range toProcess { + for i, provider := range m.providers { + if provider != nil { + hash, err := proptools.CalculateHash(provider) + if err != nil { + errors = append(errors, fmt.Errorf("provider %q on module %q was modified after being set, and no longer hashable afterwards: %s", providerRegistry[i].typ, m.Name(), err.Error())) + continue + } + if m.providerInitialValueHashes[i] != hash { + errors = append(errors, fmt.Errorf("provider %q on module %q was modified after being set", providerRegistry[i].typ, m.Name())) + } + } else if m.providerInitialValueHashes[i] != 0 { + // This should be unreachable, because in setProvider we check if the provider has already been set. + errors = append(errors, fmt.Errorf("provider %q on module %q was unset somehow, this is an internal error", providerRegistry[i].typ, m.Name())) + } } - } else if m.providerInitialValueHashes[i] != 0 { - // This should be unreachable, because in setProvider we check if the provider has already been set. - errors = append(errors, fmt.Errorf("provider %q on module %q was unset somehow, this is an internal error", providerRegistry[i].typ, m.Name())) } - } + if errors != nil { + errorCh <- errors + } + wg.Done() + }() + } + go func() { + wg.Wait() + close(errorCh) + }() + + var errors []error + for newErrors := range errorCh { + errors = append(errors, newErrors...) } return errors } @@ -4159,7 +4161,7 @@ func (c *Context) VerifyProvidersWereUnchanged() []error { // WriteBuildFile writes the Ninja manifest text for the generated build // actions to w. If this is called before PrepareBuildActions successfully // completes then ErrBuildActionsNotReady is returned. -func (c *Context) WriteBuildFile(w StringWriterWriter) error { +func (c *Context) WriteBuildFile(w StringWriterWriter, shardNinja bool, ninjaFileName string) error { var err error pprof.Do(c.Context, pprof.Labels("blueprint", "WriteBuildFile"), func(ctx context.Context) { if !c.buildActionsReady { @@ -4199,7 +4201,7 @@ func (c *Context) WriteBuildFile(w StringWriterWriter) error { return } - if err = c.writeAllModuleActions(nw); err != nil { + if err = c.writeAllModuleActions(nw, shardNinja, ninjaFileName); err != nil { return } @@ -4468,14 +4470,19 @@ func (s moduleSorter) Swap(i, j int) { s.modules[i], s.modules[j] = s.modules[j], s.modules[i] } -func (c *Context) writeAllModuleActions(nw *ninjaWriter) error { +func GetNinjaShardFiles(ninjaFile string) []string { + ninjaShardCnt := 10 + fileNames := make([]string, ninjaShardCnt) + + for i := 0; i < ninjaShardCnt; i++ { + fileNames[i] = fmt.Sprintf("%s.%d", ninjaFile, i) + } + return fileNames +} + +func (c *Context) writeAllModuleActions(nw *ninjaWriter, shardNinja bool, ninjaFileName string) error { c.BeginEvent("modules") defer c.EndEvent("modules") - headerTemplate := template.New("moduleHeader") - if _, err := headerTemplate.Parse(moduleHeaderTemplate); err != nil { - // This is a programming error. - panic(err) - } modules := make([]*moduleInfo, 0, len(c.moduleInfo)) for _, module := range c.moduleInfo { @@ -4488,6 +4495,67 @@ func (c *Context) writeAllModuleActions(nw *ninjaWriter) error { return err } + headerTemplate := template.New("moduleHeader") + if _, err := headerTemplate.Parse(moduleHeaderTemplate); err != nil { + // This is a programming error. + panic(err) + } + + if shardNinja { + var wg sync.WaitGroup + errorCh := make(chan error) + files := GetNinjaShardFiles(ninjaFileName) + shardedModules := proptools.ShardByCount(modules, len(files)) + for i, batchModules := range shardedModules { + file := files[i] + wg.Add(1) + go func(file string, batchModules []*moduleInfo) { + defer wg.Done() + f, err := os.OpenFile(JoinPath(c.SrcDir(), file), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, OutFilePermissions) + if err != nil { + errorCh <- fmt.Errorf("error opening Ninja file shard: %s", err) + return + } + defer func() { + err := f.Close() + if err != nil { + errorCh <- err + } + }() + buf := bufio.NewWriterSize(f, 16*1024*1024) + defer func() { + err := buf.Flush() + if err != nil { + errorCh <- err + } + }() + writer := newNinjaWriter(buf) + err = c.writeModuleAction(batchModules, writer, headerTemplate) + if err != nil { + errorCh <- err + } + }(file, batchModules) + nw.Subninja(file) + } + go func() { + wg.Wait() + close(errorCh) + }() + + var errors []error + for newErrors := range errorCh { + errors = append(errors, newErrors) + } + if len(errors) > 0 { + return proptools.MergeErrors(errors) + } + return nil + } else { + return c.writeModuleAction(modules, nw, headerTemplate) + } +} + +func (c *Context) writeModuleAction(modules []*moduleInfo, nw *ninjaWriter, headerTemplate *template.Template) error { buf := bytes.NewBuffer(nil) for _, module := range modules { @@ -5113,48 +5181,9 @@ Singleton: {{.name}} Factory: {{.goFactory}} ` -// Blueprint module type that can be used to gate blueprint files beneath this directory -type PackageIncludes struct { - properties struct { - // Package will be included if all include tags in this list are set - Match_all []string - } - name *string `blueprint:"mutated"` -} - -func (pi *PackageIncludes) Name() string { - return proptools.String(pi.name) -} - -// This module type does not have any build actions -func (pi *PackageIncludes) GenerateBuildActions(ctx ModuleContext) { -} - -func newPackageIncludesFactory() (Module, []interface{}) { - module := &PackageIncludes{} - AddLoadHook(module, func(ctx LoadHookContext) { - module.name = proptools.StringPtr(ctx.ModuleDir() + "_includes") // Generate a synthetic name - }) - return module, []interface{}{&module.properties} -} - -func RegisterPackageIncludesModuleType(ctx *Context) { - ctx.RegisterModuleType("blueprint_package_includes", newPackageIncludesFactory) -} - -func (pi *PackageIncludes) MatchAll() []string { - return pi.properties.Match_all -} - -// Returns true if all requested include tags are set in the Context object -func (pi *PackageIncludes) matchesIncludeTags(ctx *Context) (bool, error) { - if len(pi.MatchAll()) == 0 { - return false, ctx.ModuleErrorf(pi, "Match_all must be a non-empty list") - } - for _, includeTag := range pi.MatchAll() { - if !ctx.ContainsIncludeTag(includeTag) { - return false, nil - } +func JoinPath(base, path string) string { + if filepath.IsAbs(path) { + return path } - return true, nil + return filepath.Join(base, path) } diff --git a/context_test.go b/context_test.go index e69f5da..d43b243 100644 --- a/context_test.go +++ b/context_test.go @@ -19,7 +19,6 @@ import ( "errors" "fmt" "hash/fnv" - "path/filepath" "reflect" "strconv" "strings" @@ -1089,78 +1088,6 @@ func Test_parallelVisit(t *testing.T) { }) } -func TestPackageIncludes(t *testing.T) { - dir1_foo_bp := ` - blueprint_package_includes { - match_all: ["use_dir1"], - } - foo_module { - name: "foo", - } - ` - dir2_foo_bp := ` - blueprint_package_includes { - match_all: ["use_dir2"], - } - foo_module { - name: "foo", - } - ` - mockFs := map[string][]byte{ - "dir1/Android.bp": []byte(dir1_foo_bp), - "dir2/Android.bp": []byte(dir2_foo_bp), - } - testCases := []struct { - desc string - includeTags []string - expectedDir string - expectedErr string - }{ - { - desc: "use_dir1 is set, use dir1 foo", - includeTags: []string{"use_dir1"}, - expectedDir: "dir1", - }, - { - desc: "use_dir2 is set, use dir2 foo", - includeTags: []string{"use_dir2"}, - expectedDir: "dir2", - }, - { - desc: "duplicate module error if both use_dir1 and use_dir2 are set", - includeTags: []string{"use_dir1", "use_dir2"}, - expectedDir: "", - expectedErr: `module "foo" already defined`, - }, - } - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - ctx := NewContext() - // Register mock FS - ctx.MockFileSystem(mockFs) - // Register module types - ctx.RegisterModuleType("foo_module", newFooModule) - RegisterPackageIncludesModuleType(ctx) - // Add include tags for test case - ctx.AddIncludeTags(tc.includeTags...) - // Run test - _, actualErrs := ctx.ParseFileList(".", []string{"dir1/Android.bp", "dir2/Android.bp"}, nil) - // Evaluate - if !strings.Contains(fmt.Sprintf("%s", actualErrs), fmt.Sprintf("%s", tc.expectedErr)) { - t.Errorf("Expected errors: %s, got errors: %s\n", tc.expectedErr, actualErrs) - } - if tc.expectedErr != "" { - return // expectedDir check not necessary - } - actualBpFile := ctx.moduleGroupFromName("foo", nil).modules.firstModule().relBlueprintsFile - if tc.expectedDir != filepath.Dir(actualBpFile) { - t.Errorf("Expected foo from %s, got %s\n", tc.expectedDir, filepath.Dir(actualBpFile)) - } - }) - } - -} - func TestDeduplicateOrderOnlyDeps(t *testing.T) { b := func(output string, inputs []string, orderOnlyDeps []string) *buildDef { return &buildDef{ @@ -1532,7 +1459,6 @@ func TestSourceRootDirs(t *testing.T) { ctx.RegisterModuleType("foo_module", newFooModule) ctx.RegisterBottomUpMutator("deps", depsMutator) ctx.AddSourceRootDirs(tc.sourceRootDirs...) - RegisterPackageIncludesModuleType(ctx) ctx.ParseFileList(".", fileList, nil) _, actualErrs := ctx.ResolveDependencies(nil) @@ -1,3 +1,3 @@ module github.com/google/blueprint -go 1.20 +go 1.22 diff --git a/optional/optional.go b/optional/optional.go new file mode 100644 index 0000000..0d7ef52 --- /dev/null +++ b/optional/optional.go @@ -0,0 +1,58 @@ +// 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 optional + +// ShallowOptional is an optional type that can be constructed from a pointer. +// It will not copy the pointer, and its size is the same size as a single pointer. +// It can be used to prevent a downstream consumer from modifying the value through +// the pointer, but is not suitable for an Optional type with stronger value semantics +// like you would expect from C++ or Rust Optionals. +type ShallowOptional[T any] struct { + inner *T +} + +// NewShallowOptional creates a new ShallowOptional from a pointer. +// The pointer will not be copied, the object could be changed by the calling +// code after the optional was created. +func NewShallowOptional[T any](inner *T) ShallowOptional[T] { + return ShallowOptional[T]{inner: inner} +} + +// IsPresent returns true if the optional contains a value +func (o *ShallowOptional[T]) IsPresent() bool { + return o.inner != nil +} + +// IsEmpty returns true if the optional does not have a value +func (o *ShallowOptional[T]) IsEmpty() bool { + return o.inner == nil +} + +// Get() returns the value inside the optional. It panics if IsEmpty() returns true +func (o *ShallowOptional[T]) Get() T { + if o.inner == nil { + panic("tried to get an empty optional") + } + return *o.inner +} + +// GetOrDefault() returns the value inside the optional if IsPresent() returns true, +// or the provided value otherwise. +func (o *ShallowOptional[T]) GetOrDefault(other T) T { + if o.inner == nil { + return other + } + return *o.inner +} diff --git a/parser/parser.go b/parser/parser.go index eaed054..81dafc9 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -390,6 +390,7 @@ func (p *parser) evaluateOperator(value1, value2 Expression, operator rune, Cases: []*SelectCase{{ Value: e1, }}, + ExpressionType: e1.Type(), } } } diff --git a/proptools/configurable.go b/proptools/configurable.go index 06b39a5..031d965 100644 --- a/proptools/configurable.go +++ b/proptools/configurable.go @@ -19,8 +19,37 @@ import ( "slices" "strconv" "strings" + + "github.com/google/blueprint/optional" ) +// ConfigurableOptional is the same as ShallowOptional, but we use this separate +// name to reserve the ability to switch to an alternative implementation later. +type ConfigurableOptional[T any] struct { + shallowOptional optional.ShallowOptional[T] +} + +// IsPresent returns true if the optional contains a value +func (o *ConfigurableOptional[T]) IsPresent() bool { + return o.shallowOptional.IsPresent() +} + +// IsEmpty returns true if the optional does not have a value +func (o *ConfigurableOptional[T]) IsEmpty() bool { + return o.shallowOptional.IsEmpty() +} + +// Get() returns the value inside the optional. It panics if IsEmpty() returns true +func (o *ConfigurableOptional[T]) Get() T { + return o.shallowOptional.Get() +} + +// GetOrDefault() returns the value inside the optional if IsPresent() returns true, +// or the provided value otherwise. +func (o *ConfigurableOptional[T]) GetOrDefault(other T) T { + return o.shallowOptional.GetOrDefault(other) +} + type ConfigurableElements interface { string | bool | []string } @@ -381,10 +410,9 @@ func NewConfigurable[T ConfigurableElements](conditions []ConfigurableCondition, // 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 { +func (c *Configurable[T]) Get(evaluator ConfigurableEvaluator) ConfigurableOptional[T] { result := c.inner.evaluate(c.propertyName, evaluator) - // Copy the result so that it can't be changed from soong - return copyConfiguredValue(result) + return configuredValuePtrToOptional(result) } // GetOrDefault is the same as Get, but will return the provided default value if the property was unset. @@ -439,6 +467,7 @@ func (c *singleConfigurable[T]) evaluateNonTransitive(propertyName string, evalu values[i] = evaluator.EvaluateConfiguration(condition, propertyName) } foundMatch := false + nonMatchingIndex := 0 var result *T for _, case_ := range c.cases { allMatch := true @@ -449,6 +478,7 @@ func (c *singleConfigurable[T]) evaluateNonTransitive(propertyName string, evalu } if !pat.matchesValue(values[i]) { allMatch = false + nonMatchingIndex = i break } } @@ -460,7 +490,8 @@ func (c *singleConfigurable[T]) evaluateNonTransitive(propertyName string, evalu if foundMatch { return result } - evaluator.PropertyErrorf(propertyName, "%s had value %s, which was not handled by the select statement", c.conditions, values) + + evaluator.PropertyErrorf(propertyName, "%s had value %s, which was not handled by the select statement", c.conditions[nonMatchingIndex].String(), values[nonMatchingIndex].String()) return nil } @@ -526,6 +557,7 @@ type configurableReflection interface { configuredType() reflect.Type clone() any isEmpty() bool + printfInto(value string) error } // Same as configurableReflection, but since initialize needs to take a pointer @@ -596,6 +628,60 @@ func (c *configurableInner[T]) setAppend(append *configurableInner[T], replace b } } +func (c Configurable[T]) printfInto(value string) error { + return c.inner.printfInto(value) +} + +func (c *configurableInner[T]) printfInto(value string) error { + for c != nil { + if err := c.single.printfInto(value); err != nil { + return err + } + c = c.next + } + return nil +} + +func (c *singleConfigurable[T]) printfInto(value string) error { + for _, c := range c.cases { + if c.value == nil { + continue + } + switch v := any(c.value).(type) { + case *string: + if err := printfIntoString(v, value); err != nil { + return err + } + case *[]string: + for i := range *v { + if err := printfIntoString(&((*v)[i]), value); err != nil { + return err + } + } + } + } + return nil +} + +func printfIntoString(s *string, configValue string) error { + count := strings.Count(*s, "%") + if count == 0 { + return nil + } + + if count > 1 { + return fmt.Errorf("list/value variable properties only support a single '%%'") + } + + if !strings.Contains(*s, "%s") { + return fmt.Errorf("unsupported %% in value variable property") + } + + *s = fmt.Sprintf(*s, configValue) + + return nil +} + func (c Configurable[T]) clone() any { return Configurable[T]{ propertyName: c.propertyName, @@ -682,6 +768,19 @@ func copyConfiguredValue[T ConfigurableElements](t *T) *T { } } +func configuredValuePtrToOptional[T ConfigurableElements](t *T) ConfigurableOptional[T] { + if t == nil { + return ConfigurableOptional[T]{optional.NewShallowOptional(t)} + } + switch t2 := any(*t).(type) { + case []string: + result := any(slices.Clone(t2)).(T) + return ConfigurableOptional[T]{optional.NewShallowOptional(&result)} + default: + return ConfigurableOptional[T]{optional.NewShallowOptional(t)} + } +} + func copyAndDereferenceConfiguredValue[T ConfigurableElements](t *T) T { switch t2 := any(*t).(type) { case []string: @@ -690,3 +789,10 @@ func copyAndDereferenceConfiguredValue[T ConfigurableElements](t *T) T { return *t } } + +// PrintfIntoConfigurable replaces %s occurrences in strings in Configurable properties +// with the provided string value. It's intention is to support soong config value variables +// on Configurable properties. +func PrintfIntoConfigurable(c any, value string) error { + return c.(configurableReflection).printfInto(value) +} diff --git a/proptools/hash_provider.go b/proptools/hash_provider.go index 6205bc7..c75bb7f 100644 --- a/proptools/hash_provider.go +++ b/proptools/hash_provider.go @@ -18,32 +18,31 @@ import ( "cmp" "encoding/binary" "fmt" - "hash/maphash" + "hash" + "hash/fnv" "math" "reflect" "sort" + "unsafe" ) -var seed maphash.Seed = maphash.MakeSeed() - // byte to insert between elements of lists, fields of structs/maps, etc in order // to try and make sure the hash is different when values are moved around between // elements. 36 is arbitrary, but it's the ascii code for a record separator var recordSeparator []byte = []byte{36} -func HashProvider(provider interface{}) (uint64, error) { - hasher := maphash.Hash{} - hasher.SetSeed(seed) +func CalculateHash(value interface{}) (uint64, error) { + hasher := fnv.New64() ptrs := make(map[uintptr]bool) - v := reflect.ValueOf(provider) + v := reflect.ValueOf(value) var err error if v.IsValid() { - err = hashProviderInternal(&hasher, v, ptrs) + err = calculateHashInternal(hasher, v, ptrs) } return hasher.Sum64(), err } -func hashProviderInternal(hasher *maphash.Hash, v reflect.Value, ptrs map[uintptr]bool) error { +func calculateHashInternal(hasher hash.Hash64, v reflect.Value, ptrs map[uintptr]bool) error { var int64Array [8]byte int64Buf := int64Array[:] binary.LittleEndian.PutUint64(int64Buf, uint64(v.Kind())) @@ -55,7 +54,7 @@ func hashProviderInternal(hasher *maphash.Hash, v reflect.Value, ptrs map[uintpt hasher.Write(int64Buf) for i := 0; i < v.NumField(); i++ { hasher.Write(recordSeparator) - err := hashProviderInternal(hasher, v.Field(i), ptrs) + err := calculateHashInternal(hasher, v.Field(i), ptrs) if err != nil { return fmt.Errorf("in field %s: %s", v.Type().Field(i).Name, err.Error()) } @@ -77,12 +76,12 @@ func hashProviderInternal(hasher *maphash.Hash, v reflect.Value, ptrs map[uintpt }) for i := 0; i < v.Len(); i++ { hasher.Write(recordSeparator) - err := hashProviderInternal(hasher, keys[indexes[i]], ptrs) + err := calculateHashInternal(hasher, keys[indexes[i]], ptrs) if err != nil { return fmt.Errorf("in map: %s", err.Error()) } hasher.Write(recordSeparator) - err = hashProviderInternal(hasher, keys[indexes[i]], ptrs) + err = calculateHashInternal(hasher, keys[indexes[i]], ptrs) if err != nil { return fmt.Errorf("in map: %s", err.Error()) } @@ -92,7 +91,7 @@ func hashProviderInternal(hasher *maphash.Hash, v reflect.Value, ptrs map[uintpt hasher.Write(int64Buf) for i := 0; i < v.Len(); i++ { hasher.Write(recordSeparator) - err := hashProviderInternal(hasher, v.Index(i), ptrs) + err := calculateHashInternal(hasher, v.Index(i), ptrs) if err != nil { return fmt.Errorf("in %s at index %d: %s", v.Kind().String(), i, err.Error()) } @@ -103,15 +102,16 @@ func hashProviderInternal(hasher *maphash.Hash, v reflect.Value, ptrs map[uintpt hasher.Write(int64Buf[:1]) return nil } - addr := v.Pointer() - binary.LittleEndian.PutUint64(int64Buf, uint64(addr)) + // Hardcoded value to indicate it is a pointer + binary.LittleEndian.PutUint64(int64Buf, uint64(0x55)) hasher.Write(int64Buf) + addr := v.Pointer() if _, ok := ptrs[addr]; ok { // We could make this an error if we want to disallow pointer cycles in the future return nil } ptrs[addr] = true - err := hashProviderInternal(hasher, v.Elem(), ptrs) + err := calculateHashInternal(hasher, v.Elem(), ptrs) if err != nil { return fmt.Errorf("in pointer: %s", err.Error()) } @@ -122,13 +122,20 @@ func hashProviderInternal(hasher *maphash.Hash, v reflect.Value, ptrs map[uintpt } else { // The only way get the pointer out of an interface to hash it or check for cycles // would be InterfaceData(), but that's deprecated and seems like it has undefined behavior. - err := hashProviderInternal(hasher, v.Elem(), ptrs) + err := calculateHashInternal(hasher, v.Elem(), ptrs) if err != nil { return fmt.Errorf("in interface: %s", err.Error()) } } case reflect.String: - hasher.WriteString(v.String()) + strLen := len(v.String()) + if strLen == 0 { + // unsafe.StringData is unspecified in this case + int64Buf[0] = 0 + hasher.Write(int64Buf[:1]) + return nil + } + hasher.Write(unsafe.Slice(unsafe.StringData(v.String()), strLen)) case reflect.Bool: if v.Bool() { int64Buf[0] = 1 @@ -146,7 +153,7 @@ func hashProviderInternal(hasher *maphash.Hash, v reflect.Value, ptrs map[uintpt binary.LittleEndian.PutUint64(int64Buf, math.Float64bits(v.Float())) hasher.Write(int64Buf) default: - return fmt.Errorf("providers may only contain primitives, strings, arrays, slices, structs, maps, and pointers, found: %s", v.Kind().String()) + return fmt.Errorf("data may only contain primitives, strings, arrays, slices, structs, maps, and pointers, found: %s", v.Kind().String()) } return nil } diff --git a/proptools/hash_provider_test.go b/proptools/hash_provider_test.go index 1c97aec..338c6e4 100644 --- a/proptools/hash_provider_test.go +++ b/proptools/hash_provider_test.go @@ -5,9 +5,9 @@ import ( "testing" ) -func mustHash(t *testing.T, provider interface{}) uint64 { +func mustHash(t *testing.T, data interface{}) uint64 { t.Helper() - result, err := HashProvider(provider) + result, err := CalculateHash(data) if err != nil { t.Fatal(err) } @@ -15,11 +15,11 @@ func mustHash(t *testing.T, provider interface{}) uint64 { } func TestHashingMapGetsSameResults(t *testing.T) { - provider := map[string]string{"foo": "bar", "baz": "qux"} - first := mustHash(t, provider) - second := mustHash(t, provider) - third := mustHash(t, provider) - fourth := mustHash(t, provider) + data := map[string]string{"foo": "bar", "baz": "qux"} + first := mustHash(t, data) + second := mustHash(t, data) + third := mustHash(t, data) + fourth := mustHash(t, data) if first != second || second != third || third != fourth { t.Fatal("Did not get the same result every time for a map") } @@ -27,29 +27,29 @@ func TestHashingMapGetsSameResults(t *testing.T) { func TestHashingNonSerializableTypesFails(t *testing.T) { testCases := []struct { - name string - provider interface{} + name string + data interface{} }{ { - name: "function pointer", - provider: []func(){nil}, + name: "function pointer", + data: []func(){nil}, }, { - name: "channel", - provider: []chan int{make(chan int)}, + name: "channel", + data: []chan int{make(chan int)}, }, { - name: "list with non-serializable type", - provider: []interface{}{"foo", make(chan int)}, + name: "list with non-serializable type", + data: []interface{}{"foo", make(chan int)}, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - _, err := HashProvider(testCase) + _, err := CalculateHash(testCase) if err == nil { t.Fatal("Expected hashing error but didn't get one") } - expected := "providers may only contain primitives, strings, arrays, slices, structs, maps, and pointers" + expected := "data may only contain primitives, strings, arrays, slices, structs, maps, and pointers" if !strings.Contains(err.Error(), expected) { t.Fatalf("Expected %q, got %q", expected, err.Error()) } @@ -59,32 +59,32 @@ func TestHashingNonSerializableTypesFails(t *testing.T) { func TestHashSuccessful(t *testing.T) { testCases := []struct { - name string - provider interface{} + name string + data interface{} }{ { - name: "int", - provider: 5, + name: "int", + data: 5, }, { - name: "string", - provider: "foo", + name: "string", + data: "foo", }, { - name: "*string", - provider: StringPtr("foo"), + name: "*string", + data: StringPtr("foo"), }, { - name: "array", - provider: [3]string{"foo", "bar", "baz"}, + name: "array", + data: [3]string{"foo", "bar", "baz"}, }, { - name: "slice", - provider: []string{"foo", "bar", "baz"}, + name: "slice", + data: []string{"foo", "bar", "baz"}, }, { name: "struct", - provider: struct { + data: struct { foo string bar int }{ @@ -94,19 +94,35 @@ func TestHashSuccessful(t *testing.T) { }, { name: "map", - provider: map[string]int{ + data: map[string]int{ "foo": 3, "bar": 4, }, }, { - name: "list of interfaces with different types", - provider: []interface{}{"foo", 3, []string{"bar", "baz"}}, + name: "list of interfaces with different types", + data: []interface{}{"foo", 3, []string{"bar", "baz"}}, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - mustHash(t, testCase.provider) + mustHash(t, testCase.data) }) } } + +func TestHashingDereferencePointers(t *testing.T) { + str1 := "this is a hash test for pointers" + str2 := "this is a hash test for pointers" + data := []struct { + content *string + }{ + {content: &str1}, + {content: &str2}, + } + first := mustHash(t, data[0]) + second := mustHash(t, data[1]) + if first != second { + t.Fatal("Got different results for the same string") + } +} diff --git a/proptools/utils.go b/proptools/utils.go new file mode 100644 index 0000000..0afb5a4 --- /dev/null +++ b/proptools/utils.go @@ -0,0 +1,42 @@ +package proptools + +import ( + "errors" + "math" + "strings" +) + +func ShardBySize[T ~[]E, E any](toShard T, shardSize int) []T { + if len(toShard) == 0 { + return nil + } + + ret := make([]T, 0, (len(toShard)+shardSize-1)/shardSize) + for len(toShard) > shardSize { + ret = append(ret, toShard[0:shardSize]) + toShard = toShard[shardSize:] + } + if len(toShard) > 0 { + ret = append(ret, toShard) + } + return ret +} + +func ShardByCount[T ~[]E, E any](toShard T, shardCount int) []T { + return ShardBySize(toShard, int(math.Ceil(float64(len(toShard))/float64(shardCount)))) +} + +// MergeErrors merges a list of errors into a single error. +func MergeErrors(errs []error) error { + if errs != nil { + var sb strings.Builder + for i, err := range errs { + if i != 0 { + sb.WriteString("\n") + } + sb.WriteString(err.Error()) + } + return errors.New(sb.String()) + } + return nil +} diff --git a/provider.go b/provider.go index 297861e..b2e0876 100644 --- a/provider.go +++ b/provider.go @@ -158,7 +158,7 @@ func (c *Context) setProvider(m *moduleInfo, provider *providerKey, value any) { if m.providerInitialValueHashes == nil { m.providerInitialValueHashes = make([]uint64, len(providerRegistry)) } - hash, err := proptools.HashProvider(value) + hash, err := proptools.CalculateHash(value) if err != nil { panic(fmt.Sprintf("Can't set value of provider %s: %s", provider.typ, err.Error())) } diff --git a/transition_test.go b/transition_test.go index a380b86..7c6e1f4 100644 --- a/transition_test.go +++ b/transition_test.go @@ -86,7 +86,18 @@ const testTransitionBp = ` } transition_module { - name: "F" + name: "F", + } + + transition_module { + name: "G", + outgoing: "h", + %s + } + + transition_module { + name: "H", + split: ["h"], } ` @@ -128,7 +139,7 @@ func checkTransitionMutate(t *testing.T, m *transitionModule, variant string) { } func TestTransition(t *testing.T) { - ctx, errs := testTransition(fmt.Sprintf(testTransitionBp, "")) + ctx, errs := testTransition(fmt.Sprintf(testTransitionBp, "", "")) assertNoErrors(t, errs) // Module A uses Split to create a and b variants @@ -154,6 +165,8 @@ func TestTransition(t *testing.T) { D_d := getTransitionModule(ctx, "D", "d") E_d := getTransitionModule(ctx, "E", "d") F := getTransitionModule(ctx, "F", "") + G := getTransitionModule(ctx, "G", "") + H_h := getTransitionModule(ctx, "H", "h") checkTransitionDeps(t, ctx, A_a, "B(a)", "C(a)") checkTransitionDeps(t, ctx, A_b, "B(b)", "C(b)") @@ -165,6 +178,8 @@ func TestTransition(t *testing.T) { checkTransitionDeps(t, ctx, D_d, "E(d)") checkTransitionDeps(t, ctx, E_d) checkTransitionDeps(t, ctx, F) + checkTransitionDeps(t, ctx, G) + checkTransitionDeps(t, ctx, H_h) checkTransitionMutate(t, A_a, "a") checkTransitionMutate(t, A_b, "b") @@ -176,11 +191,14 @@ func TestTransition(t *testing.T) { checkTransitionMutate(t, D_d, "d") checkTransitionMutate(t, E_d, "d") checkTransitionMutate(t, F, "") + checkTransitionMutate(t, G, "") + checkTransitionMutate(t, H_h, "h") } func TestPostTransitionDeps(t *testing.T) { ctx, errs := testTransition(fmt.Sprintf(testTransitionBp, - `post_transition_deps: ["C", "D:late", "E:d", "F"],`)) + `post_transition_deps: ["C", "D:late", "E:d", "F"],`, + `post_transition_deps: ["H"],`)) assertNoErrors(t, errs) // Module A uses Split to create a and b variants @@ -206,6 +224,8 @@ func TestPostTransitionDeps(t *testing.T) { D_d := getTransitionModule(ctx, "D", "d") E_d := getTransitionModule(ctx, "E", "d") F := getTransitionModule(ctx, "F", "") + G := getTransitionModule(ctx, "G", "") + H_h := getTransitionModule(ctx, "H", "h") checkTransitionDeps(t, ctx, A_a, "B(a)", "C(a)") checkTransitionDeps(t, ctx, A_b, "B(b)", "C(b)") @@ -222,6 +242,8 @@ func TestPostTransitionDeps(t *testing.T) { checkTransitionDeps(t, ctx, D_d, "E(d)") checkTransitionDeps(t, ctx, E_d) checkTransitionDeps(t, ctx, F) + checkTransitionDeps(t, ctx, G, "H(h)") + checkTransitionDeps(t, ctx, H_h) checkTransitionMutate(t, A_a, "a") checkTransitionMutate(t, A_b, "b") @@ -233,12 +255,14 @@ func TestPostTransitionDeps(t *testing.T) { checkTransitionMutate(t, D_d, "d") checkTransitionMutate(t, E_d, "d") checkTransitionMutate(t, F, "") + checkTransitionMutate(t, G, "") + checkTransitionMutate(t, H_h, "h") } func TestPostTransitionDepsMissingVariant(t *testing.T) { // TODO: eventually this will create the missing variant on demand _, errs := testTransition(fmt.Sprintf(testTransitionBp, - `post_transition_deps: ["E:missing"],`)) + `post_transition_deps: ["E:missing"],`, "")) expectedError := `Android.bp:8:4: dependency "E" of "B" missing variant: transition:missing available variants: |