diff --git a/doc/05-repositories.md b/doc/05-repositories.md index 7bd3d1fe8..658bc42b6 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -649,6 +649,26 @@ the console will read `Symlinked from ../../packages/my-package`. If symlinking is _not_ possible the package will be copied. In that case, the console will output `Mirrored from ../../packages/my-package`. +Instead of default fallback strategy you can force to use symlink with `"symlink": true` or +mirroring with `"symlink": false` option. +Forcing mirroring can be useful when deploying or generating package from a monolithic repository. + +```json +{ + "repositories": [ + { + "type": "path", + "url": "../../packages/my-package", + "options": { + "symlink": false + } + } + ] +} +``` + + + Instead of using a relative path, an absolute path can also be used. > **Note:** Repository paths can also contain wildcards like ``*`` and ``?``. diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index ac4d9c457..1156ff62d 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -25,6 +25,9 @@ use Symfony\Component\Filesystem\Filesystem; */ class PathDownloader extends FileDownloader { + const STRATEGY_SYMLINK = 10; + const STRATEGY_MIRROR = 20; + /** * {@inheritdoc} */ @@ -45,6 +48,21 @@ class PathDownloader extends FileDownloader )); } + // Get the transport options with default values + $transportOptions = $package->getTransportOptions() + array('symlink'=>null); + + // When symlink transport option is null, both symlink and mirror are allowed + $currentStrategy = self::STRATEGY_SYMLINK; + $allowedStrategies = array(self::STRATEGY_SYMLINK, self::STRATEGY_MIRROR); + + if (true === $transportOptions['symlink']) { + $currentStrategy = self::STRATEGY_SYMLINK; + $allowedStrategies = array(self::STRATEGY_SYMLINK); + } elseif(false === $transportOptions['symlink']) { + $currentStrategy = self::STRATEGY_MIRROR; + $allowedStrategies = array(self::STRATEGY_MIRROR); + } + $fileSystem = new Filesystem(); $this->filesystem->removeDirectory($path); @@ -54,17 +72,29 @@ class PathDownloader extends FileDownloader $package->getFullPrettyVersion() )); - 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); - $fileSystem->symlink($shortestPath, $path); - $this->io->writeError(sprintf(' Symlinked from %s', $url)); + if (self::STRATEGY_SYMLINK == $currentStrategy) { + 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); + $fileSystem->symlink($shortestPath, $path); + $this->io->writeError(sprintf(' Symlinked from %s', $url)); + } + } catch (IOException $e) { + if (in_array(self::STRATEGY_MIRROR, $allowedStrategies)) { + $this->io->writeError(' Symlink failed, fallback to use mirroring!'); + $currentStrategy = self::STRATEGY_MIRROR; + } else { + throw new \RuntimeException(sprintf('Symlink from "%s" to "%s" failed!', $realUrl, $path)); + } } - } catch (IOException $e) { + } + + // Fallback if symlink failed or if symlink is not allowed for the package + if (self::STRATEGY_MIRROR == $currentStrategy) { $fileSystem->mirror($realUrl, $path); $this->io->writeError(sprintf(' Mirrored from %s', $url)); } diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 3aac09ca4..8e230d3b0 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -41,7 +41,14 @@ use Composer\Util\ProcessExecutor; * { * "type": "path", * "url": "/absolute/path/to/several/packages/*" - * } + * }, + * { + * "type": "path", + * "url": "../../relative/path/to/package/", + * "options": { + * "symlink": false + * } + * }, * ] * @endcode * @@ -75,6 +82,11 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn */ private $process; + /** + * @var array + */ + private $options; + /** * Initializes path repository. * @@ -88,11 +100,12 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn throw new \RuntimeException('You must specify the `url` configuration for the path repository'); } - $this->loader = new ArrayLoader(); + $this->loader = new ArrayLoader(null, true); $this->url = $repoConfig['url']; $this->process = new ProcessExecutor($io); $this->versionGuesser = new VersionGuesser($config, $this->process, new VersionParser()); $this->repoConfig = $repoConfig; + $this->options = isset($repoConfig['options']) ? $repoConfig['options'] : array(); parent::__construct(); } @@ -126,6 +139,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn 'url' => $url, 'reference' => sha1($json), ); + $package['transport-options'] = $this->getOptions(); if (!isset($package['version'])) { $package['version'] = $this->versionGuesser->guessVersion($package, $path) ?: 'dev-master'; @@ -135,7 +149,6 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) { $package['dist']['reference'] = trim($output); } - $package = $this->loader->load($package); $this->addPackage($package); } @@ -153,4 +166,20 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn return str_replace(DIRECTORY_SEPARATOR, '/', $val); }, glob($this->url, GLOB_MARK | GLOB_ONLYDIR)); } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param array $options + */ + public function setOptions($options) + { + $this->options = $options; + } }