1#![forbid(clippy::unwrap_used)]
4#![warn(missing_docs)]
5
6mod cache;
7mod digest;
8mod lock;
9mod manifest;
10
11pub use cache::{Cache, Local as LocalCache, Write as WriteCache};
12pub use digest::{Digest, Reader as DigestReader, Writer as DigestWriter};
13pub use lock::{Entry as LockEntry, EntrySource as LockEntrySource, Lock};
14pub use manifest::{Entry as ManifestEntry, Manifest};
15
16pub use futures;
17pub use tokio;
18
19use core::array;
20
21use std::collections::{BTreeSet, HashMap, HashSet};
22use std::ffi::{OsStr, OsString};
23use std::path::{Path, PathBuf};
24
25use anyhow::Context;
26use futures::{try_join, AsyncRead, AsyncWrite, FutureExt, Stream, TryStreamExt};
27use tokio::fs;
28use tokio_stream::wrappers::ReadDirStream;
29use tracing::{debug, instrument, trace};
30
31pub type Identifier = String;
33fn is_wit(path: impl AsRef<Path>) -> bool {
38 path.as_ref()
39 .extension()
40 .is_some_and(|ext| ext.eq_ignore_ascii_case("wit"))
41}
42
43#[instrument(level = "trace", skip(path))]
44async fn remove_dir_all(path: impl AsRef<Path>) -> std::io::Result<()> {
45 let path = path.as_ref();
46 match fs::remove_dir_all(path).await {
47 Ok(()) => {
48 trace!("removed `{}`", path.display());
49 Ok(())
50 }
51 Err(e) => Err(std::io::Error::new(
52 e.kind(),
53 format!("failed to remove `{}`: {e}", path.display()),
54 )),
55 }
56}
57
58#[instrument(level = "trace", skip(path))]
59async fn recreate_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
60 let path = path.as_ref();
61 match remove_dir_all(path).await {
62 Ok(()) => {}
63 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
64 Err(e) => return Err(e),
65 }
66 fs::create_dir_all(path)
67 .await
68 .map(|()| trace!("recreated `{}`", path.display()))
69 .map_err(|e| {
70 std::io::Error::new(
71 e.kind(),
72 format!("failed to create `{}`: {e}", path.display()),
73 )
74 })
75}
76
77#[instrument(level = "trace", skip(path))]
79async fn read_wits(
80 path: impl AsRef<Path>,
81) -> std::io::Result<impl Stream<Item = std::io::Result<OsString>>> {
82 let path = path.as_ref();
83 let st = fs::read_dir(path)
84 .await
85 .map(ReadDirStream::new)
86 .map_err(|e| {
87 std::io::Error::new(
88 e.kind(),
89 format!("failed to read directory at `{}`: {e}", path.display()),
90 )
91 })?;
92 Ok(st.try_filter_map(|e| async move {
93 let name = e.file_name();
94 if !is_wit(&name) {
95 trace!("{} is not a WIT definition, skip", name.to_string_lossy());
96 return Ok(None);
97 }
98 if e.file_type().await?.is_dir() {
99 trace!("{} is a directory, skip", name.to_string_lossy());
100 return Ok(None);
101 }
102 Ok(Some(name))
103 }))
104}
105
106#[instrument(level = "trace", skip(src, dst))]
108async fn install_wits(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
109 let src = src.as_ref();
110 let dst = dst.as_ref();
111 recreate_dir(dst).await?;
112 read_wits(src)
113 .await?
114 .try_for_each_concurrent(None, |name| async {
115 let src = src.join(&name);
116 let dst = dst.join(name);
117 fs::copy(&src, &dst)
118 .await
119 .map(|_| trace!("copied `{}` to `{}`", src.display(), dst.display()))
120 .map_err(|e| {
121 std::io::Error::new(
122 e.kind(),
123 format!(
124 "failed to copy `{}` to `{}`: {e}",
125 src.display(),
126 dst.display()
127 ),
128 )
129 })
130 })
131 .await
132}
133
134#[instrument(level = "trace", skip(src, dst, skip_deps))]
137async fn copy_wits(
138 src: impl AsRef<Path>,
139 dst: impl AsRef<Path>,
140 skip_deps: &HashSet<Identifier>,
141) -> std::io::Result<HashMap<Identifier, PathBuf>> {
142 let src = src.as_ref();
143 let deps = src.join("deps");
144 let dst = dst.as_ref();
145 try_join!(install_wits(src, dst), async {
146 match (dst.parent(), fs::read_dir(&deps).await) {
147 (Some(base), Ok(dir)) => {
148 ReadDirStream::new(dir)
149 .try_filter_map(|e| async move {
150 let name = e.file_name();
151 let Some(id) = name.to_str().map(Identifier::from) else {
152 return Ok(None);
153 };
154 if skip_deps.contains(&id) {
155 return Ok(None);
156 }
157 let ft = e.file_type().await?;
158 if !(ft.is_dir()
159 || ft.is_symlink() && fs::metadata(e.path()).await?.is_dir())
160 {
161 return Ok(None);
162 }
163 Ok(Some(id))
164 })
165 .and_then(|id| async {
166 let dst = base.join(&id);
167 install_wits(deps.join(&id), &dst).await?;
168 Ok((id, dst))
169 })
170 .try_collect()
171 .await
172 }
173 (None, _) => Ok(HashMap::default()),
174 (_, Err(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(HashMap::default()),
175 (_, Err(e)) => Err(std::io::Error::new(
176 e.kind(),
177 format!("failed to read directory at `{}`: {e}", deps.display()),
178 )),
179 }
180 })
181 .map(|((), ids)| ids)
182}
183
184#[instrument(level = "trace", skip(tar, dst, skip_deps))]
191pub async fn untar(
192 tar: impl AsyncRead + Unpin,
193 dst: impl AsRef<Path>,
194 skip_deps: &HashSet<Identifier>,
195 prefix: &str,
196) -> std::io::Result<HashMap<Identifier, PathBuf>> {
197 use std::io::{Error, Result};
198
199 async fn unpack(e: &mut async_tar::Entry<impl Unpin + AsyncRead>, dst: &Path) -> Result<()> {
200 e.unpack(dst).await.map_err(|e| {
201 Error::new(
202 e.kind(),
203 format!("failed to unpack `{}`: {e}", dst.display()),
204 )
205 })?;
206 trace!("unpacked `{}`", dst.display());
207 Ok(())
208 }
209
210 let dst = dst.as_ref();
211 recreate_dir(dst).await?;
212 async_tar::Archive::new(tar)
213 .entries()
214 .map_err(|e| Error::new(e.kind(), format!("failed to unpack archive metadata: {e}")))?
215 .try_fold(HashMap::default(), |mut untared, mut e| async move {
216 let path = e
217 .path()
218 .map_err(|e| Error::new(e.kind(), format!("failed to query entry path: {e}")))?;
219 let Ok(path) = path.strip_prefix(prefix) else {
220 return Ok(untared);
221 };
222 let mut path = path.into_iter();
223 match array::from_fn::<_, 6, _>(|_| path.next().and_then(OsStr::to_str)) {
224 [Some(name), None, ..]
225 | [Some("wit"), Some(name), None, ..]
226 | [Some(_), Some("wit"), Some(name), None, ..]
227 if is_wit(name) =>
228 {
229 let dst = dst.join(name);
230 unpack(&mut e, &dst).await?;
231 Ok(untared)
232 }
233 [Some("deps"), Some(id), Some(name), None, ..]
234 | [Some("wit"), Some("deps"), Some(id), Some(name), None, ..]
235 | [Some(_), Some("wit"), Some("deps"), Some(id), Some(name), None]
236 if !skip_deps.contains(id) && is_wit(name) =>
237 {
238 let id = Identifier::from(id);
239 if let Some(base) = dst.parent() {
240 let dst = base.join(&id);
241 if !untared.contains_key(&id) {
242 recreate_dir(&dst).await?;
243 }
244 let wit = dst.join(name);
245 unpack(&mut e, &wit).await?;
246 untared.insert(id, dst);
247 Ok(untared)
248 } else {
249 Ok(untared)
250 }
251 }
252 _ => Ok(untared),
253 }
254 })
255 .await
256}
257
258#[instrument(level = "trace", skip(path, dst))]
264pub async fn tar<T>(path: impl AsRef<Path>, dst: T) -> std::io::Result<T>
265where
266 T: AsyncWrite + Sync + Send + Unpin,
267{
268 let path = path.as_ref();
269 let mut tar = async_tar::Builder::new(dst);
270 tar.mode(async_tar::HeaderMode::Deterministic);
271 for name in read_wits(path).await?.try_collect::<BTreeSet<_>>().await? {
272 tar.append_path_with_name(path.join(&name), Path::new("wit").join(name))
273 .await?;
274 }
275 tar.into_inner().await
276}
277
278fn cache() -> Option<impl Cache> {
279 LocalCache::cache_dir().map(|cache| {
280 debug!("using cache at `{cache}`");
281 cache
282 })
283}
284
285#[instrument(level = "trace", skip(at, manifest, lock, deps))]
293pub async fn lock(
294 at: Option<impl AsRef<Path>>,
295 manifest: impl AsRef<str>,
296 lock: Option<impl AsRef<str>>,
297 deps: impl AsRef<Path>,
298) -> anyhow::Result<Option<String>> {
299 let manifest: Manifest =
300 toml::from_str(manifest.as_ref()).context("failed to decode manifest")?;
301
302 let old_lock = lock
303 .as_ref()
304 .map(AsRef::as_ref)
305 .map(toml::from_str)
306 .transpose()
307 .context("failed to decode lock")?;
308
309 let deps = deps.as_ref();
310 let lock = manifest
311 .lock(at, deps, old_lock.as_ref(), cache().as_ref())
312 .await
313 .with_context(|| format!("failed to lock deps to `{}`", deps.display()))?;
314 match old_lock {
315 Some(old_lock) if lock == old_lock => Ok(None),
316 _ => toml::to_string(&lock)
317 .map(Some)
318 .context("failed to encode lock"),
319 }
320}
321
322#[instrument(level = "trace", skip(at, manifest, deps))]
330pub async fn update(
331 at: Option<impl AsRef<Path>>,
332 manifest: impl AsRef<str>,
333 deps: impl AsRef<Path>,
334) -> anyhow::Result<String> {
335 let manifest: Manifest =
336 toml::from_str(manifest.as_ref()).context("failed to decode manifest")?;
337
338 let deps = deps.as_ref();
339 let lock = manifest
340 .lock(at, deps, None, cache().map(WriteCache).as_ref())
341 .await
342 .with_context(|| format!("failed to lock deps to `{}`", deps.display()))?;
343 toml::to_string(&lock).context("failed to encode lock")
344}
345
346async fn read_manifest_string(path: impl AsRef<Path>) -> std::io::Result<String> {
347 let path = path.as_ref();
348 fs::read_to_string(&path).await.map_err(|e| {
349 std::io::Error::new(
350 e.kind(),
351 format!("failed to read manifest at `{}`: {e}", path.display()),
352 )
353 })
354}
355
356async fn write_lock(path: impl AsRef<Path>, buf: impl AsRef<[u8]>) -> std::io::Result<()> {
357 let path = path.as_ref();
358 if let Some(parent) = path.parent() {
359 fs::create_dir_all(parent).await.map_err(|e| {
360 std::io::Error::new(
361 e.kind(),
362 format!(
363 "failed to create lock parent directory `{}`: {e}",
364 parent.display()
365 ),
366 )
367 })?;
368 }
369 fs::write(&path, &buf).await.map_err(|e| {
370 std::io::Error::new(
371 e.kind(),
372 format!("failed to write lock to `{}`: {e}", path.display()),
373 )
374 })
375}
376
377#[instrument(level = "trace", skip(manifest_path, lock_path, deps))]
385pub async fn lock_path(
386 manifest_path: impl AsRef<Path>,
387 lock_path: impl AsRef<Path>,
388 deps: impl AsRef<Path>,
389) -> anyhow::Result<bool> {
390 let manifest_path = manifest_path.as_ref();
391 let lock_path = lock_path.as_ref();
392 let (manifest, lock) = try_join!(
393 read_manifest_string(manifest_path),
394 fs::read_to_string(&lock_path).map(|res| match res {
395 Ok(lock) => Ok(Some(lock)),
396 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
397 Err(e) => Err(std::io::Error::new(
398 e.kind(),
399 format!("failed to read lock at `{}`: {e}", lock_path.display())
400 )),
401 }),
402 )?;
403 if let Some(lock) = self::lock(manifest_path.parent(), manifest, lock, deps)
404 .await
405 .context("failed to lock dependencies")?
406 {
407 write_lock(lock_path, lock).await?;
408 Ok(true)
409 } else {
410 Ok(false)
411 }
412}
413
414#[instrument(level = "trace", skip(manifest_path, lock_path, deps))]
420pub async fn update_path(
421 manifest_path: impl AsRef<Path>,
422 lock_path: impl AsRef<Path>,
423 deps: impl AsRef<Path>,
424) -> anyhow::Result<()> {
425 let manifest_path = manifest_path.as_ref();
426 let manifest = read_manifest_string(manifest_path).await?;
427 let lock = self::update(manifest_path.parent(), manifest, deps)
428 .await
429 .context("failed to lock dependencies")?;
430 write_lock(lock_path, lock).await?;
431 Ok(())
432}
433
434#[macro_export]
437macro_rules! lock {
438 () => {
439 $crate::lock!("wit")
440 };
441 ($dir:literal $(,)?) => {
442 async {
443 use $crate::tokio::fs;
444
445 use std::io::{Error, ErrorKind};
446
447 let lock = match fs::read_to_string(concat!($dir, "/deps.lock")).await {
448 Ok(lock) => Some(lock),
449 Err(e) if e.kind() == ErrorKind::NotFound => None,
450 Err(e) => {
451 return Err(Error::new(
452 e.kind(),
453 format!(
454 "failed to read lock at `{}`: {e}",
455 concat!($dir, "/deps.lock")
456 ),
457 ))
458 }
459 };
460 match $crate::lock(
461 Some($dir),
462 include_str!(concat!($dir, "/deps.toml")),
463 lock,
464 concat!($dir, "/deps"),
465 )
466 .await
467 {
468 Ok(Some(lock)) => fs::write(concat!($dir, "/deps.lock"), lock)
469 .await
470 .map_err(|e| {
471 Error::new(
472 e.kind(),
473 format!(
474 "failed to write lock at `{}`: {e}",
475 concat!($dir, "/deps.lock")
476 ),
477 )
478 }),
479 Ok(None) => Ok(()),
480 Err(e) => Err(Error::new(ErrorKind::Other, e)),
481 }
482 }
483 };
484}
485
486#[cfg(feature = "sync")]
487#[macro_export]
489macro_rules! lock_sync {
490 ($($args:tt)*) => {
491 $crate::tokio::runtime::Builder::new_multi_thread()
492 .thread_name("wit-deps/lock_sync")
493 .enable_io()
494 .enable_time()
495 .build()?
496 .block_on($crate::lock!($($args)*))
497 };
498}