Эх сурвалжийг харах

cli: anchor version manager (avm) (#1385)

Tom Linton 3 жил өмнө
parent
commit
75469f423c
7 өөрчлөгдсөн 509 нэмэгдсэн , 84 устгасан
  1. 1 0
      CHANGELOG.md
  2. 112 84
      Cargo.lock
  3. 1 0
      Cargo.toml
  4. 26 0
      avm/Cargo.toml
  5. 24 0
      avm/src/anchor/main.rs
  6. 287 0
      avm/src/lib.rs
  7. 58 0
      avm/src/main.rs

+ 1 - 0
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
 

+ 112 - 84
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",
 ]

+ 1 - 0
Cargo.toml

@@ -6,6 +6,7 @@ codegen-units = 1
 
 [workspace]
 members = [
+    "avm",
     "cli",
     "client",
     "lang",

+ 26 - 0
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"

+ 24 - 0
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::<Vec<String>>();
+
+    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(())
+}

+ 287 - 0
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<PathBuf> = 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<Version> {
+    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 == &current_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<semver::Version> {
+    #[derive(Deserialize)]
+    struct Release {
+        #[serde(rename = "name", deserialize_with = "version_deserializer")]
+        version: semver::Version,
+    }
+
+    fn version_deserializer<'de, D>(deserializer: D) -> Result<semver::Version, D::Error>
+    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<Release> = 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<semver::Version> {
+    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::<semver::Version>()
+                .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);
+    }
+}

+ 58 - 0
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<Version, Error> {
+    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)
+}