blob: 40e3842c82086c8f19170bc1a7ba49818ff6dc38 [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.exec;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.LineProcessor;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
import com.google.devtools.build.lib.analysis.AnalysisUtils;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.IORuntimeException;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* Representation of a Fileset manifest.
*/
public final class FilesetManifest {
private static final Logger logger = Logger.getLogger(FilesetManifest.class.getName());
/**
* Mode that determines how to handle relative target paths.
*/
public enum RelativeSymlinkBehavior {
/** Ignore any relative target paths. */
IGNORE,
/** Give an error if a relative target path is encountered. */
ERROR,
/** Resolve all relative target paths. */
RESOLVE;
}
public static FilesetManifest parseManifestFile(
PathFragment manifest,
Path execRoot,
String workspaceName,
RelativeSymlinkBehavior relSymlinkBehavior)
throws IOException {
Path file = execRoot.getRelative(AnalysisUtils.getManifestPathFromFilesetPath(manifest));
try {
return FileSystemUtils.asByteSource(file).asCharSource(UTF_8)
.readLines(
new ManifestLineProcessor(workspaceName, manifest, relSymlinkBehavior));
} catch (IORuntimeException e) {
// We can't throw IOException from getResult below, so we instead use an unchecked exception,
// and convert it to an IOException here.
throw new IOException(e.getMessage(), e);
}
}
public static FilesetManifest constructFilesetManifest(
List<FilesetOutputSymlink> outputSymlinks,
PathFragment targetPrefix,
RelativeSymlinkBehavior relSymlinkbehavior)
throws IOException {
LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>();
Map<PathFragment, String> relativeLinks = new HashMap<>();
Map<String, FileArtifactValue> artifactValues = new HashMap<>();
for (FilesetOutputSymlink outputSymlink : outputSymlinks) {
PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.getName());
String artifact = outputSymlink.getTargetPath().getPathString();
artifact = artifact.isEmpty() ? null : artifact;
addSymlinkEntry(artifact, fullLocation, relSymlinkbehavior, entries, relativeLinks);
if (outputSymlink.getMetadata() instanceof FileArtifactValue) {
artifactValues.put(artifact, (FileArtifactValue) outputSymlink.getMetadata());
}
}
try {
return constructFilesetManifest(entries, relativeLinks, artifactValues);
} catch (IORuntimeException e) {
throw new IOException(e.getMessage(), e);
}
}
private static final class ManifestLineProcessor implements LineProcessor<FilesetManifest> {
private final String workspaceName;
private final PathFragment targetPrefix;
private final RelativeSymlinkBehavior relSymlinkBehavior;
private int lineNum;
private final LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>();
// Resolution order of relative links can affect the outcome of the resolution. In particular,
// if there's a symlink to a symlink, then resolution fails if the first symlink is resolved
// first, but works if the second symlink is resolved first.
private final LinkedHashMap<PathFragment, String> relativeLinks = new LinkedHashMap<>();
ManifestLineProcessor(
String workspaceName,
PathFragment targetPrefix,
RelativeSymlinkBehavior relSymlinkBehavior) {
this.workspaceName = workspaceName;
this.targetPrefix = targetPrefix;
this.relSymlinkBehavior = relSymlinkBehavior;
}
@Override
public boolean processLine(String line) throws IOException {
if (++lineNum % 2 == 0) {
// Digest line, skip.
return true;
}
if (line.isEmpty()) {
return true;
}
String artifact;
PathFragment location;
int pos = line.indexOf(' ');
if (pos == -1) {
location = PathFragment.create(line);
artifact = null;
} else {
location = PathFragment.create(line.substring(0, pos));
String targetPath = line.substring(pos + 1);
artifact = targetPath.isEmpty() ? null : targetPath;
if (!workspaceName.isEmpty()) {
if (!location.getSegment(0).equals(workspaceName)) {
throw new IOException(
String.format(
"fileset manifest line must start with '%s': '%s'", workspaceName, location));
} else {
// Erase "<workspaceName>/" prefix.
location = location.subFragment(1);
}
}
}
PathFragment fullLocation = targetPrefix.getRelative(location);
addSymlinkEntry(artifact, fullLocation, relSymlinkBehavior, entries, relativeLinks);
return true;
}
@Override
public FilesetManifest getResult() {
return constructFilesetManifest(entries, relativeLinks, ImmutableMap.of());
}
}
private static void addSymlinkEntry(
String artifact,
PathFragment fullLocation,
RelativeSymlinkBehavior relSymlinkBehavior,
LinkedHashMap<PathFragment, String> entries,
Map<PathFragment, String> relativeLinks)
throws IOException {
if (!entries.containsKey(fullLocation)) {
boolean isRelativeSymlink = artifact != null && !artifact.startsWith("/");
if (isRelativeSymlink && relSymlinkBehavior.equals(RelativeSymlinkBehavior.ERROR)) {
throw new IOException(String.format("runfiles target is not absolute: %s", artifact));
}
if (!isRelativeSymlink || relSymlinkBehavior.equals(RelativeSymlinkBehavior.RESOLVE)) {
entries.put(fullLocation, artifact);
if (artifact != null && !artifact.startsWith("/")) {
relativeLinks.put(fullLocation, artifact);
}
}
}
}
private static final int MAX_SYMLINK_TRAVERSALS = 256;
private static FilesetManifest constructFilesetManifest(
Map<PathFragment, String> entries,
Map<PathFragment, String> relativeLinks,
Map<String, FileArtifactValue> artifactValues)
throws IORuntimeException {
// Resolve relative symlinks. Note that relativeLinks only contains entries in RESOLVE mode.
// We must find targets for these symlinks that are not inside the Fileset itself.
for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
PathFragment location = e.getKey();
String value = e.getValue();
String actual = Preconditions.checkNotNull(value, e);
Preconditions.checkState(!actual.startsWith("/"), e);
PathFragment actualLocation = location;
// Recursively resolve relative symlinks.
LinkedHashSet<String> seen = new LinkedHashSet<>();
int i = 0;
do {
actualLocation = actualLocation.getParentDirectory().getRelative(actual);
actual = entries.get(actualLocation);
} while (actual != null
&& !actual.startsWith("/")
&& seen.add(actual)
&& ++i < MAX_SYMLINK_TRAVERSALS);
if (actual == null) {
// We've found a relative symlink that points out of the fileset. We should really always
// throw here, but current behavior is that we tolerate such symlinks when they occur in
// runfiles, which is the only time this code is hit.
// TODO(b/113128395): throw here.
logger.warning(
"Symlink "
+ location
+ " (transitively) points to "
+ actualLocation
+ " that is not in this fileset (or was pruned because of a cycle)");
entries.remove(location);
} else if (i >= MAX_SYMLINK_TRAVERSALS) {
logger.warning(
"Symlink "
+ location
+ " is part of a chain of length at least "
+ i
+ " which exceeds Blaze's maximum allowable symlink chain length");
entries.remove(location);
} else if (!actual.startsWith("/")) {
// TODO(b/113128395): throw here.
logger.warning("Symlink " + location + " forms a symlink cycle: " + seen);
// Removing the entry here will lead to slightly vague log lines for the other entries in
// the cycle, since they will fail when they don't find this entry, as opposed to
// discovering their own cycles. But this log line should be informative enough.
entries.remove(location);
} else {
entries.put(location, actual);
}
}
return new FilesetManifest(entries, artifactValues);
}
private final Map<PathFragment, String> entries;
private final Map<String, FileArtifactValue> artifactValues;
private FilesetManifest(Map<PathFragment, String> entries,
Map<String, FileArtifactValue> artifactValues) {
this.entries = Collections.unmodifiableMap(entries);
this.artifactValues = artifactValues;
}
public Map<PathFragment, String> getEntries() {
return entries;
}
public Map<String, FileArtifactValue> getArtifactValues() {
return artifactValues;
}
}