-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Move away from std::Path in bevy_assets #19133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Welcome, new contributor! Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨ |
My intuition is that these should generally be something like |
let's wait for other reviewers then :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love that you've taken this on! Just a couple of minor points mostly, but the biggest concern I have is we should probably add more tests that cover the operations we previously relied on Path
for. e.g., parent()
, path_components()
, etc.
label: Option<CowArc<'a, str>>, | ||
} | ||
|
||
/// `PartialEq` needs to be derived manually for backwards compatibility. | ||
/// As `path` used to be `std::path::Path`, equality was tricky with a trailing slash. | ||
/// For example, "martin/stephan#dave" should be equal to "martin/stephan/#dave". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very interesting point to raise.
Future PR: Should we keep this kind of equality? martin/stephan#dave
reads to me as the asset stephan
with the label dave
, while martin/stephan/#dave
reads like an asset with no name but labelled as dave
. I think we should consider changing semantics to make these degenerate states unrepresentable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My opinion is we should not support this behaviour. I am fine to leave this for another PR, but I would appreciate if you (@lielfr) opened the PR after this one lands. At least it would be a good place to discuss thoughts on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can remove it, it will just require changing some of the tests to account for it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for not supporting the behaviour. However if we do make this change, two asset paths martin/stephan#dave
and martin/stephan/#dave
which compare as different would map to Path
s which are equal and return the same file.
Given we're inventing our own platform independent version of paths, maybe we could always require trailing slashes for directories.
I was thinking about this myself. For now, I agree the path forward is
So in the longer term, I would propose something like: pub struct AssetPathId<'a>(CowArc<'a, str>); Then |
label: Option<CowArc<'a, str>>, | ||
} | ||
|
||
/// `PartialEq` needs to be derived manually for backwards compatibility. | ||
/// As `path` used to be `std::path::Path`, equality was tricky with a trailing slash. | ||
/// For example, "martin/stephan#dave" should be equal to "martin/stephan/#dave". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My opinion is we should not support this behaviour. I am fine to leave this for another PR, but I would appreciate if you (@lielfr) opened the PR after this one lands. At least it would be a good place to discuss thoughts on this.
sender: Sender<AssetSourceEvent>, | ||
debounce_wait_time: Duration, | ||
) -> Result<Self, notify::Error> { | ||
let root = normalize_path(&path).canonicalize()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this canonicalize is important here. It makes paths with dots work correctly "../../my_assets_dir"
. Relevant PR: #18345 (hey look its me! I forgot all about this lol)
Should FileWatcher even be dealing with asset paths? This seems like a case where a file system path actually does make sense? I'm not sure though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not familiar enough (yet?) with the codebase to decide, so I'd love some guidance here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we would have to roll out own version of canonicalize to maintain being able to use ".." for relative paths
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into this a bit more, and I'm doubling down: I think FileWatcher should continue to deal with Path
directly. I think the main thing we want to replace is the fact that our asset paths (as in, the ones that we pass into asset_server.load()
) include some platform specific nonsense. That does not mean that our entire stack needs to deal only with strings - at some point, we need to convert that asset path into a concrete file path, so a Path
instance.
So coming at it from the other angle, FileWatcher
should be dealing with concrete paths because it is listening for real file system events for a particular directory. So I think we should revert what's happening in this file_watcher
mod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan that cart and I discussed a couple of years ago:
- Asset server will certainly use FilePaths internally, for environments that have filesystems. For web environments, FilePaths may not be needed at all.
- However, the semantics of FilePaths should not leak to the external asset server API. There should be a clear distinction between file paths and asset paths. In general, the upper layer uses asset paths, and the lower layer uses file paths.
- Path manipulations on asset paths (concatenation, parent, etc) should use custom methods written for Bevy (these shouldn't be difficult, it's just string manipulation). So for example
AssetPath::resolve
shouldn't use FilePath methods. - Path manipulations on file paths should use FilePath methods - so for example adding the asset directory base path to the asset path would be a FilePath method.
@@ -255,7 +288,7 @@ impl<'a> AssetPath<'a> { | |||
/// Gets the path to the asset in the "virtual filesystem". | |||
#[inline] | |||
pub fn path(&self) -> &Path { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be returning a str here instead? The comment says "path to the asset in the virtual filesystem", which implies we shouldn't be returning a native OS path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's possible but will require more extensive changes throughout bevy_asset. Is it ok to do this in the scope of this PR, or is it too big?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should remove this in this PR. It will be more changes, but it will ensure we aren't accidentally converting to paths somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking on this. As far as I can tell, this PR still leaves Path
s in the public API, I imagine the final version to use String/str everywhere. But splitting into multiple PRs may be worthwhile.
In addition to the other reviews above, my main comment is to avoid the lossy conversions -- changing the public APIs to take Strings in the same PR would avoid having to do the conversions. I would also love to see tests for std functions for path manipulation we replaced.
which tests? I saw there are already some that cover parent and some other manipulations |
the force-push was a result of a rebase from upstream |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the changes! I think I meant testing the new equality behaviour, but I now see there is one test for it.
I had a few other questions/thoughts, given we're essentially rolling our own platform independent version of paths, does it make sense to invest in some niceties for manipulating different parts? Not in this PR necessarily, although I think we probably have to roll our own canonicalize in this PR as @andriyDev pointed out.
sender: Sender<AssetSourceEvent>, | ||
debounce_wait_time: Duration, | ||
) -> Result<Self, notify::Error> { | ||
let root = normalize_path(&path).canonicalize()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we would have to roll out own version of canonicalize to maintain being able to use ".." for relative paths
label: Option<CowArc<'a, str>>, | ||
} | ||
|
||
/// `PartialEq` needs to be derived manually for backwards compatibility. | ||
/// As `path` used to be `std::path::Path`, equality was tricky with a trailing slash. | ||
/// For example, "martin/stephan#dave" should be equal to "martin/stephan/#dave". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for not supporting the behaviour. However if we do make this change, two asset paths martin/stephan#dave
and martin/stephan/#dave
which compare as different would map to Path
s which are equal and return the same file.
Given we're inventing our own platform independent version of paths, maybe we could always require trailing slashes for directories.
return true; | ||
} | ||
let mut simplified = Vec::new(); | ||
for component in self.path_components() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be more readable and potentially more reusable path parsing if we roll our own version of path components with an enum?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's possible, but I'm not sure about this. Do the other maintainers also agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion here, leaning to no for simplicity?
regarding implementing our own I assume it does need to support Regarding |
See also this comment by cart from a couple years ago: #9528 (comment) Basically he and I agreed that we should get rid of Path; it has more features than we want, and some of those features produce unexpected and inappropriate behavior when used as an asset path. (Example, asset paths with drive letters should not be supported). Unfortunately, this work never got done. |
sender: Sender<AssetSourceEvent>, | ||
debounce_wait_time: Duration, | ||
) -> Result<Self, notify::Error> { | ||
let root = normalize_path(&path).canonicalize()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into this a bit more, and I'm doubling down: I think FileWatcher should continue to deal with Path
directly. I think the main thing we want to replace is the fact that our asset paths (as in, the ones that we pass into asset_server.load()
) include some platform specific nonsense. That does not mean that our entire stack needs to deal only with strings - at some point, we need to convert that asset path into a concrete file path, so a Path
instance.
So coming at it from the other angle, FileWatcher
should be dealing with concrete paths because it is listening for real file system events for a particular directory. So I think we should revert what's happening in this file_watcher
mod.
@@ -255,7 +288,7 @@ impl<'a> AssetPath<'a> { | |||
/// Gets the path to the asset in the "virtual filesystem". | |||
#[inline] | |||
pub fn path(&self) -> &Path { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should remove this in this PR. It will be more changes, but it will ensure we aren't accidentally converting to paths somewhere.
return true; | ||
} | ||
let mut simplified = Vec::new(); | ||
for component in self.path_components() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion here, leaning to no for simplicity?
pub(crate) fn normalize_path(path: &Path) -> PathBuf { | ||
let mut result_path = PathBuf::new(); | ||
for elt in path.iter() { | ||
pub(crate) fn normalize_path<'b>(path: &'b [&'b str]) -> Vec<&'b str> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be missing something... It seems like we don't normalize our asset paths today. I think that means if you load "asset.txt"
and "blah/../asset.txt"
, they are treated as different assets.
Doesn't need to be addressed in this PR, just realized this grossness.
sorry for the delay. pub(crate) fn normalize_path<'b, P>(path: &'b [P]) -> Vec<P>
where
P: Into<&'b str> + Copy,
{
let mut result_path = Vec::new();
for &elt in path {
if elt.into() == "." {
// Skip
} else if elt.into() == ".." {
if result_path.is_empty() {
// Preserve ".." if insufficient matches (per RFC 1808).
result_path.push(elt);
} else {
result_path.pop();
}
} else {
result_path.push(elt);
}
}
result_path
} but it doesn't seem to work. Will it be ok to create a separate function for PathBuf? or am I missing something? |
I tried to change the readers to use |
…or Path replacement methods
… encoding, test for empty vs root paths
rebased from main again |
Objective
Solution
&str
internally instead of keeping aPath
Path
orPathBuf
, please let me know if these should be changed as wellTesting
PartialEq
implementation