blob: 0dbdfdf333babd5a89289e4fa349f1d254e7402b [file] [log] [blame]
ulfjackc174cc22017-10-02 14:49:09 +02001// Copyright 2017 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
fellyf6408d62019-07-16 12:51:14 -070014package com.google.devtools.build.lib.actions;
ulfjackc174cc22017-10-02 14:49:09 +020015
janakr206c19b2018-08-29 09:07:34 -070016import com.google.common.base.Preconditions;
Googler18b08442018-10-04 09:55:06 -070017import com.google.common.base.Strings;
janakr91470ae2020-04-13 10:58:58 -070018import com.google.common.flogger.GoogleLogger;
felly6ace3f12020-06-05 11:05:18 -070019import com.google.devtools.build.lib.vfs.FileSystemUtils;
20import com.google.devtools.build.lib.vfs.Path;
ulfjackc174cc22017-10-02 14:49:09 +020021import com.google.devtools.build.lib.vfs.PathFragment;
felly6ace3f12020-06-05 11:05:18 -070022import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
ulfjackc174cc22017-10-02 14:49:09 +020023import java.io.IOException;
24import java.util.Collections;
25import java.util.HashMap;
26import java.util.LinkedHashMap;
janakr206c19b2018-08-29 09:07:34 -070027import java.util.LinkedHashSet;
kush2ce45a22018-05-02 14:15:37 -070028import java.util.List;
ulfjackc174cc22017-10-02 14:49:09 +020029import java.util.Map;
Googler1ca80992018-10-11 17:47:22 -070030import javax.annotation.Nullable;
ulfjackc174cc22017-10-02 14:49:09 +020031
32/**
33 * Representation of a Fileset manifest.
34 */
35public final class FilesetManifest {
felly6ace3f12020-06-05 11:05:18 -070036 private static final int MAX_SYMLINK_TRAVERSALS = 256;
janakr91470ae2020-04-13 10:58:58 -070037 private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
janakr206c19b2018-08-29 09:07:34 -070038
ulfjackc174cc22017-10-02 14:49:09 +020039 /**
40 * Mode that determines how to handle relative target paths.
41 */
42 public enum RelativeSymlinkBehavior {
43 /** Ignore any relative target paths. */
44 IGNORE,
45
46 /** Give an error if a relative target path is encountered. */
47 ERROR,
48
janakr206c19b2018-08-29 09:07:34 -070049 /** Resolve all relative target paths. */
felly6ace3f12020-06-05 11:05:18 -070050 RESOLVE,
51
52 /** Fully resolve all relative paths, even those pointing to internal directories. */
53 RESOLVE_FULLY;
ulfjackc174cc22017-10-02 14:49:09 +020054 }
55
kush2ce45a22018-05-02 14:15:37 -070056 public static FilesetManifest constructFilesetManifest(
57 List<FilesetOutputSymlink> outputSymlinks,
58 PathFragment targetPrefix,
Googler9dc3e792018-10-17 05:16:47 -070059 RelativeSymlinkBehavior relSymlinkBehavior)
kush2ce45a22018-05-02 14:15:37 -070060 throws IOException {
61 LinkedHashMap<PathFragment, String> entries = new LinkedHashMap<>();
62 Map<PathFragment, String> relativeLinks = new HashMap<>();
felly943a9c72018-08-09 12:55:47 -070063 Map<String, FileArtifactValue> artifactValues = new HashMap<>();
kush2ce45a22018-05-02 14:15:37 -070064 for (FilesetOutputSymlink outputSymlink : outputSymlinks) {
kush4b120e72018-07-11 16:21:27 -070065 PathFragment fullLocation = targetPrefix.getRelative(outputSymlink.getName());
Googler9dc3e792018-10-17 05:16:47 -070066 String artifact = Strings.emptyToNull(outputSymlink.getTargetPath().getPathString());
67 if (isRelativeSymlink(outputSymlink)) {
Googler1ca80992018-10-11 17:47:22 -070068 addRelativeSymlinkEntry(artifact, fullLocation, relSymlinkBehavior, relativeLinks);
69 } else if (!entries.containsKey(fullLocation)) { // Keep consistent behavior: no overwriting.
70 entries.put(fullLocation, artifact);
71 }
felly943a9c72018-08-09 12:55:47 -070072 if (outputSymlink.getMetadata() instanceof FileArtifactValue) {
73 artifactValues.put(artifact, (FileArtifactValue) outputSymlink.getMetadata());
74 }
kush2ce45a22018-05-02 14:15:37 -070075 }
felly6ace3f12020-06-05 11:05:18 -070076 resolveRelativeSymlinks(entries, relativeLinks, targetPrefix.isAbsolute(), relSymlinkBehavior);
Googler9dc3e792018-10-17 05:16:47 -070077 return new FilesetManifest(entries, artifactValues);
78 }
79
80 private static boolean isRelativeSymlink(FilesetOutputSymlink symlink) {
81 return !symlink.getTargetPath().isEmpty()
82 && !symlink.getTargetPath().isAbsolute()
83 && !symlink.isRelativeToExecRoot();
ulfjackc174cc22017-10-02 14:49:09 +020084 }
85
Googler1ca80992018-10-11 17:47:22 -070086 /** Potentially adds the relative symlink to the map, depending on {@code relSymlinkBehavior}. */
87 private static void addRelativeSymlinkEntry(
88 @Nullable String artifact,
kush2ce45a22018-05-02 14:15:37 -070089 PathFragment fullLocation,
90 RelativeSymlinkBehavior relSymlinkBehavior,
kush2ce45a22018-05-02 14:15:37 -070091 Map<PathFragment, String> relativeLinks)
92 throws IOException {
Googler1ca80992018-10-11 17:47:22 -070093 switch (relSymlinkBehavior) {
94 case ERROR:
95 throw new IOException("runfiles target is not absolute: " + artifact);
96 case RESOLVE:
felly6ace3f12020-06-05 11:05:18 -070097 case RESOLVE_FULLY:
Googler1ca80992018-10-11 17:47:22 -070098 if (!relativeLinks.containsKey(fullLocation)) { // Keep consistent behavior: no overwriting.
kush2ce45a22018-05-02 14:15:37 -070099 relativeLinks.put(fullLocation, artifact);
100 }
Googler1ca80992018-10-11 17:47:22 -0700101 break;
102 case IGNORE:
103 break; // Do nothing.
kush2ce45a22018-05-02 14:15:37 -0700104 }
105 }
106
felly6ace3f12020-06-05 11:05:18 -0700107 /** Fully resolve relative symlinks including internal directory symlinks. */
108 private static void fullyResolveRelativeSymlinks(
109 Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks, boolean absolute)
110 throws IOException {
111 // Construct an in-memory Filesystem containing all the non-relative-symlink entries in the
112 // Fileset. Treat these as regular files in the filesystem whose contents are the "real" symlink
113 // pointing out of the Fileset. For relative symlinks, we encode these as symlinks in the
114 // in-memory Filesystem. This allows us to then crawl the filesystem for files. Any readable
115 // file is a valid part of the FilesetManifest. Dangling internal links or symlink cycles will
116 // be discovered by the in-memory filesystem.
117 InMemoryFileSystem fs = new InMemoryFileSystem();
118 Path root = fs.getPath("/");
119 for (Map.Entry<PathFragment, String> e : entries.entrySet()) {
120 PathFragment location = e.getKey();
121 Path locationPath = root.getRelative(location);
122 locationPath.getParentDirectory().createDirectoryAndParents();
123 FileSystemUtils.writeContentAsLatin1(locationPath, Strings.nullToEmpty(e.getValue()));
124 }
125 for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
126 PathFragment location = e.getKey();
127 Path locationPath = fs.getPath("/").getRelative(location);
128 PathFragment value = PathFragment.create(Preconditions.checkNotNull(e.getValue(), e));
129 Preconditions.checkState(!value.isAbsolute(), e);
130
131 locationPath.getParentDirectory().createDirectoryAndParents();
132 locationPath.createSymbolicLink(value);
133 }
134
135 addSymlinks(root, entries, absolute);
136 }
137
138 private static void addSymlinks(Path root, Map<PathFragment, String> entries, boolean absolute)
139 throws IOException {
140 for (Path path : root.getDirectoryEntries()) {
141 try {
142 if (path.isDirectory()) {
143 addSymlinks(path, entries, absolute);
144 } else {
145 String contents = new String(FileSystemUtils.readContentAsLatin1(path));
146 entries.put(
147 absolute ? path.asFragment() : path.asFragment().toRelative(),
148 Strings.emptyToNull(contents));
149 }
150 } catch (IOException e) {
151 logger.atWarning().log("Symlink %s is dangling or cyclic: %s", path, e.getMessage());
152 }
153 }
154 }
janakr206c19b2018-08-29 09:07:34 -0700155
Googler9dc3e792018-10-17 05:16:47 -0700156 /**
157 * Resolves relative symlinks and puts them in the {@code entries} map.
158 *
159 * <p>Note that {@code relativeLinks} should only contain entries in {@link
felly6ace3f12020-06-05 11:05:18 -0700160 * RelativeSymlinkBehavior#RESOLVE} or {@link RelativeSymlinkBehavior#RESOLVE_FULLY} mode.
Googler9dc3e792018-10-17 05:16:47 -0700161 */
162 private static void resolveRelativeSymlinks(
felly6ace3f12020-06-05 11:05:18 -0700163 Map<PathFragment, String> entries,
164 Map<PathFragment, String> relativeLinks,
165 boolean absolute,
166 RelativeSymlinkBehavior relSymlinkBehavior)
167 throws IOException {
168 if (relSymlinkBehavior == RelativeSymlinkBehavior.RESOLVE_FULLY && !relativeLinks.isEmpty()) {
169 fullyResolveRelativeSymlinks(entries, relativeLinks, absolute);
170 } else if (relSymlinkBehavior == RelativeSymlinkBehavior.RESOLVE) {
171 for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
172 PathFragment location = e.getKey();
173 String value = e.getValue();
174 String actual = Preconditions.checkNotNull(value, e);
175 Preconditions.checkState(!actual.startsWith("/"), e);
176 PathFragment actualLocation = location;
Googler1ca80992018-10-11 17:47:22 -0700177
felly6ace3f12020-06-05 11:05:18 -0700178 // Recursively resolve relative symlinks.
179 LinkedHashSet<String> seen = new LinkedHashSet<>();
180 int traversals = 0;
181 do {
182 actualLocation = actualLocation.getParentDirectory().getRelative(actual);
183 actual = relativeLinks.get(actualLocation);
184 } while (++traversals <= MAX_SYMLINK_TRAVERSALS && actual != null && seen.add(actual));
Googler1ca80992018-10-11 17:47:22 -0700185
felly6ace3f12020-06-05 11:05:18 -0700186 if (traversals >= MAX_SYMLINK_TRAVERSALS) {
187 logger.atWarning().log(
188 "Symlink %s is part of a chain of length at least %d"
189 + " which exceeds Blaze's maximum allowable symlink chain length",
190 location, traversals);
191 } else if (actual != null) {
192 // TODO(b/113128395): throw here.
193 logger.atWarning().log("Symlink %s forms a symlink cycle: %s", location, seen);
194 } else if (!entries.containsKey(actualLocation)) {
195 // We've found a relative symlink that points out of the fileset. We should really always
196 // throw here, but current behavior is that we tolerate such symlinks when they occur in
197 // runfiles, which is the only time this code is hit.
198 // TODO(b/113128395): throw here.
199 logger.atWarning().log(
200 "Symlink %s (transitively) points to %s"
201 + " that is not in this fileset (or was pruned because of a cycle)",
202 location, actualLocation);
203 } else {
204 // We have successfully resolved the symlink.
205 entries.put(location, entries.get(actualLocation));
206 }
ulfjack278a5e32018-06-04 05:27:38 -0700207 }
kush2ce45a22018-05-02 14:15:37 -0700208 }
kush2ce45a22018-05-02 14:15:37 -0700209 }
210
ulfjackc174cc22017-10-02 14:49:09 +0200211 private final Map<PathFragment, String> entries;
felly943a9c72018-08-09 12:55:47 -0700212 private final Map<String, FileArtifactValue> artifactValues;
ulfjackc174cc22017-10-02 14:49:09 +0200213
Googler9dc3e792018-10-17 05:16:47 -0700214 private FilesetManifest(
215 Map<PathFragment, String> entries, Map<String, FileArtifactValue> artifactValues) {
ulfjackc174cc22017-10-02 14:49:09 +0200216 this.entries = Collections.unmodifiableMap(entries);
felly943a9c72018-08-09 12:55:47 -0700217 this.artifactValues = artifactValues;
ulfjackc174cc22017-10-02 14:49:09 +0200218 }
219
Googler9dc3e792018-10-17 05:16:47 -0700220 /**
221 * Returns a mapping of symlink name to its target path.
222 *
223 * <p>Values in this map can be:
224 *
225 * <ul>
226 * <li>An absolute path.
227 * <li>A relative path, which should be considered relative to the exec root.
228 * <li>{@code null}, which represents an empty file.
229 * </ul>
230 */
ulfjackc174cc22017-10-02 14:49:09 +0200231 public Map<PathFragment, String> getEntries() {
232 return entries;
233 }
felly943a9c72018-08-09 12:55:47 -0700234
Googler9dc3e792018-10-17 05:16:47 -0700235 /**
236 * Returns a mapping of target path to {@link FileArtifactValue}.
237 *
238 * <p>The keyset of this map is a subset of the values in the map returned by {@link #getEntries}.
239 */
felly943a9c72018-08-09 12:55:47 -0700240 public Map<String, FileArtifactValue> getArtifactValues() {
241 return artifactValues;
242 }
janakr206c19b2018-08-29 09:07:34 -0700243}