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):
<img
src="https://github.com/zed-industries/zed/assets/81528246/6391d217-0f44-4638-9569-88c46e5fc4ba"
width="600"/>

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 <elliott.codes@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Owen Law 2024-05-27 21:01:20 -04:00 committed by GitHub
parent 7e3ab9acc9
commit f7115be3d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 463 additions and 18 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -342,6 +342,16 @@ impl AutoUpdater {
}
async fn update(this: Model<Self>, 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)
})?;

View File

@ -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::<Vec<_>>()[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<PathBuf> {
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<OsString> {
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 {

View File

@ -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")
};

View File

@ -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"
}
]
}
]
}

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>$APP_ID</id>
<metadata_license>MIT</metadata_license>
<project_license>AGPL-3.0-or-later and Apache-2.0 and GPL-3.0-or-later</project_license>
<name>$APP_NAME</name>
<summary>High-performance, multiplayer code editor</summary>
<developer id="dev.zed">
<name translate="no">Zed Industries, Inc.</name>
</developer>
<description>
<p>
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.
</p>
<p>Features:</p>
<ul>
<li>Performance: Efficiently uses every CPU core and your GPU for instant startup, quick file loading, and responsive keystrokes.</li>
<li>Language-aware: Maintains a syntax tree for precise highlighting, and auto-indent, with LSP support for autocompletion and refactoring.</li>
<li>Collaboration: Real-time editing and navigation for multiple developers in a shared workspace.</li>
<li>AI Integration: Integrates GitHub Copilot and GPT-4 for natural language code generation.</li>
</ul>
</description>
<launchable type="desktop-id">$APP_ID.desktop</launchable>
<branding>
<color type="primary" scheme_preference="light">$BRANDING_LIGHT</color>
<color type="primary" scheme_preference="dark">$BRANDING_DARK</color>
</branding>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute>
</content_rating>
<url type="homepage">https://zed.dev</url>
<url type="bugtracker">https://github.com/zed-industries/zed/issues</url>
<url type="faq">https://zed.dev/faq</url>
<url type="help">https://zed.dev/docs/getting-started</url>
<url type="contact">https://zed.dev/docs/feedback-and-support</url>
<url type="vcs-browser">https://github.com/zed-industries/zed</url>
<url type="contribute">https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md</url>
<supports>
<internet>offline-only</internet>
</supports>
<recommends>
<control>pointing</control>
<control>keyboard</control>
<display_length compare="ge">768</display_length>
</recommends>
<screenshots>
<screenshot type="default">
<caption>Zed with a large project open, showing language server and gitblame support</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-1.png</image>
</screenshot>
<screenshot>
<caption>Zed with a file open and a channel message thread in the right sidebar</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-2.png</image>
</screenshot>
<screenshot>
<caption>Example of a channel's shared document</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-3.png</image>
</screenshot>
<screenshot>
<caption>Zed's extension list</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-4.png</image>
</screenshot>
<screenshot>
<caption>Theme switcher UI and example theme</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-5.png</image>
</screenshot>
</screenshots>
<releases>
@release_info@
</releases>
</component>

View File

@ -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;

View File

@ -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

View File

@ -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"

46
script/flatpak/bundle-flatpak Executable file
View File

@ -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=$(<crates/zed/RELEASE_CHANNEL)
export CHANNEL="$channel"
export ARCHIVE="$archive"
if [[ "$channel" == "dev" ]]; then
export APP_ID="dev.zed.Zed-Dev"
export APP_NAME="Zed Devel"
export BRANDING_LIGHT="#99c1f1"
export BRANDING_DARK="#1a5fb4"
export ICON_FILE="app-icon-dev"
elif [[ "$channel" == "nightly" ]]; then
export APP_ID="dev.zed.Zed-Nightly"
export APP_NAME="Zed Nightly"
export BRANDING_LIGHT="#e9aa6a"
export BRANDING_DARK="#1a5fb4"
export ICON_FILE="app-icon-nightly"
elif [[ "$channel" == "preview" ]]; then
export APP_ID="dev.zed.Zed-Preview"
export APP_NAME="Zed Preview"
export BRANDING_LIGHT="#99c1f1"
export BRANDING_DARK="#1a5fb4"
export ICON_FILE="app-icon-preview"
elif [[ "$channel" == "stable" ]]; then
export APP_ID="dev.zed.Zed"
export APP_NAME="Zed"
export BRANDING_LIGHT="#99c1f1"
export BRANDING_DARK="#1a5fb4"
export ICON_FILE="app-icon"
else
echo "Invalid channel: '$channel'"
exit
fi
envsubst < "crates/zed/resources/flatpak/manifest-template.json" > "$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'"

View File

@ -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"<", "&lt;", line)
line = re.sub(r">", "&gt;", 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"(?<!\`)`([^`\n]+)`(?!`)", lambda match: f"<code>{match.group(1)}</code>", 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 += "</ul>\n"
if (not in_code_fence and contains_code_fence) or (not in_list and is_list):
formatted += "<ul>\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" <li>{line}</li>"
elif in_code_fence or contains_code_fence:
line = f" <li><code> {line}</code></li>"
else:
line = f"<p>{line}</p>"
formatted += f"{line}\n"
if (not in_code_fence and contains_code_fence):
formatted += "</ul>\n"
if in_code_fence or in_list:
formatted += "</ul>\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 <tag> <channel>")
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"<release version=\"{version}\" date=\"{date}\">\n"
release_info_str += f" <description>\n"
release_info_str += textwrap.indent(body, " " * 8)
release_info_str += f" </description>\n"
release_info_str += f" <url>https://github.com/zed-industries/zed/releases/tag/{tag}</url>\n"
release_info_str += "</release>\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}'")

9
script/flatpak/deps Executable file
View File

@ -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}