Parse and load starlark-defined options in full package-path form.

This includes the the following features available to native options parsing:
* --(no)boolean_flag syntax
* --flag=value syntax
* --flag value syntax
* single-dash syntax
* for multiple values of same option, last option wins
* internal-only options are treated like they don't exist

There is potential here to combine some of the code in StarlarkOptionsParser.java with OptionsParserImpl.java since they support the same formats of flags. This is complicated by the fact that for native flags, you have a lot of information (type, internal vs external) before parsing whereas for starlark flags, you need to parse out the name first and load it to get this information. So I haven't done the combining in this CL.

This brings up an interesting point that now all commands that use starlark flags will need to do loading. For now we limit starlark parsing to commands that already do building (which is possibly all the commands we care about).

This behavior is still guarded behind --experimental_build_setting_api

Work towards #5574 and #5577

PiperOrigin-RevId: 223600443
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
index 2f9d6a5..2f6a0e9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
@@ -576,6 +576,8 @@
     return ruleContext.getHostConfiguration();
   }
 
+  // TODO(juliexxia): special-case label-typed build settings so they return the providers of the
+  // target represented by the label instead of the actual label.
   @Override
   @Nullable
   public Object getBuildSettingValue() throws EvalException {
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 e4bf647..d76738f 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
@@ -312,20 +312,7 @@
       // Early exit. We need to guarantee that the ErrOut and Reporter setup below never error out,
       // so any invariants they need must be checked before this point.
       if (!earlyExitCode.equals(ExitCode.SUCCESS)) {
-        // Partial replay of the printed events before we exit.
-        PrintingEventHandler printingEventHandler =
-            new PrintingEventHandler(outErr, EventKind.ALL_EVENTS);
-        for (String note : optionHandler.getRcfileNotes()) {
-          printingEventHandler.handle(Event.info(note));
-        }
-        for (Event event : storedEventHandler.getEvents()) {
-          printingEventHandler.handle(event);
-        }
-        for (Postable post : storedEventHandler.getPosts()) {
-          env.getEventBus().post(post);
-        }
-        result = BlazeCommandResult.exitCode(earlyExitCode);
-        return result;
+        return replayEarlyExitEvents(outErr, optionHandler, storedEventHandler, env, earlyExitCode);
       }
 
       Reporter reporter = env.getReporter();
@@ -480,6 +467,13 @@
         }
       }
 
+      // Parse starlark options.
+      earlyExitCode = optionHandler.parseStarlarkOptions(env, storedEventHandler);
+      if (!earlyExitCode.equals(ExitCode.SUCCESS)) {
+        return replayEarlyExitEvents(outErr, optionHandler, storedEventHandler, env, earlyExitCode);
+      }
+      options = optionHandler.getOptionsResult();
+
       result = command.exec(env, options);
       ExitCode moduleExitCode = env.precompleteCommand(result.getExitCode());
       // If Blaze did not suffer an infrastructure failure, check for errors in modules.
@@ -512,6 +506,26 @@
     }
   }
 
+  private static BlazeCommandResult replayEarlyExitEvents(
+      OutErr outErr,
+      BlazeOptionHandler optionHandler,
+      StoredEventHandler storedEventHandler,
+      CommandEnvironment env,
+      ExitCode earlyExitCode) {
+    PrintingEventHandler printingEventHandler =
+        new PrintingEventHandler(outErr, EventKind.ALL_EVENTS);
+    for (String note : optionHandler.getRcfileNotes()) {
+      printingEventHandler.handle(Event.info(note));
+    }
+    for (Event event : storedEventHandler.getEvents()) {
+      printingEventHandler.handle(event);
+    }
+    for (Postable post : storedEventHandler.getPosts()) {
+      env.getEventBus().post(post);
+    }
+    return BlazeCommandResult.exitCode(earlyExitCode);
+  }
+
   /**
    * For testing ONLY. Same as {@link #exec(InvocationPolicy, List, OutErr, LockingMode, String,
    * long, Optional<List<Pair<String, String>>>)}, but automatically uses the current time.
@@ -529,7 +543,6 @@
         Optional.empty() /* startupOptionBundles */);
   }
 
-
   private OutErr bufferOut(OutErr outErr, boolean fully) {
     OutputStream wrappedOut;
     if (fully) {
@@ -560,7 +573,6 @@
     return OutErr.create(outErr.getOutputStream(), wrappedErr);
   }
 
-
   private OutErr tee(OutErr outErr, List<OutErr> additionalOutErrs) {
     if (additionalOutErrs.isEmpty()) {
       return outErr;
@@ -600,7 +612,7 @@
       throw new IllegalStateException(e);
     }
     Command annotation = command.getClass().getAnnotation(Command.class);
-    OptionsParser parser = OptionsParser.newOptionsParser(optionsData);
+    OptionsParser parser = OptionsParser.newOptionsParser(optionsData, "--//");
     parser.setAllowResidue(annotation.allowResidue());
     return parser;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
index 733ef84..9287d53 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
@@ -227,6 +227,30 @@
   }
 
   /**
+   * TODO(bazel-team): When we move BuildConfiguration.Options options to be defined in starlark,
+   * make sure they're not passed in here during {@link #getOptionsResult}.
+   */
+  ExitCode parseStarlarkOptions(CommandEnvironment env, ExtendedEventHandler eventHandler) {
+    // For now, restrict starlark options to commands that already build to ensure that loading
+    // will work. We may want to open this up to other commands in the future. The "info"
+    // and "clean" commands have builds=true set in their annotation but don't actually do any
+    // building (b/120041419).
+    if (!commandAnnotation.builds()
+        || commandAnnotation.name().equals("info")
+        || commandAnnotation.name().equals("clean")) {
+      return ExitCode.SUCCESS;
+    }
+    try {
+      StarlarkOptionsParser.newStarlarkOptionsParser(env, optionsParser, runtime)
+          .parse(commandAnnotation, eventHandler);
+    } catch (OptionsParsingException e) {
+      eventHandler.handle(Event.error(e.getMessage()));
+      return ExitCode.COMMAND_LINE_ERROR;
+    }
+    return ExitCode.SUCCESS;
+  }
+
+  /**
    * Parses the options, taking care not to generate any output to outErr, return, or throw an
    * exception.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
index fa92bd4..3250b57 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/CommandEnvironment.java
@@ -584,6 +584,22 @@
             options);
   }
 
+  public void syncPackageLoading(
+      PackageCacheOptions packageCacheOptions,
+      SkylarkSemanticsOptions skylarkSemanticsOptions,
+      String defaultsPackageContents)
+      throws AbruptExitException {
+    getSkyframeExecutor()
+        .syncPackageLoading(
+            packageCacheOptions,
+            packageLocator,
+            skylarkSemanticsOptions,
+            defaultsPackageContents,
+            getCommandId(),
+            clientEnv,
+            timestampGranularityMonitor);
+  }
+
   public void recordLastExecutionTime() {
     workspace.recordLastExecutionTime(getCommandStartTime());
   }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java b/src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java
new file mode 100644
index 0000000..fe60f39
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/StarlarkOptionsParser.java
@@ -0,0 +1,263 @@
+// Copyright 2018 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 static com.google.devtools.build.lib.packages.BuildType.LABEL;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
+import static com.google.devtools.build.lib.syntax.Type.BOOLEAN;
+import static com.google.devtools.build.lib.syntax.Type.INTEGER;
+import static com.google.devtools.build.lib.syntax.Type.STRING;
+import static com.google.devtools.build.lib.syntax.Type.STRING_LIST;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.LabelConverter;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.LabelListConverter;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.BuildSetting;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.SkylarkSemanticsOptions;
+import com.google.devtools.build.lib.pkgcache.LoadingOptions;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue;
+import com.google.devtools.build.lib.syntax.Type;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.Converter;
+import com.google.devtools.common.options.Converters.BooleanConverter;
+import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
+import com.google.devtools.common.options.Converters.IntegerConverter;
+import com.google.devtools.common.options.Converters.StringConverter;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An options parser for starlark defined options. Takes a mutable {@link OptionsParser} that has
+ * already parsed all native options (including those needed for loading). This class is in charge
+ * of parsing and setting the starlark options for this {@link OptionsParser}.
+ */
+public class StarlarkOptionsParser {
+
+  private final SkyframeExecutor skyframeExecutor;
+  private final PathFragment relativeWorkingDirectory;
+  private final Reporter reporter;
+  private final OptionsParser nativeOptionsParser;
+
+  private final ImmutableMap<Type<?>, Converter<?>> converters =
+      new ImmutableMap.Builder<Type<?>, Converter<?>>()
+          .put(INTEGER, new IntegerConverter())
+          .put(BOOLEAN, new BooleanConverter())
+          .put(STRING, new StringConverter())
+          .put(STRING_LIST, new CommaSeparatedOptionListConverter())
+          .put(LABEL, new LabelConverter())
+          .put(LABEL_LIST, new LabelListConverter())
+          .build();
+
+  private StarlarkOptionsParser(
+      SkyframeExecutor skyframeExecutor,
+      PathFragment relativeWorkingDirectory,
+      Reporter reporter,
+      OptionsParser nativeOptionsParser) {
+    this.skyframeExecutor = skyframeExecutor;
+    this.relativeWorkingDirectory = relativeWorkingDirectory;
+    this.reporter = reporter;
+    this.nativeOptionsParser = nativeOptionsParser;
+  }
+
+  static StarlarkOptionsParser newStarlarkOptionsParser(
+      CommandEnvironment env, OptionsParser optionsParser, BlazeRuntime runtime)
+      throws OptionsParsingException {
+    try {
+      env.syncPackageLoading(
+          optionsParser.getOptions(PackageCacheOptions.class),
+          optionsParser.getOptions(SkylarkSemanticsOptions.class),
+          runtime.getDefaultsPackageContent());
+    } catch (AbruptExitException e) {
+      throw new OptionsParsingException(e.getMessage());
+    }
+    return new StarlarkOptionsParser(
+        env.getSkyframeExecutor(),
+        env.getRelativeWorkingDirectory(),
+        env.getReporter(),
+        optionsParser);
+  }
+
+  // TODO(juliexxia): This method somewhat reinvents the wheel of
+  // OptionsParserImpl.identifyOptionAndPossibleArgument. Consider combining. This would probably
+  // require multiple rounds of parsing to fit starlark-defined options into native option format.
+  @VisibleForTesting
+  public void parse(Command command, ExtendedEventHandler eventHandler)
+      throws OptionsParsingException {
+    ImmutableList.Builder<String> residue = new ImmutableList.Builder<>();
+    // Map of <option name (label), <unparsed option value, loaded option>>.
+    Map<String, Pair<String, BuildSetting>> unparsedOptions =
+        Maps.newHashMapWithExpectedSize(nativeOptionsParser.getResidue().size());
+
+    // sort the old residue into starlark flags and legitimate residue
+    Iterator<String> unparsedArgs = nativeOptionsParser.getPreDoubleDashResidue().iterator();
+    while (unparsedArgs.hasNext()) {
+      String arg = unparsedArgs.next();
+
+      // TODO(bazel-team): support single dash options?
+      if (!arg.startsWith("--")) {
+        residue.add(arg);
+        continue;
+      }
+
+      parseArg(arg, unparsedArgs, unparsedOptions, command, eventHandler);
+    }
+    residue.addAll(nativeOptionsParser.getPostDoubleDashResidue());
+    nativeOptionsParser.setResidue(residue.build());
+
+    if (unparsedOptions.isEmpty()) {
+      return;
+    }
+
+    ImmutableMap.Builder<String, Object> parsedOptions = new ImmutableMap.Builder<>();
+    for (Map.Entry<String, Pair<String, BuildSetting>> option : unparsedOptions.entrySet()) {
+      String loadedFlag = option.getKey();
+      String unparsedValue = option.getValue().first;
+      BuildSetting buildSetting = option.getValue().second;
+      // Do not recognize internal options, which are treated as if they did not exist.
+      if (!buildSetting.isFlag()) {
+        throw new OptionsParsingException(
+            String.format("Unrecognized option: %s=%s", loadedFlag, unparsedValue));
+      }
+      Type<?> type = buildSetting.getType();
+      Converter<?> converter = converters.get(type);
+      Object value;
+      try {
+        value = converter.convert(unparsedValue);
+      } catch (OptionsParsingException e) {
+        throw new OptionsParsingException(
+            String.format(
+                "While parsing option %s=%s: '%s' is not a %s",
+                loadedFlag, unparsedValue, unparsedValue, type),
+            e);
+      }
+      parsedOptions.put(loadedFlag, value);
+    }
+    // TODO(juliexxia): change this method to setStarlarkOptions
+    nativeOptionsParser.setStarlarkOptions(parsedOptions.build());
+  }
+
+  private void parseArg(
+      String arg,
+      Iterator<String> unparsedArgs,
+      Map<String, Pair<String, BuildSetting>> unparsedOptions,
+      Command command,
+      ExtendedEventHandler eventHandler)
+      throws OptionsParsingException {
+    int equalsAt = arg.indexOf('=');
+    String name = equalsAt == -1 ? arg.substring(2) : arg.substring(2, equalsAt);
+    if (name.trim().isEmpty()) {
+      throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
+    }
+    String value = equalsAt == -1 ? null : arg.substring(equalsAt + 1);
+
+    if (value != null) {
+      // --flag=value or -flag=value form
+      BuildSetting current = loadBuildSetting(name, nativeOptionsParser, command, eventHandler);
+      unparsedOptions.put(name, new Pair<>(value, current));
+    } else {
+      boolean booleanValue = true;
+      // check --noflag form
+      if (name.startsWith("no")) {
+        booleanValue = false;
+        name = name.substring(2);
+      }
+      BuildSetting current = loadBuildSetting(name, nativeOptionsParser, command, eventHandler);
+      if (current.getType().equals(BOOLEAN)) {
+        // --boolean_flag or --noboolean_flag
+        unparsedOptions.put(name, new Pair<>(String.valueOf(booleanValue), current));
+      } else {
+        if (!booleanValue) {
+          // --no(non_boolean_flag)
+          throw new OptionsParsingException(
+              "Illegal use of 'no' prefix on non-boolean option: " + name, name);
+        }
+        if (unparsedArgs.hasNext()) {
+          // --flag value
+          unparsedOptions.put(name, new Pair<>(unparsedArgs.next(), current));
+        } else {
+          throw new OptionsParsingException("Expected value after " + arg);
+        }
+      }
+    }
+  }
+
+  private BuildSetting loadBuildSetting(
+      String targetToBuild,
+      OptionsParser optionsParser,
+      Command command,
+      ExtendedEventHandler eventHandler)
+      throws OptionsParsingException {
+    Rule associatedRule;
+    try {
+      TargetPatternPhaseValue result =
+          skyframeExecutor.loadTargetPatterns(
+              reporter,
+              Collections.singletonList(targetToBuild),
+              relativeWorkingDirectory,
+              optionsParser.getOptions(LoadingOptions.class),
+              SkyframeExecutor.DEFAULT_THREAD_COUNT,
+              optionsParser.getOptions(KeepGoingOption.class).keepGoing,
+              command.name().equals("test"));
+      associatedRule =
+          Iterables.getOnlyElement(
+                  result.getTargets(eventHandler, skyframeExecutor.getPackageManager()))
+              .getAssociatedRule();
+    } catch (InterruptedException | TargetParsingException e) {
+      Thread.currentThread().interrupt();
+      throw new OptionsParsingException(
+          "Error loading option " + targetToBuild + ": " + e.getMessage(), e);
+    }
+    if (associatedRule == null || associatedRule.getRuleClassObject().getBuildSetting() == null) {
+      throw new OptionsParsingException("Unrecognized option: " + targetToBuild);
+    }
+    return associatedRule.getRuleClassObject().getBuildSetting();
+  }
+
+  @VisibleForTesting
+  public static StarlarkOptionsParser newStarlarkOptionsParserForTesting(
+      SkyframeExecutor skyframeExecutor,
+      Reporter reporter,
+      PathFragment relativeWorkingDirectory,
+      OptionsParser nativeOptionsParser) {
+    return new StarlarkOptionsParser(
+        skyframeExecutor, relativeWorkingDirectory, reporter, nativeOptionsParser);
+  }
+
+  @VisibleForTesting
+  public void setResidueForTesting(List<String> residue) {
+    nativeOptionsParser.setResidue(residue);
+  }
+
+  @VisibleForTesting
+  public OptionsParser getNativeOptionsParserFortesting() {
+    return nativeOptionsParser;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index 16e52cbf..36f9282 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -2277,7 +2277,7 @@
         tsgm);
   }
 
-  protected void syncPackageLoading(
+  public void syncPackageLoading(
       PackageCacheOptions packageCacheOptions,
       PathPackageLocator pathPackageLocator,
       SkylarkSemanticsOptions skylarkSemanticsOptions,
@@ -2565,3 +2565,4 @@
     return buildDriver.evaluate(roots, evaluationContext);
   }
 }
+
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 fb0b9e3..00077c5 100644
--- a/src/main/java/com/google/devtools/common/options/OptionsParser.java
+++ b/src/main/java/com/google/devtools/common/options/OptionsParser.java
@@ -14,7 +14,6 @@
 
 package com.google.devtools.common.options;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Throwables;
@@ -176,8 +175,18 @@
     return new OptionsParser((OptionsData) optionsData);
   }
 
+  /**
+   * Create a new {@link OptionsParser}, using {@link OpaqueOptionsData} previously returned from
+   * {@link #getOptionsData} and a prefix that signifies the parser should skip parsing args that
+   * begin with that prefix.
+   */
+  public static OptionsParser newOptionsParser(
+      OpaqueOptionsData optionsData, String skippedPrefix) {
+    return new OptionsParser((OptionsData) optionsData, skippedPrefix);
+  }
+
   private final OptionsParserImpl impl;
-  private final List<String> residue = new ArrayList<String>();
+  private List<String> residue = new ArrayList<>();
   private final List<String> postDoubleDashResidue = new ArrayList<>();
   private boolean allowResidue = true;
   private Map<String, Object> starlarkOptions = new HashMap<>();
@@ -186,6 +195,10 @@
     impl = new OptionsParserImpl(optionsData);
   }
 
+  OptionsParser(OptionsData optionsData, String skippedPrefix) {
+    impl = new OptionsParserImpl(optionsData, skippedPrefix);
+  }
+
   /**
    * Indicates whether or not the parser will allow a non-empty residue; that
    * is, iff this value is true then a call to one of the {@code parse}
@@ -201,8 +214,7 @@
     return starlarkOptions;
   }
 
-  @VisibleForTesting
-  public void setStarlarkOptionsForTesting(Map<String, Object> starlarkOptions) {
+  public void setStarlarkOptions(Map<String, Object> starlarkOptions) {
     this.starlarkOptions = starlarkOptions;
   }
 
@@ -706,6 +718,14 @@
             .collect(Collectors.toList());
   }
 
+  public List<String> getPostDoubleDashResidue() {
+    return postDoubleDashResidue;
+  }
+
+  public void setResidue(List<String> residue) {
+    this.residue = residue;
+  }
+
   /** Returns a list of warnings about problems encountered by previous parse calls. */
   public List<String> getWarnings() {
     return impl.getWarnings();
@@ -905,3 +925,4 @@
             + "}");
   }
 }
+
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 135e808..23fca45 100644
--- a/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java
+++ b/src/main/java/com/google/devtools/common/options/OptionsParserImpl.java
@@ -86,10 +86,23 @@
 
   private ArgsPreProcessor argsPreProcessor = args -> args;
 
+  private final String skippedPrefix;
+
   /** Create a new parser object. Do not accept a null OptionsData object. */
   OptionsParserImpl(OptionsData optionsData) {
     Preconditions.checkNotNull(optionsData);
     this.optionsData = optionsData;
+    this.skippedPrefix = null;
+  }
+
+  /**
+   * Creates a new parser object. Do not accept a null OptionsData object. Takes a prefix that
+   * signifies the parser should skip parsing args that begin with that prefix.
+   */
+  OptionsParserImpl(OptionsData optionsData, String skippedPrefix) {
+    Preconditions.checkNotNull(optionsData);
+    this.optionsData = optionsData;
+    this.skippedPrefix = skippedPrefix;
   }
 
   OptionsData getOptionsData() {
@@ -312,6 +325,11 @@
         continue; // not an option arg
       }
 
+      if (skippedPrefix != null && arg.startsWith(skippedPrefix)) {
+        unparsedArgs.add(arg);
+        continue;
+      }
+
       if (arg.equals("--")) { // "--" means all remaining args aren't options
         Iterators.addAll(unparsedPostDoubleDashArgs, argsIterator);
         break;
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
index a33f235..bf69366 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/util/BuildViewTestCase.java
@@ -342,7 +342,7 @@
     return ResourceSet.createWithRamCpu(Double.MAX_VALUE, Double.MAX_VALUE);
   }
 
-  private BuildConfigurationCollection createConfigurations(
+  protected final BuildConfigurationCollection createConfigurations(
       ImmutableMap<String, Object> skylarkOptions, String... args) throws Exception {
     optionsParser =
         OptionsParser.newOptionsParser(
@@ -359,7 +359,7 @@
     optionsParser.parse(args);
 
     // TODO(juliexxia): when the starlark options parsing work goes in, add type verification here.
-    optionsParser.setStarlarkOptionsForTesting(skylarkOptions);
+    optionsParser.setStarlarkOptions(skylarkOptions);
 
     InvocationPolicyEnforcer optionsPolicyEnforcer =
         getAnalysisMock().getInvocationPolicyEnforcer();
@@ -2216,3 +2216,4 @@
     }
   }
 }
+
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/BUILD b/src/test/java/com/google/devtools/build/lib/skylark/BUILD
index 0fdccf6..6f3d861 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skylark/BUILD
@@ -55,8 +55,11 @@
         "//src/main/java/com/google/devtools/build/lib:build-base",
         "//src/main/java/com/google/devtools/build/lib:classpath-util",
         "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:exitcode-external",
+        "//src/main/java/com/google/devtools/build/lib:keep-going-option",
         "//src/main/java/com/google/devtools/build/lib:packages",
         "//src/main/java/com/google/devtools/build/lib:python-rules",
+        "//src/main/java/com/google/devtools/build/lib:runtime",
         "//src/main/java/com/google/devtools/build/lib:skylark_semantics",
         "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
         "//src/main/java/com/google/devtools/build/lib:util",
@@ -71,6 +74,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
+        "//src/main/java/com/google/devtools/common/options",
         "//src/test/java/com/google/devtools/build/lib:actions_testutil",
         "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
         "//src/test/java/com/google/devtools/build/lib:foundations_testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/StarlarkOptionsParsingTest.java b/src/test/java/com/google/devtools/build/lib/skylark/StarlarkOptionsParsingTest.java
new file mode 100644
index 0000000..c47347b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skylark/StarlarkOptionsParsingTest.java
@@ -0,0 +1,399 @@
+// Copyright 2018 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.skylark;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.SkylarkSemanticsOptions;
+import com.google.devtools.build.lib.pkgcache.LoadingOptions;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.runtime.BlazeCommand;
+import com.google.devtools.build.lib.runtime.BlazeCommandEventHandler.Options;
+import com.google.devtools.build.lib.runtime.BlazeCommandResult;
+import com.google.devtools.build.lib.runtime.ClientOptions;
+import com.google.devtools.build.lib.runtime.Command;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.runtime.CommonCommandOptions;
+import com.google.devtools.build.lib.runtime.KeepGoingOption;
+import com.google.devtools.build.lib.runtime.StarlarkOptionsParser;
+import com.google.devtools.build.lib.skylark.util.SkylarkTestCase;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.OptionsParsingResult;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit test for the {@code StarlarkOptionsParser}. */
+@RunWith(JUnit4.class)
+public class StarlarkOptionsParsingTest extends SkylarkTestCase {
+
+  private StarlarkOptionsParser starlarkOptionsParser;
+
+  private static final List<Class<? extends OptionsBase>> requiredOptionsClasses =
+      ImmutableList.of(
+          PackageCacheOptions.class,
+          SkylarkSemanticsOptions.class,
+          KeepGoingOption.class,
+          LoadingOptions.class,
+          ClientOptions.class,
+          Options.class,
+          CommonCommandOptions.class);
+
+  @Before
+  public void setUp() throws Exception {
+    optionsParser =
+        OptionsParser.newOptionsParser(
+            Iterables.concat(requiredOptionsClasses, ruleClassProvider.getConfigurationOptions()));
+    starlarkOptionsParser =
+        StarlarkOptionsParser.newStarlarkOptionsParserForTesting(
+            skyframeExecutor, reporter, PathFragment.EMPTY_FRAGMENT, optionsParser);
+  }
+
+  @Command(
+      name = "residue",
+      builds = true,
+      options = {
+        PackageCacheOptions.class,
+        SkylarkSemanticsOptions.class,
+        KeepGoingOption.class,
+        LoadingOptions.class,
+        ClientOptions.class,
+        Options.class,
+      },
+      allowResidue = true,
+      shortDescription =
+          "a dummy command for testing that allows residue and recognizes all"
+              + " relevant options for starlark options parsing.",
+      help = "")
+  private static class ResidueCommand implements BlazeCommand {
+    @Override
+    public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
+      return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
+    }
+
+    @Override
+    public void editOptions(OptionsParser optionsParser) {}
+  }
+
+  private OptionsParsingResult parseStarlarkOptions(String options) throws Exception {
+    starlarkOptionsParser.setResidueForTesting(Arrays.asList(options.split(" ")));
+    starlarkOptionsParser.parse(
+        ResidueCommand.class.getAnnotation(Command.class), new StoredEventHandler());
+    return starlarkOptionsParser.getNativeOptionsParserFortesting();
+  }
+
+  private void writeBuildSetting(String type, String defaultValue, boolean isFlag)
+      throws Exception {
+    setSkylarkSemanticsOptions("--experimental_build_setting_api=True");
+
+    String flag = isFlag ? "True" : "False";
+
+    scratch.file(
+        "test/build_setting.bzl",
+        "def _build_setting_impl(ctx):",
+        "  return []",
+        type + "_setting = rule(",
+        "  implementation = _build_setting_impl,",
+        "  build_setting = config." + type + "(flag=" + flag + ")",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:build_setting.bzl', '" + type + "_setting')",
+        type
+            + "_setting(name = 'my_"
+            + type
+            + "_setting', build_setting_default = "
+            + defaultValue
+            + ")");
+  }
+
+  private void writeBasicIntFlag() throws Exception {
+    writeBuildSetting("int", "42", true);
+  }
+
+  // test --flag=value
+  @Test
+  public void testFlagEqualsValueForm() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting=666");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(666);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --flag value
+  @Test
+  public void testFlagSpaceValueForm() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_int_setting 666");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(666);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --fake_flag=value
+  @Test
+  public void testBadFlag_equalsForm() throws Exception {
+    scratch.file("test/BUILD");
+    reporter.removeHandler(failFastHandler);
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//fake_flag=blahblahblah"));
+
+    assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
+  }
+
+  // test --fake_flag value
+  @Test
+  public void testBadFlag_spaceForm() throws Exception {
+    scratch.file("test/BUILD");
+    reporter.removeHandler(failFastHandler);
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//fake_flag blahblahblah"));
+
+    assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
+  }
+
+  // test --fake_flag
+  @Test
+  public void testBadFlag_boolForm() throws Exception {
+    scratch.file("test/BUILD");
+    reporter.removeHandler(failFastHandler);
+
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//fake_flag"));
+
+    assertThat(e).hasMessageThat().contains("Error loading option //fake_flag");
+  }
+
+  // test -flag=value (Note - there's currently no way in real life to allow single dash long form
+  // options.)
+  @Test
+  public void testSingleDash_notAllowed() throws Exception {
+    optionsParser.setAllowSingleDashLongOptions(false);
+
+    writeBasicIntFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("-//test:my_int_setting=666");
+
+    assertThat(result.getStarlarkOptions()).isEmpty();
+    assertThat(result.getResidue()).containsExactly("-//test:my_int_setting=666");
+  }
+
+  // test --non_flag_setting=value
+  @Test
+  public void testNonFlagParsing() throws Exception {
+    setSkylarkSemanticsOptions("--experimental_build_setting_api=True");
+
+    scratch.file(
+        "test/build_setting.bzl",
+        "def _build_setting_impl(ctx):",
+        "  return []",
+        "int_flag = rule(",
+        "  implementation = _build_setting_impl,",
+        "  build_setting = config.int(flag=False)",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:build_setting.bzl', 'int_flag')",
+        "int_flag(name = 'my_int_setting', build_setting_default = 42)");
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//test:my_int_setting=666"));
+
+    assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:my_int_setting=666");
+  }
+
+  private void writeBasicBoolFlag() throws Exception {
+    writeBuildSetting("bool", "True", true);
+  }
+
+  // test --bool_flag
+  @Test
+  public void testBooleanFlag() throws Exception {
+    writeBasicBoolFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--//test:my_bool_setting");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_bool_setting")).isEqualTo(true);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --nobool_flag
+  @Test
+  public void testNoPrefixedBooleanFlag() throws Exception {
+    writeBasicBoolFlag();
+
+    OptionsParsingResult result = parseStarlarkOptions("--no//test:my_bool_setting");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_bool_setting")).isEqualTo(false);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --noint_flag
+  @Test
+  public void testNoPrefixedNonBooleanFlag() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class, () -> parseStarlarkOptions("--no//test:my_int_setting"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Illegal use of 'no' prefix on non-boolean option: //test:my_int_setting");
+  }
+
+  // test --int_flag
+  @Test
+  public void testFlagWithoutValue() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class, () -> parseStarlarkOptions("--//test:my_int_setting"));
+
+    assertThat(e).hasMessageThat().isEqualTo("Expected value after --//test:my_int_setting");
+  }
+
+  // test --flag --flag
+  @Test
+  public void testRepeatFlagLastOneWins() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingResult result =
+        parseStarlarkOptions("--//test:my_int_setting=4 --//test:my_int_setting=7");
+
+    assertThat(result.getStarlarkOptions()).hasSize(1);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(7);
+    assertThat(result.getResidue()).isEmpty();
+  }
+
+  // test --flagA=valueA --flagB=valueB
+  @Test
+  public void testMultipleFlags() throws Exception {
+    setSkylarkSemanticsOptions("--experimental_build_setting_api=True");
+
+    scratch.file(
+        "test/build_setting.bzl",
+        "def _build_setting_impl(ctx):",
+        "  return []",
+        "int_flag = rule(",
+        "  implementation = _build_setting_impl,",
+        "  build_setting = config.int(flag=True)",
+        ")");
+    scratch.file(
+        "test/BUILD",
+        "load('//test:build_setting.bzl', 'int_flag')",
+        "int_flag(name = 'my_int_setting', build_setting_default = 42)",
+        "int_flag(name = 'my_other_int_setting', build_setting_default = 77)");
+
+    OptionsParsingResult result =
+        parseStarlarkOptions("--//test:my_int_setting=0 --//test:my_other_int_setting=0");
+
+    assertThat(result.getResidue()).isEmpty();
+    assertThat(result.getStarlarkOptions()).hasSize(2);
+    assertThat(result.getStarlarkOptions().get("//test:my_int_setting")).isEqualTo(0);
+    assertThat(result.getStarlarkOptions().get("//test:my_other_int_setting")).isEqualTo(0);
+  }
+
+  // test --non_build_setting
+  @Test
+  public void testNonBuildSetting() throws Exception {
+    scratch.file(
+        "test/rules.bzl",
+        "def _impl(ctx):",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        ")");
+    scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'my_rule')");
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:my_rule"));
+    assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:my_rule");
+  }
+
+  // test --non_rule_configured_target
+  @Test
+  public void testNonRuleConfiguredTarget() throws Exception {
+    scratch.file(
+        "test/BUILD",
+        "genrule(",
+        "  name = 'my_gen',",
+        "  srcs = ['x.in'],",
+        "  outs = ['x.cc'],",
+        "  cmd = '$(locations :tool) $< >$@',",
+        "  tools = [':tool'],",
+        ")",
+        "cc_library(name = 'tool-dep')");
+    OptionsParsingException e =
+        assertThrows(OptionsParsingException.class, () -> parseStarlarkOptions("--//test:x.in"));
+    assertThat(e).hasMessageThat().isEqualTo("Unrecognized option: //test:x.in");
+  }
+
+  // test --int_flag=non_int_value
+  @Test
+  public void testWrongValueType_int() throws Exception {
+    writeBasicIntFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//test:my_int_setting=woohoo"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("While parsing option //test:my_int_setting=woohoo: 'woohoo' is not a int");
+  }
+
+  // test --bool_flag=non_bool_value
+  @Test
+  public void testWrongValueType_bool() throws Exception {
+    writeBasicBoolFlag();
+
+    OptionsParsingException e =
+        assertThrows(
+            OptionsParsingException.class,
+            () -> parseStarlarkOptions("--//test:my_bool_setting=woohoo"));
+
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("While parsing option //test:my_bool_setting=woohoo: 'woohoo' is not a boolean");
+  }
+}
diff --git a/src/test/shell/integration/starlark_configurations_test.sh b/src/test/shell/integration/starlark_configurations_test.sh
new file mode 100755
index 0000000..984d626
--- /dev/null
+++ b/src/test/shell/integration/starlark_configurations_test.sh
@@ -0,0 +1,227 @@
+#!/bin/bash
+#
+# Copyright 2018 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.
+#
+# starlark_configuration_test.sh: integration tests for starlark build configurations
+
+# --- begin runfiles.bash initialization ---
+# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash).
+set -euo pipefail
+if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  if [[ -f "$0.runfiles_manifest" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
+  elif [[ -f "$0.runfiles/MANIFEST" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
+  elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+    export RUNFILES_DIR="$0.runfiles"
+  fi
+fi
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+  source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
+elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
+            "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
+else
+  echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
+  exit 1
+fi
+# --- end runfiles.bash initialization ---
+
+source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
+  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
+
+case "$(uname -s | tr [:upper:] [:lower:])" in
+msys*|mingw*|cygwin*)
+  declare -r is_windows=true
+  ;;
+*)
+  declare -r is_windows=false
+  ;;
+esac
+
+if "$is_windows"; then
+  export MSYS_NO_PATHCONV=1
+  export MSYS2_ARG_CONV_EXCL="*"
+fi
+
+add_to_bazelrc "build --package_path=%workspace%"
+
+#### HELPER FXNS #######################################################
+
+function write_build_setting_bzl() {
+ cat > $pkg/build_setting.bzl <<EOF
+BuildSettingInfo = provider(fields = ['name', 'value'])
+
+def _build_setting_impl(ctx):
+  return [BuildSettingInfo(name = ctx.attr.name, value = ctx.build_setting_value)]
+
+drink_attribute = rule(
+  implementation = _build_setting_impl,
+  build_setting = config.string(flag = True),
+)
+EOF
+
+  cat > $pkg/rules.bzl <<EOF
+load("//$pkg:build_setting.bzl", "BuildSettingInfo")
+
+def _impl(ctx):
+  _type_name = ctx.attr._type[BuildSettingInfo].name
+  _type_setting = ctx.attr._type[BuildSettingInfo].value
+  print(_type_name + "=" + str(_type_setting))
+  _temp_name = ctx.attr._temp[BuildSettingInfo].name
+  _temp_setting = ctx.attr._temp[BuildSettingInfo].value
+  print(_temp_name + "=" + str(_temp_setting))
+  print("strict_java_deps=" + ctx.fragments.java.strict_java_deps)
+
+drink = rule(
+  implementation = _impl,
+  attrs = {
+    "_type":attr.label(default = Label("//$pkg:type")),
+    "_temp":attr.label(default = Label("//$pkg:temp")),
+  },
+  fragments = ["java"],
+)
+EOF
+
+  cat > $pkg/BUILD <<EOF
+load("//$pkg:build_setting.bzl", "drink_attribute")
+load("//$pkg:rules.bzl", "drink")
+
+drink(name = 'my_drink')
+
+drink_attribute(name = 'type', build_setting_default = 'unknown')
+drink_attribute(name = 'temp', build_setting_default = 'unknown')
+EOF
+}
+
+#### TESTS #############################################################
+
+function test_default_flag() {
+ local -r pkg=$FUNCNAME
+ mkdir -p $pkg
+
+  write_build_setting_bzl
+
+  bazel build //$pkg:my_drink --experimental_build_setting_api > output \
+    2>"$TEST_log" || fail "Expected success"
+
+  expect_log "type=unknown"
+}
+
+function test_set_flag() {
+  local -r pkg=$FUNCNAME
+  mkdir -p $pkg
+
+  write_build_setting_bzl
+
+  bazel build //$pkg:my_drink --//$pkg:type="coffee" \
+    --experimental_build_setting_api > output 2>"$TEST_log" \
+    || fail "Expected success"
+
+  expect_log "type=coffee"
+}
+
+function test_starlark_and_native_flag() {
+  local -r pkg=$FUNCNAME
+  mkdir -p $pkg
+
+  write_build_setting_bzl
+
+  bazel build //$pkg:my_drink --//$pkg:type=coffee --strict_java_deps=off \
+    --experimental_build_setting_api > output 2>"$TEST_log" \
+    || fail "Expected success"
+
+  expect_log "type=coffee"
+  expect_log "strict_java_deps=off"
+}
+
+function test_dont_parse_flags_after_dash_dash() {
+  local -r pkg=$FUNCNAME
+  mkdir -p $pkg
+
+  write_build_setting_bzl
+
+  bazel build //$pkg:my_drink --//$pkg:type=coffee \
+    --experimental_build_setting_api -- --//$pkg:temp=iced \
+     > output 2>"$TEST_log" \
+    && fail "Expected failure"
+
+  expect_log "invalid package name '-//test_dont_parse_flags_after_dash_dash'"
+}
+
+function test_doesnt_work_without_experimental_flag() {
+  local -r pkg=$FUNCNAME
+  mkdir -p $pkg
+
+  write_build_setting_bzl
+
+  bazel build //$pkg:my_drink --//$pkg:type=coffee > output 2>"$TEST_log" \
+    && fail "Expected failure"
+
+  expect_log "Error loading option //$pkg:type:"
+  expect_log "Extension file '$pkg/build_setting.bzl' has errors"
+}
+
+function test_multiple_starlark_flags() {
+  local -r pkg=$FUNCNAME
+  mkdir -p $pkg
+
+  write_build_setting_bzl
+
+  bazel build //$pkg:my_drink --//$pkg:type="coffee" --//$pkg:temp="iced" \
+    --experimental_build_setting_api > output 2>"$TEST_log" \
+    || fail "Expected success"
+
+  expect_log "type=coffee"
+  expect_log "temp=iced"
+
+  # Ensure that order doesn't matter.
+  bazel build //$pkg:my_drink --//$pkg:temp="iced" --//$pkg:type="coffee" \
+    --experimental_build_setting_api > output 2>"$TEST_log" \
+    || fail "Expected success"
+
+  expect_log "type=coffee"
+  expect_log "temp=iced"
+}
+
+function test_flag_default_change() {
+  local -r pkg=$FUNCNAME
+  mkdir -p $pkg
+
+  write_build_setting_bzl
+
+  bazel build //$pkg:my_drink --experimental_build_setting_api > output \
+    2>"$TEST_log" || fail "Expected success"
+
+  expect_log "type=unknown"
+
+  cat > $pkg/BUILD <<EOF
+load("//$pkg:build_setting.bzl", "drink_attribute")
+load("//$pkg:rules.bzl", "drink")
+
+drink(name = 'my_drink')
+
+drink_attribute(name = 'type', build_setting_default = 'cowabunga')
+drink_attribute(name = 'temp', build_setting_default = 'cowabunga')
+EOF
+
+  bazel build //$pkg:my_drink --experimental_build_setting_api > output \
+    2>"$TEST_log" || fail "Expected success"
+
+  expect_log "type=cowabunga"
+}
+
+
+run_suite "${PRODUCT_NAME} starlark configurations tests"