From ee6badf4b096b60da1e4725a6c9ddb10392c0487 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 31 Aug 2026 17:37:00 +0000 Subject: [PATCH] Support building with corgi (#63396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [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 --- Cargo.lock | 8 +- Cargo.toml | 12 ++ corgi-patches/scratch/Cargo.toml | 9 + corgi-patches/scratch/LICENSE-APACHE | 1 + corgi-patches/scratch/build.rs | 8 + corgi-patches/scratch/src/lib.rs | 19 ++ corgi.toml | 83 ++++++++ crates/agent/Cargo.toml | 1 - crates/agent/src/templates.rs | 13 +- crates/assets/Cargo.toml | 2 +- crates/assets/src/assets.rs | 32 +-- crates/edit_prediction_cli/Cargo.toml | 1 - .../src/filter_languages.rs | 19 +- .../edit_prediction_cli/src/prompt_assets.rs | 11 +- crates/gpui/src/gpui.rs | 2 - crates/gpui_apple/Cargo.toml | 1 - crates/gpui_apple/build.rs | 27 ++- crates/grammars/Cargo.toml | 1 + crates/grammars/src/grammars.rs | 13 +- crates/inspector_ui/build.rs | 20 -- crates/inspector_ui/src/inspector.rs | 13 +- crates/remote/src/transport.rs | 14 +- crates/settings/Cargo.toml | 1 - crates/settings/src/settings.rs | 16 +- crates/util/src/util.rs | 190 ++++++++++++++++++ 25 files changed, 422 insertions(+), 95 deletions(-) create mode 100644 corgi-patches/scratch/Cargo.toml create mode 120000 corgi-patches/scratch/LICENSE-APACHE create mode 100644 corgi-patches/scratch/build.rs create mode 100644 corgi-patches/scratch/src/lib.rs create mode 100644 corgi.toml delete mode 100644 crates/inspector_ui/build.rs diff --git a/Cargo.lock b/Cargo.lock index 638abc4b9a..c63f256a80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 65e6f16477..6e4907333e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/corgi-patches/scratch/Cargo.toml b/corgi-patches/scratch/Cargo.toml new file mode 100644 index 0000000000..26ea3f872e --- /dev/null +++ b/corgi-patches/scratch/Cargo.toml @@ -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" diff --git a/corgi-patches/scratch/LICENSE-APACHE b/corgi-patches/scratch/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/corgi-patches/scratch/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/corgi-patches/scratch/build.rs b/corgi-patches/scratch/build.rs new file mode 100644 index 0000000000..af80ec2a34 --- /dev/null +++ b/corgi-patches/scratch/build.rs @@ -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); +} diff --git a/corgi-patches/scratch/src/lib.rs b/corgi-patches/scratch/src/lib.rs new file mode 100644 index 0000000000..40bb008e3e --- /dev/null +++ b/corgi-patches/scratch/src/lib.rs @@ -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 +} diff --git a/corgi.toml b/corgi.toml new file mode 100644 index 0000000000..1245308a63 --- /dev/null +++ b/corgi.toml @@ -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"] diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index f9b9671c82..62d6f271c4 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -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 diff --git a/crates/agent/src/templates.rs b/crates/agent/src/templates.rs index 9384d07a2f..d5799ecb19 100644 --- a/crates/agent/src/templates.rs +++ b/crates/agent/src/templates.rs @@ -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>); diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml index a56cd109f1..475b6d457d 100644 --- a/crates/assets/Cargo.toml +++ b/crates/assets/Cargo.toml @@ -14,4 +14,4 @@ workspace = true [dependencies] anyhow.workspace = true gpui.workspace = true -rust-embed.workspace = true +util.workspace = true diff --git a/crates/assets/src/assets.rs b/crates/assets/src/assets.rs index 5c7e671159..193eb2a0b1 100644 --- a/crates/assets/src/assets.rs +++ b/crates/assets/src/assets.rs @@ -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>> { diff --git a/crates/edit_prediction_cli/Cargo.toml b/crates/edit_prediction_cli/Cargo.toml index 4649e2a83e..6283a276b5 100644 --- a/crates/edit_prediction_cli/Cargo.toml +++ b/crates/edit_prediction_cli/Cargo.toml @@ -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 diff --git a/crates/edit_prediction_cli/src/filter_languages.rs b/crates/edit_prediction_cli/src/filter_languages.rs index 754ca7d1ba..1379f54345 100644 --- a/crates/edit_prediction_cli/src/filter_languages.rs +++ b/crates/edit_prediction_cli/src/filter_languages.rs @@ -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 { 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, diff --git a/crates/edit_prediction_cli/src/prompt_assets.rs b/crates/edit_prediction_cli/src/prompt_assets.rs index cc3497a612..abbcaa00c2 100644 --- a/crates/edit_prediction_cli/src/prompt_assets.rs +++ b/crates/edit_prediction_cli/src/prompt_assets.rs @@ -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 { diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index b77028146b..51585b5e5e 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -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; diff --git a/crates/gpui_apple/Cargo.toml b/crates/gpui_apple/Cargo.toml index 1d8db9b4f0..fc04bccf64 100644 --- a/crates/gpui_apple/Cargo.toml +++ b/crates/gpui_apple/Cargo.toml @@ -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"] } diff --git a/crates/gpui_apple/build.rs b/crates/gpui_apple/build.rs index aee655fe50..993ba061eb 100644 --- a/crates/gpui_apple/build.rs +++ b/crates/gpui_apple/build.rs @@ -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(); } } diff --git a/crates/grammars/Cargo.toml b/crates/grammars/Cargo.toml index 3cd75c8c2f..00b35ce964 100644 --- a/crates/grammars/Cargo.toml +++ b/crates/grammars/Cargo.toml @@ -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 } diff --git a/crates/grammars/src/grammars.rs b/crates/grammars/src/grammars.rs index b0bdc50b48..426926d0db 100644 --- a/crates/grammars/src/grammars.rs +++ b/crates/grammars/src/grammars.rs @@ -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. /// diff --git a/crates/inspector_ui/build.rs b/crates/inspector_ui/build.rs deleted file mode 100644 index e7d393b5ef..0000000000 --- a/crates/inspector_ui/build.rs +++ /dev/null @@ -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()); -} diff --git a/crates/inspector_ui/src/inspector.rs b/crates/inspector_ui/src/inspector.rs index 36eed3bc72..84f3d2195d 100644 --- a/crates/inspector_ui/src/inspector.rs +++ b/crates/inspector_ui/src/inspector.rs @@ -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!( "{}:{}:{}", diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs index 43007c0473..133fbecdf8 100644 --- a/crates/remote/src/transport.rs +++ b/crates/remote/src/transport.rs @@ -246,7 +246,6 @@ async fn build_remote_server_from_source( cx: &mut AsyncApp, ) -> Result> { 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) diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index b4c54af17d..0cebdf801c 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -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 diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index b5daebdae0..b0d5237246 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -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()); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 6ab84f029e..36aeb20f2a 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -634,6 +634,196 @@ pub fn asset_str(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 `/target//`, 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> = 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> = + 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 { + 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> + '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 { fn sorted(&self) -> Self; fn to_inclusive(&self) -> RangeInclusive;