wit_parser/resolve/fs.rs
1//! Filesystem operations for [`Resolve`].
2
3use alloc::format;
4use std::path::Path;
5use std::vec::Vec;
6
7use anyhow::{Context, Result, bail};
8
9use super::{PackageSources, Resolve};
10use crate::{SourceMap, UnresolvedPackageGroup};
11
12/// All the sources used during resolving a directory or path.
13#[derive(Clone, Debug)]
14pub struct PackageSourceMap {
15 inner: PackageSources,
16}
17
18impl PackageSourceMap {
19 fn from_single_source(package_id: super::PackageId, source: &Path) -> Result<Self> {
20 let path_str = source
21 .to_str()
22 .ok_or_else(|| anyhow::anyhow!("path is not valid utf-8: {source:?}"))?;
23 Ok(Self {
24 inner: PackageSources::from_single_source(package_id, path_str),
25 })
26 }
27
28 fn from_inner(inner: PackageSources) -> Self {
29 Self { inner }
30 }
31
32 /// All unique source paths.
33 pub fn paths(&self) -> impl Iterator<Item = &Path> {
34 self.inner.source_names().map(Path::new)
35 }
36
37 /// Source paths for package
38 pub fn package_paths(&self, id: super::PackageId) -> Option<impl Iterator<Item = &Path>> {
39 self.inner
40 .package_source_names(id)
41 .map(|iter| iter.map(Path::new))
42 }
43}
44
45enum ParsedFile {
46 #[cfg(feature = "decoding")]
47 Package(super::PackageId),
48 Unresolved(UnresolvedPackageGroup),
49}
50
51impl Resolve {
52 /// Parse WIT packages from the input `path`.
53 ///
54 /// The input `path` can be one of:
55 ///
56 /// * A directory containing a WIT package with an optional `deps` directory
57 /// for local dependencies. In this case `deps` is parsed first and then
58 /// the parent `path` is parsed and returned.
59 /// * A single standalone WIT file.
60 /// * A wasm-encoded WIT package as a single file in either the text or
61 /// binary format.
62 ///
63 /// More information can also be found at [`Resolve::push_dir`] and
64 /// [`Resolve::push_file`].
65 pub fn push_path(
66 &mut self,
67 path: impl AsRef<Path>,
68 ) -> Result<(super::PackageId, PackageSourceMap)> {
69 self._push_path(path.as_ref())
70 }
71
72 fn _push_path(&mut self, path: &Path) -> Result<(super::PackageId, PackageSourceMap)> {
73 if path.is_dir() {
74 self.push_dir(path).with_context(|| {
75 format!(
76 "failed to resolve directory while parsing WIT for path [{}]",
77 path.display()
78 )
79 })
80 } else {
81 let id = self.push_file(path)?;
82 Ok((id, PackageSourceMap::from_single_source(id, path)?))
83 }
84 }
85
86 /// Parses the filesystem directory at `path` as a WIT package and returns
87 /// a fully resolved [`super::PackageId`] list as a result.
88 ///
89 /// All `*.wit` files in `path` are parsed as a single package and then
90 /// inserted into this `Resolve`. The `path` specified may have a `deps`
91 /// subdirectory which is probed automatically for any other WIT
92 /// dependencies.
93 ///
94 /// The `deps` folder may contain:
95 ///
96 /// * `$path/deps/my-package/*.wit` - a directory that may contain multiple
97 /// WIT files. The directory is parsed as a single package and then
98 /// inserted into this [`Resolve`]. Note that it cannot recursively
99 /// contain a `deps` directory.
100 /// * `$path/deps/my-package.wit` - a single-file WIT package. This is
101 /// parsed with [`Resolve::push_file`] and then added to `self` for
102 /// name resolution.
103 /// * `$path/deps/my-package.{wasm,wat}` - a wasm-encoded WIT package either
104 /// in the text for binary format.
105 ///
106 /// In all cases entries in the `deps` folder are added to `self` first
107 /// before adding files found in `path` itself. All WIT packages found are
108 /// candidates for name-based resolution that other packages may use.
109 ///
110 /// This function returns a tuple of two values. The first value is a
111 /// [`super::PackageId`], which represents the main WIT package found within
112 /// `path`. This argument is useful for passing to [`Resolve::select_world`]
113 /// for choosing something to bindgen with.
114 ///
115 /// The second value returned is a [`PackageSourceMap`], which contains all the sources
116 /// that were parsed during resolving. This can be useful for:
117 /// * build systems that want to rebuild bindings whenever one of the files changed
118 /// * or other tools, which want to identify the sources for the resolved packages
119 pub fn push_dir(
120 &mut self,
121 path: impl AsRef<Path>,
122 ) -> Result<(super::PackageId, PackageSourceMap)> {
123 self._push_dir(path.as_ref())
124 }
125
126 fn _push_dir(&mut self, path: &Path) -> Result<(super::PackageId, PackageSourceMap)> {
127 let top_pkg = self
128 .parse_dir(path)
129 .with_context(|| format!("failed to parse package: {}", path.display()))?;
130 let deps = path.join("deps");
131 let deps = self
132 .parse_deps_dir(&deps)
133 .with_context(|| format!("failed to parse dependency directory: {}", deps.display()))?;
134
135 let (pkg_id, inner) = self.sort_unresolved_packages(top_pkg, deps)?;
136 Ok((pkg_id, PackageSourceMap::from_inner(inner)))
137 }
138
139 /// Reads `*.wit` files from `path` into a [`SourceMap`] and parses them as
140 /// an [`UnresolvedPackageGroup`]. See [`Resolve::parse_source_map`] for the
141 /// failure-path semantics.
142 fn parse_dir(&mut self, path: &Path) -> Result<UnresolvedPackageGroup> {
143 let mut map = SourceMap::default();
144 map.push_dir(path)?;
145 self.parse_source_map(map)
146 }
147
148 fn parse_deps_dir(&mut self, path: &Path) -> Result<Vec<UnresolvedPackageGroup>> {
149 let mut ret = Vec::new();
150 if !path.exists() {
151 return Ok(ret);
152 }
153 let mut entries = path
154 .read_dir()
155 .and_then(|i| i.collect::<std::io::Result<Vec<_>>>())
156 .context("failed to read directory")?;
157 entries.sort_by_key(|e| e.file_name());
158 for dep in entries {
159 let path = dep.path();
160 let pkg = if dep.file_type()?.is_dir() || path.metadata()?.is_dir() {
161 // If this entry is a directory or a symlink point to a
162 // directory then always parse it as an `UnresolvedPackage`
163 // since it's intentional to not support recursive `deps`
164 // directories.
165 self.parse_dir(&path)
166 .with_context(|| format!("failed to parse package: {}", path.display()))?
167 } else {
168 // If this entry is a file then we may want to ignore it but
169 // this may also be a standalone WIT file or a `*.wasm` or
170 // `*.wat` encoded package.
171 let filename = dep.file_name();
172 match Path::new(&filename).extension().and_then(|s| s.to_str()) {
173 Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? {
174 #[cfg(feature = "decoding")]
175 ParsedFile::Package(_) => continue,
176 ParsedFile::Unresolved(pkg) => pkg,
177 },
178
179 // Other files in deps dir are ignored for now to avoid
180 // accidentally including things like `.DS_Store` files in
181 // the call below to `parse_dir`.
182 _ => continue,
183 }
184 };
185 ret.push(pkg);
186 }
187 Ok(ret)
188 }
189
190 /// Parses the contents of `path` from the filesystem and pushes the result
191 /// into this `Resolve`.
192 ///
193 /// The `path` referenced here can be one of:
194 ///
195 /// * A WIT file. Note that in this case this single WIT file will be the
196 /// entire package and any dependencies it has must already be in `self`.
197 /// * A WIT package encoded as WebAssembly, either in text or binary form.
198 /// In this the package and all of its dependencies are automatically
199 /// inserted into `self`.
200 ///
201 /// In both situations the `PackageId`s of the resulting resolved packages
202 /// are returned from this method. The return value is mostly useful in
203 /// conjunction with [`Resolve::select_world`].
204 pub fn push_file(&mut self, path: impl AsRef<Path>) -> Result<super::PackageId> {
205 match self._push_file(path.as_ref())? {
206 #[cfg(feature = "decoding")]
207 ParsedFile::Package(id) => Ok(id),
208 ParsedFile::Unresolved(pkg) => Ok(self.push_group(pkg)?),
209 }
210 }
211
212 fn _push_file(&mut self, path: &Path) -> Result<ParsedFile> {
213 let contents = std::fs::read(path)
214 .with_context(|| format!("failed to read path for WIT [{}]", path.display()))?;
215
216 // If decoding is enabled at compile time then try to see if this is a
217 // wasm file.
218 #[cfg(feature = "decoding")]
219 {
220 use crate::decoding::{DecodedWasm, decode};
221
222 #[cfg(feature = "wat")]
223 let is_wasm = wat::Detect::from_bytes(&contents).is_wasm();
224 #[cfg(not(feature = "wat"))]
225 let is_wasm = wasmparser::Parser::is_component(&contents);
226
227 if is_wasm {
228 #[cfg(feature = "wat")]
229 let contents = wat::parse_bytes(&contents).map_err(|mut e| {
230 e.set_path(path);
231 e
232 })?;
233
234 match decode(&contents)? {
235 DecodedWasm::Component(..) => {
236 bail!("found an actual component instead of an encoded WIT package in wasm")
237 }
238 DecodedWasm::WitPackage(resolve, pkg) => {
239 let remap = self.merge(resolve)?;
240 return Ok(ParsedFile::Package(remap.packages[pkg.index()]));
241 }
242 }
243 }
244 }
245
246 // If this wasn't a wasm file then assume it's a WIT file.
247 let text = match core::str::from_utf8(&contents) {
248 Ok(s) => s,
249 Err(_) => bail!("input file is not valid utf-8 [{}]", path.display()),
250 };
251 let mut map = SourceMap::default();
252 map.push(path, text);
253 Ok(ParsedFile::Unresolved(self.parse_source_map(map)?))
254 }
255
256 /// Parses `contents` as a WIT package and pushes it into this `Resolve`.
257 ///
258 /// The `path` provided is used for error messages but otherwise is not
259 /// read. This method does not touch the filesystem. The `contents` provided
260 /// are the contents of a WIT package.
261 pub fn push_str(&mut self, path: impl AsRef<Path>, contents: &str) -> Result<super::PackageId> {
262 let path = path
263 .as_ref()
264 .to_str()
265 .ok_or_else(|| anyhow::anyhow!("path is not valid utf-8: {:?}", path.as_ref()))?;
266 self.push_source(path, contents)
267 }
268}