From bc390f48b55df9063519834e9943679925a9b5a6 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:49:44 +0800 Subject: [PATCH] fix(sdk): detect versioned PHP binaries such as php8.1 on PATH PHP discovery matched only the exact name php on PATH, so Debian and Ubuntu installs with several versions (ppa:ondrej/php, packages.sury.org) listed only the alternatives default. PATH directories are now also scanned for php (php8.1, php81), skipping shim directories and the php-cgi / phpize / phpdbg helpers. The plain php comes first, so deduplication by real path keeps /usr/bin/php over the php8.3 it links to. --- crates/ide-sdk/src/discover.rs | 39 ++++++++++++++++++++++++++++++++-- crates/ide-sdk/src/tests.rs | 19 +++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/ide-sdk/src/discover.rs b/crates/ide-sdk/src/discover.rs index d5141e270..4870f4b0d 100644 --- a/crates/ide-sdk/src/discover.rs +++ b/crates/ide-sdk/src/discover.rs @@ -282,8 +282,38 @@ pub(crate) fn php_source(path: &Path, home: Option<&Path>) -> &'static str { } } -/// `php` CLIs: every one on `PATH`, Homebrew's versioned formulae, PHP version managers, -/// Laravel Herd, and the usual Windows distributions (XAMPP, Laragon). +/// A PHP CLI named with its version, as Debian / Ubuntu (`ppa:ondrej/php`) install them: +/// `php8.1`, `php81`. Not `php-cgi8.1`, `phpize8.1` or `phpdbg8.1`. +pub(crate) fn is_versioned_php_name(name: &str) -> bool { + let name = name.strip_suffix(".exe").unwrap_or(name); + let Some(version) = name.strip_prefix("php") else { return false }; + let mut parts = version.splitn(2, '.'); + let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()); + parts.next().is_some_and(digits) && parts.next().is_none_or(digits) +} + +/// The versioned PHP CLIs in `dirs`, in `PATH` order (sorted by name within a directory). +pub(crate) fn versioned_php_in(dirs: impl IntoIterator) -> Vec { + let mut found = Vec::new(); + for dir in dirs { + if version_managers::is_shims_dir(&dir) { + continue; + } + let mut names: Vec = std::fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .map(|e| e.file_name().to_string_lossy().into_owned()) + .filter(|n| is_versioned_php_name(n)) + .collect(); + names.sort(); + found.extend(names.into_iter().map(|n| dir.join(n)).filter(|p| p.is_file())); + } + found +} + +/// `php` CLIs: every one on `PATH` (also versioned ones, `php8.1`), Homebrew's versioned +/// formulae, PHP version managers, Laravel Herd, and the usual Windows distributions (XAMPP, Laragon). fn php() -> Vec { let mut found: Vec<(PathBuf, &'static str)> = Vec::new(); let home = home_dir(); @@ -294,6 +324,11 @@ fn php() -> Vec { let source = php_source(&p, home.as_deref()); found.push((p, source)); } + // After the plain `php`: deduplication keeps `/usr/bin/php` over the `php8.3` it links to. + for p in versioned_php_in(std::env::split_paths(&search_path())) { + let source = php_source(&p, home.as_deref()); + found.push((p, source)); + } let named = |dir: &Path, prefix: &str| -> Vec { subdirs(dir) .into_iter() diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index a97835c9f..b0b2561e2 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -1322,9 +1322,28 @@ fn php_sources_and_selected_interpreter() { assert_eq!(source("/opt/homebrew/opt/php@8.2/bin/php"), "homebrew"); assert_eq!(source("/home/me/bin/php"), "path"); + use crate::discover::{is_versioned_php_name, versioned_php_in}; + for name in ["php8.1", "php81", "php8", "php8.3.exe"] { + assert!(is_versioned_php_name(name), "{name}"); + } + for name in ["php", "php-cgi8.1", "phpize8.1", "phpdbg8.1", "php-fpm8.1", "php8.", "php.8", "php8.1.2x", "phpunit"] + { + assert!(!is_versioned_php_name(name), "{name}"); + } + if cfg!(windows) { return; // Unix paths } + let bins = tempfile::tempdir().unwrap(); + for name in ["php", "php8.3", "php8.1", "php-cgi8.1", "phpize8.1", "phpdbg8.1"] { + std::fs::write(bins.path().join(name), "").unwrap(); + } + std::fs::create_dir(bins.path().join("php9.0")).unwrap(); + assert_eq!( + versioned_php_in([bins.path().to_path_buf()]), + vec![bins.path().join("php8.1"), bins.path().join("php8.3")] + ); + let tmp = tempfile::tempdir().unwrap(); let mut s = Settings::default(); assert_eq!(crate::apply::php_dir(tmp.path(), &s), None);