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(String),
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.0));
514 let newer_version = Self::check_for_newer_version(
515 *RELEASE_CHANNEL,
516 app_commit_sha,
517 installed_version,
518 previous_status.clone(),
519 fetched_version,
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_for_newer_version(
561 release_channel: ReleaseChannel,
562 app_commit_sha: Result<Option<String>>,
563 installed_version: SemanticVersion,
564 status: AutoUpdateStatus,
565 fetched_version: String,
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;
573 let newer_version =
574 should_download.then(|| VersionCheckType::Sha(fetched_version));
575 return Ok(newer_version);
576 }
577 VersionCheckType::Semantic(cached_version) => {
578 return Self::check_for_newer_version_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.then(|| VersionCheckType::Sha(fetched_version));
594 Ok(newer_version)
595 }
596 _ => Self::check_for_newer_version_non_nightly(
597 installed_version,
598 parsed_fetched_version?,
599 ),
600 }
601 }
602
603 async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
604 let filename = match OS {
605 "macos" => anyhow::Ok("Zed.dmg"),
606 "linux" => Ok("zed.tar.gz"),
607 "windows" => Ok("ZedUpdateInstaller.exe"),
608 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
609 }?;
610
611 #[cfg(not(target_os = "windows"))]
612 anyhow::ensure!(
613 which::which("rsync").is_ok(),
614 "Aborting. Could not find rsync which is required for auto-updates."
615 );
616
617 Ok(installer_dir.path().join(filename))
618 }
619
620 async fn binary_path(
621 installer_dir: InstallerDir,
622 target_path: PathBuf,
623 cx: &AsyncApp,
624 ) -> Result<PathBuf> {
625 match OS {
626 "macos" => install_release_macos(&installer_dir, target_path, cx).await,
627 "linux" => install_release_linux(&installer_dir, target_path, cx).await,
628 "windows" => install_release_windows(target_path).await,
629 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
630 }
631 }
632
633 fn check_for_newer_version_non_nightly(
634 installed_version: SemanticVersion,
635 fetched_version: SemanticVersion,
636 ) -> Result<Option<VersionCheckType>> {
637 let should_download = fetched_version > installed_version;
638 let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
639 Ok(newer_version)
640 }
641
642 pub fn set_should_show_update_notification(
643 &self,
644 should_show: bool,
645 cx: &App,
646 ) -> Task<Result<()>> {
647 cx.background_spawn(async move {
648 if should_show {
649 KEY_VALUE_STORE
650 .write_kvp(
651 SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
652 "".to_string(),
653 )
654 .await?;
655 } else {
656 KEY_VALUE_STORE
657 .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
658 .await?;
659 }
660 Ok(())
661 })
662 }
663
664 pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
665 cx.background_spawn(async move {
666 Ok(KEY_VALUE_STORE
667 .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
668 .is_some())
669 })
670 }
671}
672
673async fn download_remote_server_binary(
674 target_path: &PathBuf,
675 release: JsonRelease,
676 client: Arc<HttpClientWithUrl>,
677 cx: &AsyncApp,
678) -> Result<()> {
679 let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
680 let mut temp_file = File::create(&temp).await?;
681 let update_request_body = build_remote_server_update_request_body(cx)?;
682 let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
683
684 let mut response = client.get(&release.url, request_body, true).await?;
685 anyhow::ensure!(
686 response.status().is_success(),
687 "failed to download remote server release: {:?}",
688 response.status()
689 );
690 smol::io::copy(response.body_mut(), &mut temp_file).await?;
691 smol::fs::rename(&temp, &target_path).await?;
692
693 Ok(())
694}
695
696fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result<UpdateRequestBody> {
697 let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
698 let telemetry = Client::global(cx).telemetry().clone();
699 let is_staff = telemetry.is_staff();
700 let installation_id = telemetry.installation_id();
701 let release_channel =
702 ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
703 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
704
705 (
706 installation_id,
707 release_channel,
708 telemetry_enabled,
709 is_staff,
710 )
711 })?;
712
713 Ok(UpdateRequestBody {
714 installation_id,
715 release_channel,
716 telemetry: telemetry_enabled,
717 is_staff,
718 destination: "remote",
719 })
720}
721
722async fn download_release(
723 target_path: &Path,
724 release: JsonRelease,
725 client: Arc<HttpClientWithUrl>,
726 cx: &AsyncApp,
727) -> Result<()> {
728 let mut target_file = File::create(&target_path).await?;
729
730 let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
731 let telemetry = Client::global(cx).telemetry().clone();
732 let is_staff = telemetry.is_staff();
733 let installation_id = telemetry.installation_id();
734 let release_channel =
735 ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
736 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
737
738 (
739 installation_id,
740 release_channel,
741 telemetry_enabled,
742 is_staff,
743 )
744 })?;
745
746 let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
747 installation_id,
748 release_channel,
749 telemetry: telemetry_enabled,
750 is_staff,
751 destination: "local",
752 })?);
753
754 let mut response = client.get(&release.url, request_body, true).await?;
755 smol::io::copy(response.body_mut(), &mut target_file).await?;
756 log::info!("downloaded update. path:{:?}", target_path);
757
758 Ok(())
759}
760
761async fn install_release_linux(
762 temp_dir: &InstallerDir,
763 downloaded_tar_gz: PathBuf,
764 cx: &AsyncApp,
765) -> Result<PathBuf> {
766 let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
767 let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
768 let running_app_path = cx.update(|cx| cx.app_path())??;
769
770 let extracted = temp_dir.path().join("zed");
771 fs::create_dir_all(&extracted)
772 .await
773 .context("failed to create directory into which to extract update")?;
774
775 let output = Command::new("tar")
776 .arg("-xzf")
777 .arg(&downloaded_tar_gz)
778 .arg("-C")
779 .arg(&extracted)
780 .output()
781 .await?;
782
783 anyhow::ensure!(
784 output.status.success(),
785 "failed to extract {:?} to {:?}: {:?}",
786 downloaded_tar_gz,
787 extracted,
788 String::from_utf8_lossy(&output.stderr)
789 );
790
791 let suffix = if channel != "stable" {
792 format!("-{}", channel)
793 } else {
794 String::default()
795 };
796 let app_folder_name = format!("zed{}.app", suffix);
797
798 let from = extracted.join(&app_folder_name);
799 let mut to = home_dir.join(".local");
800
801 let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
802
803 if let Some(prefix) = running_app_path
804 .to_str()
805 .and_then(|str| str.strip_suffix(&expected_suffix))
806 {
807 to = PathBuf::from(prefix);
808 }
809
810 let output = Command::new("rsync")
811 .args(["-av", "--delete"])
812 .arg(&from)
813 .arg(&to)
814 .output()
815 .await?;
816
817 anyhow::ensure!(
818 output.status.success(),
819 "failed to copy Zed update from {:?} to {:?}: {:?}",
820 from,
821 to,
822 String::from_utf8_lossy(&output.stderr)
823 );
824
825 Ok(to.join(expected_suffix))
826}
827
828async fn install_release_macos(
829 temp_dir: &InstallerDir,
830 downloaded_dmg: PathBuf,
831 cx: &AsyncApp,
832) -> Result<PathBuf> {
833 let running_app_path = cx.update(|cx| cx.app_path())??;
834 let running_app_filename = running_app_path
835 .file_name()
836 .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
837
838 let mount_path = temp_dir.path().join("Zed");
839 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
840
841 mounted_app_path.push("/");
842 let output = Command::new("hdiutil")
843 .args(["attach", "-nobrowse"])
844 .arg(&downloaded_dmg)
845 .arg("-mountroot")
846 .arg(temp_dir.path())
847 .output()
848 .await?;
849
850 anyhow::ensure!(
851 output.status.success(),
852 "failed to mount: {:?}",
853 String::from_utf8_lossy(&output.stderr)
854 );
855
856 // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
857 let _unmounter = MacOsUnmounter {
858 mount_path: mount_path.clone(),
859 };
860
861 let output = Command::new("rsync")
862 .args(["-av", "--delete"])
863 .arg(&mounted_app_path)
864 .arg(&running_app_path)
865 .output()
866 .await?;
867
868 anyhow::ensure!(
869 output.status.success(),
870 "failed to copy app: {:?}",
871 String::from_utf8_lossy(&output.stderr)
872 );
873
874 Ok(running_app_path)
875}
876
877async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
878 let output = Command::new(downloaded_installer)
879 .arg("/verysilent")
880 .arg("/update=true")
881 .arg("!desktopicon")
882 .arg("!quicklaunchicon")
883 .output()
884 .await?;
885 anyhow::ensure!(
886 output.status.success(),
887 "failed to start installer: {:?}",
888 String::from_utf8_lossy(&output.stderr)
889 );
890 Ok(std::env::current_exe()?)
891}
892
893pub fn check_pending_installation() -> bool {
894 let Some(installer_path) = std::env::current_exe()
895 .ok()
896 .and_then(|p| p.parent().map(|p| p.join("updates")))
897 else {
898 return false;
899 };
900
901 // The installer will create a flag file after it finishes updating
902 let flag_file = installer_path.join("versions.txt");
903 if flag_file.exists() {
904 if let Some(helper) = installer_path
905 .parent()
906 .map(|p| p.join("tools\\auto_update_helper.exe"))
907 {
908 let _ = std::process::Command::new(helper).spawn();
909 return true;
910 }
911 }
912 false
913}
914
915#[cfg(test)]
916mod tests {
917 use super::*;
918
919 #[test]
920 fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
921 let release_channel = ReleaseChannel::Stable;
922 let app_commit_sha = Ok(Some("a".to_string()));
923 let installed_version = SemanticVersion::new(1, 0, 0);
924 let status = AutoUpdateStatus::Idle;
925 let fetched_version = SemanticVersion::new(1, 0, 0);
926
927 let newer_version = AutoUpdater::check_for_newer_version(
928 release_channel,
929 app_commit_sha,
930 installed_version,
931 status,
932 fetched_version.to_string(),
933 );
934
935 assert_eq!(newer_version.unwrap(), None);
936 }
937
938 #[test]
939 fn test_stable_does_update_when_fetched_version_is_higher() {
940 let release_channel = ReleaseChannel::Stable;
941 let app_commit_sha = Ok(Some("a".to_string()));
942 let installed_version = SemanticVersion::new(1, 0, 0);
943 let status = AutoUpdateStatus::Idle;
944 let fetched_version = SemanticVersion::new(1, 0, 1);
945
946 let newer_version = AutoUpdater::check_for_newer_version(
947 release_channel,
948 app_commit_sha,
949 installed_version,
950 status,
951 fetched_version.to_string(),
952 );
953
954 assert_eq!(
955 newer_version.unwrap(),
956 Some(VersionCheckType::Semantic(fetched_version))
957 );
958 }
959
960 #[test]
961 fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
962 let release_channel = ReleaseChannel::Stable;
963 let app_commit_sha = Ok(Some("a".to_string()));
964 let installed_version = SemanticVersion::new(1, 0, 0);
965 let status = AutoUpdateStatus::Updated {
966 binary_path: PathBuf::new(),
967 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
968 };
969 let fetched_version = SemanticVersion::new(1, 0, 1);
970
971 let newer_version = AutoUpdater::check_for_newer_version(
972 release_channel,
973 app_commit_sha,
974 installed_version,
975 status,
976 fetched_version.to_string(),
977 );
978
979 assert_eq!(newer_version.unwrap(), None);
980 }
981
982 #[test]
983 fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
984 let release_channel = ReleaseChannel::Stable;
985 let app_commit_sha = Ok(Some("a".to_string()));
986 let installed_version = SemanticVersion::new(1, 0, 0);
987 let status = AutoUpdateStatus::Updated {
988 binary_path: PathBuf::new(),
989 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
990 };
991 let fetched_version = SemanticVersion::new(1, 0, 2);
992
993 let newer_version = AutoUpdater::check_for_newer_version(
994 release_channel,
995 app_commit_sha,
996 installed_version,
997 status,
998 fetched_version.to_string(),
999 );
1000
1001 assert_eq!(
1002 newer_version.unwrap(),
1003 Some(VersionCheckType::Semantic(fetched_version))
1004 );
1005 }
1006
1007 #[test]
1008 fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1009 let release_channel = ReleaseChannel::Nightly;
1010 let app_commit_sha = Ok(Some("a".to_string()));
1011 let installed_version = SemanticVersion::new(1, 0, 0);
1012 let status = AutoUpdateStatus::Idle;
1013 let fetched_sha = "a".to_string();
1014
1015 let newer_version = AutoUpdater::check_for_newer_version(
1016 release_channel,
1017 app_commit_sha,
1018 installed_version,
1019 status,
1020 fetched_sha,
1021 );
1022
1023 assert_eq!(newer_version.unwrap(), None);
1024 }
1025
1026 #[test]
1027 fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1028 let release_channel = ReleaseChannel::Nightly;
1029 let app_commit_sha = Ok(Some("a".to_string()));
1030 let installed_version = SemanticVersion::new(1, 0, 0);
1031 let status = AutoUpdateStatus::Idle;
1032 let fetched_sha = "b".to_string();
1033
1034 let newer_version = AutoUpdater::check_for_newer_version(
1035 release_channel,
1036 app_commit_sha,
1037 installed_version,
1038 status,
1039 fetched_sha.clone(),
1040 );
1041
1042 assert_eq!(
1043 newer_version.unwrap(),
1044 Some(VersionCheckType::Sha(fetched_sha))
1045 );
1046 }
1047
1048 #[test]
1049 fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
1050 let release_channel = ReleaseChannel::Nightly;
1051 let app_commit_sha = Ok(Some("a".to_string()));
1052 let installed_version = SemanticVersion::new(1, 0, 0);
1053 let status = AutoUpdateStatus::Updated {
1054 binary_path: PathBuf::new(),
1055 version: VersionCheckType::Sha("b".to_string()),
1056 };
1057 let fetched_sha = "b".to_string();
1058
1059 let newer_version = AutoUpdater::check_for_newer_version(
1060 release_channel,
1061 app_commit_sha,
1062 installed_version,
1063 status,
1064 fetched_sha,
1065 );
1066
1067 assert_eq!(newer_version.unwrap(), None);
1068 }
1069
1070 #[test]
1071 fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
1072 let release_channel = ReleaseChannel::Nightly;
1073 let app_commit_sha = Ok(Some("a".to_string()));
1074 let installed_version = SemanticVersion::new(1, 0, 0);
1075 let status = AutoUpdateStatus::Updated {
1076 binary_path: PathBuf::new(),
1077 version: VersionCheckType::Sha("b".to_string()),
1078 };
1079 let fetched_sha = "c".to_string();
1080
1081 let newer_version = AutoUpdater::check_for_newer_version(
1082 release_channel,
1083 app_commit_sha,
1084 installed_version,
1085 status,
1086 fetched_sha.clone(),
1087 );
1088
1089 assert_eq!(
1090 newer_version.unwrap(),
1091 Some(VersionCheckType::Sha(fetched_sha))
1092 );
1093 }
1094
1095 #[test]
1096 fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1097 let release_channel = ReleaseChannel::Nightly;
1098 let app_commit_sha = Ok(None);
1099 let installed_version = SemanticVersion::new(1, 0, 0);
1100 let status = AutoUpdateStatus::Idle;
1101 let fetched_sha = "a".to_string();
1102
1103 let newer_version = AutoUpdater::check_for_newer_version(
1104 release_channel,
1105 app_commit_sha,
1106 installed_version,
1107 status,
1108 fetched_sha.clone(),
1109 );
1110
1111 assert_eq!(
1112 newer_version.unwrap(),
1113 Some(VersionCheckType::Sha(fetched_sha))
1114 );
1115 }
1116
1117 #[test]
1118 fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1119 {
1120 let release_channel = ReleaseChannel::Nightly;
1121 let app_commit_sha = Ok(None);
1122 let installed_version = SemanticVersion::new(1, 0, 0);
1123 let status = AutoUpdateStatus::Updated {
1124 binary_path: PathBuf::new(),
1125 version: VersionCheckType::Sha("b".to_string()),
1126 };
1127 let fetched_sha = "b".to_string();
1128
1129 let newer_version = AutoUpdater::check_for_newer_version(
1130 release_channel,
1131 app_commit_sha,
1132 installed_version,
1133 status,
1134 fetched_sha,
1135 );
1136
1137 assert_eq!(newer_version.unwrap(), None);
1138 }
1139
1140 #[test]
1141 fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1142 {
1143 let release_channel = ReleaseChannel::Nightly;
1144 let app_commit_sha = Ok(None);
1145 let installed_version = SemanticVersion::new(1, 0, 0);
1146 let status = AutoUpdateStatus::Updated {
1147 binary_path: PathBuf::new(),
1148 version: VersionCheckType::Sha("b".to_string()),
1149 };
1150 let fetched_sha = "c".to_string();
1151
1152 let newer_version = AutoUpdater::check_for_newer_version(
1153 release_channel,
1154 app_commit_sha,
1155 installed_version,
1156 status,
1157 fetched_sha.clone(),
1158 );
1159
1160 assert_eq!(
1161 newer_version.unwrap(),
1162 Some(VersionCheckType::Sha(fetched_sha))
1163 );
1164 }
1165}