diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00208291..f5cd54f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,12 +65,15 @@ Thanks to the isolation we're less likely to run into problem with conflicting d "command": { "composer-bin-plugin": { "package": "behat/behat", - "namespace": "behat" + "namespace": "behat", + "links": {"/tools/behat": "behat"} } } } ``` +The `links` attribute is optional, but it's recommended for packages that provide commands. + #### phar-download Downloads a phar from the given URL and puts it into the specified location. diff --git a/Makefile b/Makefile index 1512c632..1e2c982f 100644 --- a/Makefile +++ b/Makefile @@ -140,7 +140,7 @@ tools/php-cs-fixer: curl -Ls http://cs.sensiolabs.org/download/php-cs-fixer-v2.phar -o tools/php-cs-fixer && chmod +x tools/php-cs-fixer tools/deptrac: - curl -Ls https://github.com/sensiolabs-de/deptrac/releases/download/0.4.0/deptrac.phar -o tools/deptrac && chmod +x tools/deptrac + curl -Ls https://github.com/sensiolabs-de/deptrac/releases/download/0.5.0/deptrac.phar -o tools/deptrac && chmod +x tools/deptrac tools/infection: tools/infection.pubkey curl -Ls https://github.com/infection/infection/releases/download/0.10.6/infection.phar -o tools/infection && chmod +x tools/infection diff --git a/resources/pre-installation.json b/resources/pre-installation.json index 08db6bb2..9f5a0480 100644 --- a/resources/pre-installation.json +++ b/resources/pre-installation.json @@ -19,6 +19,9 @@ "command": { "composer-global-install": { "package": "bamarni/composer-bin-plugin" + }, + "sh": { + "command": "composer global config extra.bamarni-bin.bin-links false" } }, "test": "composer global show bamarni/composer-bin-plugin", @@ -37,4 +40,4 @@ "tags": ["pre-installation"] } ] -} \ No newline at end of file +} diff --git a/resources/tools.json b/resources/tools.json index e4c4771f..6257a5f1 100644 --- a/resources/tools.json +++ b/resources/tools.json @@ -33,7 +33,8 @@ "command": { "composer-bin-plugin": { "package": "bmitch/churn-php", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/churn": "churn"} } }, "test": "churn list", @@ -106,7 +107,8 @@ "command": { "composer-bin-plugin": { "package": "exussum12/coverage-checker", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/diffFilter": "diffFilter"} } }, "test": "diffFilter -v" @@ -160,7 +162,8 @@ "command": { "composer-bin-plugin": { "package": "brianium/paratest", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/paratest": "paratest"} } }, "test": "paratest --version" @@ -198,7 +201,8 @@ "command": { "composer-bin-plugin": { "package": "akeneo/php-coupling-detector", - "namespace": "php-coupling-detector" + "namespace": "php-coupling-detector", + "links": {"%target-dir%/php-coupling-detector": "php-coupling-detector"} } }, "test": "php-coupling-detector list" @@ -278,7 +282,8 @@ "command": { "composer-bin-plugin": { "package": "rskuipers/php-assumptions", - "namespace": "php-assumptions" + "namespace": "php-assumptions", + "links": {"%target-dir%/phpa": "phpa"} } }, "test": "phpa --version", @@ -291,7 +296,8 @@ "command": { "composer-bin-plugin": { "package": "wapmorgan/php-code-analyzer", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/phpca": "phpca"} } }, "test": "phpca -h" @@ -327,7 +333,8 @@ "command": { "composer-bin-plugin": { "package": "wapmorgan/php-code-fixer", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/phpcf": "phpcf"} } }, "test": "phpcf -h" @@ -405,7 +412,8 @@ "command": { "composer-bin-plugin": { "package": "overtrue/phplint", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/phplint": "phplint"} } }, "test": "phplint -V" @@ -454,7 +462,8 @@ "command": { "composer-bin-plugin": { "package": "povils/phpmnd", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/phpmnd": "phpmnd"} } }, "test": "phpmnd -V" @@ -479,7 +488,8 @@ "command": { "composer-bin-plugin": { "package": "phpstan/phpstan", - "namespace": "phpstan" + "namespace": "phpstan", + "links": {"%target-dir%/phpstan": "phpstan"} } }, "test": "phpstan --version", @@ -642,7 +652,8 @@ "command": { "composer-bin-plugin": { "package": "psecio/parse:dev-master", - "namespace": "psecio-parse" + "namespace": "psecio-parse", + "links": {"%target-dir%/psecio-parse": "psecio-parse"} } }, "test": "psecio-parse --version" @@ -654,7 +665,8 @@ "command": { "composer-bin-plugin": { "package": "rector/rector", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/rector": "rector"} } }, "test": "rector --version" @@ -692,7 +704,8 @@ "command": { "composer-bin-plugin": { "package": "symfony/phpunit-bridge", - "namespace": "symfony" + "namespace": "symfony", + "links": {"%target-dir%/simple-phpunit": "simple-phpunit"} }, "sh": { "command": "simple-phpunit install" @@ -707,7 +720,8 @@ "command": { "composer-bin-plugin": { "package": "edsonmedina/php_testability:dev-master", - "namespace": "tools" + "namespace": "tools", + "links": {"%target-dir%/testability": "testability"} } }, "test": "testability --help" diff --git a/src/Json/Factory/ComposerBinPluginCommandFactory.php b/src/Json/Factory/ComposerBinPluginCommandFactory.php index 54b2ef61..32d5312f 100644 --- a/src/Json/Factory/ComposerBinPluginCommandFactory.php +++ b/src/Json/Factory/ComposerBinPluginCommandFactory.php @@ -2,8 +2,10 @@ namespace Zalas\Toolbox\Json\Factory; +use Zalas\Toolbox\Tool\Collection; use Zalas\Toolbox\Tool\Command; use Zalas\Toolbox\Tool\Command\ComposerBinPluginCommand; +use Zalas\Toolbox\Tool\Command\ComposerBinPluginLinkCommand; final class ComposerBinPluginCommandFactory { @@ -11,6 +13,18 @@ public static function import(array $command): Command { Assert::requireFields(['package', 'namespace'], $command, 'ComposerBinPluginCommand'); - return new ComposerBinPluginCommand($command['package'], $command['namespace']); + return new ComposerBinPluginCommand($command['package'], $command['namespace'], self::importLinks($command)); + } + + private static function importLinks(array $command): Collection + { + $links = $command['links'] ?? []; + $namespace = $command['namespace']; + + return Collection::create( + \array_map(function (string $source, string $target) use ($namespace) { + return new ComposerBinPluginLinkCommand($source, $target, $namespace); + }, \array_values($links), \array_keys($links)) + ); } } diff --git a/src/Tool/Command/ComposerBinPluginCommand.php b/src/Tool/Command/ComposerBinPluginCommand.php index ca45def5..01145199 100644 --- a/src/Tool/Command/ComposerBinPluginCommand.php +++ b/src/Tool/Command/ComposerBinPluginCommand.php @@ -2,22 +2,27 @@ namespace Zalas\Toolbox\Tool\Command; +use Zalas\Toolbox\Tool\Collection; use Zalas\Toolbox\Tool\Command; final class ComposerBinPluginCommand implements Command { private $package; + private $namespace; - public function __construct(string $package, string $namespace) + private $links; + + public function __construct(string $package, string $namespace, Collection $links) { $this->package = $package; $this->namespace = $namespace; + $this->links = $links; } public function __toString(): string { - return \sprintf('composer global bin %s require --no-suggest --prefer-dist --update-no-dev -n %s', $this->namespace, $this->package); + return \sprintf('composer global bin %s require --no-suggest --prefer-dist --update-no-dev -n %s%s', $this->namespace, $this->package, $this->linkCommand()); } public function package(): string @@ -29,4 +34,16 @@ public function namespace(): string { return $this->namespace; } + + public function links(): Collection + { + return $this->links; + } + + private function linkCommand(): string + { + return $this->links->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) { + return $command.' && '.$link; + }); + } } diff --git a/src/Tool/Command/ComposerBinPluginLinkCommand.php b/src/Tool/Command/ComposerBinPluginLinkCommand.php new file mode 100644 index 00000000..cb2cd99e --- /dev/null +++ b/src/Tool/Command/ComposerBinPluginLinkCommand.php @@ -0,0 +1,29 @@ +source = $source; + $this->target = $target; + $this->namespace = $namespace; + } + + public function __toString(): string + { + return \sprintf( + 'ln -sf ${COMPOSER_HOME:-"~/.composer"}/vendor-bin/%s/vendor/bin/%s %s', + $this->namespace, + $this->source, + $this->target + ); + } +} diff --git a/src/Tool/Command/OptimisedComposerBinPluginCommand.php b/src/Tool/Command/OptimisedComposerBinPluginCommand.php index 40c4d1a4..29ca3011 100644 --- a/src/Tool/Command/OptimisedComposerBinPluginCommand.php +++ b/src/Tool/Command/OptimisedComposerBinPluginCommand.php @@ -23,7 +23,7 @@ public function __construct(Collection $commands) public function __toString(): string { - return \implode(' && ', $this->commandsToRun($this->packagesGroupedByNamespace())); + return \implode(' && ', \array_merge($this->commandsToRun($this->packagesGroupedByNamespace()), $this->linksToCreate())); } private function packagesGroupedByNamespace(): array @@ -44,4 +44,18 @@ private function commandsToRun(array $packagesGrouped): array { return \array_map([$this, 'commandToRun'], \array_keys($packagesGrouped), $packagesGrouped); } + + private function linksToCreate(): array + { + return $this->commands + ->filter(function (ComposerBinPluginCommand $command) { + return !$command->links()->empty(); + }) + ->map(function (ComposerBinPluginCommand $command) { + return $command->links()->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) { + return !empty($command) ? $command.' && '.$link : $link; + }); + }) + ->toArray(); + } } diff --git a/tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php b/tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php index 221434fb..4d9d52b2 100644 --- a/tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php +++ b/tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php @@ -4,7 +4,9 @@ use PHPUnit\Framework\TestCase; use Zalas\Toolbox\Json\Factory\ComposerBinPluginCommandFactory; +use Zalas\Toolbox\Tool\Collection; use Zalas\Toolbox\Tool\Command\ComposerBinPluginCommand; +use Zalas\Toolbox\Tool\Command\ComposerBinPluginLinkCommand; class ComposerBinPluginCommandFactoryTest extends TestCase { @@ -21,6 +23,23 @@ public function test_it_creates_a_command() $this->assertInstanceOf(ComposerBinPluginCommand::class, $command); } + public function test_it_creates_a_command_with_links_in_tools() + { + $command = ComposerBinPluginCommandFactory::import([ + 'package' => self::PACKAGE, + 'namespace' => self::NAMESPACE, + 'links' => ['/tools/phpstan' => 'phpstan'], + ]); + + $this->assertInstanceOf(ComposerBinPluginCommand::class, $command); + $this->assertEquals( + Collection::create([ + new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', self::NAMESPACE) + ]), + $command->links() + ); + } + /** * @dataProvider provideRequiredProperties */ diff --git a/tests/Tool/Command/ComposerBinPluginCommandTest.php b/tests/Tool/Command/ComposerBinPluginCommandTest.php index 05d3fef1..b25ac947 100644 --- a/tests/Tool/Command/ComposerBinPluginCommandTest.php +++ b/tests/Tool/Command/ComposerBinPluginCommandTest.php @@ -3,8 +3,10 @@ namespace Zalas\Toolbox\Tests\Tool\Command; use PHPUnit\Framework\TestCase; +use Zalas\Toolbox\Tool\Collection; use Zalas\Toolbox\Tool\Command; use Zalas\Toolbox\Tool\Command\ComposerBinPluginCommand; +use Zalas\Toolbox\Tool\Command\ComposerBinPluginLinkCommand; class ComposerBinPluginCommandTest extends TestCase { @@ -18,7 +20,11 @@ class ComposerBinPluginCommandTest extends TestCase protected function setUp() { - $this->command = new ComposerBinPluginCommand(self::PACKAGE, self::NAMESPACE); + $this->command = new ComposerBinPluginCommand( + self::PACKAGE, + self::NAMESPACE, + Collection::create([]) + ); } public function test_it_is_a_command() @@ -36,4 +42,21 @@ public function test_it_exposes_the_package_and_namespace() $this->assertSame(self::PACKAGE, $this->command->package()); $this->assertSame(self::NAMESPACE, $this->command->namespace()); } + + public function test_it_optionally_creates_a_symlink() + { + $links = Collection::create([ + new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', self::NAMESPACE) + ]); + $this->command = new ComposerBinPluginCommand(self::PACKAGE, self::NAMESPACE, $links); + + $this->assertSame($links, $this->command->links()); + $this->assertRegExp('#composer global bin tools require .*? phpstan/phpstan#', (string) $this->command); + $this->assertRegExp('# && ln -sf.*?phpstan /tools/phpstan#', (string) $this->command); + } + + public function test_it_does_not_create_a_symlink_if_links_option_was_not_given() + { + $this->assertNotRegExp('#ln -s#', (string) $this->command); + } } diff --git a/tests/Tool/Command/ComposerBinPluginLinkCommandTest.php b/tests/Tool/Command/ComposerBinPluginLinkCommandTest.php new file mode 100644 index 00000000..b50c2b83 --- /dev/null +++ b/tests/Tool/Command/ComposerBinPluginLinkCommandTest.php @@ -0,0 +1,34 @@ +command = new ComposerBinPluginLinkCommand(self::SOURCE, self::TARGET, self::NAMESPACE); + } + + public function test_it_is_a_command() + { + $this->assertInstanceOf(Command::class, $this->command); + } + + public function test_it_generates_a_symlink_command() + { + $this->assertRegExp('#ln -sf \$\{COMPOSER_HOME:-"~/.composer"\}/vendor-bin/tools/vendor/bin/churn /tools/churn#', (string) $this->command); + } +} diff --git a/tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php b/tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php index 1957f25b..705e4e4e 100644 --- a/tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php +++ b/tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php @@ -7,21 +7,22 @@ use Zalas\Toolbox\Tool\Collection; use Zalas\Toolbox\Tool\Command; use Zalas\Toolbox\Tool\Command\ComposerBinPluginCommand; +use Zalas\Toolbox\Tool\Command\ComposerBinPluginLinkCommand; use Zalas\Toolbox\Tool\Command\OptimisedComposerBinPluginCommand; class OptimisedComposerBinPluginCommandTest extends TestCase { public function test_it_is_a_command() { - $this->assertInstanceOf(Command::class, new OptimisedComposerBinPluginCommand(Collection::create([new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan')]))); + $this->assertInstanceOf(Command::class, new OptimisedComposerBinPluginCommand(Collection::create([new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([]))]))); } public function test_it_groups_composer_bin_command_by_namespace() { $commands = [ - new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan'), - new ComposerBinPluginCommand('phan/phan', 'tools'), - new ComposerBinPluginCommand('behat/behat', 'tools'), + new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([])), + new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([])), + new ComposerBinPluginCommand('behat/behat', 'tools', Collection::create([])), ]; $command = new OptimisedComposerBinPluginCommand(Collection::create($commands)); @@ -35,4 +36,55 @@ public function test_it_throws_an_exception_if_there_is_no_commands() new OptimisedComposerBinPluginCommand(Collection::create([])); } + + public function test_it_creates_links_to_composer_bin_commands() + { + $commands = [ + new ComposerBinPluginCommand( + 'phpstan/phpstan', + 'phpstan', + Collection::create([ + new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', 'phpstan'), + new ComposerBinPluginLinkCommand('phpstan', '/other/path/phpstan', 'phpstan'), + ]) + ), + new ComposerBinPluginCommand( + 'phan/phan', + 'tools', + Collection::create([ + new ComposerBinPluginLinkCommand('phan', '/tools/phan', 'tools'), + ]) + ), + new ComposerBinPluginCommand( + 'behat/behat', + 'tools', + Collection::create([ + new ComposerBinPluginLinkCommand('behat', '/tools/behat', 'tools'), + ]) + ), + ]; + + $command = new OptimisedComposerBinPluginCommand(Collection::create($commands)); + + $this->assertRegExp('#composer global bin phpstan require .*? phpstan/phpstan && composer global bin tools require .*? phan/phan behat/behat#', (string) $command); + $this->assertRegExp('# && ln -sf.*?phpstan /tools/phpstan#', (string) $command); + $this->assertRegExp('# && ln -sf.*?phpstan /other/path/phpstan#', (string) $command); + $this->assertRegExp('# && ln -sf.*?phan /tools/phan#', (string) $command); + $this->assertRegExp('# && ln -sf.*?behat /tools/behat#', (string) $command); + $this->assertNotRegExp('#&&\s*&&#', (string) $command, 'It does not generate empty commands'); + } + + public function test_it_does_not_create_links_if_commands_have_no_links_defined() + { + $commands = [ + new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([])), + new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([])), + new ComposerBinPluginCommand('behat/behat', 'tools', Collection::create([])), + ]; + + $command = new OptimisedComposerBinPluginCommand(Collection::create($commands)); + + $this->assertNotRegExp('#ln -s#', (string) $command); + $this->assertNotRegExp('#&&\s*&&#', (string) $command, 'It does not generate empty commands'); + } } diff --git a/tests/UseCase/InstallToolsTest.php b/tests/UseCase/InstallToolsTest.php index 7e15f079..10228bcf 100644 --- a/tests/UseCase/InstallToolsTest.php +++ b/tests/UseCase/InstallToolsTest.php @@ -75,8 +75,8 @@ public function test_it_does_not_include_empty_commands() public function test_it_groups_composer_bin_plugin_commands() { $this->tools->all(Argument::type(Filter::class))->willReturn(Collection::create([ - $this->tool(new ComposerBinPluginCommand('phpstan/phpstan', 'tools')), - $this->tool(new ComposerBinPluginCommand('phan/phan', 'tools')), + $this->tool(new ComposerBinPluginCommand('phpstan/phpstan', 'tools', Collection::create([]))), + $this->tool(new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([]))), ])); $command = $this->useCase->__invoke($this->filter());