Handle simple symlinks in mixed builds

Bug: 180945121
Test: build/bazel/ci/mixed_libc.sh
Change-Id: I49fba569a41dcb8cd4c2e58560817443697f58f1
This commit is contained in:
Liz Kammer 2021-06-08 15:04:11 -04:00
parent a4d9b86c8b
commit c49e682f37
3 changed files with 253 additions and 12 deletions

View file

@ -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

View file

@ -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

View file

@ -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
}