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