Skip to content

Commit 867f751

Browse files
committed
Add --incompatible_merge_fixed_and_default_shell_env
With the new flag, Starlark actions that specify both `env` and `use_default_shell_env` will no longer have `env` ignored. Instead, the values of `env` will override the default shell environment. This allows Starlark actions to both pick up user-configured variables such as `PATH` from the shell environment as well as set variables to fixed values required for the action, e.g., variables provided by the C++ toolchain. Rationale for having `env` override the default shell env: The rules know best which values they have to set specific environment variables to in order to successfully execute an action, so a situation where users break an action by a globally applied `--action_env` is prevented. If users really do have to be able to modify an environment variables fixed by the rule, the rule can always make this configurable via an attribute. Work towards #5980
1 parent 31c5a72 commit 867f751

File tree

4 files changed

+133
-12
lines changed

4 files changed

+133
-12
lines changed

src/main/java/com/google/devtools/build/lib/analysis/actions/SpawnAction.java

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -681,12 +681,35 @@ public SpawnAction build(ActionOwner owner, BuildConfigurationValue configuratio
681681
result.addCommandLine(pair);
682682
}
683683
CommandLines commandLines = result.build();
684-
ActionEnvironment env =
685-
actionEnvironment != null
686-
? actionEnvironment
687-
: useDefaultShellEnvironment
688-
? configuration.getActionEnvironment()
689-
: ActionEnvironment.create(environment, inheritedEnvironment);
684+
ActionEnvironment env;
685+
if (actionEnvironment != null) {
686+
env = actionEnvironment;
687+
} else if (!useDefaultShellEnvironment) {
688+
env = ActionEnvironment.create(environment, inheritedEnvironment);
689+
} else {
690+
// Inherited variables override fixed variables in ActionEnvironment. Since we want the
691+
// fixed part of the action-provided environment to override the inherited part of the
692+
// user-provided environment, we have to explicitly filter the inherited part.
693+
ImmutableMap<String, String> actionFixedEnv =
694+
environment != null ? environment : ImmutableMap.of();
695+
ImmutableSet<String> actionInheritedEnv =
696+
inheritedEnvironment != null ? inheritedEnvironment : ImmutableSet.of();
697+
ImmutableSet<String> userFilteredInheritedEnv = configuration.getActionEnvironment()
698+
.getInheritedEnv()
699+
.stream()
700+
.filter(key -> !actionFixedEnv.containsKey(key))
701+
.collect(toImmutableSet());
702+
// Do not create a new ActionEnvironment in the common case where no vars have been filtered
703+
// out.
704+
if (userFilteredInheritedEnv.equals(
705+
configuration.getActionEnvironment().getInheritedEnv())) {
706+
env = configuration.getActionEnvironment();
707+
} else {
708+
env = ActionEnvironment.create(configuration.getActionEnvironment().getFixedEnv(),
709+
userFilteredInheritedEnv);
710+
}
711+
env = env.withAdditionalVariables(actionFixedEnv, actionInheritedEnv);
712+
}
690713
return buildSpawnAction(
691714
owner, commandLines, configuration.getCommandLineLimits(), configuration, env);
692715
}
@@ -962,6 +985,19 @@ public Builder useDefaultShellEnvironment() {
962985
return this;
963986
}
964987

988+
/**
989+
* Same as {@link #useDefaultShellEnvironment()}, but additionally sets the provided fixed
990+
* environment variables, which take precendence over the variables contained in the default
991+
* shell environment.
992+
*/
993+
@CanIgnoreReturnValue
994+
public Builder useDefaultShellEnvironmentWithOverrides(Map<String, String> environment) {
995+
this.environment = ImmutableMap.copyOf(environment);
996+
this.inheritedEnvironment = null;
997+
this.useDefaultShellEnvironment = true;
998+
return this;
999+
}
1000+
9651001
/**
9661002
* Sets the executable path; the path is interpreted relative to the execution root, unless it's
9671003
* a bare file name.

src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkActionFactory.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction;
4848
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
4949
import com.google.devtools.build.lib.analysis.actions.StarlarkAction;
50+
import com.google.devtools.build.lib.analysis.actions.StarlarkAction.Builder;
5051
import com.google.devtools.build.lib.analysis.actions.Substitution;
5152
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
5253
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
@@ -712,15 +713,23 @@ private void registerStarlarkAction(
712713
} catch (IllegalArgumentException e) {
713714
throw Starlark.errorf("%s", e.getMessage());
714715
}
715-
if (envUnchecked != Starlark.NONE) {
716-
builder.setEnvironment(
717-
ImmutableMap.copyOf(Dict.cast(envUnchecked, String.class, String.class, "env")));
718-
}
719716
if (progressMessage != Starlark.NONE) {
720717
builder.setProgressMessageFromStarlark((String) progressMessage);
721718
}
719+
720+
ImmutableMap<String, String> env = null;
721+
if (envUnchecked != Starlark.NONE) {
722+
env = ImmutableMap.copyOf(Dict.cast(envUnchecked, String.class, String.class, "env"));
723+
}
722724
if (Starlark.truth(useDefaultShellEnv)) {
723-
builder.useDefaultShellEnvironment();
725+
if (env != null && getSemantics().getBool(
726+
BuildLanguageOptions.INCOMPATIBLE_MERGE_FIXED_AND_DEFAULT_SHELL_ENV)) {
727+
builder.useDefaultShellEnvironmentWithOverrides(env);
728+
} else {
729+
builder.useDefaultShellEnvironment();
730+
}
731+
} else if (env != null) {
732+
builder.setEnvironment(env);
724733
}
725734

726735
ImmutableMap<String, String> executionInfo =

src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,18 @@ public final class BuildLanguageOptions extends OptionsBase {
649649
+ " specified through features configuration.")
650650
public boolean experimentalGetFixedConfiguredEnvironment;
651651

652+
@Option(
653+
name = "incompatible_merge_fixed_and_default_shell_env",
654+
defaultValue = "false",
655+
documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
656+
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
657+
metadataTags = {OptionMetadataTag.INCOMPATIBLE_CHANGE},
658+
help = "If enabled, actions registered with ctx.actions.run and ctx.actions.run_shell with"
659+
+ " both 'env' and 'use_default_shell_env = True' specified will use an environment"
660+
+ " obtained from the default shell environment by overriding with the values passed in"
661+
+ " to 'env'. If disabled, the value of 'env' is completely ignored in this case.")
662+
public boolean incompatibleMergeFixedAndDefaultShellEnv;
663+
652664
/**
653665
* An interner to reduce the number of StarlarkSemantics instances. A single Blaze instance should
654666
* never accumulate a large number of these and being able to shortcut on object identity makes a
@@ -740,6 +752,9 @@ public StarlarkSemantics toStarlarkSemantics() {
740752
.setBool(
741753
EXPERIMENTAL_GET_FIXED_CONFIGURED_ACTION_ENV,
742754
experimentalGetFixedConfiguredEnvironment)
755+
.setBool(
756+
INCOMPATIBLE_MERGE_FIXED_AND_DEFAULT_SHELL_ENV,
757+
incompatibleMergeFixedAndDefaultShellEnv)
743758
.build();
744759
return INTERNER.intern(semantics);
745760
}
@@ -827,6 +842,8 @@ public StarlarkSemantics toStarlarkSemantics() {
827842
"-incompatible_disable_starlark_host_transitions";
828843
public static final String EXPERIMENTAL_GET_FIXED_CONFIGURED_ACTION_ENV =
829844
"-experimental_get_fixed_configured_action_env";
845+
public static final String INCOMPATIBLE_MERGE_FIXED_AND_DEFAULT_SHELL_ENV =
846+
"-experimental_merge_fixed_and_default_shell_env";
830847

831848
// non-booleans
832849
public static final StarlarkSemantics.Key<String> EXPERIMENTAL_BUILTINS_BZL_PATH =

src/test/shell/integration/action_env_test.sh

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ load("//pkg:build.bzl", "environ")
3939
4040
environ(name = "no_default_env", env = 0)
4141
environ(name = "with_default_env", env = 1)
42+
environ(
43+
name = "with_default_and_fixed_env",
44+
env = 1,
45+
fixed_env = {
46+
"ACTION_FIXED": "action",
47+
"ACTION_AND_CLIENT_FIXED": "action",
48+
"ACTION_AND_CLIENT_INHERITED": "action",
49+
},
50+
)
4251
4352
sh_test(
4453
name = "test_env_foo",
@@ -72,12 +81,16 @@ def _impl(ctx):
7281
ctx.actions.run_shell(
7382
inputs=[],
7483
outputs=[output],
84+
env = ctx.attr.fixed_env,
7585
use_default_shell_env = ctx.attr.env,
7686
command="env > %s" % output.path)
7787
7888
environ = rule(
7989
implementation=_impl,
80-
attrs={"env": attr.bool(default=True)},
90+
attrs={
91+
"env": attr.bool(default=True),
92+
"fixed_env": attr.string_dict(),
93+
},
8194
outputs={"out": "%{name}.env"},
8295
)
8396
EOF
@@ -222,6 +235,52 @@ function test_use_default_shell_env {
222235
&& fail "dynamic action_env used, even though requested not to") || true
223236
}
224237

238+
function test_use_default_shell_env_and_fixed_env {
239+
ACTION_AND_CLIENT_INHERITED=client CLIENT_INHERITED=client \
240+
bazel build \
241+
--noincompatible_merge_fixed_and_default_shell_env \
242+
--action_env=ACTION_AND_CLIENT_FIXED=client \
243+
--action_env=ACTION_AND_CLIENT_INHERITED \
244+
--action_env=CLIENT_FIXED=client \
245+
--action_env=CLIENT_INHERITED \
246+
//pkg:with_default_and_fixed_env
247+
echo
248+
cat bazel-bin/pkg/with_default_and_fixed_env.env
249+
echo
250+
grep -q ACTION_AND_CLIENT_FIXED=client bazel-bin/pkg/with_default_and_fixed_env.env \
251+
|| fail "static action environment not honored"
252+
grep -q ACTION_AND_CLIENT_INHERITED=client bazel-bin/pkg/with_default_and_fixed_env.env \
253+
|| fail "dynamic action environment not honored"
254+
grep -q ACTION_FIXED bazel-bin/pkg/with_default_and_fixed_env.env \
255+
&& fail "fixed env provided by action should have been ignored"
256+
grep -q CLIENT_FIXED=client bazel-bin/pkg/with_default_and_fixed_env.env \
257+
|| fail "static action environment not honored"
258+
grep -q CLIENT_INHERITED=client bazel-bin/pkg/with_default_and_fixed_env.env \
259+
|| fail "dynamic action environment not honored"
260+
261+
ACTION_AND_CLIENT_INHERITED=client CLIENT_INHERITED=client \
262+
bazel build \
263+
--incompatible_merge_fixed_and_default_shell_env \
264+
--action_env=ACTION_AND_CLIENT_FIXED=client \
265+
--action_env=ACTION_AND_CLIENT_INHERITED \
266+
--action_env=CLIENT_FIXED=client \
267+
--action_env=CLIENT_INHERITED \
268+
//pkg:with_default_and_fixed_env
269+
echo
270+
cat bazel-bin/pkg/with_default_and_fixed_env.env
271+
echo
272+
grep -q ACTION_AND_CLIENT_FIXED=action bazel-bin/pkg/with_default_and_fixed_env.env \
273+
|| fail "action-provided env should have overridden static --action_env"
274+
grep -q ACTION_AND_CLIENT_INHERITED=action bazel-bin/pkg/with_default_and_fixed_env.env \
275+
|| fail "action-provided env should have overridden dynamic --action_env"
276+
grep -q ACTION_FIXED=action bazel-bin/pkg/with_default_and_fixed_env.env \
277+
|| fail "action-provided env should have been honored"
278+
grep -q CLIENT_FIXED=client bazel-bin/pkg/with_default_and_fixed_env.env \
279+
|| fail "static action environment not honored"
280+
grep -q CLIENT_INHERITED=client bazel-bin/pkg/with_default_and_fixed_env.env \
281+
|| fail "dynamic action environment not honored"
282+
}
283+
225284
function test_action_env_changes_honored {
226285
# Verify that changes to the explicitly specified action_env in honored in
227286
# tests. Regression test for #3265.

0 commit comments

Comments
 (0)