make.project.inc

  1. 8.0.x commands/make/make.project.inc
  2. 6.x commands/make/make.project.inc
  3. 7.x commands/make/make.project.inc
  4. 5.x commands/make/make.project.inc
  5. master commands/make/make.project.inc

Drush Make processing classes.

Classes

Namesort descending Description
DrushMakeProject The base project class.
DrushMakeProject_Core For processing Drupal core projects.
DrushMakeProject_Library For processing libraries.
DrushMakeProject_Module For processing modules.
DrushMakeProject_Profile For processing installation profiles.
DrushMakeProject_Theme For processing themes.
DrushMakeProject_Translation For processing translations.

File

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