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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The behaviour of the plugin can be tuned via the following configuration keys:
| `absolute-path` | `false` | Create symlinks using the real path to the target. |
| `throw-exception` | `true` | Throw an exception on errors instead of just printing the message. |
| `force-create` | `false` | Remove any existing file or directory at the link path before creating the symlink. |
| `cleanup` | `false` | Remove symlinks that were created previously but are no longer present in the configuration. |

You can set personal configs for any symlink.
For personal configs `link` must be defined
Expand All @@ -47,12 +48,19 @@ For personal configs `link` must be defined
"force-create": false,
"skip-missing-target": false,
"absolute-path": false,
"throw-exception": true
"throw-exception": true,
"cleanup": true
}
}
}
```

When `cleanup` is enabled the plugin keeps a registry of every symlink it
created in the file `vendor/composer-symlinks-state.json`. On the next run the
registry is compared with the current configuration: entries missing from the
configuration are deleted from both the registry and the filesystem. The file
is recreated automatically and can safely be ignored by VCS.

### Placeholder syntax

Symlink paths support the following placeholders which are expanded before
Expand Down Expand Up @@ -103,6 +111,14 @@ $ SYMLINKS_DRY_RUN=1 composer install --no-interaction
[DRY RUN] Symlinking /tmp/sample/linked.txt to /tmp/sample/source/file.txt
```

### Uninstalling

Running `composer remove somework/composer-symlinks` will trigger the plugin's
`uninstall()` hook. The hook reads the registry file and removes every stored
symlink from the filesystem before deleting the registry itself. This ensures
that no links created by the plugin are left behind when the package is
removed.

### Typical error messages

| Message | Meaning |
Expand Down
55 changes: 54 additions & 1 deletion src/Symlinks/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ public function deactivate(Composer $composer, IOInterface $io): void

public function uninstall(Composer $composer, IOInterface $io): void
{
$fileSystem = new Filesystem();
$vendorDir = $this->resolveVendorDir($composer, $fileSystem);
if ($vendorDir === null) {
return;
}

$registry = new SymlinksRegistry($fileSystem, $vendorDir);
$registry->removeAll();
}

protected function createLinks(): callable
Expand All @@ -46,7 +54,52 @@ protected function createLinks(): callable
$dryRun = getenv('SYMLINKS_DRY_RUN') === '1' || getenv('SYMLINKS_DRY_RUN') === 'true';
$processor = new SymlinksProcessor($fileSystem, $dryRun);

$this->runSymlinks($factory, $processor, $event->getIO(), $dryRun);
$processedSymlinks = $this->runSymlinks($factory, $processor, $event->getIO(), $dryRun);

if ($dryRun) {
return;
}

$vendorDir = $factory->getVendorDirPath();
if ($vendorDir === null) {
return;
}

$registry = new SymlinksRegistry($fileSystem, $vendorDir);
$registry->sync($factory->getConfiguredSymlinks(), $processedSymlinks, $factory->isCleanupEnabled());
};
}

private function resolveVendorDir(Composer $composer, Filesystem $filesystem): ?string
{
try {
$config = $composer->getConfig();
} catch (\TypeError $exception) {
$config = null;
}

$vendorDir = null;
if ($config !== null) {
$vendorDir = $config->get('vendor-dir');
}

if (!$vendorDir) {
$projectDir = getcwd();
if ($projectDir === false) {
return null;
}
$vendorDir = $projectDir . DIRECTORY_SEPARATOR . 'vendor';
}

if (!$filesystem->isAbsolutePath($vendorDir)) {
$projectDir = getcwd();
if ($projectDir === false) {
return null;
}
$combined = $projectDir . DIRECTORY_SEPARATOR . $vendorDir;
$vendorDir = realpath($combined) ?: $combined;
}

return $vendorDir;
}
}
9 changes: 8 additions & 1 deletion src/Symlinks/SymlinksExecutionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@

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

$processed[] = $symlink;

$io->write(sprintf(
' %sSymlinking <comment>%s</comment> to <comment>%s</comment>',
$dryRun ? '[DRY RUN] ' : '',
Expand All @@ -42,5 +48,6 @@ private function runSymlinks(
));
}
}
return $processed;
}
}
42 changes: 41 additions & 1 deletion src/Symlinks/SymlinksFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class SymlinksFactory

protected Filesystem $fileSystem;
protected Event $event;
/**
* @var array<string, string>
*/
private array $configuredSymlinks = [];
private ?string $vendorDir = null;

public function __construct(Event $event, Filesystem $filesystem)
{
Expand All @@ -36,6 +41,7 @@ public function __construct(Event $event, Filesystem $filesystem)
*/
public function process(): array
{
$this->configuredSymlinks = [];
$symlinksData = $this->getSymlinksData();

$symlinks = [];
Expand Down Expand Up @@ -142,6 +148,7 @@ protected function processSymlink(string $target, $linkData): ?Symlink
is_link($linkPath) &&
realpath(dirname($linkPath) . DIRECTORY_SEPARATOR . readlink($linkPath)) === $targetPath
) {
$this->registerConfiguredSymlink($linkPath, $targetPath);
$this->event->getIO()->write(
sprintf(
' Symlink <comment>%s</comment> to <comment>%s</comment> already created',
Expand All @@ -152,11 +159,15 @@ protected function processSymlink(string $target, $linkData): ?Symlink
return null;
}

return (new Symlink())
$symlink = (new Symlink())
->setTarget($targetPath)
->setLink($linkPath)
->setAbsolutePath($this->getConfig(static::ABSOLUTE_PATH, $linkData, false))
->setForceCreate($this->getConfig(static::FORCE_CREATE, $linkData, false));

$this->registerConfiguredSymlink($linkPath, $targetPath);

return $symlink;
}

/**
Expand Down Expand Up @@ -245,7 +256,36 @@ private function getProjectDir(): ?string
return $path === false ? $cwd : $path;
}

public function getVendorDirPath(): ?string
{
if ($this->vendorDir === null) {
$this->vendorDir = $this->resolveVendorDir();
}

return $this->vendorDir;
}

public function getConfiguredSymlinks(): array
{
return $this->configuredSymlinks;
}

public function isCleanupEnabled(): bool
{
return $this->getConfig('cleanup', null, false);
}

private function registerConfiguredSymlink(string $link, string $target): void
{
$this->configuredSymlinks[$link] = $target;
}

private function getVendorDir(): ?string
{
return $this->getVendorDirPath();
}

private function resolveVendorDir(): ?string
{
$composer = $this->event->getComposer();
$vendorDir = null;
Expand Down
Loading