lib.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. use anyhow::{anyhow, Result};
  2. use cargo_toml::Manifest;
  3. use once_cell::sync::Lazy;
  4. use reqwest::header::USER_AGENT;
  5. use reqwest::StatusCode;
  6. use semver::{Prerelease, Version};
  7. use serde::{de, Deserialize};
  8. use std::fs;
  9. use std::io::Write;
  10. use std::path::PathBuf;
  11. use std::process::Stdio;
  12. /// Storage directory for AVM, ~/.avm
  13. pub static AVM_HOME: Lazy<PathBuf> = Lazy::new(|| {
  14. cfg_if::cfg_if! {
  15. if #[cfg(test)] {
  16. let dir = tempfile::tempdir().expect("Could not create temporary directory");
  17. dir.path().join(".avm")
  18. } else {
  19. let mut user_home = dirs::home_dir().expect("Could not find home directory");
  20. user_home.push(".avm");
  21. user_home
  22. }
  23. }
  24. });
  25. /// Path to the current version file ~/.avm/.version
  26. fn current_version_file_path() -> PathBuf {
  27. AVM_HOME.join(".version")
  28. }
  29. /// Path to the current version file ~/.avm/bin
  30. fn get_bin_dir_path() -> PathBuf {
  31. AVM_HOME.join("bin")
  32. }
  33. /// Path to the binary for the given version
  34. pub fn version_binary_path(version: &Version) -> PathBuf {
  35. get_bin_dir_path().join(format!("anchor-{version}"))
  36. }
  37. /// Ensure the users home directory is setup with the paths required by AVM.
  38. pub fn ensure_paths() {
  39. let home_dir = AVM_HOME.to_path_buf();
  40. if !home_dir.exists() {
  41. fs::create_dir_all(&home_dir).expect("Could not create .avm directory");
  42. }
  43. let bin_dir = get_bin_dir_path();
  44. if !bin_dir.exists() {
  45. fs::create_dir_all(bin_dir).expect("Could not create .avm/bin directory");
  46. }
  47. if !current_version_file_path().exists() {
  48. fs::File::create(current_version_file_path()).expect("Could not create .version file");
  49. }
  50. }
  51. /// Read the current version from the version file
  52. pub fn current_version() -> Result<Version> {
  53. fs::read_to_string(current_version_file_path())
  54. .map_err(|e| anyhow!("Could not read version file: {}", e))?
  55. .trim_end_matches('\n')
  56. .parse::<Version>()
  57. .map_err(|e| anyhow!("Could not parse version file: {}", e))
  58. }
  59. /// Update the current version to a new version
  60. pub fn use_version(opt_version: Option<Version>) -> Result<()> {
  61. let version = match opt_version {
  62. Some(version) => version,
  63. None => read_anchorversion_file()?,
  64. };
  65. // Make sure the requested version is installed
  66. let installed_versions = read_installed_versions()?;
  67. if !installed_versions.contains(&version) {
  68. if let Ok(current) = current_version() {
  69. println!("Version {version} is not installed, staying on version {current}.");
  70. } else {
  71. println!("Version {version} is not installed, no current version.");
  72. }
  73. return Err(anyhow!(
  74. "You need to run 'avm install {}' to install it before using it.",
  75. version
  76. ));
  77. }
  78. let mut current_version_file = fs::File::create(current_version_file_path())?;
  79. current_version_file.write_all(version.to_string().as_bytes())?;
  80. println!("Now using anchor version {}.", current_version()?);
  81. Ok(())
  82. }
  83. #[derive(Clone)]
  84. pub enum InstallTarget {
  85. Version(Version),
  86. Commit(String),
  87. }
  88. /// Update to the latest version
  89. pub fn update() -> Result<()> {
  90. let latest_version = get_latest_version()?;
  91. install_version(InstallTarget::Version(latest_version), false)
  92. }
  93. /// The commit sha provided can be shortened,
  94. ///
  95. /// returns the full commit sha3 for unique versioning downstream
  96. pub fn check_and_get_full_commit(commit: &str) -> Result<String> {
  97. let client = reqwest::blocking::Client::new();
  98. let response = client
  99. .get(format!(
  100. "https://api.github.com/repos/coral-xyz/anchor/commits/{commit}"
  101. ))
  102. .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor")
  103. .send()?;
  104. if response.status() != StatusCode::OK {
  105. return Err(anyhow!(
  106. "Error checking commit {commit}: {}",
  107. response.text()?
  108. ));
  109. };
  110. #[derive(Deserialize)]
  111. struct GetCommitResponse {
  112. sha: String,
  113. }
  114. response
  115. .json::<GetCommitResponse>()
  116. .map(|resp| resp.sha)
  117. .map_err(|err| anyhow!("Failed to parse the response to JSON: {err:?}"))
  118. }
  119. fn get_anchor_version_from_commit(commit: &str) -> Result<Version> {
  120. // We read the version from cli/Cargo.toml since there is no simpler way to do so
  121. let client = reqwest::blocking::Client::new();
  122. let response = client
  123. .get(format!(
  124. "https://raw.githubusercontent.com/coral-xyz/anchor/{commit}/cli/Cargo.toml"
  125. ))
  126. .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor")
  127. .send()?;
  128. if response.status() != StatusCode::OK {
  129. return Err(anyhow!(
  130. "Could not find anchor-cli version for commit: {response:?}"
  131. ));
  132. };
  133. let anchor_cli_cargo_toml = response.text()?;
  134. let anchor_cli_manifest = Manifest::from_str(&anchor_cli_cargo_toml)?;
  135. let mut version = anchor_cli_manifest.package().version().parse::<Version>()?;
  136. version.pre = Prerelease::new(commit)?;
  137. Ok(version)
  138. }
  139. /// Install a version of anchor-cli
  140. pub fn install_version(install_target: InstallTarget, force: bool) -> Result<()> {
  141. let mut args: Vec<String> = vec![
  142. "install".into(),
  143. "--git".into(),
  144. "https://github.com/coral-xyz/anchor".into(),
  145. "anchor-cli".into(),
  146. "--locked".into(),
  147. "--root".into(),
  148. AVM_HOME.to_str().unwrap().into(),
  149. ];
  150. let version = match install_target {
  151. InstallTarget::Version(version) => {
  152. args.extend(["--tag".into(), format!("v{}", version), "anchor-cli".into()]);
  153. version
  154. }
  155. InstallTarget::Commit(commit) => {
  156. args.extend(["--rev".into(), commit.clone()]);
  157. get_anchor_version_from_commit(&commit)?
  158. }
  159. };
  160. // If version is already installed we ignore the request.
  161. let installed_versions = read_installed_versions()?;
  162. if installed_versions.contains(&version) && !force {
  163. println!("Version {version} is already installed");
  164. return Ok(());
  165. }
  166. let exit = std::process::Command::new("cargo")
  167. .args(args)
  168. .stdout(Stdio::inherit())
  169. .stderr(Stdio::inherit())
  170. .output()
  171. .map_err(|e| anyhow!("Cargo install for {} failed: {}", version, e.to_string()))?;
  172. if !exit.status.success() {
  173. return Err(anyhow!(
  174. "Failed to install {}, is it a valid version?",
  175. version
  176. ));
  177. }
  178. let bin_dir = get_bin_dir_path();
  179. fs::rename(
  180. bin_dir.join("anchor"),
  181. bin_dir.join(format!("anchor-{version}")),
  182. )?;
  183. // If .version file is empty or not parseable, write the newly installed version to it
  184. if current_version().is_err() {
  185. let mut current_version_file = fs::File::create(current_version_file_path())?;
  186. current_version_file.write_all(version.to_string().as_bytes())?;
  187. }
  188. use_version(Some(version))
  189. }
  190. /// Remove an installed version of anchor-cli
  191. pub fn uninstall_version(version: &Version) -> Result<()> {
  192. let version_path = get_bin_dir_path().join(format!("anchor-{version}"));
  193. if !version_path.exists() {
  194. return Err(anyhow!("anchor-cli {} is not installed", version));
  195. }
  196. if version == &current_version()? {
  197. return Err(anyhow!("anchor-cli {} is currently in use", version));
  198. }
  199. fs::remove_file(version_path)?;
  200. Ok(())
  201. }
  202. /// Read version from .anchorversion
  203. pub fn read_anchorversion_file() -> Result<Version> {
  204. fs::read_to_string(".anchorversion")
  205. .map_err(|e| anyhow!(".anchorversion file not found: {e}"))
  206. .map(|content| Version::parse(content.trim()))?
  207. .map_err(|e| anyhow!("Unable to parse version: {e}"))
  208. }
  209. /// Retrieve a list of installable versions of anchor-cli using the GitHub API and tags on the Anchor
  210. /// repository.
  211. pub fn fetch_versions() -> Result<Vec<Version>> {
  212. #[derive(Deserialize)]
  213. struct Release {
  214. #[serde(rename = "name", deserialize_with = "version_deserializer")]
  215. version: Version,
  216. }
  217. fn version_deserializer<'de, D>(deserializer: D) -> Result<Version, D::Error>
  218. where
  219. D: de::Deserializer<'de>,
  220. {
  221. let s: &str = de::Deserialize::deserialize(deserializer)?;
  222. Version::parse(s.trim_start_matches('v')).map_err(de::Error::custom)
  223. }
  224. let versions = reqwest::blocking::Client::new()
  225. .get("https://api.github.com/repos/coral-xyz/anchor/tags")
  226. .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor")
  227. .send()?
  228. .json::<Vec<Release>>()?
  229. .into_iter()
  230. .map(|release| release.version)
  231. .collect();
  232. Ok(versions)
  233. }
  234. /// Print available versions and flags indicating installed, current and latest
  235. pub fn list_versions() -> Result<()> {
  236. let mut installed_versions = read_installed_versions()?;
  237. let mut available_versions = fetch_versions()?;
  238. // Reverse version list so latest versions are printed last
  239. available_versions.reverse();
  240. let print_versions =
  241. |versions: Vec<Version>, installed_versions: &mut Vec<Version>, show_latest: bool| {
  242. versions.iter().enumerate().for_each(|(i, v)| {
  243. print!("{v}");
  244. let mut flags = vec![];
  245. if i == versions.len() - 1 && show_latest {
  246. flags.push("latest");
  247. }
  248. if let Some(position) = installed_versions.iter().position(|iv| iv == v) {
  249. flags.push("installed");
  250. installed_versions.remove(position);
  251. }
  252. if current_version().map(|cv| &cv == v).unwrap_or_default() {
  253. flags.push("current");
  254. }
  255. if flags.is_empty() {
  256. println!();
  257. } else {
  258. println!("\t({})", flags.join(", "));
  259. }
  260. })
  261. };
  262. print_versions(available_versions, &mut installed_versions, true);
  263. print_versions(installed_versions.clone(), &mut installed_versions, false);
  264. Ok(())
  265. }
  266. pub fn get_latest_version() -> Result<Version> {
  267. fetch_versions()?
  268. .into_iter()
  269. .next()
  270. .ok_or_else(|| anyhow!("First version not found"))
  271. }
  272. /// Read the installed anchor-cli versions by reading the binaries in the AVM_HOME/bin directory.
  273. pub fn read_installed_versions() -> Result<Vec<Version>> {
  274. const PREFIX: &str = "anchor-";
  275. let versions = fs::read_dir(get_bin_dir_path())?
  276. .filter_map(|entry_result| entry_result.ok())
  277. .filter_map(|entry| entry.file_name().to_str().map(|f| f.to_owned()))
  278. .filter(|file_name| file_name.starts_with(PREFIX))
  279. .filter_map(|file_name| file_name.trim_start_matches(PREFIX).parse::<Version>().ok())
  280. .collect();
  281. Ok(versions)
  282. }
  283. #[cfg(test)]
  284. mod tests {
  285. use crate::*;
  286. use semver::Version;
  287. use std::fs;
  288. use std::io::Write;
  289. use std::path::Path;
  290. #[test]
  291. fn test_ensure_paths() {
  292. ensure_paths();
  293. assert!(AVM_HOME.exists());
  294. let bin_dir = get_bin_dir_path();
  295. assert!(bin_dir.exists());
  296. let current_version_file = current_version_file_path();
  297. assert!(current_version_file.exists());
  298. }
  299. #[test]
  300. fn test_version_binary_path() {
  301. assert_eq!(
  302. version_binary_path(&Version::parse("0.18.2").unwrap()),
  303. get_bin_dir_path().join("anchor-0.18.2")
  304. );
  305. }
  306. #[test]
  307. fn test_read_anchorversion() -> Result<()> {
  308. ensure_paths();
  309. let anchorversion_path = Path::new(".anchorversion");
  310. let test_version = "0.26.0";
  311. fs::write(anchorversion_path, test_version)?;
  312. let version = read_anchorversion_file()?;
  313. assert_eq!(version.to_string(), test_version);
  314. fs::remove_file(anchorversion_path)?;
  315. Ok(())
  316. }
  317. #[test]
  318. fn test_current_version() {
  319. ensure_paths();
  320. let mut current_version_file = fs::File::create(current_version_file_path()).unwrap();
  321. current_version_file.write_all("0.18.2".as_bytes()).unwrap();
  322. // Sync the file to disk before the read in current_version() to
  323. // mitigate the read not seeing the written version bytes.
  324. current_version_file.sync_all().unwrap();
  325. assert_eq!(
  326. current_version().unwrap(),
  327. Version::parse("0.18.2").unwrap()
  328. );
  329. }
  330. #[test]
  331. #[should_panic(expected = "anchor-cli 0.18.1 is not installed")]
  332. fn test_uninstall_non_installed_version() {
  333. uninstall_version(&Version::parse("0.18.1").unwrap()).unwrap();
  334. }
  335. #[test]
  336. #[should_panic(expected = "anchor-cli 0.18.2 is currently in use")]
  337. fn test_uninstalled_in_use_version() {
  338. ensure_paths();
  339. let version = Version::parse("0.18.2").unwrap();
  340. let mut current_version_file = fs::File::create(current_version_file_path()).unwrap();
  341. current_version_file.write_all("0.18.2".as_bytes()).unwrap();
  342. // Sync the file to disk before the read in current_version() to
  343. // mitigate the read not seeing the written version bytes.
  344. current_version_file.sync_all().unwrap();
  345. // Create a fake binary for anchor-0.18.2 in the bin directory
  346. fs::File::create(version_binary_path(&version)).unwrap();
  347. uninstall_version(&version).unwrap();
  348. }
  349. #[test]
  350. fn test_read_installed_versions() {
  351. ensure_paths();
  352. let version = Version::parse("0.18.2").unwrap();
  353. // Create a fake binary for anchor-0.18.2 in the bin directory
  354. fs::File::create(version_binary_path(&version)).unwrap();
  355. let expected = vec![version];
  356. assert_eq!(read_installed_versions().unwrap(), expected);
  357. // Should ignore this file because its not anchor- prefixed
  358. fs::File::create(AVM_HOME.join("bin").join("garbage").as_path()).unwrap();
  359. assert_eq!(read_installed_versions().unwrap(), expected);
  360. }
  361. #[test]
  362. fn test_get_anchor_version_from_commit() {
  363. let version =
  364. get_anchor_version_from_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9").unwrap();
  365. assert_eq!(
  366. version.to_string(),
  367. "0.28.0-e1afcbf71e0f2e10fae14525934a6a68479167b9"
  368. )
  369. }
  370. #[test]
  371. fn test_check_and_get_full_commit_when_full_commit() {
  372. assert_eq!(
  373. check_and_get_full_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9").unwrap(),
  374. "e1afcbf71e0f2e10fae14525934a6a68479167b9"
  375. )
  376. }
  377. #[test]
  378. fn test_check_and_get_full_commit_when_partial_commit() {
  379. assert_eq!(
  380. check_and_get_full_commit("e1afcbf").unwrap(),
  381. "e1afcbf71e0f2e10fae14525934a6a68479167b9"
  382. )
  383. }
  384. }