From 008b7eff0cab1af9f2eb5f2302c2a0a93d6dcb04 Mon Sep 17 00:00:00 2001
From: s_leu <s_leu@1336864.no-reply.drupal.org>
Date: Mon, 18 Oct 2021 18:20:57 +0000
Subject: [PATCH] Issue #3240971 by phenaproxima, s_leu, tedbow: Support
 install profiles/distributions

---
 package_manager/src/ComposerUtility.php       | 54 ++++++++++------
 .../tests/fixtures/distro_core/composer.json  | 12 ++++
 .../tests/fixtures/distro_core/composer.lock  | 16 +++++
 .../distro_core_recommended/composer.json     | 12 ++++
 .../distro_core_recommended/composer.lock     | 23 +++++++
 .../tests/src/Kernel/ComposerUtilityTest.php  | 63 +++++++++++++++++++
 tests/fixtures/fake-site/composer.json        |  8 ++-
 tests/fixtures/fake-site/composer.lock        | 24 ++++++-
 .../no_core_requirements/composer.lock        |  4 ++
 tests/src/Build/UpdateTestBase.php            | 10 ++-
 tests/src/Kernel/UpdaterTest.php              | 17 ++---
 11 files changed, 215 insertions(+), 28 deletions(-)
 create mode 100644 package_manager/tests/fixtures/distro_core/composer.json
 create mode 100644 package_manager/tests/fixtures/distro_core/composer.lock
 create mode 100644 package_manager/tests/fixtures/distro_core_recommended/composer.json
 create mode 100644 package_manager/tests/fixtures/distro_core_recommended/composer.lock
 create mode 100644 package_manager/tests/src/Kernel/ComposerUtilityTest.php
 create mode 100644 tests/fixtures/project_staged_validation/no_core_requirements/composer.lock

diff --git a/package_manager/src/ComposerUtility.php b/package_manager/src/ComposerUtility.php
index 121d067647..39ca395271 100644
--- a/package_manager/src/ComposerUtility.php
+++ b/package_manager/src/ComposerUtility.php
@@ -5,6 +5,7 @@ namespace Drupal\package_manager;
 use Composer\Composer;
 use Composer\Factory;
 use Composer\IO\NullIO;
+use Composer\Package\PackageInterface;
 use Drupal\Component\Serialization\Json;
 
 /**
@@ -85,21 +86,27 @@ class ComposerUtility {
   }
 
   /**
-   * Returns the names of the core packages required in composer.json.
+   * Returns the names of the core packages in the lock file.
    *
    * All packages listed in ../core_packages.json are considered core packages.
    *
    * @return string[]
    *   The names of the required core packages.
    *
-   * @throws \LogicException
-   *   If neither drupal/core or drupal/core-recommended are required.
-   *
    * @todo Make this return a keyed array of packages, not just names.
    */
   public function getCorePackageNames(): array {
-    $requirements = array_keys($this->composer->getPackage()->getRequires());
-    return array_intersect(static::getCorePackageList(), $requirements);
+    $core_packages = array_intersect(
+      array_keys($this->getLockedPackages()),
+      static::getCorePackageList()
+    );
+
+    // If drupal/core-recommended is present, it supersedes drupal/core, since
+    // drupal/core will always be one of its direct dependencies.
+    if (in_array('drupal/core-recommended', $core_packages, TRUE)) {
+      $core_packages = array_diff($core_packages, ['drupal/core']);
+    }
+    return array_values($core_packages);
   }
 
   /**
@@ -112,24 +119,35 @@ class ComposerUtility {
    *   All Drupal extension packages in the lock file, keyed by name.
    */
   public function getDrupalExtensionPackages(): array {
+    $filter = function (PackageInterface $package): bool {
+      $drupal_package_types = [
+        'drupal-module',
+        'drupal-theme',
+        'drupal-custom-module',
+        'drupal-custom-theme',
+      ];
+      return in_array($package->getType(), $drupal_package_types, TRUE);
+    };
+    return array_filter($this->getLockedPackages(), $filter);
+  }
+
+  /**
+   * Returns all packages in the lock file.
+   *
+   * @return \Composer\Package\PackageInterface[]
+   *   All packages in the lock file, keyed by name.
+   */
+  protected function getLockedPackages(): array {
     $locked_packages = $this->composer->getLocker()
       ->getLockedRepository(TRUE)
       ->getPackages();
 
-    $drupal_package_types = [
-      'drupal-module',
-      'drupal-theme',
-      'drupal-custom-module',
-      'drupal-custom-theme',
-    ];
-    $drupal_packages = [];
+    $packages = [];
     foreach ($locked_packages as $package) {
-      if (in_array($package->getType(), $drupal_package_types, TRUE)) {
-        $key = $package->getName();
-        $drupal_packages[$key] = $package;
-      }
+      $key = $package->getName();
+      $packages[$key] = $package;
     }
-    return $drupal_packages;
+    return $packages;
   }
 
 }
diff --git a/package_manager/tests/fixtures/distro_core/composer.json b/package_manager/tests/fixtures/distro_core/composer.json
new file mode 100644
index 0000000000..a7a8274cb6
--- /dev/null
+++ b/package_manager/tests/fixtures/distro_core/composer.json
@@ -0,0 +1,12 @@
+{
+    "require": {
+        "drupal/test-distribution": "*"
+    },
+    "extra": {
+        "_comment": [
+            "This is a fake composer.json simulating a site which requires a distribution.",
+            "The required core packages are determined by scanning the lock file.",
+            "The fake distribution requires Drupal core directly."
+        ]
+    }
+}
diff --git a/package_manager/tests/fixtures/distro_core/composer.lock b/package_manager/tests/fixtures/distro_core/composer.lock
new file mode 100644
index 0000000000..56572fd029
--- /dev/null
+++ b/package_manager/tests/fixtures/distro_core/composer.lock
@@ -0,0 +1,16 @@
+{
+    "packages": [
+        {
+            "name": "drupal/test-distribution",
+            "version": "1.0.0",
+            "require": {
+                "drupal/core": "*"
+            }
+        },
+        {
+            "name": "drupal/core",
+            "version": "9.8.0"
+        }
+    ],
+    "packages-dev": []
+}
diff --git a/package_manager/tests/fixtures/distro_core_recommended/composer.json b/package_manager/tests/fixtures/distro_core_recommended/composer.json
new file mode 100644
index 0000000000..3a62074ca2
--- /dev/null
+++ b/package_manager/tests/fixtures/distro_core_recommended/composer.json
@@ -0,0 +1,12 @@
+{
+    "require": {
+        "drupal/test-distribution": "*"
+    },
+    "extra": {
+        "_comment": [
+            "This is a fake composer.json simulating a site which requires a distribution.",
+            "The required core packages are determined by scanning the lock file.",
+            "The fake distribution uses drupal/core-recommended to require Drupal core."
+        ]
+    }
+}
diff --git a/package_manager/tests/fixtures/distro_core_recommended/composer.lock b/package_manager/tests/fixtures/distro_core_recommended/composer.lock
new file mode 100644
index 0000000000..dd29a0514d
--- /dev/null
+++ b/package_manager/tests/fixtures/distro_core_recommended/composer.lock
@@ -0,0 +1,23 @@
+{
+    "packages": [
+        {
+            "name": "drupal/test-distribution",
+            "version": "1.0.0",
+            "require": {
+                "drupal/core-recommended": "*"
+            }
+        },
+        {
+            "name": "drupal/core-recommended",
+            "version": "9.8.0",
+            "require": {
+                "drupal/core": "9.8.0"
+            }
+        },
+        {
+            "name": "drupal/core",
+            "version": "9.8.0"
+        }
+    ],
+    "packages-dev": []
+}
diff --git a/package_manager/tests/src/Kernel/ComposerUtilityTest.php b/package_manager/tests/src/Kernel/ComposerUtilityTest.php
new file mode 100644
index 0000000000..3b3cac5d92
--- /dev/null
+++ b/package_manager/tests/src/Kernel/ComposerUtilityTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\Tests\package_manager\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\package_manager\ComposerUtility;
+
+/**
+ * @coversDefaultClass \Drupal\package_manager\ComposerUtility
+ *
+ * @group package_manager
+ */
+class ComposerUtilityTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['package_manager'];
+
+  /**
+   * Data provider for ::testCorePackagesFromLockFile().
+   *
+   * @return array[]
+   *   Sets of arguments to pass to the test method.
+   */
+  public function providerCorePackagesFromLockFile(): array {
+    $fixtures_dir = __DIR__ . '/../../fixtures';
+
+    return [
+      'distro with drupal/core-recommended' => [
+        // This fixture's lock file mentions drupal/core, which is considered a
+        // canonical core package, but it will be ignored in favor of
+        // drupal/core-recommended, which always requires drupal/core as one of
+        // its direct dependencies.
+        "$fixtures_dir/distro_core_recommended",
+        ['drupal/core-recommended'],
+      ],
+      'distro with drupal/core' => [
+        "$fixtures_dir/distro_core",
+        ['drupal/core'],
+      ],
+    ];
+  }
+
+  /**
+   * Tests that required core packages are found by scanning the lock file.
+   *
+   * @param string $dir
+   *   The path of the fake site fixture.
+   * @param string[] $expected_packages
+   *   The names of the core packages which should be detected.
+   *
+   * @covers ::getCorePackageNames
+   *
+   * @dataProvider providerCorePackagesFromLockFile
+   */
+  public function testCorePackagesFromLockFile(string $dir, array $expected_packages): void {
+    $packages = ComposerUtility::createForDirectory($dir)
+      ->getCorePackageNames();
+    $this->assertSame($expected_packages, $packages);
+  }
+
+}
diff --git a/tests/fixtures/fake-site/composer.json b/tests/fixtures/fake-site/composer.json
index 74d8204d88..21939a78ce 100644
--- a/tests/fixtures/fake-site/composer.json
+++ b/tests/fixtures/fake-site/composer.json
@@ -1,5 +1,11 @@
 {
     "require": {
-        "drupal/core": "*"
+        "drupal/test-distribution": "*"
+    },
+    "extra": {
+        "_comment": [
+            "This is a fake composer.json simulating a site which requires a distribution.",
+            "The required core packages are determined by scanning the lock file."
+        ]
     }
 }
diff --git a/tests/fixtures/fake-site/composer.lock b/tests/fixtures/fake-site/composer.lock
index 0967ef424b..dd29a0514d 100644
--- a/tests/fixtures/fake-site/composer.lock
+++ b/tests/fixtures/fake-site/composer.lock
@@ -1 +1,23 @@
-{}
+{
+    "packages": [
+        {
+            "name": "drupal/test-distribution",
+            "version": "1.0.0",
+            "require": {
+                "drupal/core-recommended": "*"
+            }
+        },
+        {
+            "name": "drupal/core-recommended",
+            "version": "9.8.0",
+            "require": {
+                "drupal/core": "9.8.0"
+            }
+        },
+        {
+            "name": "drupal/core",
+            "version": "9.8.0"
+        }
+    ],
+    "packages-dev": []
+}
diff --git a/tests/fixtures/project_staged_validation/no_core_requirements/composer.lock b/tests/fixtures/project_staged_validation/no_core_requirements/composer.lock
new file mode 100644
index 0000000000..b44dcb4aea
--- /dev/null
+++ b/tests/fixtures/project_staged_validation/no_core_requirements/composer.lock
@@ -0,0 +1,4 @@
+{
+    "packages": [],
+    "packages-dev": []
+}
diff --git a/tests/src/Build/UpdateTestBase.php b/tests/src/Build/UpdateTestBase.php
index 56f7f60ec5..8826c0bac9 100644
--- a/tests/src/Build/UpdateTestBase.php
+++ b/tests/src/Build/UpdateTestBase.php
@@ -181,8 +181,16 @@ END;
     // @see ::installQuickStart()
     $this->webRoot = $data['extra']['drupal-scaffold']['locations']['web-root'];
 
-    // Update the test site's composer.json and install dependencies.
+    // Update the test site's composer.json.
     $this->writeJson($composer, $data);
+    // Don't install drupal/core-dev, which is defined as a dev dependency in
+    // both project templates.
+    // @todo Handle dev dependencies properly once
+    //   https://www.drupal.org/project/automatic_updates/issues/3244412 is
+    //   is resolved.
+    $this->executeCommand('composer remove --dev --no-update drupal/core-dev');
+    $this->assertCommandSuccessful();
+    // Install production dependencies.
     $this->executeCommand('composer install --no-dev');
     $this->assertCommandSuccessful();
   }
diff --git a/tests/src/Kernel/UpdaterTest.php b/tests/src/Kernel/UpdaterTest.php
index 602ea49baf..d6bd48448f 100644
--- a/tests/src/Kernel/UpdaterTest.php
+++ b/tests/src/Kernel/UpdaterTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\automatic_updates\Kernel;
 
+use Drupal\automatic_updates\PathLocator;
 use Prophecy\Argument;
 
 /**
@@ -27,6 +28,14 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
   public function testCorrectVersionsStaged() {
     $this->setReleaseMetadata(__DIR__ . '/../../fixtures/release-history/drupal.9.8.1-security.xml');
 
+    // Point to a fake site which requires Drupal core via a distribution. The
+    // lock file should be scanned to determine the core packages, which should
+    // result in drupal/core-recommended being updated.
+    $locator = $this->prophesize(PathLocator::class);
+    $locator->getActiveDirectory()->willReturn(__DIR__ . '/../../fixtures/fake-site');
+    $locator->getStageDirectory()->willReturn('/tmp');
+    $this->container->set('automatic_updates.path_locator', $locator->reveal());
+
     $this->container->get('automatic_updates.updater')->begin([
       'drupal' => '9.8.1',
     ]);
@@ -38,13 +47,7 @@ class UpdaterTest extends AutomaticUpdatesKernelTestBase {
     $stager = $this->prophesize('\PhpTuf\ComposerStager\Domain\StagerInterface');
     $command = [
       'require',
-      'drupal/core:9.8.1',
-      // These two plugins are in the root composer.json that ships with a
-      // git clone of Drupal core, so they will be included when determining
-      // which core packages to update.
-      // @see \Drupal\automatic_updates\Updater::getCorePackageNames()
-      'drupal/core-project-message:9.8.1',
-      'drupal/core-vendor-hardening:9.8.1',
+      'drupal/core-recommended:9.8.1',
       '--update-with-all-dependencies',
     ];
     $stager->stage($command, Argument::cetera())->shouldBeCalled();
-- 
GitLab