wasm_pkg_core/
manifest.rs1use std::{
4 collections::HashMap,
5 path::{Path, PathBuf},
6};
7
8use anyhow::{Context, Result};
9use semver::VersionReq;
10use serde::{Deserialize, Serialize};
11mod paths;
12pub mod workspace;
13
14use workspace::*;
15
16use crate::manifest::paths::{find_root_iter, find_root_manifest_for_wd};
17
18pub const MANIFEST_FILE_NAME: &str = "wkg.toml";
20pub const WORKSPACE_OUT_DIR: &str = "wkg";
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
27#[serde(deny_unknown_fields)]
28pub struct Manifest {
29 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub workspace: Option<WorkspaceConfig>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub overrides: Option<HashMap<String, Override>>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub metadata: Option<Metadata>,
40}
41
42impl Manifest {
43 fn from_toml(contents: &str) -> Result<Manifest> {
44 let manifest: Manifest = toml::from_str(contents)?;
45 manifest.validate()?;
46 Ok(manifest)
47 }
48
49 pub async fn load_from_path(path: impl AsRef<Path>) -> Result<Manifest> {
51 let path = path.as_ref();
52 tracing::info!(path = %path.display(), "loading wkg manifest file");
53 let contents = std::fs::read_to_string(path)
54 .with_context(|| format!("unable to load manifest from {}", path.display()))?;
55 let mut manifest = Self::from_toml(&contents)
56 .with_context(|| format!("invalid manifest at {}", path.display()))?;
57 if let Some(WorkspaceConfig::Root(root)) = &mut manifest.workspace {
58 root.root_dir = path
59 .parent()
60 .with_context(|| {
61 format!("manifest path has no parent directory: {}", path.display())
62 })?
63 .to_path_buf();
64 root.members = WorkspaceRootConfig::resolve_members(&root.members, &root.root_dir);
66 }
67 Ok(manifest)
68 }
69
70 fn root(&self) -> Option<&WorkspaceRootConfig> {
71 if let Some(WorkspaceConfig::Root(root)) = &self.workspace {
72 return Some(root);
73 }
74 None
75 }
76
77 fn validate(&self) -> Result<()> {
79 self.validate_workspace_exclusivity()?;
80 Ok(())
82 }
83
84 fn validate_workspace_exclusivity(&self) -> Result<()> {
86 if self.workspace.is_none() {
87 return Ok(());
88 }
89 let mut conflicts = Vec::new();
90 if self.overrides.is_some() {
91 conflicts.push("overrides");
92 }
93 if self.metadata.is_some() {
94 conflicts.push("metadata");
95 }
96 if conflicts.is_empty() {
97 return Ok(());
98 }
99 anyhow::bail!(
100 "`[workspace]` cannot coexist with: `[{}]` - \
101 use `[workspace.metadata]` for workspace level values",
102 conflicts.join("]`, `[")
103 );
104 }
105
106 pub async fn load() -> Result<Manifest> {
111 let manifest_path = PathBuf::from(MANIFEST_FILE_NAME);
112 if !tokio::fs::try_exists(&manifest_path).await? {
113 return Ok(Manifest::default());
114 }
115 Self::load_from_path(manifest_path).await
116 }
117
118 pub async fn load_root_workspace(cwd: &Path) -> Result<Option<WorkspaceRootConfig>> {
122 let Some(manifest_file) = find_root_manifest_for_wd(cwd) else {
123 return Ok(None);
124 };
125 let manifest_dir = manifest_file
126 .parent()
127 .context("unexpectedly missing directory containing manifest")?;
128 let manifest = Self::load_from_path(&manifest_file).await?;
129
130 if let Some(root) = manifest.root() {
131 return Ok(Some(root.clone()));
132 }
133
134 for file in find_root_iter(&manifest_file) {
136 let manifest = Self::load_from_path(&file).await?;
137 if let Some(WorkspaceConfig::Root(root)) = manifest.workspace
138 && root.is_explicitly_listed_member(manifest_dir)
139 {
140 return Ok(Some(root));
141 }
142 }
143
144 Ok(None)
145 }
146
147 pub async fn write(&self, path: impl AsRef<Path>) -> Result<()> {
149 let contents = toml::to_string_pretty(self)?;
150 tokio::fs::write(path, contents)
151 .await
152 .context("unable to write manifest to path")
153 }
154
155 pub(crate) fn has_override(&self, path: impl AsRef<Path>) -> bool {
157 let path = path.as_ref().canonicalize().ok();
158 self.overrides
159 .iter()
160 .flat_map(|map| map.iter())
161 .find(|(_, o)| o.path.as_ref().and_then(|p| p.canonicalize().ok()) == path)
162 .is_some()
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
167#[serde(deny_unknown_fields)]
168pub struct Override {
169 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub path: Option<PathBuf>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub version: Option<VersionReq>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
180#[serde(deny_unknown_fields)]
181pub struct Metadata {
182 #[serde(default, skip_serializing_if = "Option::is_none", alias = "author")]
185 pub authors: Option<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub description: Option<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none", alias = "license")]
191 pub licenses: Option<String>,
192 #[serde(default, skip_serializing_if = "Option::is_none", alias = "repository")]
194 pub source: Option<String>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub homepage: Option<String>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub revision: Option<String>,
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[tokio::test]
208 async fn test_roundtrip() {
209 let tempdir = tempfile::tempdir().unwrap();
210 let manifest_path = tempdir.path().join(MANIFEST_FILE_NAME);
211 let manifest = Manifest {
212 workspace: None,
213 overrides: Some(HashMap::from([(
214 "foo:bar".to_string(),
215 Override {
216 path: Some(PathBuf::from("bar")),
217 version: Some(VersionReq::parse("1.0.0").unwrap()),
218 },
219 )])),
220 metadata: Some(Metadata {
221 authors: Some("Foo Bar".to_string()),
222 description: Some("Foobar baz".to_string()),
223 licenses: Some("FBB".to_string()),
224 source: Some("https://gitfoo/bar".to_string()),
225 homepage: Some("https://foo.bar".to_string()),
226 revision: Some("f00ba4".to_string()),
227 }),
228 };
229
230 manifest
231 .write(&manifest_path)
232 .await
233 .expect("unable to write manifest");
234 let loaded_manifest = Manifest::load_from_path(manifest_path)
235 .await
236 .expect("unable to load manifest");
237 assert_eq!(
238 manifest, loaded_manifest,
239 "manifest loaded from file does not match original manifest"
240 );
241 }
242}