Mount the repo contents cache under the sandbox's hermetic /tmp (https://github.com/bazelbuild/bazel/pull/29876)

### Description

### Motivation
A repo fetched into the repo contents cache is materialized in the output base as a symlink into the cache. When the cache lives under /tmp but outside the output base (e.g. when relocated via `--repository_cache` or `--repo_contents_cache`), the symlink target wasn't made available inside a sandbox that uses a hermetic `/tmp`.

Fixes #29649

### Build API Changes

No

### Checklist

- [ ] I have added tests for the new use cases (if any).
- [ ] I have updated the documentation (if applicable).

### Release Notes

RELNOTES: None

Closes #29876.

PiperOrigin-RevId: 935046365
Change-Id: Idbc024b147665b764582b6a15606508bd9bbf428
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 554bb98..07cb396 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
@@ -680,6 +680,15 @@
     return workspace.getSkyframeExecutor();
   }
 
+  /**
+   * Returns the path of the repo contents cache directory, or {@code null} if the repo contents
+   * cache is disabled.
+   */
+  @Nullable
+  public Path getRepoContentsCachePath() {
+    return getSkyframeExecutor().getRepoContentsCachePath();
+  }
+
   public SkyframeBuildView getSkyframeBuildView() {
     return getSkyframeExecutor().getSkyframeBuildView();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index 2b80aaa..f70bf30 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -61,6 +61,7 @@
 import java.time.Duration;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -184,7 +185,9 @@
     // or well-known children of /tmp from the host.
     // TODO(bazel-team): Review all flags whose path may have to be considered here.
     return Stream.concat(
-            Stream.of(sandboxBase, cmdEnv.getOutputBase()),
+            Stream.concat(
+                Stream.of(sandboxBase, cmdEnv.getOutputBase()),
+                Optional.ofNullable(cmdEnv.getRepoContentsCachePath()).stream()),
             cmdEnv.getPackageLocator().getPathEntries().stream().map(Root::asPath))
         .filter(p -> p.startsWith(slashTmp))
         // For any path /tmp/dir1/dir2 we encounter, we instead mount /tmp/dir1 (first two
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java b/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java
index 614011f..0947320 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java
@@ -119,6 +119,15 @@
   }
 
   /**
+   * Returns the path of the repo contents cache directory, or {@code null} if the repo contents
+   * cache is disabled. Fetched repos may be materialized as symlinks into this directory.
+   */
+  @Nullable
+  public Path getRepoContentsCachePath() {
+    return repoContentsCachePathSupplier.get();
+  }
+
+  /**
    * The action to take when an external path is encountered. See {@link FileType} for the
    * definition of "external".
    */
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 0b682eb..a3d2ced 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
@@ -2794,6 +2794,15 @@
     return packageManager;
   }
 
+  /**
+   * Returns the path of the repo contents cache directory, or {@code null} if the repo contents
+   * cache is disabled.
+   */
+  @Nullable
+  public Path getRepoContentsCachePath() {
+    return externalFilesHelper.getRepoContentsCachePath();
+  }
+
   public QueryTransitivePackagePreloader getQueryTransitivePackagePreloader() {
     return queryTransitivePackagePreloader;
   }
diff --git a/src/test/shell/integration/sandboxing_test.sh b/src/test/shell/integration/sandboxing_test.sh
index 0855814..eadc733 100755
--- a/src/test/shell/integration/sandboxing_test.sh
+++ b/src/test/shell/integration/sandboxing_test.sh
@@ -827,6 +827,58 @@
   [[ -f "${temp_dir}/file" ]] || fail "Expected ${temp_dir}/file to exist"
 }
 
+# Regression test for https://github.com/bazelbuild/bazel/issues/29649
+function test_repo_contents_cache_under_hermetic_tmp {
+  if ! is_linux; then
+    echo "Skipping test: hermetic /tmp is only supported in Linux" 1>&2
+    return 0
+  fi
+
+  if [[ "${PRODUCT_NAME}" != "bazel" ]]; then
+    echo "Skipping test: repo contents cache is only supported in Bazel" 1>&2
+    return 0
+  fi
+
+  # Place the repo contents cache directly under /tmp at a location that is not
+  # contained in the output base.
+  # Not declared local so that it is still bound when the EXIT trap runs.
+  repo_contents_cache=$(mktemp -d /tmp/repo_contents_cache.XXXXXX)
+  trap 'rm -rf ${repo_contents_cache}' EXIT
+
+  cat > repo.bzl <<'EOF'
+def _cached_repo_impl(rctx):
+    rctx.file("BUILD", "exports_files(['data.txt'])")
+    rctx.file("data.txt", "hello from the cached repo\n")
+    # Mark the repo as reproducible so it is stored in the repo contents cache
+    # and materialized as a symlink into it.
+    return rctx.repo_metadata(reproducible = True)
+
+cached_repo = repository_rule(implementation = _cached_repo_impl)
+EOF
+  touch BUILD
+
+  cat >> MODULE.bazel <<'EOF'
+cached_repo = use_repo_rule("//:repo.bzl", "cached_repo")
+cached_repo(name = "cached_repo")
+EOF
+
+  mkdir -p pkg
+  cat > pkg/BUILD <<'EOF'
+genrule(
+    name = "use_cached_repo",
+    srcs = ["@cached_repo//:data.txt"],
+    outs = ["out.txt"],
+    cmd = "cat $(location @cached_repo//:data.txt) > $@",
+)
+EOF
+
+  bazel build //pkg:use_cached_repo \
+    --repo_contents_cache="${repo_contents_cache}" &>"$TEST_log" \
+    || fail "Expected build to succeed"
+
+  assert_contains "hello from the cached repo" bazel-genfiles/pkg/out.txt
+}
+
 function test_sandbox_reuse_stashes_sandbox() {
   mkdir pkg
   cat >pkg/BUILD <<'EOF'