bevy_asset/io/embedded/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
#[cfg(feature = "embedded_watcher")]
mod embedded_watcher;

#[cfg(feature = "embedded_watcher")]
pub use embedded_watcher::*;

use crate::io::{
    memory::{Dir, MemoryAssetReader, Value},
    AssetSource, AssetSourceBuilders,
};
use bevy_ecs::system::Resource;
use std::path::{Path, PathBuf};

pub const EMBEDDED: &str = "embedded";

/// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended
/// to be shared with a [`MemoryAssetReader`].
/// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this.
///
/// [`embedded_asset`]: crate::embedded_asset
#[derive(Resource, Default)]
pub struct EmbeddedAssetRegistry {
    dir: Dir,
    #[cfg(feature = "embedded_watcher")]
    root_paths: alloc::sync::Arc<parking_lot::RwLock<bevy_utils::HashMap<Box<Path>, PathBuf>>>,
}

impl EmbeddedAssetRegistry {
    /// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
    /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
    /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
    /// or a [`Vec<u8>`].
    #[cfg_attr(
        not(feature = "embedded_watcher"),
        expect(
            unused_variables,
            reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
        )
    )]
    pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into<Value>) {
        #[cfg(feature = "embedded_watcher")]
        self.root_paths
            .write()
            .insert(full_path.into(), asset_path.to_owned());
        self.dir.insert_asset(asset_path, value);
    }

    /// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
    /// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
    /// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
    /// or a [`Vec<u8>`].
    #[cfg_attr(
        not(feature = "embedded_watcher"),
        expect(
            unused_variables,
            reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
        )
    )]
    pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into<Value>) {
        #[cfg(feature = "embedded_watcher")]
        self.root_paths
            .write()
            .insert(full_path.into(), asset_path.to_owned());
        self.dir.insert_meta(asset_path, value);
    }

    /// Removes an asset stored using `full_path` (the full path as [`file`] would return for that file, if it was capable of
    /// running in a non-rust file). If no asset is stored with at `full_path` its a no-op.
    /// It returning `Option` contains the originally stored `Data` or `None`.
    pub fn remove_asset(&self, full_path: &Path) -> Option<super::memory::Data> {
        self.dir.remove_asset(full_path)
    }

    pub fn register_source(&self, sources: &mut AssetSourceBuilders) {
        let dir = self.dir.clone();
        let processed_dir = self.dir.clone();

        #[cfg_attr(
            not(feature = "embedded_watcher"),
            expect(
                unused_mut,
                reason = "Variable is only mutated when `embedded_watcher` feature is enabled."
            )
        )]
        let mut source = AssetSource::build()
            .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }))
            .with_processed_reader(move || {
                Box::new(MemoryAssetReader {
                    root: processed_dir.clone(),
                })
            })
            // Note that we only add a processed watch warning because we don't want to warn
            // noisily about embedded watching (which is niche) when users enable file watching.
            .with_processed_watch_warning(
                "Consider enabling the `embedded_watcher` cargo feature.",
            );

        #[cfg(feature = "embedded_watcher")]
        {
            let root_paths = self.root_paths.clone();
            let dir = self.dir.clone();
            let processed_root_paths = self.root_paths.clone();
            let processed_dir = self.dir.clone();
            source = source
                .with_watcher(move |sender| {
                    Some(Box::new(EmbeddedWatcher::new(
                        dir.clone(),
                        root_paths.clone(),
                        sender,
                        core::time::Duration::from_millis(300),
                    )))
                })
                .with_processed_watcher(move |sender| {
                    Some(Box::new(EmbeddedWatcher::new(
                        processed_dir.clone(),
                        processed_root_paths.clone(),
                        sender,
                        core::time::Duration::from_millis(300),
                    )))
                });
        }
        sources.insert(EMBEDDED, source);
    }
}

/// Returns the [`Path`] for a given `embedded` asset.
/// This is used internally by [`embedded_asset`] and can be used to get a [`Path`]
/// that matches the [`AssetPath`](crate::AssetPath) used by that asset.
///
/// [`embedded_asset`]: crate::embedded_asset
#[macro_export]
macro_rules! embedded_path {
    ($path_str: expr) => {{
        embedded_path!("src", $path_str)
    }};

    ($source_path: expr, $path_str: expr) => {{
        let crate_name = module_path!().split(':').next().unwrap();
        $crate::io::embedded::_embedded_asset_path(
            crate_name,
            $source_path.as_ref(),
            file!().as_ref(),
            $path_str.as_ref(),
        )
    }};
}

/// Implementation detail of `embedded_path`, do not use this!
///
/// Returns an embedded asset path, given:
///   - `crate_name`: name of the crate where the asset is embedded
///   - `src_prefix`: path prefix of the crate's source directory, relative to the workspace root
///   - `file_path`: `std::file!()` path of the source file where `embedded_path!` is called
///   - `asset_path`: path of the embedded asset relative to `file_path`
#[doc(hidden)]
pub fn _embedded_asset_path(
    crate_name: &str,
    src_prefix: &Path,
    file_path: &Path,
    asset_path: &Path,
) -> PathBuf {
    let mut maybe_parent = file_path.parent();
    let after_src = loop {
        let Some(parent) = maybe_parent else {
            panic!("Failed to find src_prefix {src_prefix:?} in {file_path:?}")
        };
        if parent.ends_with(src_prefix) {
            break file_path.strip_prefix(parent).unwrap();
        }
        maybe_parent = parent.parent();
    };
    let asset_path = after_src.parent().unwrap().join(asset_path);
    Path::new(crate_name).join(asset_path)
}

/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary
/// and registering those bytes with the `embedded` [`AssetSource`].
///
/// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second.
///
/// By default this will generate an [`AssetPath`] using the following rules:
///
/// 1. Search for the first `$crate_name/src/` in the path and trim to the path past that point.
/// 2. Re-add the current `$crate_name` to the front of the path
///
/// For example, consider the following file structure in the theoretical `bevy_rock` crate, which provides a Bevy [`Plugin`](bevy_app::Plugin)
/// that renders fancy rocks for scenes.
///
/// ```text
/// bevy_rock
/// ├── src
/// │   ├── render
/// │   │   ├── rock.wgsl
/// │   │   └── mod.rs
/// │   └── lib.rs
/// └── Cargo.toml
/// ```
///
/// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following
/// in `bevy_rock/src/render/mod.rs`:
///
/// `embedded_asset!(app, "rock.wgsl")`
///
/// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path:
///
/// ```no_run
/// # use bevy_asset::{Asset, AssetServer};
/// # use bevy_reflect::TypePath;
/// # let asset_server: AssetServer = panic!();
/// # #[derive(Asset, TypePath)]
/// # struct Shader;
/// let shader = asset_server.load::<Shader>("embedded://bevy_rock/render/rock.wgsl");
/// ```
///
/// Some things to note in the path:
/// 1. The non-default `embedded://` [`AssetSource`]
/// 2. `src` is trimmed from the path
///
/// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in
/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`embedded_asset`] searches for the
/// _first instance_ of `bevy_rock/src` in the path.
///
/// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples),
/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`embedded_asset`]:
///
/// `embedded_asset!(app, "/examples/rock_stuff/", "rock.wgsl")`
///
/// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are
/// equivalent:
///
/// `embedded_asset!(app, "rock.wgsl")`
/// `embedded_asset!(app, "/src/", "rock.wgsl")`
///
/// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes.
/// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't
/// available for some reason, you can use the [`embedded_path`] macro to debug.
///
/// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature.
///
/// [`AssetPath`]: crate::AssetPath
/// [`embedded_asset`]: crate::embedded_asset
/// [`embedded_path`]: crate::embedded_path
#[macro_export]
macro_rules! embedded_asset {
    ($app: ident, $path: expr) => {{
        $crate::embedded_asset!($app, "src", $path)
    }};

    ($app: ident, $source_path: expr, $path: expr) => {{
        let mut embedded = $app
            .world_mut()
            .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>();
        let path = $crate::embedded_path!($source_path, $path);
        let watched_path = $crate::io::embedded::watched_path(file!(), $path);
        embedded.insert_asset(watched_path, &path, include_bytes!($path));
    }};
}

/// Returns the path used by the watcher.
#[doc(hidden)]
#[cfg(feature = "embedded_watcher")]
pub fn watched_path(source_file_path: &'static str, asset_path: &'static str) -> PathBuf {
    PathBuf::from(source_file_path)
        .parent()
        .unwrap()
        .join(asset_path)
}

/// Returns an empty PathBuf.
#[doc(hidden)]
#[cfg(not(feature = "embedded_watcher"))]
pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) -> PathBuf {
    PathBuf::from("")
}

/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle.
#[macro_export]
macro_rules! load_internal_asset {
    ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
        assets.insert($handle.id(), ($loader)(
            include_str!($path_str),
            std::path::Path::new(file!())
                .parent()
                .unwrap()
                .join($path_str)
                .to_string_lossy()
        ));
    }};
    // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded
    ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{
        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
        assets.insert($handle.id(), ($loader)(
            include_str!($path_str),
            std::path::Path::new(file!())
                .parent()
                .unwrap()
                .join($path_str)
                .to_string_lossy(),
            $($param),+
        ));
    }};
}

/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle.
#[macro_export]
macro_rules! load_internal_binary_asset {
    ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
        let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
        assets.insert(
            $handle.id(),
            ($loader)(
                include_bytes!($path_str).as_ref(),
                std::path::Path::new(file!())
                    .parent()
                    .unwrap()
                    .join($path_str)
                    .to_string_lossy()
                    .into(),
            ),
        );
    }};
}

#[cfg(test)]
mod tests {
    use super::{EmbeddedAssetRegistry, _embedded_asset_path};
    use std::path::Path;

    // Relative paths show up if this macro is being invoked by a local crate.
    // In this case we know the relative path is a sub- path of the workspace
    // root.

    #[test]
    fn embedded_asset_path_from_local_crate() {
        let asset_path = _embedded_asset_path(
            "my_crate",
            "src".as_ref(),
            "src/foo/plugin.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
    }

    // A blank src_path removes the embedded's file path altogether only the
    // asset path remains.
    #[test]
    fn embedded_asset_path_from_local_crate_blank_src_path_questionable() {
        let asset_path = _embedded_asset_path(
            "my_crate",
            "".as_ref(),
            "src/foo/some/deep/path/plugin.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
        assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
    }

    #[test]
    #[should_panic(expected = "Failed to find src_prefix \"NOT-THERE\" in \"src")]
    fn embedded_asset_path_from_local_crate_bad_src() {
        let _asset_path = _embedded_asset_path(
            "my_crate",
            "NOT-THERE".as_ref(),
            "src/foo/plugin.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
    }

    #[test]
    fn embedded_asset_path_from_local_example_crate() {
        let asset_path = _embedded_asset_path(
            "example_name",
            "examples/foo".as_ref(),
            "examples/foo/example.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
        assert_eq!(asset_path, Path::new("example_name/the/asset.png"));
    }

    // Absolute paths show up if this macro is being invoked by an external
    // dependency, e.g. one that's being checked out from a crates repo or git.
    #[test]
    fn embedded_asset_path_from_external_crate() {
        let asset_path = _embedded_asset_path(
            "my_crate",
            "src".as_ref(),
            "/path/to/crate/src/foo/plugin.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
    }

    #[test]
    fn embedded_asset_path_from_external_crate_root_src_path() {
        let asset_path = _embedded_asset_path(
            "my_crate",
            "/path/to/crate/src".as_ref(),
            "/path/to/crate/src/foo/plugin.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
    }

    // Although extraneous slashes are permitted at the end, e.g., "src////",
    // one or more slashes at the beginning are not.
    #[test]
    #[should_panic(expected = "Failed to find src_prefix \"////src\" in")]
    fn embedded_asset_path_from_external_crate_extraneous_beginning_slashes() {
        let asset_path = _embedded_asset_path(
            "my_crate",
            "////src".as_ref(),
            "/path/to/crate/src/foo/plugin.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
        assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
    }

    // We don't handle this edge case because it is ambiguous with the
    // information currently available to the embedded_path macro.
    #[test]
    fn embedded_asset_path_from_external_crate_is_ambiguous() {
        let asset_path = _embedded_asset_path(
            "my_crate",
            "src".as_ref(),
            "/path/to/.cargo/registry/src/crate/src/src/plugin.rs".as_ref(),
            "the/asset.png".as_ref(),
        );
        // Really, should be "my_crate/src/the/asset.png"
        assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
    }

    #[test]
    fn remove_embedded_asset() {
        let reg = EmbeddedAssetRegistry::default();
        let path = std::path::PathBuf::from("a/b/asset.png");
        reg.insert_asset(path.clone(), &path, &[]);
        assert!(reg.dir.get_asset(&path).is_some());
        assert!(reg.remove_asset(&path).is_some());
        assert!(reg.dir.get_asset(&path).is_none());
        assert!(reg.remove_asset(&path).is_none());
    }
}