Skip to content

Composer global-home fallback checks ~/.composer before the XDG home, so scan -g finds no Composer 2 global packages when a stale ~/.composer exists and composer isn't on PATH #586

Description

[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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions