use std::{
collections::BTreeMap,
fs::{self, File},
io::{BufWriter, Read, Seek, SeekFrom, Write},
os::unix::fs::OpenOptionsExt,
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
};
use sha2::{Digest, Sha256};
use super::{
super::{
alloc,
config::{self, CONFIG_JSON_CAP, HIDDEN_SIZE, POSITION_ROWS, POSITION_WIDTH},
error::{
AllocationSite, AmbiguousArtifact, Artifact, CompileFailure, Error, ImageField, IoFailure,
IoOperation, Result, TableCell, TokenizerImageDefect,
},
table::{POSITION_SENTINEL_CELLS, TOKEN_SENTINEL_CELLS},
tokenizer::{
compile::{TOKENIZER_JSON_CAP, compile_image, image_name, past_cap},
image::{HEADER_SIZE, TOKENIZER_IMAGE_VERSION, header},
},
},
hex, manifest_sha256,
naming::{ID_HEX, compiled_bundle, parse_compiled_bundle, source_package},
stamp::{
BundleEntry, Currency, FileEntry, ImageEntry, KIT, LOOSE, Origin, PACKAGES, STAMP_FORMAT,
STAMP_NAME, SentinelEntry, Stamp,
},
tree::{self, Kind, PastCap, Refusal, Resolved, io, resolve},
};
use crate::Model;
const LOCK_NAME: &str = ".coremlit-embeddinggemma2.lock";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CompileReport {
compiled: Vec<String>,
kept: Vec<String>,
}
impl CompileReport {
pub fn compiled(&self) -> &[String] {
&self.compiled
}
pub fn kept(&self) -> &[String] {
&self.kept
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PruneReport {
removed: Vec<PathBuf>,
}
impl PruneReport {
pub fn removed(&self) -> &[PathBuf] {
&self.removed
}
}
fn temporary(final_path: &Path) -> PathBuf {
static NONCE: AtomicU64 = AtomicU64::new(0);
let nonce = NONCE.fetch_add(1, Ordering::Relaxed);
let mut name = final_path.as_os_str().to_owned();
name.push(format!(".tmp-{}-{nonce}", std::process::id()));
PathBuf::from(name)
}
fn lock(dir: &Path) -> Result<File> {
let lock_path = dir.join(LOCK_NAME);
let lock = File::options()
.create(true)
.truncate(false)
.write(true)
.custom_flags(libc::O_NOFOLLOW)
.open(&lock_path)
.map_err(io(IoOperation::Lock, &lock_path))?;
lock.lock().map_err(io(IoOperation::Lock, &lock_path))?;
Ok(lock)
}
#[derive(Clone, Copy)]
struct Capped {
cap: u64,
refuse: fn(PastCap) -> Error,
}
const fn capped(artifact: Artifact) -> Option<Capped> {
match artifact {
Artifact::TokenizerSource => Some(Capped {
cap: TOKENIZER_JSON_CAP,
refuse: past_cap,
}),
Artifact::Config => Some(Capped {
cap: CONFIG_JSON_CAP,
refuse: config::past_cap,
}),
_ => None,
}
}
fn named(path: &Path, capped: Option<Capped>) -> impl FnOnce(Refusal) -> Error + '_ {
move |refusal| match refusal {
Refusal::Absent => io(IoOperation::Read, path)(std::io::ErrorKind::NotFound.into()),
Refusal::PastCap(past) => match capped {
Some(capped) => (capped.refuse)(past),
None => {
let what = format!("`{}`", path.display());
io(IoOperation::Read, path)(std::io::Error::other(past.sentence(&what)))
}
},
Refusal::Named(error) => error,
Refusal::Failed(e) => io(IoOperation::Read, path)(e),
}
}
fn digest(path: &Path, capped: Option<Capped>) -> Result<[u8; 32]> {
let mut hasher = Sha256::new();
tree::stream(path, capped.map(|capped| capped.cap), |bytes| {
hasher.update(bytes);
})
.map_err(named(path, capped))?;
Ok(hasher.finalize().into())
}
fn files_under(base: &Path, root: &Path) -> Result<Vec<(String, Currency)>> {
let entries = tree::list(root)?;
let mut out = alloc::with_capacity(entries.len(), AllocationSite::StampDigests)?;
for entry in &entries {
if entry.kind == Kind::File {
let rel = entry
.path
.strip_prefix(base)
.unwrap_or(&entry.path)
.to_string_lossy()
.into_owned();
out.push((rel, Currency::of(&entry.metadata)));
}
}
out.sort_unstable_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
Ok(out)
}
fn is_dir(path: &Path) -> Result<bool> {
Ok(resolve(path)?.is_some_and(|found| found.is_dir()))
}
fn sources(dir: &Path) -> Result<BTreeMap<String, Currency>> {
let mut out = BTreeMap::new();
for package in &PACKAGES {
for root in [
dir.join(source_package(package.stem)),
dir.join(format!("{}.mlmodelc", package.stem)),
] {
if is_dir(&root)? {
out.extend(files_under(dir, &root)?);
}
}
}
for (rel, _) in &LOOSE {
if let Some(found) = resolve(&dir.join(rel))?.filter(Resolved::is_file) {
out.insert((*rel).to_owned(), Currency::of(&found.metadata));
}
}
Ok(out)
}
fn current(dir: &Path) -> Result<Option<Stamp>> {
let stamp = match Stamp::read(dir) {
Ok(stamp) => stamp,
Err(Error::Unstamped(_)) => return Ok(None),
Err(error) => return Err(error),
};
let present = sources(dir)?;
let sources_unchanged = present.len() == stamp.files.len()
&& present
.iter()
.zip(&stamp.files)
.all(|((rel, currency), (stamped, entry))| rel == stamped && *currency == entry.currency);
let mut derived_exist = true;
for bundle in stamp.bundles.values() {
derived_exist &= is_dir(&dir.join(&bundle.name))?;
}
if let Some(image) = &stamp.tokenizer_image {
derived_exist &= resolve(&dir.join(&image.name))?.is_some_and(|found| found.is_file());
}
let mut supplied_unchanged = true;
for package in &PACKAGES {
let plain = is_dir(&dir.join(format!("{}.mlmodelc", package.stem)))?;
let stamped_supplied = stamp
.bundles
.get(package.key)
.is_some_and(|b| b.origin == Origin::Supplied);
supplied_unchanged &= plain == stamped_supplied;
}
Ok((sources_unchanged && derived_exist && supplied_unchanged).then_some(stamp))
}
pub fn compile_artifacts(dir: impl AsRef<Path>) -> Result<CompileReport> {
let dir = dir.as_ref();
if let Some(stamp) = current(dir)? {
return kept_report(&stamp);
}
let _lock = lock(dir)?;
if let Some(stamp) = current(dir)? {
return kept_report(&stamp);
}
build(dir)
}
fn kept_report(stamp: &Stamp) -> Result<CompileReport> {
let mut kept = alloc::with_capacity(stamp.bundles.len() + 1, AllocationSite::StampDigests)?;
kept.extend(stamp.bundles.values().map(|b| b.name.clone()));
kept.extend(stamp.tokenizer_image.as_ref().map(|i| i.name.clone()));
Ok(CompileReport {
compiled: Vec::new(),
kept,
})
}
enum Image {
Kept(ImageEntry),
Built(Built),
}
struct Built {
entry: ImageEntry,
bytes: Vec<u8>,
}
fn build(dir: &Path) -> Result<CompileReport> {
let previous = match Stamp::read(dir) {
Ok(stamp) => Some(stamp),
Err(Error::Unstamped(_)) => None,
Err(error) => return Err(error),
};
for (rel, artifact) in &LOOSE {
if let Some(capped) = capped(*artifact)
&& let Some(found) = resolve(&dir.join(rel))?.filter(Resolved::is_file)
&& found.metadata.len() > capped.cap
{
return Err((capped.refuse)(PastCap::at_size(
found.metadata.len(),
capped.cap,
)));
}
}
let mut report = CompileReport::default();
let mut files = BTreeMap::new();
for (rel, artifact) in &LOOSE {
let path = dir.join(rel);
let Some(found) = resolve(&path)?.filter(Resolved::is_file) else {
continue;
};
let currency = Currency::of(&found.metadata);
let reused = previous
.as_ref()
.and_then(|stamp| stamp.files.get(*rel))
.filter(|entry| entry.currency == currency)
.cloned();
let entry = match reused {
Some(entry) => entry,
None => {
let sha256 = hex(&digest(&path, capped(*artifact))?);
let sentinels = match artifact {
Artifact::TokenTable => read_sentinels(&path, &TOKEN_SENTINEL_CELLS, |c| {
(u64::from(c.row()) * HIDDEN_SIZE as u64 + u64::from(c.column())) * 2
})?,
Artifact::PositionTable => read_sentinels(&path, &POSITION_SENTINEL_CELLS, |c| {
let row = u64::from(c.table().unwrap_or(0)) * POSITION_ROWS as u64 + u64::from(c.row());
(row * POSITION_WIDTH as u64 + u64::from(c.column())) * 2
})?,
_ => Vec::new(),
};
FileEntry {
sha256,
currency,
sentinels,
}
}
};
files.insert((*rel).to_owned(), entry);
}
let image = match files.get("tokenizer.json") {
Some(source) => Some(tokenizer_image(dir, &source.sha256)?),
None => None,
};
let mut bundles = BTreeMap::new();
for package in &PACKAGES {
let source = dir.join(source_package(package.stem));
let plain = dir.join(format!("{}.mlmodelc", package.stem));
match (is_dir(&source)?, is_dir(&plain)?) {
(true, true) => {
return Err(Error::AmbiguousArtifact(AmbiguousArtifact::new(
package.artifact,
vec![source, plain],
)));
}
(true, false) => {
let digests = digest_files(dir, &source, &mut files)?;
let lines = digests
.iter()
.map(|(rel, sha)| (rel.as_str(), sha.as_str()));
let name = compiled_bundle(package.stem, &hex(&manifest_sha256(lines))[..ID_HEX]);
let final_path = dir.join(&name);
if is_dir(&final_path)? {
report.kept.push(name.clone());
} else {
compile_package(package.artifact, &source, &final_path)?;
report.compiled.push(name.clone());
}
bundles.insert(
package.key.to_owned(),
BundleEntry {
name,
origin: Origin::Compiled,
digests,
},
);
}
(false, true) => {
let digests = digest_files(dir, &plain, &mut files)?;
report.kept.push(format!("{}.mlmodelc", package.stem));
bundles.insert(
package.key.to_owned(),
BundleEntry {
name: format!("{}.mlmodelc", package.stem),
origin: Origin::Supplied,
digests,
},
);
}
(false, false) => {}
}
}
let tokenizer_image = match image {
Some(Image::Kept(entry)) => {
report.kept.push(entry.name.clone());
Some(entry)
}
Some(Image::Built(Built { entry, bytes })) => {
write_new(&dir.join(&entry.name), false, |out| out.write_all(&bytes))?;
report.compiled.push(entry.name.clone());
Some(entry)
}
None => None,
};
let stamp = Stamp {
format: STAMP_FORMAT,
kit: KIT.to_owned(),
coremlit: env!("CARGO_PKG_VERSION").to_owned(),
os_build: os_build(),
files,
bundles,
tokenizer_image,
};
write_new(&dir.join(STAMP_NAME), true, |out| {
serde_json::to_writer_pretty(out, &stamp).map_err(std::io::Error::other)
})?;
Ok(report)
}
fn digest_files(
dir: &Path,
root: &Path,
files: &mut BTreeMap<String, FileEntry>,
) -> Result<BTreeMap<String, String>> {
let mut digests = BTreeMap::new();
for (rel, currency) in files_under(dir, root)? {
let sha256 = hex(&digest(&dir.join(&rel), None)?);
digests.insert(rel.clone(), sha256.clone());
files.insert(
rel,
FileEntry {
sha256,
currency,
sentinels: Vec::new(),
},
);
}
Ok(digests)
}
fn read_sentinels(
path: &Path,
cells: &[TableCell],
offset: impl Fn(TableCell) -> u64,
) -> Result<Vec<SentinelEntry>> {
let mut file = tree::open(path, None).map_err(named(path, None))?.file;
cells
.iter()
.map(|&cell| {
let mut pair = [0u8; 2];
file
.seek(SeekFrom::Start(offset(cell)))
.map_err(io(IoOperation::Read, path))?;
file
.read_exact(&mut pair)
.map_err(io(IoOperation::Read, path))?;
Ok(SentinelEntry {
table: cell.table(),
row: cell.row(),
column: cell.column(),
bits: u16::from_le_bytes(pair),
})
})
.collect()
}
fn check_image(path: &Path) -> Result<(bool, [u8; 32])> {
let mut head = [0u8; HEADER_SIZE];
let mut held = 0;
let (mut whole, mut body) = (Sha256::new(), Sha256::new());
tree::stream(path, None, |bytes| {
whole.update(bytes);
let into_head = (HEADER_SIZE - held).min(bytes.len());
head[held..held + into_head].copy_from_slice(&bytes[..into_head]);
held += into_head;
body.update(&bytes[into_head..]);
})
.map_err(named(path, None))?;
let holds = held == HEADER_SIZE
&& head[header::IMAGE_DIGEST..header::IMAGE_DIGEST + 32] == body.finalize()[..];
Ok((holds, whole.finalize().into()))
}
fn tokenizer_image(dir: &Path, source: &str) -> Result<Image> {
let source_bytes: [u8; 32] = {
let mut out = [0u8; 32];
for (i, pair) in source
.as_bytes()
.as_chunks::<2>()
.0
.iter()
.enumerate()
.take(32)
{
out[i] = u8::from_str_radix(core::str::from_utf8(pair).unwrap_or("00"), 16).unwrap_or(0);
}
out
};
let name = image_name(&source_bytes);
let final_path = dir.join(&name);
if resolve(&final_path)?.is_some_and(|found| found.is_file()) {
let (holds_its_digest, image_sha256) = check_image(&final_path)?;
if holds_its_digest {
return Ok(Image::Kept(ImageEntry {
name,
format: TOKENIZER_IMAGE_VERSION,
source_sha256: source.to_owned(),
image_sha256: hex(&image_sha256),
}));
}
return Err(Error::TokenizerImage(TokenizerImageDefect::new(
ImageField::ImageDigest,
"the image's own digest".to_owned(),
format!("`{name}` holds another; remove it, it is never replaced"),
)));
}
let json_path = dir.join("tokenizer.json");
let json = tree::read(&json_path, TOKENIZER_JSON_CAP, AllocationSite::ArtifactRead)
.map_err(named(&json_path, capped(Artifact::TokenizerSource)))?;
let image = compile_image(&json)?;
let image_sha256 = hex(&Sha256::digest(&image).into());
Ok(Image::Built(Built {
entry: ImageEntry {
name,
format: TOKENIZER_IMAGE_VERSION,
source_sha256: source.to_owned(),
image_sha256,
},
bytes: image,
}))
}
fn write_new(
final_path: &Path,
replace: bool,
write: impl FnOnce(&mut BufWriter<File>) -> std::io::Result<()>,
) -> Result<()> {
if !replace && fs::symlink_metadata(final_path).is_ok() {
return Err(Error::Io(IoFailure::new(
IoOperation::Write,
final_path.to_path_buf(),
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"an artifact's name is never replaced",
),
)));
}
let tmp = temporary(final_path);
let file = File::options()
.write(true)
.create_new(true)
.open(&tmp)
.map_err(io(IoOperation::Write, &tmp))?;
let mut out = BufWriter::new(file);
write(&mut out).map_err(io(IoOperation::Write, &tmp))?;
let file = out.into_inner().map_err(|e| {
Error::Io(IoFailure::new(
IoOperation::Write,
tmp.clone(),
e.into_error(),
))
})?;
file.sync_all().map_err(io(IoOperation::Sync, &tmp))?;
fs::rename(&tmp, final_path).map_err(io(IoOperation::Rename, final_path))
}
fn compile_package(artifact: Artifact, source: &Path, final_path: &Path) -> Result<()> {
let compiled =
Model::compile(source).map_err(|e| Error::Compile(CompileFailure::new(artifact, e)))?;
let tmp = temporary(final_path);
copy_tree(&compiled, &tmp)?;
fs::rename(&tmp, final_path).map_err(io(IoOperation::Rename, final_path))?;
let _ = fs::remove_dir_all(&compiled);
Ok(())
}
fn copy_tree(from: &Path, to: &Path) -> Result<()> {
let entries = tree::list(from)?;
fs::create_dir(to).map_err(io(IoOperation::Write, to))?;
for entry in &entries {
let target = to.join(
entry
.path
.strip_prefix(from)
.expect("a listed path lies under its root"),
);
if entry.kind == Kind::Dir {
fs::create_dir(&target).map_err(io(IoOperation::Write, &target))?;
} else {
fs::copy(&entry.path, &target).map_err(io(IoOperation::Copy, &entry.path))?;
File::open(&target)
.and_then(|f| f.sync_all())
.map_err(io(IoOperation::Sync, &target))?;
}
}
Ok(())
}
fn os_build() -> String {
std::process::Command::new("sw_vers")
.arg("-buildVersion")
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())
.map(|s| s.trim().to_owned())
.unwrap_or_default()
}
fn derived(name: &str) -> bool {
let hex12 = |s: &str| {
s.len() == ID_HEX
&& s
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
};
let bundle = parse_compiled_bundle(name)
.is_some_and(|(stem, _)| PACKAGES.iter().any(|package| package.stem == stem));
let image = name
.strip_prefix("tokenizer.v")
.and_then(|rest| rest.strip_suffix(".gemmabpe"))
.and_then(|rest| rest.split_once('-'))
.is_some_and(|(version, digest)| {
!version.is_empty() && version.bytes().all(|b| b.is_ascii_digit()) && hex12(digest)
});
bundle || image || name.contains(".tmp-")
}
pub fn prune(dir: impl AsRef<Path>) -> Result<PruneReport> {
let dir = dir.as_ref();
let _lock = lock(dir)?;
let stamp = Stamp::read(dir)?;
let named = |name: &str| {
stamp.bundles.values().any(|b| b.name == name)
|| stamp
.tokenizer_image
.as_ref()
.is_some_and(|image| image.name == name)
};
let mut report = PruneReport::default();
for entry in fs::read_dir(dir).map_err(io(IoOperation::Read, dir))? {
let entry = entry.map_err(io(IoOperation::Read, dir))?;
let file_name = entry.file_name();
let Some(name) = file_name.to_str() else {
continue;
};
if !derived(name) || named(name) {
continue;
}
let path = entry.path();
let kind = fs::symlink_metadata(&path)
.map_err(io(IoOperation::Stat, &path))?
.file_type();
if kind.is_symlink() {
continue;
}
alloc::reserve(&mut report.removed, 1, AllocationSite::StampDigests)?;
if kind.is_dir() {
fs::remove_dir_all(&path).map_err(io(IoOperation::Remove, &path))?;
} else {
fs::remove_file(&path).map_err(io(IoOperation::Remove, &path))?;
}
report.removed.push(path);
}
report.removed.sort_unstable();
Ok(report)
}
#[cfg(test)]
mod tests;