Skip to content
Merged
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ For personal configs `link` must be defined
}
```

### 2. Refresh symlinks

Symlinks are created automatically on `composer install`/`update`, but you can
trigger the process manually with the built-in command:

```bash
$ composer symlinks:refresh
```

Add the `--dry-run` flag to preview the operations without touching the
filesystem:

```bash
$ composer symlinks:refresh --dry-run
```

The legacy environment variable `SYMLINKS_DRY_RUN=1` is still honoured during
Composer hooks for backwards compatibility.

### 3. Execute composer

DO NOT use --no-plugins for composer install or update
Expand Down
16 changes: 16 additions & 0 deletions src/Symlinks/Command/CommandProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);

namespace SomeWork\Symlinks\Command;

use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;

class CommandProvider implements CommandProviderCapability
{
public function getCommands(): array
{
return [
new RefreshCommand(),
];
}
}
45 changes: 45 additions & 0 deletions src/Symlinks/Command/RefreshCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);

namespace SomeWork\Symlinks\Command;

use Composer\Command\BaseCommand;
use Composer\Script\Event;
use Composer\Util\Filesystem;
use SomeWork\Symlinks\SymlinksExecutionTrait;
use SomeWork\Symlinks\SymlinksFactory;
use SomeWork\Symlinks\SymlinksProcessor;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class RefreshCommand extends BaseCommand
{
use SymlinksExecutionTrait;

protected function configure(): void
{
$this
->setName('symlinks:refresh')
->setDescription('Create symlinks defined in extra.somework/composer-symlinks.')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show the operations without creating links.');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$composer = $this->requireComposer();
$io = $this->getIO();

$dryRun = (bool) $input->getOption('dry-run');

$event = new Event('symlinks:refresh', $composer, $io);
$filesystem = new Filesystem();
$factory = new SymlinksFactory($event, $filesystem);
$processor = new SymlinksProcessor($filesystem, $dryRun);

$this->runSymlinks($factory, $processor, $io, $dryRun);

return Command::SUCCESS;
}
}
48 changes: 13 additions & 35 deletions src/Symlinks/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,31 @@

use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
use Composer\Plugin\Capable;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Util\Filesystem;

class Plugin implements PluginInterface
class Plugin implements PluginInterface, Capable
{
use SymlinksExecutionTrait;

public function activate(Composer $composer, IOInterface $io): void
{
$eventDispatcher = $composer->getEventDispatcher();
$eventDispatcher->addListener(ScriptEvents::POST_INSTALL_CMD, $this->createLinks());
$eventDispatcher->addListener(ScriptEvents::POST_UPDATE_CMD, $this->createLinks());
}

public function getCapabilities(): array
{
return [
CommandProviderCapability::class => Command\CommandProvider::class,
];
}

public function deactivate(Composer $composer, IOInterface $io): void
{
}
Expand All @@ -35,40 +46,7 @@ protected function createLinks(): callable
$dryRun = getenv('SYMLINKS_DRY_RUN') === '1' || getenv('SYMLINKS_DRY_RUN') === 'true';
$processor = new SymlinksProcessor($fileSystem, $dryRun);

$symlinks = $factory->process();
foreach ($symlinks as $symlink) {
try {
if (!$processor->processSymlink($symlink)) {
throw new RuntimeException('Unknown error');
}
$event
->getIO()
->write(sprintf(
' %sSymlinking <comment>%s</comment> to <comment>%s</comment>',
$dryRun ? '[DRY RUN] ' : '',
$symlink->getLink(),
$symlink->getTarget()
));
} catch (LinkDirectoryError $exception) {
$event
->getIO()
->write(sprintf(
' Symlinking <comment>%s</comment> to <comment>%s</comment> - %s',
$symlink->getLink(),
$symlink->getTarget(),
'Skipped'
));
} catch (\Exception $exception) {
$event
->getIO()
->writeError(sprintf(
' Symlinking <comment>%s</comment> to <comment>%s</comment> - %s',
$symlink->getLink(),
$symlink->getTarget(),
$exception->getMessage()
));
}
}
$this->runSymlinks($factory, $processor, $event->getIO(), $dryRun);
};
}
}
46 changes: 46 additions & 0 deletions src/Symlinks/SymlinksExecutionTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);

namespace SomeWork\Symlinks;

use Composer\IO\IOInterface;

trait SymlinksExecutionTrait
{
private function runSymlinks(
SymlinksFactory $factory,
SymlinksProcessor $processor,
IOInterface $io,
bool $dryRun
): void {
$symlinks = $factory->process();
foreach ($symlinks as $symlink) {
try {
if (!$processor->processSymlink($symlink)) {
throw new RuntimeException('Unknown error');
}

$io->write(sprintf(
' %sSymlinking <comment>%s</comment> to <comment>%s</comment>',
$dryRun ? '[DRY RUN] ' : '',
$symlink->getLink(),
$symlink->getTarget()
));
} catch (LinkDirectoryError $exception) {
$io->write(sprintf(
' Symlinking <comment>%s</comment> to <comment>%s</comment> - %s',
$symlink->getLink(),
$symlink->getTarget(),
'Skipped'
));
} catch (\Exception $exception) {
$io->writeError(sprintf(
' Symlinking <comment>%s</comment> to <comment>%s</comment> - %s',
$symlink->getLink(),
$symlink->getTarget(),
$exception->getMessage()
));
}
}
}
}
22 changes: 22 additions & 0 deletions tests/PluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use Composer\Composer;
use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\NullIO;
use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
use Composer\Script\ScriptEvents;
use PHPUnit\Framework\TestCase;
use SomeWork\Symlinks\Plugin;
use SomeWork\Symlinks\Command\RefreshCommand;

class PluginTest extends TestCase
{
Expand Down Expand Up @@ -35,4 +37,24 @@ public function addListener(string $eventName, $listener, int $priority = 0): vo
$this->assertArrayHasKey(ScriptEvents::POST_UPDATE_CMD, $dispatcher->recorded);
$this->assertIsCallable($dispatcher->recorded[ScriptEvents::POST_INSTALL_CMD][0]);
}

public function testGetCapabilitiesRegistersCommand(): void
{
$plugin = new Plugin();

$capabilities = $plugin->getCapabilities();

$this->assertArrayHasKey(CommandProviderCapability::class, $capabilities);
$this->assertSame(
\SomeWork\Symlinks\Command\CommandProvider::class,
$capabilities[CommandProviderCapability::class]
);

$providerClass = $capabilities[CommandProviderCapability::class];
$provider = new $providerClass();
$commands = $provider->getCommands();

$this->assertNotEmpty($commands);
$this->assertInstanceOf(RefreshCommand::class, $commands[0]);
}
}
101 changes: 101 additions & 0 deletions tests/RefreshCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace SomeWork\Symlinks\Tests;

use Composer\Composer;
use Composer\Console\Application;
use Composer\EventDispatcher\EventDispatcher;
use Composer\IO\NullIO;
use Composer\Package\RootPackage;
use PHPUnit\Framework\TestCase;
use SomeWork\Symlinks\Command\RefreshCommand;
use Symfony\Component\Console\Tester\CommandTester;

class RefreshCommandTest extends TestCase
{
public function testCommandCreatesSymlinks(): void
{
$tmp = sys_get_temp_dir() . '/command_' . uniqid();
mkdir($tmp);
mkdir($tmp . '/source');
file_put_contents($tmp . '/source/file.txt', 'data');

$cwd = getcwd();
chdir($tmp);

$composer = $this->createComposer([
'somework/composer-symlinks' => [
'symlinks' => [
'source/file.txt' => 'link.txt',
],
],
]);

$this->runCommand($composer);

$this->assertTrue(is_link($tmp . '/link.txt'));
$this->assertSame(
realpath($tmp . '/source/file.txt'),
realpath(dirname($tmp . '/link.txt') . '/' . readlink($tmp . '/link.txt'))
);

chdir($cwd);
}

public function testDryRunOptionDoesNotCreateLinks(): void
{
$tmp = sys_get_temp_dir() . '/command_' . uniqid();
mkdir($tmp);
mkdir($tmp . '/source');
file_put_contents($tmp . '/source/file.txt', 'data');

$cwd = getcwd();
chdir($tmp);

$composer = $this->createComposer([
'somework/composer-symlinks' => [
'symlinks' => [
'source/file.txt' => 'link.txt',
],
],
]);

$this->runCommand($composer, ['--dry-run' => true]);

$this->assertFalse(file_exists($tmp . '/link.txt'));

chdir($cwd);
}

private function createComposer(array $extra): Composer
{
$composer = new Composer();
$io = new NullIO();
$dispatcher = new EventDispatcher($composer, $io);
$composer->setEventDispatcher($dispatcher);

$package = new RootPackage('test/test', '1.0.0', '1.0.0');
$package->setExtra($extra);
$composer->setPackage($package);

$composer->setConfig(new \Composer\Config());

return $composer;
}

private function runCommand(Composer $composer, array $input = []): void
{
$application = new Application();
$application->setAutoExit(false);
$io = new NullIO();

$command = new RefreshCommand();
$command->setComposer($composer);
$command->setIO($io);
$application->add($command);

$tester = new CommandTester($application->find('symlinks:refresh'));
$tester->execute(array_merge(['command' => 'symlinks:refresh'], $input));
}
}