diff --git a/core/core.services.yml b/core/core.services.yml index 7d7449d..d60c4cf 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -303,7 +303,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%', '@profile_handler'] lazy: true config.storage: class: Drupal\Core\Config\CachedStorage @@ -519,6 +519,9 @@ services: - { name: module_install.uninstall_validator } arguments: ['@string_translation'] lazy: true + profile_handler: + class: Drupal\Core\Extension\ProfileHandler + arguments: ['@app.root', '@info_parser'] theme_handler: class: Drupal\Core\Extension\ThemeHandler arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser'] diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 6fcc2d2..708472b 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -443,6 +443,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('profile_handler')->getProfiles($profile); + $profile_directories = array_map(function($extension) { + return $extension->getPath(); + }, $profiles); + $listing->setProfileDirectories($profile_directories); } // Use the language from the profile configuration, if available, to override @@ -1210,6 +1216,8 @@ function install_select_profile(&$install_state) { * - For non-interactive installations via install_drupal() settings. * - A discovered profile that is a distribution. If multiple profiles are * 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 @@ -1232,11 +1240,8 @@ function _install_select_profile(&$install_state) { } } // Check for a distribution profile. - foreach ($install_state['profiles'] as $profile) { - $profile_info = install_profile_info($profile->getName()); - if (!empty($profile_info['distribution'])) { - return $profile->getName(); - } + if ($distribution = \Drupal::service('profile_handler')->selectDistribution(array_keys($install_state['profiles']))) { + return $distribution; } // Get all visible (not hidden) profiles. @@ -1569,7 +1574,10 @@ 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(array(drupal_get_profile()), FALSE); + $profiles = \Drupal::service('profile_handler')->getProfiles(); + + // Install all the profiles. + \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 6bb6c11..167f910 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -1043,6 +1043,14 @@ function drupal_check_module($module) { * - install: Optional parameters to override the installer: * - theme: The machine name of a theme to use in the installer instead of * Drupal's default installer theme. + * - base profile: Existence of this key denotes that the installation profile + * depends on a parent installation profile. + * - name: The shortname of the base installation profile. + * - excluded_dependencies: An array of shortnames of other modules that have + * to be excluded from the base profile requirements. This allows e.g. to + * disable a demo module that would be installed by the base profile. + * If there are no excluded_dependencies, a shortcut of "base profile: name" + * can be used. * * 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 @@ -1069,18 +1077,7 @@ function install_profile_info($profile, $langcode = 'en') { $cache = &drupal_static(__FUNCTION__, array()); if (!isset($cache[$profile][$langcode])) { - // Set defaults for module info. - $defaults = array( - 'dependencies' => array(), - 'themes' => array('stark'), - 'description' => '', - 'version' => NULL, - 'hidden' => FALSE, - 'php' => DRUPAL_MINIMUM_PHP, - ); - $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; - $info = \Drupal::service('info_parser')->parse($profile_file); - $info += $defaults; + $info = \Drupal::service('profile_handler')->getProfileInfo($profile); // drupal_required_modules() includes the current profile as a dependency. // Remove that dependency, since a module cannot depend on itself. diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 5d50cd1..777e646 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -6,6 +6,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityDependency; +use Drupal\Core\Extension\ProfileHandlerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ConfigInstaller implements ConfigInstallerInterface { @@ -53,6 +54,13 @@ class ConfigInstaller implements ConfigInstallerInterface { protected $sourceStorage; /** + * The profile handler. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Is configuration being created as part of a configuration sync. * * @var bool @@ -81,14 +89,17 @@ class ConfigInstaller implements ConfigInstallerInterface { * The event dispatcher. * @param string $install_profile * The name of the currently active installation profile. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - 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, ProfileHandlerInterface $profile_handler = 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->profileHandler = $profile_handler ?: \Drupal::service('profile_handler'); } /** @@ -471,7 +482,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->profileHandler->getProfiles(); + 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 16fecbc..cd1525b 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Config; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\ProfileHandlerInterface; /** * Storage to access configuration and schema in enabled extensions. @@ -52,9 +53,11 @@ class ExtensionInstallStorage extends InstallStorage { * (optional) The current installation profile. This parameter will be * mandatory in Drupal 9.0.0. In Drupal 8.3.0 not providing this parameter * will trigger a silenced deprecation warning. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - 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, ProfileHandlerInterface $profile_handler = NULL) { + parent::__construct($directory, $collection, $profile_handler); $this->configStorage = $config_storage; $this->includeProfile = $include_profile; if (is_null($profile)) { @@ -93,19 +96,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->profileHandler); 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 = array(); foreach (array_keys($modules) as $module) { @@ -126,18 +121,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(array($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->profileHandler->getProfiles($this->installProfile)); } } return $this->folders; diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index bd9bf30..e459d9f 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -4,6 +4,7 @@ use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\Extension; +use Drupal\Core\Extension\ProfileHandlerInterface; /** * Storage used by the Drupal installer. @@ -48,6 +49,13 @@ class InstallStorage extends FileStorage { protected $directory; /** + * The profile handler used to find additional folders to scan for config. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Constructs an InstallStorage object. * * @param string $directory @@ -56,9 +64,12 @@ class InstallStorage extends FileStorage { * @param string $collection * (optional) The collection to store configuration in. Defaults to the * default collection. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) { + public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, ProfileHandlerInterface $profile_handler = NULL) { parent::__construct($directory, $collection); + $this->profileHandler = $profile_handler ?: \Drupal::service('profile_handler'); } /** @@ -151,21 +162,12 @@ protected function getAllFolders() { if (!isset($this->folders)) { $this->folders = array(); $this->folders += $this->getCoreNames(); + // Get dependent profiles and add the extension components. + $this->folders += $this->getComponentNames($this->profileHandler->getProfiles()); // 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_get_profile()) { - $profile_list = $listing->scan('profile'); - if (isset($profile_list[$profile])) { - // 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(array($profile_list[$profile])); - } - } // @todo Remove as part of https://www.drupal.org/node/2186491 $this->folders += $this->getComponentNames($listing->scan('module')); $this->folders += $this->getComponentNames($listing->scan('theme')); diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 177f011..60f935c 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -92,6 +92,15 @@ class ExtensionDiscovery { protected $sitePath; /** + * The profile handler. + * + * Used to determine the directories in which we want to scan for modules. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface|null + */ + protected $profileHandler; + + /** * Constructs a new ExtensionDiscovery object. * * @param string $root @@ -102,12 +111,27 @@ class ExtensionDiscovery { * The available profile directories * @param string $site_path * The path to the site. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * (optional) The profile handler. */ - 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, ProfileHandlerInterface $profile_handler = 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 create a fallback profile handler if the + // profile_handler service is unavailable. + if ($profile_handler) { + $this->profileHandler = $profile_handler; + } + elseif (\Drupal::hasService('profile_handler')) { + $this->profileHandler = \Drupal::service('profile_handler'); + } + else { + $this->profileHandler = new FallbackProfileHandler($root); + } } /** @@ -241,7 +265,11 @@ public function setProfileDirectoriesFromSettings() { // In case both profile directories contain the same extension, the actual // profile always has precedence. if ($profile) { - $this->profileDirectories[] = drupal_get_path('profile', $profile); + $profiles = $this->profileHandler->getProfiles($profile); + $profile_directories = array_map(function($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/FallbackProfileHandler.php b/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php new file mode 100644 index 0000000..f858ca2 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/FallbackProfileHandler.php @@ -0,0 +1,75 @@ +root = $root; + } + + /** + * The stored profile info. + * + * @var array[] + */ + protected $profileInfo = []; + + /** + * {@inheritdoc} + */ + public function getProfileInfo($profile) { + if (isset($this->profileInfo[$profile])) { + return $this->profileInfo[$profile]; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function setProfileInfo($profile, array $info) { + $this->profileInfo[$profile] = $info; + } + + /** + * {@inheritdoc} + */ + public function clearProfileCache() { + unset($this->profileInfo); + } + + /** + * {@inheritdoc} + */ + public function getProfiles($profile = NULL) { + $profile_path = drupal_get_path('profile', $profile); + return [ + $profile => new Extension($this->root, 'profile', $profile_path), + ]; + } + + /** + * {@inheritdoc} + */ + public function selectDistribution(array $profile_list) { + return NULL; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ProfileHandler.php b/core/lib/Drupal/Core/Extension/ProfileHandler.php new file mode 100644 index 0000000..a8e1d2f --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandler.php @@ -0,0 +1,296 @@ +root = $root; + $this->infoParser = $info_parser; + $this->extensionDiscovery = $extension_discovery ?: new ExtensionDiscovery($root, TRUE, NULL, NULL, $this); + } + + /** + * Return the full path to a profile. + * + * Wrapper around drupal_get_path. If profile path is not available yet we + * call scan('profile') and prime the cache. + * + * @param string $profile + * The name of the profile. + * + * @return string + * The full path to the profile. + */ + protected function getProfilePath($profile) { + // Check to see if system_rebuild_module_data cache is primed. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $modules_cache = &drupal_static('system_rebuild_module_data'); + if (!$this->scanCache && !isset($modules_cache)) { + // Find installation profiles. This needs to happen before performing a + // module scan as the module scan requires knowing what the active profile + // is. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $profiles = $this->extensionDiscovery->scan('profile'); + foreach ($profiles as $profile_name => $extension) { + // 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_name, $extension->getPathname()); + } + $this->scanCache = TRUE; + } + return drupal_get_path('profile', $profile); + } + + /** + * {@inheritdoc} + */ + public function getProfileInfo($profile) { + // Even though info_parser caches the info array, we need to also cache + // this since it is recursive. + if (!isset($this->infoCache[$profile])) { + // Set defaults for profile info. + $defaults = [ + 'dependencies' => [], + 'themes' => ['stark'], + 'description' => '', + 'version' => NULL, + 'hidden' => FALSE, + 'php' => DRUPAL_MINIMUM_PHP, + 'base profile' => [ + 'name' => '', + 'excluded_dependencies' => [], + 'excluded_themes' => [], + ], + ]; + + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $info = $this->infoParser->parse($profile_file) + $defaults; + + // Normalize any base profile info. + if (is_string($info['base profile'])) { + $info['base profile'] = [ + 'name' => $info['base profile'], + 'excluded_dependencies' => [], + 'excluded_themes' => [], + ]; + } + + $profile_list = []; + // Get the base profile dependencies. + if ($base_profile_name = $info['base profile']['name']) { + $base_info = $this->getProfileInfo($base_profile_name); + $profile_list += $base_info['profile_list']; + + // Ensure all dependencies are cleanly merged. + $info['dependencies'] = array_merge($info['dependencies'], $base_info['dependencies']); + // Apply excluded dependencies. + $info['dependencies'] = array_diff($info['dependencies'], $info['base profile']['excluded_dependencies']); + // Ensure there's no circular dependency. + $info['dependencies'] = array_diff($info['dependencies'], [$profile]); + + // Ensure all themes are cleanly merged. + $info['themes'] = array_unique(array_merge($info['themes'], $base_info['themes'])); + // Apply excluded themes. + $info['themes'] = array_diff($info['themes'], $info['base profile']['excluded_themes']); + // Ensure each theme is listed only once. + $info['themes'] = array_unique($info['themes']); + } + $profile_list[$profile] = $profile; + $info['profile_list'] = $profile_list; + + // Ensure the same dependency notation as in modules can be used. + array_walk($info['dependencies'], function(&$dependency) { + $dependency = ModuleHandler::parseDependency($dependency)['name']; + }); + + // Installation profiles are hidden by default, unless explicitly + // specified otherwise in the .info.yml file. + $info['hidden'] = isset($info['hidden']) ? $info['hidden'] : TRUE; + + $this->infoCache[$profile] = $info; + } + return $this->infoCache[$profile]; + } + + /** + * {@inheritdoc} + */ + public function setProfileInfo($profile, array $info) { + $this->infoCache[$profile] = $info; + // Also unset the cached profile extension so the updated info will + // be picked up. + unset($this->profilesWithParentsCache[$profile]); + } + + /** + * {@inheritdoc} + */ + public function clearProfileCache() { + $this->profilesWithParentsCache = []; + $this->infoCache = []; + } + + /** + * Create an Extension object for a profile. + * + * @param string $profile + * The name of the profile. + * + * @return \Drupal\Core\Extension\Extension + * The extension object for the profile + * Properties added to extension: + * info: The parsed info.yml data. + * origin: The directory origin as used in ExtensionDiscovery. + */ + protected function getProfileExtension($profile) { + $profile_info = $this->getProfileInfo($profile); + + $type = $profile_info['type']; + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $filename = file_exists($profile_path . "/$profile.$type") ? "$profile.$type" : NULL; + $extension = new Extension($this->root, $type, $profile_file, $filename); + + $extension->info = $profile_info; + $extension->origin = ''; + + return $extension; + } + + /** + * Get a list of dependent profile names. + * + * @param string $profile + * Name of profile. + * + * @return string[] + * An associative array of profile names, keyed by profile name + * in descending order of their dependencies. + * (parent profiles first, main profile last) + */ + protected function getProfileList($profile) { + $profile_info = $this->getProfileInfo($profile); + return $profile_info['profile_list']; + } + + /** + * {@inheritdoc} + */ + public function getProfiles($profile = NULL) { + if (empty($profile)) { + $profile = drupal_get_profile(); + } + if (!isset($this->profilesWithParentsCache[$profile])) { + $profiles = []; + // Check if a valid profile name was given. + if (!empty($profile)) { + $list = $this->getProfileList($profile); + + // Starting weight for profiles ensures their hooks run last. + $weight = 1000; + + // Loop through profile list and create Extension objects. + $profiles = []; + foreach ($list as $profile_name) { + $extension = $this->getProfileExtension($profile_name); + $extension->weight = $weight; + $weight++; + $profiles[$profile_name] = $extension; + } + } + $this->profilesWithParentsCache[$profile] = $profiles; + } + return $this->profilesWithParentsCache[$profile]; + } + + /** + * {@inheritdoc} + */ + public function selectDistribution(array $profile_list) { + // First, find all profiles marked as distributions + $distributions = []; + foreach ($profile_list as $profile_name) { + $profile_info = $this->getProfileInfo($profile_name); + if (!empty($profile_info['distribution'])) { + $distributions[$profile_name] = $profile_name; + } + } + // Remove any base profiles. + foreach ($profile_list as $profile_name) { + $profile_info = $this->getProfileInfo($profile_name); + if ($base_profile = $profile_info['base profile']['name']) { + unset($distributions[$base_profile]); + } + } + return !empty($distributions) ? current($distributions) : NULL; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php new file mode 100644 index 0000000..37b4bdf --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php @@ -0,0 +1,79 @@ +getProfiles(); // 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'] = array('#tree' => TRUE); foreach ($uninstallable as $module_key => $module) { $name = $module->info['name'] ?: $module->getName(); @@ -142,8 +145,9 @@ public function buildForm(array $form, FormStateInterface $form_state) { // 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.) - foreach (array_keys($module->required_by) as $dependent) { - if ($dependent != $profile && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) { + $dependents = array_diff_key($module->required_by, $profiles); + foreach (array_keys($dependents) as $dependent) { + 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; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 6581e60..4c62328 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -950,27 +950,13 @@ function system_get_info($type, $name = NULL) { function _system_rebuild_module_data() { $listing = new ExtensionDiscovery(\Drupal::root()); - // Find installation profiles. This needs to happen before performing a - // module scan as the module scan requires knowing what the active profile is. - // @todo Remove as part of https://www.drupal.org/node/2186491. - $profiles = $listing->scan('profile'); - $profile = drupal_get_profile(); - if ($profile && isset($profiles[$profile])) { - // 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, $profiles[$profile]->getPathname()); - } - // Find modules. $modules = $listing->scan('module'); - // Include the installation profile in modules that are loaded. - if ($profile) { - $modules[$profile] = $profiles[$profile]; - // Installation profile hooks are always executed last. - $modules[$profile]->weight = 1000; - } + + // Find profiles. + /** @var \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler */ + $profile_handler = \Drupal::service('profile_handler'); + $modules = array_merge($modules, $profile_handler->getProfiles()); // Set defaults for module info. $defaults = array( @@ -984,7 +970,14 @@ function _system_rebuild_module_data() { // Read info files for each module. foreach ($modules as $key => $module) { // Look for the info file. - $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); + // @todo On the longrun we should leverage the extension lists services, + // see https://www.drupal.org/node/2208429. + if ($module->getType() === 'profile') { + $module->info = $profile_handler->getProfileInfo($module->getName()); + } + else { + $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); + } // Add the info file modification time, so it becomes available for // contributed modules to use for ordering module lists. @@ -993,12 +986,6 @@ function _system_rebuild_module_data() { // Merge in defaults and save. $modules[$key]->info = $module->info + $defaults; - // Installation profiles are hidden by default, unless explicitly specified - // otherwise in the .info.yml file. - if ($key == $profile && !isset($modules[$key]->info['hidden'])) { - $modules[$key]->info['hidden'] = TRUE; - } - // Invoke hook_system_info_alter() to give installed modules a chance to // modify the data in the .info.yml files if necessary. // @todo Remove $type argument, obsolete with $module->getType(). @@ -1012,7 +999,8 @@ function _system_rebuild_module_data() { _system_rebuild_module_data_ensure_required($module, $modules); } - + // This must be done after _system_rebuild_module_data_ensure_required(). + $profile = drupal_get_profile(); if ($profile && isset($modules[$profile])) { // The installation profile is required, if it's a valid module. $modules[$profile]->info['required'] = TRUE; 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 0000000..3650c6c --- /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 0000000..67aeeee --- /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/testing_inherited.info.yml b/core/profiles/testing_inherited/testing_inherited.info.yml new file mode 100644 index 0000000..6f584ae --- /dev/null +++ b/core/profiles/testing_inherited/testing_inherited.info.yml @@ -0,0 +1,20 @@ +name: Testing Inherited +type: profile +description: 'Profile for testing base profile inheritance.' +version: VERSION +core: 8.x +hidden: true + +base profile: + name: testing + excluded_dependencies: + - page_cache + excluded_themes: + - classy + +dependencies: + - block + - config + +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 0000000..fd54021 --- /dev/null +++ b/core/profiles/testing_inherited/tests/src/Functional/InheritedProfileTest.php @@ -0,0 +1,40 @@ +assertInstanceOf(BlockInterface::class, Block::load('stable_login')); + + // Check that stable is the default theme. + $this->assertEquals('stable', $this->config('system.theme')->get('default')); + + // Check the excluded_dependencies flag on installation profiles. + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('config')); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('page_cache')); + + // Check that all themes were installed, except excluded ones. + $this->assertTrue(\Drupal::service('theme_handler')->themeExists('stable')); + $this->assertFalse(\Drupal::service('theme_handler')->themeExists('classy')); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php new file mode 100644 index 0000000..fad80c0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ProfileHandlerTest.php @@ -0,0 +1,106 @@ +container->get('profile_handler'); + $info = $profile_handler->getProfileInfo('testing_inherited'); + $this->assertNotEmpty($info); + $this->assertEquals($info['name'], 'Testing Inherited'); + $this->assertEquals($info['base profile']['name'], 'minimal'); + $this->assertEquals($info['base profile']['excluded_dependencies'], ['dblog']); + $this->assertTrue(in_array('config', $info['dependencies'], 'config should be found in dependencies')); + $this->assertFalse(in_array('dblog', $info['dependencies'], 'dblog should not be found in dependencies')); + $this->assertTrue($info['hidden'], 'Profiles should be hidden'); + $this->assertNotEmpty($info['profile_list']); + $profile_list = $info['profile_list']; + // Testing order of profile list. + $this->assertEquals($profile_list, [ + 'minimal' => 'minimal', + 'testing_inherited' => 'testing_inherited' + ]); + + // Test that profiles without any base return normalized info. + $info = $profile_handler->getProfileInfo('minimal'); + $this->assertEquals($info['base profile'], ['name' => '', 'excluded_dependencies' => []]); + } + + /** + * Tests getting profile dependency list. + * + * @covers ::getProfiles + */ + public function testGetProfiles() { + $profile_handler = $this->container->get('profile_handler'); + $profiles = $profile_handler->getProfiles('testing_inherited'); + $this->assertCount(2, $profiles); + + $first_profile = current($profiles); + $this->assertEquals(get_class($first_profile), 'Drupal\Core\Extension\Extension'); + $this->assertEquals($first_profile->getName(), 'minimal'); + $this->assertEquals($first_profile->weight, 1000); + $this->assertObjectHasAttribute('origin', $first_profile); + + $second_profile = next($profiles); + $this->assertEquals(get_class($second_profile), 'Drupal\Core\Extension\Extension'); + $this->assertEquals($second_profile->getName(), 'testing_inherited'); + $this->assertEquals($second_profile->weight, 1001); + $this->assertObjectHasAttribute('origin', $second_profile); + } + + /** + * @covers ::selectDistribution + * @covers ::setProfileInfo + */ + public function testSelectDistribution() { + /** @var \Drupal\Core\Extension\ProfileHandler $profile_handler */ + $profile_handler = $this->container->get('profile_handler'); + $profiles = ['minimal', 'testing_inherited']; + $base_info = $profile_handler->getProfileInfo('minimal'); + $profile_info = $profile_handler->getProfileInfo('testing_inherited'); + + // Neither profile has distribution set + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEmpty($distribution, 'No distribution should be selected'); + + // Set base profile distribution + $base_info['distribution']['name'] = 'Minimal'; + $profile_handler->setProfileInfo('minimal', $base_info); + // Base profile distribution should not be selected + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEmpty($distribution, 'Base profile distribution should not be selected'); + + // Set main profile distribution + $profile_info['distribution']['name'] = 'Testing Inherited'; + $profile_handler->setProfileInfo('testing_inherited', $profile_info); + // Main profile distribution should be selected + $distribution = $profile_handler->selectDistribution($profiles); + $this->assertEquals($distribution, 'testing_inherited'); + } + +}