aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools
diff options
context:
space:
mode:
authorGravatar Alex Humesky <ahumesky@google.com>2015-09-29 01:42:00 +0000
committerGravatar Florian Weikert <fwe@google.com>2015-09-30 09:33:06 +0000
commit2f3f4cf925a760019fd089dd5ee771a3552fb278 (patch)
treec3950d8da89e776e6c0bccaf0802c5291397cfed /src/main/java/com/google/devtools
parentc2579a5041568592913165bc0a167bd0b70f46a2 (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')
-rw-r--r--src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandDispatcher.java15
-rw-r--r--src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java8
-rw-r--r--src/main/java/com/google/devtools/build/lib/runtime/InvocationPolicyEnforcer.java404
-rw-r--r--src/main/java/com/google/devtools/common/options/Option.java6
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionPriority.java6
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsData.java19
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsParser.java72
-rw-r--r--src/main/java/com/google/devtools/common/options/OptionsParserImpl.java40
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);
}
}
-
}