Merge pull request #4845 from curry684/pull-4690
Implement junctioning on Windows for path repositoriespull/4920/head
commit
d6d0435c54
|
@ -13,6 +13,7 @@
|
||||||
namespace Composer\Downloader;
|
namespace Composer\Downloader;
|
||||||
|
|
||||||
use Composer\Package\PackageInterface;
|
use Composer\Package\PackageInterface;
|
||||||
|
use Composer\Util\Platform;
|
||||||
use Symfony\Component\Filesystem\Exception\IOException;
|
use Symfony\Component\Filesystem\Exception\IOException;
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
@ -54,9 +55,16 @@ class PathDownloader extends FileDownloader
|
||||||
));
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (Platform::isWindows()) {
|
||||||
|
// Implement symlinks as NTFS junctions on Windows
|
||||||
|
$this->filesystem->junction($realUrl, $path);
|
||||||
|
$this->io->writeError(sprintf(' Junctioned from %s', $url));
|
||||||
|
|
||||||
|
} else {
|
||||||
$shortestPath = $this->filesystem->findShortestPath($path, $realUrl);
|
$shortestPath = $this->filesystem->findShortestPath($path, $realUrl);
|
||||||
$fileSystem->symlink($shortestPath, $path);
|
$fileSystem->symlink($shortestPath, $path);
|
||||||
$this->io->writeError(sprintf(' Symlinked from %s', $url));
|
$this->io->writeError(sprintf(' Symlinked from %s', $url));
|
||||||
|
}
|
||||||
} catch (IOException $e) {
|
} catch (IOException $e) {
|
||||||
$fileSystem->mirror($realUrl, $path);
|
$fileSystem->mirror($realUrl, $path);
|
||||||
$this->io->writeError(sprintf(' Mirrored from %s', $url));
|
$this->io->writeError(sprintf(' Mirrored from %s', $url));
|
||||||
|
|
|
@ -14,6 +14,7 @@ namespace Composer\Util;
|
||||||
|
|
||||||
use RecursiveDirectoryIterator;
|
use RecursiveDirectoryIterator;
|
||||||
use RecursiveIteratorIterator;
|
use RecursiveIteratorIterator;
|
||||||
|
use Symfony\Component\Filesystem\Exception\IOException;
|
||||||
use Symfony\Component\Finder\Finder;
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,6 +99,10 @@ class Filesystem
|
||||||
return $this->unlinkSymlinkedDirectory($directory);
|
return $this->unlinkSymlinkedDirectory($directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isJunction($directory)) {
|
||||||
|
return $this->removeJunction($directory);
|
||||||
|
}
|
||||||
|
|
||||||
if (!file_exists($directory) || !is_dir($directory)) {
|
if (!file_exists($directory) || !is_dir($directory)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -576,4 +581,64 @@ class Filesystem
|
||||||
|
|
||||||
return $resolved;
|
return $resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an NTFS junction.
|
||||||
|
*
|
||||||
|
* @param string $target
|
||||||
|
* @param string $junction
|
||||||
|
*/
|
||||||
|
public function junction($target, $junction)
|
||||||
|
{
|
||||||
|
if (!Platform::isWindows()) {
|
||||||
|
throw new \LogicException(sprintf('Function %s is not available on non-Windows platform', __CLASS__));
|
||||||
|
}
|
||||||
|
if (!is_dir($target)) {
|
||||||
|
throw new IOException(sprintf('Cannot junction to "%s" as it is not a directory.', $target), 0, null, $target);
|
||||||
|
}
|
||||||
|
$cmd = sprintf('mklink /J %s %s',
|
||||||
|
ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $junction)),
|
||||||
|
ProcessExecutor::escape(realpath($target)));
|
||||||
|
if ($this->getProcess()->execute($cmd, $output) !== 0) {
|
||||||
|
throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 0, null, $target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the target directory is a Windows NTFS Junction.
|
||||||
|
*
|
||||||
|
* @param string $junction Path to check.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isJunction($junction)
|
||||||
|
{
|
||||||
|
if (!Platform::isWindows()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!is_dir($junction) || is_link($junction)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Junctions have no link stat but are otherwise indistinguishable from real directories
|
||||||
|
$stat = lstat($junction);
|
||||||
|
return ($stat['mode'] === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a Windows NTFS junction.
|
||||||
|
*
|
||||||
|
* @param string $junction
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function removeJunction($junction)
|
||||||
|
{
|
||||||
|
if (!Platform::isWindows()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$junction = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $junction), DIRECTORY_SEPARATOR);
|
||||||
|
if (!$this->isJunction($junction)) {
|
||||||
|
throw new IOException(sprintf('%s is not a junction and thus cannot be removed as one', $junction));
|
||||||
|
}
|
||||||
|
$cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape($junction));
|
||||||
|
return ($this->getProcess()->execute($cmd, $output) === 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,4 +266,33 @@ class FilesystemTest extends TestCase
|
||||||
$this->assertFalse(file_exists($symlinkedTrailingSlash));
|
$this->assertFalse(file_exists($symlinkedTrailingSlash));
|
||||||
$this->assertFalse(file_exists($symlinked));
|
$this->assertFalse(file_exists($symlinked));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testJunctions()
|
||||||
|
{
|
||||||
|
@mkdir($this->workingDir . '/real/nesting/testing', 0777, true);
|
||||||
|
$fs = new Filesystem();
|
||||||
|
|
||||||
|
// Non-Windows systems do not support this and will return false on all tests, and an exception on creation
|
||||||
|
if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
|
||||||
|
$this->assertFalse($fs->isJunction($this->workingDir));
|
||||||
|
$this->assertFalse($fs->removeJunction($this->workingDir));
|
||||||
|
$this->setExpectedException('LogicException', 'not available on non-Windows platform');
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $this->workingDir . '/real/../real/nesting';
|
||||||
|
$junction = $this->workingDir . '/junction';
|
||||||
|
|
||||||
|
// Create and detect junction
|
||||||
|
$fs->junction($target, $junction);
|
||||||
|
$this->assertTrue($fs->isJunction($junction));
|
||||||
|
$this->assertFalse($fs->isJunction($target));
|
||||||
|
$this->assertTrue($fs->isJunction($target . '/../../junction'));
|
||||||
|
$this->assertFalse($fs->isJunction($junction . '/../real'));
|
||||||
|
$this->assertTrue($fs->isJunction($junction . '/../junction'));
|
||||||
|
|
||||||
|
// Remove junction
|
||||||
|
$this->assertTrue(is_dir($junction));
|
||||||
|
$this->assertTrue($fs->removeJunction($junction));
|
||||||
|
$this->assertFalse(is_dir($junction));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue