lib.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. use anyhow::{anyhow, Error, Result};
  2. use cargo_toml::Manifest;
  3. use chrono::{TimeZone, Utc};
  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::{BufRead, Write};
  10. use std::path::PathBuf;
  11. use std::process::{Command, Stdio};
  12. use std::sync::LazyLock;
  13. /// Storage directory for AVM, customizable by setting the $AVM_HOME, defaults to ~/.avm
  14. pub static AVM_HOME: LazyLock<PathBuf> = LazyLock::new(|| {
  15. cfg_if::cfg_if! {
  16. if #[cfg(test)] {
  17. let dir = tempfile::tempdir().expect("Could not create temporary directory");
  18. dir.path().join(".avm")
  19. } else {
  20. if let Ok(avm_home) = std::env::var("AVM_HOME") {
  21. PathBuf::from(avm_home)
  22. } else {
  23. let mut user_home = dirs::home_dir().expect("Could not find home directory");
  24. user_home.push(".avm");
  25. user_home
  26. }
  27. }
  28. }
  29. });
  30. /// Path to the current version file $AVM_HOME/.version
  31. fn current_version_file_path() -> PathBuf {
  32. AVM_HOME.join(".version")
  33. }
  34. /// Path to the current version file $AVM_HOME/bin
  35. fn get_bin_dir_path() -> PathBuf {
  36. AVM_HOME.join("bin")
  37. }
  38. /// Path to the binary for the given version
  39. pub fn version_binary_path(version: &Version) -> PathBuf {
  40. get_bin_dir_path().join(format!("anchor-{version}"))
  41. }
  42. /// Ensure the users home directory is setup with the paths required by AVM.
  43. pub fn ensure_paths() {
  44. let home_dir = AVM_HOME.to_path_buf();
  45. if !home_dir.exists() {
  46. fs::create_dir_all(&home_dir).expect("Could not create .avm directory");
  47. }
  48. let bin_dir = get_bin_dir_path();
  49. if !bin_dir.exists() {
  50. fs::create_dir_all(bin_dir).expect("Could not create .avm/bin directory");
  51. }
  52. if !current_version_file_path().exists() {
  53. fs::File::create(current_version_file_path()).expect("Could not create .version file");
  54. }
  55. }
  56. /// Read the current version from the version file
  57. pub fn current_version() -> Result<Version> {
  58. fs::read_to_string(current_version_file_path())
  59. .map_err(|e| anyhow!("Could not read version file: {}", e))?
  60. .trim_end_matches('\n')
  61. .parse::<Version>()
  62. .map_err(|e| anyhow!("Could not parse version file: {}", e))
  63. }
  64. /// Update the current version to a new version
  65. pub fn use_version(opt_version: Option<Version>) -> Result<()> {
  66. let version = match opt_version {
  67. Some(version) => version,
  68. None => read_anchorversion_file()?,
  69. };
  70. // Make sure the requested version is installed
  71. let installed_versions = read_installed_versions()?;
  72. if !installed_versions.contains(&version) {
  73. println!("Version {version} is not installed. Would you like to install? [y/n]");
  74. let input = std::io::stdin()
  75. .lock()
  76. .lines()
  77. .next()
  78. .expect("Expected input")?;
  79. match input.as_str() {
  80. "y" | "yes" => return install_version(InstallTarget::Version(version), false, false),
  81. _ => return Err(anyhow!("Installation rejected.")),
  82. };
  83. }
  84. let mut current_version_file = fs::File::create(current_version_file_path())?;
  85. current_version_file.write_all(version.to_string().as_bytes())?;
  86. println!("Now using anchor version {}.", current_version()?);
  87. Ok(())
  88. }
  89. #[derive(Clone)]
  90. pub enum InstallTarget {
  91. Version(Version),
  92. Commit(String),
  93. }
  94. /// Update to the latest version
  95. pub fn update() -> Result<()> {
  96. let latest_version = get_latest_version()?;
  97. install_version(InstallTarget::Version(latest_version), false, false)
  98. }
  99. /// The commit sha provided can be shortened,
  100. ///
  101. /// returns the full commit sha3 for unique versioning downstream
  102. pub fn check_and_get_full_commit(commit: &str) -> Result<String> {
  103. let client = reqwest::blocking::Client::new();
  104. let response = client
  105. .get(format!(
  106. "https://api.github.com/repos/coral-xyz/anchor/commits/{commit}"
  107. ))
  108. .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor")
  109. .send()?;
  110. if response.status() != StatusCode::OK {
  111. return Err(anyhow!(
  112. "Error checking commit {commit}: {}",
  113. response.text()?
  114. ));
  115. };
  116. #[derive(Deserialize)]
  117. struct GetCommitResponse {
  118. sha: String,
  119. }
  120. response
  121. .json::<GetCommitResponse>()
  122. .map(|resp| resp.sha)
  123. .map_err(|err| anyhow!("Failed to parse the response to JSON: {err:?}"))
  124. }
  125. fn get_anchor_version_from_commit(commit: &str) -> Result<Version> {
  126. // We read the version from cli/Cargo.toml since there is no simpler way to do so
  127. let client = reqwest::blocking::Client::new();
  128. let response = client
  129. .get(format!(
  130. "https://raw.githubusercontent.com/coral-xyz/anchor/{commit}/cli/Cargo.toml"
  131. ))
  132. .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor")
  133. .send()?;
  134. if response.status() != StatusCode::OK {
  135. return Err(anyhow!(
  136. "Could not find anchor-cli version for commit: {response:?}"
  137. ));
  138. };
  139. let anchor_cli_cargo_toml = response.text()?;
  140. let anchor_cli_manifest = Manifest::from_str(&anchor_cli_cargo_toml)?;
  141. let mut version = anchor_cli_manifest.package().version().parse::<Version>()?;
  142. version.pre = Prerelease::new(commit)?;
  143. Ok(version)
  144. }
  145. /// Install a version of anchor-cli
  146. pub fn install_version(
  147. install_target: InstallTarget,
  148. force: bool,
  149. from_source: bool,
  150. ) -> Result<()> {
  151. let version = match &install_target {
  152. InstallTarget::Version(version) => version.to_owned(),
  153. InstallTarget::Commit(commit) => get_anchor_version_from_commit(commit)?,
  154. };
  155. // Return early if version is already installed
  156. if !force && read_installed_versions()?.contains(&version) {
  157. eprintln!("Version `{version}` is already installed");
  158. return Ok(());
  159. }
  160. let is_commit = matches!(install_target, InstallTarget::Commit(_));
  161. let is_older_than_v0_31_0 = version < Version::parse("0.31.0")?;
  162. if from_source || is_commit || is_older_than_v0_31_0 {
  163. // Build from source using `cargo install --git`
  164. let mut args: Vec<String> = vec![
  165. "install".into(),
  166. "anchor-cli".into(),
  167. "--git".into(),
  168. "https://github.com/coral-xyz/anchor".into(),
  169. "--locked".into(),
  170. "--root".into(),
  171. AVM_HOME.to_str().unwrap().into(),
  172. ];
  173. let conditional_args = match install_target {
  174. InstallTarget::Version(version) => ["--tag".into(), format!("v{}", version)],
  175. InstallTarget::Commit(commit) => ["--rev".into(), commit],
  176. };
  177. args.extend_from_slice(&conditional_args);
  178. // If the version is older than v0.31, install using `rustc 1.79.0` to get around the problem
  179. // explained in https://github.com/coral-xyz/anchor/pull/3143
  180. if is_older_than_v0_31_0 {
  181. const REQUIRED_VERSION: &str = "1.79.0";
  182. let is_installed = Command::new("rustup")
  183. .args(["toolchain", "list"])
  184. .output()
  185. .map(|output| String::from_utf8(output.stdout))??
  186. .lines()
  187. .any(|line| line.starts_with(REQUIRED_VERSION));
  188. if !is_installed {
  189. let exit_status = Command::new("rustup")
  190. .args(["toolchain", "install", REQUIRED_VERSION])
  191. .spawn()?
  192. .wait()?;
  193. if !exit_status.success() {
  194. return Err(anyhow!(
  195. "Installation of `rustc {REQUIRED_VERSION}` failed. \
  196. `rustc <1.80` is required to install Anchor v{version} from source. \
  197. See https://github.com/coral-xyz/anchor/pull/3143 for more information."
  198. ));
  199. }
  200. }
  201. // Prepend the toolchain to use with the `cargo install` command
  202. args.insert(0, format!("+{REQUIRED_VERSION}"));
  203. }
  204. let output = Command::new("cargo")
  205. .args(args)
  206. .stdout(Stdio::inherit())
  207. .stderr(Stdio::inherit())
  208. .output()
  209. .map_err(|e| anyhow!("`cargo install` for version `{version}` failed: {e}"))?;
  210. if !output.status.success() {
  211. return Err(anyhow!(
  212. "Failed to install {version}, is it a valid version?"
  213. ));
  214. }
  215. let bin_dir = get_bin_dir_path();
  216. let bin_name = if cfg!(target_os = "windows") {
  217. "anchor.exe"
  218. } else {
  219. "anchor"
  220. };
  221. fs::rename(bin_dir.join(bin_name), version_binary_path(&version))?;
  222. } else {
  223. let output = Command::new("rustc").arg("-vV").output()?;
  224. let target = core::str::from_utf8(&output.stdout)?
  225. .lines()
  226. .find(|line| line.starts_with("host:"))
  227. .and_then(|line| line.split(':').last())
  228. .ok_or_else(|| anyhow!("`host` not found from `rustc -vV` output"))?
  229. .trim();
  230. let ext = if cfg!(target_os = "windows") {
  231. ".exe"
  232. } else {
  233. ""
  234. };
  235. let res = reqwest::blocking::get(format!(
  236. "https://github.com/coral-xyz/anchor/releases/download/v{version}/anchor-{version}-{target}{ext}"
  237. ))?;
  238. if !res.status().is_success() {
  239. return Err(anyhow!(
  240. "Failed to download the binary for version `{version}` (status code: {})",
  241. res.status()
  242. ));
  243. }
  244. let bin_path = version_binary_path(&version);
  245. fs::write(&bin_path, res.bytes()?)?;
  246. // Set file to executable on UNIX
  247. #[cfg(unix)]
  248. fs::set_permissions(
  249. bin_path,
  250. <fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o775),
  251. )?;
  252. }
  253. // If .version file is empty or not parseable, write the newly installed version to it
  254. if current_version().is_err() {
  255. let mut current_version_file = fs::File::create(current_version_file_path())?;
  256. current_version_file.write_all(version.to_string().as_bytes())?;
  257. }
  258. use_version(Some(version))
  259. }
  260. /// Remove an installed version of anchor-cli
  261. pub fn uninstall_version(version: &Version) -> Result<()> {
  262. let version_path = version_binary_path(version);
  263. if !version_path.exists() {
  264. return Err(anyhow!("anchor-cli {} is not installed", version));
  265. }
  266. if version == &current_version()? {
  267. return Err(anyhow!("anchor-cli {} is currently in use", version));
  268. }
  269. fs::remove_file(version_path)?;
  270. Ok(())
  271. }
  272. /// Read version from .anchorversion
  273. pub fn read_anchorversion_file() -> Result<Version> {
  274. fs::read_to_string(".anchorversion")
  275. .map_err(|e| anyhow!(".anchorversion file not found: {e}"))
  276. .map(|content| Version::parse(content.trim()))?
  277. .map_err(|e| anyhow!("Unable to parse version: {e}"))
  278. }
  279. /// Retrieve a list of installable versions of anchor-cli using the GitHub API and tags on the Anchor
  280. /// repository.
  281. pub fn fetch_versions() -> Result<Vec<Version>, Error> {
  282. #[derive(Deserialize)]
  283. struct Release {
  284. #[serde(rename = "name", deserialize_with = "version_deserializer")]
  285. version: Version,
  286. }
  287. fn version_deserializer<'de, D>(deserializer: D) -> Result<Version, D::Error>
  288. where
  289. D: de::Deserializer<'de>,
  290. {
  291. let s: &str = de::Deserialize::deserialize(deserializer)?;
  292. Version::parse(s.trim_start_matches('v')).map_err(de::Error::custom)
  293. }
  294. let response = reqwest::blocking::Client::new()
  295. .get("https://api.github.com/repos/coral-xyz/anchor/tags")
  296. .header(USER_AGENT, "avm https://github.com/coral-xyz/anchor")
  297. .send()?;
  298. if response.status().is_success() {
  299. let releases: Vec<Release> = response.json()?;
  300. let versions = releases.into_iter().map(|r| r.version).collect();
  301. Ok(versions)
  302. } else {
  303. let reset_time_header = response
  304. .headers()
  305. .get("X-RateLimit-Reset")
  306. .map_or("unknown", |v| v.to_str().unwrap());
  307. let t = Utc.timestamp_opt(reset_time_header.parse::<i64>().unwrap(), 0);
  308. let reset_time = t
  309. .single()
  310. .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string())
  311. .unwrap_or_else(|| "unknown".to_string());
  312. Err(anyhow!(
  313. "GitHub API rate limit exceeded. Try again after {} UTC.",
  314. reset_time
  315. ))
  316. }
  317. }
  318. /// Print available versions and flags indicating installed, current and latest
  319. pub fn list_versions() -> Result<()> {
  320. let mut installed_versions = read_installed_versions()?;
  321. let mut available_versions = fetch_versions()?;
  322. // Reverse version list so latest versions are printed last
  323. available_versions.reverse();
  324. let print_versions =
  325. |versions: Vec<Version>, installed_versions: &mut Vec<Version>, show_latest: bool| {
  326. versions.iter().enumerate().for_each(|(i, v)| {
  327. print!("{v}");
  328. let mut flags = vec![];
  329. if i == versions.len() - 1 && show_latest {
  330. flags.push("latest");
  331. }
  332. if let Some(position) = installed_versions.iter().position(|iv| iv == v) {
  333. flags.push("installed");
  334. installed_versions.remove(position);
  335. }
  336. if current_version().map(|cv| &cv == v).unwrap_or_default() {
  337. flags.push("current");
  338. }
  339. if flags.is_empty() {
  340. println!();
  341. } else {
  342. println!("\t({})", flags.join(", "));
  343. }
  344. })
  345. };
  346. print_versions(available_versions, &mut installed_versions, true);
  347. print_versions(installed_versions.clone(), &mut installed_versions, false);
  348. Ok(())
  349. }
  350. pub fn get_latest_version() -> Result<Version> {
  351. fetch_versions()?
  352. .into_iter()
  353. .next()
  354. .ok_or_else(|| anyhow!("First version not found"))
  355. }
  356. /// Read the installed anchor-cli versions by reading the binaries in the AVM_HOME/bin directory.
  357. pub fn read_installed_versions() -> Result<Vec<Version>> {
  358. const PREFIX: &str = "anchor-";
  359. let versions = fs::read_dir(get_bin_dir_path())?
  360. .filter_map(|entry_result| entry_result.ok())
  361. .filter_map(|entry| entry.file_name().to_str().map(|f| f.to_owned()))
  362. .filter(|file_name| file_name.starts_with(PREFIX))
  363. .filter_map(|file_name| file_name.trim_start_matches(PREFIX).parse::<Version>().ok())
  364. .collect();
  365. Ok(versions)
  366. }
  367. #[cfg(test)]
  368. mod tests {
  369. use crate::*;
  370. use semver::Version;
  371. use std::fs;
  372. use std::io::Write;
  373. use std::path::Path;
  374. #[test]
  375. fn test_ensure_paths() {
  376. ensure_paths();
  377. assert!(AVM_HOME.exists());
  378. let bin_dir = get_bin_dir_path();
  379. assert!(bin_dir.exists());
  380. let current_version_file = current_version_file_path();
  381. assert!(current_version_file.exists());
  382. }
  383. #[test]
  384. fn test_version_binary_path() {
  385. assert_eq!(
  386. version_binary_path(&Version::parse("0.18.2").unwrap()),
  387. get_bin_dir_path().join("anchor-0.18.2")
  388. );
  389. }
  390. #[test]
  391. fn test_read_anchorversion() -> Result<()> {
  392. ensure_paths();
  393. let anchorversion_path = Path::new(".anchorversion");
  394. let test_version = "0.26.0";
  395. fs::write(anchorversion_path, test_version)?;
  396. let version = read_anchorversion_file()?;
  397. assert_eq!(version.to_string(), test_version);
  398. fs::remove_file(anchorversion_path)?;
  399. Ok(())
  400. }
  401. #[test]
  402. fn test_current_version() {
  403. ensure_paths();
  404. let mut current_version_file = fs::File::create(current_version_file_path()).unwrap();
  405. current_version_file.write_all("0.18.2".as_bytes()).unwrap();
  406. // Sync the file to disk before the read in current_version() to
  407. // mitigate the read not seeing the written version bytes.
  408. current_version_file.sync_all().unwrap();
  409. assert_eq!(
  410. current_version().unwrap(),
  411. Version::parse("0.18.2").unwrap()
  412. );
  413. }
  414. #[test]
  415. #[should_panic(expected = "anchor-cli 0.18.1 is not installed")]
  416. fn test_uninstall_non_installed_version() {
  417. uninstall_version(&Version::parse("0.18.1").unwrap()).unwrap();
  418. }
  419. #[test]
  420. #[should_panic(expected = "anchor-cli 0.18.2 is currently in use")]
  421. fn test_uninstalled_in_use_version() {
  422. ensure_paths();
  423. let version = Version::parse("0.18.2").unwrap();
  424. let mut current_version_file = fs::File::create(current_version_file_path()).unwrap();
  425. current_version_file.write_all("0.18.2".as_bytes()).unwrap();
  426. // Sync the file to disk before the read in current_version() to
  427. // mitigate the read not seeing the written version bytes.
  428. current_version_file.sync_all().unwrap();
  429. // Create a fake binary for anchor-0.18.2 in the bin directory
  430. fs::File::create(version_binary_path(&version)).unwrap();
  431. uninstall_version(&version).unwrap();
  432. }
  433. #[test]
  434. fn test_read_installed_versions() {
  435. ensure_paths();
  436. let version = Version::parse("0.18.2").unwrap();
  437. // Create a fake binary for anchor-0.18.2 in the bin directory
  438. fs::File::create(version_binary_path(&version)).unwrap();
  439. let expected = vec![version];
  440. assert_eq!(read_installed_versions().unwrap(), expected);
  441. // Should ignore this file because its not anchor- prefixed
  442. fs::File::create(AVM_HOME.join("bin").join("garbage").as_path()).unwrap();
  443. assert_eq!(read_installed_versions().unwrap(), expected);
  444. }
  445. #[test]
  446. fn test_get_anchor_version_from_commit() {
  447. let version =
  448. get_anchor_version_from_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9").unwrap();
  449. assert_eq!(
  450. version.to_string(),
  451. "0.28.0-e1afcbf71e0f2e10fae14525934a6a68479167b9"
  452. )
  453. }
  454. #[test]
  455. fn test_check_and_get_full_commit_when_full_commit() {
  456. assert_eq!(
  457. check_and_get_full_commit("e1afcbf71e0f2e10fae14525934a6a68479167b9").unwrap(),
  458. "e1afcbf71e0f2e10fae14525934a6a68479167b9"
  459. )
  460. }
  461. #[test]
  462. fn test_check_and_get_full_commit_when_partial_commit() {
  463. assert_eq!(
  464. check_and_get_full_commit("e1afcbf").unwrap(),
  465. "e1afcbf71e0f2e10fae14525934a6a68479167b9"
  466. )
  467. }
  468. }