Skip to content

Feat/macos port clean#202

Open
dlicudi wants to merge 19 commits into
samsoir:mainfrom
dlicudi:feat/macos-port-clean
Open

Feat/macos port clean#202
dlicudi wants to merge 19 commits into
samsoir:mainfrom
dlicudi:feat/macos-port-clean

Conversation

@dlicudi

@dlicudi dlicudi commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Ports XEarthLayer to macOS. The branch is strictly portability: no feature
changes, no behavior changes on Linux. Tested on Apple Silicon (M5 Max,
macOS 26, macFUSE 5.2) against stable X-Plane 12.4.2 with multi-hour
flights and no session losses.

Developed with AI assistance (Claude); all changes flight-tested by me.

Changes

  • Mount the fuse3 backend on macFUSE, with unified unmount handling and
    mount verification (system/unmount.rs)
  • Page-cache the virtual DDS files so X-Plane's mmap reads do not crash
  • Force-unmount on shutdown so the mountpoint is not left stale
  • Auto-recover stale FUSE mounts at startup (a run killed by crash or
    SIGKILL leaves a dead mount; detection must use opendir, not stat,
    because macOS serves cached attributes for dead macFUSE mountpoints)
  • Platform-aware mount failure hints (macFUSE install/approval/umount
    on macOS instead of apt/fusermount)
  • macOS location for the X-Plane install reference file
    (~/Library/Preferences/x-plane_install_12.txt)
  • BSD-compatible split invocation in the publisher (probe and -b)
  • run falls back to Custom Scenery auto-detection like the packages
    commands already did
  • fuse3 pinned by rev to a fork carrying one reply-resilience patch,
    submitted upstream as fix(macos): don't kill the session on a transient reply error Sherlock-Holo/fuse3#137 (rebased on upstream
    master, which already includes feat(init): add configurable max_background and congestion_threshold to ReplyInit Sherlock-Holo/fuse3#136). Once
    fix(macos): don't kill the session on a transient reply error Sherlock-Holo/fuse3#137 merges, one commit
    repoints to upstream and also retires the samsoir/fuse3 branch pin
    currently on main.
  • Docs: platform support updated in README and CLAUDE.md

Related Issues

Test Plan

  • make pre-commit passes (fmt + clippy + tests)
  • Full test suite green on macOS (2,420 lib tests + integration)
  • Manual on macOS: xearthlayer run --airport <ICAO> mounts, streams
    tiles, and unmounts cleanly on Ctrl-C; 2h and 4h+ real X-Plane
    flights without crashes
  • Stale mount recovery verified end-to-end: mount, kill -9, restart,
    observe "Stale FUSE mount detected" warning and healthy remount
  • Regression check on Linux (no Linux hardware on my side; CI covers
    build and tests)

Checklist

  • Code follows the project's SOLID principles and patterns
  • Tests added or updated for new/changed behavior
  • Documentation updated if applicable (CLAUDE.md, docs/, config reference)
  • No new warnings from cargo clippy

dlicudi added 12 commits June 12, 2026 00:46
Spike proving the live fuse3 backend works on macOS via macFUSE, so
no fuser port is needed:

- system/filesystem: widen statvfs block counts (u32 on macOS) to u64
- fuse3/shared: add macOS-only crtime/flags fields to FileAttr literals
- fuse3/filesystem: implement open/release; macFUSE lacks
  FUSE_NO_OPEN_SUPPORT and forwards ENOSYS from open to read()
- add ignored macos_mount_spike integration test (mount, read, readdir)
- add system::unmount::unmount_fuse: platform-abstracted unmount
  (fusermount on Linux, umount -f on macOS) used by both the panic
  handler and the SpawnedMountHandle Drop fallback, replacing the
  duplicated fusermount shell-outs
- verify the production Fuse3OrthoUnionFS mounts on macFUSE end-to-end
  (mount, read scenery file, readdir, unmount) in macos_mount_spike
- cover unmount_fuse with a fast non-mountpoint unit test

The async MountHandle::unmount path remains the normal (fast) teardown;
the shell-out helper is only the panic/Drop fallback, so macOS force-
detaches rather than risk blocking.
macFUSE rejects some read replies with EINVAL under load; the previously
pinned fuse3 commit treated that as fatal and tore down the mount. Point at
dlicudi/fuse3 fix/macos-reply-error-resilience, which is samsoir's
configurable-background plus a patch that drops the bad reply and keeps the
session alive.
is_mounted() only read /proc/mounts, which doesn't exist on macOS, so it
always returned false and the umount fallback in unmount_sync never ran. The
fuse3 task completing also doesn't prove macFUSE released the mount. Make
is_mounted() platform-correct (mount(8) on macOS) and always verify + escalate
to umount -f rather than trusting task completion. Ends the stale-mount that
blocked every restart.
BSD split on macOS supports neither --version nor --bytes, causing
'split command failed' panics in the publisher on stock macOS. Probe
availability with a portable no-op (split -b 1 /dev/null) and invoke
with -b <size>, which both GNU and BSD variants accept. Part naming
(.aa/.ab suffixes) is identical across variants, so manifests are
unaffected.

Fixes test_build_archive_splits_large_file on macOS.
The X-Plane installer writes x-plane_install_12.txt to
~/Library/Preferences on macOS, not ~/.x-plane (Linux). Auto-detection
of the installation and Custom Scenery directory silently failed on
macOS, so 'packages install' could not create overlay symlinks without
a manually configured custom_scenery_path.

Add a macos cfg branch to get_install_reference_path() and update the
config docs that hardcoded the Linux path.
The run command only read packages.custom_scenery_path and
xplane.scenery_dir from config, while the packages commands also fall
back to detect_custom_scenery(). With both keys unset, run failed with
a configuration error even when the X-Plane installation is
discoverable. Add the same auto-detect fallback for consistency.

Required for out-of-the-box startup on macOS, where the install
reference file detection was previously broken (see prior commit) and
setup left both keys empty.
Replace the mutable branch reference to dlicudi/fuse3 with an immutable
rev pin. The branch was rebased onto upstream master (which now includes
samsoir's configurable-background PR samsoir#136), carrying the single
reply-resilience patch pending as an upstream PR.

Required for the macOS port: macFUSE can transiently reject reply writes
under load; without the patch the whole session tears down.
A run that dies without unmounting (crash, SIGKILL, system sleep) leaves
a dead mount on the mountpoint; the next mount attempt fails with ENXIO
'Device not configured' and the user must umount by hand.

Detect the corpse before mounting and force-unmount it. Detection must
use opendir(2), not stat(2): macOS serves cached attributes for a dead
macFUSE mountpoint, so stat succeeds while reading the directory fails
with ENXIO (verified against a real killed mount; a metadata()-based
probe silently missed it). Linux's equivalent dead-mount errno ENOTCONN
is classified too.

Also make the mount-failure hints platform-aware: the previous text
suggested apt/fusermount on macOS; now it points at macFUSE install,
Privacy & Security approval, and umount.

Verified end-to-end on macOS: mount, SIGKILL, restart -> WARN 'Stale
FUSE mount detected from a previous run; force-unmounting' -> healthy
mount.
The reply-resilience patch is now submitted upstream as
Sherlock-Holo/fuse3#137. Once merged, repoint
the dependency to Sherlock-Holo/fuse3 (which also retires main's
samsoir/fuse3 branch pin, merged upstream as samsoir#136).
Update platform support in README and CLAUDE.md: macOS works with
macFUSE (Apple Silicon tested, multi-hour X-Plane flights). Notes the
fuse3 fork rev pin and its pending upstream PR (Sherlock-Holo/fuse3#137).
@samsoir

samsoir commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Thanks for this, a huge high-quality contribution that I would like progress towards main safely.

The macOS-specific reasoning is exactly right (the mmap/direct_io crash, opendir-vs-stat stale detection, transient reply-error resilience), the pure-function extractions are unit-tested the way we like, and it's clearly been put through real flights. A few things I'd like to work through before we bring it in, and a process note up front:

Process: This is shaping up as a 0.5.0 line, and we still have 0.4.x work to finish on main, so I'm going to spin up a 0.5 integration branch and I'll ask you to retarget this PR onto it. I'll also get CI running on it (it didn't trigger here) and add a macOS job, since right now none of the #[cfg(target_os = "macos")] code is built by CI.

Must-fix:

  1. filesystem.rs open() hardcodes FOPEN_DIRECT_IO for virtual inodes and redefines the constant locally, that's the very thing VIRTUAL_DDS_OPEN_FLAGS exists to prevent, and it'd reintroduce the macOS mmap crash on the passthrough path. Let's lift the platform-aware flag into one shared place (e.g. shared.rs) and have both filesystems use it.
  2. Mount detection is duplicated: you ported SpawnedMountHandle::is_mounted (great) but manager/local.rs::check_mount_status still returns Unknown on macOS. Let's unify them so there's one detection path.

Should-fix for "complete" macOS support:

  1. The setup wizard's hardware detection (detect_total_memory, detect_storage_type) is Linux-only, on macOS it silently falls back to 8 GB / SSD, so a Mac user gets a quietly worse config. Even a sysctl hw.memsize read + a "couldn't detect, please pick" prompt would close this.
  2. Docs: a short macOS install/approval guide, and importantly please document that page-cache mode disables per-read FUSE visibility on macOS, since FuseLoadMonitor/SceneTracker/InferenceAdapter depend on it. Which leads to:
  3. Question: with direct_io off on macOS, does the FUSE-access-pattern position inference still do anything, or is prefetch effectively Web-API-only there? If the latter, that's fine but we should say so.
  4. The run auto-detect fallback and the stale-mount recovery are nice, but they change Linux behavior too, could you split those out / add a test so they're not riding in under "portability only"?
  5. macos_mount_spike.rs you flagged it as a non-permanent spike; let's either promote it to a proper macOS-gated integration test or drop it.

Dependency note: nice work getting onto upstream-master + a single patch. That's a better position than our current samsoir/fuse3 pin. One ask: so we're not depending on a personal fork that could move, I'll mirror 849e84c into our org fork (or we wait for #137 upstream) before this ships.

Personal note: I really appreciate this contribution. If we can address the points above, macOS support can be a major component of the 0.5.0 release. I am still traveling until early July, so unable to do any local testing until I return. When I am back home I will start laying the ground for 0.5.x development.

@samsoir

samsoir commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Quick housekeeping note: I edited the PR description to qualify the fuse3 references. The fuse3 bullet's bare #136/#137 were resolving against this repo, linking xearthlayer's own #137 ("TelemetryDeck analytics") and the closed #136 (prefetch chore) rather than the upstream Sherlock-Holo/fuse3 PRs you meant. They now read Sherlock-Holo/fuse3#136 / Sherlock-Holo/fuse3#137, so the description no longer implies any link to those local issues.

The same bare refs still live in two commit messages (f31671846, 2405f5576), no action needed now. When you reopen this against the upcoming 0.5.x integration branch, we'll either reword those commits or squash-merge, which clears the erroneous references from history. Thanks again really appreciate the work on this.

dlicudi and others added 3 commits June 21, 2026 19:11
Fuse3PassthroughFS::open() hardcoded FOPEN_DIRECT_IO for virtual inodes
(with a locally redefined constant), bypassing the platform-aware
VIRTUAL_DDS_OPEN_FLAGS that Fuse3OrthoUnionFS already used. On macOS that
reintroduces the macFUSE mmap crash: X-Plane memory-maps DDS textures and
faults with EXC_BAD_ACCESS when a direct_io file is mapped, since direct_io
has no page cache for mmap to use.

Lift FOPEN_DIRECT_IO and VIRTUAL_DDS_OPEN_FLAGS into shared.rs as the single
source of truth and have both filesystems report it from open(), so the
macOS page-cache-not-direct_io rule cannot drift between mounts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mount detection was implemented twice: SpawnedMountHandle::is_mounted
(cross-platform, ported in this branch) and manager's check_mount_status
(Linux-only, returned Unknown on macOS). So `packages list` always showed
"Unknown" mount status on macOS even for correctly mounted packages.

Extract one platform-aware is_path_mounted() into system::unmount alongside
unmount_fuse / is_stale_fuse_mount (the platform mount-table module), move
the pure /proc/mounts and mount(8) parsers + their tests there, and have both
SpawnedMountHandle::is_mounted and check_mount_status delegate to it. macOS
now reports real Mounted/NotMounted status instead of Unknown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fuse3 instruments every FUSE handler with an INFO span, so at the default
`info` level it emits a log line per syscall. On macOS, Finder / Spotlight /
the open-save panel constantly stat the mount, and combined with the debug
build's FmtSpan::CLOSE + pretty() format this floods the log at ~10 MB/s
(observed: a 187 GB xearthlayer.log) and burns ~1.5 cores formatting it.

Pin `fuse3=warn` in the default and --debug filter arms (the --profile arms
already did this), matching the established pattern. fuse3 goes quiet while
xearthlayer's own logs are untouched; RUST_LOG still overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dlicudi dlicudi force-pushed the feat/macos-port-clean branch from 91c9f01 to 8ae9839 Compare June 21, 2026 18:38
dlicudi and others added 4 commits June 21, 2026 22:59
detect_total_memory() only parsed /proc/meminfo (Linux); on macOS it fell
through to the 8GB fallback. The setup wizard sizes the memory cache as
RAM/12, so every Mac got memory_size ≈ 682 MB regardless of actual RAM — a
quietly undersized config (e.g. a 48 GB Mac should get ~4 GB).

Add a macOS arm reading hw.memsize via sysctl(8), with a pure, unit-tested
parser. Other platforms keep the 8GB fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
detect_storage_type() was Linux-only (/sys/block rotational); on macOS it
warned and returned None, so Auto always fell back to the SSD profile — under-
using NVMe (64 vs 256 concurrent I/O) on every Apple Silicon Mac.

Add a macOS arm: resolve the cache path to its backing device with df, query
diskutil info, and map Solid State + Protocol to a profile (Apple Fabric /
PCI-Express / NVMe -> Nvme, other SSD -> Ssd, spinning -> Hdd). Output parsing
is split into pure, unit-tested functions; unclassifiable devices return None
so the caller keeps the safe SSD default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The diagnostics report's collectors read Linux-only sources (/proc/cpuinfo,
/proc/meminfo, /etc/os-release, `ip route`), so on macOS the Operating System,
Hardware, and Network sections came out blank — useless for Mac bug reports.

Add macOS branches: CPU/memory via sysctl, OS name via sw_vers, default
interface via `route -n get default`. Output parsing is a pure, unit-tested
function; Linux collectors are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ixes

Repoint the dlicudi/fuse3 pin from the reply-resilience rev (849e84c) to
feat/macos-mount-options (74abf55), which adds two macOS fixes on top:

- comma-join mount options so all `-o` flags actually apply on macFUSE
  (the builder dropped every option after the first)
- log recoverable reply drops at debug, not warn — macOS file browsers
  (Finder) crawl the mount and produce a harmless ~40/sec stream of
  "dropping a failed fuse reply" warnings; demoting to debug silences the
  flood while the session-continues handling is unchanged.

Verified on macOS: reply-drop log rate 0/sec after the bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dlicudi

dlicudi commented Jun 22, 2026

Copy link
Copy Markdown
Author

@samsoir just a quick note, pushed a few more changes to this branch, but still work in progress. Looking forward to 0.5 branch, I can rebase this macOS port branch to 0.5 once it's available. For the fuse3 fork, I agree best to rely on your fork; my experimental branch is only pinned to my fuse3 fork for purposes of development/testing.

Will post again once I've had a chance to pick this up again and review remaining items.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: macOS (macFUSE) support - working spike branch below

2 participants