diff --git a/core/lib/Drupal/Core/Command/GenerateTheme.php b/core/lib/Drupal/Core/Command/GenerateTheme.php index 66b373d12086bf6182db61da9256f8c52171f15c..e613358f2b19cfa09d7787a37bb9b9d5e84dd869 100644 --- a/core/lib/Drupal/Core/Command/GenerateTheme.php +++ b/core/lib/Drupal/Core/Command/GenerateTheme.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Command; use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; @@ -14,7 +15,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Process\Process; use Twig\Util\TemplateDirIterator; /** @@ -128,6 +131,43 @@ protected function execute(InputInterface $input, OutputInterface $output): int $info['core_version_requirement'] = '^' . $this->getVersion(); + if (!array_key_exists('version', $info)) { + $confirm_versionless_source_theme = new ConfirmationQuestion(sprintf('The source theme %s does not have a version specified. This makes tracking changes in the source theme difficult. Are you sure you want to continue?', $source_theme->getName())); + if (!$io->askQuestion($confirm_versionless_source_theme)) { + return 0; + } + } + + $source_version = $info['version'] ?? 'unknown-version'; + if ($source_version === 'VERSION') { + $source_version = \Drupal::VERSION; + } + // A version in the generator string like "9.4.0-dev" is not very helpful. + // When this occurs, generate a version string that points to a commit. + if (VersionParser::parseStability($source_version) === 'dev') { + $git_check = Process::fromShellCommandline('git --help'); + $git_check->run(); + if ($git_check->getExitCode()) { + $io->error(sprintf('The source theme %s has a development version number (%s). Determining a specific commit is not possible because git is not installed. Either install git or use a tagged release to generate a theme.', $source_theme->getName(), $source_version)); + return 1; + } + + // Get the git commit for the source theme. + $git_get_commit = Process::fromShellCommandline("git rev-list --max-count=1 --abbrev-commit HEAD -C $source"); + $git_get_commit->run(); + if ($git_get_commit->getOutput() === '') { + $confirm_packaged_dev_release = new ConfirmationQuestion(sprintf('The source theme %s has a development version number (%s). Because it is not a git checkout, a specific commit could not be identified. This makes tracking changes in the source theme difficult. Are you sure you want to continue?', $source_theme->getName(), $source_version)); + if (!$io->askQuestion($confirm_packaged_dev_release)) { + return 0; + } + $source_version .= '#unknown-commit'; + } + else { + $source_version .= '#' . trim($git_get_commit->getOutput()); + } + } + $info['generator'] = "$source_theme_name:$source_version"; + if ($description = $input->getOption('description')) { $info['description'] = $description; } diff --git a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php index 054e174886fc1c799f830aa3696211a23a98f04d..792c707e1e209c8b67a08e834b8cecf1830d9a95 100644 --- a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php +++ b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php @@ -40,9 +40,12 @@ protected function setUp(): void { } /** - * Tests the generate-theme command. + * Generates PHP process to generate a theme from core's starterkit theme. + * + * @return \Symfony\Component\Process\Process + * The PHP process */ - public function test() { + private function generateThemeFromStarterkit() : Process { $install_command = [ $this->php, 'core/scripts/drupal', @@ -53,15 +56,48 @@ public function test() { ]; $process = new Process($install_command, NULL); $process->setTimeout(60); + return $process; + } + + /** + * Asserts the theme exists. Returns the parsed *.info.yml file. + * + * @param string $theme_path_relative + * The core-relative path to the theme. + * + * @return array + * The parsed *.info.yml file. + */ + private function assertThemeExists(string $theme_path_relative): array { + $theme_path_absolute = $this->getWorkspaceDirectory() . "/$theme_path_relative"; + $theme_name = basename($theme_path_relative); + $info_yml_filename = "$theme_name.info.yml"; + $this->assertFileExists($theme_path_absolute . '/' . $info_yml_filename); + $info = Yaml::decode(file_get_contents($theme_path_absolute . '/' . $info_yml_filename)); + return $info; + } + + /** + * Tests the generate-theme command. + */ + public function test() { + // Do not rely on \Drupal::VERSION: change the version to a concrete version + // number, to simulate using a tagged core release. + $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml'; + $info = Yaml::decode(file_get_contents($starterkit_info_yml)); + $info['version'] = '9.4.0'; + file_put_contents($starterkit_info_yml, Yaml::encode($info)); + + $process = $this->generateThemeFromStarterkit(); $result = $process->run(); $this->assertEquals('Theme generated successfully to themes/test_custom_theme', trim($process->getOutput()), $process->getErrorOutput()); $this->assertSame(0, $result); $theme_path_relative = 'themes/test_custom_theme'; - $theme_path_absolute = $this->getWorkspaceDirectory() . "/$theme_path_relative"; - $this->assertFileExists($theme_path_absolute . '/test_custom_theme.info.yml'); - $info = Yaml::decode(file_get_contents($theme_path_absolute . '/test_custom_theme.info.yml')); + $info = $this->assertThemeExists($theme_path_relative); self::assertArrayNotHasKey('hidden', $info); + self::assertArrayHasKey('generator', $info); + self::assertEquals('starterkit_theme:9.4.0', $info['generator']); // Ensure that the generated theme can be installed. $this->installQuickStart('minimal'); @@ -72,10 +108,12 @@ public function test() { $this->getMink()->getSession()->getPage()->clickLink('Install "Test custom starterkit theme" theme'); $this->getMink()->assertSession()->pageTextContains('The "Test custom starterkit theme" theme has been installed.'); + // Ensure that a new theme cannot be generated when the destination + // directory already exists. + $theme_path_absolute = $this->getWorkspaceDirectory() . "/$theme_path_relative"; $this->assertFileExists($theme_path_absolute . '/test_custom_theme.theme'); unlink($theme_path_absolute . '/test_custom_theme.theme'); - $process = new Process($install_command, NULL); - $process->setTimeout(60); + $process = $this->generateThemeFromStarterkit(); $result = $process->run(); $this->assertStringContainsString('Theme could not be generated because the destination directory', $process->getErrorOutput()); $this->assertStringContainsString($theme_path_relative, $process->getErrorOutput()); @@ -83,6 +121,145 @@ public function test() { $this->assertFileDoesNotExist($theme_path_absolute . '/test_custom_theme.theme'); } + /** + * Tests the generate-theme command on a dev snapshot of Drupal core. + */ + public function testDevSnapshot() { + // Do not rely on \Drupal::VERSION: change the version to a development + // snapshot version number, to simulate using a branch snapshot of core. + $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml'; + $info = Yaml::decode(file_get_contents($starterkit_info_yml)); + $info['version'] = '9.4.0-dev'; + file_put_contents($starterkit_info_yml, Yaml::encode($info)); + + $process = $this->generateThemeFromStarterkit(); + $result = $process->run(); + $this->assertEquals('Theme generated successfully to themes/test_custom_theme', trim($process->getOutput()), $process->getErrorOutput()); + $this->assertSame(0, $result); + + $theme_path_relative = 'themes/test_custom_theme'; + $info = $this->assertThemeExists($theme_path_relative); + self::assertArrayNotHasKey('hidden', $info); + self::assertArrayHasKey('generator', $info); + self::assertMatchesRegularExpression('/^starterkit_theme\:9.4.0-dev#[0-9a-f]+$/', $info['generator']); + } + + /** + * Tests the generate-theme command on a theme with a release version number. + */ + public function testContribStarterkit(): void { + // Change the version to a concrete version number, to simulate using a + // contrib theme as the starterkit. + $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml'; + $info = Yaml::decode(file_get_contents($starterkit_info_yml)); + $info['version'] = '1.20'; + file_put_contents($starterkit_info_yml, Yaml::encode($info)); + + $process = $this->generateThemeFromStarterkit(); + $result = $process->run(); + $this->assertEquals('Theme generated successfully to themes/test_custom_theme', trim($process->getOutput()), $process->getErrorOutput()); + $this->assertSame(0, $result); + $info = $this->assertThemeExists('themes/test_custom_theme'); + self::assertArrayNotHasKey('hidden', $info); + self::assertArrayHasKey('generator', $info); + self::assertEquals('starterkit_theme:1.20', $info['generator']); + } + + /** + * Tests the generate-theme command on a theme with a dev version number. + */ + public function testContribStarterkitDevSnapshot(): void { + // Change the version to a development snapshot version number, to simulate + // using a contrib theme as the starterkit. + $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml'; + $info = Yaml::decode(file_get_contents($starterkit_info_yml)); + $info['core_version_requirement'] = '*'; + $info['version'] = '7.x-dev'; + file_put_contents($starterkit_info_yml, Yaml::encode($info)); + + // Avoid the core git commit from being considered the source theme's: move + // it out of core. + Process::fromShellCommandline('mv core/themes/starterkit_theme themes/', $this->getWorkspaceDirectory())->run(); + + $process = $this->generateThemeFromStarterkit(); + $result = $process->run(); + $this->assertEquals("The source theme starterkit_theme has a development version number (7.x-dev). Because it is not a git checkout, a specific commit could not be identified. This makes tracking changes in the source theme difficult. Are you sure you want to continue? (yes/no) [yes]:\n > Theme generated successfully to themes/test_custom_theme", trim($process->getOutput()), $process->getErrorOutput()); + $this->assertSame(0, $result); + $info = $this->assertThemeExists('themes/test_custom_theme'); + self::assertArrayNotHasKey('hidden', $info); + self::assertArrayHasKey('generator', $info); + self::assertEquals('starterkit_theme:7.x-dev#unknown-commit', $info['generator']); + } + + /** + * Tests the generate-theme command on a theme with a dev version without git. + */ + public function testContribStarterkitDevSnapshotWithGitNotInstalled(): void { + // Change the version to a development snapshot version number, to simulate + // using a contrib theme as the starterkit. + $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml'; + $info = Yaml::decode(file_get_contents($starterkit_info_yml)); + $info['core_version_requirement'] = '*'; + $info['version'] = '7.x-dev'; + file_put_contents($starterkit_info_yml, Yaml::encode($info)); + + // Avoid the core git commit from being considered the source theme's: move + // it out of core. + Process::fromShellCommandline('mv core/themes/starterkit_theme themes/', $this->getWorkspaceDirectory())->run(); + + // Confirm that 'git' is available. + $output = []; + exec('git --help', $output, $status); + $this->assertEquals(0, $status); + // Modify our $PATH so that it begins with a path that contains an + // executable script named 'git' that always exits with 127, as if git were + // not found. Note that we run our tests using process isolation, so we do + // not need to restore the PATH when we are done. + $unavailableGitPath = $this->getWorkspaceDirectory() . '/bin'; + mkdir($unavailableGitPath); + $bash = <<<SH +#!/bin/bash +exit 127 + +SH; + file_put_contents($unavailableGitPath . '/git', $bash); + chmod($unavailableGitPath . '/git', 0755); + $oldPath = getenv('PATH'); + putenv('PATH=' . $unavailableGitPath . ':' . getenv('PATH')); + // Confirm that 'git' is no longer available. + $output = []; + exec('git --help', $output, $status); + $this->assertEquals(127, $status); + + $process = $this->generateThemeFromStarterkit(); + $result = $process->run(); + $this->assertEquals("[ERROR] The source theme starterkit_theme has a development version number \n (7.x-dev). Determining a specific commit is not possible because git is\n not installed. Either install git or use a tagged release to generate a\n theme.", trim($process->getOutput()), $process->getErrorOutput()); + $this->assertSame(1, $result); + $this->assertFileDoesNotExist($this->getWorkspaceDirectory() . "/themes/test_custom_theme"); + + putenv('PATH=' . $oldPath . ':' . getenv('PATH')); + } + + /** + * Tests the generate-theme command on a theme without a version number. + */ + public function testCustomStarterkit(): void { + // Omit the version, to simulate using a custom theme as the starterkit. + $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml'; + $info = Yaml::decode(file_get_contents($starterkit_info_yml)); + unset($info['version']); + file_put_contents($starterkit_info_yml, Yaml::encode($info)); + + $process = $this->generateThemeFromStarterkit(); + $result = $process->run(); + $this->assertEquals("The source theme starterkit_theme does not have a version specified. This makes tracking changes in the source theme difficult. Are you sure you want to continue? (yes/no) [yes]:\n > Theme generated successfully to themes/test_custom_theme", trim($process->getOutput()), $process->getErrorOutput()); + $this->assertSame(0, $result); + $info = $this->assertThemeExists('themes/test_custom_theme'); + self::assertArrayNotHasKey('hidden', $info); + self::assertArrayHasKey('generator', $info); + self::assertEquals('starterkit_theme:unknown-version', $info['generator']); + } + /** * Tests themes that do not exist return an error. */ diff --git a/core/themes/starterkit_theme/starterkit_theme.info.yml b/core/themes/starterkit_theme/starterkit_theme.info.yml index b1673d73f6833d807e34d31d30b9014adf4ee1b3..aa46cbc9b089b9a52150ef4e8d9f97f2d8233345 100644 --- a/core/themes/starterkit_theme/starterkit_theme.info.yml +++ b/core/themes/starterkit_theme/starterkit_theme.info.yml @@ -3,6 +3,7 @@ type: theme 'base theme': stable9 hidden: true starterkit: true +version: VERSION libraries: - starterkit_theme/base - starterkit_theme/messages