From 6ead35f189f73f236c69755893181d7b77c90840 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 26 Dec 2013 19:09:06 +0100 Subject: [PATCH] Add version arg, docs for --rollback and reorganize the code, refs #2522 --- doc/03-cli.md | 11 +- src/Composer/Command/SelfUpdateCommand.php | 195 ++++++++++----------- 2 files changed, 105 insertions(+), 101 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index 38db3f925..f3fa574fe 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -269,11 +269,20 @@ command. It will replace your `composer.phar` with the latest version. $ php composer.phar self-update +If you would like to instead update to a specific release simply specify it: + + $ composer self-update 1.0.0-alpha7 + If you have installed composer for your entire system (see [global installation](00-intro.md#globally)), -you have to run the command with `root` privileges +you may have to run the command with `root` privileges $ sudo composer self-update +### Options + +* **--rollback (-r):** Rollback to the last version you had installed. +* **--clean-backups:** Delete old backups during an update. This makes the current version of composer the only backup available after the update. + ## config The `config` command allows you to edit some basic composer settings in either diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index d29d50b36..b6830650d 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -19,32 +19,19 @@ use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** * @author Igor Wiedler + * @author Kevin Ran + * @author Jordi Boggiano */ 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 @@ -52,8 +39,9 @@ class SelfUpdateCommand extends Command ->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') + new InputOption('rollback', 'r', InputOption::VALUE_NONE, 'Revert to an older installation of composer'), + new InputOption('clean-backups', null, InputOption::VALUE_NONE, 'Delete old backups during an update. This makes the current version of composer the only backup available after the update'), + new InputArgument('version', InputArgument::OPTIONAL, 'The version to update to'), )) ->setHelp(<<self-update command checks getcomposer.org for newer @@ -68,122 +56,140 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { + $baseUrl = (extension_loaded('openssl') ? 'https' : 'http') . '://' . self::HOMEPAGE; + $remoteFilesystem = new RemoteFilesystem($this->getIO()); $config = Factory::createConfig(); - $cacheDir = rtrim($config->get('cache-dir'), '/'); + $cacheDir = $config->get('cache-dir'); + $rollbackDir = $config->get('home'); + $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; - // Check if current dir is writable and if not try the cache dir from settings - $tmpDir = is_writable(dirname($this->localFilename))? dirname($this->localFilename) : $cacheDir; + // check if current dir is writable and if not try the cache dir from settings + $tmpDir = is_writable(dirname($localFilename)) ? dirname($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($this->localFilename)) { - throw new FilesystemException('Composer update failed: the "'.$this->localFilename.'" file could not be written'); + if (!is_writable($localFilename)) { + throw new FilesystemException('Composer update failed: the "'.$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 ($input->getOption('rollback')) { + return $this->rollback($output, $rollbackDir, $localFilename); } - // 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'); - } + $latestVersion = trim($remoteFilesystem->getContents(self::HOMEPAGE, $baseUrl. '/version', false)); + $updateVersion = $input->getArgument('version') ?: $latestVersion; - $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; + if (preg_match('{^[0-9a-f]{40}$}', $updateVersion) && $updateVersion !== $latestVersion) { + $output->writeln('You can not update to a specific SHA-1 as those phars are not available for download'); - 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'); - } + return 1; } - $updateVersion = ($rollbackVersion)? $rollbackVersion : $this->getLatestVersion(); - if (Composer::VERSION === $updateVersion) { $output->writeln('You are already using composer version '.$updateVersion.'.'); return 0; } - $tempFilename = $tmpDir . '/' . basename($this->localFilename, '.phar').'-temp.phar'; - $backupFile = ($rollbackVersion)? false : $rollbackDir . '/' . Composer::VERSION . self::OLD_INSTALL_EXT; + $tempFilename = $tmpDir . '/' . basename($localFilename, '.phar').'-temp.phar'; + $backupFile = sprintf( + '%s/%s-%s%s', + $rollbackDir, + strtr(Composer::RELEASE_DATE, ' :', '_-'), + preg_replace('{^([0-9a-f]{7})[0-9a-f]{33}$}', '$1', Composer::VERSION), + self::OLD_INSTALL_EXT + ); - 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; + $output->writeln(sprintf("Updating to version %s.", $updateVersion)); + $remoteFilename = $baseUrl . (preg_match('{^[0-9a-f]{40}$}', $updateVersion) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"); + $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename); + if (!file_exists($tempFilename)) { + $output->writeln('The download of the new composer version failed for an unexpected reason'); - $output->writeln(sprintf("Updating to version %s.", $updateVersion)); + return 1; + } - $this->remoteFS->copy(self::HOMEPAGE, $remoteFilename, $tempFilename); + // remove saved installations of composer + if ($input->getOption('clean-backups')) { + $files = $this->getOldInstallationFiles($rollbackDir); - // @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'); + if (!empty($files)) { + $fs = new Filesystem; - return 1; - } - - // 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); - } + foreach ($files as $file) { + $output->writeln('Removing: '.$file); + $fs->remove($file); } } } - if ($err = $this->setLocalPhar($tempFilename, $backupFile)) { + if ($err = $this->setLocalPhar($localFilename, $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); + if (file_exists($backupFile)) { + $output->writeln('Use composer self-update --rollback to return to version '.Composer::VERSION); + } else { + $output->writeln('A backup of the current version could not be written to '.$backupFile.', no rollback possible'); } } - protected function setLocalPhar($filename, $backupFile) + protected function rollback(OutputInterface $output, $rollbackDir, $localFilename) + { + $rollbackVersion = $this->getLastBackupVersion($rollbackDir); + if (!$rollbackVersion) { + throw new \UnexpectedValueException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); + } + + 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'); + } + + $oldFile = $rollbackDir . "/{$rollbackVersion}" . self::OLD_INSTALL_EXT; + $output->writeln(sprintf("Rolling back to version %s.", $rollbackVersion)); + if ($err = $this->setLocalPhar($localFilename, $oldFile)) { + $output->writeln('The backup file was corrupted ('.$err->getMessage().') and has been removed.'); + + return 1; + } + + return 0; + } + + protected function setLocalPhar($localFilename, $newFilename, $backupTarget = null) { try { - @chmod($filename, 0777 & ~umask()); + @chmod($newFilename, 0777 & ~umask()); // test the phar validity - $phar = new \Phar($filename); + $phar = new \Phar($newFilename); // free the variable to unlock the file unset($phar); // copy current file into installations dir - if ($backupFile) { - copy($this->localFilename, $backupFile); + if ($backupTarget && file_exists($localFilename)) { + @copy($localFilename, $backupTarget); } unset($phar); - rename($filename, $this->localFilename); + rename($newFilename, $localFilename); } catch (\Exception $e) { - @unlink($filename); + if ($backupTarget) { + @unlink($newFilename); + } if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { throw $e; } @@ -192,31 +198,20 @@ EOT } } - protected function getLastVersion($rollbackDir) + protected function getLastBackupVersion($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); + sort($files); + + return basename(end($files), 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; + return glob($rollbackDir . '/*' . self::OLD_INSTALL_EXT) ?: array(); } }