From 75469f423cbfe2527a52156ec3131630b6c193e8 Mon Sep 17 00:00:00 2001 From: Tom Linton Date: Fri, 4 Feb 2022 13:13:40 +1300 Subject: [PATCH] cli: anchor version manager (avm) (#1385) --- CHANGELOG.md | 1 + Cargo.lock | 196 ++++++++++++++++------------ Cargo.toml | 1 + avm/Cargo.toml | 26 ++++ avm/src/anchor/main.rs | 24 ++++ avm/src/lib.rs | 287 +++++++++++++++++++++++++++++++++++++++++ avm/src/main.rs | 58 +++++++++ 7 files changed, 509 insertions(+), 84 deletions(-) create mode 100644 avm/Cargo.toml create mode 100644 avm/src/anchor/main.rs create mode 100644 avm/src/lib.rs create mode 100644 avm/src/main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 620e3688..2477edcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ incremented for features. * ts: Add new `methods` namespace to the program client, introducing a more ergonomic builder API ([#1324](https://github.com/project-serum/anchor/pull/1324)). * ts: Add registry utility for fetching the latest verified build ([#1371](https://github.com/project-serum/anchor/pull/1371)). * cli: Expose the solana-test-validator --account flag in Anchor.toml via [[test.validator.account]] ([#1366](https://github.com/project-serum/anchor/pull/1366)). +* cli: Add avm, a tool for managing anchor-cli versions ([#1385](https://github.com/project-serum/anchor/pull/1385)). ### Breaking diff --git a/Cargo.lock b/Cargo.lock index f0335dc9..e099f57b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,11 +154,11 @@ dependencies = [ "cargo_toml", "chrono", "clap 3.0.13", - "dirs", + "dirs 3.0.2", "flate2", "heck 0.3.3", "pathdiff", - "rand 0.7.3", + "rand", "reqwest", "semver 1.0.4", "serde", @@ -301,6 +301,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "avm" +version = "0.1.0" +dependencies = [ + "anyhow", + "cfg-if 1.0.0", + "clap 3.0.13", + "dialoguer 0.9.0", + "dirs 4.0.0", + "once_cell", + "reqwest", + "semver 1.0.4", + "serde", + "serde_json", + "tempfile", + "thiserror", +] + [[package]] name = "backtrace" version = "0.3.63" @@ -959,6 +977,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "dialoguer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" +dependencies = [ + "console 0.15.0", + "lazy_static", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.8.1" @@ -995,6 +1025,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1074,7 +1113,7 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", "ed25519", - "rand 0.7.3", + "rand", "serde", "serde_bytes", "sha2", @@ -1197,6 +1236,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "feature-probe" version = "0.1.1" @@ -1430,9 +1478,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "h2" -version = "0.3.7" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" dependencies = [ "bytes 1.1.0", "fnv", @@ -1552,7 +1600,7 @@ checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes 1.1.0", "fnv", - "itoa", + "itoa 0.4.8", ] [[package]] @@ -1599,7 +1647,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 0.4.8", "pin-project-lite", "socket2 0.4.2", "tokio", @@ -1610,17 +1658,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "futures-util", + "http", "hyper", - "log", "rustls", "tokio", "tokio-rustls", - "webpki", ] [[package]] @@ -1717,6 +1763,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jobserver" version = "0.1.24" @@ -1794,7 +1846,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.7.3", + "rand", "serde", "sha2", "typenum", @@ -2390,21 +2442,9 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha 0.2.2", + "rand_chacha", "rand_core 0.5.1", - "rand_hc 0.2.0", -] - -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", - "rand_hc 0.3.1", + "rand_hc", ] [[package]] @@ -2417,16 +2457,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.3", -] - [[package]] name = "rand_core" version = "0.5.1" @@ -2441,9 +2471,6 @@ name = "rand_core" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom 0.2.3", -] [[package]] name = "rand_hc" @@ -2454,15 +2481,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.3", -] - [[package]] name = "rayon" version = "1.5.1" @@ -2541,15 +2559,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.6" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" dependencies = [ "base64 0.13.0", "bytes 1.1.0", "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "hyper", @@ -2565,6 +2584,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", @@ -2636,17 +2656,25 @@ dependencies = [ [[package]] name = "rustls" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" dependencies = [ - "base64 0.13.0", "log", "ring", "sct", "webpki", ] +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64 0.13.0", +] + [[package]] name = "rustversion" version = "1.0.5" @@ -2692,9 +2720,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -2764,9 +2792,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] @@ -2782,9 +2810,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2 1.0.32", "quote 1.0.10", @@ -2793,11 +2821,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.71" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" dependencies = [ - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -2809,7 +2837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", - "itoa", + "itoa 0.4.8", "ryu", "serde", ] @@ -2846,7 +2874,7 @@ dependencies = [ "arrayref", "bincode", "bs58 0.3.1", - "rand 0.7.3", + "rand", "serde", "serde_json", "serum-borsh", @@ -3099,7 +3127,7 @@ dependencies = [ "either", "lazy_static", "libc", - "rand_chacha 0.2.2", + "rand_chacha", "regex-syntax", "reqwest", "ring", @@ -3219,7 +3247,7 @@ dependencies = [ "clap 2.33.3", "log", "nix", - "rand 0.7.3", + "rand", "serde", "serde_derive", "socket2 0.3.19", @@ -3246,7 +3274,7 @@ dependencies = [ "libc", "log", "nix", - "rand 0.7.3", + "rand", "rayon", "serde", "solana-logger", @@ -3278,7 +3306,7 @@ dependencies = [ "log", "num-derive", "num-traits", - "rand 0.7.3", + "rand", "rustc_version 0.2.3", "rustversion", "serde", @@ -3311,7 +3339,7 @@ checksum = "82b91d441ed00427226b08e9990367ecb4c952c70ab827c0250bd233e1ae9540" dependencies = [ "base32", "console 0.14.1", - "dialoguer", + "dialoguer 0.6.2", "hidapi", "log", "num-derive", @@ -3351,7 +3379,7 @@ dependencies = [ "num-traits", "num_cpus", "ouroboros", - "rand 0.7.3", + "rand", "rayon", "regex", "rustc_version 0.2.3", @@ -3409,8 +3437,8 @@ dependencies = [ "num-traits", "pbkdf2 0.6.0", "qstring", - "rand 0.7.3", - "rand_chacha 0.2.2", + "rand", + "rand_chacha", "rand_core 0.6.3", "rustc_version 0.2.3", "rustversion", @@ -3675,13 +3703,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", + "fastrand", "libc", - "rand 0.8.4", "redox_syscall 0.2.10", "remove_dir_all", "winapi", @@ -3770,7 +3798,7 @@ dependencies = [ "hmac 0.8.1", "once_cell", "pbkdf2 0.4.0", - "rand 0.7.3", + "rand", "rustc-hash", "sha2", "thiserror", @@ -3837,9 +3865,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" dependencies = [ "rustls", "tokio", @@ -3915,7 +3943,7 @@ dependencies = [ "input_buffer", "log", "native-tls", - "rand 0.7.3", + "rand", "sha-1", "url", "utf-8", @@ -4144,9 +4172,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.21.4" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ "ring", "untrusted", @@ -4154,9 +4182,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.21.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" dependencies = [ "webpki", ] diff --git a/Cargo.toml b/Cargo.toml index f2de96cf..6d7db428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ codegen-units = 1 [workspace] members = [ + "avm", "cli", "client", "lang", diff --git a/avm/Cargo.toml b/avm/Cargo.toml new file mode 100644 index 00000000..56235888 --- /dev/null +++ b/avm/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "avm" +version = "0.1.0" +edition = "2018" + +[[bin]] +name = "avm" +path = "src/main.rs" + +[[bin]] +name = "anchor" +path = "src/anchor/main.rs" + +[dependencies] +clap = { version = "3.0.13", features = [ "derive" ]} +cfg-if = "1.0.0" +anyhow = "1.0.32" +dialoguer = "0.9.0" +dirs = "4.0" +semver = "1.0.4" +serde = { version = "1.0.136", features = [ "derive" ]} +serde_json = "1.0.78" +thiserror = "1.0.30" +once_cell = { version = "1.8.0" } +reqwest = { version = "0.11.9", features = ['blocking', 'json'] } +tempfile = "3.3.0" diff --git a/avm/src/anchor/main.rs b/avm/src/anchor/main.rs new file mode 100644 index 00000000..5b6d2e95 --- /dev/null +++ b/avm/src/anchor/main.rs @@ -0,0 +1,24 @@ +use std::{env, fs, process::Command}; + +fn main() -> anyhow::Result<()> { + let args = env::args().skip(1).collect::>(); + + let version = avm::current_version() + .map_err(|_e| anyhow::anyhow!("Anchor version not set. Please run `avm use latest`."))?; + + let binary_path = avm::version_binary_path(&version); + if fs::metadata(&binary_path).is_err() { + anyhow::bail!( + "anchor-cli {} not installed. Please run `avm use {}`.", + version, + version + ); + } + Command::new(binary_path) + .args(args) + .spawn()? + .wait_with_output() + .expect("Failed to run anchor-cli"); + + Ok(()) +} diff --git a/avm/src/lib.rs b/avm/src/lib.rs new file mode 100644 index 00000000..7d76d920 --- /dev/null +++ b/avm/src/lib.rs @@ -0,0 +1,287 @@ +use anyhow::{anyhow, Result}; +use dialoguer::Input; +use once_cell::sync::Lazy; +use reqwest::header::USER_AGENT; +use semver::Version; +use serde::{de, Deserialize}; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::process::Stdio; + +/// Storage directory for AVM, ~/.avm +pub static AVM_HOME: Lazy = Lazy::new(|| { + cfg_if::cfg_if! { + if #[cfg(test)] { + let dir = tempfile::tempdir().expect("Could not create temporary directory"); + dir.path().join(".avm") + } else { + let mut user_home = dirs::home_dir().expect("Could not find home directory"); + user_home.push(".avm"); + user_home + } + } +}); + +/// Path to the current version file ~/.avm/.version +pub fn current_version_file_path() -> PathBuf { + let mut current_version_file_path = AVM_HOME.to_path_buf(); + current_version_file_path.push(".version"); + current_version_file_path +} + +/// Read the current version from the version file +pub fn current_version() -> Result { + let v = fs::read_to_string(current_version_file_path().as_path()) + .map_err(|e| anyhow!("Could not read version file: {}", e))?; + Version::parse(v.trim_end_matches('\n').to_string().as_str()) + .map_err(|e| anyhow!("Could not parse version file: {}", e)) +} + +/// Path to the binary for the given version +pub fn version_binary_path(version: &Version) -> PathBuf { + let mut version_path = AVM_HOME.join("bin"); + version_path.push(format!("anchor-{}", version)); + version_path +} + +/// Update the current version to a new version +pub fn use_version(version: &Version) -> Result<()> { + let installed_versions = read_installed_versions(); + // Make sure the requested version is installed + if !installed_versions.contains(version) { + let input: String = Input::new() + .with_prompt(format!( + "anchor-cli {} is not installed, would you like to install it? (y/n)", + version + )) + .with_initial_text("y") + .default("n".into()) + .interact_text()?; + if matches!(input.as_str(), "y" | "yy" | "Y" | "yes" | "Yes") { + install_version(version)?; + } + } + + let mut current_version_file = fs::File::create(current_version_file_path().as_path())?; + current_version_file.write_all(version.to_string().as_bytes())?; + Ok(()) +} + +/// Install a version of anchor-cli +pub fn install_version(version: &Version) -> Result<()> { + let exit = std::process::Command::new("cargo") + .args(&[ + "install", + "--git", + "https://github.com/project-serum/anchor", + "--tag", + &format!("v{}", &version), + "anchor-cli", + "--locked", + "--root", + AVM_HOME.to_str().unwrap(), + ]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .map_err(|e| { + anyhow::format_err!("Cargo install for {} failed: {}", version, e.to_string()) + })?; + if !exit.status.success() { + return Err(anyhow!( + "Failed to install {}, is it a valid version?", + version + )); + } + fs::rename( + &AVM_HOME.join("bin").join("anchor"), + &AVM_HOME.join("bin").join(format!("anchor-{}", version)), + )?; + Ok(()) +} + +/// Remove an installed version of anchor-cli +pub fn uninstall_version(version: &Version) -> Result<()> { + let version_path = AVM_HOME.join("bin").join(format!("anchor-{}", version)); + if !version_path.exists() { + return Err(anyhow!("anchor-cli {} is not installed", version)); + } + if version == ¤t_version().unwrap() { + return Err(anyhow!("anchor-cli {} is currently in use", version)); + } + fs::remove_file(version_path.as_path())?; + Ok(()) +} + +/// Ensure the users home directory is setup with the paths required by AVM. +pub fn ensure_paths() { + let home_dir = AVM_HOME.to_path_buf(); + if !home_dir.as_path().exists() { + fs::create_dir_all(home_dir.clone()).expect("Could not create .avm directory"); + } + let bin_dir = home_dir.join("bin"); + if !bin_dir.as_path().exists() { + fs::create_dir_all(bin_dir).expect("Could not create .avm/bin directory"); + } + if !current_version_file_path().exists() { + fs::File::create(current_version_file_path()).expect("Could not create .version file"); + } +} + +/// Retrieve a list of installable versions of anchor-cli using the GitHub API and tags on the Anchor +/// repository. +pub fn fetch_versions() -> Vec { + #[derive(Deserialize)] + struct Release { + #[serde(rename = "name", deserialize_with = "version_deserializer")] + version: semver::Version, + } + + fn version_deserializer<'de, D>(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s: &str = de::Deserialize::deserialize(deserializer)?; + Version::parse(s.trim_start_matches('v')).map_err(de::Error::custom) + } + + let client = reqwest::blocking::Client::new(); + let versions: Vec = client + .get("https://api.github.com/repos/project-serum/anchor/tags") + .header(USER_AGENT, "avm https://github.com/project-serum/anchor") + .send() + .unwrap() + .json() + .unwrap(); + versions.into_iter().map(|r| r.version).collect() +} + +/// Print available versions and flags indicating installed, current and latest +pub fn list_versions() -> Result<()> { + let 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().unwrap() == v.clone() { + flags.push("current"); + } + if flags.is_empty() { + println!(); + } else { + println!("\t({})", flags.join(", ")); + } + }); + + Ok(()) +} + +pub fn get_latest_version() -> semver::Version { + let available_versions = fetch_versions(); + available_versions.first().unwrap().clone() +} + +/// Read the installed anchor-cli versions by reading the binaries in the AVM_HOME/bin directory. +pub fn read_installed_versions() -> Vec { + let home_dir = AVM_HOME.to_path_buf(); + let mut versions = vec![]; + for file in fs::read_dir(&home_dir.join("bin")).unwrap() { + let file_name = file.unwrap().file_name(); + // Match only things that look like anchor-* + if file_name.to_str().unwrap().starts_with("anchor-") { + let version = file_name + .to_str() + .unwrap() + .trim_start_matches("anchor-") + .parse::() + .unwrap(); + versions.push(version); + } + } + + versions +} + +#[cfg(test)] +mod tests { + use crate::*; + use semver::Version; + use std::fs; + use std::io::Write; + + #[test] + fn test_ensure_paths() { + ensure_paths(); + assert!(AVM_HOME.exists()); + let bin_dir = AVM_HOME.join("bin"); + assert!(bin_dir.exists()); + let current_version_file = AVM_HOME.join(".version"); + assert!(current_version_file.exists()); + } + + #[test] + fn test_current_version_file_path() { + ensure_paths(); + assert!(current_version_file_path().exists()); + } + + #[test] + fn test_version_binary_path() { + assert!( + version_binary_path(&Version::parse("0.18.2").unwrap()) + == AVM_HOME.join("bin/anchor-0.18.2") + ); + } + + #[test] + fn test_current_version() { + ensure_paths(); + let mut current_version_file = + fs::File::create(current_version_file_path().as_path()).unwrap(); + current_version_file.write_all("0.18.2".as_bytes()).unwrap(); + assert!(current_version().unwrap() == Version::parse("0.18.2").unwrap()); + } + + #[test] + #[should_panic(expected = "anchor-cli 0.18.1 is not installed")] + fn test_uninstall_non_installed_version() { + uninstall_version(&Version::parse("0.18.1").unwrap()).unwrap(); + } + + #[test] + #[should_panic(expected = "anchor-cli 0.18.2 is currently in use")] + fn test_uninstalled_in_use_version() { + ensure_paths(); + let version = Version::parse("0.18.2").unwrap(); + let mut current_version_file = + fs::File::create(current_version_file_path().as_path()).unwrap(); + current_version_file.write_all("0.18.2".as_bytes()).unwrap(); + // Create a fake binary for anchor-0.18.2 in the bin directory + fs::File::create(version_binary_path(&version)).unwrap(); + uninstall_version(&version).unwrap(); + } + + #[test] + fn test_read_installed_versions() { + ensure_paths(); + let version = Version::parse("0.18.2").unwrap(); + // Create a fake binary for anchor-0.18.2 in the bin directory + fs::File::create(version_binary_path(&version)).unwrap(); + let expected = vec![version]; + assert!(read_installed_versions() == expected); + // Should ignore this file because its not anchor- prefixed + fs::File::create(AVM_HOME.join("bin").join("garbage").as_path()).unwrap(); + assert!(read_installed_versions() == expected); + } +} diff --git a/avm/src/main.rs b/avm/src/main.rs new file mode 100644 index 00000000..9d2d932c --- /dev/null +++ b/avm/src/main.rs @@ -0,0 +1,58 @@ +use anyhow::{Error, Result}; +use clap::{Parser, Subcommand}; +use semver::Version; + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Parser)] +#[clap(name = "avm", about = "Anchor version manager")] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + #[clap(about = "Use a specific version of Anchor")] + Use { + #[clap(parse(try_from_str = parse_version))] + version: Version, + }, + #[clap(about = "Install a version of Anchor")] + Install { + #[clap(parse(try_from_str = parse_version))] + version: Version, + }, + #[clap(about = "Uninstall a version of Anchor")] + Uninstall { + #[clap(parse(try_from_str = parse_version))] + version: Version, + }, + #[clap(about = "List available versions of Anchor")] + List {}, +} + +// If `latest` is passed use the latest available version. +fn parse_version(version: &str) -> Result { + if version == "latest" { + Ok(avm::get_latest_version()) + } else { + Version::parse(version).map_err(|e| anyhow::anyhow!(e)) + } +} +pub fn entry(opts: Cli) -> Result<()> { + match opts.command { + Commands::Use { version } => avm::use_version(&version), + Commands::Install { version } => avm::install_version(&version), + Commands::Uninstall { version } => avm::uninstall_version(&version), + Commands::List {} => avm::list_versions(), + } +} + +fn main() -> Result<()> { + // Make sure the user's home directory is setup with the paths required by AVM. + avm::ensure_paths(); + + let opt = Cli::parse(); + entry(opt) +}