diff --git a/README.md b/README.md index 1950951..ff6da6d 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,13 @@ You also have a special `all` namespace to interact with all the bin namespaces: $ composer bin all update ``` +You can use the `root` namespace to run a command from the project root +without forwarding it to bin namespaces: + +```bash +$ composer bin root update +``` + ## Installation @@ -146,6 +153,10 @@ to _all_ bin directories. This is a replacement for the tasks shown in section [Auto-installation](#auto-installation). +If you need to skip forwarding for a single command invocation, run the command +via `composer bin root ...` to force +execution in the root project without forwarding. + ## Tips & Tricks diff --git a/e2e/scenario14/README.md b/e2e/scenario14/README.md new file mode 100644 index 0000000..80f0e1c --- /dev/null +++ b/e2e/scenario14/README.md @@ -0,0 +1 @@ +Check that `composer bin root` executes commands in root without forwarding. diff --git a/e2e/scenario14/composer.json b/e2e/scenario14/composer.json new file mode 100644 index 0000000..a1cc737 --- /dev/null +++ b/e2e/scenario14/composer.json @@ -0,0 +1,22 @@ +{ + "repositories": [ + { + "type": "path", + "url": "../../" + } + ], + "require-dev": { + "bamarni/composer-bin-plugin": "dev-master" + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } + }, + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": true + } + } +} diff --git a/e2e/scenario14/expected.txt b/e2e/scenario14/expected.txt new file mode 100644 index 0000000..6608447 --- /dev/null +++ b/e2e/scenario14/expected.txt @@ -0,0 +1,8 @@ +[bamarni-bin] Checking namespace /path/to/project/e2e/scenario14 +Loading composer repositories with package information +Updating dependencies +Nothing to modify in lock file +Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove +Generating autoload files diff --git a/e2e/scenario14/script.sh b/e2e/scenario14/script.sh new file mode 100755 index 0000000..c0dd928 --- /dev/null +++ b/e2e/scenario14/script.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Set env variables in order to experience a behaviour closer to what happens +# in the CI locally. It should not hurt to set those in the CI as the CI should +# contain those values. +export CI=1 +export COMPOSER_NO_INTERACTION=1 + +readonly ORIGINAL_WORKING_DIR=$(pwd) + +trap "cd ${ORIGINAL_WORKING_DIR}" err exit + +# Change to script directory +cd "$(dirname "$0")" + +# Ensure we have a clean state +rm -rf actual.txt || true +rm -rf composer.lock || true +rm -rf vendor || true +rm -rf vendor-bin/*/composer.lock || true +rm -rf vendor-bin/*/vendor || true + +# Install the plugin once. +composer update --no-audit > /dev/null + +# Actual command to execute the test itself +composer bin root update --no-audit 2>&1 | tee > actual.txt diff --git a/e2e/scenario14/vendor-bin/ns1/composer.json b/e2e/scenario14/vendor-bin/ns1/composer.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/e2e/scenario14/vendor-bin/ns1/composer.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/BamarniBinPlugin.php b/src/BamarniBinPlugin.php index 19e36e0..6e388cc 100644 --- a/src/BamarniBinPlugin.php +++ b/src/BamarniBinPlugin.php @@ -131,9 +131,17 @@ private function onEvent( } } - if ($config->isCommandForwarded() - && in_array($commandName, self::FORWARDED_COMMANDS, true) - ) { + if (!in_array($commandName, self::FORWARDED_COMMANDS, true)) { + return true; + } + + if (CommandForwardingContext::isCommandForwardingDisabled()) { + $this->logger->logDebug('Command forwarding is disabled in this process context.'); + + return true; + } + + if ($config->isCommandForwarded()) { return $this->onForwardedCommand($input, $output); } diff --git a/src/Command/BinCommand.php b/src/Command/BinCommand.php index 69c4aa9..30119e1 100644 --- a/src/Command/BinCommand.php +++ b/src/Command/BinCommand.php @@ -8,6 +8,7 @@ use Bamarni\Composer\Bin\Config\ConfigFactory; use Bamarni\Composer\Bin\ApplicationFactory\FreshInstanceApplicationFactory; use Bamarni\Composer\Bin\Input\BinInputFactory; +use Bamarni\Composer\Bin\CommandForwardingContext; use Bamarni\Composer\Bin\Logger; use Bamarni\Composer\Bin\ApplicationFactory\NamespaceApplicationFactory; use Composer\Command\BaseCommand; @@ -40,6 +41,7 @@ class BinCommand extends BaseCommand { private const ALL_NAMESPACES = 'all'; + private const ROOT_NAMESPACE = 'root'; private const NAMESPACE_ARG = 'namespace'; @@ -127,14 +129,23 @@ public function execute(InputInterface $input, OutputInterface $output): int $input ); + if (self::ROOT_NAMESPACE === $namespace) { + return $this->executeInNamespace( + $currentWorkingDir, + $currentWorkingDir, + $binInput, + $output, + true + ); + } + return (self::ALL_NAMESPACES !== $namespace) ? $this->executeInNamespace( $currentWorkingDir, $vendorRoot.'/'.$namespace, $binInput, $output - ) - : $this->executeAllNamespaces( + ) : $this->executeAllNamespaces( $currentWorkingDir, $vendorRoot, $binInput, @@ -184,7 +195,8 @@ private function executeInNamespace( string $originalWorkingDir, string $namespace, InputInterface $input, - OutputInterface $output + OutputInterface $output, + bool $disableCommandForwarding = false ): int { $this->logger->logStandard( sprintf( @@ -216,9 +228,25 @@ private function executeInNamespace( $this->getApplication() ); + $previousCommandForwardingDisabled = CommandForwardingContext::isCommandForwardingDisabled(); + + if ($disableCommandForwarding) { + CommandForwardingContext::setCommandForwardingDisabled(true); + } + // 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 { + $cleanUp = function () use ( + $originalWorkingDir, + $disableCommandForwarding, + $previousCommandForwardingDisabled + ): void { + if ($disableCommandForwarding) { + CommandForwardingContext::setCommandForwardingDisabled( + $previousCommandForwardingDisabled + ); + } + $this->chdir($originalWorkingDir); $this->resetComposers(); }; diff --git a/src/CommandForwardingContext.php b/src/CommandForwardingContext.php new file mode 100644 index 0000000..d1dfdef --- /dev/null +++ b/src/CommandForwardingContext.php @@ -0,0 +1,27 @@ +activate( + $this->createComposer(true), + self::createConsoleIO($input) + ); + + $event = new CommandEvent( + PluginEvents::COMMAND, + 'update', + $input, + new NullOutput() + ); + + self::assertTrue($plugin->onCommandEvent($event)); + self::assertSame(0, $plugin->forwardedCommandCount); + } + + public function test_it_keeps_forwarding_commands_when_the_context_allows_forwarding(): void + { + $input = new ArgvInput(['composer', 'update']); + + $plugin = new BamarniBinPluginSpy(); + $plugin->activate( + $this->createComposer(true), + self::createConsoleIO($input) + ); + + $event = new CommandEvent( + PluginEvents::COMMAND, + 'update', + $input, + new NullOutput() + ); + + self::assertTrue($plugin->onCommandEvent($event)); + self::assertSame(1, $plugin->forwardedCommandCount); + } + + private static function createConsoleIO(InputInterface $input): ConsoleIO + { + return new ConsoleIO( + $input, + new NullOutput(), + new HelperSet() + ); + } + + private function createComposer(bool $forwardCommand): Composer + { + $package = $this->createMock(PackageInterface::class); + $package + ->method('getExtra') + ->willReturn([ + Config::EXTRA_CONFIG_KEY => [ + Config::BIN_LINKS_ENABLED => false, + Config::FORWARD_COMMAND => $forwardCommand, + Config::TARGET_DIRECTORY => 'vendor-bin', + ], + ]); + + $composer = $this->createMock(Composer::class); + $composer->method('getPackage')->willReturn($package); + + return $composer; + } +} + +final class BamarniBinPluginSpy extends BamarniBinPlugin +{ + /** + * @var int + */ + public $forwardedCommandCount = 0; + + protected function onForwardedCommand( + InputInterface $input, + OutputInterface $output + ): bool { + ++$this->forwardedCommandCount; + + return true; + } +} diff --git a/tests/Command/BinCommandTest.php b/tests/Command/BinCommandTest.php index b47f8ae..d1d193d 100644 --- a/tests/Command/BinCommandTest.php +++ b/tests/Command/BinCommandTest.php @@ -4,6 +4,7 @@ namespace Bamarni\Composer\Bin\Tests\Command; +use Bamarni\Composer\Bin\CommandForwardingContext; use Bamarni\Composer\Bin\Command\BinCommand; use Bamarni\Composer\Bin\Tests\Fixtures\MyTestCommand; use Bamarni\Composer\Bin\Tests\Fixtures\ReuseApplicationFactory; @@ -126,6 +127,25 @@ public function test_the_all_namespace_can_be_called(): void $this->assertNoMoreDataFound(); } + public function test_the_root_namespace_can_be_called(): void + { + self::assertFalse(CommandForwardingContext::isCommandForwardingDisabled()); + + $input = new StringInput('bin root mytest'); + $output = new NullOutput(); + + $this->application->doRun($input, $output); + + $this->assertHasAccessToComposer(); + $this->assertDataSetRecordedIs( + $this->tmpDir.'/vendor/bin', + $this->tmpDir, + $this->tmpDir.'/vendor' + ); + $this->assertNoMoreDataFound(); + self::assertFalse(CommandForwardingContext::isCommandForwardingDisabled()); + } + public function test_a_command_can_be_executed_in_each_namespace_via_the_all_namespace(): void { $namespaces = ['namespace1', 'namespace2'];