From 55555bb41fe12fecdf8656a611e328e4f4c617d7 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 3 May 2024 16:43:28 +0200 Subject: [PATCH] Enable first version of auto-updates on Linux (#11348) This downloads Nightly/Preview releases on Linux and copies the contents the `zed-.app` to `~/.local`. What's missing: - Check if we're not installed in ~/.local and abort - Update `.desktop` file Release Notes: - N/A --- crates/auto_update/src/auto_update.rs | 237 ++++++++++++++++++-------- 1 file changed, 165 insertions(+), 72 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 2f04324bdb..2c95f9af5f 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -15,7 +15,7 @@ use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPrevi use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; -use smol::io::AsyncReadExt; +use smol::{fs, io::AsyncReadExt}; use settings::{Settings, SettingsSources, SettingsStore}; use smol::{fs::File, process::Command}; @@ -24,6 +24,7 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use std::{ env::consts::{ARCH, OS}, ffi::OsString, + path::PathBuf, sync::Arc, time::Duration, }; @@ -340,9 +341,15 @@ impl AutoUpdater { (this.http_client.clone(), this.current_version) })?; + let asset = match OS { + "linux" => format!("zed-linux-{}.tar.gz", ARCH), + "macos" => "Zed.dmg".into(), + _ => return Err(anyhow!("auto-update not supported for OS {:?}", OS)), + }; + let mut url_string = client.build_url(&format!( - "/api/releases/latest?asset=Zed.dmg&os={}&arch={}", - OS, ARCH + "/api/releases/latest?asset={}&os={}&arch={}", + asset, OS, ARCH )); cx.update(|cx| { if let Some(param) = ReleaseChannel::try_global(cx) @@ -361,6 +368,7 @@ impl AutoUpdater { .read_to_end(&mut body) .await .context("error reading release")?; + let release: JsonRelease = serde_json::from_slice(body.as_slice()).context("error deserializing release")?; @@ -389,81 +397,18 @@ impl AutoUpdater { let temp_dir = tempfile::Builder::new() .prefix("zed-auto-update") .tempdir()?; - let dmg_path = temp_dir.path().join("Zed.dmg"); - let mount_path = temp_dir.path().join("Zed"); - let running_app_path = ZED_APP_PATH - .clone() - .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?; - let running_app_filename = running_app_path - .file_name() - .ok_or_else(|| anyhow!("invalid running app path"))?; - let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into(); - mounted_app_path.push("/"); - - let mut dmg_file = File::create(&dmg_path).await?; - - let (installation_id, release_channel, telemetry) = cx.update(|cx| { - let installation_id = Client::global(cx).telemetry().installation_id(); - let release_channel = ReleaseChannel::try_global(cx) - .map(|release_channel| release_channel.display_name()); - let telemetry = TelemetrySettings::get_global(cx).metrics; - - (installation_id, release_channel, telemetry) - })?; - - let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody { - installation_id, - release_channel, - telemetry, - })?); - - let mut response = client.get(&release.url, request_body, true).await?; - smol::io::copy(response.body_mut(), &mut dmg_file).await?; - log::info!("downloaded update. path:{:?}", dmg_path); + let downloaded_asset = download_release(&temp_dir, release, &asset, client, &cx).await?; this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Installing; cx.notify(); })?; - let output = Command::new("hdiutil") - .args(&["attach", "-nobrowse"]) - .arg(&dmg_path) - .arg("-mountroot") - .arg(&temp_dir.path()) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to mount: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - let output = Command::new("rsync") - .args(&["-av", "--delete"]) - .arg(&mounted_app_path) - .arg(&running_app_path) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to copy app: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - let output = Command::new("hdiutil") - .args(&["detach"]) - .arg(&mount_path) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to unmount: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } + match OS { + "macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await, + "linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await, + _ => Err(anyhow!("not supported: {:?}", OS)), + }?; this.update(&mut cx, |this, cx| { this.set_should_show_update_notification(true, cx) @@ -471,6 +416,7 @@ impl AutoUpdater { this.status = AutoUpdateStatus::Updated; cx.notify(); })?; + Ok(()) } @@ -504,3 +450,150 @@ impl AutoUpdater { }) } } + +async fn download_release( + temp_dir: &tempfile::TempDir, + release: JsonRelease, + target_filename: &str, + client: Arc, + cx: &AsyncAppContext, +) -> Result { + let target_path = temp_dir.path().join(target_filename); + let mut target_file = File::create(&target_path).await?; + + let (installation_id, release_channel, telemetry) = cx.update(|cx| { + let installation_id = Client::global(cx).telemetry().installation_id(); + let release_channel = + ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name()); + let telemetry = TelemetrySettings::get_global(cx).metrics; + + (installation_id, release_channel, telemetry) + })?; + + let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody { + installation_id, + release_channel, + telemetry, + })?); + + let mut response = client.get(&release.url, request_body, true).await?; + smol::io::copy(response.body_mut(), &mut target_file).await?; + log::info!("downloaded update. path:{:?}", target_path); + + Ok(target_path) +} + +async fn install_release_linux( + temp_dir: &tempfile::TempDir, + downloaded_tar_gz: PathBuf, + cx: &AsyncAppContext, +) -> Result<()> { + let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?; + let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?); + + let extracted = temp_dir.path().join("zed"); + fs::create_dir_all(&extracted) + .await + .context("failed to create directory into which to extract update")?; + + let output = Command::new("tar") + .arg("-xzf") + .arg(&downloaded_tar_gz) + .arg("-C") + .arg(&extracted) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to extract {:?} to {:?}: {:?}", + downloaded_tar_gz, + extracted, + String::from_utf8_lossy(&output.stderr) + ); + + let suffix = if channel != "stable" { + format!("-{}", channel) + } else { + String::default() + }; + let app_folder_name = format!("zed{}.app", suffix); + + let from = extracted.join(&app_folder_name); + let to = home_dir.join(".local"); + + let output = Command::new("rsync") + .args(&["-av", "--delete"]) + .arg(&from) + .arg(&to) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to copy Zed update from {:?} to {:?}: {:?}", + from, + to, + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) +} + +async fn install_release_macos( + temp_dir: &tempfile::TempDir, + downloaded_dmg: PathBuf, + cx: &AsyncAppContext, +) -> Result<()> { + let running_app_path = ZED_APP_PATH + .clone() + .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?; + let running_app_filename = running_app_path + .file_name() + .ok_or_else(|| anyhow!("invalid running app path"))?; + + let mount_path = temp_dir.path().join("Zed"); + let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into(); + + mounted_app_path.push("/"); + let output = Command::new("hdiutil") + .args(&["attach", "-nobrowse"]) + .arg(&downloaded_dmg) + .arg("-mountroot") + .arg(&temp_dir.path()) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to mount: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + + let output = Command::new("rsync") + .args(&["-av", "--delete"]) + .arg(&mounted_app_path) + .arg(&running_app_path) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to copy app: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + + let output = Command::new("hdiutil") + .args(&["detach"]) + .arg(&mount_path) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to unount: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) +}