Loading android/bazel_handler.go +7 −7 Original line number Diff line number Diff line Loading @@ -825,14 +825,14 @@ func (c *bazelSingleton) GenerateBuildActions(ctx SingletonContext) { for _, depset := range ctx.Config().BazelContext.AqueryDepsets() { var outputs []Path for _, depsetDepId := range depset.TransitiveDepSetIds { otherDepsetName := bazelDepsetName(depsetDepId) for _, depsetDepHash := range depset.TransitiveDepSetHashes { otherDepsetName := bazelDepsetName(depsetDepHash) outputs = append(outputs, PathForPhony(ctx, otherDepsetName)) } for _, artifactPath := range depset.DirectArtifacts { outputs = append(outputs, PathForBazelOut(ctx, artifactPath)) } thisDepsetName := bazelDepsetName(depset.Id) thisDepsetName := bazelDepsetName(depset.ContentHash) ctx.Build(pctx, BuildParams{ Rule: blueprint.Phony, Outputs: []WritablePath{PathForPhony(ctx, thisDepsetName)}, Loading Loading @@ -874,8 +874,8 @@ func (c *bazelSingleton) GenerateBuildActions(ctx SingletonContext) { for _, inputPath := range buildStatement.InputPaths { cmd.Implicit(PathForBazelOut(ctx, inputPath)) } for _, inputDepsetId := range buildStatement.InputDepsetIds { otherDepsetName := bazelDepsetName(inputDepsetId) for _, inputDepsetHash := range buildStatement.InputDepsetHashes { otherDepsetName := bazelDepsetName(inputDepsetHash) cmd.Implicit(PathForPhony(ctx, otherDepsetName)) } Loading Loading @@ -924,6 +924,6 @@ func GetConfigKey(ctx ModuleContext) configKey { } } func bazelDepsetName(depsetId int) string { return fmt.Sprintf("bazel_depset_%d", depsetId) func bazelDepsetName(contentHash string) string { return fmt.Sprintf("bazel_depset_%s", contentHash) } bazel/aquery.go +176 −100 Original line number Diff line number Diff line Loading @@ -15,10 +15,13 @@ package bazel import ( "crypto/sha256" "encoding/json" "fmt" "path/filepath" "reflect" "regexp" "sort" "strings" "github.com/google/blueprint/proptools" Loading @@ -44,15 +47,16 @@ type KeyValuePair struct { } // AqueryDepset is a depset definition from Bazel's aquery response. This is // akin to the `depSetOfFiles` in the response proto, except that direct // artifacts are enumerated by full path instead of by ID. // akin to the `depSetOfFiles` in the response proto, except: // * direct artifacts are enumerated by full path instead of by ID // * has a hash of the depset contents, instead of an int ID (for determinism) // A depset is a data structure for efficient transitive handling of artifact // paths. A single depset consists of one or more artifact paths and one or // more "child" depsets. type AqueryDepset struct { Id int ContentHash string DirectArtifacts []string TransitiveDepSetIds []int TransitiveDepSetHashes []string } // depSetOfFiles contains relevant portions of Bazel's aquery proto, DepSetOfFiles. Loading Loading @@ -99,19 +103,24 @@ type BuildStatement struct { // input paths. There should be no overlap between these fields; an input // path should either be included as part of an unexpanded depset or a raw // input path string, but not both. InputDepsetIds []int InputDepsetHashes []string InputPaths []string } // A helper type for aquery processing which facilitates retrieval of path IDs from their // less readable Bazel structures (depset and path fragment). type aqueryArtifactHandler struct { // Maps depset Id to depset struct. depsetIdToDepset map[int]depSetOfFiles // Maps depset id to AqueryDepset, a representation of depset which is // post-processed for middleman artifact handling, unhandled artifact // dropping, content hashing, etc. depsetIdToAqueryDepset map[int]AqueryDepset // Maps content hash to AqueryDepset. depsetHashToAqueryDepset map[string]AqueryDepset // depsetIdToArtifactIdsCache is a memoization of depset flattening, because flattening // may be an expensive operation. depsetIdToArtifactIdsCache map[int][]int // Maps artifact Id to fully expanded path. depsetHashToArtifactPathsCache map[string][]string // Maps artifact ContentHash to fully expanded path. artifactIdToPath map[int]string } Loading @@ -127,7 +136,7 @@ var TemplateActionOverriddenTokens = map[string]string{ var manifestFilePattern = regexp.MustCompile(".*/.+\\.runfiles/MANIFEST$") // The file name of py3wrapper.sh, which is used by py_binary targets. var py3wrapperFileName = "/py3wrapper.sh" const py3wrapperFileName = "/py3wrapper.sh" func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler, error) { pathFragments := map[int]pathFragment{} Loading @@ -144,7 +153,7 @@ func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler artifactIdToPath[artifact.Id] = artifactPath } // Map middleman artifact Id to input artifact depset ID. // Map middleman artifact ContentHash to input artifact depset ID. // Middleman artifacts are treated as "substitute" artifacts for mixed builds. For example, // if we find a middleman action which has outputs [foo, bar], and output [baz_middleman], then, // for each other action which has input [baz_middleman], we add [foo, bar] to the inputs for Loading @@ -165,19 +174,46 @@ func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler } depsetIdToDepset := map[int]depSetOfFiles{} for _, depset := range aqueryResult.DepSetOfFiles { depsetIdToDepset[depset.Id] = depset } aqueryHandler := aqueryArtifactHandler{ depsetIdToAqueryDepset: map[int]AqueryDepset{}, depsetHashToAqueryDepset: map[string]AqueryDepset{}, depsetHashToArtifactPathsCache: map[string][]string{}, artifactIdToPath: artifactIdToPath, } // Validate and adjust aqueryResult.DepSetOfFiles values. for _, depset := range aqueryResult.DepSetOfFiles { filteredArtifactIds := []int{} _, err := aqueryHandler.populateDepsetMaps(depset, middlemanIdToDepsetIds, depsetIdToDepset) if err != nil { return nil, err } } return &aqueryHandler, nil } // Ensures that the handler's depsetIdToAqueryDepset map contains an entry for the given // depset. func (a *aqueryArtifactHandler) populateDepsetMaps(depset depSetOfFiles, middlemanIdToDepsetIds map[int][]int, depsetIdToDepset map[int]depSetOfFiles) (AqueryDepset, error) { if aqueryDepset, containsDepset := a.depsetIdToAqueryDepset[depset.Id]; containsDepset { return aqueryDepset, nil } transitiveDepsetIds := depset.TransitiveDepSetIds directArtifactPaths := []string{} for _, artifactId := range depset.DirectArtifactIds { path, pathExists := artifactIdToPath[artifactId] path, pathExists := a.artifactIdToPath[artifactId] if !pathExists { return nil, fmt.Errorf("undefined input artifactId %d", artifactId) return AqueryDepset{}, fmt.Errorf("undefined input artifactId %d", artifactId) } // Filter out any inputs which are universally dropped, and swap middleman // artifacts with their corresponding depsets. if depsetsToUse, isMiddleman := middlemanIdToDepsetIds[artifactId]; isMiddleman { // Swap middleman artifacts with their corresponding depsets and drop the middleman artifacts. depset.TransitiveDepSetIds = append(depset.TransitiveDepSetIds, depsetsToUse...) transitiveDepsetIds = append(transitiveDepsetIds, depsetsToUse...) } else if strings.HasSuffix(path, py3wrapperFileName) || manifestFilePattern.MatchString(path) { // Drop these artifacts. // See go/python-binary-host-mixed-build for more details. Loading @@ -192,23 +228,30 @@ func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler // SourceSymlinkManifest and SymlinkTree actions are supported. } else { // TODO(b/216194240): Filter out bazel tools. filteredArtifactIds = append(filteredArtifactIds, artifactId) directArtifactPaths = append(directArtifactPaths, path) } } depset.DirectArtifactIds = filteredArtifactIds for _, childDepsetId := range depset.TransitiveDepSetIds { if _, exists := depsetIds[childDepsetId]; !exists { return nil, fmt.Errorf("undefined input depsetId %d (referenced by depsetId %d)", childDepsetId, depset.Id) childDepsetHashes := []string{} for _, childDepsetId := range transitiveDepsetIds { childDepset, exists := depsetIdToDepset[childDepsetId] if !exists { return AqueryDepset{}, fmt.Errorf("undefined input depsetId %d (referenced by depsetId %d)", childDepsetId, depset.Id) } childAqueryDepset, err := a.populateDepsetMaps(childDepset, middlemanIdToDepsetIds, depsetIdToDepset) if err != nil { return AqueryDepset{}, err } depsetIdToDepset[depset.Id] = depset childDepsetHashes = append(childDepsetHashes, childAqueryDepset.ContentHash) } return &aqueryArtifactHandler{ depsetIdToDepset: depsetIdToDepset, depsetIdToArtifactIdsCache: map[int][]int{}, artifactIdToPath: artifactIdToPath, }, nil aqueryDepset := AqueryDepset{ ContentHash: depsetContentHash(directArtifactPaths, childDepsetHashes), DirectArtifacts: directArtifactPaths, TransitiveDepSetHashes: childDepsetHashes, } a.depsetIdToAqueryDepset[depset.Id] = aqueryDepset a.depsetHashToAqueryDepset[aqueryDepset.ContentHash] = aqueryDepset return aqueryDepset, nil } // getInputPaths flattens the depsets of the given IDs and returns all transitive Loading @@ -219,15 +262,12 @@ func (a *aqueryArtifactHandler) getInputPaths(depsetIds []int) ([]string, error) inputPaths := []string{} for _, inputDepSetId := range depsetIds { inputArtifacts, err := a.artifactIdsFromDepsetId(inputDepSetId) depset := a.depsetIdToAqueryDepset[inputDepSetId] inputArtifacts, err := a.artifactPathsFromDepsetHash(depset.ContentHash) if err != nil { return nil, err } for _, inputId := range inputArtifacts { inputPath, exists := a.artifactIdToPath[inputId] if !exists { return nil, fmt.Errorf("undefined input artifactId %d", inputId) } for _, inputPath := range inputArtifacts { inputPaths = append(inputPaths, inputPath) } } Loading @@ -235,23 +275,23 @@ func (a *aqueryArtifactHandler) getInputPaths(depsetIds []int) ([]string, error) return inputPaths, nil } func (a *aqueryArtifactHandler) artifactIdsFromDepsetId(depsetId int) ([]int, error) { if result, exists := a.depsetIdToArtifactIdsCache[depsetId]; exists { func (a *aqueryArtifactHandler) artifactPathsFromDepsetHash(depsetHash string) ([]string, error) { if result, exists := a.depsetHashToArtifactPathsCache[depsetHash]; exists { return result, nil } if depset, exists := a.depsetIdToDepset[depsetId]; exists { result := depset.DirectArtifactIds for _, childId := range depset.TransitiveDepSetIds { childArtifactIds, err := a.artifactIdsFromDepsetId(childId) if depset, exists := a.depsetHashToAqueryDepset[depsetHash]; exists { result := depset.DirectArtifacts for _, childHash := range depset.TransitiveDepSetHashes { childArtifactIds, err := a.artifactPathsFromDepsetHash(childHash) if err != nil { return nil, err } result = append(result, childArtifactIds...) } a.depsetIdToArtifactIdsCache[depsetId] = result a.depsetHashToArtifactPathsCache[depsetHash] = result return result, nil } else { return nil, fmt.Errorf("undefined input depsetId %d", depsetId) return nil, fmt.Errorf("undefined input depset hash %d", depsetHash) } } Loading @@ -261,9 +301,6 @@ func (a *aqueryArtifactHandler) artifactIdsFromDepsetId(depsetId int) ([]int, er // 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) { buildStatements := []BuildStatement{} depsets := []AqueryDepset{} var aqueryResult actionGraphContainer err := json.Unmarshal(aqueryJsonProto, &aqueryResult) if err != nil { Loading @@ -274,6 +311,8 @@ func AqueryBuildStatements(aqueryJsonProto []byte) ([]BuildStatement, []AqueryDe return nil, nil, err } var buildStatements []BuildStatement for _, actionEntry := range aqueryResult.Actions { if shouldSkipAction(actionEntry) { continue Loading @@ -298,38 +337,75 @@ func AqueryBuildStatements(aqueryJsonProto []byte) ([]BuildStatement, []AqueryDe buildStatements = append(buildStatements, buildStatement) } // Iterate over depset IDs in the initial aquery order to preserve determinism. for _, depset := range aqueryResult.DepSetOfFiles { // Use the depset in the aqueryHandler, as this contains the augmented depsets. depset = aqueryHandler.depsetIdToDepset[depset.Id] directPaths := []string{} for _, artifactId := range depset.DirectArtifactIds { pathString := aqueryHandler.artifactIdToPath[artifactId] directPaths = append(directPaths, pathString) } aqueryDepset := AqueryDepset{ Id: depset.Id, DirectArtifacts: directPaths, TransitiveDepSetIds: depset.TransitiveDepSetIds, depsetsByHash := map[string]AqueryDepset{} depsets := []AqueryDepset{} for _, aqueryDepset := range aqueryHandler.depsetIdToAqueryDepset { if prevEntry, hasKey := depsetsByHash[aqueryDepset.ContentHash]; hasKey { // Two depsets collide on hash. Ensure that their contents are identical. if !reflect.DeepEqual(aqueryDepset, prevEntry) { return nil, nil, fmt.Errorf("Two different depsets have the same hash: %v, %v", prevEntry, aqueryDepset) } } else { depsetsByHash[aqueryDepset.ContentHash] = aqueryDepset depsets = append(depsets, aqueryDepset) } } // Build Statements and depsets must be sorted by their content hash to // preserve determinism between builds (this will result in consistent ninja file // output). Note they are not sorted by their original IDs nor their Bazel ordering, // as Bazel gives nondeterministic ordering / identifiers in aquery responses. sort.Slice(buildStatements, func(i, j int) bool { // For build statements, compare output lists. In Bazel, each output file // may only have one action which generates it, so this will provide // a deterministic ordering. outputs_i := buildStatements[i].OutputPaths outputs_j := buildStatements[j].OutputPaths if len(outputs_i) != len(outputs_j) { return len(outputs_i) < len(outputs_j) } if len(outputs_i) == 0 { // No outputs for these actions, so compare commands. return buildStatements[i].Command < buildStatements[j].Command } // There may be multiple outputs, but the output ordering is deterministic. return outputs_i[0] < outputs_j[0] }) sort.Slice(depsets, func(i, j int) bool { return depsets[i].ContentHash < depsets[j].ContentHash }) return buildStatements, depsets, nil } func (aqueryHandler *aqueryArtifactHandler) validateInputDepsets(inputDepsetIds []int) ([]int, error) { // Validate input depsets correspond to real depsets. // depsetContentHash computes and returns a SHA256 checksum of the contents of // the given depset. This content hash may serve as the depset's identifier. // Using a content hash for an identifier is superior for determinism. (For example, // using an integer identifier which depends on the order in which the depsets are // created would result in nondeterministic depset IDs.) func depsetContentHash(directPaths []string, transitiveDepsetHashes []string) string { h := sha256.New() // Use newline as delimiter, as paths cannot contain newline. h.Write([]byte(strings.Join(directPaths, "\n"))) h.Write([]byte(strings.Join(transitiveDepsetHashes, "\n"))) fullHash := fmt.Sprintf("%016x", h.Sum(nil)) return fullHash } func (aqueryHandler *aqueryArtifactHandler) depsetContentHashes(inputDepsetIds []int) ([]string, error) { hashes := []string{} for _, depsetId := range inputDepsetIds { if _, exists := aqueryHandler.depsetIdToDepset[depsetId]; !exists { if aqueryDepset, exists := aqueryHandler.depsetIdToAqueryDepset[depsetId]; !exists { return nil, fmt.Errorf("undefined input depsetId %d", depsetId) } else { hashes = append(hashes, aqueryDepset.ContentHash) } } return inputDepsetIds, nil return hashes, nil } func (aqueryHandler *aqueryArtifactHandler) normalActionBuildStatement(actionEntry action) (BuildStatement, error) { command := strings.Join(proptools.ShellEscapeListIncludingSpaces(actionEntry.Arguments), " ") inputDepsetIds, err := aqueryHandler.validateInputDepsets(actionEntry.InputDepSetIds) inputDepsetHashes, err := aqueryHandler.depsetContentHashes(actionEntry.InputDepSetIds) if err != nil { return BuildStatement{}, err } Loading @@ -342,7 +418,7 @@ func (aqueryHandler *aqueryArtifactHandler) normalActionBuildStatement(actionEnt Command: command, Depfile: depfile, OutputPaths: outputPaths, InputDepsetIds: inputDepsetIds, InputDepsetHashes: inputDepsetHashes, Env: actionEntry.EnvironmentVariables, Mnemonic: actionEntry.Mnemonic, } Loading Loading @@ -413,7 +489,7 @@ func (aqueryHandler *aqueryArtifactHandler) templateExpandActionBuildStatement(a // See go/python-binary-host-mixed-build for more details. command := fmt.Sprintf(`/bin/bash -c 'echo "%[1]s" | sed "s/\\\\n/\\n/g" > %[2]s && chmod a+x %[2]s'`, escapeCommandlineArgument(expandedTemplateContent), outputPaths[0]) inputDepsetIds, err := aqueryHandler.validateInputDepsets(actionEntry.InputDepSetIds) inputDepsetHashes, err := aqueryHandler.depsetContentHashes(actionEntry.InputDepSetIds) if err != nil { return BuildStatement{}, err } Loading @@ -422,7 +498,7 @@ func (aqueryHandler *aqueryArtifactHandler) templateExpandActionBuildStatement(a Command: command, Depfile: depfile, OutputPaths: outputPaths, InputDepsetIds: inputDepsetIds, InputDepsetHashes: inputDepsetHashes, Env: actionEntry.EnvironmentVariables, Mnemonic: actionEntry.Mnemonic, } Loading bazel/aquery_test.go +39 −38 Original line number Diff line number Diff line Loading @@ -234,7 +234,6 @@ func TestAqueryMultiArchGenrule(t *testing.T) { OutputPaths: []string{ fmt.Sprintf("bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-%s.S", arch), }, InputDepsetIds: []int{1}, Env: []KeyValuePair{ KeyValuePair{Key: "PATH", Value: "/bin:/usr/bin:/usr/local/bin"}, }, Loading @@ -248,11 +247,14 @@ func TestAqueryMultiArchGenrule(t *testing.T) { "../sourceroot/bionic/libc/tools/gensyscalls.py", "../bazel_tools/tools/genrule/genrule-setup.sh", } actualFlattenedInputs := flattenDepsets([]int{1}, actualDepsets) // In this example, each depset should have the same expected inputs. for _, actualDepset := range actualDepsets { actualFlattenedInputs := flattenDepsets([]string{actualDepset.ContentHash}, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedFlattenedInputs) { t.Errorf("Expected flattened inputs %v, but got %v", expectedFlattenedInputs, actualFlattenedInputs) } } } func TestInvalidOutputId(t *testing.T) { const inputString = ` Loading Loading @@ -748,7 +750,6 @@ func TestTransitiveInputDepsets(t *testing.T) { BuildStatement{ Command: "/bin/bash -c 'touch bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_out'", OutputPaths: []string{"bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_out"}, InputDepsetIds: []int{1}, Mnemonic: "Action", }, } Loading @@ -763,7 +764,8 @@ func TestTransitiveInputDepsets(t *testing.T) { } expectedFlattenedInputs = append(expectedFlattenedInputs, "bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_root") actualFlattenedInputs := flattenDepsets([]int{1}, actualDepsets) actualDepsetHashes := actualbuildStatements[0].InputDepsetHashes actualFlattenedInputs := flattenDepsets(actualDepsetHashes, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedFlattenedInputs) { t.Errorf("Expected flattened inputs %v, but got %v", expectedFlattenedInputs, actualFlattenedInputs) } Loading Loading @@ -844,38 +846,24 @@ func TestMiddlemenAction(t *testing.T) { t.Fatalf("Expected %d build statements, got %d", expected, len(actualBuildStatements)) } expectedDepsetFiles := [][]string{ []string{"middleinput_one", "middleinput_two"}, []string{"middleinput_one", "middleinput_two", "maininput_one", "maininput_two"}, } assertFlattenedDepsets(t, actualDepsets, expectedDepsetFiles) bs := actualBuildStatements[0] if len(bs.InputPaths) > 0 { t.Errorf("Expected main action raw inputs to be empty, but got %q", bs.InputPaths) } expectedInputDepsets := []int{2} if !reflect.DeepEqual(bs.InputDepsetIds, expectedInputDepsets) { t.Errorf("Expected main action depset IDs %v, but got %v", expectedInputDepsets, bs.InputDepsetIds) } expectedOutputs := []string{"output"} if !reflect.DeepEqual(bs.OutputPaths, expectedOutputs) { t.Errorf("Expected main action outputs %q, but got %q", expectedOutputs, bs.OutputPaths) } expectedAllDepsets := []AqueryDepset{ { Id: 1, DirectArtifacts: []string{"middleinput_one", "middleinput_two"}, }, { Id: 2, DirectArtifacts: []string{"maininput_one", "maininput_two"}, TransitiveDepSetIds: []int{1}, }, } if !reflect.DeepEqual(actualDepsets, expectedAllDepsets) { t.Errorf("Expected depsets %v, but got %v", expectedAllDepsets, actualDepsets) } expectedFlattenedInputs := []string{"middleinput_one", "middleinput_two", "maininput_one", "maininput_two"} actualFlattenedInputs := flattenDepsets(bs.InputDepsetIds, actualDepsets) actualFlattenedInputs := flattenDepsets(bs.InputDepsetHashes, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedFlattenedInputs) { t.Errorf("Expected flattened inputs %v, but got %v", expectedFlattenedInputs, actualFlattenedInputs) Loading @@ -883,29 +871,42 @@ func TestMiddlemenAction(t *testing.T) { } // Returns the contents of given depsets in concatenated post order. func flattenDepsets(depsetIdsToFlatten []int, allDepsets []AqueryDepset) []string { depsetsById := map[int]AqueryDepset{} func flattenDepsets(depsetHashesToFlatten []string, allDepsets []AqueryDepset) []string { depsetsByHash := map[string]AqueryDepset{} for _, depset := range allDepsets { depsetsById[depset.Id] = depset depsetsByHash[depset.ContentHash] = depset } result := []string{} for _, depsetId := range depsetIdsToFlatten { result = append(result, flattenDepset(depsetId, depsetsById)...) for _, depsetId := range depsetHashesToFlatten { result = append(result, flattenDepset(depsetId, depsetsByHash)...) } return result } // Returns the contents of a given depset in post order. func flattenDepset(depsetIdToFlatten int, allDepsets map[int]AqueryDepset) []string { depset := allDepsets[depsetIdToFlatten] func flattenDepset(depsetHashToFlatten string, allDepsets map[string]AqueryDepset) []string { depset := allDepsets[depsetHashToFlatten] result := []string{} for _, depsetId := range depset.TransitiveDepSetIds { for _, depsetId := range depset.TransitiveDepSetHashes { result = append(result, flattenDepset(depsetId, allDepsets)...) } result = append(result, depset.DirectArtifacts...) return result } func assertFlattenedDepsets(t *testing.T, actualDepsets []AqueryDepset, expectedDepsetFiles [][]string) { t.Helper() if len(actualDepsets) != len(expectedDepsetFiles) { t.Errorf("Expected %d depsets, but got %d depsets", expectedDepsetFiles, actualDepsets) } for i, actualDepset := range actualDepsets { actualFlattenedInputs := flattenDepsets([]string{actualDepset.ContentHash}, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedDepsetFiles[i]) { t.Errorf("Expected depset files: %v, but got %v", expectedDepsetFiles[i], actualFlattenedInputs) } } } func TestSimpleSymlink(t *testing.T) { const inputString = ` { Loading Loading
android/bazel_handler.go +7 −7 Original line number Diff line number Diff line Loading @@ -825,14 +825,14 @@ func (c *bazelSingleton) GenerateBuildActions(ctx SingletonContext) { for _, depset := range ctx.Config().BazelContext.AqueryDepsets() { var outputs []Path for _, depsetDepId := range depset.TransitiveDepSetIds { otherDepsetName := bazelDepsetName(depsetDepId) for _, depsetDepHash := range depset.TransitiveDepSetHashes { otherDepsetName := bazelDepsetName(depsetDepHash) outputs = append(outputs, PathForPhony(ctx, otherDepsetName)) } for _, artifactPath := range depset.DirectArtifacts { outputs = append(outputs, PathForBazelOut(ctx, artifactPath)) } thisDepsetName := bazelDepsetName(depset.Id) thisDepsetName := bazelDepsetName(depset.ContentHash) ctx.Build(pctx, BuildParams{ Rule: blueprint.Phony, Outputs: []WritablePath{PathForPhony(ctx, thisDepsetName)}, Loading Loading @@ -874,8 +874,8 @@ func (c *bazelSingleton) GenerateBuildActions(ctx SingletonContext) { for _, inputPath := range buildStatement.InputPaths { cmd.Implicit(PathForBazelOut(ctx, inputPath)) } for _, inputDepsetId := range buildStatement.InputDepsetIds { otherDepsetName := bazelDepsetName(inputDepsetId) for _, inputDepsetHash := range buildStatement.InputDepsetHashes { otherDepsetName := bazelDepsetName(inputDepsetHash) cmd.Implicit(PathForPhony(ctx, otherDepsetName)) } Loading Loading @@ -924,6 +924,6 @@ func GetConfigKey(ctx ModuleContext) configKey { } } func bazelDepsetName(depsetId int) string { return fmt.Sprintf("bazel_depset_%d", depsetId) func bazelDepsetName(contentHash string) string { return fmt.Sprintf("bazel_depset_%s", contentHash) }
bazel/aquery.go +176 −100 Original line number Diff line number Diff line Loading @@ -15,10 +15,13 @@ package bazel import ( "crypto/sha256" "encoding/json" "fmt" "path/filepath" "reflect" "regexp" "sort" "strings" "github.com/google/blueprint/proptools" Loading @@ -44,15 +47,16 @@ type KeyValuePair struct { } // AqueryDepset is a depset definition from Bazel's aquery response. This is // akin to the `depSetOfFiles` in the response proto, except that direct // artifacts are enumerated by full path instead of by ID. // akin to the `depSetOfFiles` in the response proto, except: // * direct artifacts are enumerated by full path instead of by ID // * has a hash of the depset contents, instead of an int ID (for determinism) // A depset is a data structure for efficient transitive handling of artifact // paths. A single depset consists of one or more artifact paths and one or // more "child" depsets. type AqueryDepset struct { Id int ContentHash string DirectArtifacts []string TransitiveDepSetIds []int TransitiveDepSetHashes []string } // depSetOfFiles contains relevant portions of Bazel's aquery proto, DepSetOfFiles. Loading Loading @@ -99,19 +103,24 @@ type BuildStatement struct { // input paths. There should be no overlap between these fields; an input // path should either be included as part of an unexpanded depset or a raw // input path string, but not both. InputDepsetIds []int InputDepsetHashes []string InputPaths []string } // A helper type for aquery processing which facilitates retrieval of path IDs from their // less readable Bazel structures (depset and path fragment). type aqueryArtifactHandler struct { // Maps depset Id to depset struct. depsetIdToDepset map[int]depSetOfFiles // Maps depset id to AqueryDepset, a representation of depset which is // post-processed for middleman artifact handling, unhandled artifact // dropping, content hashing, etc. depsetIdToAqueryDepset map[int]AqueryDepset // Maps content hash to AqueryDepset. depsetHashToAqueryDepset map[string]AqueryDepset // depsetIdToArtifactIdsCache is a memoization of depset flattening, because flattening // may be an expensive operation. depsetIdToArtifactIdsCache map[int][]int // Maps artifact Id to fully expanded path. depsetHashToArtifactPathsCache map[string][]string // Maps artifact ContentHash to fully expanded path. artifactIdToPath map[int]string } Loading @@ -127,7 +136,7 @@ var TemplateActionOverriddenTokens = map[string]string{ var manifestFilePattern = regexp.MustCompile(".*/.+\\.runfiles/MANIFEST$") // The file name of py3wrapper.sh, which is used by py_binary targets. var py3wrapperFileName = "/py3wrapper.sh" const py3wrapperFileName = "/py3wrapper.sh" func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler, error) { pathFragments := map[int]pathFragment{} Loading @@ -144,7 +153,7 @@ func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler artifactIdToPath[artifact.Id] = artifactPath } // Map middleman artifact Id to input artifact depset ID. // Map middleman artifact ContentHash to input artifact depset ID. // Middleman artifacts are treated as "substitute" artifacts for mixed builds. For example, // if we find a middleman action which has outputs [foo, bar], and output [baz_middleman], then, // for each other action which has input [baz_middleman], we add [foo, bar] to the inputs for Loading @@ -165,19 +174,46 @@ func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler } depsetIdToDepset := map[int]depSetOfFiles{} for _, depset := range aqueryResult.DepSetOfFiles { depsetIdToDepset[depset.Id] = depset } aqueryHandler := aqueryArtifactHandler{ depsetIdToAqueryDepset: map[int]AqueryDepset{}, depsetHashToAqueryDepset: map[string]AqueryDepset{}, depsetHashToArtifactPathsCache: map[string][]string{}, artifactIdToPath: artifactIdToPath, } // Validate and adjust aqueryResult.DepSetOfFiles values. for _, depset := range aqueryResult.DepSetOfFiles { filteredArtifactIds := []int{} _, err := aqueryHandler.populateDepsetMaps(depset, middlemanIdToDepsetIds, depsetIdToDepset) if err != nil { return nil, err } } return &aqueryHandler, nil } // Ensures that the handler's depsetIdToAqueryDepset map contains an entry for the given // depset. func (a *aqueryArtifactHandler) populateDepsetMaps(depset depSetOfFiles, middlemanIdToDepsetIds map[int][]int, depsetIdToDepset map[int]depSetOfFiles) (AqueryDepset, error) { if aqueryDepset, containsDepset := a.depsetIdToAqueryDepset[depset.Id]; containsDepset { return aqueryDepset, nil } transitiveDepsetIds := depset.TransitiveDepSetIds directArtifactPaths := []string{} for _, artifactId := range depset.DirectArtifactIds { path, pathExists := artifactIdToPath[artifactId] path, pathExists := a.artifactIdToPath[artifactId] if !pathExists { return nil, fmt.Errorf("undefined input artifactId %d", artifactId) return AqueryDepset{}, fmt.Errorf("undefined input artifactId %d", artifactId) } // Filter out any inputs which are universally dropped, and swap middleman // artifacts with their corresponding depsets. if depsetsToUse, isMiddleman := middlemanIdToDepsetIds[artifactId]; isMiddleman { // Swap middleman artifacts with their corresponding depsets and drop the middleman artifacts. depset.TransitiveDepSetIds = append(depset.TransitiveDepSetIds, depsetsToUse...) transitiveDepsetIds = append(transitiveDepsetIds, depsetsToUse...) } else if strings.HasSuffix(path, py3wrapperFileName) || manifestFilePattern.MatchString(path) { // Drop these artifacts. // See go/python-binary-host-mixed-build for more details. Loading @@ -192,23 +228,30 @@ func newAqueryHandler(aqueryResult actionGraphContainer) (*aqueryArtifactHandler // SourceSymlinkManifest and SymlinkTree actions are supported. } else { // TODO(b/216194240): Filter out bazel tools. filteredArtifactIds = append(filteredArtifactIds, artifactId) directArtifactPaths = append(directArtifactPaths, path) } } depset.DirectArtifactIds = filteredArtifactIds for _, childDepsetId := range depset.TransitiveDepSetIds { if _, exists := depsetIds[childDepsetId]; !exists { return nil, fmt.Errorf("undefined input depsetId %d (referenced by depsetId %d)", childDepsetId, depset.Id) childDepsetHashes := []string{} for _, childDepsetId := range transitiveDepsetIds { childDepset, exists := depsetIdToDepset[childDepsetId] if !exists { return AqueryDepset{}, fmt.Errorf("undefined input depsetId %d (referenced by depsetId %d)", childDepsetId, depset.Id) } childAqueryDepset, err := a.populateDepsetMaps(childDepset, middlemanIdToDepsetIds, depsetIdToDepset) if err != nil { return AqueryDepset{}, err } depsetIdToDepset[depset.Id] = depset childDepsetHashes = append(childDepsetHashes, childAqueryDepset.ContentHash) } return &aqueryArtifactHandler{ depsetIdToDepset: depsetIdToDepset, depsetIdToArtifactIdsCache: map[int][]int{}, artifactIdToPath: artifactIdToPath, }, nil aqueryDepset := AqueryDepset{ ContentHash: depsetContentHash(directArtifactPaths, childDepsetHashes), DirectArtifacts: directArtifactPaths, TransitiveDepSetHashes: childDepsetHashes, } a.depsetIdToAqueryDepset[depset.Id] = aqueryDepset a.depsetHashToAqueryDepset[aqueryDepset.ContentHash] = aqueryDepset return aqueryDepset, nil } // getInputPaths flattens the depsets of the given IDs and returns all transitive Loading @@ -219,15 +262,12 @@ func (a *aqueryArtifactHandler) getInputPaths(depsetIds []int) ([]string, error) inputPaths := []string{} for _, inputDepSetId := range depsetIds { inputArtifacts, err := a.artifactIdsFromDepsetId(inputDepSetId) depset := a.depsetIdToAqueryDepset[inputDepSetId] inputArtifacts, err := a.artifactPathsFromDepsetHash(depset.ContentHash) if err != nil { return nil, err } for _, inputId := range inputArtifacts { inputPath, exists := a.artifactIdToPath[inputId] if !exists { return nil, fmt.Errorf("undefined input artifactId %d", inputId) } for _, inputPath := range inputArtifacts { inputPaths = append(inputPaths, inputPath) } } Loading @@ -235,23 +275,23 @@ func (a *aqueryArtifactHandler) getInputPaths(depsetIds []int) ([]string, error) return inputPaths, nil } func (a *aqueryArtifactHandler) artifactIdsFromDepsetId(depsetId int) ([]int, error) { if result, exists := a.depsetIdToArtifactIdsCache[depsetId]; exists { func (a *aqueryArtifactHandler) artifactPathsFromDepsetHash(depsetHash string) ([]string, error) { if result, exists := a.depsetHashToArtifactPathsCache[depsetHash]; exists { return result, nil } if depset, exists := a.depsetIdToDepset[depsetId]; exists { result := depset.DirectArtifactIds for _, childId := range depset.TransitiveDepSetIds { childArtifactIds, err := a.artifactIdsFromDepsetId(childId) if depset, exists := a.depsetHashToAqueryDepset[depsetHash]; exists { result := depset.DirectArtifacts for _, childHash := range depset.TransitiveDepSetHashes { childArtifactIds, err := a.artifactPathsFromDepsetHash(childHash) if err != nil { return nil, err } result = append(result, childArtifactIds...) } a.depsetIdToArtifactIdsCache[depsetId] = result a.depsetHashToArtifactPathsCache[depsetHash] = result return result, nil } else { return nil, fmt.Errorf("undefined input depsetId %d", depsetId) return nil, fmt.Errorf("undefined input depset hash %d", depsetHash) } } Loading @@ -261,9 +301,6 @@ func (a *aqueryArtifactHandler) artifactIdsFromDepsetId(depsetId int) ([]int, er // 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) { buildStatements := []BuildStatement{} depsets := []AqueryDepset{} var aqueryResult actionGraphContainer err := json.Unmarshal(aqueryJsonProto, &aqueryResult) if err != nil { Loading @@ -274,6 +311,8 @@ func AqueryBuildStatements(aqueryJsonProto []byte) ([]BuildStatement, []AqueryDe return nil, nil, err } var buildStatements []BuildStatement for _, actionEntry := range aqueryResult.Actions { if shouldSkipAction(actionEntry) { continue Loading @@ -298,38 +337,75 @@ func AqueryBuildStatements(aqueryJsonProto []byte) ([]BuildStatement, []AqueryDe buildStatements = append(buildStatements, buildStatement) } // Iterate over depset IDs in the initial aquery order to preserve determinism. for _, depset := range aqueryResult.DepSetOfFiles { // Use the depset in the aqueryHandler, as this contains the augmented depsets. depset = aqueryHandler.depsetIdToDepset[depset.Id] directPaths := []string{} for _, artifactId := range depset.DirectArtifactIds { pathString := aqueryHandler.artifactIdToPath[artifactId] directPaths = append(directPaths, pathString) } aqueryDepset := AqueryDepset{ Id: depset.Id, DirectArtifacts: directPaths, TransitiveDepSetIds: depset.TransitiveDepSetIds, depsetsByHash := map[string]AqueryDepset{} depsets := []AqueryDepset{} for _, aqueryDepset := range aqueryHandler.depsetIdToAqueryDepset { if prevEntry, hasKey := depsetsByHash[aqueryDepset.ContentHash]; hasKey { // Two depsets collide on hash. Ensure that their contents are identical. if !reflect.DeepEqual(aqueryDepset, prevEntry) { return nil, nil, fmt.Errorf("Two different depsets have the same hash: %v, %v", prevEntry, aqueryDepset) } } else { depsetsByHash[aqueryDepset.ContentHash] = aqueryDepset depsets = append(depsets, aqueryDepset) } } // Build Statements and depsets must be sorted by their content hash to // preserve determinism between builds (this will result in consistent ninja file // output). Note they are not sorted by their original IDs nor their Bazel ordering, // as Bazel gives nondeterministic ordering / identifiers in aquery responses. sort.Slice(buildStatements, func(i, j int) bool { // For build statements, compare output lists. In Bazel, each output file // may only have one action which generates it, so this will provide // a deterministic ordering. outputs_i := buildStatements[i].OutputPaths outputs_j := buildStatements[j].OutputPaths if len(outputs_i) != len(outputs_j) { return len(outputs_i) < len(outputs_j) } if len(outputs_i) == 0 { // No outputs for these actions, so compare commands. return buildStatements[i].Command < buildStatements[j].Command } // There may be multiple outputs, but the output ordering is deterministic. return outputs_i[0] < outputs_j[0] }) sort.Slice(depsets, func(i, j int) bool { return depsets[i].ContentHash < depsets[j].ContentHash }) return buildStatements, depsets, nil } func (aqueryHandler *aqueryArtifactHandler) validateInputDepsets(inputDepsetIds []int) ([]int, error) { // Validate input depsets correspond to real depsets. // depsetContentHash computes and returns a SHA256 checksum of the contents of // the given depset. This content hash may serve as the depset's identifier. // Using a content hash for an identifier is superior for determinism. (For example, // using an integer identifier which depends on the order in which the depsets are // created would result in nondeterministic depset IDs.) func depsetContentHash(directPaths []string, transitiveDepsetHashes []string) string { h := sha256.New() // Use newline as delimiter, as paths cannot contain newline. h.Write([]byte(strings.Join(directPaths, "\n"))) h.Write([]byte(strings.Join(transitiveDepsetHashes, "\n"))) fullHash := fmt.Sprintf("%016x", h.Sum(nil)) return fullHash } func (aqueryHandler *aqueryArtifactHandler) depsetContentHashes(inputDepsetIds []int) ([]string, error) { hashes := []string{} for _, depsetId := range inputDepsetIds { if _, exists := aqueryHandler.depsetIdToDepset[depsetId]; !exists { if aqueryDepset, exists := aqueryHandler.depsetIdToAqueryDepset[depsetId]; !exists { return nil, fmt.Errorf("undefined input depsetId %d", depsetId) } else { hashes = append(hashes, aqueryDepset.ContentHash) } } return inputDepsetIds, nil return hashes, nil } func (aqueryHandler *aqueryArtifactHandler) normalActionBuildStatement(actionEntry action) (BuildStatement, error) { command := strings.Join(proptools.ShellEscapeListIncludingSpaces(actionEntry.Arguments), " ") inputDepsetIds, err := aqueryHandler.validateInputDepsets(actionEntry.InputDepSetIds) inputDepsetHashes, err := aqueryHandler.depsetContentHashes(actionEntry.InputDepSetIds) if err != nil { return BuildStatement{}, err } Loading @@ -342,7 +418,7 @@ func (aqueryHandler *aqueryArtifactHandler) normalActionBuildStatement(actionEnt Command: command, Depfile: depfile, OutputPaths: outputPaths, InputDepsetIds: inputDepsetIds, InputDepsetHashes: inputDepsetHashes, Env: actionEntry.EnvironmentVariables, Mnemonic: actionEntry.Mnemonic, } Loading Loading @@ -413,7 +489,7 @@ func (aqueryHandler *aqueryArtifactHandler) templateExpandActionBuildStatement(a // See go/python-binary-host-mixed-build for more details. command := fmt.Sprintf(`/bin/bash -c 'echo "%[1]s" | sed "s/\\\\n/\\n/g" > %[2]s && chmod a+x %[2]s'`, escapeCommandlineArgument(expandedTemplateContent), outputPaths[0]) inputDepsetIds, err := aqueryHandler.validateInputDepsets(actionEntry.InputDepSetIds) inputDepsetHashes, err := aqueryHandler.depsetContentHashes(actionEntry.InputDepSetIds) if err != nil { return BuildStatement{}, err } Loading @@ -422,7 +498,7 @@ func (aqueryHandler *aqueryArtifactHandler) templateExpandActionBuildStatement(a Command: command, Depfile: depfile, OutputPaths: outputPaths, InputDepsetIds: inputDepsetIds, InputDepsetHashes: inputDepsetHashes, Env: actionEntry.EnvironmentVariables, Mnemonic: actionEntry.Mnemonic, } Loading
bazel/aquery_test.go +39 −38 Original line number Diff line number Diff line Loading @@ -234,7 +234,6 @@ func TestAqueryMultiArchGenrule(t *testing.T) { OutputPaths: []string{ fmt.Sprintf("bazel-out/sourceroot/k8-fastbuild/bin/bionic/libc/syscalls-%s.S", arch), }, InputDepsetIds: []int{1}, Env: []KeyValuePair{ KeyValuePair{Key: "PATH", Value: "/bin:/usr/bin:/usr/local/bin"}, }, Loading @@ -248,11 +247,14 @@ func TestAqueryMultiArchGenrule(t *testing.T) { "../sourceroot/bionic/libc/tools/gensyscalls.py", "../bazel_tools/tools/genrule/genrule-setup.sh", } actualFlattenedInputs := flattenDepsets([]int{1}, actualDepsets) // In this example, each depset should have the same expected inputs. for _, actualDepset := range actualDepsets { actualFlattenedInputs := flattenDepsets([]string{actualDepset.ContentHash}, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedFlattenedInputs) { t.Errorf("Expected flattened inputs %v, but got %v", expectedFlattenedInputs, actualFlattenedInputs) } } } func TestInvalidOutputId(t *testing.T) { const inputString = ` Loading Loading @@ -748,7 +750,6 @@ func TestTransitiveInputDepsets(t *testing.T) { BuildStatement{ Command: "/bin/bash -c 'touch bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_out'", OutputPaths: []string{"bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_out"}, InputDepsetIds: []int{1}, Mnemonic: "Action", }, } Loading @@ -763,7 +764,8 @@ func TestTransitiveInputDepsets(t *testing.T) { } expectedFlattenedInputs = append(expectedFlattenedInputs, "bazel-out/sourceroot/k8-fastbuild/bin/testpkg/test_root") actualFlattenedInputs := flattenDepsets([]int{1}, actualDepsets) actualDepsetHashes := actualbuildStatements[0].InputDepsetHashes actualFlattenedInputs := flattenDepsets(actualDepsetHashes, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedFlattenedInputs) { t.Errorf("Expected flattened inputs %v, but got %v", expectedFlattenedInputs, actualFlattenedInputs) } Loading Loading @@ -844,38 +846,24 @@ func TestMiddlemenAction(t *testing.T) { t.Fatalf("Expected %d build statements, got %d", expected, len(actualBuildStatements)) } expectedDepsetFiles := [][]string{ []string{"middleinput_one", "middleinput_two"}, []string{"middleinput_one", "middleinput_two", "maininput_one", "maininput_two"}, } assertFlattenedDepsets(t, actualDepsets, expectedDepsetFiles) bs := actualBuildStatements[0] if len(bs.InputPaths) > 0 { t.Errorf("Expected main action raw inputs to be empty, but got %q", bs.InputPaths) } expectedInputDepsets := []int{2} if !reflect.DeepEqual(bs.InputDepsetIds, expectedInputDepsets) { t.Errorf("Expected main action depset IDs %v, but got %v", expectedInputDepsets, bs.InputDepsetIds) } expectedOutputs := []string{"output"} if !reflect.DeepEqual(bs.OutputPaths, expectedOutputs) { t.Errorf("Expected main action outputs %q, but got %q", expectedOutputs, bs.OutputPaths) } expectedAllDepsets := []AqueryDepset{ { Id: 1, DirectArtifacts: []string{"middleinput_one", "middleinput_two"}, }, { Id: 2, DirectArtifacts: []string{"maininput_one", "maininput_two"}, TransitiveDepSetIds: []int{1}, }, } if !reflect.DeepEqual(actualDepsets, expectedAllDepsets) { t.Errorf("Expected depsets %v, but got %v", expectedAllDepsets, actualDepsets) } expectedFlattenedInputs := []string{"middleinput_one", "middleinput_two", "maininput_one", "maininput_two"} actualFlattenedInputs := flattenDepsets(bs.InputDepsetIds, actualDepsets) actualFlattenedInputs := flattenDepsets(bs.InputDepsetHashes, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedFlattenedInputs) { t.Errorf("Expected flattened inputs %v, but got %v", expectedFlattenedInputs, actualFlattenedInputs) Loading @@ -883,29 +871,42 @@ func TestMiddlemenAction(t *testing.T) { } // Returns the contents of given depsets in concatenated post order. func flattenDepsets(depsetIdsToFlatten []int, allDepsets []AqueryDepset) []string { depsetsById := map[int]AqueryDepset{} func flattenDepsets(depsetHashesToFlatten []string, allDepsets []AqueryDepset) []string { depsetsByHash := map[string]AqueryDepset{} for _, depset := range allDepsets { depsetsById[depset.Id] = depset depsetsByHash[depset.ContentHash] = depset } result := []string{} for _, depsetId := range depsetIdsToFlatten { result = append(result, flattenDepset(depsetId, depsetsById)...) for _, depsetId := range depsetHashesToFlatten { result = append(result, flattenDepset(depsetId, depsetsByHash)...) } return result } // Returns the contents of a given depset in post order. func flattenDepset(depsetIdToFlatten int, allDepsets map[int]AqueryDepset) []string { depset := allDepsets[depsetIdToFlatten] func flattenDepset(depsetHashToFlatten string, allDepsets map[string]AqueryDepset) []string { depset := allDepsets[depsetHashToFlatten] result := []string{} for _, depsetId := range depset.TransitiveDepSetIds { for _, depsetId := range depset.TransitiveDepSetHashes { result = append(result, flattenDepset(depsetId, allDepsets)...) } result = append(result, depset.DirectArtifacts...) return result } func assertFlattenedDepsets(t *testing.T, actualDepsets []AqueryDepset, expectedDepsetFiles [][]string) { t.Helper() if len(actualDepsets) != len(expectedDepsetFiles) { t.Errorf("Expected %d depsets, but got %d depsets", expectedDepsetFiles, actualDepsets) } for i, actualDepset := range actualDepsets { actualFlattenedInputs := flattenDepsets([]string{actualDepset.ContentHash}, actualDepsets) if !reflect.DeepEqual(actualFlattenedInputs, expectedDepsetFiles[i]) { t.Errorf("Expected depset files: %v, but got %v", expectedDepsetFiles[i], actualFlattenedInputs) } } } func TestSimpleSymlink(t *testing.T) { const inputString = ` { Loading