blob: 4e15d86861e5dfbadd59df84a1077f0261c56efb [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 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
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010016import com.google.common.cache.Cache;
17import com.google.common.cache.CacheBuilder;
Janak Ramakrishnance372c32016-03-14 16:19:18 +000018import com.google.common.collect.Maps;
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +000019import com.google.common.collect.Sets;
Kristina Chodorow73fa2032015-08-28 17:57:46 +000020import com.google.devtools.build.lib.cmdline.PackageIdentifier;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import com.google.devtools.build.lib.collect.nestedset.NestedSet;
22import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
Janak Ramakrishnance372c32016-03-14 16:19:18 +000023import com.google.devtools.build.lib.util.Preconditions;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010024import com.google.devtools.build.lib.vfs.Dirent;
25import com.google.devtools.build.lib.vfs.Dirent.Type;
26import com.google.devtools.build.lib.vfs.PathFragment;
27import com.google.devtools.build.lib.vfs.RootedPath;
28import com.google.devtools.build.lib.vfs.UnixGlob;
29import com.google.devtools.build.skyframe.SkyFunction;
30import com.google.devtools.build.skyframe.SkyFunctionException;
31import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
32import com.google.devtools.build.skyframe.SkyKey;
33import com.google.devtools.build.skyframe.SkyValue;
Janak Ramakrishnance372c32016-03-14 16:19:18 +000034import java.util.Map;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010035import java.util.regex.Pattern;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010036import javax.annotation.Nullable;
37
38/**
39 * A {@link SkyFunction} for {@link GlobValue}s.
40 *
41 * <p>This code drives the glob matching process.
42 */
Nathan Harmata029de3d2015-07-27 18:08:09 +000043public final class GlobFunction implements SkyFunction {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010044
45 private final Cache<String, Pattern> regexPatternCache =
Eric Fellheimer72744da2015-10-23 21:38:28 +000046 CacheBuilder.newBuilder().maximumSize(10000).concurrencyLevel(4).build();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010047
Eric Fellheimer434e4732015-08-04 18:08:45 +000048 private final boolean alwaysUseDirListing;
49
50 public GlobFunction(boolean alwaysUseDirListing) {
51 this.alwaysUseDirListing = alwaysUseDirListing;
52 }
53
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010054 @Override
Janak Ramakrishnan3c0adb22016-08-15 21:54:55 +000055 public SkyValue compute(SkyKey skyKey, Environment env)
56 throws GlobFunctionException, InterruptedException {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010057 GlobDescriptor glob = (GlobDescriptor) skyKey.argument();
58
Nathan Harmatab795e6b2016-02-04 01:10:19 +000059 // Note that the glob's package is assumed to exist which implies that the package's BUILD file
60 // exists which implies that the package's directory exists.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010061 PathFragment globSubdir = glob.getSubdir();
62 if (!globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
Janak Ramakrishnance372c32016-03-14 16:19:18 +000063 PackageLookupValue globSubdirPkgLookupValue =
64 (PackageLookupValue)
65 env.getValue(
66 PackageLookupValue.key(
67 PackageIdentifier.create(
68 glob.getPackageId().getRepository(),
69 glob.getPackageId().getPackageFragment().getRelative(globSubdir))));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010070 if (globSubdirPkgLookupValue == null) {
71 return null;
72 }
73 if (globSubdirPkgLookupValue.packageExists()) {
74 // We crossed the package boundary, that is, pkg/subdir contains a BUILD file and thus
75 // defines another package, so glob expansion should not descend into that subdir.
76 return GlobValue.EMPTY;
77 }
78 }
79
80 String pattern = glob.getPattern();
81 // Split off the first path component of the pattern.
Ulf Adams07dba942015-03-05 14:47:37 +000082 int slashPos = pattern.indexOf('/');
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010083 String patternHead;
84 String patternTail;
85 if (slashPos == -1) {
86 patternHead = pattern;
87 patternTail = null;
88 } else {
89 // Substrings will share the backing array of the original glob string. That should be fine.
90 patternHead = pattern.substring(0, slashPos);
91 patternTail = pattern.substring(slashPos + 1);
92 }
93
94 NestedSetBuilder<PathFragment> matches = NestedSetBuilder.stableOrder();
95
Janak Ramakrishnance372c32016-03-14 16:19:18 +000096 boolean globMatchesBareFile = patternTail == null;
97
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010098 // "**" also matches an empty segment, so try the case where it is not present.
99 if ("**".equals(patternHead)) {
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000100 if (globMatchesBareFile) {
Nathan Harmata86c319e2016-02-25 01:12:22 +0000101 // Recursive globs aren't supposed to match the package's directory.
102 if (!glob.excludeDirs() && !globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100103 matches.add(globSubdir);
104 }
105 } else {
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000106 SkyKey globKey =
107 GlobValue.internalKey(
108 glob.getPackageId(),
109 glob.getPackageRoot(),
110 globSubdir,
111 patternTail,
112 glob.excludeDirs());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100113 GlobValue globValue = (GlobValue) env.getValue(globKey);
114 if (globValue == null) {
115 return null;
116 }
117 matches.addTransitive(globValue.getMatches());
118 }
119 }
120
121 PathFragment dirPathFragment = glob.getPackageId().getPackageFragment().getRelative(globSubdir);
Nathan Harmatab795e6b2016-02-04 01:10:19 +0000122 RootedPath dirRootedPath = RootedPath.toRootedPath(glob.getPackageRoot(), dirPathFragment);
Eric Fellheimer434e4732015-08-04 18:08:45 +0000123 if (alwaysUseDirListing || containsGlobs(patternHead)) {
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000124 String subdirPattern = "**".equals(patternHead) ? glob.getPattern() : patternTail;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100125 // Pattern contains globs, so a directory listing is required.
126 //
127 // Note that we have good reason to believe the directory exists: if this is the
128 // top-level directory of the package, the package's existence implies the directory's
129 // existence; if this is a lower-level directory in the package, then we got here from
130 // previous directory listings. Filesystem operations concurrent with build could mean the
131 // directory no longer exists, but DirectoryListingFunction handles that gracefully.
132 DirectoryListingValue listingValue = (DirectoryListingValue)
133 env.getValue(DirectoryListingValue.key(dirRootedPath));
134 if (listingValue == null) {
135 return null;
136 }
137
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000138 // In order to batch Skyframe requests, we do three passes over the directory:
139 // (1) Process every dirent, keeping track of values we need to request if the dirent cannot
140 // be processed with current information (symlink targets and subdirectory globs/package
141 // lookups for some subdirectories).
142 // (2) Get those values and process the symlinks, keeping track of subdirectory globs/package
143 // lookups we may need to request in case the symlink's target is a directory.
144 // (3) Process the necessary subdirectories.
145 int direntsSize = listingValue.getDirents().size();
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000146 Map<SkyKey, Dirent> symlinkFileMap = Maps.newHashMapWithExpectedSize(direntsSize);
147 Map<SkyKey, Dirent> subdirMap = Maps.newHashMapWithExpectedSize(direntsSize);
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000148 Map<Dirent, Object> sortedResultMap = Maps.newTreeMap();
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000149 // First pass: do normal files and collect SkyKeys to request for subdirectories and symlinks.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100150 for (Dirent dirent : listingValue.getDirents()) {
151 Type direntType = dirent.getType();
152 String fileName = dirent.getName();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100153 if (!UnixGlob.matches(patternHead, fileName, regexPatternCache)) {
154 continue;
155 }
156
157 if (direntType == Dirent.Type.SYMLINK) {
158 // TODO(bazel-team): Consider extracting the symlink resolution logic.
159 // For symlinks, look up the corresponding FileValue. This ensures that if the symlink
160 // changes and "switches types" (say, from a file to a directory), this value will be
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000161 // invalidated. We also need the target's type to properly process the symlink.
162 symlinkFileMap.put(
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000163 FileValue.key(
164 RootedPath.toRootedPath(
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000165 glob.getPackageRoot(), dirPathFragment.getRelative(fileName))),
166 dirent);
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000167 continue;
Janak Ramakrishnan4bf00182016-02-29 19:59:44 +0000168 }
Janak Ramakrishnan4bf00182016-02-29 19:59:44 +0000169
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000170 if (direntType == Dirent.Type.DIRECTORY) {
171 SkyKey keyToRequest = getSkyKeyForSubdir(fileName, glob, subdirPattern);
172 if (keyToRequest != null) {
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000173 subdirMap.put(keyToRequest, dirent);
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000174 }
175 } else if (globMatchesBareFile) {
176 sortedResultMap.put(dirent, glob.getSubdir().getRelative(fileName));
177 }
178 }
179
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000180 Map<SkyKey, SkyValue> subdirAndSymlinksResult =
181 env.getValues(Sets.union(subdirMap.keySet(), symlinkFileMap.keySet()));
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000182 if (env.valuesMissing()) {
183 return null;
184 }
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000185 Map<SkyKey, Dirent> symlinkSubdirMap = Maps.newHashMapWithExpectedSize(symlinkFileMap.size());
186 // Second pass: process the symlinks and subdirectories from the first pass, and maybe
187 // collect further SkyKeys if fully resolved symlink targets are themselves directories.
188 // Also process any known directories.
189 for (Map.Entry<SkyKey, SkyValue> lookedUpKeyAndValue : subdirAndSymlinksResult.entrySet()) {
190 if (symlinkFileMap.containsKey(lookedUpKeyAndValue.getKey())) {
191 FileValue symlinkFileValue = (FileValue) lookedUpKeyAndValue.getValue();
192 if (!symlinkFileValue.isSymlink()) {
193 throw new GlobFunctionException(
194 new InconsistentFilesystemException(
195 "readdir and stat disagree about whether "
196 + ((RootedPath) lookedUpKeyAndValue.getKey().argument()).asPath()
197 + " is a symlink."),
198 Transience.TRANSIENT);
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000199 }
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000200 if (!symlinkFileValue.exists()) {
201 continue;
202 }
203 Dirent dirent = symlinkFileMap.get(lookedUpKeyAndValue.getKey());
204 String fileName = dirent.getName();
205 if (symlinkFileValue.isDirectory()) {
206 SkyKey keyToRequest = getSkyKeyForSubdir(fileName, glob, subdirPattern);
207 if (keyToRequest != null) {
208 symlinkSubdirMap.put(keyToRequest, dirent);
209 }
210 } else if (globMatchesBareFile) {
211 sortedResultMap.put(dirent, glob.getSubdir().getRelative(fileName));
212 }
213 } else {
214 processSubdir(lookedUpKeyAndValue, subdirMap, glob, sortedResultMap);
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000215 }
216 }
217
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000218 Map<SkyKey, SkyValue> symlinkSubdirResult = env.getValues(symlinkSubdirMap.keySet());
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000219 if (env.valuesMissing()) {
220 return null;
221 }
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000222 // Third pass: do needed subdirectories of symlinked directories discovered during the second
223 // pass.
224 for (Map.Entry<SkyKey, SkyValue> lookedUpKeyAndValue : symlinkSubdirResult.entrySet()) {
225 processSubdir(lookedUpKeyAndValue, symlinkSubdirMap, glob, sortedResultMap);
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000226 }
227 for (Map.Entry<Dirent, Object> fileMatches : sortedResultMap.entrySet()) {
228 addToMatches(fileMatches.getValue(), matches);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100229 }
230 } else {
231 // Pattern does not contain globs, so a direct stat is enough.
232 String fileName = patternHead;
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000233 RootedPath fileRootedPath =
234 RootedPath.toRootedPath(glob.getPackageRoot(), dirPathFragment.getRelative(fileName));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100235 FileValue fileValue = (FileValue) env.getValue(FileValue.key(fileRootedPath));
236 if (fileValue == null) {
237 return null;
238 }
239 if (fileValue.exists()) {
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000240 if (fileValue.isDirectory()) {
241 SkyKey keyToRequest = getSkyKeyForSubdir(fileName, glob, patternTail);
242 if (keyToRequest != null) {
243 SkyValue valueRequested = env.getValue(keyToRequest);
244 if (env.valuesMissing()) {
245 return null;
246 }
247 Object fileMatches = getSubdirMatchesFromSkyValue(fileName, glob, valueRequested);
248 if (fileMatches != null) {
249 addToMatches(fileMatches, matches);
250 }
251 }
252 } else if (globMatchesBareFile) {
253 matches.add(glob.getSubdir().getRelative(fileName));
254 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100255 }
256 }
257
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000258 Preconditions.checkState(!env.valuesMissing(), skyKey);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100259
260 NestedSet<PathFragment> matchesBuilt = matches.build();
261 // Use the same value to represent that we did not match anything.
262 if (matchesBuilt.isEmpty()) {
263 return GlobValue.EMPTY;
264 }
265 return new GlobValue(matchesBuilt);
266 }
267
Janak Ramakrishnanbce6fc52016-08-18 16:46:09 +0000268 private static void processSubdir(
269 Map.Entry<SkyKey, SkyValue> keyAndValue,
270 Map<SkyKey, Dirent> subdirMap,
271 GlobDescriptor glob,
272 Map<Dirent, Object> sortedResultMap) {
273 Dirent dirent = Preconditions.checkNotNull(subdirMap.get(keyAndValue.getKey()), keyAndValue);
274 String fileName = dirent.getName();
275 Object dirMatches = getSubdirMatchesFromSkyValue(fileName, glob, keyAndValue.getValue());
276 if (dirMatches != null) {
277 sortedResultMap.put(dirent, dirMatches);
278 }
279 }
280
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000281 /** Returns true if the given pattern contains globs. */
282 private static boolean containsGlobs(String pattern) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100283 return pattern.contains("*") || pattern.contains("?");
284 }
285
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000286 @SuppressWarnings("unchecked") // cast to NestedSet<PathFragment>
287 private static void addToMatches(Object toAdd, NestedSetBuilder<PathFragment> matches) {
288 if (toAdd instanceof PathFragment) {
289 matches.add((PathFragment) toAdd);
290 } else {
291 matches.addTransitive((NestedSet<PathFragment>) toAdd);
292 }
293 }
294
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100295 /**
296 * Includes the given file/directory in the glob.
297 *
298 * <p>{@code fileName} must exist.
299 *
300 * <p>{@code isDirectory} must be true iff the file is a directory.
301 *
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000302 * <p>Returns a {@link SkyKey} for a value that is needed to compute the files that will be added
303 * to {@code matches}, or {@code null} if no additional value is needed. The returned value should
304 * be opaquely passed to {@link #getSubdirMatchesFromSkyValue}.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100305 */
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000306 private static SkyKey getSkyKeyForSubdir(
307 String fileName, GlobDescriptor glob, String subdirPattern) {
308 if (subdirPattern == null) {
309 if (glob.excludeDirs()) {
310 return null;
311 } else {
312 return PackageLookupValue.key(
313 PackageIdentifier.create(
314 glob.getPackageId().getRepository(),
315 glob.getPackageId()
316 .getPackageFragment()
317 .getRelative(glob.getSubdir())
318 .getRelative(fileName)));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100319 }
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000320 } else {
321 // There is some more pattern to match. Get the glob for the subdirectory. Note that this
322 // directory may also match directly in the case of a pattern that starts with "**", but that
323 // match will be found in the subdirectory glob.
324 return GlobValue.internalKey(
325 glob.getPackageId(),
326 glob.getPackageRoot(),
327 glob.getSubdir().getRelative(fileName),
328 subdirPattern,
329 glob.excludeDirs());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100330 }
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000331 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100332
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000333 /**
334 * Returns matches coming from the directory {@code fileName} if appropriate, either an individual
335 * file or a nested set of files.
336 *
337 * <p>{@code valueRequested} must be the SkyValue whose key was returned by
338 * {@link #getSkyKeyForSubdir} for these parameters.
339 */
340 @Nullable
341 private static Object getSubdirMatchesFromSkyValue(
342 String fileName,
343 GlobDescriptor glob,
344 SkyValue valueRequested) {
345 if (valueRequested instanceof GlobValue) {
346 return ((GlobValue) valueRequested).getMatches();
347 } else {
348 Preconditions.checkState(
349 valueRequested instanceof PackageLookupValue,
350 "%s is not a GlobValue or PackageLookupValue (%s %s)",
351 valueRequested,
352 fileName,
353 glob);
354 if (!((PackageLookupValue) valueRequested).packageExists()) {
355 return glob.getSubdir().getRelative(fileName);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100356 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100357 }
Janak Ramakrishnance372c32016-03-14 16:19:18 +0000358 return null;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100359 }
360
361 @Nullable
362 @Override
363 public String extractTag(SkyKey skyKey) {
364 return null;
365 }
366
367 /**
368 * Used to declare all the exception types that can be wrapped in the exception thrown by
369 * {@link GlobFunction#compute}.
370 */
371 private static final class GlobFunctionException extends SkyFunctionException {
372 public GlobFunctionException(InconsistentFilesystemException e, Transience transience) {
373 super(e, transience);
374 }
375 }
376}