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