bp2build: framework for generating BazelTargetModules.

This CL creates the framework necessary for generating
BazelTargetModules from regular Soong Android modules.
BazelTargetModules are code-generated into Bazel targets in BUILD files.

See the follow-up CL for examples of creating filegroup/genrule
BazelTargetModules.

Test: GENERATE_BAZEL_FILES=true m nothing # creates out/soong/bp2build
with no BUILD files, because there are no BazelTargetModules in the
module graph.

Change-Id: I33a96365bd439043b13af6db9e439592e9983188
diff --git a/bp2build/Android.bp b/bp2build/Android.bp
index 49587f4..8007574 100644
--- a/bp2build/Android.bp
+++ b/bp2build/Android.bp
@@ -10,6 +10,7 @@
     ],
     deps: [
         "soong-android",
+        "soong-bazel",
     ],
     testSrcs: [
         "build_conversion_test.go",
diff --git a/bp2build/androidbp_to_build_templates.go b/bp2build/androidbp_to_build_templates.go
index 75c3ccb..9bac86b 100644
--- a/bp2build/androidbp_to_build_templates.go
+++ b/bp2build/androidbp_to_build_templates.go
@@ -15,7 +15,7 @@
 package bp2build
 
 const (
-	// The default `load` preamble for every generated BUILD file.
+	// The default `load` preamble for every generated queryview BUILD file.
 	soongModuleLoad = `package(default_visibility = ["//visibility:public"])
 load("//build/bazel/queryview_rules:soong_module.bzl", "soong_module")
 
@@ -31,6 +31,10 @@
     module_deps = %s,
 %s)`
 
+	bazelTarget = `%s(
+    name = "%s",
+%s)`
+
 	// A simple provider to mark and differentiate Soong module rule shims from
 	// regular Bazel rules. Every Soong module rule shim returns a
 	// SoongModuleInfo provider, and can only depend on rules returning
diff --git a/bp2build/bp2build.go b/bp2build/bp2build.go
index 49729e0..75b6097 100644
--- a/bp2build/bp2build.go
+++ b/bp2build/bp2build.go
@@ -28,9 +28,9 @@
 
 	ruleShims := CreateRuleShims(android.ModuleTypeFactories())
 
-	buildToTargets := GenerateSoongModuleTargets(ctx.Context())
+	buildToTargets := GenerateSoongModuleTargets(ctx.Context(), true)
 
-	filesToWrite := CreateBazelFiles(ruleShims, buildToTargets)
+	filesToWrite := CreateBazelFiles(ruleShims, buildToTargets, true)
 	for _, f := range filesToWrite {
 		if err := writeFile(outputDir, ctx, f); err != nil {
 			fmt.Errorf("Failed to write %q (dir %q) due to %q", f.Basename, f.Dir, err)
diff --git a/bp2build/build_conversion.go b/bp2build/build_conversion.go
index bece8f6..da2fb7f 100644
--- a/bp2build/build_conversion.go
+++ b/bp2build/build_conversion.go
@@ -73,16 +73,51 @@
 	return attributes
 }
 
-func GenerateSoongModuleTargets(ctx bpToBuildContext) map[string][]BazelTarget {
+func GenerateSoongModuleTargets(ctx bpToBuildContext, bp2buildEnabled bool) map[string][]BazelTarget {
 	buildFileToTargets := make(map[string][]BazelTarget)
 	ctx.VisitAllModules(func(m blueprint.Module) {
 		dir := ctx.ModuleDir(m)
-		t := generateSoongModuleTarget(ctx, m)
+		var t BazelTarget
+
+		if bp2buildEnabled {
+			if _, ok := m.(android.BazelTargetModule); !ok {
+				return
+			}
+			t = generateBazelTarget(ctx, m)
+		} else {
+			t = generateSoongModuleTarget(ctx, m)
+		}
+
 		buildFileToTargets[ctx.ModuleDir(m)] = append(buildFileToTargets[dir], t)
 	})
 	return buildFileToTargets
 }
 
+func generateBazelTarget(ctx bpToBuildContext, m blueprint.Module) BazelTarget {
+	// extract the bazel attributes from the module.
+	props := getBuildProperties(ctx, m)
+
+	// extract the rule class name from the attributes. Since the string value
+	// will be string-quoted, remove the quotes here.
+	ruleClass := strings.Replace(props.Attrs["rule_class"], "\"", "", 2)
+	// Delete it from being generated in the BUILD file.
+	delete(props.Attrs, "rule_class")
+
+	// Return the Bazel target with rule class and attributes, ready to be
+	// code-generated.
+	attributes := propsToAttributes(props.Attrs)
+	targetName := targetNameForBp2Build(ctx, m)
+	return BazelTarget{
+		name: targetName,
+		content: fmt.Sprintf(
+			bazelTarget,
+			ruleClass,
+			targetName,
+			attributes,
+		),
+	}
+}
+
 // Convert a module and its deps and props into a Bazel macro/rule
 // representation in the BUILD file.
 func generateSoongModuleTarget(ctx bpToBuildContext, m blueprint.Module) BazelTarget {
@@ -306,6 +341,10 @@
 	return strings.Repeat("    ", indent)
 }
 
+func targetNameForBp2Build(c bpToBuildContext, logicModule blueprint.Module) string {
+	return strings.Replace(c.ModuleName(logicModule), "__bp2build__", "", 1)
+}
+
 func targetNameWithVariant(c bpToBuildContext, logicModule blueprint.Module) string {
 	name := ""
 	if c.ModuleSubDir(logicModule) != "" {
diff --git a/bp2build/build_conversion_test.go b/bp2build/build_conversion_test.go
index 4e31aa7..0217580 100644
--- a/bp2build/build_conversion_test.go
+++ b/bp2build/build_conversion_test.go
@@ -200,7 +200,7 @@
 		_, errs = ctx.PrepareBuildActions(config)
 		android.FailIfErrored(t, errs)
 
-		bazelTargets := GenerateSoongModuleTargets(ctx.Context.Context)[dir]
+		bazelTargets := GenerateSoongModuleTargets(ctx.Context.Context, false)[dir]
 		if g, w := len(bazelTargets), 1; g != w {
 			t.Fatalf("Expected %d bazel target, got %d", w, g)
 		}
@@ -210,7 +210,58 @@
 			t.Errorf(
 				"Expected generated Bazel target to be '%s', got '%s'",
 				testCase.expectedBazelTarget,
-				actualBazelTarget,
+				actualBazelTarget.content,
+			)
+		}
+	}
+}
+
+func TestGenerateBazelTargetModules(t *testing.T) {
+	testCases := []struct {
+		bp                  string
+		expectedBazelTarget string
+	}{
+		{
+			bp: `custom {
+	name: "foo",
+    string_list_prop: ["a", "b"],
+    string_prop: "a",
+}`,
+			expectedBazelTarget: `custom(
+    name = "foo",
+    string_list_prop = [
+        "a",
+        "b",
+    ],
+    string_prop = "a",
+)`,
+		},
+	}
+
+	dir := "."
+	for _, testCase := range testCases {
+		config := android.TestConfig(buildDir, nil, testCase.bp, nil)
+		ctx := android.NewTestContext(config)
+		ctx.RegisterModuleType("custom", customModuleFactory)
+		ctx.RegisterBp2BuildMutator("custom", customBp2BuildMutator)
+		ctx.RegisterForBazelConversion()
+
+		_, errs := ctx.ParseFileList(dir, []string{"Android.bp"})
+		android.FailIfErrored(t, errs)
+		_, errs = ctx.ResolveDependencies(config)
+		android.FailIfErrored(t, errs)
+
+		bazelTargets := GenerateSoongModuleTargets(ctx.Context.Context, true)[dir]
+		if g, w := len(bazelTargets), 1; g != w {
+			t.Fatalf("Expected %d bazel target, got %d", w, g)
+		}
+
+		actualBazelTarget := bazelTargets[0]
+		if actualBazelTarget.content != testCase.expectedBazelTarget {
+			t.Errorf(
+				"Expected generated Bazel target to be '%s', got '%s'",
+				testCase.expectedBazelTarget,
+				actualBazelTarget.content,
 			)
 		}
 	}
diff --git a/bp2build/bzl_conversion_test.go b/bp2build/bzl_conversion_test.go
index 8bea3f6..01c7271 100644
--- a/bp2build/bzl_conversion_test.go
+++ b/bp2build/bzl_conversion_test.go
@@ -172,7 +172,7 @@
 			content: "irrelevant",
 		},
 	}
-	files := CreateBazelFiles(ruleShims, make(map[string][]BazelTarget))
+	files := CreateBazelFiles(ruleShims, make(map[string][]BazelTarget), false)
 
 	var actualSoongModuleBzl BazelFile
 	for _, f := range files {
diff --git a/bp2build/conversion.go b/bp2build/conversion.go
index cdfb38b..cccc474 100644
--- a/bp2build/conversion.go
+++ b/bp2build/conversion.go
@@ -17,7 +17,8 @@
 
 func CreateBazelFiles(
 	ruleShims map[string]RuleShim,
-	buildToTargets map[string][]BazelTarget) []BazelFile {
+	buildToTargets map[string][]BazelTarget,
+	bp2buildEnabled bool) []BazelFile {
 	files := make([]BazelFile, 0, len(ruleShims)+len(buildToTargets)+numAdditionalFiles)
 
 	// Write top level files: WORKSPACE and BUILD. These files are empty.
@@ -26,22 +27,30 @@
 	files = append(files, newFile("", "BUILD", ""))
 
 	files = append(files, newFile(bazelRulesSubDir, "BUILD", ""))
-	files = append(files, newFile(bazelRulesSubDir, "providers.bzl", providersBzl))
 
-	for bzlFileName, ruleShim := range ruleShims {
-		files = append(files, newFile(bazelRulesSubDir, bzlFileName+".bzl", ruleShim.content))
+	if !bp2buildEnabled {
+		// These files are only used for queryview.
+		files = append(files, newFile(bazelRulesSubDir, "providers.bzl", providersBzl))
+
+		for bzlFileName, ruleShim := range ruleShims {
+			files = append(files, newFile(bazelRulesSubDir, bzlFileName+".bzl", ruleShim.content))
+		}
+		files = append(files, newFile(bazelRulesSubDir, "soong_module.bzl", generateSoongModuleBzl(ruleShims)))
 	}
-	files = append(files, newFile(bazelRulesSubDir, "soong_module.bzl", generateSoongModuleBzl(ruleShims)))
 
-	files = append(files, createBuildFiles(buildToTargets)...)
+	files = append(files, createBuildFiles(buildToTargets, bp2buildEnabled)...)
 
 	return files
 }
 
-func createBuildFiles(buildToTargets map[string][]BazelTarget) []BazelFile {
+func createBuildFiles(buildToTargets map[string][]BazelTarget, bp2buildEnabled bool) []BazelFile {
 	files := make([]BazelFile, 0, len(buildToTargets))
 	for _, dir := range android.SortedStringKeys(buildToTargets) {
 		content := soongModuleLoad
+		if bp2buildEnabled {
+			// No need to load soong_module for bp2build BUILD files.
+			content = ""
+		}
 		targets := buildToTargets[dir]
 		sort.Slice(targets, func(i, j int) bool { return targets[i].name < targets[j].name })
 		for _, t := range targets {
diff --git a/bp2build/conversion_test.go b/bp2build/conversion_test.go
index a38fa6a..b40aa1b 100644
--- a/bp2build/conversion_test.go
+++ b/bp2build/conversion_test.go
@@ -19,12 +19,44 @@
 	"testing"
 )
 
-func TestCreateBazelFiles_AddsTopLevelFiles(t *testing.T) {
-	files := CreateBazelFiles(map[string]RuleShim{}, map[string][]BazelTarget{})
-	expectedFilePaths := []struct {
-		dir      string
-		basename string
-	}{
+type filepath struct {
+	dir      string
+	basename string
+}
+
+func assertFilecountsAreEqual(t *testing.T, actual []BazelFile, expected []filepath) {
+	if a, e := len(actual), len(expected); a != e {
+		t.Errorf("Expected %d files, got %d", e, a)
+	}
+}
+
+func assertFileContent(t *testing.T, actual []BazelFile, expected []filepath) {
+	for i := range actual {
+		if g, w := actual[i], expected[i]; g.Dir != w.dir || g.Basename != w.basename {
+			t.Errorf("Did not find expected file %s/%s", g.Dir, g.Basename)
+		} else if g.Basename == "BUILD" || g.Basename == "WORKSPACE" {
+			if g.Contents != "" {
+				t.Errorf("Expected %s to have no content.", g)
+			}
+		} else if g.Contents == "" {
+			t.Errorf("Contents of %s unexpected empty.", g)
+		}
+	}
+}
+
+func sortFiles(files []BazelFile) {
+	sort.Slice(files, func(i, j int) bool {
+		if dir1, dir2 := files[i].Dir, files[j].Dir; dir1 == dir2 {
+			return files[i].Basename < files[j].Basename
+		} else {
+			return dir1 < dir2
+		}
+	})
+}
+
+func TestCreateBazelFiles_QueryView_AddsTopLevelFiles(t *testing.T) {
+	files := CreateBazelFiles(map[string]RuleShim{}, map[string][]BazelTarget{}, false)
+	expectedFilePaths := []filepath{
 		{
 			dir:      "",
 			basename: "BUILD",
@@ -47,27 +79,29 @@
 		},
 	}
 
-	if g, w := len(files), len(expectedFilePaths); g != w {
-		t.Errorf("Expected %d files, got %d", w, g)
+	assertFilecountsAreEqual(t, files, expectedFilePaths)
+	sortFiles(files)
+	assertFileContent(t, files, expectedFilePaths)
+}
+
+func TestCreateBazelFiles_Bp2Build_AddsTopLevelFiles(t *testing.T) {
+	files := CreateBazelFiles(map[string]RuleShim{}, map[string][]BazelTarget{}, true)
+	expectedFilePaths := []filepath{
+		{
+			dir:      "",
+			basename: "BUILD",
+		},
+		{
+			dir:      "",
+			basename: "WORKSPACE",
+		},
+		{
+			dir:      bazelRulesSubDir,
+			basename: "BUILD",
+		},
 	}
 
-	sort.Slice(files, func(i, j int) bool {
-		if dir1, dir2 := files[i].Dir, files[j].Dir; dir1 == dir2 {
-			return files[i].Basename < files[j].Basename
-		} else {
-			return dir1 < dir2
-		}
-	})
-
-	for i := range files {
-		if g, w := files[i], expectedFilePaths[i]; g.Dir != w.dir || g.Basename != w.basename {
-			t.Errorf("Did not find expected file %s/%s", g.Dir, g.Basename)
-		} else if g.Basename == "BUILD" || g.Basename == "WORKSPACE" {
-			if g.Contents != "" {
-				t.Errorf("Expected %s to have no content.", g)
-			}
-		} else if g.Contents == "" {
-			t.Errorf("Contents of %s unexpected empty.", g)
-		}
-	}
+	assertFilecountsAreEqual(t, files, expectedFilePaths)
+	sortFiles(files)
+	assertFileContent(t, files, expectedFilePaths)
 }
diff --git a/bp2build/testing.go b/bp2build/testing.go
index 2da32c6..4c31d2d 100644
--- a/bp2build/testing.go
+++ b/bp2build/testing.go
@@ -2,6 +2,9 @@
 
 import (
 	"android/soong/android"
+	"android/soong/bazel"
+
+	"github.com/google/blueprint/proptools"
 )
 
 type nestedProps struct {
@@ -100,3 +103,37 @@
 	android.InitDefaultsModule(m)
 	return m
 }
+
+type customBazelModuleAttributes struct {
+	Name             *string
+	String_prop      string
+	String_list_prop []string
+}
+
+type customBazelModule struct {
+	android.BazelTargetModuleBase
+	customBazelModuleAttributes
+}
+
+func customBazelModuleFactory() android.Module {
+	module := &customBazelModule{}
+	module.AddProperties(&module.customBazelModuleAttributes)
+	android.InitBazelTargetModule(module)
+	return module
+}
+
+func (m *customBazelModule) Name() string                                          { return m.BaseModuleName() }
+func (m *customBazelModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {}
+
+func customBp2BuildMutator(ctx android.TopDownMutatorContext) {
+	if m, ok := ctx.Module().(*customModule); ok {
+		name := "__bp2build__" + m.Name()
+		ctx.CreateModule(customBazelModuleFactory, &customBazelModuleAttributes{
+			Name:             proptools.StringPtr(name),
+			String_prop:      m.props.String_prop,
+			String_list_prop: m.props.String_list_prop,
+		}, &bazel.BazelTargetModuleProperties{
+			Rule_class: "custom",
+		})
+	}
+}