162 lines
5.4 KiB
Rust
162 lines
5.4 KiB
Rust
//! Workspace discovery and source/artifact copies through the world's shared VFS.
|
|
use anyhow::{Context, Result};
|
|
use sausage_vfs::{Layer, Open};
|
|
use std::{collections::HashMap, path::Path};
|
|
|
|
/// Source hashes before the native command changes its copy.
|
|
pub type Snapshot = HashMap<String, [u8; 32]>;
|
|
|
|
pub async fn read(root: &dyn Layer, path: &str) -> Result<Vec<u8>> {
|
|
let file = root
|
|
.open(
|
|
path,
|
|
Open {
|
|
follow: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await?;
|
|
let size = file.stat().await?.size;
|
|
Ok(file.read(0, usize::try_from(size)?).await?)
|
|
}
|
|
pub async fn workspace(root: &dyn Layer, cwd: &str) -> Result<String> {
|
|
let mut path = sausage_vfs::resolve("/", cwd);
|
|
let mut nearest = None;
|
|
loop {
|
|
let manifest = sausage_vfs::resolve(&path, "Cargo.toml");
|
|
if let Ok(bytes) = read(root, &manifest).await {
|
|
let value: toml::Value = toml::from_str(std::str::from_utf8(&bytes)?)?;
|
|
if value.get("workspace").is_some() {
|
|
return Ok(path);
|
|
}
|
|
if nearest.is_none() {
|
|
nearest = Some(path.clone());
|
|
}
|
|
}
|
|
if path == "/" {
|
|
break;
|
|
}
|
|
path = sausage_vfs::resolve(&path, "..");
|
|
}
|
|
nearest.context("no Cargo.toml above cwd")
|
|
}
|
|
pub async fn copy_out(root: &dyn Layer, source: &str, destination: &Path) -> Result<Snapshot> {
|
|
std::fs::create_dir_all(destination)?;
|
|
let mut before = Snapshot::new();
|
|
for entry in root.list(source).await? {
|
|
if matches!(entry.name.as_str(), "target" | ".git") {
|
|
continue;
|
|
}
|
|
let from = sausage_vfs::resolve(source, &entry.name);
|
|
let to = destination.join(&entry.name);
|
|
if entry.symlink {
|
|
std::os::unix::fs::symlink(root.readlink(&from).await?, &to)?;
|
|
} else if entry.directory {
|
|
before.extend(Box::pin(copy_out(root, &from, &to)).await?);
|
|
} else {
|
|
let bytes = read(root, &from).await?;
|
|
before.insert(from, openssl::sha::sha256(&bytes));
|
|
std::fs::write(to, bytes)?;
|
|
}
|
|
}
|
|
Ok(before)
|
|
}
|
|
pub async fn mkdir(root: &dyn Layer, path: &str) -> Result<()> {
|
|
let mut current = String::new();
|
|
for part in path.split('/').filter(|p| !p.is_empty()) {
|
|
current.push('/');
|
|
current.push_str(part);
|
|
if let Err(error) = root.mkdir(¤t).await
|
|
&& error.kind() != std::io::ErrorKind::AlreadyExists
|
|
{
|
|
return Err(error.into());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
pub async fn write(root: &dyn Layer, path: &str, bytes: Vec<u8>) -> Result<()> {
|
|
if read(root, path).await.ok().as_deref() == Some(bytes.as_slice()) {
|
|
return Ok(());
|
|
}
|
|
mkdir(root, path.rsplit_once('/').unwrap().0).await?;
|
|
let file = root
|
|
.open(
|
|
path,
|
|
Open {
|
|
follow: true,
|
|
write: true,
|
|
create: true,
|
|
truncate: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await?;
|
|
let mut offset = 0;
|
|
while offset < bytes.len() {
|
|
let written = file.write(offset as u64, bytes[offset..].to_vec()).await?;
|
|
anyhow::ensure!(written != 0, "source write made no progress");
|
|
offset += written;
|
|
}
|
|
Ok(())
|
|
}
|
|
/// A snapshot selects changed sources. Without a snapshot, publish only WASM artifacts.
|
|
pub async fn copy_back(
|
|
root: &dyn Layer,
|
|
source: &Path,
|
|
destination: &str,
|
|
before: Option<&Snapshot>,
|
|
) -> Result<()> {
|
|
for entry in std::fs::read_dir(source)? {
|
|
let entry = entry?;
|
|
let name = entry.file_name().to_string_lossy().into_owned();
|
|
if matches!(name.as_str(), "target" | ".git") {
|
|
continue;
|
|
}
|
|
let to = sausage_vfs::resolve(destination, &name);
|
|
if entry.file_type()?.is_symlink() {
|
|
continue;
|
|
}
|
|
if entry.file_type()?.is_dir() {
|
|
Box::pin(copy_back(root, &entry.path(), &to, before)).await?;
|
|
} else if before.is_some() || entry.path().extension().is_some_and(|e| e == "wasm") {
|
|
let bytes = std::fs::read(entry.path())?;
|
|
if before.is_none_or(|before| before.get(&to) != Some(&openssl::sha::sha256(&bytes))) {
|
|
write(root, &to, bytes).await?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove published artifacts that Cargo clean removed from the native target.
|
|
pub async fn clean_artifacts(root: &dyn Layer, native: &Path, world: &str) -> Result<()> {
|
|
let entries = match root.list(world).await {
|
|
Ok(entries) => entries,
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
|
Err(error) => return Err(error.into()),
|
|
};
|
|
for entry in entries {
|
|
let path = sausage_vfs::resolve(world, &entry.name);
|
|
let native = native.join(&entry.name);
|
|
if entry.directory {
|
|
Box::pin(clean_artifacts(root, &native, &path)).await?;
|
|
} else if entry.name.ends_with(".wasm") && !native.exists() {
|
|
root.remove_file(&path).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn size(path: &Path) -> Result<u64> {
|
|
let mut total = 0;
|
|
for entry in std::fs::read_dir(path)? {
|
|
let entry = entry?;
|
|
total += if entry.file_type()?.is_dir() {
|
|
size(&entry.path())?
|
|
} else {
|
|
entry.metadata()?.len()
|
|
};
|
|
}
|
|
Ok(total)
|
|
}
|