When --enforce_project_configs is enabled for a valid --scl_config, do not allow any other flags to be set in the user blazerc (~/.blazerc or --blazerc) or command line.
PiperOrigin-RevId: 686252332
Change-Id: I20a1de3a261b9b75141ebe1661ee08e6e6e732d3
diff --git a/src/main/cpp/option_processor.cc b/src/main/cpp/option_processor.cc
index 9fb9422..df08aa0 100644
--- a/src/main/cpp/option_processor.cc
+++ b/src/main/cpp/option_processor.cc
@@ -657,7 +657,9 @@
const std::vector<std::string>& env) {
// Provide terminal options as coming from the least important rc file.
std::vector<std::string> result = {
+ // LINT.IfChange
"--rc_source=client",
+ // LINT.ThenChange(src/main/java/com/google/devtools/common/options/GlobalRcUtils.java)
"--default_override=0:common=--isatty=" +
blaze_util::ToString(IsStandardTerminal()),
"--default_override=0:common=--terminal_columns=" +
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/Project.java b/src/main/java/com/google/devtools/build/lib/analysis/Project.java
index 0353aff..2facd49 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/Project.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/Project.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
@@ -120,6 +121,7 @@
public static FlagSetValue modifyBuildOptionsWithFlagSets(
Label projectFile,
BuildOptions targetOptions,
+ ImmutableSet<String> userOptions,
boolean enforceCanonicalConfigs,
ExtendedEventHandler eventHandler,
SkyframeExecutor skyframeExecutor)
@@ -130,6 +132,7 @@
projectFile,
targetOptions.get(CoreOptions.class).sclConfig,
targetOptions,
+ userOptions,
enforceCanonicalConfigs);
EvaluationResult<SkyValue> result =
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
index 02f3a22..b0ce8b4 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
@@ -202,6 +202,7 @@
static ProjectEvaluationResult evaluateProjectFile(
BuildRequest request,
BuildOptions buildOptions,
+ ImmutableSet<String> userOptions,
TargetPatternPhaseValue targetPatternPhaseValue,
CommandEnvironment env)
throws LoadingFailedException, InvalidConfigurationException {
@@ -263,6 +264,7 @@
buildOptions =
BuildTool.applySclConfigs(
buildOptions,
+ userOptions,
projectFile,
request.getBuildOptions().enforceProjectConfigs,
env.getSkyframeExecutor(),
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
index 3496572..067fa91 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildRequest.java
@@ -21,6 +21,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.AnalysisOptions;
import com.google.devtools.build.lib.analysis.AspectCollection;
import com.google.devtools.build.lib.analysis.OutputGroupInfo;
@@ -52,10 +53,9 @@
import javax.annotation.Nullable;
/**
- * A BuildRequest represents a single invocation of the build tool by a user.
- * A request specifies a list of targets to be built for a single
- * configuration, a pair of output/error streams, and additional options such
- * as --keep_going, --jobs, etc.
+ * A BuildRequest represents a single invocation of the build tool by a user. A request specifies a
+ * list of targets to be built for a single configuration, a pair of output/error streams, and
+ * additional options such as --keep_going, --jobs, etc.
*/
public class BuildRequest implements OptionsProvider {
@@ -189,10 +189,7 @@
/** A human-readable description of all the non-default option settings. */
private final String optionsDescription;
- /**
- * The name of the Blaze command that the user invoked.
- * Used for --announce.
- */
+ /** The name of the Blaze command that the user invoked. Used for --announce. */
private final String commandName;
private final OutErr outErr;
@@ -205,6 +202,7 @@
private final boolean runTests;
private final boolean checkForActionConflicts;
private final boolean reportIncompatibleTargets;
+ private final ImmutableSet<String> userOptions;
private BuildRequest(
String commandName,
@@ -224,6 +222,8 @@
this.targets = targets;
this.id = id;
this.startTimeMillis = startTimeMillis;
+ this.userOptions =
+ options.getUserOptions() == null ? ImmutableSet.of() : options.getUserOptions();
this.optionsCache =
Caffeine.newBuilder()
.build(
@@ -278,15 +278,20 @@
}
/**
- * Returns a unique identifier that universally identifies this build.
+ * Returns the list of options that were parsed from either a user blazerc file or the command
+ * line.
*/
+ @Override
+ public ImmutableSet<String> getUserOptions() {
+ return userOptions;
+ }
+
+ /** Returns a unique identifier that universally identifies this build. */
public UUID getId() {
return id;
}
- /**
- * Returns the name of the Blaze command that the user invoked.
- */
+ /** Returns the name of the Blaze command that the user invoked. */
public String getCommandName() {
return commandName;
}
@@ -295,24 +300,19 @@
return runningInEmacs;
}
- /**
- * Returns true if tests should be run by the build tool.
- */
+ /** Returns true if tests should be run by the build tool. */
public boolean shouldRunTests() {
return runTests;
}
- /**
- * Returns the (immutable) list of targets to build in commandline
- * form.
- */
+ /** Returns the (immutable) list of targets to build in commandline form. */
public List<String> getTargets() {
return targets;
}
/**
- * Returns the output/error streams to which errors and progress messages
- * should be sent during the fulfillment of this request.
+ * Returns the output/error streams to which errors and progress messages should be sent during
+ * the fulfillment of this request.
*/
public OutErr getOutErr() {
return outErr;
@@ -324,10 +324,7 @@
return (T) optionsCache.get(clazz).orNull();
}
-
- /**
- * Returns the set of command-line options specified for this request.
- */
+ /** Returns the set of command-line options specified for this request. */
public BuildRequestOptions getBuildOptions() {
return getOptions(BuildRequestOptions.class);
}
@@ -337,17 +334,12 @@
return getOptions(PackageOptions.class);
}
- /**
- * Returns the set of options related to the loading phase.
- */
+ /** Returns the set of options related to the loading phase. */
public LoadingOptions getLoadingOptions() {
return getOptions(LoadingOptions.class);
}
- /**
- * Returns the set of command-line options related to the view specified for
- * this request.
- */
+ /** Returns the set of command-line options related to the view specified for this request. */
public AnalysisOptions getViewOptions() {
return getOptions(AnalysisOptions.class);
}
@@ -361,24 +353,20 @@
int getLoadingPhaseThreadCount() {
return getOptions(LoadingPhaseThreadsOption.class).threads;
}
- /**
- * Returns the set of execution options specified for this request.
- */
+
+ /** Returns the set of execution options specified for this request. */
public ExecutionOptions getExecutionOptions() {
return getOptions(ExecutionOptions.class);
}
- /**
- * Returns the human-readable description of the non-default options
- * for this build request.
- */
+ /** Returns the human-readable description of the non-default options for this build request. */
public String getOptionsDescription() {
return optionsDescription;
}
/**
- * Return the time (according to System.currentTimeMillis()) at which the
- * service of this request was started.
+ * Return the time (according to System.currentTimeMillis()) at which the service of this request
+ * was started.
*/
public long getStartTime() {
return startTimeMillis;
@@ -403,8 +391,10 @@
int jobs = getBuildOptions().jobs;
if (localTestJobs > jobs) {
warnings.add(
- String.format("High value for --local_test_jobs: %d. This exceeds the value for --jobs: "
- + "%d. Only up to %d local tests will run concurrently.", localTestJobs, jobs, jobs));
+ String.format(
+ "High value for --local_test_jobs: %d. This exceeds the value for --jobs: "
+ + "%d. Only up to %d local tests will run concurrently.",
+ localTestJobs, jobs, jobs));
}
// Validate other BuildRequest options.
@@ -423,7 +413,7 @@
getOptions(BuildEventProtocolOptions.class).expandFilesets,
getOptions(BuildEventProtocolOptions.class).fullyResolveFilesetSymlinks,
OutputGroupInfo.determineOutputGroups(
- buildOptions.outputGroups, validationMode(), /*shouldRunTests=*/ shouldRunTests()));
+ buildOptions.outputGroups, validationMode(), /* shouldRunTests= */ shouldRunTests()));
}
public ImmutableList<String> getAspects() {
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
index 3092930..55ac41e 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -235,7 +235,8 @@
env.setWorkspaceName(targetPatternPhaseValue.getWorkspaceName());
ProjectEvaluationResult projectEvaluationResult =
- evaluateProjectFile(request, buildOptions, targetPatternPhaseValue, env);
+ evaluateProjectFile(
+ request, buildOptions, request.getUserOptions(), targetPatternPhaseValue, env);
var analysisCachingDeps =
RemoteAnalysisCachingDependenciesProviderImpl.forAnalysis(
@@ -1050,6 +1051,7 @@
/** Creates a BuildOptions class for the given options taken from an {@link OptionsProvider}. */
public static BuildOptions applySclConfigs(
BuildOptions buildOptionsBeforeFlagSets,
+ ImmutableSet<String> userOptions,
Label projectFile,
boolean enforceCanonicalConfigs,
SkyframeExecutor skyframeExecutor,
@@ -1060,6 +1062,7 @@
Project.modifyBuildOptionsWithFlagSets(
projectFile,
buildOptionsBeforeFlagSets,
+ userOptions,
enforceCanonicalConfigs,
eventHandler,
skyframeExecutor);
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 550dcad..2d38b60 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
@@ -3254,6 +3254,11 @@
java.util.function.Predicate<? super ParsedOptionDescription> filter) {
return ImmutableMap.of();
}
+
+ @Override
+ public ImmutableSet<String> getUserOptions() {
+ return ImmutableSet.of();
+ }
});
}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java
index 5b95668..6703112 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java
@@ -13,10 +13,16 @@
// limitations under the License.
package com.google.devtools.build.lib.skyframe.config;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.Label.RepoContext;
import com.google.devtools.build.lib.cmdline.RepositoryName;
@@ -83,7 +89,9 @@
projectValue,
key.getSclConfig(),
key.enforceCanonical(),
- env.getListener());
+ env.getListener(),
+ key.getTargetOptions(),
+ key.getUserOptions());
ParsedFlagsValue parsedFlags = parseFlags(sclConfigAsStarlarkList, env);
if (parsedFlags == null) {
return null;
@@ -102,7 +110,9 @@
ProjectValue sclContent,
String sclConfigName,
boolean enforceCanonical,
- ExtendedEventHandler eventHandler)
+ ExtendedEventHandler eventHandler,
+ BuildOptions targetOptions,
+ ImmutableSet<String> userOptions)
throws FlagSetFunctionException {
var configs = (Dict<String, Collection<String>>) sclContent.getResidualGlobal(CONFIGS);
var sclConfigValue = configs == null ? null : configs.get(sclConfigName);
@@ -119,18 +129,9 @@
// is silently consider a no-op.
return sclConfigValue == null ? ImmutableList.of() : ImmutableList.copyOf(sclConfigValue);
} else if (sclConfigName.isEmpty()) {
+ ImmutableList<String> defaultConfigValue = ImmutableList.of();
try {
- ImmutableList<String> defaultConfigValue =
- validateDefaultConfig(defaultConfigName, configs, supportedConfigs);
- if (!defaultConfigWarningShown.get()) {
- eventHandler.handle(
- Event.info(
- String.format(
- "Applying flags from the default config defined in %s: %s ",
- projectFile, defaultConfigValue)));
- defaultConfigWarningShown.set(true);
- }
- return defaultConfigValue;
+ defaultConfigValue = validateDefaultConfig(defaultConfigName, configs, supportedConfigs);
} catch (InvalidProjectFileException e) {
// there's no default config set.
throw new FlagSetFunctionException(
@@ -140,6 +141,16 @@
e.getMessage(), supportedConfigsDesc(projectFile, supportedConfigs))),
Transience.PERSISTENT);
}
+ validateNoExtraFlagsSet(targetOptions, userOptions);
+ if (!defaultConfigWarningShown.get()) {
+ eventHandler.handle(
+ Event.info(
+ String.format(
+ "Applying flags from the default config defined in %s: %s ",
+ projectFile, defaultConfigValue)));
+ defaultConfigWarningShown.set(true);
+ }
+ return defaultConfigValue;
} else if (!supportedConfigs.containsKey(sclConfigName)) {
// This project declares supported configs and user set --scl_config to an unsupported config.
throw new FlagSetFunctionException(
@@ -150,6 +161,14 @@
Transience.PERSISTENT);
}
+ if (enforceCanonical) {
+ validateNoExtraFlagsSet(targetOptions, userOptions);
+ }
+ eventHandler.handle(
+ Event.info(
+ String.format(
+ "Applying flags from the config defined in %s: %s ", projectFile, sclConfigValue)));
+
return ImmutableList.copyOf(sclConfigValue);
}
@@ -176,6 +195,44 @@
return ImmutableList.copyOf(configs.get(defaultConfigName));
}
+ /**
+ * Enforces that the user did not set any output-affecting options in a blazerc or on the command
+ * line. Flags set in global RC files and flags that do not affect outputs are allowed.
+ */
+ // TODO(steinman): Allow user options if they are also part of the --scl_config.
+ private void validateNoExtraFlagsSet(BuildOptions targetOptions, ImmutableSet<String> userOptions)
+ throws FlagSetFunctionException {
+ ImmutableList.Builder<String> allOptionsAsStringsBuilder = new ImmutableList.Builder<>();
+ // All potentially conflicting user options also appear in targetOptions
+ targetOptions.getStarlarkOptions().keySet().stream()
+ .map(Object::toString)
+ .forEach(allOptionsAsStringsBuilder::add);
+ for (FragmentOptions fragmentOptions : targetOptions.getNativeOptions()) {
+ fragmentOptions.asMap().keySet().forEach(allOptionsAsStringsBuilder::add);
+ }
+ ImmutableList<String> allOptionsAsStrings = allOptionsAsStringsBuilder.build();
+ ImmutableSet<String> overlap =
+ userOptions.stream()
+ // Remove options that aren't part of BuildOptions
+ .filter(
+ option ->
+ allOptionsAsStrings.contains(
+ Iterables.get(Splitter.on("=").split(option), 0).replaceFirst("--", "")))
+ .filter(option -> !option.startsWith("--scl_config"))
+ .collect(toImmutableSet());
+ // TODO(b/341930725): Allow user options if they are also part of the --scl_config.
+ if (!overlap.isEmpty()) {
+ throw new FlagSetFunctionException(
+ new UnsupportedConfigException(
+ String.format(
+ "When --enforce_project_configs is set, --scl_config must be the only"
+ + " configuration-affecting flag in the build. Found %s in the command line"
+ + " or user blazerc",
+ overlap)),
+ Transience.PERSISTENT);
+ }
+ }
+
/** Returns a user-friendly description of project-supported configurations. */
private static String supportedConfigsDesc(
Label projectFile, Dict<String, String> supportedConfigs) {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java
index cf58fb9..425163b 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Strings.nullToEmpty;
import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
@@ -40,6 +41,7 @@
private final Label projectFile;
private final String sclConfig;
private final BuildOptions targetOptions;
+ private final ImmutableSet<String> userOptions;
private final boolean enforceCanonical;
@@ -47,16 +49,23 @@
Label projectFile,
@Nullable String sclConfig,
BuildOptions targetOptions,
+ ImmutableSet<String> userOptions,
boolean enforceCanonical) {
this.projectFile = Verify.verifyNotNull(projectFile);
this.sclConfig = nullToEmpty(sclConfig);
this.targetOptions = Verify.verifyNotNull(targetOptions);
+ this.userOptions = Verify.verifyNotNull(userOptions);
this.enforceCanonical = enforceCanonical;
}
public static Key create(
- Label projectFile, String sclConfig, BuildOptions targetOptions, boolean enforceCanonical) {
- return interner.intern(new Key(projectFile, sclConfig, targetOptions, enforceCanonical));
+ Label projectFile,
+ String sclConfig,
+ BuildOptions targetOptions,
+ ImmutableSet<String> userOptions,
+ boolean enforceCanonical) {
+ return interner.intern(
+ new Key(projectFile, sclConfig, targetOptions, userOptions, enforceCanonical));
}
public Label getProjectFile() {
@@ -71,6 +80,10 @@
return targetOptions;
}
+ public ImmutableSet<String> getUserOptions() {
+ return userOptions;
+ }
+
/**
* Whether {@code --scl_config} must match an officially supported project configuration. See
* {@link com.google.devtools.build.lib.buildtool.BuildRequestOptions#enforceProjectConfigs}.
@@ -101,12 +114,13 @@
return Objects.equals(projectFile, key.projectFile)
&& Objects.equals(sclConfig, key.sclConfig)
&& Objects.equals(targetOptions, key.targetOptions)
+ && Objects.equals(userOptions, key.userOptions)
&& (enforceCanonical == key.enforceCanonical);
}
@Override
public int hashCode() {
- return Objects.hash(projectFile, sclConfig, targetOptions, enforceCanonical);
+ return Objects.hash(projectFile, sclConfig, targetOptions, userOptions, enforceCanonical);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ImmutableSetCodec.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ImmutableSetCodec.java
index 97a9109..3ab6951 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ImmutableSetCodec.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ImmutableSetCodec.java
@@ -30,7 +30,7 @@
/** {@link ObjectCodec} for {@link ImmutableSet} and other sets that should be immutable. */
@SuppressWarnings({"rawtypes", "unchecked"})
-final class ImmutableSetCodec extends DeferredObjectCodec<Set> {
+public final class ImmutableSetCodec extends DeferredObjectCodec<Set> {
// Conversion of the types below to ImmutableSet is sound because the underlying types are hidden
// and only referenceable as the Set type.
diff --git a/src/main/java/com/google/devtools/build/lib/testing/common/FakeOptions.java b/src/main/java/com/google/devtools/build/lib/testing/common/FakeOptions.java
index 71f4e41..8d5ae92 100644
--- a/src/main/java/com/google/devtools/build/lib/testing/common/FakeOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/testing/common/FakeOptions.java
@@ -15,6 +15,7 @@
import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.devtools.common.options.Options;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsProvider;
@@ -114,4 +115,9 @@
Predicate<? super ParsedOptionDescription> filter) {
return ImmutableMap.of();
}
+
+ @Override
+ public ImmutableSet<String> getUserOptions() {
+ return ImmutableSet.of();
+ }
}
diff --git a/src/main/java/com/google/devtools/common/options/GlobalRcUtils.java b/src/main/java/com/google/devtools/common/options/GlobalRcUtils.java
new file mode 100644
index 0000000..7d8da99
--- /dev/null
+++ b/src/main/java/com/google/devtools/common/options/GlobalRcUtils.java
@@ -0,0 +1,34 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.common.options;
+
+import java.util.function.Predicate;
+
+/** Utility functions for global RC files. */
+final class GlobalRcUtils {
+
+ private GlobalRcUtils() {}
+
+ /** No global RC files in Bazel. Consider "client" options to be global. */
+ static final Predicate<ParsedOptionDescription> IS_GLOBAL_RC_OPTION =
+ // LINT.IfChange
+ (option) -> {
+ if (option.getOrigin().getSource() != null
+ && option.getOrigin().getSource().equals("client")) {
+ return true;
+ }
+ return false;
+ };
+ // LINT.ThenChange(//src/main/cpp/option_processor.cc)
+}
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 0fa39ee..8678e10 100644
--- a/src/main/java/com/google/devtools/common/options/OptionsParser.java
+++ b/src/main/java/com/google/devtools/common/options/OptionsParser.java
@@ -160,6 +160,7 @@
public static final class Builder {
private final OptionsParserImpl.Builder implBuilder;
private boolean allowResidue = true;
+ private boolean ignoreUserOptions = false;
private Builder(OptionsParserImpl.Builder implBuilder) {
this.implBuilder = implBuilder;
@@ -232,6 +233,13 @@
return this;
}
+ /** Sets whether the parser should ignore user options. If true, returns no user options. */
+ @CanIgnoreReturnValue
+ public Builder ignoreUserOptions() {
+ this.ignoreUserOptions = true;
+ return this;
+ }
+
/** Sets the string the parser should look for as an identifier for flag aliases. */
@CanIgnoreReturnValue
public Builder withAliasFlag(@Nullable String aliasFlag) {
@@ -256,7 +264,7 @@
/** Returns a new {@link OptionsParser}. */
public OptionsParser build() {
- return new OptionsParser(implBuilder.build(), allowResidue);
+ return new OptionsParser(implBuilder.build(), allowResidue, ignoreUserOptions);
}
}
@@ -273,13 +281,16 @@
private final List<String> residue = new ArrayList<>();
private final List<String> postDoubleDashResidue = new ArrayList<>();
private final boolean allowResidue;
+ private final boolean ignoreUserOptions;
+
private ImmutableSortedMap<String, Object> starlarkOptions = ImmutableSortedMap.of();
private final Map<String, String> aliases = new HashMap<>();
private boolean success = true;
- private OptionsParser(OptionsParserImpl impl, boolean allowResidue) {
+ private OptionsParser(OptionsParserImpl impl, boolean allowResidue, boolean ignoreUserOptions) {
this.impl = impl;
this.allowResidue = allowResidue;
+ this.ignoreUserOptions = ignoreUserOptions;
}
public Object getConversionContext() {
@@ -864,6 +875,33 @@
return impl.asCanonicalizedList();
}
+ @Override
+ public ImmutableSet<String> getUserOptions() {
+ if (ignoreUserOptions) {
+ return ImmutableSet.of();
+ }
+ ImmutableSet.Builder<String> userOptions = ImmutableSet.builder();
+
+ return userOptions
+ .addAll(
+ asListOfExplicitOptions().stream()
+ .filter(GlobalRcUtils.IS_GLOBAL_RC_OPTION.negate())
+ .filter(option -> !option.getCanonicalForm().contains("default_override"))
+ .map(option -> option.getCanonicalForm())
+ .collect(toImmutableSet()))
+ .addAll(
+ impl.getSkippedOptions().stream()
+ .filter(GlobalRcUtils.IS_GLOBAL_RC_OPTION.negate())
+ .map(option -> option.getUnconvertedValue())
+ .filter(
+ o ->
+ getStarlarkOptions()
+ .containsKey(
+ Iterables.get(Splitter.on('=').split(o.replace("--", "")), 0)))
+ .collect(toImmutableSet()))
+ .build();
+ }
+
/** Returns all options fields of the given options class, in alphabetic order. */
public static ImmutableList<? extends OptionDefinition> getOptionDefinitions(
Class<? extends OptionsBase> optionsClass) {
diff --git a/src/main/java/com/google/devtools/common/options/OptionsProvider.java b/src/main/java/com/google/devtools/common/options/OptionsProvider.java
index b45f887..2974e74 100644
--- a/src/main/java/com/google/devtools/common/options/OptionsProvider.java
+++ b/src/main/java/com/google/devtools/common/options/OptionsProvider.java
@@ -14,13 +14,14 @@
package com.google.devtools.common.options;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import java.util.Map;
import java.util.function.Predicate;
import javax.annotation.Nullable;
/**
- * A read-only interface for options parser results, which only allows to query the options of
- * a specific class, but not e.g. the residue any other information pertaining to the command line.
+ * A read-only interface for options parser results, which only allows to query the options of a
+ * specific class, but not e.g. the residue any other information pertaining to the command line.
*/
public interface OptionsProvider {
public static final OptionsProvider EMPTY =
@@ -41,16 +42,22 @@
Predicate<? super ParsedOptionDescription> filter) {
return ImmutableMap.of();
}
+
+ @Override
+ public ImmutableSet<String> getUserOptions() {
+ return ImmutableSet.of();
+ }
};
/**
- * Returns the options instance for the given {@code optionsClass}, that is,
- * the parsed options, or null if it is not among those available.
+ * Returns the options instance for the given {@code optionsClass}, that is, the parsed options,
+ * or null if it is not among those available.
*
- * <p>The returned options should be treated by library code as immutable and
- * a provider is permitted to return the same options instance multiple times.
+ * <p>The returned options should be treated by library code as immutable and a provider is
+ * permitted to return the same options instance multiple times.
*/
- @Nullable <O extends OptionsBase> O getOptions(Class<O> optionsClass);
+ @Nullable
+ <O extends OptionsBase> O getOptions(Class<O> optionsClass);
/**
* Returns the starlark options in a name:value map.
@@ -68,4 +75,10 @@
* the given filter criteria.
*/
Map<String, Object> getExplicitStarlarkOptions(Predicate<? super ParsedOptionDescription> filter);
+
+ /**
+ * Returns the options that were parsed from either a user blazerc file or the command line as a
+ * map of option name to option value.
+ */
+ ImmutableSet<String> getUserOptions();
}
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BlazeRuntimeWrapper.java b/src/test/java/com/google/devtools/build/lib/buildtool/util/BlazeRuntimeWrapper.java
index c25f6b9..a6d23d3 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/util/BlazeRuntimeWrapper.java
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BlazeRuntimeWrapper.java
@@ -310,7 +310,10 @@
Iterables.addAll(options, module.getCommandOptions(commandAnnotation));
}
options.addAll(runtime.getRuleClassProvider().getFragmentRegistry().getOptionsClasses());
- return OptionsParser.builder().optionsClasses(options).build();
+ // Because the tests that use this class don't set sources for their options, the normal logic
+ // for determining user options assumes that all options are user options. This causes tests
+ // that enable PROJECT.scl files to fail, so ignore user options instead.
+ return OptionsParser.builder().optionsClasses(options).ignoreUserOptions().build();
}
void executeNonBuildCommand() throws Exception {
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java
index 293d271..1edda61 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java
@@ -17,6 +17,7 @@
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.PlatformOptions;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
@@ -91,6 +92,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
"test_config",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ false);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -121,6 +123,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
"unknown_config",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ false);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -139,6 +142,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
"",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ false);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -159,6 +163,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
"random_config_name",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ false);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -191,6 +196,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
"test_config",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -203,6 +209,179 @@
}
@Test
+ public void enforceCanonicalConfigsExtraNativeFlag_withSclConfig_fails() throws Exception {
+ scratch.file(
+ "test/build_settings.bzl",
+ """
+string_flag = rule(implementation = lambda ctx: [], build_setting = config.string(flag = True))
+""");
+ scratch.file(
+ "test/BUILD",
+ """
+ load("//test:build_settings.bzl", "string_flag")
+ string_flag(
+ name = "myflag",
+ build_setting_default = "default",
+ )
+ """);
+ scratch.file(
+ "test/PROJECT.scl",
+ """
+ configs = {
+ "test_config": ['--//test:myflag=test_config_value'],
+ "other_config": ['--//test:myflag=other_config_value'],
+ }
+ supported_configs = {
+ "test_config": "User documentation for what this config means",
+ }
+ """);
+ setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+ BuildOptions buildOptions = createBuildOptions("--define=foo=bar");
+
+ FlagSetValue.Key key =
+ FlagSetValue.Key.create(
+ Label.parseCanonical("//test:PROJECT.scl"),
+ "test_config",
+ buildOptions,
+ /* userOptions= */ ImmutableSet.of("--define=foo=bar"),
+ /* enforceCanonical= */ true);
+
+ var thrown = assertThrows(Exception.class, () -> executeFunction(key));
+ assertThat(thrown).hasMessageThat().contains("Found [--define=foo=bar]");
+ }
+
+ @Test
+ public void enforceCanonicalConfigsExtraNativeFlag_noSupportedConfigs_passes() throws Exception {
+ scratch.file(
+ "test/build_settings.bzl",
+ """
+string_flag = rule(implementation = lambda ctx: [], build_setting = config.string(flag = True))
+""");
+ scratch.file(
+ "test/BUILD",
+ """
+ load("//test:build_settings.bzl", "string_flag")
+ string_flag(
+ name = "myflag",
+ build_setting_default = "default",
+ )
+ """);
+ scratch.file(
+ "test/PROJECT.scl",
+ """
+ configs = {
+ "test_config": ['--//test:myflag=test_config_value'],
+ "other_config": ['--//test:myflag=other_config_value'],
+ }
+ """);
+ setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+ BuildOptions buildOptions = createBuildOptions("--define=foo=bar");
+
+ FlagSetValue.Key key =
+ FlagSetValue.Key.create(
+ Label.parseCanonical("//test:PROJECT.scl"),
+ null,
+ buildOptions,
+ /* userOptions= */ ImmutableSet.of("--define=foo=bar"),
+ /* enforceCanonical= */ true);
+
+ var unused = executeFunction(key);
+ assertDoesNotContainEvent("--scl_config must be the only configuration-affecting flag");
+ }
+
+ @Test
+ public void enforceCanonicalConfigsExtraStarlarkFlag_fails() throws Exception {
+ scratch.file(
+ "test/build_settings.bzl",
+ """
+string_flag = rule(implementation = lambda ctx: [], build_setting = config.string(flag = True))
+""");
+ scratch.file(
+ "test/BUILD",
+ """
+ load("//test:build_settings.bzl", "string_flag")
+ string_flag(
+ name = "myflag",
+ build_setting_default = "default",
+ )
+ string_flag(
+ name = "starlark_flags_always_affect_configuration",
+ build_setting_default = "default",
+ )
+ """);
+ scratch.file(
+ "test/PROJECT.scl",
+ """
+ configs = {
+ "test_config": ['--//test:myflag=test_config_value'],
+ "other_config": ['--//test:myflag=other_config_value'],
+ }
+ supported_configs = {
+ "test_config": "User documentation for what this config means",
+ }
+ """);
+ setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+ BuildOptions buildOptions =
+ createBuildOptions("--//test:starlark_flags_always_affect_configuration=yes_they_do");
+
+ FlagSetValue.Key key =
+ FlagSetValue.Key.create(
+ Label.parseCanonical("//test:PROJECT.scl"),
+ "test_config",
+ buildOptions,
+ /* userOptions= */ ImmutableSet.of(
+ "--//test:starlark_flags_always_affect_configuration=yes_they_do"),
+ /* enforceCanonical= */ true);
+
+ var thrown = assertThrows(Exception.class, () -> executeFunction(key));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Found [--//test:starlark_flags_always_affect_configuration=yes_they_do]");
+ }
+
+ @Test
+ public void noEnforceCanonicalConfigsExtraFlag_passes() throws Exception {
+ scratch.file(
+ "test/build_settings.bzl",
+ """
+string_flag = rule(implementation = lambda ctx: [], build_setting = config.string(flag = True))
+""");
+ scratch.file(
+ "test/BUILD",
+ """
+ load("//test:build_settings.bzl", "string_flag")
+ string_flag(
+ name = "myflag",
+ build_setting_default = "default",
+ )
+ """);
+ scratch.file(
+ "test/PROJECT.scl",
+ """
+ configs = {
+ "test_config": ['--//test:myflag=test_config_value'],
+ "other_config": ['--//test:myflag=other_config_value'],
+ }
+ supported_configs = {
+ "test_config": "User documentation for what this config means",
+ }
+ """);
+ setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+ BuildOptions buildOptions = createBuildOptions("--define=foo=bar");
+
+ FlagSetValue.Key key =
+ FlagSetValue.Key.create(
+ Label.parseCanonical("//test:PROJECT.scl"),
+ "test_config",
+ buildOptions,
+ /* userOptions= */ ImmutableSet.of("--define=foo=bar"),
+ /* enforceCanonical= */ false);
+
+ var unused = executeFunction(key);
+ assertDoesNotContainEvent("--scl_config must be the only configuration-affecting flag");
+ }
+
+ @Test
public void enforceCanonicalConfigsUnsupportedConfig() throws Exception {
createStringFlag("//test:myflag", /* defaultValue= */ "default");
scratch.file(
@@ -226,6 +405,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
"other_config",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
var thrown = assertThrows(Exception.class, () -> executeFunction(key));
@@ -258,6 +438,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
"non_existent_config",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
var thrown = assertThrows(Exception.class, () -> executeFunction(key));
@@ -290,6 +471,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
/* sclConfig= */ "",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
var thrown = assertThrows(Exception.class, () -> executeFunction(key));
@@ -324,6 +506,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
/* sclConfig= */ "",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
var thrown = assertThrows(Exception.class, () -> executeFunction(key));
@@ -359,6 +542,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
/* sclConfig= */ "",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
var thrown = assertThrows(Exception.class, () -> executeFunction(key));
@@ -394,6 +578,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
/* sclConfig= */ "",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -428,6 +613,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
/* sclConfig= */ "",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -455,6 +641,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
/* sclConfig= */ "test_config",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
FlagSetValue flagSetsValue = executeFunction(key);
@@ -491,6 +678,7 @@
Label.parseCanonical("//test:PROJECT.scl"),
/* sclConfig= */ "",
buildOptions,
+ /* userOptions= */ ImmutableSet.of(),
/* enforceCanonical= */ true);
var thrown = assertThrows(Exception.class, () -> executeFunction(key));
diff --git a/src/test/java/com/google/devtools/common/options/OptionsParserTest.java b/src/test/java/com/google/devtools/common/options/OptionsParserTest.java
index 846d2e7..a599712 100644
--- a/src/test/java/com/google/devtools/common/options/OptionsParserTest.java
+++ b/src/test/java/com/google/devtools/common/options/OptionsParserTest.java
@@ -2625,6 +2625,22 @@
assertThat(parser.getOptions(ExpandingOptionsFallback.class)).isNull();
}
+ @Test
+ public void testOptionsParser_getUserOptions_excludesClientOptions() throws Exception {
+ OptionsParser parser =
+ OptionsParser.builder()
+ .optionsClasses(ExpandingOptions.class, ExpandingOptionsFallback.class)
+ .build();
+ parser.parseWithSourceFunction(
+ PriorityCategory.RC_FILE, o -> "client", ImmutableList.of("--foo"), null);
+ assertThat(parser.getUserOptions()).isEmpty();
+
+ parser.parseWithSourceFunction(
+ PriorityCategory.RC_FILE, o -> ".bazelrc", ImmutableList.of("--foo"), null);
+
+ assertThat(parser.getUserOptions()).containsExactly("--foo");
+ }
+
private static OptionInstanceOrigin createInvocationPolicyOrigin() {
return createInvocationPolicyOrigin(/*implicitDependent=*/ null, /*expandedFrom=*/ null);
}
diff --git a/src/test/shell/integration/BUILD b/src/test/shell/integration/BUILD
index 4342824..cd530af 100644
--- a/src/test/shell/integration/BUILD
+++ b/src/test/shell/integration/BUILD
@@ -676,6 +676,17 @@
],
)
+sh_test(
+ name = "flagset_test",
+ size = "large",
+ srcs = ["flagset_test.sh"],
+ data = [
+ ":test-deps",
+ "@bazel_tools//tools/bash/runfiles",
+ ],
+ shard_count = 3,
+)
+
# Copy protoc into a known location, since //third_party/protobuf:protoc
# might be an alias. This is referenced from testenv.sh.
genrule(
diff --git a/src/test/shell/integration/flagset_test.sh b/src/test/shell/integration/flagset_test.sh
new file mode 100755
index 0000000..4aebbc6
--- /dev/null
+++ b/src/test/shell/integration/flagset_test.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+#
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# An end-to-end test for flagsets.
+
+# --- begin runfiles.bash initialization ---
+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; }
+
+function set_up_project_file() {
+ mkdir -p test
+ cat > test/BUILD <<EOF
+load("//test:test.bzl", "string_flag")
+
+genrule(name='test', outs=['test.txt'], cmd='echo hi > \$@')
+
+string_flag(
+ name = "starlark_flags_always_affect_configuration",
+ build_setting_default = "default",
+)
+EOF
+ cat > test/PROJECT.scl <<EOF
+configs = {
+ "test_config": ["--define=foo=bar"],
+}
+supported_configs = {
+ "test_config": "User documentation for what this config means",
+}
+EOF
+
+ touch test/test.bzl
+ cat > test/test.bzl <<EOF
+string_flag = rule(implementation = lambda ctx: [], build_setting = config.string(flag = True))
+EOF
+}
+
+function test_scl_config_plus_user_bazelrc_fails(){
+ set_up_project_file
+ add_to_bazelrc "build '--//test:starlark_flags_always_affect_configuration=yes'"
+ add_to_bazelrc "build --define=bar=baz"
+ cat .bazelrc >> test/test.bazelrc
+ bazel --bazelrc=test/test.bazelrc build --nobuild //test:test --enforce_project_configs --scl_config=test_config --experimental_enable_scl_dialect &> "$TEST_log" && \
+ fail "Scl enabled build expected to fail with starlark flag in user bazelrc"
+ expect_log "--scl_config must be the only configuration-affecting flag"
+ expect_log "--//test:starlark_flags_always_affect_configuration=yes"
+ expect_log "--define=bar=baz"
+}
+
+function test_scl_config_plus_command_line_flag_fails(){
+ set_up_project_file
+ bazel build --nobuild //test:test --enforce_project_configs --scl_config=test_config --experimental_enable_scl_dialect --//test:starlark_flags_always_affect_configuration=yes --define=bar=baz &> "$TEST_log" && \
+ fail "Scl enabled build expected to fail with command-line flags"
+ expect_log "--scl_config must be the only configuration-affecting flag"
+ expect_log "--//test:starlark_flags_always_affect_configuration=yes"
+ expect_log "--define=bar=baz"
+}
+
+function test_scl_config_plus_expanded_command_line_flag_fails(){
+ set_up_project_file
+ bazel build --nobuild //test:test --enforce_project_configs --scl_config=test_config --experimental_enable_scl_dialect -c opt &> "$TEST_log" && \
+ fail "Scl enabled build expected to fail with command line flag"
+ expect_log "--scl_config must be the only configuration-affecting flag"
+ expect_log "--compilation_mode=opt"
+}
+
+run_suite "Integration tests for flagsets/scl_config"
\ No newline at end of file