From e085a72f6450a83d173f0eaa7399158a7f5fd686 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Thu, 31 Jan 2019 11:20:17 +0000 Subject: [PATCH 01/10] Fix mode bitmask when detecting a Windows junction --- src/Composer/Util/Filesystem.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index ebb7dfbd3..3a8f523fc 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -648,6 +648,10 @@ class Filesystem /** * Returns whether the target directory is a Windows NTFS Junction. * + * We test if the path is a directory and not an ordinary link, then check + * that the mode value returned from lstat (which gives the status of the + * link itself) is not a directory. + * * @param string $junction Path to check. * @return bool */ @@ -659,22 +663,14 @@ class Filesystem if (!is_dir($junction) || is_link($junction)) { return false; } - /** - * According to MSDN at https://msdn.microsoft.com/en-us/library/14h5k7ff.aspx we can detect a junction now - * using the 'mode' value from stat: "The _S_IFDIR bit is set if path specifies a directory; the _S_IFREG bit - * is set if path specifies an ordinary file or a device." We have just tested for a directory above, so if - * we have a directory that isn't one according to lstat(...) we must have a junction. - * - * #define _S_IFDIR 0x4000 - * #define _S_IFREG 0x8000 - * - * Stat cache should be cleared before to avoid accidentally reading wrong information from previous installs. - */ + + // Important to clear cache first clearstatcache(true, $junction); clearstatcache(false); $stat = lstat($junction); - return !($stat['mode'] & 0xC000); + // S_IFDIR is 0x4000, S_IFMT is the 0xF000 bitmask + return $stat ? 0x4000 !== ($stat['mode'] & 0xF000) : false; } /** From da0dc74414fd8e6eca65619a6d9c1dd1a04facc6 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Sun, 10 Feb 2019 14:41:20 +0000 Subject: [PATCH 02/10] Update doc block, remove redundant clearstatcache --- src/Composer/Util/Filesystem.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 3a8f523fc..2ebc434b7 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -652,6 +652,11 @@ class Filesystem * that the mode value returned from lstat (which gives the status of the * link itself) is not a directory. * + * This relies on the fact that PHP does not set this value because there is + * no universal file type flag for a junction or a mount point. However a + * bug in PHP can cause a random value to be returned and this could result + * in a junction not being detected: https://bugs.php.net/bug.php?id=77552 + * * @param string $junction Path to check. * @return bool */ @@ -664,9 +669,8 @@ class Filesystem return false; } - // Important to clear cache first + // Important to clear all caches first clearstatcache(true, $junction); - clearstatcache(false); $stat = lstat($junction); // S_IFDIR is 0x4000, S_IFMT is the 0xF000 bitmask From a5cd912c02b94e8f5c97a0506eda456d47286a37 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 11 Feb 2019 10:51:32 +0100 Subject: [PATCH 03/10] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db9890bda..36ceddfc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### [1.8.4] 2019-02-11 + + * Fixed long standing solver bug leading to odd solving issues in edge cases, see #7946 + * Fixed HHVM support for upcoming releases + * Fixed unix proxy for binaries to be POSIX compatible instead of breaking some shells + * Fixed invalid deprecation warning for composer-plugin-api + * Fixed edge case issues with Windows junctions when working with path repositories + ### [1.8.3] 2019-01-30 * Fixed regression when executing partial updates @@ -729,6 +737,10 @@ * Initial release +[1.8.4]: https://github.com/composer/composer/compare/1.8.3...1.8.4 +[1.8.3]: https://github.com/composer/composer/compare/1.8.2...1.8.3 +[1.8.2]: https://github.com/composer/composer/compare/1.8.1...1.8.2 +[1.8.1]: https://github.com/composer/composer/compare/1.8.0...1.8.1 [1.8.0]: https://github.com/composer/composer/compare/1.7.3...1.8.0 [1.7.3]: https://github.com/composer/composer/compare/1.7.2...1.7.3 [1.7.2]: https://github.com/composer/composer/compare/1.7.1...1.7.2 From c903a63f100cd6a5afbbcbdca4bb8fceae2261fd Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Mon, 28 Jan 2019 21:18:26 +0000 Subject: [PATCH 04/10] Update appveyor to PHP 7.3 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index e7c20c9e6..cb0f3d33c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ clone_depth: 5 environment: # This sets the PHP version (from Chocolatey) - PHPCI_CHOCO_VERSION: 7.2.9 + PHPCI_CHOCO_VERSION: 7.3.1 PHPCI_CACHE: C:\tools\phpci PHPCI_PHP: C:\tools\phpci\php PHPCI_COMPOSER: C:\tools\phpci\composer From d1cf69fa922d6a46cad283e9d347761175ee0b0a Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Sun, 10 Feb 2019 19:04:58 +0000 Subject: [PATCH 05/10] Remove junctions with PHP rather than system rmdir PHP will happily remove junctions using its `rmdir` function (tested on versions back to 5.2.17). This saves invoking system `rmdir` through cmd.exe. --- src/Composer/Util/Filesystem.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 2ebc434b7..8b0546f3a 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -692,9 +692,7 @@ class Filesystem 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)); - clearstatcache(true, $junction); - return ($this->getProcess()->execute($cmd, $output) === 0); + return $this->rmdir($junction); } } From 6212eadcb07349ed946fc8e9a7698f49d20084b7 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Mon, 11 Feb 2019 21:30:56 +0000 Subject: [PATCH 06/10] Only use junctions if they can be safely removed --- src/Composer/Downloader/PathDownloader.php | 30 ++++++++++++++++++++++ src/Composer/Util/Filesystem.php | 17 +++++++----- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index e7084bd97..a63a3d82d 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -82,6 +82,12 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $allowedStrategies = array(self::STRATEGY_MIRROR); } + // Check we can use junctions safely if we are on Windows + if (Platform::isWindows() && self::STRATEGY_SYMLINK === $currentStrategy && !$this->safeJunctions()) { + $currentStrategy = self::STRATEGY_MIRROR; + $allowedStrategies = array(self::STRATEGY_MIRROR); + } + $fileSystem = new Filesystem(); $this->filesystem->removeDirectory($path); @@ -172,4 +178,28 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter return $packageVersion['commit']; } } + + /** + * Returns true if junctions can be safely used on Windows + * + * A PHP bug makes junction detection fragile, leading to possible data loss + * when removing a package. See https://bugs.php.net/bug.php?id=77552 + * + * For safety we require a minimum version of Windows 7, so we can call the + * system rmdir which can detect junctions and not delete target content. + * + * @return bool + */ + private function safeJunctions() + { + // Bug fixed in 7.3.3 and 7.2.16 + if (PHP_VERSION_ID >= 70303 || (PHP_VERSION_ID >= 70216 && PHP_VERSION_ID < 70300)) { + return true; + } + + // Windows 7 is version 6.1 + return function_exists('proc_open') && + (PHP_WINDOWS_VERSION_MAJOR > 6 || + (PHP_WINDOWS_VERSION_MAJOR === 6 && PHP_WINDOWS_VERSION_MINOR >= 1)); + } } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 8b0546f3a..c4e1b1b12 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -650,12 +650,17 @@ class Filesystem * * We test if the path is a directory and not an ordinary link, then check * that the mode value returned from lstat (which gives the status of the - * link itself) is not a directory. + * link itself) is not a directory, by replicating the POSIX S_ISDIR test. * - * This relies on the fact that PHP does not set this value because there is - * no universal file type flag for a junction or a mount point. However a - * bug in PHP can cause a random value to be returned and this could result - * in a junction not being detected: https://bugs.php.net/bug.php?id=77552 + * This logic works because PHP does not set the mode value for a junction, + * since there is no universal file type flag for it. Unfortunately an + * uninitialized variable in PHP prior to 7.2.16 and 7.3.3 may cause a + * random value to be returned. See https://bugs.php.net/bug.php?id=77552 + * + * If this random value passes the S_ISDIR test, then a junction will not be + * detected and a recursive delete operation could lead to loss of data in + * the target directory. Note that Windows rmdir can handle this situation + * and will only delete the junction (from Windows 7 onwards). * * @param string $junction Path to check. * @return bool @@ -673,7 +678,7 @@ class Filesystem clearstatcache(true, $junction); $stat = lstat($junction); - // S_IFDIR is 0x4000, S_IFMT is the 0xF000 bitmask + // S_ISDIR test (S_IFDIR is 0x4000, S_IFMT is 0xF000 bitmask) return $stat ? 0x4000 !== ($stat['mode'] & 0xF000) : false; } From ae9ed20812a2f0d6b2959dc882fcee26746bd361 Mon Sep 17 00:00:00 2001 From: John Stevenson Date: Tue, 12 Feb 2019 12:09:25 +0000 Subject: [PATCH 07/10] Update 05-repositories.md --- doc/05-repositories.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/05-repositories.md b/doc/05-repositories.md index 9cd1dbf28..ba5713aed 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -666,6 +666,10 @@ Instead of default fallback strategy you can force to use symlink with mirroring can be useful when deploying or generating package from a monolithic repository. +> **Note:** On Windows, directory symlinks are implemented using NTFS junctions +> because they can be created by non-admin users. Mirroring will always be used +> on versions below Windows 7 or if `proc_open` has been disabled. + ```json { "repositories": [ From fc2c445c063cc71eafc945e6a19125215b0acb00 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 12 Feb 2019 11:54:23 +0100 Subject: [PATCH 08/10] Make sure we properly usleep() on windows rmdir/unlink usleep() returns void, therefore the previous code didn't work --- src/Composer/Util/Filesystem.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index c4e1b1b12..7c342573b 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -199,9 +199,15 @@ class Filesystem */ public function unlink($path) { - if (!@$this->unlinkImplementation($path)) { + $unlinked = @$this->unlinkImplementation($path); + if (!$unlinked) { // retry after a bit on windows since it tends to be touchy with mass removals - if (!Platform::isWindows() || (usleep(350000) && !@$this->unlinkImplementation($path))) { + if (Platform::isWindows()) { + usleep(350000); + $unlinked = @$this->unlinkImplementation($path); + } + + if (!$unlinked) { $error = error_get_last(); $message = 'Could not delete '.$path.': ' . @$error['message']; if (Platform::isWindows()) { @@ -224,9 +230,15 @@ class Filesystem */ public function rmdir($path) { - if (!@rmdir($path)) { + $deleted = @rmdir($path); + if (!$deleted) { // retry after a bit on windows since it tends to be touchy with mass removals - if (!Platform::isWindows() || (usleep(350000) && !@rmdir($path))) { + if (Platform::isWindows()) { + usleep(350000); + $deleted = !@rmdir($path); + } + + if (!$deleted) { $error = error_get_last(); $message = 'Could not delete '.$path.': ' . @$error['message']; if (Platform::isWindows()) { From 0aa030f09d3fb47b708e39cffeb7470099010f0c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 13 Feb 2019 07:26:14 +0100 Subject: [PATCH 09/10] Fixed typo introduced in recent fix --- src/Composer/Util/Filesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 7c342573b..1903f1c8d 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -235,7 +235,7 @@ class Filesystem // retry after a bit on windows since it tends to be touchy with mass removals if (Platform::isWindows()) { usleep(350000); - $deleted = !@rmdir($path); + $deleted = @rmdir($path); } if (!$deleted) { From d9f873d00e6670f6802441e21538e0e4704128a2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 16 Feb 2019 17:39:59 +0100 Subject: [PATCH 10/10] Abort when HHVM 4.0 is detected to output a clear user message, refs #7990 --- bin/composer | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/composer b/bin/composer index 5e142662c..3585787f2 100755 --- a/bin/composer +++ b/bin/composer @@ -18,6 +18,11 @@ $xdebug = new XdebugHandler('Composer', '--ansi'); $xdebug->check(); unset($xdebug); +if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '4.0', '>=')) { + echo 'HHVM 4.0 has dropped support for Composer, please use PHP instead. Aborting.'.PHP_EOL; + exit(1); +} + if (function_exists('ini_set')) { @ini_set('display_errors', 1);