Skip to content

Releases: jamesgober/mod-tempdir

v1.0.1 — Patch (mod-rand 1.0)

Choose a tag to compare

@jamesgober jamesgober released this 18 May 15:23

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-rand symbol this crate calls
    (mod_rand::tier2::unique_name) is in mod-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 what 0.9.5 produced for the same call
    sequence.
  • The tier2 feature flag on mod-rand continues to exist and
    continues to gate the same module.
  • mod-rand's 1.x SemVer policy
    (STABILITY.md)
    pins this surface for the entire 1.x line.

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: pulls mod-rand 1.0.0 from 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 pass
  • cargo 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.75 within the 1.x line. Any MSRV
    bump ships in a minor release with a notice; major MSRV changes
    wait for 2.0.
  • The mod-rand feature 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} for TempDir,
    .tmpfile-{pid}-{name12} for NamedTempFile) are part of the
    contract — they are what cleanup_orphans parses.

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.0 so this release is unambiguously a dep-refresh signal.
  • No doc rewrites or example changes. The 1.0.0 documentation
    remains canonical.

Release ceremony

Standard pattern carried over:

  1. git tag -a v1.0.1 -m "Release v1.0.1 — mod-rand 1.0 refresh"
  2. git push origin main
  3. git push origin v1.0.1
  4. cargo publish --dry-run --all-features
  5. cargo publish
  6. Confirm https://crates.io/crates/mod-tempdir/1.0.1 is 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

Choose a tag to compare

@jamesgober jamesgober released this 14 May 02:26

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-startup paragraph rewrite, PersistAtomicError
    rustdoc example) ship as part of an official cut for the first
    time. They have been on main since 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.75 within the 1.x line. Any MSRV bump
    ships in a minor release with a notice; major MSRV changes wait
    for 2.0.
  • The mod-rand feature 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} for TempDir,
    .tmpfile-{pid}-{name12} for NamedTempFile) are part of the
    contract — they are what cleanup_orphans parses. 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.0 so this release is unambiguously a stability signal.
  • The fsys integration 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:

  1. git tag -a v1.0.0 -m "Release v1.0.0 — stable API"
  2. git push origin main
  3. git push origin v1.0.0
  4. cargo publish --dry-run --all-features
  5. cargo publish
  6. Confirm https://crates.io/crates/mod-tempdir/1.0.0 is 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

Pre-release

Choose a tag to compare

@jamesgober jamesgober released this 13 May 23:31

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 to target and disable cleanup on
    drop. Performs the canonical "atomic durable write" sequence:

    1. fsync the temp file
      (std::fs::File::sync_all)
    2. atomic std::fs::rename (POSIX rename(2) on Unix,
      MoveFileExW with MOVEFILE_REPLACE_EXISTING on Windows)
    3. best-effort fsync of the target's parent directory so the
      rename itself survives a crash.

    Atomic within a single filesystem. Cross-filesystem target
    returns EXDEV (Unix) or the equivalent (Windows) inside the
    error.

  • PersistAtomicError { error: io::Error, file: NamedTempFile }
    Structured error type. On any failure of persist_atomic, the
    temp file is preserved on disk and the original NamedTempFile
    is returned to the caller inside the error so a retry or
    fallback path does not lose the source. Implements Debug,
    Display, std::error::Error, and From<PersistAtomicError> for io::Error for callers that only need the underlying IO
    error.

Test additions

tests/persist_atomic.rs — four integration tests:

  1. Move + content preservation on the same filesystem.
  2. Replacement of an existing target.
  3. Data-integrity error path: target's parent directory missing
    → source survives, recovered NamedTempFile points at the
    original temp path.
  4. 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_atomic and PersistAtomicError in
    the public API surface.
  • Rustdoc on persist_atomic includes 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_rename is pub(crate) — not callable
    from outside the crate.
  • fsys::Handle::rename requires both paths to live under a
    single handle root, which does not fit a generic
    temp_dir → arbitrary_target move.
  • std::fs::rename invokes the same OS primitives fsys uses
    internally (POSIX rename(2) on Unix, MoveFileExW on
    Windows), so the std-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-features
  • cargo +1.75 build --all-features (MSRV check)
  • cargo fmt --all -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test --all-features: 63 tests pass (Windows)
  • cargo test (default): 59 tests pass
  • cargo doc --no-deps and --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_atomic is atomic only within a single filesystem.
    Cross-mount target returns EXDEV / equivalent. Callers
    needing cross-filesystem finalization should copy through the
    target filesystem first using TempDir::with_prefix rooted at
    the target's parent.
  • Parent-directory fsync is best-effort; on Windows it requires
    FILE_FLAG_BACKUP_SEMANTICS to 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 successful persist_atomic consumes self; an unsuccessful
    one returns it inside PersistAtomicError.file. Callers that
    use io::Result<PathBuf> via the From conversion lose the
    recovered file. Use the structured Result<PathBuf, PersistAtomicError>
    return type directly when retry-on-failure matters.

v0.9.2 — cleanup orphans + PID base names

Choose a tag to compare

@jamesgober jamesgober released this 13 May 22:15

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
into DESIGN-v0.9.1.md and 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

  1. 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_orphans never 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-XYZ still
      starts with .tmp-. No test rewrites needed beyond the new ones.
  2. cleanup_orphans removal condition is (PID dead) AND (age >= max_age_hours). Both must hold.

  3. 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_hours larger 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 in std. Adding those deps is
      forbidden by the project rules. The honest cross-platform
      story is the asymmetric fallback above.
  4. 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.

  5. Per-entry errors are silent, matching the Drop philosophy.
    The function returns the count of successful removals. The only
    Err path 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

v0.9.0

v0.9.0 Pre-release
Pre-release

Choose a tag to compare

@jamesgober jamesgober released this 13 May 13:15

Full Changelog: v0.1.0...v0.9.0