Skip to content

Add afanctl: applesmc fan supervisor for pre-T2 Intel Macs (A1708) - #476

Open
yadav-prakhar wants to merge 1 commit into
omacom:masterfrom
yadav-prakhar:afanctl
Open

yadav-prakhar wants to merge 1 commit into
omacom:masterfrom
yadav-prakhar:afanctl

Conversation

@yadav-prakhar

@yadav-prakhar yadav-prakhar commented Sep 16, 2026

Copy link
Copy Markdown

What

afanctl supervises the single fan of pre-T2 Intel Macs — machines whose fan is driven by the Apple SMC through applesmc and which expose no pwm* attributes, so fancontrol/pwmconfig cannot drive it and the firmware curve is the only stock option.

It reads coretemp, drives fan1_manual/fan1_output, and returns the fan to the firmware on every failure path. Upstream: https://github.com/yadav-prakhar/afanctl (tag v0.1.0), which is also where it is distributed — a tagged GitHub release with a one-command install (releases/latest/download/install.sh resolves the package through the release's SHA256SUMS, verifies its SHA-256 and runs pacman -U). Verified on MacBookPro14,1 (MacBook Pro A1708) — the same machine class the repository already ships omarchy-brightness-display-apple and the other Apple hardware leaves for.

Packaging notes

  • source: local, built from the tagged GitHub tarball (${url}/archive/refs/tags/v${pkgver}.tar.gz); .omarchy/package.json declares a github upstream watch with a version pattern, so sync-upstream follows upstream releases on its own.
  • There is no AUR package to import from — registration there is closed to new maintainers at the moment — which is precisely why this sources the tagged tarball directly instead of starting from an AUR PKGBUILD. The watch resolves v0.1.0 today.
  • arch=('x86_64')applesmc and coretemp are Intel-only; there is no aarch64 build to invent.
  • depends=('glibc' 'gcc-libs'): readelf -d on the built binary lists libc.so.6 and libgcc_s.so.1 and nothing else. makedepends=('cargo') (extra/rust provides it).
  • Installs: the binary, the systemd unit, /etc/afanctl/afanctl.toml (backup=, so user edits survive upgrades), a pristine default copy under /usr/share/afanctl/, an optional polkit rule allowing passwordless pkexec afanctl status|observe|curve|hold <rpm> for wheel users in local active sessions, and the license.
  • The unit installs disabled, and its ExecStart is daemon --mode observe: installing changes nothing until the user enables it, and even then the daemon writes nothing until they ask for curve or hold. /run is tmpfs, so every reboot lands back on the firmware curve.

Safety

It writes to /sys on machines that get hot, so the failure policy is the design: a per-poll verify/re-assert with a 3-strike ladder that hands the fan back to AUTO and latches monitor-only; a pre-opened fd plus panic/signal handlers that restore AUTO in a single write(2) (proven by a hidden selftest-panic probe); and systemd Type=notify + WatchdogSec=15 + Restart=always + sandboxing (ProtectSystem=strict, ReadWritePaths pinned to the applesmc platform directory). afanctl doctor is read-only except a confirm-flagged 2-second roundtrip; a broken config refuses to start rather than silently running on defaults.

Validation

  • python helpers/upstream-watch.py check pkgbuilds/afanctl{"status": "skipped", "current": "0.1.0", "reason": "already current"}; python tests/upstream-watch.py → 22 tests OK on this checkout.
  • makepkg in pkgbuilds/afanctl builds afanctl 0.1.0-1 from the tagged tarball; the package carries the binary, unit, config, default copy, polkit rule and license.
  • Upstream verification: the full hardware acceptance gate passed on the A1708 — doctor, a write roundtrip, the deliberate-panic death path, a 1 h+ observe soak (0.05 % CPU, 2.4 MB peak RSS), SIGKILL rescue in curve mode, a 600-sample doctor --compare table, polkit hold, and the reboot test — after four adversarial review rounds. 170 tests and four quality gates; a v* tag builds and publishes the release artifacts in CI.
  • This recipe and the published release artifact build the same tagged sources; .gitattributes export-ignores the upstream AUR recipe directory so the tarball contains nothing self-referential.

Known limits

  • One fan, one machine class, by design. No GUI — a shell plugin is expected to consume status --json and the /run/afanctl/cmd.json control channel instead.
  • hold can keep the fan quiet under load. The overshoot guard, the clamp to fan1_min..fan1_max and both fallback layers stay active, and doctor warns while a hold is active.
  • Its curve is more responsive to coretemp, not quieter: doctor --compare 600 measured a mean Δ of 191 rpm against the SMC curve, with ours louder on 392 of 600 samples (the SMC's own die sensors read 10–15 °C hotter and miss short coretemp spikes). This is a verifiability and responsiveness argument, not a noise one.

afanctl is a safety-first single-fan supervisor for Macs whose fan is driven
by the Apple SMC through applesmc (no pwm* attributes, so fancontrol cannot be
used): it reads coretemp and drives fan1_manual/fan1_output, and every failure
path returns the fan to the firmware. Verified on MacBookPro14,1 (A1708) with
the full hardware acceptance gate passed.

The recipe follows the repository's owned-recipe + upstream watch model: the
source tarball is the tagged GitHub release, and .omarchy/package.json carries
a github watch with a version pattern so bin/sync-upstream tracks releases on
its own. The unit installs disabled and the daemon starts in observe mode, so
installing changes nothing until the user enables it.

Checked locally with helpers/upstream-watch.py (status: skipped, already
current) and by building the recipe with makepkg (binary + unit + default
config + polkit rule + license).
yadav-prakhar added a commit to yadav-prakhar/afanctl that referenced this pull request Sep 16, 2026
The Omarchy Package Repository owns its recipes and tracks releases through
their own upstream watch, so the submission is omacom/omarchy-pkgs#476 rather
than an AUR import. Also spells out the AUR account step (register, verify the
email, then paste the pubkey) and the AUR host key fingerprint to check.
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