diff --git a/public/scripts/install_cli.ps1 b/public/scripts/install_cli.ps1 index d895bd10..0476c3b0 100644 --- a/public/scripts/install_cli.ps1 +++ b/public/scripts/install_cli.ps1 @@ -21,6 +21,43 @@ $BIN_DIR = "$env:USERPROFILE\.aptoscli\bin" $FORCE = $false $ACCEPT_ALL = $false $VERSION = "" +$UNDO = $false + +# Show usage information +function Show-Usage { + Write-Host @" +Usage: install_cli.ps1 [OPTIONS] + +Installs the latest version of the Aptos CLI on Windows. + +During upgrades, the installer automatically backs up the current binary +so you can roll back with --undo. If the upgrade crosses a major version +boundary (e.g. v1.x.x -> v2.x.x), a warning is displayed with a link to +the CHANGELOG for breaking changes: + https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md + +OPTIONS: + -f, --force Install even if the same version is already installed + -y, --yes Accept all prompts automatically + --bin-dir DIR Install the CLI binary to DIR + (default: %USERPROFILE%\.aptoscli\bin) + --cli-version VER Install a specific version instead of the latest + --undo Restore the previous CLI version from the backup + created during the last upgrade. Only one backup is + kept at a time. No network access is required. + -h, --help Show this help message and exit + +EXAMPLES: + # Install the latest version + .\install_cli.ps1 + + # Install a specific version + .\install_cli.ps1 --cli-version 3.5.0 + + # Roll back to the previously installed version + .\install_cli.ps1 --undo +"@ +} # Print colored message function Write-ColorMessage { @@ -44,6 +81,60 @@ function Test-CommandExists { return [bool](Get-Command -Name $Command -ErrorAction SilentlyContinue) } +# Check for major version upgrade and warn user +function Test-MajorVersionUpgrade { + param( + [string]$OldVersion, + [string]$NewVersion + ) + + if (-not $OldVersion -or -not $NewVersion) { + return + } + + $oldMajor = $OldVersion.Split('.')[0] + $newMajor = $NewVersion.Split('.')[0] + + if ($oldMajor -ne $newMajor) { + Write-Host "" + Write-ColorMessage -Color $YELLOW -Message "WARNING: This is a major version upgrade (v${oldMajor}.x.x -> v${newMajor}.x.x)." + Write-ColorMessage -Color $YELLOW -Message "Major version upgrades may include breaking changes." + Write-ColorMessage -Color $YELLOW -Message "Please review the CHANGELOG before continuing:" + Write-ColorMessage -Color $CYAN -Message " https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md" + Write-Host "" + } +} + +# Backup the current CLI binary before overwriting (keeps only one backup) +function Backup-CurrentBinary { + $cliPath = Join-Path $BIN_DIR $SCRIPT + if (Test-Path $cliPath) { + $backupPath = "$cliPath.backup" + Write-ColorMessage -Color $CYAN -Message "Backing up current CLI binary to $backupPath..." + Copy-Item -Path $cliPath -Destination $backupPath -Force + Write-ColorMessage -Color $GREEN -Message "Backup complete. Use --undo to restore the previous version." + } +} + +# Restore the previously backed up CLI binary +function Undo-Upgrade { + $cliPath = Join-Path $BIN_DIR $SCRIPT + $backupPath = "$cliPath.backup" + if (-not (Test-Path $backupPath)) { + Die "No backup found at $backupPath. Nothing to undo." + } + + Write-ColorMessage -Color $CYAN -Message "Restoring previous CLI version..." + Move-Item -Path $backupPath -Destination $cliPath -Force + + $restoredVersion = (& $cliPath --version | Select-String -Pattern '\d+\.\d+\.\d+').Matches.Value + if ($restoredVersion) { + Write-ColorMessage -Color $GREEN -Message "Successfully restored Aptos CLI version $restoredVersion." + } else { + Write-ColorMessage -Color $GREEN -Message "Previous CLI version restored." + } +} + # Get the latest version from GitHub API function Get-LatestVersion { try { @@ -94,6 +185,9 @@ function Install-CLI { curl.exe -L $url -o $zipPath } + # Backup current binary before overwriting + Backup-CurrentBinary + # Extract the zip file Expand-Archive -Path $zipPath -DestinationPath $BIN_DIR -Force @@ -134,11 +228,26 @@ function Main { Die "No version specified for --cli-version" } } + '--undo' { $UNDO = $true } + '-h' { + Show-Usage + return + } + '--help' { + Show-Usage + return + } default { - Die "Unknown option: $($args[$i])" + Die "Unknown option: $($args[$i]). Use --help for usage information." } } } + + # Handle undo (no network needed) + if ($UNDO) { + Undo-Upgrade + return + } # Get version if not specified if (-not $VERSION) { @@ -156,6 +265,7 @@ function Main { Write-ColorMessage -Color $YELLOW -Message "Aptos CLI version $VERSION is already installed." return } + Test-MajorVersionUpgrade -OldVersion $currentVersion -NewVersion $VERSION } # Install the CLI diff --git a/public/scripts/install_cli.py b/public/scripts/install_cli.py index cbd141b1..bf471fe7 100644 --- a/public/scripts/install_cli.py +++ b/public/scripts/install_cli.py @@ -248,12 +248,14 @@ def __init__( accept_all: bool = False, bin_dir: Optional[str] = None, from_source: bool = False, + undo: bool = False, ) -> None: self._version = version self._force = force self._accept_all = accept_all self._bin_dir = Path(bin_dir).expanduser() if bin_dir else None self._from_source = from_source + self._undo = undo self._release_info = None self._latest_release_info = None @@ -268,6 +270,10 @@ def bin_dir(self) -> Path: def bin_path(self): return self.bin_dir.joinpath(SCRIPT) + @property + def backup_path(self): + return self.bin_dir.joinpath(SCRIPT + ".backup") + @property def release_info(self): if not self._release_info: @@ -283,6 +289,10 @@ def latest_release_info(self): raise RuntimeError("Failed to find latest CLI release") def run(self) -> int: + # Handle undo (no network needed) + if self._undo: + return self._undo_upgrade() + # Handle installation from source if self._from_source: return self.run_from_source() @@ -322,6 +332,10 @@ def install(self, version, target): self._install_comment(version, "Downloading...") self.bin_dir.mkdir(parents=True, exist_ok=True) + + # Backup current binary before overwriting + self._backup_current_binary() + if self.bin_path.exists(): self.bin_path.unlink() @@ -441,6 +455,80 @@ def display_post_message_unix(self, version: str) -> None: ) ) + def _backup_current_binary(self) -> None: + """Backup the current CLI binary before overwriting (keeps only one backup).""" + if self.bin_path.exists(): + self._write( + colorize("info", f"Backing up current CLI binary to {self.backup_path}...") + ) + shutil.copy2(self.bin_path, self.backup_path) + self._write( + colorize("success", "Backup complete. Use --undo to restore the previous version.") + ) + + def _undo_upgrade(self) -> int: + """Restore the previously backed up CLI binary.""" + if not self.backup_path.exists(): + self._write( + colorize("error", f"No backup found at {self.backup_path}. Nothing to undo.") + ) + return 1 + + self._write(colorize("info", "Restoring previous CLI version...")) + + # Replace current binary with backup + if self.bin_path.exists(): + self.bin_path.unlink() + shutil.move(str(self.backup_path), str(self.bin_path)) + os.chmod(self.bin_path, 0o755) + + try: + out = subprocess.check_output( + [self.bin_path, "--version"], + universal_newlines=True, + ) + restored_version = out.split(" ")[-1].rstrip().lstrip() + self._write( + colorize("success", f"Successfully restored Aptos CLI version {restored_version}.") + ) + except Exception: + self._write(colorize("success", "Previous CLI version restored.")) + + return 0 + + def _warn_major_upgrade(self, current_version: str, new_version: str) -> None: + """Warn the user if this is a major version upgrade.""" + if not current_version or not new_version: + return + + try: + current_major = current_version.split(".")[0] + new_major = new_version.split(".")[0] + except (IndexError, AttributeError): + return + + if current_major != new_major: + self._write("") + self._write( + colorize( + "warning", + f"WARNING: This is a major version upgrade (v{current_major}.x.x -> v{new_major}.x.x).", + ) + ) + self._write( + colorize("warning", "Major version upgrades may include breaking changes.") + ) + self._write( + colorize("warning", "Please review the CHANGELOG before continuing:") + ) + self._write( + colorize( + "info", + " https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md", + ) + ) + self._write("") + def get_version(self): if self._version: version_to_install = self._version @@ -484,6 +572,8 @@ def get_version(self): return None, current_version else: + if current_version: + self._warn_major_upgrade(current_version, version_to_install) self._write(f"Installing {colorize('b', version_to_install)}") return version_to_install, current_version @@ -772,6 +862,7 @@ def install_from_source(self, version: Optional[str] = None) -> int: if current_version == latest_version: self._write(colorize("warning", f"Aptos CLI version {latest_version} is already installed.")) return 0 + self._warn_major_upgrade(current_version, latest_version) except (FileNotFoundError, PermissionError, subprocess.CalledProcessError, OSError): pass # CLI not installed, proceed @@ -826,6 +917,9 @@ def install_from_source(self, version: Optional[str] = None) -> int: # Move the binary to the bin directory dest_binary = self.bin_path try: + # Backup current binary before overwriting + self._backup_current_binary() + if dest_binary.exists(): dest_binary.unlink() shutil.copy2(source_binary, dest_binary) @@ -876,6 +970,7 @@ def run_from_source(self) -> int: if current_version == version: self._write(colorize("warning", f"Aptos CLI version {version} is already installed.")) return 0 + self._warn_major_upgrade(current_version, version) except (FileNotFoundError, PermissionError, subprocess.CalledProcessError, OSError): # CLI not installed or not runnable, proceed with installation pass @@ -892,34 +987,58 @@ def main(): return 1 parser = argparse.ArgumentParser( - description="Installs the latest version of the Aptos CLI" + description="Installs the latest version of the Aptos CLI.", + epilog=( + "UPGRADE BEHAVIOR:\n" + " During upgrades, the installer automatically backs up the current\n" + " binary so you can roll back with --undo. If the upgrade crosses a\n" + " major version boundary (e.g. v1.x.x -> v2.x.x), a warning is\n" + " displayed with a link to the CHANGELOG for breaking changes:\n" + " https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md\n" + "\n" + "EXAMPLES:\n" + " python install_cli.py # Install latest version\n" + " python install_cli.py --cli-version 3.5.0 # Install specific version\n" + " python install_cli.py --undo # Roll back last upgrade\n" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "-f", "--force", - help="Forcibly install on top of existing version", + help="install even if the same version is already installed", action="store_true", default=False, ) parser.add_argument( "-y", "--yes", - help="Accept all prompts", + help="accept all prompts automatically", dest="accept_all", action="store_true", default=False, ) parser.add_argument( "--bin-dir", - help="If given, the CLI binary will be downloaded here instead", + help="install the CLI binary to this directory instead of the default", ) parser.add_argument( "--cli-version", - help="If given, the CLI version to install", + help="install a specific CLI version instead of the latest", ) parser.add_argument( "--from-source", - help="Build and install from source instead of downloading pre-built binary", + help="build and install from source instead of downloading a pre-built binary (requires git)", + action="store_true", + default=False, + ) + parser.add_argument( + "--undo", + help=( + "restore the previous CLI version from the backup created during " + "the last upgrade. Only one backup is kept at a time. " + "No network access is required" + ), action="store_true", default=False, ) @@ -932,6 +1051,7 @@ def main(): bin_dir=args.bin_dir, version=args.cli_version, from_source=args.from_source, + undo=args.undo, ) try: diff --git a/public/scripts/install_cli.sh b/public/scripts/install_cli.sh index 8cb1345e..3d1353e6 100755 --- a/public/scripts/install_cli.sh +++ b/public/scripts/install_cli.sh @@ -23,9 +23,49 @@ ACCEPT_ALL=false VERSION="" GENERIC_LINUX=false FROM_SOURCE=false +UNDO=false UNIVERSAL_INSTALLER_URL="https://raw.githubusercontent.com/gregnazario/universal-installer/main/scripts/install_pkg.sh" APTOS_REPO_URL="https://github.com/aptos-labs/aptos-core.git" +# Show usage information +show_usage() { + cat < v2.x.x), a warning is displayed with a link to +the CHANGELOG for breaking changes: + https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md + +OPTIONS: + -f, --force Install even if the same version is already installed + -y, --yes Accept all prompts automatically + --bin-dir DIR Install the CLI binary to DIR (default: ~/.local/bin) + --cli-version VER Install a specific version instead of the latest + --generic-linux Use the generic Linux binary instead of a + distro-specific build + --from-source Build and install from source instead of downloading + a pre-built binary (requires git) + --undo Restore the previous CLI version from the backup + created during the last upgrade. Only one backup is + kept at a time. No network access is required. + -h, --help Show this help message and exit + +EXAMPLES: + # Install the latest version + curl -fsSL https://aptos.dev/scripts/install_cli.sh | sh + + # Install a specific version + curl -fsSL https://aptos.dev/scripts/install_cli.sh | sh -s -- --cli-version 3.5.0 + + # Roll back to the previously installed version + curl -fsSL https://aptos.dev/scripts/install_cli.sh | sh -s -- --undo +EOF +} + # Print colored message print_message() { color=$1 @@ -105,6 +145,57 @@ validate_version() { fi } +# Check for major version upgrade and warn user +warn_major_upgrade() { + old_version=$1 + new_version=$2 + + if [ -z "$old_version" ] || [ -z "$new_version" ]; then + return + fi + + old_major=$(echo "$old_version" | cut -d. -f1) + new_major=$(echo "$new_version" | cut -d. -f1) + + if [ "$old_major" != "$new_major" ]; then + printf "\n" + print_message "$YELLOW" "WARNING: This is a major version upgrade (v${old_major}.x.x -> v${new_major}.x.x)." + print_message "$YELLOW" "Major version upgrades may include breaking changes." + print_message "$YELLOW" "Please review the CHANGELOG before continuing:" + print_message "$CYAN" " https://github.com/aptos-labs/aptos-core/blob/main/crates/aptos/CHANGELOG.md" + printf "\n" + fi +} + +# Backup the current CLI binary before overwriting (keeps only one backup) +backup_current_binary() { + if [ -x "$BIN_DIR/$SCRIPT" ]; then + backup_path="$BIN_DIR/$SCRIPT.backup" + print_message "$CYAN" "Backing up current CLI binary to $backup_path..." + cp "$BIN_DIR/$SCRIPT" "$backup_path" || die "Failed to backup current binary" + print_message "$GREEN" "Backup complete. Use --undo to restore the previous version." + fi +} + +# Restore the previously backed up CLI binary +undo_upgrade() { + backup_path="$BIN_DIR/$SCRIPT.backup" + if [ ! -f "$backup_path" ]; then + die "No backup found at $backup_path. Nothing to undo." + fi + + print_message "$CYAN" "Restoring previous CLI version..." + mv "$backup_path" "$BIN_DIR/$SCRIPT" || die "Failed to restore backup" + chmod +x "$BIN_DIR/$SCRIPT" + + if "$BIN_DIR/$SCRIPT" --version >/dev/null 2>&1; then + restored_version=$("$BIN_DIR/$SCRIPT" --version 2>/dev/null | awk '{print $NF}') + print_message "$GREEN" "Successfully restored Aptos CLI version $restored_version." + else + print_message "$GREEN" "Previous CLI version restored." + fi +} + # Sort version tags - with fallback for systems without GNU sort -V sort_version_tags() { # Try GNU sort -V first, fall back to basic sort if not available @@ -163,6 +254,9 @@ install_from_source() { print_message "$GREEN" "Aptos CLI version $version is already installed. Use --force to rebuild." exit 0 fi + if [ -n "$installed_version" ]; then + warn_major_upgrade "$installed_version" "$version" + fi fi print_message "$CYAN" "Checking out $latest_tag..." @@ -186,6 +280,7 @@ install_from_source() { # Move the binary to the bin directory if [ -f "target/release/aptos" ]; then + backup_current_binary mv "target/release/aptos" "$BIN_DIR/" || die "Failed to move binary to $BIN_DIR" chmod +x "$BIN_DIR/aptos" print_message "$GREEN" "Aptos CLI version $version installed successfully from source!" @@ -300,6 +395,9 @@ install_cli() { die "unzip is not installed. Please install it." fi + # Backup current binary before overwriting + backup_current_binary + # Move the binary to the bin directory mv "$tmp_dir/aptos" "$BIN_DIR/" chmod +x "$BIN_DIR/aptos" @@ -309,9 +407,6 @@ install_cli() { # Main installation process main() { - # Install required packages first - install_required_packages - # Parse command line arguments while [ $# -gt 0 ]; do case "$1" in @@ -339,11 +434,28 @@ main() { FROM_SOURCE=true shift ;; + --undo) + UNDO=true + shift + ;; + -h|--help) + show_usage + exit 0 + ;; *) - die "Unknown option: $1" + die "Unknown option: $1. Use --help for usage information." ;; esac done + + # Handle undo (no packages or network needed) + if [ "$UNDO" = true ]; then + undo_upgrade + exit 0 + fi + + # Install required packages + install_required_packages # Handle installation from source if [ "$FROM_SOURCE" = true ]; then @@ -356,6 +468,7 @@ main() { print_message "$YELLOW" "Aptos CLI version $VERSION is already installed." exit 0 fi + warn_major_upgrade "$current_version" "$VERSION" fi fi @@ -376,6 +489,7 @@ main() { print_message "$YELLOW" "Aptos CLI version $VERSION is already installed." exit 0 fi + warn_major_upgrade "$current_version" "$VERSION" fi # Install the CLI