Files
sausage/host/src/builder/files.rs
T

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(&current).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)
}