Releases: jamesgober/mod-tempdir
Release list
v1.0.1 — Patch (mod-rand 1.0)
mod-tempdir v1.0.1 — mod-rand 1.0 Refresh
Date: 2026-05-18
Compare: v1.0.0...v1.0.1
Headline
Pure dependency-refresh release. The optional mod-rand dep is
bumped from 0.9.5 to 1.0 so that downstream consumers enabling
the mod-rand feature pick up mod-rand's SemVer-locked stable
surface. No mod-tempdir public API changes; no behavioural
changes.
mod-tempdir 1.0.0 shipped against mod-rand 0.9.5 because that
was the latest at the time of the v1.0 cut. Cargo's caret matching
treats 0.9.x → 1.0 as a major hop and will not auto-upgrade
across it, so this is the manifest bump that lets new builds of
mod-tempdir track the locked-API release of mod-rand.
What changed
Optional mod-rand dep bumped to 1.0
-mod-rand = { version = "0.9.5", optional = true, default-features = false, features = ["tier2"] }
+mod-rand = { version = "1.0", optional = true, default-features = false, features = ["tier2"] }mod-rand 1.0.0 is documented as a strict superset of
0.9.5:
- The single
mod-randsymbol this crate calls
(mod_rand::tier2::unique_name) is inmod-rand's API freeze
audit under "carried forward, unchanged." - Naming output (uniformly-distributed Crockford base32 from the
PID + nanos + atomic-counter + Stafford-mix-13 pipeline) is
byte-identical to what0.9.5produced for the same call
sequence. - The
tier2feature flag onmod-randcontinues to exist and
continues to gate the same module. mod-rand's1.xSemVer policy
(STABILITY.md)
pins this surface for the entire1.xline.
This bump means cargo install mod-tempdir --features mod-rand
on a fresh machine now downloads mod-rand 1.0 rather than the
older 0.9.5. Existing users staying on mod-tempdir 1.0.0 are
unaffected and can keep mod-rand 0.9.5 in their lockfile until
they choose to upgrade.
Version bump only — no other code or doc changes
Cargo.toml:version = "1.0.0"→version = "1.0.1".Cargo.toml:mod-rand = "0.9.5"→mod-rand = "1.0".CHANGELOG.md:[1.0.1]entry added; compare links updated.
No edits to src/, tests/, docs/, README.md, REPS.md, or
any examples. The trust handshake established at 1.0.0 is
intact.
Frozen public surface (unchanged from 1.0.0)
Listed here for reference; identical to what 1.0.0 shipped:
pub struct TempDir { /* private */ }
impl TempDir {
pub fn new() -> io::Result<Self>;
pub fn with_prefix(prefix: &str) -> io::Result<Self>;
pub fn path(&self) -> &Path;
pub fn persist(self) -> PathBuf;
pub fn cleanup_on_drop(&self) -> bool;
}
impl Drop for TempDir { /* recursive cleanup */ }
pub struct NamedTempFile { /* private */ }
impl NamedTempFile {
pub fn new() -> io::Result<Self>;
pub fn with_prefix(prefix: &str) -> io::Result<Self>;
pub fn path(&self) -> &Path;
pub fn persist(self) -> PathBuf;
pub fn persist_atomic(self, target: impl AsRef<Path>) -> Result<PathBuf, PersistAtomicError>;
pub fn cleanup_on_drop(&self) -> bool;
}
impl Drop for NamedTempFile { /* remove_file */ }
pub struct PersistAtomicError { pub error: io::Error, pub file: NamedTempFile }
pub fn cleanup_orphans(max_age_hours: u64) -> io::Result<usize>;The mod-rand feature flag is part of the stable surface. Default
basename formats (.tmp-{pid}-{name12} for TempDir,
.tmpfile-{pid}-{name12} for NamedTempFile) are part of the
stable contract — cleanup_orphans parses them.
MSRV
1.75, unchanged. Verified by CI on every push. mod-rand 1.0
also pins to 1.75, so the dep bump does not move this floor.
Verification
Full matrix run on Windows host (x86_64-pc-windows-msvc):
cargo update -p mod-rand: pullsmod-rand 1.0.0from crates.io.cargo tree --features mod-rand: one node deep
(mod-tempdir 1.0.1 → mod-rand 1.0.0). No transitive deps.cargo build(default features): ✓cargo build --features mod-rand: ✓cargo build --all-features: ✓cargo +1.75 build --all-features(MSRV): ✓cargo fmt --all -- --check: ✓cargo clippy --all-targets --all-features -- -D warnings: ✓cargo clippy --all-targets --no-default-features -- -D warnings: ✓cargo doc --all-features --no-deps: ✓cargo test --no-default-features: all passcargo test --all-features: all pass
CI matrix: ubuntu-latest, macos-latest, windows-latest, plus the
MSRV-1.75 job.
Migration from 1.0.0
No source changes required. Upgrade by changing your dependency
spec:
mod-tempdir = "1.0.1" # or "1.0", or "1"If you depend on mod-tempdir with features = ["mod-rand"], the
upgrade transitively shifts your mod-rand resolution to 1.0.
That move is also non-breaking; see the mod-rand 1.0.0 release
note for the full freeze audit.
Stability commitment (carried forward from 1.0.0)
From 1.0.0 forward, unchanged:
- Breaking API changes require
2.0.0. Renames, signature
changes that aren't purely additive, removed methods, removed
types, and changes to documented Drop / persist semantics all
count as breaking. - Additive API changes (new methods on existing types, new
free functions, new types) bump the minor version (1.1.0,
1.2.0, …). - Bug fixes, doc improvements, internal-only changes bump
the patch version (1.0.x). This release is one of those. - MSRV stays at Rust
1.75within the1.xline. Any MSRV
bump ships in a minor release with a notice; major MSRV changes
wait for2.0. - The
mod-randfeature flag is part of the stable surface. Its
behaviour (uniformly distributed Crockford base32 names via
mod_rand::tier2::unique_name) is part of the contract. - Default basename formats (
.tmp-{pid}-{name12}forTempDir,
.tmpfile-{pid}-{name12}forNamedTempFile) are part of the
contract — they are whatcleanup_orphansparses.
Explicitly out of scope for 1.0.1
- No new features. None ship in this cut.
- No API additions. Even purely-additive surface is deferred to
1.1.0so this release is unambiguously a dep-refresh signal. - No doc rewrites or example changes. The
1.0.0documentation
remains canonical.
Release ceremony
Standard pattern carried over:
git tag -a v1.0.1 -m "Release v1.0.1 — mod-rand 1.0 refresh"git push origin maingit push origin v1.0.1cargo publish --dry-run --all-featurescargo publish- Confirm
https://crates.io/crates/mod-tempdir/1.0.1is live.
GitHub release title: v1.0.1 — mod-rand 1.0 refresh. Not tagged
as pre-release.
Full Changelog: v1.0.0...v1.0.1
v1.0.0 — Initial Release
mod-tempdir — v1.0.0 Design Note: Stable API Declaration
Phase A output for the v1.0.0 milestone.
Goal
Declare the public API stable. From v1.0.0 onward, breaking changes
require a major-version bump per SemVer. The crate ships exactly the
surface that landed in v0.9.3. No new features, no removals, no
signature changes. v1.0.0 is the trust handshake, not new code.
What ships in 1.0.0
Pure stability declaration. The public API is identical to v0.9.3:
pub struct TempDir { /* private */ }
impl TempDir {
pub fn new() -> io::Result<Self>;
pub fn with_prefix(prefix: &str) -> io::Result<Self>;
pub fn path(&self) -> &Path;
pub fn persist(self) -> PathBuf;
pub fn cleanup_on_drop(&self) -> bool;
}
impl Drop for TempDir { /* recursive cleanup */ }
pub struct NamedTempFile { /* private */ }
impl NamedTempFile {
pub fn new() -> io::Result<Self>;
pub fn with_prefix(prefix: &str) -> io::Result<Self>;
pub fn path(&self) -> &Path;
pub fn persist(self) -> PathBuf;
pub fn persist_atomic(self, target: impl AsRef<Path>) -> Result<PathBuf, PersistAtomicError>;
pub fn cleanup_on_drop(&self) -> bool;
}
impl Drop for NamedTempFile { /* remove_file */ }
pub struct PersistAtomicError { pub error: io::Error, pub file: NamedTempFile }
// + Debug + Display + std::error::Error + From<PersistAtomicError> for io::Error
pub fn cleanup_orphans(max_age_hours: u64) -> io::Result<usize>;The default build still has zero runtime dependencies outside std.
The mod-rand feature flag remains the only opt-in surface.
Differences from v0.9.3 are limited to:
- Version number bumped to
1.0.0. - README, REPS, ROADMAP, PROMPTS updated for the post-stability state.
- CHANGELOG rolled with a stability-declaration entry.
- The two audit-commit doc fixes from
1b890e9(README
cleanup-on-startupparagraph rewrite,PersistAtomicError
rustdoc example) ship as part of an official cut for the first
time. They have been onmainsince the audit but never reached
crates.io.
Stability commitment
From this release forward:
- Breaking API changes require
2.0.0. Renames, signature
changes that aren't purely additive, removed methods, removed
types, and changes to documented Drop / persist semantics all
count as breaking. - Additive API changes (new methods on existing types, new free
functions, new types) bump the minor version (1.1.0,1.2.0,
...). - Bug fixes, doc improvements, internal-only changes bump the
patch version (1.0.1,1.0.2, ...). - MSRV stays at Rust
1.75within the1.xline. Any MSRV bump
ships in a minor release with a notice; major MSRV changes wait
for2.0. - The
mod-randfeature flag is part of the stable surface. Its
behavior (uniformly distributed Crockford base32 names via
mod_rand::tier2::unique_name) is part of the contract. - Default basename formats (
.tmp-{pid}-{name12}forTempDir,
.tmpfile-{pid}-{name12}forNamedTempFile) are part of the
contract — they are whatcleanup_orphansparses. Changing them
would break callers that rely on either the format or the
cleanup behaviour, and requires a major bump.
Explicitly out of scope for 1.0.0
- No new features. None ship in this cut.
- No API additions. Even purely-additive surface is deferred to
1.1.0so this release is unambiguously a stability signal. - The
fsysintegration for atomic persistence is not revisited
here; if it ever returns, it returns as additive minor work.
Verification (must hold at the version-bump boundary)
- 11/11 build matrix green:
cargo build(default,
--no-default-features,--features mod-rand,--all-features),
cargo +1.75 build --all-features,cargo fmt --all -- --check,
cargo clippy --all-targets --all-features -- -D warnings,
cargo clippy --all-targets --no-default-features -- -D warnings,
cargo test --all-features,cargo test --no-default-features,
cargo doc --all-features --no-deps. - 59 tests pass under
--all-features(42 unit/integration + 17
doctests). - Banned-word + em-dash scan: zero hits across shipping files.
- Cross-platform test gating verified for
#[cfg(windows)],
#[cfg(target_os = "linux")], and feature gates.
Release ceremony
Standard pattern carried over from the 0.9.x line:
git tag -a v1.0.0 -m "Release v1.0.0 — stable API"git push origin maingit push origin v1.0.0cargo publish --dry-run --all-featurescargo publish- Confirm
https://crates.io/crates/mod-tempdir/1.0.0is live.
GitHub release title: v1.0.0 — stable API. Tagged as not-prerelease
for the first time.
Full Changelog: v0.9.3...v1.0.0
v0.9.3 — mod-tempdir
mod-tempdir v0.9.3 — Release Notes
Date: 2026-05-13
Compare: v0.9.2...v0.9.3
Headline
Crash-safe file finalization via NamedTempFile::persist_atomic,
with a structured error type that preserves the source temp file
on failure so a retry path never loses data.
What's new
Public API additions
-
NamedTempFile::persist_atomic(target) -> Result<PathBuf, PersistAtomicError>
Atomically move the temp file totargetand disable cleanup on
drop. Performs the canonical "atomic durable write" sequence:fsyncthe temp file
(std::fs::File::sync_all)- atomic
std::fs::rename(POSIXrename(2)on Unix,
MoveFileExWwithMOVEFILE_REPLACE_EXISTINGon Windows) - best-effort
fsyncof the target's parent directory so the
rename itself survives a crash.
Atomic within a single filesystem. Cross-filesystem
target
returnsEXDEV(Unix) or the equivalent (Windows) inside the
error. -
PersistAtomicError { error: io::Error, file: NamedTempFile }
Structured error type. On any failure ofpersist_atomic, the
temp file is preserved on disk and the originalNamedTempFile
is returned to the caller inside the error so a retry or
fallback path does not lose the source. ImplementsDebug,
Display,std::error::Error, andFrom<PersistAtomicError> for io::Errorfor callers that only need the underlying IO
error.
Test additions
tests/persist_atomic.rs — four integration tests:
- Move + content preservation on the same filesystem.
- Replacement of an existing target.
- Data-integrity error path: target's parent directory missing
→ source survives, recoveredNamedTempFilepoints at the
original temp path. - Post-success invariant: nothing remains at the original temp
path after a successful persist.
Documentation
- README gets a new "Atomic persistence" section with the retry
pattern. - REPS.md §3 lists
persist_atomicandPersistAtomicErrorin
the public API surface. - Rustdoc on
persist_atomicincludes both the happy-path example
and the retry pattern on recoverable error.
Why no fsys
The roadmap reserved a possible v0.9.3+ fsys integration for
this milestone. After auditing the fsys public API, the
integration was not taken:
fsys::platform::atomic_renameispub(crate)— not callable
from outside the crate.fsys::Handle::renamerequires both paths to live under a
single handle root, which does not fit a generic
temp_dir → arbitrary_targetmove.std::fs::renameinvokes the same OS primitivesfsysuses
internally (POSIXrename(2)on Unix,MoveFileExWon
Windows), so thestd-only path is functionally equivalent for
this use case and keeps the default zero-dep build intact.
Same architectural call as the retired v0.9.1 fsys-for-directory-ops
milestone: when fsys's value lives in its internals rather than
its public surface, adding the dep does not pay off.
Migration
Purely additive. No code edits needed for existing callers. To
use the new method:
use mod_tempdir::NamedTempFile;
use std::io::Write;
let f = NamedTempFile::new()?;
{
let mut h = std::fs::OpenOptions::new().write(true).open(f.path())?;
h.write_all(b"finalized payload")?;
}
match f.persist_atomic("config.toml") {
Ok(landed) => { /* `landed` is the target path */ }
Err(e) => {
// `e.file` is the original NamedTempFile, intact for retry.
// `e.error` is the underlying io::Error.
}
}
# Ok::<(), std::io::Error>(())Verification
cargo build/--features mod-rand/--all-featurescargo +1.75 build --all-features(MSRV check)cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all-features: 63 tests pass (Windows)cargo test(default): 59 tests passcargo doc --no-depsand--all-features: 0 warnings- Banned-word scan and em-dash scan: 0 hits across shipping files.
CI matrix: ubuntu-latest, macos-latest, windows-latest.
Limitations
persist_atomicis atomic only within a single filesystem.
Cross-mounttargetreturnsEXDEV/ equivalent. Callers
needing cross-filesystem finalization should copy through the
target filesystem first usingTempDir::with_prefixrooted at
the target's parent.- Parent-directory
fsyncis best-effort; on Windows it requires
FILE_FLAG_BACKUP_SEMANTICSto acquire the directory handle.
Failures here are silent and do not affect the return value
(the rename itself has already succeeded by that point). - The data-integrity contract (source preserved on failure) means
a successfulpersist_atomicconsumesself; an unsuccessful
one returns it insidePersistAtomicError.file. Callers that
useio::Result<PathBuf>via theFromconversion lose the
recovered file. Use the structuredResult<PathBuf, PersistAtomicError>
return type directly when retry-on-failure matters.
v0.9.2 — cleanup orphans + PID base names
mod-tempdir — v0.9.2 Design Note: cleanup_orphans
Local-only working document. Phase A output for the v0.9.2 milestone.
Concise on purpose; the heavy decision-by-decision write-up went
intoDESIGN-v0.9.1.mdand the patterns there
carry over.
Goal
Add cleanup_orphans(max_age_hours: u64) -> io::Result<usize>: a
top-level free function that sweeps the OS temp dir for default-prefix
entries this crate could have created, identifies orphans from dead
processes, and removes them.
Key calls
-
Default-basename format gains a PID segment.
TempDir:.tmp-{pid}-{name12}(was.tmp-{name12}in 0.9.0).NamedTempFile:.tmpfile-{pid}-{name12}(was.tmpfile-{name12}
in 0.9.1).with_prefix(p)is unchanged.cleanup_orphansnever touches
entries outside the default prefixes. The user's namespace is
theirs to clean.- All existing tests that assert
starts_with(".tmp-")/
starts_with(".tmpfile-")keep passing:.tmp-1234-XYZstill
starts with.tmp-. No test rewrites needed beyond the new ones.
-
cleanup_orphansremoval condition is(PID dead) AND (age >= max_age_hours). Both must hold. -
PID liveness check is platform-conditional, no deps added.
- Linux:
Path::new(&format!("/proc/{pid}")).exists(). - macOS, Windows: liveness check is a no-op (treated as "dead"
for the purpose of the AND). The age check is the sole gate
on those platforms. Documented behavior. Picking
max_age_hourslarger than any legitimate process lifetime is
the caller's responsibility on non-Linux. - Cross-platform PID introspection without
libc/windows-sys
is genuinely not available instd. Adding those deps is
forbidden by the project rules. The honest cross-platform
story is the asymmetric fallback above.
- Linux:
-
Legacy 0.9.0 / 0.9.1 entries (no PID in name) are ignored.
The parser requires a{digits}-segment after the prefix; entries
without one are not eligible. Safer than trying to guess PIDs. -
Per-entry errors are silent, matching the Drop philosophy.
The function returns the count of successful removals. The only
Errpath is failure to read the OS temp dir itself.
Test strategy
Five tests in tests/cleanup_orphans.rs. Each creates orphan entries
under unique-to-this-test bogus PIDs to avoid cross-test interference
with the shared temp dir.
- Remove-eligible old orphan dir: removed.
- Remove-eligible old orphan file: removed.
- Recent default-prefix entry: kept (age guard).
- Custom-prefix entry: kept (out of namespace).
- Legacy-format default-prefix entry (no PID): kept (not eligible).
- Linux-only: orphan under the current process's PID with backdated
mtime: kept (live process guard).
File mtime backdating uses std::fs::File::set_modified, stable in
MSRV 1.75. If that turns out not to be in 1.75 after all, I'll fall
back to set_times.
Doc and CHANGELOG updates
- README: bump the default-basename table, add the new function to
the API listing, add a short "Cleaning up after crashes" section
showing typical usage. - REPS.md §2: add the cleanup capability. §3: add the new function
signature. - CHANGELOG.md
[Unreleased]:### Added+### Changed(the
basename format shift) + a migration sentence.
No version bump, no release ceremony. Accumulating into the next
release per the new workflow.
Full Changelog: v0.9.0...v0.9.2