// 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 outputSymlinks, PathFragment targetPrefix, RelativeSymlinkBehavior relSymlinkbehavior) throws IOException { LinkedHashMap entries = new LinkedHashMap<>(); Map 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 { private final String workspaceName; private final PathFragment targetPrefix; private final RelativeSymlinkBehavior relSymlinkBehavior; private int lineNum; private final LinkedHashMap entries = new LinkedHashMap<>(); private final Map relativeLinks = new HashMap<>(); 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 "/" 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 entries, Map 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 entries, Map relativeLinks) { // Resolve relative symlinks if possible. Note that relativeLinks only contains entries in // RESOLVE mode. for (Map.Entry e : relativeLinks.entrySet()) { PathFragment location = e.getKey(); String value = e.getValue(); PathFragment actualLocation = location.getParentDirectory().getRelative(value); String actual = entries.get(actualLocation); boolean isActualAcceptable = actual == null || actual.startsWith("/"); if (!entries.containsKey(actualLocation) || !isActualAcceptable) { throw new IllegalStateException( String.format( "runfiles target '%s' is not absolute, and could not be resolved in the same " + "Fileset", value)); } entries.put(location, actual); } return new FilesetManifest(entries); } private final Map entries; private FilesetManifest(Map entries) { this.entries = Collections.unmodifiableMap(entries); } public Map getEntries() { return entries; } }