Commit 93d9fe46 authored by catch's avatar catch
Browse files

Issue #3501582 by phenaproxima, juxelle: Package Manager should always run...

Issue #3501582 by phenaproxima, juxelle: Package Manager should always run Composer through the PHP interpreter, rather than directly

(cherry picked from commit 9d045a84)
parent 09b2afc5
Loading
Loading
Loading
Loading
Loading
+1 −4
Original line number Diff line number Diff line
@@ -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:
+54 −0
Original line number Diff line number Diff line
<?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);
  }

}
+0 −97
Original line number Diff line number Diff line
<?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');
  }

}
+4 −1
Original line number Diff line number Diff line
@@ -47,7 +47,10 @@ public function testComposerInfoShown(): void {
    $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.");
  }

}
+0 −36
Original line number Diff line number Diff line
<?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']);
  }

}
Loading