Skip to content

Commit 9275dd3

Browse files
author
Eric Swanson
authored
feat: self update (#41)
1 parent 106aceb commit 9275dd3

23 files changed

Lines changed: 671 additions & 32 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased] - ReleaseDate
1111

12+
- Added `dfxvm self update` command, which updates dfxvm to the latest version.
13+
1214
## [0.1.2] - 2023-12-19
1315

1416
- dfxvm-init now alters profile scripts to modify the PATH environment variable.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
# dfxvm self update
22

33
Updates to the newest version of dfxvm.
4+
5+
## Usage
6+
7+
```bash
8+
dfxvm self update
9+
```

src/dfx.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::dfxvm::cleanup_self_updater;
12
use crate::error::dfx;
23
use crate::error::dfx::Error::Exec;
34
use crate::error::dfx::{
@@ -18,6 +19,7 @@ use std::path::PathBuf;
1819
use std::process::ExitCode;
1920

2021
pub fn main(args: &[OsString], locations: &Locations) -> Result<ExitCode, dfx::Error> {
22+
cleanup_self_updater(locations)?;
2123
let Some((version, args)) = get_dfx_version_and_command_args(args, locations)? else {
2224
err!("Unable to determine which dfx version to call. To set a default version, run:");
2325
err!(" {}", style_command("dfxvm default <version>"));

src/dfxvm.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ mod update;
99

1010
pub use cli::main;
1111
pub use default::set_default;
12+
pub use self_update::cleanup_self_updater;
13+
pub use self_update::self_replace;
1214
pub use update::update;

src/dfxvm/cli.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::dfxvm::{
2-
default::default, install::install, list::list, self_uninstall::self_uninstall,
3-
self_update::self_update, uninstall::uninstall, update::update,
2+
cleanup_self_updater, default::default, install::install, list::list,
3+
self_uninstall::self_uninstall, self_update::self_update, uninstall::uninstall, update::update,
44
};
55
use crate::error::dfxvm;
66
use crate::locations::Locations;
@@ -80,13 +80,14 @@ pub struct SelfUpdateOpts {}
8080
pub struct SelfUninstallOpts {}
8181

8282
pub async fn main(args: &[OsString], locations: &Locations) -> Result<ExitCode, dfxvm::Error> {
83+
cleanup_self_updater(locations)?;
8384
let cli = Cli::parse_from(args);
8485
match cli.command {
8586
Command::Default(opts) => default(opts.version, locations).await?,
8687
Command::Install(opts) => install(opts.version, locations).await?,
8788
Command::List(_opts) => list(locations)?,
8889
Command::SelfCommand(opts) => match opts.command {
89-
SelfCommand::Update(_opts) => self_update(locations)?,
90+
SelfCommand::Update(_opts) => self_update(locations).await?,
9091
SelfCommand::Uninstall(_opts) => self_uninstall(locations)?,
9192
},
9293
Command::Uninstall(opts) => uninstall(opts.version, locations)?,

src/dfxvm/self_update.rs

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,138 @@
1-
use crate::error::dfxvm::SelfUpdateError;
1+
use crate::dist_manifest::lookup_latest_version;
2+
use crate::download::{download_file, verify_checksum};
3+
use crate::error::dfxvm::self_update::CleanupSelfUpdaterError;
4+
use crate::error::dfxvm::{
5+
self_update::{
6+
DownloadLatestBinaryError,
7+
DownloadLatestBinaryError::CreateTempDirIn,
8+
ExtractBinaryError,
9+
ExtractBinaryError::{DfxvmNotFound, ReadArchiveEntries, UnpackBinary},
10+
FormatTarballUrlError, SelfReplaceError,
11+
},
12+
SelfUpdateError,
13+
SelfUpdateError::Exec,
14+
};
15+
use crate::fs::{open_file, remove_file};
16+
use crate::installation::install_binaries;
217
use crate::locations::Locations;
18+
use crate::settings::Settings;
19+
use flate2::read::GzDecoder;
20+
use reqwest::{Client, Url};
21+
use std::os::unix::prelude::CommandExt;
22+
use std::path::Path;
23+
use tar::Archive;
324

4-
pub fn self_update(_locations: &Locations) -> Result<(), SelfUpdateError> {
5-
println!("update dfxvm to latest");
25+
pub async fn self_update(locations: &Locations) -> Result<(), SelfUpdateError> {
26+
info!("checking for self-update");
27+
let settings = Settings::load_or_default(&locations.settings_path())?;
28+
let latest_version = lookup_latest_version(&settings).await?;
29+
let our_version = env!("CARGO_PKG_VERSION");
30+
if latest_version == our_version {
31+
info!("dfxvm unchanged - {latest_version}");
32+
return Ok(());
33+
}
34+
35+
info!("updating to {latest_version}");
36+
37+
let tarball_url = format_tarball_url(&settings)?;
38+
let self_update_path = locations.self_update_path();
39+
40+
download_latest_binary(&tarball_url, &self_update_path, locations).await?;
41+
42+
let mut command = std::process::Command::new(self_update_path);
43+
command.arg("--self-replace");
44+
let err = command.exec();
45+
Err(Exec {
46+
command,
47+
source: err,
48+
})
49+
}
50+
51+
pub fn self_replace(locations: &Locations) -> Result<(), SelfReplaceError> {
52+
install_binaries(&locations.bin_dir())?;
53+
Ok(())
54+
}
55+
56+
// called on next execution of dfx or dfxvm
57+
pub fn cleanup_self_updater(locations: &Locations) -> Result<(), CleanupSelfUpdaterError> {
58+
let path = locations.self_update_path();
59+
60+
if path.exists() {
61+
remove_file(&path)?;
62+
}
63+
64+
Ok(())
65+
}
66+
67+
fn format_tarball_url(settings: &Settings) -> Result<Url, FormatTarballUrlError> {
68+
#[cfg(target_arch = "aarch64")]
69+
let architecture = "aarch64-apple-darwin";
70+
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
71+
let architecture = "x86_64-apple-darwin";
72+
#[cfg(target_os = "linux")]
73+
let architecture = "x86_64-unknown-linux-gnu";
74+
75+
let basename = format!("dfxvm-{}", architecture);
76+
let url = format!(
77+
"{}/{basename}.tar.gz",
78+
settings.dfxvm_latest_download_root()
79+
);
80+
81+
Url::parse(&url).map_err(|source| FormatTarballUrlError { url, source })
82+
}
83+
84+
async fn download_latest_binary(
85+
tarball_url: &Url,
86+
binary_path: &Path,
87+
locations: &Locations,
88+
) -> Result<(), DownloadLatestBinaryError> {
89+
let shasum_url = Url::parse(&format!("{tarball_url}.sha256"))?;
90+
91+
let download_dir = tempfile::Builder::new()
92+
.prefix("dfxvm-download")
93+
.tempdir_in(locations.data_local_dir())
94+
.map_err(|source| CreateTempDirIn {
95+
path: locations.data_local_dir().to_path_buf(),
96+
source,
97+
})?;
98+
99+
let downloaded_tarball_path = download_dir.path().join("dfxvm.tar.gz");
100+
let downloaded_shasum_path = download_dir.path().join("dfxvm.tar.gz.sha256");
101+
102+
let client = Client::new();
103+
104+
download_file(&client, &shasum_url, &downloaded_shasum_path).await?;
105+
let computed_hash = download_file(&client, tarball_url, &downloaded_tarball_path).await?;
106+
verify_checksum(computed_hash, &downloaded_shasum_path)?;
107+
108+
extract_binary(binary_path, &downloaded_tarball_path)?;
109+
Ok(())
110+
}
111+
112+
fn extract_binary(
113+
binary_path: &Path,
114+
downloaded_tarball_path: &Path,
115+
) -> Result<(), ExtractBinaryError> {
116+
let tar_gz = open_file(downloaded_tarball_path)?;
117+
let tar = GzDecoder::new(tar_gz);
118+
119+
Archive::new(tar)
120+
.entries()
121+
.map_err(ReadArchiveEntries)?
122+
.enumerate()
123+
.filter_map(|(_i, entry)| entry.ok())
124+
.find(|entry| {
125+
entry
126+
.header()
127+
.path()
128+
.ok()
129+
.as_ref()
130+
.and_then(|x| x.to_str())
131+
.map(|str_path| str_path.ends_with("dfxvm"))
132+
.unwrap_or(false)
133+
})
134+
.ok_or(DfxvmNotFound)?
135+
.unpack(binary_path)
136+
.map_err(UnpackBinary)?;
6137
Ok(())
7138
}

src/dfxvm_init/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::dfxvm::self_replace;
12
use crate::dfxvm_init::initialize::initialize;
23
use crate::dfxvm_init::plan::{
34
DfxVersion::{Latest, Specific},
@@ -29,6 +30,12 @@ pub struct Cli {
2930
}
3031

3132
pub async fn main(args: &[OsString], locations: &Locations) -> Result<ExitCode, dfxvm_init::Error> {
33+
let arg1 = args.get(1).map(|a| &**a);
34+
if arg1 == Some("--self-replace".as_ref()) {
35+
self_replace(locations)?;
36+
return Ok(ExitCode::SUCCESS);
37+
}
38+
3239
let opts = Cli::parse_from(args);
3340

3441
let confirmation = if opts.proceed {

src/dfxvm_init/plan.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub struct Plan {
5656

5757
impl Plan {
5858
pub fn new(options: PlanOptions, locations: &Locations) -> Self {
59-
let bin_dir = locations.data_local_dir().join("bin");
59+
let bin_dir = locations.bin_dir();
6060
let env_path = locations.data_local_dir().join("env");
6161
let env_path_user_facing = get_env_path_user_facing().to_string();
6262
let profile_scripts = get_detected_profile_scripts();

src/dist_manifest.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use crate::error::dfxvm::self_update::LookupLatestVersionError;
2+
use crate::json::fetch_json;
3+
use crate::settings::Settings;
4+
use serde::Deserialize;
5+
use url::Url;
6+
7+
#[derive(Deserialize, Debug)]
8+
struct Release {
9+
app_name: String,
10+
app_version: String,
11+
}
12+
13+
#[derive(Deserialize, Debug)]
14+
struct DistManifest {
15+
releases: Vec<Release>,
16+
}
17+
18+
pub async fn lookup_latest_version(
19+
settings: &Settings,
20+
) -> Result<String, LookupLatestVersionError> {
21+
let dist_manifest_url = format!(
22+
"{}/dist-manifest.json",
23+
settings.dfxvm_latest_download_root()
24+
);
25+
let url =
26+
Url::parse(&dist_manifest_url).map_err(|source| LookupLatestVersionError::ParseUrl {
27+
url: dist_manifest_url,
28+
source,
29+
})?;
30+
let dist_manifest = fetch_json::<DistManifest>(&url).await?;
31+
let dfxvm_release = dist_manifest
32+
.releases
33+
.iter()
34+
.find(|release| release.app_name == "dfxvm")
35+
.ok_or(LookupLatestVersionError::NoDfxvmRelease { url })?;
36+
let latest_version = dfxvm_release.app_version.clone();
37+
Ok(latest_version)
38+
}

src/error/dfx.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
use crate::error::{env::GetCurrentDirError, fs::CanonicalizePathError, json::LoadJsonFileError};
1+
use crate::error::{
2+
dfxvm::self_update::CleanupSelfUpdaterError, env::GetCurrentDirError,
3+
fs::CanonicalizePathError, json::LoadJsonFileError,
4+
};
25
use std::process::Command;
36
use thiserror::Error;
47

58
#[derive(Error, Debug)]
69
pub enum Error {
10+
#[error(transparent)]
11+
CleanupSelfUpdater(#[from] CleanupSelfUpdaterError),
12+
713
#[error(transparent)]
814
DetermineDfxVersion(#[from] DetermineDfxVersionError),
915

0 commit comments

Comments
 (0)