[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
When composer global config home can't be run (composer isn't on PATH, for example when it's installed as composer.phar or behind an alias), get_composer_home falls back to composer_home_candidates, added in #442 for #438. That function tries ~/.composer before $XDG_CONFIG_HOME/composer / ~/.config/composer and returns the first directory that exists. Composer 2 uses the opposite order. On an XDG system (any XDG_* env var set, or /etc/xdg present, i.e. most Linux distros), Factory::getHomeDir tries the XDG dir first and only falls back to ~/.composer. Composer 1 does use ~/.composer first.
So on Linux with Composer 2, any leftover ~/.composer directory (even an empty one, e.g. from the Composer 1 era) hides the real global home. scan -g prints "No global packages found." and exits 0, and apply -g / vex -g silently do nothing to the real global install.
The doc comment at composer_home_candidates ("elsewhere: ~/.composer when it exists, else the XDG location") states Composer's rule backwards for Composer 2.
Impact
Globally installed Composer tools (phpunit, php-cs-fixer, laravel/installer, …) go silently unpatched, with a success exit code, on Linux machines that have both directories and no composer on PATH. Same symptom class as #438.
Repro (Linux, PHP 8.3, Composer 2.10.3 phar, main 203e092)
B=$(mktemp -d); mkdir -p $B/home/.composer $B/home/.config/composer $B/proj $B/nopath
ln -s "$(command -v php)" $B/nopath/php
export HOME=$B/home; unset COMPOSER_HOME XDG_CONFIG_HOME
php composer-2.10.3.phar global config home # -> $B/home/.config/composer
php composer-2.10.3.phar global require psr/log:1.1.4 -n
ls $HOME/.config/composer/vendor/composer/installed.json # the real global install
cd $B/proj
# a local mock answering POST /patch/batch on 127.0.0.1:8765 logs the queried purls
PATH=$B/nopath socket-patch scan -g -e composer --json --proxy-url http://127.0.0.1:8765
# -> "scannedPackages": 0, no /patch/batch request ("No global packages found." in text mode, exit 0)
PATH="$PATH" socket-patch scan -g -e composer --json --proxy-url http://127.0.0.1:8765 # composer on PATH (control)
# -> "scannedPackages": 1, batch queries pkg:composer/psr/log@1.1.4
rmdir $HOME/.composer; PATH=$B/nopath socket-patch scan -g -e composer --json ... # control
# -> "scannedPackages": 1
The same happens with XDG_CONFIG_HOME=$B/xdg and the global install in $B/xdg/composer.
Expected vs actual
Matrix (Linux, PHP 8.3; each scan run twice, same result)
| Composer |
Composer's own home (both dirs exist) |
socket-patch, no composer on PATH |
control: on PATH |
control: no ~/.composer |
| 1.10.28 |
~/.composer (both cases) |
matches Composer (not affected) |
n/a |
n/a |
| 2.2.30 |
~/.config/composer / $XDG_CONFIG_HOME/composer |
0 packages ×2 layouts |
n/a |
n/a |
| 2.10.3 |
~/.config/composer / $XDG_CONFIG_HOME/composer |
0 packages ×2 layouts |
1 package |
1 package |
macOS: untested. Composer's useXdg() is normally false there (no XDG_* vars, no /etc/xdg), so ~/.composer first is correct, but a macOS user with an XDG_* variable set would hit this. Windows isn't affected (%APPDATA%\Composer comes first).
First bad version
This isn't a regression in #442. The pre-#442 fallback (v4.0.0) already probed ~/.composer before ~/.config/composer. #442 added $XDG_CONFIG_HOME but kept the order.
Suspect code
crates/socket-patch-core/src/crawlers/composer_crawler.rs:407-434 (composer_home_candidates: home.join(".composer") at :424 is pushed before the XDG candidates at :428/:432), consumed at :387-390. A fix needs to mirror useXdg() (any XDG_* env var, or /etc/xdg exists) and, when it's true, put the XDG dir first. Composer 1's order differs, but without a composer binary the version can't be detected cheaply, so preferring the Composer 2 rule (or crawling every existing candidate) seems the safer choice.
[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
When
composer global config homecan't be run (composerisn't on PATH, for example when it's installed ascomposer.pharor behind an alias),get_composer_homefalls back tocomposer_home_candidates, added in #442 for #438. That function tries~/.composerbefore$XDG_CONFIG_HOME/composer/~/.config/composerand returns the first directory that exists. Composer 2 uses the opposite order. On an XDG system (anyXDG_*env var set, or/etc/xdgpresent, i.e. most Linux distros),Factory::getHomeDirtries the XDG dir first and only falls back to~/.composer. Composer 1 does use~/.composerfirst.So on Linux with Composer 2, any leftover
~/.composerdirectory (even an empty one, e.g. from the Composer 1 era) hides the real global home.scan -gprints "No global packages found." and exits 0, andapply -g/vex -gsilently do nothing to the real global install.The doc comment at
composer_home_candidates("elsewhere:~/.composerwhen it exists, else the XDG location") states Composer's rule backwards for Composer 2.Impact
Globally installed Composer tools (phpunit, php-cs-fixer, laravel/installer, …) go silently unpatched, with a success exit code, on Linux machines that have both directories and no
composeron PATH. Same symptom class as #438.Repro (Linux, PHP 8.3, Composer 2.10.3 phar, main
203e092)The same happens with
XDG_CONFIG_HOME=$B/xdgand the global install in$B/xdg/composer.Expected vs actual
-goperates on the global install Composer itself uses (get_composer_homemirrorsFactory::getHomeDir, per its doc comment and the On Windows, scan -g finds no Composer global packages in the default %APPDATA%\Composer home, so apply -g and vex -g silently do nothing #438 fix). Composer 2:$XDG_CONFIG_HOME/composeror~/.config/composerfirst when XDG is in use, then~/.composer.~/.composerwins whenever it exists, and 0 packages are scanned with exit 0.Matrix (Linux, PHP 8.3; each scan run twice, same result)
composeron PATH~/.composer~/.composer(both cases)~/.config/composer/$XDG_CONFIG_HOME/composer~/.config/composer/$XDG_CONFIG_HOME/composermacOS: untested. Composer's
useXdg()is normally false there (noXDG_*vars, no/etc/xdg), so~/.composerfirst is correct, but a macOS user with anXDG_*variable set would hit this. Windows isn't affected (%APPDATA%\Composercomes first).First bad version
This isn't a regression in #442. The pre-#442 fallback (v4.0.0) already probed
~/.composerbefore~/.config/composer. #442 added$XDG_CONFIG_HOMEbut kept the order.Suspect code
crates/socket-patch-core/src/crawlers/composer_crawler.rs:407-434(composer_home_candidates:home.join(".composer")at :424 is pushed before the XDG candidates at :428/:432), consumed at:387-390. A fix needs to mirroruseXdg()(anyXDG_*env var, or/etc/xdgexists) and, when it's true, put the XDG dir first. Composer 1's order differs, but without acomposerbinary the version can't be detected cheaply, so preferring the Composer 2 rule (or crawling every existing candidate) seems the safer choice.