From 39d9a5b6c560c01f09947e4d318ccbb6893b87ae Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 22 Aug 2024 11:45:25 +0200 Subject: [PATCH] Fix relative:true not being respected in path repo installs, fixes #12074 (#12092) --- src/Composer/Downloader/PathDownloader.php | 42 ++++++++++++++++----- src/Composer/Util/Filesystem.php | 10 +++-- tests/Composer/Test/Util/FilesystemTest.php | 10 +++-- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 791521bd6..8997f561e 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -43,6 +43,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter { $path = Filesystem::trimTrailingSlash($path); $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot download.'); + } $realUrl = realpath($url); if (false === $realUrl || !file_exists($realUrl) || !is_dir($realUrl)) { throw new \RuntimeException(sprintf( @@ -79,7 +82,13 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter { $path = Filesystem::trimTrailingSlash($path); $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot install.'); + } $realUrl = realpath($url); + if (false === $realUrl) { + throw new \RuntimeException('Failed to realpath '.$url); + } if (realpath($path) === $realUrl) { if ($output) { @@ -111,16 +120,16 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter } $this->filesystem->junction($realUrl, $path); } else { - $absolutePath = $path; - if (!$this->filesystem->isAbsolutePath($absolutePath)) { - $absolutePath = Platform::getCwd() . DIRECTORY_SEPARATOR . $path; - } - $shortestPath = $this->filesystem->findShortestPath($absolutePath, $realUrl); $path = rtrim($path, "/"); if ($output) { $this->io->writeError(sprintf('Symlinking from %s', $url), false); } - if ($transportOptions['relative']) { + if ($transportOptions['relative'] === true) { + $absolutePath = $path; + if (!$this->filesystem->isAbsolutePath($absolutePath)) { + $absolutePath = Platform::getCwd() . DIRECTORY_SEPARATOR . $path; + } + $shortestPath = $this->filesystem->findShortestPath($absolutePath, $realUrl, false, true); $symfonyFilesystem->symlink($shortestPath.'/', $path); } else { $symfonyFilesystem->symlink($realUrl.'/', $path); @@ -185,13 +194,18 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter return \React\Promise\resolve(null); } + $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot remove.'); + } + // ensure that the source path (dist url) is not the same as the install path, which // can happen when using custom installers, see https://github.com/composer/composer/pull/9116 // not using realpath here as we do not want to resolve the symlink to the original dist url // it points to $fs = new Filesystem; $absPath = $fs->isAbsolutePath($path) ? $path : Platform::getCwd() . '/' . $path; - $absDistUrl = $fs->isAbsolutePath($package->getDistUrl()) ? $package->getDistUrl() : Platform::getCwd() . '/' . $package->getDistUrl(); + $absDistUrl = $fs->isAbsolutePath($url) ? $url : Platform::getCwd() . '/' . $url; if ($fs->normalizePath($absPath) === $fs->normalizePath($absDistUrl)) { if ($output) { $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); @@ -214,7 +228,8 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $dumper = new ArrayDumper; $packageConfig = $dumper->dump($package); - if ($packageVersion = $guesser->guessVersion($packageConfig, $path)) { + $packageVersion = $guesser->guessVersion($packageConfig, $path); + if ($packageVersion !== null) { return $packageVersion['commit']; } @@ -226,7 +241,14 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter */ protected function getInstallOperationAppendix(PackageInterface $package, string $path): string { - $realUrl = realpath($package->getDistUrl()); + $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot install.'); + } + $realUrl = realpath($url); + if (false === $realUrl) { + throw new \RuntimeException('Failed to realpath '.$url); + } if (realpath($path) === $realUrl) { return ': Source already present'; @@ -257,7 +279,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $allowedStrategies = [self::STRATEGY_SYMLINK, self::STRATEGY_MIRROR]; $mirrorPathRepos = Platform::getEnv('COMPOSER_MIRROR_PATH_REPOS'); - if ($mirrorPathRepos) { + if ((bool) $mirrorPathRepos) { $currentStrategy = self::STRATEGY_MIRROR; } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 0bb0ef869..8aa743c71 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -436,10 +436,11 @@ class Filesystem * Returns the shortest path from $from to $to * * @param bool $directories if true, the source/target are considered to be directories + * @param bool $preferRelative if true, relative paths will be preferred even if longer * @throws \InvalidArgumentException * @return string */ - public function findShortestPath(string $from, string $to, bool $directories = false) + public function findShortestPath(string $from, string $to, bool $directories = false, bool $preferRelative = false) { if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); @@ -471,7 +472,7 @@ class Filesystem $commonPathCode = str_repeat('../', $sourcePathDepth); // allow top level /foo & /bar dirs to be addressed relatively as this is common in Docker setups - if ('/' === $commonPath && $sourcePathDepth > 1) { + if (!$preferRelative && '/' === $commonPath && $sourcePathDepth > 1) { return $to; } @@ -487,10 +488,11 @@ class Filesystem * Returns PHP code that, when executed in $from, will return the path to $to * * @param bool $directories if true, the source/target are considered to be directories + * @param bool $preferRelative if true, relative paths will be preferred even if longer * @throws \InvalidArgumentException * @return string */ - public function findShortestPathCode(string $from, string $to, bool $directories = false, bool $staticCode = false) + public function findShortestPathCode(string $from, string $to, bool $directories = false, bool $staticCode = false, bool $preferRelative = false) { if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); @@ -520,7 +522,7 @@ class Filesystem $sourcePathDepth = substr_count((string) substr($from, \strlen($commonPath)), '/') + (int) $directories; // allow top level /foo & /bar dirs to be addressed relatively as this is common in Docker setups - if ('/' === $commonPath && $sourcePathDepth > 1) { + if (!$preferRelative && '/' === $commonPath && $sourcePathDepth > 1) { return var_export($to, true); } diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index cfc0bacaa..e947e4fe9 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -54,10 +54,10 @@ class FilesystemTest extends TestCase /** * @dataProvider providePathCouplesAsCode */ - public function testFindShortestPathCode(string $a, string $b, bool $directory, string $expected, bool $static = false): void + public function testFindShortestPathCode(string $a, string $b, bool $directory, string $expected, bool $static = false, bool $preferRelative = false): void { $fs = new Filesystem; - self::assertEquals($expected, $fs->findShortestPathCode($a, $b, $directory, $static)); + self::assertEquals($expected, $fs->findShortestPathCode($a, $b, $directory, $static, $preferRelative)); } public static function providePathCouplesAsCode(): array @@ -77,6 +77,7 @@ class FilesystemTest extends TestCase ['/foo/bar', '/foo/baz', true, "dirname(__DIR__).'/baz'"], ['/foo/bin/run', '/foo/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"], ['/foo/bin/run', '/bar/bin/run', true, "'/bar/bin/run'"], + ['/app/vendor/foo/bar', '/lib', true, "dirname(dirname(dirname(dirname(__DIR__)))).'/lib'", false, true], ['/bin/run', '/bin/run', true, "__DIR__"], ['c:/bin/run', 'C:\\bin/run', true, "__DIR__"], ['c:/bin/run', 'c:/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"], @@ -113,10 +114,10 @@ class FilesystemTest extends TestCase /** * @dataProvider providePathCouples */ - public function testFindShortestPath(string $a, string $b, string $expected, bool $directory = false): void + public function testFindShortestPath(string $a, string $b, string $expected, bool $directory = false, bool $preferRelative = false): void { $fs = new Filesystem; - self::assertEquals($expected, $fs->findShortestPath($a, $b, $directory)); + self::assertEquals($expected, $fs->findShortestPath($a, $b, $directory, $preferRelative)); } public static function providePathCouples(): array @@ -152,6 +153,7 @@ class FilesystemTest extends TestCase ['C:/Temp', 'c:\Temp\..\..\test', "../test", true], ['C:/Temp/../..', 'c:\Temp\..\..\test', "./test", true], ['C:/Temp/../..', 'D:\Temp\..\..\test', "D:/test", true], + ['/app/vendor/foo/bar', '/lib', '../../../../lib', true, true], ['/tmp', '/tmp/../../test', '../test', true], ['/tmp', '/test', '../test', true], ['/foo/bar', '/foo/bar_vendor', '../bar_vendor', true],