Commit d72c5836 authored by catch's avatar catch
Browse files

Issue #3135247 by greg.1.anderson, alexpott, ridhimaabrol24, Mixologic,...

Issue #3135247 by greg.1.anderson, alexpott, ridhimaabrol24, Mixologic, tedbow, xjm, catch, jwilson3, longwave: Composer's "prefer-stable" setting cannot be relied on to produce a stable release
parent 7bb639fd
Loading
Loading
Loading
Loading
+76 −3
Original line number Diff line number Diff line
@@ -5,7 +5,9 @@
use Composer\Composer as ComposerApp;
use Composer\Script\Event;
use Composer\Semver\Comparator;
use Composer\Semver\VersionParser;
use Drupal\Composer\Generator\PackageGenerator;
use Symfony\Component\Finder\Finder;

/**
 * Provides static functions for composer script events. See also
@@ -21,17 +23,71 @@ class Composer {
   * Update metapackages whenever composer.lock is updated.
   *
   * @param \Composer\Script\Event $event
   *   The Composer event.
   */
  public static function generateMetapackages(Event $event) {
  public static function generateMetapackages(Event $event): void {
    $generator = new PackageGenerator();
    $generator->generate($event->getIO(), getcwd());
  }

  /**
   * Set the version of Drupal; used in release process and by the test suite.
   *
   * @param string $root
   *   Path to root of drupal/drupal repository.
   * @param string $version
   *   Semver version to set Drupal's version to.
   *
   * @return string
   *   Stability level of the provided version (stable, RC, alpha, etc.)
   *
   * @throws \UnexpectedValueException
   */
  public static function setDrupalVersion(string $root, string $version): void {
    // We use VersionParser::normalize to validate that $version is valid.
    // It will throw an exception if it is not.
    $versionParser = new VersionParser();
    $versionParser->normalize($version);

    // Rewrite Drupal.php with the provided version string.
    $drupal_static_path = "$root/core/lib/Drupal.php";
    $drupal_static_source = file_get_contents($drupal_static_path);
    $drupal_static_source = preg_replace('#const VERSION = [^;]*#', "const VERSION = '$version'", $drupal_static_source);
    file_put_contents($drupal_static_path, $drupal_static_source);

    // Update the template project stability to match the version we set.
    static::setTemplateProjectStability($root, $version);
  }

  /**
   * Set the stability of the template projects to match the Drupal version.
   *
   * @param string $root
   *   Path to root of drupal/drupal repository.
   * @param string $version
   *   Semver version that Drupal was set to.
   *
   * @return string
   *   Stability level of the provided version (stable, RC, alpha, etc.)
   */
  protected static function setTemplateProjectStability(string $root, string $version): void {
    $stability = VersionParser::parseStability($version);

    $templateProjectPaths = static::composerSubprojectPaths($root, 'Template');
    foreach ($templateProjectPaths as $path) {
      $dir = dirname($path);
      exec("composer --working-dir=$dir config minimum-stability $stability", $output, $status);
      if ($status) {
        throw new \Exception('Could not set minimum-stability for template project ' . basename($dir));
      }
    }
  }

  /**
   * Ensure that the minimum required version of Composer is running.
   * Throw an exception if Composer is too old.
   */
  public static function ensureComposerVersion() {
  public static function ensureComposerVersion(): void {
    $composerVersion = method_exists(ComposerApp::class, 'getVersion') ?
      ComposerApp::getVersion() : ComposerApp::VERSION;
    if (Comparator::lessThan($composerVersion, '1.9.0')) {
@@ -45,8 +101,25 @@ public static function ensureComposerVersion() {
   * @return string
   *   A branch name, e.g. 8.9.x or 9.0.x.
   */
  public static function drupalVersionBranch() {
  public static function drupalVersionBranch(): string {
    return preg_replace('#\.[0-9]+-dev#', '.x-dev', \Drupal::VERSION);
  }

  /**
   * Return the list of subprojects of a given type.
   *
   * @param string $root
   *   Path to root of drupal/drupal repository.
   * @param string $subprojectType
   *   Type of subproject - one of Metapackage, Plugin, or Template
   *
   * @return \Symfony\Component\Finder\Finder
   */
  public static function composerSubprojectPaths(string $root, string $subprojectType): Finder {
    return Finder::create()
      ->files()
      ->name('composer.json')
      ->in("$root/composer/$subprojectType");
  }

}
+1 −1
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ How do I set it up?

Use Composer to create a new project using the desired starter template:

    composer -n create-project -s dev drupal/recommended-project my-project
    composer -n create-project drupal/recommended-project my-project

Add new modules and themes with `composer require`:

+1 −0
Original line number Diff line number Diff line
@@ -1628,6 +1628,7 @@ svgz
svibanj
swcf
symfony's
symlinked
symlinking
symlinks
synchronizable
+62 −17
Original line number Diff line number Diff line
@@ -5,7 +5,6 @@
use Composer\Json\JsonFile;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Composer\Composer;
use Symfony\Component\Finder\Finder;

/**
 * Demonstrate that Composer project templates are buildable as patched.
@@ -40,10 +39,7 @@ class ComposerProjectTemplatesTest extends BuildTestBase {
   */
  public function getPathReposForType($workspace_directory, $subdir) {
    // Find the Composer items that we want to be path repos.
    $path_repos = Finder::create()
      ->files()
      ->name('composer.json')
      ->in($workspace_directory . '/composer/' . $subdir);
    $path_repos = Composer::composerSubprojectPaths($workspace_directory, $subdir);

    $data = [];
    /* @var $path_repo \SplFileInfo */
@@ -78,10 +74,7 @@ public function testVerifyTemplateTestProviderIsAccurate() {
    $data = $this->provideTemplateCreateProject($root);

    // Find all the templates.
    $template_files = Finder::create()
      ->files()
      ->name('composer.json')
      ->in($root . '/composer/Template');
    $template_files = Composer::composerSubprojectPaths($root, 'Template');

    $this->assertSame(count($template_files), count($data));

@@ -114,6 +107,8 @@ public function testTemplateCreateProject($project, $package_dir, $docroot_dir)
    // Make a working COMPOSER_HOME directory for setting global composer config
    $composer_home = $this->getWorkspaceDirectory() . '/composer-home';
    mkdir($composer_home);
    // Create an empty global composer.json file, just to avoid warnings.
    file_put_contents("$composer_home/composer.json", '{}');

    // Disable packagist globally (but only in our own custom COMPOSER_HOME).
    // It is necessary to do this globally rather than in our SUT composer.json
@@ -126,14 +121,25 @@ public function testTemplateCreateProject($project, $package_dir, $docroot_dir)
    // 8.9.x-dev for the 8.9.x branch.
    $core_version = Composer::drupalVersionBranch();

    // In order to use create-project on our template, we must have stable
    // versions of drupal/core and our other SUT repositories. Since we have
    // provided these as path repositories, they will take on the version of
    // the root project. We'll make a simulated version number that is stable
    // to fulfill this role.
    $simulated_stable_version = str_replace('.x-dev', '.99', $core_version);

    // Create a "Composer"-type repository containing one entry for every
    // package in the vendor directory.
    $vendor_packages_path = $this->getWorkspaceDirectory() . '/vendor_packages/packages.json';
    $this->makeVendorPackage($vendor_packages_path);

    // Make a copy of the code to alter.
    // Make a copy of the code to alter in the workspace directory.
    $this->copyCodebase();

    // Set the Drupal version and minimum stability of the template projects
    Composer::setDrupalVersion($this->getWorkspaceDirectory(), $simulated_stable_version);
    $this->assertDrupalVersion($simulated_stable_version, $this->getWorkspaceDirectory());

    // Remove the packages.drupal.org entry (and any other custom repository)
    // from the SUT's repositories section. There is no way to do this via
    // `composer config --unset`, so we read and rewrite composer.json.
@@ -146,6 +152,7 @@ public function testTemplateCreateProject($project, $package_dir, $docroot_dir)
    // Set up the template to use our path repos. Inclusion of metapackages is
    // reported differently, so we load up a separate set for them.
    $metapackage_path_repos = $this->getPathReposForType($this->getWorkspaceDirectory(), 'Metapackage');
    $this->assertArrayHasKey('drupal/core-recommended', $metapackage_path_repos);
    $path_repos = array_merge($metapackage_path_repos, $this->getPathReposForType($this->getWorkspaceDirectory(), 'Plugin'));
    // Always add drupal/core as a path repo.
    $path_repos['drupal/core'] = $this->getWorkspaceDirectory() . '/core';
@@ -153,28 +160,47 @@ public function testTemplateCreateProject($project, $package_dir, $docroot_dir)
      $this->executeCommand("composer config --no-interaction repositories.$name path $path", $package_dir);
      $this->assertCommandSuccessful();
    }
    // Fix up drupal/core-recommended so that it requires a stable version
    // of drupal/core rather than a dev version.
    $core_recommended_dir = 'composer/Metapackage/CoreRecommended';
    $this->executeCommand("composer remove --no-interaction drupal/core --no-update", $core_recommended_dir);
    $this->assertCommandSuccessful();
    $this->executeCommand("composer require --no-interaction drupal/core:^$simulated_stable_version --no-update", $core_recommended_dir);
    $this->assertCommandSuccessful();

    // Add our vendor package repository to our SUT's repositories section.
    // Call it "local" (although the name does not matter).
    $this->executeCommand("composer config --no-interaction repositories.local composer file://" . $vendor_packages_path, $package_dir);
    $this->assertCommandSuccessful();

    $repository_path = $this->getWorkspaceDirectory() . '/test_repository/packages.json';
    $this->makeTestPackage($repository_path, $core_version);
    $this->makeTestPackage($repository_path, $simulated_stable_version);

    $installed_composer_json = $this->getWorkspaceDirectory() . '/testproject/composer.json';
    $autoloader = $this->getWorkspaceDirectory() . '/testproject' . $docroot_dir . '/autoload.php';
    $this->assertFileNotExists($autoloader);

    $this->executeCommand("COMPOSER_HOME=$composer_home COMPOSER_ROOT_VERSION=$core_version composer create-project --no-ansi $project testproject $core_version -s dev -vv --repository $repository_path");
    // At the moment, we are only testing stable versions. If we used a
    // non-stable version instead of $simulated_stable_version, then we would
    // also need to pass the --stability flag to composer create-project.
    $this->executeCommand("COMPOSER_HOME=$composer_home COMPOSER_ROOT_VERSION=$simulated_stable_version composer create-project --no-ansi $project testproject -vvv --repository $repository_path");
    $this->assertCommandSuccessful();

    // Ensure we used the project from our codebase.
    $this->assertErrorOutputContains("Installing $project ($core_version): Symlinking from $package_dir");
    $this->assertErrorOutputContains("Installing $project ($simulated_stable_version): Symlinking from $package_dir");
    // Ensure that we used drupal/core from our codebase. This probably means
    // that drupal/core-recommended was added successfully by the project.
    $this->assertErrorOutputContains("Installing drupal/core ($core_version): Symlinking from");
    $this->assertErrorOutputContains("Installing drupal/core ($simulated_stable_version): Symlinking from");
    // Verify that there is an autoloader. This is written by the scaffold
    // plugin, so its existence assures us that scaffolding happened.
    $this->assertFileExists($autoloader);

    // Verify that the minimum stability in the installed composer.json file
    // is 'stable'
    $this->assertFileExists($installed_composer_json);
    $composer_json_contents = file_get_contents($installed_composer_json);
    $this->assertStringContainsString('"minimum-stability": "stable"', $composer_json_contents);

    // In order to verify that Composer used the path repos for our project, we
    // have to get the requirements from the project composer.json so we can
    // reconcile our expectations.
@@ -195,13 +221,29 @@ public function testTemplateCreateProject($project, $package_dir, $docroot_dir)
        // we still must check that their installed version matches
        // COMPOSER_CORE_VERSION.
        if (array_key_exists($package_name, $metapackage_path_repos)) {
          $this->assertErrorOutputContains("Installing $package_name ($core_version)");
          $this->assertErrorOutputContains("Installing $package_name ($simulated_stable_version)");
        }
        else {
          $this->assertErrorOutputContains("Installing $package_name ($core_version): Symlinking from");
          $this->assertErrorOutputContains("Installing $package_name ($simulated_stable_version): Symlinking from");
        }
      }
    }
  }

  /**
   * Assert that the VERSION constant in Drupal.php is the expected value.
   *
   * @param string $expectedVersion
   * @param string $dir
   */
  protected function assertDrupalVersion($expectedVersion, $dir) {
    $drupal_php_path = $dir . '/core/lib/Drupal.php';
    $this->assertFileExists($drupal_php_path);

    // Read back the Drupal version that was set and assert it matches expectations.
    $this->executeCommand("php -r 'include \"$drupal_php_path\"; print \Drupal::VERSION;'");
    $this->assertCommandSuccessful();
    $this->assertCommandOutputContains($expectedVersion);
  }

  /**
@@ -267,7 +309,10 @@ protected function makeVendorPackage($repository_path) {
      $full_path = "$root/$path";
      // We are building a set of path repositories to projects in the vendor
      // directory, so we will skip any project that does not exist in vendor.
      if (is_dir($full_path)) {
      // Also skip the projects that are symlinked in vendor. These are in our
      // metapackage. They will be represented as path repositories in the test
      // project's composer.json.
      if (is_dir($full_path) && !is_link($full_path)) {
        $packages['packages'][$name] = [
          $version => [
            "name" => $name,