Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions crates/uffs-mft/src/reader/index_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,13 @@ impl MftReader {
// 1. Open `X:\$MFT` as a file (filesystem-mediated)
// 2. Re-open volume with FILE_FLAG_NO_BUFFERING (bypasses cache manager,
// direct device I/O)
warn!(
// INFO, not WARN: the cascade below is designed,
// automatically-handled resilience — a normal run
// stays quiet (WARN-default subscribers show
// nothing) and `-v`/verbose reveals the chain when
// debugging. If every fallback fails too, the `?`
// propagates a real error.
info!(
volume = %self.volume,
error = %iocp_err,
"⚠️ IOCP inline read failed — trying fallback strategies"
Expand Down Expand Up @@ -920,7 +926,10 @@ impl MftReader {
return result;
}
Err(err) => {
warn!(
// INFO for the same reason as the IOCP-failure line: an
// intermediate rung of the handled fallback chain, not an
// operator-actionable warning.
info!(
volume = %self.volume,
error = %err,
"⚠️ Fallback 1 ($MFT file) failed — trying unbuffered volume I/O"
Expand Down
71 changes: 9 additions & 62 deletions just/build.just
Original file line number Diff line number Diff line change
Expand Up @@ -123,75 +123,22 @@ copy-binary profile:
done
if ! echo "$PATH" | grep -q "$HOME/bin"; then printf "\033[1;33m⚠️ ~/bin not in PATH. Add this to your shell profile:\033[0m\n"; printf " export PATH=\"\$HOME/bin:\$PATH\"\n"; fi

# Build the entire workspace (release) locally and copy ALL binary artifacts
# to ~/bin. One `cargo build --release --workspace` does the heavy lifting;
# then every binary in target/release/ is installed.
# Build the whole workspace from source and install every binary to ~/bin.
#
# Use this when you want to test in-development code. Prefer `just use`
# when you want the reproducible, GitHub-built release binaries — those
# bytes are what your end users download.
#
# Body is a rust-script (scripts/dev/install-bins.rs) so it runs identically
# under just's unix shell and its powershell windows-shell — a just shebang
# recipe eats Windows temp-path backslashes, and a `bash script.sh` line hits
# WSL's cargo-less bash. The script stops the running daemon/MCP first,
# builds once, then installs every binary cargo reports having built
# (auto-discovered — a new [[bin]] anywhere is picked up automatically).
#
# Build workspace from source and install all binaries to ~/bin (dev).
[unix]
use-local:
#!/usr/bin/env bash
set -euo pipefail
printf "\033[1;34m📦 Build + install ALL UFFS binaries to ~/bin\033[0m\n"
echo "========================================================"

# 0. Stop the running daemon + MCP so the old binaries release their locks
# and no stale in-memory index shadows the freshly installed build.
printf "\n\033[1;33m🔪 Stopping daemon + MCP...\033[0m\n"
timeout 10 uffs --daemon kill >/dev/null 2>&1 || true
pkill -x uffsd 2>/dev/null || true
pkill -x uffsmcp 2>/dev/null || true

# 1. Single workspace-wide release build
printf "\n\033[0;34m🔨 Building release workspace...\033[0m\n"
START=$(date +%s)
cargo build --release --workspace
END=$(date +%s)
printf "\033[0;32m → Built in %ds\033[0m\n" $((END - START))

# 2. Copy every binary artifact to ~/bin
BIN_DIR="$HOME/bin"
mkdir -p "$BIN_DIR"
INSTALLED=0
printf "\n\033[0;34m📦 Installing to %s\033[0m\n" "$BIN_DIR"
for SRC in target/release/*; do
[[ -f "$SRC" && -x "$SRC" ]] || continue
NAME=$(basename "$SRC")
# Skip non-binaries (shared libs, build scripts, deps fingerprints, etc.)
[[ "$NAME" == *.d ]] && continue
[[ "$NAME" == *.dylib ]] && continue
[[ "$NAME" == *.so ]] && continue
[[ "$NAME" == *.rlib ]] && continue
# Skip if it contains a dot (e.g. libfoo.a) — real binaries have no extension on Unix
[[ "$NAME" == *.* ]] && continue
DEST="$BIN_DIR/$NAME"
# If a directory exists with the same name, remove it first
if [[ -d "$DEST" ]]; then
printf " \033[1;33m⚠️ Removing stale directory %s\033[0m\n" "$DEST"
rm -rf "$DEST"
fi
# rm -f before cp so the new file gets a fresh inode — overwrites in
# place share the inode, which lets macOS re-use a path-cached launch
# services deny verdict against an earlier broken copy at the same path.
rm -f "$DEST"
cp "$SRC" "$DEST"
chmod +x "$DEST"
SIZE=$(du -sh "$BIN_DIR/$NAME" | cut -f1)
printf " \033[0;32m✅ %-30s %s\033[0m\n" "$NAME" "$SIZE"
INSTALLED=$((INSTALLED + 1))
done

printf "\n\033[0;32m✅ Installed %d binaries to ~/bin\033[0m\n" "$INSTALLED"

# Check PATH
if ! echo "$PATH" | grep -q "$HOME/bin"; then
printf "\n\033[1;33m⚠️ ~/bin not in PATH. Add to your shell profile:\033[0m\n"
printf " export PATH=\"\$HOME/bin:\$PATH\"\n"
fi
rust-script scripts/dev/install-bins.rs

# Install UFFS binaries from GitHub Release (cached in dist/) to ~/bin.
#
Expand Down
252 changes: 252 additions & 0 deletions scripts/dev/install-bins.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#!/usr/bin/env rust-script
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2025-2026 SKY, LLC.
//!
//! install-bins.rs — build the workspace and install **every** binary it
//! produces to `~/bin`.
//!
//! The binary list is not hardcoded: after building, the same build is
//! re-run with `--message-format=json` (instant from cache) and every
//! `compiler-artifact` message's `executable` path is installed. A new
//! `[[bin]]` anywhere in the workspace is picked up automatically.
//!
//! Called by `just use-local` as
//! `rust-script scripts/dev/install-bins.rs`. A rust-script on purpose:
//!
//! - a just shebang recipe dies on Windows (just hands bash a raw
//! `C:\...` temp path whose backslashes bash eats, exit 127);
//! - a plain `bash script.sh` line dies too when `bash` on PATH resolves
//! to WSL's `System32\bash.exe`, which has no cargo ("cargo: command
//! not found").
//!
//! rust-script runs identically under just's unix shell and its
//! powershell windows-shell, and spawns `cargo` from the real PATH.
//!
//! Usage:
//! rust-script scripts/dev/install-bins.rs

use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

fn main() {
let build_args: &[&str] = &["build", "--release", "--workspace"];

eprintln!("📦 Build (release) + install UFFS binaries to ~/bin");
eprintln!("========================================================");

// Stop the running daemon + MCP first (best effort), mirroring the
// previous [unix] bash recipe: the old binaries release their file
// locks (Windows can't overwrite a running .exe at all) and no stale
// in-memory index shadows the freshly installed build.
stop_running_services();

eprintln!();
eprintln!("🔨 cargo {}", build_args.join(" "));

let started = Instant::now();
let status = Command::new("cargo").args(build_args).status();
match status {
Ok(code) if code.success() => {
eprintln!(" → Built in {}s", started.elapsed().as_secs());
}
Ok(code) => {
eprintln!("❌ build failed with {code:?}");
std::process::exit(1);
}
Err(err) => {
eprintln!("❌ failed to launch cargo: {err}");
std::process::exit(1);
}
}

// EVERY binary the workspace just built — discovered from cargo, not
// a hardcoded list, so a new bin target is picked up automatically and
// nothing is ever silently missed. `--message-format=json` re-emits
// one `compiler-artifact` message per crate straight from the build
// cache (instant, since the real build above already ran), and each
// binary target carries a non-null `"executable"` path. Libraries
// carry `"executable":null` and are skipped.
let executables = workspace_executables(build_args);
if executables.is_empty() {
eprintln!("❌ cargo reported no workspace binaries to install");
std::process::exit(1);
}

let home_var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
let Ok(home) = std::env::var(home_var) else {
eprintln!("❌ {home_var} is not set — nowhere to install to");
std::process::exit(1);
};
let bin_dir = PathBuf::from(&home).join("bin");
if let Err(err) = std::fs::create_dir_all(&bin_dir) {
eprintln!("❌ cannot create {}: {err}", bin_dir.display());
std::process::exit(1);
}

let mut installed = 0_u32;
let mut skipped = 0_u32;
eprintln!();
eprintln!("📦 Installing {} binaries to {}", executables.len(), bin_dir.display());
for src in &executables {
let Some(file_name) = src.file_name() else {
continue;
};
let dest = bin_dir.join(file_name);
let name = file_name.to_string_lossy();
if !src.is_file() {
eprintln!(" ⚠️ {name:<28} not found at {} (skipping)", src.display());
skipped += 1;
continue;
}
// Remove first so the copy gets a fresh inode — overwrites in place
// share the inode, which lets macOS re-use a path-cached Launch
// Services deny verdict against an earlier broken copy.
if dest.is_dir() {
let _ = std::fs::remove_dir_all(&dest);
}
let _ = std::fs::remove_file(&dest);
match std::fs::copy(src, &dest) {
Ok(bytes) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let _ = std::fs::set_permissions(
&dest,
std::fs::Permissions::from_mode(0o755),
);
}
eprintln!(" ✅ {name:<28} {:.1} MB", bytes as f64 / 1_048_576.0);
installed += 1;
}
Err(err) => {
eprintln!(" ❌ {name:<28} copy failed: {err}");
skipped += 1;
}
}
}

eprintln!();
eprintln!("✅ Installed {installed} binaries ({skipped} skipped)");
let on_path = std::env::var("PATH")
.map(|path| std::env::split_paths(&path).any(|entry| entry == bin_dir))
.unwrap_or(false);
if !on_path {
eprintln!("⚠️ {} is not on PATH", bin_dir.display());
}
if skipped > 0 {
std::process::exit(1);
}
}

/// Best-effort shutdown of the resident daemon + MCP before installing.
///
/// `uffs --daemon kill` is given 10 seconds (a wedged daemon must not
/// hang the install), then any survivors are hard-killed by image name —
/// `pkill -x` on Unix, `taskkill /IM … /F` on Windows. Every step is
/// best-effort: on a machine with nothing running (or no `uffs` on PATH
/// yet), all of this silently no-ops.
fn stop_running_services() {
eprintln!();
eprintln!("🔪 Stopping daemon + MCP (best effort)...");
if let Ok(mut child) = Command::new("uffs")
.args(["--daemon", "kill"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(100));
}
_ => {
let _ = child.kill();
break;
}
}
}
}
for name in ["uffsd", "uffsmcp"] {
let status = if cfg!(windows) {
Command::new("taskkill")
.args(["/IM", &format!("{name}.exe"), "/F"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
} else {
Command::new("pkill")
.args(["-x", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
};
let _ = status;
}
}

/// Every binary the workspace build produced, as absolute paths.
///
/// Re-runs the same build with `--message-format=json` — instant, since
/// the real build already populated the cache — and collects each
/// `compiler-artifact` message's non-null `executable` path. This is the
/// authoritative "what did we build" list: no name guessing, no `.exe`
/// handling, no missed target when a new `[[bin]]` is added.
fn workspace_executables(build_args: &[&str]) -> Vec<PathBuf> {
let mut args: Vec<&str> = build_args.to_vec();
args.push("--message-format=json");
let output = Command::new("cargo")
.args(&args)
.stderr(Stdio::inherit())
.output();
let Ok(output) = output else {
return Vec::new();
};
let Ok(text) = String::from_utf8(output.stdout) else {
return Vec::new();
};
let mut paths = Vec::new();
for line in text.lines() {
if let Some(exe) = json_executable(line) {
paths.push(PathBuf::from(exe));
}
}
paths.sort();
paths.dedup();
paths
}

/// The non-null `"executable"` path from one cargo JSON message line, if
/// present. `"executable":null` (a library artifact) yields `None`.
fn json_executable(line: &str) -> Option<String> {
let key = "\"executable\":";
let after = &line[line.find(key)? + key.len()..];
// Value is either `null` or `"<escaped path>"`.
let rest = after.strip_prefix('"')?;
let end = rest.find('"')?;
Some(unescape_json_path(&rest[..end]))
}

/// Undo JSON string escaping in a path (`\\` → `\`, `\/` → `/`).
fn unescape_json_path(escaped: &str) -> String {
let mut out = String::with_capacity(escaped.len());
let mut chars = escaped.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('\\') => out.push('\\'),
Some('/') => out.push('/'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
} else {
out.push(ch);
}
}
out
}
Loading