[9.4.0] Add `external_directory` info item (#31170) (#32222)

`bazel info external_directory` prints the directory under which
external repositories are fetched (currently `$(bazel info
output_base)/external`).

If users don't have to hardcode the `external/` string, we could change
it to `bazel-external/` at some point in the future.

No

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

RELNOTES: Added `bazel info external_directory`, which prints the
directory under which external repositories are fetched.

Closes #31170


COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/bazel/pull/31170
from fmeum:claude/external-root-info-item-d3h3bs
8dc0c3f7b0419d431c63476c4cd92804ac46dca8 PiperOrigin-RevId: 1002378183
Change-Id: I816de546dc6486af8dc90b3d9cc4662e97160c02

<!--
Thank you for contributing to Bazel!
Please read the contribution guidelines: https://bazel.build/contribute
and guidelines for AI-assisted contributions:
https://bazel.build/contribute/ai-assisted-contributions
-->

### Description
<!--
Please provide a brief summary of the changes in this PR.
-->

### Motivation
<!--
Why is this change important? Does it fix a specific bug or add a new
feature?
If this PR fixes an existing issue, please link it here (e.g. "Fixes
#1234").
-->

### Build API Changes
<!--
Does this PR affect the Build API? (e.g. Starlark API, providers,
command-line flags, native rules)
If yes, please answer the following:
1. Has this been discussed in a design doc or issue? (Please link it)
2. Is the change backward compatible?
3. If it's a breaking change, what is the migration plan?
-->

No

### Checklist

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

### Release Notes

<!--
If this is a new feature, please add 'RELNOTES[NEW]: <description>'
here.
If this is a breaking change, please add 'RELNOTES[INC]: <reason>' here.
If this change should be mentioned in release notes, please add
'RELNOTES: <reason>' here.
-->

RELNOTES: None

Commit
https://github.com/bazelbuild/bazel/commit/263a69f54f4f1b6583199b0a7955ba1e58f7d54d

Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
diff --git a/docs/external/overview.mdx b/docs/external/overview.mdx
index 2a06cc6c..0c9b023 100644
--- a/docs/external/overview.mdx
+++ b/docs/external/overview.mdx
@@ -212,14 +212,14 @@
 
 ### Directory layout {:#directory-layout}
 
-After being fetched, the repo can be found in the subdirectory `external` in the
-[output base](/remote/output-directories), under its canonical name.
+After being fetched, the repo can be found in the external directory, which is
+printed by `bazel info external_directory`, under its canonical name.
 
 You can run the following command to see the contents of the repo with the
 canonical name `canonical_name`:
 
 ```posix-terminal
-ls $(bazel info output_base)/external/{{ '<var>' }} canonical_name {{ '</var>' }}
+ls $(bazel info external_directory)/{{ '<var>' }} canonical_name {{ '</var>' }}
 ```
 
 ### REPO.bazel file {:#repo.bazel}
diff --git a/docs/external/vendor.mdx b/docs/external/vendor.mdx
index 60b094d..dfc1896 100644
--- a/docs/external/vendor.mdx
+++ b/docs/external/vendor.mdx
@@ -140,7 +140,7 @@
 ## Understand how vendor mode works {:#how-vendor-mode-works}
 
 Bazel fetches external dependencies of a project under `$(bazel info
-output_base)/external`. Vendoring external dependencies means moving out
+external_directory)`. Vendoring external dependencies means moving out
 relevant files and directories to the given vendor directory and use the
 vendored source for later builds.
 
@@ -151,7 +151,7 @@
 
 During a build, if the vendored marker file is up-to-date or the repo is
 pinned in the VENDOR.bazel file, then Bazel uses the vendored source by creating
-a symlink to it under `$(bazel info output_base)/external` instead of actually
+a symlink to it under `$(bazel info external_directory)` instead of actually
 running the repository rule. Otherwise, a warning is printed and Bazel will
 fallback to fetching the latest version of the repo.
 
@@ -175,16 +175,15 @@
 strategy to rewrite symlinks in the vendored source:
 
 - Create a symlink `<vendor_dir>/bazel-external` that points to `$(bazel info
-output_base)/external`. It is refreshed by every Bazel command
-  automatically.
+  external_directory)`. It is refreshed by every Bazel command automatically.
 - For the vendored source, rewrite all symlinks that originally point to a
-  path under `$(bazel info output_base)/external` to a relative path under
+  path under `$(bazel info external_directory)` to a relative path under
   `<vendor_dir>/bazel-external`.
 
 For example, if the original symlink is
 
 ```none
-<vendor_dir>/repo_foo+/link  =>  $(bazel info output_base)/external/repo_bar+/file
+<vendor_dir>/repo_foo+/link  =>  $(bazel info external_directory)/repo_bar+/file
 ```
 
 It will be rewritten to
@@ -196,7 +195,7 @@
 where
 
 ```none
-<vendor_dir>/bazel-external  =>  $(bazel info output_base)/external  # This might be new if output base is changed
+<vendor_dir>/bazel-external  =>  $(bazel info external_directory)  # This might be new if output base is changed
 ```
 
 Since `<vendor_dir>/bazel-external` is generated by Bazel automatically, it's
@@ -206,8 +205,8 @@
 after the vendored source is moved to another location or the bazel output base
 is changed.
 
-Note: symlinks that point to an absolute path outside of $(bazel info
-output_base)/external are not rewritten. Therefore, it could still break
+Note: symlinks that point to an absolute path outside of `$(bazel info
+external_directory)` are not rewritten. Therefore, it could still break
 cross-machine compatibility.
 
 Note: On Windows, vendoring symlinks only works with
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
index 20937b6..1a1f7c6 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/InfoCommand.java
@@ -44,6 +44,7 @@
 import com.google.devtools.build.lib.runtime.commands.info.DefaultPackagePathInfoItem;
 import com.google.devtools.build.lib.runtime.commands.info.DefaultsPackageInfoItem;
 import com.google.devtools.build.lib.runtime.commands.info.ExecutionRootInfoItem;
+import com.google.devtools.build.lib.runtime.commands.info.ExternalDirectoryInfoItem;
 import com.google.devtools.build.lib.runtime.commands.info.GcCountInfoItem;
 import com.google.devtools.build.lib.runtime.commands.info.GcTimeInfoItem;
 import com.google.devtools.build.lib.runtime.commands.info.InfoItemHandler;
@@ -305,6 +306,7 @@
             new InstallMd5InfoItem(),
             new OutputBaseInfoItem(productName),
             new ExecutionRootInfoItem(),
+            new ExternalDirectoryInfoItem(),
             new OutputPathInfoItem(),
             new ClientEnv(),
             new BlazeBinInfoItem(productName),
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/info/ExternalDirectoryInfoItem.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/info/ExternalDirectoryInfoItem.java
new file mode 100644
index 0000000..6ba0a7b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/info/ExternalDirectoryInfoItem.java
@@ -0,0 +1,44 @@
+// Copyright 2026 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.build.lib.runtime.commands.info;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Supplier;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
+import com.google.devtools.build.lib.cmdline.LabelConstants;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.runtime.InfoItem;
+
+/** Info item for the external_directory, under which external repositories are fetched. */
+public final class ExternalDirectoryInfoItem extends InfoItem {
+  public ExternalDirectoryInfoItem() {
+    super(
+        "external_directory",
+        "The directory under which external repositories are fetched, one subdirectory per"
+            + " canonical repository name.",
+        false);
+  }
+
+  @Override
+  public byte[] get(
+      Supplier<BuildConfigurationValue> configurationSupplier, CommandEnvironment env) {
+    checkNotNull(env);
+    return print(
+        env.getDirectories()
+            .getOutputBase()
+            .getRelative(LabelConstants.EXTERNAL_REPOSITORY_LOCATION));
+  }
+}
diff --git a/src/test/shell/integration/info_test.sh b/src/test/shell/integration/info_test.sh
index fb1a883..adb54ad 100755
--- a/src/test/shell/integration/info_test.sh
+++ b/src/test/shell/integration/info_test.sh
@@ -65,6 +65,13 @@
   expect_log '[0-9]*'
 }
 
+function test_external_directory() {
+  bazel info external_directory >$TEST_log \
+    || fail "${PRODUCT_NAME} info failed"
+  local -r output_base="$(bazel info output_base 2>/dev/null)"
+  expect_log "^${output_base}/external\$"
+}
+
 function test_used_heap_size_after_gc() {
   bazel info used-heap-size-after-gc >$TEST_log \
     || fail "${PRODUCT_NAME} info failed"