aboutsummaryrefslogtreecommitdiff
path: root/src/remote.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 22:51:28 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 22:51:28 +0300
commiteac2314e6ccb4ba91b7272523e68110035167279 (patch)
treecfa813cec6164a75366dfeeaa88d97ab9fa6e0fa /src/remote.rs
parent427c2606d8f62558e3949d2f44173175e96bed49 (diff)
fix: handle package-manager remote updates
Diffstat (limited to 'src/remote.rs')
-rw-r--r--src/remote.rs245
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",