Loading composer/Plugin/Scaffold/Plugin.php +41 −0 Original line number Diff line number Diff line Loading @@ -103,6 +103,47 @@ public static function getSubscribedEvents() { */ public function postCmd(Event $event) { $this->handler()->scaffold(); $this->ensureAutoloadRuntimeFile(); } /** * Generates autoload_runtime.php when Handler::scaffold() did not. * * When this plugin package is itself updated during a Composer command, * the Handler class may already have been loaded from the previous version * of the plugin (via postPackage()) before the plugin files were replaced * on disk. PHP keeps the loaded class for the rest of the process, so * Handler::scaffold() then runs the old code, which does not generate the * autoload_runtime.php file (introduced in Drupal 11.4.0). * * Composer reloads this Plugin class from the new plugin code after the * plugin package is updated. It must only use other classes of this plugin * through APIs that exist in every version of the plugin that can be updated * from, because those classes may still be loaded from a previous version. * * @todo Remove after upgrades from Drupal 11.3 are no longer supported. * * @see \Composer\Plugin\PluginManager::registerPackage() * @see https://www.drupal.org/project/drupal/issues/3607866 */ protected function ensureAutoloadRuntimeFile(): void { $manage_options = new ManageOptions($this->composer); $allowed_packages = (new AllowedPackages($this->composer, $this->io, $manage_options))->getAllowedPackages(); if (empty($allowed_packages)) { return; } $web_root = $manage_options->getOptions()->getLocation('web-root'); if (file_exists($web_root . '/autoload_runtime.php')) { return; } $vendor_dir = $this->composer->getConfig()->get('vendor-dir'); $vendor_path = (new Filesystem())->normalizePath(realpath($vendor_dir)); $package_name = $this->composer->getPackage()->getName(); $result = GenerateAutoloadRuntimeReferenceFile::generateAutoloadRuntime($this->io, $package_name, $web_root, $vendor_path); $git_ignore_manager = new ManageGitIgnore($this->io, getcwd()); $git_ignore_manager->manageIgnored([$result], $manage_options->getOptions()); } /** Loading core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php +118 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,13 @@ protected function setUp(): void { $this->fixtures->createIsolatedComposerCacheDir(); } /** * {@inheritdoc} */ protected function tearDown(): void { $this->fixtures->tearDown(); } /** * Tests upgrading the Composer Scaffold plugin. */ Loading Loading @@ -99,6 +106,117 @@ public function testScaffoldUpgrade(): void { $this->assertFileExists("$sut/index.php"); } /** * Tests upgrading the plugin when the Handler class is stale. * * @see \Drupal\Composer\Plugin\Scaffold\Plugin::ensureAutoloadRuntimeFile() */ public function testUpgradeWithStaleHandler(): void { $this->fixturesDir = $this->fixtures->tmpDir($this->name()); $pluginSource = $this->fixturesDir . '/composer-scaffold-plugin'; $replacements = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $pluginSource]; $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements); $sut = $this->fixturesDir . '/drupal-drupal'; // The Scaffold plugin must be copied rather than symlinked into the // vendor directory, so that the installed plugin keeps the old code // after the plugin source is updated below. $composerJson = json_decode(file_get_contents("$sut/composer.json"), TRUE); $composerJson['repositories']['composer-scaffold']['options']['symlink'] = FALSE; file_put_contents("$sut/composer.json", json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); // Create an "old" copy of the Scaffold plugin that behaves like the // plugin from before Drupal 11.4.0: it knows nothing about // autoload_runtime.php. $this->copyPluginVersion($pluginSource, '100.0.0', TRUE); $this->mustExec('composer install --no-ansi', $sut); $this->assertFileExists("$sut/autoload.php"); $this->assertFileDoesNotExist("$sut/autoload_runtime.php"); // Replace the plugin source with the current code, then require a // package containing a Composer plugin flagged with // 'plugin-modifies-downloads'. Composer will install that package before // the Scaffold plugin's update operation, so its post-package-install // event fires while the old Scaffold plugin code is still installed, // pinning the old Handler class for the remainder of the process. $this->copyPluginVersion($pluginSource, '100.0.1', FALSE); $composerJson['repositories']['downloads-modifier'] = [ 'type' => 'path', 'url' => '../composer-plugin-downloads-modifier', ]; $composerJson['require']['fixtures/composer-plugin-downloads-modifier'] = '*'; $composerJson['config']['allow-plugins']['fixtures/composer-plugin-downloads-modifier'] = TRUE; file_put_contents("$sut/composer.json", json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $stdout = $this->mustExec('composer update --no-ansi --no-interaction 2>&1', $sut); // Verify that this test executed the stale Handler class: the // downloads-modifier plugin must have been installed before the Scaffold // plugin was updated, and the marker written by the stale Handler code // must appear in the output. $installPos = strpos($stdout, 'Installing fixtures/composer-plugin-downloads-modifier'); $upgradePos = strpos($stdout, 'Upgrading drupal/core-composer-scaffold (100.0.0 => 100.0.1):'); $this->assertNotFalse($installPos, $stdout); $this->assertNotFalse($upgradePos, $stdout); $this->assertLessThan($upgradePos, $installPos, $stdout); $this->assertStringContainsString('SCAFFOLD_TEST_STALE_HANDLER', $stdout); // Even though the stale Handler did not generate autoload_runtime.php, // the refreshed Plugin class must have generated it. $this->assertFileExists("$sut/autoload_runtime.php"); $this->assertStringContainsString("require __DIR__ . '/vendor/autoload_runtime.php'", file_get_contents("$sut/autoload_runtime.php")); } /** * Copies the Scaffold plugin source to a target directory with a version. * * @param string $target * Directory to place the plugin copy in. * @param string $version * Version to set in the composer.json of the copy, so that the path * repository provides an update when the version is bumped. * @param bool $stale * When TRUE, remove all knowledge of autoload_runtime.php from the copy * to simulate the plugin from before Drupal 11.4.0. A marker is written * to output by the modified Handler so that tests can verify that the * stale Handler code ran. */ protected function copyPluginVersion(string $target, string $version, bool $stale): void { $filesystem = new Filesystem(); $filesystem->remove($target); $filesystem->copy($this->fixtures->projectRoot(), $target); $composerJson = json_decode(file_get_contents("$target/composer.json"), TRUE); $composerJson['version'] = $version; file_put_contents("$target/composer.json", json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); if ($stale) { $generateRuntime = <<<'PHP' // The same is done for the autoload_runtime file that loads the Symfony // runtime. if (!GenerateAutoloadRuntimeReferenceFile::autoloadRuntimeFileCommitted($this->io, $this->rootPackageName(), $web_root)) { $scaffold_results[] = GenerateAutoloadRuntimeReferenceFile::generateAutoloadRuntime($this->io, $this->rootPackageName(), $web_root, $this->getVendorPath()); } PHP; $this->replaceCodeInFile("$target/Handler.php", $generateRuntime, " \$this->io->write('SCAFFOLD_TEST_STALE_HANDLER');"); $this->replaceCodeInFile("$target/Plugin.php", " \$this->ensureAutoloadRuntimeFile();\n", ''); unlink("$target/GenerateAutoloadRuntimeReferenceFile.php"); } } /** * Replaces a code fragment in a file. * * @param string $path * Path of the file to modify. * @param string $search * The code fragment to replace. * @param string $replace * The replacement code fragment. */ protected function replaceCodeInFile(string $path, string $search, string $replace): void { $contents = file_get_contents($path); $this->assertStringContainsString($search, $contents, "Expected code not found in $path; update this test to match the current code."); file_put_contents($path, str_replace($search, $replace, $contents)); } /** * Copy the provided source directory and create a temporary git repository. * Loading core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-plugin-downloads-modifier/composer.json 0 → 100644 +16 −0 Original line number Diff line number Diff line { "name": "fixtures/composer-plugin-downloads-modifier", "type": "composer-plugin", "require": { "composer-plugin-api": "^2" }, "autoload": { "psr-4": { "Drupal\\Tests\\fixture\\Composer\\Plugin\\DownloadsModifier\\": "src" } }, "extra": { "class": "Drupal\\Tests\\fixture\\Composer\\Plugin\\DownloadsModifier\\DownloadsModifierPlugin", "plugin-modifies-downloads": true } } core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-plugin-downloads-modifier/src/DownloadsModifierPlugin.php 0 → 100644 +40 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\fixture\Composer\Plugin\DownloadsModifier; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; /** * A do-nothing Composer plugin flagged with 'plugin-modifies-downloads'. * * Composer moves the install operation of such a plugin in front of all * other plugin operations. Tests use this to force a post-package-install * event to fire before the Scaffold plugin's own update operation executes. * * @see \Composer\DependencyResolver\Transaction::movePluginsToFront() */ class DownloadsModifierPlugin implements PluginInterface { /** * {@inheritdoc} */ public function activate(Composer $composer, IOInterface $io): void { } /** * {@inheritdoc} */ public function deactivate(Composer $composer, IOInterface $io): void { } /** * {@inheritdoc} */ public function uninstall(Composer $composer, IOInterface $io): void { } } Loading
composer/Plugin/Scaffold/Plugin.php +41 −0 Original line number Diff line number Diff line Loading @@ -103,6 +103,47 @@ public static function getSubscribedEvents() { */ public function postCmd(Event $event) { $this->handler()->scaffold(); $this->ensureAutoloadRuntimeFile(); } /** * Generates autoload_runtime.php when Handler::scaffold() did not. * * When this plugin package is itself updated during a Composer command, * the Handler class may already have been loaded from the previous version * of the plugin (via postPackage()) before the plugin files were replaced * on disk. PHP keeps the loaded class for the rest of the process, so * Handler::scaffold() then runs the old code, which does not generate the * autoload_runtime.php file (introduced in Drupal 11.4.0). * * Composer reloads this Plugin class from the new plugin code after the * plugin package is updated. It must only use other classes of this plugin * through APIs that exist in every version of the plugin that can be updated * from, because those classes may still be loaded from a previous version. * * @todo Remove after upgrades from Drupal 11.3 are no longer supported. * * @see \Composer\Plugin\PluginManager::registerPackage() * @see https://www.drupal.org/project/drupal/issues/3607866 */ protected function ensureAutoloadRuntimeFile(): void { $manage_options = new ManageOptions($this->composer); $allowed_packages = (new AllowedPackages($this->composer, $this->io, $manage_options))->getAllowedPackages(); if (empty($allowed_packages)) { return; } $web_root = $manage_options->getOptions()->getLocation('web-root'); if (file_exists($web_root . '/autoload_runtime.php')) { return; } $vendor_dir = $this->composer->getConfig()->get('vendor-dir'); $vendor_path = (new Filesystem())->normalizePath(realpath($vendor_dir)); $package_name = $this->composer->getPackage()->getName(); $result = GenerateAutoloadRuntimeReferenceFile::generateAutoloadRuntime($this->io, $package_name, $web_root, $vendor_path); $git_ignore_manager = new ManageGitIgnore($this->io, getcwd()); $git_ignore_manager->manageIgnored([$result], $manage_options->getOptions()); } /** Loading
core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php +118 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,13 @@ protected function setUp(): void { $this->fixtures->createIsolatedComposerCacheDir(); } /** * {@inheritdoc} */ protected function tearDown(): void { $this->fixtures->tearDown(); } /** * Tests upgrading the Composer Scaffold plugin. */ Loading Loading @@ -99,6 +106,117 @@ public function testScaffoldUpgrade(): void { $this->assertFileExists("$sut/index.php"); } /** * Tests upgrading the plugin when the Handler class is stale. * * @see \Drupal\Composer\Plugin\Scaffold\Plugin::ensureAutoloadRuntimeFile() */ public function testUpgradeWithStaleHandler(): void { $this->fixturesDir = $this->fixtures->tmpDir($this->name()); $pluginSource = $this->fixturesDir . '/composer-scaffold-plugin'; $replacements = ['SYMLINK' => 'false', 'PROJECT_ROOT' => $pluginSource]; $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements); $sut = $this->fixturesDir . '/drupal-drupal'; // The Scaffold plugin must be copied rather than symlinked into the // vendor directory, so that the installed plugin keeps the old code // after the plugin source is updated below. $composerJson = json_decode(file_get_contents("$sut/composer.json"), TRUE); $composerJson['repositories']['composer-scaffold']['options']['symlink'] = FALSE; file_put_contents("$sut/composer.json", json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); // Create an "old" copy of the Scaffold plugin that behaves like the // plugin from before Drupal 11.4.0: it knows nothing about // autoload_runtime.php. $this->copyPluginVersion($pluginSource, '100.0.0', TRUE); $this->mustExec('composer install --no-ansi', $sut); $this->assertFileExists("$sut/autoload.php"); $this->assertFileDoesNotExist("$sut/autoload_runtime.php"); // Replace the plugin source with the current code, then require a // package containing a Composer plugin flagged with // 'plugin-modifies-downloads'. Composer will install that package before // the Scaffold plugin's update operation, so its post-package-install // event fires while the old Scaffold plugin code is still installed, // pinning the old Handler class for the remainder of the process. $this->copyPluginVersion($pluginSource, '100.0.1', FALSE); $composerJson['repositories']['downloads-modifier'] = [ 'type' => 'path', 'url' => '../composer-plugin-downloads-modifier', ]; $composerJson['require']['fixtures/composer-plugin-downloads-modifier'] = '*'; $composerJson['config']['allow-plugins']['fixtures/composer-plugin-downloads-modifier'] = TRUE; file_put_contents("$sut/composer.json", json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $stdout = $this->mustExec('composer update --no-ansi --no-interaction 2>&1', $sut); // Verify that this test executed the stale Handler class: the // downloads-modifier plugin must have been installed before the Scaffold // plugin was updated, and the marker written by the stale Handler code // must appear in the output. $installPos = strpos($stdout, 'Installing fixtures/composer-plugin-downloads-modifier'); $upgradePos = strpos($stdout, 'Upgrading drupal/core-composer-scaffold (100.0.0 => 100.0.1):'); $this->assertNotFalse($installPos, $stdout); $this->assertNotFalse($upgradePos, $stdout); $this->assertLessThan($upgradePos, $installPos, $stdout); $this->assertStringContainsString('SCAFFOLD_TEST_STALE_HANDLER', $stdout); // Even though the stale Handler did not generate autoload_runtime.php, // the refreshed Plugin class must have generated it. $this->assertFileExists("$sut/autoload_runtime.php"); $this->assertStringContainsString("require __DIR__ . '/vendor/autoload_runtime.php'", file_get_contents("$sut/autoload_runtime.php")); } /** * Copies the Scaffold plugin source to a target directory with a version. * * @param string $target * Directory to place the plugin copy in. * @param string $version * Version to set in the composer.json of the copy, so that the path * repository provides an update when the version is bumped. * @param bool $stale * When TRUE, remove all knowledge of autoload_runtime.php from the copy * to simulate the plugin from before Drupal 11.4.0. A marker is written * to output by the modified Handler so that tests can verify that the * stale Handler code ran. */ protected function copyPluginVersion(string $target, string $version, bool $stale): void { $filesystem = new Filesystem(); $filesystem->remove($target); $filesystem->copy($this->fixtures->projectRoot(), $target); $composerJson = json_decode(file_get_contents("$target/composer.json"), TRUE); $composerJson['version'] = $version; file_put_contents("$target/composer.json", json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); if ($stale) { $generateRuntime = <<<'PHP' // The same is done for the autoload_runtime file that loads the Symfony // runtime. if (!GenerateAutoloadRuntimeReferenceFile::autoloadRuntimeFileCommitted($this->io, $this->rootPackageName(), $web_root)) { $scaffold_results[] = GenerateAutoloadRuntimeReferenceFile::generateAutoloadRuntime($this->io, $this->rootPackageName(), $web_root, $this->getVendorPath()); } PHP; $this->replaceCodeInFile("$target/Handler.php", $generateRuntime, " \$this->io->write('SCAFFOLD_TEST_STALE_HANDLER');"); $this->replaceCodeInFile("$target/Plugin.php", " \$this->ensureAutoloadRuntimeFile();\n", ''); unlink("$target/GenerateAutoloadRuntimeReferenceFile.php"); } } /** * Replaces a code fragment in a file. * * @param string $path * Path of the file to modify. * @param string $search * The code fragment to replace. * @param string $replace * The replacement code fragment. */ protected function replaceCodeInFile(string $path, string $search, string $replace): void { $contents = file_get_contents($path); $this->assertStringContainsString($search, $contents, "Expected code not found in $path; update this test to match the current code."); file_put_contents($path, str_replace($search, $replace, $contents)); } /** * Copy the provided source directory and create a temporary git repository. * Loading
core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-plugin-downloads-modifier/composer.json 0 → 100644 +16 −0 Original line number Diff line number Diff line { "name": "fixtures/composer-plugin-downloads-modifier", "type": "composer-plugin", "require": { "composer-plugin-api": "^2" }, "autoload": { "psr-4": { "Drupal\\Tests\\fixture\\Composer\\Plugin\\DownloadsModifier\\": "src" } }, "extra": { "class": "Drupal\\Tests\\fixture\\Composer\\Plugin\\DownloadsModifier\\DownloadsModifierPlugin", "plugin-modifies-downloads": true } }
core/tests/Drupal/Tests/Composer/Plugin/Scaffold/fixtures/composer-plugin-downloads-modifier/src/DownloadsModifierPlugin.php 0 → 100644 +40 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\fixture\Composer\Plugin\DownloadsModifier; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Plugin\PluginInterface; /** * A do-nothing Composer plugin flagged with 'plugin-modifies-downloads'. * * Composer moves the install operation of such a plugin in front of all * other plugin operations. Tests use this to force a post-package-install * event to fire before the Scaffold plugin's own update operation executes. * * @see \Composer\DependencyResolver\Transaction::movePluginsToFront() */ class DownloadsModifierPlugin implements PluginInterface { /** * {@inheritdoc} */ public function activate(Composer $composer, IOInterface $io): void { } /** * {@inheritdoc} */ public function deactivate(Composer $composer, IOInterface $io): void { } /** * {@inheritdoc} */ public function uninstall(Composer $composer, IOInterface $io): void { } }