diff --git a/core/core.services.yml b/core/core.services.yml index 276e19d..759920e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -290,7 +290,7 @@ services: - { name: event_subscriber } config.installer: class: Drupal\Core\Config\ConfigInstaller - arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] + arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '@profile_handler'] lazy: true config.storage: class: Drupal\Core\Config\CachedStorage @@ -498,6 +498,9 @@ services: - { name: module_install.uninstall_validator } arguments: ['@string_translation'] lazy: true + profile_handler: + class: Drupal\Core\Extension\ProfileHandler + arguments: ['@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 1019270..8dda481 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -21,6 +21,7 @@ use Drupal\Core\StringTranslation\Translator\FileTranslation; use Drupal\Core\StackMiddleware\ReverseProxyMiddleware; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\ProfileHandler; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; @@ -439,6 +440,10 @@ 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. + $profile_handler = \Drupal::service('profile_handler'); + $profile_directories = $profile_handler->getProfileDirectories($profile); + $listing->setProfileDirectories($profile_directories); } // Use the language from the profile configuration, if available, to override @@ -1550,7 +1555,11 @@ 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); + $profile_handler = \Drupal::service('profile_handler'); + $profiles = $profile_handler->getProfileDependencies(drupal_get_profile()); + + // Install all the profiles. + \Drupal::service('module_installer')->install($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 3a9c2bc..29e6cef 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -11,6 +11,7 @@ use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\ModuleHandler; use Drupal\Core\Site\Settings; /** @@ -1029,6 +1030,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 @@ -1073,6 +1082,27 @@ function install_profile_info($profile, $langcode = 'en') { $locale = !empty($langcode) && $langcode != 'en' ? array('locale') : array(); + // Get the base profile chain. + $base_profile_name = !empty($info['base profile']['name']) ? $info['base profile']['name'] : + (!empty($info['base profile']) ? $info['base profile'] : ''); + if ($base_profile_name) { + $base_profile = install_profile_info($base_profile_name, $langcode); + + // Ensure all dependencies are cleanly merged. + $info['dependencies'] = array_merge($info['dependencies'], $base_profile['dependencies']); + // If there are dependency excludes from the base apply them now. + if (!empty($info['base profile']['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'], array($profile)); + } + + // Ensure the same dependency notation as in modules can be used. + array_walk($info['dependencies'], function(&$dependency) { + $dependency = ModuleHandler::parseDependency($dependency)['name']; + }); + $info['dependencies'] = array_unique(array_merge($required, $info['dependencies'], $locale)); $cache[$profile][$langcode] = $info; diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 97c3688..694b694 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -7,6 +7,7 @@ use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityDependency; use Drupal\Core\Site\Settings; +use Drupal\Core\Extension\ProfileHandlerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ConfigInstaller implements ConfigInstallerInterface { @@ -54,6 +55,13 @@ class ConfigInstaller implements ConfigInstallerInterface { protected $sourceStorage; /** + * ProfileHandler + * + * @var \Drupal\Core\Extension\ProfileHandler + */ + protected $profileHandler; + + /** * Is configuration being created as part of a configuration sync. * * @var bool @@ -73,13 +81,16 @@ class ConfigInstaller implements ConfigInstallerInterface { * The configuration manager. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher. + * @param \Drupal\Core\Extension\ProfileHandlerInterface $profile_handler + * The profile handler. */ - public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) { + public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, ProfileHandlerInterface $profile_handler) { $this->configFactory = $config_factory; $this->activeStorages[$active_storage->getCollectionName()] = $active_storage; $this->typedConfig = $typed_config; $this->configManager = $config_manager; $this->eventDispatcher = $event_dispatcher; + $this->profileHandler = $profile_handler; } /** @@ -462,7 +473,7 @@ 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()) { + if (!$this->profileHandler->isProfile($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 14e80dd..b6c1393 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -86,13 +86,13 @@ protected function getAllFolders() { $modules = $extensions['module']; // Remove the install profile as this is handled later. unset($modules[$install_profile]); - $profile_list = $listing->scan('profile'); - if ($profile && isset($profile_list[$profile])) { + $profiles = $listing->scan('profile'); + foreach ($profiles as $profile_name => $profile_object) { // 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()); + drupal_get_filename('profile', $profile_name, $profile_object->getPathname()); } $module_list_scan = $listing->scan('module'); $module_list = array(); @@ -121,9 +121,12 @@ protected function getAllFolders() { if (!isset($profile_list)) { $profile_list = $listing->scan('profile'); } - if (isset($profile_list[$profile])) { - $profile_folders = $this->getComponentNames(array($profile_list[$profile])); - $this->folders = $profile_folders + $this->folders; + $profile_names = \Drupal::service('profile_handler')->getProfileDependencies($profile); + foreach ($profile_names as $profile_name) { + if (isset($profile_list[$profile_name])) { + $profile_folders = $this->getComponentNames(array($profile_list[$profile_name])); + $this->folders = $profile_folders + $this->folders; + } } } } diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index bd9bf30..d97f61b 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -155,16 +155,18 @@ protected function getAllFolders() { // 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])); - } + $profiles = $listing->scan('profile'); + foreach ($profiles as $profile_name => $profile_object) { + // 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, $profile_object->getPathname()); + } + // Now that we can fetch the path, get dependent profiles and add the extensions + $profile_names = \Drupal::service('profile_handler')->getProfileDependencies(drupal_get_profile()); + foreach ($profile_names as $profile_name) { + $this->folders += $this->getComponentNames(array($profiles[$profile_name])); } // @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/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 0d9283c..d46cc5d 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -7,6 +7,7 @@ use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator; use Drupal\Core\Site\Settings; use Symfony\Component\HttpFoundation\Request; +use Drupal\Core\Extension\ProfileHandler; /** * Discovers available extensions in the filesystem. @@ -248,8 +249,15 @@ 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); + if (\Drupal::hasService('profile_handler')) { + $profile_handler = \Drupal::service('profile_handler'); + $this->profileDirectories = array_merge($profile_handler->getProfileDirectories($profile), $this->profileDirectories); + } + else { + $this->profileDirectories[] = drupal_get_path('profile', $profile); + } } + return $this; } diff --git a/core/lib/Drupal/Core/Extension/ProfileHandler.php b/core/lib/Drupal/Core/Extension/ProfileHandler.php new file mode 100644 index 0000000..1411cd4 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandler.php @@ -0,0 +1,83 @@ +infoParser = $info_parser; + } + + /** + * {@inheritdoc} + */ + public function getProfileDependencies($profile) { + if (!isset(static::$cache[$profile])) { + $profiles = array(); + // Check if a valid profile name was given. + if (!empty($profile)) { + // We can't use install_profile_info() because that could trigger an + // endless loop. So we read this on our own. + $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; + $profile_info = $this->infoParser->parse($profile_file); + + // Check of the profile has a base profile and if so add it - recursion. + $base_profile_name = !empty($profile_info['base profile']['name']) ? $profile_info['base profile']['name'] : + (!empty($profile_info['base profile']) ? $profile_info['base profile'] : ''); + if ($base_profile_name) { + $profiles += $this->getProfileDependencies($base_profile_name); + } + // Add requested profile as last one. + $profiles[$profile] = $profile; + } + static::$cache[$profile] = $profiles; + } + return static::$cache[$profile]; + } + + /** + * {@inheritdoc} + */ + public function getProfileDirectories($profile) { + $profile_directories = array(); + foreach ($this->getProfileDependencies($profile) as $profile_dependency => $profile_dependency_info) { + $profile_directories[] = drupal_get_path('profile', $profile_dependency); + } + return $profile_directories; + } + + /** + * {@inheritdoc} + */ + public function isProfile($profile) { + $current_profile = drupal_get_profile(); + $base_profiles = $this->getProfileDependencies($current_profile); + return isset($base_profiles[$profile]); + } + +} diff --git a/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php new file mode 100644 index 0000000..3d8197b --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php @@ -0,0 +1,45 @@ +scan('profile'); - $profile = drupal_get_profile(); - if ($profile && isset($profiles[$profile])) { + foreach ($profiles as $profile_name => $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()); + drupal_get_filename('profile', $profile_name, $profile->getPathname()); } // Find modules. $modules = $listing->scan('module'); - // Include the installation profile in modules that are loaded. + + $profile = drupal_get_profile(); if ($profile) { - $modules[$profile] = $profiles[$profile]; + $profile_names = \Drupal::service('profile_handler')->getProfileDependencies($profile); + } + else { + $profile_names = array_keys($profiles); + } + // Include the installation profile in modules that are loaded. + $weight = 1000; + foreach ($profile_names as $profile_name) { + $modules[$profile_name] = $profiles[$profile_name]; // Installation profile hooks are always executed last. - $modules[$profile]->weight = 1000; + $modules[$profile_name]->weight = $weight; + $weight++; } // Set defaults for module info. @@ -1004,7 +1013,7 @@ function _system_rebuild_module_data() { // Installation profiles are hidden by default, unless explicitly specified // otherwise in the .info.yml file. - if ($key == $profile && !isset($modules[$key]->info['hidden'])) { + if (in_array($key, $profile_names) && !isset($modules[$key]->info['hidden'])) { $modules[$key]->info['hidden'] = TRUE; }