use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use anstream::eprintln;
use anyhow::Context;
use clap::{Args, Subcommand};
use tempfile::NamedTempFile;
use wasm_pkg_client::caching::{CachingClient, FileCache};
use wasm_pkg_common::package::{PackageRef, Version};
use wasm_pkg_core::wit::WIT_DEPS_DIR;
use wasm_pkg_core::{
lock::{LOCK_FILE_NAME, LockFile, LockedPackage},
manifest::{MANIFEST_FILE_NAME, Manifest, workspace::WorkspaceRootConfig},
resolver::DependencyResolutionMap,
wit::{self, OutputType},
};
use crate::Common;
use crate::overlay::PublishVerifier;
#[derive(Debug, Subcommand)]
pub enum WitCommands {
Build(BuildArgs),
Fetch(FetchArgs),
Update(UpdateArgs),
}
impl WitCommands {
pub async fn run(self) -> anyhow::Result<()> {
match self {
WitCommands::Build(args) => args.run().await,
WitCommands::Fetch(args) => args.run().await,
WitCommands::Update(args) => args.run().await,
}
}
}
#[derive(Debug, Args)]
pub struct BuildArgs {
#[clap(short = 'd', long = "wit-dir", default_value = "wit")]
pub dir: PathBuf,
#[clap(short = 'o', long = "output")]
pub output: Option<PathBuf>,
#[clap(flatten)]
pub common: Common,
}
#[derive(Debug, Args)]
pub struct FetchArgs {
pub dir: Option<PathBuf>,
#[clap(short = 't', long = "type")]
pub output_type: Option<OutputType>,
#[clap(flatten)]
pub common: Common,
}
#[derive(Debug, Args)]
pub struct UpdateArgs {
#[clap(short = 'd', long = "wit-dir", default_value = "wit")]
pub dir: PathBuf,
#[clap(short = 't', long = "type")]
pub output_type: Option<OutputType>,
#[clap(flatten)]
pub common: Common,
}
impl BuildArgs {
pub async fn run(self) -> anyhow::Result<()> {
let client = self.common.get_client().await?;
let mut lock_file = LockFile::load(false).await?;
let (pkg_ref, version, bytes) = build_wit_dir(&self.dir, client, &mut lock_file).await?;
let output_path = if let Some(path) = self.output {
path
} else {
let mut file_name = pkg_ref.to_string();
if let Some(version) = version {
file_name.push_str(&format!("@{version}"));
}
file_name.push_str(".wasm");
PathBuf::from(file_name)
};
tokio::fs::write(&output_path, bytes).await?;
lock_file.write().await?;
eprintln!("WIT package written to {}", output_path.display());
Ok(())
}
}
pub async fn build_wit_dir(
dir: impl AsRef<Path>,
client: CachingClient<FileCache>,
lock_file: &mut LockFile,
) -> anyhow::Result<(PackageRef, Option<Version>, Vec<u8>)> {
check_dir(&dir).await?;
let manifest = Manifest::load().await?;
let result = wit::build_package(&manifest, dir.as_ref(), lock_file, client)
.await
.with_context(|| format!("failed to build WIT directory `{}`", dir.as_ref().display()))?;
Ok(result)
}
pub async fn temp_wit_file(package: &PackageRef, bytes: &[u8]) -> anyhow::Result<NamedTempFile> {
let prefix: String = package.to_string().replace([':', '/'], "_");
let tmp_handle = tempfile::Builder::new()
.prefix(&prefix)
.suffix(".wasm")
.tempfile()
.context("Failed to create temporary file for built WIT package")
.with_context(|| format!("package: {package}"))?;
tokio::fs::write(tmp_handle.path(), &bytes)
.await
.context("Failed to write built WIT package to temp file")
.with_context(|| format!("package: {package}"))?;
tracing::debug!(tmp_pkg_path = %tmp_handle.path().display(), "Wrote temporary WIT package file");
Ok(tmp_handle)
}
impl FetchArgs {
pub async fn run(self) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
let root = Manifest::load_root_workspace(&cwd).await?;
let dirs = if let Some(dir) = self.dir.clone() {
vec![dir]
} else {
root.as_ref()
.map(|r| r.members.clone())
.unwrap_or_else(|| vec![PathBuf::from("wit")])
};
let manifest = match root.as_ref() {
Some(root) => {
let manifest_path = root.root_dir().join(MANIFEST_FILE_NAME);
Manifest::load_from_path(manifest_path).await?
}
None => Manifest::load().await?,
};
let output = self.output_type.unwrap_or_default();
for dir in &dirs {
check_dir(dir).await?;
}
match root {
Some(root) => {
self.run_workspace_fetch(&dirs, output, &manifest, root)
.await
}
None => self.fetch_into_lock(&dirs, &manifest, output).await,
}
}
async fn run_workspace_fetch(
&self,
dirs: &[PathBuf],
output: OutputType,
manifest: &Manifest,
root: WorkspaceRootConfig,
) -> anyhow::Result<()> {
let lock_path = root.root_dir().join(LOCK_FILE_NAME);
let mut lock_file = load_or_create_lock(&lock_path).await?;
let verifier = PublishVerifier::try_new(
root.members.as_ref(),
"tmp_local_fetch",
self.common.load_config().await?,
self.common.load_cache().await?,
&mut lock_file,
false,
)
.await?;
let mut merged = DependencyResolutionMap::default();
for dir in dirs {
let resolved =
wit::resolve_dependencies(manifest, dir, Some(&lock_file), verifier.client.clone())
.await
.with_context(|| {
format!("failed to resolve dependencies for {}", dir.display())
})?;
for (pkg, resolution) in resolved.as_ref() {
if verifier.packages.contains(pkg) {
continue;
}
merged.insert(pkg.clone(), resolution.clone());
}
}
lock_file.update_dependencies(&merged);
lock_file
.write()
.await
.with_context(|| format!("failed to commit lock file at {}", lock_path.display()))?;
let out_dir = root.out_dir();
tokio::fs::create_dir_all(&out_dir)
.await
.with_context(|| format!("failed to create {}", out_dir.display()))?;
wit::populate_dependencies_workspace(&out_dir, &merged, output)
.await
.with_context(|| {
format!(
"failed to populate workspace deps at {}",
out_dir.join(WIT_DEPS_DIR).display(),
)
})
}
async fn fetch_into_lock(
&self,
dirs: &[PathBuf],
manifest: &Manifest,
output: OutputType,
) -> anyhow::Result<()> {
let client = self.common.get_client().await?;
let mut lock_file = LockFile::load(false).await?;
let mut union: BTreeSet<LockedPackage> = BTreeSet::new();
merge_locked_packages(&mut union, std::mem::take(&mut lock_file.packages));
for dir in dirs {
wit::fetch_dependencies(manifest, dir, &mut lock_file, client.clone(), output)
.await
.with_context(|| format!("failed to fetch dependencies for {}", dir.display()))?;
merge_locked_packages(&mut union, std::mem::take(&mut lock_file.packages));
}
lock_file.packages = union;
lock_file.write().await?;
Ok(())
}
}
async fn load_or_create_lock(path: &Path) -> anyhow::Result<LockFile> {
if !tokio::fs::try_exists(path).await? {
let mut empty = LockFile::new_with_path([], path).await?;
empty.write().await?;
drop(empty);
}
LockFile::load_from_path(path, false)
.await
.with_context(|| format!("failed to load lock file at {}", path.display()))
}
fn merge_locked_packages(acc: &mut BTreeSet<LockedPackage>, incoming: BTreeSet<LockedPackage>) {
for pkg in incoming {
if let Some(existing) = acc.take(&pkg) {
let mut merged = existing;
for v in pkg.versions {
if let Some(slot) = merged
.versions
.iter_mut()
.find(|e| e.requirement == v.requirement)
{
*slot = v;
} else {
merged.versions.push(v);
}
}
acc.insert(merged);
} else {
acc.insert(pkg);
}
}
}
impl UpdateArgs {
pub async fn run(self) -> anyhow::Result<()> {
check_dir(&self.dir).await?;
let client = self.common.get_client().await?;
let manifest = Manifest::load().await?;
let mut lock_file = LockFile::load(false).await?;
lock_file.packages.clear();
wit::fetch_dependencies(
&manifest,
self.dir,
&mut lock_file,
client,
self.output_type.unwrap_or_default(),
)
.await?;
lock_file.write().await?;
todo!()
}
}
async fn check_dir(dir: impl AsRef<Path>) -> anyhow::Result<()> {
let dir = dir.as_ref();
tokio::fs::metadata(dir).await
.with_context(|| format!("unable to read wit directory: {}", dir.display()))
.context("This command should be run from the parent directory of the wit directory or a directory can be overridden with the --wit-dir argument")
.map(|_|())
}