60e73257c73aa32394fcee1c117bdceb0abbddc1
[yaffs-website] / vendor / drush / drush / commands / make / make.project.inc
1 <?php
2 /**
3  * @file
4  * Drush Make processing classes.
5  */
6
7 use Drush\Log\LogLevel;
8
9 /**
10  * The base project class.
11  */
12 class DrushMakeProject {
13
14   /**
15    * TRUE if make() has been called, otherwise FALSE.
16    */
17   protected $made = FALSE;
18
19   /**
20    * TRUE if download() method has been called successfully, otherwise FALSE.
21    */
22   protected $downloaded = NULL;
23
24   /**
25    * Download location to use.
26    */
27   protected $download_location = NULL;
28
29   /**
30    * Keep track of instances.
31    *
32    * @see DrushMakeProject::getInstance()
33    */
34   protected static $self = array();
35
36   /**
37    * Keeps track of projects being processed to prevent recursive conflicts.
38    *
39    * Simple array of machine names.
40    *
41    * @var array
42    */
43   protected $manifest = array();
44
45   /**
46    * Default to overwrite to allow recursive builds to process properly.
47    *
48    * TODO refactor this to be more selective. Ideally a merge would take place
49    * instead of an overwrite.
50    */
51   protected $overwrite = TRUE;
52
53   /**
54    * Recursively process any makefiles found in downloaded projects.
55    */
56   protected $do_recursion = TRUE;
57
58   /**
59    * Which variant of profiles to download.
60    */
61   protected $variant = 'profile-only';
62
63   /**
64    * Set attributes and retrieve project information.
65    */
66   protected function __construct($project) {
67     $project['base_contrib_destination'] = $project['contrib_destination'];
68     foreach ($project as $key => $value) {
69       $this->{$key} = $value;
70     }
71     if (!empty($this->options['working-copy'])) {
72       $this->download['working-copy'] = TRUE;
73     }
74     // Don't recurse when we're using a pre-built profile tarball.
75     if ($this->variant == 'projects') {
76       $this->do_recursion = FALSE;
77     }
78   }
79
80   /**
81    * Get an instance for the type and project.
82    *
83    * @param string $type
84    *   Type of project: core, library, module, profile, or translation.
85    * @param array $project
86    *   Project information.
87    *
88    * @return mixed
89    *   An instance for the project or FALSE if invalid type.
90    */
91   public static function getInstance($type, $project) {
92     if (!isset(self::$self[$type][$project['name']])) {
93       $class = 'DrushMakeProject_' . $type;
94       self::$self[$type][$project['name']] = class_exists($class) ? new $class($project) : FALSE;
95     }
96     return self::$self[$type][$project['name']];
97   }
98
99   /**
100    * Set the manifest array.
101    *
102    * @param array $manifest
103    *   An array of projects as generated by `make_projects`.
104    */
105   public function setManifest($manifest) {
106     $this->manifest = $manifest;
107   }
108
109   /**
110    * Download a project.
111    */
112   function download() {
113     $this->downloaded = TRUE;
114
115     // In some cases, make_download_factory() is going to need to know the
116     // full version string of the project we're trying to download. However,
117     // the version is a project-level attribute, not a download-level
118     // attribute. So, if we don't already have a full version string in the
119     // download array (e.g. if it was initialized via the release history XML
120     // for the PM case), we take the version info from the project-level
121     // attribute, convert it into a full version string, and stuff it into
122     // $this->download so that the download backend has access to it, too.
123     if (!empty($this->version) && empty($this->download['full_version'])) {
124       $full_version = '';
125       $matches = array();
126       // Core needs different conversion rules than contrib.
127       if (!empty($this->type) && $this->type == 'core') {
128         // Generally, the version for core is already set properly.
129         $full_version = $this->version;
130         // However, it might just be something like '7' or '7.x', in which
131         // case we need to turn that into '7.x-dev';
132         if (preg_match('/^\d+(\.x)?$/', $this->version, $matches)) {
133           // If there's no '.x' already, append it.
134           if (empty($matches[1])) {
135             $full_version .= '.x';
136           }
137           $full_version .= '-dev';
138         }
139       }
140       // Contrib.
141       else {
142         // If the version doesn't already define a core version, prepend it.
143         if (!preg_match('/^\d+\.x-\d+.*$/', $this->version)) {
144           // Just find the major version from $this->core so we don't end up
145           // with version strings like '7.12-2.0'.
146           $core_parts = explode('.', $this->core);
147           $full_version = $core_parts[0] . '.x-';
148         }
149         $full_version .= $this->version;
150         // If the project-level version attribute is just a number it's a major
151         // version.
152         if (preg_match('/^\d+(\.x)?$/', $this->version, $matches)) {
153           // If there's no '.x' already, append it.
154           if (empty($matches[1])) {
155             $full_version .= '.x';
156           }
157           $full_version .= '-dev';
158         }
159       }
160       $this->download['full_version'] = $full_version;
161     }
162
163     $this->download['variant'] = $this->variant;
164
165     if (make_download_factory($this->name, $this->type, $this->download, $this->download_location) === FALSE) {
166       $this->downloaded = FALSE;
167     }
168     return $this->downloaded;
169   }
170
171   /**
172    * Build a project.
173    */
174   function make() {
175     if ($this->made) {
176       drush_log(dt('Attempt to build project @project more then once prevented.', array('@project' => $this->name)));
177       return TRUE;
178     }
179     $this->made = TRUE;
180
181     if (!isset($this->download_location)) {
182       $this->download_location = $this->findDownloadLocation();
183     }
184     if ($this->download() === FALSE) {
185       return FALSE;
186     }
187     if (!$this->addLockfile($this->download_location)) {
188       return FALSE;
189     }
190     if (!$this->applyPatches($this->download_location)) {
191       return FALSE;
192     }
193     if (!$this->getTranslations($this->download_location)) {
194       return FALSE;
195     }
196     // Handle .info file re-writing (if so desired).
197     if (!drush_get_option('no-gitinfofile', FALSE) && isset($this->download['type']) && $this->download['type'] == 'git') {
198       $this->processGitInfoFiles();
199     }
200     // Clean-up .git directories.
201     if (!_get_working_copy_option($this->download)) {
202       $this->removeGitDirectory();
203     }
204     if (!$this->recurse($this->download_location)) {
205       return FALSE;
206     }
207     return TRUE;
208   }
209
210   /**
211    * Determine the location to download project to.
212    */
213   function findDownloadLocation() {
214     $this->path = $this->generatePath();
215     $this->project_directory = !empty($this->directory_name) ? $this->directory_name : $this->name;
216     $this->download_location = $this->path . '/' . $this->project_directory;
217     // This directory shouldn't exist yet -- if it does, stop,
218     // unless overwrite has been set to TRUE.
219     if (is_dir($this->download_location) && !$this->overwrite) {
220       return drush_set_error('MAKE_DIRECTORY_EXISTS', dt('Directory not empty: !directory', array('!directory' => $this->download_location)));
221     }
222     elseif ($this->download['type'] === 'pm') {
223       // pm-download will create the final contrib directory.
224       drush_mkdir(dirname($this->download_location));
225     }
226     else {
227       drush_mkdir($this->download_location);
228     }
229     return $this->download_location;
230   }
231
232   /**
233    * Rewrite relative URLs and file:/// URLs
234    *
235    * relative path -> absolute path using the make_directory
236    * local file:/// urls -> local paths
237    *
238    * @param mixed &$info
239    *   Either an array or a simple url string. The `$info` variable will be
240    *   transformed into an array.
241    */
242   protected function preprocessLocalFileUrl(&$info) {
243     if (is_string($info)) {
244       $info = array('url' => $info, 'local' => FALSE);
245     }
246
247     if (!_drush_is_url($info['url']) && !drush_is_absolute_path($info['url'])) {
248       $info['url'] = $this->make_directory . '/' . $info['url'];
249       $info['local'] = TRUE;
250     } elseif (substr($info['url'], 0, 8) == 'file:///') {
251       $info['url'] = substr($info['url'], 7);
252       $info['local'] = TRUE;
253     }
254   }
255
256   /**
257    * Retrieve and apply any patches specified by the makefile to this project.
258    */
259   function applyPatches($project_directory) {
260     if (empty($this->patch)) {
261       return TRUE;
262     }
263
264     $patches_txt = '';
265     $local_patches = array();
266     $ignore_checksums = drush_get_option('ignore-checksums');
267     foreach ($this->patch as $info) {
268       $this->preprocessLocalFileUrl($info);
269
270       // Download the patch.
271       if ($filename = _make_download_file($info['url'])) {
272         $patched = FALSE;
273         $output = '';
274         // Test each patch style; -p1 is the default with git. See
275         // http://drupal.org/node/1054616
276         $patch_levels = array('-p1', '-p0');
277         foreach ($patch_levels as $patch_level) {
278           $checked = drush_shell_cd_and_exec($project_directory, 'git --git-dir=. apply --check %s %s --verbose', $patch_level, $filename);
279           if ($checked) {
280             // Apply the first successful style.
281             $patched = drush_shell_cd_and_exec($project_directory, 'git --git-dir=. apply %s %s --verbose', $patch_level, $filename);
282             break;
283           }
284         }
285
286         // In some rare cases, git will fail to apply a patch, fallback to using
287         // the 'patch' command.
288         if (!$patched) {
289           foreach ($patch_levels as $patch_level) {
290             // --no-backup-if-mismatch here is a hack that fixes some
291             // differences between how patch works on windows and unix.
292             if ($patched = drush_shell_exec("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $project_directory, $filename)) {
293               break;
294             }
295           }
296         }
297
298         if ($output = drush_shell_exec_output()) {
299           // Log any command output, visible only in --verbose or --debug mode.
300           drush_log(implode("\n", $output));
301         }
302
303         // Set up string placeholders to pass to dt().
304         $dt_args = array(
305           '@name' => $this->name,
306           '@filename' => basename($filename),
307         );
308
309         if ($patched) {
310           if (!$ignore_checksums && !_make_verify_checksums($info, $filename)) {
311              return FALSE;
312           }
313           $patch_url = $info['url'];
314
315           // If this is a local patch, copy that into place as well.
316           if ($info['local']) {
317             $local_patches[] = $info['url'];
318             // Use a local path for the PATCHES.txt file.
319             $pathinfo = pathinfo($patch_url);
320             $patch_url = $pathinfo['basename'];
321           }
322           $patches_txt .= '- ' . $patch_url . "\n";
323
324           drush_log(dt('@name patched with @filename.', $dt_args), LogLevel::OK);
325         }
326         else {
327           make_error('PATCH_ERROR', dt("Unable to patch @name with @filename.", $dt_args));
328         }
329         drush_op('unlink', $filename);
330       }
331       else {
332         make_error('DOWNLOAD_ERROR', 'Unable to download ' . $info['url'] . '.');
333         return FALSE;
334       }
335     }
336     if (!empty($patches_txt) && !drush_get_option('no-patch-txt') && !file_exists($project_directory . '/PATCHES.txt')) {
337       $patches_txt = "The following patches have been applied to this project:\n" .
338         $patches_txt .
339         "\nThis file was automatically generated by Drush Make (http://drupal.org/project/drush).\n";
340       file_put_contents($project_directory . '/PATCHES.txt', $patches_txt);
341       drush_log('Generated PATCHES.txt file for ' . $this->name, LogLevel::OK);
342
343       // Copy local patches into place.
344       foreach ($local_patches as $url) {
345         $pathinfo = pathinfo($url);
346         drush_copy_dir($url, $project_directory . '/' . $pathinfo['basename']);
347       }
348     }
349     return TRUE;
350   }
351
352   /**
353    * Process info files when downloading things from git.
354    */
355   function processGitInfoFiles() {
356     // Bail out if this isn't hosted on Drupal.org (unless --force-gitinfofile option was specified).
357     if (!drush_get_option('force-gitinfofile', FALSE) && isset($this->download['url']) && strpos($this->download['url'], 'drupal.org') === FALSE) {
358       return;
359     }
360
361     // Figure out the proper version string to use based on the .make file.
362     // Best case is the .make file author told us directly.
363     if (!empty($this->download['full_version'])) {
364       $full_version = $this->download['full_version'];
365     }
366     // Next best is if we have a tag, since those are identical to versions.
367     elseif (!empty($this->download['tag'])) {
368       $full_version = $this->download['tag'];
369     }
370     // If we have a branch, append '-dev'.
371     elseif (!empty($this->download['branch'])) {
372       $full_version = $this->download['branch'] . '-dev';
373     }
374     // Ugh. Not sure what else we can do in this case.
375     elseif (!empty($this->download['revision'])) {
376       $full_version = $this->download['revision'];
377     }
378     // Probably can never reach this case.
379     else {
380       $full_version = 'unknown';
381     }
382
383     // If the version string ends in '.x-dev' do the Git magic to figure out
384     // the appropriate 'rebuild version' string, e.g. '7.x-1.2+7-dev'.
385     $matches = array();
386     if (preg_match('/^(.+).x-dev$/', $full_version, $matches)) {
387       require_once dirname(__FILE__) . '/../pm/package_handler/git_drupalorg.inc';
388       $rebuild_version = drush_pm_git_drupalorg_compute_rebuild_version($this->download_location, $matches[1]);
389       if ($rebuild_version) {
390         $full_version = $rebuild_version;
391       }
392     }
393     require_once dirname(__FILE__) . '/../pm/pm.drush.inc';
394     if (drush_shell_cd_and_exec($this->download_location, 'git log -1 --pretty=format:%ct')) {
395       $output = drush_shell_exec_output();
396       $datestamp = $output[0];
397     }
398     else {
399       $datestamp = time();
400     }
401     drush_pm_inject_info_file_metadata($this->download_location, $this->name, $full_version, $datestamp);
402   }
403
404   /**
405    * Remove the .git directory from a project.
406    */
407   function removeGitDirectory() {
408     if (isset($this->download['type']) && $this->download['type'] == 'git' && file_exists($this->download_location . '/.git')) {
409       drush_delete_dir($this->download_location . '/.git', TRUE);
410     }
411   }
412
413   /**
414    * Add a lock file.
415    */
416   function addLockfile($project_directory) {
417     if (!empty($this->lock)) {
418       file_put_contents($project_directory . '/.drush-lock-update', $this->lock);
419     }
420     return TRUE;
421   }
422
423   /**
424    * Retrieve translations for this project.
425    */
426   function getTranslations($project_directory) {
427     static $cache = array();
428     $langcodes = $this->translations;
429     if ($langcodes && in_array($this->type, array('core', 'module', 'profile', 'theme'), TRUE)) {
430       // Support the l10n_path, l10n_url keys from l10n_update. Note that the
431       // l10n_server key is not supported.
432       if (isset($this->l10n_path)) {
433         $update_url = $this->l10n_path;
434       }
435       else {
436         if (isset($this->l10n_url)) {
437           $l10n_server = $this->l10n_url;
438         }
439         else {
440           $l10n_server = FALSE;
441         }
442         if ($l10n_server) {
443           if (!isset($cache[$l10n_server])) {
444             $this->preprocessLocalFileUrl($l10n_server);
445             $l10n_server = $l10n_server['url'];
446             if ($filename = _make_download_file($l10n_server)) {
447               $server_info = simplexml_load_string(file_get_contents($filename));
448               $cache[$l10n_server] = !empty($server_info->update_url) ? $server_info->update_url : FALSE;
449             }
450           }
451           if ($cache[$l10n_server]) {
452             $update_url = $cache[$l10n_server];
453           }
454           else {
455             make_error('XML_ERROR', dt("Could not retrieve l10n update url for !project.", array('!project' => $this->name)));
456             return FALSE;
457           }
458         }
459       }
460       if ($update_url) {
461         $failed = array();
462         foreach ($langcodes as $langcode) {
463           $variables = array(
464             '%project' => $this->name,
465             '%release' => $this->download['full_version'],
466             '%core' => $this->core,
467             '%language' => $langcode,
468             '%filename' => '%filename',
469           );
470           $url = strtr($update_url, $variables);
471
472           // Download the translation file.  Since its contents are volatile,
473           // cache for only 4 hours.
474           if ($filename = _make_download_file($url, 3600 * 4)) {
475             // If this is the core project type, download the translation file
476             // and place it in every profile and an additional copy in
477             // modules/system/translations where it can be detected for import
478             // by other non-default install profiles.
479             if ($this->type === 'core') {
480               $profiles = drush_scan_directory($project_directory . '/profiles', '/.*/', array(), 0, FALSE, 'filename', 0, TRUE);
481               foreach ($profiles as $profile) {
482                 if (is_dir($project_directory . '/profiles/' . $profile->basename)) {
483                   drush_mkdir($project_directory . '/profiles/' . $profile->basename . '/translations');
484                   drush_copy_dir($filename, $project_directory . '/profiles/' . $profile->basename . '/translations/' . $langcode . '.po');
485                 }
486               }
487               drush_mkdir($project_directory . '/modules/system/translations');
488               drush_copy_dir($filename, $project_directory . '/modules/system/translations/' . $langcode . '.po');
489             }
490             else {
491               drush_mkdir($project_directory . '/translations');
492               drush_copy_dir($filename, $project_directory . '/translations/' . $langcode . '.po', FILE_EXISTS_OVERWRITE);
493             }
494           }
495           else {
496             $failed[] = $langcode;
497           }
498         }
499         if (empty($failed)) {
500           drush_log('All translations downloaded for ' . $this->name, LogLevel::OK);
501         }
502         else {
503           drush_log('Unable to download translations for ' . $this->name . ': ' . implode(', ', $failed), LogLevel::WARNING);
504         }
505       }
506     }
507     return TRUE;
508   }
509
510   /**
511    * Generate the proper path for this project type.
512    *
513    * @param boolean $base
514    *   Whether include the base part (tmp dir). Defaults to TRUE.
515    */
516   protected function generatePath($base = TRUE) {
517     $path = array();
518     if ($base) {
519       $path[] = make_tmp();
520       $path[] = '__build__';
521     }
522     if (!empty($this->contrib_destination)) {
523       $path[] = $this->contrib_destination;
524     }
525     if (!empty($this->subdir)) {
526       $path[] = $this->subdir;
527     }
528     return implode('/', $path);
529   }
530
531   /**
532    * Return the proper path for dependencies to be placed in.
533    *
534    * @return string
535    *   The path that dependencies will be placed in.
536    */
537   protected function buildPath($directory) {
538     return $this->base_contrib_destination;
539   }
540
541   /**
542    * Recurse to process additional makefiles that may be found during
543    * processing.
544    */
545   function recurse($path) {
546     if (!$this->do_recursion || drush_get_option('no-recursion')) {
547       drush_log(dt("Preventing recursive makefile parsing for !project",
548                 array("!project" => $this->name)), LogLevel::NOTICE);
549       return TRUE;
550     }
551     $candidates = array(
552       $this->name . '.make.yml',
553       $this->name . '.make',
554       'drupal-org.make.yml',
555       'drupal-org.make',
556     );
557     $makefile = FALSE;
558     foreach ($candidates as $filename) {
559       if (file_exists($this->download_location . '/' . $filename)) {
560         $makefile = $this->download_location . '/' . $filename;
561         break;
562       }
563     }
564
565     if (!$makefile) {
566       return TRUE;
567     }
568
569     drush_log(dt("Found makefile: !makefile", array("!makefile" => basename($makefile))), LogLevel::OK);
570
571     // Save the original state of the 'custom' context.
572     $custom_context = &drush_get_context('custom');
573     $original_custom_context_values = $custom_context;
574
575     $info = make_parse_info_file($makefile, TRUE, $this->options);
576     if (!($info = make_validate_info_file($info))) {
577       $result = FALSE;
578     }
579     else {
580       // Inherit the translations specified in the extender makefile.
581       if (!empty($this->translations)) {
582         $info['translations'] = $this->translations;
583       }
584       // Strip out any modules that have already been processed before this.
585       foreach ($this->manifest as $name) {
586         unset($info['projects'][$name]);
587       }
588       $build_path = $this->buildPath($this->name);
589       make_projects(TRUE, trim($build_path, '/'), $info, $this->build_path, $this->download_location);
590       make_libraries(TRUE, trim($build_path, '/'), $info, $this->build_path, $this->download_location);
591       $result = TRUE;
592     }
593     // Restore original 'custom' context so that any
594     // settings changes made are used.
595     $custom_context = $original_custom_context_values;
596
597     return $result;
598   }
599 }
600
601 /**
602  * For processing Drupal core projects.
603  */
604 class DrushMakeProject_Core extends DrushMakeProject {
605   /**
606    * Override constructor for core to adjust project info.
607    */
608   protected function __construct(&$project) {
609     parent::__construct($project);
610     // subdir and contrib_destination are not allowed for core.
611     $this->subdir = '';
612     $this->contrib_destination = '';
613   }
614
615   /**
616    * Determine the location to download project to.
617    */
618   function findDownloadLocation() {
619     $this->path = $this->download_location = $this->generatePath();
620     $this->project_directory = '';
621     if (is_dir($this->download_location)) {
622       return drush_set_error('MAKE_DIRECTORY_EXISTS', dt('Directory not empty: !directory', array('!directory' => $this->download_location)));
623     }
624     elseif ($this->download['type'] === 'pm') {
625       // pm-download will create the final __build__ directory, so nothing to do
626       // here.
627     }
628     else {
629       drush_mkdir($this->download_location);
630     }
631     return $this->download_location;
632   }
633 }
634
635 /**
636  * For processing libraries.
637  */
638 class DrushMakeProject_Library extends DrushMakeProject {
639   /**
640    * Override constructor for libraries to properly set contrib destination.
641    */
642   protected function __construct(&$project) {
643     parent::__construct($project);
644     // Allow libraries to specify where they should live in the build path.
645     if (isset($project['destination'])) {
646       $project_path = $project['destination'];
647     }
648     else {
649       $project_path = 'libraries';
650     }
651
652     $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . $project_path;
653   }
654
655   /**
656    * No recursion for libraries, sorry :-(
657    */
658   function recurse($path) {
659     // Return TRUE so that processing continues in the make() method.
660     return TRUE;
661   }
662
663   /**
664    * No translations for libraries.
665    */
666   function getTranslations($download_location) {
667     // Return TRUE so that processing continues in the make() method.
668     return TRUE;
669   }
670 }
671
672 /**
673  * For processing modules.
674  */
675 class DrushMakeProject_Module extends DrushMakeProject {
676   /**
677    * Override constructor for modules to properly set contrib destination.
678    */
679   protected function __construct(&$project) {
680     parent::__construct($project);
681     $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . 'modules';
682   }
683 }
684
685 /**
686  * For processing installation profiles.
687  */
688 class DrushMakeProject_Profile extends DrushMakeProject {
689   /**
690    * Override contructor for installation profiles to properly set contrib
691    * destination.
692    */
693   protected function __construct(&$project) {
694     parent::__construct($project);
695     $this->contrib_destination = (!empty($this->destination) ? $this->destination : 'profiles');
696   }
697
698   /**
699    * Find the build path.
700    */
701   protected function buildPath($directory) {
702     return $this->generatePath(FALSE) . '/' . $directory;
703   }
704 }
705
706 /**
707  * For processing themes.
708  */
709 class DrushMakeProject_Theme extends DrushMakeProject {
710   /**
711    * Override contructor for themes to properly set contrib destination.
712    */
713   protected function __construct(&$project) {
714     parent::__construct($project);
715     $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . 'themes';
716   }
717 }
718
719 /**
720  * For processing translations.
721  */
722 class DrushMakeProject_Translation extends DrushMakeProject {
723   /**
724    * Override constructor for translations to properly set contrib destination.
725    */
726   protected function __construct(&$project) {
727     parent::__construct($project);
728     switch ($project['core']) {
729       case '5.x':
730         // Don't think there's an automatic place we can put 5.x translations,
731         // so we'll toss them in a translations directory in the Drupal root.
732         $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . 'translations';
733         break;
734
735       default:
736         $this->contrib_destination = '';
737         break;
738     }
739   }
740 }