Conditional inclusion of blueprint modules

Bug: 270654958

This introduces a new `blueprint_package_includes` module type. If
present, other blueprint modules in that file will be analyzed
if and only if the requested include tags are met

example syntax:
```
Android.bp
blueprint_packgage_includes {
  match_all: ["tag1", "tag2", ...],
}

other_module1 {...}
other_module2 {...}
```
other_module1 and other_module2 will not be analyzed unless
tag1,tag2, ... are set

This also adds a new object of type `IncludeTags` to the Context object,
which is a container for these string keys.

Test: In build/blueprint, go test ./
Test: TH

Change-Id: I79de0d7da3224a5b2025c27a5137f39d00c7382e
Merged-In: I79de0d7da3224a5b2025c27a5137f39d00c7382e
(cherry picked from commit fd1f976100fb78d50902bd6feb550e92f70fa962)
diff --git a/bootstrap/command.go b/bootstrap/command.go
index 8c045b4..64bcce0 100644
--- a/bootstrap/command.go
+++ b/bootstrap/command.go
@@ -92,6 +92,7 @@
 	ctx.RegisterModuleType("bootstrap_go_package", newGoPackageModuleFactory())
 	ctx.RegisterModuleType("blueprint_go_binary", newGoBinaryModuleFactory())
 	ctx.RegisterSingletonType("bootstrap", newSingletonFactory())
+	blueprint.RegisterPackageIncludesModuleType(ctx)
 
 	ctx.BeginEvent("parse_bp")
 	blueprintFiles, errs := ctx.ParseFileList(".", filesToParse, config)
diff --git a/context.go b/context.go
index 6496948..1296eed 100644
--- a/context.go
+++ b/context.go
@@ -135,6 +135,31 @@
 
 	// Can be set by tests to avoid invalidating Module values after mutators.
 	skipCloneModulesAfterMutators bool
+
+	// String values that can be used to gate build graph traversal
+	includeTags *IncludeTags
+}
+
+// A container for String keys. The keys can be used to gate build graph traversal
+type IncludeTags map[string]bool
+
+func (tags *IncludeTags) Add(names ...string) {
+	for _, name := range names {
+		(*tags)[name] = true
+	}
+}
+
+func (tags *IncludeTags) Contains(tag string) bool {
+	_, exists := (*tags)[tag]
+	return exists
+}
+
+func (c *Context) AddIncludeTags(names ...string) {
+	c.includeTags.Add(names...)
+}
+
+func (c *Context) ContainsIncludeTag(name string) bool {
+	return c.includeTags.Contains(name)
 }
 
 // An Error describes a problem that was encountered that is related to a
@@ -392,6 +417,7 @@
 		globs:              make(map[globKey]pathtools.GlobResult),
 		fs:                 pathtools.OsFs,
 		finishedMutators:   make(map[*mutatorInfo]bool),
+		includeTags: &IncludeTags{},
 		outDir:             nil,
 		requiredNinjaMajor: 1,
 		requiredNinjaMinor: 7,
@@ -709,6 +735,33 @@
 	return c.ParseFileList(baseDir, pathsToParse, config)
 }
 
+// Returns a boolean for whether this file should be analyzed
+// Evaluates to true if the file either
+// 1. does not contain a blueprint_package_includes
+// 2. contains a blueprint_package_includes and all requested tags are set
+// This should be processed before adding any modules to the build graph
+func shouldVisitFile(c *Context, file *parser.File) (bool, []error) {
+	for _, def := range file.Defs {
+		switch def := def.(type) {
+		case *parser.Module:
+			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 true, errs
+			}
+			logicModule, _ := c.cloneLogicModule(module)
+			pi := logicModule.(*PackageIncludes)
+			return pi.MatchesIncludeTags(c), []error{}
+		}
+	}
+	return true, []error{}
+}
+
+
 func (c *Context) ParseFileList(rootDir string, filePaths []string,
 	config interface{}) (deps []string, errs []error) {
 
@@ -761,6 +814,15 @@
 			}
 			return nil
 		}
+		shouldVisit, errs := shouldVisitFile(c, file)
+		if len(errs) > 0 {
+			atomic.AddUint32(&numErrs, uint32(len(errs)))
+			errsCh <- errs
+		}
+		if !shouldVisit {
+			// TODO: Write a file that lists the skipped bp files
+			return
+		}
 
 		for _, def := range file.Defs {
 			switch def := def.(type) {
@@ -4230,3 +4292,49 @@
 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 {
+	if len(pi.MatchAll()) == 0 {
+		ctx.ModuleErrorf(pi, "Match_all must be a non-empty list")
+	}
+	for _, includeTag := range pi.MatchAll() {
+		if !ctx.ContainsIncludeTag(includeTag) {
+			return false
+		}
+	}
+	return true
+}
diff --git a/context_test.go b/context_test.go
index 6308ba9..5b045bd 100644
--- a/context_test.go
+++ b/context_test.go
@@ -18,6 +18,7 @@
 	"bytes"
 	"errors"
 	"fmt"
+	"path/filepath"
 	"reflect"
 	"strings"
 	"sync"
@@ -1084,3 +1085,73 @@
 		}
 	})
 }
+
+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 {
+		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 != "" {
+			continue // 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))
+		}
+	}
+
+}