Skip to content

Commit 7f92e2c

Browse files
feat: Auto-update network launcher after updating icp-cli (#401)
1 parent 0639f59 commit 7f92e2c

7 files changed

Lines changed: 97 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ pkcs8 = { version = "0.10.2", features = ["encryption", "std"] }
7373
rand = "0.9.1"
7474
scrypt = "0.11.0"
7575
sec1 = { version = "0.7.3", features = ["pkcs8"] }
76+
semver = "1"
7677
serde = { version = "1.0", features = ["derive"] }
7778
serde_json = "1.0"
7879
serde_yaml = "0.9.34"

crates/icp-cli/src/commands/network/start.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use icp::{
77
identity::manifest::IdentityList,
88
network::{
99
Configuration,
10-
managed::cache::{download_launcher_version, get_cached_launcher_version},
10+
managed::cache::{download_launcher_version, get_cached_launcher_version_if_fresh},
1111
run_network,
1212
},
1313
settings::Settings,
@@ -127,7 +127,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow::
127127
ctx.dirs
128128
.package_cache()?
129129
.with_write(async |pkg| {
130-
if let Some(path) = get_cached_launcher_version(pkg.read(), version)? {
130+
if let Some(path) = get_cached_launcher_version_if_fresh(pkg.read(), version)? {
131131
anyhow::Ok(Some(path))
132132
} else {
133133
debug!("Downloading icp-cli-network-launcher version `{version}`");

crates/icp-cli/tests/common/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ impl TestContext {
136136
.into_write()
137137
.await
138138
.unwrap();
139-
if let Some(path) = icp::network::managed::cache::get_cached_launcher_version(
139+
if let Some(path) = icp::network::managed::cache::get_cached_launcher_version_if_fresh(
140140
cache.as_ref().read(),
141141
"latest",
142142
)

crates/icp/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ rand = { workspace = true }
5151
reqwest = { workspace = true }
5252
schemars = { workspace = true }
5353
scrypt = { workspace = true }
54+
semver = { workspace = true }
5455
sec1 = { workspace = true }
5556
serde = { workspace = true }
5657
serde_json = { workspace = true }

crates/icp/src/network/managed/cache.rs

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ use snafu::{ResultExt, Snafu};
77
use tar::Archive;
88

99
use crate::fs::lock::{LRead, LWrite};
10-
use crate::package::{PackageCachePaths, get_tag, set_tag};
10+
use crate::package::{PackageCachePaths, get_tag, get_tag_with_updater, set_tag_with_updater};
1111
use crate::prelude::*;
1212

13+
const LAUNCHER_NAME: &str = "icp-cli-network-launcher";
14+
1315
pub fn get_cached_launcher_version(
1416
paths: LRead<&PackageCachePaths>,
1517
version: &str,
1618
) -> Result<Option<PathBuf>, ReadCacheError> {
1719
let declared_version = if version == "latest" {
18-
let Some(version) =
19-
get_tag(paths, "icp-cli-network-launcher", "latest").context(LoadTagSnafu)?
20-
else {
20+
let Some(version) = get_tag(paths, LAUNCHER_NAME, "latest").context(LoadTagSnafu)? else {
2121
return Ok(None);
2222
};
2323
version.to_owned()
@@ -27,12 +27,55 @@ pub fn get_cached_launcher_version(
2727
};
2828
let version_path = paths.launcher_version(&declared_version);
2929
if version_path.exists() {
30-
Ok(Some(version_path.join("icp-cli-network-launcher")))
30+
Ok(Some(version_path.join(LAUNCHER_NAME)))
31+
} else {
32+
Ok(None)
33+
}
34+
}
35+
36+
/// Like [`get_cached_launcher_version`], but for the "latest" tag also checks
37+
/// whether the launcher was downloaded by an older CLI version, returning `None`
38+
/// if so. Pinned versions are never considered stale.
39+
pub fn get_cached_launcher_version_if_fresh(
40+
paths: LRead<&PackageCachePaths>,
41+
version: &str,
42+
) -> Result<Option<PathBuf>, ReadCacheError> {
43+
let declared_version = if version == "latest" {
44+
let (tag, updater) =
45+
get_tag_with_updater(paths, LAUNCHER_NAME, "latest").context(LoadTagSnafu)?;
46+
let Some(version) = tag else {
47+
return Ok(None);
48+
};
49+
if is_updater_stale(updater.as_deref()) {
50+
return Ok(None);
51+
}
52+
version
53+
} else {
54+
assert!(version.starts_with('v'));
55+
version.to_owned()
56+
};
57+
let version_path = paths.launcher_version(&declared_version);
58+
if version_path.exists() {
59+
Ok(Some(version_path.join(LAUNCHER_NAME)))
3160
} else {
3261
Ok(None)
3362
}
3463
}
3564

65+
/// Returns true if the given updater version is older than the current CLI version
66+
/// (or if no updater version is recorded).
67+
fn is_updater_stale(updater_version: Option<&str>) -> bool {
68+
let Some(updater_version) = updater_version else {
69+
return true;
70+
};
71+
let current = semver::Version::parse(env!("CARGO_PKG_VERSION"))
72+
.expect("package versions should always be valid semver");
73+
let Ok(stored) = semver::Version::parse(updater_version) else {
74+
return true;
75+
};
76+
stored < current
77+
}
78+
3679
#[derive(Debug, Snafu)]
3780
pub enum ReadCacheError {
3881
#[snafu(display("failed to read package tag"))]
@@ -67,7 +110,14 @@ pub async fn download_launcher_version(
67110
) -> Result<(String, PathBuf), DownloadLauncherError> {
68111
let pkg_version = if version_req == "latest" {
69112
let latest = get_latest_launcher_version(client).await?;
70-
set_tag(paths, "icp-cli-network-launcher", &latest, "latest").context(CreateTagSnafu)?;
113+
set_tag_with_updater(
114+
paths,
115+
LAUNCHER_NAME,
116+
&latest,
117+
"latest",
118+
env!("CARGO_PKG_VERSION"),
119+
)
120+
.context(CreateTagSnafu)?;
71121
latest
72122
} else {
73123
assert!(version_req.starts_with('v'));
@@ -136,7 +186,7 @@ pub async fn download_launcher_version(
136186
from: extracted_dir_path,
137187
to: &version_path,
138188
})?;
139-
Ok((pkg_version, version_path.join("icp-cli-network-launcher")))
189+
Ok((pkg_version, version_path.join(LAUNCHER_NAME)))
140190
}
141191

142192
#[derive(Debug, Snafu)]

crates/icp/src/package.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,41 @@ pub fn set_tag(
251251
Ok(())
252252
}
253253

254+
/// Like [`set_tag`], but also records the CLI version that performed the update.
255+
pub fn set_tag_with_updater(
256+
paths: LWrite<&PackageCachePaths>,
257+
tool: &str,
258+
version: &str,
259+
tag: &str,
260+
updater_version: &str,
261+
) -> Result<(), crate::fs::json::Error> {
262+
let mut manifest: Manifest = crate::fs::json::load_or_default(&paths.manifest())?;
263+
manifest
264+
.tags
265+
.insert(format!("{tool}:{tag}"), version.to_string());
266+
manifest
267+
.updater_versions
268+
.insert(tool.to_string(), updater_version.to_string());
269+
crate::fs::json::save(&paths.manifest(), &manifest)?;
270+
Ok(())
271+
}
272+
273+
/// Like [`get_tag`], but also returns the updater version for the tool.
274+
/// Returns `(tag_value, updater_version)`.
275+
pub fn get_tag_with_updater(
276+
paths: LRead<&PackageCachePaths>,
277+
tool: &str,
278+
tag: &str,
279+
) -> Result<(Option<String>, Option<String>), crate::fs::json::Error> {
280+
let manifest: Manifest = crate::fs::json::load_or_default(&paths.manifest())?;
281+
let tag_value = manifest.tags.get(&format!("{tool}:{tag}")).cloned();
282+
let updater = manifest.updater_versions.get(tool).cloned();
283+
Ok((tag_value, updater))
284+
}
285+
254286
#[derive(Serialize, Deserialize, Default)]
255287
struct Manifest {
256288
tags: HashMap<String, String>,
289+
#[serde(default)]
290+
updater_versions: HashMap<String, String>,
257291
}

0 commit comments

Comments
 (0)