diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 4074d8bad..d29d50b36 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -14,9 +14,11 @@ namespace Composer\Command; use Composer\Composer; use Composer\Factory; +use Composer\Util\Filesystem; use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -24,12 +26,35 @@ use Symfony\Component\Console\Output\OutputInterface; */ class SelfUpdateCommand extends Command { + const ROLLBACK = 'rollback'; + const CLEAN_ROLLBACKS = 'clean-rollbacks'; + const HOMEPAGE = 'getcomposer.org'; + const OLD_INSTALL_EXT = '-old.phar'; + + protected $remoteFS; + protected $latestVersion; + protected $homepageURL; + protected $localFilename; + + public function __construct($name = null) + { + parent::__construct($name); + $protocol = (extension_loaded('openssl') ? 'https' : 'http') . '://'; + $this->homepageURL = $protocol . self::HOMEPAGE; + $this->remoteFS = new RemoteFilesystem($this->getIO()); + $this->localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; + } + protected function configure() { $this ->setName('self-update') ->setAliases(array('selfupdate')) ->setDescription('Updates composer.phar to the latest version.') + ->setDefinition(array( + new InputOption(self::ROLLBACK, 'r', InputOption::VALUE_NONE, 'Revert to an older installation of composer'), + new InputOption(self::CLEAN_ROLLBACKS, null, InputOption::VALUE_NONE, 'Delete old snapshots during an update. This makes the current version of composer the only rollback snapshot after the update') + )) ->setHelp(<<self-update command checks getcomposer.org for newer versions of composer and if found, installs the latest. @@ -44,57 +69,154 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { $config = Factory::createConfig(); - $cacheDir = $config->get('cache-dir'); - - $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; + $cacheDir = rtrim($config->get('cache-dir'), '/'); // Check if current dir is writable and if not try the cache dir from settings - $tmpDir = is_writable(dirname($localFilename))? dirname($localFilename) : $cacheDir; - $tempFilename = $tmpDir . '/' . basename($localFilename, '.phar').'-temp.phar'; + $tmpDir = is_writable(dirname($this->localFilename))? dirname($this->localFilename) : $cacheDir; // check for permissions in local filesystem before start connection process if (!is_writable($tmpDir)) { throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written'); } - if (!is_writable($localFilename)) { - throw new FilesystemException('Composer update failed: the "'.$localFilename. '" file could not be written'); + if (!is_writable($this->localFilename)) { + throw new FilesystemException('Composer update failed: the "'.$this->localFilename.'" file could not be written'); + } + + $rollbackVersion = false; + $rollbackDir = rtrim($config->get('home'), '/'); + + // rollback specified, get last phar + if ($input->getOption(self::ROLLBACK)) { + $rollbackVersion = $this->getLastVersion($rollbackDir); + if (!$rollbackVersion) { + throw new FilesystemException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); + } + } + + // if a rollback version is specified, check for permissions and rollback installation + if ($rollbackVersion) { + if (!is_writable($rollbackDir)) { + throw new FilesystemException('Composer rollback failed: the "'.$rollbackDir.'" dir could not be written to'); + } + + $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; + + if (!is_file($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be found'); + } + if (!is_readable($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be read'); + } + } + + $updateVersion = ($rollbackVersion)? $rollbackVersion : $this->getLatestVersion(); + + if (Composer::VERSION === $updateVersion) { + $output->writeln('You are already using composer version '.$updateVersion.'.'); + + return 0; } - $protocol = extension_loaded('openssl') ? 'https' : 'http'; - $rfs = new RemoteFilesystem($this->getIO()); - $latest = trim($rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/version', false)); + $tempFilename = $tmpDir . '/' . basename($this->localFilename, '.phar').'-temp.phar'; + $backupFile = ($rollbackVersion)? false : $rollbackDir . '/' . Composer::VERSION . self::OLD_INSTALL_EXT; - if (Composer::VERSION !== $latest) { - $output->writeln(sprintf("Updating to version %s.", $latest)); + if ($rollbackVersion) { + rename($rollbackDir . "/{$rollbackVersion}" . self::OLD_INSTALL_EXT, $tempFilename); + $output->writeln(sprintf("Rolling back to cached version %s.", $rollbackVersion)); + } else { + $endpoint = ($updateVersion === $this->getLatestVersion()) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"; + $remoteFilename = $this->homepageURL . $endpoint; - $remoteFilename = $protocol . '://getcomposer.org/composer.phar'; + $output->writeln(sprintf("Updating to version %s.", $updateVersion)); - $rfs->copy('getcomposer.org', $remoteFilename, $tempFilename); + $this->remoteFS->copy(self::HOMEPAGE, $remoteFilename, $tempFilename); + // @todo: handle snapshot versions not being found! if (!file_exists($tempFilename)) { $output->writeln('The download of the new composer version failed for an unexpected reason'); return 1; } - try { - @chmod($tempFilename, 0777 & ~umask()); - // test the phar validity - $phar = new \Phar($tempFilename); - // free the variable to unlock the file - unset($phar); - rename($tempFilename, $localFilename); - } catch (\Exception $e) { - @unlink($tempFilename); - if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { - throw $e; + // remove saved installations of composer + if ($input->getOption(self::CLEAN_ROLLBACKS)) { + $files = $this->getOldInstallationFiles($rollbackDir); + + if (!empty($files)) { + $fs = new Filesystem; + + foreach ($files as $file) { + $output->writeln('Removing: '.$file); + $fs->remove($file); + } } - $output->writeln('The download is corrupted ('.$e->getMessage().').'); - $output->writeln('Please re-run the self-update command to try again.'); } - } else { - $output->writeln("You are using the latest composer version."); } + + if ($err = $this->setLocalPhar($tempFilename, $backupFile)) { + $output->writeln('The file is corrupted ('.$err->getMessage().').'); + $output->writeln('Please re-run the self-update command to try again.'); + + return 1; + } + + if ($backupFile) { + $output->writeln('Saved rollback snapshot '.$backupFile); + } + } + + protected function setLocalPhar($filename, $backupFile) + { + try { + @chmod($filename, 0777 & ~umask()); + // test the phar validity + $phar = new \Phar($filename); + // free the variable to unlock the file + unset($phar); + + // copy current file into installations dir + if ($backupFile) { + copy($this->localFilename, $backupFile); + } + + unset($phar); + rename($filename, $this->localFilename); + } catch (\Exception $e) { + @unlink($filename); + if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { + throw $e; + } + + return $e; + } + } + + protected function getLastVersion($rollbackDir) + { + $files = $this->getOldInstallationFiles($rollbackDir); + + if (empty($files)) { + return false; + } + + $fileTimes = array_map('filemtime', $files); + $map = array_combine($fileTimes, $files); + $latest = max($fileTimes); + return basename($map[$latest], self::OLD_INSTALL_EXT); + } + + protected function getOldInstallationFiles($rollbackDir) + { + return glob($rollbackDir . '/*' . self::OLD_INSTALL_EXT); + } + + protected function getLatestVersion() + { + if (!$this->latestVersion) { + $this->latestVersion = trim($this->remoteFS->getContents(self::HOMEPAGE, $this->homepageURL. '/version', false)); + } + + return $this->latestVersion; } }