diff --git a/core/core.services.yml b/core/core.services.yml index 6310c21..2925d99 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -298,7 +298,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 @@ -506,6 +506,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/bootstrap.inc b/core/includes/bootstrap.inc index ec12097..e9c5595 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -231,6 +231,10 @@ function drupal_get_filename($type, $name, $filename = NULL) { } // If still unknown, create a user-level error message. if (!isset($files[$type][$name])) { + $trace = debug_backtrace(); + foreach ($trace as $key => $value) { + print "$key " . $value['function'] . ' : ' . $value['line'] . "\n"; + } trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', array('@type' => $type, '@name' => $name)), E_USER_WARNING); } } diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 1019270..9808188 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -439,6 +439,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 @@ -1550,7 +1556,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 3a9c2bc..152a911 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -11,6 +11,8 @@ use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\ModuleHandler; +use Drupal\Core\Extension\ProfileHandler; use Drupal\Core\Site\Settings; /** @@ -1029,6 +1031,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 @@ -1054,18 +1064,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 97c3688..ad29260 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; /** + * The profile handler. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + 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,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 14e80dd..14a9ba7 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -86,14 +86,6 @@ 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])) { - // 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()); - } $module_list_scan = $listing->scan('module'); $module_list = array(); foreach (array_keys($modules) as $module) { @@ -114,15 +106,15 @@ 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. + // 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. if (isset($profile)) { - if (!isset($profile_list)) { - $profile_list = $listing->scan('profile'); - } - if (isset($profile_list[$profile])) { - $profile_folders = $this->getComponentNames(array($profile_list[$profile])); + // Get the profile and any parents. + $profiles = \Drupal::service('profile_handler')->getProfiles($profile); + foreach ($profiles as $extension) { + $profile_folders = $this->getComponentNames(array($extension)); $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..80d370d 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -151,21 +151,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(\Drupal::service('profile_handler')->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 0d9283c..cef6af0 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -5,6 +5,7 @@ use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Core\DrupalKernel; use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator; +use Drupal\Core\Extension\ProfileHandlerInterface; use Drupal\Core\Site\Settings; use Symfony\Component\HttpFoundation\Request; @@ -99,6 +100,13 @@ class ExtensionDiscovery { protected $sitePath; /** + * The profile handler object. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Constructs a new ExtensionDiscovery object. * * @param string $root @@ -109,12 +117,20 @@ class ExtensionDiscovery { * The available profile directories * @param string $site_path * The path to the site. + * @param Drupal\Core\Extension\ProfileHandlerInterface + * The Profile Handler instance to use. */ - 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, $profile_handler = NULL) { $this->root = $root; $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL; $this->profileDirectories = $profile_directories; $this->sitePath = $site_path; + if (!isset($profile_handler) && \Drupal::hasService('profile_handler')) { + $this->profileHandler = \Drupal::service('profile_handler'); + } + else { + $this->profileHandler = $profile_handler; + } } /** @@ -248,8 +264,20 @@ 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); + // ExtensionDiscovery can be called without a service container. + // (@drupalKernel::moduleData) so check if profile_handler is available. + if (isset($this->profileHandler)) { + $profiles = $this->profileHandler->getProfiles($profile); + $profile_directories = array_map(function($extension) { + return $extension->getPath(); + }, $profiles); + $this->profileDirectories = array_merge($profile_directories, $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..ad3c6d7 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandler.php @@ -0,0 +1,259 @@ +root = $root; + $this->infoParser = $info_parser; + } + + /** + * 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 + * Name of 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->scan_cache && !isset($modules_cache)) { + $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'); + 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->scan_cache = 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->info_cache[$profile])) { + + // Set defaults for profile info. + $defaults = array( + 'dependencies' => array(), + 'themes' => array('stark'), + 'description' => '', + 'version' => NULL, + 'hidden' => FALSE, + 'php' => DRUPAL_MINIMUM_PHP, + ); + + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $info = $this->infoParser->parse($profile_file); + $info += $defaults; + + $profile_list = array(); + // Get the base profile dependencies. + $base_profile_name = ProfileHandler::getProfileBaseName($info); + if ($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']); + // 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)); + } + $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; + + // Add a default distribution name if the profile did not provide one. + // @see install_profile_info() + // @see drupal_install_profile_distribution_name() + if (!isset($info['distribution']['name'])) { + $info['distribution']['name'] = 'Drupal'; + } + + $this->info_cache[$profile] = $info; + } + return $this->info_cache[$profile]; + } + + /** + * 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. + * info_defaults: Additional $info key/values to be added to the parsed + * info array in System::_system_rebuild_module_data(). + * weight: The weight for running hooks (ala module weight). + * origin: The directory origin as used in ExtensionDiscovery. + * + * @todo remove need for info_defaults + */ + 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); + + $info_defaults = array(); + + // Installation profiles are hidden by default, unless explicitly specified + // otherwise in the .info.yml file. + $info_defaults['hidden'] = isset($profile_info['hidden']) ? $profile_info['hidden'] : TRUE; + + $extension->info = $profile_info + $info_defaults; + $extension->origin = ''; + + // Pass needed defaults to info separately so + // _system_rebuild_module_data can merge it with the parsed info. + // @todo Determine why _system_rebuild_module_data needs freshly + // parsed info data and cannot use cached info. See issue: + // https://www.drupal.org/node/1356276#comment-11464109. + $extension->info_defaults = $info_defaults; + + 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->cache[$profile])) { + $profiles = array(); + // 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 = array(); + foreach ($list as $profile_name) { + $extension = $this->getProfileExtension($profile_name); + $extension->weight = $weight; + $weight++; + $profiles[$profile_name] = $extension; + } + } + $this->cache[$profile] = $profiles; + } + return $this->cache[$profile]; + } + + /** + * {@inheritdoc} + */ + static function getProfileBaseName($info) { + return !empty($info['base profile']['name']) + ? $info['base profile']['name'] + : (!empty($info['base profile']) ? $info['base 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..454d6e0 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php @@ -0,0 +1,63 @@ +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. + $profile_handler = \Drupal::service('profile_handler'); + $modules = array_merge($modules, $profile_handler->getProfiles()); // Set defaults for module info. $defaults = array( @@ -993,7 +978,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 each extension handler should be responsible for loading the + // info data. For now we just have a ProfileHandler. + 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. @@ -1002,12 +994,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(). @@ -1021,16 +1007,11 @@ 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; - // Add a default distribution name if the profile did not provide one. - // @see install_profile_info() - // @see drupal_install_profile_distribution_name() - if (!isset($modules[$profile]->info['distribution']['name'])) { - $modules[$profile]->info['distribution']['name'] = 'Drupal'; - } } return $modules;