Verified Commit f8090e61 authored by Dave Long's avatar Dave Long
Browse files

Issue #3416522 by catch, alexpott, longwave, phenaproxima, nicxvan, wim leers,...

Issue #3416522 by catch, alexpott, longwave, phenaproxima, nicxvan, wim leers, smustgrave, larowlan, berdir, godotislate, dww: Add the ability to install multiple modules and only do a single container rebuild to ModuleInstaller
parent 69dc12e1
Loading
Loading
Loading
Loading
Loading
+0 −6
Original line number Diff line number Diff line
@@ -3241,12 +3241,6 @@
	'count' => 1,
	'path' => __DIR__ . '/lib/Drupal/Core/Config/ConfigImporter.php',
];
$ignoreErrors[] = [
	// identifier: missingType.return
	'message' => '#^Method Drupal\\\\Core\\\\Config\\\\ConfigImporter\\:\\:processExtension\\(\\) has no return type specified\\.$#',
	'count' => 1,
	'path' => __DIR__ . '/lib/Drupal/Core/Config/ConfigImporter.php',
];
$ignoreErrors[] = [
	// identifier: missingType.return
	'message' => '#^Method Drupal\\\\Core\\\\Config\\\\ConfigImporter\\:\\:processExtensions\\(\\) has no return type specified\\.$#',
+35 −10
Original line number Diff line number Diff line
@@ -1608,10 +1608,32 @@ function install_profile_modules(&$install_state) {
  arsort($non_required);

  $batch_builder = new BatchBuilder();
  foreach ($required + $non_required as $module => $weight) {

  // Put modules into groups of up to the maximum batch size, or until a module
  // states that it needs a container rebuild immediately after install.
  $index = 0;
  $module_groups = [];
  foreach (array_keys($required + $non_required) as $module) {
    $module_groups[$index][] = $module;
    if (count($module_groups[$index]) === Settings::get('core.multi_module_install_batch_size', 20)) {
      $index++;
    }
    // @todo Consider reversing this logic so that modules must explicitly
    // state that they need a container build.
    // @see https://www.drupal.org/project/drupal/issues/3492235
    elseif (!isset($files[$module]->info['container_rebuild_required']) || $files[$module]->info['container_rebuild_required']) {
      $index++;
    }
  }

  foreach ($module_groups as $module_group) {
    $names = [];
    foreach ($module_group as $module) {
      $names[] = $files[$module]->info['name'];
    }
    $batch_builder->addOperation(
      '_install_module_batch',
      [$module, $files[$module]->info['name']],
      [$module_group, $names],
    );
  }
  $batch_builder
@@ -1912,10 +1934,10 @@ function install_finished(&$install_state) {
 *
 * Performs batch installation of modules.
 */
function _install_module_batch($module, $module_name, &$context) {
  \Drupal::service('module_installer')->install([$module], FALSE);
  $context['results'][] = $module;
  $context['message'] = t('Installed %module module.', ['%module' => $module_name]);
function _install_module_batch(array $modules, array $module_names, array &$context) {
  \Drupal::service('module_installer')->install($modules, FALSE);
  $context['results'] = array_merge($context['results'], $modules);
  $context['message'] = \Drupal::translation()->formatPlural(count($module_names), 'Installed %module module.', 'Installed %module modules.', ['%module' => implode(', ', $module_names)]);
}

/**
@@ -2602,12 +2624,15 @@ function install_recipe_required_modules() {
  // The system module is already installed. See install_base_system().
  unset($required['system']);

  $modules = [];
  $names = [];
  foreach ($required as $module => $weight) {
    $batch_builder->addOperation(
      '_install_module_batch',
      [$module, $files[$module]->info['name']],
    );
    $modules[] = $module;
    $names[] = $files[$module]->info['name'];
  }
  $batch_builder->addOperation('_install_module_batch',
    [$modules, $names]
  );
  return $batch_builder->toArray();
}

+24 −12
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
@@ -366,11 +367,12 @@ public function getProcessedExtensions() {
   *   The type of extension, either 'theme' or 'module'.
   * @param string $op
   *   The change operation performed, either install or uninstall.
   * @param string $name
   *   The name of the extension processed.
   * @param string|array $name
   *   The name or names of the extension(s) processed.
   */
  protected function setProcessedExtension($type, $op, $name) {
    $this->processedExtensions[$type][$op][] = $name;
    $name = (array) $name;
    $this->processedExtensions[$type][$op] = array_merge($this->processedExtensions[$type][$op], $name);
  }

  /**
@@ -627,7 +629,11 @@ protected function processExtensions(&$context) {
    $operation = $this->getNextExtensionOperation();
    if (!empty($operation)) {
      $this->processExtension($operation['type'], $operation['op'], $operation['name']);
      $context['message'] = $this->t('Synchronizing extensions: @op @name.', ['@op' => $operation['op'], '@name' => $operation['name']]);
      $names = implode(', ', (array) $operation['name']);
      $context['message'] = match ($operation['op']) {
        'install' => $this->t('Synchronizing extensions: installed @name.', ['@name' => $names]),
        'uninstall' => $this->t('Synchronizing extensions: uninstalled @name.', ['@name' => $names]),
      };
      $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
      $processed_count += count($this->processedExtensions['theme']['uninstall']) + count($this->processedExtensions['theme']['install']);
      $context['finished'] = $processed_count / $this->totalExtensionsToProcess;
@@ -750,10 +756,16 @@ protected function getNextExtensionOperation() {
      foreach ($types as $type) {
        $unprocessed = $this->getUnprocessedExtensions($type);
        if (!empty($unprocessed[$op])) {
          if ($type === 'module' && $op === 'install') {
            $name = array_slice($unprocessed[$op], 0, Settings::get('core.multi_module_install_batch_size', 20));
          }
          else {
            $name = array_shift($unprocessed[$op]);
          }
          return [
            'op' => $op,
            'type' => $type,
            'name' => array_shift($unprocessed[$op]),
            'name' => $name,
          ];
        }
      }
@@ -865,16 +877,17 @@ protected function processConfiguration($collection, $op, $name) {
   *   The type of extension, either 'module' or 'theme'.
   * @param string $op
   *   The change operation.
   * @param string $name
   *   The name of the extension to process.
   * @param string|array $names
   *   The name or names of the extension(s) to process.
   */
  protected function processExtension($type, $op, $name) {
  protected function processExtension(string $type, string $op, string|array $names): void {
    $names = (array) $names;
    // Set the config installer to use the sync directory instead of the
    // extensions own default config directories.
    \Drupal::service('config.installer')
      ->setSourceStorage($this->storageComparer->getSourceStorage());
    if ($type == 'module') {
      $this->moduleInstaller->$op([$name], FALSE);
      $this->moduleInstaller->$op($names, FALSE);
      // Installing a module can cause a kernel boot therefore inject all the
      // services again.
      $this->reInjectMe();
@@ -893,10 +906,9 @@ protected function processExtension($type, $op, $name) {
        $this->configManager->getConfigFactory()->reset('system.theme');
        $this->processedSystemTheme = TRUE;
      }
      \Drupal::service('theme_installer')->$op([$name]);
      \Drupal::service('theme_installer')->$op($names);
    }

    $this->setProcessedExtension($type, $op, $name);
    $this->setProcessedExtension($type, $op, $names);
  }

  /**
+142 −77
Original line number Diff line number Diff line
@@ -105,7 +105,7 @@ public function __construct(ConfigFactoryInterface $config_factory, StorageInter
  /**
   * {@inheritdoc}
   */
  public function installDefaultConfig($type, $name) {
  public function installDefaultConfig($type, $name, DefaultConfigMode $mode = DefaultConfigMode::All) {
    $extension_path = $this->extensionPathResolver->getPath($type, $name);
    // Refresh the schema cache if the extension provides configuration schema
    // or is a theme.
@@ -113,6 +113,7 @@ public function installDefaultConfig($type, $name) {
      $this->typedConfig->clearCachedDefinitions();
    }

    if ($mode->createInstallConfig()) {
      $default_install_path = $this->getDefaultConfigDirectory($type, $name);
      if (is_dir($default_install_path)) {
        if (!$this->isSyncing()) {
@@ -131,11 +132,32 @@ public function installDefaultConfig($type, $name) {
        // Gets profile storages to search for overrides if necessary.
        $profile_storages = $this->getProfileStorages($name);

        if ($mode === DefaultConfigMode::InstallEntities) {
          // This is an optimization. If we're installing only config entities
          // then we're only interested in the default collection.
          $collections = [StorageInterface::DEFAULT_COLLECTION];
        }
        else {
          // Gather information about all the supported collections.
      $collection_info = $this->configManager->getConfigCollectionInfo();
      foreach ($collection_info->getCollectionNames() as $collection) {
          $collections = $this->configManager->getConfigCollectionInfo()->getCollectionNames();
        }

        foreach ($collections as $collection) {
          $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storages);
        if ($name == $this->drupalGetProfile()) {

          if ($collection === StorageInterface::DEFAULT_COLLECTION && ($mode === DefaultConfigMode::InstallEntities || $mode === DefaultConfigMode::InstallSimple)) {
            // Filter out config depending on the mode. The mode can be used to
            // only install simple config or config entities.
            $config_to_create = array_filter($config_to_create, function ($config_name) use ($mode) {
              $is_config_entity = $this->configManager->getEntityTypeIdByName($config_name) !== NULL;
              if ($is_config_entity) {
                return $mode === DefaultConfigMode::InstallEntities;
              }
              return $mode === DefaultConfigMode::InstallSimple;
            }, ARRAY_FILTER_USE_KEY);
          }

          if ($name === $this->drupalGetProfile()) {
            // If we're installing a profile ensure simple configuration that
            // already exists is excluded as it will have already been written.
            // This means that if the configuration is changed by something else
@@ -145,12 +167,15 @@ public function installDefaultConfig($type, $name) {
            });
            $config_to_create = array_diff_key($config_to_create, array_flip($existing_configuration));
          }

          if (!empty($config_to_create)) {
            $this->createConfiguration($collection, $config_to_create);
          }
        }
      }
    }

    if ($mode->createOptionalConfig()) {
      // During a drupal installation optional configuration is installed at the
      // end of the installation process. Once the install profile is installed
      // optional configuration should be installed as usual.
@@ -163,12 +188,23 @@ public function installDefaultConfig($type, $name) {
          $storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION);
          $this->installOptionalConfig($storage, '');
        }
      }
    }

    if ($mode->createSiteOptionalConfig()) {
      // During a drupal installation optional configuration is installed at the
      // end of the installation process. Once the install profile is installed
      // optional configuration should be installed as usual.
      // @see install_install_profile()
      $profile_installed = in_array($this->drupalGetProfile(), $this->getEnabledExtensions(), TRUE);
      if (!$this->isSyncing() && (!InstallerKernel::installationAttempted() || $profile_installed)) {
        // Install any optional configuration entities whose dependencies can now
        // be met. This searches all the installed modules config/optional
        // directories.
        $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE, $this->installProfile);
        $this->installOptionalConfig($storage, [$type => $name]);
      }
    }

    // Reset all the static caches and list caches.
    $this->configFactory->reset();
@@ -370,6 +406,7 @@ protected function createConfiguration($collection, array $config_to_create) {
        if ($this->isSyncing()) {
          continue;
        }

        /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage */
        $entity_storage = $this->configManager
          ->getEntityTypeManager()
@@ -478,11 +515,16 @@ public function isSyncing() {
   *   it and the extension has been uninstalled and is about to the
   *   reinstalled.
   *
   * @param \Drupal\Core\Config\StorageInterface $storage
   *   The storage containing the default configuration.
   * @param $previous_config_names
   *   An array of configuration names that have previously been checked.
   *
   * @return array
   *   Array of configuration object names that already exist keyed by
   *   collection.
   */
  protected function findPreExistingConfiguration(StorageInterface $storage) {
  protected function findPreExistingConfiguration(StorageInterface $storage, array $previous_config_names = []) {
    $existing_configuration = [];
    // Gather information about all the supported collections.
    $collection_info = $this->configManager->getConfigCollectionInfo();
@@ -491,7 +533,7 @@ protected function findPreExistingConfiguration(StorageInterface $storage) {
      $config_to_create = array_keys($this->getConfigToCreate($storage, $collection));
      $active_storage = $this->getActiveStorages($collection);
      foreach ($config_to_create as $config_name) {
        if ($active_storage->exists($config_name)) {
        if ($active_storage->exists($config_name) || array_search($config_name, $previous_config_names[$collection] ?? [], TRUE) !== FALSE) {
          $existing_configuration[$collection][] = $config_name;
        }
      }
@@ -508,36 +550,57 @@ public function checkConfigurationToInstall($type, $name) {
      // validation events.
      return;
    }
    $names = (array) $name;
    $enabled_extensions = $this->getEnabledExtensions();
    $previous_config_names = [];

    foreach ($names as $name) {
      // Add the extension that will be enabled to the list of enabled extensions.
      $enabled_extensions[] = $name;

      $config_install_path = $this->getDefaultConfigDirectory($type, $name);
      if (!is_dir($config_install_path)) {
      return;
        continue;
      }

      $storage = new FileStorage($config_install_path, StorageInterface::DEFAULT_COLLECTION);

    $enabled_extensions = $this->getEnabledExtensions();
    // Add the extension that will be enabled to the list of enabled extensions.
    $enabled_extensions[] = $name;
      // Gets profile storages to search for overrides if necessary.
      $profile_storages = $this->getProfileStorages($name);

      // Check the dependencies of configuration provided by the module.
    [$invalid_default_config, $missing_dependencies] = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storages);
      [
        $invalid_default_config,
        $missing_dependencies,
      ] = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storages, $previous_config_names);
      if (!empty($invalid_default_config)) {
        throw UnmetDependenciesException::create($name, array_unique($missing_dependencies, SORT_REGULAR));
      }

      // 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()) {
        // 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.
      $existing_configuration = $this->findPreExistingConfiguration($storage);
        $existing_configuration = $this->findPreExistingConfiguration($storage, $previous_config_names);
        if (!empty($existing_configuration)) {
          throw PreExistingConfigException::create($name, $existing_configuration);
        }
      }

      // Store the config names for the checked module in order to add them to
      // the list of active configuration for the next module.
      foreach ($this->configManager->getConfigCollectionInfo()->getCollectionNames() as $collection) {
        $config_to_create = array_keys($this->getConfigToCreate($storage, $collection));
        if (!isset($previous_config_names[$collection])) {
          $previous_config_names[$collection] = $config_to_create;
        }
        else {
          $previous_config_names[$collection] = array_merge($previous_config_names[$collection], $config_to_create);
        }
      }
    }
  }

  /**
@@ -550,6 +613,8 @@ public function checkConfigurationToInstall($type, $name) {
   * @param \Drupal\Core\Config\StorageInterface[] $profile_storages
   *   An array of storage interfaces containing profile configuration to check
   *   for overrides.
   * @param string[][] $previously_checked_config
   *   A list of previously checked configuration. Keyed by collection name.
   *
   * @return array
   *   An array containing:
@@ -557,10 +622,10 @@ public function checkConfigurationToInstall($type, $name) {
   *     - An array that will be filled with the missing dependency names, keyed
   *       by the dependents' names.
   */
  protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, array $profile_storages = []) {
  protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, array $profile_storages = [], array $previously_checked_config = []) {
    $missing_dependencies = [];
    $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storages);
    $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create));
    $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create), $previously_checked_config[StorageInterface::DEFAULT_COLLECTION] ?? []);
    foreach ($config_to_create as $config_name => $config) {
      if ($missing = $this->getMissingDependencies($config_name, $config, $enabled_extensions, $all_config)) {
        $missing_dependencies[$config_name] = $missing;
+7 −3
Original line number Diff line number Diff line
@@ -27,10 +27,14 @@ interface ConfigInstallerInterface {
   *   The extension type; e.g., 'module' or 'theme'.
   * @param string $name
   *   The name of the module or theme to install default configuration for.
   * @param \Drupal\Core\Config\DefaultConfigMode $mode
   *   The default value DefaultConfigMode::All means create install, optional
   *   and site optional configuration. The other modes create a single type
   *   config.
   *
   * @see \Drupal\Core\Config\ExtensionInstallStorage
   */
  public function installDefaultConfig($type, $name);
  public function installDefaultConfig($type, $name, DefaultConfigMode $mode = DefaultConfigMode::All);

  /**
   * Installs optional configuration.
@@ -108,8 +112,8 @@ public function isSyncing();
   *
   * @param string $type
   *   Type of extension to install.
   * @param string $name
   *   Name of extension to install.
   * @param string|array $name
   *   Name or names of extensions to install.
   *
   * @throws \Drupal\Core\Config\UnmetDependenciesException
   * @throws \Drupal\Core\Config\PreExistingConfigException
Loading