diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a8b35..7db9bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate +- Added `dfxvm self update` command, which updates dfxvm to the latest version. + ## [0.1.2] - 2023-12-19 - dfxvm-init now alters profile scripts to modify the PATH environment variable. diff --git a/docs/cli-reference/dfxvm/dfxvm-self-update.md b/docs/cli-reference/dfxvm/dfxvm-self-update.md index 94528ac..ace3de1 100644 --- a/docs/cli-reference/dfxvm/dfxvm-self-update.md +++ b/docs/cli-reference/dfxvm/dfxvm-self-update.md @@ -1,3 +1,9 @@ # dfxvm self update Updates to the newest version of dfxvm. + +## Usage + +```bash +dfxvm self update +``` diff --git a/src/dfx.rs b/src/dfx.rs index 49b9a13..f255c01 100644 --- a/src/dfx.rs +++ b/src/dfx.rs @@ -1,3 +1,4 @@ +use crate::dfxvm::cleanup_self_updater; use crate::error::dfx; use crate::error::dfx::Error::Exec; use crate::error::dfx::{ @@ -18,6 +19,7 @@ use std::path::PathBuf; use std::process::ExitCode; pub fn main(args: &[OsString], locations: &Locations) -> Result { + cleanup_self_updater(locations)?; let Some((version, args)) = get_dfx_version_and_command_args(args, locations)? else { err!("Unable to determine which dfx version to call. To set a default version, run:"); err!(" {}", style_command("dfxvm default ")); diff --git a/src/dfxvm.rs b/src/dfxvm.rs index d830bbd..d942ac2 100644 --- a/src/dfxvm.rs +++ b/src/dfxvm.rs @@ -9,4 +9,6 @@ mod update; pub use cli::main; pub use default::set_default; +pub use self_update::cleanup_self_updater; +pub use self_update::self_replace; pub use update::update; diff --git a/src/dfxvm/cli.rs b/src/dfxvm/cli.rs index 501f8bd..83d75b7 100644 --- a/src/dfxvm/cli.rs +++ b/src/dfxvm/cli.rs @@ -1,6 +1,6 @@ use crate::dfxvm::{ - default::default, install::install, list::list, self_uninstall::self_uninstall, - self_update::self_update, uninstall::uninstall, update::update, + cleanup_self_updater, default::default, install::install, list::list, + self_uninstall::self_uninstall, self_update::self_update, uninstall::uninstall, update::update, }; use crate::error::dfxvm; use crate::locations::Locations; @@ -80,13 +80,14 @@ pub struct SelfUpdateOpts {} pub struct SelfUninstallOpts {} pub async fn main(args: &[OsString], locations: &Locations) -> Result { + cleanup_self_updater(locations)?; let cli = Cli::parse_from(args); match cli.command { Command::Default(opts) => default(opts.version, locations).await?, Command::Install(opts) => install(opts.version, locations).await?, Command::List(_opts) => list(locations)?, Command::SelfCommand(opts) => match opts.command { - SelfCommand::Update(_opts) => self_update(locations)?, + SelfCommand::Update(_opts) => self_update(locations).await?, SelfCommand::Uninstall(_opts) => self_uninstall(locations)?, }, Command::Uninstall(opts) => uninstall(opts.version, locations)?, diff --git a/src/dfxvm/self_update.rs b/src/dfxvm/self_update.rs index 01cca9f..c043a61 100644 --- a/src/dfxvm/self_update.rs +++ b/src/dfxvm/self_update.rs @@ -1,7 +1,138 @@ -use crate::error::dfxvm::SelfUpdateError; +use crate::dist_manifest::lookup_latest_version; +use crate::download::{download_file, verify_checksum}; +use crate::error::dfxvm::self_update::CleanupSelfUpdaterError; +use crate::error::dfxvm::{ + self_update::{ + DownloadLatestBinaryError, + DownloadLatestBinaryError::CreateTempDirIn, + ExtractBinaryError, + ExtractBinaryError::{DfxvmNotFound, ReadArchiveEntries, UnpackBinary}, + FormatTarballUrlError, SelfReplaceError, + }, + SelfUpdateError, + SelfUpdateError::Exec, +}; +use crate::fs::{open_file, remove_file}; +use crate::installation::install_binaries; use crate::locations::Locations; +use crate::settings::Settings; +use flate2::read::GzDecoder; +use reqwest::{Client, Url}; +use std::os::unix::prelude::CommandExt; +use std::path::Path; +use tar::Archive; -pub fn self_update(_locations: &Locations) -> Result<(), SelfUpdateError> { - println!("update dfxvm to latest"); +pub async fn self_update(locations: &Locations) -> Result<(), SelfUpdateError> { + info!("checking for self-update"); + let settings = Settings::load_or_default(&locations.settings_path())?; + let latest_version = lookup_latest_version(&settings).await?; + let our_version = env!("CARGO_PKG_VERSION"); + if latest_version == our_version { + info!("dfxvm unchanged - {latest_version}"); + return Ok(()); + } + + info!("updating to {latest_version}"); + + let tarball_url = format_tarball_url(&settings)?; + let self_update_path = locations.self_update_path(); + + download_latest_binary(&tarball_url, &self_update_path, locations).await?; + + let mut command = std::process::Command::new(self_update_path); + command.arg("--self-replace"); + let err = command.exec(); + Err(Exec { + command, + source: err, + }) +} + +pub fn self_replace(locations: &Locations) -> Result<(), SelfReplaceError> { + install_binaries(&locations.bin_dir())?; + Ok(()) +} + +// called on next execution of dfx or dfxvm +pub fn cleanup_self_updater(locations: &Locations) -> Result<(), CleanupSelfUpdaterError> { + let path = locations.self_update_path(); + + if path.exists() { + remove_file(&path)?; + } + + Ok(()) +} + +fn format_tarball_url(settings: &Settings) -> Result { + #[cfg(target_arch = "aarch64")] + let architecture = "aarch64-apple-darwin"; + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] + let architecture = "x86_64-apple-darwin"; + #[cfg(target_os = "linux")] + let architecture = "x86_64-unknown-linux-gnu"; + + let basename = format!("dfxvm-{}", architecture); + let url = format!( + "{}/{basename}.tar.gz", + settings.dfxvm_latest_download_root() + ); + + Url::parse(&url).map_err(|source| FormatTarballUrlError { url, source }) +} + +async fn download_latest_binary( + tarball_url: &Url, + binary_path: &Path, + locations: &Locations, +) -> Result<(), DownloadLatestBinaryError> { + let shasum_url = Url::parse(&format!("{tarball_url}.sha256"))?; + + let download_dir = tempfile::Builder::new() + .prefix("dfxvm-download") + .tempdir_in(locations.data_local_dir()) + .map_err(|source| CreateTempDirIn { + path: locations.data_local_dir().to_path_buf(), + source, + })?; + + let downloaded_tarball_path = download_dir.path().join("dfxvm.tar.gz"); + let downloaded_shasum_path = download_dir.path().join("dfxvm.tar.gz.sha256"); + + let client = Client::new(); + + download_file(&client, &shasum_url, &downloaded_shasum_path).await?; + let computed_hash = download_file(&client, tarball_url, &downloaded_tarball_path).await?; + verify_checksum(computed_hash, &downloaded_shasum_path)?; + + extract_binary(binary_path, &downloaded_tarball_path)?; + Ok(()) +} + +fn extract_binary( + binary_path: &Path, + downloaded_tarball_path: &Path, +) -> Result<(), ExtractBinaryError> { + let tar_gz = open_file(downloaded_tarball_path)?; + let tar = GzDecoder::new(tar_gz); + + Archive::new(tar) + .entries() + .map_err(ReadArchiveEntries)? + .enumerate() + .filter_map(|(_i, entry)| entry.ok()) + .find(|entry| { + entry + .header() + .path() + .ok() + .as_ref() + .and_then(|x| x.to_str()) + .map(|str_path| str_path.ends_with("dfxvm")) + .unwrap_or(false) + }) + .ok_or(DfxvmNotFound)? + .unpack(binary_path) + .map_err(UnpackBinary)?; Ok(()) } diff --git a/src/dfxvm_init/cli.rs b/src/dfxvm_init/cli.rs index 5868ae6..630ffdf 100644 --- a/src/dfxvm_init/cli.rs +++ b/src/dfxvm_init/cli.rs @@ -1,3 +1,4 @@ +use crate::dfxvm::self_replace; use crate::dfxvm_init::initialize::initialize; use crate::dfxvm_init::plan::{ DfxVersion::{Latest, Specific}, @@ -29,6 +30,12 @@ pub struct Cli { } pub async fn main(args: &[OsString], locations: &Locations) -> Result { + let arg1 = args.get(1).map(|a| &**a); + if arg1 == Some("--self-replace".as_ref()) { + self_replace(locations)?; + return Ok(ExitCode::SUCCESS); + } + let opts = Cli::parse_from(args); let confirmation = if opts.proceed { diff --git a/src/dfxvm_init/plan.rs b/src/dfxvm_init/plan.rs index 7ab12c3..f1e3317 100644 --- a/src/dfxvm_init/plan.rs +++ b/src/dfxvm_init/plan.rs @@ -56,7 +56,7 @@ pub struct Plan { impl Plan { pub fn new(options: PlanOptions, locations: &Locations) -> Self { - let bin_dir = locations.data_local_dir().join("bin"); + let bin_dir = locations.bin_dir(); let env_path = locations.data_local_dir().join("env"); let env_path_user_facing = get_env_path_user_facing().to_string(); let profile_scripts = get_detected_profile_scripts(); diff --git a/src/dist_manifest.rs b/src/dist_manifest.rs new file mode 100644 index 0000000..49bd4a2 --- /dev/null +++ b/src/dist_manifest.rs @@ -0,0 +1,38 @@ +use crate::error::dfxvm::self_update::LookupLatestVersionError; +use crate::json::fetch_json; +use crate::settings::Settings; +use serde::Deserialize; +use url::Url; + +#[derive(Deserialize, Debug)] +struct Release { + app_name: String, + app_version: String, +} + +#[derive(Deserialize, Debug)] +struct DistManifest { + releases: Vec, +} + +pub async fn lookup_latest_version( + settings: &Settings, +) -> Result { + let dist_manifest_url = format!( + "{}/dist-manifest.json", + settings.dfxvm_latest_download_root() + ); + let url = + Url::parse(&dist_manifest_url).map_err(|source| LookupLatestVersionError::ParseUrl { + url: dist_manifest_url, + source, + })?; + let dist_manifest = fetch_json::(&url).await?; + let dfxvm_release = dist_manifest + .releases + .iter() + .find(|release| release.app_name == "dfxvm") + .ok_or(LookupLatestVersionError::NoDfxvmRelease { url })?; + let latest_version = dfxvm_release.app_version.clone(); + Ok(latest_version) +} diff --git a/src/error/dfx.rs b/src/error/dfx.rs index fb109ab..12b217d 100644 --- a/src/error/dfx.rs +++ b/src/error/dfx.rs @@ -1,9 +1,15 @@ -use crate::error::{env::GetCurrentDirError, fs::CanonicalizePathError, json::LoadJsonFileError}; +use crate::error::{ + dfxvm::self_update::CleanupSelfUpdaterError, env::GetCurrentDirError, + fs::CanonicalizePathError, json::LoadJsonFileError, +}; use std::process::Command; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { + #[error(transparent)] + CleanupSelfUpdater(#[from] CleanupSelfUpdaterError), + #[error(transparent)] DetermineDfxVersion(#[from] DetermineDfxVersionError), diff --git a/src/error/dfxvm.rs b/src/error/dfxvm.rs index 9fc121a..830abd8 100644 --- a/src/error/dfxvm.rs +++ b/src/error/dfxvm.rs @@ -1,4 +1,5 @@ use crate::error::{ + dfxvm::self_update::CleanupSelfUpdaterError, fs::{RemoveDirAllError, RemoveFileError, RenameError}, json::{FetchJsonDocError, LoadJsonFileError}, }; @@ -7,13 +8,18 @@ use thiserror::Error; pub mod default; pub mod install; +pub mod self_update; pub use default::DefaultError; pub use default::SetDefaultError; pub use install::InstallError; +pub use self_update::SelfUpdateError; #[derive(Error, Debug)] pub enum Error { + #[error(transparent)] + CleanupSelfUpdater(#[from] CleanupSelfUpdaterError), + #[error(transparent)] Default(#[from] DefaultError), @@ -77,6 +83,3 @@ pub enum UpdateError { #[derive(Error, Debug)] pub enum SelfUninstallError {} - -#[derive(Error, Debug)] -pub enum SelfUpdateError {} diff --git a/src/error/dfxvm/self_update.rs b/src/error/dfxvm/self_update.rs new file mode 100644 index 0000000..7bbf002 --- /dev/null +++ b/src/error/dfxvm/self_update.rs @@ -0,0 +1,105 @@ +use crate::error::{ + download::{DownloadFileError, VerifyChecksumError}, + env::NoHomeDirectoryError, + fs::{OpenFileError, RemoveFileError}, + installation::InstallBinariesError, + json::{FetchJsonDocError, LoadJsonFileError}, +}; +use std::path::PathBuf; +use std::process::Command; +use thiserror::Error; +use url::Url; + +#[derive(Error, Debug)] +pub enum SelfUpdateError { + #[error(transparent)] + DownloadLatestBinaryError(#[from] DownloadLatestBinaryError), + + #[error("failed to execute {command:#?}")] + Exec { + command: Command, + source: std::io::Error, + }, + + #[error(transparent)] + LoadJsonFile(#[from] LoadJsonFileError), + + #[error(transparent)] + LookupLatestVersionError(#[from] LookupLatestVersionError), + + #[error(transparent)] + FormatTarballUrl(#[from] FormatTarballUrlError), +} + +#[derive(Error, Debug)] +#[error("failed to format tarball url {url}")] +pub struct FormatTarballUrlError { + pub url: String, + pub source: url::ParseError, +} + +#[derive(Error, Debug)] +pub enum LookupLatestVersionError { + #[error(transparent)] + FetchJsonDoc(#[from] FetchJsonDocError), + + #[error("failed to parse url {url}")] + ParseUrl { + url: String, + source: url::ParseError, + }, + + #[error("no dfxvm release found at {url}")] + NoDfxvmRelease { url: Url }, +} + +#[derive(Error, Debug)] +pub enum DownloadLatestBinaryError { + #[error("failed to create a temporary directory in {path}")] + CreateTempDirIn { + path: PathBuf, + source: std::io::Error, + }, + + #[error(transparent)] + DownloadFile(#[from] DownloadFileError), + + #[error(transparent)] + ExtractBinary(#[from] ExtractBinaryError), + + #[error(transparent)] + ParseUrl(#[from] url::ParseError), + + #[error(transparent)] + VerifyChecksum(#[from] VerifyChecksumError), +} + +#[derive(Error, Debug)] +pub enum ExtractBinaryError { + #[error("dfxvm not found in archive")] + DfxvmNotFound, + + #[error(transparent)] + OpenFile(#[from] OpenFileError), + + #[error("failed to read archive entries")] + ReadArchiveEntries(#[source] std::io::Error), + + #[error("failed to unpack binary")] + UnpackBinary(#[source] std::io::Error), +} + +#[derive(Error, Debug)] +pub enum SelfReplaceError { + #[error(transparent)] + InstallBinaries(#[from] InstallBinariesError), + + #[error(transparent)] + NoHomeDirectory(#[from] NoHomeDirectoryError), +} + +#[derive(Error, Debug)] +pub enum CleanupSelfUpdaterError { + #[error(transparent)] + RemoveFile(#[from] RemoveFileError), +} diff --git a/src/error/dfxvm_init.rs b/src/error/dfxvm_init.rs index 046a22d..bc95b43 100644 --- a/src/error/dfxvm_init.rs +++ b/src/error/dfxvm_init.rs @@ -1,5 +1,6 @@ use crate::error::{ dfxvm, + dfxvm::self_update::SelfReplaceError, fs::{AppendToFileError, CreateDirAllError, ReadToStringError, WriteFileError}, installation::InstallBinariesError, }; @@ -12,6 +13,9 @@ pub enum Error { #[error(transparent)] Interact(#[from] InteractError), + + #[error(transparent)] + SelfReplace(#[from] SelfReplaceError), } #[derive(Error, Debug)] diff --git a/src/locations.rs b/src/locations.rs index b5e96ec..b9e58d2 100644 --- a/src/locations.rs +++ b/src/locations.rs @@ -25,12 +25,19 @@ impl Locations { self.version_dir(version).join("dfx") } + pub fn self_update_path(&self) -> PathBuf { + self.bin_dir().join("dfxvm-init-self-update") + } + pub fn config_dir(&self) -> &Path { &self.config_dir } pub fn data_local_dir(&self) -> &Path { &self.data_local_dir } + pub fn bin_dir(&self) -> PathBuf { + self.data_local_dir.join("bin") + } pub fn settings_path(&self) -> PathBuf { self.config_dir.join(SETTINGS_FILENAME) diff --git a/src/main.rs b/src/main.rs index 5cf2cc8..10a1c27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod cli; mod dfx; mod dfxvm; mod dfxvm_init; +mod dist_manifest; mod download; mod env; mod error; diff --git a/src/settings.rs b/src/settings.rs index 7de436e..63f3037 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -6,6 +6,8 @@ use serde_json::Value; use std::path::Path; const DEFAULT_DOWNLOAD_URL_TEMPLATE: &str = "https://github.com/dfinity/sdk/releases/download/{{version}}/dfx-{{version}}-{{arch}}-{{platform}}.tar.gz"; +const DEFAULT_DFXVM_LATEST_DOWNLOAD_ROOT_URL: &str = + "https://github.com/dfinity/dfxvm/releases/latest/download"; const DEFAULT_MANIFEST_URL: &str = "https://sdk.dfinity.org/manifest.json"; #[derive(Clone, Debug, Default, Deserialize, Serialize)] @@ -13,6 +15,9 @@ pub struct Settings { #[serde(skip_serializing_if = "Option::is_none")] pub default_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + dfxvm_latest_download_root: Option, + #[serde(skip_serializing_if = "Option::is_none")] download_url_template: Option, @@ -24,6 +29,12 @@ pub struct Settings { } impl Settings { + pub fn dfxvm_latest_download_root(&self) -> String { + self.dfxvm_latest_download_root + .clone() + .unwrap_or_else(|| DEFAULT_DFXVM_LATEST_DOWNLOAD_ROOT_URL.to_string()) + } + pub fn download_url_template(&self) -> String { self.download_url_template .clone() diff --git a/tests/suite/common/file_contents.rs b/tests/suite/common/file_contents.rs index dddcb2d..1e60907 100644 --- a/tests/suite/common/file_contents.rs +++ b/tests/suite/common/file_contents.rs @@ -1,3 +1,4 @@ +use crate::common::ReleaseAsset; use flate2::write::GzEncoder; use flate2::Compression; use serde_json::json; @@ -16,7 +17,7 @@ pub fn dfx_tar_gz(script: &str) -> Vec { /// x dfx /// $ ls -l dfx /// -rwxr-xr-x 1 ericswanson staff 128330472 Oct 5 00:45 dfx -fn binary_tar_gz(binary_name: &str, contents: &[u8]) -> Vec { +pub fn binary_tar_gz(binary_name: &str, contents: &[u8]) -> Vec { let tar_buffer = Vec::new(); let mut tar = Builder::new(Vec::new()); @@ -63,3 +64,62 @@ pub fn manifest_json(latest: &str) -> String { }) .to_string() } + +pub fn dist_manifest_json(latest: &str) -> String { + json!({ + "releases": [ + { + "app_name": "dfxvm", + "app_version": latest + } + ] + }) + .to_string() +} + +// dfxvm tarball looks like: +// $ tar -tvf dfxvm-aarch64-apple-darwin.tar.gz +// drwxr-xr-x 0 501 20 0 Dec 19 11:24 dfxvm-aarch64-apple-darwin/ +// -rw-r--r-- 0 501 20 1342 Dec 19 11:21 dfxvm-aarch64-apple-darwin/README.md +// -rw-r--r-- 0 501 20 1277 Dec 19 11:21 dfxvm-aarch64-apple-darwin/CHANGELOG.md +// -rw-r--r-- 0 501 20 11357 Dec 19 11:21 dfxvm-aarch64-apple-darwin/LICENSE +// -rwxr-xr-x 0 501 20 5747075 Dec 19 11:24 dfxvm-aarch64-apple-darwin/dfxvm + +pub fn dfxvm_tarball(contents: &[u8]) -> Vec { + let dirname = ReleaseAsset::dfxvm_tarball_basename(); + + let tar_buffer = Vec::new(); + let mut tar = Builder::new(Vec::new()); + + append_file(&mut tar, 0o644, &dirname, "README.md", b"the readme\n"); + append_file( + &mut tar, + 0o644, + &dirname, + "CHANGELOG.md", + b"the changelog\n", + ); + append_file(&mut tar, 0o644, &dirname, "LICENSE", b"the license\n"); + append_file(&mut tar, 0o755, &dirname, "dfxvm", contents); + + let mut gzipped = GzEncoder::new(tar_buffer, Compression::default()); + gzipped.write_all(&tar.into_inner().unwrap()).unwrap(); + + gzipped.finish().unwrap() +} + +fn append_file( + tar: &mut Builder>, + mode: u32, + dirname: &str, + filename: &str, + contents: &[u8], +) { + let path = format!("{}/{}", dirname, filename); + let mut file_header = tar::Header::new_gnu(); + file_header.set_mode(mode); + file_header.set_size(contents.len() as u64); + file_header.set_cksum(); + + tar.append_data(&mut file_header, path, contents).unwrap(); +} diff --git a/tests/suite/common/mod.rs b/tests/suite/common/mod.rs index 5dfb99d..00ab071 100644 --- a/tests/suite/common/mod.rs +++ b/tests/suite/common/mod.rs @@ -12,6 +12,6 @@ pub use release_server::ReleaseServer; pub use settings::Settings; pub use temp_home_dir::TempHomeDir; -fn dfxvm_path() -> &'static str { +pub fn dfxvm_path() -> &'static str { env!("CARGO_BIN_EXE_dfxvm") } diff --git a/tests/suite/common/release_asset.rs b/tests/suite/common/release_asset.rs index 7b4f7a1..761757b 100644 --- a/tests/suite/common/release_asset.rs +++ b/tests/suite/common/release_asset.rs @@ -6,9 +6,10 @@ use crate::common::{ use httptest::http::{response, Response}; use semver::Version; +#[derive(Clone)] pub struct ReleaseAsset { - pub version: Version, pub filename: String, + pub url_path: String, pub contents: Vec, } @@ -16,9 +17,13 @@ impl ReleaseAsset { pub fn dfx_tarball(version: &str, snippet: &str) -> ReleaseAsset { let filename = Self::dfx_tarball_filename(version); let version = Version::parse(version).unwrap(); + + // must match the download_url_template in ReleaseServer::new + let url_path = format!("/any/arbitrary/path/{version}/{filename}"); + let contents = dfx_tar_gz(&bash_script(snippet)); ReleaseAsset { - version, + url_path, filename, contents, } @@ -26,11 +31,12 @@ impl ReleaseAsset { pub fn sha256(asset: &ReleaseAsset) -> ReleaseAsset { let filename = format!("{}.sha256", asset.filename); + let url_path = format!("{}.sha256", asset.url_path); let contents = file_contents::sha256(&asset.filename, &asset.contents) .as_bytes() .to_vec(); ReleaseAsset { - version: asset.version.clone(), + url_path, filename, contents, } @@ -43,8 +49,44 @@ impl ReleaseAsset { .unwrap() } - fn dfx_tarball_filename(version: &str) -> String { + pub fn dfx_tarball_filename(version: &str) -> String { let platform = target::platform(); format!("dfx-{version}-x86_64-{platform}.tar.gz") } + + pub fn dfxvm_tarball_basename() -> String { + #[cfg(target_arch = "aarch64")] + let arch_and_os = "aarch64-apple-darwin"; + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] + let arch_and_os = "x86_64-apple-darwin"; + #[cfg(target_os = "linux")] + let arch_and_os = "x86_64-unknown-linux-gnu"; + + format!("dfxvm-{}", arch_and_os) + } + + // tricky about testing this: + // - we need to test that the dfxvm binary is updated + // - we only have the current dfxvm binary to test with + // - so we copy the binary and append a couple bytes to it. + pub fn altered_dfxvm_binary() -> Vec { + let mut altered_dfxvm = std::fs::read(crate::common::dfxvm_path()).unwrap(); + altered_dfxvm.push(0xCC); + altered_dfxvm.push(0xCC); + altered_dfxvm + } + + pub fn altered_dfxvm_tarball() -> ReleaseAsset { + let altered_dfxvm = Self::altered_dfxvm_binary(); + + let basename = Self::dfxvm_tarball_basename(); + let filename = format!("{basename}.tar.gz"); + let url_path = format!("/dfxvm-latest-download-root/{filename}"); + let contents = file_contents::dfxvm_tarball(&altered_dfxvm); + ReleaseAsset { + url_path, + filename, + contents, + } + } } diff --git a/tests/suite/common/release_server.rs b/tests/suite/common/release_server.rs index c39a12a..8813795 100644 --- a/tests/suite/common/release_server.rs +++ b/tests/suite/common/release_server.rs @@ -18,19 +18,22 @@ impl ReleaseServer { .write_download_url_template(&download_url_template); let manifest_url = server.url_str("/manifest.json"); home_dir.settings().write_manifest_url(&manifest_url); + home_dir + .settings() + .write_dfxvm_latest_download_root_url(&server.url_str("/dfxvm-latest-download-root")); Self { server } } pub fn expect_get(&self, asset: &ReleaseAsset) { self.server.expect( - Expectation::matching(request::method_path("GET", url_path(asset))) + Expectation::matching(request::method_path("GET", asset.url_path.clone())) .respond_with(asset.ok_response()), ); } pub fn expect_get_respond_not_found(&self, asset: &ReleaseAsset) { self.server.expect( - Expectation::matching(request::method_path("GET", url_path(asset))) + Expectation::matching(request::method_path("GET", asset.url_path.clone())) .respond_with(status_code(404)), ); } @@ -53,10 +56,19 @@ impl ReleaseServer { self.expect_get(&sha256); self.expect_get_manifest(&manifest_json("0.15.0")); } -} -fn url_path(asset: &ReleaseAsset) -> String { - let version = &asset.version; - let filename = &asset.filename; - format!("/any/arbitrary/path/{version}/{filename}") + pub fn expect_get_dist_manifest(&self, contents: &str) { + self.server.expect( + Expectation::matching(request::method_path( + "GET", + "/dfxvm-latest-download-root/dist-manifest.json", + )) + .respond_with( + response::Builder::new() + .status(200) + .body(contents.as_bytes().to_vec()) + .unwrap(), + ), + ); + } } diff --git a/tests/suite/common/settings.rs b/tests/suite/common/settings.rs index 2cf95a7..892a9a8 100644 --- a/tests/suite/common/settings.rs +++ b/tests/suite/common/settings.rs @@ -38,6 +38,10 @@ impl Settings { self.set_field("manifest_url", url_template); } + pub fn write_dfxvm_latest_download_root_url(&self, url_template: &str) { + self.set_field("dfxvm_latest_download_root", url_template); + } + pub fn write(&self, s: &str) { create_dir_all(self.path.parent().unwrap()).unwrap(); std::fs::write(&self.path, s).unwrap(); diff --git a/tests/suite/common/temp_home_dir.rs b/tests/suite/common/temp_home_dir.rs index 4e53c56..51e5f35 100644 --- a/tests/suite/common/temp_home_dir.rs +++ b/tests/suite/common/temp_home_dir.rs @@ -5,6 +5,7 @@ use crate::common::{ file_contents::bash_script, project_dirs, Settings, }; +use itertools::Itertools; use std::cell::Cell; use std::ffi::OsStr; use std::fs::create_dir_all; @@ -72,12 +73,16 @@ impl TempHomeDir { command } - pub fn dfxvm_as_file_named(&self, filename: &str) -> PathBuf { - let path = self.path().join(filename); + pub fn copy_dfxvm_to_path(&self, path: &Path) { if !path.exists() { - std::fs::copy(dfxvm_path(), &path).unwrap(); - wait_until_file_is_not_busy(&path); + std::fs::copy(dfxvm_path(), path).unwrap(); + wait_until_file_is_not_busy(path); } + } + + pub fn dfxvm_as_file_named(&self, filename: &str) -> PathBuf { + let path = self.path().join(filename); + self.copy_dfxvm_to_path(&path); path } @@ -86,6 +91,24 @@ impl TempHomeDir { self.new_command(path) } + pub fn installed_dfxvm(&self) -> Command { + self.new_command(self.install_dfxvm_bin()) + } + + pub fn install_dfxvm_bin(&self) -> PathBuf { + let path = self.installed_dfxvm_path(); + create_dir_all(path.parent().unwrap()).unwrap(); + self.copy_dfxvm_to_path(&path); + path + } + + pub fn install_dfxvm_bin_as_dfx_proxy(&self) -> PathBuf { + let path = self.installed_dfx_proxy_path(); + create_dir_all(path.parent().unwrap()).unwrap(); + self.copy_dfxvm_to_path(&path); + path + } + pub fn config_dir(&self) -> PathBuf { self.path().join(".config").join("dfx") } @@ -110,6 +133,16 @@ impl TempHomeDir { .collect() } + pub fn installed_binaries(&self) -> Vec { + self.data_local_dir() + .join("bin") + .read_dir() + .unwrap() + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .sorted() + .collect() + } + pub fn installed_dfx_path(&self, version: &str) -> PathBuf { self.dfx_version_dir(version).join("dfx") } diff --git a/tests/suite/dfxvm/self_update.rs b/tests/suite/dfxvm/self_update.rs index 7f5f33a..cbb37f4 100644 --- a/tests/suite/dfxvm/self_update.rs +++ b/tests/suite/dfxvm/self_update.rs @@ -1,12 +1,174 @@ -use crate::common::TempHomeDir; +use crate::common::file_contents::dist_manifest_json; +use crate::common::{ReleaseAsset, ReleaseServer, TempHomeDir}; use assert_cmd::prelude::*; +use predicates::str::contains; +use semver::Version; + +fn different_version(patch_diff: i64) -> String { + let current_version = env!("CARGO_PKG_VERSION"); + let ver = Version::parse(current_version).unwrap(); + let patch = (ver.patch as i64 + patch_diff) as u64; + format!("{}.{}.{}", ver.major, ver.minor, patch) +} + +fn older_version() -> String { + different_version(-1) +} + +fn newer_version() -> String { + different_version(1) +} + +#[test] +fn self_update_older() { + self_update(&older_version()); +} + +#[test] +fn self_update_newer() { + self_update(&newer_version()); +} #[test] -fn self_update() { +fn dfx_cleans_up_after_self_update() { + let home_dir = self_update(&newer_version()); + assert_eq!( + home_dir.installed_binaries(), + ["dfx", "dfxvm", "dfxvm-init-self-update"] + ); + + home_dir.dfx().arg("anything").assert().failure(); + assert_eq!(home_dir.installed_binaries(), ["dfx", "dfxvm"]); +} + +#[test] +fn dfxvm_cleans_up_after_self_update() { + let home_dir = self_update(&newer_version()); + + assert_eq!( + home_dir.installed_binaries(), + ["dfx", "dfxvm", "dfxvm-init-self-update"] + ); + + home_dir + .installed_dfxvm() + .arg("anything") + .assert() + .failure(); + assert_eq!(home_dir.installed_binaries(), ["dfx", "dfxvm"]); +} + +fn self_update(to_version: &str) -> TempHomeDir { let home_dir = TempHomeDir::new(); - let mut cmd = home_dir.dfxvm(); + let server = ReleaseServer::new(&home_dir); + + let tarball = ReleaseAsset::altered_dfxvm_tarball(); + let sha256 = ReleaseAsset::sha256(&tarball); + server.expect_get(&tarball); + server.expect_get(&sha256); + server.expect_get_dist_manifest(&dist_manifest_json(to_version)); + + // before we do this, the installed dfxvm and dfx proxy should be the one we're testing with + home_dir.install_dfxvm_bin(); + home_dir.install_dfxvm_bin_as_dfx_proxy(); + let installed_dfxvm = std::fs::read(home_dir.installed_dfxvm_path()).unwrap(); + let built_dfxvm = std::fs::read(crate::common::dfxvm_path()).unwrap(); + let altered_dfxvm = ReleaseAsset::altered_dfxvm_binary(); + assert!( + installed_dfxvm == built_dfxvm, + "installed dfxvm is not the built dfxvm" + ); + assert!( + installed_dfxvm != altered_dfxvm, + "installed dfxvm is the altered dfxvm" + ); + + let mut cmd = home_dir.installed_dfxvm(); + cmd.arg("self"); + cmd.arg("update"); + + cmd.assert() + .success() + .stderr(contains("checking for self-update")) + .stderr(contains("verified checksum")); + + // // after self update, the installed dfxvm should be the one we downloaded + let installed_dfxvm = std::fs::read(home_dir.installed_dfxvm_path()).unwrap(); + assert!( + installed_dfxvm != built_dfxvm, + "installed dfxvm is still the built dfxvm" + ); + assert!( + installed_dfxvm == altered_dfxvm, + "installed dfxvm is not the altered dfxvm" + ); + // as should the dfx proxy binary + let installed_dfx_proxy = std::fs::read(home_dir.installed_dfx_proxy_path()).unwrap(); + assert!( + installed_dfx_proxy != built_dfxvm, + "installed dfx proxy is still the built dfxvm" + ); + assert!( + installed_dfx_proxy == altered_dfxvm, + "installed dfx proxy is not the altered dfxvm" + ); + + assert_eq!( + home_dir.installed_binaries(), + ["dfx", "dfxvm", "dfxvm-init-self-update"] + ); + + home_dir +} + +#[test] +fn unchanged() { + let home_dir = TempHomeDir::new(); + let server = ReleaseServer::new(&home_dir); + + let current_version = env!("CARGO_PKG_VERSION"); + + server.expect_get_dist_manifest(&dist_manifest_json(current_version)); + + // before we do this, the installed dfxvm and dfx proxy should be the one we're testing with + home_dir.install_dfxvm_bin(); + home_dir.install_dfxvm_bin_as_dfx_proxy(); + + let mut cmd = home_dir.installed_dfxvm(); + cmd.arg("self"); + cmd.arg("update"); + + cmd.assert() + .success() + .stderr(contains("checking for self-update")) + .stderr(contains(format!("dfxvm unchanged - {current_version}"))); +} + +#[test] +fn incorrect_sha256() { + let home_dir = TempHomeDir::new(); + let server = ReleaseServer::new(&home_dir); + + let tarball = ReleaseAsset::altered_dfxvm_tarball(); + let wrong = ReleaseAsset { + contents: b"not the right contents".to_vec(), + ..tarball.clone() + }; + let sha256 = ReleaseAsset::sha256(&wrong); + server.expect_get(&tarball); + server.expect_get(&sha256); + server.expect_get_dist_manifest(&dist_manifest_json(&newer_version())); + + // before we do this, the installed dfxvm and dfx proxy should be the one we're testing with + home_dir.install_dfxvm_bin(); + home_dir.install_dfxvm_bin_as_dfx_proxy(); + + let mut cmd = home_dir.installed_dfxvm(); cmd.arg("self"); cmd.arg("update"); - cmd.assert().success().stdout("update dfxvm to latest\n"); + cmd.assert() + .failure() + .stderr(contains("checking for self-update")) + .stderr(contains("checksum did not match")); }