| // Copyright 2023 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.analysis.config; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.SetMultimap; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.util.OrderedSetMultimap; |
| import com.google.devtools.common.options.OptionDefinition; |
| import com.google.devtools.common.options.OptionsParser; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * A diff class for BuildOptions. Fields are meant to be populated and returned by {@link |
| * OptionsDiff#diff}. |
| */ |
| public final class OptionsDiff { |
| /** Returns the difference between two BuildOptions in a new {@link OptionsDiff}. */ |
| public static OptionsDiff diff(BuildOptions first, BuildOptions second) { |
| return diff(new OptionsDiff(), first, second); |
| } |
| |
| /** |
| * Returns the difference between two BuildOptions in a pre-existing {@link OptionsDiff}. |
| * |
| * <p>In a single pass through this method, the method can only compare a single "first" {@link |
| * BuildOptions} and single "second" BuildOptions; but an OptionsDiff instance can store the diff |
| * between a single "first" BuildOptions and multiple "second" BuildOptions. Being able to |
| * maintain a single OptionsDiff over multiple calls to diff is useful for, for example, |
| * aggregating the difference between a single BuildOptions and the results of applying a {@link |
| * com.google.devtools.build.lib.analysis.config.transitions.SplitTransition}) to it. |
| */ |
| @SuppressWarnings("ReferenceEquality") // See comment above == comparison. |
| public static OptionsDiff diff(OptionsDiff diff, BuildOptions first, BuildOptions second) { |
| checkArgument( |
| !diff.hasStarlarkOptions, |
| "OptionsDiff cannot handle multiple 'second' BuildOptions with Starlark options and is" |
| + " trying to diff against %s", |
| diff); |
| checkNotNull(first); |
| checkNotNull(second); |
| if (first.equals(second)) { |
| return diff; |
| } |
| |
| // Check and report if either class has been trimmed of an options class that exists in the |
| // other. |
| ImmutableSet<Class<? extends FragmentOptions>> firstOptionClasses = |
| first.getNativeOptions().stream() |
| .map(FragmentOptions::getClass) |
| .collect(ImmutableSet.toImmutableSet()); |
| ImmutableSet<Class<? extends FragmentOptions>> secondOptionClasses = |
| second.getNativeOptions().stream() |
| .map(FragmentOptions::getClass) |
| .collect(ImmutableSet.toImmutableSet()); |
| Sets.difference(firstOptionClasses, secondOptionClasses).forEach(diff::addExtraFirstFragment); |
| Sets.difference(secondOptionClasses, firstOptionClasses).stream() |
| .map(second::get) |
| .forEach(diff::addExtraSecondFragment); |
| |
| // For fragments in common, report differences. |
| for (Class<? extends FragmentOptions> clazz : |
| Sets.intersection(firstOptionClasses, secondOptionClasses)) { |
| FragmentOptions firstOptions = first.get(clazz); |
| FragmentOptions secondOptions = second.get(clazz); |
| // We avoid calling #equals because we are going to do a field-by-field comparison anyway. |
| if (firstOptions == secondOptions) { |
| continue; |
| } |
| for (OptionDefinition definition : OptionsParser.getOptionDefinitions(clazz)) { |
| Object firstValue = firstOptions.getValueFromDefinition(definition); |
| Object secondValue = secondOptions.getValueFromDefinition(definition); |
| if (!Objects.equals(firstValue, secondValue)) { |
| diff.addDiff(clazz, definition, firstValue, secondValue); |
| } |
| } |
| } |
| |
| // Compare Starlark options for the two classes. |
| ImmutableMap<Label, Object> starlarkFirst = first.getStarlarkOptions(); |
| ImmutableMap<Label, Object> starlarkSecond = second.getStarlarkOptions(); |
| for (Label buildSetting : Sets.union(starlarkFirst.keySet(), starlarkSecond.keySet())) { |
| if (starlarkFirst.get(buildSetting) == null) { |
| diff.addExtraSecondStarlarkOption(buildSetting, starlarkSecond.get(buildSetting)); |
| } else if (starlarkSecond.get(buildSetting) == null) { |
| diff.addExtraFirstStarlarkOption(buildSetting); |
| } else if (!starlarkFirst.get(buildSetting).equals(starlarkSecond.get(buildSetting))) { |
| diff.putStarlarkDiff( |
| buildSetting, starlarkFirst.get(buildSetting), starlarkSecond.get(buildSetting)); |
| } |
| } |
| return diff; |
| } |
| |
| private final ListMultimap<Class<? extends FragmentOptions>, OptionDefinition> differingOptions = |
| ArrayListMultimap.create(); |
| // The keyset for the {@link first} and {@link second} maps are identical and indicate which |
| // specific options differ between the first and second built options. |
| private final Map<OptionDefinition, Object> first = new LinkedHashMap<>(); |
| // Since this class can be used to track the result of transitions, {@link second} is a multimap |
| // to be able to handle {@link SplitTransition}s. |
| private final SetMultimap<OptionDefinition, Object> second = OrderedSetMultimap.create(); |
| // List of "extra" fragments for each BuildOption aka fragments that were trimmed off one |
| // BuildOption but not the other. |
| private final Set<Class<? extends FragmentOptions>> extraFirstFragments = new HashSet<>(); |
| private final Set<FragmentOptions> extraSecondFragments = new HashSet<>(); |
| |
| private final Map<Label, Object> starlarkFirst = new LinkedHashMap<>(); |
| // TODO(b/112041323): This should also be multimap but we don't diff multiple times with |
| // Starlark options anywhere yet so add that feature when necessary. |
| private final Map<Label, Object> starlarkSecond = new LinkedHashMap<>(); |
| |
| private final List<Label> extraStarlarkOptionsFirst = new ArrayList<>(); |
| private final Map<Label, Object> extraStarlarkOptionsSecond = new HashMap<>(); |
| |
| private boolean hasStarlarkOptions = false; |
| |
| @VisibleForTesting |
| Set<Class<? extends FragmentOptions>> getExtraFirstFragmentClassesForTesting() { |
| return extraFirstFragments; |
| } |
| |
| @VisibleForTesting |
| Set<FragmentOptions> getExtraSecondFragmentsForTesting() { |
| return extraSecondFragments; |
| } |
| |
| public Map<OptionDefinition, Object> getFirst() { |
| return first; |
| } |
| |
| public Multimap<OptionDefinition, Object> getSecond() { |
| return second; |
| } |
| |
| private void addDiff( |
| Class<? extends FragmentOptions> fragmentOptionsClass, |
| OptionDefinition option, |
| Object firstValue, |
| Object secondValue) { |
| differingOptions.put(fragmentOptionsClass, option); |
| first.put(option, firstValue); |
| second.put(option, secondValue); |
| } |
| |
| private void addExtraFirstFragment(Class<? extends FragmentOptions> options) { |
| extraFirstFragments.add(options); |
| } |
| |
| private void addExtraSecondFragment(FragmentOptions options) { |
| extraSecondFragments.add(options); |
| } |
| |
| private void putStarlarkDiff(Label buildSetting, Object firstValue, Object secondValue) { |
| starlarkFirst.put(buildSetting, firstValue); |
| starlarkSecond.put(buildSetting, secondValue); |
| hasStarlarkOptions = true; |
| } |
| |
| private void addExtraFirstStarlarkOption(Label buildSetting) { |
| extraStarlarkOptionsFirst.add(buildSetting); |
| hasStarlarkOptions = true; |
| } |
| |
| private void addExtraSecondStarlarkOption(Label buildSetting, Object value) { |
| extraStarlarkOptionsSecond.put(buildSetting, value); |
| hasStarlarkOptions = true; |
| } |
| |
| /** |
| * Returns the labels of all starlark options that caused a difference between the first and |
| * second options set. |
| */ |
| public ImmutableSet<Label> getChangedStarlarkOptions() { |
| return ImmutableSet.<Label>builder() |
| .addAll(starlarkFirst.keySet()) |
| .addAll(starlarkSecond.keySet()) |
| .addAll(extraStarlarkOptionsFirst) |
| .addAll(extraStarlarkOptionsSecond.keySet()) |
| .build(); |
| } |
| |
| @VisibleForTesting |
| Map<Label, Object> getStarlarkFirstForTesting() { |
| return starlarkFirst; |
| } |
| |
| @VisibleForTesting |
| Map<Label, Object> getStarlarkSecondForTesting() { |
| return starlarkSecond; |
| } |
| |
| @VisibleForTesting |
| List<Label> getExtraStarlarkOptionsFirstForTesting() { |
| return extraStarlarkOptionsFirst; |
| } |
| |
| @VisibleForTesting |
| Map<Label, Object> getExtraStarlarkOptionsSecondForTesting() { |
| return extraStarlarkOptionsSecond; |
| } |
| |
| /** |
| * Note: it's not enough for first and second to be empty, with trimming, they must also contain |
| * the same options classes. |
| */ |
| boolean areSame() { |
| return first.isEmpty() |
| && second.isEmpty() |
| && extraSecondFragments.isEmpty() |
| && extraFirstFragments.isEmpty() |
| && differingOptions.isEmpty() |
| && starlarkFirst.isEmpty() |
| && starlarkSecond.isEmpty() |
| && extraStarlarkOptionsFirst.isEmpty() |
| && extraStarlarkOptionsSecond.isEmpty(); |
| } |
| |
| public String prettyPrint() { |
| StringBuilder toReturn = new StringBuilder(); |
| for (String diff : getPrettyPrintList()) { |
| toReturn.append(diff).append(System.lineSeparator()); |
| } |
| return toReturn.toString(); |
| } |
| |
| public List<String> getPrettyPrintList() { |
| List<String> toReturn = new ArrayList<>(); |
| first.forEach( |
| (option, value) -> |
| toReturn.add(option.getOptionName() + ":" + value + " -> " + second.get(option))); |
| starlarkFirst.forEach( |
| (option, value) -> toReturn.add(option + ":" + value + starlarkSecond.get(option))); |
| return toReturn; |
| } |
| } |