Commit b607eb2a authored by larowlan's avatar larowlan

Issue #2952888 by alexpott, dawehner, phenaproxima: Allow install profiles to require modules

parent 5a6f2954
......@@ -1089,7 +1089,7 @@ function install_base_system(&$install_state) {
// Save the list of other modules to install for the upcoming tasks.
// State can be set to the database now that system.module is installed.
$modules = $install_state['profile_info']['dependencies'];
$modules = $install_state['profile_info']['install'];
\Drupal::state()->set('install_profile_modules', array_diff($modules, ['system']));
$install_state['base_system_verified'] = TRUE;
......
......@@ -574,7 +574,7 @@ function drupal_verify_profile($install_state) {
$present_modules[] = $profile;
// Verify that all of the profile's required modules are present.
$missing_modules = array_diff($info['dependencies'], $present_modules);
$missing_modules = array_diff($info['install'], $present_modules);
$requirements = [];
......@@ -957,7 +957,7 @@ function drupal_check_profile($profile) {
$drupal_root = \Drupal::root();
$module_list = (new ExtensionDiscovery($drupal_root))->scan('module');
foreach ($info['dependencies'] as $module) {
foreach ($info['install'] as $module) {
// If the module is in the module list we know it exists and we can continue
// including and registering it.
// @see \Drupal\Core\Extension\ExtensionDiscovery::scanDirectory()
......@@ -1039,6 +1039,8 @@ function drupal_check_module($module) {
* - description: A brief description of the profile.
* - dependencies: An array of shortnames of other modules that this install
* profile requires.
* - install: An array of shortname of other modules to install that are not
* required by this install profile.
*
* Additional, less commonly-used information that can appear in a
* profile.info.yml file but not in a normal Drupal module .info.yml file
......@@ -1067,7 +1069,7 @@ function drupal_check_module($module) {
* @code
* name: Minimal
* description: Start fresh, with only a few modules enabled.
* dependencies:
* install:
* - block
* - dblog
* @endcode
......@@ -1087,6 +1089,7 @@ function install_profile_info($profile, $langcode = 'en') {
// Set defaults for module info.
$defaults = [
'dependencies' => [],
'install' => [],
'themes' => ['stark'],
'description' => '',
'version' => NULL,
......@@ -1103,7 +1106,9 @@ function install_profile_info($profile, $langcode = 'en') {
$locale = !empty($langcode) && $langcode != 'en' ? ['locale'] : [];
$info['dependencies'] = array_unique(array_merge($required, $info['dependencies'], $locale));
// Merge dependencies, required modules and locale into install list and
// remove any duplicates.
$info['install'] = array_unique(array_merge($info['install'], $required, $info['dependencies'], $locale));
$cache[$profile][$langcode] = $info;
}
......
......@@ -31,6 +31,24 @@ public function parse($filename) {
if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
$parsed_info['version'] = \Drupal::VERSION;
}
// Special backwards compatible handling profiles and their 'dependencies'
// key.
if ($parsed_info['type'] === 'profile' && isset($parsed_info['dependencies']) && !array_key_exists('install', $parsed_info)) {
// Only trigger the deprecation message if we are actually using the
// profile with the missing 'install' key. This avoids triggering the
// deprecation when scanning all the available install profiles.
global $install_state;
if (isset($install_state['parameters']['profile'])) {
$pattern = '@' . preg_quote(DIRECTORY_SEPARATOR . $install_state['parameters']['profile'] . '.info.yml') . '$@';
if (preg_match($pattern, $filename)) {
@trigger_error("The install profile $filename only implements a 'dependencies' key. As of Drupal 8.6.0 profile's support a new 'install' key for modules that should be installed but not depended on. See https://www.drupal.org/node/2952947.", E_USER_DEPRECATED);
}
}
// Move dependencies to install so that if a profile has both
// dependencies and install then dependencies are real.
$parsed_info['install'] = $parsed_info['dependencies'];
$parsed_info['dependencies'] = [];
}
}
return $parsed_info;
}
......
......@@ -47,6 +47,9 @@ interface InfoParserInterface {
*
* See bartik.info.yml for an example of a theme .info.yml file.
*
* For information stored in a profile .info.yml file see
* install_profile_info().
*
* @param string $filename
* The file we are parsing. Accepts file with relative or absolute path.
*
......@@ -56,6 +59,8 @@ interface InfoParserInterface {
* @throws \Drupal\Core\Extension\InfoParserException
* Exception thrown if there is a parsing error or the .info.yml file does
* not contain a required key.
*
* @see install_profile_info()
*/
public function parse($filename);
......
......@@ -350,7 +350,6 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
if ($uninstall_dependents) {
// Add dependent modules to the list. The new modules will be processed as
// the foreach loop continues.
$profile = drupal_get_profile();
foreach ($module_list as $module => $value) {
foreach (array_keys($module_data[$module]->required_by) as $dependent) {
if (!isset($module_data[$dependent])) {
......@@ -359,7 +358,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
}
// Skip already uninstalled modules.
if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent])) {
$module_list[$dependent] = $dependent;
}
}
......
......@@ -12,6 +12,7 @@ class ProfileExtensionList extends ExtensionList {
*/
protected $defaults = [
'dependencies' => [],
'install' => [],
'description' => '',
'package' => 'Other',
'version' => NULL,
......
......@@ -116,8 +116,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
return $form;
}
$profile = drupal_get_profile();
// Sort all modules by their name.
uasort($uninstallable, 'system_sort_modules_by_info_name');
$validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable));
......@@ -142,10 +140,9 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['uninstall'][$module->getName()]['#disabled'] = TRUE;
}
// All modules which depend on this one must be uninstalled first, before
// we can allow this module to be uninstalled. (The installation profile
// is excluded from this list.)
// we can allow this module to be uninstalled.
foreach (array_keys($module->required_by) as $dependent) {
if ($dependent != $profile && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) {
if (drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) {
$name = isset($modules[$dependent]->info['name']) ? $modules[$dependent]->info['name'] : $dependent;
$form['modules'][$module->getName()]['#required_by'][] = $name;
$form['uninstall'][$module->getName()]['#disabled'] = TRUE;
......
......@@ -170,20 +170,26 @@ public function testDependencyResolution() {
/**
* Tests uninstalling a module that is a "dependency" of a profile.
*
* Note this test does not trigger the deprecation error because of static
* caching in \Drupal\Core\Extension\InfoParser::parse().
*
* @group legacy
*/
public function testUninstallProfileDependency() {
$profile = 'testing';
$dependency = 'page_cache';
public function testUninstallProfileDependencyBC() {
$profile = 'testing_install_profile_dependencies_bc';
$dependency = 'dblog';
$this->setSetting('install_profile', $profile);
// Prime the drupal_get_filename() static cache with the location of the
// minimal profile as it is not the currently active profile and we don't
// testing profile as it is not the currently active profile and we don't
// yet have any cached way to retrieve its location.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('profile', $profile, 'core/profiles/' . $profile . '/' . $profile . '.info.yml');
$this->enableModules(['module_test', $profile]);
$data = \Drupal::service('extension.list.module')->reset()->getList();
$this->assertTrue(isset($data[$profile]->requires[$dependency]));
$this->assertFalse(isset($data[$profile]->requires[$dependency]));
$this->assertContains($dependency, $data[$profile]->info['install']);
$this->moduleInstaller()->install([$dependency]);
$this->assertTrue($this->moduleHandler()->moduleExists($dependency));
......@@ -200,6 +206,76 @@ public function testUninstallProfileDependency() {
$this->assertFalse(in_array($profile, $uninstalled_modules), 'The installation profile is not in the list of uninstalled modules.');
}
/**
* Tests uninstalling a module installed by a profile.
*/
public function testUninstallProfileDependency() {
$profile = 'testing_install_profile_dependencies';
$dependency = 'dblog';
$non_dependency = 'ban';
$this->setSetting('install_profile', $profile);
// Prime the drupal_get_filename() static cache with the location of the
// testing_install_profile_dependencies profile as it is not the currently
// active profile and we don't yet have any cached way to retrieve its
// location.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('profile', $profile, 'core/profiles/' . $profile . '/' . $profile . '.info.yml');
$this->enableModules(['module_test', $profile]);
$data = \Drupal::service('extension.list.module')->reset()->getList();
$this->assertArrayHasKey($dependency, $data[$profile]->requires);
$this->assertArrayNotHasKey($non_dependency, $data[$profile]->requires);
$this->moduleInstaller()->install([$dependency, $non_dependency]);
$this->assertTrue($this->moduleHandler()->moduleExists($dependency));
// Uninstall the profile module that is not a dependent.
$result = $this->moduleInstaller()->uninstall([$non_dependency]);
$this->assertTrue($result, 'ModuleInstaller::uninstall() returns TRUE.');
$this->assertFalse($this->moduleHandler()->moduleExists($non_dependency));
$this->assertEquals(drupal_get_installed_schema_version($non_dependency), SCHEMA_UNINSTALLED, "$dependency module was uninstalled.");
// Verify that the installation profile itself was not uninstalled.
$uninstalled_modules = \Drupal::state()->get('module_test.uninstall_order') ?: [];
$this->assertContains($non_dependency, $uninstalled_modules, "$dependency module is in the list of uninstalled modules.");
$this->assertNotContains($profile, $uninstalled_modules, 'The installation profile is not in the list of uninstalled modules.');
// Try uninstalling the required module.
$this->setExpectedException(ModuleUninstallValidatorException::class, 'The following reasons prevent the modules from being uninstalled: The Testing install profile dependencies module is required');
$this->moduleInstaller()->uninstall([$dependency]);
}
/**
* Tests that a profile can supply only real dependencies
*/
public function testProfileAllDependencies() {
$profile = 'testing_install_profile_all_dependencies';
$dependencies = ['dblog', 'ban'];
$this->setSetting('install_profile', $profile);
// Prime the drupal_get_filename() static cache with the location of the
// testing_install_profile_dependencies profile as it is not the currently
// active profile and we don't yet have any cached way to retrieve its
// location.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('profile', $profile, 'core/profiles/' . $profile . '/' . $profile . '.info.yml');
$this->enableModules(['module_test', $profile]);
$data = \Drupal::service('extension.list.module')->reset()->getList();
foreach ($dependencies as $dependency) {
$this->assertArrayHasKey($dependency, $data[$profile]->requires);
}
$this->moduleInstaller()->install($dependencies);
foreach ($dependencies as $dependency) {
$this->assertTrue($this->moduleHandler()->moduleExists($dependency));
}
// Try uninstalling the dependencies.
$this->setExpectedException(ModuleUninstallValidatorException::class, 'The following reasons prevent the modules from being uninstalled: The Testing install profile all dependencies module is required');
$this->moduleInstaller()->uninstall($dependencies);
}
/**
* Tests uninstalling a module that has content.
*/
......
......@@ -3,7 +3,7 @@ type: profile
description: 'Install an example site that shows off some of Drupal’s capabilities.'
version: VERSION
core: 8.x
dependencies:
install:
- node
- history
- big_pipe
......
......@@ -3,7 +3,7 @@ type: profile
description: 'Build a custom site without pre-configured functionality. Suitable for advanced users.'
version: VERSION
core: 8.x
dependencies:
install:
- node
- block
- dblog
......
......@@ -3,7 +3,7 @@ type: profile
description: 'Install with commonly used features pre-configured.'
version: VERSION
core: 8.x
dependencies:
install:
- node
- history
- block
......
......@@ -4,7 +4,7 @@ description: 'Minimal profile for running tests. Includes absolutely required mo
version: VERSION
core: 8.x
hidden: true
dependencies:
install:
# Enable page_cache and dynamic_page_cache in testing, to ensure that as many
# tests as possible run with them enabled.
- page_cache
......
......@@ -4,7 +4,7 @@ description: 'Tests install profiles in the config importer.'
version: VERSION
core: 8.x
hidden: true
dependencies:
install:
- syslog
themes:
- stark
......@@ -4,7 +4,7 @@ description: 'Minimal profile for running tests with config overrides in a profi
version: VERSION
core: 8.x
hidden: true
dependencies:
install:
- action
- language
- tour
name: 'Testing install profile all dependencies'
type: profile
description: 'Profile for testing that install profiles can require a module.'
version: VERSION
core: 8.x
hidden: true
dependencies:
- ban
- dblog
install: []
themes:
- classy
name: 'Testing install profile dependencies'
type: profile
description: 'Profile for testing that install profiles can require a module.'
version: VERSION
core: 8.x
hidden: true
dependencies:
- dblog
install:
- ban
themes:
- classy
name: 'Testing install profile dependencies BC layer'
type: profile
description: 'Profile for testing BC layer for profile dependencies.'
version: VERSION
core: 8.x
hidden: true
dependencies:
- dblog
- ban
themes:
- classy
......@@ -4,7 +4,7 @@ description: 'Minimal profile for running a test when dependencies are listed bu
version: VERSION
core: 8.x
hidden: true
dependencies:
install:
- missing_module1
- missing_module2
keep_english: true
......@@ -4,6 +4,6 @@ description: 'Minimal profile for running tests with a multilingual installer.'
version: VERSION
core: 8.x
hidden: true
dependencies:
install:
- locale
- tour
......@@ -4,6 +4,6 @@ description: 'Minimal profile for running tests with a multilingual installer.'
version: VERSION
core: 8.x
hidden: true
dependencies:
install:
- locale
keep_english: true
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that an install profile with only dependencies works as expected.
*
* @group Installer
* @group legacy
*/
class InstallProfileDependenciesBcTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_install_profile_dependencies_bc';
/**
* Tests that the install profile BC layer for dependencies key works.
*
* @expectedDeprecation The install profile core/profiles/testing_install_profile_dependencies_bc/testing_install_profile_dependencies_bc.info.yml only implements a 'dependencies' key. As of Drupal 8.6.0 profile's support a new 'install' key for modules that should be installed but not depended on. See https://www.drupal.org/node/2952947.
*/
public function testUninstallingModules() {
$user = $this->drupalCreateUser(['administer modules']);
$this->drupalLogin($user);
$this->drupalGet('admin/modules/uninstall');
$this->getSession()->getPage()->checkField('uninstall[ban]');
$this->getSession()->getPage()->checkField('uninstall[dblog]');
$this->click('#edit-submit');
// Click the confirm button.
$this->click('#edit-submit');
$this->assertSession()->responseContains('The selected modules have been uninstalled.');
$this->assertSession()->responseContains('No modules are available to uninstall.');
// We've uninstalled modules therefore we need to rebuild the container in
// the test runner.
$this->rebuildContainer();
$module_handler = $this->container->get('module_handler');
$this->assertFalse($module_handler->moduleExists('ban'));
$this->assertFalse($module_handler->moduleExists('dblog'));
}
}
<?php
namespace Drupal\FunctionalTests\Installer;
use Drupal\Core\Extension\ModuleUninstallValidatorException;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that an install profile can require modules.
*
* @group Installer
*/
class InstallProfileDependenciesTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $profile = 'testing_install_profile_dependencies';
/**
* Tests that an install profile can require modules.
*/
public function testUninstallingModules() {
$user = $this->drupalCreateUser(['administer modules']);
$this->drupalLogin($user);
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->fieldDisabled('uninstall[dblog]');
$this->getSession()->getPage()->checkField('uninstall[ban]');
$this->click('#edit-submit');
// Click the confirm button.
$this->click('#edit-submit');
$this->assertSession()->responseContains('The selected modules have been uninstalled.');
// We've uninstalled a module therefore we need to rebuild the container in
// the test runner.
$this->rebuildContainer();
$this->assertFalse($this->container->get('module_handler')->moduleExists('ban'));
try {
$this->container->get('module_installer')->uninstall(['dblog']);
$this->fail('Uninstalled dblog module.');
}
catch (ModuleUninstallValidatorException $e) {
$this->assertContains('The Testing install profile dependencies module is required', $e->getMessage());
}
}
}
......@@ -52,8 +52,8 @@ public function testInstallerTranslationCache() {
$info_en = install_profile_info('testing', 'en');
$info_nl = install_profile_info('testing', 'nl');
$this->assertFalse(in_array('locale', $info_en['dependencies']), 'Locale is not set when installing in English.');
$this->assertTrue(in_array('locale', $info_nl['dependencies']), 'Locale is set when installing in Dutch.');
$this->assertNotContains('locale', $info_en['install'], 'Locale is not set when installing in English.');
$this->assertContains('locale', $info_nl['install'], 'Locale is set when installing in Dutch.');
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment