Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/wasm-pkg-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ wasm-pkg-client = { workspace = true }
wit-component = { workspace = true }
wit-parser = { workspace = true }
petgraph = { workspace = true }
glob = { workspace = true }

[target.'cfg(unix)'.dependencies.libc]
version = "0.2"
Expand Down
1 change: 0 additions & 1 deletion crates/wasm-pkg-core/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,6 @@ impl AsRef<File> for Locker {
// NOTE(thomastaylor312): These lock file primitives from here on down are mostly copied wholesale
// from the lock file implementation of cargo-component with some minor modifications to make them
// work with tokio

impl Locker {
// NOTE(thomastaylor312): I am keeping around these try methods for possible later use. Right
// now we're ignoring the dead code
Expand Down
109 changes: 100 additions & 9 deletions crates/wasm-pkg-core/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,28 @@ use std::{
use anyhow::{Context, Result};
use semver::VersionReq;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
mod paths;
pub mod workspace;

use workspace::*;

use crate::manifest::paths::{find_root_iter, find_root_manifest_for_wd};

/// The default name of the manifest file.
pub const MANIFEST_FILE_NAME: &str = "wkg.toml";
/// Directory next to the root [`MANIFEST_FILE_NAME`] that holds multi-package `deps` and `config.toml`.
pub const WORKSPACE_OUT_DIR: &str = "wkg";

/// The structure for a wkg.toml manifest file. This file is entirely optional and is used for
/// overriding and annotating wasm packages.
/// `workspace` is mutually exclusive with `overrides` and top-level `metadata`
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
/// Workspace declaration.
// TODO: this should be a `TomlWorkspace` so that serialization is not coupled to the config structure
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace: Option<WorkspaceConfig>,
/// Overrides for various packages
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overrides: Option<HashMap<String, Override>>,
Expand All @@ -28,17 +40,70 @@ pub struct Manifest {
}

impl Manifest {
fn from_toml(contents: &str) -> Result<Manifest> {
let manifest: Manifest =
toml::from_str(contents).context("unable to parse manifest file")?;
manifest.validate()?;
Ok(manifest)
}

/// Loads a manifest file from the given path.
pub async fn load_from_path(path: impl AsRef<Path>) -> Result<Manifest> {
tracing::info!(path = %path.as_ref().display(), "loading wkg manifest file");
let contents = tokio::fs::read_to_string(path)
.await
.context("unable to load manifest from file")?;
let manifest: Manifest =
toml::from_str(&contents).context("unable to parse manifest file")?;
let path = path.as_ref();
tracing::info!(path = %path.display(), "loading wkg manifest file");
let contents = std::fs::read_to_string(path)
.with_context(|| format!("unable to load manifest from {}", path.display()))?;
let mut manifest = Self::from_toml(&contents)
.with_context(|| format!("invalid manifest at {}", path.display()))?;
if let Some(WorkspaceConfig::Root(root)) = &mut manifest.workspace {
root.root_dir = path
.parent()
.with_context(|| {
format!("manifest path has no parent directory: {}", path.display())
})?
.to_path_buf();
// Resolve globs and relative paths eagerly
root.members = WorkspaceRootConfig::resolve_members(&root.members, &root.root_dir);
}
Ok(manifest)
}

fn root(&self) -> Option<&WorkspaceRootConfig> {
if let Some(WorkspaceConfig::Root(root)) = &self.workspace {
return Some(root);
}
None
}

// `Manifest` validations, mirrors cargo's `Workspace::validate`
fn validate(&self) -> Result<()> {
self.validate_workspace_exclusivity()?;
// Add new validation rules with `self.validate_*()?;`
Ok(())
}

// no overrides or top-level metadata when workspace is present
fn validate_workspace_exclusivity(&self) -> Result<()> {
if self.workspace.is_none() {
return Ok(());
}
let mut conflicts = Vec::new();
if self.overrides.is_some() {
conflicts.push("overrides");
}
if self.metadata.is_some() {
conflicts.push("metadata");
}
if conflicts.is_empty() {
return Ok(());
}
anyhow::bail!(
"`[workspace]` cannot coexist with: `[{}]` - \
use `[workspace.metadata]` for workspace level values",
conflicts.join("]`, `[")
);
}

/// Attempts to load the manifest from the current directory. Most of the time, users of this
/// crate should use this function. Right now it just checks for a `wkg.toml` file in the current
/// directory, but we could add more resolution logic in the future. If the file is not found, a
Expand All @@ -51,11 +116,36 @@ impl Manifest {
Self::load_from_path(manifest_path).await
}

/// Tries to find the root workspace config
/// Returns `Ok(None)` when there is no `wkg.toml` ancestor that can be [`WorkspaceRootConfig`]
// TODO(maktychev): reconcile load_from_path and load_root_workspace
pub async fn load_root_workspace(cwd: &Path) -> Result<Option<WorkspaceRootConfig>> {
let Some(manifest_file) = find_root_manifest_for_wd(cwd) else {
return Ok(None);
};
let manifest_dir = manifest_file.parent().unwrap();
let manifest = Self::load_from_path(&manifest_file).await?;

if let Some(root) = manifest.root() {
return Ok(Some(root.clone()));
}

// keep walking up if we have not found root
for file in find_root_iter(&manifest_file) {
let manifest = Self::load_from_path(&file).await?;
if let Some(WorkspaceConfig::Root(root)) = manifest.workspace
&& root.is_explicitly_listed_member(manifest_dir) {
return Ok(Some(root));
}
}

Ok(None)
}

/// Serializes and writes the manifest to the given path.
pub async fn write(&self, path: impl AsRef<Path>) -> Result<()> {
let contents = toml::to_string_pretty(self)?;
let mut file = tokio::fs::File::create(path).await?;
file.write_all(contents.as_bytes())
tokio::fs::write(path, contents)
.await
.context("unable to write manifest to path")
}
Expand Down Expand Up @@ -117,6 +207,7 @@ mod tests {
let tempdir = tempfile::tempdir().unwrap();
let manifest_path = tempdir.path().join(MANIFEST_FILE_NAME);
let manifest = Manifest {
workspace: None,
overrides: Some(HashMap::from([(
"foo:bar".to_string(),
Override {
Expand Down
102 changes: 102 additions & 0 deletions crates/wasm-pkg-core/src/manifest/paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use anyhow::Result;
use std::path::{Component, Path, PathBuf};

use crate::manifest::MANIFEST_FILE_NAME;

/// Find the first ancestor [`super::Manifest`] path for current working directory.
pub(crate) fn find_root_manifest_for_wd(cwd: impl AsRef<Path>) -> Option<PathBuf> {
for current in cwd.as_ref().ancestors() {
let manifest = current.join(MANIFEST_FILE_NAME);
if manifest.exists() {
return Some(manifest);
}
}
None
}

// Returns first ancestor [`super::Manifest`] path for a given existing manifest
pub(crate) fn find_root_iter<'a>(manifest_path: &'a Path) -> impl Iterator<Item = PathBuf> + 'a {
manifest_path
.ancestors()
.skip(2) // skip `manifest_path` and the parent dir
.map(|dir| dir.join(MANIFEST_FILE_NAME))
.filter(|path| path.exists())
}

// copied from:
// https://github.com/rust-lang/cargo/blob/a595d0da21f228b7fdae64d3d5c0e527ea66bb59/crates/cargo-util/src/paths.rs#L84-L84
/// Normalize a path, removing things like `.` and `..`.
///
/// CAUTION: This does not resolve symlinks (unlike
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
/// behavior at times. This should be used carefully. Unfortunately,
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on.
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};

for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(Component::RootDir);
}
Component::CurDir => {}
Component::ParentDir => {
if ret.ends_with(Component::ParentDir) {
ret.push(Component::ParentDir);
} else {
let popped = ret.pop();
if !popped && !ret.has_root() {
ret.push(Component::ParentDir);
}
}
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}

// Check if path holds at least one `.wit` file.
fn dir_contains_wit(dir: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(dir) else {
return false;
};
entries.filter_map(Result::ok).any(|entry| {
entry.path().extension().is_some_and(|ext| ext == "wit")
&& entry.file_type().is_ok_and(|ft| ft.is_file())
})
}

// Expand a string if is determined to be a glob pattern
pub(crate) fn expand_globs(entry: &Path, root_dir: &Path) -> Result<Vec<PathBuf>> {
let joined = root_dir.join(entry);
// TODO(mkatychev): handle escaping glob chars
let is_glob = entry.to_str().is_some_and(|s| s.contains(['*', '?', '[']));
let pattern = joined.to_str().filter(|_| is_glob);
let Some(pattern) = pattern else {
return Ok(vec![joined]);
};
let matches = glob::glob(pattern)?;
let mut expanded: Vec<PathBuf> = matches
.filter_map(Result::ok)
.filter(|p| p.is_dir() && dir_contains_wit(p))
.collect();

if expanded.is_empty() {
return Ok(vec![joined]);
}

expanded.sort();
Ok(expanded)
}
Loading
Loading