diff options
Diffstat (limited to 'core')
7 files changed, 123 insertions, 143 deletions
diff --git a/core/modules/package_manager/package_manager.services.yml b/core/modules/package_manager/package_manager.services.yml index 059be554270..ead88254823 100644 --- a/core/modules/package_manager/package_manager.services.yml +++ b/core/modules/package_manager/package_manager.services.yml @@ -14,9 +14,6 @@ services: Drupal\package_manager\ExecutableFinder: public: false decorates: 'PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface' - Drupal\package_manager\ProcessFactory: - public: false - decorates: 'PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface' Drupal\package_manager\TranslatableStringFactory: public: false decorates: 'PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface' @@ -175,7 +172,7 @@ services: PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface: class: PhpTuf\ComposerStager\Internal\Process\Factory\ProcessFactory PhpTuf\ComposerStager\API\Process\Service\ComposerProcessRunnerInterface: - class: PhpTuf\ComposerStager\Internal\Process\Service\ComposerProcessRunner + class: Drupal\package_manager\ComposerRunner PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface: class: PhpTuf\ComposerStager\Internal\Process\Service\OutputCallback PhpTuf\ComposerStager\API\Process\Service\ProcessInterface: diff --git a/core/modules/package_manager/src/ComposerRunner.php b/core/modules/package_manager/src/ComposerRunner.php new file mode 100644 index 00000000000..dbc029f6aa3 --- /dev/null +++ b/core/modules/package_manager/src/ComposerRunner.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\package_manager; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\File\FileSystemInterface; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; +use PhpTuf\ComposerStager\API\Path\Value\PathInterface; +use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PhpTuf\ComposerStager\API\Process\Service\ComposerProcessRunnerInterface; +use PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface; +use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface; +use Symfony\Component\Process\PhpExecutableFinder; + +// cspell:ignore BINDIR + +/** + * Runs Composer through the current PHP interpreter. + * + * @internal + * This is an internal part of Package Manager and may be changed or removed + * at any time without warning. External code should not interact with this + * class. + */ +final class ComposerRunner implements ComposerProcessRunnerInterface { + + public function __construct( + private readonly ExecutableFinderInterface $executableFinder, + private readonly ProcessFactoryInterface $processFactory, + private readonly FileSystemInterface $fileSystem, + private readonly ConfigFactoryInterface $configFactory, + ) {} + + /** + * {@inheritdoc} + */ + public function run(array $command, ?PathInterface $cwd = NULL, array $env = [], ?OutputCallbackInterface $callback = NULL, int $timeout = ProcessInterface::DEFAULT_TIMEOUT): void { + // Run Composer through the PHP interpreter so we don't have to rely on + // PHP being in the PATH. + array_unshift($command, (new PhpExecutableFinder())->find(), $this->executableFinder->find('composer')); + + $home = $this->fileSystem->getTempDirectory(); + $home .= '/package_manager_composer_home-'; + $home .= $this->configFactory->get('system.site')->get('uuid'); + $this->fileSystem->prepareDirectory($home, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); + + $process = $this->processFactory->create($command, $cwd, $env + ['COMPOSER_HOME' => $home]); + $process->setTimeout($timeout); + $process->mustRun($callback); + } + +} diff --git a/core/modules/package_manager/src/ProcessFactory.php b/core/modules/package_manager/src/ProcessFactory.php deleted file mode 100644 index 7e92977e7ff..00000000000 --- a/core/modules/package_manager/src/ProcessFactory.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\package_manager; - -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\File\FileSystemInterface; -use PhpTuf\ComposerStager\API\Path\Value\PathInterface; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; -use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface; - -// cspell:ignore BINDIR - -/** - * Defines a process factory which sets the COMPOSER_HOME environment variable. - * - * @internal - * This is an internal part of Package Manager and may be changed or removed - * at any time without warning. External code should not interact with this - * class. - */ -final class ProcessFactory implements ProcessFactoryInterface { - - public function __construct( - private readonly FileSystemInterface $fileSystem, - private readonly ConfigFactoryInterface $configFactory, - private readonly ProcessFactoryInterface $decorated, - ) {} - - /** - * {@inheritdoc} - */ - public function create(array $command, ?PathInterface $cwd = NULL, array $env = []): ProcessInterface { - $process = $this->decorated->create($command, $cwd, $env); - - $env = $process->getEnv(); - if ($command && $this->isComposerCommand($command)) { - $env['COMPOSER_HOME'] = $this->getComposerHomePath(); - } - // Ensure that the current PHP installation is the first place that will be - // searched when looking for the PHP interpreter. - $env['PATH'] = static::getPhpDirectory() . ':' . getenv('PATH'); - $process->setEnv($env); - return $process; - } - - /** - * Returns the directory which contains the PHP interpreter. - * - * @return string - * The path of the directory containing the PHP interpreter. If the server - * is running in a command-line interface, the directory portion of - * PHP_BINARY is returned; otherwise, the compile-time PHP_BINDIR is. - * - * @see php_sapi_name() - * @see https://www.php.net/manual/en/reserved.constants.php - */ - private static function getPhpDirectory(): string { - if (PHP_SAPI === 'cli' || PHP_SAPI === 'cli-server') { - return dirname(PHP_BINARY); - } - return PHP_BINDIR; - } - - /** - * Returns the path to use as the COMPOSER_HOME environment variable. - * - * @return string - * The path which should be used as COMPOSER_HOME. - */ - private function getComposerHomePath(): string { - $home_path = $this->fileSystem->getTempDirectory(); - $home_path .= '/package_manager_composer_home-'; - $home_path .= $this->configFactory->get('system.site')->get('uuid'); - $this->fileSystem->prepareDirectory($home_path, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); - - return $home_path; - } - - /** - * Determines if a command is running Composer. - * - * @param string[] $command - * The command parts. - * - * @return bool - * TRUE if the command is running Composer, FALSE otherwise. - */ - private function isComposerCommand(array $command): bool { - $executable = $command[0]; - $executable_parts = explode('/', $executable); - $file = array_pop($executable_parts); - return str_starts_with($file, 'composer'); - } - -} diff --git a/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php b/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php index 45046a8e2ad..15a7a2aab6b 100644 --- a/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php +++ b/core/modules/package_manager/tests/src/Functional/ComposerRequirementTest.php @@ -47,7 +47,10 @@ class ComposerRequirementTest extends PackageManagerTestBase { $config->set('executables.composer', '/path/to/composer')->save(); $this->getSession()->reload(); $assert_session->statusCodeEquals(200); - $assert_session->pageTextContains('Composer was not found. The error message was: Failed to run process: The command "\'/path/to/composer\' \'--format=json\'" failed.'); + $assert_session->pageTextContains('Composer was not found. The error message was: '); + // Check for the part of the command string that is constant (the path to + // the PHP interpreter will vary). + $assert_session->pageTextContains("/php' '/path/to/composer' '--format=json'\" failed."); } } diff --git a/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php b/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php deleted file mode 100644 index db27b06efa8..00000000000 --- a/core/modules/package_manager/tests/src/Kernel/ProcessFactoryTest.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\package_manager\Kernel; - -use Drupal\package_manager\ProcessFactory; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; - -/** - * @coversDefaultClass \Drupal\package_manager\ProcessFactory - * @group auto_updates - * @internal - */ -class ProcessFactoryTest extends PackageManagerKernelTestBase { - - /** - * Tests that the process factory prepends the PHP directory to PATH. - */ - public function testPhpDirectoryPrependedToPath(): void { - $factory = $this->container->get(ProcessFactoryInterface::class); - $this->assertInstanceOf(ProcessFactory::class, $factory); - - // Ensure that the directory of the PHP interpreter can be found. - $reflector = new \ReflectionObject($factory); - $method = $reflector->getMethod('getPhpDirectory'); - $php_dir = $method->invoke(NULL); - $this->assertNotEmpty($php_dir); - - // The process factory should always put the PHP interpreter's directory - // at the beginning of the PATH environment variable. - $env = $factory->create(['whoami'])->getEnv(); - $this->assertStringStartsWith("$php_dir:", $env['PATH']); - } - -} diff --git a/core/modules/package_manager/tests/src/Kernel/ServicesTest.php b/core/modules/package_manager/tests/src/Kernel/ServicesTest.php index a1db5a517c1..9e52f9b4075 100644 --- a/core/modules/package_manager/tests/src/Kernel/ServicesTest.php +++ b/core/modules/package_manager/tests/src/Kernel/ServicesTest.php @@ -9,14 +9,12 @@ use Drupal\package_manager\ExecutableFinder; use Drupal\package_manager\LoggingBeginner; use Drupal\package_manager\LoggingCommitter; use Drupal\package_manager\LoggingStager; -use Drupal\package_manager\ProcessFactory; use Drupal\package_manager\TranslatableStringFactory; use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait; use PhpTuf\ComposerStager\API\Core\BeginnerInterface; use PhpTuf\ComposerStager\API\Core\CommitterInterface; use PhpTuf\ComposerStager\API\Core\StagerInterface; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; -use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface; /** @@ -38,11 +36,9 @@ class ServicesTest extends KernelTestBase { * Tests that Package Manager's public services can be instantiated. */ public function testPackageManagerServices(): void { - // Ensure that any overridden Composer Stager services were overridden - // correctly. + // Ensure that certain Composer Stager services are decorated correctly. $overrides = [ ExecutableFinderInterface::class => ExecutableFinder::class, - ProcessFactoryInterface::class => ProcessFactory::class, TranslatableFactoryInterface::class => TranslatableStringFactory::class, BeginnerInterface::class => LoggingBeginner::class, StagerInterface::class => LoggingStager::class, diff --git a/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php b/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php new file mode 100644 index 00000000000..91253d377d2 --- /dev/null +++ b/core/modules/package_manager/tests/src/Unit/ComposerRunnerTest.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\package_manager\Unit; + +use Drupal\Core\File\FileSystemInterface; +use Drupal\package_manager\ComposerRunner; +use Drupal\Tests\UnitTestCase; +use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; +use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use Prophecy\Argument; + +// cspell:ignore BINDIR + +/** + * Tests Package Manager's Composer runner service. + * + * @internal + */ +#[CoversClass(ComposerRunner::class)] +#[Group('package_manager')] +class ComposerRunnerTest extends UnitTestCase { + + /** + * Tests that the Composer runner runs Composer through the PHP interpreter. + */ + public function testRunner(): void { + $executable_finder = $this->prophesize(ExecutableFinderInterface::class); + $executable_finder->find('composer') + ->willReturn('/mock/composer') + ->shouldBeCalled(); + + $process_factory = $this->prophesize(ProcessFactoryInterface::class); + $process_factory->create( + // Internally, ComposerRunner uses Symfony's PhpExecutableFinder to locate + // the PHP interpreter, which should resolve to PHP_BINARY a command-line + // test environment. + [PHP_BINARY, '/mock/composer', '--version'], + NULL, + Argument::withKey('COMPOSER_HOME'), + )->shouldBeCalled(); + + $file_system = $this->prophesize(FileSystemInterface::class); + $file_system->getTempDirectory()->shouldBeCalled(); + $file_system->prepareDirectory(Argument::cetera())->shouldBeCalled(); + + $runner = new ComposerRunner( + $executable_finder->reveal(), + $process_factory->reveal(), + $file_system->reveal(), + $this->getConfigFactoryStub([ + 'system.site' => [ + 'uuid' => 'testing', + ], + ]), + ); + $runner->run(['--version']); + } + +} |