ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
Patches.php
Go to the documentation of this file.
1 <?php
2 
8 namespace cweagans\Composer;
9 
27 
28 class Patches implements PluginInterface, EventSubscriberInterface {
29 
33  protected $composer;
37  protected $io;
41  protected $eventDispatcher;
45  protected $executor;
49  protected $patches;
50 
57  public function activate(Composer $composer, IOInterface $io) {
58  $this->composer = $composer;
59  $this->io = $io;
60  $this->eventDispatcher = $composer->getEventDispatcher();
61  $this->executor = new ProcessExecutor($this->io);
62  $this->patches = array();
63  }
64 
68  public static function getSubscribedEvents() {
69  return 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",
76  );
77  }
78 
83  public function checkPatches(Event $event) {
84  if (!$this->isPatchingEnabled()) {
85  return;
86  }
87 
88  try {
89  $repositoryManager = $this->composer->getRepositoryManager();
90  $localRepository = $repositoryManager->getLocalRepository();
91  $installationManager = $this->composer->getInstallationManager();
92  $packages = $localRepository->getPackages();
93 
94  $tmp_patches = $this->grabPatches();
95  if ($tmp_patches == FALSE) {
96  $this->io->write('<info>No patches supplied.</info>');
97  return;
98  }
99 
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);
104  }
105 
106  // Remove packages for which the patch set has changed.
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);
119  }
120  }
121  }
122  }
123  // If the Locker isn't available, then we don't need to do this.
124  // It's the first time packages have been installed.
125  catch (\LogicException $e) {
126  return;
127  }
128  }
129 
135  public function gatherPatches(PackageEvent $event) {
136  // If we've already done this, then don't do it again.
137  if (isset($this->patches['_patchesGathered'])) {
138  return;
139  }
140  // If patching has been disabled, bail out here.
141  elseif (!$this->isPatchingEnabled()) {
142  return;
143  }
144 
145  $this->patches = $this->grabPatches();
146  if (empty($this->patches)) {
147  $this->io->write('<info>No patches supplied.</info>');
148  }
149 
150  // Now add all the patches from dependencies that will be installed.
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') {
155  $package = $this->getPackageFromOperation($operation);
156  $extra = $package->getExtra();
157  if (isset($extra['patches'])) {
158  $this->patches = array_merge_recursive($this->patches, $extra['patches']);
159  }
160  }
161  }
162 
163  // If we're in verbose mode, list the projects we're going to patch.
164  if ($this->io->isVerbose()) {
165  foreach ($this->patches as $package => $patches) {
166  $number = count($patches);
167  $this->io->write('<info>Found ' . $number . ' patches for ' . $package . '.</info>');
168  }
169  }
170 
171  // Make sure we don't gather patches again. Extra keys in $this->patches
172  // won't hurt anything, so we'll just stash it there.
173  $this->patches['_patchesGathered'] = TRUE;
174  }
175 
181  public function grabPatches() {
182  // First, try to get the patches from the root composer.json.
183  $extra = $this->composer->getPackage()->getExtra();
184  if (isset($extra['patches'])) {
185  $this->io->write('<info>Gathering patches for root package.</info>');
186  $patches = $extra['patches'];
187  return $patches;
188  }
189  // If it's not specified there, look for a patches-file definition.
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']);
193  $patches = json_decode($patches, TRUE);
194  $error = json_last_error();
195  if ($error != 0) {
196  switch ($error) {
197  case JSON_ERROR_DEPTH:
198  $msg = ' - Maximum stack depth exceeded';
199  break;
200  case JSON_ERROR_STATE_MISMATCH:
201  $msg = ' - Underflow or the modes mismatch';
202  break;
203  case JSON_ERROR_CTRL_CHAR:
204  $msg = ' - Unexpected control character found';
205  break;
206  case JSON_ERROR_SYNTAX:
207  $msg = ' - Syntax error, malformed JSON';
208  break;
209  case JSON_ERROR_UTF8:
210  $msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded';
211  break;
212  default:
213  $msg = ' - Unknown error';
214  break;
215  }
216  throw new \Exception('There was an error in the supplied patches file:' . $msg);
217  }
218  if (isset($patches['patches'])) {
219  $patches = $patches['patches'];
220  return $patches;
221  }
222  elseif(!$patches) {
223  throw new \Exception('There was an error in the supplied patch file');
224  }
225  }
226  else {
227  return array();
228  }
229  }
230 
235  public function postInstall(PackageEvent $event) {
236  // Get the package object for the current operation.
237  $operation = $event->getOperation();
239  $package = $this->getPackageFromOperation($operation);
240  $package_name = $package->getName();
241 
242  if (!isset($this->patches[$package_name])) {
243  if ($this->io->isVerbose()) {
244  $this->io->write('<info>No patches found for ' . $package_name . '.</info>');
245  }
246  return;
247  }
248  $this->io->write(' - Applying patches for <info>' . $package_name . '</info>');
249 
250  // Get the install path from the package object.
251  $manager = $event->getComposer()->getInstallationManager();
252  $install_path = $manager->getInstaller($package->getType())->getInstallPath($package);
253 
254  // Set up a downloader.
255  $downloader = new RemoteFilesystem($this->io, $this->composer->getConfig());
256 
257  // Track applied patches in the package info in installed.json
258  $localRepository = $this->composer->getRepositoryManager()->getLocalRepository();
259  $localPackage = $localRepository->findPackage($package_name, $package->getVersion());
260  $extra = $localPackage->getExtra();
261  $extra['patches_applied'] = array();
262 
263  foreach ($this->patches[$package_name] as $description => $url) {
264  $this->io->write(' <info>' . $url . '</info> (<comment>' . $description. '</comment>)');
265  try {
266  $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description));
267  $this->getAndApplyPatch($downloader, $install_path, $url);
268  $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description));
269  $extra['patches_applied'][$description] = $url;
270  }
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)!");
275  }
276  }
277  }
278  $localPackage->setExtra($extra);
279 
280  $this->io->write('');
281  $this->writePatchReport($this->patches[$package_name], $install_path);
282  }
283 
291  protected function getPackageFromOperation(OperationInterface $operation) {
292  if ($operation instanceof InstallOperation) {
293  $package = $operation->getPackage();
294  }
295  elseif ($operation instanceof UpdateOperation) {
296  $package = $operation->getTargetPackage();
297  }
298  else {
299  throw new \Exception('Unknown operation: ' . get_class($operation));
300  }
301 
302  return $package;
303  }
304 
313  protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url) {
314 
315  // Local patch file.
316  if (file_exists($patch_url)) {
317  $filename = $patch_url;
318  }
319  else {
320  // Generate random (but not cryptographically so) filename.
321  $filename = uniqid("/tmp/") . ".patch";
322 
323  // Download file from remote filesystem to this location.
324  $hostname = parse_url($patch_url, PHP_URL_HOST);
325  $downloader->copy($hostname, $patch_url, $filename, FALSE);
326  }
327 
328  // Modified from drush6:make.project.inc
329  $patched = FALSE;
330  // The order here is intentional. p1 is most likely to apply with git apply.
331  // p0 is next likely. p2 is extremely unlikely, but for some special cases,
332  // it might be useful.
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);
336  if ($checked) {
337  // Apply the first successful style.
338  $patched = $this->executeCommand('cd %s && GIT_DIR=. git apply %s %s', $install_path, $patch_level, $filename);
339  break;
340  }
341  }
342 
343  // In some rare cases, git will fail to apply a patch, fallback to using
344  // the 'patch' command.
345  if (!$patched) {
346  foreach ($patch_levels as $patch_level) {
347  // --no-backup-if-mismatch here is a hack that fixes some
348  // differences between how patch works on windows and unix.
349  if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) {
350  break;
351  }
352  }
353  }
354 
355  // Clean up the temporary patch file.
356  if (isset($hostname)) {
357  unlink($filename);
358  }
359  // If the patch *still* isn't applied, then give up and throw an Exception.
360  // Otherwise, let the user know it worked.
361  if (!$patched) {
362  throw new \Exception("Cannot apply patch $patch_url");
363  }
364  }
365 
372  protected function isPatchingEnabled() {
373  $extra = $this->composer->getPackage()->getExtra();
374 
375  if (empty($extra['patches'])) {
376  // The root package has no patches of its own, so only allow patching if
377  // it has specifically opted in.
378  return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE;
379  }
380  else {
381  return TRUE;
382  }
383  }
384 
391  protected function writePatchReport($patches, $directory) {
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";
394  foreach ($patches as $description => $url) {
395  $output .= $description . "\n";
396  $output .= 'Source: ' . $url . "\n\n\n";
397  }
398  file_put_contents($directory . "/PATCHES.txt", $output);
399  }
400 
407  protected function executeCommand($cmd) {
408  // Shell-escape all arguments except the command.
409  $args = func_get_args();
410  foreach ($args as $index => $arg) {
411  if ($index !== 0) {
412  $args[$index] = escapeshellarg($arg);
413  }
414  }
415 
416  // And replace the arguments.
417  $command = call_user_func_array('sprintf', $args);
418  $output = '';
419  if ($this->io->isVerbose()) {
420  $this->io->write('<comment>' . $command . '</comment>');
421  $io = $this->io;
422  $output = function ($type, $data) use ($io) {
423  if ($type == Process::ERR) {
424  $io->write('<error>' . $data . '</error>');
425  }
426  else {
427  $io->write('<comment>' . $data . '</comment>');
428  }
429  };
430  }
431  return ($this->executor->execute($command, $output) == 0);
432  }
433 }
executeCommand($cmd)
Executes a shell command with escaping.
Definition: Patches.php:407
$type
grabPatches()
Get the patches from root composer or external file.
Definition: Patches.php:181
$index
Definition: metadata.php:60
checkPatches(Event $event)
Before running composer install,.
Definition: Patches.php:83
isPatchingEnabled()
Checks if the root package enables patching.
Definition: Patches.php:372
if(!is_dir( $entity_dir)) exit("Fatal Error ([A-Za-z0-9]+)\+" &#(? foreach( $entity_files as $file) $output
$error
Definition: Error.php:17
static getSubscribedEvents()
Returns an array of event names this subscriber wants to listen to.
Definition: Patches.php:68
getPackageFromOperation(OperationInterface $operation)
Get a Package object from an OperationInterface object.
Definition: Patches.php:291
activate(Composer $composer, IOInterface $io)
Apply plugin modifications to composer.
Definition: Patches.php:57
Create styles array
The data for the language used.
$url
getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url)
Apply a patch on code in the specified directory.
Definition: Patches.php:313
writePatchReport($patches, $directory)
Writes a patch report to the target directory.
Definition: Patches.php:391
gatherPatches(PackageEvent $event)
Gather patches from dependencies and store them for later use.
Definition: Patches.php:135