diff --git a/core/core.services.yml b/core/core.services.yml
index 582f0bd907..64c445fffb 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -304,7 +304,7 @@ services:
- { name: event_subscriber }
config.installer:
class: Drupal\Core\Config\ConfigInstaller
- arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '%install_profile%']
+ arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '%install_profile%', '@extension.list.profile']
lazy: true
config.storage:
class: Drupal\Core\Config\CachedStorage
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 907200a8f3..ed51a3d078 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -468,6 +468,12 @@ function install_begin_request($class_loader, &$install_state) {
if (isset($install_state['profile_info']['distribution']['install']['theme'])) {
$install_state['theme'] = $install_state['profile_info']['distribution']['install']['theme'];
}
+ // Ensure all profile directories are registered.
+ $profiles = \Drupal::service('extension.list.profile')->getAncestors($profile);
+ $profile_directories = array_map(function($extension) {
+ return $extension->getPath();
+ }, $profiles);
+ $listing->setProfileDirectories($profile_directories);
}
// Before having installed the system module and being able to do a module
@@ -1263,7 +1269,9 @@ function install_select_profile(&$install_state) {
* - For interactive installations via request query parameters.
* - For non-interactive installations via install_drupal() settings.
* - One of the available profiles is a distribution. If multiple profiles are
- * distributions, then the first discovered profile will be selected.
+ * distributions, then the first discovered profile will be selected. If an
+ * inherited profile is detected that is a distribution, it will be chosen
+ * over its base profile.
* - Only one visible profile is available.
*
* @param array $install_state
@@ -1288,12 +1296,9 @@ function _install_select_profile(&$install_state) {
return $profile;
}
}
- // If any of the profiles are distribution profiles, return the first one.
- foreach ($install_state['profiles'] as $profile) {
- $profile_info = install_profile_info($profile->getName());
- if (!empty($profile_info['distribution'])) {
- return $profile->getName();
- }
+ // Check for a distribution profile.
+ if ($distribution = \Drupal::service('extension.list.profile')->selectDistribution(array_keys($install_state['profiles']))) {
+ return $distribution;
}
// Get all visible (not hidden) profiles.
$visible_profiles = array_filter($install_state['profiles'], function ($profile) {
@@ -1642,7 +1647,9 @@ function install_profile_themes(&$install_state) {
* An array of information about the current installation state.
*/
function install_install_profile(&$install_state) {
- \Drupal::service('module_installer')->install([$install_state['parameters']['profile']], FALSE);
+ // Install all the profiles.
+ $profiles = \Drupal::service('extension.list.profile')->getAncestors();
+ \Drupal::service('module_installer')->install(array_keys($profiles), FALSE);
// Install all available optional config. During installation the module order
// is determined by dependencies. If there are no dependencies between modules
// then the order in which they are installed is dependent on random factors
diff --git a/core/includes/install.inc b/core/includes/install.inc
index 3f2f79b6fc..afe1719032 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -572,7 +572,6 @@ function install_ensure_config_directory($type) {
* The list of modules to install.
*/
function drupal_verify_profile($install_state) {
- $profile = $install_state['parameters']['profile'];
$info = $install_state['profile_info'];
// Get the list of available modules for the selected installation profile.
@@ -581,10 +580,11 @@ function drupal_verify_profile($install_state) {
foreach ($listing->scan('module') as $present_module) {
$present_modules[] = $present_module->getName();
}
-
- // The installation profile is also a module, which needs to be installed
- // after all the other dependencies have been installed.
- $present_modules[] = $profile;
+ // Get the list of available profiles, which may be used as base profiles or
+ // ancestors of the selected installation profile.
+ foreach ($listing->scan('profile') as $present_profile) {
+ $present_modules[] = $present_profile->getName();
+ }
// Verify that all of the profile's required modules are present.
$missing_modules = array_diff($info['install'], $present_modules);
@@ -1092,6 +1092,9 @@ function drupal_check_module($module) {
* Drupal's default installer theme.
* - finish_url: A destination to visit after the installation of the
* distribution is finished
+ * - base profile: The shortname of the base installation profile. Existence of
+ * this key denotes that the installation profile depends on a parent
+ * installation profile.
*
* Note that this function does an expensive file system scan to get info file
* information for dependencies. If you only need information from the info
@@ -1119,20 +1122,8 @@ function install_profile_info($profile, $langcode = 'en') {
static $cache = [];
if (!isset($cache[$profile][$langcode])) {
- // Set defaults for module info.
- $defaults = [
- 'dependencies' => [],
- 'install' => [],
- 'themes' => ['stark'],
- 'description' => '',
- 'version' => NULL,
- 'hidden' => FALSE,
- 'php' => DRUPAL_MINIMUM_PHP,
- 'config_install_path' => NULL,
- ];
$profile_path = drupal_get_path('profile', $profile);
- $info = \Drupal::service('info_parser')->parse("$profile_path/$profile.info.yml");
- $info += $defaults;
+ $info = \Drupal::service('extension.list.profile')->getExtensionInfo($profile);
$dependency_name_function = function ($dependency) {
return Dependency::createFromString($dependency)->getName();
diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php
index da188315eb..64e6eaa3b4 100644
--- a/core/lib/Drupal/Core/Config/ConfigInstaller.php
+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -4,6 +4,7 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
+use Drupal\Core\Extension\ProfileExtensionList;
use Drupal\Core\Installer\InstallerKernel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -51,6 +52,13 @@ class ConfigInstaller implements ConfigInstallerInterface {
*/
protected $sourceStorage;
+ /**
+ * The profile list.
+ *
+ * @var \Drupal\Core\Extension\ProfileExtensionList
+ */
+ protected $profileList;
+
/**
* Is configuration being created as part of a configuration sync.
*
@@ -80,14 +88,17 @@ class ConfigInstaller implements ConfigInstallerInterface {
* The event dispatcher.
* @param string $install_profile
* The name of the currently active installation profile.
+ * @param \Drupal\Core\Extension\ProfileExtensionList|null $profile_list
+ * (optional) The profile list.
*/
- public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile) {
+ public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile, ProfileExtensionList $profile_list = NULL) {
$this->configFactory = $config_factory;
$this->activeStorages[$active_storage->getCollectionName()] = $active_storage;
$this->typedConfig = $typed_config;
$this->configManager = $config_manager;
$this->eventDispatcher = $event_dispatcher;
$this->installProfile = $install_profile;
+ $this->profileList = $profile_list ?: \Drupal::service('extension.list.profile');
}
/**
@@ -511,7 +522,8 @@ public function checkConfigurationToInstall($type, $name) {
// Install profiles can not have config clashes. Configuration that
// has the same name as a module's configuration will be used instead.
- if ($name != $this->drupalGetProfile()) {
+ $profiles = $this->profileList->getAncestors($this->installProfile);
+ if (!isset($profiles[$name])) {
// Throw an exception if the module being installed contains configuration
// that already exists. Additionally, can not continue installing more
// modules because those may depend on the current module being installed.
diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
index 02bfacb49f..28a2167ca4 100644
--- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
+++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php
@@ -3,6 +3,8 @@
namespace Drupal\Core\Config;
use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Extension\ProfileExtensionList;
+use Drupal\Core\Extension\ProfileHandlerInterface;
/**
* Storage to access configuration and schema in enabled extensions.
@@ -54,9 +56,11 @@ class ExtensionInstallStorage extends InstallStorage {
* @param string|null $profile
* (optional) The current installation profile. This parameter will be
* mandatory in Drupal 9.0.0.
+ * @param \Drupal\Core\Extension\ProfileExtensionList $profile_list
+ * (optional) The profile extension list service.
*/
- public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, $profile = NULL) {
- parent::__construct($directory, $collection);
+ public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE, $profile = NULL, ProfileExtensionList $profile_list = NULL) {
+ parent::__construct($directory, $collection, $profile_list);
$this->configStorage = $config_storage;
$this->includeProfile = $include_profile;
if (!isset($profile) && count(func_get_args()) < 5) {
@@ -96,19 +100,11 @@ protected function getAllFolders() {
$extensions = $this->configStorage->read('core.extension');
// @todo Remove this scan as part of https://www.drupal.org/node/2186491
- $listing = new ExtensionDiscovery(\Drupal::root());
+ $listing = new ExtensionDiscovery(\Drupal::root(), TRUE, NULL, NULL, $this->profileList);
if (!empty($extensions['module'])) {
$modules = $extensions['module'];
// Remove the install profile as this is handled later.
unset($modules[$this->installProfile]);
- $profile_list = $listing->scan('profile');
- if ($this->installProfile && isset($profile_list[$this->installProfile])) {
- // Prime the drupal_get_filename() static cache with the profile info
- // file location so we can use drupal_get_path() on the active profile
- // during the module scan.
- // @todo Remove as part of https://www.drupal.org/node/2186491
- drupal_get_filename('profile', $this->installProfile, $profile_list[$this->installProfile]->getPathname());
- }
$module_list_scan = $listing->scan('module');
$module_list = [];
foreach (array_keys($modules) as $module) {
@@ -129,18 +125,11 @@ protected function getAllFolders() {
}
if ($this->includeProfile) {
- // The install profile can override module default configuration. We do
- // this by replacing the config file path from the module/theme with the
- // install profile version if there are any duplicates.
- if ($this->installProfile) {
- if (!isset($profile_list)) {
- $profile_list = $listing->scan('profile');
- }
- if (isset($profile_list[$this->installProfile])) {
- $profile_folders = $this->getComponentNames([$profile_list[$this->installProfile]]);
- $this->folders = $profile_folders + $this->folders;
- }
- }
+ // The install profile (and any parent profiles) can override module
+ // default configuration. We do this by replacing the config file path
+ // from the module/theme with the install profile version if there are
+ // any duplicates.
+ $this->folders += $this->getComponentNames($this->profileList->getAncestors($this->installProfile));
}
}
return $this->folders;
diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php
index 1ad7017f5d..5d87ee0404 100644
--- a/core/lib/Drupal/Core/Config/InstallStorage.php
+++ b/core/lib/Drupal/Core/Config/InstallStorage.php
@@ -4,6 +4,8 @@
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ProfileExtensionList;
+use Drupal\Core\Extension\ProfileHandlerInterface;
/**
* Storage used by the Drupal installer.
@@ -47,6 +49,13 @@ class InstallStorage extends FileStorage {
*/
protected $directory;
+ /**
+ * The profile list, used to find additional folders to scan for config.
+ *
+ * @var \Drupal\Core\Extension\ProfileExtensionList
+ */
+ protected $profileList;
+
/**
* Constructs an InstallStorage object.
*
@@ -56,9 +65,14 @@ class InstallStorage extends FileStorage {
* @param string $collection
* (optional) The collection to store configuration in. Defaults to the
* default collection.
+ * @param \Drupal\Core\Extension\ProfileExtensionList $profile_list
+ * (optional) The profile list.
*/
- public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) {
+ public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, ProfileExtensionList $profile_list = NULL) {
parent::__construct($directory, $collection);
+ if (\Drupal::hasService('extension.list.profile')) {
+ $this->profileList = $profile_list ?: \Drupal::service('extension.list.profile');
+ }
}
/**
@@ -151,20 +165,22 @@ protected function getAllFolders() {
if (!isset($this->folders)) {
$this->folders = [];
$this->folders += $this->getCoreNames();
+ // Get dependent profiles and add the extension components.
+ $this->folders += $this->getComponentNames($this->profileList->getAncestors());
// Perform an ExtensionDiscovery scan as we cannot use drupal_get_path()
// yet because the system module may not yet be enabled during install.
// @todo Remove as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
if ($profile = \Drupal::installProfile()) {
- $profile_list = $listing->scan('profile');
- if (isset($profile_list[$profile])) {
+ $profiles = $this->profileList->getAncestors($profile);
+ foreach ($profiles as $p) {
// Prime the drupal_get_filename() static cache with the profile info
// file location so we can use drupal_get_path() on the active profile
// during the module scan.
// @todo Remove as part of https://www.drupal.org/node/2186491
- drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname());
- $this->folders += $this->getComponentNames([$profile_list[$profile]]);
+ drupal_get_filename('profile', $profile, $p->getPathname());
}
+ $this->folders += $this->getComponentNames($profiles);
}
// @todo Remove as part of https://www.drupal.org/node/2186491
$this->folders += $this->getComponentNames($listing->scan('module'));
diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
index f521bb1655..35fa8c2112 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
@@ -119,12 +119,18 @@ protected function validateModules(ConfigImporter $config_importer) {
$config_importer->logError($this->t('Unable to install the %module module since it does not exist.', ['%module' => $module]));
}
+ // Get a list of parent profiles and the main profile.
+ /* @var $profiles \Drupal\Core\Extension\Extension[] */
+ $profiles = \Drupal::service('extension.list.profile')->getAncestors();
+ /* @var $main_profile \Drupal\Core\Extension\Extension */
+ $main_profile = end($profiles);
+
// Ensure that all modules being installed have their dependencies met.
$installs = $config_importer->getExtensionChangelist('module', 'install');
foreach ($installs as $module) {
$missing_dependencies = [];
foreach (array_keys($module_data[$module]->requires) as $required_module) {
- if (!isset($core_extension['module'][$required_module])) {
+ if (!isset($core_extension['module'][$required_module]) && !array_key_exists($module, $profiles)) {
$missing_dependencies[] = $module_data[$required_module]->info['name'];
}
}
@@ -144,18 +150,44 @@ protected function validateModules(ConfigImporter $config_importer) {
$uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
foreach ($uninstalls as $module) {
foreach (array_keys($module_data[$module]->required_by) as $dependent_module) {
- if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE) && $dependent_module !== $install_profile) {
- $module_name = $module_data[$module]->info['name'];
- $dependent_module_name = $module_data[$dependent_module]->info['name'];
- $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', ['%module' => $module_name, '%dependent_module' => $dependent_module_name]));
+ if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE)) {
+ if (!array_key_exists($dependent_module, $profiles)) {
+ $module_name = $module_data[$module]->info['name'];
+ $dependent_module_name = $module_data[$dependent_module]->info['name'];
+ $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', [
+ '%module' => $module_name,
+ '%dependent_module' => $dependent_module_name
+ ]));
+ }
}
}
}
- // Ensure that the install profile is not being uninstalled.
- if (in_array($install_profile, $uninstalls, TRUE)) {
- $profile_name = $module_data[$install_profile]->info['name'];
- $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the install profile.', ['%profile' => $profile_name]));
+ // Don't allow profiles to be uninstalled. It's possible for no profile to
+ // be set yet if the config is being imported during initial site install.
+ if ($main_profile instanceof \Drupal\Core\Extension\Extension) {
+ if (in_array($main_profile->getName(), $uninstalls, TRUE)) {
+ // Ensure that the active profile is not being uninstalled.
+ $profile_name = $main_profile->info['name'];
+ $config_importer->logError($this->t('Unable to uninstall the %profile profile since it is the main install profile.', ['%profile' => $profile_name]));
+ }
+ if ($profile_uninstalls = array_intersect_key($profiles, array_flip($uninstalls))) {
+ // Ensure that none of the parent profiles are being uninstalled.
+ $profile_names = [];
+ foreach ($profile_uninstalls as $profile) {
+ if ($profile->getName() !== $main_profile->getName()) {
+ $profile_names[] = $module_data[$profile->getName()]->info['name'];
+ }
+ }
+ if (!empty($profile_names)) {
+ $message = $this->formatPlural(count($profile_names),
+ 'Unable to uninstall the :profile profile since it is a parent of another installed profile.',
+ 'Unable to uninstall the :profile profiles since they are parents of another installed profile.',
+ [':profile' => implode(', ', $profile_names)]
+ );
+ $config_importer->logError($message);
+ }
+ }
}
}
diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
index f9c7421dd7..d933881022 100644
--- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -91,6 +91,15 @@ class ExtensionDiscovery {
*/
protected $sitePath;
+ /**
+ * The profile list.
+ *
+ * Used to determine the directories in which we want to scan for modules.
+ *
+ * @var \Drupal\Core\Extension\ProfileExtensionList
+ */
+ protected $profileList;
+
/**
* Constructs a new ExtensionDiscovery object.
*
@@ -102,12 +111,24 @@ class ExtensionDiscovery {
* The available profile directories
* @param string $site_path
* The path to the site.
+ * @param \Drupal\Core\Extension\ProfileExtensionList|null $profile_list
+ * (optional) The profile list.
*/
- public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) {
+ public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL, ProfileExtensionList $profile_list = NULL) {
$this->root = $root;
$this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL;
$this->profileDirectories = $profile_directories;
$this->sitePath = $site_path;
+
+ // ExtensionDiscovery can be used without a service container
+ // (@drupalKernel::moduleData), so only use the profile list service if it
+ // is available to us.
+ if ($profile_list) {
+ $this->profileList = $profile_list;
+ }
+ elseif (\Drupal::hasService('extension.list.profile')) {
+ $this->profileList = \Drupal::service('extension.list.profile');
+ }
}
/**
@@ -229,7 +250,19 @@ public function scan($type, $include_tests = NULL) {
public function setProfileDirectoriesFromSettings() {
$this->profileDirectories = [];
if ($profile = \Drupal::installProfile()) {
- $this->profileDirectories[] = drupal_get_path('profile', $profile);
+ if ($this->profileList) {
+ $profiles = $this->profileList->getAncestors($profile);
+ }
+ else {
+ $profiles = [
+ $profile => new Extension($this->root, 'profile', drupal_get_path('profile', $profile)),
+ ];
+ }
+
+ $profile_directories = array_map(function(Extension $extension) {
+ return $extension->getPath();
+ }, $profiles);
+ $this->profileDirectories = array_unique(array_merge($profile_directories, $this->profileDirectories));
}
return $this;
}
diff --git a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
index 60b08cc8b5..e2830e8ad5 100644
--- a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
+++ b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
@@ -41,7 +41,7 @@ class ModuleExtensionList extends ExtensionList {
/**
* The profile list needed by this module list.
*
- * @var \Drupal\Core\Extension\ExtensionList
+ * @var \Drupal\Core\Extension\ProfileExtensionList
*/
protected $profileList;
@@ -62,14 +62,14 @@ class ModuleExtensionList extends ExtensionList {
* The state.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
- * @param \Drupal\Core\Extension\ExtensionList $profile_list
+ * @param \Drupal\Core\Extension\ProfileExtensionList $profile_list
* The site profile listing.
* @param string $install_profile
* The install profile used by the site.
* @param array[] $container_modules_info
* (optional) The module locations coming from the compiled container.
*/
- public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ExtensionList $profile_list, $install_profile, array $container_modules_info = []) {
+ public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ProfileExtensionList $profile_list, $install_profile, array $container_modules_info = []) {
parent::__construct($root, $type, $cache, $info_parser, $module_handler, $state, $install_profile);
$this->configFactory = $config_factory;
@@ -105,9 +105,7 @@ protected function getExtensionDiscovery() {
*/
protected function getProfileDirectories(ExtensionDiscovery $discovery) {
$discovery->setProfileDirectories([]);
- $all_profiles = $discovery->scan('profile');
- $active_profile = $all_profiles[$this->installProfile];
- $profiles = array_intersect_key($all_profiles, $this->configFactory->get('core.extension')->get('module') ?: [$active_profile->getName() => 0]);
+ $profiles = $this->profileList->getAncestors($this->installProfile);
$profile_directories = array_map(function (Extension $profile) {
return $profile->getPath();
@@ -135,13 +133,9 @@ protected function getActiveProfile() {
*/
protected function doScanExtensions() {
$extensions = parent::doScanExtensions();
-
- $profiles = $this->profileList->getList();
- // Modify the active profile object that was previously added to the module
- // list.
- if ($this->installProfile && isset($profiles[$this->installProfile])) {
- $extensions[$this->installProfile] = $profiles[$this->installProfile];
- }
+ // Merge in the install profile and any profile ancestors.
+ $profiles = $this->profileList->getAncestors($this->installProfile);
+ $extensions = array_merge($extensions, $profiles);
return $extensions;
}
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 62dbb6c9b0..8cafcb8de4 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -377,7 +377,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
return FALSE;
}
- // Skip already uninstalled modules.
+ // Skip already uninstalled modules and dependencies of profiles.
if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent])) {
$module_list[$dependent] = $dependent;
}
diff --git a/core/lib/Drupal/Core/Extension/ProfileExtensionList.php b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php
index 7c415168d8..3ed552cac7 100644
--- a/core/lib/Drupal/Core/Extension/ProfileExtensionList.php
+++ b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php
@@ -23,13 +23,169 @@ class ProfileExtensionList extends ExtensionList {
'package' => 'Other',
'version' => NULL,
'php' => DRUPAL_MINIMUM_PHP,
+ 'themes' => ['stark'],
+ 'hidden' => FALSE,
+ 'base profile' => '',
];
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtensionInfo($extension_name) {
+ $all_info = $this->getAllAvailableInfo();
+ if (isset($all_info[$extension_name])) {
+ return $all_info[$extension_name];
+ }
+ throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist.");
+ }
+
+ /**
+ * Returns a list comprised of the profile, its parent profile if it has one,
+ * and any further ancestors.
+ *
+ * @param string $profile
+ * (optional) The name of profile. Defaults to the current install profile.
+ *
+ * @return \Drupal\Core\Extension\Extension[]
+ * An associative array of Extension objects, keyed by profile name in
+ * descending order of their dependencies (ancestors first). If the profile
+ * is not given and cannot be determined, returns an empty array.
+ */
+ public function getAncestors($profile = NULL) {
+ $ancestors = [];
+
+ if (empty($profile)) {
+ $profile = $this->installProfile;
+ }
+ if (empty($profile)) {
+ return $ancestors;
+ }
+
+ $extension = $this->get($profile);
+
+ foreach ($extension->ancestors as $ancestor) {
+ $ancestors[$ancestor] = $this->get($ancestor);
+ }
+ $ancestors[$profile] = $extension;
+
+ return $ancestors;
+ }
+
+ /**
+ * Returns all available profiles which are distributions.
+ *
+ * @return \Drupal\Core\Extension\Extension[]
+ * Processed extension objects, keyed by machine name.
+ */
+ public function listDistributions() {
+ return array_filter($this->getList(), function (Extension $profile) {
+ return !empty($profile->info['distribution']);
+ });
+ }
+
+ /**
+ * Select the install distribution from the list of profiles.
+ *
+ * If there are multiple profiles marked as distributions, select the first.
+ * If there is an inherited profile marked as a distribution, select it over
+ * its base profile.
+ *
+ * @param string[] $profiles
+ * List of profile names to search.
+ *
+ * @return string|null
+ * The selected distribution profile name, or NULL if none is found.
+ */
+ public function selectDistribution(array $profiles = NULL) {
+ $distributions = $this->listDistributions();
+
+ if ($profiles) {
+ $distributions = array_intersect_key($distributions, array_flip($profiles));
+ }
+
+ // Remove any distributions which are extended by another one.
+ foreach ($distributions as $profile_name => $profile) {
+ if (!empty($profile->info['base profile'])) {
+ $base_profile = $profile->info['base profile'];
+ unset($distributions[$base_profile]);
+ }
+ }
+
+ return key($distributions) ?: NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doList() {
+ $profiles = parent::doList();
+
+ // Compute the ancestry of each profile before any further processing.
+ foreach ($profiles as $profile) {
+ // Maintain a list of profiles which depend on this one.
+ $profile->children = [];
+
+ // Maintain a list of profiles that this one depends on, in reverse
+ // ancestral order (immediate parent first).
+ $profile->ancestors = $this->computeAncestry($profiles, $profile);
+
+ // Give the profile a heavy weight to ensure that its hooks run last.
+ $profile->weight = count($profile->ancestors) + 1000;
+ }
+
+ // For each profile, merge in ancestors' module and theme lists.
+ foreach ($profiles as $profile_name => $profile) {
+ if (empty($profile->ancestors)) {
+ continue;
+ }
+ // Reference the extension info here for readability.
+ $info = &$profile->info;
+
+ // Add the parent profile as a hard dependency.
+ $info['dependencies'][] = reset($profile->ancestors);
+
+ // Add all themes and extensions listed by ancestors.
+ foreach ($profile->ancestors as $ancestor) {
+ $ancestor = $profiles[$ancestor];
+
+ // Add the current profile as a child of the ancestor.
+ $ancestor->children[] = $profile_name;
+ $info['install'] = array_merge($info['install'], $ancestor->info['install']);
+ $info['themes'] = array_merge($info['themes'], $ancestor->info['themes']);
+ }
+ $info['install'] = array_unique($info['install']);
+ $info['themes'] = array_unique($info['themes']);
+ }
+ return $profiles;
+ }
+
+ /**
+ * Computes and returns the ancestral lineage of a profile.
+ *
+ * @param \Drupal\Core\Extension\Extension[] $profiles
+ * All discovered profiles.
+ * @param \Drupal\Core\Extension\Extension $profile
+ * The profile for which to compute the ancestry.
+ *
+ * @return string[]
+ * The names of the ancestors of the given profile, in order.
+ */
+ protected function computeAncestry(array $profiles, Extension $profile) {
+ $ancestors = [];
+
+ while (!empty($profile->info['base profile'])) {
+ array_unshift($ancestors, $profile->info['base profile']);
+ $profile = $profile->info['base profile'];
+ $profile = $profiles[$profile];
+ }
+ return $ancestors;
+ }
+
/**
* {@inheritdoc}
*/
protected function getInstalledExtensionNames() {
- return [$this->installProfile];
+ return array_keys($this->getAncestors());
}
}
diff --git a/core/lib/Drupal/Core/Installer/InstallerProfileExtensionList.php b/core/lib/Drupal/Core/Installer/InstallerProfileExtensionList.php
new file mode 100644
index 0000000000..d5c42adea3
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/InstallerProfileExtensionList.php
@@ -0,0 +1,58 @@
+addedPathNames[$extension_name])) {
+ return $this->addedPathNames[$extension_name];
+ }
+ elseif (isset($this->pathNames[$extension_name])) {
+ return $this->pathNames[$extension_name];
+ }
+ elseif (isset(static::$staticAddedPathNames[$extension_name])) {
+ return static::$staticAddedPathNames[$extension_name];
+ }
+ elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) {
+ // Ensure we don't have to do path scanning more than really needed.
+ foreach ($path_names as $extension => $path_name) {
+ static::$staticAddedPathNames[$extension] = $path_name;
+ }
+ return $path_names[$extension_name];
+ }
+ throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist.");
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php
index dd4ae5f470..e98aa3dcc8 100644
--- a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php
+++ b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php
@@ -62,6 +62,7 @@ public function register(ContainerBuilder $container) {
// Use a performance optimised module extension list.
$container->getDefinition('extension.list.module')->setClass('Drupal\Core\Installer\InstallerModuleExtensionList');
+ $container->getDefinition('extension.list.profile')->setClass('Drupal\Core\Installer\InstallerProfileExtensionList');
}
/**
diff --git a/core/modules/config/tests/src/Functional/ConfigImportBaseInstallProfileTest.php b/core/modules/config/tests/src/Functional/ConfigImportBaseInstallProfileTest.php
new file mode 100644
index 0000000000..583c31153b
--- /dev/null
+++ b/core/modules/config/tests/src/Functional/ConfigImportBaseInstallProfileTest.php
@@ -0,0 +1,96 @@
+webUser = $this->drupalCreateUser(['synchronize configuration']);
+ $this->drupalLogin($this->webUser);
+ $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
+ }
+
+ /**
+ * Tests config importer cannot uninstall parent install profiles and
+ * dependencies of parent profiles can be uninstalled.
+ *
+ * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+ */
+ public function testInstallParentProfileValidation() {
+ $sync = $this->container->get('config.storage.sync');
+ $this->copyConfig($this->container->get('config.storage'), $sync);
+ $core = $sync->read('core.extension');
+
+ // Ensure that parent profile can not be uninstalled.
+ unset($core['module']['testing']);
+ $sync->write('core.extension', $core);
+
+ $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+ $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
+ $this->assertText('Unable to uninstall the Testing profile since it is a parent of another installed profile.');
+
+ // Uninstall dependencies of parent profile.
+ $core['module']['testing'] = 0;
+ unset($core['module']['dynamic_page_cache']);
+ $sync->write('core.extension', $core);
+ $sync->deleteAll('dynamic_page_cache.');
+ $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+ $this->assertText('The configuration was imported successfully.');
+ $this->rebuildContainer();
+ $this->assertFalse(\Drupal::moduleHandler()->moduleExists('dynamic_page_cache'), 'The dynamic_page_cache module has been uninstalled.');
+ }
+
+ /**
+ * Tests config importer cannot uninstall sub-profiles and dependencies of
+ * sub-profiles can be uninstalled.
+ *
+ * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+ */
+ public function testInstallSubProfileValidation() {
+ $sync = $this->container->get('config.storage.sync');
+ $this->copyConfig($this->container->get('config.storage'), $sync);
+ $core = $sync->read('core.extension');
+
+ // Ensure install sub-profiles can not be uninstalled.
+ unset($core['module']['testing_inherited']);
+ $sync->write('core.extension', $core);
+
+ $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+ $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
+ $this->assertText('Unable to uninstall the Testing Inherited profile since it is the main install profile.');
+
+ // Uninstall dependencies of main profile.
+ $core['module']['testing_inherited'] = 0;
+ unset($core['module']['syslog']);
+ $sync->write('core.extension', $core);
+ $sync->deleteAll('syslog.');
+ $this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
+ $this->assertText('The configuration was imported successfully.');
+ $this->rebuildContainer();
+ $this->assertFalse(\Drupal::moduleHandler()->moduleExists('syslog'), 'The syslog module has been uninstalled.');
+ }
+
+}
diff --git a/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php b/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php
index 8b0787d6ee..d8d33da0e6 100644
--- a/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php
+++ b/core/modules/config/tests/src/Functional/ConfigImportInstallProfileTest.php
@@ -56,7 +56,7 @@ public function testInstallProfileValidation() {
$this->drupalPostForm('admin/config/development/configuration', [], t('Import all'));
$this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
- $this->assertText('Unable to uninstall the Testing config import profile since it is the install profile.');
+ $this->assertText('Unable to uninstall the Testing config import profile since it is the main install profile.');
// Uninstall dependencies of testing_config_import.
$core['module']['testing_config_import'] = 0;
diff --git a/core/modules/system/src/Form/ModulesUninstallForm.php b/core/modules/system/src/Form/ModulesUninstallForm.php
index 9370050d15..086a0ed8b1 100644
--- a/core/modules/system/src/Form/ModulesUninstallForm.php
+++ b/core/modules/system/src/Form/ModulesUninstallForm.php
@@ -127,10 +127,15 @@ public function buildForm(array $form, FormStateInterface $form_state) {
return $form;
}
+ $profiles = \Drupal::service('extension.list.profile')->getAncestors();
+
// Sort all modules by their name.
uasort($uninstallable, 'system_sort_modules_by_info_name');
$validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable));
+ // Remove any profiles from the list.
+ $uninstallable = array_diff_key($uninstallable, $profiles);
+
$form['uninstall'] = ['#tree' => TRUE];
foreach ($uninstallable as $module_key => $module) {
$name = $module->info['name'] ?: $module->getName();
@@ -151,7 +156,8 @@ 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.
+ // we can allow this module to be uninstalled. (Installation profiles are
+ // excluded from this list.)
foreach (array_keys($module->required_by) as $dependent) {
if (drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) {
$name = isset($modules[$dependent]->info['name']) ? $modules[$dependent]->info['name'] : $dependent;
diff --git a/core/profiles/testing_inherited/config/install/block.block.stable_login.yml b/core/profiles/testing_inherited/config/install/block.block.stable_login.yml
new file mode 100644
index 0000000000..3650c6c41a
--- /dev/null
+++ b/core/profiles/testing_inherited/config/install/block.block.stable_login.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - user
+ theme:
+ - stable
+id: stable_login
+theme: stable
+region: sidebar_first
+weight: 0
+provider: null
+plugin: user_login_block
+settings:
+ id: user_login_block
+ label: 'User login'
+ provider: user
+ label_display: visible
+visibility: { }
diff --git a/core/profiles/testing_inherited/config/install/system.theme.yml b/core/profiles/testing_inherited/config/install/system.theme.yml
new file mode 100644
index 0000000000..67aeeeeac7
--- /dev/null
+++ b/core/profiles/testing_inherited/config/install/system.theme.yml
@@ -0,0 +1,2 @@
+# @todo: Remove this file in https://www.drupal.org/node/2352949
+default: stable
diff --git a/core/profiles/testing_inherited/modules/child_profile_module/child_profile_module.info.yml b/core/profiles/testing_inherited/modules/child_profile_module/child_profile_module.info.yml
new file mode 100644
index 0000000000..a3eec3e6df
--- /dev/null
+++ b/core/profiles/testing_inherited/modules/child_profile_module/child_profile_module.info.yml
@@ -0,0 +1,6 @@
+name: 'Child profile module'
+core: 8.x
+type: module
+description: 'A module contained in a child profile, for testing.'
+package: Testing
+version: VERSION
diff --git a/core/profiles/testing_inherited/modules/contrib/contrib_child_profile_module/contrib_child_profile_module.info.yml b/core/profiles/testing_inherited/modules/contrib/contrib_child_profile_module/contrib_child_profile_module.info.yml
new file mode 100644
index 0000000000..8eb63b3597
--- /dev/null
+++ b/core/profiles/testing_inherited/modules/contrib/contrib_child_profile_module/contrib_child_profile_module.info.yml
@@ -0,0 +1,6 @@
+name: 'Contrib child profile module'
+core: 8.x
+type: module
+description: 'A contrib module contained in a child profile, for testing.'
+package: Testing
+version: VERSION
diff --git a/core/profiles/testing_inherited/modules/custom/custom_child_profile_module/custom_child_profile_module.info.yml b/core/profiles/testing_inherited/modules/custom/custom_child_profile_module/custom_child_profile_module.info.yml
new file mode 100644
index 0000000000..5ffc61ff92
--- /dev/null
+++ b/core/profiles/testing_inherited/modules/custom/custom_child_profile_module/custom_child_profile_module.info.yml
@@ -0,0 +1,6 @@
+name: 'Custom child profile module'
+core: 8.x
+type: module
+description: 'A custom module contained in a child profile, for testing.'
+package: Testing
+version: VERSION
diff --git a/core/profiles/testing_inherited/testing_inherited.info.yml b/core/profiles/testing_inherited/testing_inherited.info.yml
new file mode 100644
index 0000000000..9f8309077e
--- /dev/null
+++ b/core/profiles/testing_inherited/testing_inherited.info.yml
@@ -0,0 +1,18 @@
+name: Testing Inherited
+type: profile
+description: 'Profile for testing base profile inheritance.'
+version: VERSION
+core: 8.x
+hidden: true
+
+base profile: testing
+
+install:
+ - block
+ - config
+ - child_profile_module
+ - contrib_child_profile_module
+ - custom_child_profile_module
+
+themes:
+ - stable
diff --git a/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php
new file mode 100644
index 0000000000..0af6c4f68d
--- /dev/null
+++ b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php
@@ -0,0 +1,46 @@
+assertInstanceOf(BlockInterface::class, Block::load('stable_login'));
+
+ // Check that stable is the default theme.
+ $this->assertSame('stable', $this->config('system.theme')->get('default'));
+
+ /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
+ $module_handler = $this->container->get('module_handler');
+ // Check that parent dependencies are installed.
+ $this->assertTrue($module_handler->moduleExists('page_cache'));
+ // Check that child profile dependencies are installed.
+ $this->assertTrue($module_handler->moduleExists('config'));
+ // Check that modules contained in the child profile are installed.
+ $this->assertTrue($module_handler->moduleExists('child_profile_module'));
+ $this->assertTrue($module_handler->moduleExists('contrib_child_profile_module'));
+ $this->assertTrue($module_handler->moduleExists('custom_child_profile_module'));
+
+ // Check that all themes were installed.
+ $this->assertTrue(\Drupal::service('theme_handler')->themeExists('stable'));
+ }
+
+}
diff --git a/core/profiles/testing_subsubprofile/modules/grandchild_profile_module/grandchild_profile_module.info.yml b/core/profiles/testing_subsubprofile/modules/grandchild_profile_module/grandchild_profile_module.info.yml
new file mode 100644
index 0000000000..d92c99d8f9
--- /dev/null
+++ b/core/profiles/testing_subsubprofile/modules/grandchild_profile_module/grandchild_profile_module.info.yml
@@ -0,0 +1,6 @@
+name: 'Grandchild profile module'
+core: 8.x
+type: module
+description: 'A module contained in a grandchild profile, for testing.'
+package: Testing
+version: VERSION
diff --git a/core/profiles/testing_subsubprofile/testing_subsubprofile.info.yml b/core/profiles/testing_subsubprofile/testing_subsubprofile.info.yml
new file mode 100644
index 0000000000..98101a9163
--- /dev/null
+++ b/core/profiles/testing_subsubprofile/testing_subsubprofile.info.yml
@@ -0,0 +1,12 @@
+name: Testing SubSubProfile
+type: profile
+description: 'Profile for testing deep profile inheritance.'
+version: VERSION
+core: 8.x
+hidden: true
+
+base profile: testing_inherited
+
+install:
+ - syslog
+ - grandchild_profile_module
diff --git a/core/profiles/testing_subsubprofile/tests/src/Functional/DeepInheritedProfileTest.php b/core/profiles/testing_subsubprofile/tests/src/Functional/DeepInheritedProfileTest.php
new file mode 100644
index 0000000000..3fa12d476a
--- /dev/null
+++ b/core/profiles/testing_subsubprofile/tests/src/Functional/DeepInheritedProfileTest.php
@@ -0,0 +1,46 @@
+assertSame('stable', $this->config('system.theme')->get('default'));
+
+ /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
+ $module_handler = $this->container->get('module_handler');
+ // page_cache was enabled in main profile.
+ $this->assertTrue($module_handler->moduleExists('page_cache'));
+ // block was enabled in parent profile.
+ $this->assertTrue($module_handler->moduleExists('block'));
+ // syslog was enabled in this profile.
+ $this->assertTrue($module_handler->moduleExists('syslog'));
+ // A module contained in this profile was installed too.
+ $this->assertTrue($module_handler->moduleExists('grandchild_profile_module'));
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
index 9bb2cd3a5c..137349acaa 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
@@ -741,9 +741,9 @@ public function testInstallProfileMisMatch() {
$error_log = $this->configImporter->getErrors();
// Install profiles can not be changed. Note that KernelTestBase currently
// does not use an install profile. This situation should be impossible
- // to get in but site's can removed the install profile setting from
+ // to get into but sites can change the install profile value in config or
// settings.php so the test is valid.
- $this->assertEqual(['Cannot change the install profile from to this_will_not_work once Drupal is installed.'], $error_log);
+ $this->assertEqual($error_log, ['Cannot change the install profile from to this_will_not_work once Drupal is installed.']);
}
}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ProfileExtensionListTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ProfileExtensionListTest.php
new file mode 100644
index 0000000000..0b4bbfa5f4
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ProfileExtensionListTest.php
@@ -0,0 +1,145 @@
+container->get('extension.list.profile');
+
+ $info = $profile_list->getExtensionInfo('testing_inherited');
+ $this->assertNotEmpty($info);
+ $this->assertSame($info['name'], 'Testing Inherited');
+ $this->assertSame($info['base profile'], 'testing');
+ $this->assertContains('config', $info['install']);
+ $this->assertContains('drupal:page_cache', $info['install']);
+ $this->assertTrue($info['hidden'], 'Profiles should be hidden');
+
+ // Test that profiles without any base return normalized info.
+ $info = $profile_list->getExtensionInfo('minimal');
+ $this->assertSame('', $info['base profile']);
+
+ // Tests three levels profile inheritance.
+ $info = $profile_list->getExtensionInfo('testing_subsubprofile');
+ $this->assertSame($info['base profile'], 'testing_inherited');
+ }
+
+ /**
+ * Tests getting profile dependency list.
+ *
+ * @covers ::getAncestors
+ */
+ public function testGetAncestors() {
+ /** @var \Drupal\Core\Extension\ProfileExtensionList $profile_list */
+ $profile_list = $this->container->get('extension.list.profile');
+
+ $profiles = $profile_list->getAncestors('testing');
+ $this->assertCount(1, $profiles);
+
+ $profiles = $profile_list->getAncestors('testing_inherited');
+ $this->assertCount(2, $profiles);
+
+ $profiles = $profile_list->getAncestors('testing_subsubprofile');
+ $this->assertCount(3, $profiles);
+
+ $first_profile = current($profiles);
+ $this->assertInstanceOf(Extension::class, $first_profile);
+ $this->assertSame($first_profile->getName(), 'testing');
+ $this->assertSame(1000, $first_profile->weight);
+ $this->assertObjectHasAttribute('origin', $first_profile);
+
+ $second_profile = next($profiles);
+ $this->assertInstanceOf(Extension::class, $second_profile);
+ $this->assertSame($second_profile->getName(), 'testing_inherited');
+ $this->assertSame(1001, $second_profile->weight);
+ $this->assertObjectHasAttribute('origin', $second_profile);
+
+ $third_profile = next($profiles);
+ $this->assertInstanceOf(Extension::class, $third_profile);
+ $this->assertSame($third_profile->getName(), 'testing_subsubprofile');
+ $this->assertSame(1002, $third_profile->weight);
+ $this->assertObjectHasAttribute('origin', $third_profile);
+ }
+
+ /**
+ * @covers ::selectDistribution
+ *
+ * @depends testGetExtensionInfo
+ */
+ public function testSelectDistribution() {
+ $profile_list = new TestProfileExtensionList(
+ $this->container->get('app.root'),
+ 'profile',
+ $this->container->get('cache.default'),
+ $this->container->get('info_parser'),
+ $this->container->get('module_handler'),
+ $this->container->get('state'),
+ $this->container->getParameter('install_profile')
+ );
+
+ $profiles = ['testing', 'testing_inherited'];
+ $base_info = $profile_list->getExtensionInfo('minimal');
+ $profile_info = $profile_list->getExtensionInfo('testing_inherited');
+
+ // Neither profile has distribution set.
+ $distribution = $profile_list->selectDistribution($profiles);
+ $this->assertEmpty($distribution, 'No distribution should be selected');
+
+ // Set base profile distribution.
+ $base_info['distribution']['name'] = 'Minimal';
+ $profile_list->profileInfo['minimal'] = $base_info;
+ // Base profile distribution should not be selected.
+ $distribution = $profile_list->selectDistribution($profiles);
+ $this->assertEmpty($distribution, 'Base profile distribution should not be selected');
+
+ // Set main profile distribution.
+ $profile_info['distribution']['name'] = 'Testing Inherited';
+ $profile_list->profileInfo['testing_inherited'] = $profile_info;
+ // Main profile distribution should be selected.
+ $distribution = $profile_list->selectDistribution($profiles);
+ $this->assertEquals($distribution, 'testing_inherited');
+ }
+
+}
+
+final class TestProfileExtensionList extends ProfileExtensionList {
+
+ /**
+ * Overridden profile info, keyed by extension name.
+ *
+ * @var array
+ */
+ public $profileInfo = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getList() {
+ $extensions = parent::getList();
+
+ foreach ($extensions as $name => $extension) {
+ if (isset($this->profileInfo[$name])) {
+ $extension->info = $this->profileInfo[$name];
+ }
+ }
+ return $extensions;
+ }
+
+}