/*
* Copyright 2016-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.cli;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.model.BuildFileTree;
import com.facebook.buck.parser.Parser;
import com.facebook.buck.rules.Cell;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.util.Console;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class OwnersReport {
final ImmutableSetMultimap<TargetNode<?, ?>, Path> owners;
final ImmutableSet<Path> inputsWithNoOwners;
final ImmutableSet<String> nonExistentInputs;
final ImmutableSet<String> nonFileInputs;
OwnersReport(
SetMultimap<TargetNode<?, ?>, Path> owners,
Set<Path> inputsWithNoOwners,
Set<String> nonExistentInputs,
Set<String> nonFileInputs) {
this.owners = ImmutableSetMultimap.copyOf(owners);
this.inputsWithNoOwners = ImmutableSet.copyOf(inputsWithNoOwners);
this.nonExistentInputs = ImmutableSet.copyOf(nonExistentInputs);
this.nonFileInputs = ImmutableSet.copyOf(nonFileInputs);
}
static OwnersReport emptyReport() {
return new OwnersReport(
ImmutableSetMultimap.of(), Sets.newHashSet(), Sets.newHashSet(), Sets.newHashSet());
}
OwnersReport updatedWith(OwnersReport other) {
SetMultimap<TargetNode<?, ?>, Path> updatedOwners = TreeMultimap.create(owners);
updatedOwners.putAll(other.owners);
return new OwnersReport(
updatedOwners,
Sets.intersection(inputsWithNoOwners, other.inputsWithNoOwners),
Sets.union(nonExistentInputs, other.nonExistentInputs),
Sets.union(nonFileInputs, other.nonFileInputs));
}
/** @return relative paths under the project root */
static Iterable<Path> getArgumentsAsPaths(Path projectRoot, Iterable<String> args)
throws IOException {
return PathArguments.getCanonicalFilesUnderProjectRoot(projectRoot, args)
.relativePathsUnderProjectRoot;
}
@VisibleForTesting
static OwnersReport generateOwnersReport(
Cell rootCell, TargetNode<?, ?> targetNode, Iterable<String> filePaths) {
// Process arguments assuming they are all relative file paths.
Set<Path> inputs = Sets.newHashSet();
Set<String> nonExistentInputs = Sets.newHashSet();
Set<String> nonFileInputs = Sets.newHashSet();
for (String filePath : filePaths) {
Path file = rootCell.getFilesystem().getPathForRelativePath(filePath);
if (!Files.exists(file)) {
nonExistentInputs.add(filePath);
} else if (!Files.isRegularFile(file)) {
nonFileInputs.add(filePath);
} else {
inputs.add(rootCell.getFilesystem().getPath(filePath));
}
}
// Try to find owners for each valid and existing file.
Set<Path> inputsWithNoOwners = Sets.newHashSet(inputs);
SetMultimap<TargetNode<?, ?>, Path> owners = TreeMultimap.create();
for (final Path commandInput : inputs) {
Predicate<Path> startsWith =
input -> !commandInput.equals(input) && commandInput.startsWith(input);
Set<Path> ruleInputs = targetNode.getInputs();
if (ruleInputs.contains(commandInput)
|| FluentIterable.from(ruleInputs).anyMatch(startsWith)) {
inputsWithNoOwners.remove(commandInput);
owners.put(targetNode, commandInput);
}
}
return new OwnersReport(owners, inputsWithNoOwners, nonExistentInputs, nonFileInputs);
}
static Builder builder(Cell rootCell, Parser parser, BuckEventBus eventBus, Console console) {
return new Builder(rootCell, parser, eventBus, console);
}
static final class Builder {
private final Cell rootCell;
private final Parser parser;
private final BuckEventBus eventBus;
private final Console console;
private Builder(Cell rootCell, Parser parser, BuckEventBus eventBus, Console console) {
this.rootCell = rootCell;
this.parser = parser;
this.eventBus = eventBus;
this.console = console;
}
OwnersReport build(
BuildFileTree buildFileTree, ListeningExecutorService executor, Iterable<String> arguments)
throws IOException, BuildFileParseException {
ProjectFilesystem cellFilesystem = rootCell.getFilesystem();
final Path rootPath = cellFilesystem.getRootPath();
Preconditions.checkState(rootPath.isAbsolute());
Map<Path, ImmutableSet<TargetNode<?, ?>>> targetNodes = new HashMap<>();
OwnersReport report = emptyReport();
for (Path filePath : getArgumentsAsPaths(rootPath, arguments)) {
Optional<Path> basePath = buildFileTree.getBasePathOfAncestorTarget(filePath);
if (!basePath.isPresent()) {
report =
report.updatedWith(
new OwnersReport(
ImmutableSetMultimap.of(),
/* inputWithNoOwners */ ImmutableSet.of(filePath),
Sets.newHashSet(),
Sets.newHashSet()));
continue;
}
Path buckFile = cellFilesystem.resolve(basePath.get()).resolve(rootCell.getBuildFileName());
Preconditions.checkState(cellFilesystem.exists(buckFile));
// Parse buck files and load target nodes.
if (!targetNodes.containsKey(buckFile)) {
try {
targetNodes.put(
buckFile,
parser.getAllTargetNodes(
eventBus, rootCell, /* enable profiling */ false, executor, buckFile));
} catch (BuildFileParseException e) {
Path targetBasePath = MorePaths.relativize(rootPath, rootPath.resolve(basePath.get()));
String targetBaseName = "//" + MorePaths.pathWithUnixSeparators(targetBasePath);
console
.getStdErr()
.format(
"Could not parse build targets for %s: %s%n",
targetBaseName, e.getHumanReadableErrorMessage());
throw e;
}
}
for (TargetNode<?, ?> targetNode : targetNodes.get(buckFile)) {
report =
report.updatedWith(
generateOwnersReport(
rootCell, targetNode, ImmutableList.of(filePath.toString())));
}
}
return report;
}
}
}