11use Composer\DependencyResolver\Operation\InstallOperation;
 
   12use Composer\DependencyResolver\Operation\UninstallOperation;
 
   13use Composer\DependencyResolver\Operation\UpdateOperation;
 
   14use Composer\DependencyResolver\Operation\OperationInterface;
 
   15use Composer\EventDispatcher\EventSubscriberInterface;
 
   16use Composer\IO\IOInterface;
 
   17use Composer\Package\AliasPackage;
 
   18use Composer\Package\PackageInterface;
 
   19use Composer\Plugin\PluginInterface;
 
   20use Composer\Installer\PackageEvents;
 
   21use Composer\Script\Event;
 
   22use Composer\Script\ScriptEvents;
 
   23use Composer\Script\PackageEvent;
 
   24use Composer\Util\ProcessExecutor;
 
   25use Composer\Util\RemoteFilesystem;
 
   26use Symfony\Component\Process\Process;
 
   28class Patches implements PluginInterface, EventSubscriberInterface {
 
   60    $this->eventDispatcher = 
$composer->getEventDispatcher();
 
   61    $this->executor = 
new ProcessExecutor($this->io);
 
   62    $this->patches = array();
 
   70      ScriptEvents::PRE_INSTALL_CMD => 
"checkPatches",
 
   71      ScriptEvents::PRE_UPDATE_CMD => 
"checkPatches",
 
   72      PackageEvents::PRE_PACKAGE_INSTALL => 
"gatherPatches",
 
   73      PackageEvents::PRE_PACKAGE_UPDATE => 
"gatherPatches",
 
   74      PackageEvents::POST_PACKAGE_INSTALL => 
"postInstall",
 
   75      PackageEvents::POST_PACKAGE_UPDATE => 
"postInstall",
 
   89      $repositoryManager = $this->composer->getRepositoryManager();
 
   90      $localRepository = $repositoryManager->getLocalRepository();
 
   91      $installationManager = $this->composer->getInstallationManager();
 
   92      $packages = $localRepository->getPackages();
 
   95      if ($tmp_patches == FALSE) {
 
   96        $this->io->write(
'<info>No patches supplied.</info>');
 
  100      foreach ($packages as $package) {
 
  101        $extra = $package->getExtra();
 
  102        $patches = isset($extra[
'patches']) ? $extra[
'patches'] : array();
 
  103        $tmp_patches = array_merge_recursive($tmp_patches, 
$patches);
 
  107      foreach ($packages as $package) {
 
  108        if (!($package instanceof AliasPackage)) {
 
  109          $package_name = $package->getName();
 
  110          $extra = $package->getExtra();
 
  111          $has_patches = isset($tmp_patches[$package_name]);
 
  112          $has_applied_patches = isset($extra[
'patches_applied']);
 
  113          if (($has_patches && !$has_applied_patches)
 
  114            || (!$has_patches && $has_applied_patches)
 
  115            || ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra[
'patches_applied'])) {
 
  116            $uninstallOperation = 
new UninstallOperation($package, 
'Removing package so it can be re-installed and re-patched.');
 
  117            $this->io->write(
'<info>Removing package ' . $package_name . 
' so that it can be re-installed and re-patched.</info>');
 
  118            $installationManager->uninstall($localRepository, $uninstallOperation);
 
  125    catch (\LogicException $e) {
 
  137    if (isset($this->patches[
'_patchesGathered'])) {
 
  146    if (empty($this->patches)) {
 
  147      $this->io->write(
'<info>No patches supplied.</info>');
 
  151    $operations = $event->getOperations();
 
  152    $this->io->write(
'<info>Gathering patches for dependencies. This might take a minute.</info>');
 
  153    foreach ($operations as $operation) {
 
  154      if ($operation->getJobType() == 
'install' || $operation->getJobType() == 
'update') {
 
  156        $extra = $package->getExtra();
 
  157        if (isset($extra[
'patches'])) {
 
  158          $this->patches = array_merge_recursive($this->patches, $extra[
'patches']);
 
  164    if ($this->io->isVerbose()) {
 
  165      foreach ($this->patches as $package => 
$patches) {
 
  167        $this->io->write(
'<info>Found ' . $number . 
' patches for ' . $package . 
'.</info>');
 
  173    $this->patches[
'_patchesGathered'] = TRUE;
 
  183    $extra = $this->composer->getPackage()->getExtra();
 
  184    if (isset($extra[
'patches'])) {
 
  185      $this->io->write(
'<info>Gathering patches for root package.</info>');
 
  190    elseif (isset($extra[
'patches-file'])) {
 
  191      $this->io->write(
'<info>Gathering patches from patch file.</info>');
 
  192      $patches = file_get_contents($extra[
'patches-file']);
 
  194      $error = json_last_error();
 
  197          case JSON_ERROR_DEPTH:
 
  198            $msg = 
' - Maximum stack depth exceeded';
 
  200          case JSON_ERROR_STATE_MISMATCH:
 
  201            $msg =  
' - Underflow or the modes mismatch';
 
  203          case JSON_ERROR_CTRL_CHAR:
 
  204            $msg = 
' - Unexpected control character found';
 
  206          case JSON_ERROR_SYNTAX:
 
  207            $msg =  
' - Syntax error, malformed JSON';
 
  209          case JSON_ERROR_UTF8:
 
  210            $msg =  
' - Malformed UTF-8 characters, possibly incorrectly encoded';
 
  213            $msg =  
' - Unknown error';
 
  216          throw new \Exception(
'There was an error in the supplied patches file:' . $msg);
 
  223        throw new \Exception(
'There was an error in the supplied patch file');
 
  235  public function postInstall(PackageEvent $event) {
 
  237    $operation = $event->getOperation();
 
  240    $package_name = $package->getName();
 
  242    if (!isset($this->patches[$package_name])) {
 
  243      if ($this->io->isVerbose()) {
 
  244        $this->io->write(
'<info>No patches found for ' . $package_name . 
'.</info>');
 
  248    $this->io->write(
'  - Applying patches for <info>' . $package_name . 
'</info>');
 
  251    $manager = $event->getComposer()->getInstallationManager();
 
  252    $install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
 
  255    $downloader = 
new RemoteFilesystem($this->io, $this->composer->getConfig());
 
  258    $localRepository = $this->composer->getRepositoryManager()->getLocalRepository();
 
  259    $localPackage = $localRepository->findPackage($package_name, $package->getVersion());
 
  260    $extra = $localPackage->getExtra();
 
  261    $extra[
'patches_applied'] = array();
 
  264      $this->io->write(
'    <info>' . 
$url . 
'</info> (<comment>' . 
$description. 
'</comment>)');
 
  271      catch (\Exception $e) {
 
  272        $this->io->write(
'   <error>Could not apply patch! Skipping.</error>');
 
  273        if (getenv(
'COMPOSER_EXIT_ON_PATCH_FAILURE')) {
 
  274          throw new \Exception(
"Cannot apply patch $description ($url)!");
 
  278    $localPackage->setExtra($extra);
 
  280    $this->io->write(
'');
 
  292    if ($operation instanceof InstallOperation) {
 
  293      $package = $operation->getPackage();
 
  295    elseif ($operation instanceof UpdateOperation) {
 
  296      $package = $operation->getTargetPackage();
 
  299      throw new \Exception(
'Unknown operation: ' . get_class($operation));
 
  313  protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url) {
 
  316    if (file_exists($patch_url)) {
 
  324      $hostname = parse_url($patch_url, PHP_URL_HOST);
 
  325      $downloader->copy($hostname, $patch_url, 
$filename, FALSE);
 
  333    $patch_levels = array(
'-p1', 
'-p0', 
'-p2');
 
  334    foreach ($patch_levels as $patch_level) {
 
  335      $checked = $this->
executeCommand(
'cd %s && GIT_DIR=. git apply --check %s %s', $install_path, $patch_level, 
$filename);
 
  338        $patched = $this->
executeCommand(
'cd %s && GIT_DIR=. git apply %s %s', $install_path, $patch_level, 
$filename);
 
  346      foreach ($patch_levels as $patch_level) {
 
  349        if ($patched = $this->
executeCommand(
"patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, 
$filename)) {
 
  356    if (isset($hostname)) {
 
  362      throw new \Exception(
"Cannot apply patch $patch_url");
 
  373    $extra = $this->composer->getPackage()->getExtra();
 
  375    if (empty($extra[
'patches'])) {
 
  378      return isset($extra[
'enable-patching']) ? $extra[
'enable-patching'] : FALSE;
 
  392    $output = 
"This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n";
 
  393    $output .= 
"Patches applied to this directory:\n\n";
 
  398    file_put_contents($directory . 
"/PATCHES.txt", 
$output);
 
  409    $args = func_get_args();
 
  410    foreach ($args as 
$index => $arg) {
 
  412        $args[
$index] = escapeshellarg($arg);
 
  417    $command = call_user_func_array(
'sprintf', $args);
 
  419    if ($this->io->isVerbose()) {
 
  420      $this->io->write(
'<comment>' . $command . 
'</comment>');
 
  423        if (
$type == Process::ERR) {
 
  424          $io->write(
'<error>' . 
$data . 
'</error>');
 
  427          $io->write(
'<comment>' . 
$data . 
'</comment>');
 
  431    return ($this->executor->execute($command, 
$output) == 0);
 
An exception for terminatinating execution or to throw for unit testing.
gatherPatches(PackageEvent $event)
Gather patches from dependencies and store them for later use.
getPackageFromOperation(OperationInterface $operation)
Get a Package object from an OperationInterface object.
activate(Composer $composer, IOInterface $io)
Apply plugin modifications to composer.
writePatchReport($patches, $directory)
Writes a patch report to the target directory.
executeCommand($cmd)
Executes a shell command with escaping.
static getSubscribedEvents()
Returns an array of event names this subscriber wants to listen to.
grabPatches()
Get the patches from root composer or external file.
checkPatches(Event $event)
Before running composer install,.
getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url)
Apply a patch on code in the specified directory.
isPatchingEnabled()
Checks if the root package enables patching.