//! One `cargo xtask ci` at a time per machine.
//!
//! Every CI run builds the workspace several times with one job per CPU. Several worktrees running
//! it at once (parallel agents) slow each other down far more than waiting in turn would, and make
//! the machine unusable meanwhile. So `ci` first takes one of `LIGHTCRAFT_CI_SLOTS` (default 1)
//! lock files in a per-user cache directory, shared by every checkout and worktree, and holds it
//! until it exits. The OS drops the lock when the process ends, crashed or not.
//!
//! - `LIGHTCRAFT_CI_SLOTS=N`: N runs at once (1–64).
//! - `LIGHTCRAFT_CI_LOCK=off`: no lock (e.g. a CI runner that only ever runs one job).
//! - `LIGHTCRAFT_CI_LOCK=
`: keep the lock files there instead.
use std::fs::{File, OpenOptions, TryLockError};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
const POLL: Duration = Duration::from_secs(2);
/// A held CI slot; dropping it releases the slot.
pub struct Held {
_file: File,
owner: PathBuf,
}
impl Drop for Held {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.owner);
}
}
/// Where the lock files live, or `None` when the lock is switched off.
fn lock_dir() -> Option {
match std::env::var_os("LIGHTCRAFT_CI_LOCK") {
Some(v) if matches!(v.to_str(), Some("off" | "0" | "false" | "no")) => return None,
Some(v) if !v.is_empty() => return Some(PathBuf::from(v)),
_ => {}
}
let cache = std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("LOCALAPPDATA").map(PathBuf::from))
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
.unwrap_or_else(std::env::temp_dir);
Some(cache.join("lightcraft"))
}
fn slots() -> usize {
std::env::var("LIGHTCRAFT_CI_SLOTS").ok().and_then(|s| s.trim().parse().ok()).unwrap_or(1).clamp(1, 64)
}
fn slot_path(dir: &Path, slot: usize) -> PathBuf {
if slot == 0 { dir.join("ci.lock") } else { dir.join(format!("ci-{}.lock", slot + 1)) }
}
fn owner_path(lock: &Path) -> PathBuf {
lock.with_extension("lock.owner")
}
/// What to do when the lock itself can't be set up (read-only home, sandbox, network mount).
const LOCK_HINT: &str = "set LIGHTCRAFT_CI_LOCK=off to run without the lock, or LIGHTCRAFT_CI_LOCK= to keep it elsewhere";
/// Take the first free slot of `slots` in `dir`, or `None` when all are held.
pub fn try_acquire(dir: &Path, slots: usize, owner: &str) -> Result