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/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ on:
jobs:
code-quality:
uses: wp-cli/.github/.github/workflows/reusable-code-quality.yml@main
with:
# Generated by the post-install-cmd hook (utils/prefix-dependencies.php):
# prefixed copies of third-party packages, and the php-scoper toolchain.
parallel-lint-excludes: |
third_party
utils/scoper/vendor
31 changes: 31 additions & 0 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ jobs:
name: manifest
path: vendor/wp-cli/wp-cli/manifest.json

# third_party/ is generated by the post-install-cmd hook (utils/prefix-dependencies.php).
# Without it the Phar would bundle unprefixed dependencies and impose them on the site,
# see https://github.com/wp-cli/wp-cli/issues/5920
- name: Verify the third-party dependencies were prefixed
run: |
if [ ! -f third_party/vendor/autoload.php ]; then
echo '::error::third_party/ is missing: `composer install` did not prefix the dependencies.'
exit 1
fi

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

Expand Down Expand Up @@ -118,6 +128,27 @@ jobs:
sudo apt-get update
sudo apt-get install ghostscript -y

# The `composer install` below prefixes the bundled dependencies through
# php-scoper, which needs PHP 8.2+, while this matrix goes down to 7.2.
# Install the toolchain with a recent PHP first and hand that interpreter
# to utils/prefix-dependencies.php, then switch to the matrix version.
- name: Set up PHP for the php-scoper toolchain
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: 'latest'
coverage: none
tools: composer
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Install the php-scoper toolchain
uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4
with:
working-directory: utils/scoper

- name: Remember the PHP interpreter for php-scoper
run: echo "WP_CLI_SCOPER_PHP=$(php -r 'echo PHP_BINARY;')" >> "$GITHUB_ENV"

- name: Set up PHP environment
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ PHAR_BUILD_VERSION
/cache
/packages
/vendor
/third_party
/*.phar
.*.swp
*.log
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"post-install-cmd": "@prefix-dependencies",
"post-update-cmd": "@prefix-dependencies",
"prefix-dependencies": "php utils/prefix-dependencies.php",
"behat": "run-behat-tests",
"behat-rerun": "rerun-behat-tests",
"lint": "run-linter-tests",
"lint": "run-linter-tests --exclude third_party --exclude utils/scoper/vendor",
"lint-gherkin": "run-gherkin-lint-tests",
"phpcs": "run-phpcs-tests",
"phpstan": "run-phpstan-tests",
Expand All @@ -93,6 +96,9 @@
"@behat"
]
},
"scripts-descriptions": {
"prefix-dependencies": "Prefix the namespaces of the Phar's third-party dependencies into third_party/ (needs PHP 8.2+; runs on install and update)."
},
"support": {
"issues": "https://github.com/wp-cli/wp-cli-bundle/issues",
"source": "https://github.com/wp-cli/wp-cli-bundle",
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. The Phar therefore ships Composer's dependency tree
# under the `WP_CLI\Vendor` prefix instead, see utils/prefix-dependencies.php.
#
# Prefixing happens on `composer install` and needs PHP 8.2+, which is why
# these scenarios build their own Phar from the checkout and only run where
# that tree exists. 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

@require-php-8.2
Scenario: A site providing its own psr/log is not broken by the bundled one
Given an empty directory
And a new Phar with the same version
And 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 run `php {PHAR_PATH} option get siteurl`
Then STDOUT should not be empty
And STDERR should be empty

@require-php-8.2
Scenario: A site providing its own Symfony Console is not broken by the bundled one
Given an empty directory
And a new Phar with the same version
And 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 run `php {PHAR_PATH} option get siteurl`
Then STDOUT should not be empty
And STDERR should be empty

@require-php-8.2
Scenario: Package management still works against the prefixed tree
# Composer itself keeps its namespace and resolves plenty of classes from
# strings at runtime, which the prefixing of static `use` statements does
# not cover.
Given an empty directory
And a new Phar with the same version

When I run `php {PHAR_PATH} package list`
Then STDERR should be empty
And the return code should be 0
14 changes: 14 additions & 0 deletions features/make-phar.feature
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,17 @@ Feature: Check `utils/make-phar.php` output
Error: Couldn't find plugin-status.mustache
"""
And the return code should be 0

# Prefixing runs on `composer install` and needs PHP 8.2+, so the tree only
# exists on such a checkout. See utils/prefix-dependencies.php.
@require-php-8.2
Scenario: Third-party dependencies are bundled under the WP_CLI\Vendor prefix
Given an empty directory
And a new Phar with the same version

When I run `php {PHAR_PATH} eval --skip-wordpress 'echo json_encode( [ interface_exists( "Psr\\Log\\LoggerInterface" ), class_exists( "Symfony\\Component\\Console\\Application" ), interface_exists( "WP_CLI\\Vendor\\Psr\\Log\\LoggerInterface" ), class_exists( "WP_CLI\\Vendor\\Symfony\\Component\\Console\\Application" ), class_exists( "Composer\\Semver\\Comparator" ) ] );'`
Then STDOUT should be:
"""
[false,false,true,true,true]
"""
And STDERR should be empty
8 changes: 8 additions & 0 deletions php/boot-phar.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
// Store the path to the Phar early on for `Utils\phar-safe-path()` function.
define( 'WP_CLI_PHAR_PATH', Phar::running( true ) );

// The bundled third-party dependencies are namespace-prefixed and ship with
// their own autoloader. See utils/prefix-dependencies.php.
$wp_cli_third_party_autoload = dirname( __DIR__ ) . '/third_party/vendor/autoload.php';
if ( file_exists( $wp_cli_third_party_autoload ) ) {
require $wp_cli_third_party_autoload;
}
unset( $wp_cli_third_party_autoload );

if ( file_exists( 'phar://wp-cli.phar/php/wp-cli.php' ) ) {
define( 'WP_CLI_ROOT', 'phar://wp-cli.phar' );
include WP_CLI_ROOT . '/php/wp-cli.php';
Expand Down
11 changes: 10 additions & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
<!-- What to scan. -->
<file>.</file>

<!-- Prefixed copies of third-party packages, generated by utils/prefix-dependencies.php. -->
<exclude-pattern>*/third_party/*</exclude-pattern>

<!-- Show progress. -->
<arg value="p"/>

Expand Down Expand Up @@ -46,16 +49,22 @@
#############################################################################
-->

<!-- These are two procedural stand-alone file that is never loaded in a
<!-- These are procedural stand-alone files that are never loaded in a
WordPress context, so these files do not have to comply with WP naming
conventions. -->
<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/prefix-dependencies\.php$</exclude-pattern>
<exclude-pattern>*/utils/scoper/prefixed-packages\.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/prefix-dependencies\.php$</exclude-pattern>
<exclude-pattern>*/utils/scoper/prefixed-packages\.php$</exclude-pattern>
<exclude-pattern>*/utils/scoper/scoper\.inc\.php$</exclude-pattern>
</rule>

</ruleset>
7 changes: 7 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ parameters:
paths:
- php
- utils
excludePaths:
analyse:
# php-scoper configuration; its classes only exist in the isolated toolchain.
- utils/scoper/scoper.inc.php
analyseAndScan:
# The isolated php-scoper toolchain.
- utils/scoper/vendor
scanDirectories:
- vendor/wp-cli/wp-cli
scanFiles:
Expand Down
Loading
Loading