Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ jobs:
name: manifest
path: vendor/wp-cli/wp-cli/manifest.json

# Prefixes the composer/composer dependency tree so the Phar stops
# imposing its own psr/log, Symfony and React versions on the site it
# runs against. See https://github.com/wp-cli/wp-cli/issues/5920
- name: Prefix bundled dependencies
run: php utils/scope-dependencies.php

- name: Build the Phar file
run: php -dphar.readonly=0 utils/make-phar.php wp-cli.phar --version=$CLI_VERSION

Expand Down
98 changes: 98 additions & 0 deletions features/dependency-isolation.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
Feature: Bundled dependencies do not conflict with the site's own

# WP-CLI's autoloader is registered 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. Prefixing the `composer/composer` dependency tree stops
# the Phar from claiming those names at all.
#
# These scenarios run against the built Phar, which is the only artifact the
# prefixing applies to; a Composer-based installation resolves its own
# dependency versions and has no conflict to avoid.
#
# See https://github.com/wp-cli/wp-cli/issues/5920

Scenario: A site providing its own psr/log is not broken by the bundled one
Given a WP installation
# Stands in for a site that ships psr/log v3 through its own vendor
# directory, as anything depending on monolog/monolog does. The typed
# signatures are incompatible with the psr/log v1 that composer/composer
# resolves to under the Phar's PHP 7.2 platform requirement, so whichever
# copy of the interface loads first decides whether this fatals.
And a wp-content/mu-plugins/site-logger.php file:
"""
<?php

spl_autoload_register(
function ( $class ) {
if ( 'Psr\\Log\\LoggerInterface' !== $class ) {
return;
}

eval(
'namespace Psr\Log;
interface LoggerInterface {
public function emergency( string $message, array $context = [] ): void;
}'
);
}
);

final class Site_Logger implements \Psr\Log\LoggerInterface {
public function emergency( string $message, array $context = [] ): void {
}
}
"""

When I try `wp option get siteurl`
Then STDERR should not contain:
"""
must be compatible with
"""
And STDERR should not contain:
"""
critical error
"""
And the return code should be 0

Scenario: A site providing its own Symfony Console is not broken by the bundled one
Given a WP installation
And a wp-content/mu-plugins/site-console.php file:
"""
<?php

spl_autoload_register(
function ( $class ) {
if ( 'Symfony\\Component\\Console\\Output\\OutputInterface' !== $class ) {
return;
}

eval(
'namespace Symfony\Component\Console\Output;
interface OutputInterface {
public function writeln( string $messages, int $options = 0 ): void;
}'
);
}
);

final class Site_Output implements \Symfony\Component\Console\Output\OutputInterface {
public function writeln( string $messages, int $options = 0 ): void {
}
}
"""

When I try `wp option get siteurl`
Then STDERR should not contain:
"""
must be compatible with
"""
And the return code should be 0

Scenario: Package management still works against the prefixed tree
# Exercises the paths where Composer resolves classes dynamically from
# strings, which prefixing of static `use` statements does not cover.
Given an empty directory

When I run `wp package list`
Then STDERR should be empty
And the return code should be 0
4 changes: 4 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<exclude-pattern>*/utils/get-package-require-from-composer\.php$</exclude-pattern>
<exclude-pattern>*/utils/make-phar\.php$</exclude-pattern>
<exclude-pattern>*/utils/scope-dependencies\.php$</exclude-pattern>
<exclude-pattern>*/utils/scoper/scoper\.inc\.php$</exclude-pattern>
</rule>
<rule ref="WordPress.WP.GlobalVariablesOverride">
<exclude-pattern>*/utils/get-package-require-from-composer\.php$</exclude-pattern>
<exclude-pattern>*/utils/make-phar\.php$</exclude-pattern>
<exclude-pattern>*/utils/scope-dependencies\.php$</exclude-pattern>
<exclude-pattern>*/utils/scoper/scoper\.inc\.php$</exclude-pattern>
</rule>

</ruleset>
5 changes: 5 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ parameters:
paths:
- php
- utils
excludePaths:
analyse:
# Isolated toolchain with its own composer.json; its dependencies are not
# installed in this project's vendor directory.
- utils/scoper/scoper.inc.php
scanDirectories:
- vendor/wp-cli/wp-cli
scanFiles:
Expand Down
21 changes: 17 additions & 4 deletions utils/make-phar.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
require WP_CLI_VENDOR_DIR . '/autoload.php';
require WP_CLI_ROOT . '/php/utils.php';

use Symfony\Component\Finder\Finder;
use WP_CLI\Utils;
use WP_CLI\Configurator;

Expand Down Expand Up @@ -190,7 +189,21 @@ function get_composer_versions( $current_version ) {
$phar->startBuffering();

// PHP files
$finder = new Finder();
/*
* `utils/scope-dependencies.php` prefixes symfony/finder along with the rest
* of the Composer tree, so the class this build script itself relies on moves
* depending on whether prefixing has already run.
*/
$finder_class = class_exists( 'Symfony\\Component\\Finder\\Finder' )
? 'Symfony\\Component\\Finder\\Finder'
: 'WP_CLI\\Vendor\\Symfony\\Component\\Finder\\Finder';

if ( ! class_exists( $finder_class ) ) {
fwrite( STDERR, 'Missing Symfony Finder; run `composer install` first.' . PHP_EOL );
exit( 1 );
}

$finder = new $finder_class();
$finder
->files()
->ignoreVCS( true )
Expand Down Expand Up @@ -265,7 +278,7 @@ function get_composer_versions( $current_version ) {
}

// other files
$finder = new Finder();
$finder = new $finder_class();
$finder
->files()
->ignoreVCS( true )
Expand All @@ -280,7 +293,7 @@ function get_composer_versions( $current_version ) {
if ( 'cli' !== BUILD ) {
// Include base project files, because the autoloader will load them
if ( WP_CLI_BASE_PATH !== WP_CLI_BUNDLE_ROOT && is_dir( WP_CLI_BASE_PATH . '/src' ) ) {
$finder = new Finder();
$finder = new $finder_class();
$finder
->files()
->ignoreVCS( true )
Expand Down
Loading
Loading