From 738131856b698c3bb94446d1cd28439b309a3a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Misty=20De=20M=C3=A9o?= Date: Wed, 28 Aug 2024 10:47:00 -0700 Subject: [PATCH 1/3] feat: support XDG_CONFIG_HOME for receipts refs https://github.com/axodotdev/cargo-dist/pull/1355. --- axoupdater/src/receipt.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/axoupdater/src/receipt.rs b/axoupdater/src/receipt.rs index 72510bf..03e9a0c 100644 --- a/axoupdater/src/receipt.rs +++ b/axoupdater/src/receipt.rs @@ -122,16 +122,27 @@ pub(crate) fn get_config_path(app_name: &str) -> AxoupdateResult { } else if let Ok(path) = env::var("AXOUPDATER_CONFIG_PATH") { Ok(Utf8PathBuf::from(path)) } else { + let xdg_home = env::var("XDG_CONFIG_HOME") + .ok() + .map(PathBuf::from) + .map(|h| h.join(app_name)); + let xdg_home_exists = xdg_home.as_ref().map(|h| h.exists()).unwrap_or(false); + let home = if cfg!(windows) { - env::var("LOCALAPPDATA").map(PathBuf::from).ok() + env::var("LOCALAPPDATA") + .map(PathBuf::from) + .map(|h| h.join(app_name)) + .ok() + } else if xdg_home_exists { + xdg_home } else { - homedir::my_home()?.map(|path| path.join(".config")) + homedir::my_home()?.map(|path| path.join(".config").join(app_name)) }; let Some(home) = home else { return Err(AxoupdateError::NoHome {}); }; - Ok(Utf8PathBuf::try_from(home)?.join(app_name)) + Ok(Utf8PathBuf::try_from(home)?) } } From 81ffaafef7e1e18351e817ea1bf7a0cb1cd8b207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Misty=20De=20M=C3=A9o?= Date: Wed, 18 Dec 2024 14:14:19 -0800 Subject: [PATCH 2/3] feat: check multiple receipt paths We now check multiple receipt paths, including XDG_CONFIG_HOME. We load a receipt from the first path which contains a receipt file, ensuring we're able to manage the absence of a receipt in that path if that environment variable is set. --- axoupdater/src/errors.rs | 10 +++++++ axoupdater/src/receipt.rs | 50 ++++++++++++++++++++++++---------- axoupdater/src/test/helpers.rs | 7 +++-- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/axoupdater/src/errors.rs b/axoupdater/src/errors.rs index 2a5eea2..014b6d2 100644 --- a/axoupdater/src/errors.rs +++ b/axoupdater/src/errors.rs @@ -85,6 +85,16 @@ pub enum AxoupdateError { app_name: String, }, + /// Not a generic receipt load failure, but the receipt itself doesn't exist. + #[error("Unable to load receipt for app {app_name}")] + #[diagnostic(help( + "This may indicate that this installation of {app_name} was installed via a method that's not eligible for upgrades." + ))] + NoReceipt { + /// This app's name + app_name: String, + }, + /// Indicates that this app's name couldn't be determined when trying /// to autodetect it. #[error("Unable to determine the name of the app to update")] diff --git a/axoupdater/src/receipt.rs b/axoupdater/src/receipt.rs index 03e9a0c..c4e16d3 100644 --- a/axoupdater/src/receipt.rs +++ b/axoupdater/src/receipt.rs @@ -116,49 +116,69 @@ impl AxoUpdater { } } -pub(crate) fn get_config_path(app_name: &str) -> AxoupdateResult { +/// Returns a Vec of possible receipt locations, beginning with +/// `XDG_CONFIG_HOME` (if set). +pub(crate) fn get_config_paths(app_name: &str) -> AxoupdateResult> { + let mut potential_homes = vec![]; + if env::var("AXOUPDATER_CONFIG_WORKING_DIR").is_ok() { - Ok(Utf8PathBuf::try_from(current_dir()?)?) + Ok(vec![Utf8PathBuf::try_from(current_dir()?)?]) } else if let Ok(path) = env::var("AXOUPDATER_CONFIG_PATH") { - Ok(Utf8PathBuf::from(path)) + Ok(vec![Utf8PathBuf::from(path)]) } else { let xdg_home = env::var("XDG_CONFIG_HOME") .ok() - .map(PathBuf::from) + .map(Utf8PathBuf::from) .map(|h| h.join(app_name)); - let xdg_home_exists = xdg_home.as_ref().map(|h| h.exists()).unwrap_or(false); - + if let Some(home) = &xdg_home { + if home.exists() { + potential_homes.push(home.to_owned()); + } + } let home = if cfg!(windows) { env::var("LOCALAPPDATA") .map(PathBuf::from) .map(|h| h.join(app_name)) .ok() - } else if xdg_home_exists { - xdg_home } else { homedir::my_home()?.map(|path| path.join(".config").join(app_name)) }; - let Some(home) = home else { + if let Some(home) = home { + potential_homes.push(Utf8PathBuf::try_from(home)?); + } + + if potential_homes.is_empty() { return Err(AxoupdateError::NoHome {}); - }; + } - Ok(Utf8PathBuf::try_from(home)?) + Ok(potential_homes) } } +/// Iterates through the list of possible receipt locations from +/// `get_config_paths` and returns the first that contains a valid receipt. +pub(crate) fn get_receipt_path(app_name: &str) -> AxoupdateResult> { + for receipt_prefix in get_config_paths(app_name)? { + let install_receipt_path = receipt_prefix.join(format!("{app_name}-receipt.json")); + if install_receipt_path.exists() { + return Ok(Some(install_receipt_path)); + } + } + + Ok(None) +} + fn load_receipt_from_path(install_receipt_path: &Utf8PathBuf) -> AxoupdateResult { Ok(SourceFile::load_local(install_receipt_path)?.deserialize_json()?) } fn load_receipt_for(app_name: &str) -> AxoupdateResult { - let Ok(receipt_prefix) = get_config_path(app_name) else { - return Err(AxoupdateError::ConfigFetchFailed { + let Some(install_receipt_path) = get_receipt_path(app_name)? else { + return Err(AxoupdateError::NoReceipt { app_name: app_name.to_owned(), }); }; - let install_receipt_path = receipt_prefix.join(format!("{app_name}-receipt.json")); - load_receipt_from_path(&install_receipt_path).map_err(|_| AxoupdateError::ReceiptLoadFailed { app_name: app_name.to_owned(), }) diff --git a/axoupdater/src/test/helpers.rs b/axoupdater/src/test/helpers.rs index edf64f7..b57432a 100644 --- a/axoupdater/src/test/helpers.rs +++ b/axoupdater/src/test/helpers.rs @@ -3,7 +3,7 @@ use std::{ process::{Command, Stdio}, }; -use crate::{receipt::get_config_path, ReleaseSourceType}; +use crate::{receipt::get_receipt_path, ReleaseSourceType}; static RECEIPT_TEMPLATE: &str = r#"{"binaries":[BINARIES],"install_prefix":"INSTALL_PREFIX","provider":{"source":"cargo-dist","version":"0.10.0-prerelease.1"},"source":{"app_name":"APP_NAME","name":"PACKAGE","owner":"OWNER","release_type":"RELEASE_TYPE"},"version":"VERSION"}"#; @@ -104,7 +104,10 @@ pub fn perform_runtest(runtest_args: &RuntestArgs) -> PathBuf { let app_home = &home.join(".cargo").join("bin"); let app_path = &app_home.join(basename); - let config_path = get_config_path(app_name).unwrap().into_std_path_buf(); + let config_path = get_receipt_path(app_name) + .unwrap() + .unwrap() + .into_std_path_buf(); // Ensure we delete any previous copy that may exist // at this path before we copy in our version. From 4de54741f66d5f464c232f1a4d5c9fefd489d072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Misty=20De=20M=C3=A9o?= Date: Thu, 19 Dec 2024 10:28:43 -0800 Subject: [PATCH 3/3] tests: add XDG_CONFIG_HOME test --- axoupdater-cli/tests/integration.rs | 112 +++++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/axoupdater-cli/tests/integration.rs b/axoupdater-cli/tests/integration.rs index 11e4433..1776e8e 100644 --- a/axoupdater-cli/tests/integration.rs +++ b/axoupdater-cli/tests/integration.rs @@ -45,10 +45,14 @@ fn install_receipt(version: &str, cargo_dist_version: &str, prefix: &Utf8PathBuf fn write_receipt( version: &str, cargo_dist_version: &str, - prefix: &Utf8PathBuf, + receipt_prefix: &Utf8PathBuf, + install_prefix: &Utf8PathBuf, ) -> std::io::Result<()> { - let contents = install_receipt(version, cargo_dist_version, prefix); - let receipt_name = prefix.join("axolotlsay-receipt.json"); + // Create the prefix in case it doesn't exist + LocalAsset::create_dir_all(receipt_prefix).unwrap(); + + let contents = install_receipt(version, cargo_dist_version, install_prefix); + let receipt_name = receipt_prefix.join("axolotlsay-receipt.json"); LocalAsset::write_new(&contents, receipt_name).unwrap(); Ok(()) @@ -94,7 +98,12 @@ fn test_upgrade() -> std::io::Result<()> { .unwrap(); // Write the receipt for the updater to use - write_receipt(base_version, "0.11.1", &bindir.to_path_buf())?; + write_receipt( + base_version, + "0.11.1", + &bindir.to_path_buf(), + &bindir.to_path_buf(), + )?; LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap(); @@ -133,6 +142,71 @@ fn test_upgrade() -> std::io::Result<()> { Ok(()) } +#[test] +fn test_upgrade_xdg_config_home() -> std::io::Result<()> { + let tempdir = TempDir::new()?; + let bindir_path = &tempdir.path().join("bin"); + let bindir = Utf8Path::from_path(bindir_path).unwrap(); + std::fs::create_dir_all(bindir)?; + let xdg_config_home = tempdir.path().join("config"); + let xdg_config_home = Utf8Path::from_path(&xdg_config_home).unwrap(); + + let base_version = "0.2.115"; + + let url = axolotlsay_tarball_path(base_version); + let compressed_path = + Utf8PathBuf::from_path_buf(tempdir.path().join("axolotlsay.tar.gz")).unwrap(); + + let client = axoasset::AxoClient::with_reqwest(axoasset::reqwest::Client::new()); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(client.load_and_write_to_file(&url, &compressed_path)) + .unwrap(); + + // Write the receipt for the updater to use + write_receipt( + base_version, + "0.11.1", + &xdg_config_home.join("axolotlsay"), + &bindir.to_path_buf(), + )?; + + LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap(); + + // Now install our copy of the updater instead of the one axolotlsay came with + let updater_path = bindir.join(format!("axolotlsay-update{EXE_SUFFIX}")); + std::fs::copy(BIN, &updater_path)?; + + let mut updater = Cmd::new(&updater_path, "run updater"); + updater.env("XDG_CONFIG_HOME", xdg_config_home); + // If we're not running in CI, try to avoid ruining the user's PATH. + if std::env::var("CI").is_err() { + updater.env("INSTALLER_NO_MODIFY_PATH", "1"); + updater.env("AXOLOTLSAY_NO_MODIFY_PATH", "1"); + } + // We'll do that manually + updater.check(false); + let res = updater.output().unwrap(); + let output_stdout = String::from_utf8(res.stdout).unwrap(); + let output_stderr = String::from_utf8(res.stderr).unwrap(); + + // Now let's check the version we just updated to + let new_axolotlsay_path = &bindir.join(format!("axolotlsay{EXE_SUFFIX}")); + assert!( + new_axolotlsay_path.exists(), + "update result was\nstdout\n{}\nstderr\n{}", + output_stdout, + output_stderr + ); + let mut new_axolotlsay = Cmd::new(new_axolotlsay_path, "version test"); + new_axolotlsay.arg("--version"); + let output = new_axolotlsay.output().unwrap(); + let stderr_string = String::from_utf8(output.stdout).unwrap(); + assert!(stderr_string.starts_with("axolotlsay ")); + assert_ne!(stderr_string, format!("axolotlsay {}\n", base_version)); + + Ok(()) +} + #[test] fn test_upgrade_allow_prerelease() -> std::io::Result<()> { let tempdir = TempDir::new()?; @@ -152,7 +226,12 @@ fn test_upgrade_allow_prerelease() -> std::io::Result<()> { .unwrap(); // Write the receipt for the updater to use - write_receipt(base_version, "0.11.1", &bindir.to_path_buf())?; + write_receipt( + base_version, + "0.11.1", + &bindir.to_path_buf(), + &bindir.to_path_buf(), + )?; LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap(); @@ -214,7 +293,12 @@ fn test_upgrade_to_specific_version() -> std::io::Result<()> { .unwrap(); // Write the receipt for the updater to use - write_receipt(base_version, "0.11.1", &bindir.to_path_buf())?; + write_receipt( + base_version, + "0.11.1", + &bindir.to_path_buf(), + &bindir.to_path_buf(), + )?; LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap(); @@ -268,7 +352,12 @@ fn test_downgrade_to_specific_version() -> std::io::Result<()> { .unwrap(); // Write the receipt for the updater to use - write_receipt(base_version, "0.11.1", &bindir.to_path_buf())?; + write_receipt( + base_version, + "0.11.1", + &bindir.to_path_buf(), + &bindir.to_path_buf(), + )?; LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap(); @@ -335,7 +424,12 @@ fn test_downgrade_to_specific_old_version() -> std::io::Result<()> { .unwrap(); // Write the receipt for the updater to use - write_receipt(base_version, "0.11.1", &bindir.to_path_buf())?; + write_receipt( + base_version, + "0.11.1", + &bindir.to_path_buf(), + &bindir.to_path_buf(), + )?; LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap(); @@ -396,7 +490,7 @@ fn test_upgrade_from_prefix_with_no_bin() -> std::io::Result<()> { // Write the receipt for the updater to use // 0.15.0 is the first cargo-dist that published fixed installers for the // /bin bug mentioned above - write_receipt(base_version, "0.15.0", &prefix)?; + write_receipt(base_version, "0.15.0", &prefix, &prefix)?; LocalAsset::untar_gz_all(&compressed_path, bindir).unwrap();