Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_asset/src/io/embedded/mod.rs
6601 views
1
#[cfg(feature = "embedded_watcher")]
2
mod embedded_watcher;
3
4
#[cfg(feature = "embedded_watcher")]
5
pub use embedded_watcher::*;
6
7
use crate::io::{
8
memory::{Dir, MemoryAssetReader, Value},
9
AssetSource, AssetSourceBuilders,
10
};
11
use crate::AssetServer;
12
use alloc::boxed::Box;
13
use bevy_app::App;
14
use bevy_ecs::{resource::Resource, world::World};
15
use std::path::{Path, PathBuf};
16
17
#[cfg(feature = "embedded_watcher")]
18
use alloc::borrow::ToOwned;
19
20
/// The name of the `embedded` [`AssetSource`],
21
/// as stored in the [`AssetSourceBuilders`] resource.
22
pub const EMBEDDED: &str = "embedded";
23
24
/// A [`Resource`] that manages "rust source files" in a virtual in memory [`Dir`], which is intended
25
/// to be shared with a [`MemoryAssetReader`].
26
/// Generally this should not be interacted with directly. The [`embedded_asset`] will populate this.
27
///
28
/// [`embedded_asset`]: crate::embedded_asset
29
#[derive(Resource, Default)]
30
pub struct EmbeddedAssetRegistry {
31
dir: Dir,
32
#[cfg(feature = "embedded_watcher")]
33
root_paths: alloc::sync::Arc<
34
parking_lot::RwLock<bevy_platform::collections::HashMap<Box<Path>, PathBuf>>,
35
>,
36
}
37
38
impl EmbeddedAssetRegistry {
39
/// Inserts a new asset. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
40
/// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
41
/// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
42
/// or a [`Vec<u8>`](alloc::vec::Vec).
43
#[cfg_attr(
44
not(feature = "embedded_watcher"),
45
expect(
46
unused_variables,
47
reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
48
)
49
)]
50
pub fn insert_asset(&self, full_path: PathBuf, asset_path: &Path, value: impl Into<Value>) {
51
#[cfg(feature = "embedded_watcher")]
52
self.root_paths
53
.write()
54
.insert(full_path.into(), asset_path.to_owned());
55
self.dir.insert_asset(asset_path, value);
56
}
57
58
/// Inserts new asset metadata. `full_path` is the full path (as [`file`] would return for that file, if it was capable of
59
/// running in a non-rust file). `asset_path` is the path that will be used to identify the asset in the `embedded`
60
/// [`AssetSource`]. `value` is the bytes that will be returned for the asset. This can be _either_ a `&'static [u8]`
61
/// or a [`Vec<u8>`](alloc::vec::Vec).
62
#[cfg_attr(
63
not(feature = "embedded_watcher"),
64
expect(
65
unused_variables,
66
reason = "The `full_path` argument is not used when `embedded_watcher` is disabled."
67
)
68
)]
69
pub fn insert_meta(&self, full_path: &Path, asset_path: &Path, value: impl Into<Value>) {
70
#[cfg(feature = "embedded_watcher")]
71
self.root_paths
72
.write()
73
.insert(full_path.into(), asset_path.to_owned());
74
self.dir.insert_meta(asset_path, value);
75
}
76
77
/// Removes an asset stored using `full_path` (the full path as [`file`] would return for that file, if it was capable of
78
/// running in a non-rust file). If no asset is stored with at `full_path` its a no-op.
79
/// It returning `Option` contains the originally stored `Data` or `None`.
80
pub fn remove_asset(&self, full_path: &Path) -> Option<super::memory::Data> {
81
self.dir.remove_asset(full_path)
82
}
83
84
/// Registers the [`EMBEDDED`] [`AssetSource`] with the given [`AssetSourceBuilders`].
85
pub fn register_source(&self, sources: &mut AssetSourceBuilders) {
86
let dir = self.dir.clone();
87
let processed_dir = self.dir.clone();
88
89
#[cfg_attr(
90
not(feature = "embedded_watcher"),
91
expect(
92
unused_mut,
93
reason = "Variable is only mutated when `embedded_watcher` feature is enabled."
94
)
95
)]
96
let mut source = AssetSource::build()
97
.with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() }))
98
.with_processed_reader(move || {
99
Box::new(MemoryAssetReader {
100
root: processed_dir.clone(),
101
})
102
})
103
// Note that we only add a processed watch warning because we don't want to warn
104
// noisily about embedded watching (which is niche) when users enable file watching.
105
.with_processed_watch_warning(
106
"Consider enabling the `embedded_watcher` cargo feature.",
107
);
108
109
#[cfg(feature = "embedded_watcher")]
110
{
111
let root_paths = self.root_paths.clone();
112
let dir = self.dir.clone();
113
let processed_root_paths = self.root_paths.clone();
114
let processed_dir = self.dir.clone();
115
source = source
116
.with_watcher(move |sender| {
117
Some(Box::new(EmbeddedWatcher::new(
118
dir.clone(),
119
root_paths.clone(),
120
sender,
121
core::time::Duration::from_millis(300),
122
)))
123
})
124
.with_processed_watcher(move |sender| {
125
Some(Box::new(EmbeddedWatcher::new(
126
processed_dir.clone(),
127
processed_root_paths.clone(),
128
sender,
129
core::time::Duration::from_millis(300),
130
)))
131
});
132
}
133
sources.insert(EMBEDDED, source);
134
}
135
}
136
137
/// Trait for the [`load_embedded_asset!`] macro, to access [`AssetServer`]
138
/// from arbitrary things.
139
///
140
/// [`load_embedded_asset!`]: crate::load_embedded_asset
141
pub trait GetAssetServer {
142
fn get_asset_server(&self) -> &AssetServer;
143
}
144
145
impl GetAssetServer for App {
146
fn get_asset_server(&self) -> &AssetServer {
147
self.world().get_asset_server()
148
}
149
}
150
151
impl GetAssetServer for World {
152
fn get_asset_server(&self) -> &AssetServer {
153
self.resource()
154
}
155
}
156
157
impl GetAssetServer for AssetServer {
158
fn get_asset_server(&self) -> &AssetServer {
159
self
160
}
161
}
162
163
/// Load an [embedded asset](crate::embedded_asset).
164
///
165
/// This is useful if the embedded asset in question is not publicly exposed, but
166
/// you need to use it internally.
167
///
168
/// # Syntax
169
///
170
/// This macro takes two arguments and an optional third one:
171
/// 1. The asset source. It may be `AssetServer`, `World` or `App`.
172
/// 2. The path to the asset to embed, as a string literal.
173
/// 3. Optionally, a closure of the same type as in [`AssetServer::load_with_settings`].
174
/// Consider explicitly typing the closure argument in case of type error.
175
///
176
/// # Usage
177
///
178
/// The advantage compared to using directly [`AssetServer::load`] is:
179
/// - This also accepts [`World`] and [`App`] arguments.
180
/// - This uses the exact same path as `embedded_asset!`, so you can keep it
181
/// consistent.
182
///
183
/// As a rule of thumb:
184
/// - If the asset in used in the same module as it is declared using `embedded_asset!`,
185
/// use this macro.
186
/// - Otherwise, use `AssetServer::load`.
187
#[macro_export]
188
macro_rules! load_embedded_asset {
189
(@get: $path: literal, $provider: expr) => {{
190
let path = $crate::embedded_path!($path);
191
let path = $crate::AssetPath::from_path_buf(path).with_source("embedded");
192
let asset_server = $crate::io::embedded::GetAssetServer::get_asset_server($provider);
193
(path, asset_server)
194
}};
195
($provider: expr, $path: literal, $settings: expr) => {{
196
let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider);
197
asset_server.load_with_settings(path, $settings)
198
}};
199
($provider: expr, $path: literal) => {{
200
let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider);
201
asset_server.load(path)
202
}};
203
}
204
205
/// Returns the [`Path`] for a given `embedded` asset.
206
/// This is used internally by [`embedded_asset`] and can be used to get a [`Path`]
207
/// that matches the [`AssetPath`](crate::AssetPath) used by that asset.
208
///
209
/// [`embedded_asset`]: crate::embedded_asset
210
#[macro_export]
211
macro_rules! embedded_path {
212
($path_str: expr) => {{
213
$crate::embedded_path!("src", $path_str)
214
}};
215
216
($source_path: expr, $path_str: expr) => {{
217
let crate_name = module_path!().split(':').next().unwrap();
218
$crate::io::embedded::_embedded_asset_path(
219
crate_name,
220
$source_path.as_ref(),
221
file!().as_ref(),
222
$path_str.as_ref(),
223
)
224
}};
225
}
226
227
/// Implementation detail of `embedded_path`, do not use this!
228
///
229
/// Returns an embedded asset path, given:
230
/// - `crate_name`: name of the crate where the asset is embedded
231
/// - `src_prefix`: path prefix of the crate's source directory, relative to the workspace root
232
/// - `file_path`: `std::file!()` path of the source file where `embedded_path!` is called
233
/// - `asset_path`: path of the embedded asset relative to `file_path`
234
#[doc(hidden)]
235
pub fn _embedded_asset_path(
236
crate_name: &str,
237
src_prefix: &Path,
238
file_path: &Path,
239
asset_path: &Path,
240
) -> PathBuf {
241
let file_path = if cfg!(not(target_family = "windows")) {
242
// Work around bug: https://github.com/bevyengine/bevy/issues/14246
243
// Note, this will break any paths on Linux/Mac containing "\"
244
PathBuf::from(file_path.to_str().unwrap().replace("\\", "/"))
245
} else {
246
PathBuf::from(file_path)
247
};
248
let mut maybe_parent = file_path.parent();
249
let after_src = loop {
250
let Some(parent) = maybe_parent else {
251
panic!("Failed to find src_prefix {src_prefix:?} in {file_path:?}")
252
};
253
if parent.ends_with(src_prefix) {
254
break file_path.strip_prefix(parent).unwrap();
255
}
256
maybe_parent = parent.parent();
257
};
258
let asset_path = after_src.parent().unwrap().join(asset_path);
259
Path::new(crate_name).join(asset_path)
260
}
261
262
/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary
263
/// and registering those bytes with the `embedded` [`AssetSource`].
264
///
265
/// This accepts the current [`App`] as the first parameter and a path `&str` (relative to the current file) as the second.
266
///
267
/// By default this will generate an [`AssetPath`] using the following rules:
268
///
269
/// 1. Search for the first `$crate_name/src/` in the path and trim to the path past that point.
270
/// 2. Re-add the current `$crate_name` to the front of the path
271
///
272
/// For example, consider the following file structure in the theoretical `bevy_rock` crate, which provides a Bevy [`Plugin`](bevy_app::Plugin)
273
/// that renders fancy rocks for scenes.
274
///
275
/// ```text
276
/// bevy_rock
277
/// ├── src
278
/// │   ├── render
279
/// │  │ ├── rock.wgsl
280
/// │  │ └── mod.rs
281
/// │   └── lib.rs
282
/// └── Cargo.toml
283
/// ```
284
///
285
/// `rock.wgsl` is a WGSL shader asset that the `bevy_rock` plugin author wants to bundle with their crate. They invoke the following
286
/// in `bevy_rock/src/render/mod.rs`:
287
///
288
/// `embedded_asset!(app, "rock.wgsl")`
289
///
290
/// `rock.wgsl` can now be loaded by the [`AssetServer`] as follows:
291
///
292
/// ```no_run
293
/// # use bevy_asset::{Asset, AssetServer, load_embedded_asset};
294
/// # use bevy_reflect::TypePath;
295
/// # let asset_server: AssetServer = panic!();
296
/// # #[derive(Asset, TypePath)]
297
/// # struct Shader;
298
/// // If we are loading the shader in the same module we used `embedded_asset!`:
299
/// let shader = load_embedded_asset!(&asset_server, "rock.wgsl");
300
/// # let _: bevy_asset::Handle<Shader> = shader;
301
///
302
/// // If the goal is to expose the asset **to the end user**:
303
/// let shader = asset_server.load::<Shader>("embedded://bevy_rock/render/rock.wgsl");
304
/// ```
305
///
306
/// Some things to note in the path:
307
/// 1. The non-default `embedded://` [`AssetSource`]
308
/// 2. `src` is trimmed from the path
309
///
310
/// The default behavior also works for cargo workspaces. Pretend the `bevy_rock` crate now exists in a larger workspace in
311
/// `$SOME_WORKSPACE/crates/bevy_rock`. The asset path would remain the same, because [`embedded_asset`] searches for the
312
/// _first instance_ of `bevy_rock/src` in the path.
313
///
314
/// For most "standard crate structures" the default works just fine. But for some niche cases (such as cargo examples),
315
/// the `src` path will not be present. You can override this behavior by adding it as the second argument to [`embedded_asset`]:
316
///
317
/// `embedded_asset!(app, "/examples/rock_stuff/", "rock.wgsl")`
318
///
319
/// When there are three arguments, the second argument will replace the default `/src/` value. Note that these two are
320
/// equivalent:
321
///
322
/// `embedded_asset!(app, "rock.wgsl")`
323
/// `embedded_asset!(app, "/src/", "rock.wgsl")`
324
///
325
/// This macro uses the [`include_bytes`] macro internally and _will not_ reallocate the bytes.
326
/// Generally the [`AssetPath`] generated will be predictable, but if your asset isn't
327
/// available for some reason, you can use the [`embedded_path`] macro to debug.
328
///
329
/// Hot-reloading `embedded` assets is supported. Just enable the `embedded_watcher` cargo feature.
330
///
331
/// [`AssetPath`]: crate::AssetPath
332
/// [`embedded_asset`]: crate::embedded_asset
333
/// [`embedded_path`]: crate::embedded_path
334
#[macro_export]
335
macro_rules! embedded_asset {
336
($app: expr, $path: expr) => {{
337
$crate::embedded_asset!($app, "src", $path)
338
}};
339
340
($app: expr, $source_path: expr, $path: expr) => {{
341
let mut embedded = $app
342
.world_mut()
343
.resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>();
344
let path = $crate::embedded_path!($source_path, $path);
345
let watched_path = $crate::io::embedded::watched_path(file!(), $path);
346
embedded.insert_asset(watched_path, &path, include_bytes!($path));
347
}};
348
}
349
350
/// Returns the path used by the watcher.
351
#[doc(hidden)]
352
#[cfg(feature = "embedded_watcher")]
353
pub fn watched_path(source_file_path: &'static str, asset_path: &'static str) -> PathBuf {
354
PathBuf::from(source_file_path)
355
.parent()
356
.unwrap()
357
.join(asset_path)
358
}
359
360
/// Returns an empty PathBuf.
361
#[doc(hidden)]
362
#[cfg(not(feature = "embedded_watcher"))]
363
pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) -> PathBuf {
364
PathBuf::from("")
365
}
366
367
/// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle.
368
#[macro_export]
369
macro_rules! load_internal_asset {
370
($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
371
let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
372
assets.insert($handle.id(), ($loader)(
373
include_str!($path_str),
374
std::path::Path::new(file!())
375
.parent()
376
.unwrap()
377
.join($path_str)
378
.to_string_lossy()
379
)).unwrap();
380
}};
381
// we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded
382
($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{
383
let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
384
assets.insert($handle.id(), ($loader)(
385
include_str!($path_str),
386
std::path::Path::new(file!())
387
.parent()
388
.unwrap()
389
.join($path_str)
390
.to_string_lossy(),
391
$($param),+
392
)).unwrap();
393
}};
394
}
395
396
/// Loads an "internal" binary asset by embedding the bytes stored in the given `path_str` and associates it with the given handle.
397
#[macro_export]
398
macro_rules! load_internal_binary_asset {
399
($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{
400
let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>();
401
assets
402
.insert(
403
$handle.id(),
404
($loader)(
405
include_bytes!($path_str).as_ref(),
406
std::path::Path::new(file!())
407
.parent()
408
.unwrap()
409
.join($path_str)
410
.to_string_lossy()
411
.into(),
412
),
413
)
414
.unwrap();
415
}};
416
}
417
418
#[cfg(test)]
419
mod tests {
420
use super::{EmbeddedAssetRegistry, _embedded_asset_path};
421
use std::path::Path;
422
423
// Relative paths show up if this macro is being invoked by a local crate.
424
// In this case we know the relative path is a sub- path of the workspace
425
// root.
426
427
#[test]
428
fn embedded_asset_path_from_local_crate() {
429
let asset_path = _embedded_asset_path(
430
"my_crate",
431
"src".as_ref(),
432
"src/foo/plugin.rs".as_ref(),
433
"the/asset.png".as_ref(),
434
);
435
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
436
}
437
438
// A blank src_path removes the embedded's file path altogether only the
439
// asset path remains.
440
#[test]
441
fn embedded_asset_path_from_local_crate_blank_src_path_questionable() {
442
let asset_path = _embedded_asset_path(
443
"my_crate",
444
"".as_ref(),
445
"src/foo/some/deep/path/plugin.rs".as_ref(),
446
"the/asset.png".as_ref(),
447
);
448
assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
449
}
450
451
#[test]
452
#[should_panic(expected = "Failed to find src_prefix \"NOT-THERE\" in \"src")]
453
fn embedded_asset_path_from_local_crate_bad_src() {
454
let _asset_path = _embedded_asset_path(
455
"my_crate",
456
"NOT-THERE".as_ref(),
457
"src/foo/plugin.rs".as_ref(),
458
"the/asset.png".as_ref(),
459
);
460
}
461
462
#[test]
463
fn embedded_asset_path_from_local_example_crate() {
464
let asset_path = _embedded_asset_path(
465
"example_name",
466
"examples/foo".as_ref(),
467
"examples/foo/example.rs".as_ref(),
468
"the/asset.png".as_ref(),
469
);
470
assert_eq!(asset_path, Path::new("example_name/the/asset.png"));
471
}
472
473
// Absolute paths show up if this macro is being invoked by an external
474
// dependency, e.g. one that's being checked out from a crates repo or git.
475
#[test]
476
fn embedded_asset_path_from_external_crate() {
477
let asset_path = _embedded_asset_path(
478
"my_crate",
479
"src".as_ref(),
480
"/path/to/crate/src/foo/plugin.rs".as_ref(),
481
"the/asset.png".as_ref(),
482
);
483
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
484
}
485
486
#[test]
487
fn embedded_asset_path_from_external_crate_root_src_path() {
488
let asset_path = _embedded_asset_path(
489
"my_crate",
490
"/path/to/crate/src".as_ref(),
491
"/path/to/crate/src/foo/plugin.rs".as_ref(),
492
"the/asset.png".as_ref(),
493
);
494
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
495
}
496
497
// Although extraneous slashes are permitted at the end, e.g., "src////",
498
// one or more slashes at the beginning are not.
499
#[test]
500
#[should_panic(expected = "Failed to find src_prefix \"////src\" in")]
501
fn embedded_asset_path_from_external_crate_extraneous_beginning_slashes() {
502
let asset_path = _embedded_asset_path(
503
"my_crate",
504
"////src".as_ref(),
505
"/path/to/crate/src/foo/plugin.rs".as_ref(),
506
"the/asset.png".as_ref(),
507
);
508
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
509
}
510
511
// We don't handle this edge case because it is ambiguous with the
512
// information currently available to the embedded_path macro.
513
#[test]
514
fn embedded_asset_path_from_external_crate_is_ambiguous() {
515
let asset_path = _embedded_asset_path(
516
"my_crate",
517
"src".as_ref(),
518
"/path/to/.cargo/registry/src/crate/src/src/plugin.rs".as_ref(),
519
"the/asset.png".as_ref(),
520
);
521
// Really, should be "my_crate/src/the/asset.png"
522
assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
523
}
524
525
#[test]
526
fn remove_embedded_asset() {
527
let reg = EmbeddedAssetRegistry::default();
528
let path = std::path::PathBuf::from("a/b/asset.png");
529
reg.insert_asset(path.clone(), &path, &[]);
530
assert!(reg.dir.get_asset(&path).is_some());
531
assert!(reg.remove_asset(&path).is_some());
532
assert!(reg.dir.get_asset(&path).is_none());
533
assert!(reg.remove_asset(&path).is_none());
534
}
535
}
536
537