Skip to main content

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(), &path.as_ref().join("deps"))
124    }
125
126    /// Same as [`Resolve::push_dir`], but uses `deps` as the directory
127    /// containing WIT dependencies.
128    pub fn push_dir_with_deps(
129        &mut self,
130        path: impl AsRef<Path>,
131        deps: impl AsRef<Path>,
132    ) -> Result<(super::PackageId, PackageSourceMap)> {
133        self._push_dir(path.as_ref(), deps.as_ref())
134    }
135
136    fn _push_dir(
137        &mut self,
138        path: &Path,
139        deps: &Path,
140    ) -> Result<(super::PackageId, PackageSourceMap)> {
141        let top_pkg = self
142            .parse_dir(path)
143            .with_context(|| format!("failed to parse package: {}", path.display()))?;
144        let deps = self
145            .parse_deps_dir(&deps)
146            .with_context(|| format!("failed to parse dependency directory: {}", deps.display()))?;
147
148        let (pkg_id, inner) = self.sort_unresolved_packages(top_pkg, deps)?;
149        Ok((pkg_id, PackageSourceMap::from_inner(inner)))
150    }
151
152    /// Reads `*.wit` files from `path` into a [`SourceMap`] and parses them as
153    /// an [`UnresolvedPackageGroup`]. See [`Resolve::parse_source_map`] for the
154    /// failure-path semantics.
155    fn parse_dir(&mut self, path: &Path) -> Result<UnresolvedPackageGroup> {
156        let mut map = SourceMap::default();
157        map.push_dir(path)?;
158        self.parse_source_map(map)
159    }
160
161    fn parse_deps_dir(&mut self, path: &Path) -> Result<Vec<UnresolvedPackageGroup>> {
162        let mut ret = Vec::new();
163        if !path.exists() {
164            return Ok(ret);
165        }
166        let mut entries = path
167            .read_dir()
168            .and_then(|i| i.collect::<std::io::Result<Vec<_>>>())
169            .context("failed to read directory")?;
170        entries.sort_by_key(|e| e.file_name());
171        for dep in entries {
172            let path = dep.path();
173            let pkg = if dep.file_type()?.is_dir() || path.metadata()?.is_dir() {
174                // If this entry is a directory or a symlink point to a
175                // directory then always parse it as an `UnresolvedPackage`
176                // since it's intentional to not support recursive `deps`
177                // directories.
178                self.parse_dir(&path)
179                    .with_context(|| format!("failed to parse package: {}", path.display()))?
180            } else {
181                // If this entry is a file then we may want to ignore it but
182                // this may also be a standalone WIT file or a `*.wasm` or
183                // `*.wat` encoded package.
184                let filename = dep.file_name();
185                match Path::new(&filename).extension().and_then(|s| s.to_str()) {
186                    Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? {
187                        #[cfg(feature = "decoding")]
188                        ParsedFile::Package(_) => continue,
189                        ParsedFile::Unresolved(pkg) => pkg,
190                    },
191
192                    // Other files in deps dir are ignored for now to avoid
193                    // accidentally including things like `.DS_Store` files in
194                    // the call below to `parse_dir`.
195                    _ => continue,
196                }
197            };
198            ret.push(pkg);
199        }
200        Ok(ret)
201    }
202
203    /// Parses the contents of `path` from the filesystem and pushes the result
204    /// into this `Resolve`.
205    ///
206    /// The `path` referenced here can be one of:
207    ///
208    /// * A WIT file. Note that in this case this single WIT file will be the
209    ///   entire package and any dependencies it has must already be in `self`.
210    /// * A WIT package encoded as WebAssembly, either in text or binary form.
211    ///   In this the package and all of its dependencies are automatically
212    ///   inserted into `self`.
213    ///
214    /// In both situations the `PackageId`s of the resulting resolved packages
215    /// are returned from this method. The return value is mostly useful in
216    /// conjunction with [`Resolve::select_world`].
217    pub fn push_file(&mut self, path: impl AsRef<Path>) -> Result<super::PackageId> {
218        match self._push_file(path.as_ref())? {
219            #[cfg(feature = "decoding")]
220            ParsedFile::Package(id) => Ok(id),
221            ParsedFile::Unresolved(pkg) => Ok(self.push_group(pkg)?),
222        }
223    }
224
225    fn _push_file(&mut self, path: &Path) -> Result<ParsedFile> {
226        let contents = std::fs::read(path)
227            .with_context(|| format!("failed to read path for WIT [{}]", path.display()))?;
228
229        // If decoding is enabled at compile time then try to see if this is a
230        // wasm file.
231        #[cfg(feature = "decoding")]
232        {
233            use crate::decoding::{DecodedWasm, decode};
234
235            #[cfg(feature = "wat")]
236            let is_wasm = wat::Detect::from_bytes(&contents).is_wasm();
237            #[cfg(not(feature = "wat"))]
238            let is_wasm = wasmparser::Parser::is_component(&contents);
239
240            if is_wasm {
241                #[cfg(feature = "wat")]
242                let contents = wat::parse_bytes(&contents).map_err(|mut e| {
243                    e.set_path(path);
244                    e
245                })?;
246
247                match decode(&contents)? {
248                    DecodedWasm::Component(..) => {
249                        bail!("found an actual component instead of an encoded WIT package in wasm")
250                    }
251                    DecodedWasm::WitPackage(resolve, pkg) => {
252                        let remap = self.merge(resolve)?;
253                        return Ok(ParsedFile::Package(remap.packages[pkg.index()]));
254                    }
255                }
256            }
257        }
258
259        // If this wasn't a wasm file then assume it's a WIT file.
260        let text = match core::str::from_utf8(&contents) {
261            Ok(s) => s,
262            Err(_) => bail!("input file is not valid utf-8 [{}]", path.display()),
263        };
264        let mut map = SourceMap::default();
265        map.push(path, text);
266        Ok(ParsedFile::Unresolved(self.parse_source_map(map)?))
267    }
268
269    /// Parses `contents` as a WIT package and pushes it into this `Resolve`.
270    ///
271    /// The `path` provided is used for error messages but otherwise is not
272    /// read. This method does not touch the filesystem. The `contents` provided
273    /// are the contents of a WIT package.
274    pub fn push_str(&mut self, path: impl AsRef<Path>, contents: &str) -> Result<super::PackageId> {
275        let path = path
276            .as_ref()
277            .to_str()
278            .ok_or_else(|| anyhow::anyhow!("path is not valid utf-8: {:?}", path.as_ref()))?;
279        self.push_source(path, contents)
280    }
281}