1use anyhow::{Context as _, Result};
2use client::{Client, TelemetrySettings};
3use db::RELEASE_CHANNEL;
4use db::kvp::KEY_VALUE_STORE;
5use gpui::{
6 App, AppContext as _, AsyncApp, Context, Entity, Global, SemanticVersion, Task, Window, actions,
7};
8use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
9use paths::remote_servers_dir;
10use release_channel::{AppCommitSha, ReleaseChannel};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use settings::{Settings, SettingsSources, SettingsStore};
14use smol::{fs, io::AsyncReadExt};
15use smol::{fs::File, process::Command};
16use std::{
17 env::{
18 self,
19 consts::{ARCH, OS},
20 },
21 ffi::OsString,
22 path::{Path, PathBuf},
23 sync::Arc,
24 time::Duration,
25};
26use workspace::Workspace;
27
28const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
29const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
30
31actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
32
33#[derive(Serialize)]
34struct UpdateRequestBody {
35 installation_id: Option<Arc<str>>,
36 release_channel: Option<&'static str>,
37 telemetry: bool,
38 is_staff: Option<bool>,
39 destination: &'static str,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub enum VersionCheckType {
44 Sha(AppCommitSha),
45 Semantic(SemanticVersion),
46}
47
48#[derive(Clone, PartialEq, Eq)]
49pub enum AutoUpdateStatus {
50 Idle,
51 Checking,
52 Downloading,
53 Installing,
54 Updated {
55 binary_path: PathBuf,
56 version: VersionCheckType,
57 },
58 Errored,
59}
60
61impl AutoUpdateStatus {
62 pub fn is_updated(&self) -> bool {
63 matches!(self, Self::Updated { .. })
64 }
65}
66
67pub struct AutoUpdater {
68 status: AutoUpdateStatus,
69 current_version: SemanticVersion,
70 http_client: Arc<HttpClientWithUrl>,
71 pending_poll: Option<Task<Option<()>>>,
72}
73
74#[derive(Deserialize, Clone, Debug)]
75pub struct JsonRelease {
76 pub version: String,
77 pub url: String,
78}
79
80struct MacOsUnmounter {
81 mount_path: PathBuf,
82}
83
84impl Drop for MacOsUnmounter {
85 fn drop(&mut self) {
86 let unmount_output = std::process::Command::new("hdiutil")
87 .args(["detach", "-force"])
88 .arg(&self.mount_path)
89 .output();
90
91 match unmount_output {
92 Ok(output) if output.status.success() => {
93 log::info!("Successfully unmounted the disk image");
94 }
95 Ok(output) => {
96 log::error!(
97 "Failed to unmount disk image: {:?}",
98 String::from_utf8_lossy(&output.stderr)
99 );
100 }
101 Err(error) => {
102 log::error!("Error while trying to unmount disk image: {:?}", error);
103 }
104 }
105 }
106}
107
108struct AutoUpdateSetting(bool);
109
110/// Whether or not to automatically check for updates.
111///
112/// Default: true
113#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
114#[serde(transparent)]
115struct AutoUpdateSettingContent(bool);
116
117impl Settings for AutoUpdateSetting {
118 const KEY: Option<&'static str> = Some("auto_update");
119
120 type FileContent = Option<AutoUpdateSettingContent>;
121
122 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
123 let auto_update = [sources.server, sources.release_channel, sources.user]
124 .into_iter()
125 .find_map(|value| value.copied().flatten())
126 .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
127
128 Ok(Self(auto_update.0))
129 }
130
131 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
132 vscode.enum_setting("update.mode", current, |s| match s {
133 "none" | "manual" => Some(AutoUpdateSettingContent(false)),
134 _ => Some(AutoUpdateSettingContent(true)),
135 });
136 }
137}
138
139#[derive(Default)]
140struct GlobalAutoUpdate(Option<Entity<AutoUpdater>>);
141
142impl Global for GlobalAutoUpdate {}
143
144pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
145 AutoUpdateSetting::register(cx);
146
147 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
148 workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx));
149
150 workspace.register_action(|_, action, _, cx| {
151 view_release_notes(action, cx);
152 });
153 })
154 .detach();
155
156 let version = release_channel::AppVersion::global(cx);
157 let auto_updater = cx.new(|cx| {
158 let updater = AutoUpdater::new(version, http_client);
159
160 let poll_for_updates = ReleaseChannel::try_global(cx)
161 .map(|channel| channel.poll_for_updates())
162 .unwrap_or(false);
163
164 if option_env!("ZED_UPDATE_EXPLANATION").is_none()
165 && env::var("ZED_UPDATE_EXPLANATION").is_err()
166 && poll_for_updates
167 {
168 let mut update_subscription = AutoUpdateSetting::get_global(cx)
169 .0
170 .then(|| updater.start_polling(cx));
171
172 cx.observe_global::<SettingsStore>(move |updater: &mut AutoUpdater, cx| {
173 if AutoUpdateSetting::get_global(cx).0 {
174 if update_subscription.is_none() {
175 update_subscription = Some(updater.start_polling(cx))
176 }
177 } else {
178 update_subscription.take();
179 }
180 })
181 .detach();
182 }
183
184 updater
185 });
186 cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
187}
188
189pub fn check(_: &Check, window: &mut Window, cx: &mut App) {
190 if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
191 drop(window.prompt(
192 gpui::PromptLevel::Info,
193 "Zed was installed via a package manager.",
194 Some(message),
195 &["Ok"],
196 cx,
197 ));
198 return;
199 }
200
201 if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") {
202 drop(window.prompt(
203 gpui::PromptLevel::Info,
204 "Zed was installed via a package manager.",
205 Some(&message),
206 &["Ok"],
207 cx,
208 ));
209 return;
210 }
211
212 if !ReleaseChannel::try_global(cx)
213 .map(|channel| channel.poll_for_updates())
214 .unwrap_or(false)
215 {
216 return;
217 }
218
219 if let Some(updater) = AutoUpdater::get(cx) {
220 updater.update(cx, |updater, cx| updater.poll(cx));
221 } else {
222 drop(window.prompt(
223 gpui::PromptLevel::Info,
224 "Could not check for updates",
225 Some("Auto-updates disabled for non-bundled app."),
226 &["Ok"],
227 cx,
228 ));
229 }
230}
231
232pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
233 let auto_updater = AutoUpdater::get(cx)?;
234 let release_channel = ReleaseChannel::try_global(cx)?;
235
236 match release_channel {
237 ReleaseChannel::Stable | ReleaseChannel::Preview => {
238 let auto_updater = auto_updater.read(cx);
239 let current_version = auto_updater.current_version;
240 let release_channel = release_channel.dev_name();
241 let path = format!("/releases/{release_channel}/{current_version}");
242 let url = &auto_updater.http_client.build_url(&path);
243 cx.open_url(url);
244 }
245 ReleaseChannel::Nightly => {
246 cx.open_url("https://github.com/zed-industries/zed/commits/nightly/");
247 }
248 ReleaseChannel::Dev => {
249 cx.open_url("https://github.com/zed-industries/zed/commits/main/");
250 }
251 }
252 None
253}
254
255#[cfg(not(target_os = "windows"))]
256struct InstallerDir(tempfile::TempDir);
257
258#[cfg(not(target_os = "windows"))]
259impl InstallerDir {
260 async fn new() -> Result<Self> {
261 Ok(Self(
262 tempfile::Builder::new()
263 .prefix("zed-auto-update")
264 .tempdir()?,
265 ))
266 }
267
268 fn path(&self) -> &Path {
269 self.0.path()
270 }
271}
272
273#[cfg(target_os = "windows")]
274struct InstallerDir(PathBuf);
275
276#[cfg(target_os = "windows")]
277impl InstallerDir {
278 async fn new() -> Result<Self> {
279 let installer_dir = std::env::current_exe()?
280 .parent()
281 .context("No parent dir for Zed.exe")?
282 .join("updates");
283 if smol::fs::metadata(&installer_dir).await.is_ok() {
284 smol::fs::remove_dir_all(&installer_dir).await?;
285 }
286 smol::fs::create_dir(&installer_dir).await?;
287 Ok(Self(installer_dir))
288 }
289
290 fn path(&self) -> &Path {
291 self.0.as_path()
292 }
293}
294
295impl AutoUpdater {
296 pub fn get(cx: &mut App) -> Option<Entity<Self>> {
297 cx.default_global::<GlobalAutoUpdate>().0.clone()
298 }
299
300 fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {
301 Self {
302 status: AutoUpdateStatus::Idle,
303 current_version,
304 http_client,
305 pending_poll: None,
306 }
307 }
308
309 pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
310 cx.spawn(async move |this, cx| {
311 loop {
312 this.update(cx, |this, cx| this.poll(cx))?;
313 cx.background_executor().timer(POLL_INTERVAL).await;
314 }
315 })
316 }
317
318 pub fn poll(&mut self, cx: &mut Context<Self>) {
319 if self.pending_poll.is_some() {
320 return;
321 }
322
323 cx.notify();
324
325 self.pending_poll = Some(cx.spawn(async move |this, cx| {
326 let result = Self::update(this.upgrade()?, cx.clone()).await;
327 this.update(cx, |this, cx| {
328 this.pending_poll = None;
329 if let Err(error) = result {
330 log::error!("auto-update failed: error:{:?}", error);
331 this.status = AutoUpdateStatus::Errored;
332 cx.notify();
333 }
334 })
335 .ok()
336 }));
337 }
338
339 pub fn current_version(&self) -> SemanticVersion {
340 self.current_version
341 }
342
343 pub fn status(&self) -> AutoUpdateStatus {
344 self.status.clone()
345 }
346
347 pub fn dismiss_error(&mut self, cx: &mut Context<Self>) -> bool {
348 if self.status == AutoUpdateStatus::Idle {
349 return false;
350 }
351 self.status = AutoUpdateStatus::Idle;
352 cx.notify();
353 true
354 }
355
356 // If you are packaging Zed and need to override the place it downloads SSH remotes from,
357 // you can override this function. You should also update get_remote_server_release_url to return
358 // Ok(None).
359 pub async fn download_remote_server_release(
360 os: &str,
361 arch: &str,
362 release_channel: ReleaseChannel,
363 version: Option<SemanticVersion>,
364 cx: &mut AsyncApp,
365 ) -> Result<PathBuf> {
366 let this = cx.update(|cx| {
367 cx.default_global::<GlobalAutoUpdate>()
368 .0
369 .clone()
370 .context("auto-update not initialized")
371 })??;
372
373 let release = Self::get_release(
374 &this,
375 "zed-remote-server",
376 os,
377 arch,
378 version,
379 Some(release_channel),
380 cx,
381 )
382 .await?;
383
384 let servers_dir = paths::remote_servers_dir();
385 let channel_dir = servers_dir.join(release_channel.dev_name());
386 let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
387 let version_path = platform_dir.join(format!("{}.gz", release.version));
388 smol::fs::create_dir_all(&platform_dir).await.ok();
389
390 let client = this.read_with(cx, |this, _| this.http_client.clone())?;
391
392 if smol::fs::metadata(&version_path).await.is_err() {
393 log::info!(
394 "downloading zed-remote-server {os} {arch} version {}",
395 release.version
396 );
397 download_remote_server_binary(&version_path, release, client, cx).await?;
398 }
399
400 Ok(version_path)
401 }
402
403 pub async fn get_remote_server_release_url(
404 os: &str,
405 arch: &str,
406 release_channel: ReleaseChannel,
407 version: Option<SemanticVersion>,
408 cx: &mut AsyncApp,
409 ) -> Result<Option<(String, String)>> {
410 let this = cx.update(|cx| {
411 cx.default_global::<GlobalAutoUpdate>()
412 .0
413 .clone()
414 .context("auto-update not initialized")
415 })??;
416
417 let release = Self::get_release(
418 &this,
419 "zed-remote-server",
420 os,
421 arch,
422 version,
423 Some(release_channel),
424 cx,
425 )
426 .await?;
427
428 let update_request_body = build_remote_server_update_request_body(cx)?;
429 let body = serde_json::to_string(&update_request_body)?;
430
431 Ok(Some((release.url, body)))
432 }
433
434 async fn get_release(
435 this: &Entity<Self>,
436 asset: &str,
437 os: &str,
438 arch: &str,
439 version: Option<SemanticVersion>,
440 release_channel: Option<ReleaseChannel>,
441 cx: &mut AsyncApp,
442 ) -> Result<JsonRelease> {
443 let client = this.read_with(cx, |this, _| this.http_client.clone())?;
444
445 if let Some(version) = version {
446 let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable");
447
448 let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",);
449
450 Ok(JsonRelease {
451 version: version.to_string(),
452 url: client.build_url(&url),
453 })
454 } else {
455 let mut url_string = client.build_url(&format!(
456 "/api/releases/latest?asset={}&os={}&arch={}",
457 asset, os, arch
458 ));
459 if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
460 url_string += "&";
461 url_string += param;
462 }
463
464 let mut response = client.get(&url_string, Default::default(), true).await?;
465 let mut body = Vec::new();
466 response.body_mut().read_to_end(&mut body).await?;
467
468 anyhow::ensure!(
469 response.status().is_success(),
470 "failed to fetch release: {:?}",
471 String::from_utf8_lossy(&body),
472 );
473
474 serde_json::from_slice(body.as_slice()).with_context(|| {
475 format!(
476 "error deserializing release {:?}",
477 String::from_utf8_lossy(&body),
478 )
479 })
480 }
481 }
482
483 async fn get_latest_release(
484 this: &Entity<Self>,
485 asset: &str,
486 os: &str,
487 arch: &str,
488 release_channel: Option<ReleaseChannel>,
489 cx: &mut AsyncApp,
490 ) -> Result<JsonRelease> {
491 Self::get_release(this, asset, os, arch, None, release_channel, cx).await
492 }
493
494 async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
495 let (client, installed_version, previous_status, release_channel) =
496 this.read_with(&mut cx, |this, cx| {
497 (
498 this.http_client.clone(),
499 this.current_version,
500 this.status.clone(),
501 ReleaseChannel::try_global(cx),
502 )
503 })?;
504
505 this.update(&mut cx, |this, cx| {
506 this.status = AutoUpdateStatus::Checking;
507 cx.notify();
508 })?;
509
510 let fetched_release_data =
511 Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
512 let fetched_version = fetched_release_data.clone().version;
513 let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full()));
514 let newer_version = Self::check_if_fetched_version_is_newer(
515 *RELEASE_CHANNEL,
516 app_commit_sha,
517 installed_version,
518 fetched_version,
519 previous_status.clone(),
520 )?;
521
522 let Some(newer_version) = newer_version else {
523 return this.update(&mut cx, |this, cx| {
524 let status = match previous_status {
525 AutoUpdateStatus::Updated { .. } => previous_status,
526 _ => AutoUpdateStatus::Idle,
527 };
528 this.status = status;
529 cx.notify();
530 });
531 };
532
533 this.update(&mut cx, |this, cx| {
534 this.status = AutoUpdateStatus::Downloading;
535 cx.notify();
536 })?;
537
538 let installer_dir = InstallerDir::new().await?;
539 let target_path = Self::target_path(&installer_dir).await?;
540 download_release(&target_path, fetched_release_data, client, &cx).await?;
541
542 this.update(&mut cx, |this, cx| {
543 this.status = AutoUpdateStatus::Installing;
544 cx.notify();
545 })?;
546
547 let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
548
549 this.update(&mut cx, |this, cx| {
550 this.set_should_show_update_notification(true, cx)
551 .detach_and_log_err(cx);
552 this.status = AutoUpdateStatus::Updated {
553 binary_path,
554 version: newer_version,
555 };
556 cx.notify();
557 })
558 }
559
560 fn check_if_fetched_version_is_newer(
561 release_channel: ReleaseChannel,
562 app_commit_sha: Result<Option<String>>,
563 installed_version: SemanticVersion,
564 fetched_version: String,
565 status: AutoUpdateStatus,
566 ) -> Result<Option<VersionCheckType>> {
567 let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
568
569 if let AutoUpdateStatus::Updated { version, .. } = status {
570 match version {
571 VersionCheckType::Sha(cached_version) => {
572 let should_download = fetched_version != cached_version.full();
573 let newer_version = should_download
574 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
575 return Ok(newer_version);
576 }
577 VersionCheckType::Semantic(cached_version) => {
578 return Self::check_if_fetched_version_is_newer_non_nightly(
579 cached_version,
580 parsed_fetched_version?,
581 );
582 }
583 }
584 }
585
586 match release_channel {
587 ReleaseChannel::Nightly => {
588 let should_download = app_commit_sha
589 .ok()
590 .flatten()
591 .map(|sha| fetched_version != sha)
592 .unwrap_or(true);
593 let newer_version = should_download
594 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
595 Ok(newer_version)
596 }
597 _ => Self::check_if_fetched_version_is_newer_non_nightly(
598 installed_version,
599 parsed_fetched_version?,
600 ),
601 }
602 }
603
604 async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
605 let filename = match OS {
606 "macos" => anyhow::Ok("Zed.dmg"),
607 "linux" => Ok("zed.tar.gz"),
608 "windows" => Ok("ZedUpdateInstaller.exe"),
609 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
610 }?;
611
612 #[cfg(not(target_os = "windows"))]
613 anyhow::ensure!(
614 which::which("rsync").is_ok(),
615 "Aborting. Could not find rsync which is required for auto-updates."
616 );
617
618 Ok(installer_dir.path().join(filename))
619 }
620
621 async fn binary_path(
622 installer_dir: InstallerDir,
623 target_path: PathBuf,
624 cx: &AsyncApp,
625 ) -> Result<PathBuf> {
626 match OS {
627 "macos" => install_release_macos(&installer_dir, target_path, cx).await,
628 "linux" => install_release_linux(&installer_dir, target_path, cx).await,
629 "windows" => install_release_windows(target_path).await,
630 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
631 }
632 }
633
634 fn check_if_fetched_version_is_newer_non_nightly(
635 installed_version: SemanticVersion,
636 fetched_version: SemanticVersion,
637 ) -> Result<Option<VersionCheckType>> {
638 let should_download = fetched_version > installed_version;
639 let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
640 Ok(newer_version)
641 }
642
643 pub fn set_should_show_update_notification(
644 &self,
645 should_show: bool,
646 cx: &App,
647 ) -> Task<Result<()>> {
648 cx.background_spawn(async move {
649 if should_show {
650 KEY_VALUE_STORE
651 .write_kvp(
652 SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
653 "".to_string(),
654 )
655 .await?;
656 } else {
657 KEY_VALUE_STORE
658 .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
659 .await?;
660 }
661 Ok(())
662 })
663 }
664
665 pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
666 cx.background_spawn(async move {
667 Ok(KEY_VALUE_STORE
668 .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
669 .is_some())
670 })
671 }
672}
673
674async fn download_remote_server_binary(
675 target_path: &PathBuf,
676 release: JsonRelease,
677 client: Arc<HttpClientWithUrl>,
678 cx: &AsyncApp,
679) -> Result<()> {
680 let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
681 let mut temp_file = File::create(&temp).await?;
682 let update_request_body = build_remote_server_update_request_body(cx)?;
683 let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
684
685 let mut response = client.get(&release.url, request_body, true).await?;
686 anyhow::ensure!(
687 response.status().is_success(),
688 "failed to download remote server release: {:?}",
689 response.status()
690 );
691 smol::io::copy(response.body_mut(), &mut temp_file).await?;
692 smol::fs::rename(&temp, &target_path).await?;
693
694 Ok(())
695}
696
697fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result<UpdateRequestBody> {
698 let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
699 let telemetry = Client::global(cx).telemetry().clone();
700 let is_staff = telemetry.is_staff();
701 let installation_id = telemetry.installation_id();
702 let release_channel =
703 ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
704 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
705
706 (
707 installation_id,
708 release_channel,
709 telemetry_enabled,
710 is_staff,
711 )
712 })?;
713
714 Ok(UpdateRequestBody {
715 installation_id,
716 release_channel,
717 telemetry: telemetry_enabled,
718 is_staff,
719 destination: "remote",
720 })
721}
722
723async fn download_release(
724 target_path: &Path,
725 release: JsonRelease,
726 client: Arc<HttpClientWithUrl>,
727 cx: &AsyncApp,
728) -> Result<()> {
729 let mut target_file = File::create(&target_path).await?;
730
731 let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
732 let telemetry = Client::global(cx).telemetry().clone();
733 let is_staff = telemetry.is_staff();
734 let installation_id = telemetry.installation_id();
735 let release_channel =
736 ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
737 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
738
739 (
740 installation_id,
741 release_channel,
742 telemetry_enabled,
743 is_staff,
744 )
745 })?;
746
747 let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
748 installation_id,
749 release_channel,
750 telemetry: telemetry_enabled,
751 is_staff,
752 destination: "local",
753 })?);
754
755 let mut response = client.get(&release.url, request_body, true).await?;
756 smol::io::copy(response.body_mut(), &mut target_file).await?;
757 log::info!("downloaded update. path:{:?}", target_path);
758
759 Ok(())
760}
761
762async fn install_release_linux(
763 temp_dir: &InstallerDir,
764 downloaded_tar_gz: PathBuf,
765 cx: &AsyncApp,
766) -> Result<PathBuf> {
767 let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
768 let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
769 let running_app_path = cx.update(|cx| cx.app_path())??;
770
771 let extracted = temp_dir.path().join("zed");
772 fs::create_dir_all(&extracted)
773 .await
774 .context("failed to create directory into which to extract update")?;
775
776 let output = Command::new("tar")
777 .arg("-xzf")
778 .arg(&downloaded_tar_gz)
779 .arg("-C")
780 .arg(&extracted)
781 .output()
782 .await?;
783
784 anyhow::ensure!(
785 output.status.success(),
786 "failed to extract {:?} to {:?}: {:?}",
787 downloaded_tar_gz,
788 extracted,
789 String::from_utf8_lossy(&output.stderr)
790 );
791
792 let suffix = if channel != "stable" {
793 format!("-{}", channel)
794 } else {
795 String::default()
796 };
797 let app_folder_name = format!("zed{}.app", suffix);
798
799 let from = extracted.join(&app_folder_name);
800 let mut to = home_dir.join(".local");
801
802 let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
803
804 if let Some(prefix) = running_app_path
805 .to_str()
806 .and_then(|str| str.strip_suffix(&expected_suffix))
807 {
808 to = PathBuf::from(prefix);
809 }
810
811 let output = Command::new("rsync")
812 .args(["-av", "--delete"])
813 .arg(&from)
814 .arg(&to)
815 .output()
816 .await?;
817
818 anyhow::ensure!(
819 output.status.success(),
820 "failed to copy Zed update from {:?} to {:?}: {:?}",
821 from,
822 to,
823 String::from_utf8_lossy(&output.stderr)
824 );
825
826 Ok(to.join(expected_suffix))
827}
828
829async fn install_release_macos(
830 temp_dir: &InstallerDir,
831 downloaded_dmg: PathBuf,
832 cx: &AsyncApp,
833) -> Result<PathBuf> {
834 let running_app_path = cx.update(|cx| cx.app_path())??;
835 let running_app_filename = running_app_path
836 .file_name()
837 .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
838
839 let mount_path = temp_dir.path().join("Zed");
840 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
841
842 mounted_app_path.push("/");
843 let output = Command::new("hdiutil")
844 .args(["attach", "-nobrowse"])
845 .arg(&downloaded_dmg)
846 .arg("-mountroot")
847 .arg(temp_dir.path())
848 .output()
849 .await?;
850
851 anyhow::ensure!(
852 output.status.success(),
853 "failed to mount: {:?}",
854 String::from_utf8_lossy(&output.stderr)
855 );
856
857 // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
858 let _unmounter = MacOsUnmounter {
859 mount_path: mount_path.clone(),
860 };
861
862 let output = Command::new("rsync")
863 .args(["-av", "--delete"])
864 .arg(&mounted_app_path)
865 .arg(&running_app_path)
866 .output()
867 .await?;
868
869 anyhow::ensure!(
870 output.status.success(),
871 "failed to copy app: {:?}",
872 String::from_utf8_lossy(&output.stderr)
873 );
874
875 Ok(running_app_path)
876}
877
878async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
879 let output = Command::new(downloaded_installer)
880 .arg("/verysilent")
881 .arg("/update=true")
882 .arg("!desktopicon")
883 .arg("!quicklaunchicon")
884 .output()
885 .await?;
886 anyhow::ensure!(
887 output.status.success(),
888 "failed to start installer: {:?}",
889 String::from_utf8_lossy(&output.stderr)
890 );
891 Ok(std::env::current_exe()?)
892}
893
894pub fn check_pending_installation() -> bool {
895 let Some(installer_path) = std::env::current_exe()
896 .ok()
897 .and_then(|p| p.parent().map(|p| p.join("updates")))
898 else {
899 return false;
900 };
901
902 // The installer will create a flag file after it finishes updating
903 let flag_file = installer_path.join("versions.txt");
904 if flag_file.exists() {
905 if let Some(helper) = installer_path
906 .parent()
907 .map(|p| p.join("tools\\auto_update_helper.exe"))
908 {
909 let _ = std::process::Command::new(helper).spawn();
910 return true;
911 }
912 }
913 false
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919
920 #[test]
921 fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
922 let release_channel = ReleaseChannel::Stable;
923 let app_commit_sha = Ok(Some("a".to_string()));
924 let installed_version = SemanticVersion::new(1, 0, 0);
925 let status = AutoUpdateStatus::Idle;
926 let fetched_version = SemanticVersion::new(1, 0, 0);
927
928 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
929 release_channel,
930 app_commit_sha,
931 installed_version,
932 fetched_version.to_string(),
933 status,
934 );
935
936 assert_eq!(newer_version.unwrap(), None);
937 }
938
939 #[test]
940 fn test_stable_does_update_when_fetched_version_is_higher() {
941 let release_channel = ReleaseChannel::Stable;
942 let app_commit_sha = Ok(Some("a".to_string()));
943 let installed_version = SemanticVersion::new(1, 0, 0);
944 let status = AutoUpdateStatus::Idle;
945 let fetched_version = SemanticVersion::new(1, 0, 1);
946
947 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
948 release_channel,
949 app_commit_sha,
950 installed_version,
951 fetched_version.to_string(),
952 status,
953 );
954
955 assert_eq!(
956 newer_version.unwrap(),
957 Some(VersionCheckType::Semantic(fetched_version))
958 );
959 }
960
961 #[test]
962 fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
963 let release_channel = ReleaseChannel::Stable;
964 let app_commit_sha = Ok(Some("a".to_string()));
965 let installed_version = SemanticVersion::new(1, 0, 0);
966 let status = AutoUpdateStatus::Updated {
967 binary_path: PathBuf::new(),
968 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
969 };
970 let fetched_version = SemanticVersion::new(1, 0, 1);
971
972 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
973 release_channel,
974 app_commit_sha,
975 installed_version,
976 fetched_version.to_string(),
977 status,
978 );
979
980 assert_eq!(newer_version.unwrap(), None);
981 }
982
983 #[test]
984 fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
985 let release_channel = ReleaseChannel::Stable;
986 let app_commit_sha = Ok(Some("a".to_string()));
987 let installed_version = SemanticVersion::new(1, 0, 0);
988 let status = AutoUpdateStatus::Updated {
989 binary_path: PathBuf::new(),
990 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
991 };
992 let fetched_version = SemanticVersion::new(1, 0, 2);
993
994 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
995 release_channel,
996 app_commit_sha,
997 installed_version,
998 fetched_version.to_string(),
999 status,
1000 );
1001
1002 assert_eq!(
1003 newer_version.unwrap(),
1004 Some(VersionCheckType::Semantic(fetched_version))
1005 );
1006 }
1007
1008 #[test]
1009 fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1010 let release_channel = ReleaseChannel::Nightly;
1011 let app_commit_sha = Ok(Some("a".to_string()));
1012 let installed_version = SemanticVersion::new(1, 0, 0);
1013 let status = AutoUpdateStatus::Idle;
1014 let fetched_sha = "a".to_string();
1015
1016 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1017 release_channel,
1018 app_commit_sha,
1019 installed_version,
1020 fetched_sha,
1021 status,
1022 );
1023
1024 assert_eq!(newer_version.unwrap(), None);
1025 }
1026
1027 #[test]
1028 fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1029 let release_channel = ReleaseChannel::Nightly;
1030 let app_commit_sha = Ok(Some("a".to_string()));
1031 let installed_version = SemanticVersion::new(1, 0, 0);
1032 let status = AutoUpdateStatus::Idle;
1033 let fetched_sha = "b".to_string();
1034
1035 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1036 release_channel,
1037 app_commit_sha,
1038 installed_version,
1039 fetched_sha.clone(),
1040 status,
1041 );
1042
1043 assert_eq!(
1044 newer_version.unwrap(),
1045 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1046 );
1047 }
1048
1049 #[test]
1050 fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
1051 let release_channel = ReleaseChannel::Nightly;
1052 let app_commit_sha = Ok(Some("a".to_string()));
1053 let installed_version = SemanticVersion::new(1, 0, 0);
1054 let status = AutoUpdateStatus::Updated {
1055 binary_path: PathBuf::new(),
1056 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1057 };
1058 let fetched_sha = "b".to_string();
1059
1060 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1061 release_channel,
1062 app_commit_sha,
1063 installed_version,
1064 fetched_sha,
1065 status,
1066 );
1067
1068 assert_eq!(newer_version.unwrap(), None);
1069 }
1070
1071 #[test]
1072 fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
1073 let release_channel = ReleaseChannel::Nightly;
1074 let app_commit_sha = Ok(Some("a".to_string()));
1075 let installed_version = SemanticVersion::new(1, 0, 0);
1076 let status = AutoUpdateStatus::Updated {
1077 binary_path: PathBuf::new(),
1078 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1079 };
1080 let fetched_sha = "c".to_string();
1081
1082 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1083 release_channel,
1084 app_commit_sha,
1085 installed_version,
1086 fetched_sha.clone(),
1087 status,
1088 );
1089
1090 assert_eq!(
1091 newer_version.unwrap(),
1092 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1093 );
1094 }
1095
1096 #[test]
1097 fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1098 let release_channel = ReleaseChannel::Nightly;
1099 let app_commit_sha = Ok(None);
1100 let installed_version = SemanticVersion::new(1, 0, 0);
1101 let status = AutoUpdateStatus::Idle;
1102 let fetched_sha = "a".to_string();
1103
1104 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1105 release_channel,
1106 app_commit_sha,
1107 installed_version,
1108 fetched_sha.clone(),
1109 status,
1110 );
1111
1112 assert_eq!(
1113 newer_version.unwrap(),
1114 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1115 );
1116 }
1117
1118 #[test]
1119 fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1120 {
1121 let release_channel = ReleaseChannel::Nightly;
1122 let app_commit_sha = Ok(None);
1123 let installed_version = SemanticVersion::new(1, 0, 0);
1124 let status = AutoUpdateStatus::Updated {
1125 binary_path: PathBuf::new(),
1126 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1127 };
1128 let fetched_sha = "b".to_string();
1129
1130 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1131 release_channel,
1132 app_commit_sha,
1133 installed_version,
1134 fetched_sha,
1135 status,
1136 );
1137
1138 assert_eq!(newer_version.unwrap(), None);
1139 }
1140
1141 #[test]
1142 fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1143 {
1144 let release_channel = ReleaseChannel::Nightly;
1145 let app_commit_sha = Ok(None);
1146 let installed_version = SemanticVersion::new(1, 0, 0);
1147 let status = AutoUpdateStatus::Updated {
1148 binary_path: PathBuf::new(),
1149 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1150 };
1151 let fetched_sha = "c".to_string();
1152
1153 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1154 release_channel,
1155 app_commit_sha,
1156 installed_version,
1157 fetched_sha.clone(),
1158 status,
1159 );
1160
1161 assert_eq!(
1162 newer_version.unwrap(),
1163 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1164 );
1165 }
1166}