Repository navigation
Support NetBSD, OpenBSD and DragonFly - #403
Merged
Merged
Conversation
FUSE was the only thing keeping desync off these three platforms. The go-fuse bindings cover linux, darwin and freebsd; everything else in the tree already builds for the remaining BSDs through the existing platform abstractions, without a single change. So exclude the three mount files there and register the same "not available on this platform" stub Windows already used. That stub is now shared rather than windows-only, and gained a Use string so that `desync mount-index` reports why it's unavailable instead of failing with "unknown command". Device node creation gets to use the atomic path rather than Darwin's resolve-then-mknod fallback: all three have mknodat(2), typed with the device number as an int exactly as on Linux, so they can share that file. FreeBSD stays separate only because it types the argument uint64. Extended attributes are the one real gap. pkg/xattr builds its BSD implementation for freebsd and netbsd only; on OpenBSD and DragonFly it compiles in stubs that return nil and do nothing, so tar records no xattrs and untar discards them without an error. OpenBSD has no extended attributes to begin with, so that is honest there. Noted in the platform table since it is silent. File permissions need nothing: FilemodeToStatMode is built from the S_IF*/S_IS* constants, which are identical across all of these. CI grows a VM job per platform alongside the FreeBSD one, and the cross-compile smoke test now covers every BSD release target. Both echo `go version`, because the toolchain comes from each system's package repo and "too old for go.mod" is the failure worth reading off a log. Release builds add netbsd and openbsd on amd64 and arm64, and dragonfly on amd64, which is its only Go port.
Two things the VM jobs caught that cross-compiling could not. NetBSD couldn't find the toolchain at all: pkgsrc installs under /usr/pkg, which isn't on the default PATH the way FreeBSD's /usr/local/bin is, so `go` was missing despite pkg_add succeeding. DragonFly can't round-trip device nodes. mknod reports success, but the node reads back with an Rdev of NODEV (0xffffffff) for every major/minor pair the test tries, 0:0 included. TestMkdev passes there, so the encoding desync computes is right and it's the trip through the kernel that loses it. Skip the round-trip test there and say so in the platform table; nothing else in the suite is affected. Worth noting the DragonFly package is Go 1.21, well under what go.mod asks for, and the toolchain download picked up 1.25.0 for dragonfly/amd64 by itself. So the `go version` echo earned its place on the first run.
Two follow-ups from the second CI round. The NetBSD PATH fix was aimed at the wrong cause. pkgsrc's `go` is a meta-package that installs no files of its own, and the versioned package it depends on lands as /usr/pkg/bin/go126 and /usr/pkg/go126/bin/go. Nothing anywhere provides an unversioned `go`, so no amount of PATH pointing at /usr/pkg/bin would have found it. Glob for the versioned directory instead, without hard-coding a number pkgsrc will bump. NetBSD 10.1 and 11.0 both currently carry 1.26.5. Build with -buildvcs=false in the VMs: git exits 128 on the tree rsynced into the guest, which failed the DragonFly build on version stamping alone after its tests had all passed. Release builds stamp through goreleaser's ldflags, so nothing is lost. Applied to all four VM jobs, since FreeBSD and OpenBSD passing it so far looks incidental rather than guaranteed.
Reading extended attributes treated every error as fatal, so a walk over a filesystem that simply doesn't implement them failed on its first entry with "operation not supported" instead of reporting a tree with no xattrs. NetBSD is where this shows up - its extattr support is per filesystem and tmpfs has none, so `desync tar` couldn't read anything under /tmp at all - but it isn't NetBSD-specific: the same is true of vfat on Linux. Tell the two cases apart and carry on when support is simply absent. A real failure to read attributes still surfaces. Note that ENOTSUP and EOPNOTSUPP are the same value on Linux but not on the BSDs, so both have to be tested. The write path is deliberately left alone: dropping attributes an archive actually carries is a loss worth an error, while finding none to read is not.
NetBSD's mknodat(2) is declared to take a uint32_t where every other BSD takes a dev_t, and the device number doesn't survive the call: nodes come back with an rdev of 0, so 1:3 read back as 0:0 and only 0:0 appeared to work. Its plain mknod(2) is the versioned __mknod50 and does carry the full dev_t, so route NetBSD through the same path Darwin already uses. The other two stay where they are. OpenBSD and DragonFly both declare mknodat with a dev_t, and OpenBSD passes the round-trip test as it stands. DragonFly's nodes come back as NODEV rather than 0, which is a different thing and not something the call convention explains, so that one keeps its skip.
folbricht
force-pushed
the
bsd-support
branch
from
September 2, 2026 09:33
4374577 to
4708c99
Compare
folbricht
added a commit
that referenced
this pull request
Sep 2, 2026
CreateDevice passed on whatever error mknod returned and never checked the result, so a filesystem that accepts a device number without recording it produced a node pointing at the wrong device, silently and with a successful exit. Two platforms are known to do exactly that. DragonFly's mknod stores NODEV. NetBSD's did the same through mknodat until #403 switched it to mknod. Neither reported anything: what gave NetBSD away was a test that happens to run as root in a VM job, and nothing would have given away a filesystem that behaved the same way under Linux or macOS, because that test never runs there - GitHub's hosted runners aren't root. So read the node back and compare. Whether a device number survives depends on the kernel and on the filesystem underneath rather than on the operating system, so this isn't something a build tag can express. An extraction that stops and says so beats a device node pointing somewhere else. On DragonFly untar now refuses device entries instead of writing wrong ones, which is the intended trade and is noted in the platform table. Verified as root on Linux, where the round-trip test had never actually run before: it passes, and a deliberate mismatch is caught with the expected message.
7 of 13 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FUSE turned out to be the only thing keeping desync off these three platforms. Everything else in the tree already builds for them through the existing platform abstractions —
ioctl_nonlinux.go,preallocate_other.go,blocksize.goand friends absorbed all three without a single change.Excluding FUSE
The go-fuse bindings cover linux, darwin and freebsd only, and there's no near-term prospect of more: upstream's Stance on BSD support is closed, DragonFly's own FUSE driver only got writes working in March 2024, OpenBSD's
fuse(4)doesn't support partial reads and writes, and NetBSD has no FUSE device at all — it emulates one over PUFFS.So
mount-index.go,mount-sparse.goandcmd/desync/mount-index.goare excluded there, and the three platforms register the same stub Windows already used. That stub is no longer windows-only, and it gained aUsestring, sodesync mount-indexnow reports why it's unavailable instead of failing with "unknown command".Device nodes take the atomic path
All three have
mknodat(2), and x/sys types the device argument asint— exactly as on Linux. Solocalfs_mknod_linux.gobecomeslocalfs_mknod_mknodat.goand they share it, rather than falling into Darwin's resolve-then-mknod fallback. FreeBSD stays separate only because it types the same argumentuint64.Extended attributes are the one real gap
pkg/xattrbuilds its BSD implementation forfreebsd || netbsdonly. OpenBSD and DragonFly compile inxattr_unsupported.go, whose functions returnniland do nothing — sotarrecords no xattrs anduntardiscards them, with no error either way. That's honest on OpenBSD, whose kernel has no extended attributes at all; DragonFly hasextattrthe library just doesn't wire up. Called out in the platform table because it's silent.File permissions needed nothing.
FilemodeToStatModeis built from theS_IF*/S_IS*constants, whose values are identical across all of these.Verification
Locally,
go vet ./...(which typechecks the test files too) passes on netbsd/{amd64,arm64,386}, openbsd/{amd64,arm64,riscv64} and dragonfly/amd64, with linux, darwin, freebsd and windows still green.goreleaser build --snapshotproduces the five new binaries and nothing unexpected.What that does not cover is runtime behaviour, which is why this adds a VM job per platform next to the existing FreeBSD one. Those jobs are the part of this PR I'd watch: the Go toolchain comes from each system's package repo, those lag, and go.mod sets a minimum — so each job echoes
go versionto make "package too old" readable straight off the log. If a platform can't get a new enough Go, the honest fix is to drop its VM job and downgrade its README row to "Builds, untested" rather than claim support the CI doesn't back.Release artifacts
netbsd and openbsd on amd64 and arm64, dragonfly on amd64 — its only Go port. goreleaser drops nonexistent combinations by itself, so the new
ignoreentries are only for targets that would otherwise build and ship (32-bit ARM and RISC-V), following the existing freebsd/arm precedent.