bevy_asset/io/embedded/
mod.rs

1#[cfg(feature = "embedded_watcher")]
2mod embedded_watcher;
3
4#[cfg(feature = "embedded_watcher")]
5pub use embedded_watcher::*;
6
7use crate::io::{
8    memory::{Dir, MemoryAssetReader, Value},
9    AssetSource, AssetSourceBuilders,
10};
11use bevy_ecs::system::Resource;
12use std::path::{Path, PathBuf};
13
14pub const EMBEDDED: &str = "embedded";
15
16/// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended
17/// to be shared with a [`MemoryAssetReader`].
18/// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this.
19///
20/// [`embedded_asset`]: crate::embedded_asset
21#[derive(Resource, Default)]
22pub struct EmbeddedAssetRegistry {
23    dir: Dir,
24    #[cfg(feature = "embedded_watcher")]
25    root_paths: std::sync::Arc<parking_lot::RwLock<bevy_utils::HashMap<Box<Path>, PathBuf>>>,
26}
27
28impl EmbeddedAssetRegistry {
29    /// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
30    /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
31    /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
32    /// or a [`Vec<u8>`].
33    #[allow(unused)]
34    pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into<Value>) {
35        #[cfg(feature = "embedded_watcher")]
36        self.root_paths
37            .write()
38            .insert(full_path.into(), asset_path.to_owned());
39        self.dir.insert_asset(asset_path, value);
40    }
41
42    /// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
43    /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
44    /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
45    /// or a [`Vec<u8>`].
46    #[allow(unused)]
47    pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into<Value>) {
48        #[cfg(feature = "embedded_watcher")]
49        self.root_paths
50            .write()
51            .insert(full_path.into(), asset_path.to_owned());
52        self.dir.insert_meta(asset_path, value);
53    }
54
55    /// Registers a `embedded` [`AssetSource`] that uses this [`EmbeddedAssetRegistry`].
56    // NOTE: unused_mut because embedded_watcher feature is the only mutable consumer of `let mut source`
57    #[allow(unused_mut)]
58    pub fn register_source(&self, sources: &mut AssetSourceBuilders) {
59        let dir = self.dir.clone();
60        let processed_dir = self.dir.clone();
61        let mut source = AssetSource::build()
62            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }))
63            .with_processed_reader(move || {
64                Box::new(MemoryAssetReader {
65                    root: processed_dir.clone(),
66                })
67            })
68            // Note that we only add a processed watch warning because we don't want to warn
69            // noisily about embedded watching (which is niche) when users enable file watching.
70            .with_processed_watch_warning(
71                "Consider enabling the `embedded_watcher` cargo feature.",
72            );
73
74        #[cfg(feature = "embedded_watcher")]
75        {
76            let root_paths = self.root_paths.clone();
77            let dir = self.dir.clone();
78            let processed_root_paths = self.root_paths.clone();
79            let processd_dir = self.dir.clone();
80            source = source
81                .with_watcher(move |sender| {
82                    Some(Box::new(EmbeddedWatcher::new(
83                        dir.clone(),
84                        root_paths.clone(),
85                        sender,
86                        std::time::Duration::from_millis(300),
87                    )))
88                })
89                .with_processed_watcher(move |sender| {
90                    Some(Box::new(EmbeddedWatcher::new(
91                        processd_dir.clone(),
92                        processed_root_paths.clone(),
93                        sender,
94                        std::time::Duration::from_millis(300),
95                    )))
96                });
97        }
98        sources.insert(EMBEDDED, source);
99    }
100}
101
102/// Returns the [`Path`] for a given `embedded` asset.
103/// This is used internally by [`embedded_asset`] and can be used to get a [`Path`]
104/// that matches the [`AssetPath`](crate::AssetPath) used by that asset.
105///
106/// [`embedded_asset`]: crate::embedded_asset
107#[macro_export]
108macro_rules! embedded_path {
109    ($path_str: expr) => {{
110        embedded_path!("src", $path_str)
111    }};
112
113    ($source_path: expr, $path_str: expr) => {{
114        let crate_name = module_path!().split(':').next().unwrap();
115        $crate::io::embedded::_embedded_asset_path(
116            crate_name,
117            $source_path.as_ref(),
118            file!().as_ref(),
119            $path_str.as_ref(),
120        )
121    }};
122}
123
124/// Implementation detail of `embedded_path`, do not use this!
125///
126/// Returns an embedded asset path, given:
127///   - `crate_name`: name of the crate where the asset is embedded
128///   - `src_prefix`: path prefix of the crate's source directory, relative to the workspace root
129///   - `file_path`: `std::file!()` path of the source file where `embedded_path!` is called
130///   - `asset_path`: path of the embedded asset relative to `file_path`
131#[doc(hidden)]
132pub fn _embedded_asset_path(
133    crate_name: &str,
134    src_prefix: &Path,
135    file_path: &Path,
136    asset_path: &Path,
137) -> PathBuf {
138    let mut maybe_parent = file_path.parent();
139    let after_src = loop {
140        let Some(parent) = maybe_parent else {
141            panic!("Failed to find src_prefix {src_prefix:?} in {file_path:?}")
142        };
143        if parent.ends_with(src_prefix) {
144            break file_path.strip_prefix(parent).unwrap();
145        }
146        maybe_parent = parent.parent();
147    };
148    let asset_path = after_src.parent().unwrap().join(asset_path);
149    Path::new(crate_name).join(asset_path)
150}
151
152/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary
153/// and registering those bytes with the `embedded` [`AssetSource`].
154///
155/// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second.
156///
157/// By default this will generate an [`AssetPath`] using the following rules:
158///
159/// 1. Search for the first `$crate_name/src/` in the path and trim to the path past that point.
160/// 2. Re-add the current `$crate_name` to the front of the path
161///
162/// For example, consider the following file structure in the theoretical `bevy_rock` crate, which provides a Bevy [`Plugin`](bevy_app::Plugin)
163/// that renders fancy rocks for scenes.
164///
165/// ```text
166/// bevy_rock
167/// ├── src
168/// │   ├── render
169/// │   │   ├── rock.wgsl
170/// │   │   └── mod.rs
171/// │   └── lib.rs
172/// └── Cargo.toml
173/// ```
174///
175/// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following
176/// in `bevy_rock/src/render/mod.rs`:
177///
178/// `embedded_asset!(app, "rock.wgsl")`
179///
180/// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path:
181///
182/// ```no_run
183/// # use bevy_asset::{Asset, AssetServer};
184/// # use bevy_reflect::TypePath;
185/// # let asset_server: AssetServer = panic!();
186/// # #[derive(Asset, TypePath)]
187/// # struct Shader;
188/// let shader = asset_server.load::<Shader>("embedded://bevy_rock/render/rock.wgsl");
189/// ```
190///
191/// Some things to note in the path:
192/// 1. The non-default `embedded://` [`AssetSource`]
193/// 2. `src` is trimmed from the path
194///
195/// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in
196/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`embedded_asset`] searches for the
197/// _first instance_ of `bevy_rock/src` in the path.
198///
199/// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples),
200/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`embedded_asset`]:
201///
202/// `embedded_asset!(app, "/examples/rock_stuff/", "rock.wgsl")`
203///
204/// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are
205/// equivalent:
206///
207/// `embedded_asset!(app, "rock.wgsl")`
208/// `embedded_asset!(app, "/src/", "rock.wgsl")`
209///
210/// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes.
211/// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't
212/// available for some reason, you can use the [`embedded_path`] macro to debug.
213///
214/// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature.
215///
216/// [`AssetPath`]: crate::AssetPath
217/// [`embedded_asset`]: crate::embedded_asset
218/// [`embedded_path`]: crate::embedded_path
219#[macro_export]
220macro_rules! embedded_asset {
221    ($app: ident, $path: expr) => {{
222        $crate::embedded_asset!($app, "src", $path)
223    }};
224
225    ($app: ident, $source_path: expr, $path: expr) => {{
226        let mut embedded = $app
227            .world_mut()
228            .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>();
229        let path = $crate::embedded_path!($source_path, $path);
230        let watched_path = $crate::io::embedded::watched_path(file!(), $path);
231        embedded.insert_asset(watched_path, &path, include_bytes!($path));
232    }};
233}
234
235/// Returns the path used by the watcher.
236#[doc(hidden)]
237#[cfg(feature = "embedded_watcher")]
238pub fn watched_path(source_file_path: &'static str, asset_path: &'static str) -> PathBuf {
239    PathBuf::from(source_file_path)
240        .parent()
241        .unwrap()
242        .join(asset_path)
243}
244
245/// Returns an empty PathBuf.
246#[doc(hidden)]
247#[cfg(not(feature = "embedded_watcher"))]
248pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) -> PathBuf {
249    PathBuf::from("")
250}
251
252/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle.
253#[macro_export]
254macro_rules! load_internal_asset {
255    ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
256        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
257        assets.insert($handle.id(), ($loader)(
258            include_str!($path_str),
259            std::path::Path::new(file!())
260                .parent()
261                .unwrap()
262                .join($path_str)
263                .to_string_lossy()
264        ));
265    }};
266    // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded
267    ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{
268        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
269        assets.insert($handle.id(), ($loader)(
270            include_str!($path_str),
271            std::path::Path::new(file!())
272                .parent()
273                .unwrap()
274                .join($path_str)
275                .to_string_lossy(),
276            $($param),+
277        ));
278    }};
279}
280
281/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle.
282#[macro_export]
283macro_rules! load_internal_binary_asset {
284    ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
285        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
286        assets.insert(
287            $handle.id(),
288            ($loader)(
289                include_bytes!($path_str).as_ref(),
290                std::path::Path::new(file!())
291                    .parent()
292                    .unwrap()
293                    .join($path_str)
294                    .to_string_lossy()
295                    .into(),
296            ),
297        );
298    }};
299}
300
301#[cfg(test)]
302mod tests {
303    use super::_embedded_asset_path;
304    use std::path::Path;
305
306    // Relative paths show up if this macro is being invoked by a local crate.
307    // In this case we know the relative path is a sub- path of the workspace
308    // root.
309
310    #[test]
311    fn embedded_asset_path_from_local_crate() {
312        let asset_path = _embedded_asset_path(
313            "my_crate",
314            "src".as_ref(),
315            "src/foo/plugin.rs".as_ref(),
316            "the/asset.png".as_ref(),
317        );
318        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
319    }
320
321    // A blank src_path removes the embedded's file path altogether only the
322    // asset path remains.
323    #[test]
324    fn embedded_asset_path_from_local_crate_blank_src_path_questionable() {
325        let asset_path = _embedded_asset_path(
326            "my_crate",
327            "".as_ref(),
328            "src/foo/some/deep/path/plugin.rs".as_ref(),
329            "the/asset.png".as_ref(),
330        );
331        assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
332    }
333
334    #[test]
335    #[should_panic(expected = "Failed to find src_prefix \"NOT-THERE\" in \"src")]
336    fn embedded_asset_path_from_local_crate_bad_src() {
337        let _asset_path = _embedded_asset_path(
338            "my_crate",
339            "NOT-THERE".as_ref(),
340            "src/foo/plugin.rs".as_ref(),
341            "the/asset.png".as_ref(),
342        );
343    }
344
345    #[test]
346    fn embedded_asset_path_from_local_example_crate() {
347        let asset_path = _embedded_asset_path(
348            "example_name",
349            "examples/foo".as_ref(),
350            "examples/foo/example.rs".as_ref(),
351            "the/asset.png".as_ref(),
352        );
353        assert_eq!(asset_path, Path::new("example_name/the/asset.png"));
354    }
355
356    // Absolute paths show up if this macro is being invoked by an external
357    // dependency, e.g. one that's being checked out from a crates repo or git.
358    #[test]
359    fn embedded_asset_path_from_external_crate() {
360        let asset_path = _embedded_asset_path(
361            "my_crate",
362            "src".as_ref(),
363            "/path/to/crate/src/foo/plugin.rs".as_ref(),
364            "the/asset.png".as_ref(),
365        );
366        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
367    }
368
369    #[test]
370    fn embedded_asset_path_from_external_crate_root_src_path() {
371        let asset_path = _embedded_asset_path(
372            "my_crate",
373            "/path/to/crate/src".as_ref(),
374            "/path/to/crate/src/foo/plugin.rs".as_ref(),
375            "the/asset.png".as_ref(),
376        );
377        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
378    }
379
380    // Although extraneous slashes are permitted at the end, e.g., "src////",
381    // one or more slashes at the beginning are not.
382    #[test]
383    #[should_panic(expected = "Failed to find src_prefix \"////src\" in")]
384    fn embedded_asset_path_from_external_crate_extraneous_beginning_slashes() {
385        let asset_path = _embedded_asset_path(
386            "my_crate",
387            "////src".as_ref(),
388            "/path/to/crate/src/foo/plugin.rs".as_ref(),
389            "the/asset.png".as_ref(),
390        );
391        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
392    }
393
394    // We don't handle this edge case because it is ambiguous with the
395    // information currently available to the embedded_path macro.
396    #[test]
397    fn embedded_asset_path_from_external_crate_is_ambiguous() {
398        let asset_path = _embedded_asset_path(
399            "my_crate",
400            "src".as_ref(),
401            "/path/to/.cargo/registry/src/crate/src/src/plugin.rs".as_ref(),
402            "the/asset.png".as_ref(),
403        );
404        // Really, should be "my_crate/src/the/asset.png"
405        assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
406    }
407}