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