#![forbid(clippy::unwrap_used)]
#![warn(clippy::pedantic)]
#![warn(missing_docs)]
mod cache;
mod digest;
mod lock;
mod manifest;
pub use cache::{Cache, Local as LocalCache, Write as WriteCache};
pub use digest::{Digest, Reader as DigestReader, Writer as DigestWriter};
pub use lock::{Entry as LockEntry, EntrySource as LockEntrySource, Lock};
pub use manifest::{Entry as ManifestEntry, Manifest};
pub use futures;
pub use tokio;
use std::collections::BTreeSet;
use std::ffi::{OsStr, OsString};
use std::path::Path;
use anyhow::{bail, Context};
use futures::{try_join, AsyncRead, AsyncWrite, FutureExt, Stream, TryStreamExt};
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
use tracing::{debug, instrument};
pub type Identifier = String;
fn is_wit(path: impl AsRef<Path>) -> bool {
path.as_ref()
.extension()
.map(|ext| ext.eq_ignore_ascii_case("wit"))
.unwrap_or_default()
}
#[instrument(level = "trace", skip(path))]
async fn read_wits(
path: impl AsRef<Path>,
) -> std::io::Result<impl Stream<Item = std::io::Result<OsString>>> {
let st = fs::read_dir(path).await.map(ReadDirStream::new)?;
Ok(st.try_filter_map(|e| async move {
let name = e.file_name();
if !is_wit(&name) {
return Ok(None);
}
if e.file_type().await?.is_dir() {
return Ok(None);
}
Ok(Some(name))
}))
}
#[instrument(level = "trace", skip(src, dst))]
async fn copy_wits(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
let src = src.as_ref();
let dst = dst.as_ref();
read_wits(src)
.await
.map_err(|e| std::io::Error::new(e.kind(), format!("failed to read `{}`", src.display())))?
.try_for_each_concurrent(None, |name| async {
let src = src.join(&name);
let dst = dst.join(name);
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"failed to create destination parent directory `{}`: {e}",
parent.display()
),
)
})?;
}
fs::copy(&src, &dst).await.map(|_| ()).map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"failed to copy `{}` to `{}`: {e}",
src.display(),
dst.display()
),
)
})
})
.await
}
#[instrument(level = "trace", skip(tar, dst))]
pub async fn untar(tar: impl AsyncRead + Unpin, dst: impl AsRef<Path>) -> anyhow::Result<()> {
let dst = dst.as_ref();
match fs::remove_dir_all(dst).await {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => bail!("failed to remove `{}`: {e}", dst.display()),
};
fs::create_dir_all(dst)
.await
.with_context(|| format!("failed to create `{}`", dst.display()))?;
async_tar::Archive::new(tar)
.entries()
.context("failed to unpack archive metadata")?
.try_for_each(|mut e| async move {
let path = e.path()?;
let mut path = path.into_iter().map(OsStr::to_str);
match (path.next(), path.next(), path.next(), path.next()) {
(Some(Some("wit")), Some(Some(name)), None, None)
| (Some(_), Some(Some("wit")), Some(Some(name)), None)
if is_wit(name) =>
{
e.unpack(dst.join(name)).await?;
Ok(())
}
_ => Ok(()),
}
})
.await
.context("failed to unpack archive")?;
Ok(())
}
#[instrument(level = "trace", skip(path, dst))]
pub async fn tar<T>(path: impl AsRef<Path>, dst: T) -> std::io::Result<T>
where
T: AsyncWrite + Sync + Send + Unpin,
{
let path = path.as_ref();
let mut tar = async_tar::Builder::new(dst);
tar.mode(async_tar::HeaderMode::Deterministic);
for name in read_wits(path).await?.try_collect::<BTreeSet<_>>().await? {
tar.append_path_with_name(path.join(&name), Path::new("wit").join(name))
.await?;
}
tar.into_inner().await
}
fn cache() -> Option<impl Cache> {
LocalCache::cache_dir().map(|cache| {
debug!("using cache at `{cache}`");
cache
})
}
#[instrument(level = "trace", skip(at, manifest, lock, deps, packages))]
pub async fn lock(
at: Option<impl AsRef<Path>>,
manifest: impl AsRef<str>,
lock: Option<impl AsRef<str>>,
deps: impl AsRef<Path>,
packages: impl IntoIterator<Item = &Identifier>,
) -> anyhow::Result<Option<String>> {
let manifest: Manifest =
toml::from_str(manifest.as_ref()).context("failed to decode manifest")?;
let old_lock = lock
.as_ref()
.map(AsRef::as_ref)
.map(toml::from_str)
.transpose()
.context("failed to decode lock")?;
let deps = deps.as_ref();
let lock = manifest
.lock(at, deps, old_lock.as_ref(), cache().as_ref(), packages)
.await
.with_context(|| format!("failed to lock deps to `{}`", deps.display()))?;
match old_lock {
Some(old_lock) if lock == old_lock => Ok(None),
_ => toml::to_string(&lock)
.map(Some)
.context("failed to encode lock"),
}
}
#[instrument(level = "trace", skip(at, manifest, deps, packages))]
pub async fn update(
at: Option<impl AsRef<Path>>,
manifest: impl AsRef<str>,
deps: impl AsRef<Path>,
packages: impl IntoIterator<Item = &Identifier>,
) -> anyhow::Result<String> {
let manifest: Manifest =
toml::from_str(manifest.as_ref()).context("failed to decode manifest")?;
let deps = deps.as_ref();
let lock = manifest
.lock(at, deps, None, cache().map(WriteCache).as_ref(), packages)
.await
.with_context(|| format!("failed to lock deps to `{}`", deps.display()))?;
toml::to_string(&lock).context("failed to encode lock")
}
async fn read_manifest_string(path: impl AsRef<Path>) -> std::io::Result<String> {
let path = path.as_ref();
fs::read_to_string(&path).await.map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to read manifest at `{}`: {e}", path.display()),
)
})
}
async fn write_lock(path: impl AsRef<Path>, buf: impl AsRef<[u8]>) -> std::io::Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"failed to create lock parent directory `{}`: {e}",
parent.display()
),
)
})?;
}
fs::write(&path, &buf).await.map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to write lock to `{}`: {e}", path.display()),
)
})
}
#[instrument(level = "trace", skip(manifest_path, lock_path, deps, packages))]
pub async fn lock_path(
at: Option<&Path>,
manifest_path: impl AsRef<Path>,
lock_path: impl AsRef<Path>,
deps: impl AsRef<Path>,
packages: impl IntoIterator<Item = &Identifier>,
) -> anyhow::Result<bool> {
let lock_path = lock_path.as_ref();
let (manifest, lock) = try_join!(
read_manifest_string(manifest_path),
fs::read_to_string(&lock_path).map(|res| match res {
Ok(lock) => Ok(Some(lock)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(std::io::Error::new(
e.kind(),
format!("failed to read lock at `{}`: {e}", lock_path.display())
)),
}),
)?;
if let Some(lock) = self::lock(at, manifest, lock, deps, packages)
.await
.context("failed to lock dependencies")?
{
write_lock(lock_path, lock).await?;
Ok(true)
} else {
Ok(false)
}
}
#[instrument(level = "trace", skip(manifest_path, lock_path, deps, packages))]
pub async fn update_path(
at: Option<&Path>,
manifest_path: impl AsRef<Path>,
lock_path: impl AsRef<Path>,
deps: impl AsRef<Path>,
packages: impl IntoIterator<Item = &Identifier>,
) -> anyhow::Result<()> {
let manifest = read_manifest_string(manifest_path).await?;
let lock = self::update(at, manifest, deps, packages)
.await
.context("failed to lock dependencies")?;
write_lock(lock_path, lock).await?;
Ok(())
}
#[macro_export]
macro_rules! lock {
() => {
$crate::lock!("wit")
};
($dir:literal $(,)?) => {
async {
use $crate::tokio::fs;
use std::io::{Error, ErrorKind};
let lock = match fs::read_to_string(concat!($dir, "/deps.lock")).await {
Ok(lock) => Some(lock),
Err(e) if e.kind() == ErrorKind::NotFound => None,
Err(e) => {
return Err(Error::new(
e.kind(),
format!(
"failed to read lock at `{}`: {e}",
concat!($dir, "/deps.lock")
),
))
}
};
match $crate::lock(
Some($dir),
include_str!(concat!($dir, "/deps.toml")),
lock,
concat!($dir, "/deps"),
None,
)
.await
{
Ok(Some(lock)) => fs::write(concat!($dir, "/deps.lock"), lock)
.await
.map_err(|e| {
Error::new(
e.kind(),
format!(
"failed to write lock at `{}`: {e}",
concat!($dir, "/deps.lock")
),
)
}),
Ok(None) => Ok(()),
Err(e) => Err(Error::new(ErrorKind::Other, e)),
}
}
};
}
#[cfg(feature = "sync")]
#[macro_export]
macro_rules! lock_sync {
($($args:tt)*) => {
$crate::tokio::runtime::Builder::new_multi_thread()
.thread_name("depit/lock_sync")
.enable_io()
.enable_time()
.build()?
.block_on($crate::lock!($($args)*))
};
}