util: Embed fs_embed assets with debug-embed (#63583)

Zed's `fs_embed!` macro normally reads assets from the checkout in debug
builds. The new `util/debug-embed` feature is intended to compile those
assets into debug binaries, but it previously changed only the
`rust_embed::Filenames` variant while leaving file loading on the
runtime path.

That breaks downstream workspaces such as Delta: the runtime loader
identifies Delta as the repository root, then looks for Zed grammar
assets under Delta's checkout. Tests consequently panic when language
configuration files cannot be found.

This makes `util/debug-embed` select a compile-time `RustEmbed`
expansion in every profile. Builds without the feature retain live
filesystem loading in debug mode. The runtime iterator no longer depends
on a feature-specific `rust_embed::Filenames` variant, so Cargo feature
unification remains safe when another package enables
`rust-embed/debug-embed` directly. The regression fixture uses a
deliberately invalid runtime path when `debug-embed` is enabled, so it
proves that the embedded path is used rather than passing only because
the Zed checkout contains the test assets.

Testing performed:

- `cargo fmt --all -- --check`
- `cargo nextest run -p util --lib`
- `cargo nextest run -p util --lib --features debug-embed`
- `cargo check -p remote_server --features debug-embed`
- `./script/clippy -p util`
- Delta's previously failing
`tools::edit_file::tests::test_edit_file_rejects_scratch_paths_as_read_only`
against this PR's head commit

Release Notes:

- N/A
This commit is contained in:
Jakub Konka authored and GitHub committed 2026-09-01 21:42:01 +00:00
1 parent 810f8313f1
commit a13d872bff
2 files changed
+81 -96

No files matched your search

+1 -5
View File
@@ -16,11 +16,7 @@ doctest = true
[features]
test-support = ["rand", "util_macros", "path/test-support"]
# Embed `fs_embed!` assets at compile time even in debug builds. Workspaces
# that want rust-embed's `debug-embed` behavior must enable it through this
# feature rather than on `rust-embed` directly: the feature changes which
# variants of `rust_embed::Filenames` exist, and `fs_embed!`'s dev arm can
# only construct the right one when this crate sees the same feature state.
# Embed `fs_embed!` assets at compile time even in debug builds.
debug-embed = ["rust-embed/debug-embed"]
[dependencies]
+80 -91
View File
@@ -1,3 +1,6 @@
#[cfg(test)]
extern crate self as util;
#[cfg(not(target_family = "wasm"))]
pub mod archive;
#[cfg(not(target_family = "wasm"))]
@@ -684,65 +687,19 @@ pub mod __rust_embed {
/// 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.
///
/// `rust_embed::Filenames` has exactly one variant per compilation context:
/// `Dynamic` without the `debug-embed` feature, `Embedded` with it. Cargo
/// unifies features across the whole build graph, so a consumer workspace
/// enabling `debug-embed` changes the enum this crate sees; each variant
/// below compiles only in the context where its `Filenames` variant exists.
#[cfg(all(debug_assertions, not(feature = "debug-embed")))]
#[doc(hidden)]
pub fn __fs_embed_iter(
root_relative: &str,
includes: &[&str],
excludes: &[&str],
) -> rust_embed::Filenames {
let names: Vec<std::borrow::Cow<'static, str>> =
fs_embed_file_names(root_relative, includes, excludes)
.into_iter()
.map(std::borrow::Cow::Owned)
.collect();
rust_embed::Filenames::Dynamic(Box::new(names.into_iter()))
) -> impl Iterator<Item = std::borrow::Cow<'static, str>> + 'static {
fs_embed_file_names(root_relative, includes, excludes)
.into_iter()
.map(std::borrow::Cow::Owned)
}
/// The `debug-embed` arm of [`fs_embed!`]'s dev `iter`. The `Embedded`
/// variant requires `'static` names, so the checkout is scanned once per
/// embed site and the resulting name list is leaked and cached. The list
/// staying fixed for the life of the process matches the macro's contract
/// that dev edits show up on the next launch; file contents still come from
/// `get`, which reads the filesystem on every call.
#[cfg(all(debug_assertions, feature = "debug-embed"))]
#[doc(hidden)]
pub fn __fs_embed_iter(
root_relative: &str,
includes: &[&str],
excludes: &[&str],
) -> rust_embed::Filenames {
use std::collections::BTreeMap;
use std::sync::Mutex;
/// Keyed by root and globs: distinct embed sites may share a root with
/// different filters.
type EmbedSiteKey = (String, Vec<String>, Vec<String>);
static LEAKED_NAMES: Mutex<BTreeMap<EmbedSiteKey, &'static [&'static str]>> =
Mutex::new(BTreeMap::new());
let key = (
root_relative.to_string(),
includes.iter().map(|glob| glob.to_string()).collect(),
excludes.iter().map(|glob| glob.to_string()).collect(),
);
let names = *LEAKED_NAMES.lock().unwrap().entry(key).or_insert_with(|| {
let names: Vec<&'static str> = fs_embed_file_names(root_relative, includes, excludes)
.into_iter()
.map(|name| &*name.leak())
.collect();
names.leak()
});
rust_embed::Filenames::Embedded(names.iter())
}
#[cfg(debug_assertions)]
#[cfg(all(debug_assertions, not(feature = "debug-embed")))]
fn fs_embed_file_names(root_relative: &str, includes: &[&str], excludes: &[&str]) -> Vec<String> {
let Some(root) = dev_repo_root().map(|root| root.join(root_relative)) else {
return Vec::new();
@@ -756,7 +713,7 @@ fn fs_embed_file_names(root_relative: &str, includes: &[&str], excludes: &[&str]
/// 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)]
#[cfg(all(debug_assertions, not(feature = "debug-embed")))]
#[doc(hidden)]
pub fn __fs_embed_get(
root_relative: &str,
@@ -774,37 +731,31 @@ pub fn __fs_embed_get(
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"],
/// }
/// ```
#[cfg(feature = "debug-embed")]
#[doc(hidden)]
#[macro_export]
macro_rules! fs_embed {
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),* $(,)?])?
$(,)?
) => {
#[derive($crate::__rust_embed::RustEmbed)]
#[crate_path = "::util::__rust_embed"]
#[folder = $crate_relative]
$($(#[include = $include])*)?
$($(#[exclude = $exclude])*)?
$vis struct $name;
};
}
#[cfg(not(feature = "debug-embed"))]
#[doc(hidden)]
#[macro_export]
macro_rules! __fs_embed {
(
$vis:vis struct $name:ident,
crate_relative = $crate_relative:literal,
@@ -875,6 +826,40 @@ macro_rules! fs_embed {
};
}
/// A `rust_embed` asset source that embeds files in release builds. Dev builds
/// read from the checkout at runtime unless the `debug-embed` feature is enabled,
/// in which case they embed the files too.
///
/// It expands to one of these arms:
/// * Release (`not(debug_assertions)`): `#[derive(RustEmbed)]` embedding
/// `crate_relative` at build time, with the given `include`/`exclude` globs.
/// * Dev with `debug-embed`: the same compile-time embedding as release.
/// * Dev without `debug-embed`: a runtime filesystem source rooted at
/// `root_relative`; edits appear on the next launch without a rebuild.
///
/// Two paths are required because the arms resolve from different bases: the
/// derive reads `crate_relative` relative to the crate's `Cargo.toml`, while the
/// runtime dev arm resolves `root_relative` relative to the repository root via
/// [`dev_repo_root`]. Baking the build-time path into that arm 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 {
($($tokens:tt)*) => {
$crate::__fs_embed!($($tokens)*);
};
}
pub trait RangeExt<T> {
fn sorted(&self) -> Self;
fn to_inclusive(&self) -> RangeInclusive<T>;
@@ -1036,13 +1021,8 @@ impl<O> From<anyhow::Result<O>> for ConnectionResult<O> {
mod tests {
use super::*;
/// Exercises `fs_embed!`'s dev arm, whose `RustEmbed::iter` implementation
/// must construct whichever `rust_embed::Filenames` variant the current
/// feature state provides. Running util's tests with and without the
/// `debug-embed` feature covers both variants.
#[cfg(debug_assertions)]
#[test]
fn test_fs_embed_dev_arm_iter_and_get() {
fn test_fs_embed_iter_and_get() {
let inherent_names: Vec<_> = FsEmbedTestAssets::iter().collect();
assert!(
inherent_names.iter().any(|name| name == "util.rs"),
@@ -1070,9 +1050,7 @@ mod tests {
);
}
// Dev arm only: the release arm's derive names this crate as
// `::util`, which does not resolve from util's own unit tests.
#[cfg(debug_assertions)]
#[cfg(not(feature = "debug-embed"))]
crate::fs_embed! {
struct FsEmbedTestAssets,
crate_relative = "src",
@@ -1081,6 +1059,17 @@ mod tests {
exclude = ["test/**/*"],
}
// A consuming workspace does not contain a git dependency's files at this
// repository-relative path, so `debug-embed` must not consult it.
#[cfg(feature = "debug-embed")]
crate::fs_embed! {
struct FsEmbedTestAssets,
crate_relative = "src",
root_relative = "this-path-must-not-be-read",
include = ["*.rs"],
exclude = ["test/**/*"],
}
#[test]
fn test_parse_os_release() {
let os_release =