applicationFactory = $applicationFactory ?? new FreshInstanceApplicationFactory(); $this->logger = $logger ?? new Logger(new NullIO()); } protected function configure(): void { $this ->setDescription('Run a command inside a bin namespace') ->addArgument( self::NAMESPACE_ARG, InputArgument::REQUIRED ) ->ignoreValidationErrors(); } public function setIO(IOInterface $io): void { parent::setIO($io); $this->logger = new Logger($io); } public function getIO(): IOInterface { $io = parent::getIO(); $this->logger = new Logger($io); return $io; } public function isProxyCommand(): bool { return true; } public function execute(InputInterface $input, OutputInterface $output): int { // Switch to requireComposer() once Composer 2.3 is set as the minimum $composer = method_exists($this, 'requireComposer') ? $this->requireComposer() : $this->getComposer(); $config = Config::fromComposer($composer); $currentWorkingDir = getcwd(); $this->logger->logDebug( sprintf( 'Current working directory: %s', $currentWorkingDir ) ); // Ensures Composer is reset – we are setting some environment variables // & co. so a fresh Composer instance is required. $this->resetComposers(); $this->configureBinLinksDir($config); $vendorRoot = $config->getTargetDirectory(); $namespace = $input->getArgument(self::NAMESPACE_ARG); $binInput = BinInputFactory::createInput( $namespace, $input ); return (self::ALL_NAMESPACES !== $namespace) ? $this->executeInNamespace( $currentWorkingDir, $vendorRoot.'/'.$namespace, $binInput, $output ) : $this->executeAllNamespaces( $currentWorkingDir, $vendorRoot, $binInput, $output ); } /** * @return list */ private static function getBinNamespaces(string $binVendorRoot): array { return glob($binVendorRoot.'/*', GLOB_ONLYDIR); } private function executeAllNamespaces( string $originalWorkingDir, string $binVendorRoot, InputInterface $input, OutputInterface $output ): int { $namespaces = self::getBinNamespaces($binVendorRoot); if (count($namespaces) === 0) { $this->logger->logStandard('Could not find any bin namespace.'); // Is a valid scenario: the user may not have set up any bin // namespace yet return 0; } $exitCode = 0; foreach ($namespaces as $namespace) { $exitCode += $this->executeInNamespace( $originalWorkingDir, $namespace, $input, $output ); } return min($exitCode, 255); } private function executeInNamespace( string $originalWorkingDir, string $namespace, InputInterface $input, OutputInterface $output ): int { $this->logger->logStandard( sprintf( 'Checking namespace %s', $namespace ) ); try { self::createNamespaceDirIfDoesNotExist($namespace); } catch (CouldNotCreateNamespaceDir $exception) { $this->logger->logStandard( sprintf( '%s', $exception->getMessage() ) ); return 1; } // Use a new application: this avoids a variety of issues: // - A command may be added in a namespace which may cause side effects // when executed in another namespace afterwards (since it is the same // process). // - Different plugins may be registered in the namespace in which case // an already executed application will not pick that up. $namespaceApplication = $this->applicationFactory->create( $this->getApplication() ); // It is important to clean up the state either for follow-up plugins // or for example the execution in the next namespace. $cleanUp = function () use ($originalWorkingDir): void { $this->chdir($originalWorkingDir); $this->resetComposers(); }; $this->chdir($namespace); $this->ensureComposerFileExists(); $namespaceInput = BinInputFactory::createNamespaceInput($input); $this->logger->logDebug( sprintf( 'Running `@composer %s`.', $namespaceInput->__toString() ) ); try { $exitCode = $namespaceApplication->doRun($namespaceInput, $output); } catch (Throwable $executionFailed) { // Ensure we do the cleanup even in case of failure $cleanUp(); throw $executionFailed; } $cleanUp(); return $exitCode; } /** * @throws CouldNotCreateNamespaceDir */ private static function createNamespaceDirIfDoesNotExist(string $namespace): void { if (file_exists($namespace)) { return; } $mkdirResult = mkdir($namespace, 0777, true); if (!$mkdirResult && !is_dir($namespace)) { throw CouldNotCreateNamespaceDir::forNamespace($namespace); } } private function configureBinLinksDir(Config $config): void { if (!$config->binLinksAreEnabled()) { return; } $binDir = ConfigFactory::createConfig()->get('bin-dir'); putenv( sprintf( 'COMPOSER_BIN_DIR=%s', $binDir ) ); $this->logger->logDebug( sprintf( 'Configuring bin directory to %s.', $binDir ) ); } private function ensureComposerFileExists(): void { // Some plugins require access to the Composer file e.g. Symfony Flex $namespaceComposerFile = Factory::getComposerFile(); if (file_exists($namespaceComposerFile)) { return; } file_put_contents($namespaceComposerFile, '{}'); $this->logger->logDebug( sprintf( 'Created the file %s.', $namespaceComposerFile ) ); } private function resetComposers(): void { $this->getApplication()->resetComposer(); foreach ($this->getApplication()->all() as $command) { if ($command instanceof BaseCommand) { $command->resetComposer(); } } } private function chdir(string $dir): void { chdir($dir); $this->logger->logDebug( sprintf( 'Changed current directory to %s.', $dir ) ); } }