wikimedia/composer-merge-plugin

Incompatible with composer/installers

grasmash opened this issue · 8 comments

Steps to reproduce

Create composer.json:

{
    "require": {
        "wikimedia/composer-merge-plugin": "^1.4"
    },
    "extra": {
        "merge-plugin": {
            "require": [
                "composer.include.json"
            ],
            "merge-extra": true,
            "merge-extra-deep": true
        }
    }
}

composer.include.json:

{
  "require": {
    "drupal/core": "^8.0",
    "composer/installers": "^1.2"
  },
  "extra": {
    "installer-paths": {
      "docroot/core": [
        "type:drupal-core"
      ]
    }
  }
}

Execute composer install.

Note that at this point, docroot/core is created as expected.

  1. mkdir subdir
  2. cp composer.* subdir/
  3. cd subdir
  4. composer install

At this point subdir/docroot/core is not created. subdir/core is instead.

It seems that composer/installers cannot correctly create the expected paths. I suspect that this is in part because the path of composer.json has changed. This only occurs when composer-merge-plugin is used.

This has something to with autoloading. If I subsequently run composer dump-autoload && composer install after the initial failed composer install, then subdir/docroot/core will be created. However, I then have both subdir/core and subdir/docroot/core.

So I'm starting to think that the installer-paths config is simply merged too late for composer/installers to recognize it.

The following output is written to screen after drupal/core is installed:

Generating autoload files
  [merge-plugin] Loading composer.include.json...
  [merge-plugin] Merging drupal/core


  [RuntimeException]
  Could not scan for classes inside "docroot/core/lib/Drupal.php" which does not appear to be a file nor a folder


Exception trace:
 () at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Autoload/ClassMapGenerator.php:69
 Composer\Autoload\ClassMapGenerator::createMap() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Autoload/AutoloadGenerator.php:336
 Composer\Autoload\AutoloadGenerator->generateClassMap() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Autoload/AutoloadGenerator.php:319
 Composer\Autoload\AutoloadGenerator->addClassMapCode() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Autoload/AutoloadGenerator.php:266
 Composer\Autoload\AutoloadGenerator->dump() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Installer.php:298
 Composer\Installer->run() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Command/InstallCommand.php:119
 Composer\Command\InstallCommand->execute() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/vendor/symfony/console/Command/Command.php:267
 Symfony\Component\Console\Command\Command->run() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/vendor/symfony/console/Application.php:846
 Symfony\Component\Console\Application->doRunCommand() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/vendor/symfony/console/Application.php:191
 Symfony\Component\Console\Application->doRun() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Console/Application.php:227
 Composer\Console\Application->doRun() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/vendor/symfony/console/Application.php:122
 Symfony\Component\Console\Application->run() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/src/Composer/Console/Application.php:100
 Composer\Console\Application->run() at phar:///usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar/bin/composer:54
 require() at /usr/local/Cellar/composer/1.2.2_1/libexec/composer.phar:24

So, composer/installers has already written drupal/core to core before composer-merge-plugin merges in the installer-paths config... I think. A subsequent composer install works but you end up with the package in two places.

@bd808 Is there a way to manually call composer-merge-plugin using a static method in order to force the merge before drupal/core is installed? Or else a way to call it earlier?

bd808 commented

When running under Composer 1.1+, composer-merge-plugin hooks the INIT hook which is the very first plugin signal sent by the platform. There is not any way to initialize sooner and still have access to the full plugin system.

Using the files from the original issue summary, (with no lock file), we do this:

$ composer install
# Composer does its thing...
$ ls docroot/
core
# Let's do a reset...
$ rm -rf docroot/
$ rm -rf vendor/
# This leaves us with only the two json files and a lock file.
$ composer install
# Eventually see this error:
  [RuntimeException]
  Could not scan for classes inside "docroot/core/lib/Drupal.php" which does
  not appear to be a file nor a folder
$ ls 
composer.include.json	composer.lock		vendor
composer.json		core
# Note no docroot/, only core/.

Remove the lock file and it works again.

The fact that drupal/core ends up in core/ tells us that composer/installers plugin is working, since that's the default location for a drupal-core package according to composer-installers.

It also tells us that it hasn't gotten the special config from composer.include.json, or it would be docroot/core/ instead.

However, the autoloader is being told that drupal/core lives in docroot/core/, which is why we get the error about scanning for classes.

The fact that this changes when you add/remove composer.lock tells us that this is a race condition between the two plugins: When there's no lock file, composer/installer is always loaded after wikimedia/composer-merge-plugin because it's merged by it. If there is a lock file, then C comes before W (or some other arbitrary ordering problem).

I'd call this a limitation of the plugin system.

Does anyone has solutions for that case?

This is easy to reproduce.
As a workaround, you can remove "installer-paths" from composer.include.json and add it to composer.json, but this is just a quickfix for the issue.

I'm seeing this too and the workaround from @rutiolma is working ok. It's a bit unfortunate because I'd love to outsource those installer paths into a separate file (even into a separate package) in order to make re-use of complex setup possible. So I hope this can be fiexed eventually.