From 5900c933102a06fbe7e5c4dedead883c807e35ce Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 15 Oct 2023 02:41:46 +1100 Subject: [PATCH] avm: Allow install, list and use from commit (#2659) --- Cargo.lock | 1 + avm/Cargo.toml | 1 + avm/src/lib.rs | 169 ++++++++++++++++++++++++++++++++++++++---------- avm/src/main.rs | 26 ++++++-- 4 files changed, 158 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56d3c2ec67..b09ee023ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -645,6 +645,7 @@ name = "avm" version = "0.28.0" dependencies = [ "anyhow", + "cargo_toml", "cfg-if", "clap 4.4.6", "dirs", diff --git a/avm/Cargo.toml b/avm/Cargo.toml index e8dbcf9103..befbbdf496 100644 --- a/avm/Cargo.toml +++ b/avm/Cargo.toml @@ -22,3 +22,4 @@ reqwest = { version = "0.11.9", default-features = false, features = ["blocking" semver = "1.0.4" serde = { version = "1.0.136", features = ["derive"] } tempfile = "3.3.0" +cargo_toml = "0.15.3" \ No newline at end of file diff --git a/avm/src/lib.rs b/avm/src/lib.rs index 25d03ac168..e5257a3106 100644 --- a/avm/src/lib.rs +++ b/avm/src/lib.rs @@ -1,12 +1,14 @@ use anyhow::{anyhow, Result}; use once_cell::sync::Lazy; use reqwest::header::USER_AGENT; -use semver::Version; +use reqwest::StatusCode; +use semver::{Prerelease, Version}; use serde::{de, Deserialize}; use std::fs; use std::io::Write; use std::path::PathBuf; use std::process::Stdio; +use std::str::FromStr; /// Storage directory for AVM, ~/.avm pub static AVM_HOME: Lazy = Lazy::new(|| { @@ -75,32 +77,97 @@ pub fn use_version(opt_version: Option) -> Result<()> { /// Update to the latest version pub fn update() -> Result<()> { // Find last stable version - let version = &get_latest_version(); + let version = get_latest_version(); - install_version(version, false) + install_anchor(InstallTarget::Version(version), false) +} + +#[derive(Clone)] +pub enum InstallTarget { + Version(Version), + Commit(String), +} + +#[derive(Deserialize)] +struct GetCommitResponse { + sha: String, +} + +/// The commit sha provided can be shortened, +/// +/// returns the full commit sha3 for unique versioning downstream +pub fn check_and_get_full_commit(commit: &str) -> Result { + let client = reqwest::blocking::Client::new(); + let response = client + .get(format!( + "https://api.github.com/repos/coral-xyz/anchor/commits/{commit}" + )) + .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor") + .send() + .unwrap(); + if response.status() != StatusCode::OK { + return Err(anyhow!( + "Error checking commit {commit}: {}", + response.text().unwrap() + )); + }; + let get_commit_response: GetCommitResponse = response.json().unwrap(); + Ok(get_commit_response.sha) +} + +fn get_anchor_version_from_commit(commit: &str) -> Version { + // We read the version from cli/Cargo.toml since there is no simpler way to do so + let client = reqwest::blocking::Client::new(); + let response = client + .get(format!( + "https://raw.githubusercontent.com/coral-xyz/anchor/{}/cli/Cargo.toml", + commit + )) + .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor") + .send() + .unwrap(); + if response.status() != StatusCode::OK { + panic!("Could not find anchor-cli version for commit: {response:?}"); + }; + let anchor_cli_cargo_toml = response.text().unwrap(); + let anchor_cli_manifest = cargo_toml::Manifest::from_str(&anchor_cli_cargo_toml).unwrap(); + let anchor_version = anchor_cli_manifest.package().version(); + let mut version = Version::parse(anchor_version).unwrap(); + version.pre = Prerelease::from_str(commit).unwrap(); + version } /// Install a version of anchor-cli -pub fn install_version(version: &Version, force: bool) -> Result<()> { +pub fn install_anchor(install_target: InstallTarget, force: bool) -> Result<()> { // If version is already installed we ignore the request. let installed_versions = read_installed_versions(); - if installed_versions.contains(version) && !force { + + let mut args: Vec = vec![ + "install".into(), + "--git".into(), + "https://github.com/coral-xyz/anchor".into(), + "anchor-cli".into(), + "--locked".into(), + "--root".into(), + AVM_HOME.to_str().unwrap().into(), + ]; + let version = match install_target { + InstallTarget::Version(version) => { + args.extend(["--tag".into(), format!("v{}", version), "anchor-cli".into()]); + version + } + InstallTarget::Commit(commit) => { + args.extend(["--rev".into(), commit.clone()]); + get_anchor_version_from_commit(&commit) + } + }; + if installed_versions.contains(&version) && !force { println!("Version {version} is already installed"); return Ok(()); } let exit = std::process::Command::new("cargo") - .args([ - "install", - "--git", - "https://github.com/coral-xyz/anchor", - "--tag", - &format!("v{}", &version), - "anchor-cli", - "--locked", - "--root", - AVM_HOME.to_str().unwrap(), - ]) + .args(args) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output() @@ -192,30 +259,37 @@ pub fn fetch_versions() -> Vec { /// Print available versions and flags indicating installed, current and latest pub fn list_versions() -> Result<()> { - let installed_versions = read_installed_versions(); + let mut installed_versions = read_installed_versions(); let mut available_versions = fetch_versions(); // Reverse version list so latest versions are printed last available_versions.reverse(); - available_versions.iter().enumerate().for_each(|(i, v)| { - print!("{v}"); - let mut flags = vec![]; - if i == available_versions.len() - 1 { - flags.push("latest"); - } - if installed_versions.contains(v) { - flags.push("installed"); - } - if current_version().is_ok() && current_version().unwrap() == v.clone() { - flags.push("current"); - } - if flags.is_empty() { - println!(); - } else { - println!("\t({})", flags.join(", ")); - } - }); + let print_versions = + |versions: Vec, installed_versions: &mut Vec, show_latest: bool| { + versions.iter().enumerate().for_each(|(i, v)| { + print!("{v}"); + let mut flags = vec![]; + if i == versions.len() - 1 && show_latest { + flags.push("latest"); + } + if let Some(position) = installed_versions.iter().position(|iv| iv == v) { + flags.push("installed"); + installed_versions.remove(position); + } + + if current_version().is_ok() && current_version().unwrap() == v.clone() { + flags.push("current"); + } + if flags.is_empty() { + println!(); + } else { + println!("\t({})", flags.join(", ")); + } + }) + }; + print_versions(available_versions, &mut installed_versions, true); + print_versions(installed_versions.clone(), &mut installed_versions, false); Ok(()) } @@ -340,4 +414,29 @@ mod tests { fs::File::create(AVM_HOME.join("bin").join("garbage").as_path()).unwrap(); assert!(read_installed_versions() == expected); } + + #[test] + fn test_get_anchor_version_from_commit() { + let version = get_anchor_version_from_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9"); + assert_eq!( + version.to_string(), + "0.28.0-e1afcbf71e0f2e10fae14525934a6a68479167b9" + ) + } + + #[test] + fn test_check_and_get_full_commit_when_full_commit() { + assert_eq!( + check_and_get_full_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9").unwrap(), + "e1afcbf71e0f2e10fae14525934a6a68479167b9" + ) + } + + #[test] + fn test_check_and_get_full_commit_when_partial_commit() { + assert_eq!( + check_and_get_full_commit("e1afcbf").unwrap(), + "e1afcbf71e0f2e10fae14525934a6a68479167b9" + ) + } } diff --git a/avm/src/main.rs b/avm/src/main.rs index f0d8cb682e..2e7d95fd0d 100644 --- a/avm/src/main.rs +++ b/avm/src/main.rs @@ -1,4 +1,5 @@ -use anyhow::{Error, Result}; +use anyhow::{anyhow, Error, Result}; +use avm::InstallTarget; use clap::{Parser, Subcommand}; use semver::Version; @@ -20,8 +21,9 @@ pub enum Commands { }, #[clap(about = "Install a version of Anchor")] Install { - #[clap(value_parser = parse_version)] - version: Version, + /// Anchor version or commit + #[clap(value_parser = parse_install_target)] + version_or_commit: InstallTarget, #[clap(long)] /// Flag to force installation even if the version /// is already installed @@ -46,10 +48,26 @@ fn parse_version(version: &str) -> Result { Version::parse(version).map_err(|e| anyhow::anyhow!(e)) } } + +fn parse_install_target(version_or_commit: &str) -> Result { + parse_version(version_or_commit) + .map(InstallTarget::Version) + .or_else(|version_error| { + avm::check_and_get_full_commit(version_or_commit) + .map(InstallTarget::Commit) + .map_err(|commit_error| { + anyhow!("Not a valid version or commit: {version_error}, {commit_error}") + }) + }) +} + pub fn entry(opts: Cli) -> Result<()> { match opts.command { Commands::Use { version } => avm::use_version(version), - Commands::Install { version, force } => avm::install_version(&version, force), + Commands::Install { + version_or_commit, + force, + } => avm::install_anchor(version_or_commit, force), Commands::Uninstall { version } => avm::uninstall_version(&version), Commands::List {} => avm::list_versions(), Commands::Update {} => avm::update(),