diff options
author | Alex Humesky <ahumesky@google.com> | 2015-09-29 01:42:00 +0000 |
---|---|---|
committer | Florian Weikert <fwe@google.com> | 2015-09-30 09:33:06 +0000 |
commit | 2f3f4cf925a760019fd089dd5ee771a3552fb278 (patch) | |
tree | c3950d8da89e776e6c0bccaf0802c5291397cfed /src/main/java/com/google/devtools | |
parent | c2579a5041568592913165bc0a167bd0b70f46a2 (diff) |
Adds a mechanism for invocation policy. The policy is taken through the --invocation_policy startup flag and allows an application invoking Bazel to set or override flag values (whether from the command line or a bazelrc).
--
MOS_MIGRATED_REVID=104160290
Diffstat (limited to 'src/main/java/com/google/devtools')
8 files changed, 559 insertions, 11 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java index a441a39c4b..dbca029f67 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java @@ -148,9 +148,10 @@ public class BlazeCommandDispatcher { return ExitCode.SUCCESS; } - private CommonCommandOptions checkOptions(OptionsParser optionsParser, - Command commandAnnotation, List<String> args, List<String> rcfileNotes, OutErr outErr) + private void parseArgsAndConfigs(OptionsParser optionsParser, Command commandAnnotation, + List<String> args, List<String> rcfileNotes, OutErr outErr) throws OptionsParsingException { + Function<String, String> commandOptionSourceFunction = new Function<String, String>() { @Override public String apply(String input) { @@ -187,8 +188,6 @@ public class BlazeCommandDispatcher { configsLoaded = commonOptions.configs; commonOptions = optionsParser.getOptions(CommonCommandOptions.class); } - - return commonOptions; } /** @@ -270,13 +269,16 @@ public class BlazeCommandDispatcher { } OptionsParser optionsParser; - CommonCommandOptions commonOptions; // Delay output of notes regarding the parsed rc file, so it's possible to disable this in the // rc file. List<String> rcfileNotes = new ArrayList<>(); try { optionsParser = createOptionsParser(command); - commonOptions = checkOptions(optionsParser, commandAnnotation, args, rcfileNotes, outErr); + parseArgsAndConfigs(optionsParser, commandAnnotation, args, rcfileNotes, outErr); + + InvocationPolicyEnforcer optionsPolicyEnforcer = + InvocationPolicyEnforcer.create(getRuntime().getStartupOptionsProvider()); + optionsPolicyEnforcer.enforce(optionsParser, commandName); } catch (OptionsParsingException e) { for (String note : rcfileNotes) { outErr.printErrLn("INFO: " + note); @@ -299,6 +301,7 @@ public class BlazeCommandDispatcher { } } + CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class); BlazeRuntime.setupLogging(commonOptions.verbosity); // Do this before an actual crash so we don't have to worry about diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java index 244cb709f7..69d69da39d 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java @@ -235,4 +235,12 @@ public class BlazeServerStartupOptions extends OptionsBase { + "changes instead of scanning every file for a change.") public boolean watchFS; + + @Option(name = "invocation_policy", + defaultValue = "", + category = "undocumented", + help = "A base64-encoded-binary-serialized or text-formated " + + "invocation_policy.InvocationPolicy proto. Unlike other options, it is an error to " + + "specify --invocation_policy multiple times.") + public String invocationPolicy; } diff --git a/src/main/java/com/google/devtools/build/lib/runtime/InvocationPolicyEnforcer.java b/src/main/java/com/google/devtools/build/lib/runtime/InvocationPolicyEnforcer.java new file mode 100644 index 0000000000..463e996684 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/InvocationPolicyEnforcer.java @@ -0,0 +1,404 @@ +// Copyright 2015 Google Inc. 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.base.CharMatcher; +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Joiner; +import com.google.common.base.Verify; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import com.google.common.io.BaseEncoding; +import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.AllowValues; +import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.DisallowValues; +import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.FlagPolicy; +import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; +import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.SetValue; +import com.google.devtools.common.options.OptionPriority; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.OptionsParser.OptionDescription; +import com.google.devtools.common.options.OptionsParser.OptionValueDescription; +import com.google.devtools.common.options.OptionsParsingException; +import com.google.devtools.common.options.OptionsProvider; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.TextFormat; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +/** + * Given an OptionsParser and a InvocationPolicy proto, enforces the FlagPolicies on an + * OptionsParser. + * + * <p>"Flag" and "Option" are used interchangeably in this file. + */ +public final class InvocationPolicyEnforcer { + + /** + * Creates an {@link InvocationPolicyEnforcer} with the invocation policy obtained from the given + * {@link OptionsProvider}. This uses the provider only to obtain the policy from the + * --invocation_policy flag and does not enforce any policy on the flags in the provider. + * + * @param startupOptionsProvider an options provider which provides a BlazeServerStartupOptions + * options class + * + * @throws OptionsParsingException if the value of --invocation_policy is invalid + */ + public static InvocationPolicyEnforcer create(OptionsProvider startupOptionsProvider) + throws OptionsParsingException { + + BlazeServerStartupOptions blazeServerStartupOptions = + startupOptionsProvider.getOptions(BlazeServerStartupOptions.class); + return new InvocationPolicyEnforcer(parsePolicy(blazeServerStartupOptions.invocationPolicy)); + } + + /** + * Parses the given InvocationPolicy string, which may be a base64-encoded binary-serialized + * InvocationPolicy message, or a text formatted InvocationPolicy message. Note that the + * text format is not backwards compatible as the binary format is, and the option to + * provide a text formatted proto is provided only for debugging. + * + * @throws OptionsParsingException if the value of --invocation_policy is invalid + */ + private static InvocationPolicy parsePolicy(String policy) throws OptionsParsingException { + if (policy == null || policy.isEmpty()) { + return null; + } + + try { + try { + // First try decoding the policy as a base64 encoded binary proto. + return InvocationPolicy.parseFrom( + BaseEncoding.base64().decode(CharMatcher.WHITESPACE.removeFrom(policy))); + } catch (IllegalArgumentException e) { + // If the flag value can't be decoded from base64, try decoding the policy as a text + // formated proto. + InvocationPolicy.Builder builder = InvocationPolicy.newBuilder(); + TextFormat.merge(policy, builder); + return builder.build(); + } + } catch (InvalidProtocolBufferException | TextFormat.ParseException e) { + throw new OptionsParsingException("Malformed value of --invocation_policy: " + policy, e); + } + } + + private static final Logger LOG = Logger.getLogger(InvocationPolicyEnforcer.class.getName()); + + @Nullable + private final InvocationPolicy invocationPolicy; + + public InvocationPolicyEnforcer(@Nullable InvocationPolicy invocationPolicy) { + this.invocationPolicy = invocationPolicy; + } + + /** + * Applies this OptionsPolicyEnforcer's policy to the given OptionsParser. + * + * @param parser The OptionsParser to enforce policy on. + * @param command The command to which the options in the OptionsParser apply. + * @throws OptionsParsingException + */ + public void enforce(OptionsParser parser, String command) throws OptionsParsingException { + if (invocationPolicy == null) { + return; + } + + if (invocationPolicy.getFlagPoliciesCount() == 0) { + LOG.warning("InvocationPolicy contains no flag policies."); + } + + Function<Object, String> sourceFunction = Functions.constant("Invocation policy"); + + for (FlagPolicy flagPolicy : invocationPolicy.getFlagPoliciesList()) { + String flagName = flagPolicy.getFlagName(); + + // Skip the flag policy if it doesn't apply to this command. + if (!flagPolicy.getCommandsList().isEmpty() + && !flagPolicy.getCommandsList().contains(command)) { + LOG.info(String.format("Skipping flag policy for flag '%s' because it " + + "applies only to commands %s and the current command is '%s'", + flagName, flagPolicy.getCommandsList(), command)); + continue; + } + + OptionValueDescription valueDescription; + try { + valueDescription = parser.getOptionValueDescription(flagName); + } catch (IllegalArgumentException e) { + // This flag doesn't exist. We are deliberately lenient if the flag policy has a flag + // we don't know about. This is for better future proofing so that as new flags are added, + // new policies can use the new flags without worrying about older versions of Bazel. + LOG.info(String.format( + "Flag '%s' specified by invocation policy does not exist", flagName)); + continue; + } + + OptionDescription optionDescription = parser.getOptionDescription(flagName); + // getOptionDescription() will return null if the option does not exist, however + // getOptionValueDescription() above would have thrown an IllegalArgumentException if that + // were the case. + Verify.verifyNotNull(optionDescription); + + switch (flagPolicy.getOperationCase()) { + case SET_VALUE: + applySetValueOperation(parser, sourceFunction, flagPolicy, flagName, + valueDescription, optionDescription); + break; + + case USE_DEFAULT: + applyUseDefaultOperation(parser, flagName); + break; + + case ALLOW_VALUES: + applyAllowValuesOperation(parser, sourceFunction, flagPolicy, + flagName, valueDescription, optionDescription); + break; + + case DISALLOW_VALUES: + applyDisallowValuesOperation(parser, sourceFunction, flagPolicy, + flagName, valueDescription, optionDescription); + break; + + case OPERATION_NOT_SET: + throw new OptionsParsingException(String.format("Flag policy for flag '%s' does not " + + "have an operation", flagName)); + + default: + LOG.warning(String.format("Unknown operation '%s' from invocation policy for flag '%s'", + flagPolicy.getOperationCase(), flagName)); + break; + } + } + } + + private static void applySetValueOperation( + OptionsParser parser, + Function<Object, String> sourceFunction, + FlagPolicy flagPolicy, + String flagName, + OptionValueDescription valueDescription, + OptionDescription optionDescription) throws OptionsParsingException { + + SetValue setValue = flagPolicy.getSetValue(); + + // SetValue.flag_value must have at least 1 value. + if (setValue.getFlagValueCount() == 0) { + throw new OptionsParsingException(String.format( + "SetValue operation from invocation policy for flag '%s' does not have a value", + flagName)); + } + + // Flag must allow multiple values if multiple values are specified by the policy. + if (setValue.getFlagValueCount() > 1 && !optionDescription.getAllowMultiple()) { + throw new OptionsParsingException(String.format( + "SetValue operation from invocation policy sets multiple values for flag '%s' which " + + "does not allow multiple values", flagName)); + } + + if (setValue.getOverridable() && valueDescription != null) { + // The user set the value for the flag but the flag policy is overridable, so keep the user's + // value. + LOG.info(String.format("Keeping value '%s' from source '%s' for flag '%s' " + + "because the invocation policy specifying the value(s) '%s' is overridable", + valueDescription.getValue(), valueDescription.getSource(), flagName, + setValue.getFlagValueList())); + } else { + + // Clear the value in case the flag is a repeated flag (so that values don't accumulate), and + // in case the flag is an expansion flag or has implicit flags (so that the additional flags + // also get cleared). + parser.clearValue(flagName); + + // Set all the flag values from the policy. + for (String flagValue : setValue.getFlagValueList()) { + if (valueDescription == null) { + LOG.info(String.format("Setting value for flag '%s' from invocation " + + "policy to '%s', overriding the default value '%s'", flagName, flagValue, + optionDescription.getDefaultValue())); + } else { + LOG.info(String.format("Setting value for flag '%s' from invocation " + + "policy to '%s', overriding value '%s' from '%s'", flagName, flagValue, + valueDescription.getValue(), valueDescription.getSource())); + } + setFlagValue(parser, flagName, flagValue, sourceFunction); + } + } + } + + private static void applyUseDefaultOperation(OptionsParser parser, String flagName) { + + Map<String, OptionValueDescription> clearedValues = parser.clearValue(flagName); + for (Entry<String, OptionValueDescription> clearedValue : clearedValues.entrySet()) { + + OptionValueDescription clearedValueDesc = clearedValue.getValue(); + String clearedFlagName = clearedValue.getKey(); + String originalValue = clearedValueDesc.getValue().toString(); + String source = clearedValueDesc.getSource(); + + OptionDescription clearedFlagDesc = parser.getOptionDescription(clearedFlagName); + Object clearedFlagdefaultValue = clearedFlagDesc.getDefaultValue(); + + LOG.info(String.format("Using default value '%s' for flag '%s' as " + + "specified by invocation policy, overriding original value '%s' from '%s'", + clearedFlagdefaultValue, clearedFlagName, originalValue, source)); + } + } + + private static void applyAllowValuesOperation( + OptionsParser parser, + Function<Object, String> sourceFunction, + FlagPolicy flagPolicy, + String flagName, + OptionValueDescription valueDescription, + OptionDescription optionDescription) throws OptionsParsingException { + + AllowValues allowValues = flagPolicy.getAllowValues(); + applyAllowDisallowValueOperation( + parser, + sourceFunction, + /*allowValues=*/ true, + allowValues.getAllowedValuesList(), + allowValues.hasNewDefaultValue() ? allowValues.getNewDefaultValue() : null, + flagName, + valueDescription, + optionDescription); + } + + private static void applyDisallowValuesOperation( + OptionsParser parser, + Function<Object, String> sourceFunction, + FlagPolicy flagPolicy, + String flagName, + OptionValueDescription valueDescription, + OptionDescription optionDescription) throws OptionsParsingException { + + DisallowValues disallowValues = flagPolicy.getDisallowValues(); + applyAllowDisallowValueOperation( + parser, + sourceFunction, + /*allowValues=*/ false, + disallowValues.getDisallowedValuesList(), + disallowValues.hasNewDefaultValue() ? disallowValues.getNewDefaultValue() : null, + flagName, + valueDescription, + optionDescription); + } + + /** + * Shared logic between AllowValues and DisallowValues operations. + * + * @param parser + * @param sourceFunction + * @param allowValues True if this is an AllowValues operation, false if DisallowValues + * @param policyValues The list of allowed or disallowed values + * @param newDefaultValue The new default to use if the default value for the flag is now allowed + * (i.e. not in the list of allowed values or in the list of disallowed values). + * @param flagName + * @param valueDescription + * @param optionDescription + * + * @throws OptionsParsingException + */ + private static void applyAllowDisallowValueOperation( + OptionsParser parser, + Function<Object, String> sourceFunction, + boolean allowValues, + List<String> policyValues, + String newDefaultValue, + String flagName, + OptionValueDescription valueDescription, + OptionDescription optionDescription) throws OptionsParsingException { + + // For error reporting. + String policyType = allowValues ? "Allow" : "Disallow"; + + // Convert all the allowed values from strings to real object using the option's + // converter so that they can be checked for equality using real .equals() instead + // of string comparison. For example, "--foo=0", "--foo=false", "--nofoo", and "-f-" + // (if the option has an abbreviation) are all equal for boolean flags. Plus converters + // can be arbitrarily complex. + Set<Object> convertedPolicyValues = Sets.newHashSet(); + for (String value : policyValues) { + convertedPolicyValues.add(optionDescription.getConverter().convert(value)); + } + + if (valueDescription == null) { + // Nothing has set the value yet, so check that the default value from the flag's + // definition is allowed. The else case below (i.e. valueDescription is not null) checks for + // the flag allowing multiple values, however, flags that allow multiple values cannot have + // default values, and their value is always the empty list if they haven't been specified, + // which is why new_default_value is not a repeated field. + // + // This is xor'ed with allowValues because if the policy is to allow these values, + // then we want to apply the new default (or throw an error) if the default value of the flag + // is not in the set of allowed values. If the policy is to disallow these values + // (allowValues is false), then we want to apply the new default (or throw an error) if + // the default value of the flag is in the set of disallowed values. This works out to xor. + if (allowValues ^ convertedPolicyValues.contains(optionDescription.getDefaultValue())) { + if (newDefaultValue != null) { + // Use the default value from the policy. + LOG.info(String.format("Overriding default value '%s' for flag '%s' with " + + "new default value '%s' specified by invocation policy. %sed values are: %s", + optionDescription.getDefaultValue(), flagName, newDefaultValue, + policyType, Joiner.on(", ").join(policyValues))); + parser.clearValue(flagName); + setFlagValue(parser, flagName, newDefaultValue, sourceFunction); + } else { + // The operation disallows the default value, but doesn't supply its own default. + throw new OptionsParsingException(String.format( + "Default flag value '%s' for flag '%s' is not allowed by invocation policy, but " + + "the policy does not provide a new default value. " + + "%sed values are: %s", optionDescription.getDefaultValue(), flagName, + policyType, Joiner.on(", ").join(policyValues))); + } + } + } else { + // Check that the flag's value is allowed. + List<?> values; + if (optionDescription.getAllowMultiple()) { + // allowMultiple requires that the type of the option be List<T>. + values = (List<?>) valueDescription.getValue(); + } else { + values = ImmutableList.of(valueDescription.getValue()); + } + + for (Object value : values) { + // See above about the xor. + if (allowValues ^ convertedPolicyValues.contains(value)) { + throw new OptionsParsingException(String.format( + "Flag value '%s' for flag '%s' is not allowed by invocation policy. " + + "%sed values are: %s", value, flagName, policyType, + Joiner.on(", ").join(policyValues))); + } + } + } + } + + private static void setFlagValue( + OptionsParser parser, + String flagName, + String flagValue, + Function<? super String, String> sourceFunction) throws OptionsParsingException { + + parser.parseWithSourceFunction(OptionPriority.INVOCATION_POLICY, sourceFunction, + Arrays.asList(String.format("--%s=%s", flagName, flagValue))); + } +} diff --git a/src/main/java/com/google/devtools/common/options/Option.java b/src/main/java/com/google/devtools/common/options/Option.java index f41a051c80..ca3add9bec 100644 --- a/src/main/java/com/google/devtools/common/options/Option.java +++ b/src/main/java/com/google/devtools/common/options/Option.java @@ -62,6 +62,9 @@ public @interface Option { * be a compile-time constant.) This special interpretation of the string * "null" is only applicable when computing the default value; if specified * on the command-line, this string will have its usual literal meaning. + * + * <p>The default value for flags that set allowMultiple to true is always + * the empty list and the value in the annotation is ignored. */ String defaultValue(); @@ -90,6 +93,9 @@ public @interface Option { * converter for this option must either match the parameter {@code T} or * {@code List<T>}. In the latter case the individual lists are concatenated * to form the full options value. + * + * <p>The {@link #defaultValue()} field of the annotation is ignored for repeatable + * flags and the default value will be the empty list. */ boolean allowMultiple() default false; diff --git a/src/main/java/com/google/devtools/common/options/OptionPriority.java b/src/main/java/com/google/devtools/common/options/OptionPriority.java index b352e88a6f..a28f012822 100644 --- a/src/main/java/com/google/devtools/common/options/OptionPriority.java +++ b/src/main/java/com/google/devtools/common/options/OptionPriority.java @@ -50,9 +50,13 @@ public enum OptionPriority { COMMAND_LINE, /** + * For options coming from invocation policy. + */ + INVOCATION_POLICY, + + /** * This priority can be used to unconditionally override any user-provided options. * This should be used rarely and with caution! */ SOFTWARE_REQUIREMENT; - } diff --git a/src/main/java/com/google/devtools/common/options/OptionsData.java b/src/main/java/com/google/devtools/common/options/OptionsData.java index d5512a1542..61d798a837 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsData.java +++ b/src/main/java/com/google/devtools/common/options/OptionsData.java @@ -71,12 +71,19 @@ final class OptionsData { */ private final Map<Field, Converter<?>> converters; + /** + * Mapping from each Option-annotated field to a boolean for whether that field allows multiple + * values. + */ + private final Map<Field, Boolean> allowMultiple; + private OptionsData(Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, Map<String, Field> nameToField, Map<Character, Field> abbrevToField, Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields, Map<Field, Object> optionDefaults, - Map<Field, Converter<?>> converters) { + Map<Field, Converter<?>> converters, + Map<Field, Boolean> allowMultiple) { this.optionsClasses = ImmutableMap.copyOf(optionsClasses); this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields); this.nameToField = ImmutableMap.copyOf(nameToField); @@ -84,6 +91,7 @@ final class OptionsData { // Can't use an ImmutableMap here because of null values. this.optionDefaults = Collections.unmodifiableMap(optionDefaults); this.converters = ImmutableMap.copyOf(converters); + this.allowMultiple = ImmutableMap.copyOf(allowMultiple); } public Collection<Class<? extends OptionsBase>> getOptionsClasses() { @@ -119,6 +127,10 @@ final class OptionsData { return converters.get(field); } + public boolean getAllowMultiple(Field field) { + return allowMultiple.get(field); + } + private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) { List<Field> allFields = Lists.newArrayList(); for (Field field : optionsClass.getFields()) { @@ -157,6 +169,7 @@ final class OptionsData { Map<Character, Field> abbrevToFieldBuilder = Maps.newHashMap(); Map<Field, Object> optionDefaultsBuilder = Maps.newHashMap(); Map<Field, Converter<?>> convertersBuilder = Maps.newHashMap(); + Map<Field, Boolean> allowMultipleBuilder = Maps.newHashMap(); // Read all Option annotations: for (Class<? extends OptionsBase> parsedOptionsClass : classes) { @@ -256,9 +269,11 @@ final class OptionsData { optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field)); convertersBuilder.put(field, OptionsParserImpl.findConverter(field)); + + allowMultipleBuilder.put(field, annotation.allowMultiple()); } } return new OptionsData(constructorBuilder, nameToFieldBuilder, abbrevToFieldBuilder, - allOptionsFieldsBuilder, optionDefaultsBuilder, convertersBuilder); + allOptionsFieldsBuilder, optionDefaultsBuilder, convertersBuilder, allowMultipleBuilder); } } diff --git a/src/main/java/com/google/devtools/common/options/OptionsParser.java b/src/main/java/com/google/devtools/common/options/OptionsParser.java index 80e56cbcca..400adee114 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsParser.java +++ b/src/main/java/com/google/devtools/common/options/OptionsParser.java @@ -187,6 +187,41 @@ public class OptionsParser implements OptionsProvider { } /** + * The metadata about an option. + */ + public static final class OptionDescription { + + private final String name; + private final Object defaultValue; + private final Converter<?> converter; + private final boolean allowMultiple; + + public OptionDescription(String name, Object defaultValue, Converter<?> converter, + boolean allowMultiple) { + this.name = name; + this.defaultValue = defaultValue; + this.converter = converter; + this.allowMultiple = allowMultiple; + } + + public String getName() { + return name; + } + + public Object getDefaultValue() { + return defaultValue; + } + + public Converter<?> getConverter() { + return converter; + } + + public boolean getAllowMultiple() { + return allowMultiple; + } + } + + /** * The name and value of an option with additional metadata describing its * priority, source, whether it was set via an implicit dependency, and if so, * by which other option. @@ -217,10 +252,16 @@ public class OptionsParser implements OptionsProvider { return value; } + /** + * @return the priority of the thing that set this value for this flag + */ public OptionPriority getPriority() { return priority; } + /** + * @return the thing that set this value for this flag + */ public String getSource() { return source; } @@ -450,10 +491,24 @@ public class OptionsParser implements OptionsProvider { } /** + * Returns a description of the option. + * + * @return The {@link OptionValueDescription} for the option, or null if there is no option by + * the given name. + */ + public OptionDescription getOptionDescription(String name) { + return impl.getOptionDescription(name); + } + + /** * Returns a description of the option value set by the last previous call to * {@link #parse(OptionPriority, String, List)} that successfully set the given * option. If the option is of type {@link List}, the description will * correspond to any one of the calls, but not necessarily the last. + * + * @return The {@link OptionValueDescription} for the option, or null if the value has not been + * set. + * @throws IllegalArgumentException if there is no option by the given name. */ public OptionValueDescription getOptionValueDescription(String name) { return impl.getOptionValueDescription(name); @@ -520,6 +575,23 @@ public class OptionsParser implements OptionsProvider { } } + /** + * Clears the given option. Also clears expansion arguments and implicit requirements for that + * option. + * + * <p>This will not affect options objects that have already been retrieved from this parser + * through {@link #getOptions(Class)}. + * + * @param optionName The full name of the option to clear. + * @return A map of an option name to the old value of the options that were cleared. + * @throws IllegalArgumentException If the flag does not exist. + */ + public Map<String, OptionValueDescription> clearValue(String optionName) { + Map<String, OptionValueDescription> clearedValues = Maps.newHashMap(); + impl.clearValue(optionName, clearedValues); + return clearedValues; + } + @Override public List<String> getResidue() { return ImmutableList.copyOf(residue); diff --git a/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java b/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java index bae5293439..59d4a0c524 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java +++ b/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java @@ -25,6 +25,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.devtools.common.options.OptionsParser.OptionDescription; import com.google.devtools.common.options.OptionsParser.OptionValueDescription; import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription; @@ -433,6 +434,28 @@ class OptionsParserImpl { entry.addValue(priority, value); } + void clearValue(String optionName, Map<String, OptionValueDescription> clearedValues) { + Field field = optionsData.getFieldFromName(optionName); + if (field == null) { + throw new IllegalArgumentException("No such option '" + optionName + "'"); + } + + ParsedOptionEntry removed = parsedValues.remove(field); + if (removed != null) { + clearedValues.put(optionName, removed.asOptionValueDescription(optionName)); + } + + // Recurse to remove any implicit or expansion flags that this flag may have added when + // originally parsed. + Option option = field.getAnnotation(Option.class); + for (String implicitRequirement : option.implicitRequirements()) { + clearValue(implicitRequirement, clearedValues); + } + for (String expansion : option.expansion()) { + clearValue(expansion, clearedValues); + } + } + private Object getValue(Field field) { ParsedOptionEntry entry = parsedValues.get(field); return entry == null ? null : entry.getValue(); @@ -450,6 +473,20 @@ class OptionsParserImpl { return entry.asOptionValueDescription(name); } + OptionDescription getOptionDescription(String name) { + Field field = optionsData.getFieldFromName(name); + if (field == null) { + return null; + } + + Option optionAnnotation = field.getAnnotation(Option.class); + return new OptionDescription( + name, + optionsData.getDefaultValue(field), + optionsData.getConverter(field), + optionAnnotation.allowMultiple()); + } + boolean containsExplicitOption(String name) { Field field = optionsData.getFieldFromName(name); if (field == null) { @@ -475,7 +512,7 @@ class OptionsParserImpl { * of options; in that case, the arg seen last takes precedence. * * <p>The method uses the invariant that if an option has neither an implicit - * dependant nor an expanded from value, then it must have been explicitly + * dependent nor an expanded from value, then it must have been explicitly * set. */ private List<String> parse(OptionPriority priority, @@ -720,5 +757,4 @@ class OptionsParserImpl { throw new AssertionError(e); } } - } |