Support building with corgi (#63396)

[corgi](https://github.com/ConradIrwin/corgi) is a cargo-compatible
build tool that runs `build.rs` scripts and proc-macros in a sandbox (no
network, no ambient env, reads limited to a package's own `.rs` sources
unless declared). This makes Zed buildable under it. Cargo builds (dev
and release) are unaffected.

### Changes

- **`scratch` patch**: patch `scratch` to a small local copy
(`corgi-patches/scratch`) that reads `OUT_DIR` at runtime instead of
baking it in at compile time. `cxx-build` writes its shared cxxbridge
headers into `scratch::path(...)`; upstream returns a single global dir
(outside any action's `OUT_DIR`) that the sandbox can't grant, while
this patch gives each build script a private, writable dir in its own
entry. webrtc-sys is self-contained, so per-crate scratch dirs are
sufficient. Kept local rather than a cxx git fork because a git checkout
of cxx needs symlink support that Windows cargo CI lacks.
- **Dev asset loading**: add `util::dev_fs_embed!` and switch dev-mode
`rust_embed` asset sources (`assets`, `settings`, `grammars`, `agent`,
`edit_prediction_cli`) to read from the checkout at runtime, locating
the repo root by walking from the executable up to the enclosing `.git`.
This avoids baking `CARGO_MANIFEST_DIR` into the binary (corgi rejects
artifacts that embed the build-time checkout path, and it's wrong in any
other worktree). Release builds still embed via `#[derive(RustEmbed)]`.
- **Stop baking checkout paths into artifacts**: drop
`gpui::GPUI_MANIFEST_DIR`; resolve `../gpui` from `gpui_apple`'s build
script; stage the metal shader into `OUT_DIR` before compiling so the
`.metallib` records the output dir, not the checkout; resolve the repo
root at runtime in `remote` and `inspector_ui` (removing
`inspector_ui`'s `build.rs` / `ZED_REPO_DIR`).
- **corgi.toml**: declare the pinned tools (`cmake`, prebuilt libwebrtc
via `LK_CUSTOM_WEBRTC`), a release-only `ZED_COMMIT_SHA` probe, and the
non-`.rs` / cross-package reads each package performs.

### Validation

- `corgi check` + `corgi build`, dev and release, for `zed` (plus `cli`,
`remote_server`, `ep`) with a warm C++ cache — clean; built binaries
embed no checkout path.
- `corgi fmt --check`, `corgi clippy`, and `corgi test` on the changed
crates (922 tests pass).
- `cargo check -p util` locally; relying on CI for the full cargo
build/test matrix.

### Notes for the reviewer

- Per the repo rule, the first two lines of `README.md` are the
review-confirmation marker; remove them before merging (this is what
Danger is failing on).
- `remote_server` cross-compilation (linux-musl via zigbuild) is not yet
covered by `corgi.toml` — host (macOS arm64) only for now.

### Suggested .rules additions

Only if the team wants corgi guidance in `.rules` (it recurred several
times here): dev builds must not bake the checkout path into artifacts —
no `env!("CARGO_MANIFEST_DIR")` or `file!()`-derived absolute paths in
library/binary output. Resolve the repo root at runtime via
`util::dev_repo_root()` (walks to `.git`), and use `util::dev_fs_embed!`
for dev-mode `rust_embed` sources.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
Conrad IrwinandBen Kunkle authored and GitHub committed 2026-08-31 17:37:00 +00:00
1 parent 76b1096cbd
commit ee6badf4b0
25 files changed
+422 -95

No files matched your search

Generated
+2 -6
View File
@@ -298,7 +298,6 @@ dependencies = [
"regex",
"release_channel",
"reqwest_client",
"rust-embed",
"sandbox",
"schemars 1.0.4",
"serde",
@@ -946,7 +945,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"rust-embed",
"util",
]
[[package]]
@@ -5473,7 +5472,6 @@ dependencies = [
"rand 0.9.4",
"release_channel",
"reqwest_client",
"rust-embed",
"serde",
"serde_json",
"settings",
@@ -7848,6 +7846,7 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"tree-sitter-yaml",
"util",
]
[[package]]
@@ -15937,8 +15936,6 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scratch"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
[[package]]
name = "screencapturekit"
@@ -16469,7 +16466,6 @@ dependencies = [
"paths",
"pretty_assertions",
"release_channel",
"rust-embed",
"schemars 1.0.4",
"serde",
"serde_json",
+12
View File
@@ -988,6 +988,18 @@ notify = { git = "https://github.com/zed-industries/notify", rev = "0890bbb8ca40
notify-types = { git = "https://github.com/zed-industries/notify", rev = "0890bbb8ca40a4b5d1f67031698dd7918b37d991" }
webrtc-sys = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "0a1c519cfce9b365229026b55de9b9dbdb6fed3c" }
# corgi runs build.rs/proc-macros in a sandbox with no write access outside the
# action's own OUT_DIR. Upstream `scratch` bakes its build-time OUT_DIR in via
# `env!` and hands every crate one shared directory, which the sandbox can't
# grant and a content-addressed build can't share. This patch reads OUT_DIR at
# runtime, so each build script gets a private, writable dir in its own entry —
# where cxx-build writes its shared cxxbridge headers. Kept local rather than a
# cxx git fork so Windows cargo builds are unaffected (a git checkout of cxx
# needs symlink support that Windows CI lacks). webrtc-sys is self-contained, so
# per-crate scratch dirs are sufficient. Upstream issue:
# https://github.com/dtolnay/cxx/issues/1752
scratch = { path = "corgi-patches/scratch" }
[profile.dev]
split-debuginfo = "unpacked"
incremental = true
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "scratch"
version = "1.0.9"
edition = "2021"
description = "corgi-patched scratch: uses a keyed, per-action shared directory"
license = "MIT OR Apache-2.0"
[lib]
path = "src/lib.rs"
+1
View File
@@ -0,0 +1 @@
../../LICENSE-APACHE
+8
View File
@@ -0,0 +1,8 @@
use std::{env, fs};
fn main() {
// Same as upstream: the fallback dir is wiped by `cargo clean`.
let out_dir = env::var_os("OUT_DIR").unwrap();
let _ = fs::remove_dir_all(&out_dir);
let _ = fs::create_dir(&out_dir);
}
+19
View File
@@ -0,0 +1,19 @@
//! A corgi-compatible `scratch` implementation.
//!
//! Upstream bakes its own OUT_DIR in at compile time (`env!`), making one
//! global directory that every crate's build script writes into --
//! cross-action shared mutable state, which a content-addressed build
//! cannot allow. This patch reads OUT_DIR at *runtime*, so each build
//! script gets a private dir inside its own (published, deterministic)
//! entry. Self-contained cxx crates are unaffected; genuine cross-crate
//! header sharing would fail loudly with a missing include, which is the
//! signal to build the union-of-exports view.
use std::fs;
use std::path::PathBuf;
pub fn path(suffix: &str) -> PathBuf {
let p = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join(suffix);
let _ = fs::create_dir_all(&p);
p
}
+83
View File
@@ -0,0 +1,83 @@
# corgi build configuration for Zed.
# Corgi source and documentation: https://github.com/ConradIrwin/corgi
#
# corgi runs build.rs scripts and proc-macros in a sandbox with no network,
# no ambient environment, and reads limited to each package's own source plus
# what is declared here. Every setting names the packages it applies to, so
# only those packages' actions key on it — comment or command-text edits
# rebuild nothing.
#
# See the top-level Cargo.toml `[patch.crates-io]` for the local `scratch` patch
# that lets cxx/webrtc-sys build under the sandbox, and `util::fs_embed!` for
# how dev builds read embedded assets from the checkout instead of baking the
# build path into the binary.
# sha256-pinned tools, fetched into the corgi store and exposed to the named
# packages' build scripts via the given environment variable. URLs and hashes
# are for the macOS arm64 build host.
#
# Note: no protoc tool is needed. `proto` and `livekit_api` compile their
# protobufs with `protox` (a pure-Rust protoc) and `prost_build::compile_fds`,
# so nothing shells out to a protoc binary.
[tools.cmake]
version = "4.1.2"
url = "https://github.com/Kitware/CMake/releases/download/v4.1.2/cmake-4.1.2-macos-universal.tar.gz"
sha256 = "3be85f5b999e327b1ac7d804cbc9acd767059e9f603c42ec2765f6ab68fbd367"
bin = "cmake-4.1.2-macos-universal/CMake.app/Contents/bin/cmake"
env = "CMAKE"
packages = ["webrtc-sys", "wasmtime-c-api-impl"]
# Prebuilt libwebrtc, so webrtc-sys-build does not have to download it over the
# network (which the sandbox forbids). The tag must match WEBRTC_TAG in the
# pinned livekit-rust-sdks revision; webrtc-sys-build reads LK_CUSTOM_WEBRTC and
# skips the download when it is set.
[tools.webrtc-prebuilt]
version = "0001d84-4"
url = "https://github.com/zed-industries/livekit-rust-sdks/releases/download/webrtc-0001d84-4/webrtc-mac-arm64-release.zip"
sha256 = "09ba71bfbc15c6aacced7fe20451a9648dfe648c24295f28d75fc386dbd7281a"
path = "mac-arm64-release"
env = "LK_CUSTOM_WEBRTC"
packages = ["webrtc-sys"]
# Plan-time probe: the git SHA is read here (outside the sandbox) and frozen
# into the keyed value. zed/build.rs prefers $ZED_COMMIT_SHA over running git,
# which the sandbox blocks. Release-only; dev builds embed no SHA (the source
# reads it via option_env!). cli/remote_server build.rs run git directly rather
# than reading this var, so they are not listed here.
[env.ZED_COMMIT_SHA]
command = "git rev-parse HEAD"
packages = ["zed"]
profiles = ["release"]
# Reads a package's actions perform that corgi's sandbox does not grant by
# default. By default only a package's own `.rs` files (plus its manifest) are
# readable, so non-`.rs` files a build script or macro reads must be listed even
# when they live inside the package. Paths are relative to the named package's
# root and are granted and hashed as inputs.
[extra-inputs]
# Own non-.rs files read at build time (protobufs, bindgen headers, doc/data).
proto = ["proto"]
livekit_api = ["vendored/protocol"]
media = ["src/bindings.h"]
gpui = ["README.md"]
git = ["src/checkpoint.gitignore"]
git_ui = ["src/commit_message_prompt.txt"]
editor = ["../grammars/src"]
edit_prediction = ["license_patterns"]
agent_skills = ["builtin"]
agent_settings = ["src/prompts"]
json_schema_store = ["src/schemas"]
prettier = ["src/prettier_server.js"]
zed = ["resources"]
# Cross-package reads (and, for gpui_apple, its own metal shader source).
extension_host = ["../extension_api/wit"]
gpui_apple = ["../gpui/src", "src/shaders.metal"]
prompt_store = ["../git_ui/src/commit_message_prompt.txt"]
release_channel = ["../zed/RELEASE_CHANNEL"]
cli = ["../../script/uninstall.sh"]
remote_server = ["../zed/Cargo.toml"]
# `../zed/Cargo.toml`: build.rs reads it for ZED_PKG_VERSION. `../grammars/src`
# and `src/prompts` are embedded in release builds; dev builds read them at
# runtime via fs_embed.
edit_prediction_cli = ["../zed/Cargo.toml", "../grammars/src", "src/prompts"]
-1
View File
@@ -55,7 +55,6 @@ prompt_store.workspace = true
quick-xml.workspace = true
rand.workspace = true
regex.workspace = true
rust-embed.workspace = true
sandbox.workspace = true
schemars.workspace = true
serde.workspace = true
+8 -5
View File
@@ -1,14 +1,17 @@
use anyhow::Result;
use gpui::SharedString;
use handlebars::Handlebars;
use rust_embed::RustEmbed;
use serde::Serialize;
use std::sync::Arc;
#[derive(RustEmbed)]
#[folder = "src/templates"]
#[include = "*.hbs"]
struct Assets;
// Dev builds read the checkout's templates at runtime instead of embedding
// them; see the `assets` crate for the rationale.
util::fs_embed! {
struct Assets,
crate_relative = "src/templates",
root_relative = "crates/agent/src/templates",
include = ["*.hbs"],
}
pub struct Templates(Handlebars<'static>);
+1 -1
View File
@@ -14,4 +14,4 @@ workspace = true
[dependencies]
anyhow.workspace = true
gpui.workspace = true
rust-embed.workspace = true
util.workspace = true
+19 -13
View File
@@ -2,20 +2,26 @@
use anyhow::Context as _;
use gpui::{App, AssetSource, Result, SharedString};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "fonts/**/*"]
#[include = "icons/**/*"]
#[include = "images/**/*"]
#[include = "themes/**/*"]
#[exclude = "themes/src/*"]
#[include = "sounds/**/*"]
#[include = "prompts/**/*"]
#[include = "*.md"]
#[exclude = "*.DS_Store"]
pub struct Assets;
// Release builds embed the assets; dev builds read them from the checkout at
// runtime so edits show up on the next launch without a rebuild and no
// build-time path is baked in (which corgi's sandbox rejects). See
// `util::fs_embed!`.
util::fs_embed! {
pub struct Assets,
crate_relative = "../../assets",
root_relative = "assets",
include = [
"fonts/**/*",
"icons/**/*",
"images/**/*",
"themes/**/*",
"sounds/**/*",
"prompts/**/*",
"*.md",
],
exclude = ["themes/src/*", "*.DS_Store"],
}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
-1
View File
@@ -70,7 +70,6 @@ rand.workspace = true
similar = "2.7.0"
flate2 = "1.1.8"
toml.workspace = true
rust-embed.workspace = true
gaoya = "0.2.0"
# Wasmtime is included as a dependency in order to enable the same
@@ -24,19 +24,16 @@ use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
// Dev builds read grammars' language configs from the checkout at runtime
// instead of embedding them, so no build-time path is baked into the binary.
#[cfg(not(feature = "dynamic_prompts"))]
mod language_configs_embedded {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../grammars/src/"]
#[include = "*/config.toml"]
pub struct LanguageConfigs;
util::fs_embed! {
struct LanguageConfigs,
crate_relative = "../grammars/src/",
root_relative = "crates/grammars/src",
include = ["*/config.toml"],
}
#[cfg(not(feature = "dynamic_prompts"))]
use language_configs_embedded::LanguageConfigs;
#[derive(Debug, Deserialize)]
struct LanguageConfig {
name: String,
@@ -100,7 +97,7 @@ pub struct FilterLanguagesArgs {
fn build_extension_to_language_map() -> HashMap<String, String> {
let mut map = HashMap::default();
for file_path in LanguageConfigs::iter() {
for file_path in LanguageConfigs::iter().filter(|path| path.ends_with("config.toml")) {
if let Some(content) = LanguageConfigs::get(&file_path) {
let content_str = match std::str::from_utf8(&content.data) {
Ok(s) => s,
@@ -26,11 +26,12 @@ pub fn get_prompt(name: &'static str) -> Cow<'static, str> {
#[cfg(not(feature = "dynamic_prompts"))]
pub fn get_prompt(name: &'static str) -> Cow<'static, str> {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "src/prompts"]
struct EmbeddedPrompts;
// Dev builds read prompts from the checkout at runtime (no baked path).
util::fs_embed! {
struct EmbeddedPrompts,
crate_relative = "src/prompts",
root_relative = "crates/edit_prediction_cli/src/prompts",
}
match EmbeddedPrompts::get(name) {
Some(file) => match file.data {
-2
View File
@@ -5,8 +5,6 @@
#![allow(unused_mut)] // False positives in platform specific code
extern crate self as gpui;
#[doc(hidden)]
pub static GPUI_MANIFEST_DIR: &'static str = env!("CARGO_MANIFEST_DIR");
#[macro_use]
mod action;
mod app;
-1
View File
@@ -38,7 +38,6 @@ parking_lot.workspace = true
[target.'cfg(target_os = "macos")'.build-dependencies]
cbindgen.workspace = true
gpui.workspace = true
[target.'cfg(target_os = "macos")'.dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
+20 -7
View File
@@ -96,9 +96,11 @@ mod macos_build {
output_path
}
/// Locate the gpui crate directory relative to this crate.
/// Locate the gpui crate directory relative to this crate. Resolved at
/// build-script runtime against this crate's manifest dir, so no checkout
/// path is baked into a compiled artifact (which corgi rejects).
fn find_gpui_crate_dir() -> PathBuf {
gpui::GPUI_MANIFEST_DIR.into()
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("../gpui")
}
/// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
@@ -129,6 +131,13 @@ mod macos_build {
PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
println!("cargo:rerun-if-changed={}", shader_path);
// The metal compiler records the resolved absolute path of its input
// unconditionally. Compile a copy staged in OUT_DIR so the recorded
// location is the build's canonical output directory, never the
// checkout (corgi rejects artifacts that embed the build path).
let staged_shader_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metal");
std::fs::copy(shader_path, &staged_shader_path).unwrap();
let output = Command::new("xcrun")
.args([
"-sdk",
@@ -138,11 +147,9 @@ mod macos_build {
"-mmacosx-version-min=10.15.7",
"-MO",
"-c",
shader_path,
"-include",
(header_path.to_str().unwrap()),
"-o",
])
.arg(&staged_shader_path)
.args(["-include", header_path.to_str().unwrap(), "-o"])
.arg(&air_output_path)
.output()
.unwrap();
@@ -157,7 +164,7 @@ mod macos_build {
let output = Command::new("xcrun")
.args(["-sdk", "macosx", "metallib"])
.arg(air_output_path)
.arg(&air_output_path)
.arg("-o")
.arg(metallib_output_path)
.output()
@@ -170,5 +177,11 @@ mod macos_build {
);
process::exit(1);
}
// The .air intermediate records the compiler's working directory in
// its debug info; the metallib built from it does not. Nothing reads
// the .air after this point, so drop it rather than leave a
// checkout-path-bearing file in OUT_DIR.
std::fs::remove_file(&air_output_path).unwrap();
}
}
+1
View File
@@ -15,6 +15,7 @@ anyhow.workspace = true
language_core.workspace = true
rust-embed.workspace = true
toml.workspace = true
util.workspace = true
tree-sitter = { workspace = true, optional = true }
tree-sitter-bash = { workspace = true, optional = true }
+8 -5
View File
@@ -2,12 +2,15 @@ use std::borrow::Cow;
use anyhow::Context as _;
use language_core::{LanguageConfig, LanguageQueries, QueryFile, QueryFileContents};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "src/"]
#[exclude = "*.rs"]
struct GrammarDir;
// Dev builds read the checkout's query files at runtime instead of embedding
// them; see the `assets` crate for the rationale.
util::fs_embed! {
struct GrammarDir,
crate_relative = "src/",
root_relative = "crates/grammars/src",
exclude = ["*.rs"],
}
/// Register all built-in native tree-sitter grammars with the provided registration function.
///
-20
View File
@@ -1,20 +0,0 @@
fn main() {
let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let mut path = std::path::PathBuf::from(&cargo_manifest_dir);
if path.file_name().as_ref().and_then(|name| name.to_str()) != Some("inspector_ui") {
panic!(
"expected CARGO_MANIFEST_DIR to end with crates/inspector_ui, but got {cargo_manifest_dir}"
);
}
path.pop();
if path.file_name().as_ref().and_then(|name| name.to_str()) != Some("crates") {
panic!(
"expected CARGO_MANIFEST_DIR to end with crates/inspector_ui, but got {cargo_manifest_dir}"
);
}
path.pop();
println!("cargo:rustc-env=ZED_REPO_DIR={}", path.display());
}
+8 -5
View File
@@ -109,10 +109,11 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
let source_location = inspector_id.path.source_location;
// For unknown reasons, for some elements the path is absolute.
let source_location_string = source_location.to_string();
let source_location_string = source_location_string
.strip_prefix(env!("ZED_REPO_DIR"))
.and_then(|s| s.strip_prefix("/"))
.map(|s| s.to_string())
// Resolve the checkout at runtime rather than baking it in at build time
// (which corgi rejects, and which would be wrong in any other worktree).
let source_location_string = util::dev_repo_root()
.and_then(|root| Path::new(&source_location_string).strip_prefix(root).ok())
.map(|path| path.display().to_string())
.unwrap_or(source_location_string);
v_flex()
@@ -160,7 +161,9 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
async fn open_zed_source_location(
location: &'static std::panic::Location<'static>,
) -> anyhow::Result<()> {
let mut path = Path::new(env!("ZED_REPO_DIR")).to_path_buf();
let mut path = util::dev_repo_root()
.context("locating the zed checkout to open sources from")?
.to_path_buf();
path.push(Path::new(location.file()));
let path_arg = format!(
"{}:{}:{}",
+10 -4
View File
@@ -246,7 +246,6 @@ async fn build_remote_server_from_source(
cx: &mut AsyncApp,
) -> Result<Option<std::path::PathBuf>> {
use std::env::VarError;
use std::path::Path;
use util::command::{Command, Stdio, new_command};
if let Ok(path) = std::env::var("ZED_COPY_REMOTE_SERVER") {
@@ -328,7 +327,10 @@ async fn build_remote_server_from_source(
log::info!("building remote server binary from source");
run_cmd(
new_command("cargo")
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.current_dir(
util::dev_repo_root()
.context("locating the zed checkout to build remote_server from source")?,
)
.args([
"build",
"--package",
@@ -374,7 +376,10 @@ async fn build_remote_server_from_source(
log::info!("building remote binary from source for {triple} with Zig");
run_cmd(
new_command("cargo")
.current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.current_dir(
util::dev_repo_root()
.context("locating the zed checkout to build remote_server from source")?,
)
.args([
"zigbuild",
"--package",
@@ -390,7 +395,8 @@ async fn build_remote_server_from_source(
)
.await?;
};
let bin_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
let bin_path = util::dev_repo_root()
.context("locating the zed checkout that built remote_server from source")?
.join("target")
.join("remote_server")
.join(&triple)
-1
View File
@@ -27,7 +27,6 @@ log.workspace = true
migrator.workspace = true
paths.workspace = true
release_channel.workspace = true
rust-embed.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
+9 -7
View File
@@ -26,7 +26,6 @@ pub mod private {
use gpui::{App, Global};
use rust_embed::RustEmbed;
use std::env;
use std::{borrow::Cow, fmt, str};
use util::asset_str;
@@ -117,12 +116,15 @@ impl fmt::Display for WorktreeId {
}
}
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "settings/*"]
#[include = "keymaps/*"]
#[exclude = "*.DS_Store"]
pub struct SettingsAssets;
// Dev builds read the checkout's files at runtime instead of embedding them;
// see the `assets` crate for the rationale.
util::fs_embed! {
pub struct SettingsAssets,
crate_relative = "../../assets",
root_relative = "assets",
include = ["settings/*", "keymaps/*"],
exclude = ["*.DS_Store"],
}
pub fn init(cx: &mut App) {
let settings = SettingsStore::new(cx, &default_settings());
+190
View File
@@ -634,6 +634,196 @@ pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
}
}
/// The checkout that produced this binary, resolved at runtime by walking up to
/// the first ancestor that contains a `.git` entry (a directory in a normal
/// clone, a file in a git worktree or submodule). Cargo and corgi both place
/// built binaries under `<repo>/target/<profile>/`, so the repository root is
/// always an ancestor of the executable.
///
/// Dev-only affordances use this instead of baking a build-time path into the
/// artifact: such a path points at the wrong checkout from any other worktree
/// and, under corgi, is rejected because artifacts must be checkout-independent
/// to be shared across worktrees.
///
/// The executable's launch path is tried first, then its canonical form, then
/// the working directory. In CI, `target/` (or the checkout root) can be a
/// symlink onto another volume, so canonicalizing the executable alone can walk
/// off the checkout and miss `.git`; the launch path and the test runner's cwd
/// (a crate dir under the checkout) stay inside it.
pub fn dev_repo_root() -> Option<&'static std::path::Path> {
use std::path::PathBuf;
static ROOT: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new();
ROOT.get_or_init(|| {
let exe = std::env::current_exe().ok();
let candidates = [
exe.clone(),
exe.and_then(|exe| exe.canonicalize().ok()),
std::env::current_dir().ok(),
];
candidates.into_iter().flatten().find_map(|start| {
Some(
start
.ancestors()
.find(|dir| dir.join(".git").exists())?
.to_path_buf(),
)
})
})
.as_deref()
}
/// Re-exports that back [`fs_embed!`] so a caller only needs to depend on `util`,
/// not on `rust_embed` directly (the macro's dependency is an implementation
/// detail). Hidden from the public API.
#[doc(hidden)]
pub mod __rust_embed {
pub use rust_embed::{EmbeddedFile, Filenames, Metadata, RustEmbed, utils};
}
/// Backs the dev arm of [`fs_embed!`]'s `iter`: every file under the root-relative
/// directory that passes the same rust_embed include/exclude globs the
/// release derive uses, so the dev and release file sets are identical. Reuses
/// rust_embed's own matcher rather than reimplementing glob semantics.
#[cfg(debug_assertions)]
#[doc(hidden)]
pub fn __fs_embed_iter(
root_relative: &str,
includes: &[&str],
excludes: &[&str],
) -> rust_embed::Filenames {
let Some(root) = dev_repo_root().map(|root| root.join(root_relative)) else {
return rust_embed::Filenames::Dynamic(Box::new(std::iter::empty::<
std::borrow::Cow<'static, str>,
>()));
};
let matcher = rust_embed::utils::PathMatcher::new(includes, excludes);
let names: Vec<std::borrow::Cow<'static, str>> =
rust_embed::utils::get_files(root.to_string_lossy().into_owned(), matcher)
.map(|entry| std::borrow::Cow::Owned(entry.rel_path))
.collect();
rust_embed::Filenames::Dynamic(Box::new(names.into_iter()))
}
/// Backs the dev arm of [`fs_embed!`]'s `get`: reads a single file from the
/// checkout, returning `None` when the include/exclude globs filter it out so
/// `get` matches release's embedded set exactly.
#[cfg(debug_assertions)]
#[doc(hidden)]
pub fn __fs_embed_get(
root_relative: &str,
file_path: &str,
includes: &[&str],
excludes: &[&str],
) -> Option<rust_embed::EmbeddedFile> {
let matcher = rust_embed::utils::PathMatcher::new(includes, excludes);
if !matcher.is_path_included(file_path) {
return None;
}
let root = dev_repo_root()
.expect("dev asset loading requires running from within the checkout")
.join(root_relative);
rust_embed::utils::read_file_from_fs(&root.join(file_path)).ok()
}
/// A `rust_embed` asset source that embeds files in release builds and reads them
/// from the checkout at runtime in dev builds (edits show up on the next launch
/// with no rebuild). One invocation replaces the previous pairing of a
/// `#[cfg(not(debug_assertions))] #[derive(RustEmbed)]` struct with a separate
/// dev macro, and keeps a single source of truth for the include/exclude globs.
///
/// It expands to both arms:
/// * Release (`not(debug_assertions)`): `#[derive(RustEmbed)]` embedding
/// `crate_relative` at build time, with the given `include`/`exclude` globs.
/// * Dev (`debug_assertions`): a runtime filesystem source rooted at
/// `root_relative`, applying those same globs through rust_embed's own matcher.
///
/// Two paths are required because the arms resolve from different bases: the
/// release derive reads `crate_relative` relative to the crate's `Cargo.toml`
/// at build time (rust_embed's rule), while the dev arm resolves `root_relative`
/// relative to the repository root at runtime via [`dev_repo_root`]. Baking the
/// build-time path into the dev artifact would point at the wrong checkout from
/// another worktree and is rejected by corgi, whose sandbox requires
/// checkout-independent output.
///
/// ```ignore
/// util::fs_embed! {
/// pub struct Assets,
/// crate_relative = "../../assets",
/// root_relative = "assets",
/// include = ["fonts/**/*", "themes/**/*", "*.md"],
/// exclude = ["themes/src/*", "*.DS_Store"],
/// }
/// ```
#[macro_export]
macro_rules! fs_embed {
(
$vis:vis struct $name:ident,
crate_relative = $crate_relative:literal,
root_relative = $root_relative:literal
$(, include = [$($include:literal),* $(,)?])?
$(, exclude = [$($exclude:literal),* $(,)?])?
$(,)?
) => {
// `crate_path` points the derive's generated code at util's re-export so
// the caller needs no direct `rust_embed` dependency.
#[cfg(not(debug_assertions))]
#[derive($crate::__rust_embed::RustEmbed)]
#[crate_path = "::util::__rust_embed"]
#[folder = $crate_relative]
$($(#[include = $include])*)?
$($(#[exclude = $exclude])*)?
$vis struct $name;
#[cfg(debug_assertions)]
$vis struct $name;
// Mirror the derive's public surface: inherent `get`/`iter` (callable
// without the trait in scope) plus the trait impl (for generic bounds
// like `util::asset_str` and `handlebars::register_embed_templates`), so
// the two arms are interchangeable at call sites.
#[cfg(debug_assertions)]
impl $name {
pub fn get(
file_path: &str,
) -> ::core::option::Option<$crate::__rust_embed::EmbeddedFile> {
$crate::__fs_embed_get(
$root_relative,
file_path,
&[$($($include),*)?],
&[$($($exclude),*)?],
)
}
pub fn iter(
) -> impl ::core::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> + 'static
{
$crate::__fs_embed_iter(
$root_relative,
&[$($($include),*)?],
&[$($($exclude),*)?],
)
}
}
#[cfg(debug_assertions)]
impl $crate::__rust_embed::RustEmbed for $name {
fn get(
file_path: &str,
) -> ::core::option::Option<$crate::__rust_embed::EmbeddedFile> {
<$name>::get(file_path)
}
fn iter() -> $crate::__rust_embed::Filenames {
$crate::__fs_embed_iter(
$root_relative,
&[$($($include),*)?],
&[$($($exclude),*)?],
)
}
}
};
}
pub trait RangeExt<T> {
fn sorted(&self) -> Self;
fn to_inclusive(&self) -> RangeInclusive<T>;