diff --git a/android/bazel_handler.go b/android/bazel_handler.go index f906c8add..e4bbe64ff 100644 --- a/android/bazel_handler.go +++ b/android/bazel_handler.go @@ -756,6 +756,10 @@ func (c *bazelSingleton) GenerateBuildActions(ctx SingletonContext) { cmd.ImplicitDepFile(PathForBazelOut(ctx, *depfile)) } + for _, symlinkPath := range buildStatement.SymlinkPaths { + cmd.ImplicitSymlinkOutput(PathForBazelOut(ctx, symlinkPath)) + } + // This is required to silence warnings pertaining to unexpected timestamps. Particularly, // some Bazel builtins (such as files in the bazel_tools directory) have far-future // timestamps. Without restat, Ninja would emit warnings that the input files of a diff --git a/bazel/aquery.go b/bazel/aquery.go index d1119cb43..8741afbc5 100644 --- a/bazel/aquery.go +++ b/bazel/aquery.go @@ -73,12 +73,13 @@ type actionGraphContainer struct { // BuildStatement contains information to register a build statement corresponding (one to one) // with a Bazel action from Bazel's action graph. type BuildStatement struct { - Command string - Depfile *string - OutputPaths []string - InputPaths []string - Env []KeyValuePair - Mnemonic string + Command string + Depfile *string + OutputPaths []string + InputPaths []string + SymlinkPaths []string + Env []KeyValuePair + Mnemonic string } // A helper type for aquery processing which facilitates retrieval of path IDs from their @@ -234,10 +235,21 @@ func AqueryBuildStatements(aqueryJsonProto []byte) ([]BuildStatement, error) { OutputPaths: outputPaths, InputPaths: inputPaths, Env: actionEntry.EnvironmentVariables, - Mnemonic: actionEntry.Mnemonic} - if len(actionEntry.Arguments) < 1 { + Mnemonic: actionEntry.Mnemonic, + } + + if isSymlinkAction(actionEntry) { + if len(inputPaths) != 1 || len(outputPaths) != 1 { + return nil, fmt.Errorf("Expect 1 input and 1 output to symlink action, got: input %q, output %q", inputPaths, outputPaths) + } + out := outputPaths[0] + outDir := proptools.ShellEscapeIncludingSpaces(filepath.Dir(out)) + out = proptools.ShellEscapeIncludingSpaces(out) + in := proptools.ShellEscapeIncludingSpaces(inputPaths[0]) + buildStatement.Command = fmt.Sprintf("mkdir -p %[1]s && rm -f %[2]s && ln -rsf %[3]s %[2]s", outDir, out, in) + buildStatement.SymlinkPaths = outputPaths[:] + } else if len(actionEntry.Arguments) < 1 { return nil, fmt.Errorf("received action with no command: [%v]", buildStatement) - continue } buildStatements = append(buildStatements, buildStatement) } @@ -245,9 +257,13 @@ func AqueryBuildStatements(aqueryJsonProto []byte) ([]BuildStatement, error) { return buildStatements, nil } +func isSymlinkAction(a action) bool { + return a.Mnemonic == "Symlink" || a.Mnemonic == "SolibSymlink" +} + func shouldSkipAction(a action) bool { - // TODO(b/180945121): Handle symlink actions. - if a.Mnemonic == "Symlink" || a.Mnemonic == "SourceSymlinkManifest" || a.Mnemonic == "SymlinkTree" || a.Mnemonic == "SolibSymlink" { + // TODO(b/180945121): Handle complex symlink actions. + if a.Mnemonic == "SymlinkTree" || a.Mnemonic == "SourceSymlinkManifest" { return true } // Middleman actions are not handled like other actions; they are handled separately as a @@ -278,6 +294,9 @@ func expandPathFragment(id int, pathFragmentsMap map[int]pathFragment) (string, return "", fmt.Errorf("undefined path fragment id %d", currId) } labels = append([]string{currFragment.Label}, labels...) + if currId == currFragment.ParentId { + return "", fmt.Errorf("Fragment cannot refer to itself as parent %#v", currFragment) + } currId = currFragment.ParentId } return filepath.Join(labels...), nil diff --git a/bazel/aquery_test.go b/bazel/aquery_test.go index 7b40dcdda..43e41554e 100644 --- a/bazel/aquery_test.go +++ b/bazel/aquery_test.go @@ -805,17 +805,229 @@ func TestMiddlemenAction(t *testing.T) { } } +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 + }] +}` + + actual, err := AqueryBuildStatements([]byte(inputString)) + + if err != nil { + t.Errorf("Unexpected error %q", err) + } + + expectedBuildStatements := []BuildStatement{ + BuildStatement{ + Command: "mkdir -p one/symlink_subdir && " + + "rm -f one/symlink_subdir/symlink && " + + "ln -rsf one/file_subdir/file one/symlink_subdir/symlink", + InputPaths: []string{"one/file_subdir/file"}, + OutputPaths: []string{"one/symlink_subdir/symlink"}, + SymlinkPaths: []string{"one/symlink_subdir/symlink"}, + Mnemonic: "Symlink", + }, + } + assertBuildStatements(t, actual, expectedBuildStatements) +} + +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 + }] +}` + + actual, err := AqueryBuildStatements([]byte(inputString)) + + if err != nil { + t.Errorf("Unexpected error %q", err) + } + + expectedBuildStatements := []BuildStatement{ + BuildStatement{ + Command: "mkdir -p 'one/symlink subdir' && " + + "rm -f 'one/symlink subdir/symlink' && " + + "ln -rsf 'one/file subdir/file' 'one/symlink subdir/symlink'", + InputPaths: []string{"one/file subdir/file"}, + OutputPaths: []string{"one/symlink subdir/symlink"}, + SymlinkPaths: []string{"one/symlink subdir/symlink"}, + Mnemonic: "SolibSymlink", + }, + } + assertBuildStatements(t, actual, expectedBuildStatements) +} + +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" + }] +}` + + _, err := AqueryBuildStatements([]byte(inputString)) + 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" + }] +}` + + _, err := AqueryBuildStatements([]byte(inputString)) + assertError(t, err, `Expect 1 input and 1 output to symlink action, got: input ["file"], output ["symlink" "other_symlink"]`) +} + func assertError(t *testing.T, err error, expected string) { + t.Helper() if err == nil { t.Errorf("expected error '%s', but got no error", expected) } else if err.Error() != expected { - t.Errorf("expected error '%s', but got: %s", expected, err.Error()) + t.Errorf("expected error:\n\t'%s', but got:\n\t'%s'", expected, err.Error()) } } // Asserts that the given actual build statements match the given expected build statements. // Build statement equivalence is determined using buildStatementEquals. func assertBuildStatements(t *testing.T, expected []BuildStatement, actual []BuildStatement) { + t.Helper() if len(expected) != len(actual) { t.Errorf("expected %d build statements, but got %d,\n expected: %v,\n actual: %v", len(expected), len(actual), expected, actual) @@ -852,6 +1064,12 @@ func buildStatementEquals(first BuildStatement, second BuildStatement) bool { if !reflect.DeepEqual(stringSet(first.OutputPaths), stringSet(second.OutputPaths)) { return false } + if !reflect.DeepEqual(stringSet(first.SymlinkPaths), stringSet(second.SymlinkPaths)) { + return false + } + if first.Depfile != second.Depfile { + return false + } return true }