Browse Source

Merge pull request #4845 from curry684/pull-4690

Implement junctioning on Windows for path repositories
Jordi Boggiano 9 years ago
parent
commit
d6d0435c54

+ 11 - 3
src/Composer/Downloader/PathDownloader.php

@@ -13,6 +13,7 @@
 namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
 use Symfony\Component\Filesystem\Exception\IOException;
 use Symfony\Component\Filesystem\Filesystem;
 
@@ -54,9 +55,16 @@ class PathDownloader extends FileDownloader
         ));
 
         try {
-            $shortestPath = $this->filesystem->findShortestPath($path, $realUrl);
-            $fileSystem->symlink($shortestPath, $path);
-            $this->io->writeError(sprintf('    Symlinked from %s', $url));
+            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);
+                $fileSystem->symlink($shortestPath, $path);
+                $this->io->writeError(sprintf('    Symlinked from %s', $url));
+            }
         } catch (IOException $e) {
             $fileSystem->mirror($realUrl, $path);
             $this->io->writeError(sprintf('    Mirrored from %s', $url));

+ 65 - 0
src/Composer/Util/Filesystem.php

@@ -14,6 +14,7 @@ namespace Composer\Util;
 
 use RecursiveDirectoryIterator;
 use RecursiveIteratorIterator;
+use Symfony\Component\Filesystem\Exception\IOException;
 use Symfony\Component\Finder\Finder;
 
 /**
@@ -98,6 +99,10 @@ class Filesystem
             return $this->unlinkSymlinkedDirectory($directory);
         }
 
+        if ($this->isJunction($directory)) {
+            return $this->removeJunction($directory);
+        }
+
         if (!file_exists($directory) || !is_dir($directory)) {
             return true;
         }
@@ -576,4 +581,64 @@ class Filesystem
 
         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);
+    }
 }

+ 29 - 0
tests/Composer/Test/Util/FilesystemTest.php

@@ -266,4 +266,33 @@ class FilesystemTest extends TestCase
         $this->assertFalse(file_exists($symlinkedTrailingSlash));
         $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));
+    }
 }