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