Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ public record ScopeType(String scopeType) {
/** The flag's value resets on exec transitions. */
public static final String TARGET = "target";

/** The flag resets on targets outside the flag's project. See PROJECT.scl. */
/**
* The flag resets on targets outside the flag's project. See PROJECT.scl.
*
* <p>Unlike {@code target} scope, this flag's value is preserved across exec transitions:
* the value propagates from the target configuration into exec configurations. The
* project-boundary enforcement (resetting out-of-scope targets to the baseline) is still
* applied independently for each target configuration via
* {@code BuildConfigurationKeyProducer}.
*/
public static final String PROJECT = "project";

/** Placeholder for flags that don't explicitly specify scope. Shouldn't be set directly. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,9 @@ private StateMachine finishConfigurationKeyProcessing(BuildOptions finalBuildOpt
private static BuildOptions resetFlags(
BuildOptionsScopeValue buildOptionsScopeValue,
BuildOptions baselineConfiguration,
Label label) {
@Nullable Label label) {
Comment thread
gregestren marked this conversation as resolved.
Preconditions.checkNotNull(buildOptionsScopeValue);
Preconditions.checkNotNull(label);


BuildOptions transitionedOptionsWithScopeType =
buildOptionsScopeValue.getResolvedBuildOptionsWithScopeTypes();
Expand All @@ -347,12 +347,10 @@ private static BuildOptions resetFlags(
Label flagLabel = flagEntry.getKey();
Scope scope = buildOptionsScopeValue.getFullyResolvedScopes().get(flagLabel);
if (scope == null) {
Scope.ScopeType flagScopeType =
transitionedOptionsWithScopeType.getScopeTypeMap().get(flagLabel);
Verify.verify(
!transitionedOptionsWithScopeType
.getScopeTypeMap()
.get(flagLabel)
.scopeType()
.equals(Scope.ScopeType.PROJECT));
flagScopeType == null || !flagScopeType.scopeType().equals(Scope.ScopeType.PROJECT));
} else if (scope.getScopeType().scopeType().equals(Scope.ScopeType.PROJECT)) {
Object flagValue = flagEntry.getValue();
Object baselineValue = baselineConfiguration.getStarlarkOptions().get(flagLabel);
Expand Down Expand Up @@ -380,8 +378,14 @@ private static BuildOptions resetFlags(
return scopedBuildOptions;
}

private static boolean isInScope(Label label, Scope.ScopeDefinition scopeDefinition) {
Preconditions.checkNotNull(scopeDefinition);
private static boolean isInScope(@Nullable Label label, @Nullable Scope.ScopeDefinition scopeDefinition) {
// A null scopeDefinition means the flag's package has no PROJECT.scl file. Treat the target
// as not in scope so the flag resets to its baseline value.
// Also, if the label is null, we are evaluating a configuration without a target, so we also treat it
// as out of scope.
if (scopeDefinition == null || label == null) {
return false;
}
for (String path : scopeDefinition.getOwnedCodePaths()) {
if (label.getCanonicalForm().startsWith(path)) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,15 @@ private static ImmutableMap<Label, Object> getExecPropagatingStarlarkFlags(
}
if (!options.get(CoreOptions.class).getExcludeStarlarkFlagsFromExecConfig()) {
// Starlark flags propagate to exec by default. This can only be changed by a flag explicitly
// setting "scope = 'target'".
// setting "scope = 'target'". Project-scoped flags also propagate through exec transitions:
// their value is maintained across exec boundaries, with project-boundary enforcement still
// applied at target-configuration time by BuildConfigurationKeyProducer.
return starlarkOptions.entrySet().stream()
.filter(
entry -> {
String scopeType = options.getScopeTypeMap().get(entry.getKey()).scopeType();
return scopeType.equals(Scope.ScopeType.UNIVERSAL)
|| scopeType.equals(Scope.ScopeType.PROJECT)
|| scopeType.equals(Scope.ScopeType.DEFAULT);
})
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
Expand All @@ -263,7 +266,10 @@ private static ImmutableMap<Label, Object> getExecPropagatingStarlarkFlags(
ImmutableMap.Builder<Label, Object> ans = ImmutableMap.builder();
for (Map.Entry<Label, Object> entry : starlarkOptions.entrySet()) {
String scopeType = options.getScopeTypeMap().get(entry.getKey()).scopeType();
if (scopeType.equals(Scope.ScopeType.UNIVERSAL)) {
if (scopeType.equals(Scope.ScopeType.UNIVERSAL) || scopeType.equals(Scope.ScopeType.PROJECT)) {
// Universal flags always propagate. Project-scoped flags also propagate through exec
// transitions; their value is maintained across exec boundaries, with project-boundary
// enforcement applied later at target-configuration time.
ans.put(entry);
} else if (scopeType.equals(Scope.ScopeType.TARGET)) {
Object onLeaveScopeValue = options.getOnLeaveScopeValues().get(entry.getKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.analysis.config.BuildOptions.MapBackedChecksumCache;
import com.google.devtools.build.lib.analysis.config.BuildOptions.OptionsChecksumCache;
import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil;
import com.google.devtools.build.lib.analysis.util.ConfigurationTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.RepositoryName;
Expand Down Expand Up @@ -498,6 +499,11 @@ public void starlarkFlagExecScopes(@TestParameter boolean propagateByDefault) th
scope = "target",
on_leave_scope = "another_value"
)
string_flag(
name = "project_scope",
build_setting_default = "default",
scope = "project",
)
string_flag(
name = "another_flag",
build_setting_default = "default",
Expand All @@ -509,15 +515,17 @@ public void starlarkFlagExecScopes(@TestParameter boolean propagateByDefault) th
)
""");

BuildConfigurationValue execConfig =
createExec(
BuildOptions targetOptions =
parseBuildOptions(
ImmutableMap.of(
"//test:default_scope",
"custom",
"//test:target_scope",
"custom",
"//test:universal_scope",
"custom",
"//test:project_scope",
"custom",
"//test:flag_in_exec_config_set_to_another_value",
"target_value",
"//test:flag_in_exec_config_reference_another_flag_value",
Expand All @@ -527,26 +535,31 @@ public void starlarkFlagExecScopes(@TestParameter boolean propagateByDefault) th
"--experimental_exclude_starlark_flags_from_exec_config="
+ (propagateByDefault ? "false" : "true"));

BuildOptions execOptions =
AnalysisTestUtil.execOptions(targetOptions, skyframeExecutor, reporter);

if (propagateByDefault) {
assertThat(execConfig.getOptions().getStarlarkOptions())
assertThat(execOptions.getStarlarkOptions())
.containsExactly(
Label.parseCanonicalUnchecked("//test:universal_scope"),
"custom",
Label.parseCanonicalUnchecked("//test:project_scope"),
"custom",
Label.parseCanonicalUnchecked("//test:default_scope"),
"custom",
Label.parseCanonicalUnchecked("//test:another_flag"),
"default");
} else {
assertThat(execConfig.getOptions().getStarlarkOptions())
assertThat(execOptions.getStarlarkOptions())
.containsExactly(
Label.parseCanonicalUnchecked("//test:universal_scope"),
"custom",
Label.parseCanonicalUnchecked("//test:project_scope"),
"custom",
Label.parseCanonicalUnchecked("//test:flag_in_exec_config_set_to_another_value"),
"another_value",
Label.parseCanonicalUnchecked(
"//test:flag_in_exec_config_reference_another_flag_value"),
"default",
Label.parseCanonicalUnchecked("//test:another_flag"),
"default");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,188 @@ public void createKey_withScopedBuildOptions_outOfScopeFlag_flagSetInTheBaseline
.containsExactlyEntriesIn(expectedScopeTypeMap);
}

@Test
public void createKey_withScopedBuildOptions_projectFlag_isPreservedForInScopeTarget()
throws Exception {
// Tests that when a target is evaluated within a flag's project boundary, the project-scoped
// flag's value is correctly preserved.
createStarlarkFlagRule();
scratch.file(
"flag/BUILD",
"""
load(":def.bzl", "basic_flag")
basic_flag(
name = "foo",
scope = "project",
build_setting_default = "default",
)
""");
scratch.file(
"flag/PROJECT.scl",
"""
load("//test:project_proto.scl", "project_pb2")
project = project_pb2.Project.create(
project_directories = ["//my_project"],
)
""");

invalidatePackages(false);

// The target being built is inside the flag's project boundary.
BuildOptions baseOptions = createBuildOptions("--//flag:foo=foo");
BuildConfigurationKey result =
fetch(baseOptions, Label.parseCanonicalUnchecked("//my_project:my_target"));
assertThat(result).isNotNull();

// The flag is in scope for //my_project:my_target, so its value should be preserved.
assertThat(
result
.getOptions()
.getStarlarkOptions()
.get(Label.parseCanonicalUnchecked("//flag:foo")))
.isEqualTo("foo");
}

@Test
public void createKey_withScopedBuildOptions_projectFlag_resetsOutsideProject()
throws Exception {
// Tests that when a project-scoped flag arrives at a target evaluation,
// if the target is outside the flag's project boundary, the project-boundary
// enforcement resets the flag to its baseline value.
createStarlarkFlagRule();
scratch.file(
"flag/BUILD",
"""
load(":def.bzl", "basic_flag")
basic_flag(
name = "foo",
scope = "project",
build_setting_default = "default",
)
""");
scratch.file(
"flag/PROJECT.scl",
"""
load("//test:project_proto.scl", "project_pb2")
project = project_pb2.Project.create(
project_directories = ["//my_project"],
)
""");

invalidatePackages(false);

// The target being built is outside the flag's project boundary.
BuildOptions baseOptions = createBuildOptions("--//flag:foo=foo");
BuildConfigurationKey result =
fetch(baseOptions, Label.parseCanonicalUnchecked("//other_project:my_target"));
assertThat(result).isNotNull();

// The flag is out of scope and resets to baseline.
// Since the baseline doesn't have //flag:foo set, it should be absent from the options.
assertThat(
result
.getOptions()
.getStarlarkOptions()
.get(Label.parseCanonicalUnchecked("//flag:foo")))
.isNull();
}

@Test
public void createKey_withNullLabel_resetsProjectScopedFlags() throws Exception {
createStarlarkFlagRule();
scratch.file(
"flag/BUILD",
"""
load(":def.bzl", "basic_flag")
basic_flag(
name = "foo",
scope = "project",
build_setting_default = "default",
)
""");
scratch.file(
"flag/PROJECT.scl",
"""
load("//test:project_proto.scl", "project_pb2")
project = project_pb2.Project.create(
project_directories = ["//my_project"],
)
""");

invalidatePackages(false);

// Provide a non-default flag value
BuildOptions baseOptions = createBuildOptions("--//flag:foo=foo");

// Fetching with a null label acts as if evaluating a top-level config or
// an exec transitions headless config. The flag should be aggressively reset to baseline
// because no project boundary could possibly be validated.
BuildConfigurationKey result = fetch(baseOptions, null);

assertThat(result).isNotNull();
// It should have reset to baseline, meaning //flag:foo is absent.
assertThat(
result
.getOptions()
.getStarlarkOptions()
.get(Label.parseCanonicalUnchecked("//flag:foo")))
.isNull();
}

@Test
public void createKey_withMissingScopeTypeMap_doesNotCrash() throws Exception {
createStarlarkFlagRule();
scratch.overwriteFile(
"flag/def.bzl",
"""
def _impl(ctx):
return []

basic_flag_without_restrictions = rule(
implementation = _impl,
build_setting = config.string(flag = True),
attrs = {
"scope": attr.string(
doc = "The scope",
default = "universal",
),
},
)
""");
scratch.file(
"flag/BUILD",
"""
load(":def.bzl", "basic_flag_without_restrictions")
basic_flag_without_restrictions(
name = "another_flag",
build_setting_default = "default",
)
basic_flag_without_restrictions(
name = "ref_flag",
scope = "exec:--//flag:another_flag",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly dos this test method cover?

I'd try to keep exec:--//some_other_flag out of this PR unless it's important in a way i'm not appreciating. Because exec:--//some_other_flag functionality isn't yet operative (although it's close). So any assertions we make over it now aren't stable.

If this is testing missing scope doesn't crash, and that's just a path toward that, is that a new vulnerability you think deserves more scrutiny here?

build_setting_default = "default",
)
""");

invalidatePackages(false);

// Provide --//flag:ref_flag=ref but DON'T provide --//flag:another_flag in the options.
BuildOptions baseOptions = createBuildOptions("--//flag:ref_flag=ref");

// This fetch invokes BuildOptionsScopeFunction which specifically injects //flag:another_flag
// into the evaluated options, BUT fails to add it to fullyResolvedScopes and scopeTypeMap.
// Ensure that resetFlags correctly validates this injected un-scoped flag without hitting NPE.
BuildConfigurationKey result = fetch(baseOptions, null);

assertThat(result).isNotNull();
assertThat(
result
.getOptions()
.getStarlarkOptions()
.get(Label.parseCanonicalUnchecked("//flag:another_flag")))
.isEqualTo("default");
}

@Test
public void checkFinalizeBuildOptions_haveCorrectScopeTypeMap_noScopingApplied()
throws Exception {
Expand Down