use std::{collections::HashSet, path::Path, str::FromStr};
use anyhow::{Context, Result};
use semver::{Version, VersionReq};
use wasm_metadata::{AddMetadata, AddMetadataField};
use wasm_pkg_client::{
caching::{CachingClient, FileCache},
PackageRef,
};
use wit_component::WitPrinter;
use wit_parser::{PackageId, PackageName, Resolve};
use crate::{
config::Config,
lock::LockFile,
resolver::{
DecodedDependency, Dependency, DependencyResolution, DependencyResolutionMap,
DependencyResolver, LocalResolution, RegistryPackage,
},
};
#[derive(Debug, Clone, Copy, Default)]
pub enum OutputType {
#[default]
Wit,
Wasm,
}
impl FromStr for OutputType {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let lower_trim = s.trim().to_lowercase();
match lower_trim.as_str() {
"wit" => Ok(Self::Wit),
"wasm" => Ok(Self::Wasm),
_ => Err(anyhow::anyhow!("Invalid output type: {}", s)),
}
}
}
pub async fn build_package(
config: &Config,
wit_dir: impl AsRef<Path>,
lock_file: &mut LockFile,
client: CachingClient<FileCache>,
) -> Result<(PackageRef, Option<Version>, Vec<u8>)> {
let dependencies = resolve_dependencies(config, &wit_dir, Some(lock_file), client)
.await
.context("Unable to resolve dependencies")?;
lock_file.update_dependencies(&dependencies);
let (resolve, pkg_id) = dependencies.generate_resolve(wit_dir).await?;
let bytes = wit_component::encode(&resolve, pkg_id)?;
let pkg = &resolve.packages[pkg_id];
let name = PackageRef::new(
pkg.name
.namespace
.parse()
.context("Invalid namespace found in package")?,
pkg.name
.name
.parse()
.context("Invalid name found in package")?,
);
let version = pkg
.name
.version
.as_ref()
.map(|v| v.to_string().parse())
.transpose()
.context("Invalid version found in package")?;
let processed_by_version = option_env!("WIT_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION"));
let metadata = config.metadata.clone().unwrap_or_default();
let add_metadata = {
fn set<T: std::fmt::Debug + Clone>(opt: Option<T>) -> AddMetadataField<T> {
opt.map(AddMetadataField::Set).unwrap_or_default()
}
let mut add = AddMetadata::default();
add.name = set(Some(format!("{}:{}", pkg.name.namespace, pkg.name.name)));
add.processed_by = vec![(
env!("CARGO_PKG_NAME").to_string(),
processed_by_version.to_string(),
)];
add.authors = set(metadata.authors.map(|v| v.parse()).transpose()?);
add.description = set(metadata.description.map(|v| v.parse()).transpose()?);
add.licenses = set(metadata.licenses.map(|v| v.parse()).transpose()?);
add.source = set(metadata.source.map(|v| v.parse()).transpose()?);
add.homepage = set(metadata.homepage.map(|v| v.parse()).transpose()?);
add.revision = set(metadata.revision.map(|v| v.parse()).transpose()?);
add.version = set(version);
add
};
let bytes = add_metadata.to_wasm(&bytes)?;
Ok((name, pkg.name.version.clone(), bytes))
}
pub async fn fetch_dependencies(
config: &Config,
wit_dir: impl AsRef<Path>,
lock_file: &mut LockFile,
client: CachingClient<FileCache>,
output: OutputType,
) -> Result<()> {
let dependencies = resolve_dependencies(config, &wit_dir, Some(lock_file), client).await?;
lock_file.update_dependencies(&dependencies);
populate_dependencies(wit_dir, &dependencies, output).await
}
pub fn get_packages(
path: impl AsRef<Path>,
) -> Result<(PackageRef, HashSet<(PackageRef, VersionReq)>)> {
let group =
wit_parser::UnresolvedPackageGroup::parse_path(path).context("Couldn't parse package")?;
let name = PackageRef::new(
group
.main
.name
.namespace
.parse()
.context("Invalid namespace found in package")?,
group
.main
.name
.name
.parse()
.context("Invalid name found in package")?,
);
let packages: HashSet<(PackageRef, VersionReq)> =
packages_from_foreign_deps(group.main.foreign_deps.into_keys())
.chain(
group
.nested
.into_iter()
.flat_map(|pkg| packages_from_foreign_deps(pkg.foreign_deps.into_keys())),
)
.collect();
Ok((name, packages))
}
pub async fn resolve_dependencies(
config: &Config,
path: impl AsRef<Path>,
lock_file: Option<&LockFile>,
client: CachingClient<FileCache>,
) -> Result<DependencyResolutionMap> {
let mut resolver = DependencyResolver::new_with_client(client, lock_file)?;
if let Some(overrides) = config.overrides.as_ref() {
for (pkg, ovr) in overrides.iter() {
let pkg: PackageRef = pkg.parse().context("Unable to parse as a package ref")?;
let dep = match (ovr.path.as_ref(), ovr.version.as_ref()) {
(Some(path), v) => {
if v.is_some() {
tracing::warn!("Ignoring version override for local package");
}
let path = tokio::fs::canonicalize(path)
.await
.with_context(|| format!("resolving local dependency {}", path.display()))?;
Dependency::Local(path)
}
(None, Some(version)) => Dependency::Package(RegistryPackage {
name: Some(pkg.clone()),
version: version.to_owned(),
registry: None,
}),
(None, None) => {
tracing::warn!("Found override without version or path, ignoring");
continue;
}
};
tracing::debug!(dependency = %dep);
resolver
.add_dependency(&pkg, &dep)
.await
.with_context(|| format!("unable to add dependency {dep}"))?;
}
}
let (_name, packages) = get_packages(path)?;
resolver.add_packages(packages).await?;
resolver.resolve().await
}
pub async fn populate_dependencies(
path: impl AsRef<Path>,
deps: &DependencyResolutionMap,
output: OutputType,
) -> Result<()> {
let path = tokio::fs::canonicalize(path).await?;
let metadata = tokio::fs::metadata(&path).await?;
if !metadata.is_dir() {
anyhow::bail!("Path is not a directory");
}
let deps_path = path.join("deps");
if let Err(e) = tokio::fs::remove_dir_all(&deps_path).await {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(anyhow::anyhow!("Unable to remove deps directory: {e}"));
}
}
tokio::fs::create_dir_all(&deps_path).await?;
if let OutputType::Wit = output {
let (resolve, pkg_id) = deps.generate_resolve(&path).await?;
return print_wit_from_resolve(&resolve, pkg_id, &deps_path).await;
}
let decoded_deps = deps.decode_dependencies().await?;
for (name, dep) in decoded_deps.iter() {
let mut output_path = deps_path.join(name_from_package_name(name));
match dep {
DecodedDependency::Wit {
resolution: DependencyResolution::Local(local),
..
} => {
tokio::fs::create_dir_all(&output_path).await?;
write_local_dep(local, output_path).await?;
}
DecodedDependency::Wit {
resolution: DependencyResolution::Registry(_),
..
} => {
anyhow::bail!("Unable to resolve dependency, this is a programmer error");
}
DecodedDependency::Wasm { resolution, .. } => {
let mut file_name = output_path.file_name().unwrap().to_owned();
file_name.push(".wasm");
output_path.set_file_name(file_name);
match resolution {
DependencyResolution::Local(local) => {
let meta = tokio::fs::metadata(&local.path).await?;
if !meta.is_file() {
anyhow::bail!("Local dependency is not single wit package file");
}
tokio::fs::copy(&local.path, output_path)
.await
.context("Unable to copy local dependency")?;
}
DependencyResolution::Registry(registry) => {
let mut reader = registry.fetch().await?;
let mut output_file = tokio::fs::File::create(output_path).await?;
tokio::io::copy(&mut reader, &mut output_file).await?;
output_file.sync_all().await?;
}
}
}
}
}
Ok(())
}
fn packages_from_foreign_deps(
deps: impl IntoIterator<Item = PackageName>,
) -> impl Iterator<Item = (PackageRef, VersionReq)> {
deps.into_iter().filter_map(|dep| {
let name = PackageRef::new(dep.namespace.parse().ok()?, dep.name.parse().ok()?);
let version = match dep.version {
Some(v) => format!("={v}"),
None => "*".to_string(),
};
Some((
name,
version
.parse()
.expect("Unable to parse into version request, this is programmer error"),
))
})
}
async fn write_local_dep(local: &LocalResolution, output_path: impl AsRef<Path>) -> Result<()> {
let meta = tokio::fs::metadata(&local.path).await?;
if meta.is_file() {
tokio::fs::copy(
&local.path,
output_path.as_ref().join(local.path.file_name().unwrap()),
)
.await?;
} else {
let mut dir = tokio::fs::read_dir(&local.path).await?;
while let Some(entry) = dir.next_entry().await? {
if !entry.metadata().await?.is_file() {
continue;
}
let entry_path = entry.path();
tokio::fs::copy(
&entry_path,
output_path.as_ref().join(entry_path.file_name().unwrap()),
)
.await?;
}
}
Ok(())
}
async fn print_wit_from_resolve(
resolve: &Resolve,
top_level_id: PackageId,
root_deps_dir: &Path,
) -> Result<()> {
for (id, pkg) in resolve
.packages
.iter()
.filter(|(id, _)| *id != top_level_id)
{
let dep_path = root_deps_dir.join(name_from_package_name(&pkg.name));
tokio::fs::create_dir_all(&dep_path).await?;
let mut printer = WitPrinter::default();
printer
.print(resolve, id, &[])
.context("Unable to print wit")?;
tokio::fs::write(dep_path.join("package.wit"), &printer.output.to_string()).await?;
}
Ok(())
}
fn name_from_package_name(package_name: &PackageName) -> String {
let package_name_str = package_name.to_string();
package_name_str.replace([':', '@'], "-")
}