use crate::manifest::paths::{expand_globs, normalize_path};
use super::*;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum WorkspaceConfig {
Root(WorkspaceRootConfig),
Member { root: Option<String> },
}
impl Default for WorkspaceConfig {
fn default() -> Self {
Self::Root(WorkspaceRootConfig::default())
}
}
impl WorkspaceConfig {
pub fn as_root(&self) -> Option<&WorkspaceRootConfig> {
match self {
WorkspaceConfig::Root(r) => Some(r),
WorkspaceConfig::Member { .. } => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct WorkspaceRootConfig {
#[serde(default, skip)]
pub(super) root_dir: PathBuf,
pub members: Vec<PathBuf>,
pub metadata: Option<Metadata>,
}
impl WorkspaceRootConfig {
pub fn root_dir(&self) -> &Path {
&self.root_dir
}
pub fn out_dir(&self) -> PathBuf {
self.root_dir.join(PathBuf::from(WORKSPACE_OUT_DIR))
}
pub(super) fn is_explicitly_listed_member(&self, manifest_path: &Path) -> bool {
let root_manifest = self.root_dir.join("Cargo.toml");
if manifest_path == root_manifest {
return true;
}
let manifest_path = normalize_path(manifest_path);
self.members.iter().any(|member| member == &manifest_path)
}
pub(super) fn resolve_members(members: &[PathBuf], root_dir: &Path) -> Vec<PathBuf> {
members
.iter()
.flat_map(|entry| expand_globs(entry, root_dir))
.flatten()
.map(|p| normalize_path(&p))
.collect()
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
fn touch(path: impl AsRef<Path>) {
let path = path.as_ref();
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, "").unwrap();
}
#[test]
fn find_root_manifest_for_wd_first_ancestor() {
let tempdir = tempfile::tempdir().unwrap();
let root_dir = tempdir.path();
touch(root_dir.join("wkg.toml"));
touch(root_dir.join("pkg/wkg.toml"));
fs::create_dir_all(root_dir.join("pkg/wit")).unwrap();
let found = find_root_manifest_for_wd(root_dir.join("pkg/wit")).unwrap();
assert_eq!(found, root_dir.join("pkg/wkg.toml"));
}
#[test]
fn find_root_manifest_for_wd_none() {
let tempdir = tempfile::tempdir().unwrap();
assert!(find_root_manifest_for_wd(tempdir.path()).is_none());
}
#[test]
fn find_root_iter_ancestors() {
let tempdir = tempfile::tempdir().unwrap();
let root_dir = tempdir.path();
touch(root_dir.join("wkg.toml"));
let member = root_dir.join("pkg/wkg.toml");
touch(&member);
let roots: Vec<PathBuf> = find_root_iter(&member).collect();
assert_eq!(roots, vec![root_dir.join("wkg.toml")]);
}
#[test]
fn resolve_members_skip_non_dirs() {
let tempdir = tempfile::tempdir().unwrap();
let root_dir = tempdir.path();
let member_wit = ["example-a/wit", "example-b/wit", "example-c/wit"];
for dir in member_wit {
fs::create_dir_all(root_dir.join(dir)).unwrap();
fs::write(root_dir.join(dir).join("pkg.wit"), "").unwrap();
}
fs::write(root_dir.join("example-d"), "").unwrap();
let members = vec![PathBuf::from("example-*/wit")];
let mut result = WorkspaceRootConfig::resolve_members(&members, root_dir);
result.sort();
let mut expected: Vec<_> = member_wit.iter().map(|p| root_dir.join(p)).collect();
expected.sort();
assert_eq!(result, expected, "glob should expand to matching dirs only");
}
#[test]
fn resolve_members_skip_non_wit_dirs() {
let tempdir = tempfile::tempdir().unwrap();
let root_dir = tempdir.path();
let core = root_dir.join("pkg/core");
let filesystem = root_dir.join("pkg/filesystem");
fs::create_dir_all(&core).unwrap();
fs::write(core.join("types.wit"), "package foo:core;\n").unwrap();
fs::create_dir_all(filesystem).unwrap();
let members = vec![PathBuf::from("pkg/*")];
let result = WorkspaceRootConfig::resolve_members(&members, root_dir);
assert_eq!(result, vec![core]);
}
#[test]
fn resolved_members_keeps_literal_even_when_empty() {
let tempdir = tempfile::tempdir().unwrap();
let root_dir = tempdir.path();
fs::create_dir_all(root_dir.join("empty-pkg")).unwrap();
let members = vec![PathBuf::from("empty-pkg")];
let result = WorkspaceRootConfig::resolve_members(&members, root_dir);
assert_eq!(result, vec![root_dir.join("empty-pkg")]);
}
#[test]
fn workspace_root() {
let toml = r#"
[workspace]
members = ["a/wit", "b/wit"]
"#;
let cfg = Manifest::from_toml(toml).unwrap();
let root = cfg.workspace.unwrap();
assert_eq!(
root.as_root().unwrap().members,
vec![PathBuf::from("a/wit"), PathBuf::from("b/wit")],
);
assert!(cfg.overrides.is_none() && cfg.metadata.is_none());
}
#[test]
fn workspace_metadata() {
let toml = r#"
[workspace]
members = ["a/wit"]
[workspace.metadata]
authors = "Webster Assembler"
"#;
let cfg = Manifest::from_toml(toml).unwrap();
let root = cfg.root().unwrap();
let meta = root.metadata.as_ref().unwrap();
assert_eq!(meta.authors.as_ref().unwrap(), "Webster Assembler");
}
#[test]
fn workspace_overrides_incompatible() {
let toml = r#"
[workspace]
members = ["a/wit"]
[overrides."foo:bar"]
path = "oo-bar"
"#;
Manifest::from_toml(toml).unwrap_err();
}
fn write(path: impl AsRef<Path>, contents: &str) {
let path = path.as_ref();
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}
#[tokio::test]
async fn load_workspace_from_root_and_member() {
let tempdir = tempfile::tempdir().unwrap();
let root_dir = tempdir.path();
write(
root_dir.join("wkg.toml"),
r#"
[workspace]
members = ["pkg-a"]
"#,
);
fs::create_dir_all(root_dir.join("pkg-a/wit")).unwrap();
let expected = root_dir.canonicalize().unwrap();
let from_root = Manifest::load_root_workspace(root_dir)
.await
.unwrap()
.unwrap();
assert_eq!(from_root.root_dir.canonicalize().unwrap(), expected);
let from_member = Manifest::load_root_workspace(&root_dir.join("pkg-a/wit"))
.await
.unwrap()
.unwrap();
assert_eq!(from_member.root_dir.canonicalize().unwrap(), expected);
}
#[tokio::test]
async fn load_workspace_no_root() {
let tempdir = tempfile::tempdir().unwrap();
let root_dir = tempdir.path();
write(
root_dir.join("wkg.toml"),
r#"
[metadata]
authors = "Webster Assembler"
"#,
);
assert!(
Manifest::load_root_workspace(root_dir)
.await
.unwrap()
.is_none()
);
}
}