diff --git a/core/includes/update.inc b/core/includes/update.inc
index 2021a662..d0af324c 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -9,6 +9,7 @@
*/
use Drupal\Component\Graph\Graph;
+use Drupal\Component\Version\DrupalSemver;
use Drupal\Core\Update\UpdateKernel;
use Drupal\Core\Utility\Error;
@@ -59,7 +60,7 @@ function update_check_incompatibility($name, $type = 'module') {
}
if (!isset($file)
|| !isset($file->info['core'])
- || $file->info['core'] != \Drupal::CORE_COMPATIBILITY
+ || !DrupalSemver::satisfies(\Drupal::VERSION, $file->info['core'])
|| version_compare(phpversion(), $file->info['php']) < 0) {
return TRUE;
}
diff --git a/core/lib/Drupal/Component/Version/DrupalSemver.php b/core/lib/Drupal/Component/Version/DrupalSemver.php
new file mode 100644
index 00000000..74cdd7be
--- /dev/null
+++ b/core/lib/Drupal/Component/Version/DrupalSemver.php
@@ -0,0 +1,37 @@
+status)) {
// Ensure this theme is compatible with this version of core.
- $theme->incompatible_core = !isset($theme->info['core']) || ($theme->info['core'] != \DRUPAL::CORE_COMPATIBILITY);
+ $theme->incompatible_core = !isset($theme->info['core']) || !DrupalSemver::satisfies(\Drupal::VERSION, $theme->info['core']);
// Require the 'content' region to make sure the main page
// content has a common place in all themes.
$theme->incompatible_region = !isset($theme->info['regions']['content']);
diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php
index c2ade11b..5c5b9268 100644
--- a/core/modules/system/src/Form/ModulesListForm.php
+++ b/core/modules/system/src/Form/ModulesListForm.php
@@ -3,6 +3,7 @@
namespace Drupal\system\Form;
use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Version\DrupalSemver;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Access\AccessManagerInterface;
@@ -281,10 +282,15 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
$reasons = [];
// Check the core compatibility.
- if ($module->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+ if (!DrupalSemver::satisfies(\Drupal::VERSION, $module->info['core'])) {
$compatible = FALSE;
$reasons[] = $this->t('This version is not compatible with Drupal @core_version and should be replaced.', [
- '@core_version' => \Drupal::CORE_COMPATIBILITY,
+ '@core_version' => \Drupal::VERSION,
+ ]);
+ $row['#requires']['core'] = $this->t('Drupal Core (@core_requirement) (incompatible with version @core_version)', [
+ '@module' => $module->getName(),
+ '@core_requirement' => $module->info['core'],
+ '@core_version' => \Drupal::VERSION,
]);
}
@@ -328,7 +334,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) {
}
// Disable the checkbox if the dependency is incompatible with this
// version of Drupal core.
- elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
+ elseif (!DrupalSemver::satisfies(\Drupal::VERSION, $modules[$dependency]->info['core'])) {
$row['#requires'][$dependency] = $this->t('@module (incompatible with this version of Drupal core)', [
'@module' => $name,
]);
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index 799869bb..1cfc4a24 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -293,7 +293,7 @@ function template_preprocess_system_themes_page(&$variables) {
// Make sure to provide feedback on compatibility.
$current_theme['incompatible'] = '';
if (!empty($theme->incompatible_core)) {
- $current_theme['incompatible'] = t("This theme is not compatible with Drupal @core_version. Check that the .info.yml file contains the correct 'core' value.", ['@core_version' => \Drupal::CORE_COMPATIBILITY]);
+ $current_theme['incompatible'] = t("This theme is not compatible with Drupal @core_version. Check that the .info.yml file contains a compatible 'core' value.", ['@core_version' => \Drupal::VERSION, '@core_constraint' => $theme->info['core']]);
}
elseif (!empty($theme->incompatible_region)) {
$current_theme['incompatible'] = t("This theme is missing a 'content' region.");
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php
new file mode 100644
index 00000000..66336095
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php
@@ -0,0 +1,38 @@
+merge('key_value')
+ ->condition('collection', 'system.schema')
+ ->condition('name', 'update_test_semver_update_n')
+ ->fields([
+ 'collection' => 'system.schema',
+ 'name' => 'update_test_semver_update_n',
+ 'value' => 'i:8000;',
+ ])
+ ->execute();
+
+// Update core.extension.
+$extensions = $connection->select('config')
+ ->fields('config', ['data'])
+ ->condition('collection', '')
+ ->condition('name', 'core.extension')
+ ->execute()
+ ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['update_test_semver_update_n'] = 8000;
+$connection->update('config')
+ ->fields([
+ 'data' => serialize($extensions),
+ ])
+ ->condition('collection', '')
+ ->condition('name', 'core.extension')
+ ->execute();
diff --git a/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml b/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml
new file mode 100644
index 00000000..8063e099
--- /dev/null
+++ b/core/modules/system/tests/modules/system_core_semver_test/system_core_semver_test.info.yml
@@ -0,0 +1,6 @@
+name: 'System core ^8 version test'
+type: module
+description: 'Support module for testing core using semver.'
+package: Testing
+version: 1.0.0
+core: ^8
diff --git a/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_99x.info.yml b/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_99x.info.yml
new file mode 100644
index 00000000..449c61b3
--- /dev/null
+++ b/core/modules/system/tests/modules/system_incompatible_core_version_test_9x/system_incompatible_core_version_test_99x.info.yml
@@ -0,0 +1,6 @@
+name: 'System incompatible core 99.x version test'
+type: module
+description: 'Support module for testing system core incompatibility.'
+package: Testing
+version: 1.0.0
+core: 99.x
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index affc20ea..e90b54a8 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -62,6 +62,10 @@ function system_test_system_info_alter(&$info, Extension $file, $type) {
}
}
+ if (($core_requirement = \Drupal::state()->get('dependency_test.core_version_requirement')) && $file->getName() === 'common_test') {
+ $info['core'] = $core_requirement;
+ }
+
// Make the system_dependencies_test visible by default.
if ($file->getName() == 'system_dependencies_test') {
$info['hidden'] = FALSE;
@@ -71,6 +75,7 @@ function system_test_system_info_alter(&$info, Extension $file, $type) {
'system_incompatible_core_version_dependencies_test',
'system_incompatible_module_version_test',
'system_incompatible_core_version_test',
+ 'system_incompatible_core_version_test_99x',
])) {
$info['hidden'] = FALSE;
}
diff --git a/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml
new file mode 100644
index 00000000..5465a62d
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.info.yml
@@ -0,0 +1,6 @@
+name: 'Update test hook_update_n semver'
+type: module
+description: 'Support module for update testing with core semver value.'
+package: Testing
+version: VERSION
+core: ^8
diff --git a/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install
new file mode 100644
index 00000000..d00ff0cf
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_semver_update_n/update_test_semver_update_n.install
@@ -0,0 +1,13 @@
+set('update_test_semver_update_n_update_8001', 'Yes, I was run. Thanks for testing!');
+}
diff --git a/core/modules/system/tests/src/Functional/Module/DependencyTest.php b/core/modules/system/tests/src/Functional/Module/DependencyTest.php
index d2938721..d8e17cf8 100644
--- a/core/modules/system/tests/src/Functional/Module/DependencyTest.php
+++ b/core/modules/system/tests/src/Functional/Module/DependencyTest.php
@@ -102,6 +102,50 @@ public function testIncompatiblePhpVersionDependency() {
$this->assert(count($checkbox) == 1, 'Checkbox for the module is disabled.');
}
+ /**
+ * Tests enabling modules with different core version specifications.
+ */
+ public function testCoreVersionDependency() {
+ $assert_session = $this->assertSession();
+ list($major, $minor) = explode('.', \Drupal::VERSION);
+
+ $next_minor = $minor + 1;
+ $next_major = $major + 1;
+
+ // Test the next minor release.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$next_minor");
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldDisabled('modules[system_incompatible_core_version_test_99x][enable]');
+ $assert_session->fieldDisabled('modules[common_test][enable]');
+
+ // Test either current major or the next one.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "^$major || ^$next_major");
+ $this->drupalGet('admin/modules');
+ $this->assertFalse($assert_session->elementExists('css', '[name="modules[common_test][enable]"]')->hasAttribute('disabled'));
+
+ // Test either a previous major or the next one.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "^1 || ^$next_major");
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldDisabled('modules[common_test][enable]');
+
+ // Test an invalid major.
+ \Drupal::state()->set('dependency_test.core_version_requirement', 'this-string-is-invalid');
+ $this->drupalGet('admin/modules');
+ $assert_session->fieldDisabled('modules[common_test][enable]');
+
+ // Test the current minor.
+ \Drupal::state()->set('dependency_test.core_version_requirement', "~$major.$minor");
+ $this->drupalGet('admin/modules');
+ $this->assertFalse($assert_session->elementExists('css', '[name="modules[common_test][enable]"]')->hasAttribute('disabled'));
+ $this->assertFalse($assert_session->elementExists('css', '[name="modules[system_core_semver_test][enable]"]')->hasAttribute('disabled'));
+
+ // Ensure the modules can actually be installed.
+ $edit['modules[common_test][enable]'] = 'common_test';
+ $edit['modules[system_core_semver_test][enable]'] = 'system_core_semver_test';
+ $this->drupalPostForm('admin/modules', $edit, t('Install'));
+ $this->assertModules(['common_test', 'system_core_semver_test'], TRUE);
+ }
+
/**
* Tests enabling a module that depends on a module which fails hook_requirements().
*/
diff --git a/core/modules/system/tests/src/Functional/System/ThemeTest.php b/core/modules/system/tests/src/Functional/System/ThemeTest.php
index 1c69d7a3..a40bf0f4 100644
--- a/core/modules/system/tests/src/Functional/System/ThemeTest.php
+++ b/core/modules/system/tests/src/Functional/System/ThemeTest.php
@@ -366,8 +366,11 @@ public function testInvalidTheme() {
$this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', ['@base_theme' => 'not_real_test_basetheme']));
$this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', ['@base_theme' => 'test_invalid_basetheme']));
$this->assertText(t('This theme requires the theme engine @theme_engine to operate correctly.', ['@theme_engine' => 'not_real_engine']));
- // Check for the error text of a theme with the wrong core version.
- $this->assertText("This theme is not compatible with Drupal 8.x. Check that the .info.yml file contains the correct 'core' value.");
+ // Check for the error text of a theme with the wrong core version
+ // using 7.x and ^7.
+ $incompatible_core_message = 'This theme is not compatible with Drupal ' . \Drupal::VERSION . ". Check that the .info.yml file contains a compatible 'core' value.";
+ $this->assertThemeIncompatibleText('Theme test with invalid core version', $incompatible_core_message);
+ $this->assertThemeIncompatibleText('Theme test with invalid semver core version', $incompatible_core_message);
// Check for the error text of a theme without a content region.
$this->assertText("This theme is missing a 'content' region.");
}
@@ -436,24 +439,29 @@ public function testUninstallingThemes() {
* Tests installing a theme and setting it as default.
*/
public function testInstallAndSetAsDefault() {
- $this->drupalGet('admin/appearance');
- // Bartik is uninstalled in the test profile and has the third "Install and
- // set as default" link.
- $this->clickLink(t('Install and set as default'), 2);
- // Test the confirmation message.
- $this->assertText('Bartik is now the default theme.');
- // Make sure Bartik is now set as the default theme in config.
- $this->assertEqual($this->config('system.theme')->get('default'), 'bartik');
-
- // This checks for a regression. See https://www.drupal.org/node/2498691.
- $this->assertNoText('The bartik theme was not found.');
-
- $themes = \Drupal::service('theme_handler')->rebuildThemeData();
- $version = $themes['bartik']->info['version'];
+ $themes = [
+ 'bartik' => 'Bartik',
+ 'test_core_semver' => 'Theme test with semver core version',
+ ];
+ foreach ($themes as $theme_machine_name => $theme_name) {
+ $this->drupalGet('admin/appearance');
+ $this->getSession()->getPage()->findLink("Install $theme_name as default theme")->click();
+ // Test the confirmation message.
+ $this->assertText("$theme_name is now the default theme.");
+ // Make sure Bartik is now set as the default theme in config.
+ $this->assertEqual($this->config('system.theme')->get('default'), $theme_machine_name);
+
+ // This checks for a regression. See https://www.drupal.org/node/2498691.
+ $this->assertNoText("The $theme_machine_name theme was not found.");
+
+ $themes = \Drupal::service('theme_handler')->rebuildThemeData();
+ $version = $themes[$theme_machine_name]->info['version'];
+
+ // Confirm Bartik is indicated as the default theme.
+ $out = $this->getSession()->getPage()->getContent();
+ $this->assertTrue((bool) preg_match("/$theme_name " . preg_quote($version) . '\s{2,}\(default theme\)/', $out));
+ }
- // Confirm Bartik is indicated as the default theme.
- $out = $this->getSession()->getPage()->getContent();
- $this->assertTrue((bool) preg_match('/Bartik ' . preg_quote($version) . '\s{2,}\(default theme\)/', $out));
}
/**
@@ -469,4 +477,16 @@ public function testThemeSettingsNoLogoNoFavicon() {
$this->assertText('The configuration options have been saved.');
}
+ /**
+ * Asserts that expected incompatibility text is displayed for a theme.
+ *
+ * @param string $theme_name
+ * Theme name to select element on page. This can be a partial name.
+ * @param $expected_text
+ * The expected incompatibility text.
+ */
+ private function assertThemeIncompatibleText($theme_name, $expected_text) {
+ $this->assertSession()->elementExists('css', ".theme-info:contains(\"$theme_name\") .incompatible:contains(\"$expected_text\")");
+ }
+
}
diff --git a/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml b/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml
new file mode 100644
index 00000000..2ba1eb76
--- /dev/null
+++ b/core/modules/system/tests/themes/test_core_semver/test_core_semver.info.yml
@@ -0,0 +1,5 @@
+name: 'Theme test with semver core version'
+type: theme
+description: 'Test theme which has semver core version.'
+version: VERSION
+core: ^8 || ^9
diff --git a/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml b/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml
new file mode 100644
index 00000000..f349cfc0
--- /dev/null
+++ b/core/modules/system/tests/themes/test_invalid_core_semver/test_invalid_core_semver.info.yml
@@ -0,0 +1,5 @@
+name: 'Theme test with invalid semver core version'
+type: theme
+description: 'Test theme which has an invalid semver core version.'
+version: VERSION
+core: ^7
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index c783d7e8..94d433de 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -11,6 +11,7 @@
* ability to install contributed modules and themes via an user interface.
*/
+use Drupal\Component\Version\DrupalSemver;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
@@ -696,7 +697,7 @@ function update_verify_update_archive($project, $archive_file, $directory) {
$info = \Drupal::service('info_parser')->parse($file->uri);
// If the module or theme is incompatible with Drupal core, set an error.
- if (empty($info['core']) || $info['core'] != \Drupal::CORE_COMPATIBILITY) {
+ if (empty($info['core']) || !DrupalSemver::satisfies(\Drupal::VERSION, $info['core'])) {
$incompatible[] = !empty($info['name']) ? $info['name'] : t('Unknown');
}
else {
@@ -716,7 +717,7 @@ function update_verify_update_archive($project, $archive_file, $directory) {
'%archive_file contains a version of %names that is not compatible with Drupal @version.',
'%archive_file contains versions of modules or themes that are not compatible with Drupal @version: %names',
[
- '@version' => \Drupal::CORE_COMPATIBILITY,
+ '@version' => \Drupal::VERSION,
'%archive_file' => $file_system->basename($archive_file),
'%names' => implode(', ', $incompatible),
]
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
index 942cb886..504eb96a 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBaseTest.php
@@ -26,6 +26,7 @@ protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-schema-enabled.php',
+ __DIR__ . '/../../../../modules/system/tests/fixtures/update/drupal-8.update-test-semver-update-n-enabled.php',
];
}
@@ -99,8 +100,11 @@ public function testUpdateHookN() {
// Ensure schema has changed.
$this->assertEqual(drupal_get_installed_schema_version('update_test_schema', TRUE), 8001);
+ $this->assertEqual(drupal_get_installed_schema_version('update_test_semver_update_n', TRUE), 8001);
// Ensure the index was added for column a.
$this->assertTrue($connection->schema()->indexExists('update_test_schema_table', 'test'), 'Version 8001 of the update_test_schema module is installed.');
+ // Ensure update_test_semver_update_n_update_8001 was run.
+ $this->assertEquals(\Drupal::state()->get('update_test_semver_update_n_update_8001'), 'Yes, I was run. Thanks for testing!');
}
/**
diff --git a/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php
new file mode 100644
index 00000000..44285eab
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Version/DrupalSemverTest.php
@@ -0,0 +1,57 @@
+assertSame($result, DrupalSemver::satisfies($version, $constraints));
+ }
+
+ public function providerSatisfies() {
+ $cases = [
+ ['8.8.0-dev', '8.x', TRUE],
+ ['8.8.0', '8.x', TRUE],
+ ['8.9.9', '8.x', TRUE],
+ ['8.0.0', '8.x', TRUE],
+ ['9.0.0', '8.x', FALSE],
+ ['9.1.0', '8.x', FALSE],
+ ['8.8.0', '~8', TRUE],
+ ['8.8.0', '^8', TRUE],
+ ['8.7.0', '^8.7.6', FALSE],
+ ['8.7.6', '^8.7.6', TRUE],
+ ['8.7.8', '^8.7.6', TRUE],
+ ['9.0.0', '^8.7.6', FALSE],
+ ['8.0.0', '^8 || ^9', TRUE],
+ ['9.1.1', '^8 || ^9', TRUE],
+ ['9.1.1', '^8.7.6 || ^9', TRUE],
+ ['8.7.8', '^8.7.6 || ^9', TRUE],
+ ['8.6.8', '^8.7.6 || ^9', FALSE],
+ ['8.6.8', '^9', FALSE],
+ ['9.1.1', '^9', TRUE],
+ ['9.0.0', '9.x', TRUE],
+ ['8.8.0', '9.x', FALSE],
+ ['8.8.0', '7.x', FALSE],
+ ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '9.x', FALSE],
+ ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '7.x', FALSE],
+ ['a-super-nonsense-string-will-not-throw-an-exception-but-also-will-not-work', '8.x', FALSE],
+ ];
+ $tests = [];
+ foreach ($cases as $case) {
+ $tests[$case[0] . ":" . $case[1]] = $case;
+ }
+ return $tests;
+ }
+
+}