Skip to content

Commit 027610e

Browse files
MAGETWO-87492: Improving upgrade script parameter validation
1 parent 2789d5c commit 027610e

File tree

1 file changed

+139
-86
lines changed

1 file changed

+139
-86
lines changed

dev/tools/UpgradeScripts/pre_composer_update_2.3.php

Lines changed: 139 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
'SYNOPSIS',
1313
<<<SYNOPSIS
1414
Updates Magento with 2.3 requirements that can't be done by `composer update` or `bin/magento setup:upgrade`.
15-
This should be run prior to running `composer update` or `bin/magento setup:upgrade`.
15+
Run this script after upgrading to PHP 7.1/7.2 and before running `composer update` or `bin/magento setup:upgrade`.
1616
1717
Steps included:
1818
- Require new version of the metapackage
1919
- Update "require-dev" section
2020
- Add "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" to composer.json "autoload":"psr-4" section
2121
- Update Magento/Updater if it's installed
22-
- Update root version label
22+
- Update name, version, and description fields in the root composer.json
2323
2424
Usage: php -f $_scriptName -- --root='</path/to/magento/root/>' [--composer='</path/to/composer/executable>']
2525
[--edition='<community|enterprise>'] [--repo='<composer_repo_url>'] [--version='<version_constraint>']
@@ -32,7 +32,7 @@
3232
Optional:
3333
--composer='</path/to/composer/executable>'
3434
Path to the composer executable
35-
- Default: The composer found in PATH
35+
- Default: The composer found in the system PATH
3636
3737
--edition='<community|enterprise>'
3838
Target Magento edition for the update. Open Source = 'community', Commerce = 'enterprise'
@@ -72,78 +72,103 @@
7272
}
7373

7474
try {
75+
if (version_compare(PHP_VERSION, '7.1', '<') || version_compare(PHP_VERSION, '7.3', '>=')) {
76+
preg_match('/^\d+\.\d+\.\d+/',PHP_VERSION, $matches);
77+
$phpVersion = $matches[0];
78+
throw new Exception("Invalid PHP version '$phpVersion'. Magento 2.3 requires PHP 7.1 or 7.2");
79+
}
80+
7581
/**** Populate and Validate Settings ****/
7682

77-
if (empty($opts['root'])) {
78-
throw new BadMethodCallException('Magento root must be supplied with --root');
83+
if (empty($opts['root']) || !is_dir($opts['root'])) {
84+
throw new BadMethodCallException('Existing Magento root directory must be supplied with --root');
7985
}
80-
8186
$rootDir = $opts['root'];
82-
if (!is_dir($rootDir)) {
83-
throw new InvalidArgumentException("Supplied Magento root directory '$rootDir' does not exist");
84-
}
85-
86-
$tempDir = findUnusedFilename($rootDir, "temp_project");
8787

88-
// The composer command uses the Magento root as the working directory so this script can be run from anywhere
89-
$cmd = (!empty($opts['composer']) ? $opts['composer'] : 'composer') . " --working-dir='$rootDir'";
88+
$composerFile = "$rootDir/composer.json";
89+
if (!file_exists($composerFile)) {
90+
throw new InvalidArgumentException("Supplied Magento root directory '$rootDir' does not contain composer.json");
91+
}
9092

91-
// Set the version constraint to any 2.3 package if not specified
92-
$constraint = !empty($opts['version']) ? $opts['version'] : '2.3.*';
93+
$composerData = json_decode(file_get_contents($composerFile), true);
9394

94-
// Grab the root composer.json contents to pull defaults or other data from when necessary
95-
$composer = json_decode(file_get_contents("$rootDir/composer.json"), true);
95+
$metapackageMatcher = '/^magento\/product\-(?<edition>community|enterprise)\-edition$/';
96+
foreach (array_keys($composerData['require']) as $requiredPackage) {
97+
if (preg_match($metapackageMatcher, $requiredPackage, $matches)) {
98+
$edition = $matches['edition'];
99+
break;
100+
}
101+
}
102+
if (empty($edition)) {
103+
throw new InvalidArgumentException("No Magento metapackage found in $composerFile");
104+
}
96105

97-
// Get the target Magento edition
106+
// Override composer.json edition if one is passed to the script
98107
if (!empty($opts['edition'])) {
99108
$edition = $opts['edition'];
100109
}
101-
else {
102-
$metapackageMatcher = '|^magento/product\-(?<edition>[a-z]+)\-edition$|';
103-
104-
foreach (array_keys($composer['require']) as $requiredPackage) {
105-
if (preg_match($metapackageMatcher, $requiredPackage, $matches)) {
106-
$edition = $matches['edition'];
107-
break;
108-
}
109-
}
110-
if (empty($edition)) {
111-
throw new InvalidArgumentException('No Magento metapackage found in composer.json requirements');
112-
}
113-
}
114110
$edition = strtolower($edition);
115111

116112
if ($edition !== 'community' && $edition !== 'enterprise') {
117113
throw new InvalidArgumentException("Only 'community' and 'enterprise' editions allowed; '$edition' given");
118114
}
119115

116+
$composerExec = (!empty($opts['composer']) ? $opts['composer'] : 'composer');
117+
if (basename($composerExec, '.phar') != 'composer') {
118+
throw new InvalidArgumentException("'$composerExec' is not a composer executable");
119+
}
120+
121+
// Use 'command -v' to check if composer is executable
122+
exec("command -v $composerExec", $out, $composerFailed);
123+
if ($composerFailed) {
124+
if ($composerExec == 'composer') {
125+
$message = 'Composer executable is not available in the system PATH';
126+
}
127+
else {
128+
$message = "Invalid composer executable '$composerExec'";
129+
}
130+
throw new InvalidArgumentException($message);
131+
}
132+
133+
// The composer command uses the Magento root as the working directory so this script can be run from anywhere
134+
$composerExec = "$composerExec --working-dir='$rootDir'";
135+
136+
// Set the version constraint to any 2.3 package if not specified
137+
$constraint = !empty($opts['version']) ? $opts['version'] : '2.3.*';
138+
120139
// Composer package names
121140
$project = "magento/project-$edition-edition";
122141
$metapackage = "magento/product-$edition-edition";
123142

124143
// Get the list of potential Magento repositories to search for the update package
125-
$repoUrls = array_map(function ($r) { return $r['url']; }, $composer['repositories']);
126144
$mageUrls = [];
145+
$authFailed = [];
127146
if (!empty($opts['repo'])) {
128147
$mageUrls[] = $opts['repo'];
129148
}
130149
else {
131-
$mageUrls = array_filter($repoUrls, function($u) { return strpos($u, '.mage') !== false; });
150+
foreach ($composerData['repositories'] as $label => $repo) {
151+
if (strpos(strtolower($label), 'mage') !== false || strpos($repo['url'], '.mage') !== false) {
152+
$mageUrls[] = $repo['url'];
153+
}
154+
}
132155

133156
if (count($mageUrls) == 0) {
134157
throw new InvalidArgumentException('No Magento repository urls found in composer.json');
135158
}
136159
}
137160

161+
$tempDir = findUnusedFilename($rootDir, 'temp_project');
138162
$projectConstraint = "$project='$constraint'";
139163
$version = null;
140164
$description = null;
141-
$versionValidator = '/^2\.3\.\d/';
165+
166+
output("**** Searching for a matching version of $project ****");
142167

143168
// Try to retrieve a 2.3 package from each Magento repository until one is found
144169
foreach ($mageUrls as $repoUrl) {
145170
try {
146-
output("Checking $repoUrl for a matching version of $project");
171+
output("\\nChecking $repoUrl");
147172
deleteFilepath($tempDir);
148173
runComposer("create-project --repository=$repoUrl $projectConstraint $tempDir --no-install");
149174

@@ -152,12 +177,12 @@
152177
$version = $newComposer['version'];
153178
$description = $newComposer['description'];
154179

155-
if (!preg_match($versionValidator, $version)) {
180+
if (strpos($version, '2.3.') !== 0) {
156181
throw new InvalidArgumentException("Bad 2.3 version constraint '$constraint'; version $version found");
157182
}
158183

159-
// If no errors occur, set this as the correct repo, forget errors from previous repos, and move forward
160-
output("Found $project version $version");
184+
// If no errors occurred, set this as the correct repo, forget errors from previous repos, and move forward
185+
output("\\n**** Found compatible $project version: $version ****");
161186
$repo = $repoUrl;
162187
unset($exception);
163188
break;
@@ -174,15 +199,16 @@
174199
throw $exception;
175200
}
176201

177-
/**** Execute Updates ****/
202+
output("\\n**** Executing Updates ****");
178203

179-
$composerBackup = findUnusedFilename($rootDir, "composer.json.bak");
180-
output("Backing up $rootDir/composer.json to $composerBackup");
181-
copy("$rootDir/composer.json", $composerBackup);
204+
$composerBackup = findUnusedFilename($rootDir, 'composer.json.bak');
205+
output("\\nBacking up $composerFile to $composerBackup");
206+
copy($composerFile, $composerBackup);
182207

183208
// Add the repository to composer.json if needed without overwriting any existing ones
209+
$repoUrls = array_map(function ($r) { return $r['url']; }, $composerData['repositories']);
184210
if (!in_array($repo, $repoUrls)) {
185-
$repoLabels = array_map('strtolower',array_keys($composer['repositories']));
211+
$repoLabels = array_map('strtolower',array_keys($composerData['repositories']));
186212
$newLabel = 'magento';
187213
if (in_array($newLabel, $repoLabels)) {
188214
$count = count($repoLabels);
@@ -193,81 +219,88 @@
193219
}
194220
}
195221
}
196-
output("Adding $repo to composer repositories under label '$newLabel'");
222+
output("\\nAdding $repo to composer repositories under label '$newLabel'");
197223
runComposer("config repositories.$newLabel composer $repo");
198224
}
199225

200-
output("Updating Magento metapackage requirement to $metapackage=$version");
226+
output("\\nUpdating Magento metapackage requirement to $metapackage=$version");
201227
if ($edition == 'enterprise') {
202228
// Community -> Enterprise upgrades need to remove the community edition metapackage
203229
runComposer('remove magento/product-community-edition --no-update');
230+
output('');
204231
}
205232
runComposer("require $metapackage=$version --no-update");
206233

207-
output('Updating "require-dev" section of composer.json');
234+
output('\nUpdating "require-dev" section of composer.json');
208235
runComposer('require --dev ' .
209236
'phpunit/phpunit:~6.2.0 ' .
210237
'friendsofphp/php-cs-fixer:~2.10.1 ' .
211238
'lusitanian/oauth:~0.8.10 ' .
212239
'pdepend/pdepend:2.5.2 ' .
213240
'sebastian/phpcpd:~3.0.0 ' .
214241
'squizlabs/php_codesniffer:3.2.2 --no-update');
242+
output('');
215243
runComposer('remove --dev sjparkinson/static-review fabpot/php-cs-fixer --no-update');
216244

217-
output('Adding "Zend\\\\Mvc\\\\Controller\\\\": "setup/src/Zend/Mvc/Controller/" to "autoload": "psr-4"');
218-
$composer['autoload']['psr-4']['Zend\\Mvc\\Controller\\'] = 'setup/src/Zend/Mvc/Controller/';
219-
220-
output('Updating root version label from ' . $composer['version'] . " to $version");
221-
$composer['version'] = $version;
245+
output('\nAdding "Zend\\\\Mvc\\\\Controller\\\\": "setup/src/Zend/Mvc/Controller/" to "autoload": "psr-4"');
246+
$composerData['autoload']['psr-4']['Zend\\Mvc\\Controller\\'] = 'setup/src/Zend/Mvc/Controller/';
222247

223-
if ($composer['name'] !== $project) {
224-
output('Updating root project name and description from ' . $composer['name'] . " to $project");
225-
$composer['name'] = $project;
226-
$composer['description'] = $description;
248+
if (preg_match('/^magento\/project\-(community|enterprise)\-edition$/', $composerData['name'])) {
249+
output('\nUpdating project name, version, and description');
250+
$composerData['name'] = $project;
251+
$composerData['version'] = $version;
252+
$composerData['description'] = $description;
227253
}
228254

229-
file_put_contents("$rootDir/composer.json", json_encode($composer, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
255+
file_put_contents($composerFile, json_encode($composerData, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
230256

231257
// Update Magento/Updater if it's installed
232-
if (file_exists("$rootDir/update")) {
233-
$updateBackup = findUnusedFilename($rootDir, "update.bak");
234-
output("Backing up Magento/Updater directory $rootDir/update to $updateBackup");
235-
rename("$rootDir/update", $updateBackup);
236-
output('Updating Magento/Updater');
237-
rename("$tempDir/update", "$rootDir/update");
258+
$updateDir = "$rootDir/update";
259+
if (file_exists($updateDir)) {
260+
$updateBackup = findUnusedFilename($rootDir, 'update.bak');
261+
output("\\nBacking up Magento/Updater directory $updateDir to $updateBackup");
262+
rename($updateDir, $updateBackup);
263+
output('\nUpdating Magento/Updater');
264+
rename("$tempDir/update", $updateDir);
238265
}
239266

240267
// Remove temp project directory that was used for repo/version validation and new source for Magento/Updater
241268
deleteFilepath($tempDir);
242269

243-
output('\n**** Script Complete! ****');
270+
output("\\n**** Script Complete! $composerFile updated to Magento version $version ****");
271+
if (count($authFailed) > 0) {
272+
output('Repository authentication failures occurred!', WARN);
273+
output(' * Failed authentication could result in incorrect package versions', WARN);
274+
output(' * To resolve, add credentials for the repositories to auth.json', WARN);
275+
output(' * URL(s) failing authentication: ' . join(', ', array_keys($authFailed)), WARN);
276+
}
244277
} catch (Exception $e) {
245278
if ($e->getPrevious()) {
246-
$message = (string)$e->getPrevious();
247-
} else {
248-
$message = $e->getMessage();
279+
$e = $e->getPrevious();
249280
}
250281

251282
try {
252-
output($message . '\n\nScript failed! See usage information with --help', ERROR);
283+
output($e->getMessage(), ERROR, get_class($e));
284+
output('Script failed! See usage information with --help', ERROR);
253285

254286
if (isset($composerBackup) && file_exists($composerBackup)) {
255-
output('Resetting composer.json backup');
256-
deleteFilepath("$rootDir/composer.json");
257-
rename($composerBackup, "$rootDir/composer.json");
287+
output("Resetting $composerFile backup");
288+
deleteFilepath($composerFile);
289+
rename($composerBackup, $composerFile);
258290
}
259291
if (isset($updateBackup) && file_exists($updateBackup)) {
260-
output('Resetting Magento/Updater backup');
261-
deleteFilepath("$rootDir/update");
262-
rename($updateBackup, "$rootDir/update");
292+
output("Resetting $updateDir backup");
293+
deleteFilepath($updateDir);
294+
rename($updateBackup, $updateDir);
263295
}
264296
if (isset($tempDir) && file_exists($tempDir)) {
265297
output('Removing temporary project directory');
266298
deleteFilepath($tempDir);
267299
}
268300
}
269301
catch (Exception $e2) {
270-
output($e2->getMessage() . '\n\nBackup restoration/directory cleanup failed', ERROR);
302+
output($e2->getMessage(), ERROR, get_class($e2));
303+
output('Backup restoration or directory cleanup failed', ERROR);
271304
}
272305

273306
exit($e->getCode() == 0 ? 1 : $e->getCode());
@@ -290,28 +323,36 @@ function findUnusedFilename($dir, $filename) {
290323
}
291324

292325
/**
293-
* Execute a composer command and output the results
326+
* Execute a composer command, reload $composerData afterwards, and check for repo authentication warnings
294327
*
295328
* @param string $command
296329
* @return array Command output split by lines
297330
* @throws RuntimeException
298331
*/
299332
function runComposer($command)
300333
{
301-
global $cmd, $composer, $rootDir;
302-
$command = "$cmd $command";
303-
output(' Running command: \n ' . $command);
334+
global $composerExec, $composerData, $composerFile, $authFailed;
335+
$command = "$composerExec $command --no-interaction";
336+
output(" Running command:\\n $command");
304337
exec("$command 2>&1", $lines, $exitCode);
305-
$output = join(PHP_EOL, $lines);
338+
$output = ' ' . join('\n ', $lines);
306339

307340
// Reload composer object from the updated composer.json
308-
$composer = json_decode(file_get_contents("$rootDir/composer.json"), true);
341+
$composerData = json_decode(file_get_contents($composerFile), true);
309342

310343
if (0 !== $exitCode) {
311-
$output = 'Error encountered running command:' . PHP_EOL . " $command" . PHP_EOL . $output;
344+
$output = "Error encountered running command:\\n $command\\n$output";
312345
throw new RuntimeException($output, $exitCode);
313346
}
314347
output($output);
348+
349+
if (strpos($output, 'URL required authentication.') !== false) {
350+
preg_match("/'(https?:\/\/)?(?<url>[^\/']+)(\/[^']*)?' URL required authentication/", $output, $matches);
351+
$authUrl = $matches['url'];
352+
$authFailed[$authUrl] = 1;
353+
output("Repository authentication failed; make sure '$authUrl' exists in auth.json", WARN);
354+
}
355+
315356
return $lines;
316357
}
317358

@@ -345,17 +386,29 @@ function deleteFilepath($path) {
345386
*
346387
* @param string $string Text to log
347388
* @param int $level One of INFO, WARN, or ERROR
389+
* @param string $label Optional message label; defaults to WARNING for $level = WARN and ERROR for $level = ERROR
348390
*/
349-
function output($string, $level = INFO) {
350-
$string = str_replace('\n', PHP_EOL, $string) . PHP_EOL;
391+
function output($string, $level = INFO, $label = '') {
392+
$string = str_replace('\n', PHP_EOL, $string);
393+
394+
if (!empty($label)) {
395+
$label = "$label: ";
396+
}
397+
else if ($level == WARN) {
398+
$label = 'WARNING: ';
399+
}
400+
else if ($level == ERROR) {
401+
$label = 'ERROR: ';
402+
}
403+
$string = "$label$string";
351404

352405
if ($level == WARN) {
353-
error_log("WARNING: $string");
406+
error_log($string);
354407
}
355408
elseif ($level == ERROR) {
356-
error_log(PHP_EOL . "ERROR: $string");
409+
error_log(PHP_EOL . $string);
357410
}
358411
else {
359-
echo $string;
412+
echo $string . PHP_EOL;
360413
}
361414
}

0 commit comments

Comments
 (0)