Fixes BEP path handling for rust analyzer specs (#4130)

The rewrite of the Rust Analyzer auto-discovery pipeline (see #4075)
introduced a bug for users who use
`--experimental_convenience_symlinks=ignore` (i.e. have no bazel-out
symlink in their workspace).

`parse_output_group_paths` documentation suggests that output paths are
always absolute


https://github.com/bazelbuild/rules_rust/blob/dedf05304354aace6a76ae78030a9d69c74e37d4/tools/rust_analyzer/bep.rs#L107-L110

but that is not true for BEP events that trigger the fallback
reconstruction path


https://github.com/bazelbuild/rules_rust/blob/dedf05304354aace6a76ae78030a9d69c74e37d4/tools/rust_analyzer/bep.rs#L241-L243

This PR detects those cases and anchors those paths to the execution
root, making them also absolute.
diff --git a/tools/rust_analyzer/bep.rs b/tools/rust_analyzer/bep.rs
index 5d30727..58eae14 100644
--- a/tools/rust_analyzer/bep.rs
+++ b/tools/rust_analyzer/bep.rs
@@ -110,6 +110,7 @@
 pub fn parse_output_group_paths(
     bep_path: &Utf8Path,
     output_group: &str,
+    execution_root: &Utf8Path,
 ) -> Result<Vec<Utf8PathBuf>> {
     let file = File::open(bep_path).with_context(|| format!("opening BEP file {bep_path}"))?;
     let reader = BufReader::new(file);
@@ -162,7 +163,12 @@
         };
         for file in &set.files {
             if let Some(path) = file_to_path(file) {
-                paths.push(path);
+                let mut absolute_path = path;
+                if !absolute_path.is_absolute() {
+                    absolute_path = execution_root.join(absolute_path);
+                }
+
+                paths.push(absolute_path);
             }
         }
         for child in &set.file_sets {
@@ -177,8 +183,11 @@
 
 /// Convenience wrapper for the `rust_analyzer_crate_spec` output group used
 /// during project discovery.
-pub fn parse_spec_paths(bep_path: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
-    parse_output_group_paths(bep_path, SPEC_OUTPUT_GROUP)
+pub fn parse_spec_paths(
+    bep_path: &Utf8Path,
+    execution_root: &Utf8Path,
+) -> Result<Vec<Utf8PathBuf>> {
+    parse_output_group_paths(bep_path, SPEC_OUTPUT_GROUP, execution_root)
 }
 
 /// Return the stderr file path captured for every completed Bazel action
@@ -347,7 +356,7 @@
 "#,
         )
         .unwrap();
-        let paths = parse_spec_paths(&bep_path).unwrap();
+        let paths = parse_spec_paths(&bep_path, Utf8Path::new("/execroot")).unwrap();
         assert_eq!(
             paths,
             vec![
@@ -358,6 +367,26 @@
     }
 
     #[test]
+    fn parse_spec_paths_handles_relative_paths() {
+        let dir = tempdir();
+        let bep_path = dir.join("bep.json");
+        std::fs::write(
+            &bep_path,
+            r#"{"id":{"namedSet":{"id":"0"}},"namedSetOfFiles":{"files":[{"uri":"bytestream://remote/blob","pathPrefix":["bazel-out","k8-fastbuild","bin"],"name":"pkg/lib.rust_analyzer_crate_spec.json"}]}}
+{"id":{"targetCompleted":{"label":"//pkg:lib"}},"completed":{"outputGroup":[{"name":"rust_analyzer_crate_spec","fileSets":[{"id":"0"}]}]}}
+"#,
+        )
+        .unwrap();
+        let paths = parse_spec_paths(&bep_path, Utf8Path::new("/execroot")).unwrap();
+        assert_eq!(
+            paths,
+            vec![Utf8PathBuf::from(
+                "/execroot/bazel-out/k8-fastbuild/bin/pkg/lib.rust_analyzer_crate_spec.json",
+            )]
+        );
+    }
+
+    #[test]
     fn parse_output_group_paths_filters_by_group() {
         let dir = tempdir();
         let bep_path = dir.join("bep.json");
@@ -369,9 +398,16 @@
 "#,
         )
         .unwrap();
-        let rustc = parse_output_group_paths(&bep_path, RUSTC_OUTPUT_GROUP).unwrap();
+        let rustc =
+            parse_output_group_paths(&bep_path, RUSTC_OUTPUT_GROUP, Utf8Path::new("/execroot"))
+                .unwrap();
         assert_eq!(rustc, vec![Utf8PathBuf::from("/abs/lib.rustc-output")]);
-        let specs = parse_output_group_paths(&bep_path, "rust_analyzer_crate_spec").unwrap();
+        let specs = parse_output_group_paths(
+            &bep_path,
+            "rust_analyzer_crate_spec",
+            Utf8Path::new("/execroot"),
+        )
+        .unwrap();
         assert_eq!(specs, vec![Utf8PathBuf::from("/abs/lib.spec.json")]);
     }
 
diff --git a/tools/rust_analyzer/lib.rs b/tools/rust_analyzer/lib.rs
index e08e931..71fbdbb 100644
--- a/tools/rust_analyzer/lib.rs
+++ b/tools/rust_analyzer/lib.rs
@@ -87,7 +87,7 @@
         &bep_file,
     )?;
 
-    let spec_paths = match bep::parse_spec_paths(&bep_file) {
+    let spec_paths = match bep::parse_spec_paths(&bep_file, execution_root) {
         Ok(paths) => paths,
         // A failed build often means a missing or partial BEP file; surface
         // the build error rather than the downstream parse error.