Add build option --repo_env

Add an option to environment variables only for repository rules. This
change is not about making more environment variables available to repository
rules, as they have access to the whole environment anyway. However, having
an option to set environment variables allows to use the config mechanism
of .bazelrc to pass information to external repositories without invalidating
the whole action graph.

This way of using external repositories to change sources according to the used
configuration is might not be in line with the idea of bazel to be able to build
for all configurations from the same source tree, but there is sufficient demand
for such a feature.

Change-Id: Ib312958f5faa055863f1cd643efbcff63d0b16a3
PiperOrigin-RevId: 243810658
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index 266e075..8b9a60e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -591,6 +591,20 @@
     public List<Map.Entry<String, String>> actionEnvironment;
 
     @Option(
+        name = "repo_env",
+        converter = Converters.OptionalAssignmentConverter.class,
+        allowMultiple = true,
+        defaultValue = "",
+        documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
+        effectTags = {OptionEffectTag.ACTION_COMMAND_LINES},
+        help =
+            "Specifies additional environment variables to be available only for repository rules."
+                + " Note that repository rules see the full environment anyway, but in this way"
+                + " configuration information can be passed to repositories through options without"
+                + " invalidating the action graph.")
+    public List<Map.Entry<String, String>> repositoryEnvironment;
+
+    @Option(
       name = "collect_code_coverage",
       defaultValue = "false",
       documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
index 1e4f2c8..4c3126d 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -217,7 +217,7 @@
 
   @Override
   public void beforeCommand(CommandEnvironment env) {
-    clientEnvironmentSupplier.set(env.getActionClientEnv());
+    clientEnvironmentSupplier.set(env.getRepoEnv());
     PackageCacheOptions pkgOptions = env.getOptions().getOptions(PackageCacheOptions.class);
     isFetch.set(pkgOptions != null && pkgOptions.fetch);
     resolvedFile = Optional.<RootedPath>absent();
diff --git a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java
index e5827c5..2b2332b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/repository/RepositoryFunction.java
@@ -40,6 +40,7 @@
 import com.google.devtools.build.lib.skyframe.ActionEnvironmentFunction;
 import com.google.devtools.build.lib.skyframe.PackageLookupFunction;
 import com.google.devtools.build.lib.skyframe.PackageLookupValue;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Type;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -54,6 +55,7 @@
 import com.google.devtools.build.skyframe.SkyKey;
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 import javax.annotation.Nullable;
@@ -309,11 +311,23 @@
     if (environ == null) {
       return null;
     }
+
+    Map<String, String> repoEnvOverride = PrecomputedValue.REPO_ENV.get(env);
+    if (repoEnvOverride == null) {
+      return null;
+    }
+
+    Map<String, String> repoEnv = new LinkedHashMap<String, String>(environ);
+    for (Map.Entry<String, String> value : repoEnvOverride.entrySet()) {
+      repoEnv.put(value.getKey(), value.getValue());
+    }
+
     // Add the dependencies to the marker file
-    for (Map.Entry<String, String> value : environ.entrySet()) {
+    for (Map.Entry<String, String> value : repoEnv.entrySet()) {
       markerData.put("ENV:" + value.getKey(), value.getValue());
     }
-    return environ;
+
+    return repoEnv;
   }
 
   /**
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 4325c4d..939975e 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
@@ -77,6 +77,7 @@
   private final Set<String> visibleActionEnv = new TreeSet<>();
   private final Set<String> visibleTestEnv = new TreeSet<>();
   private final Map<String, String> actionClientEnv = new TreeMap<>();
+  private final Map<String, String> repoEnv = new TreeMap<>();
   private final TimestampGranularityMonitor timestampGranularityMonitor;
   private final Thread commandThread;
   private final Command command;
@@ -202,6 +203,14 @@
         }
       }
     }
+
+    repoEnv.putAll(actionClientEnv);
+    BuildConfiguration.Options configOpts = options.getOptions(BuildConfiguration.Options.class);
+    if (configOpts != null) {
+      for (Map.Entry<String, String> entry : configOpts.repositoryEnvironment) {
+        repoEnv.put(entry.getKey(), entry.getValue());
+      }
+    }
   }
 
   // Returns whether the given command supports --package_path
@@ -712,4 +721,9 @@
   public Map<String, String> getActionClientEnv() {
     return Collections.unmodifiableMap(actionClientEnv);
   }
+
+  /** Returns the client environment with all settings from --action_env and --repo_env. */
+  public Map<String, String> getRepoEnv() {
+    return Collections.unmodifiableMap(repoEnv);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
index e2b6261..a40f55c 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
@@ -92,6 +92,9 @@
   public static final Precomputed<Map<String, String>> ACTION_ENV =
       new Precomputed<>(Key.create("action_env"));
 
+  public static final Precomputed<Map<String, String>> REPO_ENV =
+      new Precomputed<>(Key.create("repo_env"));
+
   static final Precomputed<ImmutableList<ActionAnalysisMetadata>> COVERAGE_REPORT_KEY =
       new Precomputed<>(Key.create("coverage_report_actions"));
 
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 134849b..5aec27b 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
@@ -2440,6 +2440,7 @@
       OptionsProvider options)
       throws InterruptedException, AbruptExitException {
     getActionEnvFromOptions(options);
+    setRepoEnv(options);
     RemoteOptions remoteOptions = options.getOptions(RemoteOptions.class);
     setRemoteOutputsMode(
         remoteOptions != null
@@ -2501,6 +2502,17 @@
     PrecomputedValue.ACTION_ENV.set(injectable(), actionEnv);
   }
 
+  private void setRepoEnv(OptionsProvider options) {
+    BuildConfiguration.Options opt = options.getOptions(BuildConfiguration.Options.class);
+    LinkedHashMap<String, String> repoEnv = new LinkedHashMap<>();
+    if (opt != null) {
+      for (Map.Entry<String, String> v : opt.repositoryEnvironment) {
+        repoEnv.put(v.getKey(), v.getValue());
+      }
+    }
+    PrecomputedValue.REPO_ENV.set(injectable(), repoEnv);
+  }
+
   public PathPackageLocator createPackageLocator(
       ExtendedEventHandler eventHandler, List<String> packagePaths, Path workingDirectory) {
     return PathPackageLocator.create(
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 8998f25..58fa36d 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
@@ -246,6 +246,7 @@
 
     ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues =
         ImmutableList.of(
+            PrecomputedValue.injected(PrecomputedValue.REPO_ENV, ImmutableMap.<String, String>of()),
             PrecomputedValue.injected(
                 RepositoryDelegatorFunction.REPOSITORY_OVERRIDES,
                 ImmutableMap.<RepositoryName, PathFragment>of()),
diff --git a/src/test/shell/bazel/skylark_repository_test.sh b/src/test/shell/bazel/skylark_repository_test.sh
index f588876..ef3c2b0 100755
--- a/src/test/shell/bazel/skylark_repository_test.sh
+++ b/src/test/shell/bazel/skylark_repository_test.sh
@@ -809,6 +809,80 @@
   file_invalidation_test_template --batch
 }
 
+function test_repo_env() {
+  setup_skylark_repository
+
+  cat > test.bzl <<'EOF'
+def _impl(ctx):
+  # Make a rule depending on the environment variable FOO,
+  # properly recording its value. Also add a time stamp
+  # to verify that the rule is rerun.
+  ctx.execute(["bash", "-c", "echo FOO=$FOO > env.txt"])
+  ctx.execute(["bash", "-c", "date +%s >> env.txt"])
+  ctx.file("BUILD", 'exports_files(["env.txt"])')
+
+repo = repository_rule(
+  implementation = _impl,
+  environ = ["FOO"],
+)
+EOF
+  cat > BUILD <<'EOF'
+genrule(
+  name = "repoenv",
+  outs = ["repoenv.txt"],
+  srcs = ["@foo//:env.txt"],
+  cmd = "cp $< $@",
+)
+
+# Have a normal rule, unrelated to the external repository.
+# To test if it was rerun, make it non-hermetic and record a
+# time stamp.
+genrule(
+  name = "unrelated",
+  outs = ["unrelated.txt"],
+  cmd = "date +%s > $@",
+)
+EOF
+  cat > .bazelrc <<EOF
+build:foo --repo_env=FOO=foo
+build:bar --repo_env=FOO=bar
+EOF
+
+  bazel build --config=foo //:repoenv //:unrelated
+  cp `bazel info bazel-genfiles 2>/dev/null`/repoenv.txt repoenv1.txt
+  cp `bazel info bazel-genfiles 2> /dev/null`/unrelated.txt unrelated1.txt
+  echo; cat repoenv1.txt; echo; cat unrelated1.txt; echo
+
+  grep -q 'FOO=foo' repoenv1.txt \
+      || fail "Expected FOO to be visible to repo rules"
+
+  sleep 2 # ensure any rerun will have a different time stamp
+
+  FOO=CHANGED bazel build --config=foo //:repoenv //:unrelated
+  # nothing should change, as actions don't see FOO and for repositories
+  # the value is fixed by --repo_env
+  cp `bazel info bazel-genfiles 2>/dev/null`/repoenv.txt repoenv2.txt
+  cp `bazel info bazel-genfiles 2> /dev/null`/unrelated.txt unrelated2.txt
+  echo; cat repoenv2.txt; echo; cat unrelated2.txt; echo
+
+  diff repoenv1.txt repoenv2.txt \
+      || fail "Expected repository to not change"
+  diff unrelated1.txt unrelated2.txt \
+      || fail "Expected unrelated action to not be rerun"
+
+  bazel build --config=bar //:repoenv //:unrelated
+  # The new config should be picked up, but the unrelated target should
+  # not be rerun
+  cp `bazel info bazel-genfiles 3>/dev/null`/repoenv.txt repoenv3.txt
+  cp `bazel info bazel-genfiles 3> /dev/null`/unrelated.txt unrelated3.txt
+  echo; cat repoenv3.txt; echo; cat unrelated3.txt; echo
+
+  grep -q 'FOO=bar' repoenv3.txt \
+      || fail "Expected FOO to be visible to repo rules"
+  diff unrelated1.txt unrelated3.txt \
+      || fail "Expected unrelated action to not be rerun"
+}
+
 function test_skylark_repository_executable_flag() {
   setup_skylark_repository