Create a new mode in soong_ui to generate API only BUILD files

The generated Bazel workspace will only contain api specific targets.
This is feasible since these targets do not have any cross dependencies
with the targets in the bp2build workspace

The advantages of a new mode are
1. Does not pollute bp2build workspace with api targets
2. Does not block api targets with the current allowlist conversion
   mechansims in bp2build
(In the future we might want to combine these two workspaces)

A Soong module type will generate a Bazel target if it implements
ApiProvider interface

Test: m apigen
Test: m nothing

Change-Id: I69c57ca6539f932e0ad554ce84a87fb7936fdba0
diff --git a/android/bazel.go b/android/bazel.go
index dd1de7b..7b227bd 100644
--- a/android/bazel.go
+++ b/android/bazel.go
@@ -134,6 +134,11 @@
 	SetBaseModuleType(baseModuleType string)
 }
 
+// ApiProvider is implemented by modules that contribute to an API surface
+type ApiProvider interface {
+	ConvertWithApiBp2build(ctx TopDownMutatorContext)
+}
+
 // MixedBuildBuildable is an interface that module types should implement in order
 // to be "handled by Bazel" in a mixed build.
 type MixedBuildBuildable interface {
@@ -415,6 +420,13 @@
 		return false
 	}
 
+	// In api_bp2build mode, all soong modules that can provide API contributions should be converted
+	// This is irrespective of its presence/absence in bp2build allowlists
+	if ctx.Config().BuildMode == ApiBp2build {
+		_, providesApis := module.(ApiProvider)
+		return providesApis
+	}
+
 	propValue := b.bazelProperties.Bazel_module.Bp2build_available
 	packagePath := ctx.OtherModuleDir(module)
 
@@ -510,6 +522,17 @@
 	bModule.ConvertWithBp2build(ctx)
 }
 
+func registerApiBp2buildConversionMutator(ctx RegisterMutatorsContext) {
+	ctx.TopDown("apiBp2build_conversion", convertWithApiBp2build).Parallel()
+}
+
+// Generate API contribution targets if the Soong module provides APIs
+func convertWithApiBp2build(ctx TopDownMutatorContext) {
+	if m, ok := ctx.Module().(ApiProvider); ok {
+		m.ConvertWithApiBp2build(ctx)
+	}
+}
+
 // GetMainClassInManifest scans the manifest file specified in filepath and returns
 // the value of attribute Main-Class in the manifest file if it exists, or returns error.
 // WARNING: this is for bp2build converters of java_* modules only.
diff --git a/android/config.go b/android/config.go
index ee432a2..e86fc27 100644
--- a/android/config.go
+++ b/android/config.go
@@ -83,6 +83,9 @@
 	// express build semantics.
 	GenerateQueryView
 
+	// Generate BUILD files for API contributions to API surfaces
+	ApiBp2build
+
 	// Create a JSON representation of the module graph and exit.
 	GenerateModuleGraph
 
diff --git a/android/mutator.go b/android/mutator.go
index 9e4aa59..83d4e66 100644
--- a/android/mutator.go
+++ b/android/mutator.go
@@ -31,22 +31,33 @@
 
 // RegisterMutatorsForBazelConversion is a alternate registration pipeline for bp2build. Exported for testing.
 func RegisterMutatorsForBazelConversion(ctx *Context, preArchMutators []RegisterMutatorFunc) {
+	bp2buildMutators := append(preArchMutators, registerBp2buildConversionMutator)
+	registerMutatorsForBazelConversion(ctx, bp2buildMutators)
+}
+
+// RegisterMutatorsForApiBazelConversion is an alternate registration pipeline for api_bp2build
+// This pipeline restricts generation of Bazel targets to Soong modules that contribute APIs
+func RegisterMutatorsForApiBazelConversion(ctx *Context, preArchMutators []RegisterMutatorFunc) {
+	bp2buildMutators := append(preArchMutators, registerApiBp2buildConversionMutator)
+	registerMutatorsForBazelConversion(ctx, bp2buildMutators)
+}
+
+func registerMutatorsForBazelConversion(ctx *Context, bp2buildMutators []RegisterMutatorFunc) {
 	mctx := &registerMutatorsContext{
 		bazelConversionMode: true,
 	}
 
-	bp2buildMutators := append([]RegisterMutatorFunc{
+	allMutators := append([]RegisterMutatorFunc{
 		RegisterNamespaceMutator,
 		RegisterDefaultsPreArchMutators,
 		// TODO(b/165114590): this is required to resolve deps that are only prebuilts, but we should
 		// evaluate the impact on conversion.
 		RegisterPrebuiltsPreArchMutators,
 	},
-		preArchMutators...)
-	bp2buildMutators = append(bp2buildMutators, registerBp2buildConversionMutator)
+		bp2buildMutators...)
 
 	// Register bp2build mutators
-	for _, f := range bp2buildMutators {
+	for _, f := range allMutators {
 		f(mctx)
 	}
 
diff --git a/android/register.go b/android/register.go
index d4ce5f1..6c69cc5 100644
--- a/android/register.go
+++ b/android/register.go
@@ -180,6 +180,16 @@
 	RegisterMutatorsForBazelConversion(ctx, bp2buildPreArchMutators)
 }
 
+// RegisterForApiBazelConversion is similar to RegisterForBazelConversion except that
+// it only generates API targets in the generated  workspace
+func (ctx *Context) RegisterForApiBazelConversion() {
+	for _, t := range moduleTypes {
+		t.register(ctx)
+	}
+
+	RegisterMutatorsForApiBazelConversion(ctx, bp2buildPreArchMutators)
+}
+
 // Register the pipeline of singletons, module types, and mutators for
 // generating build.ninja and other files for Kati, from Android.bp files.
 func (ctx *Context) Register() {
diff --git a/android/testing.go b/android/testing.go
index 7b74c89..4018659 100644
--- a/android/testing.go
+++ b/android/testing.go
@@ -461,6 +461,12 @@
 	RegisterMutatorsForBazelConversion(ctx.Context, ctx.bp2buildPreArch)
 }
 
+// RegisterForApiBazelConversion prepares a test context for API bp2build conversion.
+func (ctx *TestContext) RegisterForApiBazelConversion() {
+	ctx.config.BuildMode = ApiBp2build
+	RegisterMutatorsForApiBazelConversion(ctx.Context, ctx.bp2buildPreArch)
+}
+
 func (ctx *TestContext) ParseFileList(rootDir string, filePaths []string) (deps []string, errs []error) {
 	// This function adapts the old style ParseFileList calls that are spread throughout the tests
 	// to the new style that takes a config.
diff --git a/bp2build/build_conversion.go b/bp2build/build_conversion.go
index 36c3a48..82ce115 100644
--- a/bp2build/build_conversion.go
+++ b/bp2build/build_conversion.go
@@ -163,6 +163,9 @@
 	// This mode is used for discovering and introspecting the existing Soong
 	// module graph.
 	QueryView
+
+	// ApiBp2build - generate BUILD files for API contribution targets
+	ApiBp2build
 )
 
 type unconvertedDepsMode int
@@ -181,6 +184,8 @@
 		return "Bp2Build"
 	case QueryView:
 		return "QueryView"
+	case ApiBp2build:
+		return "ApiBp2build"
 	default:
 		return fmt.Sprintf("%d", mode)
 	}
@@ -327,6 +332,10 @@
 				errs = append(errs, err)
 			}
 			targets = append(targets, t)
+		case ApiBp2build:
+			if aModule, ok := m.(android.Module); ok && aModule.IsConvertedByBp2build() {
+				targets, errs = generateBazelTargets(bpCtx, aModule)
+			}
 		default:
 			errs = append(errs, fmt.Errorf("Unknown code-generation mode: %s", ctx.Mode()))
 			return
diff --git a/bp2build/build_conversion_test.go b/bp2build/build_conversion_test.go
index d513d04..c168ecb 100644
--- a/bp2build/build_conversion_test.go
+++ b/bp2build/build_conversion_test.go
@@ -1848,3 +1848,27 @@
 			},
 		})
 }
+
+func TestGenerateApiBazelTargets(t *testing.T) {
+	bp := `
+	custom {
+		name: "foo",
+		api: "foo.txt",
+	}
+	`
+	expectedBazelTarget := MakeBazelTarget(
+		"custom_api_contribution",
+		"foo",
+		AttrNameToString{
+			"api": `"foo.txt"`,
+		},
+	)
+	registerCustomModule := func(ctx android.RegistrationContext) {
+		ctx.RegisterModuleType("custom", customModuleFactoryHostAndDevice)
+	}
+	RunApiBp2BuildTestCase(t, registerCustomModule, Bp2buildTestCase{
+		Blueprint:            bp,
+		ExpectedBazelTargets: []string{expectedBazelTarget},
+		Description:          "Generating API contribution Bazel targets for custom module",
+	})
+}
diff --git a/bp2build/bzl_conversion_test.go b/bp2build/bzl_conversion_test.go
index 28d2c75..a8e557d 100644
--- a/bp2build/bzl_conversion_test.go
+++ b/bp2build/bzl_conversion_test.go
@@ -85,6 +85,7 @@
         "soong_module_name": attr.string(mandatory = True),
         "soong_module_variant": attr.string(),
         "soong_module_deps": attr.label_list(providers = [SoongModuleInfo]),
+        "api": attr.string(),
         "arch_paths": attr.string_list(),
         "arch_paths_exclude": attr.string_list(),
         # bazel_module start
@@ -119,6 +120,7 @@
         "soong_module_name": attr.string(mandatory = True),
         "soong_module_variant": attr.string(),
         "soong_module_deps": attr.label_list(providers = [SoongModuleInfo]),
+        "api": attr.string(),
         "arch_paths": attr.string_list(),
         "arch_paths_exclude": attr.string_list(),
         "bool_prop": attr.bool(),
@@ -149,6 +151,7 @@
         "soong_module_name": attr.string(mandatory = True),
         "soong_module_variant": attr.string(),
         "soong_module_deps": attr.label_list(providers = [SoongModuleInfo]),
+        "api": attr.string(),
         "arch_paths": attr.string_list(),
         "arch_paths_exclude": attr.string_list(),
         "bool_prop": attr.bool(),
diff --git a/bp2build/conversion.go b/bp2build/conversion.go
index 522c10e..4d8b8a4 100644
--- a/bp2build/conversion.go
+++ b/bp2build/conversion.go
@@ -97,7 +97,7 @@
 		targets.sort()
 
 		var content string
-		if mode == Bp2Build {
+		if mode == Bp2Build || mode == ApiBp2build {
 			content = `# READ THIS FIRST:
 # This file was automatically generated by bp2build for the Bazel migration project.
 # Feel free to edit or test it, but do *not* check it into your version control system.
diff --git a/bp2build/testing.go b/bp2build/testing.go
index c2c1b19..31aa830 100644
--- a/bp2build/testing.go
+++ b/bp2build/testing.go
@@ -24,6 +24,8 @@
 	"strings"
 	"testing"
 
+	"github.com/google/blueprint/proptools"
+
 	"android/soong/android"
 	"android/soong/android/allowlists"
 	"android/soong/bazel"
@@ -88,6 +90,22 @@
 }
 
 func RunBp2BuildTestCase(t *testing.T, registerModuleTypes func(ctx android.RegistrationContext), tc Bp2buildTestCase) {
+	bp2buildSetup := func(ctx *android.TestContext) {
+		registerModuleTypes(ctx)
+		ctx.RegisterForBazelConversion()
+	}
+	runBp2BuildTestCaseWithSetup(t, bp2buildSetup, tc)
+}
+
+func RunApiBp2BuildTestCase(t *testing.T, registerModuleTypes func(ctx android.RegistrationContext), tc Bp2buildTestCase) {
+	apiBp2BuildSetup := func(ctx *android.TestContext) {
+		registerModuleTypes(ctx)
+		ctx.RegisterForApiBazelConversion()
+	}
+	runBp2BuildTestCaseWithSetup(t, apiBp2BuildSetup, tc)
+}
+
+func runBp2BuildTestCaseWithSetup(t *testing.T, setup func(ctx *android.TestContext), tc Bp2buildTestCase) {
 	t.Helper()
 	dir := "."
 	filesystem := make(map[string][]byte)
@@ -103,7 +121,7 @@
 	config := android.TestConfig(buildDir, nil, tc.Blueprint, filesystem)
 	ctx := android.NewTestContext(config)
 
-	registerModuleTypes(ctx)
+	setup(ctx)
 	ctx.RegisterModuleType(tc.ModuleTypeUnderTest, tc.ModuleTypeUnderTestFactory)
 
 	// A default configuration for tests to not have to specify bp2build_available on top level targets.
@@ -118,7 +136,6 @@
 		})
 	}
 	ctx.RegisterBp2BuildConfig(bp2buildConfig)
-	ctx.RegisterForBazelConversion()
 
 	_, parseErrs := ctx.ParseFileList(dir, toParse)
 	if errored(t, tc, parseErrs) {
@@ -198,6 +215,8 @@
 
 	// Prop used to indicate this conversion should be 1 module -> multiple targets
 	One_to_many_prop *bool
+
+	Api *string // File describing the APIs of this module
 }
 
 type customModule struct {
@@ -320,6 +339,7 @@
 	String_ptr_prop     *string
 	String_list_prop    []string
 	Arch_paths          bazel.LabelListAttribute
+	Api                 bazel.LabelAttribute
 }
 
 func (m *customModule) ConvertWithBp2build(ctx android.TopDownMutatorContext) {
@@ -364,6 +384,23 @@
 	ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: m.Name()}, attrs)
 }
 
+var _ android.ApiProvider = (*customModule)(nil)
+
+func (c *customModule) ConvertWithApiBp2build(ctx android.TopDownMutatorContext) {
+	props := bazel.BazelTargetModuleProperties{
+		Rule_class: "custom_api_contribution",
+	}
+	apiAttribute := bazel.MakeLabelAttribute(
+		android.BazelLabelForModuleSrcSingle(ctx, proptools.String(c.props.Api)).Label,
+	)
+	attrs := &customBazelModuleAttributes{
+		Api: *apiAttribute,
+	}
+	ctx.CreateBazelTargetModule(props,
+		android.CommonAttributes{Name: c.Name()},
+		attrs)
+}
+
 // A bp2build mutator that uses load statements and creates a 1:M mapping from
 // module to target.
 func customBp2buildOneToMany(ctx android.TopDownMutatorContext, m *customModule) {
diff --git a/cmd/soong_build/main.go b/cmd/soong_build/main.go
index 0b8cc88..770ad0c 100644
--- a/cmd/soong_build/main.go
+++ b/cmd/soong_build/main.go
@@ -24,6 +24,7 @@
 	"time"
 
 	"android/soong/android"
+	"android/soong/bazel"
 	"android/soong/bp2build"
 	"android/soong/shared"
 	"android/soong/ui/metrics/bp2build_metrics_proto"
@@ -48,11 +49,12 @@
 	delveListen string
 	delvePath   string
 
-	moduleGraphFile   string
-	moduleActionsFile string
-	docFile           string
-	bazelQueryViewDir string
-	bp2buildMarker    string
+	moduleGraphFile     string
+	moduleActionsFile   string
+	docFile             string
+	bazelQueryViewDir   string
+	bazelApiBp2buildDir string
+	bp2buildMarker      string
 
 	cmdlineArgs bootstrap.Args
 )
@@ -81,6 +83,7 @@
 	flag.StringVar(&moduleActionsFile, "module_actions_file", "", "JSON file to output inputs/outputs of actions of modules")
 	flag.StringVar(&docFile, "soong_docs", "", "build documentation file to output")
 	flag.StringVar(&bazelQueryViewDir, "bazel_queryview_dir", "", "path to the bazel queryview directory relative to --top")
+	flag.StringVar(&bazelApiBp2buildDir, "bazel_api_bp2build_dir", "", "path to the bazel api_bp2build directory relative to --top")
 	flag.StringVar(&bp2buildMarker, "bp2build_marker", "", "If set, run bp2build, touch the specified marker file then exit")
 	flag.StringVar(&cmdlineArgs.OutFile, "o", "build.ninja", "the Ninja file to output")
 	flag.BoolVar(&cmdlineArgs.EmptyNinjaFile, "empty-ninja-file", false, "write out a 0-byte ninja file")
@@ -129,6 +132,8 @@
 		buildMode = android.Bp2build
 	} else if bazelQueryViewDir != "" {
 		buildMode = android.GenerateQueryView
+	} else if bazelApiBp2buildDir != "" {
+		buildMode = android.ApiBp2build
 	} else if moduleGraphFile != "" {
 		buildMode = android.GenerateModuleGraph
 	} else if docFile != "" {
@@ -178,7 +183,7 @@
 	defer ctx.EventHandler.End("queryview")
 	codegenContext := bp2build.NewCodegenContext(configuration, *ctx, bp2build.QueryView)
 	absoluteQueryViewDir := shared.JoinPath(topDir, queryviewDir)
-	if err := createBazelQueryView(codegenContext, absoluteQueryViewDir); err != nil {
+	if err := createBazelWorkspace(codegenContext, absoluteQueryViewDir); err != nil {
 		fmt.Fprintf(os.Stderr, "%s", err)
 		os.Exit(1)
 	}
@@ -186,6 +191,96 @@
 	touch(shared.JoinPath(topDir, queryviewMarker))
 }
 
+// Run the code-generation phase to convert API contributions to BUILD files.
+// Return marker file for the new synthetic workspace
+func runApiBp2build(configuration android.Config, extraNinjaDeps []string) string {
+	// Create a new context and register mutators that are only meaningful to API export
+	ctx := android.NewContext(configuration)
+	ctx.EventHandler.Begin("api_bp2build")
+	defer ctx.EventHandler.End("api_bp2build")
+	ctx.SetNameInterface(newNameResolver(configuration))
+	ctx.RegisterForApiBazelConversion()
+
+	// Register the Android.bp files in the tree
+	// Add them to the workspace's .d file
+	ctx.SetModuleListFile(cmdlineArgs.ModuleListFile)
+	if paths, err := ctx.ListModulePaths("."); err == nil {
+		extraNinjaDeps = append(extraNinjaDeps, paths...)
+	} else {
+		panic(err)
+	}
+
+	// Run the loading and analysis phase
+	ninjaDeps := bootstrap.RunBlueprint(cmdlineArgs,
+		bootstrap.StopBeforePrepareBuildActions,
+		ctx.Context,
+		configuration)
+	ninjaDeps = append(ninjaDeps, extraNinjaDeps...)
+
+	// Add the globbed dependencies
+	globs := writeBuildGlobsNinjaFile(ctx, configuration.SoongOutDir(), configuration)
+	ninjaDeps = append(ninjaDeps, globs...)
+
+	// Run codegen to generate BUILD files
+	codegenContext := bp2build.NewCodegenContext(configuration, *ctx, bp2build.ApiBp2build)
+	absoluteApiBp2buildDir := shared.JoinPath(topDir, bazelApiBp2buildDir)
+	if err := createBazelWorkspace(codegenContext, absoluteApiBp2buildDir); err != nil {
+		fmt.Fprintf(os.Stderr, "%s", err)
+		os.Exit(1)
+	}
+	ninjaDeps = append(ninjaDeps, codegenContext.AdditionalNinjaDeps()...)
+
+	// Create soong_injection repository
+	soongInjectionFiles := bp2build.CreateSoongInjectionFiles(configuration, bp2build.CodegenMetrics{})
+	absoluteSoongInjectionDir := shared.JoinPath(topDir, configuration.SoongOutDir(), bazel.SoongInjectionDirName)
+	for _, file := range soongInjectionFiles {
+		writeReadOnlyFile(absoluteSoongInjectionDir, file)
+	}
+
+	workspace := shared.JoinPath(configuration.SoongOutDir(), "api_bp2build")
+
+	excludes := bazelArtifacts()
+	// Exclude all src BUILD files
+	excludes = append(excludes, apiBuildFileExcludes()...)
+
+	// Create the symlink forest
+	symlinkDeps := bp2build.PlantSymlinkForest(
+		configuration,
+		topDir,
+		workspace,
+		bazelApiBp2buildDir,
+		".",
+		excludes)
+	ninjaDeps = append(ninjaDeps, symlinkDeps...)
+
+	workspaceMarkerFile := workspace + ".marker"
+	writeDepFile(workspaceMarkerFile, *ctx.EventHandler, ninjaDeps)
+	touch(shared.JoinPath(topDir, workspaceMarkerFile))
+	return workspaceMarkerFile
+}
+
+// With some exceptions, api_bp2build does not have any dependencies on the checked-in BUILD files
+// Exclude them from the generated workspace to prevent unrelated errors during the loading phase
+func apiBuildFileExcludes() []string {
+	ret := make([]string, 0)
+
+	srcs, err := getExistingBazelRelatedFiles(topDir)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error determining existing Bazel-related files: %s\n", err)
+		os.Exit(1)
+	}
+	for _, src := range srcs {
+		if src != "WORKSPACE" &&
+			src != "BUILD" &&
+			src != "BUILD.bazel" &&
+			!strings.HasPrefix(src, "build/bazel") &&
+			!strings.HasPrefix(src, "prebuilts/clang") {
+			ret = append(ret, src)
+		}
+	}
+	return ret
+}
+
 func writeMetrics(configuration android.Config, eventHandler metrics.EventHandler, metricsDir string) {
 	if len(metricsDir) < 1 {
 		fmt.Fprintf(os.Stderr, "\nMissing required env var for generating soong metrics: LOG_DIR\n")
@@ -248,6 +343,8 @@
 		return bp2buildMarker
 	} else if configuration.IsMixedBuildsEnabled() {
 		runMixedModeBuild(configuration, ctx, extraNinjaDeps)
+	} else if configuration.BuildMode == android.ApiBp2build {
+		return runApiBp2build(configuration, extraNinjaDeps)
 	} else {
 		var stopBefore bootstrap.StopBefore
 		if configuration.BuildMode == android.GenerateModuleGraph {
@@ -476,6 +573,16 @@
 	return files, nil
 }
 
+func bazelArtifacts() []string {
+	return []string{
+		"bazel-bin",
+		"bazel-genfiles",
+		"bazel-out",
+		"bazel-testlogs",
+		"bazel-" + filepath.Base(topDir),
+	}
+}
+
 // Run Soong in the bp2build mode. This creates a standalone context that registers
 // an alternate pipeline of mutators and singletons specifically for generating
 // Bazel BUILD files instead of Ninja files.
@@ -524,13 +631,7 @@
 		generatedRoot := shared.JoinPath(configuration.SoongOutDir(), "bp2build")
 		workspaceRoot := shared.JoinPath(configuration.SoongOutDir(), "workspace")
 
-		excludes := []string{
-			"bazel-bin",
-			"bazel-genfiles",
-			"bazel-out",
-			"bazel-testlogs",
-			"bazel-" + filepath.Base(topDir),
-		}
+		excludes := bazelArtifacts()
 
 		if outDir[0] != '/' {
 			excludes = append(excludes, outDir)
diff --git a/cmd/soong_build/queryview.go b/cmd/soong_build/queryview.go
index 983dbf0..cd1d6fb 100644
--- a/cmd/soong_build/queryview.go
+++ b/cmd/soong_build/queryview.go
@@ -23,8 +23,9 @@
 	"android/soong/bp2build"
 )
 
-func createBazelQueryView(ctx *bp2build.CodegenContext, bazelQueryViewDir string) error {
-	os.RemoveAll(bazelQueryViewDir)
+// A helper function to generate a Read-only Bazel workspace in outDir
+func createBazelWorkspace(ctx *bp2build.CodegenContext, outDir string) error {
+	os.RemoveAll(outDir)
 	ruleShims := bp2build.CreateRuleShims(android.ModuleTypeFactories())
 
 	res, err := bp2build.GenerateBazelTargets(ctx, true)
@@ -33,9 +34,9 @@
 	}
 
 	filesToWrite := bp2build.CreateBazelFiles(ctx.Config(), ruleShims, res.BuildDirToTargets(),
-		bp2build.QueryView)
+		ctx.Mode())
 	for _, f := range filesToWrite {
-		if err := writeReadOnlyFile(bazelQueryViewDir, f); err != nil {
+		if err := writeReadOnlyFile(outDir, f); err != nil {
 			return err
 		}
 	}
diff --git a/ui/build/config.go b/ui/build/config.go
index 14a99d0..2060660 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -71,6 +71,7 @@
 	checkbuild      bool
 	dist            bool
 	jsonModuleGraph bool
+	apiBp2build     bool // Generate BUILD files for Soong modules that contribute APIs
 	bp2build        bool
 	queryview       bool
 	reportMkMetrics bool // Collect and report mk2bp migration progress metrics.
@@ -756,6 +757,8 @@
 			c.jsonModuleGraph = true
 		} else if arg == "bp2build" {
 			c.bp2build = true
+		} else if arg == "api_bp2build" {
+			c.apiBp2build = true
 		} else if arg == "queryview" {
 			c.queryview = true
 		} else if arg == "soong_docs" {
@@ -833,7 +836,7 @@
 		return true
 	}
 
-	if !c.JsonModuleGraph() && !c.Bp2Build() && !c.Queryview() && !c.SoongDocs() {
+	if !c.JsonModuleGraph() && !c.Bp2Build() && !c.Queryview() && !c.SoongDocs() && !c.ApiBp2build() {
 		// Command line was empty, the default Ninja target is built
 		return true
 	}
@@ -916,6 +919,10 @@
 	return shared.JoinPath(c.SoongOutDir(), "queryview.marker")
 }
 
+func (c *configImpl) ApiBp2buildMarkerFile() string {
+	return shared.JoinPath(c.SoongOutDir(), "api_bp2build.marker")
+}
+
 func (c *configImpl) ModuleGraphFile() string {
 	return shared.JoinPath(c.SoongOutDir(), "module-graph.json")
 }
@@ -957,6 +964,10 @@
 	return c.bp2build
 }
 
+func (c *configImpl) ApiBp2build() bool {
+	return c.apiBp2build
+}
+
 func (c *configImpl) Queryview() bool {
 	return c.queryview
 }
diff --git a/ui/build/soong.go b/ui/build/soong.go
index ac00fe6..e0d67cc 100644
--- a/ui/build/soong.go
+++ b/ui/build/soong.go
@@ -45,6 +45,7 @@
 	bp2buildTag        = "bp2build"
 	jsonModuleGraphTag = "modulegraph"
 	queryviewTag       = "queryview"
+	apiBp2buildTag     = "api_bp2build"
 	soongDocsTag       = "soong_docs"
 
 	// bootstrapEpoch is used to determine if an incremental build is incompatible with the current
@@ -237,6 +238,7 @@
 		config.NamedGlobFile(bp2buildTag),
 		config.NamedGlobFile(jsonModuleGraphTag),
 		config.NamedGlobFile(queryviewTag),
+		config.NamedGlobFile(apiBp2buildTag),
 		config.NamedGlobFile(soongDocsTag),
 	}
 }
@@ -307,6 +309,19 @@
 		fmt.Sprintf("generating the Soong module graph as a Bazel workspace at %s", queryviewDir),
 	)
 
+	// The BUILD files will be generated in out/soong/.api_bp2build (no symlinks to src files)
+	// The final workspace will be generated in out/soong/api_bp2build
+	apiBp2buildDir := filepath.Join(config.SoongOutDir(), ".api_bp2build")
+	apiBp2buildInvocation := primaryBuilderInvocation(
+		config,
+		apiBp2buildTag,
+		config.ApiBp2buildMarkerFile(),
+		[]string{
+			"--bazel_api_bp2build_dir", apiBp2buildDir,
+		},
+		fmt.Sprintf("generating BUILD files for API contributions at %s", apiBp2buildDir),
+	)
+
 	soongDocsInvocation := primaryBuilderInvocation(
 		config,
 		soongDocsTag,
@@ -345,6 +360,7 @@
 			bp2buildInvocation,
 			jsonModuleGraphInvocation,
 			queryviewInvocation,
+			apiBp2buildInvocation,
 			soongDocsInvocation},
 	}
 
@@ -417,6 +433,10 @@
 			checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(queryviewTag))
 		}
 
+		if config.ApiBp2build() {
+			checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(apiBp2buildTag))
+		}
+
 		if config.SoongDocs() {
 			checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(soongDocsTag))
 		}
@@ -480,6 +500,10 @@
 		targets = append(targets, config.QueryviewMarkerFile())
 	}
 
+	if config.ApiBp2build() {
+		targets = append(targets, config.ApiBp2buildMarkerFile())
+	}
+
 	if config.SoongDocs() {
 		targets = append(targets, config.SoongDocsHtml())
 	}