diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index d75ed9f..7a46d2e 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -439,6 +439,13 @@ 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_dependencies = $listing->getProfileDependencies($profile); + $profile_directories = array(); + foreach ($profile_dependencies as $profile_dependency => $profile_dependency_info) { + $profile_directories[] = drupal_get_path('profile', $profile_dependency); + } + $listing->setProfileDirectories($profile_directories); } // Use the language from the profile configuration, if available, to override @@ -1550,6 +1557,13 @@ function install_profile_themes(&$install_state) { * An array of information about the current installation state. */ function install_install_profile(&$install_state) { + // Install base profiles first. + foreach ($install_state['profile_info']['base_profiles'] as $base_profile) { + \Drupal::service('module_installer')->install(array($base_profile), FALSE); + //\Drupal::service('config.installer')->installOptionalConfig(); + } + + // Now install the profile. \Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE); // Install all available optional config. During installation the module order // is determined by dependencies. If there are no dependencies between modules @@ -2182,6 +2196,16 @@ function install_write_profile($install_state) { 'value' => $install_state['parameters']['profile'], 'required' => TRUE, ); + } + // Add specified base_profile to the settings profile_directories. + if (!empty($install_state['profile_info']['base_profile']['name']) && isset($install_state['profiles'][$install_state['profile_info']['base_profile']['name']]) && empty(Settings::get('profile_directories'))) { + $path = $install_state['profiles'][$install_state['profile_info']['base_profile']['name']]->getPath(); + $settings['settings']['profile_directories'] = (object) array( + 'value' => array($path), + 'required' => TRUE, + ); + } + if (!empty($settings)) { drupal_rewrite_settings($settings); } } diff --git a/core/includes/install.inc b/core/includes/install.inc index 61725da..6612d2f 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; /** @@ -1012,6 +1013,8 @@ function drupal_check_module($module) { * - description: A brief description of the profile. * - dependencies: An array of shortnames of other modules that this install * profile requires. + * - base_profiles: An array of shortnames of other profiles this install + * profile depends on. * * Additional, less commonly-used information that can appear in a * profile.info.yml file but not in a normal Drupal module .info.yml file @@ -1029,6 +1032,12 @@ 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. * * 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 @@ -1062,6 +1071,7 @@ function install_profile_info($profile, $langcode = 'en') { 'version' => NULL, 'hidden' => FALSE, 'php' => DRUPAL_MINIMUM_PHP, + 'base_profiles' => array(), ); $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; $info = \Drupal::service('info_parser')->parse($profile_file); @@ -1073,6 +1083,27 @@ function install_profile_info($profile, $langcode = 'en') { $locale = !empty($langcode) && $langcode != 'en' ? array('locale') : array(); + // Get the base profile chain. + if (!empty($info['base_profile']['name'])) { + $info['base_profiles'][$info['base_profile']['name']] = $info['base_profile']['name']; + $base_profile = install_profile_info($info['base_profile']['name'], $langcode); + $info['base_profiles'] += $base_profile['base_profiles']; + + // 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 cc0d88f..612d545 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -462,7 +462,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 ($name != $this->drupalGetProfile() && !in_array($name, $this->drupalGetBaseProfiles())) { // 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. @@ -646,6 +646,20 @@ protected function drupalGetProfile() { } /** + * Gets the base profiles of the install profile. + * + * @return array $profile + * The list of base profile names the currently installed profile depends + * on. + */ + protected function drupalGetBaseProfiles() { + $profile = $this->drupalGetProfile(); + include_once DRUPAL_ROOT . '/core/includes/install.inc'; + $profile_info = install_profile_info($profile); + return $profile_info['base_profiles']; + } + + /** * Wrapper for drupal_installation_attempted(). * * @return bool diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 1a3b943..377ec18 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -718,6 +718,10 @@ protected function moduleData($module) { $profile_directories = array_map(function ($profile) { return $profile->getPath(); }, $profiles); + + // Allow additional profile directories to be added from settings.php. + // This provides support for "base profiles". + $profile_directories = array_merge(Settings::get('profile_directories', []), $profile_directories); $listing->setProfileDirectories($profile_directories); // Now find modules. diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 337629b..9690fbd 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -16,6 +16,14 @@ * $settings['extension_discovery_scan_tests'] = TRUE; * @endcode * to your settings.php. + * + * To add additional profile directories, add + * @code + * $settings['profile_directories'] = array(path); + * @encode + * to your settings.php. If multiple paths are specified, they are searched + * from last to first. + * */ class ExtensionDiscovery { @@ -248,8 +256,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); + foreach ($this->getProfileDependencies($profile) as $profile_dependency => $profile_dependency_info) { + $this->profileDirectories[] = drupal_get_path('profile', $profile_dependency); + } } + + // Allow additional profile directories to be added from settings.php. + // This provides support for "base profiles". + $this->profileDirectories = array_merge(Settings::get('profile_directories', []), $this->profileDirectories); + return $this; } @@ -265,6 +280,40 @@ public function getProfileDirectories() { } /** + * Returns a list of related installation profiles. + * + * @param $profile + * Name of profile. + * + * @return array + * List of dependent installation profiles in descending order of their + * dependencies. + */ + public function getProfileDependencies($profile) { + $cache = &drupal_static(__FUNCTION__, array()); + if (!isset($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 = \Drupal::service('info_parser')->parse($profile_file); + + // Check of the profile has a base profile and if so add it - recursion. + // @TODO do we need a endless-loop protection? + if (!empty($profile_info['base_profile']['name'])) { + $profiles += $this->getProfileDependencies($profile_info['base_profile']['name']); + } + // Add requested profile as last one. + $profiles[$profile] = $profile; + } + $cache[$profile] = $profiles; + } + return $cache[$profile]; + } + + /** * Sets explicit profile directories to scan. * * @param array $paths