/* * 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.event.ConsoleEvent; import com.facebook.buck.graph.AbstractBreadthFirstTraversal; import com.facebook.buck.graph.AcyclicDepthFirstPostOrderTraversal; import com.facebook.buck.graph.AcyclicDepthFirstPostOrderTraversal.CycleException; import com.facebook.buck.hashing.FileHashLoader; import com.facebook.buck.hashing.FilePathHashLoader; import com.facebook.buck.io.BuckPaths; import com.facebook.buck.json.BuildFileParseException; import com.facebook.buck.log.Logger; import com.facebook.buck.model.BuildFileTree; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargetException; import com.facebook.buck.model.InMemoryBuildFileTree; import com.facebook.buck.parser.BuildFileSpec; import com.facebook.buck.parser.ParserConfig; import com.facebook.buck.parser.TargetNodePredicateSpec; import com.facebook.buck.rules.ActionGraph; import com.facebook.buck.rules.ActionGraphAndResolver; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.BuildRuleType; import com.facebook.buck.rules.Description; import com.facebook.buck.rules.HasTests; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.TargetGraphAndBuildTargets; import com.facebook.buck.rules.TargetGraphAndTargetNodes; import com.facebook.buck.rules.TargetGraphAndTargets; import com.facebook.buck.rules.TargetGraphHashing; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.rules.TargetNodes; import com.facebook.buck.rules.keys.DefaultRuleKeyFactory; import com.facebook.buck.rules.keys.RuleKeyFieldLoader; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.MoreExceptions; import com.facebook.buck.util.ObjectMappers; import com.facebook.buck.util.PatternsMatcher; import com.facebook.buck.util.immutables.BuckStyleImmutable; import com.facebook.buck.versions.VersionException; import com.facebook.infer.annotation.SuppressFieldNotInitialized; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Supplier; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.hash.HashCode; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.IOException; import java.io.PrintStream; import java.io.StringWriter; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.SortedMap; import org.immutables.value.Value; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; public class TargetsCommand extends AbstractCommand { private static final Logger LOG = Logger.get(TargetsCommand.class); // TODO(mbolin): Use org.kohsuke.args4j.spi.PathOptionHandler. Currently, we resolve paths // manually, which is likely the path to madness. @Option( name = "--referenced-file", aliases = {"--referenced_file"}, usage = "The referenced file list, --referenced-file file1 file2 ... fileN --other_option", handler = StringSetOptionHandler.class ) @SuppressFieldNotInitialized private Supplier<ImmutableSet<String>> referencedFiles; @Option( name = "--detect-test-changes", usage = "Modifies the --referenced-file and --show-target-hash flags to pretend that " + "targets depend on their tests (experimental)" ) private boolean isDetectTestChanges; @Option( name = "--type", usage = "The types of target to filter by, --type type1 type2 ... typeN --other_option", handler = StringSetOptionHandler.class ) @SuppressFieldNotInitialized private Supplier<ImmutableSet<String>> types; @Option(name = "--json", usage = "Print JSON representation of each target") private boolean json; @Option(name = "--print0", usage = "Delimit targets using the ASCII NUL character.") private boolean print0; @Option( name = "--resolve-alias", aliases = {"--resolvealias"}, usage = "Print the fully-qualified build target for the specified alias[es]" ) private boolean isResolveAlias; @Option( name = "--show-cell-path", aliases = {"--show_cell_path"}, usage = "Print the absolute path of the cell for each rule after the rule name." ) private boolean isShowCellPath; @Option( name = "--show-output", aliases = {"--show_output"}, usage = "Print the path to the output, relative to the cell path, for each rule after the " + "rule name. Use '--show-full-output' to obtain the full absolute path." ) private boolean isShowOutput; @Option( name = "--show-full-output", aliases = {"--show_full_output"}, usage = "Print the absolute path to the output, for each rule after the rule name." ) private boolean isShowFullOutput; @Option( name = "--show-rulekey", aliases = {"--show_rulekey"}, usage = "Print the RuleKey of each rule after the rule name." ) private boolean isShowRuleKey; @Option( name = "--show-target-hash", usage = "Print a stable hash of each target after the target name." ) private boolean isShowTargetHash; private enum TargetHashFileMode { PATHS_AND_CONTENTS, PATHS_ONLY, } @Option( name = "--target-hash-file-mode", usage = "Modifies computation of target hashes. If set to PATHS_AND_CONTENTS (the " + "default), the contents of all files referenced from the targets will be used to " + "compute the target hash. If set to PATHS_ONLY, only files' paths contribute to the " + "hash. See also --target-hash-modified-paths." ) private TargetHashFileMode targetHashFileMode = TargetHashFileMode.PATHS_AND_CONTENTS; @Option( name = "--target-hash-modified-paths", usage = "Modifies computation of target hashes. Only effective when " + "--target-hash-file-mode is set to PATHS_ONLY. If a target or its dependencies " + "reference a file from this set, the target's hash will be different than if this " + "option was omitted. Otherwise, the target's hash will be the same as if this option " + "was omitted.", handler = StringSetOptionHandler.class ) @SuppressFieldNotInitialized private Supplier<ImmutableSet<String>> targetHashModifiedPaths; @Option( name = "--output-attributes", usage = "List of attributes to output, --output-attributes attr1 att2 ... attrN. " + "Attributes can be regular expressions. ", handler = StringSetOptionHandler.class ) @SuppressFieldNotInitialized private Supplier<ImmutableSet<String>> outputAttributes; @Argument private List<String> arguments = new ArrayList<>(); public List<String> getArguments() { return arguments; } public ImmutableSet<String> getTypes() { return types.get(); } public PathArguments.ReferencedFiles getReferencedFiles(Path projectRoot) throws IOException { return PathArguments.getCanonicalFilesUnderProjectRoot(projectRoot, referencedFiles.get()); } /** @return {@code true} if {@code --detect-test-changes} was specified. */ public boolean isDetectTestChanges() { return isDetectTestChanges; } public boolean getPrintJson() { return json; } public boolean isPrint0() { return print0; } /** @return {@code true} if {@code --resolve-alias} was specified. */ public boolean isResolveAlias() { return isResolveAlias; } /** @return {@code true} if {@code --show-cell-path} was specified. */ public boolean isShowCellPath() { return isShowCellPath; } /** @return {@code true} if {@code --show-output} was specified. */ public boolean isShowOutput() { return isShowOutput; } /** @return {@code true} if {@code --show-output} was specified. */ public boolean isShowFullOutput() { return isShowFullOutput; } /** @return {@code true} if {@code --show-rulekey} was specified. */ public boolean isShowRuleKey() { return isShowRuleKey; } /** @return {@code true} if {@code --show-targethash} was specified. */ public boolean isShowTargetHash() { return isShowTargetHash; } /** @return mode passed to {@code --target-hash-file-mode}. */ public TargetHashFileMode getTargetHashFileMode() { return targetHashFileMode; } /** @return attributes pass in {@code --output-attributes}. */ public ImmutableSet<String> getOutputAttributes() { return outputAttributes.get(); } /** * Determines if the results should be in JSON format. This is true either when explicitly * required by the {@code --json} argument or when there is at least one item in the {@code * --output-attributes} arguments. * * <p>The {@code --output--attributes} arguments implicitly enables JSON format because there is * currently no way to output attributes in non-JSON format. Also, it keeps this command * consistent with the query command. */ public boolean shouldUseJsonFormat() { return getPrintJson() || !getOutputAttributes().isEmpty(); } /** * @return set of paths passed to {@code --assume-modified-files} if either {@code * --assume-modified-files} or {@code --assume-no-modified-files} is used, absent otherwise. */ public ImmutableSet<Path> getTargetHashModifiedPaths() { return targetHashModifiedPaths .get() .stream() .map(Paths::get) .collect(MoreCollectors.toImmutableSet()); } @Override public int runWithoutHelp(CommandRunnerParams params) throws IOException, InterruptedException { if (isShowRuleKey() && isShowTargetHash()) { throw new HumanReadableException("Cannot show rule key and target hash at the same time."); } try (CommandThreadManager pool = new CommandThreadManager("Targets", getConcurrencyLimit(params.getBuckConfig()))) { ListeningExecutorService executor = pool.getExecutor(); // Exit early if --resolve-alias is passed in: no need to parse any build files. if (isResolveAlias()) { return ResolveAliasHelper.resolveAlias( params, executor, getEnableParserProfiling(), getArguments()); } return runWithExecutor(params, executor); } catch (BuildTargetException | BuildFileParseException | CycleException | VersionException e) { params .getBuckEventBus() .post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e))); return 1; } } private int runWithExecutor(CommandRunnerParams params, ListeningExecutorService executor) throws IOException, InterruptedException, BuildFileParseException, BuildTargetException, CycleException, VersionException { Optional<ImmutableSet<Class<? extends Description<?>>>> descriptionClasses = getDescriptionClassFromParams(params); if (!descriptionClasses.isPresent()) { return 1; } if (isShowCellPath() || isShowOutput() || isShowFullOutput() || isShowRuleKey() || isShowTargetHash()) { ImmutableMap<BuildTarget, ShowOptions> showRulesResult; TargetGraphAndBuildTargets targetGraphAndBuildTargetsForShowRules = buildTargetGraphAndTargetsForShowRules(params, executor, descriptionClasses); boolean useVersioning = isShowRuleKey() || isShowOutput() || isShowFullOutput() ? params.getBuckConfig().getBuildVersions() : params.getBuckConfig().getTargetsVersions(); targetGraphAndBuildTargetsForShowRules = useVersioning ? toVersionedTargetGraph(params, targetGraphAndBuildTargetsForShowRules) : targetGraphAndBuildTargetsForShowRules; showRulesResult = computeShowRules( params, executor, TargetGraphAndTargetNodes.fromTargetGraphAndBuildTargets( targetGraphAndBuildTargetsForShowRules)); if (shouldUseJsonFormat()) { Iterable<TargetNode<?, ?>> matchingNodes = targetGraphAndBuildTargetsForShowRules .getTargetGraph() .getAll(targetGraphAndBuildTargetsForShowRules.getBuildTargets()); printJsonForTargets( params, executor, matchingNodes, showRulesResult, getOutputAttributes()); } else { printShowRules(showRulesResult, params); } return 0; } return printResults( params, executor, getMatchingNodes(params, buildTargetGraphAndTargets(params, executor), descriptionClasses)); } private TargetGraphAndBuildTargets buildTargetGraphAndTargetsForShowRules( CommandRunnerParams params, ListeningExecutorService executor, Optional<ImmutableSet<Class<? extends Description<?>>>> descriptionClasses) throws InterruptedException, BuildFileParseException, BuildTargetException, IOException { if (getArguments().isEmpty()) { ParserConfig parserConfig = params.getBuckConfig().getView(ParserConfig.class); TargetGraphAndBuildTargets completeTargetGraphAndBuildTargets = params .getParser() .buildTargetGraphForTargetNodeSpecs( params.getBuckEventBus(), params.getCell(), getEnableParserProfiling(), executor, ImmutableList.of( TargetNodePredicateSpec.of( x -> true, BuildFileSpec.fromRecursivePath( Paths.get(""), params.getCell().getRoot()))), parserConfig.getDefaultFlavorsMode()); SortedMap<String, TargetNode<?, ?>> matchingNodes = getMatchingNodes(params, completeTargetGraphAndBuildTargets, descriptionClasses); Iterable<BuildTarget> buildTargets = FluentIterable.from(matchingNodes.values()).transform(TargetNode::getBuildTarget); return TargetGraphAndBuildTargets.builder() .setTargetGraph(completeTargetGraphAndBuildTargets.getTargetGraph()) .setBuildTargets(buildTargets) .build(); } else { return params .getParser() .buildTargetGraphForTargetNodeSpecs( params.getBuckEventBus(), params.getCell(), getEnableParserProfiling(), executor, parseArgumentsAsTargetNodeSpecs(params.getBuckConfig(), getArguments())); } } /** Print out matching targets in alphabetical order. */ private int printResults( CommandRunnerParams params, ListeningExecutorService executor, SortedMap<String, TargetNode<?, ?>> matchingNodes) throws BuildFileParseException { if (shouldUseJsonFormat()) { printJsonForTargets( params, executor, matchingNodes.values(), ImmutableMap.of(), getOutputAttributes()); } else if (isPrint0()) { printNullDelimitedTargets(matchingNodes.keySet(), params.getConsole().getStdOut()); } else { for (String target : matchingNodes.keySet()) { params.getConsole().getStdOut().println(target); } } return 0; } private SortedMap<String, TargetNode<?, ?>> getMatchingNodes( CommandRunnerParams params, TargetGraphAndBuildTargets targetGraphAndBuildTargets, Optional<ImmutableSet<Class<? extends Description<?>>>> descriptionClasses) throws IOException { PathArguments.ReferencedFiles referencedFiles = getReferencedFiles(params.getCell().getFilesystem().getRootPath()); SortedMap<String, TargetNode<?, ?>> matchingNodes; // If all of the referenced files are paths outside the project root, then print nothing. if (!referencedFiles.absolutePathsOutsideProjectRootOrNonExistingPaths.isEmpty() && referencedFiles.relativePathsUnderProjectRoot.isEmpty()) { matchingNodes = ImmutableSortedMap.of(); } else { ParserConfig parserConfig = params.getBuckConfig().getView(ParserConfig.class); ImmutableSet<BuildTarget> matchingBuildTargets = targetGraphAndBuildTargets.getBuildTargets(); matchingNodes = getMatchingNodes( targetGraphAndBuildTargets.getTargetGraph(), referencedFiles.relativePathsUnderProjectRoot.isEmpty() ? Optional.empty() : Optional.of(referencedFiles.relativePathsUnderProjectRoot), matchingBuildTargets.isEmpty() ? Optional.empty() : Optional.of(matchingBuildTargets), descriptionClasses.get().isEmpty() ? Optional.empty() : descriptionClasses, isDetectTestChanges(), parserConfig.getBuildFileName()); } return matchingNodes; } private TargetGraphAndBuildTargets buildTargetGraphAndTargets( CommandRunnerParams params, ListeningExecutorService executor) throws IOException, InterruptedException, BuildFileParseException, BuildTargetException, VersionException { ParserConfig parserConfig = params.getBuckConfig().getView(ParserConfig.class); // Parse the entire action graph, or (if targets are specified), only the specified targets and // their dependencies. If we're detecting test changes we need the whole graph as tests are not // dependencies. TargetGraphAndBuildTargets targetGraphAndBuildTargets; if (getArguments().isEmpty() || isDetectTestChanges()) { targetGraphAndBuildTargets = TargetGraphAndBuildTargets.builder() .setBuildTargets(ImmutableSet.of()) .setTargetGraph( params .getParser() .buildTargetGraphForTargetNodeSpecs( params.getBuckEventBus(), params.getCell(), getEnableParserProfiling(), executor, ImmutableList.of( TargetNodePredicateSpec.of( x -> true, BuildFileSpec.fromRecursivePath( Paths.get(""), params.getCell().getRoot()))), parserConfig.getDefaultFlavorsMode()) .getTargetGraph()) .build(); } else { targetGraphAndBuildTargets = params .getParser() .buildTargetGraphForTargetNodeSpecs( params.getBuckEventBus(), params.getCell(), getEnableParserProfiling(), executor, parseArgumentsAsTargetNodeSpecs(params.getBuckConfig(), getArguments()), parserConfig.getDefaultFlavorsMode()); } return params.getBuckConfig().getTargetsVersions() ? toVersionedTargetGraph(params, targetGraphAndBuildTargets) : targetGraphAndBuildTargets; } @SuppressWarnings("unchecked") private Optional<ImmutableSet<Class<? extends Description<?>>>> getDescriptionClassFromParams( CommandRunnerParams params) { ImmutableSet<String> types = getTypes(); ImmutableSet.Builder<Class<? extends Description<?>>> descriptionClassesBuilder = ImmutableSet.builder(); for (String name : types) { try { BuildRuleType type = params.getCell().getBuildRuleType(name); Description<?> description = params.getCell().getDescription(type); descriptionClassesBuilder.add((Class<? extends Description<?>>) description.getClass()); } catch (IllegalArgumentException e) { params.getBuckEventBus().post(ConsoleEvent.severe("Invalid build rule type: " + name)); return Optional.empty(); } } return Optional.of(descriptionClassesBuilder.build()); } private void printShowRules( Map<BuildTarget, ShowOptions> showRulesResult, CommandRunnerParams params) { for (Entry<BuildTarget, ShowOptions> entry : ImmutableSortedMap.copyOf(showRulesResult).entrySet()) { ImmutableList.Builder<String> builder = ImmutableList.builder(); builder.add(entry.getKey().getFullyQualifiedName()); ShowOptions showOptions = entry.getValue(); if (showOptions.getRuleKey().isPresent()) { builder.add(showOptions.getRuleKey().get()); } if (isShowCellPath()) { builder.add(entry.getKey().getCellPath().toString()); } if (showOptions.getOutputPath().isPresent()) { builder.add(showOptions.getOutputPath().get()); } if (showOptions.getTargetHash().isPresent()) { builder.add(showOptions.getTargetHash().get()); } params.getConsole().getStdOut().println(Joiner.on(' ').join(builder.build())); } } @Override public boolean isReadOnly() { return true; } /** * @param graph Graph used to resolve dependencies between targets and find all build files. * @param referencedFiles If present, the result will be limited to the nodes that transitively * depend on at least one of those. * @param matchingBuildTargets If present, the result will be limited to the specified targets. * @param buildRuleTypes If present, the result will be limited to targets with the specified * types. * @param detectTestChanges If true, tests are considered to be dependencies of the targets they * are testing. * @return A map of target names to target nodes. */ @VisibleForTesting ImmutableSortedMap<String, TargetNode<?, ?>> getMatchingNodes( TargetGraph graph, Optional<ImmutableSet<Path>> referencedFiles, final Optional<ImmutableSet<BuildTarget>> matchingBuildTargets, final Optional<ImmutableSet<Class<? extends Description<?>>>> descriptionClasses, boolean detectTestChanges, String buildFileName) { ImmutableSet<TargetNode<?, ?>> directOwners; if (referencedFiles.isPresent()) { BuildFileTree buildFileTree = new InMemoryBuildFileTree( graph .getNodes() .stream() .map(TargetNode::getBuildTarget) .collect(MoreCollectors.toImmutableSet())); directOwners = FluentIterable.from(graph.getNodes()) .filter(new DirectOwnerPredicate(buildFileTree, referencedFiles.get(), buildFileName)) .toSet(); } else { directOwners = graph.getNodes(); } Iterable<TargetNode<?, ?>> selectedReferrers = FluentIterable.from(getDependentNodes(graph, directOwners, detectTestChanges)) .filter( targetNode -> { if (matchingBuildTargets.isPresent() && !matchingBuildTargets.get().contains(targetNode.getBuildTarget())) { return false; } if (descriptionClasses.isPresent() && !descriptionClasses .get() .contains(targetNode.getDescription().getClass())) { return false; } return true; }); ImmutableSortedMap.Builder<String, TargetNode<?, ?>> matchingNodesBuilder = ImmutableSortedMap.naturalOrder(); for (TargetNode<?, ?> targetNode : selectedReferrers) { matchingNodesBuilder.put(targetNode.getBuildTarget().getFullyQualifiedName(), targetNode); } return matchingNodesBuilder.build(); } /** * @param graph A graph used to resolve dependencies between targets. * @param nodes A set of nodes. * @param detectTestChanges If true, tests are considered to be dependencies of the targets they * are testing. * @return A set of all nodes that transitively depend on {@code nodes} (a superset of {@code * nodes}). */ private static ImmutableSet<TargetNode<?, ?>> getDependentNodes( final TargetGraph graph, ImmutableSet<TargetNode<?, ?>> nodes, boolean detectTestChanges) { ImmutableMultimap.Builder<TargetNode<?, ?>, TargetNode<?, ?>> extraEdgesBuilder = ImmutableMultimap.builder(); if (detectTestChanges) { for (TargetNode<?, ?> node : graph.getNodes()) { if (node.getConstructorArg() instanceof HasTests) { ImmutableSortedSet<BuildTarget> tests = ((HasTests) node.getConstructorArg()).getTests(); for (BuildTarget testTarget : tests) { Optional<TargetNode<?, ?>> testNode = graph.getOptional(testTarget); if (!testNode.isPresent()) { throw new HumanReadableException( "'%s' (test of '%s') is not in the target graph.", testTarget, node); } extraEdgesBuilder.put(testNode.get(), node); } } } } final ImmutableMultimap<TargetNode<?, ?>, TargetNode<?, ?>> extraEdges = extraEdgesBuilder.build(); final ImmutableSet.Builder<TargetNode<?, ?>> builder = ImmutableSet.builder(); AbstractBreadthFirstTraversal<TargetNode<?, ?>> traversal = new AbstractBreadthFirstTraversal<TargetNode<?, ?>>(nodes) { @Override public ImmutableSet<TargetNode<?, ?>> visit(TargetNode<?, ?> targetNode) { builder.add(targetNode); return FluentIterable.from(graph.getIncomingNodesFor(targetNode)) .append(extraEdges.get(targetNode)) .toSet(); } }; traversal.start(); return builder.build(); } @Override public String getShortDescription() { return "prints the list of buildable targets"; } @VisibleForTesting void printJsonForTargets( CommandRunnerParams params, ListeningExecutorService executor, Iterable<TargetNode<?, ?>> targetNodes, ImmutableMap<BuildTarget, ShowOptions> showRulesResult, ImmutableSet<String> outputAttributes) throws BuildFileParseException { PatternsMatcher attributesPatternsMatcher = new PatternsMatcher(outputAttributes); // Print the JSON representation of the build node for the specified target(s). params.getConsole().getStdOut().println("["); Iterator<TargetNode<?, ?>> targetNodeIterator = targetNodes.iterator(); while (targetNodeIterator.hasNext()) { TargetNode<?, ?> targetNode = targetNodeIterator.next(); Map<String, Object> sortedTargetRule; sortedTargetRule = params .getParser() .getRawTargetNode( params.getBuckEventBus(), params.getCell(), getEnableParserProfiling(), executor, targetNode); if (sortedTargetRule == null) { params .getConsole() .printErrorText( "unable to find rule for target " + targetNode.getBuildTarget().getFullyQualifiedName()); continue; } sortedTargetRule = attributesPatternsMatcher.filterMatchingMapKeys(sortedTargetRule); ShowOptions showOptions = showRulesResult.get(targetNode.getBuildTarget()); if (showOptions != null) { putIfValuePresentAndMatches( ShowOptionsName.RULE_KEY.getName(), showOptions.getRuleKey(), sortedTargetRule, attributesPatternsMatcher); putIfValuePresentAndMatches( ShowOptionsName.OUTPUT_PATH.getName(), showOptions.getOutputPath(), sortedTargetRule, attributesPatternsMatcher); putIfValuePresentAndMatches( ShowOptionsName.TARGET_HASH.getName(), showOptions.getTargetHash(), sortedTargetRule, attributesPatternsMatcher); } String fullyQualifiedNameAttribute = "fully_qualified_name"; if (attributesPatternsMatcher.matches(fullyQualifiedNameAttribute)) { sortedTargetRule.put( fullyQualifiedNameAttribute, targetNode.getBuildTarget().getFullyQualifiedName()); } String cellPathAttribute = "buck.cell_path"; if (isShowCellPath() && attributesPatternsMatcher.matches(cellPathAttribute)) { sortedTargetRule.put(cellPathAttribute, targetNode.getBuildTarget().getCellPath()); } // Print the build rule information as JSON. StringWriter stringWriter = new StringWriter(); try { ObjectMappers.WRITER.withDefaultPrettyPrinter().writeValue(stringWriter, sortedTargetRule); } catch (IOException e) { // Shouldn't be possible while writing to a StringWriter... throw new RuntimeException(e); } String output = stringWriter.getBuffer().toString(); if (targetNodeIterator.hasNext()) { output += ","; } params.getConsole().getStdOut().println(output); } params.getConsole().getStdOut().println("]"); } private void putIfValuePresentAndMatches( String key, Optional<String> value, Map<String, Object> targetMap, PatternsMatcher patternsMatcher) { if (value.isPresent() && patternsMatcher.matches(key)) { targetMap.put(key, value.get()); } } @VisibleForTesting static void printNullDelimitedTargets(Iterable<String> targets, PrintStream printStream) { for (String target : targets) { printStream.print(target + '\0'); } } /** * Assumes at least one target is specified. Computes each of the specified targets, followed by * the rule key, output path, and/or target hash, depending on what flags are passed in. * * @return An immutable map consisting of result of show options for to each target rule */ private ImmutableMap<BuildTarget, ShowOptions> computeShowRules( CommandRunnerParams params, ListeningExecutorService executor, TargetGraphAndTargetNodes targetGraphAndTargetNodes) throws IOException, InterruptedException, BuildFileParseException, BuildTargetException, CycleException { Map<BuildTarget, ShowOptions.Builder> showOptionBuilderMap = new HashMap<>(); if (isShowTargetHash()) { computeShowTargetHash(params, executor, targetGraphAndTargetNodes, showOptionBuilderMap); } // We only need the action graph if we're showing the output or the keys, and the // RuleKeyFactory if we're showing the keys. Optional<ActionGraph> actionGraph = Optional.empty(); Optional<BuildRuleResolver> buildRuleResolver = Optional.empty(); Optional<DefaultRuleKeyFactory> ruleKeyFactory = Optional.empty(); if (isShowRuleKey() || isShowOutput() || isShowFullOutput()) { ActionGraphAndResolver result = params .getActionGraphCache() .getActionGraph( params.getBuckEventBus(), params.getBuckConfig().isActionGraphCheckingEnabled(), params.getBuckConfig().isSkipActionGraphCache(), targetGraphAndTargetNodes.getTargetGraph(), params.getBuckConfig().getKeySeed()); actionGraph = Optional.of(result.getActionGraph()); buildRuleResolver = Optional.of(result.getResolver()); if (isShowRuleKey()) { SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(result.getResolver()); ruleKeyFactory = Optional.of( new DefaultRuleKeyFactory( new RuleKeyFieldLoader(params.getBuckConfig().getKeySeed()), params.getFileHashCache(), new SourcePathResolver(ruleFinder), ruleFinder)); } } for (TargetNode<?, ?> targetNode : targetGraphAndTargetNodes.getTargetNodes()) { ShowOptions.Builder showOptionsBuilder = getShowOptionBuilder(showOptionBuilderMap, targetNode.getBuildTarget()); Preconditions.checkNotNull(showOptionsBuilder); if (actionGraph.isPresent()) { BuildRule rule = buildRuleResolver.get().requireRule(targetNode.getBuildTarget()); if (isShowRuleKey()) { showOptionsBuilder.setRuleKey(ruleKeyFactory.get().build(rule).toString()); } if (isShowOutput() || isShowFullOutput()) { Optional<Path> outputPath = getUserFacingOutputPath( new SourcePathResolver(new SourcePathRuleFinder(buildRuleResolver.get())), rule, isShowFullOutput(), params.getBuckConfig().getBuckOutCompatLink()); if (outputPath.isPresent()) { showOptionsBuilder.setOutputPath(outputPath.get().toString()); } } } } ImmutableMap.Builder<BuildTarget, ShowOptions> builder = new ImmutableMap.Builder<>(); for (Entry<BuildTarget, ShowOptions.Builder> entry : showOptionBuilderMap.entrySet()) { builder.put(entry.getKey(), entry.getValue().build()); } return builder.build(); } static Optional<Path> getUserFacingOutputPath( SourcePathResolver pathResolver, BuildRule rule, boolean absolute, boolean buckOutCompatLink) { Optional<Path> outputPathOptional = Optional.ofNullable(rule.getSourcePathToOutput()).map(pathResolver::getRelativePath); // When using buck out compat mode, we favor using the default buck output path in the UI, so // amend the output paths when this is set. if (outputPathOptional.isPresent() && buckOutCompatLink) { BuckPaths paths = rule.getProjectFilesystem().getBuckPaths(); if (outputPathOptional.get().startsWith(paths.getConfiguredBuckOut())) { outputPathOptional = Optional.of( paths .getBuckOut() .resolve( outputPathOptional .get() .subpath( paths.getConfiguredBuckOut().getNameCount(), outputPathOptional.get().getNameCount()))); } } if (absolute) { return outputPathOptional.map(rule.getProjectFilesystem()::resolve); } else { return outputPathOptional; } } private TargetGraphAndTargetNodes computeTargetsAndGraphToShowTargetHash( CommandRunnerParams params, ListeningExecutorService executor, TargetGraphAndTargetNodes targetGraphAndTargetNodes) throws InterruptedException, BuildFileParseException, BuildTargetException, IOException { if (isDetectTestChanges()) { ImmutableSet<BuildTarget> explicitTestTargets = TargetGraphAndTargets.getExplicitTestTargets( targetGraphAndTargetNodes .getTargetGraph() .getSubgraph(targetGraphAndTargetNodes.getTargetNodes()) .getNodes() .iterator()); LOG.debug("Got explicit test targets: %s", explicitTestTargets); Iterable<BuildTarget> matchingBuildTargetsWithTests = mergeBuildTargets(targetGraphAndTargetNodes.getTargetNodes(), explicitTestTargets); // Parse the BUCK files for the tests of the targets passed in from the command line. TargetGraph targetGraphWithTests = params .getParser() .buildTargetGraph( params.getBuckEventBus(), params.getCell(), getEnableParserProfiling(), executor, matchingBuildTargetsWithTests); return TargetGraphAndTargetNodes.builder() .setTargetGraph(targetGraphWithTests) .setTargetNodes(targetGraphWithTests.getAll(matchingBuildTargetsWithTests)) .build(); } else { return targetGraphAndTargetNodes; } } private Iterable<BuildTarget> mergeBuildTargets( Iterable<TargetNode<?, ?>> targetNodes, Iterable<BuildTarget> buildTargets) { ImmutableSet.Builder<BuildTarget> targetsBuilder = ImmutableSet.builder(); targetsBuilder.addAll(buildTargets); for (TargetNode<?, ?> node : targetNodes) { targetsBuilder.add(node.getBuildTarget()); } return targetsBuilder.build(); } private FileHashLoader createOrGetFileHashLoader(CommandRunnerParams params) throws IOException { TargetHashFileMode targetHashFileMode = getTargetHashFileMode(); switch (targetHashFileMode) { case PATHS_AND_CONTENTS: return params.getFileHashCache(); case PATHS_ONLY: return new FilePathHashLoader(params.getCell().getRoot(), getTargetHashModifiedPaths()); } throw new IllegalStateException( "Invalid value for target hash file mode: " + targetHashFileMode); } private void computeShowTargetHash( CommandRunnerParams params, ListeningExecutorService executor, TargetGraphAndTargetNodes targetGraphAndTargetNodes, Map<BuildTarget, ShowOptions.Builder> showRulesResult) throws IOException, InterruptedException, BuildFileParseException, BuildTargetException, CycleException { LOG.debug("Getting target hash for %s", targetGraphAndTargetNodes.getTargetNodes()); TargetGraphAndTargetNodes targetGraphAndNodesWithTests = computeTargetsAndGraphToShowTargetHash(params, executor, targetGraphAndTargetNodes); TargetGraph targetGraphWithTests = targetGraphAndNodesWithTests.getTargetGraph(); FileHashLoader fileHashLoader = createOrGetFileHashLoader(params); // Hash each target's rule description and contents of any files. ImmutableMap<BuildTarget, HashCode> buildTargetHashes = new TargetGraphHashing( params.getBuckEventBus(), targetGraphWithTests, fileHashLoader, targetGraphAndNodesWithTests.getTargetNodes()) .setNumThreads(params.getBuckConfig().getNumThreads()) .hashTargetGraph(); ImmutableMap<BuildTarget, HashCode> finalHashes = rehashWithTestsIfNeeded( targetGraphWithTests, targetGraphAndTargetNodes.getTargetNodes(), buildTargetHashes); for (TargetNode<?, ?> targetNode : targetGraphAndTargetNodes.getTargetNodes()) { processTargetHash(targetNode.getBuildTarget(), showRulesResult, finalHashes); } } private ImmutableMap<BuildTarget, HashCode> rehashWithTestsIfNeeded( final TargetGraph targetGraphWithTests, Iterable<TargetNode<?, ?>> inputTargets, ImmutableMap<BuildTarget, HashCode> buildTargetHashes) throws CycleException { if (!isDetectTestChanges()) { return buildTargetHashes; } AcyclicDepthFirstPostOrderTraversal<TargetNode<?, ?>> traversal = new AcyclicDepthFirstPostOrderTraversal<>( node -> targetGraphWithTests.getAll(node.getParseDeps()).iterator()); Map<BuildTarget, HashCode> hashesWithTests = new HashMap<>(); for (TargetNode<?, ?> node : traversal.traverse(inputTargets)) { hashNodeWithDependencies(buildTargetHashes, hashesWithTests, node); } return ImmutableMap.copyOf(hashesWithTests); } private void hashNodeWithDependencies( ImmutableMap<BuildTarget, HashCode> buildTargetHashes, Map<BuildTarget, HashCode> hashesWithTests, TargetNode<?, ?> node) { HashCode nodeHashCode = getHashCodeOrThrow(buildTargetHashes, node.getBuildTarget()); Hasher hasher = Hashing.sha1().newHasher(); hasher.putBytes(nodeHashCode.asBytes()); Iterable<BuildTarget> dependentTargets = node.getParseDeps(); LOG.debug("Hashing target %s with dependent nodes %s", node, dependentTargets); for (BuildTarget targetToHash : dependentTargets) { HashCode dependencyHash = getHashCodeOrThrow(hashesWithTests, targetToHash); hasher.putBytes(dependencyHash.asBytes()); } if (isDetectTestChanges()) { for (BuildTarget targetToHash : Preconditions.checkNotNull(TargetNodes.getTestTargetsForNode(node))) { HashCode testNodeHashCode = getHashCodeOrThrow(buildTargetHashes, targetToHash); hasher.putBytes(testNodeHashCode.asBytes()); } } hashesWithTests.put(node.getBuildTarget(), hasher.hash()); } private void processTargetHash( BuildTarget buildTarget, Map<BuildTarget, ShowOptions.Builder> showRulesResult, ImmutableMap<BuildTarget, HashCode> finalHashes) { ShowOptions.Builder showOptionsBuilder = getShowOptionBuilder(showRulesResult, buildTarget); HashCode hashCode = getHashCodeOrThrow(finalHashes, buildTarget); showOptionsBuilder.setTargetHash(hashCode.toString()); } private static HashCode getHashCodeOrThrow( Map<BuildTarget, HashCode> buildTargetHashCodes, BuildTarget buildTarget) { HashCode hashCode = buildTargetHashCodes.get(buildTarget); return Preconditions.checkNotNull(hashCode, "Cannot find hash for target: %s", buildTarget); } private static class DirectOwnerPredicate implements Predicate<TargetNode<?, ?>> { private final ImmutableSet<Path> referencedInputs; private final ImmutableSet<Path> basePathOfTargets; private final String buildFileName; /** * @param referencedInputs A {@link TargetNode} must reference at least one of these paths as * input to match the predicate. All the paths must be relative to the project root. Ignored * if empty. */ public DirectOwnerPredicate( BuildFileTree buildFileTree, ImmutableSet<Path> referencedInputs, String buildFileName) { this.referencedInputs = referencedInputs; ImmutableSet.Builder<Path> basePathOfTargetsBuilder = ImmutableSet.builder(); for (Path input : referencedInputs) { Optional<Path> path = buildFileTree.getBasePathOfAncestorTarget(input); if (path.isPresent()) { basePathOfTargetsBuilder.add(path.get()); } } this.basePathOfTargets = basePathOfTargetsBuilder.build(); this.buildFileName = buildFileName; } @Override public boolean apply(TargetNode<?, ?> node) { // For any referenced file, only those with the nearest target base path can // directly depend on that file. if (!basePathOfTargets.contains(node.getBuildTarget().getBasePath())) { return false; } for (Path input : node.getInputs()) { for (Path referencedInput : referencedInputs) { if (referencedInput.startsWith(input)) { return true; } } } return referencedInputs.contains(node.getBuildTarget().getBasePath().resolve(buildFileName)); } } private ShowOptions.Builder getShowOptionBuilder( Map<BuildTarget, ShowOptions.Builder> showRulesBuilderMap, BuildTarget buildTarget) { if (!showRulesBuilderMap.containsKey(buildTarget)) { ShowOptions.Builder builder = ShowOptions.builder(); showRulesBuilderMap.put(buildTarget, builder); return builder; } return showRulesBuilderMap.get(buildTarget); } @Value.Immutable @BuckStyleImmutable abstract static class AbstractShowOptions { public abstract Optional<String> getOutputPath(); public abstract Optional<String> getRuleKey(); public abstract Optional<String> getTargetHash(); } private enum ShowOptionsName { OUTPUT_PATH("buck.outputPath"), TARGET_HASH("buck.targetHash"), RULE_KEY("buck.ruleKey"); private String name; ShowOptionsName(String name) { this.name = name; } public String getName() { return name; } } }