Skip to content

Commit 286b9d6

Browse files
committed
Add better Composer 2 support.
1 parent 34c1372 commit 286b9d6

File tree

4 files changed

+179
-21
lines changed

4 files changed

+179
-21
lines changed

.lando.yml

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: drupal-libaries-installer
2+
3+
services:
4+
app: &appserver
5+
type: php:7.2
6+
via: cli
7+
composer_version: 1.10.20
8+
xdebug: false
9+
overrides:
10+
environment:
11+
PHP_IDE_CONFIG: "serverName=appserver"
12+
XDEBUG_CONFIG: ""
13+
XDEBUG_MODE: debug
14+
config:
15+
php: .php.ini
16+
app2:
17+
<<: *appserver
18+
composer_version: 2
19+
20+
tooling:
21+
composer: { service: app, cmd: /app/vendor/bin/composer, description: Run local Composer }
22+
composer1: { service: app, cmd: /usr/local/bin/composer, description: Run Composer 1 }
23+
composer2: { service: app2, cmd: /usr/local/bin/composer, description: Run Composer 2 }
24+
25+
xdebug-on:
26+
description: Enable xdebug.
27+
cmd:
28+
- app: &xdebug_on docker-php-ext-enable xdebug 2>/dev/null && pkill -o -USR2 php-fpm && echo "Enabled xdebug"
29+
- app2: *xdebug_on
30+
user: root
31+
xdebug-off:
32+
description: Disable xdebug.
33+
cmd:
34+
- app: &xdebug_off rm -rf /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && pkill -o -USR2 php-fpm && echo "Disabled xdebug"
35+
- app2: *xdebug_off
36+
user: root

.php.ini

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[xdebug]
2+
; https://xdebug.org/docs/all_settings
3+
xdebug.show_exception_trace = 0
4+
xdebug.max_nesting_level = 9999
5+
xdebug.idekey = PHPSTORM
6+
# For xdebug 3 - https://xdebug.org/docs/upgrade_guide
7+
xdebug.mode = debug
8+
; Start only when triggered.
9+
xdebug.start_with_request = trigger
10+
xdebug.client_host = host.docker.internal
11+
xdebug.connect_timeout_ms = 200
12+
xdebug.client_port = 9000
13+
; xdebug.discover_client_host = false

CHANGELOG.md

+13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
1.4.1 / 2021-02-22
2+
========================
3+
* Add better Composer 2 support.
4+
5+
* Fix issue where libraries could not be downloaded on an empty cache, creating an empty folder instead.
6+
7+
Composer 2 introduces additional steps to [`DownloaderInterface`][composer-2-upgrade], which
8+
needed integration as well as support for resolving the promises properly. [Additional reference][composer-2-download-support].
9+
* Support parallel library downloads on Composer 2, while keeping existing synchronous download support on Composer 1.
10+
* Fix issue with the plugin failing early if the plugin package is an `AliasPackage`.
11+
112
1.4.0 / 2020-11-23
213
========================
314
* Add Composer 2 support.
@@ -38,4 +49,6 @@ definition for supporting:
3849
========================
3950
* Initial MVP plugin.
4051

52+
[composer-2-upgrade]: https://getcomposer.org/upgrade/UPGRADE-2.0.md
53+
[composer-2-download-support]: https://github.com/composer/composer/issues/9209
4154
[ckeditor-downloads]: https://github.com/balbuf/drupal-libraries-installer/issues/6

src/Plugin.php

+117-21
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
use Composer\Composer;
66
use Composer\EventDispatcher\EventSubscriberInterface;
7+
use Composer\IO\ConsoleIO;
78
use Composer\IO\IOInterface;
89
use Composer\Json\JsonFile;
9-
use Composer\Package\CompletePackage;
10+
use Composer\Package\CompletePackageInterface;
1011
use Composer\Package\Package;
1112
use Composer\Package\PackageInterface;
1213
use Composer\Plugin\Capable;
@@ -309,12 +310,14 @@ protected function removeUnusedLibraries(array $old_libraries) {
309310
* The currently installed libraries.
310311
*/
311312
protected function downloadLibraries(array $processed_libraries, array $applied_drupal_libraries) {
313+
$download_promises = [];
314+
$install_promises = [];
315+
316+
$libraries_to_install = [];
312317
foreach ($processed_libraries as $library_name => $processed_library) {
313318
$library_package = $this->getLibraryPackage($library_name, $processed_library);
314-
315-
$ignore_patterns = $processed_library['ignore'];
316-
$rename = $processed_library['rename'] ?? NULL;
317319
$install_path = $this->installationManager->getInstallPath($library_package);
320+
318321
if (
319322
(
320323
!isset($applied_drupal_libraries[$library_name]) ||
@@ -326,25 +329,103 @@ protected function downloadLibraries(array $processed_libraries, array $applied_
326329
// - wasn't in the lock file.
327330
// - doesn't match what is in the lock file.
328331
// - doesn't exist on disk.
329-
$this->downloadPackage($library_package, $install_path, $ignore_patterns, $rename);
332+
$download_result = $this->downloadManager->download($library_package, $install_path);
333+
if ($download_result instanceof PromiseInterface) {
334+
// https://github.com/composer/composer/issues/9209
335+
/* @see \Composer\Util\SyncHelper::downloadAndInstallPackageSync */
336+
$download_promises[] = $download_result
337+
// Prepare for install.
338+
->then(function () use ($library_package, $install_path) {
339+
return $this->downloadManager->prepare('install', $library_package, $install_path);
340+
})
341+
// Clean up after any download errors.
342+
->then(NULL, function ($e) use ($library_package, $install_path) {
343+
$this->composer->getLoop()
344+
->wait([
345+
$this->downloadManager->cleanup('install', $library_package, $install_path),
346+
]);
347+
throw $e;
348+
});
349+
350+
// Install after the download resolves.
351+
$libraries_to_install[] = [
352+
$library_name,
353+
$library_package,
354+
$install_path,
355+
];
356+
}
357+
else {
358+
// Attempt to install synchronously.
359+
$install_result = $this->installPackage($library_package, $install_path, $processed_library);
360+
if ($install_result instanceof PromiseInterface) {
361+
$install_promises[] = $install_result;
362+
}
363+
}
330364
}
331365
}
366+
367+
if (count($download_promises)) {
368+
// Wait on the download asynchronous promises to resolve.
369+
$this->waitOnPromises($download_promises);
370+
}
371+
372+
foreach ($libraries_to_install as $library_to_install) {
373+
[$library_name, $library_package, $install_path] = $library_to_install;
374+
$install_result = $this->installPackage($library_package, $install_path, $processed_libraries[$library_name]);
375+
if ($install_result instanceof PromiseInterface) {
376+
$install_promises[] = $install_result;
377+
}
378+
}
379+
380+
if (count($install_promises)) {
381+
// Wait on the install promises to resolve.
382+
$this->composer->getLoop()->wait($install_promises);
383+
}
332384
}
333385

334386
/**
335-
* Downloads a library package to disk.
387+
* Wait synchronously for an array of promises to resolve.
388+
*
389+
* @param array $promises
390+
* Promises to await.
391+
*/
392+
protected function waitOnPromises(array $promises) {
393+
$progress = NULL;
394+
if ($this->io instanceof ConsoleIO && !$this->io->isDebug() && count($promises) > 1 && !getenv('COMPOSER_DISABLE_PROGRESS_BAR')) {
395+
// Disable progress bar by setting COMPOSER_DISABLE_PROGRESS_BAR=1 as we
396+
// are unable to read composer's "--no-input" option easily from here
397+
// without introducing extra complexity with the PluginEvents::COMMAND
398+
// event.
399+
$progress = $this->io->getProgressBar();
400+
}
401+
$this->composer->getLoop()->wait($promises, $progress);
402+
if ($progress) {
403+
$progress->clear();
404+
}
405+
}
406+
407+
/**
408+
* Installs a library package to disk.
336409
*
337410
* @param \Composer\Package\Package $library_package
338411
* The library package.
339412
* @param string $install_path
340413
* The package install path.
341-
* @param array $ignore_patterns
342-
* File patterns to ignore.
343-
* @param array|null $rename
344-
* Array mapping of files/folders to rename.
414+
* @param array $processed_library
415+
* The library definition.
416+
*
417+
* @return \React\Promise\PromiseInterface|void
418+
* Returns a promise or void.
345419
*/
346-
protected function downloadPackage(Package $library_package, $install_path, array $ignore_patterns, array $rename = NULL) {
347-
$process_download = function () use ($install_path, $ignore_patterns, $rename) {
420+
protected function installPackage(Package $library_package, $install_path, array $processed_library) {
421+
$ignore_patterns = $processed_library['ignore'];
422+
$rename = $processed_library['rename'] ?? NULL;
423+
424+
$process_install = function () use ($library_package, $install_path, $ignore_patterns, $rename) {
425+
if ($ignore_patterns || $rename) {
426+
$package_name = $library_package->getName();
427+
$this->io->writeError(" - Processing <info>$package_name</info> files...");
428+
}
348429

349430
// Delete files/folders according to the ignore pattern(s).
350431
if ($ignore_patterns) {
@@ -411,17 +492,32 @@ function ($file) use ($patterns) {
411492
}
412493
};
413494

414-
// Let composer download and unpack the library for us!
415-
$promise = $this->downloadManager->download($library_package, $install_path);
495+
if (version_compare(PluginInterface::PLUGIN_API_VERSION, '2.0', '>=')) {
496+
// Install the package after downloading (Composer 2 only).
497+
$promise = $this->downloadManager->install($library_package, $install_path);
416498

417-
// Composer v2 might return a promise here.
418-
if ($promise && $promise instanceof PromiseInterface) {
419-
$promise->then($process_download);
420-
return;
499+
if (!($promise instanceof PromiseInterface)) {
500+
// Not a promise, create one that can be cleaned up after.
501+
$promise = \React\Promise\resolve();
502+
}
503+
504+
return $promise
505+
->then($process_install)
506+
// Clean up after the install.
507+
->then(function () use ($library_package, $install_path) {
508+
return $this->downloadManager->cleanup('install', $library_package, $install_path);
509+
}, function ($e) use ($library_package, $install_path) {
510+
// Clean up after any errors.
511+
$this->composer->getLoop()
512+
->wait([
513+
$this->downloadManager->cleanup('install', $library_package, $install_path),
514+
]);
515+
throw $e;
516+
});
421517
}
422518

423-
// Execute as normal (composer v1, or v2 without async)
424-
$process_download();
519+
// Execute as normal (Composer v1)
520+
return $process_install();
425521
}
426522

427523
/**
@@ -500,7 +596,7 @@ public function getInstalledJsonPath() {
500596
'*'
501597
);
502598

503-
if (!$installer_library_package || !$installer_library_package instanceof CompletePackage) {
599+
if (!$installer_library_package || !$installer_library_package instanceof CompletePackageInterface) {
504600
// Could not resolve the package. The package is most likely being
505601
// uninstalled.
506602
return FALSE;

0 commit comments

Comments
 (0)