Damien Martin-Guillerez | f88f4d8 | 2015-09-25 13:56:55 +0000 | [diff] [blame] | 1 | // Copyright 2015 The Bazel Authors. All rights reserved. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 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. |
| 14 | package com.google.devtools.build.lib.skyframe; |
| 15 | |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 16 | import static com.google.common.base.Preconditions.checkState; |
| 17 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 18 | import com.google.common.collect.ImmutableList; |
| 19 | import com.google.common.collect.ImmutableSet; |
| 20 | import com.google.common.collect.Iterables; |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 21 | import com.google.devtools.build.lib.actions.Artifact; |
| 22 | import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 23 | import com.google.devtools.build.lib.actions.FilesetOutputSymlink; |
| 24 | import com.google.devtools.build.lib.actions.FilesetTraversalParams; |
| 25 | import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversal; |
felly | f1e30f3 | 2019-07-09 12:26:10 -0700 | [diff] [blame] | 26 | import com.google.devtools.build.lib.actions.HasDigest; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 27 | import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.RecursiveFilesystemTraversalException; |
| 28 | import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile; |
buchgr | ffad587 | 2019-06-18 08:06:32 -0700 | [diff] [blame] | 29 | import com.google.devtools.build.lib.vfs.Path; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 30 | import com.google.devtools.build.lib.vfs.PathFragment; |
| 31 | import com.google.devtools.build.skyframe.SkyFunction; |
| 32 | import com.google.devtools.build.skyframe.SkyFunctionException; |
| 33 | import com.google.devtools.build.skyframe.SkyKey; |
| 34 | import com.google.devtools.build.skyframe.SkyValue; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 35 | import java.util.LinkedHashMap; |
| 36 | import java.util.Map; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 37 | import java.util.Set; |
| 38 | import java.util.TreeMap; |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 39 | import java.util.function.Function; |
Googler | 5cc598c | 2022-07-06 03:29:45 -0700 | [diff] [blame] | 40 | import javax.annotation.Nullable; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 41 | |
| 42 | /** SkyFunction for {@link FilesetEntryValue}. */ |
| 43 | public final class FilesetEntryFunction implements SkyFunction { |
| 44 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 45 | private static final class FilesetEntryFunctionException extends SkyFunctionException { |
| 46 | FilesetEntryFunctionException(RecursiveFilesystemTraversalException e) { |
| 47 | super(e, Transience.PERSISTENT); |
| 48 | } |
| 49 | } |
| 50 | |
buchgr | ffad587 | 2019-06-18 08:06:32 -0700 | [diff] [blame] | 51 | private final Function<String, Path> getExecRoot; |
Googler | 18b0844 | 2018-10-04 09:55:06 -0700 | [diff] [blame] | 52 | |
buchgr | ffad587 | 2019-06-18 08:06:32 -0700 | [diff] [blame] | 53 | public FilesetEntryFunction(Function<String, Path> getExecRoot) { |
| 54 | this.getExecRoot = getExecRoot; |
Googler | 18b0844 | 2018-10-04 09:55:06 -0700 | [diff] [blame] | 55 | } |
| 56 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 57 | @Override |
Googler | 5cc598c | 2022-07-06 03:29:45 -0700 | [diff] [blame] | 58 | @Nullable |
Janak Ramakrishnan | 3c0adb2 | 2016-08-15 21:54:55 +0000 | [diff] [blame] | 59 | public SkyValue compute(SkyKey key, Environment env) |
| 60 | throws FilesetEntryFunctionException, InterruptedException { |
buchgr | ffad587 | 2019-06-18 08:06:32 -0700 | [diff] [blame] | 61 | WorkspaceNameValue workspaceNameValue = |
| 62 | (WorkspaceNameValue) env.getValue(WorkspaceNameValue.key()); |
| 63 | if (workspaceNameValue == null) { |
| 64 | return null; |
| 65 | } |
| 66 | |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 67 | FilesetTraversalParams params = checkParams((FilesetEntryKey) key); |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 68 | |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 69 | 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 | } |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 86 | |
Googler | c5cb07a | 2024-04-25 06:07:15 -0700 | [diff] [blame] | 87 | // Check if directory traversal is permitted |
| 88 | if (resolvedRoot.getType().isDirectory() && !params.getDirectTraversal().permitDirectories()) { |
| 89 | throw new FilesetEntryFunctionException( |
| 90 | new RecursiveFilesystemTraversalException( |
| 91 | String.format( |
Googler | 9350bcb | 2024-09-04 02:34:25 -0700 | [diff] [blame] | 92 | "%s contains a directory artifact '%s'", |
| 93 | params.getOwnerLabelForErrorMessages(), params.getDestPath()), |
Googler | c5cb07a | 2024-04-25 06:07:15 -0700 | [diff] [blame] | 94 | RecursiveFilesystemTraversalException.Type.FILE_OPERATION_FAILURE)); |
| 95 | } |
| 96 | |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 97 | // 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 |
jingwen | 68c57f0 | 2018-11-21 16:17:17 -0800 | [diff] [blame] | 106 | // it's a symlink to one) then we have to create symlinks to each of their children. |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 107 | // (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. |
Googler | 762e37b | 2022-07-12 16:03:18 -0700 | [diff] [blame] | 113 | DirectTraversal direct = params.getDirectTraversal(); |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 114 | |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 115 | // 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 | |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 123 | Iterable<ResolvedFile> results; |
Googler | 73d952d | 2024-04-25 06:03:34 -0700 | [diff] [blame] | 124 | if (resolvedRoot.getType().isDirectory() && !resolvedRoot.getType().isSymlink()) { |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 125 | // 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(); |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 136 | for (ResolvedFile f : traversalValue.getTransitiveFiles().toList()) { |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 137 | PathFragment path = f.getNameInSymlinkTree().relativeTo(prefixToRemove); |
| 138 | if (!path.isEmpty()) { |
Googler | ae87f3e | 2022-04-21 12:49:12 -0700 | [diff] [blame] | 139 | path = params.getDestPath().getRelative(path); |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 140 | DirectoryTree dir = root; |
jhorvitz | ca6c459 | 2021-03-31 14:20:14 -0700 | [diff] [blame] | 141 | for (String segment : path.getParentDirectory().segments()) { |
| 142 | dir = dir.addOrGetSubdir(segment); |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 143 | } |
| 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); |
kush | d8ba904 | 2017-09-22 15:39:21 -0400 | [diff] [blame] | 155 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 156 | |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 157 | // 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 | |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 162 | SpecialArtifact enclosingTreeArtifact = getTreeArtifactOrNull(params); |
| 163 | |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 164 | // 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 Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 168 | |
jhorvitz | ca6c459 | 2021-03-31 14:20:14 -0700 | [diff] [blame] | 169 | // 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. |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 173 | // TODO(b/64754128): Investigate if we could have made the exclude earlier before |
| 174 | // unnecessarily iterating over all the files in an excluded directory. |
Googler | ae87f3e | 2022-04-21 12:49:12 -0700 | [diff] [blame] | 175 | if (!linkName.isEmpty() && params.getExcludedFiles().contains(linkName.getSegment(0))) { |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 176 | continue; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 177 | } |
| 178 | |
Googler | f953a41 | 2024-09-27 08:49:00 -0700 | [diff] [blame] | 179 | PathFragment targetName = f.getPath().asPath().asFragment(); |
felly | 2b3befd | 2018-08-10 10:37:56 -0700 | [diff] [blame] | 180 | maybeStoreSymlink( |
| 181 | linkName, |
| 182 | targetName, |
| 183 | f.getMetadata(), |
Googler | ae87f3e | 2022-04-21 12:49:12 -0700 | [diff] [blame] | 184 | params.getDestPath(), |
buchgr | ffad587 | 2019-06-18 08:06:32 -0700 | [diff] [blame] | 185 | outputSymlinks, |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 186 | getExecRoot.apply(workspaceNameValue.getName()), |
| 187 | enclosingTreeArtifact); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 188 | } |
| 189 | |
| 190 | return FilesetEntryValue.of(ImmutableSet.copyOf(outputSymlinks.values())); |
| 191 | } |
| 192 | |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 193 | @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 Ramakrishnan | b0b33a7 | 2016-08-01 19:20:31 +0000 | [diff] [blame] | 203 | /** Stores an output symlink unless it would overwrite an existing one. */ |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 204 | private static void maybeStoreSymlink( |
Janak Ramakrishnan | b0b33a7 | 2016-08-01 19:20:31 +0000 | [diff] [blame] | 205 | PathFragment linkName, |
| 206 | PathFragment linkTarget, |
felly | f1e30f3 | 2019-07-09 12:26:10 -0700 | [diff] [blame] | 207 | HasDigest metadata, |
Janak Ramakrishnan | b0b33a7 | 2016-08-01 19:20:31 +0000 | [diff] [blame] | 208 | PathFragment destPath, |
buchgr | ffad587 | 2019-06-18 08:06:32 -0700 | [diff] [blame] | 209 | Map<PathFragment, FilesetOutputSymlink> result, |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 210 | Path execRoot, |
| 211 | @Nullable SpecialArtifact enclosingTreeArtifact) { |
Laszlo Csomor | 0ad729f | 2015-12-02 15:20:35 +0000 | [diff] [blame] | 212 | linkName = destPath.getRelative(linkName); |
| 213 | if (!result.containsKey(linkName)) { |
kush | b39c693 | 2018-07-12 21:25:23 -0700 | [diff] [blame] | 214 | result.put( |
Googler | 18b0844 | 2018-10-04 09:55:06 -0700 | [diff] [blame] | 215 | linkName, |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 216 | FilesetOutputSymlink.create( |
| 217 | linkName, linkTarget, metadata, execRoot.asFragment(), enclosingTreeArtifact)); |
kush | 4b120e7 | 2018-07-11 16:21:27 -0700 | [diff] [blame] | 218 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 219 | } |
| 220 | |
mschaller | 163e61f | 2019-02-11 10:01:33 -0800 | [diff] [blame] | 221 | /** |
buchgr | ffad587 | 2019-06-18 08:06:32 -0700 | [diff] [blame] | 222 | * 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 |
Googler | 5900d6d | 2023-02-24 05:28:08 -0800 | [diff] [blame] | 224 | * when {@code DirectTraversal.isGenerated()}. |
mschaller | 163e61f | 2019-02-11 10:01:33 -0800 | [diff] [blame] | 225 | */ |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 226 | public static TraversalRequest getDependencyForRewinding(FilesetEntryKey key) { |
| 227 | FilesetTraversalParams params = checkParams(key); |
| 228 | checkState( |
Googler | 762e37b | 2022-07-12 16:03:18 -0700 | [diff] [blame] | 229 | params.getDirectTraversal().isGenerated(), |
mschaller | 163e61f | 2019-02-11 10:01:33 -0800 | [diff] [blame] | 230 | "Rewinding is only supported for outputs: %s", |
Googler | ae87f3e | 2022-04-21 12:49:12 -0700 | [diff] [blame] | 231 | params); |
mschaller | 163e61f | 2019-02-11 10:01:33 -0800 | [diff] [blame] | 232 | // Traversals in the output tree inline any recursive TraversalRequest evaluations, i.e. there |
| 233 | // won't be any transitively depended-on TraversalRequests. |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 234 | return FilesetTraversalRequest.create(params); |
mschaller | 163e61f | 2019-02-11 10:01:33 -0800 | [diff] [blame] | 235 | } |
| 236 | |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 237 | private static FilesetTraversalParams checkParams(FilesetEntryKey key) { |
| 238 | FilesetTraversalParams params = key.argument(); |
| 239 | checkState( |
Googler | 762e37b | 2022-07-12 16:03:18 -0700 | [diff] [blame] | 240 | params.getDirectTraversal() != null && params.getNestedArtifact() == null, |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 241 | "FilesetEntry does not support nested traversal: %s", |
| 242 | params); |
| 243 | return params; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 244 | } |
| 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 |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 257 | * 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 Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 260 | * |
| 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. |
Googler | 77fc643 | 2024-06-04 10:15:53 -0700 | [diff] [blame] | 265 | final Map<String, ResolvedFile> files = new TreeMap<>(); |
| 266 | final Map<String, DirectoryTree> dirs = new TreeMap<>(); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 267 | |
| 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(); |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 298 | Iterable<Map.Entry<String, DirectoryTree>> noDirSymlinkes = |
| 299 | Iterables.filter(dirs.entrySet(), input -> !fileNames.contains(input.getKey())); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 300 | |
| 301 | // 2. Extract the iterables of the true subdirectories. |
jcater | cecb3a8 | 2018-05-01 14:37:48 -0700 | [diff] [blame] | 302 | Iterable<Iterable<ResolvedFile>> subdirIters = |
Googler | d78fb57 | 2022-07-06 11:44:42 -0700 | [diff] [blame] | 303 | Iterables.transform(noDirSymlinkes, input -> input.getValue().iterateFiles()); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 304 | |
| 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 | } |