Version 1
[yaffs-website] / vendor / drush / drush / commands / make / make.drush.inc
1 <?php
2 /**
3  * @file
4  * Drush Make commands.
5  */
6
7 use Drush\Log\LogLevel;
8 use Drush\UpdateService\ReleaseInfo;
9
10 /**
11  * Default localization server for downloading translations.
12  */
13 define('MAKE_DEFAULT_L10N_SERVER', 'http://ftp.drupal.org/files/translations/l10n_server.xml');
14
15 /**
16  * Make refuses to build makefiles whose api version is mismatched
17  * with make command.
18  */
19 define('MAKE_API', 2);
20
21 include_once 'make.utilities.inc';
22 include_once 'make.download.inc';
23 include_once 'make.project.inc';
24 include_once 'generate.contents.make.inc';
25
26 /**
27  * Implements hook_drush_help().
28  */
29 function make_drush_help($section) {
30   switch ($section) {
31     case 'meta:make:title':
32       return dt('Make commands');
33     case 'meta:make:summary':
34       return dt('Manage Drupal codebases using manifests of projects and libraries.');
35     case 'drush:make':
36       return dt('Turns a makefile into a Drupal codebase. For a full description of options and makefile syntax, see docs/make.txt and examples/example.make.');
37     case 'drush:make-generate':
38       return dt('Generate a makefile from the current Drupal site, specifying project version numbers unless not known or otherwise specified. Unversioned projects will be interpreted later by drush make as "most recent stable release"');
39   }
40 }
41
42 /**
43  * Implements hook_drush_command().
44  */
45 function make_drush_command() {
46   $projects = array(
47     'description' => 'Restrict the make to this comma-separated list of projects. To specify all projects, pass *.',
48     'example-value' => 'views,ctools',
49   );
50   $libraries = array(
51     'description' => 'Restrict the make to this comma-separated list of libraries. To specify all libraries, pass *.',
52     'example-value' => 'tinymce',
53   );
54
55   $items['make'] = array(
56     'bootstrap' => DRUSH_BOOTSTRAP_NONE,
57     'description' => 'Turns a makefile into a working Drupal codebase.',
58     'arguments' => array(
59       'makefile' => 'Filename of the makefile to use for this build.',
60       'build path' => 'The path at which to build the makefile.',
61     ),
62     'examples' => array(
63       'drush make example.make example' => 'Build the example.make makefile in the example directory.',
64       'drush make --no-core --contrib-destination=. installprofile.make' => 'Build an installation profile within an existing Drupal site',
65       'drush make http://example.com/example.make example' => 'Build the remote example.make makefile in the example directory.',
66       'drush make example.make --no-build --lock=example.lock' => 'Write a new makefile to example.lock. All project versions will be resolved.',
67     ),
68     'options' => array(
69       'version' => 'Print the make API version and exit.',
70       'concurrency' => array(
71         'description' => 'Set the number of concurrent projects that will be processed at the same time. The default is 1.',
72         'example-value' => '1',
73       ),
74       'contrib-destination' => 'Specify a path under which modules and themes should be placed. Defaults to sites/all for Drupal 6,7 and the corresponding directory in the Drupal root for Drupal 8 and above.',
75       'force-complete' => 'Force a complete build even if errors occur.',
76       'ignore-checksums' => 'Ignore md5 checksums for downloads.',
77       'md5' => array(
78         'description' => 'Output an md5 hash of the current build after completion. Use --md5=print to print to stdout.',
79         'example-value' => 'print',
80         'value' => 'optional',
81       ),
82       'make-update-default-url' => 'The default location to load the XML update information from.',
83       'no-build' => 'Do not build a codebase. Makes the `build path` argument optional.',
84       'no-cache' => 'Do not use the pm-download caching (defaults to cache enabled).',
85       'no-clean' => 'Leave temporary build directories in place instead of cleaning up after completion.',
86       'no-core' => 'Do not require a Drupal core project to be specified.',
87       'no-recursion' => 'Do not recurse into the makefiles of any downloaded projects; you can also set [do_recursion] = 0 on a per-project basis in the makefile.',
88       'no-patch-txt' => 'Do not write a PATCHES.txt file in the directory of each patched project.',
89       'no-gitinfofile' => 'Do not modify .info files when cloning from Git.',
90       'force-gitinfofile' => 'Force a modification of .info files when cloning from Git even if repository isn\'t hosted on Drupal.org.',
91       'no-gitprojectinfo' => 'Do not inject project info into .info files when cloning from Git.',
92       'overwrite' => 'Overwrite existing directories. Default is to merge.',
93       'prepare-install' => 'Prepare the built site for installation. Generate a properly permissioned settings.php and files directory.',
94       'tar' => 'Generate a tar archive of the build. The output filename will be [build path].tar.gz.',
95       'test' => 'Run a temporary test build and clean up.',
96       'translations' => 'Retrieve translations for the specified comma-separated list of language(s) if available for all projects.',
97       'working-copy' => 'Preserves VCS directories, like .git, for projects downloaded using such methods.',
98       'download-mechanism' => 'How to download files. Should be autodetected, but this is an override if it doesn\'t work. Options are "curl" and "make" (a native download method).',
99       'projects' => $projects,
100       'libraries' => $libraries,
101       'allow-override' => array(
102         'description' => 'Restrict the make options to a comma-separated list. Defaults to unrestricted.',
103       ),
104       'lock' => array(
105         'description' => 'Generate a makefile, based on the one passed in, with all versions *resolved*. Defaults to printing to the terminal, but an output file may be provided.',
106         'example-value' => 'example.make.lock',
107       ),
108       'shallow-clone' => array(
109         'description' => 'For makefile entries which use git for downloading, this option will utilize shallow clones where possible (ie. by using the git-clone\'s depth=1 option). If the "working-copy" option is not desired, this option will significantly speed up makes which involve modules stored in very large git repos. In fact, if "working-copy" option is enabled, this option cannot be used.',
110       ),
111       'bundle-lockfile' => array(
112         'description' => 'Generate a lockfile for this build and copy it into the codebase (at sites/all/drush/platform.lock). An alternate path (relative to the Drupal root) can also be specified',
113         'example-value' => 'sites/all/drush/example.make.lock',
114       ),
115       'format' => array(
116         'description' => 'The format for generated lockfiles. Options are "yaml" or "ini". Defaults to "yaml".',
117         'example-value' => 'ini',
118       ),
119       'core-quick-drupal' => array(
120         'description' => 'Return project info for use by core-quick-drupal.',
121         'hidden' => TRUE,
122       ),
123       'includes' => 'A list of makefiles to include at build-time.',
124       'overrides' => 'A list of makefiles to that can override values in other makefiles.',
125     ),
126     'engines' => array('release_info'),
127     'topics' => array('docs-make', 'docs-make-example'),
128   );
129
130   $items['make-generate'] = array(
131     'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL,
132     'description' => 'Generate a makefile from the current Drupal site.',
133     'examples' => array(
134       'drush generate-makefile example.make' => 'Generate a makefile with ALL projects versioned (should a project have a known version number)',
135       'drush generate-makefile example.make --exclude-versions' => 'Generate a makefile with NO projects versioned',
136       'drush generate-makefile example.make --exclude-versions=drupal,views,cck' => 'Generate a makefile with ALL projects versioned EXCEPT core, Views and CCK',
137       'drush generate-makefile example.make --include-versions=admin_menu,og,ctools (--exclude-versions)' => 'Generate a makefile with NO projects versioned EXCEPT Admin Menu, OG and CTools.',
138     ),
139     'options' => array(
140       'exclude-versions' => 'Exclude all version numbers (default is include all version numbers) or optionally specify a list of projects to exclude from versioning',
141       'include-versions' => 'Include a specific list of projects, while all other projects remain unversioned in the makefile (so implies --exclude-versions)',
142       'format' => array(
143         'description' => 'The format for generated makefile. Options are "yaml" or "ini". Defaults to "yaml".',
144         'example-value' => 'ini',
145       ),
146     ),
147     'engines' => array('release_info'),
148     'aliases' => array('generate-makefile'),
149   );
150
151   $items['make-convert'] = array(
152     'bootstrap' => DRUSH_BOOTSTRAP_NONE,
153     'description' => 'Convert a legacy makefile into another format. Defaults to converting .make => .make.yml.',
154     'arguments' => array(
155       'makefile' => 'Filename of the makefile to convert.',
156     ),
157     'options' => array(
158       'projects' => $projects,
159       'libraries' => $libraries,
160       'includes' => 'A list of makefiles to include at build-time.',
161       'format' => 'The format to which the make file should be converted. Accepted values include make, composer, and yml.',
162     ),
163     'required-arguments' => TRUE,
164     'examples' => array(
165       'drush make-convert example.make --format=composer  > composer.json' => 'Convert example.make to composer.json',
166       'drush make-convert example.make --format=yml > example.make.yml' => 'Convert example.make to example.make.yml',
167       'drush make-convert composer.lock --format=make > example.make' => 'Convert composer.lock example.make',
168     ),
169   );
170
171   // Hidden command to build a group of projects.
172   $items['make-process'] = array(
173     'hidden' => TRUE,
174     'arguments' => array(
175       'directory' => 'The temporary working directory to use',
176     ),
177     'options' => array(
178       'projects-location' => 'Name of a temporary file containing json-encoded output of make_projects().',
179       'manifest' => 'An array of projects already being processed.',
180     ),
181     'bootstrap' => DRUSH_BOOTSTRAP_NONE,
182     'engines' => array('release_info'),
183   );
184
185   $items['make-update'] = array(
186     'bootstrap' => DRUSH_BOOTSTRAP_NONE,
187     'description' => 'Process a makefile and outputs an equivalent makefile with projects version resolved to latest available.',
188     'arguments' => array(
189       'makefile' => 'Filename of the makefile to use for this build.',
190     ),
191     'options' => array(
192       'result-file' => array(
193         'description' => 'Save to a file. If not provided, the updated makefile will be dumped to stdout.',
194         'example-value' => 'updated.make',
195       ),
196       'format' => array(
197         'description' => 'The format for generated lockfiles. Options are "yaml" or "ini". Defaults to "yaml".',
198         'example-value' => 'ini',
199       ),
200       'includes' => 'A list of makefiles to include at build-time.',
201     ),
202     'engines' => array('release_info', 'update_status'),
203   );
204
205   $items['make-lock'] = array(
206     'bootstrap' => DRUSH_BOOTSTRAP_NONE,
207     'description' => 'Process a makefile and outputs an equivalent makefile with projects version *resolved*. Respects pinned versions.',
208     'arguments' => array(
209       'makefile' => 'Filename of the makefile to use for this build.',
210     ),
211     'options' => array(
212       'result-file' => array(
213         'description' => 'Save to a file. If not provided, the lockfile will be dumped to stdout.',
214         'example-value' => 'platform.lock',
215       ),
216       'format' => array(
217         'description' => 'The format for generated lockfiles. Options are "yaml" or "ini". Defaults to "yaml".',
218         'example-value' => 'ini',
219       ),
220       'includes' => 'A list of makefiles to include at build-time.',
221     ),
222     'allow-additional-options' => TRUE,
223     'engines' => array('release_info', 'update_status'),
224   );
225
226   // Add docs topic.
227   $docs_dir = drush_get_context('DOC_PREFIX', DRUSH_BASE_PATH);
228   $items['docs-make'] = array(
229     'description' => 'Drush Make overview with examples',
230     'hidden' => TRUE,
231     'topic' => TRUE,
232     'bootstrap' => DRUSH_BOOTSTRAP_NONE,
233     'callback' => 'drush_print_file',
234     'callback arguments' => array($docs_dir . '/docs/make.md'),
235   );
236   $items['docs-make-example'] = array(
237     'description' => 'Drush Make example makefile',
238     'hidden' => TRUE,
239     'topic' => TRUE,
240     'bootstrap' => DRUSH_BOOTSTRAP_NONE,
241     'callback' => 'drush_print_file',
242     'callback arguments' => array($docs_dir . '/examples/example.make.yml'),
243   );
244   return $items;
245 }
246
247 /**
248  * Command argument complete callback.
249  *
250  * @return array
251  *   Strong glob of files to complete on.
252  */
253 function make_make_complete() {
254   return array(
255     'files' => array(
256       'directories' => array(
257         'pattern' => '*',
258         'flags' => GLOB_ONLYDIR,
259       ),
260       'make' => array(
261         'pattern' => '*.make',
262       ),
263     ),
264   );
265 }
266
267 /**
268  * Validation callback for make command.
269  */
270 function drush_make_validate($makefile = NULL, $build_path = NULL) {
271   // Don't validate if --version option is supplied.
272   if (drush_get_option('version', FALSE)) {
273     return;
274   }
275
276   if (drush_get_option('shallow-clone', FALSE) && drush_get_option('working-copy', FALSE)) {
277     return drush_set_error('MAKE_SHALLOW_CLONE_WORKING_COPY_CONFLICT', dt('You cannot use "--shallow-clone" and "--working-copy" options together.'));
278   }
279
280   // Error out if the build path is not valid and --no-build was not supplied.
281   if (!drush_get_option('no-build', FALSE) && !make_build_path($build_path)) {
282     return FALSE;
283   }
284 }
285
286 /**
287  * Implements drush_hook_pre_COMMAND().
288  *
289  * If --version option is supplied, print it and prevent execution of the command.
290  */
291 function drush_make_pre_make($makefile = NULL, $build_path = NULL) {
292   if (drush_get_option('version', FALSE)) {
293     drush_print(dt('Drush make API version !version', array('!version' => MAKE_API)));
294     drush_print_pipe(MAKE_API);
295     // Prevent command execution.
296     return FALSE;
297   }
298 }
299
300 /**
301  * Drush callback; make based on the makefile.
302  */
303 function drush_make($makefile = NULL, $build_path = NULL) {
304   // Set the cache option based on our '--no-cache' option.
305   _make_enable_cache();
306
307   // Build.
308   if (!drush_get_option('no-build', FALSE)) {
309     $info = make_parse_info_file($makefile);
310     drush_log(dt('Beginning to build !makefile.', array('!makefile' => $makefile)), LogLevel::OK);
311
312     // Default contrib destination depends on Drupal core version.
313     $core_version = str_replace('.x', '', $info['core'][0]);
314     $sitewide = drush_drupal_sitewide_directory($core_version);
315     $contrib_destination = drush_get_option('contrib-destination', $sitewide);
316
317     $build_path = make_build_path($build_path);
318     $make_dir = realpath(dirname($makefile));
319
320     $success = make_projects(FALSE, $contrib_destination, $info, $build_path, $make_dir);
321     if ($success) {
322       make_libraries(FALSE, $contrib_destination, $info, $build_path, $make_dir);
323
324       if (drush_get_option('prepare-install')) {
325         make_prepare_install($build_path);
326       }
327       if ($option = drush_get_option('md5')) {
328         $md5 = make_md5();
329         if ($option === 'print') {
330           drush_print($md5);
331         }
332         else {
333           drush_log(dt('Build hash: %md5', array('%md5' => $md5)), LogLevel::OK);
334         }
335       }
336       // Only take final build steps if not in testing mode.
337       if (!drush_get_option('test')) {
338         if (drush_get_option('tar')) {
339           make_tar($build_path);
340         }
341         else {
342           make_move_build($build_path);
343         }
344       }
345       make_clean_tmp();
346     }
347     else {
348       return make_error('MAKE_PROJECTS_FAILED', dt('Drush Make failed to download all projects. See the log above for the specific errors.'));
349     }
350   }
351
352   // Process --lock and --bundle-lockfile
353   $lockfiles = array();
354   if ($result_file = drush_get_option('bundle-lockfile', FALSE)) {
355     if ($result_file === TRUE) {
356       $result_file = 'sites/all/drush/platform.make';
357     }
358     $lockfiles[] = $build_path . '/' . $result_file;
359   }
360   if ($result_file = drush_get_option('lock', FALSE)) {
361     $lockfiles[] = $result_file;
362   }
363   if (count($lockfiles)) {
364     foreach ($lockfiles as $lockfile) {
365       if ($lockfile !== TRUE) {
366         $result_file = drush_normalize_path($lockfile);
367         drush_mkdir(dirname($result_file), $required = TRUE);
368         drush_set_option('result-file', $result_file);
369       }
370       drush_invoke('make-lock', $makefile);
371       drush_unset_option('result-file');
372     }
373   }
374
375   // Used by core-quick-drupal command.
376   // @see drush_core_quick_drupal().
377   if (drush_get_option('core-quick-drupal', FALSE)) {
378     return $info;
379   }
380 }
381
382 /**
383  * Command callback; convert ini makefile to YAML.
384  */
385 function drush_make_convert($source) {
386   $dest_format = drush_get_option('format', 'yml');
387
388   // Load source data.
389   $source_format = pathinfo($source, PATHINFO_EXTENSION);
390
391   if ($source_format == $dest_format || $source_format == 'lock' && $dest_format == 'composer') {
392     drush_print('The source format cannot be the same as the destination format.');
393   }
394
395   // Obtain drush make $info array, converting if necessary.
396   switch ($source_format) {
397     case 'make':
398     case 'yml':
399     case 'yaml':
400       $info = make_parse_info_file($source);
401       break;
402     case 'lock':
403       $composer_json_file = str_replace('lock', 'json', $source);
404       if (!file_exists($composer_json_file)) {
405         drush_print('Please ensure that a composer.json file is in the same directory as the specified composer.lock file.');
406         return FALSE;
407       }
408       $composer_json = json_decode(make_get_data($composer_json_file), TRUE);
409       $composer_lock = json_decode(make_get_data($source), TRUE);
410       $info = drush_make_convert_composer_to_make($composer_lock, $composer_json);
411       break;
412     case 'json':
413       drush_print('Please use composer.lock instead of composer.json as source for conversion.');
414       return FALSE;
415       break;
416   }
417
418   // Output into destination formation.
419   switch ($dest_format) {
420     case 'yml':
421     case 'yaml':
422       $output = drush_make_convert_make_to_yml($info);
423       break;
424
425     case 'make':
426       foreach ($info['projects'] as $key => $project) {
427         $info['projects'][$key]['_type'] = $info['projects'][$key]['type'];
428       }
429       foreach ($info['libraries'] as $key => $library) {
430         $info['libraries'][$key]['_type'] = 'librarie';
431       }
432       $output = _drush_make_generate_makefile_contents($info['projects'], $info['libraries'], $info['core'], $info['defaults']);
433
434       break;
435
436     case 'composer':
437       $output = drush_make_convert_make_to_composer($info);
438       break;
439   }
440
441   drush_print($output);
442 }
443
444 /**
445  * Converts a composer.lock array into a traditional drush make array.
446  *
447  * @param array $composer_lock
448  *   An array of composer.lock data.
449  *
450  * @param array $composer_json
451  *   An array of composer.json data.
452  *
453  * @return array A traditional drush make info array.
454  * A traditional drush make info array.
455  */
456 function drush_make_convert_composer_to_make($composer_lock, $composer_json) {
457   $info = array(
458     'core' => array(),
459     'api' => 2,
460     'defaults' => array(
461       'projects' => array(
462         'subdir' => 'contrib',
463       ),
464     ),
465     'projects' => array(),
466     'libraries' => array(),
467   );
468
469   // The make generation function requires that projects be grouped by type,
470   // or else duplicative project groups will be created.
471   $core = array();
472   $modules = array();
473   $themes = array();
474   $profiles = array();
475   $libraries = array();
476   foreach ($composer_lock['packages'] as $key => $package) {
477     if (strpos($package['name'], 'drupal/') === 0 && in_array($package['type'], array('drupal-core', 'drupal-theme', 'drupal-module', 'drupal-profile'))) {
478       $project_name = str_replace('drupal/', '', $package['name']);
479
480       switch ($package['type']) {
481         case 'drupal-core':
482           $project_name = 'drupal';
483           $group =& $core;
484           $group[$project_name]['type'] = 'core';
485           $info['core'] = substr($package['version'], 0, 1) . '.x';
486           break;
487         case 'drupal-theme':
488           $group =& $themes;
489           $group[$project_name]['type'] = 'theme';
490           break;
491         case 'drupal-module':
492           $group =& $modules;
493           $group[$project_name]['type'] = 'module';
494           break;
495         case 'drupal-profile':
496           $group =& $profiles;
497           $group[$project_name]['type'] = 'profile';
498           break;
499       }
500
501       $group[$project_name]['download']['type'] = 'git';
502       $group[$project_name]['download']['url'] = $package['source']['url'];
503       // Dev versions should use git branch + revision, otherwise a tag is used.
504       if (strstr($package['version'], 'dev')) {
505         // 'dev-' prefix indicates a branch-alias. Stripping the dev prefix from
506         // the branch name is sufficient.
507         // @see https://getcomposer.org/doc/articles/aliases.md
508         if (strpos($package['version'], 'dev-') === 0) {
509           $group[$project_name]['download']['branch'] = substr($package['version'], 4);
510         }
511         // Otherwise, leave as is. Version may already use '-dev' suffix.
512         else {
513           $group[$project_name]['download']['branch'] = $package['version'];
514         }
515         $group[$project_name]['download']['revision'] = $package['source']['reference'];
516       }
517       elseif ($package['type'] == 'drupal-core') {
518         // For 7.x tags, replace 7.xx.0 with 7.xx.
519         if ($info['core'] == '7.x') {
520           $group[$project_name]['download']['tag']= substr($package['version'], 0, 4);
521         }
522         else {
523           $group[$project_name]['download']['tag'] = $package['version'];
524         }
525       }
526       else {
527         // Make tag versioning drupal-friendly. 8.1.0-alpha1 => 8.x-1.0-alpha1.
528         $major_version = substr($package['version'], 0 ,1);
529         $the_rest = substr($package['version'], 2, strlen($package['version']));
530         $group[$project_name]['download']['tag'] = "$major_version.x-$the_rest";
531       }
532
533       if (!empty($package['extra']['patches_applied'])) {
534         foreach ($package['extra']['patches_applied'] as $desc => $url) {
535           $group[$project_name]['patch'][] = $url;
536         }
537       }
538     }
539     // Include any non-drupal libraries that exist in both .lock and .json.
540     elseif (!in_array($package['type'], array('composer-plugin', 'metapackage'))
541       && array_key_exists($package['name'], $composer_json['require'])) {
542       $project_name = $package['name'];
543       $libraries[$project_name]['type'] = 'library';
544       $libraries[$project_name]['download']['type'] = 'git';
545       $libraries[$project_name]['download']['url'] = $package['source']['url'];
546       $libraries[$project_name]['download']['branch'] = $package['version'];
547       $libraries[$project_name]['download']['revision'] = $package['source']['reference'];
548     }
549   }
550
551   $info['projects'] = $core + $modules + $themes;
552   $info['libraries'] = $libraries;
553
554   return $info;
555 }
556
557 /**
558  * Converts a drush info array to a composer.json array.
559  *
560  * @param array $info
561  *   A drush make info array.
562  *
563  * @return string
564  *   A json encoded composer.json schema object.
565  */
566 function drush_make_convert_make_to_composer($info) {
567   $core_major_version = substr($info['core'], 0, 1);
568   $core_project_name = $core_major_version == 7 ? 'drupal/drupal' : 'drupal/core';
569
570   // Add default projects.
571   $projects = array(
572     'composer/installers' => '^1.0.20',
573     'cweagans/composer-patches' => '~1.0',
574     $core_project_name => str_replace('x', '*', $info['core']),
575   );
576
577   $patches = array();
578
579   // Iterate over projects, populating composer-friendly array.
580   foreach ($info['projects'] as $project_name => $project) {
581     switch ($project['type']) {
582       case 'core':
583         $project['name'] = 'drupal/core';
584         $projects[$project['name']] = str_replace('x', '*', $project['version']);
585         break;
586
587       default:
588         $project['name'] = "drupal/$project_name";
589         $projects[$project['name']] = drush_make_convert_project_to_composer($project, $core_major_version);
590         break;
591     }
592
593     // Add project patches.
594     if (!empty($project['patch'])) {
595       foreach($project['patch'] as $key => $patch) {
596         $patch_description = "Enter {$project['name']} patch #$key description here";
597         $patches[$project['name']][$patch_description] = $patch;
598       }
599     }
600   }
601
602   // Iterate over libraries, populating composer-friendly array.
603   if (!empty($info['libraries'])) {
604     foreach ($info['libraries'] as $library_name => $library) {
605       $library_name = 'Verify project name: ' . $library_name;
606       $projects[$library_name] = drush_make_convert_project_to_composer($library, $core_major_version);
607     }
608   }
609
610   $output = array(
611     'name' => 'Enter project name here',
612     'description' => 'Enter project description here',
613     'type' => 'project',
614     'repositories' => array(
615       array('type' => 'composer', 'url' => 'https://packagist.drupal-composer.org'),
616     ),
617     'require' => $projects,
618     'minimum-stability' => 'dev',
619     'prefer-stable' => TRUE,
620     'extra' => array(
621       'installer-paths' => array(
622         'core' => array('type:drupal-core'),
623         'docroot/modules/contrib/{$name}' => array('type:drupal-module'),
624         'docroot/profiles/contrib/{$name}' => array('type:drupal-profile'),
625         'docroot/themes/contrib/{$name}' => array('type:drupal-theme'),
626         'drush/contrib/{$name}' => array('type:drupal-drush'),
627       ),
628       'patches' => $patches,
629     ),
630   );
631
632   $output = json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
633
634   return $output;
635 }
636
637 /**
638  * Converts a make file project array into a composer project version string.
639  *
640  * @param array $original_project
641  *   A project dependency, as defined in a make file.
642  *
643  * @param string $core_major_version
644  *   The major core version. E.g., 6, 7, 8, etc.
645  *
646  * @return string
647  *   The project version, in composer syntax.
648  *
649  */
650 function drush_make_convert_project_to_composer($original_project, $core_major_version) {
651   // Typical specified version with major version "x" removed.
652   if (!empty($original_project['version'])) {
653     $version = str_replace('x', '0', $original_project['version']);
654   }
655   // Git branch or revision.
656   elseif (!empty($original_project['download'])) {
657     switch ($original_project['download']['type']) {
658       case 'git':
659         if (!empty($original_project['download']['branch'])) {
660           // @todo Determine if '0' will always be correct.
661           $version = str_replace('x', '0', $original_project['download']['branch']);
662         }
663         if (!empty($original_project['download']['tag'])) {
664           // @todo Determine if '0' will always be correct.
665           $version = str_replace('x', '0', $original_project['download']['tag']);
666         }
667         if (!empty($project['download']['revision'])) {
668           $version .= '#' . $original_project['download']['revision'];
669         }
670         break;
671
672       default:
673         $version = 'Enter correct project name and version number';
674         break;
675     }
676   }
677
678   $version = "$core_major_version." . $version;
679
680   return $version;
681 }
682
683 /**
684  * Converts a drush info array to a YAML array.
685  *
686  * @param array $info
687  *   A drush make info array.
688  *
689  * @return string
690  *   A yaml encoded info array.
691  */
692 function drush_make_convert_make_to_yml($info) {
693   // Remove incorrect value.
694   unset($info['format']);
695
696   // Replace "*" with "~" for project versions.
697   foreach ($info['projects'] as $key => $project) {
698     if ($project['version'] == '*') {
699       $info['projects'][$key]['version'] = '~';
700     }
701   }
702
703   $dumper = drush_load_engine('outputformat', 'yaml');
704   $output = $dumper->format($info, array());
705
706   return $output;
707 }
708
709 /**
710  * Drush callback: hidden file to process an individual project.
711  *
712  * @param string $directory
713  *   Directory where the project is being built.
714  */
715 function drush_make_process($directory) {
716   drush_get_engine('release_info');
717
718   // Set the temporary directory.
719   make_tmp(TRUE, $directory);
720   if (!$projects_location = drush_get_option('projects-location')) {
721     return drush_set_error('MAKE-PROCESS', dt('No projects passed to drush_make_process'));
722   }
723   $projects = json_decode(file_get_contents($projects_location), TRUE);
724   $manifest = drush_get_option('manifest', array());
725
726   foreach ($projects as $project) {
727     if ($instance = DrushMakeProject::getInstance($project['type'], $project)) {
728       $instance->setManifest($manifest);
729       $instance->make();
730     }
731     else {
732       make_error('PROJECT-TYPE', dt('Non-existent project type %type on project %project.', array('%type' => $project['type'], '%project' => $project['name'])));
733     }
734   }
735 }
736
737 /**
738  * Gather additional data on all projects specified in the make file.
739  */
740 function make_prepare_projects($recursion, $info, $contrib_destination = '', $build_path = '', $make_dir = '') {
741   $release_info = drush_get_engine('release_info');
742
743   // Nothing to make if the project list is empty. Maybe complain about it.
744   if (empty($info['projects'])) {
745     if (drush_get_option('no-core') || $recursion) {
746       return TRUE;
747     }
748     else {
749       return drush_set_error('MAKE_NO_CORE', dt('No core project specified.'));
750     }
751   }
752
753   // Obtain translations to download along with the projects.
754   $translations = array();
755   if (isset($info['translations'])) {
756     $translations = $info['translations'];
757   }
758   if ($arg_translations = drush_get_option('translations', FALSE)) {
759     $translations = array_merge(explode(',', $arg_translations), $translations);
760   }
761
762   // Normalize projects.
763   $projects = array();
764   $ignore_checksums = drush_get_option('ignore-checksums');
765   foreach ($info['projects'] as $key => $project) {
766     // Merge the known data onto the project info.
767     $project += array(
768       'name'                => $key,
769       'type'                => 'module',
770       'core'                => $info['core'],
771       'translations'        => $translations,
772       'build_path'          => $build_path,
773       'contrib_destination' => $contrib_destination,
774       'version'             => '',
775       'location'            => drush_get_option('make-update-default-url', ReleaseInfo::DEFAULT_URL),
776       'subdir'              => '',
777       'directory_name'      => '',
778       'make_directory'      => $make_dir,
779       'options'             => array(),
780     );
781     // MD5 Checksum.
782     if ($ignore_checksums) {
783       unset($project['download']['md5']);
784     }
785     elseif (!empty($project['md5'])) {
786       $project['download']['md5'] = $project['md5'];
787     }
788
789     // If download components are specified, but not the download
790     // type, default to git.
791     if (isset($project['download']) && !isset($project['download']['type'])) {
792       $project['download']['type'] = 'git';
793     }
794     // Localization server.
795     if (!isset($project['l10n_url']) && ($project['location'] == ReleaseInfo::DEFAULT_URL)) {
796       $project['l10n_url'] = MAKE_DEFAULT_L10N_SERVER;
797     }
798     // Classify projects in core or contrib.
799     if ($project['type'] == 'core') {
800       $project['download_type'] = 'core';
801     }
802     elseif ($project['location'] != ReleaseInfo::DEFAULT_URL || !isset($project['download'])) {
803       $request = make_prepare_request($project);
804       $is_core = $release_info->checkProject($request, 'core');
805       $project['download_type'] = ($is_core ? 'core' : 'contrib');
806       $project['type'] = $is_core ? 'core' : $project['type'];
807     }
808     else {
809       $project['download_type'] = ($project['name'] == 'drupal' ? 'core' : 'contrib');
810     }
811     $projects[$project['download_type']][$project['name']] = $project;
812   }
813
814   // Verify there're enough cores, but not too many.
815   $cores = !empty($projects['core']) ? count($projects['core']) : 0;
816   if (drush_get_option('no-core')) {
817     unset($projects['core']);
818   }
819   elseif ($cores == 0 && !$recursion) {
820     return drush_set_error('MAKE_NO_CORE', dt('No core project specified.'));
821   }
822   elseif ($cores == 1 && $recursion) {
823     unset($projects['core']);
824   }
825   elseif ($cores > 1) {
826     return drush_set_error('MAKE_MULTIPLE_CORES', dt('More than one core project specified.'));
827   }
828
829   // Set download type = pm for suitable projects.
830   foreach (array_keys($projects) as $project_type) {
831     foreach ($projects[$project_type] as $project) {
832       if (make_project_needs_release_info($project)) {
833         $request = make_prepare_request($project, $project_type);
834         $release = $release_info->selectReleaseBasedOnStrategy($request, '', 'ignore');
835         if ($release === FALSE) {
836           return FALSE;
837         }
838         // Override default project type with data from update service.
839         if (!isset($info['projects'][$project['name']]['type'])) {
840           $project['type'] = $release_info->get($request)->getType();
841         }
842
843         if (!isset($project['download'])) {
844           $project['download'] = array(
845             'type' => 'pm',
846             'full_version' => $release['version'],
847             'download_link' => $release['download_link'],
848             'status url' => $request['status url'],
849           );
850         }
851       }
852       $projects[$project_type][$project['name']] = $project;
853     }
854   }
855   if (!$recursion) {
856     $projects += array(
857       'core' => array(),
858       'contrib' => array(),
859     );
860     drush_set_option('DRUSH_MAKE_PROJECTS', array_merge($projects['core'], $projects['contrib']));
861   }
862   return $projects;
863 }
864
865 /**
866  * Process all projects specified in the make file.
867  */
868 function make_projects($recursion, $contrib_destination, $info, $build_path, $make_dir) {
869   $projects = make_prepare_projects($recursion, $info, $contrib_destination, $build_path, $make_dir);
870   // Abort if there was an error processing projects.
871   if ($projects === FALSE) {
872     return FALSE;
873   }
874
875   // Core is built in place, rather than using make-process.
876   if (!empty($projects['core']) && count($projects['core'])) {
877     $project = current($projects['core']);
878     $project = DrushMakeProject::getInstance('core', $project);
879     $project->make();
880   }
881
882   // Process all projects concurrently using make-process.
883   if (isset($projects['contrib'])) {
884     $concurrency = drush_get_option('concurrency', 1);
885     // Generate $concurrency sub-processes to do the actual work.
886     $invocations = array();
887     $thread = 0;
888     foreach ($projects['contrib'] as $project) {
889       $thread = ++$thread % $concurrency;
890       // Ensure that we've set this sub-process up.
891       if (!isset($invocations[$thread])) {
892         $invocations[$thread] = array(
893           'args' => array(
894             make_tmp(),
895           ),
896           'options' => array(
897             'projects' => array(),
898           ),
899           'site' => array(),
900         );
901       }
902       // Add the project to this sub-process.
903       $invocations[$thread]['options']['projects'][] = $project;
904       // Add the manifest so recursive downloads do not override projects.
905       $invocations[$thread]['options']['manifest'] = array_keys($projects['contrib']);
906     }
907     if (!empty($invocations)) {
908       // Backend options.
909       $backend_options = array(
910         'concurrency' => $concurrency,
911         'method' => 'POST',
912       );
913
914       // Store projects in temporary files since passing this much data on the
915       // pipe buffer can break on certain systems.
916       _make_write_project_json($invocations);
917
918       $common_options = drush_redispatch_get_options();
919       // Merge in stdin options since we process makefiles recursively. See http://drupal.org/node/1510180.
920       $common_options = array_merge($common_options, drush_get_context('stdin'));
921       // Package handler should use 'wget'.
922       $common_options['package-handler'] = 'wget';
923
924       // Avoid any prompts from CLI.
925       $common_options['yes'] = TRUE;
926
927       // Use cache unless explicitly turned off.
928       if (!drush_get_option('no-cache', FALSE)) {
929         $common_options['cache'] = TRUE;
930       }
931       // Unless --verbose or --debug are passed, quiter backend output.
932       if (empty($common_options['verbose']) && empty($common_options['debug'])) {
933         $backend_options['#output-label'] = FALSE;
934         $backend_options['integrate'] = TRUE;
935       }
936       $results = drush_backend_invoke_concurrent($invocations, $common_options, $backend_options, 'make-process', '@none');
937       if (count($results['error_log'])) {
938         return FALSE;
939       }
940     }
941   }
942   return TRUE;
943 }
944
945 /**
946  * Writes out project data to temporary files.
947  *
948  * @param array &$invocations
949  *   An array containing projects sorted by thread.
950  */
951 function _make_write_project_json(array &$invocations) {
952   foreach ($invocations as $thread => $info) {
953     $projects = $info['options']['projects'];
954     unset($invocations[$thread]['options']['projects']);
955     $temp_file = drush_tempnam('make_projects');
956     file_put_contents($temp_file, json_encode($projects));
957     $invocations[$thread]['options']['projects-location'] = $temp_file;
958   }
959 }
960
961 /**
962  * Gather additional data on all libraries specified in the make file.
963  */
964 function make_prepare_libraries($recursion, $info, $contrib_destination = '', $build_path = '', $make_dir = '') {
965   // Nothing to make if the libraries list is empty.
966   if (empty($info['libraries'])) {
967     return;
968   }
969
970   $libraries = array();
971   $ignore_checksums = drush_get_option('ignore-checksums');
972   foreach ($info['libraries'] as $key => $library) {
973     if (!is_string($key) || !is_array($library)) {
974       // TODO Print a prettier message.
975       continue;
976     }
977     // Merge the known data onto the library info.
978     $library += array(
979       'name'                => $key,
980       'core'                => $info['core'],
981       'build_path'          => $build_path,
982       'contrib_destination' => $contrib_destination,
983       'subdir'              => '',
984       'directory_name'      => $key,
985       'make_directory'      => $make_dir,
986     );
987     if ($ignore_checksums) {
988       unset($library['download']['md5']);
989     }
990     $libraries[$key] = $library;
991   }
992   if (!$recursion) {
993     drush_set_option('DRUSH_MAKE_LIBRARIES', $info['libraries']);
994   }
995   return $libraries;
996 }
997
998 /**
999  * Process all libraries specified in the make file.
1000  */
1001 function make_libraries($recursion, $contrib_destination, $info, $build_path, $make_dir) {
1002   $libraries = make_prepare_libraries($recursion, $info, $contrib_destination, $build_path, $make_dir);
1003   if (empty($libraries)) {
1004     return;
1005   }
1006   foreach ($libraries as $key => $library) {
1007     $class = DrushMakeProject::getInstance('library', $library);
1008     $class->make();
1009   }
1010 }
1011
1012 /**
1013  * The path where the final build will be placed.
1014  */
1015 function make_build_path($build_path) {
1016   static $saved_path;
1017   if (isset($saved_path)) {
1018     return $saved_path;
1019   }
1020
1021   // Determine the base of the build.
1022   if (drush_get_option('tar')) {
1023     $build_path = dirname($build_path) . '/' . basename($build_path, '.tar.gz') . '.tar.gz';
1024   }
1025   elseif (isset($build_path) && (!empty($build_path) || $build_path == '.')) {
1026     $build_path = rtrim($build_path, '/');
1027   }
1028   // Allow tests to run without a specified base path.
1029   elseif (drush_get_option('test') || drush_confirm(dt("Make new site in the current directory?"))) {
1030     $build_path = '.';
1031   }
1032   else {
1033     return drush_user_abort(dt('Build aborted.'));
1034   }
1035   if ($build_path != '.' && file_exists($build_path) && !drush_get_option('no-core', FALSE)) {
1036     return drush_set_error('MAKE_PATH_EXISTS', dt('Base path %path already exists.', array('%path' => $build_path)));
1037   }
1038   $saved_path = $build_path;
1039   return $build_path;
1040 }
1041
1042 /**
1043  * Move the completed build into place.
1044  */
1045 function make_move_build($build_path) {
1046   $tmp_path = make_tmp();
1047   $ret = TRUE;
1048   if ($build_path == '.' || (drush_get_option('no-core', FALSE) && file_exists($build_path))) {
1049     $info = drush_scan_directory($tmp_path . DIRECTORY_SEPARATOR . '__build__', '/./', array('.', '..'), 0, FALSE, 'filename', 0, TRUE);
1050     foreach ($info as $file) {
1051       $destination = $build_path . DIRECTORY_SEPARATOR . $file->basename;
1052       if (file_exists($destination)) {
1053         // To prevent the removal of top-level directories such as 'modules' or
1054         // 'themes', descend in a level if the file exists.
1055         // TODO: This only protects one level of directories from being removed.
1056         $overwrite = drush_get_option('overwrite', FALSE) ? FILE_EXISTS_OVERWRITE : FILE_EXISTS_MERGE;
1057         if (is_dir($destination)) {
1058           $files = drush_scan_directory($file->filename, '/./', array('.', '..'), 0, FALSE);
1059           foreach ($files as $file) {
1060             $ret = $ret && drush_copy_dir($file->filename, $destination . DIRECTORY_SEPARATOR . $file->basename, $overwrite);
1061           }
1062         }
1063         else {
1064           $ret = $ret && drush_copy_dir($file->filename, $destination, $overwrite);
1065         }
1066       }
1067       else {
1068         $ret = $ret && drush_copy_dir($file->filename, $destination);
1069       }
1070     }
1071   }
1072   else {
1073     drush_mkdir(dirname($build_path));
1074     $ret = drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . '__build__', $tmp_path . DIRECTORY_SEPARATOR . basename($build_path), TRUE);
1075     $ret = $ret && drush_copy_dir($tmp_path . DIRECTORY_SEPARATOR . basename($build_path), $build_path);
1076   }
1077
1078   // Copying to final destination resets write permissions. Re-apply.
1079   if (drush_get_option('prepare-install')) {
1080     $default = $build_path . '/sites/default';
1081     chmod($default . '/settings.php', 0666);
1082     chmod($default . '/files', 0777);
1083   }
1084
1085   if (!$ret) {
1086     return drush_set_error('MAKE_CANNOT_MOVE_BUILD', dt("Cannot move build into place."));
1087   }
1088   return $ret;
1089 }
1090
1091 /**
1092  * Create a request array suitable for release_info engine.
1093  *
1094  * This is a convenience function to easily integrate drush_make
1095  * with drush release_info engine.
1096  *
1097  * @todo: refactor 'make' to internally work with release_info keys.
1098  *
1099  * @param array $project
1100  *   Project array.
1101  * @param string $type
1102  *   'contrib' or 'core'.
1103  */
1104 function make_prepare_request($project, $type = 'contrib') {
1105   $request = array(
1106     'name' => $project['name'],
1107     'drupal_version' => $project['core'],
1108     'status url' => $project['location'],
1109   );
1110   if ($project['version'] != '') {
1111     $request['project_version'] = $project['version'];
1112     $request['version'] = $type == 'core' ? $project['version'] : $project['core'] . '-' . $project['version'];
1113   }
1114   return $request;
1115 }
1116
1117 /**
1118  * Determine if the release information is required for this
1119  * project. When it is determined that it is, this potentially results
1120  * in the use of pm-download to process the project.
1121  *
1122  * If the location of the project is not customized (uses d.o), and
1123  * one of the following is true, then release information is required:
1124  *
1125  * - $project['type'] has not been specified
1126  * - $project['download'] has not been specified
1127  *
1128  * @see make_projects()
1129  */
1130 function make_project_needs_release_info($project) {
1131   return isset($project['location'])
1132     // Only fetch release info if the project type is unknown OR if
1133     // download attributes are unspecified.
1134     && (!isset($project['type']) || !isset($project['download']));
1135 }
1136
1137 /**
1138  * Enables caching if not explicitly disabled.
1139  *
1140  * @return bool
1141  *   The previous value of the 'cache' option.
1142  */
1143 function _make_enable_cache() {
1144   $cache_before = drush_get_option('cache');
1145   if (!drush_get_option('no-cache', FALSE)) {
1146     drush_set_option('cache', TRUE);
1147   }
1148   return $cache_before;
1149 }