blob: 3d457dbccd58c73cef6a80649b1b0a86a0e5d176 [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.actions;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
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;
import javax.annotation.Nullable;
/**
* 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 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 = Strings.emptyToNull(outputSymlink.getTargetPath().getPathString());
if (isRelativeSymlink(outputSymlink)) {
addRelativeSymlinkEntry(artifact, fullLocation, relSymlinkBehavior, relativeLinks);
} else if (!entries.containsKey(fullLocation)) { // Keep consistent behavior: no overwriting.
entries.put(fullLocation, artifact);
}
if (outputSymlink.getMetadata() instanceof FileArtifactValue) {
artifactValues.put(artifact, (FileArtifactValue) outputSymlink.getMetadata());
}
}
resolveRelativeSymlinks(entries, relativeLinks);
return new FilesetManifest(entries, artifactValues);
}
private static boolean isRelativeSymlink(FilesetOutputSymlink symlink) {
return !symlink.getTargetPath().isEmpty()
&& !symlink.getTargetPath().isAbsolute()
&& !symlink.isRelativeToExecRoot();
}
/** Potentially adds the relative symlink to the map, depending on {@code relSymlinkBehavior}. */
private static void addRelativeSymlinkEntry(
@Nullable String artifact,
PathFragment fullLocation,
RelativeSymlinkBehavior relSymlinkBehavior,
Map<PathFragment, String> relativeLinks)
throws IOException {
switch (relSymlinkBehavior) {
case ERROR:
throw new IOException("runfiles target is not absolute: " + artifact);
case RESOLVE:
if (!relativeLinks.containsKey(fullLocation)) { // Keep consistent behavior: no overwriting.
relativeLinks.put(fullLocation, artifact);
}
break;
case IGNORE:
break; // Do nothing.
}
}
private static final int MAX_SYMLINK_TRAVERSALS = 256;
/**
* Resolves relative symlinks and puts them in the {@code entries} map.
*
* <p>Note that {@code relativeLinks} should only contain entries in {@link
* RelativeSymlinkBehavior#RESOLVE} mode.
*/
private static void resolveRelativeSymlinks(
Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks) {
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 traversals = 0;
do {
actualLocation = actualLocation.getParentDirectory().getRelative(actual);
actual = relativeLinks.get(actualLocation);
} while (++traversals <= MAX_SYMLINK_TRAVERSALS && actual != null && seen.add(actual));
if (traversals >= MAX_SYMLINK_TRAVERSALS) {
logger.warning(
"Symlink "
+ location
+ " is part of a chain of length at least "
+ traversals
+ " which exceeds Blaze's maximum allowable symlink chain length");
} else if (actual != null) {
// TODO(b/113128395): throw here.
logger.warning("Symlink " + location + " forms a symlink cycle: " + seen);
} else if (!entries.containsKey(actualLocation)) {
// 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)");
} else {
// We have successfully resolved the symlink.
entries.put(location, entries.get(actualLocation));
}
}
}
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;
}
/**
* Returns a mapping of symlink name to its target path.
*
* <p>Values in this map can be:
*
* <ul>
* <li>An absolute path.
* <li>A relative path, which should be considered relative to the exec root.
* <li>{@code null}, which represents an empty file.
* </ul>
*/
public Map<PathFragment, String> getEntries() {
return entries;
}
/**
* Returns a mapping of target path to {@link FileArtifactValue}.
*
* <p>The keyset of this map is a subset of the values in the map returned by {@link #getEntries}.
*/
public Map<String, FileArtifactValue> getArtifactValues() {
return artifactValues;
}
}