From 5f9e0d4ab5e34c3d102809f60aa47c37f094e1ae Mon Sep 17 00:00:00 2001 From: Dylan Frankland Date: Mon, 31 Aug 2026 12:22:13 -0700 Subject: [PATCH] refactor(xcresult): find a language server without requiring xcrun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `xcrun --find` is the only way to locate a tool inside an Xcode toolchain, so it is right on macOS and useless anywhere else — it returned `None` off macOS by construction. That was fine while the only consumer read `.xcresult` bundles, which cannot exist without Xcode. The Swift toolchain on Linux ships `sourcekit-lsp` on `PATH` and has no `xcrun` at all, so discovery now tries `xcrun` on macOS and falls back to a `PATH` scan. Nothing else in `test_locations.rs` or `lsp.rs` is platform-specific, which makes this the only thing standing between the declaration index and a non-Apple host. The scan also checks the executable bit rather than just for a file of the right name, so a stray non-executable `sourcekit-lsp` reports "not found" instead of failing later at spawn time with something less obvious. Both new tests run on any platform, which is the point. Co-Authored-By: Claude Opus 5 (1M context) --- xcresult/src/test_locations.rs | 4 +- xcresult/src/xcrun.rs | 81 +++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/xcresult/src/test_locations.rs b/xcresult/src/test_locations.rs index 36abb5cb..01d71051 100644 --- a/xcresult/src/test_locations.rs +++ b/xcresult/src/test_locations.rs @@ -11,7 +11,7 @@ use std::{ use ignore::{WalkBuilder, types::TypesBuilder}; use lsp_types::{DocumentSymbol, SymbolKind}; -use crate::{file_attribution::ReportedPath, lsp::LanguageServer, xcrun::xcrun_find}; +use crate::{file_attribution::ReportedPath, lsp::LanguageServer, xcrun::find_program}; /// Kinds that can declare a test. const METHOD_KINDS: [SymbolKind; 3] = [ @@ -357,7 +357,7 @@ impl Resolver { if files.is_empty() || self.unresolved.is_empty() { return; } - let Some(program) = xcrun_find(kind.program) else { + let Some(program) = find_program(kind.program) else { tracing::warn!( "{} not found; {} source file(s) left unparsed", kind.program, diff --git a/xcresult/src/xcrun.rs b/xcresult/src/xcrun.rs index 9498c84e..ea5296b2 100644 --- a/xcresult/src/xcrun.rs +++ b/xcresult/src/xcrun.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, path::PathBuf, process::Command}; +use std::{ffi::OsStr, fs, path::Path, path::PathBuf, process::Command}; use lazy_static::lazy_static; use serde::Deserialize; @@ -34,6 +34,45 @@ pub fn xcresulttool_get_test_results_summary>( } /// `None` when `name` ships in neither Xcode nor the Command Line Tools. +/// Locate a developer tool. `xcrun` is the only way to find one inside an Xcode toolchain, +/// but on Linux the Swift toolchain puts `sourcekit-lsp` on `PATH` and there is no `xcrun`. +pub fn find_program(name: &str) -> Option { + if cfg!(target_os = "macos") + && let Some(path) = xcrun_find(name) + { + return Some(path); + } + which_program(name) +} + +fn which_program(name: &str) -> Option { + which_program_in(name, &std::env::var_os("PATH")?) +} + +fn which_program_in(name: &str, path_var: &OsStr) -> Option { + std::env::split_paths(path_var) + .map(|directory| directory.join(name)) + .find(|candidate| is_executable_file(candidate)) +} + +/// A non-executable file of the right name is not the program, and spawning it would fail +/// later with something far less obvious than "not found". +fn is_executable_file(path: &Path) -> bool { + let Ok(metadata) = fs::metadata(path) else { + return false; + }; + if !metadata.is_file() { + return false; + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + metadata.permissions().mode() & 0o111 != 0 + } + #[cfg(not(unix))] + true +} + pub fn xcrun_find(name: &str) -> Option { if !cfg!(target_os = "macos") { return None; @@ -160,3 +199,43 @@ fn xcrun>(args: &[T]) -> anyhow::Result { let result = String::from_utf8(data)?; Ok(result) } + +#[cfg(test)] +mod tests { + use super::*; + + // The Linux Swift toolchain puts `sourcekit-lsp` on `PATH` with no `xcrun` to ask, so the + // fallback is the only way the declaration path can find a server there. + #[test] + fn a_program_on_path_is_found_without_xcrun() { + let temp_dir = tempfile::tempdir().unwrap(); + let elsewhere = tempfile::tempdir().unwrap(); + let program = temp_dir.path().join("pretend-lsp"); + fs::write(&program, b"#!/bin/sh\n").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&program, fs::Permissions::from_mode(0o755)).unwrap(); + } + let path_var = std::env::join_paths([elsewhere.path(), temp_dir.path()]).unwrap(); + + assert_eq!( + which_program_in("pretend-lsp", &path_var), + Some(program.clone()) + ); + assert_eq!(which_program_in("not-installed", &path_var), None); + } + + #[cfg(unix)] + #[test] + fn a_file_without_the_executable_bit_is_not_the_program() { + use std::os::unix::fs::PermissionsExt; + let temp_dir = tempfile::tempdir().unwrap(); + let program = temp_dir.path().join("pretend-lsp"); + fs::write(&program, b"not executable").unwrap(); + fs::set_permissions(&program, fs::Permissions::from_mode(0o644)).unwrap(); + let path_var = std::env::join_paths([temp_dir.path()]).unwrap(); + + assert_eq!(which_program_in("pretend-lsp", &path_var), None); + } +}