pre_composer_update_2.3.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. #!/usr/bin/php
  2. <?php
  3. /**
  4. * Copyright © Magento, Inc. All rights reserved.
  5. * See COPYING.txt for license details.
  6. */
  7. declare(strict_types=1);
  8. $_scriptName = basename(__FILE__);
  9. define(
  10. 'SYNOPSIS',
  11. <<<SYNOPSIS
  12. Updates Magento with 2.3 requirements that can't be done by `composer update` or `bin/magento setup:upgrade`.
  13. Run this script after upgrading to PHP 7.1/7.2 and before running `composer update` or `bin/magento setup:upgrade`.
  14. Steps included:
  15. - Require new version of the metapackage
  16. - Update "require-dev" section
  17. - Add "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" to composer.json "autoload":"psr-4" section
  18. - Update Magento/Updater if it's installed
  19. - Update name, version, and description fields in the root composer.json
  20. Usage: php -f $_scriptName -- --root='</path/to/magento/root/>' [--composer='</path/to/composer/executable>']
  21. [--edition='<community|enterprise>'] [--repo='<composer_repo_url>'] [--version='<version_constraint>']
  22. [--help]
  23. Required:
  24. --root='</path/to/magento/root/>'
  25. Path to the Magento installation root directory
  26. Optional:
  27. --composer='</path/to/composer/executable>'
  28. Path to the composer executable
  29. - Default: The composer found in the system PATH
  30. --edition='<community|enterprise>'
  31. Target Magento edition for the update. Open Source = 'community', Commerce = 'enterprise'
  32. - Default: The edition currently required in composer.json
  33. --repo='<composer_repo_url>'
  34. The Magento repository url to use to pull the new packages
  35. - Default: The Magento repository configured in composer.json
  36. --version='<version_constraint>'
  37. A composer version constraint for allowable 2.3 packages. Versions other than 2.3 are not handled by this script
  38. See https://getcomposer.org/doc/articles/versions.md#writing-version-constraints for more information.
  39. - Default: The latest 2.3 version available in the Magento repository
  40. --help
  41. Display this message
  42. SYNOPSIS
  43. );
  44. $opts = getopt('', [
  45. 'root:',
  46. 'composer:',
  47. 'edition:',
  48. 'repo:',
  49. 'version:',
  50. 'help'
  51. ]);
  52. // Log levels available for use with output() function
  53. define('INFO', 0);
  54. define('WARN', 1);
  55. define('ERROR', 2);
  56. if (isset($opts['help'])) {
  57. output(SYNOPSIS);
  58. exit(0);
  59. }
  60. try {
  61. if (version_compare(PHP_VERSION, '7.1', '<') || version_compare(PHP_VERSION, '7.3', '>=')) {
  62. preg_match('/^\d+\.\d+\.\d+/',PHP_VERSION, $matches);
  63. $phpVersion = $matches[0];
  64. throw new Exception("Invalid PHP version '$phpVersion'. Magento 2.3 requires PHP 7.1 or 7.2");
  65. }
  66. /**** Populate and Validate Settings ****/
  67. if (empty($opts['root']) || !is_dir($opts['root'])) {
  68. throw new BadMethodCallException('Existing Magento root directory must be supplied with --root');
  69. }
  70. $rootDir = $opts['root'];
  71. $composerFile = "$rootDir/composer.json";
  72. if (!file_exists($composerFile)) {
  73. throw new InvalidArgumentException("Supplied Magento root directory '$rootDir' does not contain composer.json");
  74. }
  75. $composerData = json_decode(file_get_contents($composerFile), true);
  76. $metapackageMatcher = '/^magento\/product\-(?<edition>community|enterprise)\-edition$/';
  77. foreach (array_keys($composerData['require']) as $requiredPackage) {
  78. if (preg_match($metapackageMatcher, $requiredPackage, $matches)) {
  79. $edition = $matches['edition'];
  80. break;
  81. }
  82. }
  83. if (empty($edition)) {
  84. throw new InvalidArgumentException("No Magento metapackage found in $composerFile");
  85. }
  86. // Override composer.json edition if one is passed to the script
  87. if (!empty($opts['edition'])) {
  88. $edition = $opts['edition'];
  89. }
  90. $edition = strtolower($edition);
  91. if ($edition !== 'community' && $edition !== 'enterprise') {
  92. throw new InvalidArgumentException("Only 'community' and 'enterprise' editions allowed; '$edition' given");
  93. }
  94. $composerExec = (!empty($opts['composer']) ? $opts['composer'] : 'composer');
  95. if (basename($composerExec, '.phar') != 'composer') {
  96. throw new InvalidArgumentException("'$composerExec' is not a composer executable");
  97. }
  98. // Use 'command -v' to check if composer is executable
  99. exec("command -v $composerExec", $out, $composerFailed);
  100. if ($composerFailed) {
  101. if ($composerExec == 'composer') {
  102. $message = 'Composer executable is not available in the system PATH';
  103. }
  104. else {
  105. $message = "Invalid composer executable '$composerExec'";
  106. }
  107. throw new InvalidArgumentException($message);
  108. }
  109. // The composer command uses the Magento root as the working directory so this script can be run from anywhere
  110. $composerExec = "$composerExec --working-dir='$rootDir'";
  111. // Set the version constraint to any 2.3 package if not specified
  112. $constraint = !empty($opts['version']) ? $opts['version'] : '2.3.*';
  113. // Composer package names
  114. $project = "magento/project-$edition-edition";
  115. $metapackage = "magento/product-$edition-edition";
  116. // Get the list of potential Magento repositories to search for the update package
  117. $mageUrls = [];
  118. $authFailed = [];
  119. if (!empty($opts['repo'])) {
  120. $mageUrls[] = $opts['repo'];
  121. }
  122. else {
  123. foreach ($composerData['repositories'] as $label => $repo) {
  124. if (is_string($label) && strpos(strtolower($label), 'mage') !== false || strpos($repo['url'], '.mage') !== false) {
  125. $mageUrls[] = $repo['url'];
  126. }
  127. }
  128. if (count($mageUrls) == 0) {
  129. throw new InvalidArgumentException('No Magento repository urls found in composer.json');
  130. }
  131. }
  132. $tempDir = findUnusedFilename($rootDir, 'temp_project');
  133. $projectConstraint = "$project='$constraint'";
  134. $version = null;
  135. $description = null;
  136. output("**** Searching for a matching version of $project ****");
  137. // Try to retrieve a 2.3 package from each Magento repository until one is found
  138. foreach ($mageUrls as $repoUrl) {
  139. try {
  140. output("\\nChecking $repoUrl");
  141. deleteFilepath($tempDir);
  142. runComposer("create-project --repository=$repoUrl $projectConstraint $tempDir --no-install");
  143. // Make sure the downloaded package is 2.3
  144. $newComposer = json_decode(file_get_contents("$tempDir/composer.json"), true);
  145. $version = $newComposer['version'];
  146. $description = $newComposer['description'];
  147. if (strpos($version, '2.3.') !== 0) {
  148. throw new InvalidArgumentException("Bad 2.3 version constraint '$constraint'; version $version found");
  149. }
  150. // If no errors occurred, set this as the correct repo, forget errors from previous repos, and move forward
  151. output("\\n**** Found compatible $project version: $version ****");
  152. $repo = $repoUrl;
  153. unset($exception);
  154. break;
  155. }
  156. catch (Exception $e) {
  157. // If this repository doesn't have a valid package, save the error but continue checking any others
  158. output("Failed to find a valid 2.3 $project package on $repoUrl", WARN);
  159. $exception = $e;
  160. }
  161. }
  162. // If a valid project package hasn't been found, throw the last error
  163. if (isset($exception)) {
  164. throw $exception;
  165. }
  166. output("\\n**** Executing Updates ****");
  167. $composerBackup = findUnusedFilename($rootDir, 'composer.json.bak');
  168. output("\\nBacking up $composerFile to $composerBackup");
  169. copy($composerFile, $composerBackup);
  170. // Add the repository to composer.json if needed without overwriting any existing ones
  171. $repoUrls = array_map(function ($r) { return $r['url']; }, $composerData['repositories']);
  172. if (!in_array($repo, $repoUrls)) {
  173. $repoLabels = array_map('strtolower',array_keys($composerData['repositories']));
  174. $newLabel = 'magento';
  175. if (in_array($newLabel, $repoLabels)) {
  176. $count = count($repoLabels);
  177. for ($i = 1; $i <= $count; $i++) {
  178. if (!in_array("$newLabel-$i", $repoLabels)) {
  179. $newLabel = "$newLabel-$i";
  180. break;
  181. }
  182. }
  183. }
  184. output("\\nAdding $repo to composer repositories under label '$newLabel'");
  185. runComposer("config repositories.$newLabel composer $repo");
  186. }
  187. output("\\nUpdating Magento metapackage requirement to $metapackage=$version");
  188. if ($edition == 'enterprise') {
  189. // Community -> Enterprise upgrades need to remove the community edition metapackage
  190. runComposer('remove magento/product-community-edition --no-update');
  191. output('');
  192. }
  193. runComposer("require $metapackage=$version --no-update");
  194. output('\nUpdating "require-dev" section of composer.json');
  195. runComposer('require --dev ' .
  196. 'phpunit/phpunit:~6.2.0 ' .
  197. 'friendsofphp/php-cs-fixer:~2.10.1 ' .
  198. 'lusitanian/oauth:~0.8.10 ' .
  199. 'pdepend/pdepend:2.5.2 ' .
  200. 'sebastian/phpcpd:~3.0.0 ' .
  201. 'squizlabs/php_codesniffer:3.2.2 --no-update');
  202. output('');
  203. runComposer('remove --dev sjparkinson/static-review fabpot/php-cs-fixer --no-update');
  204. output('\nAdding "Zend\\\\Mvc\\\\Controller\\\\": "setup/src/Zend/Mvc/Controller/" to "autoload": "psr-4"');
  205. $composerData['autoload']['psr-4']['Zend\\Mvc\\Controller\\'] = 'setup/src/Zend/Mvc/Controller/';
  206. if (preg_match('/^magento\/project\-(community|enterprise)\-edition$/', $composerData['name'])) {
  207. output('\nUpdating project name, version, and description');
  208. $composerData['name'] = $project;
  209. $composerData['version'] = $version;
  210. $composerData['description'] = $description;
  211. }
  212. file_put_contents($composerFile, json_encode($composerData, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
  213. // Update Magento/Updater if it's installed
  214. $updateDir = "$rootDir/update";
  215. if (file_exists($updateDir)) {
  216. $updateBackup = findUnusedFilename($rootDir, 'update.bak');
  217. output("\\nBacking up Magento/Updater directory $updateDir to $updateBackup");
  218. rename($updateDir, $updateBackup);
  219. output('\nUpdating Magento/Updater');
  220. rename("$tempDir/update", $updateDir);
  221. }
  222. // Remove temp project directory that was used for repo/version validation and new source for Magento/Updater
  223. deleteFilepath($tempDir);
  224. output("\\n**** Script Complete! $composerFile updated to Magento version $version ****");
  225. if (count($authFailed) > 0) {
  226. output('Repository authentication failures occurred!', WARN);
  227. output(' * Failed authentication could result in incorrect package versions', WARN);
  228. output(' * To resolve, add credentials for the repositories to auth.json', WARN);
  229. output(' * URL(s) failing authentication: ' . join(', ', array_keys($authFailed)), WARN);
  230. }
  231. } catch (Exception $e) {
  232. if ($e->getPrevious()) {
  233. $e = $e->getPrevious();
  234. }
  235. try {
  236. output($e->getMessage(), ERROR, get_class($e));
  237. output('Script failed! See usage information with --help', ERROR);
  238. if (isset($composerBackup) && file_exists($composerBackup)) {
  239. output("Resetting $composerFile backup");
  240. deleteFilepath($composerFile);
  241. rename($composerBackup, $composerFile);
  242. }
  243. if (isset($updateBackup) && file_exists($updateBackup)) {
  244. output("Resetting $updateDir backup");
  245. deleteFilepath($updateDir);
  246. rename($updateBackup, $updateDir);
  247. }
  248. if (isset($tempDir) && file_exists($tempDir)) {
  249. output('Removing temporary project directory');
  250. deleteFilepath($tempDir);
  251. }
  252. }
  253. catch (Exception $e2) {
  254. output($e2->getMessage(), ERROR, get_class($e2));
  255. output('Backup restoration or directory cleanup failed', ERROR);
  256. }
  257. exit($e->getCode() == 0 ? 1 : $e->getCode());
  258. }
  259. /**
  260. * Gets a variant of a filename that doesn't already exist so we don't overwrite anything
  261. *
  262. * @param string $dir
  263. * @param string $filename
  264. * @return string
  265. */
  266. function findUnusedFilename($dir, $filename) {
  267. $unique = "$dir/$filename";
  268. if (file_exists($unique)) {
  269. $unique = tempnam($dir, "$filename.");
  270. unlink($unique);
  271. }
  272. return $unique;
  273. }
  274. /**
  275. * Execute a composer command, reload $composerData afterwards, and check for repo authentication warnings
  276. *
  277. * @param string $command
  278. * @return array Command output split by lines
  279. * @throws RuntimeException
  280. */
  281. function runComposer($command)
  282. {
  283. global $composerExec, $composerData, $composerFile, $authFailed;
  284. $command = "$composerExec $command --no-interaction";
  285. output(" Running command:\\n $command");
  286. exec("$command 2>&1", $lines, $exitCode);
  287. $output = ' ' . join('\n ', $lines);
  288. // Reload composer object from the updated composer.json
  289. $composerData = json_decode(file_get_contents($composerFile), true);
  290. if (0 !== $exitCode) {
  291. $output = "Error encountered running command:\\n $command\\n$output";
  292. throw new RuntimeException($output, $exitCode);
  293. }
  294. output($output);
  295. if (strpos($output, 'URL required authentication.') !== false) {
  296. preg_match("/'(https?:\/\/)?(?<url>[^\/']+)(\/[^']*)?' URL required authentication/", $output, $matches);
  297. $authUrl = $matches['url'];
  298. $authFailed[$authUrl] = 1;
  299. output("Repository authentication failed; make sure '$authUrl' exists in auth.json", WARN);
  300. }
  301. return $lines;
  302. }
  303. /**
  304. * Deletes a file or a directory and all its contents
  305. *
  306. * @param string $path
  307. * @throws Exception
  308. */
  309. function deleteFilepath($path) {
  310. if (!file_exists($path)) {
  311. return;
  312. }
  313. if (is_dir($path)) {
  314. $files = array_diff(scandir($path), array('..', '.'));
  315. foreach ($files as $file) {
  316. deleteFilepath("$path/$file");
  317. }
  318. rmdir($path);
  319. }
  320. else {
  321. unlink($path);
  322. }
  323. if (file_exists($path)) {
  324. throw new Exception("Failed to delete $path");
  325. }
  326. }
  327. /**
  328. * Logs the given text with \n newline replacement and log level formatting
  329. *
  330. * @param string $string Text to log
  331. * @param int $level One of INFO, WARN, or ERROR
  332. * @param string $label Optional message label; defaults to WARNING for $level = WARN and ERROR for $level = ERROR
  333. */
  334. function output($string, $level = INFO, $label = '') {
  335. $string = str_replace('\n', PHP_EOL, $string);
  336. if (!empty($label)) {
  337. $label = "$label: ";
  338. }
  339. else if ($level == WARN) {
  340. $label = 'WARNING: ';
  341. }
  342. else if ($level == ERROR) {
  343. $label = 'ERROR: ';
  344. }
  345. $string = "$label$string";
  346. if ($level == WARN) {
  347. error_log($string);
  348. }
  349. elseif ($level == ERROR) {
  350. error_log(PHP_EOL . $string);
  351. }
  352. else {
  353. echo $string . PHP_EOL;
  354. }
  355. }