diff --git a/.gitignore b/.gitignore index 48e329d820..e0dfc13d37 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ /script/node_modules /crates/theme/schemas/theme.json /crates/collab/seed.json +/crates/zed/resources/flatpak/flatpak-cargo-sources.json +/dev.zed.Zed*.json /assets/*licenses.md **/venv .build @@ -25,3 +27,4 @@ DerivedData/ .blob_store .vscode .wrangler +.flatpak-builder diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index ca61ad75ea..871d053a58 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -342,6 +342,16 @@ impl AutoUpdater { } async fn update(this: Model, mut cx: AsyncAppContext) -> Result<()> { + // Skip auto-update for flatpaks + #[cfg(target_os = "linux")] + if matches!(std::env::var("ZED_IS_FLATPAK_INSTALL"), Ok(_)) { + this.update(&mut cx, |this, cx| { + this.status = AutoUpdateStatus::Idle; + cx.notify(); + })?; + return Ok(()); + } + let (client, current_version) = this.read_with(&cx, |this, _| { (this.http_client.clone(), this.current_version) })?; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c1752a481f..ea61c13521 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -60,6 +60,13 @@ fn parse_path_with_position( } fn main() -> Result<()> { + // Exit flatpak sandbox if needed + #[cfg(target_os = "linux")] + { + flatpak::try_restart_to_host(); + flatpak::ld_extra_libs(); + } + // Intercept version designators #[cfg(target_os = "macos")] if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) { @@ -72,6 +79,9 @@ fn main() -> Result<()> { } let args = Args::parse(); + #[cfg(target_os = "linux")] + let args = flatpak::set_bin_if_no_escape(args); + let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?; if args.version { @@ -275,6 +285,114 @@ mod linux { } } +#[cfg(target_os = "linux")] +mod flatpak { + use std::ffi::OsString; + use std::path::PathBuf; + use std::process::Command; + use std::{env, process}; + + const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH"; + const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE"; + + /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak + pub fn ld_extra_libs() { + let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") { + env::split_paths(&paths).collect() + } else { + Vec::new() + }; + + if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) { + paths.push(extra_path.into()); + } + + env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap()); + } + + /// Restarts outside of the sandbox if currently running within it + pub fn try_restart_to_host() { + if let Some(flatpak_dir) = get_flatpak_dir() { + let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()]; + args.append(&mut get_xdg_env_args()); + args.push("--env=ZED_IS_FLATPAK_INSTALL=1".into()); + args.push( + format!( + "--env={EXTRA_LIB_ENV_NAME}={}", + flatpak_dir.join("lib").to_str().unwrap() + ) + .into(), + ); + args.push(flatpak_dir.join("bin").join("zed").into()); + + let mut is_app_location_set = false; + for arg in &env::args_os().collect::>()[1..] { + args.push(arg.clone()); + is_app_location_set |= arg == "--zed"; + } + + if !is_app_location_set { + args.push("--zed".into()); + args.push(flatpak_dir.join("bin").join("zed-app").into()); + } + + let error = exec::execvp("/usr/bin/flatpak-spawn", args); + eprintln!("failed restart cli on host: {:?}", error); + process::exit(1); + } + } + + pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args { + if env::var(NO_ESCAPE_ENV_NAME).is_ok() + && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed")) + { + if args.zed.is_none() { + args.zed = Some("/app/bin/zed-app".into()); + env::set_var("ZED_IS_FLATPAK_INSTALL", "1"); + } + } + args + } + + fn get_flatpak_dir() -> Option { + if env::var(NO_ESCAPE_ENV_NAME).is_ok() { + return None; + } + + if let Ok(flatpak_id) = env::var("FLATPAK_ID") { + if !flatpak_id.starts_with("dev.zed.Zed") { + return None; + } + + let install_dir = Command::new("/usr/bin/flatpak-spawn") + .arg("--host") + .arg("flatpak") + .arg("info") + .arg("--show-location") + .arg(flatpak_id) + .output() + .unwrap(); + let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim()); + Some(install_dir.join("files")) + } else { + None + } + } + + fn get_xdg_env_args() -> Vec { + let xdg_keys = [ + "XDG_DATA_HOME", + "XDG_CONFIG_HOME", + "XDG_CACHE_HOME", + "XDG_STATE_HOME", + ]; + env::vars() + .filter(|(key, _)| xdg_keys.contains(&key.as_str())) + .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into()) + .collect() + } +} + // todo("windows") #[cfg(target_os = "windows")] mod windows { diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 02182efc6c..45b17d23f8 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -13,9 +13,11 @@ lazy_static::lazy_static! { .expect("failed to determine RoamingAppData directory") .join("Zed") } else if cfg!(target_os = "linux") { - dirs::config_dir() - .expect("failed to determine XDG_CONFIG_HOME directory") - .join("zed") + if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") { + flatpak_xdg_config.into() + } else { + dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory") + }.join("zed") } else { HOME.join(".config").join("zed") }; @@ -39,9 +41,11 @@ lazy_static::lazy_static! { pub static ref SUPPORT_DIR: PathBuf = if cfg!(target_os = "macos") { HOME.join("Library/Application Support/Zed") } else if cfg!(target_os = "linux") { - dirs::data_local_dir() - .expect("failed to determine XDG_DATA_DIR directory") - .join("zed") + if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") { + flatpak_xdg_data.into() + } else { + dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory") + }.join("zed") } else if cfg!(target_os = "windows") { dirs::data_local_dir() .expect("failed to determine LocalAppData directory") @@ -80,9 +84,11 @@ lazy_static::lazy_static! { .expect("failed to determine LocalAppData directory") .join("Zed") } else if cfg!(target_os = "linux") { - dirs::cache_dir() - .expect("failed to determine XDG_CACHE_HOME directory") - .join("zed") + if let Ok(flatpak_xdg_cache) = std::env::var("FLATPAK_XDG_CACHE_HOME") { + flatpak_xdg_cache.into() + } else { + dirs::cache_dir().expect("failed to determine XDG_CACHE_HOME directory") + }.join("zed") } else { HOME.join(".cache").join("zed") }; diff --git a/crates/zed/resources/flatpak/manifest-template.json b/crates/zed/resources/flatpak/manifest-template.json new file mode 100644 index 0000000000..2a748c74b3 --- /dev/null +++ b/crates/zed/resources/flatpak/manifest-template.json @@ -0,0 +1,59 @@ +{ + "id": "$APP_ID", + "runtime": "org.freedesktop.Platform", + "runtime-version": "23.08", + "sdk": "org.freedesktop.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.rust-stable" + ], + "command": "zed", + "finish-args": [ + "--talk-name=org.freedesktop.Flatpak", + "--device=dri", + "--share=ipc", + "--share=network", + "--socket=wayland", + "--socket=fallback-x11", + "--socket=pulseaudio", + "--filesystem=host" + ], + "build-options": { + "append-path": "/usr/lib/sdk/rust-stable/bin" + }, + "modules": [ + { + "name": "zed", + "buildsystem": "simple", + "build-options": { + "env": { + "APP_ID": "$APP_ID", + "APP_ICON": "$APP_ID", + "APP_NAME": "$APP_NAME", + "BRANDING_LIGHT": "$BRANDING_LIGHT", + "BRANDING_DARK": "$BRANDING_DARK", + "APP_ARGS": "--foreground", + "DO_STARTUP_NOTIFY": "false" + } + }, + "build-commands": [ + "install -Dm644 $ICON_FILE.png /app/share/icons/hicolor/512x512/apps/$APP_ID.png", + "envsubst < zed.desktop.in > zed.desktop && install -Dm644 zed.desktop /app/share/applications/$APP_ID.desktop", + "envsubst < flatpak/zed.metainfo.xml.in > zed.metainfo.xml && install -Dm644 zed.metainfo.xml /app/share/metainfo/$APP_ID.metainfo.xml", + "sed -i -e '/@release_info@/{r flatpak/release-info/$CHANNEL' -e 'd}' /app/share/metainfo/$APP_ID.metainfo.xml", + "install -Dm755 bin/cli /app/bin/zed", + "install -Dm755 bin/zed /app/bin/zed-app", + "install -Dm755 lib/* -t /app/lib" + ], + "sources": [ + { + "type": "archive", + "path": "./target/release/$ARCHIVE" + }, + { + "type": "dir", + "path": "./crates/zed/resources" + } + ] + } + ] +} diff --git a/crates/zed/resources/flatpak/release-info/dev b/crates/zed/resources/flatpak/release-info/dev new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed/resources/flatpak/release-info/nightly b/crates/zed/resources/flatpak/release-info/nightly new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed/resources/flatpak/release-info/preview b/crates/zed/resources/flatpak/release-info/preview new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed/resources/flatpak/release-info/stable b/crates/zed/resources/flatpak/release-info/stable new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed/resources/flatpak/zed.metainfo.xml.in b/crates/zed/resources/flatpak/zed.metainfo.xml.in new file mode 100644 index 0000000000..b0f669af52 --- /dev/null +++ b/crates/zed/resources/flatpak/zed.metainfo.xml.in @@ -0,0 +1,80 @@ + + + $APP_ID + MIT + AGPL-3.0-or-later and Apache-2.0 and GPL-3.0-or-later + + $APP_NAME + High-performance, multiplayer code editor + + Zed Industries, Inc. + + +

+ Productive coding starts with a tool that stays out of your way. Zed blends the power of an IDE with the speed of a lightweight editor for productivity you can feel under your fingertips. +

+

Features:

+
    +
  • Performance: Efficiently uses every CPU core and your GPU for instant startup, quick file loading, and responsive keystrokes.
  • +
  • Language-aware: Maintains a syntax tree for precise highlighting, and auto-indent, with LSP support for autocompletion and refactoring.
  • +
  • Collaboration: Real-time editing and navigation for multiple developers in a shared workspace.
  • +
  • AI Integration: Integrates GitHub Copilot and GPT-4 for natural language code generation.
  • +
+
+ + $APP_ID.desktop + + + $BRANDING_LIGHT + $BRANDING_DARK + + + + intense + intense + + + https://zed.dev + https://github.com/zed-industries/zed/issues + https://zed.dev/faq + https://zed.dev/docs/getting-started + https://zed.dev/docs/feedback-and-support + https://github.com/zed-industries/zed + https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md + + + offline-only + + + pointing + keyboard + 768 + + + + + Zed with a large project open, showing language server and gitblame support + https://zed.dev/img/flatpak/flatpak-1.png + + + Zed with a file open and a channel message thread in the right sidebar + https://zed.dev/img/flatpak/flatpak-2.png + + + Example of a channel's shared document + https://zed.dev/img/flatpak/flatpak-3.png + + + Zed's extension list + https://zed.dev/img/flatpak/flatpak-4.png + + + Theme switcher UI and example theme + https://zed.dev/img/flatpak/flatpak-5.png + + + + + @release_info@ + +
diff --git a/crates/zed/resources/zed.desktop b/crates/zed/resources/zed.desktop.in similarity index 58% rename from crates/zed/resources/zed.desktop rename to crates/zed/resources/zed.desktop.in index 7920e63fd7..45295c5a9d 100644 --- a/crates/zed/resources/zed.desktop +++ b/crates/zed/resources/zed.desktop.in @@ -1,13 +1,13 @@ [Desktop Entry] Version=1.0 Type=Application -Name=Zed +Name=$APP_NAME GenericName=Text Editor Comment=A high-performance, multiplayer code editor. TryExec=zed -StartupNotify=true -Exec=zed -Icon=zed -Categories=TextEditor;Development;IDE; +StartupNotify=$DO_STARTUP_NOTIFY +Exec=zed $APP_ARGS +Icon=$APP_ICON +Categories=Utility;TextEditor;Development;IDE; Keywords=zed; MimeType=text/plain;inode/directory; diff --git a/docs/src/development/linux.md b/docs/src/development/linux.md index e96d5e0c16..b28b5a48ae 100644 --- a/docs/src/development/linux.md +++ b/docs/src/development/linux.md @@ -70,6 +70,18 @@ cargo test --workspace Zed has basic support for both modes. The mode is selected at runtime. If you're on wayland and want to run in X11 mode, you can set `WAYLAND_DISPLAY='' cargo run` to do so. +## Flatpak + +> [!WARNING] +> Zed's current Flatpak integration simply exits the sandbox on startup. Workflows that rely on Flatpak's sandboxing may not work as expected. + +To build & install the Flatpak package locally follow the steps below: + +1. Install Flatpak for your distribution as outlined [here](https://flathub.org/setup). +2. Run the `script/flatpak/deps` script to install the required dependencies. +3. Run `script/flatpak/bundle-flatpak`. +4. Now the package has been installed and has a bundle available at `target/release/{app-id}.flatpak`. + ## Troubleshooting ### Can't compile zed diff --git a/script/bundle-linux b/script/bundle-linux index 274441e554..45270bf85a 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -79,14 +79,21 @@ mkdir -p "${zed_dir}/share/icons/hicolor/1024x1024/apps" cp "crates/zed/resources/app-icon$suffix@2x.png" "${zed_dir}/share/icons/hicolor/1024x1024/apps/zed.png" # .desktop -mkdir -p "${zed_dir}/share/applications" -cp "crates/zed/resources/zed.desktop" "${zed_dir}/share/applications/zed$suffix.desktop" +export DO_STARTUP_NOTIFY="true" +export APP_ICON="zed" if [[ "$channel" == "preview" ]]; then - sed -i "s|Name=Zed|Name=Zed Preview|g" "${zed_dir}/share/applications/zed$suffix.desktop" + export APP_NAME="Zed Preview" elif [[ "$channel" == "nightly" ]]; then - sed -i "s|Name=Zed|Name=Zed Nightly|g" "${zed_dir}/share/applications/zed$suffix.desktop" + export APP_NAME="Zed Nightly" +elif [[ "$channel" == "dev" ]]; then + export APP_NAME="Zed Devel" +else + export APP_NAME="Zed" fi +mkdir -p "${zed_dir}/share/applications" +envsubst < "crates/zed/resources/zed.desktop.in" > "${zed_dir}/share/applications/zed$suffix.desktop" + # Licenses cp "assets/licenses.md" "${zed_dir}/licenses.md" @@ -102,4 +109,6 @@ else fi rm -rf "${archive}" +remove_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz" +ls target/release | grep -E ${remove_match} | xargs -d "\n" -I {} rm -f target/release/{} || true tar -czvf target/release/$archive -C ${temp_dir} "zed$suffix.app" diff --git a/script/flatpak/bundle-flatpak b/script/flatpak/bundle-flatpak new file mode 100755 index 0000000000..effaa7bdfa --- /dev/null +++ b/script/flatpak/bundle-flatpak @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "$0")/../.." +shopt -s extglob + +script/bundle-linux +archive_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz" +archive=$(ls "target/release" | grep -E ${archive_match}) +channel=$( "$APP_ID.json" +flatpak-builder --user --install --force-clean build "$APP_ID.json" +flatpak build-bundle ~/.local/share/flatpak/repo "target/release/$APP_ID.flatpak" "$APP_ID" +echo "Created 'target/release/$APP_ID.flatpak'" diff --git a/script/flatpak/convert-release-notes.py b/script/flatpak/convert-release-notes.py new file mode 100644 index 0000000000..49d57a2286 --- /dev/null +++ b/script/flatpak/convert-release-notes.py @@ -0,0 +1,93 @@ +import re +import requests +import sys +import textwrap +import os + +def clean_line(line: str, in_code_fence: bool) -> str: + line = re.sub(r"<", "<", line) + line = re.sub(r">", ">", line) + line = re.sub(r"\(\[(#\d+)\]\([\w|\d\:|\/|\.|\-|_]*\)\)", lambda match: f"[{match.group(1)}]", line) + line = re.sub(r"\[(#\d+)\]\([\w|\d\:|\/|\.|\-|_]*\)", lambda match: f"[{match.group(1)}]", line) + if not in_code_fence: + line = line.strip() + + return line + + +def convert_body(body: str) -> str: + formatted = "" + + in_code_fence = False + in_list = False + for line in body.splitlines(): + line = clean_line(line, in_code_fence) + if not line: + continue + if re.search(r'\[[\w|\d|:|\/|\.|\-|_]*\]\([\w|\d|:|\/|\.|\-|_]*\)', line): + continue + line = re.sub(r"(?{match.group(1)}", line) + + contains_code_fence = bool(re.search(r"```", line)) + is_list = bool(re.search(r"^-\s*", line)) + + if in_list and not is_list: + formatted += "\n" + if (not in_code_fence and contains_code_fence) or (not in_list and is_list): + formatted += "
    \n" + in_list = is_list + in_code_fence = contains_code_fence != in_code_fence + + if is_list: + line = re.sub(r"^-\s*", "", line) + line = f"
  • {line}
  • " + elif in_code_fence or contains_code_fence: + line = f"
  • {line}
  • " + else: + line = f"

    {line}

    " + formatted += f"{line}\n" + + if (not in_code_fence and contains_code_fence): + formatted += "
\n" + if in_code_fence or in_list: + formatted += "\n" + return formatted + +def get_release_info(tag: str): + url = f"https://api.github.com/repos/zed-industries/zed/releases/tags/{tag}" + response = requests.get(url) + if response.status_code == 200: + return response.json() + else: + print(f"Failed to fetch release info for tag '{tag}'. Status code: {response.status_code}") + quit() + + +if __name__ == "__main__": + os.chdir(sys.path[0]) + + if len(sys.argv) != 3: + print("Usage: python convert-release-notes.py ") + sys.exit(1) + + tag = sys.argv[1] + channel = sys.argv[2] + + release_info = get_release_info(tag) + body = convert_body(release_info["body"]) + version = tag.removeprefix("v").removesuffix("-pre") + date = release_info["published_at"] + + release_info_str = f"\n" + release_info_str += f" \n" + release_info_str += textwrap.indent(body, " " * 8) + release_info_str += f" \n" + release_info_str += f" https://github.com/zed-industries/zed/releases/tag/{tag}\n" + release_info_str += "\n" + + channel_releases_file = f"../../crates/zed/resources/flatpak/release-info/{channel}" + with open(channel_releases_file) as f: + old_release_info = f.read() + with open(channel_releases_file, "w") as f: + f.write(textwrap.indent(release_info_str, " " * 8) + old_release_info) + print(f"Added release notes from {tag} to '{channel_releases_file}'") diff --git a/script/flatpak/deps b/script/flatpak/deps new file mode 100755 index 0000000000..dec24dfc87 --- /dev/null +++ b/script/flatpak/deps @@ -0,0 +1,9 @@ +#!/bin/sh + +flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo + +arch=$(arch) +fd_version=23.08 +flatpak install -y --user org.freedesktop.Platform/${arch}/${fd_version} +flatpak install -y --user org.freedesktop.Sdk/${arch}/${fd_version} +flatpak install -y --user org.freedesktop.Sdk.Extension.rust-stable/${arch}/${fd_version}