/*
* Copyright 2014-present Facebook, Inc.
*
* 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.facebook.buck.model;
import com.facebook.buck.io.PathOrGlobMatcher;
import com.facebook.buck.io.ProjectFilesystem;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.Optional;
/**
* Class to allow looking up parents and children of build files. E.g. for a directory structure
* that looks like:
*
* <pre>
* foo/BUCK
* foo/bar/baz/BUCK
* foo/bar/qux/BUCK
* </pre>
*
* <p>foo/BUCK is the parent of foo/bar/baz/BUCK and foo/bar/qux/BUCK.
*/
public class FilesystemBackedBuildFileTree extends BuildFileTree {
private final ProjectFilesystem projectFilesystem;
private final String buildFileName;
private final LoadingCache<Path, Boolean> pathExistenceCache;
/**
* Cache for the base path of a given path. This is useful as many files may share common
* ancestors before reaching a base path.
*/
private final LoadingCache<Path, Optional<Path>> basePathOfAncestorCache =
CacheBuilder.newBuilder()
.weakValues()
.build(
new CacheLoader<Path, Optional<Path>>() {
@Override
public Optional<Path> load(Path filePath) throws Exception {
Path parent = filePath.getParent();
if (parent == null) {
return checkProjectRoot();
}
// If filePath names a directory with a build file, filePath is a base path.
// If filePath or any of its parents are in ignoredPaths, we should keep looking.
if (isBasePath(parent)) {
return Optional.of(parent);
}
// If filePath isn't root, keep looking.
if (parent.equals(projectFilesystem.getRootPath())) {
return checkProjectRoot();
}
return basePathOfAncestorCache.get(parent);
}
});
public FilesystemBackedBuildFileTree(ProjectFilesystem projectFilesystem, String buildFileName) {
this.projectFilesystem = projectFilesystem;
this.buildFileName = buildFileName;
this.pathExistenceCache =
CacheBuilder.newBuilder()
.build(
new CacheLoader<Path, Boolean>() {
@Override
public Boolean load(Path key) throws Exception {
return FilesystemBackedBuildFileTree.this.projectFilesystem.isFile(key);
}
});
}
/** @return paths relative to BuildTarget that contain their own build files. */
@Override
public Collection<Path> getChildPaths(BuildTarget target) {
// Crawl the subdirectories of target's base path, looking for build files.
// When we find one, we can stop crawling anything under the directory it's in.
final ImmutableSet.Builder<Path> childPaths = ImmutableSet.builder();
final Path basePath = target.getBasePath();
final ImmutableSet<PathOrGlobMatcher> ignoredPaths = projectFilesystem.getIgnorePaths();
try {
projectFilesystem.walkRelativeFileTree(
basePath,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
for (PathOrGlobMatcher ignoredPath : ignoredPaths) {
if (ignoredPath.matches(dir)) {
return FileVisitResult.SKIP_SUBTREE;
}
}
if (dir.equals(basePath)) {
return FileVisitResult.CONTINUE;
}
Path buildFile = dir.resolve(buildFileName);
if (pathExistenceCache.getUnchecked(buildFile)) {
childPaths.add(basePath.relativize(dir));
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
return childPaths.build();
}
/**
* Returns the base path for a given path. The base path is the nearest directory at or above
* filePath that contains a build file. If no base directory is found, returns an empty path.
*/
@Override
public Optional<Path> getBasePathOfAncestorTarget(Path filePath) {
// Since the initial file being passed in tends to be a file instead of a directory, we can skip
// this initial check of filePath/BUCK if that's the case.
if (!projectFilesystem.isFile(filePath) && isBasePath(filePath)) {
return Optional.of(filePath);
}
return basePathOfAncestorCache.getUnchecked(filePath);
}
/** Returns whether the given path is a directory containing a buck file, i.e. a base path. */
private boolean isBasePath(Path filePath) {
return pathExistenceCache.getUnchecked(filePath.resolve(buildFileName))
&& !isBuckOutput(filePath)
&& !projectFilesystem.isIgnored(filePath);
}
private Optional<Path> checkProjectRoot() {
// No build file found in any directory, check the project root
Path rootBuckFile = Paths.get(buildFileName);
if (pathExistenceCache.getUnchecked(rootBuckFile)
&& !projectFilesystem.isIgnored(rootBuckFile)) {
return Optional.of(Paths.get(""));
}
// filePath does not fall under any build file
return Optional.empty();
}
/**
* Assume that any directory called "buck-out", "buck-out/cache" or ".buckd" can be ignored. Not
* the world's best heuristic, but it works in every existing code base we have access to.
*/
private boolean isBuckOutput(Path path) {
Path sameFsBuckOut =
path.getFileSystem().getPath(projectFilesystem.getBuckPaths().getBuckOut().toString());
Path sameFsBuckCache = projectFilesystem.getBuckPaths().getCacheDir();
for (Path segment : path) {
if (sameFsBuckOut.equals(segment) || sameFsBuckCache.equals(segment)) {
return true;
}
}
return false;
}
}