composer = $composer;
$this->io = $io;
$this->executor = new ProcessExecutor($this->io);
$this->patches = array();
$this->installedPatches = array();
$this->lockFile = new JsonFile(
static::getPatchesLockFilePath(),
null,
$this->io
);
$this->locker = new Locker($this->lockFile);
$this->configuration = [
'disable-resolvers' => [
'type' => 'list',
'default' => [],
],
'disable-downloaders' => [
'type' => 'list',
'default' => [],
],
'disable-patchers' => [
'type' => 'list',
'default' => [],
],
'default-patch-depth' => [
'type' => 'int',
'default' => 1,
],
'package-depths' => [
'type' => 'list',
'default' => [],
],
'patches-file' => [
'type' => 'string',
'default' => 'patches.json',
],
"ignore-dependency-patches" => [
'type' => 'list',
'default' => [],
],
];
$this->configure($this->composer->getPackage()->getExtra(), 'composer-patches');
}
/**
* Returns an array of event names this subscriber wants to listen to.
*
* @calls resolvePatches
*/
public static function getSubscribedEvents(): array
{
return array(
PackageEvents::PRE_PACKAGE_INSTALL => ['loadLockedPatches'],
PackageEvents::PRE_PACKAGE_UPDATE => ['loadLockedPatches'],
// The POST_PACKAGE_* events are a higher weight for compatibility with
// https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with
// any Composer Plugin which deploys downloaded packages to other locations. In the cast that you want
// those plugins to deploy patched files, those plugins have to run *after* this plugin.
// @see: https://github.com/cweagans/composer-patches/pull/153
PackageEvents::POST_PACKAGE_INSTALL => ['patchPackage', 10],
PackageEvents::POST_PACKAGE_UPDATE => ['patchPackage', 10],
);
}
/**
* Return a list of plugin capabilities.
*
* @return array
*/
public function getCapabilities(): array
{
return [
ResolverProvider::class => CoreResolverProvider::class,
DownloaderProvider::class => CoreDownloaderProvider::class,
PatcherProvider::class => CorePatcherProvider::class,
CommandProviderCapability::class => CommandProvider::class,
];
}
/**
* Discover patches using all available Resolvers.
*/
public function resolvePatches()
{
$resolver = new Resolver($this->composer, $this->io, $this->getConfig('disable-resolvers'));
return $resolver->loadFromResolvers();
}
/**
* Resolve and download patches so that all sha256 sums can be included in the lock file.
*/
public function createNewPatchesLock()
{
$this->patchCollection = $this->resolvePatches();
$downloader = new Downloader($this->composer, $this->io, $this->getConfig('disable-downloaders'));
foreach ($this->patchCollection->getPatchedPackages() as $package) {
foreach ($this->patchCollection->getPatchesForPackage($package) as $patch) {
$this->download($patch);
$this->guessDepth($patch);
}
}
$this->locker->setLockData($this->patchCollection);
}
/**
* Load previously discovered patches from the Composer lock file.
*
* @param PackageEvent $event
* The event provided by Composer.
*/
public function loadLockedPatches()
{
$locked = $this->locker->isLocked();
if (!$locked) {
$filename = pathinfo($this->getLockFile()->getPath(), \PATHINFO_BASENAME);
$this->io->write("$filename does not exist. Creating a new $filename.");
$this->createNewPatchesLock();
return;
}
$this->patchCollection = PatchCollection::fromJson($this->locker->getLockData());
}
public function download(Patch $patch)
{
static $downloader;
if (is_null($downloader)) {
$downloader = new Downloader($this->composer, $this->io, $this->getConfig('disable-downloaders'));
}
$this->composer->getEventDispatcher()->dispatch(
PatchEvents::PRE_PATCH_DOWNLOAD,
new PatchEvent(PatchEvents::PRE_PATCH_DOWNLOAD, $patch, $this->composer, $this->io)
);
$downloader->downloadPatch($patch);
$this->composer->getEventDispatcher()->dispatch(
PatchEvents::POST_PATCH_DOWNLOAD,
new PatchEvent(PatchEvents::POST_PATCH_DOWNLOAD, $patch, $this->composer, $this->io)
);
}
public function guessDepth(Patch $patch)
{
$event = new PatchEvent(PatchEvents::PRE_PATCH_GUESS_DEPTH, $patch, $this->composer, $this->io);
$this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_GUESS_DEPTH, $event);
$patch = $event->getPatch();
$depth = $patch->depth ??
$this->getConfig('package-depths')[$patch->package] ??
Util::getDefaultPackagePatchDepth($patch->package) ??
$this->getConfig('default-patch-depth');
$patch->depth = $depth;
}
public function apply(Patch $patch, string $install_path)
{
static $patcher;
if (is_null($patcher)) {
$patcher = new Patcher($this->composer, $this->io, $this->getConfig('disable-patchers'));
}
$this->guessDepth($patch);
$event = new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $patch, $this->composer, $this->io);
$this->composer->getEventDispatcher()->dispatch(PatchEvents::PRE_PATCH_APPLY, $event);
$patch = $event->getPatch();
$this->io->write(
" - Applying patch {$patch->localPath} (depth: {$patch->depth})",
true,
IOInterface::DEBUG
);
$status = $patcher->applyPatch($patch, $install_path);
if ($status === false) {
$e = new Exception("No available patcher was able to apply patch {$patch->url} to {$patch->package}");
$this->composer->getEventDispatcher()->dispatch(
PatchEvents::POST_PATCH_APPLY_ERROR,
new PatchEvent(PatchEvents::POST_PATCH_APPLY_ERROR, $patch, $this->composer, $this->io, $e)
);
throw $e;
}
$this->composer->getEventDispatcher()->dispatch(
PatchEvents::POST_PATCH_APPLY,
new PatchEvent(PatchEvents::POST_PATCH_APPLY, $patch, $this->composer, $this->io)
);
}
/**
* Download and apply patches.
*
* @param PackageEvent $event
* The event that Composer provided to us.
*/
public function patchPackage(PackageEvent $event)
{
// Sometimes, patchPackage is called before a patch loading function (for instance, when composer-patches itself
// is installed -- the pre-install event can't be invoked before this plugin is installed, but the post-install
// event *can* be. Skipping composer-patches and composer-configurable-plugin ensures that this plugin and its
// dependency won't cause an error to be thrown when attempting to read from an uninitialized PatchCollection.
// This also means that neither composer-patches nor composer-configurable-plugin can have patches applied.
$package = $this->getPackageFromOperation($event->getOperation());
if (in_array($package->getName(), ['cweagans/composer-patches', 'cweagans/composer-configurable-plugin'])) {
return;
}
// If there aren't any patches, there's nothing to do.
if (empty($this->patchCollection->getPatchesForPackage($package->getName()))) {
$this->io->write(
"No patches found for {$package->getName()}",
true,
IOInterface::DEBUG,
);
return;
}
$install_path = $this->composer->getInstallationManager()
->getInstaller($package->getType())
->getInstallPath($package);
$this->io->write(" - Patching {$package->getName()}");
foreach ($this->patchCollection->getPatchesForPackage($package->getName()) as $patch) {
/** @var $patch Patch */
// Download patch.
$this->io->write(
" - Downloading and applying patch {$patch->url} ({$patch->description})",
true,
IOInterface::VERBOSE
);
$this->io->write(" - Downloading patch {$patch->url}", true, IOInterface::DEBUG);
$this->download($patch);
$this->guessDepth($patch);
// Apply patch.
$this->io->write(
" - Applying downloaded patch {$patch->localPath}",
true,
IOInterface::DEBUG
);
$this->apply($patch, $install_path);
}
$this->io->write(
" - All patches for {$package->getName()} have been applied.",
true,
IOInterface::DEBUG
);
}
/**
* Get a Package object from an OperationInterface object.
*
* @param OperationInterface $operation
* @return PackageInterface
* @throws InvalidArgumentException
*/
protected function getPackageFromOperation(OperationInterface $operation): PackageInterface
{
if ($operation instanceof InstallOperation) {
$package = $operation->getPackage();
} elseif ($operation instanceof UpdateOperation) {
$package = $operation->getTargetPackage();
} else {
throw new InvalidArgumentException('Unknown operation: ' . get_class($operation));
}
return $package;
}
public function getLocker(): Locker
{
return $this->locker;
}
public function getLockFile(): JsonFile
{
return $this->lockFile;
}
public function getPatchCollection(): ?PatchCollection
{
return $this->patchCollection;
}
public function deactivate(Composer $composer, IOInterface $io)
{
}
public function uninstall(Composer $composer, IOInterface $io)
{
}
}