blob: 1680bf0c914e21494f6f779cd3b3446191860b2f [file] [log] [blame]
/*
* Copyright 2012-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.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.model.BuildFileTree;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetException;
import com.facebook.buck.model.FilesystemBackedBuildFileTree;
import com.facebook.buck.parser.Parser;
import com.facebook.buck.parser.ParserConfig;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.util.Ansi;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
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.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Outputs targets that own a specified list of files.
*/
public class AuditOwnerCommand extends AbstractCommandRunner<AuditOwnerOptions> {
private static final String FILE_INDENT = " ";
private static final int BUILD_TARGET_ERROR = 13;
public AuditOwnerCommand(CommandRunnerParams params) {
super(params);
}
@VisibleForTesting
static final class OwnersReport {
final ImmutableSetMultimap<TargetNode<?>, Path> owners;
final ImmutableSet<Path> inputsWithNoOwners;
final ImmutableSet<String> nonExistentInputs;
final ImmutableSet<String> nonFileInputs;
public 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);
}
public static OwnersReport emptyReport() {
return new OwnersReport(
ImmutableSetMultimap.<TargetNode<?>, Path>of(),
Sets.<Path>newHashSet(),
Sets.<String>newHashSet(),
Sets.<String>newHashSet());
}
public 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));
}
}
@Override
AuditOwnerOptions createOptions(BuckConfig buckConfig) {
return new AuditOwnerOptions(buckConfig);
}
@Override
int runCommandWithOptionsInternal(AuditOwnerOptions options)
throws IOException, InterruptedException {
OwnersReport report = OwnersReport.emptyReport();
Map<Path, List<TargetNode<?>>> targetNodes = Maps.newHashMap();
ParserConfig parserConfig = new ParserConfig(options.getBuckConfig());
BuildFileTree buildFileTree = new FilesystemBackedBuildFileTree(
getProjectFilesystem(),
parserConfig.getBuildFileName());
for (Path filePath : options.getArgumentsAsPaths(getProjectFilesystem().getRootPath())) {
Optional<Path> basePath = buildFileTree.getBasePathOfAncestorTarget(filePath);
if (!basePath.isPresent()) {
report = report.updatedWith(
new OwnersReport(
ImmutableSetMultimap.<TargetNode<?>, Path>of(),
/* inputWithNoOwners */ ImmutableSet.of(filePath),
Sets.<String>newHashSet(),
Sets.<String>newHashSet()));
continue;
}
Path buckFile = basePath.get().resolve(parserConfig.getBuildFileName());
Preconditions.checkState(getProjectFilesystem().exists(buckFile));
// Get the target base name.
Path targetBasePath = MorePaths.relativize(
getProjectFilesystem().getRootPath().toAbsolutePath(),
buckFile.toAbsolutePath().getParent());
String targetBaseName = "//" + targetBasePath.toString();
// Parse buck files and load target nodes.
if (!targetNodes.containsKey(buckFile)) {
try {
Parser parser = getParser();
List<Map<String, Object>> buildFileTargets = parser.parseBuildFile(
buckFile,
parserConfig,
environment,
console,
getBuckEventBus());
for (Map<String, Object> buildFileTarget : buildFileTargets) {
if (!buildFileTarget.containsKey("name")) {
continue;
}
BuildTarget target = BuildTarget.builder(
targetBaseName,
(String) buildFileTarget.get("name")).build();
if (!targetNodes.containsKey(buckFile)) {
targetNodes.put(buckFile, Lists.<TargetNode<?>>newArrayList());
}
TargetNode<?> parsedTargetNode = parser.getTargetNode(target);
if (parsedTargetNode != null) {
targetNodes.get(buckFile).add(parsedTargetNode);
}
}
} catch (BuildFileParseException | BuildTargetException e) {
console.getStdErr().format("Could not parse build targets for %s", targetBaseName);
return BUILD_TARGET_ERROR;
}
}
for (TargetNode<?> targetNode : targetNodes.get(buckFile)) {
report = report.updatedWith(
generateOwnersReport(
targetNode,
ImmutableList.of(filePath.toString()),
options.isGuessForDeletedEnabled()));
}
}
printReport(options, report);
return 0;
}
@VisibleForTesting
OwnersReport generateOwnersReport(
TargetNode<?> targetNode,
Iterable<String> filePaths,
boolean guessForDeletedEnabled) {
// Process arguments assuming they are all relative file paths.
Set<Path> inputs = Sets.newHashSet();
Set<String> nonExistentInputs = Sets.newHashSet();
Set<String> nonFileInputs = Sets.newHashSet();
ProjectFilesystem projectFilesystem = getProjectFilesystem();
for (String filePath : filePaths) {
File file = projectFilesystem.getFileForRelativePath(filePath);
if (!file.exists()) {
nonExistentInputs.add(filePath);
} else if (!file.isFile()) {
nonFileInputs.add(filePath);
} else {
inputs.add(Paths.get(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 = new Predicate<Path>() {
@Override
public boolean apply(Path input) {
return !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);
}
}
// Try to guess owners for nonexistent files.
if (guessForDeletedEnabled) {
for (String nonExsitentInput : nonExistentInputs) {
owners.put(targetNode, new File(nonExsitentInput).toPath());
}
}
return new OwnersReport(owners, inputsWithNoOwners, nonExistentInputs, nonFileInputs);
}
private void printReport(AuditOwnerOptions options, OwnersReport report) throws IOException {
if (options.isFullReportEnabled()) {
printFullReport(report);
} else {
if (options.shouldGenerateJsonOutput()) {
printOwnersOnlyJsonReport(report);
} else {
printOwnersOnlyReport(report);
}
}
}
/**
* Print only targets which were identified as owners.
*/
private void printOwnersOnlyReport(OwnersReport report) {
Set<TargetNode<?>> sortedTargetNodes = report.owners.keySet();
for (TargetNode<?> targetNode : sortedTargetNodes) {
console.getStdOut().println(targetNode.getBuildTarget().getFullyQualifiedName());
}
}
/**
* Print only targets which were identified as owners in JSON.
*/
@VisibleForTesting
void printOwnersOnlyJsonReport(OwnersReport report) throws IOException {
final Multimap<String, String> output = TreeMultimap.create();
Set<TargetNode<?>> sortedTargetNodes = report.owners.keySet();
for (TargetNode<?> targetNode : sortedTargetNodes) {
Set<Path> files = report.owners.get(targetNode);
for (Path input : files) {
output.put(input.toString(), targetNode.getBuildTarget().getFullyQualifiedName());
}
}
getObjectMapper().writeValue(console.getStdOut(), output.asMap());
}
/**
* Print detailed report on all owners.
*/
private void printFullReport(OwnersReport report) {
PrintStream out = console.getStdOut();
Ansi ansi = console.getAnsi();
if (report.owners.isEmpty()) {
out.println(ansi.asErrorText("No owners found"));
} else {
out.println(ansi.asSuccessText("Owners:"));
for (TargetNode<?> targetNode : report.owners.keySet()) {
out.println(targetNode.getBuildTarget().getFullyQualifiedName());
Set<Path> files = report.owners.get(targetNode);
for (Path input : files) {
out.println(FILE_INDENT + input);
}
}
}
if (!report.inputsWithNoOwners.isEmpty()) {
out.println();
out.println(ansi.asErrorText("Files without owners:"));
for (Path input : report.inputsWithNoOwners) {
out.println(FILE_INDENT + input);
}
}
if (!report.nonExistentInputs.isEmpty()) {
out.println();
out.println(ansi.asErrorText("Non existent files:"));
for (String input : report.nonExistentInputs) {
out.println(FILE_INDENT + input);
}
}
if (!report.nonFileInputs.isEmpty()) {
out.println();
out.println(ansi.asErrorText("Non-file inputs:"));
for (String input : report.nonFileInputs) {
out.println(FILE_INDENT + input);
}
}
}
@Override
String getUsageIntro() {
return "prints targets that own specified files";
}
}