Skip to content

Complete macOS support and cover it in CI - #19

Merged
jorge-menjivar merged 1 commit into
mainfrom
macos-support
Aug 20, 2026
Merged

Complete macOS support and cover it in CI#19
jorge-menjivar merged 1 commit into
mainfrom
macos-support

Conversation

@jorge-menjivar

Copy link
Copy Markdown
Owner

Every job in ci.yml runs on ubuntu-latest, so nothing has ever exercised the macOS code paths. Building the crate on an M2 MacBook Air (macOS 26.5) surfaced a lint failure, an example that had drifted, and a memory field that was never populated.

Fixes

kfd.rs — the lib did not compile on macOS. ComputeNode and the non-Linux nodes() stub are only reachable from drm::detect, which is #[cfg(target_os = "linux")]. Dead-code analysis is per compilation unit, and the lib target is built without cfg(test) — where the stub's only caller lives — so both tripped -D warnings:

error: struct `ComputeNode` is never constructed   --> src/kfd.rs:19:19
error: function `nodes` is never used              --> src/kfd.rs:94:15

metal.rs — used/free were never reported on Apple Silicon. Both were hardcoded None, so watch showed usage unknown on every Apple machine. Now read from vm_stat (no Metal linkage, no new dependencies, same subprocess approach as the existing system_profiler/sysctl calls):

     total     8.0 GiB
     used      6.0 GiB
     free      1.9 GiB
     [██████████████████████████████░░░░░░░░░░] 75.8%

used is active + wired + compressor, the figure Activity Monitor labels "Memory Used". Three details worth review:

  • Reads occupied by compressor, not stored in. The latter counts pages pre-compression — 1,263,882 pages on the test machine, ~19 GiB on 8 GiB of RAM. A test asserts the fixture exposes that trap.
  • Page size comes from the vm_stat header rather than hw.pagesize, so counts and multiplier share a source. Apple Silicon is 16 KiB, Intel Macs 4 KiB.
  • Only unified GPUs are touched. A discrete card on an Intel Mac already reported real VRAM and keeps None. The vm_stat read is lazy, matching the deferral drm.rs uses, so a discrete-only Mac spawns no subprocess.

metal.rs — M5 support. Maps to apple10, confirmed against two sources rather than inferred, since this function's stated doctrine is that a wrong family gets acted on while an absent one does not:

  • The macOS 26.5 SDK declares MTLGPUFamilyApple10 = 1010; the 15.4 SDK stops at Apple9.
  • Apple's Metal Feature Set Tables (May 21, 2026): M1-series → Apple7, M2-series → Apple8, M3-series → Apple9, M4-series → Apple9, M5-series → Apple10.

Apple documents these per series, so the single row 5 => 10 covers M5, M5 Pro, M5 Max and a future M5 Ultra — the same way 1 => 7 already covers M1 Max. The doc comment now cites that table instead of describing the mapping as name-derived guesswork, and a new test pins apple10 rendering since AppleFamily had only ever formatted single digits.

watch.rsvulkan_host() was never printed. The probe landed in #17 but the example was not updated, so the host block showed only oneapi/rocm/cuda. Also corrects a "Both rows" comment that was already stale at three probes.

CI

Adds a macos-latest job running clippy with the same pedantic flags, tests, doctests, --no-default-features, and the detect example.

Standalone rather than an os dimension on the existing matrix: that matrix varies the toolchain, and this breakage is platform-specific, so three more macOS runners would buy little. It runs --no-default-features because nvidia is a default feature pulling in NVML that no Mac can use, so Apple-only consumers build that way and the check belongs on their platform. The detect step prints the runner's GPU so probe regressions appear in the log rather than hiding behind an empty-result path.

Had this job existed, it would have caught the kfd.rs break directly.

Verification

On an M2 MacBook Air, macOS 26.5, Rust 1.96.0 — just ci exits 0:

Check Result
cargo fmt --check clean
clippy (pedantic, -D warnings) clean, 0 build warnings
Unit tests 100 passed (was 94)
Integration + doctests 9 + 13 passed
--no-default-features 100 + 9 + 13 passed
cargo publish --dry-run packages cleanly

detect() reports Apple M2 (Apple, apple8): 8.0 GiB total, 1.9 GiB free, with used/free cross-checked against an independent awk calculation over vm_stat (6.01/1.99 GiB, matching to sampling jitter).

Known gap, not addressed here

vulkan.rs probes only libvulkan.so* and Linux ICD paths, so MoltenVK is undetectable. None is correct on a Mac without the Vulkan SDK, but it is a false negative on one with it. Left for a follow-up.

🤖 Generated with Claude Code

Every CI job ran on ubuntu-latest, so nothing exercised the macOS code
paths. Building on an M2 surfaced a lint failure, an example that had
drifted, and a memory field that was never populated.

- kfd: `ComputeNode` and the non-Linux `nodes()` stub are only reachable
  from the Linux-gated `drm::detect`, so the lib target — built without
  `cfg(test)`, where the only caller lives — failed `-D warnings` on
  macOS.
- metal: report used/free on Apple Silicon from `vm_stat`, counting
  active + wired + compressor pages, the figure Activity Monitor labels
  "Memory Used". Unified memory means system-wide usage is the GPU's
  usage; a discrete card on an Intel Mac reported its own VRAM and is
  left untouched. Read lazily, so such a Mac spawns no subprocess.
- metal: map M5 to apple10, per Apple's Metal Feature Set Tables
  (May 21, 2026). Apple documents these per series, so one row covers
  M5, M5 Pro and M5 Max, as `1 => 7` already does for M1.
- watch: print `vulkan_host()`, missing since the probe landed in #17.
- ci: add a macos-latest job. The Linux jobs structurally cannot cover
  these paths — the modules are cfg'd per target, so code that is dead
  or broken only on macOS still compiles clean on ubuntu.

Verified on an M2 MacBook Air (macOS 26.5): detects Apple M2 / apple8 /
8.0 GiB, with used/free matching an independent vm_stat calculation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jorge-menjivar
jorge-menjivar merged commit 964e9dc into main Aug 20, 2026
10 checks passed
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.

1 participant