util: Make fs_embed! compatible with rust-embed's debug-embed feature (#63577)

Zed's `fs_embed!` macro hand-implements rust-embed's `RustEmbed` trait
in its dev arm so debug builds read assets from the checkout at runtime
(introduced with corgi support in #63396). That trait returns
`rust_embed::Filenames`, an enum with exactly one variant per
compilation context: `Dynamic` when rust-embed's `debug-embed` feature
is off, `Embedded` when it is on. The dev arm constructed
`Filenames::Dynamic` unconditionally.

Cargo unifies features across the entire build graph, so a downstream
workspace that consumes Zed's crates by git dependency and enables
`rust-embed/debug-embed` anywhere (Delta did, for self-contained debug
binaries) changes the enum's shape for `util` itself, and `util` stops
compiling with `E0599: no variant named Dynamic`. Zed's own CI can never
catch this because nothing in Zed's graph enables the feature; it only
surfaces in embedding workspaces, where it blocks any dependency bump
past #63396.

This change makes the combination supported. `util` gains a forwarding
`debug-embed` feature, and `__fs_embed_iter` now has two implementations
under cfgs that mirror rust-embed's variant availability: the existing
boxed-iterator path when the feature is off, and an `Embedded` path when
it is on, which scans the checkout once per embed site and leaks the
cached name list (the `Embedded` variant requires `'static` names; a
per-process list matches the macro's documented contract that dev edits
appear on the next launch, and `get` still reads file contents fresh on
every call). Consumers must enable the feature through
`util/debug-embed` rather than directly on rust-embed — enabling it
behind util's back still breaks, which only rust-embed itself could fix;
the feature's documentation says so.

A new test exercises the dev arm's `iter` and `get` through the macro,
and running util's suite with `--features debug-embed` covers the other
variant; both configurations pass 133 tests, plus clippy and fmt.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Mikayla MakiandConrad Irwin authored and GitHub committed 2026-09-01 19:59:31 +00:00
1 parent 283460f5df
commit d3e54c97f4
2 files changed
+112 -10

No files matched your search

+6
View File
@@ -16,6 +16,12 @@ 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.
debug-embed = ["rust-embed/debug-embed"]
[dependencies]
anyhow.workspace = true
+106 -10
View File
@@ -684,26 +684,75 @@ 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.
#[cfg(debug_assertions)]
///
/// `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 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))
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()))
}
/// 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)]
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();
};
let matcher = rust_embed::utils::PathMatcher::new(includes, excludes);
rust_embed::utils::get_files(root.to_string_lossy().into_owned(), matcher)
.map(|entry| entry.rel_path)
.collect()
}
/// 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.
@@ -813,7 +862,9 @@ macro_rules! fs_embed {
<$name>::get(file_path)
}
fn iter() -> $crate::__rust_embed::Filenames {
fn iter(
) -> impl ::core::iter::Iterator<Item = ::std::borrow::Cow<'static, str>> + 'static
{
$crate::__fs_embed_iter(
$root_relative,
&[$($($include),*)?],
@@ -985,6 +1036,51 @@ 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() {
let inherent_names: Vec<_> = FsEmbedTestAssets::iter().collect();
assert!(
inherent_names.iter().any(|name| name == "util.rs"),
"iter should list files matching the include globs, got {inherent_names:?}"
);
assert!(
!inherent_names.iter().any(|name| name.ends_with(".toml")),
"iter should filter files excluded by the globs, got {inherent_names:?}"
);
let trait_names: Vec<_> =
<FsEmbedTestAssets as crate::__rust_embed::RustEmbed>::iter().collect();
assert_eq!(inherent_names, trait_names);
let file = FsEmbedTestAssets::get("util.rs").expect("util.rs should be readable");
assert!(
std::str::from_utf8(&file.data)
.expect("util.rs should be utf-8")
.contains("fs_embed"),
"get should read the real file contents"
);
assert!(
FsEmbedTestAssets::get("Cargo.toml").is_none(),
"get should filter files excluded by the globs"
);
}
// 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)]
crate::fs_embed! {
struct FsEmbedTestAssets,
crate_relative = "src",
root_relative = "crates/util/src",
include = ["*.rs"],
exclude = ["test/**/*"],
}
#[test]
fn test_parse_os_release() {
let os_release =