diff --git a/CHANGELOG.md b/CHANGELOG.md index 79dec16047..947cdb9d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Add `test.upgradeable`, `test.genesis.upgradeable` setting in anchor.toml to support testing upgradeable programs ([#2641](https://github.com/coral-xyz/anchor/pull/2642)). - cli, client, lang, spl: Update Solana toolchain and dependencies to `1.17.0`, `1.16` remains supported ([#2645](https://github.com/coral-xyz/anchor/pull/2645)). - spl: Add support for memo program ([#2661](https://github.com/coral-xyz/anchor/pull/2661)). +- avm: Add `anchor-cli` installation from commit ([#2659](https://github.com/coral-xyz/anchor/pull/2659)). - cli: Add `toolchain` property in `Anchor.toml` to override Anchor and Solana versions ([#2649](https://github.com/coral-xyz/anchor/pull/2649)). ### Fixes diff --git a/avm/Cargo.toml b/avm/Cargo.toml index befbbdf496..8ef6547e06 100644 --- a/avm/Cargo.toml +++ b/avm/Cargo.toml @@ -15,6 +15,7 @@ path = "src/anchor/main.rs" [dependencies] anyhow = "1.0.32" cfg-if = "1.0.0" +cargo_toml = "0.15.3" clap = { version = "4.2.4", features = ["derive"] } dirs = "4.0.0" once_cell = "1.8.0" @@ -22,4 +23,3 @@ 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/anchor/main.rs b/avm/src/anchor/main.rs index cc3a00cfb0..a470f7ac2c 100644 --- a/avm/src/anchor/main.rs +++ b/avm/src/anchor/main.rs @@ -1,4 +1,4 @@ -use std::{env, fs, process::Command}; +use std::{env, process::Command}; fn main() -> anyhow::Result<()> { let args = env::args().skip(1).collect::>(); @@ -7,13 +7,14 @@ fn main() -> anyhow::Result<()> { .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() { + if !binary_path.exists() { anyhow::bail!( "anchor-cli {} not installed. Please run `avm use {}`.", version, version ); } + let exit = Command::new(binary_path) .args(args) .spawn()? diff --git a/avm/src/lib.rs b/avm/src/lib.rs index e5257a3106..32881f9c81 100644 --- a/avm/src/lib.rs +++ b/avm/src/lib.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use cargo_toml::Manifest; use once_cell::sync::Lazy; use reqwest::header::USER_AGENT; use reqwest::StatusCode; @@ -8,7 +9,6 @@ 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(|| { @@ -25,25 +25,44 @@ pub static AVM_HOME: Lazy = Lazy::new(|| { }); /// 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 +fn current_version_file_path() -> PathBuf { + AVM_HOME.join(".version") } -/// 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 current version file ~/.avm/bin +fn get_bin_dir_path() -> PathBuf { + AVM_HOME.join("bin") } /// 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 + get_bin_dir_path().join(format!("anchor-{version}")) +} + +/// 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.exists() { + fs::create_dir_all(&home_dir).expect("Could not create .avm directory"); + } + + let bin_dir = get_bin_dir_path(); + if !bin_dir.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"); + } +} + +/// Read the current version from the version file +pub fn current_version() -> Result { + fs::read_to_string(current_version_file_path()) + .map_err(|e| anyhow!("Could not read version file: {}", e))? + .trim_end_matches('\n') + .parse::() + .map_err(|e| anyhow!("Could not parse version file: {}", e)) } /// Update the current version to a new version @@ -53,8 +72,8 @@ pub fn use_version(opt_version: Option) -> Result<()> { None => read_anchorversion_file()?, }; - let installed_versions = read_installed_versions(); // Make sure the requested version is installed + let installed_versions = read_installed_versions()?; if !installed_versions.contains(&version) { if let Ok(current) = current_version() { println!("Version {version} is not installed, staying on version {current}."); @@ -68,29 +87,22 @@ pub fn use_version(opt_version: Option) -> Result<()> { )); } - let mut current_version_file = fs::File::create(current_version_file_path().as_path())?; + let mut current_version_file = fs::File::create(current_version_file_path())?; current_version_file.write_all(version.to_string().as_bytes())?; println!("Now using anchor version {}.", current_version()?); Ok(()) } -/// Update to the latest version -pub fn update() -> Result<()> { - // Find last stable version - let version = get_latest_version(); - - install_anchor(InstallTarget::Version(version), false) -} - #[derive(Clone)] pub enum InstallTarget { Version(Version), Commit(String), } -#[derive(Deserialize)] -struct GetCommitResponse { - sha: String, +/// Update to the latest version +pub fn update() -> Result<()> { + let latest_version = get_latest_version()?; + install_version(InstallTarget::Version(latest_version), false) } /// The commit sha provided can be shortened, @@ -103,45 +115,52 @@ pub fn check_and_get_full_commit(commit: &str) -> Result { "https://api.github.com/repos/coral-xyz/anchor/commits/{commit}" )) .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor") - .send() - .unwrap(); + .send()?; + if response.status() != StatusCode::OK { return Err(anyhow!( "Error checking commit {commit}: {}", - response.text().unwrap() + response.text()? )); }; - let get_commit_response: GetCommitResponse = response.json().unwrap(); - Ok(get_commit_response.sha) + + #[derive(Deserialize)] + struct GetCommitResponse { + sha: String, + } + + response + .json::() + .map(|resp| resp.sha) + .map_err(|err| anyhow!("Failed to parse the response to JSON: {err:?}")) } -fn get_anchor_version_from_commit(commit: &str) -> Version { +fn get_anchor_version_from_commit(commit: &str) -> Result { // 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 + "https://raw.githubusercontent.com/coral-xyz/anchor/{commit}/cli/Cargo.toml" )) .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor") - .send() - .unwrap(); + .send()?; + if response.status() != StatusCode::OK { - panic!("Could not find anchor-cli version for commit: {response:?}"); + return Err(anyhow!( + "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 + + let anchor_cli_cargo_toml = response.text()?; + let anchor_cli_manifest = Manifest::from_str(&anchor_cli_cargo_toml)?; + let mut version = anchor_cli_manifest.package().version().parse::()?; + version.pre = Prerelease::new(commit)?; + + Ok(version) } /// Install a version of anchor-cli -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(); - +pub fn install_version(install_target: InstallTarget, force: bool) -> Result<()> { let mut args: Vec = vec![ "install".into(), "--git".into(), @@ -158,9 +177,12 @@ pub fn install_anchor(install_target: InstallTarget, force: bool) -> Result<()> } InstallTarget::Commit(commit) => { args.extend(["--rev".into(), commit.clone()]); - get_anchor_version_from_commit(&commit) + get_anchor_version_from_commit(&commit)? } }; + + // If version is already installed we ignore the request. + let installed_versions = read_installed_versions()?; if installed_versions.contains(&version) && !force { println!("Version {version} is already installed"); return Ok(()); @@ -171,38 +193,40 @@ pub fn install_anchor(install_target: InstallTarget, force: bool) -> Result<()> .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output() - .map_err(|e| { - anyhow::format_err!("Cargo install for {} failed: {}", version, e.to_string()) - })?; + .map_err(|e| anyhow!("Cargo install for {} failed: {}", version, e.to_string()))?; if !exit.status.success() { return Err(anyhow!( "Failed to install {}, is it a valid version?", version )); } + + let bin_dir = get_bin_dir_path(); fs::rename( - AVM_HOME.join("bin").join("anchor"), - AVM_HOME.join("bin").join(format!("anchor-{version}")), + bin_dir.join("anchor"), + bin_dir.join(format!("anchor-{version}")), )?; + // If .version file is empty or not parseable, write the newly installed version to it if current_version().is_err() { - let mut current_version_file = fs::File::create(current_version_file_path().as_path())?; + let mut current_version_file = fs::File::create(current_version_file_path())?; current_version_file.write_all(version.to_string().as_bytes())?; } - use_version(Some(version.clone())) + use_version(Some(version)) } /// 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}")); + let version_path = get_bin_dir_path().join(format!("anchor-{version}")); if !version_path.exists() { return Err(anyhow!("anchor-cli {} is not installed", version)); } - if version == ¤t_version().unwrap() { + if version == ¤t_version()? { return Err(anyhow!("anchor-cli {} is currently in use", version)); } - fs::remove_file(version_path.as_path())?; + fs::remove_file(version_path)?; + Ok(()) } @@ -214,31 +238,16 @@ pub fn read_anchorversion_file() -> Result { .map_err(|e| anyhow!("Unable to parse version: {e}")) } -/// 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 { +pub fn fetch_versions() -> Result> { #[derive(Deserialize)] struct Release { #[serde(rename = "name", deserialize_with = "version_deserializer")] - version: semver::Version, + version: Version, } - fn version_deserializer<'de, D>(deserializer: D) -> Result + fn version_deserializer<'de, D>(deserializer: D) -> Result where D: de::Deserializer<'de>, { @@ -246,22 +255,23 @@ pub fn fetch_versions() -> Vec { Version::parse(s.trim_start_matches('v')).map_err(de::Error::custom) } - let client = reqwest::blocking::Client::new(); - let versions: Vec = client + let versions = reqwest::blocking::Client::new() .get("https://api.github.com/repos/coral-xyz/anchor/tags") .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor") - .send() - .unwrap() - .json() - .unwrap(); - versions.into_iter().map(|r| r.version).collect() + .send()? + .json::>()? + .into_iter() + .map(|release| release.version) + .collect(); + + Ok(versions) } /// Print available versions and flags indicating installed, current and latest pub fn list_versions() -> Result<()> { - let mut installed_versions = read_installed_versions(); + let mut installed_versions = read_installed_versions()?; - let mut available_versions = fetch_versions(); + let mut available_versions = fetch_versions()?; // Reverse version list so latest versions are printed last available_versions.reverse(); @@ -277,10 +287,10 @@ pub fn list_versions() -> Result<()> { flags.push("installed"); installed_versions.remove(position); } - - if current_version().is_ok() && current_version().unwrap() == v.clone() { + if current_version().map(|cv| &cv == v).unwrap_or_default() { flags.push("current"); } + if flags.is_empty() { println!(); } else { @@ -294,90 +304,80 @@ pub fn list_versions() -> Result<()> { Ok(()) } -pub fn get_latest_version() -> semver::Version { - let available_versions = fetch_versions(); - available_versions.first().unwrap().clone() +pub fn get_latest_version() -> Result { + fetch_versions()? + .into_iter() + .next() + .ok_or_else(|| anyhow!("First version not found")) } /// 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 +pub fn read_installed_versions() -> Result> { + const PREFIX: &str = "anchor-"; + let versions = fs::read_dir(get_bin_dir_path())? + .filter_map(|entry_result| entry_result.ok()) + .filter_map(|entry| entry.file_name().to_str().map(|f| f.to_owned())) + .filter(|file_name| file_name.starts_with(PREFIX)) + .filter_map(|file_name| file_name.trim_start_matches(PREFIX).parse::().ok()) + .collect(); + + Ok(versions) } #[cfg(test)] mod tests { use crate::*; use semver::Version; - use std::env; use std::fs; use std::io::Write; - - #[test] - fn test_read_anchorversion() { - ensure_paths(); - let mut dir = env::current_dir().unwrap(); - dir.push(".anchorversion"); - let mut file_created = fs::File::create(&dir).unwrap(); - let test_version = "0.26.0"; - file_created.write_all(test_version.as_bytes()).unwrap(); - - let version = read_anchorversion_file().unwrap(); - - assert_eq!(version.to_string(), test_version); - - fs::remove_file(&dir).unwrap(); - } + use std::path::Path; #[test] fn test_ensure_paths() { ensure_paths(); assert!(AVM_HOME.exists()); - let bin_dir = AVM_HOME.join("bin"); + let bin_dir = get_bin_dir_path(); assert!(bin_dir.exists()); - let current_version_file = AVM_HOME.join(".version"); + let current_version_file = current_version_file_path(); assert!(current_version_file.exists()); } #[test] - fn test_current_version_file_path() { - ensure_paths(); - assert!(current_version_file_path().exists()); + fn test_version_binary_path() { + assert_eq!( + version_binary_path(&Version::parse("0.18.2").unwrap()), + get_bin_dir_path().join("anchor-0.18.2") + ); } #[test] - fn test_version_binary_path() { - assert!( - version_binary_path(&Version::parse("0.18.2").unwrap()) - == AVM_HOME.join("bin/anchor-0.18.2") - ); + fn test_read_anchorversion() -> Result<()> { + ensure_paths(); + + let anchorversion_path = Path::new(".anchorversion"); + let test_version = "0.26.0"; + fs::write(anchorversion_path, test_version)?; + + let version = read_anchorversion_file()?; + assert_eq!(version.to_string(), test_version); + + fs::remove_file(anchorversion_path)?; + + Ok(()) } #[test] fn test_current_version() { ensure_paths(); - let mut current_version_file = - fs::File::create(current_version_file_path().as_path()).unwrap(); + let mut current_version_file = fs::File::create(current_version_file_path()).unwrap(); current_version_file.write_all("0.18.2".as_bytes()).unwrap(); // Sync the file to disk before the read in current_version() to // mitigate the read not seeing the written version bytes. current_version_file.sync_all().unwrap(); - assert!(current_version().unwrap() == Version::parse("0.18.2").unwrap()); + assert_eq!( + current_version().unwrap(), + Version::parse("0.18.2").unwrap() + ); } #[test] @@ -391,8 +391,7 @@ mod tests { 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(); + let mut current_version_file = fs::File::create(current_version_file_path()).unwrap(); current_version_file.write_all("0.18.2".as_bytes()).unwrap(); // Sync the file to disk before the read in current_version() to // mitigate the read not seeing the written version bytes. @@ -406,18 +405,21 @@ mod tests { 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); + assert_eq!(read_installed_versions().unwrap(), 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); + assert_eq!(read_installed_versions().unwrap(), expected); } #[test] fn test_get_anchor_version_from_commit() { - let version = get_anchor_version_from_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9"); + let version = + get_anchor_version_from_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9").unwrap(); assert_eq!( version.to_string(), "0.28.0-e1afcbf71e0f2e10fae14525934a6a68479167b9" diff --git a/avm/src/main.rs b/avm/src/main.rs index 2e7d95fd0d..fa7a1a4c67 100644 --- a/avm/src/main.rs +++ b/avm/src/main.rs @@ -43,15 +43,22 @@ pub enum Commands { // If `latest` is passed use the latest available version. fn parse_version(version: &str) -> Result { if version == "latest" { - Ok(avm::get_latest_version()) + avm::get_latest_version() } else { - Version::parse(version).map_err(|e| anyhow::anyhow!(e)) + Version::parse(version).map_err(|e| anyhow!(e)) } } fn parse_install_target(version_or_commit: &str) -> Result { parse_version(version_or_commit) - .map(InstallTarget::Version) + .map(|version| { + if version.pre.is_empty() { + InstallTarget::Version(version) + } else { + // Allow `avm install 0.28.0-6cf200493a307c01487c7b492b4893e0d6f6cb23` + InstallTarget::Commit(version.pre.to_string()) + } + }) .or_else(|version_error| { avm::check_and_get_full_commit(version_or_commit) .map(InstallTarget::Commit) @@ -67,7 +74,7 @@ pub fn entry(opts: Cli) -> Result<()> { Commands::Install { version_or_commit, force, - } => avm::install_anchor(version_or_commit, force), + } => avm::install_version(version_or_commit, force), Commands::Uninstall { version } => avm::uninstall_version(&version), Commands::List {} => avm::list_versions(), Commands::Update {} => avm::update(),