diff --git a/package_manager/src/ComposerInspector.php b/package_manager/src/ComposerInspector.php index 588b11d64e11ed1edbb9a3b92b362b4b22ae22af..ed905603772a107574f51ff91943c37b1e4b4e92 100644 --- a/package_manager/src/ComposerInspector.php +++ b/package_manager/src/ComposerInspector.php @@ -50,15 +50,11 @@ class ComposerInspector implements LoggerAwareInterface { /** * A semantic version constraint for the supported version(s) of Composer. * - * Only versions supported by Composer are supported: the LTS and the latest - * minor version. Those are currently (May 2023) 2.2 and 2.5, but will be 2.5 - * and 2.6 later this year (December 2023): the 2.2 LTS ends in December 2023. - * * @see https://endoflife.date/composer * * @var string */ - final public const SUPPORTED_VERSION = '~2.5.5 || ^2.6'; + final public const SUPPORTED_VERSION = '^2.6'; /** * Constructs a ComposerInspector object. diff --git a/package_manager/tests/fixtures/fake_site/composer.json b/package_manager/tests/fixtures/fake_site/composer.json index 0158b2eded763a12554ef955a46d577e7754593e..a7f2b4cd69dff7efcf68ea89ce63ac81412e19a6 100644 --- a/package_manager/tests/fixtures/fake_site/composer.json +++ b/package_manager/tests/fixtures/fake_site/composer.json @@ -23,39 +23,11 @@ "baz": null }, "repositories": { - "packagist.org": false, - "drupal/core-recommended": { - "type": "path", - "version": "9.8.0", - "url": "../path_repos/drupal--core-recommended", - "options": { - "symlink": false - } + "fake-packages": { + "type": "composer", + "url": "./" }, - "drupal/core-dev": { - "type": "path", - "version": "9.8.0", - "url": "../path_repos/drupal--core-dev", - "options": { - "symlink": false - } - }, - "drupal/core": { - "type": "path", - "version": "9.8.0", - "url": "../path_repos/drupal--core", - "options": { - "symlink": false - } - }, - "cweagans/composer-patches": { - "type": "path", - "version": "1.7.333", - "url": "../path_repos/cweagans--composer-patches", - "options": { - "symlink": false - } - } + "packagist.org": false }, "minimum-stability": "stable", "config": { diff --git a/package_manager/tests/fixtures/fake_site/composer.lock b/package_manager/tests/fixtures/fake_site/composer.lock index 5fbd98bb419dd3507585f3c0463c6a71caabd472..c0a62b3d7336121fb1c5a21f81a070c0db23676b 100644 --- a/package_manager/tests/fixtures/fake_site/composer.lock +++ b/package_manager/tests/fixtures/fake_site/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "672a2db4be3ba5e4f025ddea6b7dacd7", + "content-hash": "0235f8f2d3ef91d87864b76e12838e89", "packages": [ { "name": "drupal/core", @@ -50,11 +50,7 @@ } } }, - "description": "A fake version of drupal/core", - "transport-options": { - "symlink": false, - "relative": true - } + "description": "A fake version of drupal/core" }, { "name": "drupal/core-recommended", @@ -65,11 +61,7 @@ "reference": "112e4f7cfe8312457cd0eb58dcbffebc148850d8" }, "type": "project", - "description": "A fake version of drupal/core-recommended", - "transport-options": { - "symlink": false, - "relative": true - } + "description": "A fake version of drupal/core-recommended" } ], "packages-dev": [ @@ -82,11 +74,7 @@ "reference": "b99a99a11ff2779b5e4c5787dc43575382a3548c" }, "type": "package", - "description": "A fake version of drupal/core-dev", - "transport-options": { - "symlink": false, - "relative": true - } + "description": "A fake version of drupal/core-dev" } ], "aliases": [], @@ -96,5 +84,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/package_manager/tests/fixtures/fake_site/packages.json b/package_manager/tests/fixtures/fake_site/packages.json new file mode 100644 index 0000000000000000000000000000000000000000..12a17cccff4b8f41cc1685143aa45a7a6a29bf04 --- /dev/null +++ b/package_manager/tests/fixtures/fake_site/packages.json @@ -0,0 +1,101 @@ +{ + "packages": { + "drupal/core-recommended": { + "9.8.0": { + "name": "drupal/core-recommended", + "description": "A fake version of drupal/core-recommended", + "type": "project", + "version": "9.8.0", + "dist": { + "type": "path", + "url": "../path_repos/drupal--core-recommended", + "reference": "112e4f7cfe8312457cd0eb58dcbffebc148850d8" + } + } + }, + "drupal/core-dev": { + "9.8.0": { + "name": "drupal/core-dev", + "description": "A fake version of drupal/core-dev", + "type": "package", + "version": "9.8.0", + "dist": { + "type": "path", + "url": "../path_repos/drupal--core-dev", + "reference": "b99a99a11ff2779b5e4c5787dc43575382a3548c" + } + } + }, + "drupal/core": { + "9.8.0": { + "name": "drupal/core", + "type": "drupal-core", + "description": "A fake version of drupal/core", + "version": "9.8.0", + "extra": { + "_readme": [ + "The 'drupal-scaffold' section below is needed because 'Drupal\\automatic_updates\\Validator\\ScaffoldFilePermissionsValidator'", + "uses this section to determine which files to check. The actual composer.json file for drupal/core will have more files listed", + "but this limited list is used in '\\Drupal\\Tests\\automatic_updates\\Kernel\\StatusCheck\\ScaffoldFilePermissionsValidatorTest'", + "to ensure this section determines the file list." + ], + "drupal-scaffold": { + "file-mapping": { + "[project-root]/.editorconfig": "assets/scaffold/files/editorconfig", + "[project-root]/.gitattributes": "assets/scaffold/files/gitattributes", + "[web-root]/.csslintrc": "assets/scaffold/files/csslintrc", + "[web-root]/.eslintignore": "assets/scaffold/files/eslintignore", + "[web-root]/.eslintrc.json": "assets/scaffold/files/eslintrc.json", + "[web-root]/.ht.router.php": "assets/scaffold/files/ht.router.php", + "[web-root]/.htaccess": "assets/scaffold/files/htaccess", + "[web-root]/example.gitignore": "assets/scaffold/files/example.gitignore", + "[web-root]/index.php": "assets/scaffold/files/index.php", + "[web-root]/INSTALL.txt": "assets/scaffold/files/drupal.INSTALL.txt", + "[web-root]/README.md": "assets/scaffold/files/drupal.README.md", + "[web-root]/robots.txt": "assets/scaffold/files/robots.txt", + "[web-root]/update.php": "assets/scaffold/files/update.php", + "[web-root]/web.config": "assets/scaffold/files/web.config", + "[web-root]/sites/README.txt": "assets/scaffold/files/sites.README.txt", + "[web-root]/sites/development.services.yml": "assets/scaffold/files/development.services.yml", + "[web-root]/sites/example.settings.local.php": "assets/scaffold/files/example.settings.local.php", + "[web-root]/sites/example.sites.php": "assets/scaffold/files/example.sites.php", + "[web-root]/sites/default/default.services.yml": "assets/scaffold/files/default.services.yml", + "[web-root]/sites/default/default.settings.php": "assets/scaffold/files/default.settings.php", + "[web-root]/modules/README.txt": "assets/scaffold/files/modules.README.txt", + "[web-root]/profiles/README.txt": "assets/scaffold/files/profiles.README.txt", + "[web-root]/themes/README.txt": "assets/scaffold/files/themes.README.txt" + } + } + }, + "dist": { + "type": "path", + "url": "../path_repos/drupal--core", + "reference": "31fd2270701526555acae45a3601c777e35508d5" + } + } + }, + "cweagans/composer-patches": { + "1.7.333": { + "name": "cweagans/composer-patches", + "description": "A fake version of cweagans/composer-patches", + "type": "composer-plugin", + "version": "1.7.333", + "extra": { + "class": "\\cweagans\\Fake\\ComposerPatches" + }, + "dist": { + "type": "path", + "url": "../path_repos/cweagans--composer-patches" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "autoload": { + "psr-4": { + "cweagans\\Fake\\": "src" + } + } + } + } + } +} diff --git a/package_manager/tests/fixtures/fake_site/vendor/composer/installed.json b/package_manager/tests/fixtures/fake_site/vendor/composer/installed.json index f4b54bb81ac534a787e2a608478d9ed2871aba3e..8831ea21904f3fd49fc017935040311e31d01848 100644 --- a/package_manager/tests/fixtures/fake_site/vendor/composer/installed.json +++ b/package_manager/tests/fixtures/fake_site/vendor/composer/installed.json @@ -47,10 +47,6 @@ }, "installation-source": "dist", "description": "A fake version of drupal/core", - "transport-options": { - "symlink": false, - "relative": true - }, "install-path": "../drupal/core" }, { @@ -65,10 +61,6 @@ "type": "package", "installation-source": "dist", "description": "A fake version of drupal/core-dev", - "transport-options": { - "symlink": false, - "relative": true - }, "install-path": "../drupal/core-dev" }, { @@ -83,10 +75,6 @@ "type": "project", "installation-source": "dist", "description": "A fake version of drupal/core-recommended", - "transport-options": { - "symlink": false, - "relative": true - }, "install-path": "../drupal/core-recommended" } ], diff --git a/package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml b/package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml index f83263a21384a8d6a18d2211d0f53980aa997714..4ea503f7188ab4c0f6d998b00cb1808840654780 100644 --- a/package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml +++ b/package_manager/tests/modules/fixture_manipulator/fixture_manipulator.services.yml @@ -3,3 +3,5 @@ services: autowire: true Drupal\fixture_manipulator\StageFixtureManipulator: decorates: 'PhpTuf\ComposerStager\API\Core\BeginnerInterface' + Drupal\fixture_manipulator\ProcessFactory: + decorates: 'PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface' diff --git a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php index 80a499c5a22555742a0a83cb93d2343bbf7c962f..af72eca2b2838421f8d4925622318e55eb0d6de7 100644 --- a/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php +++ b/package_manager/tests/modules/fixture_manipulator/src/FixtureManipulator.php @@ -377,18 +377,6 @@ class FixtureManipulator { } } - /** - * Ensures a path is writable. - * - * @param string $path - * The path. - */ - private static function ensureFilePathIsWritable(string $path): void { - if (!is_writable($path)) { - throw new \LogicException("'$path' is not writable."); - } - } - /** * Creates an empty .git folder after being provided a path. */ @@ -587,31 +575,18 @@ class FixtureManipulator { $this->createPathRepo($package, $repo_path, NULL); } - // Register the repository, keyed by package name and version. This ensures - // each package is registered only once and its version can be updated (but - // must have unique versions). - $repo_key = "repo.$name"; - if ($is_new_or_fork === 'fork') { - $repositories = json_decode(file_get_contents($this->dir . '/composer.json'), TRUE, flags: JSON_THROW_ON_ERROR)['repositories']; - // @todo consistently use 'version' or 'options.versions.PACKAGE_NAME', by fixing ComposerFixtureCreator in https://drupal.org/i/3347055 - $original_version = isset($repositories[$name]['version']) ? $repositories[$name]['version'] : $repositories[$name]['options']['versions'][$name]; - if ($package['version'] !== $original_version) { - $repo_key .= "--" . $package['version']; - } - } - $repository = json_encode([ + // Add the package to the Composer repository defined for the site. + $packages_json = $this->dir . '/packages.json'; + $packages_data = file_get_contents($packages_json); + $packages_data = json_decode($packages_data, TRUE, flags: JSON_THROW_ON_ERROR); + + $version = $package['version']; + $package['dist'] = [ 'type' => 'path', 'url' => $repo_path, - 'options' => [ - 'symlink' => FALSE, - 'versions' => [ - $name => $package['version'], - ], - // @see https://getcomposer.org/repoprio - 'canonical' => FALSE, - ], - ], JSON_UNESCAPED_SLASHES); - $this->runComposerCommand(['config', $repo_key, $repository]); + ]; + $packages_data['packages'][$name][$version] = $package; + file_put_contents($packages_json, json_encode($packages_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); return $repo_path; } @@ -631,8 +606,8 @@ class FixtureManipulator { } // Update all the repos in the composer.json file to point to the new // repos at the absolute path. - $composer_json = file_get_contents($this->dir . '/composer.json'); - file_put_contents($this->dir . '/composer.json', str_replace('../path_repos/', "$path_repo_base/", $composer_json)); + $composer_json = file_get_contents($this->dir . '/packages.json'); + file_put_contents($this->dir . '/packages.json', str_replace('../path_repos/', "$path_repo_base/", $composer_json)); $this->runComposerCommand(['update', '--lock']); $this->runComposerCommand(['install']); } diff --git a/package_manager/tests/modules/fixture_manipulator/src/ProcessFactory.php b/package_manager/tests/modules/fixture_manipulator/src/ProcessFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..3066e3b724a9648b8fad4fca2ddfbf00f0fe43ae --- /dev/null +++ b/package_manager/tests/modules/fixture_manipulator/src/ProcessFactory.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\fixture_manipulator; + +use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface; + +/** + * Process factory that always sets the COMPOSER_MIRROR_PATH_REPOS env variable. + * + * This is necessary because the fake_site fixture is built from a Composer-type + * repository, which will normally try to symlink packages which are installed + * from local directories, which in turn will break Package Manager because it + * does not support symlinks pointing outside the main code base. The + * COMPOSER_MIRROR_PATH_REPOS environment variable forces Composer to mirror, + * rather than symlink, local directories when installing packages. + * + * @see \Drupal\fixture_manipulator\FixtureManipulator::setUpRepos() + */ +final class ProcessFactory implements ProcessFactoryInterface { + + /** + * Constructs a ProcessFactory object. + * + * @param \PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface $decorated + * The decorated process factory service. + */ + public function __construct(private readonly ProcessFactoryInterface $decorated) {} + + /** + * {@inheritdoc} + */ + public function create(array $command): ProcessInterface { + $process = $this->decorated->create($command); + + $env = $process->getEnv(); + $env['COMPOSER_MIRROR_PATH_REPOS'] = '1'; + return $process->setEnv($env); + } + +} diff --git a/package_manager/tests/src/Kernel/ComposerInspectorTest.php b/package_manager/tests/src/Kernel/ComposerInspectorTest.php index f4932ee6c52dc2b960b98c5ed216376f9e57d680..c38e5fc00cdff4da1c103df48d1bcaeed05a62ba 100644 --- a/package_manager/tests/src/Kernel/ComposerInspectorTest.php +++ b/package_manager/tests/src/Kernel/ComposerInspectorTest.php @@ -223,8 +223,8 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase { * @testWith ["2.2.12", "<default>"] * ["2.2.13", "<default>"] * ["2.5.0", "<default>"] - * ["2.5.5", null] - * ["2.5.11", null] + * ["2.5.5", "<default>"] + * ["2.5.11", "<default>"] * ["2.6.0", null] * ["2.2.11", "<default>"] * ["2.2.0-dev", "<default>"] diff --git a/package_manager/tests/src/Kernel/FixtureManipulatorTest.php b/package_manager/tests/src/Kernel/FixtureManipulatorTest.php index 7c6c9da198cc58b4fd3735e8edbf608f0727e04c..00b5c370dfec9f23c705f6c5edc3293d1513bc6f 100644 --- a/package_manager/tests/src/Kernel/FixtureManipulatorTest.php +++ b/package_manager/tests/src/Kernel/FixtureManipulatorTest.php @@ -145,10 +145,10 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase { public function testModifyPackageConfig(): void { // Assert ::modifyPackage() works with a package in an existing fixture not // created by ::addPackage(). - $decode_composer_json = function ($package_name): array { - return json_decode(file_get_contents($this->dir . "/vendor/$package_name/composer.json"), TRUE, flags: JSON_THROW_ON_ERROR); + $decode_packages_json = function (): array { + return json_decode(file_get_contents($this->dir . "/packages.json"), TRUE, flags: JSON_THROW_ON_ERROR); }; - $original_composer_json = $decode_composer_json('my/dev-package'); + $original_packages_json = $decode_packages_json(); (new ActiveFixtureManipulator()) // @see ::setUp() ->modifyPackageConfig('my/dev-package', '2.1.0', ['description' => 'something else'], TRUE) @@ -156,10 +156,10 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase { // Verify that the package is indeed properly installed. $this->assertSame('2.1.0', $this->inspector->getInstalledPackagesList($this->dir)['my/dev-package']?->version); // Verify that the original exists, but has no description. - $this->assertSame('my/dev-package', $original_composer_json['name']); - $this->assertArrayNotHasKey('description', $original_composer_json); + $this->assertSame('my/dev-package', $original_packages_json['packages']['my/dev-package']['2.1.0']['name']); + $this->assertArrayNotHasKey('description', $original_packages_json); // Verify that the description was updated. - $this->assertSame('something else', $decode_composer_json('my/dev-package')['description']); + $this->assertSame('something else', $decode_packages_json()['packages']['my/dev-package']['2.1.0']['description']); (new ActiveFixtureManipulator()) // Add a key to an existing package. @@ -167,9 +167,8 @@ class FixtureManipulatorTest extends PackageManagerKernelTestBase { // Change a key in an existing package. ->setVersion('my/dev-package', '3.2.1', TRUE) ->commitChanges(); - $this->assertSame(['foo' => 'bar'], $decode_composer_json('my/package')['extra']); + $this->assertSame(['foo' => 'bar'], $decode_packages_json()['packages']['my/package']['1.2.3']['extra']); $this->assertSame('3.2.1', $this->inspector->getInstalledPackagesList($this->dir)['my/dev-package']?->version); - } /** diff --git a/scripts/PackageManagerFixtureCreator.php b/scripts/PackageManagerFixtureCreator.php index db17dff6a428e626bd526a372f32511d9fe4c946..d3721b7b227b7902f547f715119d6fd889815b2d 100755 --- a/scripts/PackageManagerFixtureCreator.php +++ b/scripts/PackageManagerFixtureCreator.php @@ -41,13 +41,21 @@ final class PackageManagerFixtureCreator { $fixture_core_composer_data = $fixture_core_composer_file->read(); $fixture_core_composer_data['extra']['drupal-scaffold']['file-mapping'] = $core_composer_data['extra']['drupal-scaffold']['file-mapping']; $fixture_core_composer_file->write($fixture_core_composer_data); + + $fixture_packages_json = new JsonFile(static::FIXTURE_PATH . '/packages.json'); + $fixture_packages_data = $fixture_packages_json->read(); + foreach ($fixture_packages_data['packages']['drupal/core'] as &$release) { + $release['extra']['drupal-scaffold']['file-mapping'] = $core_composer_data['extra']['drupal-scaffold']['file-mapping']; + } + $fixture_packages_json->write($fixture_packages_data); + $fs = new Filesystem(); $fs->remove(static::FIXTURE_PATH . "/composer.lock"); // Remove all the vendor folders but leave our 2 test files. // @see \Drupal\Tests\package_manager\Kernel\PathExcluder\VendorHardeningExcluderTest - self::removeAllExcept(self::FIXTURE_PATH . "/vendor", ['.htaccess', 'web.config']); + self::removeAllExcept(static::FIXTURE_PATH . "/vendor", ['.htaccess', 'web.config']); - static::doComposerInstall(); + self::runComposerCommand(['install']); static::removeAllExcept(static::FIXTURE_PATH . '/vendor/composer', ['installed.json', 'installed.php']); $fs->remove(static::FIXTURE_PATH . '/vendor/autoload.php'); print "\nFixture updated.\nRunning phpcbf"; @@ -70,7 +78,9 @@ final class PackageManagerFixtureCreator { private static function runComposerCommand(array $command): string { array_unshift($command, 'composer'); $command[] = "--working-dir=" . static::FIXTURE_PATH; - $process = new Process($command); + $process = new Process($command, env: [ + 'COMPOSER_MIRROR_PATH_REPOS' => '1', + ]); $process->run(); if (!$process->isSuccessful()) { throw new ProcessFailedException($process); @@ -100,11 +110,4 @@ final class PackageManagerFixtureCreator { } } - /** - * Runs `composer install`. - */ - private static function doComposerInstall(): void { - self::runComposerCommand(['install']); - } - }