| // Copyright 2024 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.runtime; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableListMultimap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; | |
| import com.google.devtools.common.options.OptionsParsingException; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Set; | |
| /** | |
| * Holds {@code --config=foo} definitions for the current invocation. | |
| * | |
| * <p>Callers can use this to determine which flags {@code --config=foo} sets: | |
| * | |
| * {@snippet : | |
| * ConfigFlagDefinitions definitions = <get a reference from your favorite provider>; | |
| * try { | |
| * ConfigValue definition = ConfigFlagDefinitions.get("foo", configDefinitions); | |
| * List<String> expandedFlags = definition.flags(); | |
| * List<String> rcfileSources = definition.rcSources(); | |
| * } catch (OptionsParsingException e) { | |
| * // --config=foo doesn't exist or doesn't resolve. | |
| * } | |
| * } | |
| * | |
| * <p>This is a pure data class, which makes it Skyframe- and cache-friendly. Resolution logic is a | |
| * pure static function over that data. | |
| */ | |
| public final class ConfigFlagDefinitions { | |
| private final ImmutableListMultimap<String, ConfigDefinition> definitions; | |
| @VisibleForTesting | |
| public static final ConfigFlagDefinitions NONE = | |
| new ConfigFlagDefinitions(ImmutableListMultimap.of()); | |
| /** | |
| * There's no need for callers outside {@code lib.runtime} to construct this or see its underlying | |
| * data. The underlying data is a complicated implementation detail of the options parsing logic. | |
| */ | |
| ConfigFlagDefinitions(ImmutableListMultimap<String, ConfigDefinition> definitions) { | |
| this.definitions = definitions; | |
| } | |
| /** | |
| * {@code --config=foo} expansion. | |
| * | |
| * @param flags the flags this --config expands to | |
| * @param rcSources full paths of the rc files that define this --config. Can be more than one | |
| * because it may call other --configs from other files. | |
| */ | |
| public record ConfigValue(List<String> flags, Set<String> rcSources) {} | |
| /** | |
| * Returns the definition of {@code --config=<configName>} | |
| * | |
| * @throws OptionsParsingException if the config doesn't exist or doesn't resolve correctly | |
| */ | |
| public static ConfigValue get(String configName, ConfigFlagDefinitions definitions) | |
| throws OptionsParsingException { | |
| if (!definitions.definitions.containsKey(configName)) { | |
| throw new OptionsParsingException(String.format("--config=%s doesn't exist", configName)); | |
| } | |
| ImmutableList.Builder<String> flags = ImmutableList.builder(); | |
| ImmutableSet.Builder<String> rcSources = ImmutableSet.builder(); | |
| Set<String> seenConfigs = new HashSet<>(); | |
| applyDefinition(configName, configName, definitions, flags, rcSources, seenConfigs); | |
| return new ConfigValue(flags.build(), rcSources.build()); | |
| } | |
| /** | |
| * Expands {@code --config=<configName>}, making recursive calls any time it encounters {@code | |
| * --config=something_else}. Throws an {@code OptionsParsingException} on bad references or | |
| * cycles. | |
| */ | |
| private static void applyDefinition( | |
| String origConfigName, | |
| String configName, | |
| ConfigFlagDefinitions definitions, | |
| ImmutableList.Builder<String> flags, | |
| ImmutableSet.Builder<String> rcSources, | |
| Set<String> seenConfigs) | |
| throws OptionsParsingException { | |
| ImmutableList<ConfigDefinition> directFlags = definitions.definitions.get(configName); | |
| if (!seenConfigs.add(configName)) { | |
| throw new OptionsParsingException( | |
| String.format( | |
| "--config=%s can't be evaluated because its definition has a cycle.", | |
| origConfigName)); | |
| } | |
| if (directFlags.isEmpty()) { | |
| throw new OptionsParsingException( | |
| String.format( | |
| "--config=%s doesn't resolve because it expands to non-existent --config=%s", | |
| origConfigName, configName)); | |
| } | |
| // The underlying data stores a multimap: { name: Collection<Definition> }. This is more | |
| // important for non-config definitions like "build --a=b" where each rc file defines its own | |
| // "build" args. For --config, just choose the first. | |
| ConfigDefinition def = directFlags.get(0); | |
| rcSources.add(def.rcSource()); | |
| for (String flag : def.flags()) { | |
| if (flag.startsWith("--config=")) { | |
| applyDefinition( | |
| origConfigName, | |
| flag.substring(flag.indexOf("=") + 1), | |
| definitions, | |
| flags, | |
| rcSources, | |
| seenConfigs); | |
| } else { | |
| flags.add(flag); | |
| } | |
| } | |
| } | |
| /** Serialization- and BUILD library-friendly version of RcChunkOfArgs. */ | |
| @AutoCodec | |
| record ConfigDefinition(ImmutableList<String> flags, String rcSource) {} | |
| @Override | |
| public boolean equals(Object o) { | |
| if (o instanceof ConfigFlagDefinitions other) { | |
| return other.definitions.equals(definitions); | |
| } | |
| return false; | |
| } | |
| @Override | |
| public int hashCode() { | |
| return definitions.hashCode(); | |
| } | |
| } |