Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions e2e/scenario14/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Check that `composer bin root` executes commands in root without forwarding.
22 changes: 22 additions & 0 deletions e2e/scenario14/composer.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
8 changes: 8 additions & 0 deletions e2e/scenario14/expected.txt
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions e2e/scenario14/script.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions e2e/scenario14/vendor-bin/ns1/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
14 changes: 11 additions & 3 deletions src/BamarniBinPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
36 changes: 32 additions & 4 deletions src/Command/BinCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,6 +41,7 @@
class BinCommand extends BaseCommand
{
private const ALL_NAMESPACES = 'all';
private const ROOT_NAMESPACE = 'root';

private const NAMESPACE_ARG = 'namespace';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
};
Expand Down
27 changes: 27 additions & 0 deletions src/CommandForwardingContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Bamarni\Composer\Bin;

final class CommandForwardingContext
{
/**
* @var bool
*/
private static $commandForwardingDisabled = false;

public static function setCommandForwardingDisabled(bool $value): void
{
self::$commandForwardingDisabled = $value;
}

public static function isCommandForwardingDisabled(): bool
{
return self::$commandForwardingDisabled;
}

private function __construct()
{
}
}
120 changes: 120 additions & 0 deletions tests/BamarniBinPluginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

namespace Bamarni\Composer\Bin\Tests;

use Bamarni\Composer\Bin\BamarniBinPlugin;
use Bamarni\Composer\Bin\CommandForwardingContext;
use Bamarni\Composer\Bin\Config\Config;
use Composer\Composer;
use Composer\IO\ConsoleIO;
use Composer\Package\PackageInterface;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;

/**
* @covers \Bamarni\Composer\Bin\BamarniBinPlugin
*/
final class BamarniBinPluginTest extends TestCase
{
protected function tearDown(): void
{
CommandForwardingContext::setCommandForwardingDisabled(false);
}

public function test_it_does_not_forward_commands_when_the_context_disables_forwarding(): void
{
CommandForwardingContext::setCommandForwardingDisabled(true);

$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(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;
}
}
20 changes: 20 additions & 0 deletions tests/Command/BinCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'];
Expand Down