Merge "Create a new mode in soong_ui to generate API only BUILD files"
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 c1fb800..7c24a94 100644
--- a/bp2build/build_conversion_test.go
+++ b/bp2build/build_conversion_test.go
@@ -1853,3 +1853,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())
 	}