Merge "Add bazelMode arguments to multiproduct_kati."
diff --git a/android/arch.go b/android/arch.go
index 75ee922..086e945 100644
--- a/android/arch.go
+++ b/android/arch.go
@@ -1687,14 +1687,12 @@
 	abi         []string
 }
 
-// getNdkAbisConfig returns the list of archConfigs that are used for bulding
-// the API stubs and static libraries that are included in the NDK. These are
-// built *without Neon*, because non-Neon is still supported and building these
-// with Neon will break those users.
+// getNdkAbisConfig returns the list of archConfigs that are used for building
+// the API stubs and static libraries that are included in the NDK.
 func getNdkAbisConfig() []archConfig {
 	return []archConfig{
 		{"arm64", "armv8-a-branchprot", "", []string{"arm64-v8a"}},
-		{"arm", "armv7-a", "", []string{"armeabi-v7a"}},
+		{"arm", "armv7-a-neon", "", []string{"armeabi-v7a"}},
 		{"x86_64", "", "", []string{"x86_64"}},
 		{"x86", "", "", []string{"x86"}},
 	}
diff --git a/android/bazel_handler.go b/android/bazel_handler.go
index e829326..c157d39 100644
--- a/android/bazel_handler.go
+++ b/android/bazel_handler.go
@@ -558,7 +558,6 @@
 		// The actual platform values here may be overridden by configuration
 		// transitions from the buildroot.
 		fmt.Sprintf("--extra_toolchains=%s", "//prebuilts/clang/host/linux-x86:all"),
-
 		// This should be parameterized on the host OS, but let's restrict to linux
 		// to keep things simple for now.
 		fmt.Sprintf("--host_platform=%s", "//build/bazel/platforms:linux_x86_64"),
@@ -927,7 +926,7 @@
 	//
 	// Use jsonproto instead of proto; actual proto parsing would require a dependency on Bazel's
 	// proto sources, which would add a number of unnecessary dependencies.
-	extraFlags := []string{"--output=jsonproto", "--include_file_write_contents"}
+	extraFlags := []string{"--output=proto", "--include_file_write_contents"}
 	if Bool(config.productVariables.ClangCoverage) {
 		extraFlags = append(extraFlags, "--collect_code_coverage")
 		paths := make([]string, 0, 2)
diff --git a/android/bazel_handler_test.go b/android/bazel_handler_test.go
index dc2261c..c857272 100644
--- a/android/bazel_handler_test.go
+++ b/android/bazel_handler_test.go
@@ -1,6 +1,7 @@
 package android
 
 import (
+	"encoding/json"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -8,6 +9,8 @@
 	"testing"
 
 	"android/soong/bazel/cquery"
+	"google.golang.org/protobuf/proto"
+	analysis_v2_proto "prebuilts/bazel/common/proto/analysis_v2"
 )
 
 var testConfig = TestConfig("out", nil, "", nil)
@@ -65,52 +68,56 @@
 	var testCases = []testCase{
 		{`
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [1],
-    "primaryOutputId": 1
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 2] }],
-  "pathFragments": [
-    { "id": 1, "label": "one" },
-    { "id": 2, "label": "two" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 }],
+ "actions": [{
+   "target_Id": 1,
+   "action_Key": "x",
+   "mnemonic": "x",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [1],
+   "output_Ids": [1],
+   "primary_output_id": 1
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1, 2] }],
+ "path_fragments": [
+   { "id": 1, "label": "one" },
+   { "id": 2, "label": "two" }]
 }`,
 			"cd 'test/exec_root' && rm -f 'one' && touch foo",
 		}, {`
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 10 },
-    { "id": 2, "pathFragmentId": 20 }],
-  "actions": [{
-    "targetId": 100,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["bogus", "command"],
-    "outputIds": [1, 2],
-    "primaryOutputId": 1
-  }],
-  "pathFragments": [
-    { "id": 10, "label": "one", "parentId": 30 },
-    { "id": 20, "label": "one.d", "parentId": 30 },
-    { "id": 30, "label": "parent" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 10 },
+   { "id": 2, "path_fragment_id": 20 }],
+ "actions": [{
+   "target_Id": 100,
+   "action_Key": "x",
+   "mnemonic": "x",
+   "arguments": ["bogus", "command"],
+   "output_Ids": [1, 2],
+   "primary_output_id": 1
+ }],
+ "path_fragments": [
+   { "id": 10, "label": "one", "parent_id": 30 },
+   { "id": 20, "label": "one.d", "parent_id": 30 },
+   { "id": 30, "label": "parent" }]
 }`,
 			`cd 'test/exec_root' && rm -f 'parent/one' && bogus command && sed -i'' -E 's@(^|\s|")bazel-out/@\1test/bazel_out/@g' 'parent/one.d'`,
 		},
 	}
 
 	for i, testCase := range testCases {
+		data, err := JsonToActionGraphContainer(testCase.input)
+		if err != nil {
+			t.Error(err)
+		}
 		bazelContext, _ := testBazelContext(t, map[bazelCommand]string{
-			bazelCommand{command: "aquery", expression: "deps(@soong_injection//mixed_builds:buildroot)"}: testCase.input})
+			bazelCommand{command: "aquery", expression: "deps(@soong_injection//mixed_builds:buildroot)"}: string(data)})
 
-		err := bazelContext.InvokeBazel(testConfig)
+		err = bazelContext.InvokeBazel(testConfig)
 		if err != nil {
 			t.Fatalf("testCase #%d: did not expect error invoking Bazel, but got %s", i+1, err)
 		}
@@ -194,3 +201,14 @@
 		requests:    map[cqueryKey]bool{},
 	}, p.soongOutDir
 }
+
+// Transform the json format to ActionGraphContainer
+func JsonToActionGraphContainer(inputString string) ([]byte, error) {
+	var aqueryProtoResult analysis_v2_proto.ActionGraphContainer
+	err := json.Unmarshal([]byte(inputString), &aqueryProtoResult)
+	if err != nil {
+		return []byte(""), err
+	}
+	data, _ := proto.Marshal(&aqueryProtoResult)
+	return data, err
+}
diff --git a/android/config.go b/android/config.go
index 1ed405b..df2c767 100644
--- a/android/config.go
+++ b/android/config.go
@@ -75,6 +75,9 @@
 	// Don't use bazel at all during module analysis.
 	AnalysisNoBazel SoongBuildMode = iota
 
+	// Symlink fores mode: merge two directory trees into a symlink forest
+	SymlinkForest
+
 	// Bp2build mode: Generate BUILD files from blueprint files and exit.
 	Bp2build
 
diff --git a/android/makevars.go b/android/makevars.go
index 5165a55..0800190 100644
--- a/android/makevars.go
+++ b/android/makevars.go
@@ -471,7 +471,7 @@
 			fmt.Fprintf(buf, " %s", dep.String())
 		}
 		fmt.Fprintln(buf)
-		fmt.Fprintln(buf, "\t@echo \"Install $@\"")
+		fmt.Fprintln(buf, "\t@echo \"Install: $@\"")
 		fmt.Fprintf(buf, "\trm -f $@ && cp -f %s $< $@\n", preserveSymlinksFlag)
 		if install.executable {
 			fmt.Fprintf(buf, "\tchmod +x $@\n")
@@ -515,7 +515,7 @@
 			fromStr = symlink.absFrom
 		}
 
-		fmt.Fprintln(buf, "\t@echo \"Symlink $@\"")
+		fmt.Fprintln(buf, "\t@echo \"Symlink: $@\"")
 		fmt.Fprintf(buf, "\trm -f $@ && ln -sfn %s $@", fromStr)
 		fmt.Fprintln(buf)
 		fmt.Fprintln(buf)
diff --git a/android/module.go b/android/module.go
index 5aecb05..b41a898 100644
--- a/android/module.go
+++ b/android/module.go
@@ -928,6 +928,8 @@
 	Tags bazel.StringListAttribute
 
 	Applicable_licenses bazel.LabelListAttribute
+
+	Testonly *bool
 }
 
 // constraintAttributes represents Bazel attributes pertaining to build constraints,
diff --git a/android/neverallow.go b/android/neverallow.go
index cf149b2..d288439 100644
--- a/android/neverallow.go
+++ b/android/neverallow.go
@@ -69,6 +69,7 @@
 func createBp2BuildRule() Rule {
 	return NeverAllow().
 		With("bazel_module.bp2build_available", "true").
+		NotIn("soong_tests"). // only used in tests
 		Because("setting bp2build_available in Android.bp is not " +
 			"supported for custom conversion, use allowlists.go instead.")
 }
diff --git a/apex/apex.go b/apex/apex.go
index 88a057f..09cdc82 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -17,13 +17,14 @@
 package apex
 
 import (
-	"android/soong/bazel/cquery"
 	"fmt"
 	"path/filepath"
 	"regexp"
 	"sort"
 	"strings"
 
+	"android/soong/bazel/cquery"
+
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/bootstrap"
 	"github.com/google/blueprint/proptools"
@@ -47,7 +48,7 @@
 
 func registerApexBuildComponents(ctx android.RegistrationContext) {
 	ctx.RegisterModuleType("apex", BundleFactory)
-	ctx.RegisterModuleType("apex_test", testApexBundleFactory)
+	ctx.RegisterModuleType("apex_test", TestApexBundleFactory)
 	ctx.RegisterModuleType("apex_vndk", vndkApexBundleFactory)
 	ctx.RegisterModuleType("apex_defaults", defaultsFactory)
 	ctx.RegisterModuleType("prebuilt_apex", PrebuiltFactory)
@@ -2563,7 +2564,7 @@
 
 // apex_test is an APEX for testing. The difference from the ordinary apex module type is that
 // certain compatibility checks such as apex_available are not done for apex_test.
-func testApexBundleFactory() android.Module {
+func TestApexBundleFactory() android.Module {
 	bundle := newApexBundle()
 	bundle.testApex = true
 	return bundle
@@ -3359,6 +3360,7 @@
 	Compressible          bazel.BoolAttribute
 	Package_name          *string
 	Logging_parent        *string
+	Tests                 bazel.LabelListAttribute
 }
 
 type convertedNativeSharedLibs struct {
@@ -3368,13 +3370,19 @@
 
 // ConvertWithBp2build performs bp2build conversion of an apex
 func (a *apexBundle) ConvertWithBp2build(ctx android.TopDownMutatorContext) {
-	// We do not convert apex_test modules at this time
-	if ctx.ModuleType() != "apex" {
+	// We only convert apex and apex_test modules at this time
+	if ctx.ModuleType() != "apex" && ctx.ModuleType() != "apex_test" {
 		return
 	}
 
 	attrs, props := convertWithBp2build(a, ctx)
-	ctx.CreateBazelTargetModule(props, android.CommonAttributes{Name: a.Name()}, &attrs)
+	commonAttrs := android.CommonAttributes{
+		Name: a.Name(),
+	}
+	if a.testApex {
+		commonAttrs.Testonly = proptools.BoolPtr(a.testApex)
+	}
+	ctx.CreateBazelTargetModule(props, commonAttrs, &attrs)
 }
 
 func convertWithBp2build(a *apexBundle, ctx android.TopDownMutatorContext) (bazelApexBundleAttributes, bazel.BazelTargetModuleProperties) {
@@ -3441,6 +3449,12 @@
 	binaries := android.BazelLabelForModuleDeps(ctx, a.properties.ApexNativeDependencies.Binaries)
 	binariesLabelListAttribute := bazel.MakeLabelListAttribute(binaries)
 
+	var testsAttrs bazel.LabelListAttribute
+	if a.testApex && len(a.properties.ApexNativeDependencies.Tests) > 0 {
+		tests := android.BazelLabelForModuleDeps(ctx, a.properties.ApexNativeDependencies.Tests)
+		testsAttrs = bazel.MakeLabelListAttribute(tests)
+	}
+
 	var updatableAttribute bazel.BoolAttribute
 	if a.properties.Updatable != nil {
 		updatableAttribute.Value = a.properties.Updatable
@@ -3483,6 +3497,7 @@
 		Compressible:          compressibleAttribute,
 		Package_name:          packageName,
 		Logging_parent:        loggingParent,
+		Tests:                 testsAttrs,
 	}
 
 	props := bazel.BazelTargetModuleProperties{
diff --git a/bazel/Android.bp b/bazel/Android.bp
index 9e7edc7..d11c78b 100644
--- a/bazel/Android.bp
+++ b/bazel/Android.bp
@@ -20,6 +20,7 @@
         "soong_build",
     ],
     deps: [
+        "bazel_analysis_v2_proto",
         "blueprint",
     ],
 }
diff --git a/bazel/aquery.go b/bazel/aquery.go
index 05f6ed4..bc823b3 100644
--- a/bazel/aquery.go
+++ b/bazel/aquery.go
@@ -17,7 +17,6 @@
 import (
 	"crypto/sha256"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
 	"path/filepath"
 	"reflect"
@@ -25,6 +24,8 @@
 	"strings"
 
 	"github.com/google/blueprint/proptools"
+	"google.golang.org/protobuf/proto"
+	analysis_v2_proto "prebuilts/bazel/common/proto/analysis_v2"
 )
 
 type artifactId int
@@ -312,11 +313,79 @@
 // BuildStatements are one-to-one with actions in the given action graph, and AqueryDepsets
 // are one-to-one with Bazel's depSetOfFiles objects.
 func AqueryBuildStatements(aqueryJsonProto []byte) ([]BuildStatement, []AqueryDepset, error) {
-	var aqueryResult actionGraphContainer
-	err := json.Unmarshal(aqueryJsonProto, &aqueryResult)
+	aqueryProto := &analysis_v2_proto.ActionGraphContainer{}
+	err := proto.Unmarshal(aqueryJsonProto, aqueryProto)
 	if err != nil {
 		return nil, nil, err
 	}
+	aqueryResult := actionGraphContainer{}
+
+	for _, protoArtifact := range aqueryProto.Artifacts {
+		aqueryResult.Artifacts = append(aqueryResult.Artifacts, artifact{artifactId(protoArtifact.Id),
+			pathFragmentId(protoArtifact.PathFragmentId)})
+	}
+
+	for _, protoAction := range aqueryProto.Actions {
+		var environmentVariable []KeyValuePair
+		var inputDepSetIds []depsetId
+		var outputIds []artifactId
+		var substitutions []KeyValuePair
+
+		for _, protoEnvironmentVariable := range protoAction.EnvironmentVariables {
+			environmentVariable = append(environmentVariable, KeyValuePair{
+				protoEnvironmentVariable.Key, protoEnvironmentVariable.Value,
+			})
+		}
+		for _, protoInputDepSetIds := range protoAction.InputDepSetIds {
+			inputDepSetIds = append(inputDepSetIds, depsetId(protoInputDepSetIds))
+		}
+		for _, protoOutputIds := range protoAction.OutputIds {
+			outputIds = append(outputIds, artifactId(protoOutputIds))
+		}
+		for _, protoSubstitutions := range protoAction.Substitutions {
+			substitutions = append(substitutions, KeyValuePair{
+				protoSubstitutions.Key, protoSubstitutions.Value,
+			})
+		}
+
+		aqueryResult.Actions = append(aqueryResult.Actions,
+			action{
+				Arguments:            protoAction.Arguments,
+				EnvironmentVariables: environmentVariable,
+				InputDepSetIds:       inputDepSetIds,
+				Mnemonic:             protoAction.Mnemonic,
+				OutputIds:            outputIds,
+				TemplateContent:      protoAction.TemplateContent,
+				Substitutions:        substitutions,
+				FileContents:         protoAction.FileContents})
+	}
+
+	for _, protoDepSetOfFiles := range aqueryProto.DepSetOfFiles {
+		var directArtifactIds []artifactId
+		var transitiveDepSetIds []depsetId
+
+		for _, protoDirectArtifactIds := range protoDepSetOfFiles.DirectArtifactIds {
+			directArtifactIds = append(directArtifactIds, artifactId(protoDirectArtifactIds))
+		}
+		for _, protoTransitiveDepSetIds := range protoDepSetOfFiles.TransitiveDepSetIds {
+			transitiveDepSetIds = append(transitiveDepSetIds, depsetId(protoTransitiveDepSetIds))
+		}
+		aqueryResult.DepSetOfFiles = append(aqueryResult.DepSetOfFiles,
+			depSetOfFiles{
+				Id:                  depsetId(protoDepSetOfFiles.Id),
+				DirectArtifactIds:   directArtifactIds,
+				TransitiveDepSetIds: transitiveDepSetIds})
+
+	}
+
+	for _, protoPathFragments := range aqueryProto.PathFragments {
+		aqueryResult.PathFragments = append(aqueryResult.PathFragments,
+			pathFragment{
+				Id:       pathFragmentId(protoPathFragments.Id),
+				Label:    protoPathFragments.Label,
+				ParentId: pathFragmentId(protoPathFragments.ParentId)})
+
+	}
 	aqueryHandler, err := newAqueryHandler(aqueryResult)
 	if err != nil {
 		return nil, nil, err
diff --git a/bazel/aquery_test.go b/bazel/aquery_test.go
index 5810364..2eacafa 100644
--- a/bazel/aquery_test.go
+++ b/bazel/aquery_test.go
@@ -15,118 +15,128 @@
 package bazel
 
 import (
+	"encoding/json"
 	"fmt"
 	"reflect"
 	"sort"
 	"testing"
+
+	"google.golang.org/protobuf/proto"
+	analysis_v2_proto "prebuilts/bazel/common/proto/analysis_v2"
 )
 
 func TestAqueryMultiArchGenrule(t *testing.T) {
 	// This input string is retrieved from a real build of bionic-related genrules.
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 6 },
-    { "id": 3, "pathFragmentId": 8 },
-    { "id": 4, "pathFragmentId": 12 },
-    { "id": 5, "pathFragmentId": 19 },
-    { "id": 6, "pathFragmentId": 20 },
-    { "id": 7, "pathFragmentId": 21 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "ab53f6ecbdc2ee8cb8812613b63205464f1f5083f6dca87081a0a398c0f1ecf7",
-    "mnemonic": "Genrule",
-    "configurationId": 1,
-    "arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py arm ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-arm.S"],
-    "environmentVariables": [{
-      "key": "PATH",
-      "value": "/bin:/usr/bin:/usr/local/bin"
-    }],
-    "inputDepSetIds": [1],
-    "outputIds": [4],
-    "primaryOutputId": 4
-  }, {
-    "targetId": 2,
-    "actionKey": "9f4309ce165dac458498cb92811c18b0b7919782cc37b82a42d2141b8cc90826",
-    "mnemonic": "Genrule",
-    "configurationId": 1,
-    "arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py x86 ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-x86.S"],
-    "environmentVariables": [{
-      "key": "PATH",
-      "value": "/bin:/usr/bin:/usr/local/bin"
-    }],
-    "inputDepSetIds": [2],
-    "outputIds": [5],
-    "primaryOutputId": 5
-  }, {
-    "targetId": 3,
-    "actionKey": "50d6c586103ebeed3a218195540bcc30d329464eae36377eb82f8ce7c36ac342",
-    "mnemonic": "Genrule",
-    "configurationId": 1,
-    "arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py x86_64 ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-x86_64.S"],
-    "environmentVariables": [{
-      "key": "PATH",
-      "value": "/bin:/usr/bin:/usr/local/bin"
-    }],
-    "inputDepSetIds": [3],
-    "outputIds": [6],
-    "primaryOutputId": 6
-  }, {
-    "targetId": 4,
-    "actionKey": "f30cbe442f5216f4223cf16a39112cad4ec56f31f49290d85cff587e48647ffa",
-    "mnemonic": "Genrule",
-    "configurationId": 1,
-    "arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py arm64 ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-arm64.S"],
-    "environmentVariables": [{
-      "key": "PATH",
-      "value": "/bin:/usr/bin:/usr/local/bin"
-    }],
-    "inputDepSetIds": [4],
-    "outputIds": [7],
-    "primaryOutputId": 7
-  }],
-  "targets": [
-    { "id": 1, "label": "@sourceroot//bionic/libc:syscalls-arm", "ruleClassId": 1 },
-    { "id": 2, "label": "@sourceroot//bionic/libc:syscalls-x86", "ruleClassId": 1 },
-    { "id": 3, "label": "@sourceroot//bionic/libc:syscalls-x86_64", "ruleClassId": 1 },
-    { "id": 4, "label": "@sourceroot//bionic/libc:syscalls-arm64", "ruleClassId": 1 }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 2, 3] },
-    { "id": 2, "directArtifactIds": [1, 2, 3] },
-    { "id": 3, "directArtifactIds": [1, 2, 3] },
-    { "id": 4, "directArtifactIds": [1, 2, 3] }],
-  "configuration": [{
-    "id": 1,
-    "mnemonic": "k8-fastbuild",
-    "platformName": "k8",
-    "checksum": "485c362832c178e367d972177f68e69e0981e51e67ef1c160944473db53fe046"
-  }],
-  "ruleClasses": [{ "id": 1, "name": "genrule"}],
-  "pathFragments": [
-    { "id": 5, "label": ".." },
-    { "id": 4, "label": "sourceroot", "parentId": 5 },
-    { "id": 3, "label": "bionic", "parentId": 4 },
-    { "id": 2, "label": "libc", "parentId": 3 },
-    { "id": 1, "label": "SYSCALLS.TXT", "parentId": 2 },
-    { "id": 7, "label": "tools", "parentId": 2 },
-    { "id": 6, "label": "gensyscalls.py", "parentId": 7 },
-    { "id": 11, "label": "bazel_tools", "parentId": 5 },
-    { "id": 10, "label": "tools", "parentId": 11 },
-    { "id": 9, "label": "genrule", "parentId": 10 },
-    { "id": 8, "label": "genrule-setup.sh", "parentId": 9 },
-    { "id": 18, "label": "bazel-out" },
-    { "id": 17, "label": "sourceroot", "parentId": 18 },
-    { "id": 16, "label": "k8-fastbuild", "parentId": 17 },
-    { "id": 15, "label": "bin", "parentId": 16 },
-    { "id": 14, "label": "bionic", "parentId": 15 },
-    { "id": 13, "label": "libc", "parentId": 14 },
-    { "id": 12, "label": "syscalls-arm.S", "parentId": 13 },
-    { "id": 19, "label": "syscalls-x86.S", "parentId": 13 },
-    { "id": 20, "label": "syscalls-x86_64.S", "parentId": 13 },
-    { "id": 21, "label": "syscalls-arm64.S", "parentId": 13 }]
-}`
-	actualbuildStatements, actualDepsets, _ := AqueryBuildStatements([]byte(inputString))
+ "Artifacts": [
+   { "Id": 1, "path_fragment_id": 1 },
+   { "Id": 2, "path_fragment_id": 6 },
+   { "Id": 3, "path_fragment_id": 8 },
+   { "Id": 4, "path_fragment_id": 12 },
+   { "Id": 5, "path_fragment_id": 19 },
+   { "Id": 6, "path_fragment_id": 20 },
+   { "Id": 7, "path_fragment_id": 21 }],
+ "Actions": [{
+   "target_id": 1,
+   "action_key": "ab53f6ecbdc2ee8cb8812613b63205464f1f5083f6dca87081a0a398c0f1ecf7",
+   "Mnemonic": "Genrule",
+   "configuration_id": 1,
+   "Arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py arm ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-arm.S"],
+   "environment_variables": [{
+     "Key": "PATH",
+     "Value": "/bin:/usr/bin:/usr/local/bin"
+   }],
+   "input_dep_set_ids": [1],
+   "output_ids": [4],
+   "primary_output_id": 4
+ }, {
+   "target_id": 2,
+   "action_key": "9f4309ce165dac458498cb92811c18b0b7919782cc37b82a42d2141b8cc90826",
+   "Mnemonic": "Genrule",
+   "configuration_id": 1,
+   "Arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py x86 ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-x86.S"],
+   "environment_variables": [{
+     "Key": "PATH",
+     "Value": "/bin:/usr/bin:/usr/local/bin"
+   }],
+   "input_dep_set_ids": [2],
+   "output_ids": [5],
+   "primary_output_id": 5
+ }, {
+   "target_id": 3,
+   "action_key": "50d6c586103ebeed3a218195540bcc30d329464eae36377eb82f8ce7c36ac342",
+   "Mnemonic": "Genrule",
+   "configuration_id": 1,
+   "Arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py x86_64 ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-x86_64.S"],
+   "environment_variables": [{
+     "Key": "PATH",
+     "Value": "/bin:/usr/bin:/usr/local/bin"
+   }],
+   "input_dep_set_ids": [3],
+   "output_ids": [6],
+   "primary_output_id": 6
+ }, {
+   "target_id": 4,
+   "action_key": "f30cbe442f5216f4223cf16a39112cad4ec56f31f49290d85cff587e48647ffa",
+   "Mnemonic": "Genrule",
+   "configuration_id": 1,
+   "Arguments": ["/bin/bash", "-c", "source ../bazel_tools/tools/genrule/genrule-setup.sh; ../sourceroot/bionic/libc/tools/gensyscalls.py arm64 ../sourceroot/bionic/libc/SYSCALLS.TXT \u003e bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-arm64.S"],
+   "environment_variables": [{
+     "Key": "PATH",
+     "Value": "/bin:/usr/bin:/usr/local/bin"
+   }],
+   "input_dep_set_ids": [4],
+   "output_ids": [7],
+   "primary_output_id": 7
+ }],
+ "Targets": [
+   { "Id": 1, "Label": "@sourceroot//bionic/libc:syscalls-arm", "rule_class_id": 1 },
+   { "Id": 2, "Label": "@sourceroot//bionic/libc:syscalls-x86", "rule_class_id": 1 },
+   { "Id": 3, "Label": "@sourceroot//bionic/libc:syscalls-x86_64", "rule_class_id": 1 },
+   { "Id": 4, "Label": "@sourceroot//bionic/libc:syscalls-arm64", "rule_class_id": 1 }],
+ "dep_set_of_files": [
+   { "Id": 1, "direct_artifact_ids": [1, 2, 3] },
+   { "Id": 2, "direct_artifact_ids": [1, 2, 3] },
+   { "Id": 3, "direct_artifact_ids": [1, 2, 3] },
+   { "Id": 4, "direct_artifact_ids": [1, 2, 3] }],
+ "Configuration": [{
+   "Id": 1,
+   "Mnemonic": "k8-fastbuild",
+   "platform_name": "k8",
+   "Checksum": "485c362832c178e367d972177f68e69e0981e51e67ef1c160944473db53fe046"
+ }],
+ "rule_classes": [{ "Id": 1, "Name": "genrule"}],
+ "path_fragments": [
+   { "Id": 5, "Label": ".." },
+   { "Id": 4, "Label": "sourceroot", "parent_id": 5 },
+   { "Id": 3, "Label": "bionic", "parent_id": 4 },
+   { "Id": 2, "Label": "libc", "parent_id": 3 },
+   { "Id": 1, "Label": "SYSCALLS.TXT", "parent_id": 2 },
+   { "Id": 7, "Label": "tools", "parent_id": 2 },
+   { "Id": 6, "Label": "gensyscalls.py", "parent_id": 7 },
+   { "Id": 11, "Label": "bazel_tools", "parent_id": 5 },
+   { "Id": 10, "Label": "tools", "parent_id": 11 },
+   { "Id": 9, "Label": "genrule", "parent_id": 10 },
+   { "Id": 8, "Label": "genrule-setup.sh", "parent_id": 9 },
+   { "Id": 18, "Label": "bazel-out" },
+   { "Id": 17, "Label": "sourceroot", "parent_id": 18 },
+   { "Id": 16, "Label": "k8-fastbuild", "parent_id": 17 },
+   { "Id": 15, "Label": "bin", "parent_id": 16 },
+   { "Id": 14, "Label": "bionic", "parent_id": 15 },
+   { "Id": 13, "Label": "libc", "parent_id": 14 },
+   { "Id": 12, "Label": "syscalls-arm.S", "parent_id": 13 },
+   { "Id": 19, "Label": "syscalls-x86.S", "parent_id": 13 },
+   { "Id": 20, "Label": "syscalls-x86_64.S", "parent_id": 13 },
+   { "Id": 21, "Label": "syscalls-arm64.S", "parent_id": 13 }]
+}
+`
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actualbuildStatements, actualDepsets, _ := AqueryBuildStatements(data)
 	var expectedBuildStatements []BuildStatement
 	for _, arch := range []string{"arm", "arm64", "x86", "x86_64"} {
 		expectedBuildStatements = append(expectedBuildStatements,
@@ -161,130 +171,155 @@
 func TestInvalidOutputId(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [3],
-    "primaryOutputId": 3
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 2] }],
-  "pathFragments": [
-    { "id": 1, "label": "one" },
-    { "id": 2, "label": "two" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "x",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [1],
+   "output_ids": [3],
+   "primary_output_id": 3
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1, 2] }],
+ "path_fragments": [
+   { "id": 1, "label": "one" },
+   { "id": 2, "label": "two" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, "undefined outputId 3")
 }
 
 func TestInvalidInputDepsetIdFromAction(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [2],
-    "outputIds": [1],
-    "primaryOutputId": 1
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 2] }],
-  "pathFragments": [
-    { "id": 1, "label": "one" },
-    { "id": 2, "label": "two" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "x",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [2],
+   "output_ids": [1],
+   "primary_output_id": 1
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1, 2] }],
+ "path_fragments": [
+   { "id": 1, "label": "one" },
+   { "id": 2, "label": "two" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, "undefined input depsetId 2")
 }
 
 func TestInvalidInputDepsetIdFromDepset(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [1],
-    "primaryOutputId": 1
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 2], "transitiveDepSetIds": [42] }],
-  "pathFragments": [
-    { "id": 1, "label": "one"},
-    { "id": 2, "label": "two" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "x",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [1],
+   "output_ids": [1],
+   "primary_output_id": 1
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1, 2], "transitive_dep_set_ids": [42] }],
+ "path_fragments": [
+   { "id": 1, "label": "one"},
+   { "id": 2, "label": "two" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, "undefined input depsetId 42 (referenced by depsetId 1)")
 }
 
 func TestInvalidInputArtifactId(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [1],
-    "primaryOutputId": 1
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 3] }],
-  "pathFragments": [
-    { "id": 1, "label": "one" },
-    { "id": 2, "label": "two" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "x",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [1],
+   "output_ids": [1],
+   "primary_output_id": 1
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1, 3] }],
+ "path_fragments": [
+   { "id": 1, "label": "one" },
+   { "id": 2, "label": "two" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, "undefined input artifactId 3")
 }
 
 func TestInvalidPathFragmentId(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [1],
-    "primaryOutputId": 1
-  }],
-  "depSetOfFiles": [
-     { "id": 1, "directArtifactIds": [1, 2] }],
-  "pathFragments": [
-    {  "id": 1, "label": "one" },
-    {  "id": 2, "label": "two", "parentId": 3 }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "x",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [1],
+   "output_ids": [1],
+   "primary_output_id": 1
+ }],
+ "dep_set_of_files": [
+    { "id": 1, "direct_artifact_ids": [1, 2] }],
+ "path_fragments": [
+   {  "id": 1, "label": "one" },
+   {  "id": 2, "label": "two", "parent_id": 3 }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, "undefined path fragment id 3")
 }
 
@@ -292,27 +327,32 @@
 	const inputString = `
 {
   "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 },
-    { "id": 3, "pathFragmentId": 3 }],
+    { "id": 1, "path_fragment_id": 1 },
+    { "id": 2, "path_fragment_id": 2 },
+    { "id": 3, "path_fragment_id": 3 }],
   "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
+    "target_Id": 1,
+    "action_Key": "x",
     "mnemonic": "x",
     "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [2, 3],
-    "primaryOutputId": 2
+    "input_dep_set_ids": [1],
+    "output_ids": [2, 3],
+    "primary_output_id": 2
   }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 2, 3] }],
-  "pathFragments": [
+  "dep_set_of_files": [
+    { "id": 1, "direct_Artifact_Ids": [1, 2, 3] }],
+  "path_fragments": [
     { "id": 1, "label": "one" },
     { "id": 2, "label": "two" },
     { "id": 3, "label": "two.d" }]
 }`
 
-	actual, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actual, _, err := AqueryBuildStatements(data)
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
 	}
@@ -332,32 +372,37 @@
 func TestMultipleDepfiles(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 },
-    { "id": 3, "pathFragmentId": 3 },
-    { "id": 4, "pathFragmentId": 4 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "x",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [2,3,4],
-    "primaryOutputId": 2
-  }],
-  "depSetOfFiles": [{
-    "id": 1,
-    "directArtifactIds": [1, 2, 3, 4]
-  }],
-  "pathFragments": [
-    { "id": 1, "label": "one" },
-    { "id": 2, "label": "two" },
-    { "id": 3, "label": "two.d" },
-    { "id": 4, "label": "other.d" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 },
+   { "id": 3, "path_fragment_id": 3 },
+   { "id": 4, "path_fragment_id": 4 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "x",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [1],
+   "output_ids": [2,3,4],
+   "primary_output_id": 2
+ }],
+ "dep_set_of_files": [{
+   "id": 1,
+   "direct_artifact_ids": [1, 2, 3, 4]
+ }],
+ "path_fragments": [
+   { "id": 1, "label": "one" },
+   { "id": 2, "label": "two" },
+   { "id": 3, "label": "two.d" },
+   { "id": 4, "label": "other.d" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, `found multiple potential depfiles "two.d", "other.d"`)
 }
 
@@ -366,74 +411,79 @@
 	// a single action with many inputs given via a deep depset.
 	const inputString = `
 {
-  "artifacts": [
-   { "id": 1, "pathFragmentId": 1 },
-   { "id": 2, "pathFragmentId": 7 },
-   { "id": 3, "pathFragmentId": 8 },
-   { "id": 4, "pathFragmentId": 9 },
-   { "id": 5, "pathFragmentId": 10 },
-   { "id": 6, "pathFragmentId": 11 },
-   { "id": 7, "pathFragmentId": 12 },
-   { "id": 8, "pathFragmentId": 13 },
-   { "id": 9, "pathFragmentId": 14 },
-   { "id": 10, "pathFragmentId": 15 },
-   { "id": 11, "pathFragmentId": 16 },
-   { "id": 12, "pathFragmentId": 17 },
-   { "id": 13, "pathFragmentId": 18 },
-   { "id": 14, "pathFragmentId": 19 },
-   { "id": 15, "pathFragmentId": 20 },
-   { "id": 16, "pathFragmentId": 21 },
-   { "id": 17, "pathFragmentId": 22 },
-   { "id": 18, "pathFragmentId": 23 },
-   { "id": 19, "pathFragmentId": 24 },
-   { "id": 20, "pathFragmentId": 25 },
-   { "id": 21, "pathFragmentId": 26 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "3b826d17fadbbbcd8313e456b90ec47c078c438088891dd45b4adbcd8889dc50",
-    "mnemonic": "Action",
-    "configurationId": 1,
-    "arguments": ["/bin/bash", "-c", "touch bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_out"],
-    "inputDepSetIds": [1],
-    "outputIds": [21],
-    "primaryOutputId": 21
-  }],
-  "depSetOfFiles": [
-    { "id": 3, "directArtifactIds": [1, 2, 3, 4, 5] },
-    { "id": 4, "directArtifactIds": [6, 7, 8, 9, 10] },
-    { "id": 2, "transitiveDepSetIds": [3, 4], "directArtifactIds": [11, 12, 13, 14, 15] },
-    { "id": 5, "directArtifactIds": [16, 17, 18, 19] },
-    { "id": 1, "transitiveDepSetIds": [2, 5], "directArtifactIds": [20] }],
-  "pathFragments": [
-    { "id": 6, "label": "bazel-out" },
-    { "id": 5, "label": "sourceroot", "parentId": 6 },
-    { "id": 4, "label": "k8-fastbuild", "parentId": 5 },
-    { "id": 3, "label": "bin", "parentId": 4 },
-    { "id": 2, "label": "testpkg", "parentId": 3 },
-    { "id": 1, "label": "test_1", "parentId": 2 },
-    { "id": 7, "label": "test_2", "parentId": 2 },
-    { "id": 8, "label": "test_3", "parentId": 2 },
-    { "id": 9, "label": "test_4", "parentId": 2 },
-    { "id": 10, "label": "test_5", "parentId": 2 },
-    { "id": 11, "label": "test_6", "parentId": 2 },
-    { "id": 12, "label": "test_7", "parentId": 2 },
-    { "id": 13, "label": "test_8", "parentId": 2 },
-    { "id": 14, "label": "test_9", "parentId": 2 },
-    { "id": 15, "label": "test_10", "parentId": 2 },
-    { "id": 16, "label": "test_11", "parentId": 2 },
-    { "id": 17, "label": "test_12", "parentId": 2 },
-    { "id": 18, "label": "test_13", "parentId": 2 },
-    { "id": 19, "label": "test_14", "parentId": 2 },
-    { "id": 20, "label": "test_15", "parentId": 2 },
-    { "id": 21, "label": "test_16", "parentId": 2 },
-    { "id": 22, "label": "test_17", "parentId": 2 },
-    { "id": 23, "label": "test_18", "parentId": 2 },
-    { "id": 24, "label": "test_19", "parentId": 2 },
-    { "id": 25, "label": "test_root", "parentId": 2 },
-    { "id": 26,"label": "test_out", "parentId": 2 }]
+ "artifacts": [
+  { "id": 1, "path_fragment_id": 1 },
+  { "id": 2, "path_fragment_id": 7 },
+  { "id": 3, "path_fragment_id": 8 },
+  { "id": 4, "path_fragment_id": 9 },
+  { "id": 5, "path_fragment_id": 10 },
+  { "id": 6, "path_fragment_id": 11 },
+  { "id": 7, "path_fragment_id": 12 },
+  { "id": 8, "path_fragment_id": 13 },
+  { "id": 9, "path_fragment_id": 14 },
+  { "id": 10, "path_fragment_id": 15 },
+  { "id": 11, "path_fragment_id": 16 },
+  { "id": 12, "path_fragment_id": 17 },
+  { "id": 13, "path_fragment_id": 18 },
+  { "id": 14, "path_fragment_id": 19 },
+  { "id": 15, "path_fragment_id": 20 },
+  { "id": 16, "path_fragment_id": 21 },
+  { "id": 17, "path_fragment_id": 22 },
+  { "id": 18, "path_fragment_id": 23 },
+  { "id": 19, "path_fragment_id": 24 },
+  { "id": 20, "path_fragment_id": 25 },
+  { "id": 21, "path_fragment_id": 26 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "3b826d17fadbbbcd8313e456b90ec47c078c438088891dd45b4adbcd8889dc50",
+   "mnemonic": "Action",
+   "configuration_id": 1,
+   "arguments": ["/bin/bash", "-c", "touch bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_out"],
+   "input_dep_set_ids": [1],
+   "output_ids": [21],
+   "primary_output_id": 21
+ }],
+ "dep_set_of_files": [
+   { "id": 3, "direct_artifact_ids": [1, 2, 3, 4, 5] },
+   { "id": 4, "direct_artifact_ids": [6, 7, 8, 9, 10] },
+   { "id": 2, "transitive_dep_set_ids": [3, 4], "direct_artifact_ids": [11, 12, 13, 14, 15] },
+   { "id": 5, "direct_artifact_ids": [16, 17, 18, 19] },
+   { "id": 1, "transitive_dep_set_ids": [2, 5], "direct_artifact_ids": [20] }],
+ "path_fragments": [
+   { "id": 6, "label": "bazel-out" },
+   { "id": 5, "label": "sourceroot", "parent_id": 6 },
+   { "id": 4, "label": "k8-fastbuild", "parent_id": 5 },
+   { "id": 3, "label": "bin", "parent_id": 4 },
+   { "id": 2, "label": "testpkg", "parent_id": 3 },
+   { "id": 1, "label": "test_1", "parent_id": 2 },
+   { "id": 7, "label": "test_2", "parent_id": 2 },
+   { "id": 8, "label": "test_3", "parent_id": 2 },
+   { "id": 9, "label": "test_4", "parent_id": 2 },
+   { "id": 10, "label": "test_5", "parent_id": 2 },
+   { "id": 11, "label": "test_6", "parent_id": 2 },
+   { "id": 12, "label": "test_7", "parent_id": 2 },
+	 { "id": 13, "label": "test_8", "parent_id": 2 },
+   { "id": 14, "label": "test_9", "parent_id": 2 },
+   { "id": 15, "label": "test_10", "parent_id": 2 },
+   { "id": 16, "label": "test_11", "parent_id": 2 },
+   { "id": 17, "label": "test_12", "parent_id": 2 },
+   { "id": 18, "label": "test_13", "parent_id": 2 },
+   { "id": 19, "label": "test_14", "parent_id": 2 },
+   { "id": 20, "label": "test_15", "parent_id": 2 },
+   { "id": 21, "label": "test_16", "parent_id": 2 },
+   { "id": 22, "label": "test_17", "parent_id": 2 },
+   { "id": 23, "label": "test_18", "parent_id": 2 },
+   { "id": 24, "label": "test_19", "parent_id": 2 },
+   { "id": 25, "label": "test_root", "parent_id": 2 },
+   { "id": 26,"label": "test_out", "parent_id": 2 }]
 }`
 
-	actualbuildStatements, actualDepsets, _ := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actualbuildStatements, actualDepsets, _ := AqueryBuildStatements(data)
 
 	expectedBuildStatements := []BuildStatement{
 		{
@@ -463,27 +513,32 @@
 func TestSymlinkTree(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "SymlinkTree",
-    "configurationId": 1,
-    "inputDepSetIds": [1],
-    "outputIds": [2],
-    "primaryOutputId": 2,
-    "executionPlatform": "//build/bazel/platforms:linux_x86_64"
-  }],
-  "pathFragments": [
-    { "id": 1, "label": "foo.manifest" },
-    { "id": 2, "label": "foo.runfiles/MANIFEST" }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1] }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "SymlinkTree",
+   "configuration_id": 1,
+   "input_dep_set_ids": [1],
+   "output_ids": [2],
+   "primary_output_id": 2,
+   "execution_platform": "//build/bazel/platforms:linux_x86_64"
+ }],
+ "path_fragments": [
+   { "id": 1, "label": "foo.manifest" },
+   { "id": 2, "label": "foo.runfiles/MANIFEST" }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1] }]
 }
 `
-	actual, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actual, _, err := AqueryBuildStatements(data)
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
 	}
@@ -499,37 +554,42 @@
 
 func TestBazelOutRemovalFromInputDepsets(t *testing.T) {
 	const inputString = `{
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 10 },
-    { "id": 2, "pathFragmentId": 20 },
-    { "id": 3, "pathFragmentId": 30 },
-    { "id": 4, "pathFragmentId": 40 }],
-  "depSetOfFiles": [{
-    "id": 1111,
-    "directArtifactIds": [3 , 4]
-  }, {
-    "id": 2222,
-    "directArtifactIds": [3]
-  }],
-  "actions": [{
-    "targetId": 100,
-    "actionKey": "x",
-    "inputDepSetIds": [1111, 2222],
-    "mnemonic": "x",
-    "arguments": ["bogus", "command"],
-    "outputIds": [2],
-    "primaryOutputId": 1
-  }],
-  "pathFragments": [
-    { "id": 10, "label": "input" },
-    { "id": 20, "label": "output" },
-    { "id": 30, "label": "dep1", "parentId": 50 },
-    { "id": 40, "label": "dep2", "parentId": 60 },
-    { "id": 50, "label": "bazel_tools", "parentId": 60 },
-    { "id": 60, "label": ".."}
-  ]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 10 },
+   { "id": 2, "path_fragment_id": 20 },
+   { "id": 3, "path_fragment_id": 30 },
+   { "id": 4, "path_fragment_id": 40 }],
+ "dep_set_of_files": [{
+   "id": 1111,
+   "direct_artifact_ids": [3 , 4]
+ }, {
+   "id": 2222,
+   "direct_artifact_ids": [3]
+ }],
+ "actions": [{
+   "target_id": 100,
+   "action_key": "x",
+   "input_dep_set_ids": [1111, 2222],
+   "mnemonic": "x",
+   "arguments": ["bogus", "command"],
+   "output_ids": [2],
+   "primary_output_id": 1
+ }],
+ "path_fragments": [
+   { "id": 10, "label": "input" },
+   { "id": 20, "label": "output" },
+   { "id": 30, "label": "dep1", "parent_id": 50 },
+   { "id": 40, "label": "dep2", "parent_id": 60 },
+   { "id": 50, "label": "bazel_tools", "parent_id": 60 },
+   { "id": 60, "label": ".."}
+ ]
 }`
-	actualBuildStatements, actualDepsets, _ := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actualBuildStatements, actualDepsets, _ := AqueryBuildStatements(data)
 	if len(actualDepsets) != 2 {
 		t.Errorf("expected 1 depset but found %#v", actualDepsets)
 		return
@@ -567,43 +627,47 @@
 func TestMiddlemenAction(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 },
-    { "id": 3, "pathFragmentId": 3 },
-    { "id": 4, "pathFragmentId": 4 },
-    { "id": 5, "pathFragmentId": 5 },
-    { "id": 6, "pathFragmentId": 6 }],
-  "pathFragments": [
-    { "id": 1, "label": "middleinput_one" },
-    { "id": 2, "label": "middleinput_two" },
-    { "id": 3, "label": "middleman_artifact" },
-    { "id": 4, "label": "maininput_one" },
-    { "id": 5, "label": "maininput_two" },
-    { "id": 6, "label": "output" }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1, 2] },
-    { "id": 2, "directArtifactIds": [3, 4, 5] }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "Middleman",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [1],
-    "outputIds": [3],
-    "primaryOutputId": 3
-  }, {
-    "targetId": 2,
-    "actionKey": "y",
-    "mnemonic": "Main action",
-    "arguments": ["touch", "foo"],
-    "inputDepSetIds": [2],
-    "outputIds": [6],
-    "primaryOutputId": 6
-  }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 },
+   { "id": 3, "path_fragment_id": 3 },
+   { "id": 4, "path_fragment_id": 4 },
+   { "id": 5, "path_fragment_id": 5 },
+   { "id": 6, "path_fragment_id": 6 }],
+ "path_fragments": [
+   { "id": 1, "label": "middleinput_one" },
+   { "id": 2, "label": "middleinput_two" },
+   { "id": 3, "label": "middleman_artifact" },
+   { "id": 4, "label": "maininput_one" },
+   { "id": 5, "label": "maininput_two" },
+   { "id": 6, "label": "output" }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1, 2] },
+   { "id": 2, "direct_artifact_ids": [3, 4, 5] }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "Middleman",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [1],
+   "output_ids": [3],
+   "primary_output_id": 3
+ }, {
+   "target_id": 2,
+   "action_key": "y",
+   "mnemonic": "Main action",
+   "arguments": ["touch", "foo"],
+   "input_dep_set_ids": [2],
+   "output_ids": [6],
+   "primary_output_id": 6
+ }]
 }`
-
-	actualBuildStatements, actualDepsets, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actualBuildStatements, actualDepsets, err := AqueryBuildStatements(data)
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
 	}
@@ -675,28 +739,32 @@
 func TestSimpleSymlink(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 3 },
-    { "id": 2, "pathFragmentId": 5 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "Symlink",
-    "inputDepSetIds": [1],
-    "outputIds": [2],
-    "primaryOutputId": 2
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1] }],
-  "pathFragments": [
-    { "id": 1, "label": "one" },
-    { "id": 2, "label": "file_subdir", "parentId": 1 },
-    { "id": 3, "label": "file", "parentId": 2 },
-    { "id": 4, "label": "symlink_subdir", "parentId": 1 },
-    { "id": 5, "label": "symlink", "parentId": 4 }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 3 },
+   { "id": 2, "path_fragment_id": 5 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "Symlink",
+   "input_dep_set_ids": [1],
+   "output_ids": [2],
+   "primary_output_id": 2
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1] }],
+ "path_fragments": [
+   { "id": 1, "label": "one" },
+   { "id": 2, "label": "file_subdir", "parent_id": 1 },
+   { "id": 3, "label": "file", "parent_id": 2 },
+   { "id": 4, "label": "symlink_subdir", "parent_id": 1 },
+   { "id": 5, "label": "symlink", "parent_id": 4 }]
 }`
-
-	actual, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actual, _, err := AqueryBuildStatements(data)
 
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
@@ -719,29 +787,33 @@
 func TestSymlinkQuotesPaths(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 3 },
-    { "id": 2, "pathFragmentId": 5 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "SolibSymlink",
-    "inputDepSetIds": [1],
-    "outputIds": [2],
-    "primaryOutputId": 2
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1] }],
-  "pathFragments": [
-    { "id": 1, "label": "one" },
-    { "id": 2, "label": "file subdir", "parentId": 1 },
-    { "id": 3, "label": "file", "parentId": 2 },
-    { "id": 4, "label": "symlink subdir", "parentId": 1 },
-    { "id": 5, "label": "symlink", "parentId": 4 }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 3 },
+   { "id": 2, "path_fragment_id": 5 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "SolibSymlink",
+   "input_dep_set_ids": [1],
+   "output_ids": [2],
+   "primary_output_id": 2
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1] }],
+ "path_fragments": [
+   { "id": 1, "label": "one" },
+   { "id": 2, "label": "file subdir", "parent_id": 1 },
+   { "id": 3, "label": "file", "parent_id": 2 },
+   { "id": 4, "label": "symlink subdir", "parent_id": 1 },
+   { "id": 5, "label": "symlink", "parent_id": 4 }]
 }`
 
-	actual, _, err := AqueryBuildStatements([]byte(inputString))
-
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actual, _, err := AqueryBuildStatements(data)
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
 	}
@@ -763,82 +835,95 @@
 func TestSymlinkMultipleInputs(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 },
-    { "id": 3, "pathFragmentId": 3 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "Symlink",
-    "inputDepSetIds": [1],
-    "outputIds": [3],
-    "primaryOutputId": 3
-  }],
-  "depSetOfFiles": [{ "id": 1, "directArtifactIds": [1,2] }],
-  "pathFragments": [
-    { "id": 1, "label": "file" },
-    { "id": 2, "label": "other_file" },
-    { "id": 3, "label": "symlink" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 2, "path_fragment_id": 2 },
+   { "id": 3, "path_fragment_id": 3 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "Symlink",
+   "input_dep_set_ids": [1],
+   "output_ids": [3],
+   "primary_output_id": 3
+ }],
+ "dep_set_of_files": [{ "id": 1, "direct_artifact_ids": [1,2] }],
+ "path_fragments": [
+   { "id": 1, "label": "file" },
+   { "id": 2, "label": "other_file" },
+   { "id": 3, "label": "symlink" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, `Expect 1 input and 1 output to symlink action, got: input ["file" "other_file"], output ["symlink"]`)
 }
 
 func TestSymlinkMultipleOutputs(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 },
-    { "id": 2, "pathFragmentId": 2 },
-    { "id": 3, "pathFragmentId": 3 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "Symlink",
-    "inputDepSetIds": [1],
-    "outputIds": [2,3],
-    "primaryOutputId": 2
-  }],
-  "depSetOfFiles": [
-    { "id": 1, "directArtifactIds": [1] }],
-  "pathFragments": [
-    { "id": 1, "label": "file" },
-    { "id": 2, "label": "symlink" },
-    { "id": 3,  "label": "other_symlink" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 },
+   { "id": 3, "path_fragment_id": 3 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "Symlink",
+   "input_dep_set_ids": [1],
+   "output_ids": [2,3],
+   "primary_output_id": 2
+ }],
+ "dep_set_of_files": [
+   { "id": 1, "direct_artifact_ids": [1] }],
+ "path_fragments": [
+   { "id": 1, "label": "file" },
+   { "id": 2, "label": "symlink" },
+   { "id": 3,  "label": "other_symlink" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
-	assertError(t, err, `Expect 1 input and 1 output to symlink action, got: input ["file"], output ["symlink" "other_symlink"]`)
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
+	assertError(t, err, "undefined outputId 2")
 }
 
 func TestTemplateExpandActionSubstitutions(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [{
-    "id": 1,
-    "pathFragmentId": 1
-  }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "TemplateExpand",
-    "configurationId": 1,
-    "outputIds": [1],
-    "primaryOutputId": 1,
-    "executionPlatform": "//build/bazel/platforms:linux_x86_64",
-    "templateContent": "Test template substitutions: %token1%, %python_binary%",
-    "substitutions": [
-      { "key": "%token1%", "value": "abcd" },
-      { "key": "%python_binary%", "value": "python3" }]
-  }],
-  "pathFragments": [
-    { "id": 1, "label": "template_file" }]
+ "artifacts": [{
+   "id": 1,
+   "path_fragment_id": 1
+ }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "TemplateExpand",
+   "configuration_id": 1,
+   "output_ids": [1],
+   "primary_output_id": 1,
+   "execution_platform": "//build/bazel/platforms:linux_x86_64",
+   "template_content": "Test template substitutions: %token1%, %python_binary%",
+   "substitutions": [
+     { "key": "%token1%", "value": "abcd" },
+     { "key": "%python_binary%", "value": "python3" }]
+ }],
+ "path_fragments": [
+   { "id": 1, "label": "template_file" }]
 }`
 
-	actual, _, err := AqueryBuildStatements([]byte(inputString))
-
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actual, _, err := AqueryBuildStatements(data)
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
 	}
@@ -857,48 +942,58 @@
 func TestTemplateExpandActionNoOutput(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "TemplateExpand",
-    "configurationId": 1,
-    "primaryOutputId": 1,
-    "executionPlatform": "//build/bazel/platforms:linux_x86_64",
-    "templateContent": "Test template substitutions: %token1%, %python_binary%",
-    "substitutions": [
-      { "key": "%token1%", "value": "abcd" },
-      { "key": "%python_binary%", "value": "python3" }]
-  }],
-  "pathFragments": [
-    { "id": 1, "label": "template_file" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "TemplateExpand",
+   "configuration_id": 1,
+   "primary_output_id": 1,
+   "execution_platform": "//build/bazel/platforms:linux_x86_64",
+   "templateContent": "Test template substitutions: %token1%, %python_binary%",
+   "substitutions": [
+     { "key": "%token1%", "value": "abcd" },
+     { "key": "%python_binary%", "value": "python3" }]
+ }],
+ "path_fragments": [
+   { "id": 1, "label": "template_file" }]
 }`
 
-	_, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	_, _, err = AqueryBuildStatements(data)
 	assertError(t, err, `Expect 1 output to template expand action, got: output []`)
 }
 
 func TestFileWrite(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "FileWrite",
-    "configurationId": 1,
-    "outputIds": [1],
-    "primaryOutputId": 1,
-    "executionPlatform": "//build/bazel/platforms:linux_x86_64",
-    "fileContents": "file data\n"
-  }],
-  "pathFragments": [
-    { "id": 1, "label": "foo.manifest" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "FileWrite",
+   "configuration_id": 1,
+   "output_ids": [1],
+   "primary_output_id": 1,
+   "execution_platform": "//build/bazel/platforms:linux_x86_64",
+   "file_contents": "file data\n"
+ }],
+ "path_fragments": [
+   { "id": 1, "label": "foo.manifest" }]
 }
 `
-	actual, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actual, _, err := AqueryBuildStatements(data)
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
 	}
@@ -914,23 +1009,28 @@
 func TestSourceSymlinkManifest(t *testing.T) {
 	const inputString = `
 {
-  "artifacts": [
-    { "id": 1, "pathFragmentId": 1 }],
-  "actions": [{
-    "targetId": 1,
-    "actionKey": "x",
-    "mnemonic": "SourceSymlinkManifest",
-    "configurationId": 1,
-    "outputIds": [1],
-    "primaryOutputId": 1,
-    "executionPlatform": "//build/bazel/platforms:linux_x86_64",
-    "fileContents": "symlink target\n"
-  }],
-  "pathFragments": [
-    { "id": 1, "label": "foo.manifest" }]
+ "artifacts": [
+   { "id": 1, "path_fragment_id": 1 }],
+ "actions": [{
+   "target_id": 1,
+   "action_key": "x",
+   "mnemonic": "SourceSymlinkManifest",
+   "configuration_id": 1,
+   "output_ids": [1],
+   "primary_output_id": 1,
+   "execution_platform": "//build/bazel/platforms:linux_x86_64",
+   "file_contents": "symlink target\n"
+ }],
+ "path_fragments": [
+   { "id": 1, "label": "foo.manifest" }]
 }
 `
-	actual, _, err := AqueryBuildStatements([]byte(inputString))
+	data, err := JsonToActionGraphContainer(inputString)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	actual, _, err := AqueryBuildStatements(data)
 	if err != nil {
 		t.Errorf("Unexpected error %q", err)
 	}
@@ -1011,3 +1111,14 @@
 	sort.Strings(sorted)
 	return sorted
 }
+
+// Transform the json format to ActionGraphContainer
+func JsonToActionGraphContainer(inputString string) ([]byte, error) {
+	var aqueryProtoResult analysis_v2_proto.ActionGraphContainer
+	err := json.Unmarshal([]byte(inputString), &aqueryProtoResult)
+	if err != nil {
+		return []byte(""), err
+	}
+	data, _ := proto.Marshal(&aqueryProtoResult)
+	return data, err
+}
diff --git a/bp2build/apex_conversion_test.go b/bp2build/apex_conversion_test.go
index a666e49..b6061e4 100644
--- a/bp2build/apex_conversion_test.go
+++ b/bp2build/apex_conversion_test.go
@@ -42,6 +42,7 @@
 	ctx.RegisterModuleType("android_app_certificate", java.AndroidAppCertificateFactory)
 	ctx.RegisterModuleType("filegroup", android.FileGroupFactory)
 	ctx.RegisterModuleType("prebuilt_etc", etc.PrebuiltEtcFactory)
+	ctx.RegisterModuleType("cc_test", cc.TestFactory)
 }
 
 func runOverrideApexTestCase(t *testing.T, tc Bp2buildTestCase) {
@@ -1249,3 +1250,28 @@
 			}),
 		}})
 }
+
+func TestApexTestBundleSimple(t *testing.T) {
+	runApexTestCase(t, Bp2buildTestCase{
+		Description:                "apex_test - simple",
+		ModuleTypeUnderTest:        "apex_test",
+		ModuleTypeUnderTestFactory: apex.TestApexBundleFactory,
+		Filesystem:                 map[string]string{},
+		Blueprint: `
+cc_test { name: "cc_test_1", bazel_module: { bp2build_available: false } }
+
+apex_test {
+	name: "test_com.android.apogee",
+	file_contexts: "file_contexts_file",
+	tests: ["cc_test_1"],
+}
+`,
+		ExpectedBazelTargets: []string{
+			MakeBazelTarget("apex", "test_com.android.apogee", AttrNameToString{
+				"file_contexts": `"file_contexts_file"`,
+				"manifest":      `"apex_manifest.json"`,
+				"testonly":      `True`,
+				"tests":         `[":cc_test_1"]`,
+			}),
+		}})
+}
diff --git a/bp2build/java_binary_host_conversion_test.go b/bp2build/java_binary_host_conversion_test.go
index 86f3d42..8000477 100644
--- a/bp2build/java_binary_host_conversion_test.go
+++ b/bp2build/java_binary_host_conversion_test.go
@@ -32,7 +32,7 @@
 	}, tc)
 }
 
-var fs = map[string]string{
+var testFs = map[string]string{
 	"test.mf": "Main-Class: com.android.test.MainClass",
 	"other/Android.bp": `cc_library_host_shared {
     name: "jni-lib-1",
@@ -43,7 +43,7 @@
 func TestJavaBinaryHost(t *testing.T) {
 	runJavaBinaryHostTestCase(t, Bp2buildTestCase{
 		Description: "java_binary_host with srcs, exclude_srcs, jni_libs, javacflags, and manifest.",
-		Filesystem:  fs,
+		Filesystem:  testFs,
 		Blueprint: `java_binary_host {
     name: "java-binary-host-1",
     srcs: ["a.java", "b.java"],
@@ -76,7 +76,7 @@
 func TestJavaBinaryHostRuntimeDeps(t *testing.T) {
 	runJavaBinaryHostTestCase(t, Bp2buildTestCase{
 		Description: "java_binary_host with srcs, exclude_srcs, jni_libs, javacflags, and manifest.",
-		Filesystem:  fs,
+		Filesystem:  testFs,
 		Blueprint: `java_binary_host {
     name: "java-binary-host-1",
     static_libs: ["java-dep-1"],
diff --git a/bp2build/symlink_forest.go b/bp2build/symlink_forest.go
index 092b240..0fa9c50 100644
--- a/bp2build/symlink_forest.go
+++ b/bp2build/symlink_forest.go
@@ -15,13 +15,16 @@
 package bp2build
 
 import (
+	"errors"
 	"fmt"
+	"io/fs"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"regexp"
+	"sync"
+	"sync/atomic"
 
-	"android/soong/android"
 	"android/soong/shared"
 )
 
@@ -31,14 +34,25 @@
 // or a directory. If excluded is true, then that file/directory should be
 // excluded from symlinking. Otherwise, the node is not excluded, but one of its
 // descendants is (otherwise the node in question would not exist)
-type node struct {
+
+type instructionsNode struct {
 	name     string
 	excluded bool // If false, this is just an intermediate node
-	children map[string]*node
+	children map[string]*instructionsNode
+}
+
+type symlinkForestContext struct {
+	verbose bool
+	topdir  string // $TOPDIR
+
+	// State
+	wg    sync.WaitGroup
+	depCh chan string
+	okay  atomic.Bool // Whether the forest was successfully constructed
 }
 
 // Ensures that the node for the given path exists in the tree and returns it.
-func ensureNodeExists(root *node, path string) *node {
+func ensureNodeExists(root *instructionsNode, path string) *instructionsNode {
 	if path == "" {
 		return root
 	}
@@ -56,15 +70,14 @@
 	if child, ok := dn.children[base]; ok {
 		return child
 	} else {
-		dn.children[base] = &node{base, false, make(map[string]*node)}
+		dn.children[base] = &instructionsNode{base, false, make(map[string]*instructionsNode)}
 		return dn.children[base]
 	}
 }
 
-// Turns a list of paths to be excluded into a tree made of "node" objects where
-// the specified paths are marked as excluded.
-func treeFromExcludePathList(paths []string) *node {
-	result := &node{"", false, make(map[string]*node)}
+// Turns a list of paths to be excluded into a tree
+func instructionsFromExcludePathList(paths []string) *instructionsNode {
+	result := &instructionsNode{"", false, make(map[string]*instructionsNode)}
 
 	for _, p := range paths {
 		ensureNodeExists(result, p).excluded = true
@@ -179,17 +192,23 @@
 
 // Recursively plants a symlink forest at forestDir. The symlink tree will
 // contain every file in buildFilesDir and srcDir excluding the files in
-// exclude. Collects every directory encountered during the traversal of srcDir
-// into acc.
-func plantSymlinkForestRecursive(cfg android.Config, topdir string, forestDir string, buildFilesDir string, srcDir string, exclude *node, acc *[]string, okay *bool) {
-	if exclude != nil && exclude.excluded {
+// instructions. Collects every directory encountered during the traversal of
+// srcDir .
+func plantSymlinkForestRecursive(context *symlinkForestContext, instructions *instructionsNode, forestDir string, buildFilesDir string, srcDir string) {
+	defer context.wg.Done()
+
+	if instructions != nil && instructions.excluded {
 		// This directory is not needed, bail out
 		return
 	}
 
-	*acc = append(*acc, srcDir)
-	srcDirMap := readdirToMap(shared.JoinPath(topdir, srcDir))
-	buildFilesMap := readdirToMap(shared.JoinPath(topdir, buildFilesDir))
+	// We don't add buildFilesDir here because the bp2build files marker files is
+	// already a dependency which covers it. If we ever wanted to turn this into
+	// a generic symlink forest creation tool, we'd need to add it, too.
+	context.depCh <- srcDir
+
+	srcDirMap := readdirToMap(shared.JoinPath(context.topdir, srcDir))
+	buildFilesMap := readdirToMap(shared.JoinPath(context.topdir, buildFilesDir))
 
 	renamingBuildFile := false
 	if _, ok := srcDirMap["BUILD"]; ok {
@@ -211,7 +230,7 @@
 		allEntries[n] = struct{}{}
 	}
 
-	err := os.MkdirAll(shared.JoinPath(topdir, forestDir), 0777)
+	err := os.MkdirAll(shared.JoinPath(context.topdir, forestDir), 0777)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "Cannot mkdir '%s': %s\n", forestDir, err)
 		os.Exit(1)
@@ -230,85 +249,146 @@
 		}
 		buildFilesChild := shared.JoinPath(buildFilesDir, f)
 
-		// Descend in the exclusion tree, if there are any excludes left
-		var excludeChild *node = nil
-		if exclude != nil {
+		// Descend in the instruction tree if it exists
+		var instructionsChild *instructionsNode = nil
+		if instructions != nil {
 			if f == "BUILD.bazel" && renamingBuildFile {
-				excludeChild = exclude.children["BUILD"]
+				instructionsChild = instructions.children["BUILD"]
 			} else {
-				excludeChild = exclude.children[f]
+				instructionsChild = instructions.children[f]
 			}
 		}
 
 		srcChildEntry, sExists := srcDirMap[f]
 		buildFilesChildEntry, bExists := buildFilesMap[f]
 
-		if excludeChild != nil && excludeChild.excluded {
+		if instructionsChild != nil && instructionsChild.excluded {
 			if bExists {
-				symlinkIntoForest(topdir, forestChild, buildFilesChild)
+				symlinkIntoForest(context.topdir, forestChild, buildFilesChild)
 			}
 			continue
 		}
 
-		sDir := sExists && isDir(shared.JoinPath(topdir, srcChild), srcChildEntry)
-		bDir := bExists && isDir(shared.JoinPath(topdir, buildFilesChild), buildFilesChildEntry)
+		sDir := sExists && isDir(shared.JoinPath(context.topdir, srcChild), srcChildEntry)
+		bDir := bExists && isDir(shared.JoinPath(context.topdir, buildFilesChild), buildFilesChildEntry)
 
 		if !sExists {
-			if bDir && excludeChild != nil {
+			if bDir && instructionsChild != nil {
 				// Not in the source tree, but we have to exclude something from under
 				// this subtree, so descend
-				plantSymlinkForestRecursive(cfg, topdir, forestChild, buildFilesChild, srcChild, excludeChild, acc, okay)
+				context.wg.Add(1)
+				go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild)
 			} else {
 				// Not in the source tree, symlink BUILD file
-				symlinkIntoForest(topdir, forestChild, buildFilesChild)
+				symlinkIntoForest(context.topdir, forestChild, buildFilesChild)
 			}
 		} else if !bExists {
-			if sDir && excludeChild != nil {
+			if sDir && instructionsChild != nil {
 				// Not in the build file tree, but we have to exclude something from
 				// under this subtree, so descend
-				plantSymlinkForestRecursive(cfg, topdir, forestChild, buildFilesChild, srcChild, excludeChild, acc, okay)
+				context.wg.Add(1)
+				go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild)
 			} else {
 				// Not in the build file tree, symlink source tree, carry on
-				symlinkIntoForest(topdir, forestChild, srcChild)
+				symlinkIntoForest(context.topdir, forestChild, srcChild)
 			}
 		} else if sDir && bDir {
 			// Both are directories. Descend.
-			plantSymlinkForestRecursive(cfg, topdir, forestChild, buildFilesChild, srcChild, excludeChild, acc, okay)
+			context.wg.Add(1)
+			go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild)
 		} else if !sDir && !bDir {
 			// Neither is a directory. Merge them.
-			srcBuildFile := shared.JoinPath(topdir, srcChild)
-			generatedBuildFile := shared.JoinPath(topdir, buildFilesChild)
+			srcBuildFile := shared.JoinPath(context.topdir, srcChild)
+			generatedBuildFile := shared.JoinPath(context.topdir, buildFilesChild)
 			// The Android.bp file that codegen used to produce `buildFilesChild` is
 			// already a dependency, we can ignore `buildFilesChild`.
-			*acc = append(*acc, srcChild)
-			err = mergeBuildFiles(shared.JoinPath(topdir, forestChild), srcBuildFile, generatedBuildFile, cfg.IsEnvTrue("BP2BUILD_VERBOSE"))
+			context.depCh <- srcChild
+			err = mergeBuildFiles(shared.JoinPath(context.topdir, forestChild), srcBuildFile, generatedBuildFile, context.verbose)
 			if err != nil {
 				fmt.Fprintf(os.Stderr, "Error merging %s and %s: %s",
 					srcBuildFile, generatedBuildFile, err)
-				*okay = false
+				context.okay.Store(false)
 			}
 		} else {
 			// Both exist and one is a file. This is an error.
 			fmt.Fprintf(os.Stderr,
 				"Conflict in workspace symlink tree creation: both '%s' and '%s' exist and exactly one is a directory\n",
 				srcChild, buildFilesChild)
-			*okay = false
+			context.okay.Store(false)
 		}
 	}
 }
 
+func removeParallelRecursive(path string, fi os.FileInfo, wg *sync.WaitGroup) {
+	defer wg.Done()
+
+	if fi.IsDir() {
+		children := readdirToMap(path)
+		childrenWg := &sync.WaitGroup{}
+		childrenWg.Add(len(children))
+
+		for child, childFi := range children {
+			go removeParallelRecursive(shared.JoinPath(path, child), childFi, childrenWg)
+		}
+
+		childrenWg.Wait()
+	}
+
+	if err := os.Remove(path); err != nil {
+		fmt.Fprintf(os.Stderr, "Cannot unlink '%s': %s\n", path, err)
+		os.Exit(1)
+	}
+}
+
+func removeParallel(path string) {
+	fi, err := os.Lstat(path)
+	if err != nil {
+		if errors.Is(err, fs.ErrNotExist) {
+			return
+		}
+
+		fmt.Fprintf(os.Stderr, "Cannot lstat '%s': %s\n", path, err)
+		os.Exit(1)
+	}
+
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+	removeParallelRecursive(path, fi, wg)
+
+	wg.Wait()
+}
+
 // Creates a symlink forest by merging the directory tree at "buildFiles" and
 // "srcDir" while excluding paths listed in "exclude". Returns the set of paths
 // under srcDir on which readdir() had to be called to produce the symlink
 // forest.
-func PlantSymlinkForest(cfg android.Config, topdir string, forest string, buildFiles string, srcDir string, exclude []string) []string {
+func PlantSymlinkForest(verbose bool, topdir string, forest string, buildFiles string, exclude []string) []string {
+	context := &symlinkForestContext{
+		verbose: verbose,
+		topdir:  topdir,
+		depCh:   make(chan string),
+	}
+
+	context.okay.Store(true)
+
+	removeParallel(shared.JoinPath(topdir, forest))
+
+	instructions := instructionsFromExcludePathList(exclude)
+	go func() {
+		context.wg.Add(1)
+		plantSymlinkForestRecursive(context, instructions, forest, buildFiles, ".")
+		context.wg.Wait()
+		close(context.depCh)
+	}()
+
 	deps := make([]string, 0)
-	os.RemoveAll(shared.JoinPath(topdir, forest))
-	excludeTree := treeFromExcludePathList(exclude)
-	okay := true
-	plantSymlinkForestRecursive(cfg, topdir, forest, buildFiles, srcDir, excludeTree, &deps, &okay)
-	if !okay {
+	for dep := range context.depCh {
+		deps = append(deps, dep)
+	}
+
+	if !context.okay.Load() {
 		os.Exit(1)
 	}
+
 	return deps
 }
diff --git a/cc/builder.go b/cc/builder.go
index 39f7dc3..75e4736 100644
--- a/cc/builder.go
+++ b/cc/builder.go
@@ -946,12 +946,8 @@
 	}
 
 	var errorMessage string
-	// When error occurs in previous version ABI diff, Developers can't just update ABI
-	// reference but need to follow instructions to ensure ABI backward compatibility.
 	if previousVersionDiff {
-		// TODO(b/241496591): Remove -advice-only after b/239792343 and b/239790286 are reolved.
-		extraFlags = append(extraFlags, "-advice-only")
-		errorMessage = "error: Please follow development/vndk/tools/header-checker/README.md to ensure the ABI compatibility between your source code and version " + strconv.Itoa(prevVersion) + "."
+		errorMessage = "error: Please follow https://android.googlesource.com/platform/development/+/master/vndk/tools/header-checker/README.md#configure-cross_version-abi-check to resolve the ABI difference between your source code and version " + strconv.Itoa(prevVersion) + "."
 		sourceVersion := prevVersion + 1
 		extraFlags = append(extraFlags, "-target-version", strconv.Itoa(sourceVersion))
 	} else {
diff --git a/cmd/soong_build/main.go b/cmd/soong_build/main.go
index 87710c0..1f3507d 100644
--- a/cmd/soong_build/main.go
+++ b/cmd/soong_build/main.go
@@ -56,6 +56,7 @@
 	bazelQueryViewDir   string
 	bazelApiBp2buildDir string
 	bp2buildMarker      string
+	symlinkForestMarker string
 
 	cmdlineArgs bootstrap.Args
 )
@@ -86,6 +87,7 @@
 	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(&symlinkForestMarker, "symlink_forest_marker", "", "If set, create the bp2build symlink forest, 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")
 	flag.BoolVar(&cmdlineArgs.BazelMode, "bazel-mode", false, "use bazel for analysis of certain modules")
@@ -130,7 +132,9 @@
 func newConfig(availableEnv map[string]string) android.Config {
 	var buildMode android.SoongBuildMode
 
-	if bp2buildMarker != "" {
+	if symlinkForestMarker != "" {
+		buildMode = android.SymlinkForest
+	} else if bp2buildMarker != "" {
 		buildMode = android.Bp2build
 	} else if bazelQueryViewDir != "" {
 		buildMode = android.GenerateQueryView
@@ -254,11 +258,10 @@
 
 	// Create the symlink forest
 	symlinkDeps := bp2build.PlantSymlinkForest(
-		configuration,
+		configuration.IsEnvTrue("BP2BUILD_VERBOSE"),
 		topDir,
 		workspace,
 		bazelApiBp2buildDir,
-		".",
 		excludes)
 	ninjaDeps = append(ninjaDeps, symlinkDeps...)
 
@@ -345,7 +348,10 @@
 // or the actual Soong build for the build.ninja file. Returns the top level
 // output file of the specific activity.
 func doChosenActivity(ctx *android.Context, configuration android.Config, extraNinjaDeps []string) string {
-	if configuration.BuildMode == android.Bp2build {
+	if configuration.BuildMode == android.SymlinkForest {
+		runSymlinkForestCreation(configuration, extraNinjaDeps)
+		return symlinkForestMarker
+	} else if configuration.BuildMode == android.Bp2build {
 		// Run the alternate pipeline of bp2build mutators and singleton to convert
 		// Blueprint to BUILD files before everything else.
 		runBp2Build(configuration, extraNinjaDeps)
@@ -519,12 +525,6 @@
 	}
 }
 
-func touchIfDoesNotExist(path string) {
-	if _, err := os.Stat(path); os.IsNotExist(err) {
-		touch(path)
-	}
-}
-
 // Find BUILD files in the srcDir which are not in the allowlist
 // (android.Bp2BuildConversionAllowlist#ShouldKeepExistingBuildFileForDir)
 // and return their paths so they can be left out of the Bazel workspace dir (i.e. ignored)
@@ -605,6 +605,54 @@
 	}
 }
 
+// This could in theory easily be separated into a binary that generically
+// merges two directories into a symlink tree. The main obstacle is that this
+// function currently depends on both Bazel-specific knowledge (the existence
+// of bazel-* symlinks) and configuration (the set of BUILD.bazel files that
+// should and should not be kept)
+//
+// Ideally, bp2build would write a file that contains instructions to the
+// symlink tree creation binary. Then the latter would not need to depend on
+// the very heavy-weight machinery of soong_build .
+func runSymlinkForestCreation(configuration android.Config, extraNinjaDeps []string) {
+	eventHandler := metrics.EventHandler{}
+
+	var ninjaDeps []string
+	ninjaDeps = append(ninjaDeps, extraNinjaDeps...)
+
+	generatedRoot := shared.JoinPath(configuration.SoongOutDir(), "bp2build")
+	workspaceRoot := shared.JoinPath(configuration.SoongOutDir(), "workspace")
+
+	excludes := bazelArtifacts()
+
+	if outDir[0] != '/' {
+		excludes = append(excludes, outDir)
+	}
+
+	existingBazelRelatedFiles, err := getExistingBazelRelatedFiles(topDir)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error determining existing Bazel-related files: %s\n", err)
+		os.Exit(1)
+	}
+
+	pathsToIgnoredBuildFiles := getPathsToIgnoredBuildFiles(configuration.Bp2buildPackageConfig, topDir, existingBazelRelatedFiles, configuration.IsEnvTrue("BP2BUILD_VERBOSE"))
+	excludes = append(excludes, pathsToIgnoredBuildFiles...)
+	excludes = append(excludes, getTemporaryExcludes()...)
+
+	// PlantSymlinkForest() returns all the directories that were readdir()'ed.
+	// Such a directory SHOULD be added to `ninjaDeps` so that a child directory
+	// or file created/deleted under it would trigger an update of the symlink
+	// forest.
+	eventHandler.Do("symlink_forest", func() {
+		symlinkForestDeps := bp2build.PlantSymlinkForest(
+			configuration.IsEnvTrue("BP2BUILD_VERBOSE"), topDir, workspaceRoot, generatedRoot, excludes)
+		ninjaDeps = append(ninjaDeps, symlinkForestDeps...)
+	})
+
+	writeDepFile(symlinkForestMarker, eventHandler, ninjaDeps)
+	touch(shared.JoinPath(topDir, symlinkForestMarker))
+}
+
 // 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.
@@ -646,43 +694,10 @@
 			codegenMetrics = bp2build.Codegen(codegenContext)
 		})
 
-		generatedRoot := shared.JoinPath(configuration.SoongOutDir(), "bp2build")
-		workspaceRoot := shared.JoinPath(configuration.SoongOutDir(), "workspace")
-
-		excludes := bazelArtifacts()
-
-		if outDir[0] != '/' {
-			excludes = append(excludes, outDir)
-		}
-
-		existingBazelRelatedFiles, err := getExistingBazelRelatedFiles(topDir)
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error determining existing Bazel-related files: %s\n", err)
-			os.Exit(1)
-		}
-
-		pathsToIgnoredBuildFiles := getPathsToIgnoredBuildFiles(configuration.Bp2buildPackageConfig, topDir, existingBazelRelatedFiles, configuration.IsEnvTrue("BP2BUILD_VERBOSE"))
-		excludes = append(excludes, pathsToIgnoredBuildFiles...)
-
-		excludes = append(excludes, getTemporaryExcludes()...)
-
-		// PlantSymlinkForest() returns all the directories that were readdir()'ed.
-		// Such a directory SHOULD be added to `ninjaDeps` so that a child directory
-		// or file created/deleted under it would trigger an update of the symlink
-		// forest.
-		eventHandler.Do("symlink_forest", func() {
-			symlinkForestDeps := bp2build.PlantSymlinkForest(
-				configuration, topDir, workspaceRoot, generatedRoot, ".", excludes)
-			ninjaDeps = append(ninjaDeps, symlinkForestDeps...)
-		})
-
 		ninjaDeps = append(ninjaDeps, codegenContext.AdditionalNinjaDeps()...)
 
 		writeDepFile(bp2buildMarker, eventHandler, ninjaDeps)
-
-		// Create an empty bp2build marker file, if it does not already exist.
-		// Note the relevant rule has `restat = true`
-		touchIfDoesNotExist(shared.JoinPath(topDir, bp2buildMarker))
+		touch(shared.JoinPath(topDir, bp2buildMarker))
 	})
 
 	// Only report metrics when in bp2build mode. The metrics aren't relevant
diff --git a/go.mod b/go.mod
index 8c1a9f0..5f0b91a 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,12 @@
 
 replace github.com/google/go-cmp v0.5.5 => ../../external/go-cmp
 
-// Indirect dep from go-cmp
-exclude golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
+require prebuilts/bazel/common/proto/analysis_v2 v0.0.0
+
+replace prebuilts/bazel/common/proto/analysis_v2 => ../../prebuilts/bazel/common/proto/analysis_v2
+
+require prebuilts/bazel/common/proto/build v0.0.0 // indirect
+
+replace prebuilts/bazel/common/proto/build => ../../prebuilts/bazel/common/proto/build
 
 go 1.18
diff --git a/tests/bootstrap_test.sh b/tests/bootstrap_test.sh
index e92a561..2331eb1 100755
--- a/tests/bootstrap_test.sh
+++ b/tests/bootstrap_test.sh
@@ -551,8 +551,45 @@
 
   run_soong bp2build
 
+  if [[ ! -f "./out/soong/bp2build_files_marker" ]]; then
+    fail "bp2build marker file was not generated"
+  fi
+
   if [[ ! -f "./out/soong/bp2build_workspace_marker" ]]; then
-    fail "Marker file was not generated"
+    fail "symlink forest marker file was not generated"
+  fi
+}
+
+function test_bp2build_add_irrelevant_file {
+  setup
+
+  mkdir -p a/b
+  touch a/b/c.txt
+  cat > a/b/Android.bp <<'EOF'
+filegroup {
+  name: "c",
+  srcs: ["c.txt"],
+  bazel_module: { bp2build_available: true },
+}
+EOF
+
+  run_soong bp2build
+  if [[ ! -e out/soong/bp2build/a/b/BUILD.bazel ]]; then
+    fail "BUILD file in symlink forest was not created";
+  fi
+
+  local mtime1=$(stat -c "%y" out/soong/bp2build/a/b/BUILD.bazel)
+
+  touch a/irrelevant.txt
+  run_soong bp2build
+  local mtime2=$(stat -c "%y" out/soong/bp2build/a/b/BUILD.bazel)
+
+  if [[ "$mtime1" != "$mtime2" ]]; then
+    fail "BUILD.bazel file was regenerated"
+  fi
+
+  if [[ ! -e "out/soong/workspace/a/irrelevant.txt" ]]; then
+    fail "New file was not symlinked into symlink forest"
   fi
 }
 
@@ -849,6 +886,7 @@
 test_bp2build_null_build
 test_bp2build_back_and_forth_null_build
 test_bp2build_add_android_bp
+test_bp2build_add_irrelevant_file
 test_bp2build_add_to_glob
 test_bp2build_bazel_workspace_structure
 test_bp2build_bazel_workspace_add_file
diff --git a/tests/lib.sh b/tests/lib.sh
index 7dd2bf3..006186a 100644
--- a/tests/lib.sh
+++ b/tests/lib.sh
@@ -85,6 +85,7 @@
   copy_directory build/blueprint
   copy_directory build/soong
   copy_directory build/make/tools/rbcrun
+  copy_directory prebuilts/bazel/common/proto
 
   symlink_directory prebuilts/sdk
   symlink_directory prebuilts/go
diff --git a/tests/mixed_mode_test.sh b/tests/mixed_mode_test.sh
index f6fffad..076ec4b 100755
--- a/tests/mixed_mode_test.sh
+++ b/tests/mixed_mode_test.sh
@@ -19,4 +19,51 @@
   run_bazel info --config=bp2build
 }
 
+function test_add_irrelevant_file {
+  setup
+  create_mock_bazel
+
+  mkdir -p soong_tests/a/b
+  touch soong_tests/a/b/c.txt
+  cat > soong_tests/a/b/Android.bp <<'EOF'
+filegroup {
+  name: "c",
+  srcs: ["c.txt"],
+  bazel_module: { bp2build_available: true },
+}
+EOF
+
+  run_soong --bazel-mode nothing
+
+  if [[ ! -e out/soong/bp2build/soong_tests/a/b/BUILD.bazel ]]; then
+    fail "BUILD.bazel not created"
+  fi
+
+  if [[ ! -e out/soong/build.ninja ]]; then
+    fail "build.ninja not created"
+  fi
+
+  local mtime_build1=$(stat -c "%y" out/soong/bp2build/soong_tests/a/b/BUILD.bazel)
+  local mtime_ninja1=$(stat -c "%y" out/soong/build.ninja)
+
+  touch soong_tests/a/irrelevant.txt
+
+  run_soong --bazel-mode nothing
+  local mtime_build2=$(stat -c "%y" out/soong/bp2build/soong_tests/a/b/BUILD.bazel)
+  local mtime_ninja2=$(stat -c "%y" out/soong/build.ninja)
+
+  if [[ "$mtime_build1" != "$mtime_build2" ]]; then
+    fail "BUILD.bazel was generated"
+  fi
+
+  if [[ "$mtime_ninja1" != "$mtime_ninja2" ]]; then
+    fail "build.ninja was regenerated"
+  fi
+
+  if [[ ! -e out/soong/workspace/soong_tests/a/irrelevant.txt ]]; then
+    fail "new file was not symlinked"
+  fi
+}
+
+test_add_irrelevant_file
 test_bazel_smoke
diff --git a/ui/build/config.go b/ui/build/config.go
index cde8d5d..36119f0 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -464,10 +464,11 @@
 
 func buildConfig(config Config) *smpb.BuildConfig {
 	c := &smpb.BuildConfig{
-		ForceUseGoma:    proto.Bool(config.ForceUseGoma()),
-		UseGoma:         proto.Bool(config.UseGoma()),
-		UseRbe:          proto.Bool(config.UseRBE()),
-		BazelMixedBuild: proto.Bool(config.BazelBuildEnabled()),
+		ForceUseGoma:                proto.Bool(config.ForceUseGoma()),
+		UseGoma:                     proto.Bool(config.UseGoma()),
+		UseRbe:                      proto.Bool(config.UseRBE()),
+		BazelMixedBuild:             proto.Bool(config.BazelBuildEnabled()),
+		ForceDisableBazelMixedBuild: proto.Bool(config.IsBazelMixedBuildForceDisabled()),
 	}
 	c.Targets = append(c.Targets, config.arguments...)
 
@@ -910,7 +911,11 @@
 	return shared.JoinPath(c.SoongOutDir(), usedEnvFile+"."+tag)
 }
 
-func (c *configImpl) Bp2BuildMarkerFile() string {
+func (c *configImpl) Bp2BuildFilesMarkerFile() string {
+	return shared.JoinPath(c.SoongOutDir(), "bp2build_files_marker")
+}
+
+func (c *configImpl) Bp2BuildWorkspaceMarkerFile() string {
 	return shared.JoinPath(c.SoongOutDir(), "bp2build_workspace_marker")
 }
 
@@ -1452,6 +1457,10 @@
 	return c.emptyNinjaFile
 }
 
+func (c *configImpl) IsBazelMixedBuildForceDisabled() bool {
+	return c.Environment().IsEnvTrue("BUILD_BROKEN_DISABLE_BAZEL")
+}
+
 func GetMetricsUploader(topDir string, env *Environment) string {
 	if p, ok := env.Get("METRICS_UPLOADER"); ok {
 		metricsUploader := filepath.Join(topDir, p)
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index 9ea5110..968544b 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -28,6 +28,7 @@
 	"android/soong/ui/logger"
 	smpb "android/soong/ui/metrics/metrics_proto"
 	"android/soong/ui/status"
+
 	"google.golang.org/protobuf/encoding/prototext"
 
 	"google.golang.org/protobuf/proto"
@@ -1015,40 +1016,55 @@
 			name:    "none set",
 			environ: Environment{},
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(false),
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(false),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
 			name:    "force use goma",
 			environ: Environment{"FORCE_USE_GOMA=1"},
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(true),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(false),
+				ForceUseGoma:                proto.Bool(true),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(false),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
 			name:    "use goma",
 			environ: Environment{"USE_GOMA=1"},
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(true),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(false),
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(true),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(false),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
 			name:    "use rbe",
 			environ: Environment{"USE_RBE=1"},
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(true),
-				BazelMixedBuild: proto.Bool(false),
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(true),
+				BazelMixedBuild:             proto.Bool(false),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
+			},
+		},
+		{
+			name:    "disable mixed builds",
+			environ: Environment{"BUILD_BROKEN_DISABLE_BAZEL=1"},
+			expectedBuildConfig: &smpb.BuildConfig{
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(false),
+				ForceDisableBazelMixedBuild: proto.Bool(true),
 			},
 		},
 		{
@@ -1056,10 +1072,11 @@
 			environ:  Environment{},
 			useBazel: true,
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(false),
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(false),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
@@ -1067,10 +1084,11 @@
 			environ:      Environment{},
 			bazelDevMode: true,
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(true),
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(true),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
@@ -1078,10 +1096,11 @@
 			environ:       Environment{},
 			bazelProdMode: true,
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(true),
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(true),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
@@ -1089,10 +1108,11 @@
 			environ:          Environment{},
 			bazelStagingMode: true,
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(true),
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(true),
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
@@ -1101,11 +1121,12 @@
 			useBazel:  true,
 			arguments: []string{"droid", "dist"},
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(false),
-				UseGoma:         proto.Bool(false),
-				UseRbe:          proto.Bool(false),
-				BazelMixedBuild: proto.Bool(false),
-				Targets:         []string{"droid", "dist"},
+				ForceUseGoma:                proto.Bool(false),
+				UseGoma:                     proto.Bool(false),
+				UseRbe:                      proto.Bool(false),
+				BazelMixedBuild:             proto.Bool(false),
+				Targets:                     []string{"droid", "dist"},
+				ForceDisableBazelMixedBuild: proto.Bool(false),
 			},
 		},
 		{
@@ -1114,14 +1135,16 @@
 				"FORCE_USE_GOMA=1",
 				"USE_GOMA=1",
 				"USE_RBE=1",
+				"BUILD_BROKEN_DISABLE_BAZEL=1",
 			},
 			useBazel:     true,
 			bazelDevMode: true,
 			expectedBuildConfig: &smpb.BuildConfig{
-				ForceUseGoma:    proto.Bool(true),
-				UseGoma:         proto.Bool(true),
-				UseRbe:          proto.Bool(true),
-				BazelMixedBuild: proto.Bool(true),
+				ForceUseGoma:                proto.Bool(true),
+				UseGoma:                     proto.Bool(true),
+				UseRbe:                      proto.Bool(true),
+				BazelMixedBuild:             proto.Bool(true),
+				ForceDisableBazelMixedBuild: proto.Bool(true),
 			},
 		},
 	}
diff --git a/ui/build/soong.go b/ui/build/soong.go
index 88e5592..ebf7166 100644
--- a/ui/build/soong.go
+++ b/ui/build/soong.go
@@ -41,12 +41,13 @@
 	availableEnvFile = "soong.environment.available"
 	usedEnvFile      = "soong.environment.used"
 
-	soongBuildTag      = "build"
-	bp2buildTag        = "bp2build"
-	jsonModuleGraphTag = "modulegraph"
-	queryviewTag       = "queryview"
-	apiBp2buildTag     = "api_bp2build"
-	soongDocsTag       = "soong_docs"
+	soongBuildTag        = "build"
+	bp2buildFilesTag     = "bp2build_files"
+	bp2buildWorkspaceTag = "bp2build_workspace"
+	jsonModuleGraphTag   = "modulegraph"
+	queryviewTag         = "queryview"
+	apiBp2buildTag       = "api_bp2build"
+	soongDocsTag         = "soong_docs"
 
 	// bootstrapEpoch is used to determine if an incremental build is incompatible with the current
 	// version of bootstrap and needs cleaning before continuing the build.  Increment this for
@@ -235,7 +236,7 @@
 func bootstrapGlobFileList(config Config) []string {
 	return []string{
 		config.NamedGlobFile(soongBuildTag),
-		config.NamedGlobFile(bp2buildTag),
+		config.NamedGlobFile(bp2buildFilesTag),
 		config.NamedGlobFile(jsonModuleGraphTag),
 		config.NamedGlobFile(queryviewTag),
 		config.NamedGlobFile(apiBp2buildTag),
@@ -276,20 +277,33 @@
 		// Mixed builds call Bazel from soong_build and they therefore need the
 		// Bazel workspace to be available. Make that so by adding a dependency on
 		// the bp2build marker file to the action that invokes soong_build .
-		mainSoongBuildInvocation.Inputs = append(mainSoongBuildInvocation.Inputs,
-			config.Bp2BuildMarkerFile())
+		mainSoongBuildInvocation.OrderOnlyInputs = append(mainSoongBuildInvocation.OrderOnlyInputs,
+			config.Bp2BuildWorkspaceMarkerFile())
 	}
 
 	bp2buildInvocation := primaryBuilderInvocation(
 		config,
-		bp2buildTag,
-		config.Bp2BuildMarkerFile(),
+		bp2buildFilesTag,
+		config.Bp2BuildFilesMarkerFile(),
 		[]string{
-			"--bp2build_marker", config.Bp2BuildMarkerFile(),
+			"--bp2build_marker", config.Bp2BuildFilesMarkerFile(),
 		},
 		fmt.Sprintf("converting Android.bp files to BUILD files at %s/bp2build", config.SoongOutDir()),
 	)
 
+	bp2buildWorkspaceInvocation := primaryBuilderInvocation(
+		config,
+		bp2buildWorkspaceTag,
+		config.Bp2BuildWorkspaceMarkerFile(),
+		[]string{
+			"--symlink_forest_marker", config.Bp2BuildWorkspaceMarkerFile(),
+		},
+		fmt.Sprintf("Creating Bazel symlink forest"),
+	)
+
+	bp2buildWorkspaceInvocation.Inputs = append(bp2buildWorkspaceInvocation.Inputs,
+		config.Bp2BuildFilesMarkerFile())
+
 	jsonModuleGraphInvocation := primaryBuilderInvocation(
 		config,
 		jsonModuleGraphTag,
@@ -361,6 +375,7 @@
 		primaryBuilderInvocations: []bootstrap.PrimaryBuilderInvocation{
 			mainSoongBuildInvocation,
 			bp2buildInvocation,
+			bp2buildWorkspaceInvocation,
 			jsonModuleGraphInvocation,
 			queryviewInvocation,
 			apiBp2buildInvocation,
@@ -426,7 +441,7 @@
 		checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(soongBuildTag))
 
 		if config.BazelBuildEnabled() || config.Bp2Build() {
-			checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(bp2buildTag))
+			checkEnvironmentFile(soongBuildEnv, config.UsedEnvFile(bp2buildFilesTag))
 		}
 
 		if config.JsonModuleGraph() {
@@ -497,7 +512,7 @@
 	}
 
 	if config.Bp2Build() {
-		targets = append(targets, config.Bp2BuildMarkerFile())
+		targets = append(targets, config.Bp2BuildWorkspaceMarkerFile())
 	}
 
 	if config.Queryview() {