diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-26 22:51:28 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-26 22:51:28 +0300 |
| commit | eac2314e6ccb4ba91b7272523e68110035167279 (patch) | |
| tree | cfa813cec6164a75366dfeeaa88d97ab9fa6e0fa /src/remote.rs | |
| parent | 427c2606d8f62558e3949d2f44173175e96bed49 (diff) | |
fix: handle package-manager remote updates
Diffstat (limited to 'src/remote.rs')
| -rw-r--r-- | src/remote.rs | 245 |
1 files changed, 234 insertions, 11 deletions
diff --git a/src/remote.rs b/src/remote.rs index 9a4cc774..3cf1e43b 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -300,9 +300,63 @@ impl RemoteHerdr { #[derive(Deserialize)] struct RemoteUpdateManifest { version: String, + protocol: Option<u32>, + assets: BTreeMap<String, String>, + #[serde(default, deserialize_with = "deserialize_remote_manifest_releases")] + releases: BTreeMap<String, RemoteReleaseMetadata>, +} + +#[derive(Deserialize)] +struct RemoteReleaseMetadata { + protocol: Option<u32>, + #[serde(default)] assets: BTreeMap<String, String>, } +fn deserialize_remote_manifest_releases<'de, D>( + deserializer: D, +) -> Result<BTreeMap<String, RemoteReleaseMetadata>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::<serde_json::Value>::deserialize(deserializer)?; + Ok(match value { + Some(serde_json::Value::Object(object)) => object + .into_iter() + .filter_map(|(version, release)| { + serde_json::from_value::<RemoteReleaseMetadata>(release) + .ok() + .map(|metadata| (version, metadata)) + }) + .collect(), + _ => BTreeMap::new(), + }) +} + +impl RemoteUpdateManifest { + fn release_for_version(&self, version: &str) -> Option<RemoteManifestReleaseRef<'_>> { + if self.version.trim_start_matches('v') == version { + return Some(RemoteManifestReleaseRef { + protocol: self.protocol, + assets: &self.assets, + }); + } + + self.releases.get(version).and_then(|release| { + (!release.assets.is_empty()).then_some(RemoteManifestReleaseRef { + protocol: release.protocol, + assets: &release.assets, + }) + }) + } +} + +#[derive(Clone, Copy)] +struct RemoteManifestReleaseRef<'a> { + protocol: Option<u32>, + assets: &'a BTreeMap<String, String>, +} + struct InstallSource { path: PathBuf, temporary_dir: Option<PathBuf>, @@ -525,11 +579,23 @@ fn remote_binary_override_path() -> io::Result<Option<PathBuf>> { } fn install_source_description(platform: &RemotePlatform, override_binary: Option<&Path>) -> String { + install_source_description_for( + platform, + override_binary, + local_binary_can_seed_remote(platform), + ) +} + +fn install_source_description_for( + platform: &RemotePlatform, + override_binary: Option<&Path>, + local_binary_can_seed_remote: bool, +) -> String { if let Some(path) = override_binary { return format!("{REMOTE_BINARY_ENV_VAR} ({})", path.display()); } - if *platform == RemotePlatform::local() { + if local_binary_can_seed_remote { "the current local herdr binary".to_string() } else { format!( @@ -549,12 +615,24 @@ fn resolve_install_source( if *platform == RemotePlatform::local() { let path = std::env::current_exe()?; - return Ok(InstallSource::persistent(path)); + if !crate::update::is_package_manager_managed_exe_path(&path) { + return Ok(InstallSource::persistent(path)); + } } download_release_asset(platform) } +fn local_binary_can_seed_remote(platform: &RemotePlatform) -> bool { + if *platform != RemotePlatform::local() { + return false; + } + + std::env::current_exe() + .map(|path| !crate::update::is_package_manager_managed_exe_path(&path)) + .unwrap_or(false) +} + #[derive(Debug, Clone, PartialEq, Eq)] enum RemoteServerStatus { Running { @@ -985,16 +1063,22 @@ fn download_release_asset(platform: &RemotePlatform) -> io::Result<InstallSource let manifest: RemoteUpdateManifest = serde_json::from_slice(&manifest_output.stdout) .map_err(|err| io::Error::other(format!("failed to parse update manifest JSON: {err}")))?; - if manifest.version.trim_start_matches('v') != CURRENT_VERSION { - return Err(io::Error::other(format!( - "remote host is {}, but this local herdr is {CURRENT_VERSION} and the latest release manifest is {}; build herdr for the remote platform or install it there manually", - platform.asset_key(), - manifest.version - ))); - } let asset_key = platform.asset_key(); - let url = manifest.assets.get(&asset_key).ok_or_else(|| { + let release = manifest.release_for_version(CURRENT_VERSION).ok_or_else(|| { + io::Error::other(format!( + "release manifest does not include herdr {CURRENT_VERSION}; build herdr for {} or install it there manually", + platform.asset_key() + )) + })?; + if let Some(protocol) = release.protocol { + if protocol != CURRENT_PROTOCOL { + return Err(io::Error::other(format!( + "release manifest has herdr {CURRENT_VERSION} protocol {protocol}, but this client needs protocol {CURRENT_PROTOCOL}; set {REMOTE_BINARY_ENV_VAR}=target/release/herdr or install a matching herdr on the remote host manually" + ))); + } + } + let url = release.assets.get(&asset_key).ok_or_else(|| { io::Error::other(format!( "no {asset_key} binary in the release manifest for herdr {CURRENT_VERSION}" )) @@ -1805,6 +1889,122 @@ mod tests { } #[test] + fn remote_update_manifest_uses_root_assets_for_latest_version() { + let manifest: RemoteUpdateManifest = serde_json::from_str( + r#"{ + "version": "1.2.3", + "assets": { + "linux-x86_64": "https://example.com/latest" + }, + "releases": { + "1.2.3": { + "assets": { + "linux-x86_64": "https://example.com/archive" + } + } + } + }"#, + ) + .unwrap(); + + assert_eq!( + manifest + .release_for_version("1.2.3") + .and_then(|release| release.assets.get("linux-x86_64")) + .map(String::as_str), + Some("https://example.com/latest") + ); + } + + #[test] + fn remote_update_manifest_reads_archived_release_assets() { + let manifest: RemoteUpdateManifest = serde_json::from_str( + r#"{ + "version": "1.2.4", + "assets": { + "linux-x86_64": "https://example.com/latest" + }, + "releases": { + "1.2.3": { + "notes": "ignored", + "assets": { + "linux-x86_64": "https://example.com/archive" + } + } + } + }"#, + ) + .unwrap(); + + assert_eq!( + manifest + .release_for_version("1.2.3") + .and_then(|release| release.assets.get("linux-x86_64")) + .map(String::as_str), + Some("https://example.com/archive") + ); + } + + #[test] + fn remote_update_manifest_uses_archived_release_protocol() { + let manifest: RemoteUpdateManifest = serde_json::from_str( + r#"{ + "version": "1.2.4", + "protocol": 42, + "assets": { + "linux-x86_64": "https://example.com/latest" + }, + "releases": { + "1.2.3": { + "notes": "ignored", + "protocol": 41, + "assets": { + "linux-x86_64": "https://example.com/archive" + } + } + } + }"#, + ) + .unwrap(); + + assert_eq!( + manifest + .release_for_version("1.2.3") + .and_then(|release| release.protocol), + Some(41) + ); + } + + #[test] + fn remote_update_manifest_does_not_inherit_latest_protocol_for_archived_assets() { + let manifest: RemoteUpdateManifest = serde_json::from_str( + r#"{ + "version": "1.2.4", + "protocol": 42, + "assets": { + "linux-x86_64": "https://example.com/latest" + }, + "releases": { + "1.2.3": { + "notes": "ignored", + "assets": { + "linux-x86_64": "https://example.com/archive" + } + } + } + }"#, + ) + .unwrap(); + + assert_eq!( + manifest + .release_for_version("1.2.3") + .and_then(|release| release.protocol), + None + ); + } + + #[test] fn remote_server_restart_reason_requires_stop_for_protocol_mismatch() { assert_eq!( remote_server_restart_reason(Some(CURRENT_VERSION), Some(0), false), @@ -1847,12 +2047,35 @@ mod tests { arch: "aarch64", }; assert_eq!( - install_source_description(&platform, Some(Path::new("/tmp/herdr-aarch64"))), + install_source_description_for(&platform, Some(Path::new("/tmp/herdr-aarch64")), false), "HERDR_REMOTE_BINARY (/tmp/herdr-aarch64)" ); } #[test] + fn install_source_description_uses_local_binary_when_allowed() { + let platform = RemotePlatform::local(); + + assert_eq!( + install_source_description_for(&platform, None, true), + "the current local herdr binary" + ); + } + + #[test] + fn install_source_description_uses_release_asset_when_local_binary_cannot_seed_remote() { + let platform = RemotePlatform::local(); + + assert_eq!( + install_source_description_for(&platform, None, false), + format!( + "the {CURRENT_VERSION} release asset for {}", + platform.asset_key() + ) + ); + } + + #[test] fn resolve_install_source_uses_override_binary_without_temporary_cleanup() { let platform = RemotePlatform { os: "linux", |
