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/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 2ebc434b7..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()) { @@ -650,12 +662,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 +690,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; } @@ -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); } }