Skip to main content

wasm_pkg_core/manifest/
workspace.rs

1use crate::manifest::paths::{expand_globs, normalize_path};
2
3use super::*;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8/// `[workspace]` table inside a `wkg.toml` manifest.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(untagged)]
11pub enum WorkspaceConfig {
12    Root(WorkspaceRootConfig),
13    // - `workspace = "path/to/workspace/root"`
14    // - TODO(mkatychev): implement workspace package level field
15    // - https://doc.rust-lang.org/cargo/reference/manifest.html#the-workspace-field
16    Member { root: Option<String> },
17}
18impl Default for WorkspaceConfig {
19    fn default() -> Self {
20        // TODO(mkatychev): use member variant instead
21        Self::Root(WorkspaceRootConfig::default())
22    }
23}
24
25impl WorkspaceConfig {
26    /// Resolves the [`WorkspaceRootConfig`] for a given [`Self`]
27    pub fn as_root(&self) -> Option<&WorkspaceRootConfig> {
28        match self {
29            WorkspaceConfig::Root(r) => Some(r),
30            WorkspaceConfig::Member { .. } => None,
31        }
32    }
33}
34
35/// Intermediate configuration of a workspace root in a manifest.
36///
37/// Knows the Workspace Root path, as well as `members` and `metadata`, which
38/// together tell if some path is recognized as a member by this root or not.
39#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
40pub struct WorkspaceRootConfig {
41    #[serde(default, skip)]
42    pub(super) root_dir: PathBuf,
43    pub members: Vec<PathBuf>,
44    pub metadata: Option<Metadata>,
45}
46
47impl WorkspaceRootConfig {
48    /// Directory containing the workspace-root `wkg.toml`.
49    pub fn root_dir(&self) -> &Path {
50        &self.root_dir
51    }
52
53    /// Directory containing workspace level `deps` `and config.toml`
54    pub fn out_dir(&self) -> PathBuf {
55        self.root_dir.join(PathBuf::from(WORKSPACE_OUT_DIR))
56    }
57
58    /// Checks if the path is explicitly listed as a workspace member.
59    ///
60    /// Returns `true` ONLY if:
61    /// - The path is the workspace root manifest itself, or
62    /// - The path matches one of the explicit `members` patterns
63    ///
64    /// NOTE: This does NOT check for implicit path dependency membership.
65    /// A `false` return does NOT mean the package is definitely not a member -
66    /// it could still be a member via path dependencies. Callers should fallback
67    /// to full workspace loading when this returns `false`.
68    // FIXME: implement cargo WorkspaceRootConfig::{member_paths, expand_member_path}
69    // this should not be eagerly evaluated
70    pub(super) fn is_explicitly_listed_member(&self, manifest_path: &Path) -> bool {
71        let root_manifest = self.root_dir.join("Cargo.toml");
72        if manifest_path == root_manifest {
73            return true;
74        }
75
76        let manifest_path = normalize_path(manifest_path);
77        self.members.iter().any(|member| member == &manifest_path)
78    }
79    // Expand globs and relative paths
80    pub(super) fn resolve_members(members: &[PathBuf], root_dir: &Path) -> Vec<PathBuf> {
81        members
82            .iter()
83            .flat_map(|entry| expand_globs(entry, root_dir))
84            .flatten()
85            .map(|p| normalize_path(&p))
86            .collect()
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use std::fs;
93
94    use super::*;
95
96    // NOTE: tree dir comments generated using `eza -T dir/`
97    fn touch(path: impl AsRef<Path>) {
98        let path = path.as_ref();
99        fs::create_dir_all(path.parent().unwrap()).unwrap();
100        fs::write(path, "").unwrap();
101    }
102
103    #[test]
104    fn find_root_manifest_for_wd_first_ancestor() {
105        // dir
106        // ├── wkg.toml
107        // └── pkg
108        //     └── wkg.toml
109        let tempdir = tempfile::tempdir().unwrap();
110        let root_dir = tempdir.path();
111        touch(root_dir.join("wkg.toml"));
112        touch(root_dir.join("pkg/wkg.toml"));
113        fs::create_dir_all(root_dir.join("pkg/wit")).unwrap();
114
115        let found = find_root_manifest_for_wd(root_dir.join("pkg/wit")).unwrap();
116        assert_eq!(found, root_dir.join("pkg/wkg.toml"));
117    }
118
119    #[test]
120    fn find_root_manifest_for_wd_none() {
121        let tempdir = tempfile::tempdir().unwrap();
122        assert!(find_root_manifest_for_wd(tempdir.path()).is_none());
123    }
124
125    #[test]
126    fn find_root_iter_ancestors() {
127        // dir
128        // ├── wkg.toml
129        // └── pkg
130        //     └── wkg.toml
131        let tempdir = tempfile::tempdir().unwrap();
132        let root_dir = tempdir.path();
133        touch(root_dir.join("wkg.toml"));
134        let member = root_dir.join("pkg/wkg.toml");
135        touch(&member);
136
137        let roots: Vec<PathBuf> = find_root_iter(&member).collect();
138        assert_eq!(roots, vec![root_dir.join("wkg.toml")]);
139    }
140
141    #[test]
142    fn resolve_members_skip_non_dirs() {
143        // dir
144        // ├── example-a
145        // │   └── wit
146        // │       └── pkg.wit
147        // ├── example-b
148        // │   └── wit
149        // │       └── pkg.wit
150        // ├── example-c
151        // │   └── wit
152        // │       └── pkg.wit
153        // └── example-d
154        let tempdir = tempfile::tempdir().unwrap();
155        let root_dir = tempdir.path();
156        let member_wit = ["example-a/wit", "example-b/wit", "example-c/wit"];
157        for dir in member_wit {
158            fs::create_dir_all(root_dir.join(dir)).unwrap();
159            fs::write(root_dir.join(dir).join("pkg.wit"), "").unwrap();
160        }
161        fs::write(root_dir.join("example-d"), "").unwrap();
162
163        let members = vec![PathBuf::from("example-*/wit")];
164        let mut result = WorkspaceRootConfig::resolve_members(&members, root_dir);
165        result.sort();
166        let mut expected: Vec<_> = member_wit.iter().map(|p| root_dir.join(p)).collect();
167        expected.sort();
168        assert_eq!(result, expected, "glob should expand to matching dirs only");
169    }
170
171    // skip directories without `.wit` files
172    #[test]
173    fn resolve_members_skip_non_wit_dirs() {
174        // dir
175        // └── pkg
176        //     ├── core
177        //     │   └── types.wit
178        //     └── filesystem
179        let tempdir = tempfile::tempdir().unwrap();
180        let root_dir = tempdir.path();
181        let core = root_dir.join("pkg/core");
182        let filesystem = root_dir.join("pkg/filesystem");
183        fs::create_dir_all(&core).unwrap();
184        fs::write(core.join("types.wit"), "package foo:core;\n").unwrap();
185        fs::create_dir_all(filesystem).unwrap();
186
187        let members = vec![PathBuf::from("pkg/*")];
188        let result = WorkspaceRootConfig::resolve_members(&members, root_dir);
189        assert_eq!(result, vec![core]);
190    }
191
192    #[test]
193    fn resolved_members_keeps_literal_even_when_empty() {
194        let tempdir = tempfile::tempdir().unwrap();
195        let root_dir = tempdir.path();
196        // dir
197        // └── empty-pkg
198        fs::create_dir_all(root_dir.join("empty-pkg")).unwrap();
199
200        let members = vec![PathBuf::from("empty-pkg")];
201        let result = WorkspaceRootConfig::resolve_members(&members, root_dir);
202        assert_eq!(result, vec![root_dir.join("empty-pkg")]);
203    }
204
205    #[test]
206    fn workspace_root() {
207        let toml = r#"
208[workspace]
209members = ["a/wit", "b/wit"]
210"#;
211        let cfg = Manifest::from_toml(toml).unwrap();
212        let root = cfg.workspace.unwrap();
213        assert_eq!(
214            root.as_root().unwrap().members,
215            vec![PathBuf::from("a/wit"), PathBuf::from("b/wit")],
216        );
217        assert!(cfg.overrides.is_none() && cfg.metadata.is_none());
218    }
219
220    #[test]
221    fn workspace_metadata() {
222        let toml = r#"
223[workspace]
224members = ["a/wit"]
225
226[workspace.metadata]
227authors = "Webster Assembler"
228"#;
229        let cfg = Manifest::from_toml(toml).unwrap();
230        let root = cfg.root().unwrap();
231        let meta = root.metadata.as_ref().unwrap();
232        assert_eq!(meta.authors.as_ref().unwrap(), "Webster Assembler");
233    }
234
235    #[test]
236    fn workspace_overrides_incompatible() {
237        let toml = r#"
238[workspace]
239members = ["a/wit"]
240
241[overrides."foo:bar"]
242path = "oo-bar"
243"#;
244        Manifest::from_toml(toml).unwrap_err();
245    }
246
247    fn write(path: impl AsRef<Path>, contents: &str) {
248        let path = path.as_ref();
249        fs::create_dir_all(path.parent().unwrap()).unwrap();
250        fs::write(path, contents).unwrap();
251    }
252
253    #[tokio::test]
254    async fn load_workspace_from_root_and_member() {
255        let tempdir = tempfile::tempdir().unwrap();
256        let root_dir = tempdir.path();
257        // dir
258        // ├── wkg.toml
259        // └── pkg-a
260        //     └── wit
261        write(
262            root_dir.join("wkg.toml"),
263            r#"
264[workspace]
265members = ["pkg-a"]
266"#,
267        );
268        fs::create_dir_all(root_dir.join("pkg-a/wit")).unwrap();
269
270        let expected = root_dir.canonicalize().unwrap();
271        let from_root = Manifest::load_root_workspace(root_dir)
272            .await
273            .unwrap()
274            .unwrap();
275        assert_eq!(from_root.root_dir.canonicalize().unwrap(), expected);
276
277        let from_member = Manifest::load_root_workspace(&root_dir.join("pkg-a/wit"))
278            .await
279            .unwrap()
280            .unwrap();
281        assert_eq!(from_member.root_dir.canonicalize().unwrap(), expected);
282    }
283
284    #[tokio::test]
285    async fn load_workspace_no_root() {
286        // Standalone: wkg.toml exists but has no [workspace] table -> None.
287        let tempdir = tempfile::tempdir().unwrap();
288        let root_dir = tempdir.path();
289        write(
290            root_dir.join("wkg.toml"),
291            r#"
292[metadata]
293authors = "Webster Assembler"
294"#,
295        );
296        assert!(
297            Manifest::load_root_workspace(root_dir)
298                .await
299                .unwrap()
300                .is_none()
301        );
302    }
303}