bin/magento module:uninstall never completes, hangs on removing code from Magento codebase:

2021-05-05T11:44:00.002-07:00

When you are developing a plugin and are writing an uninstaller for it to clean up your attributes and models, you might notice that it hangs on removing code from Magento Code base:

It then blinks, and hangs and does nothing for a long time, no matter how long you wait. In this blog post, I will try to explain the cause and a solution for the problem. For the solution, scroll down to the end, where the Solution header is.

$output->writeln('<info>Removing code from Magento codebase:</info>');
$packages = [];
/** @var \Magento\Framework\Module\PackageInfo $packageInfo */
$packageInfo = $this->objectManager->get(\Magento\Framework\Module\PackageInfoFactory::class)->create();
foreach ($modules as $module) {
    $packages[] = $packageInfo->getPackageName($module);
}
$this->remove->remove($packages);

If we check what is called in the code of the factory, versus what is in the constructor of the MagentoComposerApplication

Factory code:

public function create()
{
    return new MagentoComposerApplication($this->pathToComposerHome, $this->pathToComposerJson);
}
public function __construct(
    $pathToComposerHome,
    $pathToComposerJson,
    Application $consoleApplication = null,
    ConsoleArrayInputFactory $consoleArrayInputFactory = null,
    BufferedOutput $consoleOutput = null
)

When this is null, a new BufferedOutput() is made. This BufferedOutput collects all the strings that are outputted during the composer actions, but not output it to STDOUT. This effectively hides all the actions behind a curtain of mystery. When the command is finished running it returns all collected input.

return $this->consoleOutput->fetch();

If you would type gibberish and press enter a few times you will see the actual error output, and see that it tries to request the access keys to repo.magento.com, as the output is then outputted by an Exception handler instead of the void.

Loading composer repositories with package information
"Warning from repo.magento.com: You haven't provided your Magento authentication keys. For instructions, visit https://devdocs.magento.com/guides/v2.3/install-gde/prereq/connect-auth.html
    Authentication required (repo.magento.com):
      Username:
      Password:
Warning from repo.magento.com: Your Magento authentication keys are invalid. Please double-check your keys in your Marketplace account. For instructions, visit https://devdocs.magento.com/guides/v2.3/install-gde/prereq/connect-auth.html
Warning from repo.magento.com: Your Magento authentication keys are invalid. Please double-check your keys in your Marketplace account. For instructions, visit https://devdocs.magento.com/guides/v2.3/install-gde/prereq/connect-auth.html
Warning from repo.magento.com: Your Magento authentication keys are invalid. Please double-check your keys in your Marketplace account. For instructions, visit https://devdocs.magento.com/guides/v2.3/install-gde/prereq/connect-auth.html

In RemoteFilesystem.php line 748:

  Invalid credentials for 'https://repo.magento.com/packages.json, aborting.

The solution, and a tip

To resolve this, copy auth.json.sample in your magento web root to auth.json in your webroot and enter in that file your public and private keys for repo.magento.com and any other access keys you might need. You can usually just blanket copy in the contents of your ~/.composer/auth.json if you only do magento development.

Make sure you add an auth.json entry in your.gitignore

You do not want your private access keys ending up on Github or Bitbucket or something. Just include auth.json on the webserver that will host Magento manually if needed there.

After that, if you run the command again, it should run properly, after a wait of roughly 30 seconds whilst it does it's magic.

<?phpnamespaceTest\Dummy\Setup;
useMagento\Catalog\Model\Product;
useMagento\Eav\Setup\EavSetupFactory;
useMagento\Framework\Setup\ModuleContextInterface;
useMagento\Framework\Setup\SchemaSetupInterface;
useMagento\Framework\Setup\UninstallInterface;
useSymfony\Component\Console\Output\ConsoleOutput;
class Uninstallimplements UninstallInterface
{
    protected $eavSetupFactory;
    protected $output;
    public function __construct(EavSetupFactory $eavSetupFactory, ConsoleOutput $output)
    {
        $this->eavSetupFactory = $eavSetupFactory;
        $this->output = $output;
    }
    /**
    *@inheritDoc
    */
    public function uninstall(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        // Sanity check
        $auth = BP.DIRECTORY_SEPARATOR.'auth.json';
        if(!file_exists($auth)) {
            $this->output->writeln("<fg=red>NO AUTH.JSON FOUND IN\"$auth\"</>");
            $this->output->writeln("<fg=yellow>In the event that this process hang indefinitely on 'Removing code from Magento CodeBase:'</>");
            $this->output->writeln("<fg=yellow>Create in your Magento root folder a auth.json copied from auth.json.sample. </>");
            $this->output->writeln("<fg=yellow>Enter in that file your repo.magento.com and other access keys. </>");
            $this->output->writeln("<fg=red>Do not forget to add auth.json to your .gitignore to prevent leaking your access keys!!!</>");
        }
        // Uninstall code
        $eavSetup = $this->eavSetupFactory->create();
        $this->output->writeln("Starting to remove the attributes for something.");
        $eavSetup->removeAttribute(Product::ENTITY, UpgradeData::SOME_ATTRIBUTE);
    }
}