-
Notifications
You must be signed in to change notification settings - Fork 15
MAGETWO-94153: Add functionality to composer update to lookahead for … #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MAGETWO-94153: Add functionality to composer update to lookahead for … #1
Conversation
…root composer.json changes when updating a magento/product* package
| $eventDispatcher->addListener( | ||
| ScriptEvents::POST_UPDATE_CMD, | ||
| [$this, 'writeUpdatedRoot'], | ||
| PHP_INT_MAX |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be at the place where we write to the file (line 230)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The listener is preferable, as adding it with PHP_INT_MAX priority will allow us to put it at the head of the event queue and write the composer.json changes before any other POST_UPDATE_CMD scripts are triggered. Line 230 handles the case where ROOT_ONLY_OPT is passed, which doesn't call the native update's execute() method and thus doesn't trigger event listeners but we still want to write the updated composer.json.
| if ($installVal === null) { | ||
| $action = static::ADD_VAL; | ||
| } else { | ||
| $action = 'change'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be static::CHANGE_VAL?
composer.json
Outdated
| ], | ||
| "type": "composer-plugin", | ||
| "require": { | ||
| "composer/composer": "*", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure that any version will work?
| @@ -0,0 +1,48 @@ | |||
|
|
|||
| Open Software License ("OSL") v. 3.0 | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to have more info about the project in the Readme.
composer.json
Outdated
| } | ||
| }, | ||
| "extra": { | ||
| "class": "Magento\\Composer\\Plugin\\RootUpdate\\RootUpdatePlugin" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How exactly the plugin is executed considering that there is no scripts declaration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Composer supports plugins (the package is of type "composer-plugin"). The "extra->class" section in composer.json tells Composer the class that implements PluginInterface and it goes from there. This is a full plugin, not a script hook, which can't touch commands.
| $updatePrepared = false; | ||
| $updater = new MagentoRootUpdater($io, $composer, $input); | ||
| try { | ||
| // Move the native UpdateCommand's deprecation message before the added Magento functionality |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this going to be maintained in the future to be in sync with new composer versions? If I understand correctly, we are duplicating some composer logic here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same as anything else: with new releases of the plugin. The current composer/composer version constraint in composer.json locks this version of the plugin to the most recent version of Composer or below, which have been tested and are compatible. As new composer versions come out that would affect the plugin's functionality, we'll update it and adjust the composer requirement to fit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it's interesting question... I believe the constraint in composer.json is what Composer will download for your plugin to use (as if you'd specify Symfony). So that composer version may be different than Composer running by the user. Interesting how this can be handled. Could you please check what happens if you run composer version different from what's specified in the constraint?
| } | ||
|
|
||
| $errorCode = null; | ||
| if (!$input->getOption(static::ROOT_ONLY_OPT)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing plugin to require instead of update like suggested by Olga, would allow to eliminate all this logic.
|
|
||
| if ($errorCode && !$updater->isStrictConstraint()) { | ||
| $io->writeError( | ||
| '<warning>Recommended: Use a specific Magento version constraint instead of "' . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How using strict constraint might fix update errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plugin uses the same version constraint as the composer file to find the target version to check for changes, but it can't do the full dependency compatibility checks that Composer does during the full update (that's what the plugin tries to help solve in the first place), which means that a non-strict constraint could have the plugin using values for a different version than is actually used by Composer, and those values might not be compatible with the real target version. A strict constraint ensures that both the plugin and the full update are using the same target package version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to use composer functionality in some dry-run mode to get the actually resolved version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that we need the plugin to fix dependencies in order to find the actual resolved version in order to know what version to use for the dependencies the plugin needs to fix; it's a recursive loop. Official Magento documentation says to use strict version constraints, so rather than over-engineering a solution that's not foolproof, a best-guess approach + a warning on the fail case should be appropriate.
| use Symfony\Component\Console\Output\OutputInterface; | ||
|
|
||
| /** | ||
| * Class RootUpdateCommand |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's ok for now, but in general it's better to provide more descriptive comments and we also don't use @Package annotation.
| return; | ||
| } | ||
|
|
||
| if (!preg_match('/\/var\/composer\.json$/', $path)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible that after installation, someone will clear the var directory along with the copied version of plugin. This will lead to silent bugs.
To prevent such situation we can add logic of copying this extension to the new version of web setup wizard, to the same place where it copies composer.json
| */ | ||
| public static function updateSetupWizardPlugin($io, $composer, $path, $version) | ||
| { | ||
| $productRegex = '/magento\/product-(community|enterprise)-edition/'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please wrap each logical piece into a separate function to improve readability. E.g. isMagentoProductRequired and isMagentoComposerUpdatePluginRequired
| $prettyBaseVersion = $fromRoot['version']; | ||
| } | ||
|
|
||
| $baseRoot = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename all variables similar to $baseRoot to $baseRootComposerJsonFile. Similarly $baseRootComposerLockFile if any
| * @param string $minimumStability | ||
| * @return array | ||
| */ | ||
| protected function extractStabilityFlags($reqName, $reqVersion, $minimumStability) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this logic to a separate class to separate our custom logic from the one copy-pasted from the composer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same relates to logic like deprecation warnings in \Magento\Composer\Plugin\RootUpdate\RootUpdateCommand::execute
…d install command and README.md text
paliarush
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please address new comments.
During this review pass I skipped the logic of the plugin, assuming that it is sufficiently covered with integration tests and has not changed much since previous review.
|
|
||
| In this case, run the following command with the appropriate values to correct the existing `composer.json` file before proceeding with the expected `composer require` command for the target Magento product. | ||
|
|
||
| composer require <current_Magento_package> <current_version> --previous-magento-package <previous_Magento_package>=<previous_Magento_version> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be previous or initial? Because if someone initially installed M 2.0 and then upgraded to 2.3 and now trying to upgrade to 2.3.1, they will have previous = 2.3, initial = 2.0. And their root composer.json will still be from M 2.0.
|
|
||
| To resolve these conflicts interactively, re-run the `composer require` command with the `--interactive-magento-conflicts` option. | ||
|
|
||
| To override all conflicting custom values with the expected Magento values, re-run the `composer require` command with the `--use-magento-values` option. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use-magento-values => use-default-magento-values ?
| To override all conflicting custom values with the expected Magento values, re-run the `composer require` command with the `--use-magento-values` option. | ||
|
|
||
| ## Bypassing the plugin | ||
| To run the native `composer require` command without the plugin's updates, use the `--skip-magento-root` option. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ski-magento-root => skip-magento-root-plugin ?
| * | ||
| * Copy and expose necessary private methods | ||
| * | ||
| * Functions here may need to be updated to match future versions of Composer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to establish some procedure around this to make sure in the future it will be clear for any developer working on this code that these classes must be updated.
| @@ -0,0 +1,48 @@ | |||
|
|
|||
| Open Software License ("OSL") v. 3.0 | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have two different licenses in here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied the pattern in core Magento, which uses both OSL and AFL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check with our legal team on which licenses should we put before making the repo public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the recently open-sourced m2-devtools repo, @StevenG from Adobe states that the dual licenses to match existing practices makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will be going through the Adobe open-sourcing process for the plugin, see https://jira.corp.magento.com/browse/MAGETWO-94868
| use Magento\ComposerRootUpdatePlugin\Plugin\Commands\UpdatePluginNamespaceCommands; | ||
|
|
||
| /** | ||
| * Class CommandProvider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documentation is now mandatory for all new classes and methods we introduce. BTW, we need to setup builds to run our static tests against this code somehow, or at the very least run the manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a build for this and it should be part of the contribution requirements to pass this build.
Please create a task for it and, ideally, get it implemented. If not, at least discuss with PO on when it can be done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @@ -0,0 +1,52 @@ | |||
| # Overview | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we duplicate readme here and in the root of the project?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Root of the project for the Github code, in src/Magento/ComposerRootUpdatePlugin for the composer package, which is generated from that directory instead of the root. This is the same as in core Magento modules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This repo is different from Magento repo. Why we don't put all the code in the root of the repo and so have single readme and any other such files? The package is created from the root of the repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The package is created from the src/Magento/ComposerRootUpdatePlugin directory.
The package can't be sourced from the root of the repo because of the integration tests. In order to test the plugin functionality on actual composer files, the tests need to install the plugin as a package in a test directory. However, composer won't allow a package to be installed in a subdirectory of itself, so the src folder where the plugin package exists needs to be separate from the tests folder.
| use Composer\Package\Version\VersionParser; | ||
|
|
||
| /** | ||
| * Class AccessibleRootPackageLoader |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is not needed.
| /** | ||
| * Class AccessibleRootPackageLoader | ||
| * | ||
| * Copy and expose necessary private methods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private methods "... of Composer library"? As it is right now, doesn't sound clear.
| @@ -0,0 +1,48 @@ | |||
|
|
|||
| Open Software License ("OSL") v. 3.0 | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check with our legal team on which licenses should we put before making the repo public.
| use Magento\ComposerRootUpdatePlugin\Plugin\Commands\UpdatePluginNamespaceCommands; | ||
|
|
||
| /** | ||
| * Class CommandProvider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a build for this and it should be part of the contribution requirements to pass this build.
Please create a task for it and, ideally, get it implemented. If not, at least discuss with PO on when it can be done.
| null, | ||
| InputOption::VALUE_REQUIRED, | ||
| 'Use a previously-installed Magento product version as the base for composer.json updates', | ||
| static::PREV_OPT_HINT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's better to put into the description? I think it may be confusing that a default value is used as a hint for people who are used to the Symfony console interface.
| static::PREVIOUS_PACKAGE_OPT, | ||
| null, | ||
| InputOption::VALUE_REQUIRED, | ||
| 'Use a previously-installed Magento product version as the base for composer.json updates', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Use ..." implies it is a binary (yes/no) option. At least this is how I read it. It'd rephrase to "Edition and version of Magento product to use as a base ..." - something like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also... how does it look like? --previous-magento-package=magento/product-community-edition=2.2.5? Looks confusing that it has two = in it. Maybe it's better to split into two options?
Also, I'd rename it to --base-... instead of --previous.
| public function execute(InputInterface $input, OutputInterface $output) | ||
| { | ||
| $updater = null; | ||
| Console::setIO($this->getIO()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why static methods of Console are used here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console boils down to a singleton IO implementation which I wanted to avoid passing around to every class that needs it (which is most of them), but it needs the IOInterface that is initially set when the command is called to function. I can change it to a non-static class if preferred, I just thought the code was more readable without passing a Console object to most methods.
|
|
||
| if ($didUpdate && $errorCode !== 0) { | ||
| // If the native execute() didn't succeed, revert the Magento changes to the composer.json file | ||
| $this->revertMageComposerFile('The native \'composer ' . $this->commandName . '\' command failed'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be useful to add some identification of this command. Otherwise for someone who doesn't know how our plugin works it will look very confusing (because he/she wouldn't know about existence of our command).
Maybe something like [magento/composer-root-update-plugin] at the beginning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean here, can you clarify?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm saying that for the user it will be hard to understand that this error is caused by the plugin (vs Composer itself). So the message should include some info that points to it.
| /** | ||
| * Class Console | ||
| * | ||
| * Shared static logger and interaction methods |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need it as a static class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console boils down to a singleton IO implementation which I wanted to avoid passing around to every class that needs it (which is most of them), but it needs the IOInterface that is initially set when the command is called to function. I can change it to a non-static class if preferred, I just thought the code was more readable without passing a Console object to most methods.
| /** | ||
| * Class WebSetupWizardPluginInstaller | ||
| */ | ||
| abstract class WebSetupWizardPluginInstaller |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it abstract?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every method inside is static without any saved values, so it doesn't need instantiation. I made it abstract to make it more clear that there was no need to create new instances of it to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
abstract means that it must be extended. It's strange to use it this way. We should reconsider the staticness of the class too. Singleton is not a good practice (similar to global vars).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WebSetupWizardPluginInstaller doesn't maintain any state, so there's nothing to instantiate. I can make the class non-abstract, but there's no reason to require the user to instantiate a new instance.
| $packageName = PluginDefinition::PACKAGE_NAME; | ||
|
|
||
| // If in ./var already or Magento or the plugin is missing from composer.json, do not install in var | ||
| if (!preg_match('/\/composer\.json$/', $filePath) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a public method, but I don't see that $filePath is validated to make sure it's not including sensitive locations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What validation would you like?
| @@ -0,0 +1,11 @@ | |||
| <?xml version="1.0"?> | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the plugin declared as a Magento module? Does it have module functionality?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module functionality allows the plugin to install/update itself inside the var directory for the WebSetupWizard functionality when bin magento setup commands are run. It only uses the module implementation for the InstallData, RecurringData, and UpgradeData classes that Magento hooks into. Without it, the plugin won't be present in var if bin magento setup:uninstall -> bin magento setup:install are run (this was caught by the upgrade verification builds). We discussed this on the call we had before the previous code review.
The new composer magento-update-plugin install command will also do the reinstallation when it is run (and will be added to the WebSetupWizard itself, that code will be in its own PR), but the module implementation makes the plugin more invisible for existing installations that won't have the updated setup wizard.
COMOPS-890: Cloud Support
…root composer.json changes when updating a magento/product* package