Add remote server cross compilation (#19136)

Mikayla Maki created

This will allow us to compile debug builds of the remote-server for a
different architecture than the one we are developing on.

This also adds a CI step for building our remote server with minimal
dependencies.

Release Notes:

- N/A

Change summary

.github/workflows/ci.yml                             |  31 ++
Cargo.lock                                           |   1 
Cargo.toml                                           |   3 
Cross.toml                                           |   2 
Dockerfile-cross                                     |  17 +
Dockerfile-cross.dockerignore                        |  16 +
crates/auto_update/src/auto_update.rs                |   1 
crates/gpui/Cargo.toml                               | 109 +++++++--
crates/gpui/src/platform.rs                          |  93 ++++++++
crates/gpui/src/platform/blade/blade_renderer.rs     |   2 
crates/gpui/src/platform/linux.rs                    |   8 
crates/gpui/src/platform/linux/platform.rs           | 150 ++++++-------
crates/gpui/src/platform/linux/wayland.rs            |  32 ++
crates/gpui/src/platform/linux/x11/client.rs         |   2 
crates/gpui/src/platform/linux/xdg_desktop_portal.rs |   2 
crates/gpui/src/platform/mac.rs                      |  11 
crates/gpui/src/platform/mac/platform.rs             |  19 +
crates/gpui/src/scene.rs                             |  20 +
crates/recent_projects/src/ssh_connections.rs        | 138 +++++++++---
crates/remote/src/ssh_session.rs                     |  14 +
crates/rpc/Cargo.toml                                |   5 
crates/zed/Cargo.toml                                |   6 
script/remote-server                                 |  17 +
23 files changed, 540 insertions(+), 159 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -99,7 +99,10 @@ jobs:
         run: cargo build -p collab
 
       - name: Build other binaries and features
-        run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
+        run: |
+          cargo build --workspace --bins --all-features
+          cargo check -p gpui --features "macos-blade"
+          cargo build -p remote_server
 
   linux_tests:
     timeout-minutes: 60
@@ -133,6 +136,32 @@ jobs:
       - name: Build Zed
         run: cargo build -p zed
 
+  build_remote_server:
+    timeout-minutes: 60
+    name: (Linux) Build Remote Server
+    runs-on:
+      - buildjet-16vcpu-ubuntu-2204
+    steps:
+      - name: Add Rust to the PATH
+        run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
+
+      - name: Checkout repo
+        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
+        with:
+          clean: false
+
+      - name: Cache dependencies
+        uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2
+        with:
+          save-if: ${{ github.ref == 'refs/heads/main' }}
+          cache-provider: "buildjet"
+
+      - name: Install Clang & Mold
+        run: ./script/remote-server && ./script/install-mold 2.34.0
+
+      - name: Build Remote Server
+        run: cargo build -p remote_server
+
   # todo(windows): Actually run the tests
   windows_tests:
     timeout-minutes: 60

Cargo.lock 🔗

@@ -14669,6 +14669,7 @@ dependencies = [
  "winresource",
  "workspace",
  "zed_actions",
+ "zstd",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -220,7 +220,7 @@ git = { path = "crates/git" }
 git_hosting_providers = { path = "crates/git_hosting_providers" }
 go_to_line = { path = "crates/go_to_line" }
 google_ai = { path = "crates/google_ai" }
-gpui = { path = "crates/gpui" }
+gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
 gpui_macros = { path = "crates/gpui_macros" }
 headless = { path = "crates/headless" }
 html_to_markdown = { path = "crates/html_to_markdown" }
@@ -477,6 +477,7 @@ wasmtime = { version = "24", default-features = false, features = [
 wasmtime-wasi = "24"
 which = "6.0.0"
 wit-component = "0.201"
+zstd = "0.11"
 
 [workspace.dependencies.async-stripe]
 git = "https://github.com/zed-industries/async-stripe"

Cross.toml 🔗

@@ -0,0 +1,2 @@
+[build]
+dockerfile = "Dockerfile-cross"

Dockerfile-cross 🔗

@@ -0,0 +1,17 @@
+# syntax=docker/dockerfile:1
+
+ARG CROSS_BASE_IMAGE
+FROM ${CROSS_BASE_IMAGE}
+WORKDIR /app
+ARG TZ=Etc/UTC \
+    LANG=C.UTF-8 \
+    LC_ALL=C.UTF-8 \
+    DEBIAN_FRONTEND=noninteractive
+ENV CARGO_TERM_COLOR=always
+
+COPY script/install-mold script/
+RUN ./script/install-mold "2.34.0"
+COPY script/remote-server script/
+RUN ./script/remote-server
+
+COPY . .

Dockerfile-cross.dockerignore 🔗

@@ -0,0 +1,16 @@
+.git
+.github
+**/.gitignore
+**/.gitkeep
+.gitattributes
+.mailmap
+**/target
+zed.xcworkspace
+.DS_Store
+compose.yml
+plugins/bin
+script/node_modules
+styles/node_modules
+crates/collab/static/styles.css
+vendor/bin
+assets/themes/

crates/auto_update/src/auto_update.rs 🔗

@@ -464,6 +464,7 @@ impl AutoUpdater {
         smol::fs::create_dir_all(&platform_dir).await.ok();
 
         let client = this.read_with(cx, |this, _| this.http_client.clone())?;
+
         if smol::fs::metadata(&version_path).await.is_err() {
             log::info!("downloading zed-remote-server {os} {arch}");
             download_remote_server_binary(&version_path, release, client, cx).await?;

crates/gpui/Cargo.toml 🔗

@@ -11,16 +11,53 @@ license = "Apache-2.0"
 workspace = true
 
 [features]
-default = ["http_client"]
+default = ["http_client", "font-kit", "wayland", "x11"]
 test-support = [
     "backtrace",
     "collections/test-support",
     "rand",
     "util/test-support",
     "http_client?/test-support",
+    "wayland",
+    "x11",
 ]
 runtime_shaders = []
 macos-blade = ["blade-graphics", "blade-macros", "blade-util", "bytemuck"]
+wayland = [
+    "blade-graphics",
+    "blade-macros",
+    "blade-util",
+    "bytemuck",
+    "ashpd",
+    "cosmic-text",
+    "font-kit",
+    "calloop-wayland-source",
+    "wayland-backend",
+    "wayland-client",
+    "wayland-cursor",
+    "wayland-protocols",
+    "wayland-protocols-plasma",
+    "filedescriptor",
+    "xkbcommon",
+    "open",
+]
+x11 = [
+    "blade-graphics",
+    "blade-macros",
+    "blade-util",
+    "bytemuck",
+    "ashpd",
+    "cosmic-text",
+    "font-kit",
+    "as-raw-xcb-connection",
+    "x11rb",
+    "xkbcommon",
+    "xim",
+    "x11-clipboard",
+    "filedescriptor",
+    "open",
+]
+
 
 [lib]
 path = "src/gpui.rs"
@@ -95,7 +132,7 @@ core-foundation.workspace = true
 core-foundation-sys = "0.8"
 core-graphics = "0.23"
 core-text = "20.1"
-font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7" }
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7", optional = true}
 foreign-types = "0.5"
 log.workspace = true
 media.workspace = true
@@ -105,31 +142,45 @@ objc = "0.2"
 [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
 pathfinder_geometry = "0.5"
 
-[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
-blade-graphics.workspace = true
-blade-macros.workspace = true
-blade-util.workspace = true
-bytemuck = "1"
+[target.'cfg(target_os = "linux")'.dependencies]
+# Always used
 flume = "0.11"
+oo7 = "0.3.0"
 
-[target.'cfg(target_os = "linux")'.dependencies]
-as-raw-xcb-connection = "1"
-ashpd.workspace = true
-calloop = "0.13.0"
-calloop-wayland-source = "0.3.0"
-cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "542b20c" }
-wayland-backend = { version = "0.3.3", features = ["client_system", "dlopen"] }
-wayland-client = { version = "0.31.2" }
-wayland-cursor = "0.31.1"
+# Used in both windowing options
+ashpd = { workspace = true, optional = true }
+blade-graphics = { workspace = true, optional = true }
+blade-macros = { workspace = true, optional = true }
+blade-util = { workspace = true, optional = true }
+bytemuck = { version = "1", optional = true }
+cosmic-text = { git = "https://github.com/pop-os/cosmic-text", rev = "542b20c", optional = true }
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7", features = [
+    "source-fontconfig-dlopen",
+], optional = true }
+calloop = { version = "0.13.0" }
+filedescriptor = { version = "0.8.2", optional = true }
+open = { version = "5.2.0", optional = true }
+
+# Wayland
+calloop-wayland-source = { version = "0.3.0", optional = true }
+wayland-backend = { version = "0.3.3", features = [
+    "client_system",
+    "dlopen",
+], optional = true }
+wayland-client = { version = "0.31.2", optional = true }
+wayland-cursor = { version = "0.31.1", optional = true }
 wayland-protocols = { version = "0.31.2", features = [
     "client",
     "staging",
     "unstable",
-] }
-wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
-oo7 = "0.3.0"
-open = "5.2.0"
-filedescriptor = "0.8.2"
+], optional = true }
+wayland-protocols-plasma = { version = "0.2.0", features = [
+    "client",
+], optional = true }
+
+
+# X11
+as-raw-xcb-connection = { version = "1", optional = true }
 x11rb = { version = "0.13.0", features = [
     "allow-unsafe-code",
     "xkb",
@@ -138,21 +189,23 @@ x11rb = { version = "0.13.0", features = [
     "cursor",
     "resource_manager",
     "sync",
-] }
+], optional = true }
 xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "fcbb4612185cc129ceeff51d22f7fb51810a03b2", features = [
     "wayland",
     "x11",
-] }
+], optional = true }
 xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf65a0ea94c70d3c4fd", features = [
     "x11rb-xcb",
     "x11rb-client",
-] }
-font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "40391b7", features = [
-    "source-fontconfig-dlopen",
-] }
-x11-clipboard = "0.9.2"
+], optional = true }
+x11-clipboard = { version = "0.9.2", optional = true }
 
 [target.'cfg(windows)'.dependencies]
+blade-util.workspace = true
+bytemuck = "1"
+blade-graphics.workspace = true
+blade-macros.workspace = true
+flume = "0.11"
 rand.workspace = true
 windows.workspace = true
 windows-core = "0.58"

crates/gpui/src/platform.rs 🔗

@@ -10,7 +10,11 @@ mod linux;
 #[cfg(target_os = "macos")]
 mod mac;
 
-#[cfg(any(target_os = "linux", target_os = "windows", feature = "macos-blade"))]
+#[cfg(any(
+    all(target_os = "linux", any(feature = "x11", feature = "wayland")),
+    target_os = "windows",
+    feature = "macos-blade"
+))]
 mod blade;
 
 #[cfg(any(test, feature = "test-support"))]
@@ -26,7 +30,7 @@ use crate::{
     RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
     SharedString, Size, SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
 };
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use async_task::Runnable;
 use futures::channel::oneshot;
 use image::codecs::gif::GifDecoder;
@@ -75,8 +79,12 @@ pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
     }
 
     match guess_compositor() {
+        #[cfg(feature = "wayland")]
         "Wayland" => Rc::new(WaylandClient::new()),
+
+        #[cfg(feature = "x11")]
         "X11" => Rc::new(X11Client::new()),
+
         "Headless" => Rc::new(HeadlessClient::new()),
         _ => unreachable!(),
     }
@@ -90,8 +98,16 @@ pub fn guess_compositor() -> &'static str {
     if std::env::var_os("ZED_HEADLESS").is_some() {
         return "Headless";
     }
+
+    #[cfg(feature = "wayland")]
     let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
+    #[cfg(not(feature = "wayland"))]
+    let wayland_display: Option<std::ffi::OsString> = None;
+
+    #[cfg(feature = "x11")]
     let x11_display = std::env::var_os("DISPLAY");
+    #[cfg(not(feature = "x11"))]
+    let x11_display: Option<std::ffi::OsString> = None;
 
     let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
     let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
@@ -426,6 +442,61 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
 }
 
+pub(crate) struct NoopTextSystem;
+
+impl NoopTextSystem {
+    #[allow(dead_code)]
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl PlatformTextSystem for NoopTextSystem {
+    fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
+        Ok(())
+    }
+
+    fn all_font_names(&self) -> Vec<String> {
+        Vec::new()
+    }
+
+    fn font_id(&self, descriptor: &Font) -> Result<FontId> {
+        Err(anyhow!("No font found for {:?}", descriptor))
+    }
+
+    fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
+        unimplemented!()
+    }
+
+    fn typographic_bounds(&self, font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
+        Err(anyhow!("No font found for {:?}", font_id))
+    }
+
+    fn advance(&self, font_id: FontId, _glyph_id: GlyphId) -> Result<Size<f32>> {
+        Err(anyhow!("No font found for {:?}", font_id))
+    }
+
+    fn glyph_for_char(&self, _font_id: FontId, _ch: char) -> Option<GlyphId> {
+        None
+    }
+
+    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
+        Err(anyhow!("No font found for {:?}", params))
+    }
+
+    fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+        _raster_bounds: Bounds<DevicePixels>,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
+        Err(anyhow!("No font found for {:?}", params))
+    }
+
+    fn layout_line(&self, _text: &str, _font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
+        unimplemented!()
+    }
+}
+
 #[derive(PartialEq, Eq, Hash, Clone)]
 pub(crate) enum AtlasKey {
     Glyph(RenderGlyphParams),
@@ -434,6 +505,10 @@ pub(crate) enum AtlasKey {
 }
 
 impl AtlasKey {
+    #[cfg_attr(
+        all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+        allow(dead_code)
+    )]
     pub(crate) fn texture_kind(&self) -> AtlasTextureKind {
         match self {
             AtlasKey::Glyph(params) => {
@@ -494,6 +569,10 @@ pub(crate) struct AtlasTextureId {
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 #[repr(C)]
+#[cfg_attr(
+    all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+    allow(dead_code)
+)]
 pub(crate) enum AtlasTextureKind {
     Monochrome = 0,
     Polychrome = 1,
@@ -521,6 +600,10 @@ pub(crate) struct PlatformInputHandler {
     handler: Box<dyn InputHandler>,
 }
 
+#[cfg_attr(
+    all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+    allow(dead_code)
+)]
 impl PlatformInputHandler {
     pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
         Self { cx, handler }
@@ -728,10 +811,15 @@ pub struct WindowOptions {
 
 /// The variables that can be configured when creating a new window
 #[derive(Debug)]
+#[cfg_attr(
+    all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+    allow(dead_code)
+)]
 pub(crate) struct WindowParams {
     pub bounds: Bounds<Pixels>,
 
     /// The titlebar configuration of the window
+    #[cfg_attr(feature = "wayland", allow(dead_code))]
     pub titlebar: Option<TitlebarOptions>,
 
     /// The kind of window to create
@@ -748,6 +836,7 @@ pub(crate) struct WindowParams {
     #[cfg_attr(target_os = "linux", allow(dead_code))]
     pub show: bool,
 
+    #[cfg_attr(feature = "wayland", allow(dead_code))]
     pub display_id: Option<DisplayId>,
 
     pub window_min_size: Option<Size<Pixels>>,

crates/gpui/src/platform/blade/blade_renderer.rs 🔗

@@ -455,7 +455,7 @@ impl BladeRenderer {
         }
     }
 
-    #[cfg_attr(target_os = "macos", allow(dead_code))]
+    #[cfg_attr(any(target_os = "macos", feature = "wayland"), allow(dead_code))]
     pub fn viewport_size(&self) -> gpu::Extent {
         self.surface_config.size
     }

crates/gpui/src/platform/linux.rs 🔗

@@ -1,14 +1,22 @@
 mod dispatcher;
 mod headless;
 mod platform;
+#[cfg(any(feature = "wayland", feature = "x11"))]
 mod text_system;
+#[cfg(feature = "wayland")]
 mod wayland;
+#[cfg(feature = "x11")]
 mod x11;
+
+#[cfg(any(feature = "wayland", feature = "x11"))]
 mod xdg_desktop_portal;
 
 pub(crate) use dispatcher::*;
 pub(crate) use headless::*;
 pub(crate) use platform::*;
+#[cfg(any(feature = "wayland", feature = "x11"))]
 pub(crate) use text_system::*;
+#[cfg(feature = "wayland")]
 pub(crate) use wayland::*;
+#[cfg(feature = "x11")]
 pub(crate) use x11::*;

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -19,32 +19,26 @@ use std::{
 };
 
 use anyhow::anyhow;
-use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
-use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
-use ashpd::{url, ActivationToken};
 use async_task::Runnable;
 use calloop::channel::Channel;
 use calloop::{EventLoop, LoopHandle, LoopSignal};
-use filedescriptor::FileDescriptor;
 use flume::{Receiver, Sender};
 use futures::channel::oneshot;
 use parking_lot::Mutex;
 use util::ResultExt;
-use wayland_client::Connection;
-use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
+
+#[cfg(any(feature = "wayland", feature = "x11"))]
 use xkbcommon::xkb::{self, Keycode, Keysym, State};
 
-use crate::platform::linux::wayland::WaylandClient;
+use crate::platform::NoopTextSystem;
 use crate::{
-    px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
-    DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
-    OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler,
-    PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, SharedString,
-    Size, Task, WindowAppearance, WindowOptions, WindowParams,
+    px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
+    ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers, OwnedMenu,
+    PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
+    PlatformWindow, Point, PromptLevel, Result, SemanticVersion, SharedString, Size, Task,
+    WindowAppearance, WindowOptions, WindowParams,
 };
 
-use super::x11::X11Client;
-
 pub(crate) const SCROLL_LINES: f32 = 3.0;
 
 // Values match the defaults on GTK.
@@ -93,7 +87,7 @@ pub(crate) struct PlatformHandlers {
 pub(crate) struct LinuxCommon {
     pub(crate) background_executor: BackgroundExecutor,
     pub(crate) foreground_executor: ForegroundExecutor,
-    pub(crate) text_system: Arc<CosmicTextSystem>,
+    pub(crate) text_system: Arc<dyn PlatformTextSystem>,
     pub(crate) appearance: WindowAppearance,
     pub(crate) auto_hide_scrollbars: bool,
     pub(crate) callbacks: PlatformHandlers,
@@ -104,7 +98,12 @@ pub(crate) struct LinuxCommon {
 impl LinuxCommon {
     pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
         let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
-        let text_system = Arc::new(CosmicTextSystem::new());
+        #[cfg(any(feature = "wayland", feature = "x11"))]
+        let text_system = Arc::new(crate::CosmicTextSystem::new());
+
+        #[cfg(not(any(feature = "wayland", feature = "x11")))]
+        let text_system = Arc::new(crate::NoopTextSystem::new());
+
         let callbacks = PlatformHandlers::default();
 
         let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
@@ -264,6 +263,11 @@ impl<P: LinuxClient + 'static> Platform for P {
         options: PathPromptOptions,
     ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
         let (done_tx, done_rx) = oneshot::channel();
+
+        #[cfg(not(any(feature = "wayland", feature = "x11")))]
+        done_tx.send(Ok(None));
+
+        #[cfg(any(feature = "wayland", feature = "x11"))]
         self.foreground_executor()
             .spawn(async move {
                 let title = if options.directories {
@@ -272,7 +276,7 @@ impl<P: LinuxClient + 'static> Platform for P {
                     "Open File"
                 };
 
-                let request = match OpenFileRequest::default()
+                let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
                     .modal(true)
                     .title(title)
                     .multiple(options.multiple)
@@ -310,37 +314,47 @@ impl<P: LinuxClient + 'static> Platform for P {
 
     fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
         let (done_tx, done_rx) = oneshot::channel();
-        let directory = directory.to_owned();
+
+        #[cfg(not(any(feature = "wayland", feature = "x11")))]
+        done_tx.send(Ok(None));
+
+        #[cfg(any(feature = "wayland", feature = "x11"))]
         self.foreground_executor()
-            .spawn(async move {
-                let request = match SaveFileRequest::default()
-                    .modal(true)
-                    .title("Save File")
-                    .current_folder(directory)
-                    .expect("pathbuf should not be nul terminated")
-                    .send()
-                    .await
-                {
-                    Ok(request) => request,
-                    Err(err) => {
-                        let result = match err {
-                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
-                            err => err.into(),
-                        };
-                        done_tx.send(Err(result));
-                        return;
-                    }
-                };
+            .spawn({
+                let directory = directory.to_owned();
+
+                async move {
+                    let request = match ashpd::desktop::file_chooser::SaveFileRequest::default()
+                        .modal(true)
+                        .title("Save File")
+                        .current_folder(directory)
+                        .expect("pathbuf should not be nul terminated")
+                        .send()
+                        .await
+                    {
+                        Ok(request) => request,
+                        Err(err) => {
+                            let result = match err {
+                                ashpd::Error::PortalNotFound(_) => {
+                                    anyhow!(FILE_PICKER_PORTAL_MISSING)
+                                }
+                                err => err.into(),
+                            };
+                            done_tx.send(Err(result));
+                            return;
+                        }
+                    };
 
-                let result = match request.response() {
-                    Ok(response) => Ok(response
-                        .uris()
-                        .first()
-                        .and_then(|uri| uri.to_file_path().ok())),
-                    Err(ashpd::Error::Response(_)) => Ok(None),
-                    Err(e) => Err(e.into()),
-                };
-                done_tx.send(result);
+                    let result = match request.response() {
+                        Ok(response) => Ok(response
+                            .uris()
+                            .first()
+                            .and_then(|uri| uri.to_file_path().ok())),
+                        Err(ashpd::Error::Response(_)) => Ok(None),
+                        Err(e) => Err(e.into()),
+                    };
+                    done_tx.send(result);
+                }
             })
             .detach();
 
@@ -518,16 +532,17 @@ impl<P: LinuxClient + 'static> Platform for P {
     fn add_recent_document(&self, _path: &Path) {}
 }
 
+#[cfg(any(feature = "wayland", feature = "x11"))]
 pub(super) fn open_uri_internal(
     executor: BackgroundExecutor,
     uri: &str,
     activation_token: Option<String>,
 ) {
-    if let Some(uri) = url::Url::parse(uri).log_err() {
+    if let Some(uri) = ashpd::url::Url::parse(uri).log_err() {
         executor
             .spawn(async move {
-                match OpenUriRequest::default()
-                    .activation_token(activation_token.clone().map(ActivationToken::from))
+                match ashpd::desktop::open_uri::OpenFileRequest::default()
+                    .activation_token(activation_token.clone().map(ashpd::ActivationToken::from))
                     .send_uri(&uri)
                     .await
                 {
@@ -551,6 +566,7 @@ pub(super) fn open_uri_internal(
     }
 }
 
+#[cfg(any(feature = "x11", feature = "wayland"))]
 pub(super) fn reveal_path_internal(
     executor: BackgroundExecutor,
     path: PathBuf,
@@ -559,8 +575,8 @@ pub(super) fn reveal_path_internal(
     executor
         .spawn(async move {
             if let Some(dir) = File::open(path.clone()).log_err() {
-                match OpenDirectoryRequest::default()
-                    .activation_token(activation_token.map(ActivationToken::from))
+                match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
+                    .activation_token(activation_token.map(ashpd::ActivationToken::from))
                     .send(&dir.as_fd())
                     .await
                 {
@@ -582,6 +598,7 @@ pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bo
     diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
 }
 
+#[cfg(any(feature = "wayland", feature = "x11"))]
 pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
     let mut locales = Vec::default();
     if let Some(locale) = std::env::var_os("LC_CTYPE") {
@@ -603,7 +620,8 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::S
     state
 }
 
-pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<Vec<u8>> {
+#[cfg(any(feature = "wayland", feature = "x11"))]
+pub(super) unsafe fn read_fd(mut fd: filedescriptor::FileDescriptor) -> Result<Vec<u8>> {
     let mut file = File::from_raw_fd(fd.as_raw_fd());
     let mut buffer = Vec::new();
     file.read_to_end(&mut buffer)?;
@@ -611,32 +629,6 @@ pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<Vec<u8>> {
 }
 
 impl CursorStyle {
-    pub(super) fn to_shape(&self) -> Shape {
-        match self {
-            CursorStyle::Arrow => Shape::Default,
-            CursorStyle::IBeam => Shape::Text,
-            CursorStyle::Crosshair => Shape::Crosshair,
-            CursorStyle::ClosedHand => Shape::Grabbing,
-            CursorStyle::OpenHand => Shape::Grab,
-            CursorStyle::PointingHand => Shape::Pointer,
-            CursorStyle::ResizeLeft => Shape::WResize,
-            CursorStyle::ResizeRight => Shape::EResize,
-            CursorStyle::ResizeLeftRight => Shape::EwResize,
-            CursorStyle::ResizeUp => Shape::NResize,
-            CursorStyle::ResizeDown => Shape::SResize,
-            CursorStyle::ResizeUpDown => Shape::NsResize,
-            CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
-            CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
-            CursorStyle::ResizeColumn => Shape::ColResize,
-            CursorStyle::ResizeRow => Shape::RowResize,
-            CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
-            CursorStyle::OperationNotAllowed => Shape::NotAllowed,
-            CursorStyle::DragLink => Shape::Alias,
-            CursorStyle::DragCopy => Shape::Copy,
-            CursorStyle::ContextualMenu => Shape::ContextMenu,
-        }
-    }
-
     pub(super) fn to_icon_name(&self) -> String {
         // Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
         // and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
@@ -668,6 +660,7 @@ impl CursorStyle {
     }
 }
 
+#[cfg(any(feature = "wayland", feature = "x11"))]
 impl Keystroke {
     pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
         let mut modifiers = modifiers;
@@ -813,6 +806,7 @@ impl Keystroke {
     }
 }
 
+#[cfg(any(feature = "wayland", feature = "x11"))]
 impl Modifiers {
     pub(super) fn from_xkb(keymap_state: &State) -> Self {
         let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);

crates/gpui/src/platform/linux/wayland.rs 🔗

@@ -6,3 +6,35 @@ mod serial;
 mod window;
 
 pub(crate) use client::*;
+
+use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
+
+use crate::CursorStyle;
+
+impl CursorStyle {
+    pub(super) fn to_shape(&self) -> Shape {
+        match self {
+            CursorStyle::Arrow => Shape::Default,
+            CursorStyle::IBeam => Shape::Text,
+            CursorStyle::Crosshair => Shape::Crosshair,
+            CursorStyle::ClosedHand => Shape::Grabbing,
+            CursorStyle::OpenHand => Shape::Grab,
+            CursorStyle::PointingHand => Shape::Pointer,
+            CursorStyle::ResizeLeft => Shape::WResize,
+            CursorStyle::ResizeRight => Shape::EResize,
+            CursorStyle::ResizeLeftRight => Shape::EwResize,
+            CursorStyle::ResizeUp => Shape::NResize,
+            CursorStyle::ResizeDown => Shape::SResize,
+            CursorStyle::ResizeUpDown => Shape::NsResize,
+            CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
+            CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
+            CursorStyle::ResizeColumn => Shape::ColResize,
+            CursorStyle::ResizeRow => Shape::RowResize,
+            CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
+            CursorStyle::OperationNotAllowed => Shape::NotAllowed,
+            CursorStyle::DragLink => Shape::Alias,
+            CursorStyle::DragCopy => Shape::Copy,
+            CursorStyle::ContextualMenu => Shape::ContextMenu,
+        }
+    }
+}

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -1421,10 +1421,12 @@ impl LinuxClient for X11Client {
     }
 
     fn open_uri(&self, uri: &str) {
+        #[cfg(any(feature = "wayland", feature = "x11"))]
         open_uri_internal(self.background_executor(), uri, None);
     }
 
     fn reveal_path(&self, path: PathBuf) {
+        #[cfg(any(feature = "x11", feature = "wayland"))]
         reveal_path_internal(self.background_executor(), path, None);
     }
 

crates/gpui/src/platform/linux/xdg_desktop_portal.rs 🔗

@@ -11,7 +11,9 @@ use crate::{BackgroundExecutor, WindowAppearance};
 
 pub enum Event {
     WindowAppearance(WindowAppearance),
+    #[cfg_attr(feature = "x11", allow(dead_code))]
     CursorTheme(String),
+    #[cfg_attr(feature = "x11", allow(dead_code))]
     CursorSize(u32),
 }
 

crates/gpui/src/platform/mac.rs 🔗

@@ -17,9 +17,14 @@ use metal_renderer as renderer;
 use crate::platform::blade as renderer;
 
 mod attributed_string;
+
+#[cfg(feature = "font-kit")]
 mod open_type;
-mod platform;
+
+#[cfg(feature = "font-kit")]
 mod text_system;
+
+mod platform;
 mod window;
 mod window_appearance;
 
@@ -39,9 +44,11 @@ pub(crate) use dispatcher::*;
 pub(crate) use display::*;
 pub(crate) use display_link::*;
 pub(crate) use platform::*;
-pub(crate) use text_system::*;
 pub(crate) use window::*;
 
+#[cfg(feature = "font-kit")]
+pub(crate) use text_system::*;
+
 trait BoolExt {
     fn to_objc(self) -> BOOL;
 }

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -6,9 +6,9 @@ use super::{
 use crate::{
     hash, Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem,
     ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, Keymap, MacDispatcher,
-    MacDisplay, MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform,
-    PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
-    WindowAppearance, WindowParams,
+    MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
+    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowAppearance,
+    WindowParams,
 };
 use anyhow::anyhow;
 use block::ConcreteBlock;
@@ -145,7 +145,7 @@ pub(crate) struct MacPlatform(Mutex<MacPlatformState>);
 pub(crate) struct MacPlatformState {
     background_executor: BackgroundExecutor,
     foreground_executor: ForegroundExecutor,
-    text_system: Arc<MacTextSystem>,
+    text_system: Arc<dyn PlatformTextSystem>,
     renderer_context: renderer::Context,
     headless: bool,
     pasteboard: id,
@@ -171,11 +171,18 @@ impl Default for MacPlatform {
 impl MacPlatform {
     pub(crate) fn new(headless: bool) -> Self {
         let dispatcher = Arc::new(MacDispatcher::new());
+
+        #[cfg(feature = "font-kit")]
+        let text_system = Arc::new(crate::MacTextSystem::new());
+
+        #[cfg(not(feature = "font-kit"))]
+        let text_system = Arc::new(crate::NoopTextSystem::new());
+
         Self(Mutex::new(MacPlatformState {
-            background_executor: BackgroundExecutor::new(dispatcher.clone()),
             headless,
+            text_system,
+            background_executor: BackgroundExecutor::new(dispatcher.clone()),
             foreground_executor: ForegroundExecutor::new(dispatcher),
-            text_system: Arc::new(MacTextSystem::new()),
             renderer_context: renderer::Context::default(),
             pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
             text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },

crates/gpui/src/scene.rs 🔗

@@ -40,6 +40,10 @@ impl Scene {
         self.surfaces.clear();
     }
 
+    #[cfg_attr(
+        all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+        allow(dead_code)
+    )]
     pub fn paths(&self) -> &[Path<ScaledPixels>] {
         &self.paths
     }
@@ -130,6 +134,10 @@ impl Scene {
         self.surfaces.sort();
     }
 
+    #[cfg_attr(
+        all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+        allow(dead_code)
+    )]
     pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
         BatchIterator {
             shadows: &self.shadows,
@@ -158,6 +166,10 @@ impl Scene {
 }
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)]
+#[cfg_attr(
+    all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+    allow(dead_code)
+)]
 pub(crate) enum PrimitiveKind {
     Shadow,
     #[default]
@@ -212,6 +224,10 @@ impl Primitive {
     }
 }
 
+#[cfg_attr(
+    all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+    allow(dead_code)
+)]
 struct BatchIterator<'a> {
     shadows: &'a [Shadow],
     shadows_start: usize,
@@ -398,6 +414,10 @@ impl<'a> Iterator for BatchIterator<'a> {
 }
 
 #[derive(Debug)]
+#[cfg_attr(
+    all(target_os = "linux", not(any(feature = "x11", feature = "wayland"))),
+    allow(dead_code)
+)]
 pub(crate) enum PrimitiveBatch<'a> {
     Shadows(&'a [Shadow]),
     Quads(&'a [Quad]),

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -390,40 +390,11 @@ impl SshClientDelegate {
 
         // In dev mode, build the remote server binary from source
         #[cfg(debug_assertions)]
-        if release_channel == ReleaseChannel::Dev
-            && platform.arch == std::env::consts::ARCH
-            && platform.os == std::env::consts::OS
-        {
-            use smol::process::{Command, Stdio};
-
-            self.update_status(Some("building remote server binary from source"), cx);
-            log::info!("building remote server binary from source");
-            run_cmd(Command::new("cargo").args([
-                "build",
-                "--package",
-                "remote_server",
-                "--target-dir",
-                "target/remote_server",
-            ]))
-            .await?;
-            // run_cmd(Command::new("strip").args(["target/remote_server/debug/remote_server"]))
-            // .await?;
-            run_cmd(Command::new("gzip").args([
-                "-9",
-                "-f",
-                "target/remote_server/debug/remote_server",
-            ]))
-            .await?;
-
-            let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
-            return Ok((path, version));
-
-            async fn run_cmd(command: &mut Command) -> Result<()> {
-                let output = command.stderr(Stdio::inherit()).output().await?;
-                if !output.status.success() {
-                    Err(anyhow::anyhow!("failed to run command: {:?}", command))?;
-                }
-                Ok(())
+        if release_channel == ReleaseChannel::Dev {
+            let result = self.build_local(cx, platform, version).await?;
+            // Fall through to a remote binary if we're not able to compile a local binary
+            if let Some(result) = result {
+                return Ok(result);
             }
         }
 
@@ -446,6 +417,105 @@ impl SshClientDelegate {
 
         Ok((binary_path, version))
     }
+
+    #[cfg(debug_assertions)]
+    async fn build_local(
+        &self,
+        cx: &mut AsyncAppContext,
+        platform: SshPlatform,
+        version: SemanticVersion,
+    ) -> Result<Option<(PathBuf, SemanticVersion)>> {
+        use smol::process::{Command, Stdio};
+
+        async fn run_cmd(command: &mut Command) -> Result<()> {
+            let output = command.stderr(Stdio::inherit()).output().await?;
+            if !output.status.success() {
+                Err(anyhow::anyhow!("failed to run command: {:?}", command))?;
+            }
+            Ok(())
+        }
+
+        if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
+            self.update_status(Some("Building remote server binary from source"), cx);
+            log::info!("building remote server binary from source");
+            run_cmd(Command::new("cargo").args([
+                "build",
+                "--package",
+                "remote_server",
+                "--target-dir",
+                "target/remote_server",
+            ]))
+            .await?;
+
+            self.update_status(Some("Compressing binary"), cx);
+
+            run_cmd(Command::new("gzip").args([
+                "-9",
+                "-f",
+                "target/remote_server/debug/remote_server",
+            ]))
+            .await?;
+
+            let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
+            return Ok(Some((path, version)));
+        } else if let Some(triple) = platform.triple() {
+            smol::fs::create_dir_all("target/remote-server").await?;
+
+            self.update_status(Some("Installing cross.rs"), cx);
+            log::info!("installing cross");
+            run_cmd(Command::new("cargo").args([
+                "install",
+                "cross",
+                "--git",
+                "https://github.com/cross-rs/cross",
+            ]))
+            .await?;
+
+            self.update_status(
+                Some(&format!(
+                    "Building remote server binary from source for {}",
+                    &triple
+                )),
+                cx,
+            );
+            log::info!("building remote server binary from source for {}", &triple);
+            run_cmd(
+                Command::new("cross")
+                    .args([
+                        "build",
+                        "--package",
+                        "remote_server",
+                        "--target-dir",
+                        "target/remote_server",
+                        "--target",
+                        &triple,
+                    ])
+                    .env(
+                        "CROSS_CONTAINER_OPTS",
+                        "--mount type=bind,src=./target,dst=/app/target",
+                    ),
+            )
+            .await?;
+
+            self.update_status(Some("Compressing binary"), cx);
+
+            run_cmd(Command::new("gzip").args([
+                "-9",
+                "-f",
+                &format!("target/remote_server/{}/debug/remote_server", triple),
+            ]))
+            .await?;
+
+            let path = std::env::current_dir()?.join(format!(
+                "target/remote_server/{}/debug/remote_server.gz",
+                triple
+            ));
+
+            return Ok(Some((path, version)));
+        } else {
+            return Ok(None);
+        }
+    }
 }
 
 pub fn connect_over_ssh(

crates/remote/src/ssh_session.rs 🔗

@@ -118,6 +118,20 @@ pub struct SshPlatform {
     pub arch: &'static str,
 }
 
+impl SshPlatform {
+    pub fn triple(&self) -> Option<String> {
+        Some(format!(
+            "{}-{}",
+            self.arch,
+            match self.os {
+                "linux" => "unknown-linux-gnu",
+                "macos" => "apple-darwin",
+                _ => return None,
+            }
+        ))
+    }
+}
+
 pub trait SshClientDelegate: Send + Sync {
     fn ask_password(
         &self,

crates/rpc/Cargo.toml 🔗

@@ -35,10 +35,7 @@ sha2.workspace = true
 strum.workspace = true
 tracing = { version = "0.1.34", features = ["log"] }
 util.workspace = true
-zstd = "0.11"
-
-[target.'cfg(target_os = "linux")'.dependencies]
-zstd = { version = "0.11", features = [ "pkg-config" ] }
+zstd.workspace = true
 
 [dev-dependencies]
 collections = { workspace = true, features = ["test-support"] }

crates/zed/Cargo.toml 🔗

@@ -51,7 +51,7 @@ futures.workspace = true
 git.workspace = true
 git_hosting_providers.workspace = true
 go_to_line.workspace = true
-gpui.workspace = true
+gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
 headless.workspace = true
 http_client.workspace = true
 image_viewer.workspace = true
@@ -125,6 +125,8 @@ winresource = "0.1"
 
 [target.'cfg(target_os = "linux")'.dependencies]
 ashpd.workspace = true
+# We don't use zstd in the zed crate, but we want to add this feature when compiling a desktop build of Zed
+zstd = { workspace = true, features = [ "pkg-config" ] }
 
 [dev-dependencies]
 call = { workspace = true, features = ["test-support"] }
@@ -169,4 +171,4 @@ osx_info_plist_exts = ["resources/info/*"]
 osx_url_schemes = ["zed"]
 
 [package.metadata.cargo-machete]
-ignored = ["profiling"]
+ignored = ["profiling", "zstd"]

script/remote-server 🔗

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+set -xeuo pipefail
+
+# if root or if sudo/unavailable, define an empty variable
+if [ "$(id -u)" -eq 0 ]
+then maysudo=''
+else maysudo="$(command -v sudo || command -v doas || true)"
+fi
+
+deps=(
+  clang
+)
+
+$maysudo apt-get update
+$maysudo apt-get install -y "${deps[@]}"
+exit 0