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 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 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); 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": [ diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index ea583cfc0..aa8abc5c9 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -91,6 +91,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); @@ -181,4 +187,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 ebb7dfbd3..1903f1c8d 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()) { @@ -648,6 +660,20 @@ 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, by replicating the POSIX S_ISDIR test. + * + * 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 */ @@ -659,22 +685,13 @@ 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 all caches first clearstatcache(true, $junction); - clearstatcache(false); $stat = lstat($junction); - return !($stat['mode'] & 0xC000); + // S_ISDIR test (S_IFDIR is 0x4000, S_IFMT is 0xF000 bitmask) + return $stat ? 0x4000 !== ($stat['mode'] & 0xF000) : false; } /** @@ -692,9 +709,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); } }