diff --git a/core/core.services.yml b/core/core.services.yml
index 99f245d48d9e6ce780382ac70b38d1d958b021b7..0fc93c146c8bfb52d993d574541c982a4d29d7ac 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -505,6 +505,12 @@ services:
       - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
     arguments: ['@app.root', '@module_handler', '@kernel', '@router.builder']
     lazy: true
+  extension.list.module:
+    class: Drupal\Core\Extension\ModuleExtensionList
+    arguments: ['@app.root', 'module', '@cache.default', '@info_parser', '@module_handler', '@state', '@config.factory', '@extension.list.profile', '%install_profile%', '%container.modules%']
+  extension.list.profile:
+    class: Drupal\Core\Extension\ProfileExtensionList
+    arguments: ['@app.root', 'profile', '@cache.default', '@info_parser', '@module_handler', '@state', '%install_profile%']
   content_uninstall_validator:
     class: Drupal\Core\Entity\ContentUninstallValidator
     tags:
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 462283a14d1ce5a2eaa060504def26d627f10e02..a67d7ff654e95f3be278f626cdfc157cd6e020b4 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -226,43 +226,45 @@ function drupal_get_filename($type, $name, $filename = NULL) {
     return 'core/core.info.yml';
   }
 
-  // Profiles are converted into modules in system_rebuild_module_data().
-  // @todo Remove false-exposure of profiles as modules.
-  if ($type == 'profile') {
-    $type = 'module';
-  }
-  if (!isset($files[$type])) {
-    $files[$type] = [];
+  if ($type === 'module' || $type === 'profile') {
+    $service_id = 'extension.list.' . $type;
+    /** @var \Drupal\Core\Extension\ExtensionList $extension_list */
+    $extension_list = \Drupal::service($service_id);
+    if (isset($filename)) {
+      // Manually add the info file path of an extension.
+      $extension_list->setPathname($name, $filename);
+    }
+    try {
+      return $extension_list->getPathname($name);
+    }
+    catch (\InvalidArgumentException $e) {
+      // Catch the exception. This will result in triggering an error.
+    }
   }
+  else {
 
-  if (isset($filename)) {
-    $files[$type][$name] = $filename;
-  }
-  elseif (!isset($files[$type][$name])) {
-    // If the pathname of the requested extension is not known, try to retrieve
-    // the list of extension pathnames from various providers, checking faster
-    // providers first.
-    // Retrieve the current module list (derived from the service container).
-    if ($type == 'module' && \Drupal::hasService('module_handler')) {
-      foreach (\Drupal::moduleHandler()->getModuleList() as $module_name => $module) {
-        $files[$type][$module_name] = $module->getPathname();
-      }
+    if (!isset($files[$type])) {
+      $files[$type] = [];
     }
-    // If still unknown, retrieve the file list prepared in state by
-    // system_rebuild_module_data() and
-    // \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData().
-    if (!isset($files[$type][$name]) && \Drupal::hasService('state')) {
-      $files[$type] += \Drupal::state()->get('system.' . $type . '.files', []);
+
+    if (isset($filename)) {
+      $files[$type][$name] = $filename;
     }
-    // If still unknown, create a user-level error message.
-    if (!isset($files[$type][$name])) {
-      trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', ['@type' => $type, '@name' => $name]), E_USER_WARNING);
+    elseif (!isset($files[$type][$name])) {
+      // If still unknown, retrieve the file list prepared in state by
+      // \Drupal\Core\Extension\ExtensionList() and
+      // \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData().
+      if (!isset($files[$type][$name]) && \Drupal::hasService('state')) {
+        $files[$type] += \Drupal::state()->get('system.' . $type . '.files', []);
+      }
     }
-  }
 
-  if (isset($files[$type][$name])) {
-    return $files[$type][$name];
+    if (isset($files[$type][$name])) {
+      return $files[$type][$name];
+    }
   }
+  // If the filename is still unknown, create a user-level error message.
+  trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', ['@type' => $type, '@name' => $name]), E_USER_WARNING);
 }
 
 /**
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 6d50c81a7ff813eb5c7fc9f64ac78013be6ba221..b4c731da3ca321dc97e89fd13b5d4cf718da8639 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -302,12 +302,6 @@ function install_begin_request($class_loader, &$install_state) {
   // Allow command line scripts to override server variables used by Drupal.
   require_once __DIR__ . '/bootstrap.inc';
 
-  // Before having installed the system module and being able to do a module
-  // rebuild, prime the drupal_get_filename() static cache with the module's
-  // exact location.
-  // @todo Remove as part of https://www.drupal.org/node/2186491
-  drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml');
-
   // If the hash salt leaks, it becomes possible to forge a valid testing user
   // agent, install a new copy of Drupal, and take over the original site.
   // The user agent header is used to pass a database prefix in the request when
@@ -414,6 +408,7 @@ function install_begin_request($class_loader, &$install_state) {
   }
   else {
     $environment = 'prod';
+    $GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\NormalInstallerServiceProvider';
   }
   $GLOBALS['conf']['container_service_providers']['InstallerConfigOverride'] = 'Drupal\Core\Installer\ConfigOverride';
 
@@ -444,6 +439,9 @@ function install_begin_request($class_loader, &$install_state) {
   // Prime drupal_get_filename()'s static cache.
   foreach ($install_state['profiles'] as $name => $profile) {
     drupal_get_filename('profile', $name, $profile->getPathname());
+    // drupal_get_filename() is called both with 'module' and 'profile', see
+    // \Drupal\Core\Config\ConfigInstaller::getProfileStorages for example.
+    drupal_get_filename('module', $name, $profile->getPathname());
   }
 
   if ($profile = _install_select_profile($install_state)) {
@@ -454,6 +452,12 @@ function install_begin_request($class_loader, &$install_state) {
     }
   }
 
+  // Before having installed the system module and being able to do a module
+  // rebuild, prime the drupal_get_filename() static cache with the system
+  // module's location.
+  // @todo Remove as part of https://www.drupal.org/node/2186491
+  drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml');
+
   // Use the language from the profile configuration, if available, to override
   // the language previously set in the parameters.
   if (isset($install_state['profile_info']['distribution']['langcode'])) {
diff --git a/core/includes/install.inc b/core/includes/install.inc
index c757e30f05ad97a8ed6b38aa31c7b029ff2b7db1..399476f102f2c19331daf74065023eba540822f9 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -612,6 +612,8 @@ function drupal_verify_profile($install_state) {
 function drupal_install_system($install_state) {
   // Remove the service provider of the early installer.
   unset($GLOBALS['conf']['container_service_providers']['InstallerServiceProvider']);
+  // Add the normal installer service provider.
+  $GLOBALS['conf']['container_service_providers']['InstallerServiceProvider'] = 'Drupal\Core\Installer\NormalInstallerServiceProvider';
 
   $request = \Drupal::request();
   // Reboot into a full production environment to continue the installation.
@@ -622,6 +624,13 @@ function drupal_install_system($install_state) {
   $kernel->rebuildContainer(FALSE);
   $kernel->prepareLegacyRequest($request);
 
+  // Before having installed the system module and being able to do a module
+  // rebuild, prime the \Drupal\Core\Extension\ModuleExtensionList static cache
+  // with the module's location.
+  // @todo Try to install system as any other module, see
+  //   https://www.drupal.org/node/2719315.
+  \Drupal::service('extension.list.module')->setPathname('system', 'core/modules/system/system.info.yml');
+
   // Install base system configuration.
   \Drupal::service('config.installer')->installDefaultConfig('core', 'core');
 
diff --git a/core/includes/module.inc b/core/includes/module.inc
index e3f446c66089708f3ea90687868157426784d918..63b3abc5ef14a4eae1109f5219900f2fcba68d89 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -57,7 +57,7 @@ function system_list($type) {
  */
 function system_list_reset() {
   drupal_static_reset('system_list');
-  drupal_static_reset('system_rebuild_module_data');
+  \Drupal::service('extension.list.module')->reset();
   \Drupal::cache('bootstrap')->delete('system_list');
 }
 
diff --git a/core/lib/Drupal/Core/Extension/ExtensionList.php b/core/lib/Drupal/Core/Extension/ExtensionList.php
new file mode 100644
index 0000000000000000000000000000000000000000..6201a08a1150cc72bcbb302b4361bef27ef8ae00
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ExtensionList.php
@@ -0,0 +1,543 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\State\StateInterface;
+
+/**
+ * Provides available extensions.
+ *
+ * The extension list is per extension type, like module, theme and profile.
+ */
+abstract class ExtensionList {
+
+  /**
+   * The type of the extension: "module", "theme" or "profile".
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $root;
+
+  /**
+   * The cache.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * Default values to be merged into *.info.yml file arrays.
+   *
+   * @var mixed[]
+   */
+  protected $defaults = [];
+
+  /**
+   * The info parser.
+   *
+   * @var \Drupal\Core\Extension\InfoParserInterface
+   */
+  protected $infoParser;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The cached extensions.
+   *
+   * @var \Drupal\Core\Extension\Extension[]|null
+   */
+  protected $extensions;
+
+  /**
+   * Static caching for extension info.
+   *
+   * Access this property's value through static::getAllInfo().
+   *
+   * @var array[]|null
+   *   Keys are extension names, and values their info arrays (mixed[]).
+   *
+   * @see \Drupal\Core\Extension\ExtensionList::getAllAvailableInfo
+   */
+  protected $extensionInfo;
+
+  /**
+   * A list of extension folder names keyed by extension name.
+   *
+   * @var string[]|null
+   */
+  protected $pathNames;
+
+  /**
+   * A list of extension folder names directly added in code (not discovered).
+   *
+   * It is important to keep a separate list to ensure that it takes priority
+   * over the discovered extension folders.
+   *
+   * @var string[]
+   *
+   * @internal
+   */
+  protected $addedPathNames = [];
+
+  /**
+   * The state store.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * The install profile used by the site.
+   *
+   * @var string
+   */
+  protected $installProfile;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param string $root
+   *   The app root.
+   * @param string $type
+   *   The extension type.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache.
+   * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
+   *   The info parser.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state.
+   * @param string $install_profile
+   *   The install profile used by the site.
+   */
+  public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, $install_profile) {
+    $this->root = $root;
+    $this->type = $type;
+    $this->cache = $cache;
+    $this->infoParser = $info_parser;
+    $this->moduleHandler = $module_handler;
+    $this->state = $state;
+    $this->installProfile = $install_profile;
+  }
+
+  /**
+   * Returns the extension discovery.
+   *
+   * @return \Drupal\Core\Extension\ExtensionDiscovery
+   */
+  protected function getExtensionDiscovery() {
+    return new ExtensionDiscovery($this->root);
+  }
+
+  /**
+   * Resets the stored extension list.
+   *
+   * We don't reset statically added filenames, as it is a static cache which
+   * logically can't change. This is done for performance reasons of the
+   * installer.
+   */
+  public function reset() {
+    $this->extensions = NULL;
+    $this->cache->delete($this->getListCacheId());
+    $this->extensionInfo = NULL;
+    $this->cache->delete($this->getInfoCacheId());
+    $this->pathNames = NULL;
+
+    try {
+      $this->state->delete($this->getPathnamesCacheId());
+    }
+    catch (DatabaseExceptionWrapper $e) {
+      // Ignore exceptions caused by a non existing {key_value} table in the
+      // early installer.
+    }
+
+    $this->cache->delete($this->getPathnamesCacheId());
+    // @todo In the long run it would be great to add the reset, but the early
+    //   installer fails due to that. https://www.drupal.org/node/2719315 could
+    //   help to resolve with that.
+    return $this;
+  }
+
+  /**
+   * Returns the extension list cache ID.
+   *
+   * @return string
+   *   The list cache ID.
+   */
+  protected function getListCacheId() {
+    return 'core.extension.list.' . $this->type;
+  }
+
+  /**
+   * Returns the extension info cache ID.
+   *
+   * @return string
+   *   The info cache ID.
+   */
+  protected function getInfoCacheId() {
+    return "system.{$this->type}.info";
+  }
+
+  /**
+   * Returns the extension filenames cache ID.
+   *
+   * @return string
+   *   The filename cache ID.
+   */
+  protected function getPathnamesCacheId() {
+    return "system.{$this->type}.files";
+  }
+
+  /**
+   * Determines if an extension exists in the filesystem.
+   *
+   * @param string $extension_name
+   *   The machine name of the extension.
+   *
+   * @return bool
+   *   TRUE if the extension exists (regardless installed or not) and FALSE if
+   *   not.
+   */
+  public function exists($extension_name) {
+    $extensions = $this->getList();
+    return isset($extensions[$extension_name]);
+  }
+
+  /**
+   * Returns the human-readable name of the extension.
+   *
+   * @param string $extension_name
+   *   The machine name of the extension.
+   *
+   * @return string
+   *   The human-readable name of the extension.
+   *
+   * @throws \InvalidArgumentException
+   *   If there is no extension with the supplied machine name.
+   */
+  public function getName($extension_name) {
+    return $this->get($extension_name)->info['name'];
+  }
+
+  /**
+   * Returns a single extension.
+   *
+   * @param string $extension_name
+   *   The machine name of the extension.
+   *
+   * @return \Drupal\Core\Extension\Extension
+   *   A processed extension object for the extension with the specified machine
+   *   name.
+   *
+   * @throws \InvalidArgumentException
+   *   If there is no extension with the supplied name.
+   */
+  public function get($extension_name) {
+    $extensions = $this->getList();
+    if (isset($extensions[$extension_name])) {
+      return $extensions[$extension_name];
+    }
+
+    throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist.");
+  }
+
+  /**
+   * Returns all available extensions.
+   *
+   * @return \Drupal\Core\Extension\Extension[]
+   *   Processed extension objects, keyed by machine name.
+   */
+  public function getList() {
+    if ($this->extensions !== NULL) {
+      return $this->extensions;
+    }
+    if ($cache = $this->cache->get($this->getListCacheId())) {
+      $this->extensions = $cache->data;
+      return $this->extensions;
+    }
+    $extensions = $this->doList();
+    $this->cache->set($this->getListCacheId(), $extensions);
+    $this->extensions = $extensions;
+    return $this->extensions;
+  }
+
+  /**
+   * Scans the available extensions.
+   *
+   * Overriding this method gives other code the chance to add additional
+   * extensions to this raw listing.
+   *
+   * @return \Drupal\Core\Extension\Extension[]
+   *   Unprocessed extension objects, keyed by machine name.
+   */
+  protected function doScanExtensions() {
+    return $this->getExtensionDiscovery()->scan($this->type);
+  }
+
+  /**
+   * Builds the list of extensions.
+   *
+   * @return \Drupal\Core\Extension\Extension[]
+   *   Processed extension objects, keyed by machine name.
+   *
+   * @throws \Drupal\Core\Extension\InfoParserException
+   *   If one of the .info.yml files is incomplete, or causes a parsing error.
+   */
+  protected function doList() {
+    // Find extensions.
+    $extensions = $this->doScanExtensions();
+
+    // Read info files for each extension.
+    foreach ($extensions as $extension_name => $extension) {
+      // Look for the info file.
+      $extension->info = $this->infoParser->parse($extension->getPathname());
+
+      // Add the info file modification time, so it becomes available for
+      // contributed extensions to use for ordering extension lists.
+      $extension->info['mtime'] = $extension->getMTime();
+
+      // Merge extension type-specific defaults.
+      $extension->info += $this->defaults;
+
+      // Invoke hook_system_info_alter() to give installed modules a chance to
+      // modify the data in the .info.yml files if necessary.
+      $this->moduleHandler->alter('system_info', $extension->info, $extension, $this->type);
+    }
+
+    return $extensions;
+  }
+
+  /**
+   * Returns information about a specified extension.
+   *
+   * This function returns the contents of the .info.yml file for the specified
+   * extension.
+   *
+   * @param string $extension_name
+   *   The name of an extension whose information shall be returned.
+   *
+   * @return mixed[]
+   *   An associative array of extension information.
+   *
+   * @throws \InvalidArgumentException
+   *   If there is no extension with the supplied name.
+   */
+  public function getExtensionInfo($extension_name) {
+    $all_info = $this->getAllInstalledInfo();
+    if (isset($all_info[$extension_name])) {
+      return $all_info[$extension_name];
+    }
+    throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist or is not installed.");
+  }
+
+  /**
+   * Returns an array of info files information of available extensions.
+   *
+   * This function returns the processed contents (with added defaults) of the
+   * .info.yml files.
+   *
+   * @return array[]
+   *   An associative array of extension information arrays, keyed by extension
+   *   name.
+   */
+  public function getAllAvailableInfo() {
+    if ($this->extensionInfo === NULL) {
+      $cache_id = $this->getInfoCacheId();
+      if ($cache = $this->cache->get($cache_id)) {
+        $info = $cache->data;
+      }
+      else {
+        $info = $this->recalculateInfo();
+        $this->cache->set($cache_id, $info);
+      }
+      $this->extensionInfo = $info;
+    }
+
+    return $this->extensionInfo;
+  }
+
+  /**
+   * Returns a list of machine names of installed extensions.
+   *
+   * @return string[]
+   *   The machine names of all installed extensions of this type.
+   */
+  abstract protected function getInstalledExtensionNames();
+
+  /**
+   * Returns an array of info files information of installed extensions.
+   *
+   * This function returns the processed contents (with added defaults) of the
+   * .info.yml files.
+   *
+   * @return array[]
+   *   An associative array of extension information arrays, keyed by extension
+   *   name.
+   */
+  public function getAllInstalledInfo() {
+    return array_intersect_key($this->getAllAvailableInfo(), array_flip($this->getInstalledExtensionNames()));
+  }
+
+  /**
+   * Generates the information from .info.yml files for extensions of this type.
+   *
+   * @return array[]
+   *   An array of arrays of .info.yml entries keyed by the machine name.
+   */
+  protected function recalculateInfo() {
+    return array_map(function (Extension $extension) {
+      return $extension->info;
+    }, $this->getList());
+  }
+
+  /**
+   * Returns a list of extension file paths keyed by machine name.
+   *
+   * @return string[]
+   */
+  public function getPathnames() {
+    if ($this->pathNames === NULL) {
+      $cache_id = $this->getPathnamesCacheId();
+      if ($cache = $this->cache->get($cache_id)) {
+        $path_names = $cache->data;
+      }
+      // We use $file_names below.
+      elseif (!$path_names = $this->state->get($cache_id)) {
+        $path_names = $this->recalculatePathnames();
+        // Store filenames to allow static::getPathname() to retrieve them
+        // without having to rebuild or scan the filesystem.
+        $this->state->set($cache_id, $path_names);
+        $this->cache->set($cache_id, $path_names);
+      }
+      $this->pathNames = $path_names;
+    }
+    return $this->pathNames;
+  }
+
+  /**
+   * Generates a sorted list of .info.yml file locations for all extensions.
+   *
+   * @return string[]
+   *   An array of .info.yml file locations keyed by the extension machine name.
+   */
+  protected function recalculatePathnames() {
+    $extensions = $this->getList();
+    ksort($extensions);
+
+    return array_map(function (Extension $extension) {
+      return $extension->getPathname();
+    }, $extensions);
+  }
+
+  /**
+   * Sets the pathname for an extension.
+   *
+   * This method is used in the Drupal bootstrapping phase, when the extension
+   * system is not fully initialized, to manually set locations of modules and
+   * profiles needed to complete bootstrapping.
+   *
+   * It is not recommended to call this method except in those rare cases.
+   *
+   * @param string $extension_name
+   *   The machine name of the extension.
+   * @param string $pathname
+   *   The pathname of the extension which is to be set explicitly rather
+   *   than by consulting the dynamic extension listing.
+   *
+   * @internal
+   *
+   * @see ::getPathname
+   */
+  public function setPathname($extension_name, $pathname) {
+    $this->addedPathNames[$extension_name] = $pathname;
+  }
+
+  /**
+   * Gets the info file path for an extension.
+   *
+   * The info path, whether provided, cached, or retrieved from the database, is
+   * only returned if the file exists.
+   *
+   * This function plays a key role in allowing Drupal's extensions (modules,
+   * themes, profiles, theme_engines, etc.) to be located in different places
+   * depending on a site's configuration. For example, a module 'foo' may
+   * legally be located in any of these four places:
+   *
+   * - core/modules/foo/foo.info.yml
+   * - modules/foo/foo.info.yml
+   * - sites/all/modules/foo/foo.info.yml
+   * - sites/example.com/modules/foo/foo.info.yml
+   *
+   * while a theme 'bar' may be located in any of the following four places:
+   *
+   * - core/themes/bar/bar.info.yml
+   * - themes/bar/bar.info.yml
+   * - sites/all/themes/bar/bar.info.yml
+   * - sites/example.com/themes/bar/bar.info.yml
+   *
+   * An installation profile maybe be located in any of the following places:
+   *
+   * - core/profiles/baz/baz.info.yml
+   * - profiles/baz/baz.info.yml
+   *
+   * Calling ExtensionList::getPathname('foo') will give you one of the above,
+   * depending on where the extension is located and what type it is.
+   *
+   * @param string $extension_name
+   *   The machine name of the extension for which the pathname is requested.
+   *
+   * @return string
+   *   The drupal-root relative filename and path of the requested extension's
+   *   .info.yml file.
+   *
+   * @throws \InvalidArgumentException
+   *   If there is no extension with the supplied machine name.
+   */
+  public function getPathname($extension_name) {
+    if (isset($this->addedPathNames[$extension_name])) {
+      return $this->addedPathNames[$extension_name];
+    }
+    elseif (isset($this->pathNames[$extension_name])) {
+      return $this->pathNames[$extension_name];
+    }
+    elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) {
+      return $path_names[$extension_name];
+    }
+    throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist.");
+  }
+
+  /**
+   * Gets the path to an extension of a specific type (module, theme, etc.).
+   *
+   * The path is the directory in which the .info file is located. This name is
+   * coming from \SplFileInfo.
+   *
+   * @param string $extension_name
+   *   The machine name of the extension for which the path is requested.
+   *
+   * @return string
+   *   The Drupal-root-relative path to the specified extension.
+   *
+   * @throws \InvalidArgumentException
+   *   If there is no extension with the supplied name.
+   */
+  public function getPath($extension_name) {
+    return dirname($this->getPathname($extension_name));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
new file mode 100644
index 0000000000000000000000000000000000000000..01fcf5909529ae74270542adb0ea9c65fa69f666
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php
@@ -0,0 +1,228 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Provides a list of available modules.
+ */
+class ModuleExtensionList extends ExtensionList {
+
+  use StringTranslationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaults = [
+    'dependencies' => [],
+    'description' => '',
+    'package' => 'Other',
+    'version' => NULL,
+    'php' => DRUPAL_MINIMUM_PHP,
+  ];
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The profile list needed by this module list.
+   *
+   * @var \Drupal\Core\Extension\ExtensionList
+   */
+  protected $profileList;
+
+  /**
+   * Constructs a new ModuleExtensionList instance.
+   *
+   * @param string $root
+   *   The app root.
+   * @param string $type
+   *   The extension type.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache.
+   * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
+   *   The info parser.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Extension\ExtensionList $profile_list
+   *   The site profile listing.
+   * @param string $install_profile
+   *   The install profile used by the site.
+   * @param array[] $container_modules_info
+   *   (optional) The module locations coming from the compiled container.
+   */
+  public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ExtensionList $profile_list, $install_profile, array $container_modules_info = []) {
+    parent::__construct($root, $type, $cache, $info_parser, $module_handler, $state, $install_profile);
+
+    $this->configFactory = $config_factory;
+    $this->profileList = $profile_list;
+
+    // Use the information from the container. This is an optimization.
+    foreach ($container_modules_info as $module_name => $info) {
+      $this->setPathname($module_name, $info['pathname']);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExtensionDiscovery() {
+    $discovery = parent::getExtensionDiscovery();
+
+    if ($active_profile = $this->getActiveProfile()) {
+      $discovery->setProfileDirectories($this->getProfileDirectories($discovery));
+    }
+
+    return $discovery;
+  }
+
+  /**
+   * Finds all installation profile paths.
+   *
+   * @param \Drupal\Core\Extension\ExtensionDiscovery $discovery
+   *   The extension discovery.
+   *
+   * @return string[]
+   *   Paths to all installation profiles.
+   */
+  protected function getProfileDirectories(ExtensionDiscovery $discovery) {
+    $discovery->setProfileDirectories([]);
+    $all_profiles = $discovery->scan('profile');
+    $active_profile = $all_profiles[$this->installProfile];
+    $profiles = array_intersect_key($all_profiles, $this->configFactory->get('core.extension')->get('module') ?: [$active_profile->getName() => 0]);
+
+    // If a module is within a profile directory but specifies another
+    // profile for testing, it needs to be found in the parent profile.
+    $parent_profile = $this->configFactory->get('simpletest.settings')->get('parent_profile');
+
+    if ($parent_profile && !isset($profiles[$parent_profile])) {
+      // In case both profile directories contain the same extension, the
+      // actual profile always has precedence.
+      $profiles = [$parent_profile => $all_profiles[$parent_profile]] + $profiles;
+    }
+
+    $profile_directories = array_map(function (Extension $profile) {
+      return $profile->getPath();
+    }, $profiles);
+    return $profile_directories;
+  }
+
+  /**
+   * Gets the processed active profile object, or null.
+   *
+   * @return \Drupal\Core\Extension\Extension|null
+   *   The active profile, if there is one.
+   */
+  protected function getActiveProfile() {
+    $profiles = $this->profileList->getList();
+    if ($this->installProfile && isset($profiles[$this->installProfile])) {
+      return $profiles[$this->installProfile];
+    }
+    return NULL;
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doScanExtensions() {
+    $extensions = parent::doScanExtensions();
+
+    $profiles = $this->profileList->getList();
+    // Modify the active profile object that was previously added to the module
+    // list.
+    if ($this->installProfile && isset($profiles[$this->installProfile])) {
+      $extensions[$this->installProfile] = $profiles[$this->installProfile];
+    }
+
+    return $extensions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doList() {
+    // Find modules.
+    $extensions = parent::doList();
+    // It is possible that a module was marked as required by
+    // hook_system_info_alter() and modules that it depends on are not required.
+    foreach ($extensions as $extension) {
+      $this->ensureRequiredDependencies($extension, $extensions);
+    }
+
+    // Add status, weight, and schema version.
+    $installed_modules = $this->configFactory->get('core.extension')->get('module') ?: [];
+    foreach ($extensions as $name => $module) {
+      $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0;
+      $module->status = (int) isset($installed_modules[$name]);
+      $module->schema_version = SCHEMA_UNINSTALLED;
+    }
+    $extensions = $this->moduleHandler->buildModuleDependencies($extensions);
+
+    if ($this->installProfile && $extensions[$this->installProfile]) {
+      $active_profile = $extensions[$this->installProfile];
+
+      // Installation profile hooks are always executed last.
+      $active_profile->weight = 1000;
+
+      // Installation profiles are hidden by default, unless explicitly
+      // specified otherwise in the .info.yml file.
+      if (!isset($active_profile->info['hidden'])) {
+        $active_profile->info['hidden'] = TRUE;
+      }
+
+      // The installation profile is required.
+      $active_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($active_profile->info['distribution']['name'])) {
+        $active_profile->info['distribution']['name'] = 'Drupal';
+      }
+    }
+
+    return $extensions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getInstalledExtensionNames() {
+    return array_keys($this->moduleHandler->getModuleList());
+  }
+
+  /**
+   * Marks dependencies of required modules as 'required', recursively.
+   *
+   * @param \Drupal\Core\Extension\Extension $module
+   *   The module extension object.
+   * @param \Drupal\Core\Extension\Extension[] $modules
+   *   Extension objects for all available modules.
+   */
+  protected function ensureRequiredDependencies(Extension $module, array $modules = []) {
+    if (!empty($module->info['required'])) {
+      foreach ($module->info['dependencies'] as $dependency) {
+        $dependency_name = ModuleHandler::parseDependency($dependency)['name'];
+        if (!isset($modules[$dependency_name]->info['required'])) {
+          $modules[$dependency_name]->info['required'] = TRUE;
+          $modules[$dependency_name]->info['explanation'] = $this->t('Dependency of required module @module', ['@module' => $module->info['name']]);
+          // Ensure any dependencies it has are required.
+          $this->ensureRequiredDependencies($modules[$dependency_name], $modules);
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index 7d69c3cb82ba82b64a8f6bb757fcd9fbeec87595..2a2113ff13df54a5c0cf193a280ced146deb4306 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -777,8 +777,7 @@ public function getModuleDirectories() {
    * {@inheritdoc}
    */
   public function getName($module) {
-    $info = system_get_info('module', $module);
-    return isset($info['name']) ? $info['name'] : $module;
+    return \Drupal::service('extension.list.module')->getName($module);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 5361421a7dfd34be73759dd42127b4c0489eafa9..2cf34bd22f08d3a8eb7c96081ebadf1306914645 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -14,6 +14,11 @@
  *
  * It registers the module in config, installs its own configuration,
  * installs the schema, updates the Drupal kernel and more.
+ *
+ * We don't inject dependencies yet, as we would need to reload them after
+ * each installation or uninstallation of a module.
+ * https://www.drupal.org/project/drupal/issues/2350111 for example tries to
+ * solve this dilemma.
  */
 class ModuleInstaller implements ModuleInstallerInterface {
 
@@ -170,7 +175,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
             $module_filenames[$name] = $current_module_filenames[$name];
           }
           else {
-            $module_path = drupal_get_path('module', $name);
+            $module_path = \Drupal::service('extension.list.module')->getPath($name);
             $pathname = "$module_path/$name.info.yml";
             $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL;
             $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename);
@@ -186,10 +191,10 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         $this->moduleHandler->load($module);
         module_load_install($module);
 
-        // Clear the static cache of system_rebuild_module_data() to pick up the
-        // new module, since it merges the installation status of modules into
-        // its statically cached list.
-        drupal_static_reset('system_rebuild_module_data');
+        // Clear the static cache of the "extension.list.module" service to pick
+        // up the new module, since it merges the installation status of modules
+        // into its statically cached list.
+        \Drupal::service('extension.list.module')->reset();
 
         // Update the kernel to include it.
         $this->updateKernel($module_filenames);
@@ -443,10 +448,10 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
       // Remove any potential cache bins provided by the module.
       $this->removeCacheBins($module);
 
-      // Clear the static cache of system_rebuild_module_data() to pick up the
-      // new module, since it merges the installation status of modules into
-      // its statically cached list.
-      drupal_static_reset('system_rebuild_module_data');
+      // Clear the static cache of the "extension.list.module" service to pick
+      // up the new module, since it merges the installation status of modules
+      // into its statically cached list.
+      \Drupal::service('extension.list.module')->reset();
 
       // Clear plugin manager caches.
       \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions();
diff --git a/core/lib/Drupal/Core/Extension/ProfileExtensionList.php b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb388b25feca7a14596bb8ea5e38e3f703b4edb0
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Core\Extension;
+
+/**
+ * Provides a list of installation profiles.
+ */
+class ProfileExtensionList extends ExtensionList {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaults = [
+    'dependencies' => [],
+    'description' => '',
+    'package' => 'Other',
+    'version' => NULL,
+    'php' => DRUPAL_MINIMUM_PHP,
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getInstalledExtensionNames() {
+    return [$this->installProfile];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php b/core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ffac735312d2e8b3c1be806e731f5de72059ef2
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\Core\Installer;
+
+use Drupal\Core\Extension\ModuleExtensionList;
+
+/**
+ * Overrides the module extension list to have a static cache.
+ */
+class InstallerModuleExtensionList extends ModuleExtensionList {
+
+  /**
+   * Static version of the added file names during the installer.
+   *
+   * @var string[]
+   *
+   * @internal
+   */
+  protected static $staticAddedPathNames;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPathname($extension_name, $pathname) {
+    parent::setPathname($extension_name, $pathname);
+
+    // In the early installer the container is rebuilt multiple times. Therefore
+    // we have to keep the added filenames across those rebuilds. This is not a
+    // final design, but rather just a workaround resolved at some point,
+    // hopefully.
+    // @todo Remove as part of https://drupal.org/project/drupal/issues/2934063
+    static::$staticAddedPathNames[$extension_name] = $pathname;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPathname($extension_name) {
+    if (isset($this->addedPathNames[$extension_name])) {
+      return $this->addedPathNames[$extension_name];
+    }
+    elseif (isset($this->pathNames[$extension_name])) {
+      return $this->pathNames[$extension_name];
+    }
+    elseif (isset(static::$staticAddedPathNames[$extension_name])) {
+      return static::$staticAddedPathNames[$extension_name];
+    }
+    elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) {
+      // Ensure we don't have to do path scanning more than really needed.
+      foreach ($path_names as $extension => $path_name) {
+        static::$staticAddedPathNames[$extension] = $path_name;
+      }
+      return $path_names[$extension_name];
+    }
+    throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist.");
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php
index 48341492bdbc767189b7f872560c137f145a86c1..dd4ae5f470ccb4901eecdc50f2b2da03f01f5e0c 100644
--- a/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php
+++ b/core/lib/Drupal/Core/Installer/InstallerServiceProvider.php
@@ -59,6 +59,9 @@ public function register(ContainerBuilder $container) {
       // The core router builder, but there is no reason here to be lazy, so
       // we don't need to ship with a custom proxy class.
       ->setLazy(FALSE);
+
+    // Use a performance optimised module extension list.
+    $container->getDefinition('extension.list.module')->setClass('Drupal\Core\Installer\InstallerModuleExtensionList');
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php b/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..c63b2d864f4f1b2716ee2565adb6fb0e482fb3f7
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\Core\Installer;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceProviderInterface;
+
+/**
+ * Service provider for the non early installer environment.
+ */
+class NormalInstallerServiceProvider implements ServiceProviderInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    // Use a performance optimised module extension list.
+    $container->getDefinition('extension.list.module')->setClass('Drupal\Core\Installer\InstallerModuleExtensionList');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Updater/Module.php b/core/lib/Drupal/Core/Updater/Module.php
index 0a244b46bc15ab4fa8ccb2ce608ba10e9cd96413..35232b3c0d4df7737238dcfde9d2f6a456c182a1 100644
--- a/core/lib/Drupal/Core/Updater/Module.php
+++ b/core/lib/Drupal/Core/Updater/Module.php
@@ -49,8 +49,9 @@ public static function getRootDirectoryRelativePath() {
   public function isInstalled() {
     // Check if the module exists in the file system, regardless of whether it
     // is enabled or not.
-    $modules = \Drupal::state()->get('system.module.files', []);
-    return isset($modules[$this->name]);
+    /** @var \Drupal\Core\Extension\ExtensionList $module_extension_list */
+    $module_extension_list = \Drupal::service('extension.list.module');
+    return $module_extension_list->exists($this->name);
   }
 
   /**
diff --git a/core/modules/book/tests/src/Kernel/BookUninstallTest.php b/core/modules/book/tests/src/Kernel/BookUninstallTest.php
index 9430991ffe7472bb054035109dd68246876f7aac..d4205ebf11bc7be8c8808745f82495be20d60bb4 100644
--- a/core/modules/book/tests/src/Kernel/BookUninstallTest.php
+++ b/core/modules/book/tests/src/Kernel/BookUninstallTest.php
@@ -76,7 +76,7 @@ public function testBookUninstall() {
 
     $book_node->delete();
     // No nodes exist therefore the book module is not required.
-    $module_data = _system_rebuild_module_data();
+    $module_data = \Drupal::service('extension.list.module')->reset()->getList();
     $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.');
 
     $node = Node::create(['title' => $this->randomString(), 'type' => $content_type->id()]);
diff --git a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
index 0608681dd7ffb90b7bdf23f8335156d324a9edb7..4823e6bf077f9b80d6cd1092c38041ab88759887 100644
--- a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
+++ b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
@@ -98,7 +98,7 @@ public function testInstallUninstall() {
 
     // Ensure that only core required modules and the install profile can not be uninstalled.
     $validation_reasons = \Drupal::service('module_installer')->validateUninstall(array_keys($all_modules));
-    $this->assertEqual(['standard', 'system', 'user'], array_keys($validation_reasons));
+    $this->assertEqual(['system', 'user', 'standard'], array_keys($validation_reasons));
 
     $modules_to_uninstall = array_filter($all_modules, function ($module) use ($validation_reasons) {
       // Filter required and not enabled modules.
diff --git a/core/modules/filter/tests/src/Functional/FilterFormTest.php b/core/modules/filter/tests/src/Functional/FilterFormTest.php
index fb0046b08a30cb811106aea9370ea7f1b0661825..d5b6f986c0043a06e4fe8d3056f0898dfb2a0f20 100644
--- a/core/modules/filter/tests/src/Functional/FilterFormTest.php
+++ b/core/modules/filter/tests/src/Functional/FilterFormTest.php
@@ -72,7 +72,7 @@ public function testFilterForm() {
     // @see https://www.drupal.org/node/2387983
     \Drupal::service('module_installer')->install(['filter_test_plugin']);
     // Force rebuild module data.
-    _system_rebuild_module_data();
+    \Drupal::service('extension.list.module')->reset();
   }
 
   /**
diff --git a/core/modules/filter/tests/src/Kernel/FilterAPITest.php b/core/modules/filter/tests/src/Kernel/FilterAPITest.php
index f74edbbcac8607da389772ef52f6cd39e935d7fb..d5ab64914e2c8a8bbe2ea44b6b4625b0d15ba925 100644
--- a/core/modules/filter/tests/src/Kernel/FilterAPITest.php
+++ b/core/modules/filter/tests/src/Kernel/FilterAPITest.php
@@ -488,7 +488,7 @@ public function testDependencyRemoval() {
 
     drupal_static_reset('filter_formats');
     \Drupal::entityManager()->getStorage('filter_format')->resetCache();
-    $module_data = _system_rebuild_module_data();
+    $module_data = \Drupal::service('extension.list.module')->reset()->getList();
     $this->assertFalse(isset($module_data['filter_test']->info['required']), 'The filter_test module is required.');
 
     // Verify that a dependency exists on the module that provides the filter
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 6bef11011c4efadf23fdf5543ee710a85c5eea24..964d7fe91a276b949903a40a16483a37ec64d451 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -12,7 +12,6 @@
 use Drupal\Core\Queue\QueueGarbageCollectionInterface;
 use Drupal\Core\Database\Query\AlterableInterface;
 use Drupal\Core\Extension\Extension;
-use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory;
 use Drupal\Core\PageCache\RequestPolicyInterface;
@@ -961,27 +960,17 @@ function system_check_directory($form_element, FormStateInterface $form_state) {
  */
 function system_get_info($type, $name = NULL) {
   if ($type == 'module') {
-    $info = &drupal_static(__FUNCTION__);
-    if (!isset($info)) {
-      if ($cache = \Drupal::cache()->get('system.module.info')) {
-        $info = $cache->data;
-      }
-      else {
-        $data = system_rebuild_module_data();
-        foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) {
-          if (isset($data[$module])) {
-            $info[$module] = $data[$module]->info;
-          }
-        }
-        // Store the module information in cache. This cache is cleared by
-        // calling system_rebuild_module_data(), for example, when listing
-        // modules, (un)installing modules, importing configuration, updating
-        // the site and when flushing all the caches.
-        \Drupal::cache()->set('system.module.info', $info);
-      }
+    /** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */
+    $module_list = \Drupal::service('extension.list.module');
+    if (isset($name)) {
+      return $module_list->getExtensionInfo($name);
+    }
+    else {
+      return $module_list->getAllInstalledInfo();
     }
   }
   else {
+    // @todo move into ThemeExtensionList https://www.drupal.org/node/2659940
     $info = [];
     $list = system_list($type);
     foreach ($list as $shortname => $item) {
@@ -989,96 +978,11 @@ function system_get_info($type, $name = NULL) {
         $info[$shortname] = $item->info;
       }
     }
-  }
-  if (isset($name)) {
-    return isset($info[$name]) ? $info[$name] : [];
-  }
-  return $info;
-}
-
-/**
- * Helper function to scan and collect module .info.yml data.
- *
- * @return \Drupal\Core\Extension\Extension[]
- *   An associative array of module information.
- */
-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;
-  }
-
-  // Set defaults for module info.
-  $defaults = [
-    'dependencies' => [],
-    'description' => '',
-    'package' => 'Other',
-    'version' => NULL,
-    'php' => DRUPAL_MINIMUM_PHP,
-  ];
-
-  // 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());
-
-    // Add the info file modification time, so it becomes available for
-    // contributed modules to use for ordering module lists.
-    $module->info['mtime'] = $module->getMTime();
-
-    // 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().
-    $type = 'module';
-    \Drupal::moduleHandler()->alter('system_info', $modules[$key]->info, $modules[$key], $type);
-  }
-
-  // It is possible that a module was marked as required by
-  // hook_system_info_alter() and modules that it depends on are not required.
-  foreach ($modules as $module) {
-    _system_rebuild_module_data_ensure_required($module, $modules);
-  }
-
-  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';
+    if (isset($name)) {
+      return isset($info[$name]) ? $info[$name] : [];
     }
+    return $info;
   }
-
-  return $modules;
 }
 
 /**
@@ -1088,8 +992,14 @@ function _system_rebuild_module_data() {
  *   The module info.
  * @param \Drupal\Core\Extension\Extension[] $modules
  *   The array of all module info.
+ *
+ * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. This
+ *   function is no longer used in Drupal core.
+ *
+ * @see https://www.drupal.org/node/2709919
  */
 function _system_rebuild_module_data_ensure_required($module, &$modules) {
+  @trigger_error("_system_rebuild_module_data_ensure_required() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. This function is no longer used in Drupal core. See https://www.drupal.org/node/2709919", E_USER_DEPRECATED);
   if (!empty($module->info['required'])) {
     foreach ($module->info['dependencies'] as $dependency) {
       $dependency_name = ModuleHandler::parseDependency($dependency)['name'];
@@ -1103,6 +1013,23 @@ function _system_rebuild_module_data_ensure_required($module, &$modules) {
   }
 }
 
+/**
+ * Helper function to scan and collect module .info.yml data.
+ *
+ * @return \Drupal\Core\Extension\Extension[]
+ *   An associative array of module information.
+ *
+ * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0.
+ *   Use \Drupal::service('extension.list.module')->reset()->getList()
+ *   instead. Note: You probably don't need the reset() method.
+ *
+ * @see https://www.drupal.org/node/2709919
+ */
+function _system_rebuild_module_data() {
+  @trigger_error("_system_rebuild_module_data() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Instead, you should use \\Drupal::service('extension.list.module')->reset()->getList(). See https://www.drupal.org/node/2709919", E_USER_DEPRECATED);
+  return \Drupal::service('extension.list.module')->reset()->getList();
+}
+
 /**
  * Rebuild, save, and return data about all currently available modules.
  *
@@ -1110,33 +1037,7 @@ function _system_rebuild_module_data_ensure_required($module, &$modules) {
  *   Array of all available modules and their data.
  */
 function system_rebuild_module_data() {
-  $modules_cache = &drupal_static(__FUNCTION__);
-  // Only rebuild once per request. $modules and $modules_cache cannot be
-  // combined into one variable, because the $modules_cache variable is reset by
-  // reference from system_list_reset() during the rebuild.
-  if (!isset($modules_cache)) {
-    $modules = _system_rebuild_module_data();
-    $files = [];
-    ksort($modules);
-    // Add status, weight, and schema version.
-    $installed_modules = \Drupal::config('core.extension')->get('module') ?: [];
-    foreach ($modules as $name => $module) {
-      $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0;
-      $module->status = (int) isset($installed_modules[$name]);
-      $module->schema_version = SCHEMA_UNINSTALLED;
-      $files[$name] = $module->getPathname();
-    }
-    $modules = \Drupal::moduleHandler()->buildModuleDependencies($modules);
-    $modules_cache = $modules;
-
-    // Store filenames to allow drupal_get_filename() to retrieve them without
-    // having to rebuild or scan the filesystem.
-    \Drupal::state()->set('system.module.files', $files);
-    // Clear the module info cache.
-    \Drupal::cache()->delete('system.module.info');
-    drupal_static_reset('system_get_info');
-  }
-  return $modules_cache;
+  return \Drupal::service('extension.list.module')->reset()->getList();
 }
 
 /**
diff --git a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
index d7c2650353885d434bf11866c0ba518394990789..a8767e92d56b9030e84e3d949da67f4a4e19ddc5 100644
--- a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
+++ b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
@@ -105,7 +105,7 @@ public function testDependencyResolution() {
     // Color will depend on Config, which depends on a non-existing module Foo.
     // Nothing should be installed.
     \Drupal::state()->set('module_test.dependency', 'missing dependency');
-    drupal_static_reset('system_rebuild_module_data');
+    \Drupal::service('extension.list.module')->reset();
 
     try {
       $result = $this->moduleInstaller()->install(['color']);
@@ -120,7 +120,7 @@ public function testDependencyResolution() {
     // Fix the missing dependency.
     // Color module depends on Config. Config depends on Help module.
     \Drupal::state()->set('module_test.dependency', 'dependency');
-    drupal_static_reset('system_rebuild_module_data');
+    \Drupal::service('extension.list.module')->reset();
 
     $result = $this->moduleInstaller()->install(['color']);
     $this->assertTrue($result, 'ModuleInstaller::install() returns the correct value.');
@@ -152,7 +152,7 @@ public function testDependencyResolution() {
     // dependency on a specific version of Help module in its info file. Make
     // sure that Drupal\Core\Extension\ModuleInstaller::install() still works.
     \Drupal::state()->set('module_test.dependency', 'version dependency');
-    drupal_static_reset('system_rebuild_module_data');
+    \Drupal::service('extension.list.module')->reset();
 
     $result = $this->moduleInstaller()->install(['color']);
     $this->assertTrue($result, 'ModuleInstaller::install() returns the correct value.');
@@ -182,8 +182,7 @@ public function testUninstallProfileDependency() {
     drupal_get_filename('profile', $profile, 'core/profiles/' . $profile . '/' . $profile . '.info.yml');
     $this->enableModules(['module_test', $profile]);
 
-    drupal_static_reset('system_rebuild_module_data');
-    $data = system_rebuild_module_data();
+    $data = \Drupal::service('extension.list.module')->reset()->getList();
     $this->assertTrue(isset($data[$profile]->requires[$dependency]));
 
     $this->moduleInstaller()->install([$dependency]);
@@ -221,7 +220,7 @@ public function testUninstallContentDependency() {
     // entity_test will depend on help. This way help can not be uninstalled
     // when there is test content preventing entity_test from being uninstalled.
     \Drupal::state()->set('module_test.dependency', 'dependency');
-    drupal_static_reset('system_rebuild_module_data');
+    \Drupal::service('extension.list.module')->reset();
 
     // Create an entity so that the modules can not be disabled.
     $entity = EntityTest::create(['name' => $this->randomString()]);
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index 0545aef8b9aea17776dfe468b5296b631fceabdb..a7e8cc3397127bda54f5c928fce4826c718f6a1e 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -625,7 +625,6 @@ function template_preprocess_update_last_check(&$variables) {
  * version of Drupal core.
  *
  * @see \Drupal\Core\Extension\ExtensionDiscovery
- * @see _system_rebuild_module_data()
  */
 function update_verify_update_archive($project, $archive_file, $directory) {
   $errors = [];
diff --git a/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php
index e0eccbf58d139fb573a0e5ed73fb95e1be960653..396facf1582a48d8d725a9a2850d0fced5e872af 100644
--- a/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php
@@ -49,20 +49,24 @@ public function testDrupalGetFilename() {
     // a fixed location and naming.
     $this->assertIdentical(drupal_get_filename('profile', 'testing'), 'core/profiles/testing/testing.info.yml');
 
-    // Generate a non-existing module name.
-    $non_existing_module = uniqid("", TRUE);
-
     // Set a custom error handler so we can ignore the file not found error.
     set_error_handler(function ($severity, $message, $file, $line) {
       // Skip error handling if this is a "file not found" error.
       if (strstr($message, 'is missing from the file system:')) {
-        \Drupal::state()->set('get_filename_test_triggered_error', TRUE);
+        \Drupal::state()->set('get_filename_test_triggered_error', $message);
         return;
       }
       throw new \ErrorException($message, 0, $severity, $file, $line);
     });
-    $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for an item that does not exist returns NULL.');
-    $this->assertTrue(\Drupal::state()->get('get_filename_test_triggered_error'), 'Searching for an item that does not exist triggers an error.');
+    $this->assertNull(drupal_get_filename('module', 'there_is_a_module_for_that'), 'Searching for an item that does not exist returns NULL.');
+    $this->assertEquals('The following module is missing from the file system: there_is_a_module_for_that', \Drupal::state()->get('get_filename_test_triggered_error'));
+
+    $this->assertNull(drupal_get_filename('theme', 'there_is_a_theme_for_you'), 'Searching for an item that does not exist returns NULL.');
+    $this->assertEquals('The following theme is missing from the file system: there_is_a_theme_for_you', \Drupal::state()->get('get_filename_test_triggered_error'));
+
+    $this->assertNull(drupal_get_filename('profile', 'there_is_an_install_profile_for_you'), 'Searching for an item that does not exist returns NULL.');
+    $this->assertEquals('The following profile is missing from the file system: there_is_an_install_profile_for_you', \Drupal::state()->get('get_filename_test_triggered_error'));
+
     // Restore the original error handler.
     restore_error_handler();
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleExtensionListTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleExtensionListTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ffef4a7140ede7a99f5ba794127eba932ec9d4a
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleExtensionListTest.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\Core\Site\Settings;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Extension\ModuleExtensionList
+ * @group Extension
+ */
+class ModuleExtensionListTest extends KernelTestBase {
+
+  /**
+   * @covers ::getList
+   */
+  public function testGetlist() {
+    $settings = Settings::getAll();
+    $settings['install_profile'] = 'testing';
+    new Settings($settings);
+
+    \Drupal::configFactory()->getEditable('core.extension')
+      ->set('module.testing', 1000)
+      ->save();
+
+    // The installation profile is provided by a container parameter.
+    // Saving the configuration doesn't automatically trigger invalidation
+    $this->container->get('kernel')->rebuildContainer();
+
+    /** @var \Drupal\Core\Extension\ModuleExtensionList $module_extension_list */
+    $module_extension_list = \Drupal::service('extension.list.module');
+    $extensions = $module_extension_list->getList();
+
+    $this->assertArrayHasKey('testing', $extensions);
+    $this->assertEquals(1000, $extensions['testing']->weight);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php b/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..80a0c7669588e0b28944a232c92a23570a8b39cd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php
@@ -0,0 +1,291 @@
+<?php
+
+namespace Drupal\Tests\Core\Extension;
+
+use Drupal\Component\Serialization\Yaml;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\Extension\ExtensionList;
+use Drupal\Core\Extension\InfoParserInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\State\StateInterface;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Extension\ExtensionList
+ * @group Extension
+ */
+class ExtensionListTest extends UnitTestCase {
+
+  /**
+   * @covers ::getName
+   */
+  public function testGetNameWithNonExistingExtension() {
+    list($cache, $info_parser, $module_handler, $state) = $this->getMocks();
+    $test_extension_list = new TestExtension($this->root, 'test_extension', $cache->reveal(), $info_parser->reveal(), $module_handler->reveal(), $state->reveal(), 'testing');
+
+    $extension_discovery = $this->prophesize(ExtensionDiscovery::class);
+    $extension_discovery->scan('test_extension')->willReturn([]);
+    $test_extension_list->setExtensionDiscovery($extension_discovery->reveal());
+
+    $this->setExpectedException(\InvalidArgumentException::class);
+    $test_extension_list->getName('test_name');
+  }
+
+  /**
+   * @covers ::getName
+   */
+  public function testGetName() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $this->assertEquals('test name', $test_extension_list->getName('test_name'));
+  }
+
+  /**
+   * @covers ::get
+   */
+  public function testGetWithNonExistingExtension() {
+    list($cache, $info_parser, $module_handler, $state) = $this->getMocks();
+    $test_extension_list = new TestExtension($this->root, 'test_extension', $cache->reveal(), $info_parser->reveal(), $module_handler->reveal(), $state->reveal(), 'testing');
+
+    $extension_discovery = $this->prophesize(ExtensionDiscovery::class);
+    $extension_discovery->scan('test_extension')->willReturn([]);
+    $test_extension_list->setExtensionDiscovery($extension_discovery->reveal());
+
+    $this->setExpectedException(\InvalidArgumentException::class);
+    $test_extension_list->get('test_name');
+  }
+
+  /**
+   * @covers ::get
+   */
+  public function testGet() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $extension = $test_extension_list->get('test_name');
+    $this->assertInstanceOf(Extension::class, $extension);
+    $this->assertEquals('test_name', $extension->getName());
+  }
+
+  /**
+   * @covers ::getList
+   */
+  public function testGetList() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $extensions = $test_extension_list->getList();
+    $this->assertCount(1, $extensions);
+    $this->assertEquals('test_name', $extensions['test_name']->getName());
+  }
+
+  /**
+   * @covers ::getExtensionInfo
+   * @covers ::getAllInstalledInfo
+   */
+  public function testGetExtensionInfo() {
+    $test_extension_list = $this->setupTestExtensionList();
+    $test_extension_list->setInstalledExtensions(['test_name']);
+
+    $info = $test_extension_list->getExtensionInfo('test_name');
+    $this->assertEquals([
+      'type' => 'test_extension',
+      'core' => '8.x',
+      'name' => 'test name',
+      'mtime' => 123456789,
+    ], $info);
+  }
+
+  /**
+   * @covers ::getAllAvailableInfo
+   */
+  public function testGetAllAvailableInfo() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $infos = $test_extension_list->getAllAvailableInfo();
+    $this->assertEquals([
+      'test_name' => [
+        'type' => 'test_extension',
+        'core' => '8.x',
+        'name' => 'test name',
+        'mtime' => 123456789,
+      ]
+    ], $infos);
+  }
+
+  /**
+   * @covers ::getAllInstalledInfo
+   */
+  public function testGetAllInstalledInfo() {
+    $test_extension_list = $this->setupTestExtensionList(['test_name', 'test_name_2']);
+    $test_extension_list->setInstalledExtensions(['test_name_2']);
+
+    $infos = $test_extension_list->getAllInstalledInfo();
+    $this->assertEquals([
+      'test_name_2' => [
+        'type' => 'test_extension',
+        'core' => '8.x',
+        'name' => 'test name',
+        'mtime' => 123456789,
+      ]
+    ], $infos);
+  }
+
+  /**
+   * @covers ::getPathnames
+   */
+  public function testGetPathnames() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $filenames = $test_extension_list->getPathnames();
+    $this->assertEquals([
+      'test_name' => 'vfs://drupal_root/example/test_name/test_name.info.yml',
+    ], $filenames);
+  }
+
+  /**
+   * @covers ::getPathname
+   */
+  public function testGetPathname() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $pathname = $test_extension_list->getPathname('test_name');
+    $this->assertEquals('vfs://drupal_root/example/test_name/test_name.info.yml', $pathname);
+  }
+
+
+  /**
+   * @covers ::setPathname
+   * @covers ::getPathname
+   */
+  public function testSetPathname() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $test_extension_list->setPathname('test_name', 'vfs://drupal_root/example2/test_name/test_name.info.yml');
+    $this->assertEquals('vfs://drupal_root/example2/test_name/test_name.info.yml', $test_extension_list->getPathname('test_name'));
+  }
+
+  /**
+   * @covers ::getPath
+   */
+  public function testGetPath() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $path = $test_extension_list->getPath('test_name');
+    $this->assertEquals('vfs://drupal_root/example/test_name', $path);
+  }
+
+  /**
+   * @covers ::reset
+   */
+  public function testReset() {
+    $test_extension_list = $this->setupTestExtensionList();
+
+    $path = $test_extension_list->getPath('test_name');
+    $this->assertEquals('vfs://drupal_root/example/test_name', $path);
+    $pathname = $test_extension_list->getPathname('test_name');
+    $this->assertEquals('vfs://drupal_root/example/test_name/test_name.info.yml', $pathname);
+    $filenames = $test_extension_list->getPathnames();
+    $this->assertEquals([
+      'test_name' => 'vfs://drupal_root/example/test_name/test_name.info.yml',
+    ], $filenames);
+
+    $test_extension_list->reset();
+
+    // Ensure that everything is still usable after the resetting.
+    $path = $test_extension_list->getPath('test_name');
+    $this->assertEquals('vfs://drupal_root/example/test_name', $path);
+    $pathname = $test_extension_list->getPathname('test_name');
+    $this->assertEquals('vfs://drupal_root/example/test_name/test_name.info.yml', $pathname);
+    $filenames = $test_extension_list->getPathnames();
+    $this->assertEquals([
+      'test_name' => 'vfs://drupal_root/example/test_name/test_name.info.yml',
+    ], $filenames);
+  }
+
+  /**
+   * @return \Drupal\Tests\Core\Extension\TestExtension
+   */
+  protected function setupTestExtensionList($extension_names = ['test_name']) {
+    vfsStream::setup('drupal_root');
+
+    $folders = ['example' => []];
+    foreach ($extension_names as $extension_name) {
+      $folders['example'][$extension_name][$extension_name . '.info.yml'] = Yaml::encode([
+        'name' => 'test name',
+        'type' => 'test_extension',
+        'core' => '8.x',
+      ]);
+    }
+    vfsStream::create($folders);
+    foreach ($extension_names as $extension_name) {
+      touch("vfs://drupal_root/example/$extension_name/$extension_name.info.yml", 123456789);
+    }
+
+    list($cache, $info_parser, $module_handler, $state) = $this->getMocks();
+    $info_parser->parse(Argument::any())->will(function ($args) {
+      return Yaml::decode(file_get_contents($args[0]));
+    });
+
+    $test_extension_list = new TestExtension('vfs://drupal_root', 'test_extension', $cache->reveal(), $info_parser->reveal(), $module_handler->reveal(), $state->reveal(), 'testing');
+
+    $extension_discovery = $this->prophesize(ExtensionDiscovery::class);
+    $extension_scan_result = [];
+    foreach ($extension_names as $extension_name) {
+      $extension_scan_result[$extension_name] = new Extension($this->root, 'test_extension', "vfs://drupal_root/example/$extension_name/$extension_name.info.yml");
+    }
+    $extension_discovery->scan('test_extension')->willReturn($extension_scan_result);
+    $test_extension_list->setExtensionDiscovery($extension_discovery->reveal());
+    return $test_extension_list;
+  }
+
+  protected function getMocks() {
+    $cache = $this->prophesize(CacheBackendInterface::class);
+    $info_parser = $this->prophesize(InfoParserInterface::class);
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class);
+    $state = $this->prophesize(StateInterface::class);
+    return [$cache, $info_parser, $module_handler, $state];
+  }
+
+}
+
+class TestExtension extends ExtensionList {
+
+  /**
+   * @var string[]
+   */
+  protected $installedExtensions = [];
+
+  /**
+   * @var \Drupal\Core\Extension\ExtensionDiscovery|null
+   */
+  protected $extensionDiscovery;
+
+  /**
+   * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
+   */
+  public function setExtensionDiscovery(ExtensionDiscovery $extension_discovery) {
+    $this->extensionDiscovery = $extension_discovery;
+  }
+
+  public function setInstalledExtensions(array $extension_names) {
+    $this->installedExtensions = $extension_names;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getInstalledExtensionNames() {
+    return $this->installedExtensions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExtensionDiscovery() {
+    return $this->extensionDiscovery ?: parent::getExtensionDiscovery();
+  }
+
+}