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