From f7115be3d18c8964d45fb8efc1941080b20acc32 Mon Sep 17 00:00:00 2001 From: Owen Law <81528246+someone13574@users.noreply.github.com> Date: Mon, 27 May 2024 21:01:20 -0400 Subject: [PATCH] Add Flatpak build system and support (#12006) ping #6687 This is the third iteration of this PR ([v2 here](https://github.com/zed-industries/zed/pull/11949)) and uses a different approach to the first two (the process wrapper lib was a maintainability nightmare). While the first two attempted to spawn the necessary processes using flatpak-spawn and host-spawn from the app inside the sandbox, this version first spawns the cli binary which then restart's itself *outside* of the sandbox using flatpak-spawn. The restarted cli process than can call the bundled app binary normally, with no need for flatpak-spawn because it is already outside of the sandbox. This is done instead of keeping the cli in the sandbox because ipc becomes very difficult and broken when trying to do it across the sandbox. Gnome software (example using nightly channel and release notes generated using the script): TODO in this PR: - [x] Bundle libs. - [x] Cleanup release note converter. Future work: - [ ] Auto-update dialog - [ ] Flatpak auto-update (complete 'Auto-update dialog' first) - [ ] Experimental [bundle](https://docs.flatpak.org/en/latest/single-file-bundles.html) releases for feedback (?). *(?) = Maybe / Request for feedback* Release Notes: - N/A --------- Co-authored-by: Marshall Bowers Co-authored-by: Mikayla Maki --- .gitignore | 3 + crates/auto_update/src/auto_update.rs | 10 ++ crates/cli/src/main.rs | 118 ++++++++++++++++++ crates/util/src/paths.rs | 24 ++-- .../resources/flatpak/manifest-template.json | 59 +++++++++ crates/zed/resources/flatpak/release-info/dev | 0 .../resources/flatpak/release-info/nightly | 0 .../resources/flatpak/release-info/preview | 0 .../zed/resources/flatpak/release-info/stable | 0 .../zed/resources/flatpak/zed.metainfo.xml.in | 80 ++++++++++++ .../resources/{zed.desktop => zed.desktop.in} | 10 +- docs/src/development/linux.md | 12 ++ script/bundle-linux | 17 ++- script/flatpak/bundle-flatpak | 46 +++++++ script/flatpak/convert-release-notes.py | 93 ++++++++++++++ script/flatpak/deps | 9 ++ 16 files changed, 463 insertions(+), 18 deletions(-) create mode 100644 crates/zed/resources/flatpak/manifest-template.json create mode 100644 crates/zed/resources/flatpak/release-info/dev create mode 100644 crates/zed/resources/flatpak/release-info/nightly create mode 100644 crates/zed/resources/flatpak/release-info/preview create mode 100644 crates/zed/resources/flatpak/release-info/stable create mode 100644 crates/zed/resources/flatpak/zed.metainfo.xml.in rename crates/zed/resources/{zed.desktop => zed.desktop.in} (58%) create mode 100755 script/flatpak/bundle-flatpak create mode 100644 script/flatpak/convert-release-notes.py create mode 100755 script/flatpak/deps 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}