/* * Copyright 2017-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.versions; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.Flavor; import com.facebook.buck.model.UnflavoredBuildTarget; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.RichStream; import com.facebook.buck.util.immutables.BuckStyleTuple; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import org.immutables.value.Value; /** * A lookup table of {@link TargetNode}s which matches {@link BuildTarget} lookups to nodes indexed * by a base target which contains a subset of the input targets flavors. It is expected that every * lookup should match at most one base target. */ @Value.Immutable @BuckStyleTuple abstract class AbstractFlavorSearchTargetNodeFinder { /** @return a map of nodes indexed by their "base" target. */ abstract ImmutableMap<BuildTarget, TargetNode<?, ?>> getBaseTargetIndex(); /** Verify that base targets correctly correspond to their node. */ @Value.Check void check() { for (Map.Entry<BuildTarget, TargetNode<?, ?>> ent : getBaseTargetIndex().entrySet()) { Preconditions.checkArgument( ent.getValue() .getBuildTarget() .getUnflavoredBuildTarget() .equals(ent.getKey().getUnflavoredBuildTarget())); Preconditions.checkArgument( ent.getValue().getBuildTarget().getFlavors().containsAll(ent.getKey().getFlavors())); } } /** * @return an index from the original node target to node. Used as an optimization to avoid a more * expensive flavor-subset-based lookup if we can find the node by the exact name. */ @Value.Derived ImmutableMap<BuildTarget, TargetNode<?, ?>> getBuildTargetIndex() { return getBaseTargetIndex() .values() .stream() .collect(MoreCollectors.toImmutableMap(TargetNode::getBuildTarget, n -> n)); } // Build the flavor map, which maps all unflavored targets to the flavors they have in the // graph. We sort the list of flavor sets from largest to smallest, so that look ups pick the // more flavored sets first. @Value.Derived ImmutableMap<UnflavoredBuildTarget, ImmutableSet<ImmutableSet<Flavor>>> getBaseTargetFlavorMap() { Map<UnflavoredBuildTarget, List<ImmutableSet<Flavor>>> flavorMapRawBuilder = new LinkedHashMap<>(); for (Map.Entry<BuildTarget, TargetNode<?, ?>> ent : getBaseTargetIndex().entrySet()) { BuildTarget baseTarget = ent.getKey(); UnflavoredBuildTarget unflavoredTarget = baseTarget.getUnflavoredBuildTarget(); if (!flavorMapRawBuilder.containsKey(unflavoredTarget)) { flavorMapRawBuilder.put(unflavoredTarget, new ArrayList<>()); } flavorMapRawBuilder.get(unflavoredTarget).add(baseTarget.getFlavors()); } ImmutableMap.Builder<UnflavoredBuildTarget, ImmutableSet<ImmutableSet<Flavor>>> flavorMapBuilder = ImmutableMap.builder(); for (Map.Entry<UnflavoredBuildTarget, List<ImmutableSet<Flavor>>> ent : flavorMapRawBuilder.entrySet()) { ent.getValue().sort((o1, o2) -> Integer.compare(o2.size(), o1.size())); flavorMapBuilder.put(ent.getKey(), ImmutableSet.copyOf(ent.getValue())); } return flavorMapBuilder.build(); } public Optional<TargetNode<?, ?>> get(BuildTarget target) { // If this node is in the graph under the given name, return it. TargetNode<?, ?> node = getBuildTargetIndex().get(target); if (node != null) { return Optional.of(node); } ImmutableSet<ImmutableSet<Flavor>> flavorList = getBaseTargetFlavorMap().get(target.getUnflavoredBuildTarget()); if (flavorList == null) { return Optional.empty(); } // Otherwise, see if this node exists in the graph with a "less" flavored name. We initially // select all targets which contain a subset of the original flavors, which should be sorted by // from largest flavor set to smallest. We then use the first match, and verify the subsequent // matches are subsets. ImmutableList<ImmutableSet<Flavor>> matches = RichStream.from(flavorList).filter(target.getFlavors()::containsAll).toImmutableList(); if (!matches.isEmpty()) { ImmutableSet<Flavor> firstMatch = matches.get(0); for (ImmutableSet<Flavor> subsequentMatch : matches.subList(1, matches.size())) { Preconditions.checkState( firstMatch.size() > subsequentMatch.size(), "Expected to find larger flavor lists earlier in the flavor map " + "index (sizeof(%s) <= sizeof(%s))", firstMatch.size(), subsequentMatch.size()); Preconditions.checkState( firstMatch.containsAll(subsequentMatch), "Found multiple disjoint flavor matches for %s: %s and %s (from %s)", target, firstMatch, subsequentMatch, matches); } return Optional.of( Preconditions.checkNotNull( getBaseTargetIndex().get(target.withFlavors(firstMatch)), "%s missing in index", target.withFlavors(firstMatch))); } // Otherwise, return `null` to indicate this node isn't in the target graph. return Optional.empty(); } }