blob: 848846f1e16d9d9ea23550cf2df6bfd0e2809416 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2015 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
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.
14package com.google.devtools.build.lib.skyframe;
15
Googlerd78fb572022-07-06 11:44:42 -070016import static com.google.common.base.Preconditions.checkState;
17
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010018import com.google.common.collect.ImmutableList;
19import com.google.common.collect.ImmutableSet;
20import com.google.common.collect.Iterables;
Googler77fc6432024-06-04 10:15:53 -070021import com.google.devtools.build.lib.actions.Artifact;
22import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010023import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
24import com.google.devtools.build.lib.actions.FilesetTraversalParams;
25import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversal;
fellyf1e30f32019-07-09 12:26:10 -070026import com.google.devtools.build.lib.actions.HasDigest;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010027import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.RecursiveFilesystemTraversalException;
28import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
buchgrffad5872019-06-18 08:06:32 -070029import com.google.devtools.build.lib.vfs.Path;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010030import com.google.devtools.build.lib.vfs.PathFragment;
31import com.google.devtools.build.skyframe.SkyFunction;
32import com.google.devtools.build.skyframe.SkyFunctionException;
33import com.google.devtools.build.skyframe.SkyKey;
34import com.google.devtools.build.skyframe.SkyValue;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010035import java.util.LinkedHashMap;
36import java.util.Map;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010037import java.util.Set;
38import java.util.TreeMap;
Googlerd78fb572022-07-06 11:44:42 -070039import java.util.function.Function;
Googler5cc598c2022-07-06 03:29:45 -070040import javax.annotation.Nullable;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010041
42/** SkyFunction for {@link FilesetEntryValue}. */
43public final class FilesetEntryFunction implements SkyFunction {
44
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010045 private static final class FilesetEntryFunctionException extends SkyFunctionException {
46 FilesetEntryFunctionException(RecursiveFilesystemTraversalException e) {
47 super(e, Transience.PERSISTENT);
48 }
49 }
50
buchgrffad5872019-06-18 08:06:32 -070051 private final Function<String, Path> getExecRoot;
Googler18b08442018-10-04 09:55:06 -070052
buchgrffad5872019-06-18 08:06:32 -070053 public FilesetEntryFunction(Function<String, Path> getExecRoot) {
54 this.getExecRoot = getExecRoot;
Googler18b08442018-10-04 09:55:06 -070055 }
56
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010057 @Override
Googler5cc598c2022-07-06 03:29:45 -070058 @Nullable
Janak Ramakrishnan3c0adb22016-08-15 21:54:55 +000059 public SkyValue compute(SkyKey key, Environment env)
60 throws FilesetEntryFunctionException, InterruptedException {
buchgrffad5872019-06-18 08:06:32 -070061 WorkspaceNameValue workspaceNameValue =
62 (WorkspaceNameValue) env.getValue(WorkspaceNameValue.key());
63 if (workspaceNameValue == null) {
64 return null;
65 }
66
Googlerd78fb572022-07-06 11:44:42 -070067 FilesetTraversalParams params = checkParams((FilesetEntryKey) key);
felly2b3befd2018-08-10 10:37:56 -070068
Googlerd78fb572022-07-06 11:44:42 -070069 RecursiveFilesystemTraversalValue traversalValue =
70 (RecursiveFilesystemTraversalValue) env.getValue(FilesetTraversalRequest.create(params));
71 if (traversalValue == null) {
72 return null;
73 }
74
75 // The root can only be absent for the EMPTY RecursiveFilesystemTraversalValue instance.
76 if (traversalValue.getResolvedRoot().isEmpty()) {
77 return FilesetEntryValue.EMPTY;
78 }
79
80 ResolvedFile resolvedRoot = traversalValue.getResolvedRoot().get();
81
82 // Handle dangling symlinks gracefully be returning empty results.
83 if (!resolvedRoot.getType().exists()) {
84 return FilesetEntryValue.EMPTY;
85 }
felly2b3befd2018-08-10 10:37:56 -070086
Googlerc5cb07a2024-04-25 06:07:15 -070087 // Check if directory traversal is permitted
88 if (resolvedRoot.getType().isDirectory() && !params.getDirectTraversal().permitDirectories()) {
89 throw new FilesetEntryFunctionException(
90 new RecursiveFilesystemTraversalException(
91 String.format(
Googler9350bcb2024-09-04 02:34:25 -070092 "%s contains a directory artifact '%s'",
93 params.getOwnerLabelForErrorMessages(), params.getDestPath()),
Googlerc5cb07a2024-04-25 06:07:15 -070094 RecursiveFilesystemTraversalException.Type.FILE_OPERATION_FAILURE));
95 }
96
felly2b3befd2018-08-10 10:37:56 -070097 // The "direct" traversal params are present, which is the case when the FilesetEntry
98 // specifies a package's BUILD file, a directory or a list of files.
99
100 // The root of the direct traversal is defined as follows.
101 //
102 // If FilesetEntry.files is specified, then a TraversalRequest is created for each entry, the
103 // root being the respective entry itself. These are all traversed for they may be
104 // directories or symlinks to directories, and we need to establish Skyframe dependencies on
105 // their contents for incremental correctness. If an entry is indeed a directory (but not when
jingwen68c57f02018-11-21 16:17:17 -0800106 // it's a symlink to one) then we have to create symlinks to each of their children.
felly2b3befd2018-08-10 10:37:56 -0700107 // (NB: there seems to be no good reason for this, it's just how legacy Fileset works. We may
108 // want to consider creating a symlink just for the directory and not for its child elements.)
109 //
110 // If FilesetEntry.files is not specified, then srcdir refers to either a BUILD file or a
111 // directory. For the former, the root will be the parent of the BUILD file. For the latter,
112 // the root will be srcdir itself.
Googler762e37b2022-07-12 16:03:18 -0700113 DirectTraversal direct = params.getDirectTraversal();
felly2b3befd2018-08-10 10:37:56 -0700114
felly2b3befd2018-08-10 10:37:56 -0700115 // The prefix to remove is the entire path of the root. This is OK:
116 // - when the root is a file, this removes the entire path, but the traversal's destination
117 // path is actually the name of the output symlink, so this works out correctly
118 // - when the root is a directory or a symlink to one then we need to strip off the
119 // directory's path from every result (this is how the output symlinks must be created)
120 // before making them relative to the destination path
121 PathFragment prefixToRemove = direct.getRoot().getRelativePart();
122
Googlerd78fb572022-07-06 11:44:42 -0700123 Iterable<ResolvedFile> results;
Googler73d952d2024-04-25 06:03:34 -0700124 if (resolvedRoot.getType().isDirectory() && !resolvedRoot.getType().isSymlink()) {
felly2b3befd2018-08-10 10:37:56 -0700125 // The traversal is recursive (requested for an entire FilesetEntry.srcdir) or it was
126 // requested for a FilesetEntry.files entry which turned out to be a directory. We need to
127 // create an output symlink for every file in it and all of its subdirectories. Only
128 // exception is when the subdirectory is really a symlink to a directory -- no output
129 // shall be created for the contents of those.
130 // Now we create Dir objects to model the filesystem tree. The object employs a trick to
131 // find directory symlinks: directory symlinks have corresponding ResolvedFile entries and
132 // are added as files too, while their children, also added as files, contain the path of
133 // the parent. Finding and discarding the children is easy if we traverse the tree from
134 // root to leaf.
135 DirectoryTree root = new DirectoryTree();
Googlerd78fb572022-07-06 11:44:42 -0700136 for (ResolvedFile f : traversalValue.getTransitiveFiles().toList()) {
felly2b3befd2018-08-10 10:37:56 -0700137 PathFragment path = f.getNameInSymlinkTree().relativeTo(prefixToRemove);
138 if (!path.isEmpty()) {
Googlerae87f3e2022-04-21 12:49:12 -0700139 path = params.getDestPath().getRelative(path);
felly2b3befd2018-08-10 10:37:56 -0700140 DirectoryTree dir = root;
jhorvitzca6c4592021-03-31 14:20:14 -0700141 for (String segment : path.getParentDirectory().segments()) {
142 dir = dir.addOrGetSubdir(segment);
felly2b3befd2018-08-10 10:37:56 -0700143 }
144 dir.maybeAddFile(f);
145 }
146 }
147 // Here's where the magic happens. The returned iterable will yield all files in the
148 // directory that are not under symlinked directories, as well as all directory symlinks.
149 results = root.iterateFiles();
150 } else {
151 // If we're on this branch then the traversal was done for just one entry in
152 // FilesetEntry.files (which was not a directory, so it was either a file, a symlink to one
153 // or a symlink to a directory), meaning we'll have only one output symlink.
154 results = ImmutableList.of(resolvedRoot);
kushd8ba9042017-09-22 15:39:21 -0400155 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100156
Googlerd78fb572022-07-06 11:44:42 -0700157 // The map of output symlinks. Each key is the path of an output symlink that the Fileset must
158 // create, relative to the Fileset.out directory, and each value specifies extra information
159 // about the link (its target, associated metadata, and again its name).
160 Map<PathFragment, FilesetOutputSymlink> outputSymlinks = new LinkedHashMap<>();
161
Googler77fc6432024-06-04 10:15:53 -0700162 SpecialArtifact enclosingTreeArtifact = getTreeArtifactOrNull(params);
163
felly2b3befd2018-08-10 10:37:56 -0700164 // Create one output symlink for each entry in the results.
165 for (ResolvedFile f : results) {
166 // The linkName has to be under the traversal's root, which is also the prefix to remove.
167 PathFragment linkName = f.getNameInSymlinkTree().relativeTo(prefixToRemove);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100168
jhorvitzca6c4592021-03-31 14:20:14 -0700169 // Check whether the symlink is excluded before attempting to resolve it. It may be dangling,
170 // but excluding it is still fine. Only top-level files can be excluded, i.e. ones that are
171 // directly under the root if the root is a directory. Matching on getSegment(0) is sufficient
172 // to satisfy this, since any specified exclusions with multiple segments will never match.
felly2b3befd2018-08-10 10:37:56 -0700173 // TODO(b/64754128): Investigate if we could have made the exclude earlier before
174 // unnecessarily iterating over all the files in an excluded directory.
Googlerae87f3e2022-04-21 12:49:12 -0700175 if (!linkName.isEmpty() && params.getExcludedFiles().contains(linkName.getSegment(0))) {
felly2b3befd2018-08-10 10:37:56 -0700176 continue;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100177 }
178
Googlerf953a412024-09-27 08:49:00 -0700179 PathFragment targetName = f.getPath().asPath().asFragment();
felly2b3befd2018-08-10 10:37:56 -0700180 maybeStoreSymlink(
181 linkName,
182 targetName,
183 f.getMetadata(),
Googlerae87f3e2022-04-21 12:49:12 -0700184 params.getDestPath(),
buchgrffad5872019-06-18 08:06:32 -0700185 outputSymlinks,
Googler77fc6432024-06-04 10:15:53 -0700186 getExecRoot.apply(workspaceNameValue.getName()),
187 enclosingTreeArtifact);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100188 }
189
190 return FilesetEntryValue.of(ImmutableSet.copyOf(outputSymlinks.values()));
191 }
192
Googler77fc6432024-06-04 10:15:53 -0700193 @Nullable
194 private static SpecialArtifact getTreeArtifactOrNull(FilesetTraversalParams params) {
195 DirectTraversal direct = params.getDirectTraversal();
196 if (direct == null) {
197 return null;
198 }
199 Artifact artifact = direct.getRoot().getOutputArtifact();
200 return artifact instanceof SpecialArtifact special && special.isTreeArtifact() ? special : null;
201 }
202
Janak Ramakrishnanb0b33a72016-08-01 19:20:31 +0000203 /** Stores an output symlink unless it would overwrite an existing one. */
Googlerd78fb572022-07-06 11:44:42 -0700204 private static void maybeStoreSymlink(
Janak Ramakrishnanb0b33a72016-08-01 19:20:31 +0000205 PathFragment linkName,
206 PathFragment linkTarget,
fellyf1e30f32019-07-09 12:26:10 -0700207 HasDigest metadata,
Janak Ramakrishnanb0b33a72016-08-01 19:20:31 +0000208 PathFragment destPath,
buchgrffad5872019-06-18 08:06:32 -0700209 Map<PathFragment, FilesetOutputSymlink> result,
Googler77fc6432024-06-04 10:15:53 -0700210 Path execRoot,
211 @Nullable SpecialArtifact enclosingTreeArtifact) {
Laszlo Csomor0ad729f2015-12-02 15:20:35 +0000212 linkName = destPath.getRelative(linkName);
213 if (!result.containsKey(linkName)) {
kushb39c6932018-07-12 21:25:23 -0700214 result.put(
Googler18b08442018-10-04 09:55:06 -0700215 linkName,
Googler77fc6432024-06-04 10:15:53 -0700216 FilesetOutputSymlink.create(
217 linkName, linkTarget, metadata, execRoot.asFragment(), enclosingTreeArtifact));
kush4b120e72018-07-11 16:21:27 -0700218 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100219 }
220
mschaller163e61f2019-02-11 10:01:33 -0800221 /**
buchgrffad5872019-06-18 08:06:32 -0700222 * Returns the {@link TraversalRequest} node used to compute the Skyframe value for {@code
223 * filesetEntryKey}. Should only be called to determine which nodes need to be rewound, and only
Googler5900d6d2023-02-24 05:28:08 -0800224 * when {@code DirectTraversal.isGenerated()}.
mschaller163e61f2019-02-11 10:01:33 -0800225 */
Googlerd78fb572022-07-06 11:44:42 -0700226 public static TraversalRequest getDependencyForRewinding(FilesetEntryKey key) {
227 FilesetTraversalParams params = checkParams(key);
228 checkState(
Googler762e37b2022-07-12 16:03:18 -0700229 params.getDirectTraversal().isGenerated(),
mschaller163e61f2019-02-11 10:01:33 -0800230 "Rewinding is only supported for outputs: %s",
Googlerae87f3e2022-04-21 12:49:12 -0700231 params);
mschaller163e61f2019-02-11 10:01:33 -0800232 // Traversals in the output tree inline any recursive TraversalRequest evaluations, i.e. there
233 // won't be any transitively depended-on TraversalRequests.
Googlerd78fb572022-07-06 11:44:42 -0700234 return FilesetTraversalRequest.create(params);
mschaller163e61f2019-02-11 10:01:33 -0800235 }
236
Googlerd78fb572022-07-06 11:44:42 -0700237 private static FilesetTraversalParams checkParams(FilesetEntryKey key) {
238 FilesetTraversalParams params = key.argument();
239 checkState(
Googler762e37b2022-07-12 16:03:18 -0700240 params.getDirectTraversal() != null && params.getNestedArtifact() == null,
Googlerd78fb572022-07-06 11:44:42 -0700241 "FilesetEntry does not support nested traversal: %s",
242 params);
243 return params;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100244 }
245
246 /**
247 * Models a FilesetEntryFunction's portion of the symlink output tree created by a Fileset rule.
248 *
249 * <p>A Fileset rule's output is computed by zero or more {@link FilesetEntryFunction}s, resulting
250 * in one {@link FilesetEntryValue} for each. Each of those represents a portion of the grand
251 * output tree of the Fileset. These portions are later merged and written to the fileset manifest
252 * file, which is then consumed by a tool that ultimately creates the symlinks in the filesystem.
253 *
254 * <p>Because the Fileset doesn't process the lists in the FilesetEntryValues any further than
255 * merging them, they have to adhere to the conventions of the manifest file. One of these is that
256 * files are alphabetically ordered (enables the consumer of the manifest to work faster than
Googler77fc6432024-06-04 10:15:53 -0700257 * otherwise) and another is that the contents of regular directories are listed, but contents of
258 * directory symlinks are not, only the symlinks are. (Other details of the manifest file are not
259 * relevant here.)
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100260 *
261 * <p>See {@link DirectoryTree#iterateFiles()} for more details.
262 */
263 private static final class DirectoryTree {
264 // Use TreeMaps for the benefit of alphabetically ordered iteration.
Googler77fc6432024-06-04 10:15:53 -0700265 final Map<String, ResolvedFile> files = new TreeMap<>();
266 final Map<String, DirectoryTree> dirs = new TreeMap<>();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100267
268 DirectoryTree addOrGetSubdir(String name) {
269 DirectoryTree result = dirs.get(name);
270 if (result == null) {
271 result = new DirectoryTree();
272 dirs.put(name, result);
273 }
274 return result;
275 }
276
277 void maybeAddFile(ResolvedFile r) {
278 String name = r.getNameInSymlinkTree().getBaseName();
279 if (!files.containsKey(name)) {
280 files.put(name, r);
281 }
282 }
283
284 /**
285 * Lazily yields all files in this directory and all of its subdirectories.
286 *
287 * <p>The function first yields all the files directly under this directory, in alphabetical
288 * order. Then come the contents of subdirectories, processed recursively in the same fashion
289 * as this directory, and also in alphabetical order.
290 *
291 * <p>If a directory symlink is encountered its contents are not listed, only the symlink is.
292 */
293 Iterable<ResolvedFile> iterateFiles() {
294 // 1. Filter directory symlinks. If the symlink target contains files, those were added
295 // as normal files so their parent directory (the symlink) would show up in the dirs map
296 // (as a directory) as well as in the files map (as a symlink to a directory).
297 final Set<String> fileNames = files.keySet();
Googlerd78fb572022-07-06 11:44:42 -0700298 Iterable<Map.Entry<String, DirectoryTree>> noDirSymlinkes =
299 Iterables.filter(dirs.entrySet(), input -> !fileNames.contains(input.getKey()));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100300
301 // 2. Extract the iterables of the true subdirectories.
jcatercecb3a82018-05-01 14:37:48 -0700302 Iterable<Iterable<ResolvedFile>> subdirIters =
Googlerd78fb572022-07-06 11:44:42 -0700303 Iterables.transform(noDirSymlinkes, input -> input.getValue().iterateFiles());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100304
305 // 3. Just concat all subdirectory iterations for one, seamless iteration.
306 Iterable<ResolvedFile> dirsIter = Iterables.concat(subdirIters);
307
308 return Iterables.concat(files.values(), dirsIter);
309 }
310 }
311}