| // 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.io.LineProcessor; | |
| 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.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.List; | |
| import java.util.Map; | |
| /** | |
| * Representation of a Fileset manifest. | |
| */ | |
| public final class FilesetManifest { | |
| /** | |
| * 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, | |
| /** | |
| * Attempt to locally resolve the relative target path. Consider a manifest with two entries, | |
| * foo points to bar, and bar points to the absolute path /foobar. In that case, we can | |
| * determine that foo actually points at /foobar. Throw an exception if the local resolution | |
| * fails, e.g., if the target is not in the current manifest, or if it points at another | |
| * symlink (we could theoretically resolve recursively, but that's more complexity). | |
| */ | |
| 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 (IllegalStateException 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<>(); | |
| for (FilesetOutputSymlink outputSymlink : outputSymlinks) { | |
| PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.name); | |
| String artifact = outputSymlink.target.getPathString(); | |
| artifact = artifact.isEmpty() ? null : artifact; | |
| addSymlinkEntry(artifact, fullLocation, relSymlinkbehavior, entries, relativeLinks); | |
| } | |
| return constructFilesetManifest(entries, relativeLinks); | |
| } | |
| 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); | |
| } | |
| } | |
| 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 FilesetManifest constructFilesetManifest( | |
| Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks) { | |
| // Resolve relative symlinks if possible. Note that relativeLinks only contains entries in | |
| // RESOLVE mode. | |
| for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) { | |
| PathFragment location = e.getKey(); | |
| String value = e.getValue(); | |
| PathFragment actualLocation = location.getParentDirectory().getRelative(value); | |
| String actual = entries.get(actualLocation); | |
| if (actual == null) { | |
| throw new IllegalStateException( | |
| String.format( | |
| "runfiles target '%s' is a relative symlink, and could not be resolved within the " | |
| + "same Fileset", | |
| value)); | |
| } | |
| if (!actual.startsWith("/")) { | |
| throw new IllegalStateException( | |
| String.format( | |
| "runfiles target '%s' is a relative symlink, and points to another symlink", | |
| value)); | |
| } | |
| entries.put(location, actual); | |
| } | |
| return new FilesetManifest(entries); | |
| } | |
| private final Map<PathFragment, String> entries; | |
| private FilesetManifest(Map<PathFragment, String> entries) { | |
| this.entries = Collections.unmodifiableMap(entries); | |
| } | |
| public Map<PathFragment, String> getEntries() { | |
| return entries; | |
| } | |
| } |