LTFS sounds like a solved cross-platform problem. There is one specification, one reference implementation on GitHub, and the same three letters appear in every vendor's datasheet. Write to the standard once and ship everywhere.
That is not how it goes. We build archiving software that targets Windows, macOS, and Linux from one Rust workspace, and the tape layer is the single place where the three operating systems refuse to converge. Not because of small API differences, but because the word LTFS names two genuinely different architectures depending on which machine you are standing at.
This post is what actually differs, written from the platform backends rather than from the specification. It includes a macOS build defect that is not documented anywhere we could find, and which will cost you a day the first time you meet it.
One name, two architectures
On Linux and macOS, LTFS is a userspace filesystem. You install a binary called ltfs, it links against FUSE, and you mount a cartridge at a path the same way you would mount anything else. The tape becomes a directory. Everything downstream is ordinary file I/O.
On Windows, LTFS is a command suite plus a filesystem driver. IBM ships a family of executables, and you drive the tape by calling them in sequence:
LtfsCmdDrives.exe enumerate attached drives
LtfsCmdAssign.exe bind a drive to a letter
LtfsCmdLoad.exe load the cartridge
LtfsCmdFormat.exe write a fresh LTFS volume
LtfsCmdEject.exe take the cartridge offline
LtfsCmdUnassign.exe release the letter
The cartridge does not appear at a path you chose. It appears at a drive letter, and the thing you check to confirm you are looking at tape rather than at somebody's USB stick is the volume's reported filesystem name.
That difference propagates through everything. A mount on Linux is a path that either has a marker in it or does not. A mount on Windows is a letter that may or may not currently be bound to what you think it is bound to. Any abstraction over both has to be honest that it is hiding two different failure modes, not one.
The tool inventory, side by side
Here is what each backend actually shells out to. This is the clearest single picture of the problem:
Windows LtfsCmdDrives / Assign / Unassign / Load / Eject / Format
Linux ltfs, mkltfs, umount, fusermount, mt, df
macOS ltfs, mkltfs, umount, mount, df
Read the third line twice. macOS is missing two tools that Linux has, and one of them is load bearing.
macOS has no way to eject a tape
On Linux, taking a cartridge offline is mt -f /dev/nst0 offline. mt comes from mt-st, it is packaged everywhere, and it has been the answer for decades.
macOS ships no tape control tool at all. There is no mt, and no system equivalent. So the eject verb, which is trivial on the other two platforms, has no implementation available to it.
What you do about that is a design decision rather than a coding one. Our answer was to make the failure honest and typed rather than silent: unmount first, which flushes the index to tape and leaves the cartridge complete and readable on another machine, then return a specific "eject unsupported" result telling the operator the cartridge is safe to remove with the drive's own button. The caller that matters here is a spanning writer whose operator is already standing at the drive with the next cartridge in hand, so it needs to tell "I could not eject" apart from "something went wrong" without matching on error strings.
There is a second route. The FUSE layer can be asked at mount time to eject on unmount, using the IOKit backend and no external tool. But it is a mount-time option, which means committing at mount to whether the session ends in an eject. That turns eject from a verb into a mode, and it is a change to the whole spanning flow rather than a one-line swap. Worth knowing it exists before you design around its absence.
FUSE cleanup differs too
When an ltfs mount hangs and you kill it, you are left with a stale FUSE endpoint that will refuse the next mount attempt. Clearing it is not the same command on both unix platforms:
Linux fusermount -u <mountpoint>
macOS umount <mountpoint> (or diskutil unmount force)
macOS has no fusermount. If your recovery path assumes it, your retry works in development and wedges on a Mac, and the symptom is a mount failure that looks like a hardware problem.
Timeouts are the other thing everyone gets wrong
Tape operations do not run on human timescales, and a single global timeout will either kill legitimate work or hide a wedged controller for hours.
Formatting a fresh LTO-9 cartridge is the extreme case. The first load of a new cartridge can take anywhere from forty minutes to two and a half hours before the drive will even talk to you, and the format itself runs long after that. A mount-class timeout of a few minutes will reliably kill a perfectly healthy format.
So the timeout has to be a property of the operation class, not of the session. Ours settles at three floors, applied identically on all three platforms:
probe 30 seconds is the drive there
index flush 1 hour unmount, writing the index to tape
long operations 4 hours format, and first-load waits
The floor matters more than the number. A caller can configure a longer timeout, but it cannot configure one shorter than the class floor, because a caller that shortens a format timeout is not expressing a preference, it is expressing a misunderstanding.
Two flavours of Linux, and they are not the two you expect
Ask which Linux distributions a desktop application supports and you expect an answer like "Ubuntu and Fedora". For a tape tool the meaningful split is not by distribution brand. It is packaged versus portable, and each carries a different set of problems.
The .deb: integrated, and honest about it
A Debian package declares its dependencies, and the package manager enforces them. Ours names the GUI toolkit and the dialog helper explicitly, and CI fails the build if either declaration goes missing from the produced package. That is the useful property: on a Debian or Ubuntu machine, a missing dependency is a clear install-time error naming the package, not a mystery crash on first launch.
The cost is a hard glibc floor. Linux binaries get built on Ubuntu 22.04 rather than on whatever "latest" currently means, because a binary linked against a newer glibc simply will not start on an older machine, and archive operators run old machines on purpose. Building on latest is the single easiest way to ship something that works on every developer's laptop and on none of the customer's servers.
The AppImage: portable, and it moves the problem
An AppImage carries its own userland and runs on distributions you never tested. That is genuinely useful for a tool whose users are on RHEL derivatives, Arch, or something a facility built itself in 2019 and will not touch.
What it does not carry is the kernel side. FUSE is a kernel feature, the tape device nodes are the host's, and the permissions on them are the host's. So an AppImage moves the dependency problem from install time to run time, and converts a clear package-manager error into a runtime failure inside the sandbox. It broadens reach and narrows diagnosability at the same time. Ship both, and know which one a given support conversation is about before you start debugging.
Building LTFS on macOS, which is where the day goes
This section is the reason we wrote the post. Building the reference LTFS implementation on Apple Silicon is not hard in the sense of being intellectually difficult. It is hard in the sense that six separate things go wrong in sequence, each with an error message pointing somewhere other than the cause.
In order, on macOS 15 on an M2:
- macFUSE needs Reduced Security. It is a third-party kernel extension, and Apple Silicon ships with Full Security, under which the kext cannot load at all. Changing that requires booting into Recovery. It cannot be done over SSH, it cannot be scripted, and it cannot be done for a customer. Budget a person at the keyboard.
autogen.shskips libtool silently. No error, no warning, and configure then fails later for a reason that has nothing to do with libtool.- icu4c is keg-only and versioned. Plain
icu4cnow returns a 404 from the Homebrew API. You need the versioned formula, and because it is keg-only it never lands on PATH, so configure dies claiming a tool calledgenrbwas not found. Nothing in that message mentions ICU. - configure demands a uuid pkg-config file. It asks for
uuid >= 1.6. macOS does have UUID, in libSystem, with no.pcfile anywhere, so the check fails on a platform that has the functionality. Installing ossp-uuid satisfies the check. - SNMP is on by default and cannot build. configure calls
net-snmp-config --cflags, which is not there, and the resulting error is truncated in a way that makes it look like a FUSE problem. It is not. Pass--disable-snmp. - macFUSE's framework is not where the compiler looks. It lives under
/Library/Filesystems/macfuse.fs/Contents/Frameworks/, which needs an explicit-F.
Work through all six and it compiles. Roughly thirty seconds on an M2. And then you run the one command that matters and it dies.
The link defect nobody documents
Enumerating drives is ltfs -o device_list. On a freshly built macOS LTFS with no drive attached, that segfaults. Exit 139, with the crash inside libsystem_malloc on what looks like recursive allocation.
Every instinct says hardware, or macFUSE, or the missing drive. None of those is it.
It is a link defect. Upstream's IOKit plugin leaves its IOKit and CoreFoundation symbols to dynamic lookup, and nothing in the build actually links the frameworks that define them. The binary links clean, loads, and then dies the moment it reaches for a symbol that was never bound. Adding the frameworks fixes it:
export LDFLAGS="-L$ICU/lib -framework IOKit -framework CoreFoundation"
With that in place, ltfs -o device_list behaves correctly: it exits 1 and prints an empty device list, which is the right answer when no drive is attached. A clean negative result instead of a crash.
The trap underneath the trap is what a caller does with the crash. Any code that treats "device_list failed" as "no drives present" will render an empty drive picker and tell the user they have no tape hardware, when the truth is the enumerator died. A segfault and an empty list are not the same fact and should never collapse into the same UI state.
What this costs you, and what to do about it
Five things we would tell anyone starting a cross-platform tape project:
- Do not abstract over Windows and unix too early. They are a command suite against a drive letter and a FUSE mount against a path. An interface that pretends otherwise hides the failure modes you most need to see.
- Make timeouts a property of the operation class, with floors a caller cannot lower. Formatting is a four-hour operation on hardware that is behaving perfectly.
- Let a platform say "I cannot do this" in a typed way. macOS cannot eject. That is better expressed as a specific result the caller can branch on than as a generic failure it has to parse a string out of.
- Never let a crashed probe read as an empty result. Enumeration failing and enumeration returning nothing are different, and only one of them should be shown to a user as "no hardware found".
- Fix a build recipe everywhere at once. Ours lived in a runbook, a knowledge base article, and a CI workflow. A fix that lands in one of three is a fix that will be un-learned by whoever reads the other two.
The honest scope of this post
Everything above is about the software layer: build systems, tool inventories, mount semantics, timeout policy, and packaging. It is drawn from platform backends we wrote and a macOS build we did on a real machine.
It is not a hardware compatibility report, and we are not going to imply otherwise. Our current Rust implementation has not yet run against a physical tape drive on any operating system. Coverage today comes from a virtual drive and a simulated backend, and the first run on real hardware is a scheduled validation window covering all three platforms. The cross-platform differences described here are real and cost real time, but the claim stops precisely at the software boundary, because a tape tool that overstates what it has tested is the exact thing this industry does not need more of.
If you are building on tape across operating systems and want to compare notes, or you want the archiving software this came out of, Hiberden is where that work lives. Adjacent problems we take on: NAS infrastructure, data archiving, and custom development when the tool you need does not exist yet.
