Changing install profiles in Drupal 8

A computer keyboard with an Under Construction sign on it
Behind the Scenes

As we put some polish on our Drupal 8 install profile, we're already in a position of technical debt: we've already launched Drupal 8 sites using the Drupal "standard" profile. Unless we update those sites to use our new D8 profile, they'll miss out on the benefits of our continuous deployment stack. There are plenty of resources for switching Drupal 7 site profiles, but what's the analogous process in Drupal 8?

The solution boils down to a relatively simple hook_update and a tweak in settings.php:

<?php
function message_agency_update_8001() {
  $profile_name = "message_agency";
  \Drupal::keyValue('system.schema')->delete('standard');
  \Drupal::keyValue('system.schema')->set($profile_name, 8000);
  
  $extension_config = \Drupal::configFactory()->getEditable('core.extension');
  $modules = $extension_config->get('module');
  $modules[$profile_name] = 1;
  $extension_config->set('module', $modules);
  $extension_config->save(); drupal_flush_all_caches(); 
}
?>

And appending a line to our settings.php, so that Drupal can find the modules and themes in our profile:

<?php
  $settings['install_profile'] = 'message_agency';
?>

Investigating this question also gave us a chance to explore how some of Drupal's core assumptions have changed.

Drupal 8 does away with "system" database tables. Module status is now split between config variables (Configuration Management Initiative or CMI), core.extension, and key-value stores (State API), system.schema.

core.extension

This config maintains a list of enabled modules—previously "status" column in system table—in a giant array.

system.schema

This key store, as its name implied, keeps track of module schema versions. Each key-value entry is a module name, and its value is the schema version.

Why go through all this trouble? The more important component here is purging modules from our Drupal 8 site installs, where those modules are included in our profile. To accomplish this, we use diff's ability to compare directory listings to generate our comparison:

> diff modules/contrib/ profiles/message_agency/modules/contrib/ | sort
   
    Common subdirectories: modules/contrib/menu_block and profiles/message_agency/modules/contrib/menu_block
    Common subdirectories: modules/contrib/responsive_tables_filter and profiles/message_agency/modules/contrib/responsive_tables_filter
    Only in modules/contrib/: gathercontent Only in modules/contrib/: view_unpublished
    Only in profiles/message_agency/modules/contrib/: admin_toolbar
    Only in profiles/message_agency/modules/contrib/: block_access
   
    ...

For each line beginning "Common subdirectories", we string together some command line tools to to remove the now-duplicate version:

diff modules/contrib/ profiles/message_agency/modules/contrib/ | grep "Common subdirectories:" | awk {'print $3'} | xargs git rm -r

Running our update hook with drush updb will address the final, critical piece of the puzzle. State key store system.module.files caches file paths for module info.yml files, which will be rebuilt by drupal_flush_all_caches(). It's important to note that we (probably) can't simply use drush cr, since drush will fail to perform a full bootstrap. Instead, drush updb will attempt a minimal bootstrap, preventing errors related to missing module files. (If this bootstrap is too much for your modules, you'll have to resort to a drush script to manually edit file paths in system.module.files.)