Prefix bundled dependencies to prevent conflicts with site code - #1110
Prefix bundled dependencies to prevent conflicts with site code#1110swissspidy wants to merge 10 commits into
Conversation
WP-CLI registers its autoloader before WordPress boots, so for any class shipped both by the Phar and by the site, the Phar's copy wins and is imposed on the site. A site using monolog/monolog against psr/log v3 gets the Phar's psr/log 1.1.4 instead and fatals on the incompatible LoggerInterface signature. Moving wp-cli/package-command to require-dev fixed this for Composer-based installations, but the Phar is still built with dev dependencies, so it continues to ship composer/composer and its tree unprefixed: symfony/console v5.4.47, psr/log 1.1.4, react/promise, seld/*. Prefix that tree with php-scoper, with two constraints: * The `Composer\` namespace itself is left alone. Third-party Composer plugins are compiled against the real `Composer\Plugin\PluginInterface`, so prefixing it would break `wp package install` for any package shipping one. References from inside `Composer\` to the prefixed vendors are still rewritten, so Composer keeps using its own psr/log. * Nothing reachable from WP-CLI's public API is touched: php-cli-tools (`Utils\make_progress_bar()`), Requests (`Utils\http_request()`, plus RequestsLibrary deliberately sharing the library with Core), and every wp-cli/* package. php-scoper needs PHP 8.2 while WP-CLI still targets 7.2.24, so the toolchain lives in utils/scoper with its own composer.json. Two things this turned up that are easy to get wrong: * php-scoper rewrites source files but not Composer's generated autoload maps. A scoped tree with a stale autoloader still advertises `Psr\Log\` and the conflict survives with nothing to show for it, so the autoloader is regenerated and then asserted on. * Excluding a namespace does not stop php-scoper prefixing string literals naming classes inside it. Composer compares `$class` against 'Composer\Package\CompletePackage', which the prefix silently breaks; a patcher restores those. Refs wp-cli/wp-cli#5920 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
`utils/scope-dependencies.php` prefixes symfony/finder along with the rest of the Composer tree, but `utils/make-phar.php` builds the Phar with that same Finder. Once prefixing has run, `Symfony\Component\Finder\Finder` no longer exists and the build dies before writing anything. Resolve the class name at runtime instead, so the build works whether or not the dependencies have been prefixed yet. Also drop `@require-mysql` from the isolation scenarios: they only need a WordPress installation, which the Behat suite can provide on SQLite too. Refs wp-cli/wp-cli#5920 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The static analysis config lints everything under `utils`, so the new build scripts need the same treatment the existing ones already get. * Exempt them from the two WordPress-context sniffs `make-phar.php` is already exempt from; they are procedural stand-alone scripts that never run inside WordPress. * Exclude `utils/scoper/scoper.inc.php` from PHPStan. It is an isolated toolchain with its own composer.json, so the classes it references are not installed in this project's vendor directory. * Swap `str_contains()` for `strpos()`. The script refuses to run below PHP 8.2, but phpcs checks this repository against a 7.2 baseline and flags the newer function. Refs wp-cli/wp-cli#5920 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
The mu-plugins used a `\Stringable|string` union type, which is a parse error on the PHP 7.2 to 7.4 jobs in the matrix, so the scenarios failed with "syntax error, unexpected '|'" rather than exercising anything. Narrowing an untyped parameter to `string` is the same contravariance violation and parses on every version the suite runs, so the scenarios still distinguish a prefixed tree from an unprefixed one: declaring the class against an unprefixed `psr/log` v1 remains a fatal error, and succeeds once the bundled copy is prefixed. Also drop `--format=count` from `wp package list`, which does not offer that format and made the step fail on argument parsing. Refs wp-cli/wp-cli#5920 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
Three separate code quality failures on the new build scripts: * PHPStan runs at level 9, where every offset read off `json_decode()` output is `mixed`. Narrow the decoded `installed.json` explicitly and build the packages list in a local variable instead of writing back through nested offsets. * Align the scoper config's array arrows on the longest key. * `marc-mabe` is a vendor name, not a misspelling of "maybe"; mark those two lines with the `spellchecker:disable-line` annotation the repo's `.typos.toml` already recognises. Re-ran the prefixing end to end against a scratch tree after the PHPStan refactor: 30 packages rewritten, autoloader regenerated, verification still passes. Refs wp-cli/wp-cli#5920 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
Refs wp-cli/wp-cli#5920 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
Replaces the earlier approach on this branch, which rewrote vendor/ in place from a separate build step. WP-CLI registers its autoloader before WordPress boots, so for any class shipped both by the Phar and by the site's own vendor/ the Phar's copy wins. A site depending on psr/log v3 therefore loads the Phar's psr/log v1 and fatals on the incompatible LoggerInterface signature. Composer's dependency tree (composer/composer and everything it pulls in, resolved from installed.json) is now rewritten under the WP_CLI\Vendor namespace into third_party/, which gets its own classmap autoloader that php/boot-phar.php registers. vendor/ is left untouched; utils/make-phar.php bundles third_party/ instead of the original packages and drops them from the main autoload maps, so the Phar stops advertising the unprefixed names. A Composer-based installation of the bundle is unaffected. The prefixing runs as Composer's post-install-cmd/post-update-cmd hook (`composer prefix-dependencies`) rather than as a separate build step. The php-scoper toolchain needs PHP 8.2+ while the bundle targets 7.2, so it lives in utils/scoper/composer.json with a committed lock file; on an older PHP the hook skips with a notice and a Phar built from that checkout bundles the unprefixed tree. CI that must test on older PHP installs the toolchain with a recent interpreter first and hands it over through WP_CLI_SCOPER_PHP, as the deployment test matrix now does. The build job refuses to build a Phar without third_party/. `Composer\` itself stays unprefixed so that third-party Composer plugins run by `wp package install` keep implementing the real interfaces, and the Symfony polyfills are copied verbatim since they only work under their global names. The hook verifies that the generated classmap advertises nothing else under an original name, and that no bundled WP-CLI package depends on a prefixed namespace. See wp-cli/wp-cli#5920 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrV8jGVsfiypo6buRmZfug
|
All the failing checks on Every job that installs dependencies dies with it, which is why the Behat cells fail after ~30s instead of running for 5–9 minutes, and why Candidate fixes, cheapest first:
Worth deciding separately from the parse bug: whether the hook should hard-fail. As wired, any scoper problem takes On the positive side, PHPCS, PHPStan and the spell check are all green on this head. Generated by Claude Code |
The toolchain lock file had been resolved against PHP 8.4, so it pinned fidry/filesystem 1.3 and symfony/string 8.1, which refuse to install on the PHP 8.2 and 8.3 legs of the test matrix. Lock against a PHP 8.2 platform. On PHP 8.5, php-scoper 0.18.18 died while parsing the first file: its own UseStmtCollection used null as an array key, and its error handler turned the resulting deprecation into an exception. Bump to 0.18.19, which fixes both, and keep deprecations out of the php-scoper run so that a future PHP cannot fail the build the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrV8jGVsfiypo6buRmZfug
|
The
The fix is to stop excluding that class from the Phar — either the whole Generated by Claude Code |
`wp package install` fataled with "Class Composer\Console\GithubActionError not found" whenever dependency resolution failed: Composer\Installer instantiates that class on every failure path. The scoper configuration had carried over make-phar.php's exclusions of Composer's Command, Console and Pear code, but those exclusions never actually applied on main: Finder's exclude() takes paths relative to the in() roots, and the ones listed are relative to vendor/ instead, so main's Phar has always shipped those files. Drop the exclusions so the prefixed tree matches what the Phar really contained before. The reusable code-quality workflow runs parallel-lint over the whole checkout with --show-deprecated, which now walks the php-scoper toolchain (whose IDE stubs redeclare PHP's own functions) and third_party/. Pass both through its parallel-lint-excludes input. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrV8jGVsfiypo6buRmZfug
Summary
This change implements namespace prefixing for WP-CLI's bundled third-party dependencies to prevent conflicts when the Phar runs against WordPress sites that ship their own versions of those libraries. Since WP-CLI's autoloader is registered before WordPress boots, unprefixed classes from the Phar take precedence over the site's own dependencies, causing fatal errors when interface signatures differ (e.g., psr/log v1 vs v3).
Key Changes
New
utils/scope-dependencies.phpscript: Orchestrates the prefixing workflow using php-scoper. The script:WP_CLI\Vendornamespacecomposer installandutils/make-phar.phpNew
utils/scoper/scoper.inc.phpconfiguration: Defines php-scoper behavior:Composer\namespace unprefixed so third-party Composer plugins continue workingComposer\classes to avoid broken dynamic class resolutionNew
utils/scoper/composer.json: Isolated toolchain configuration requiring php-scoper ^0.18, kept separate because php-scoper needs PHP 8.2+ while WP-CLI targets PHP 7.2.24Updated
utils/make-phar.php: Adapts to work with both prefixed and unprefixed Symfony Finder class depending on whetherscope-dependencies.phphas already runNew
features/dependency-isolation.feature: Behat scenarios validating that:Updated
.github/workflows/deployment.yml: Adds prefixing step to the Phar build pipelineNotable Implementation Details
--classmap-authoritativeflag ensures the ClassLoader consults only the classmap, preventing any leftover PSR-4 rules from resurrecting unprefixed nameshttps://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi