blob: 5f24d33bc50c9e8974ea05be4997a3a2c64ed16a [file] [log] [blame]
// Copyright 2015 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.skyframe;
import static com.google.common.collect.ImmutableSetMultimap.flatteningToImmutableSetMultimap;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.bugreport.BugReport;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.cmdline.ResolvedTargets;
import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.cmdline.TargetPattern;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.pkgcache.AbstractRecursivePackageProvider.MissingDepException;
import com.google.devtools.build.lib.pkgcache.CompileOneDependencyTransformer;
import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
import com.google.devtools.build.lib.pkgcache.LoadingPhaseCompleteEvent;
import com.google.devtools.build.lib.pkgcache.ParsingFailedEvent;
import com.google.devtools.build.lib.pkgcache.TargetParsingCompleteEvent;
import com.google.devtools.build.lib.pkgcache.TestFilter;
import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue.TargetPatternPhaseKey;
import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Takes a list of target patterns corresponding to a command line and turns it into a set of
* resolved Targets.
*/
final class TargetPatternPhaseFunction implements SkyFunction {
public TargetPatternPhaseFunction() {}
@Override
@Nullable
public TargetPatternPhaseValue compute(SkyKey key, Environment env) throws InterruptedException {
TargetPatternPhaseKey options = (TargetPatternPhaseKey) key.argument();
WorkspaceNameValue workspaceName = (WorkspaceNameValue) env.getValue(WorkspaceNameValue.key());
RepositoryMappingValue repositoryMappingValue =
(RepositoryMappingValue) env.getValue(RepositoryMappingValue.key(RepositoryName.MAIN));
if (repositoryMappingValue == null) {
return null;
}
// Determine targets to build:
List<String> failedPatterns = new ArrayList<>();
List<ExpandedPattern> expandedPatterns =
getTargetsToBuild(
env, options, repositoryMappingValue.getRepositoryMapping(), failedPatterns);
ResolvedTargets<Target> targets =
env.valuesMissing()
? null
: mergeAll(expandedPatterns, !failedPatterns.isEmpty(), env, options);
// If the --build_tests_only option was specified or we want to run tests, we need to determine
// the list of targets to test. For that, we remove manual tests and apply the command-line
// filters. Also, if --build_tests_only is specified, then the list of filtered targets will be
// set as build list as well.
ResolvedTargets<Target> testTargets = null;
if (options.getDetermineTests() || options.getBuildTestsOnly()) {
testTargets =
determineTests(
env,
options.getTargetPatterns(),
options.getOffset(),
repositoryMappingValue.getRepositoryMapping(),
options.getTestFilter());
Preconditions.checkState(env.valuesMissing() || (testTargets != null));
}
Map<Label, SkyKey> testExpansionKeys = new LinkedHashMap<>();
if (targets != null) {
for (Target target : targets.getTargets()) {
if (TargetUtils.isTestSuiteRule(target) && options.isExpandTestSuites()) {
Label label = target.getLabel();
SkyKey testExpansionKey = TestsForTargetPatternValue.key(ImmutableSet.of(label));
testExpansionKeys.put(label, testExpansionKey);
}
}
}
SkyframeLookupResult expandedTests = env.getValuesAndExceptions(testExpansionKeys.values());
if (env.valuesMissing()) {
return null;
}
ImmutableSet<Target> filteredTargets = targets.getFilteredTargets();
ImmutableSet<Target> testsToRun = null;
ImmutableSet<Target> testFilteredTargets = ImmutableSet.of();
if (testTargets != null) {
// Parse the targets to get the tests.
if (testTargets.getTargets().isEmpty() && !testTargets.getFilteredTargets().isEmpty()) {
env.getListener().handle(Event.warn("All specified test targets were excluded by filters"));
}
if (options.getBuildTestsOnly()) {
// Replace original targets to build with test targets, so that only targets that are
// actually going to be built are loaded in the loading phase. Note that this has a side
// effect that any test_suite target requested to be built is replaced by the set of *_test
// targets it represents; for example, this affects the status and the summary reports.
Set<Target> allFilteredTargets = new HashSet<>();
allFilteredTargets.addAll(targets.getTargets());
allFilteredTargets.addAll(targets.getFilteredTargets());
allFilteredTargets.removeAll(testTargets.getTargets());
allFilteredTargets.addAll(testTargets.getFilteredTargets());
testFilteredTargets = ImmutableSet.copyOf(allFilteredTargets);
filteredTargets = ImmutableSet.of();
targets =
ResolvedTargets.<Target>builder()
.merge(testTargets)
.mergeError(targets.hasError())
.build();
if (options.getDetermineTests()) {
testsToRun = testTargets.getTargets();
}
} else /*if (determineTests)*/ {
testsToRun = testTargets.getTargets();
targets =
ResolvedTargets.<Target>builder()
.merge(targets)
// Merging in all testsToRun guarantees that targets that will be built (because
// they are tests) are not considered to be "filtered out", even if they were
// initially filtered out. We can't merge in testTargets because its set of
// filteredTargets could include targets that we're building but not testing.
.merge(ResolvedTargets.<Target>builder().addAll(testsToRun).build())
.mergeError(testTargets.hasError())
.build();
filteredTargets = targets.getFilteredTargets();
}
if (testsToRun != null) {
// Note that testsToRun can still be null here, if buildTestsOnly && !shouldRunTests.
if (!targets.getTargets().containsAll(testsToRun)) {
throw new IllegalStateException(
String.format(
"Internal consistency check failed; some targets are scheduled for test execution"
+ " but not for building (%s)",
Sets.difference(testsToRun, targets.getTargets())));
}
}
}
if (targets.hasError()) {
env.getListener().handle(Event.warn("Target pattern parsing failed."));
}
maybeReportDeprecation(env.getListener(), targets.getTargets());
ResolvedTargets.Builder<Label> expandedLabelsBuilder = ResolvedTargets.builder();
ImmutableMap.Builder<Label, ImmutableSet<Label>> testSuiteExpansions =
ImmutableMap.builderWithExpectedSize(testExpansionKeys.size());
for (Target target : targets.getTargets()) {
Label label = target.getLabel();
if (TargetUtils.isTestSuiteRule(target) && options.isExpandTestSuites()) {
SkyKey expansionKey = Preconditions.checkNotNull(testExpansionKeys.get(label));
var value = (TestsForTargetPatternValue) expandedTests.get(expansionKey);
if (value == null) {
return null;
}
ResolvedTargets<Label> testExpansion = value.getLabels();
expandedLabelsBuilder.merge(testExpansion);
testSuiteExpansions.put(label, testExpansion.getTargets());
} else {
expandedLabelsBuilder.add(label);
}
}
ResolvedTargets<Label> targetLabels = expandedLabelsBuilder.build();
ResolvedTargets<Target> expandedTargets =
TestsForTargetPatternFunction.labelsToTargets(
env, targetLabels.getTargets(), targetLabels.hasError());
Set<Target> testSuiteTargets =
Sets.difference(targets.getTargets(), expandedTargets.getTargets());
ImmutableSet<Label> testsToRunLabels = null;
if (testsToRun != null) {
testsToRunLabels =
testsToRun.stream().map(Target::getLabel).collect(ImmutableSet.toImmutableSet());
}
ImmutableSet<Label> removedTargetLabels =
testSuiteTargets.stream().map(Target::getLabel).collect(ImmutableSet.toImmutableSet());
TargetPatternPhaseValue result =
new TargetPatternPhaseValue(
targetLabels.getTargets(),
testsToRunLabels,
targets.hasError(),
expandedTargets.hasError(),
workspaceName.getName());
env.getListener()
.post(
new TargetParsingCompleteEvent(
targets.getTargets(),
filteredTargets,
testFilteredTargets,
options.getTargetPatterns(),
expandedTargets.getTargets(),
ImmutableList.copyOf(failedPatterns),
mapOriginalPatternsToLabels(expandedPatterns, targets.getTargets()),
testSuiteExpansions.buildOrThrow()));
env.getListener()
.post(
new LoadingPhaseCompleteEvent(
result.getTargetLabels(),
removedTargetLabels,
repositoryMappingValue.getRepositoryMapping()));
return result;
}
/**
* Emit a warning when a deprecated target is mentioned on the command line.
*
* <p>Note that this does not stop us from emitting "target X depends on deprecated target Y"
* style warnings for the same target and it is a good thing; <i>depending</i> on a target and
* <i>wanting</i> to build it are different things.
*/
private static void maybeReportDeprecation(
ExtendedEventHandler eventHandler, Collection<Target> targets) {
for (Rule rule : Iterables.filter(targets, Rule.class)) {
if (rule.isAttributeValueExplicitlySpecified("deprecation")) {
eventHandler.handle(
Event.warn(
rule.getLocation(),
String.format(
"target '%s' is deprecated: %s",
rule.getLabel(),
NonconfigurableAttributeMapper.of(rule).get("deprecation", Type.STRING))));
}
}
}
/**
* Interprets the command-line arguments by expanding each pattern to targets and populating the
* list of {@code failedPatterns}.
*
* @param env the Starlark environment
* @param options the command-line arguments in structured form
* @param failedPatterns a list into which failed patterns are added
*/
private static List<ExpandedPattern> getTargetsToBuild(
Environment env,
TargetPatternPhaseKey options,
RepositoryMapping repoMapping,
List<String> failedPatterns)
throws InterruptedException {
TargetPattern.Parser parser =
new TargetPattern.Parser(options.getOffset(), RepositoryName.MAIN, repoMapping);
FilteringPolicy policy =
options.getBuildManualTests()
? FilteringPolicies.NO_FILTER
: FilteringPolicies.FILTER_MANUAL;
List<TargetPatternKey> patternSkyKeys = new ArrayList<>(options.getTargetPatterns().size());
for (String pattern : options.getTargetPatterns()) {
try {
patternSkyKeys.add(
TargetPatternValue.key(SignedTargetPattern.parse(pattern, parser), policy));
} catch (TargetParsingException e) {
failedPatterns.add(pattern);
// We post a PatternExpandingError here - the pattern could not be parsed, so we don't even
// get to run TargetPatternFunction.
env.getListener().post(PatternExpandingError.failed(pattern, e.getMessage()));
// We generally skip patterns that don't parse. We report a parsing failed exception to the
// event bus here, but not in determineTests below, which goes through the same list. Note
// that the TargetPatternFunction otherwise reports these events (but only if the target
// pattern could be parsed successfully).
env.getListener().post(new ParsingFailedEvent(pattern, e.getMessage()));
try {
env.getValueOrThrow(TargetPatternErrorFunction.key(e), TargetParsingException.class);
} catch (TargetParsingException ignore) {
// We ignore this. Keep going is active.
}
env.getListener().handle(Event.error("Skipping '" + pattern + "': " + e.getMessage()));
}
}
SkyframeLookupResult resolvedPatterns = env.getValuesAndExceptions(patternSkyKeys);
List<ExpandedPattern> expandedPatterns = new ArrayList<>(patternSkyKeys.size());
for (TargetPatternKey pattern : patternSkyKeys) {
TargetPatternValue value;
try {
value =
(TargetPatternValue) resolvedPatterns.getOrThrow(pattern, TargetParsingException.class);
} catch (TargetParsingException e) {
String rawPattern = pattern.getPattern();
String errorMessage = e.getMessage();
failedPatterns.add(rawPattern);
env.getListener().post(PatternExpandingError.failed(rawPattern, errorMessage));
env.getListener().handle(Event.error("Skipping '" + rawPattern + "': " + errorMessage));
continue;
}
if (value == null) {
continue;
}
// TODO(ulfjack): This is terribly inefficient.
ResolvedTargets<Target> asTargets =
TestsForTargetPatternFunction.labelsToTargets(
env, value.getTargets().getTargets(), value.getTargets().hasError());
if (asTargets == null) {
continue;
}
expandedPatterns.add(ExpandedPattern.of(pattern, asTargets));
}
return expandedPatterns;
}
/** Merges expansions from all patterns into a single {@link ResolvedTargets} instance. */
@Nullable
private static ResolvedTargets<Target> mergeAll(
List<ExpandedPattern> expandedPatterns,
boolean hasError,
Environment env,
TargetPatternPhaseKey options)
throws InterruptedException {
ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
builder.mergeError(hasError);
for (ExpandedPattern expansion : expandedPatterns) {
if (expansion.pattern().isNegative()) {
builder.filter(Predicates.not(Predicates.in(expansion.resolvedTargets().getTargets())));
} else {
builder.merge(expansion.resolvedTargets());
}
}
ResolvedTargets<Target> result =
builder.filter(TargetUtils.tagFilter(options.getBuildTargetFilter())).build();
if (options.getCompileOneDependency()) {
EnvironmentBackedRecursivePackageProvider environmentBackedRecursivePackageProvider =
new EnvironmentBackedRecursivePackageProvider(env);
try {
result =
new CompileOneDependencyTransformer(environmentBackedRecursivePackageProvider)
.transformCompileOneDependency(env.getListener(), result);
} catch (MissingDepException e) {
return null;
} catch (TargetParsingException e) {
try {
env.getValueOrThrow(TargetPatternErrorFunction.key(e), TargetParsingException.class);
} catch (TargetParsingException ignore) {
// We ignore this. Keep going is active.
}
env.getListener().handle(Event.error(e.getMessage()));
return ResolvedTargets.failed();
}
if (environmentBackedRecursivePackageProvider.encounteredPackageErrors()) {
result = ResolvedTargets.<Target>builder().merge(result).setError().build();
}
}
return result;
}
/**
* Interpret test target labels from the command-line arguments and return the corresponding set
* of targets, handling the filter flags, and expanding test suites.
*
* @param targetPatterns the list of command-line target patterns specified by the user
* @param repoMapping the repository mapping to apply to repos in the patterns
* @param testFilter the test filter
*/
@Nullable
private static ResolvedTargets<Target> determineTests(
Environment env,
List<String> targetPatterns,
PathFragment offset,
RepositoryMapping repoMapping,
TestFilter testFilter)
throws InterruptedException {
TargetPattern.Parser parser =
new TargetPattern.Parser(offset, RepositoryName.MAIN, repoMapping);
List<TargetPatternKey> patternSkyKeys = new ArrayList<>();
for (String pattern : targetPatterns) {
try {
patternSkyKeys.add(
TargetPatternValue.key(
SignedTargetPattern.parse(pattern, parser), FilteringPolicies.FILTER_TESTS));
} catch (TargetParsingException e) {
// Skip.
}
}
SkyframeLookupResult resolvedPatterns = env.getValuesAndExceptions(patternSkyKeys);
if (env.valuesMissing()) {
return null;
}
List<SkyKey> expandedSuiteKeys = new ArrayList<>();
for (TargetPatternKey key : patternSkyKeys) {
TargetPatternValue value;
try {
value = (TargetPatternValue) resolvedPatterns.getOrThrow(key, TargetParsingException.class);
if (value == null) {
BugReport.sendBugReport(
new IllegalStateException(
"TargetPatternValue " + key + " was missing, this should never happen"));
return null;
}
} catch (TargetParsingException e) {
// Skip.
continue;
}
expandedSuiteKeys.add(TestsForTargetPatternValue.key(value.getTargets().getTargets()));
}
SkyframeLookupResult expandedSuites = env.getValuesAndExceptions(expandedSuiteKeys);
if (env.valuesMissing()) {
return null;
}
ResolvedTargets.Builder<Target> testTargetsBuilder = ResolvedTargets.builder();
int suiteKeyIndex = 0;
for (TargetPatternKey pattern : patternSkyKeys) {
TargetPatternValue value;
try {
value =
(TargetPatternValue) resolvedPatterns.getOrThrow(pattern, TargetParsingException.class);
if (value == null) {
BugReport.sendBugReport(
new IllegalStateException(
"TargetPatternValue " + pattern + " was missing, this should never happen"));
return null;
}
} catch (TargetParsingException e) {
// This was already reported in getTargetsToBuild (maybe merge the two code paths?).
continue;
}
TestsForTargetPatternValue expandedSuitesValue =
(TestsForTargetPatternValue) expandedSuites.get(expandedSuiteKeys.get(suiteKeyIndex++));
if (expandedSuitesValue == null) {
BugReport.logUnexpected("Value for: '%s' was missing, this should never happen", pattern);
return null;
}
if (pattern.isNegative()) {
ResolvedTargets<Target> negativeTargets =
TestsForTargetPatternFunction.labelsToTargets(
env,
expandedSuitesValue.getLabels().getTargets(),
expandedSuitesValue.getLabels().hasError());
testTargetsBuilder.filter(Predicates.not(Predicates.in(negativeTargets.getTargets())));
testTargetsBuilder.mergeError(negativeTargets.hasError());
} else {
ResolvedTargets<Target> positiveTargets =
TestsForTargetPatternFunction.labelsToTargets(
env,
expandedSuitesValue.getLabels().getTargets(),
expandedSuitesValue.getLabels().hasError());
testTargetsBuilder.addAll(positiveTargets.getTargets());
testTargetsBuilder.mergeError(positiveTargets.hasError());
}
}
testTargetsBuilder.filter(testFilter);
return testTargetsBuilder.build();
}
private static ImmutableSetMultimap<String, Label> mapOriginalPatternsToLabels(
List<ExpandedPattern> expandedPatterns, Set<Target> includedTargets) {
return expandedPatterns.stream()
.filter(expansion -> !expansion.pattern().isNegative())
.collect(
flatteningToImmutableSetMultimap(
expansion -> expansion.pattern().getPattern(),
expansion ->
expansion.resolvedTargets().getTargets().stream()
.filter(includedTargets::contains)
.map(Target::getLabel)));
}
/** Represents the expansion of a single target pattern. */
@AutoValue
abstract static class ExpandedPattern {
static ExpandedPattern of(TargetPatternKey pattern, ResolvedTargets<Target> resolvedTargets) {
return new AutoValue_TargetPatternPhaseFunction_ExpandedPattern(pattern, resolvedTargets);
}
abstract TargetPatternKey pattern();
abstract ResolvedTargets<Target> resolvedTargets();
}
}