Repository navigation
Catch a wrong-architecture FFmpeg, and say what it is - #643
Conversation
An Apple Silicon Mac reported "Bad CPU type in executable (os error 86)" on install, and the same machine later reported it again from inside a job, this time naming ffprobe (#637). The trigger is that the macOS FFmpeg fallback is architecture-blind. The primary source publishes per-architecture builds and is picked by std::env::consts::ARCH. The fallback, evermeet.cx, is one URL and one hash for both architectures, and publishes Intel builds. So when the primary is unreachable, which is the reporter's situation, an Apple Silicon Mac is handed a binary it cannot execute unless Rosetta happens to be installed. A bare exec() never triggers Rosetta's install-on-demand prompt, so it just fails. That source gap is not fixed here; it needs an arm64 build to point at, and pinning one means verifying its hashes first. What is fixed is everything that turned it into a dead end. ffprobe is now executed, not stat'd. Verification ran ffmpeg and only checked that ffprobe existed, so a pair that disagreed about architecture passed setup and failed later inside Python as "Could not read file duration: [Errno 86]". That is why the issue title names ffprobe. The data directory is prepended to every child's PATH, so a stale wrong-arch ffprobe there also shadows a working system one. ffprobeReady now reports whether it runs rather than whether the file is present, which had both cases backwards: a wrong-arch binary counted as ready, and a working ffprobe found on PATH did not. A present-but-unusable binary no longer ends the search. resolve_existing_ffmpeg only asks whether the file exists, so a bad download failed identically on every launch afterwards with nothing to do but delete the folder by hand. It now falls through to the system PATH and the download paths, which can replace it. An explicit STEMDECK_FFMPEG override is exempt: that file is the user's choice and replacing it is not ours to make. The setup wizard stops calling it a network problem. The hint was attached to every failure of the step, so an architecture mismatch was reported as a firewall block. It now recognises EBADARCH and gives the two things that actually work: install Rosetta, or point STEMDECK_FFMPEG_URL at a matching build. ffprobe_beside replaces a .parent().unwrap_or_else() whose fallback was unreachable, since parent() on a bare file name is Some("") rather than None. It reached the right answer by accident and the fallback it named was wrong; the test pins the behaviour so a cleanup does not "fix" it into a bug. Verified: cargo fmt, clippy --tests -D warnings and test on Windows (96) and on the Linux target via WSL (105). The macOS cfg blocks do not compile on either host and are left to PR CI.
Not part of #637. It is here because it fails the macOS Rust Check on every branch, including this one, so nothing else can go green until it is fixed. torchvision_version_for_tag was gated `any(not(macos), test)` while the constant it returns, CPU_TORCHVISION_VERSION, is gated `not(macos)`. A macOS test build therefore compiled a function whose only value did not exist: error[E0425]: cannot find value `CPU_TORCHVISION_VERSION` in this scope The `, test` was copied from torch_version_for_tag above it, where it is correct: that one is called by a test which runs on every platform, and it returns literals. This one is called by two tests that are both `not(macos)`, and by one non-test caller that is `not(macos)` as well, so nothing wanted it in a macOS test build in the first place. Narrowing the gate to match its constant and its callers is the whole fix. Untouched by this branch otherwise: the same mismatch is on main. It went unseen because every recent run of that workflow was cancelled rather than failing, so the job has not reported a real result in months. Cannot be verified on this host. Windows and the Linux target compile the constant either way, so the two gates are indistinguishable here; the macOS job on this PR is the check that matters.
The verification added here is correct and was never reached on the one
machine that needed it. Dragging an MP3 onto a build of this branch, on
an Apple Silicon Mac with a wrong-architecture pair already installed,
still produced the reported error verbatim:
Could not read file duration: [Errno 86] Bad CPU type in executable:
'.../StemDeck/ffmpeg/ffprobe'
Two things kept it alive.
probe_runtime reported readiness from resolve_existing_ffmpeg().is_some()
-- is_file(), on ffmpeg alone, never ffprobe, never running anything. A
wrong-architecture pair is a pair that exists, so setup.js short-circuited
straight to the studio and ensure_ffmpeg, which does verify and recover,
was never called. It now runs the pair.
The backend was then never told which FFmpeg had been settled on. It
re-derives one from the data directory and asks only whether a file is
there (ffprobe_executable, app/core/config.py), so the rejected binary is
what it execs -- by absolute path, where there is no PATH search to fall
past it. apply_ffmpeg_path now names both halves outright via
STEMDECK_FFMPEG and STEMDECK_FFPROBE, read back from what setup recorded.
A bare "ffmpeg" is resolved to an absolute path first, since that is what
ensure_ffmpeg returns for a system install and it means nothing to a child
with a different PATH. Windows records the same bare name, where it is the
one spelling that never exists on disk, hence the .exe arm.
Nothing under app/ changes, and nothing is deleted.
Verified on an Apple Silicon Mac against a profile holding a working
ffmpeg beside an unrunnable ffprobe: setup now rejects the pair, recovers,
records both halves, and the import that failed before succeeds. cargo
fmt, the workflow's clippy and cargo test all pass there, 99 tests.
|
Tested this on an Apple Silicon Mac. The machine turned out to already be in the reported state: Rosetta is installed here, so those binaries run and the reporter's failure cannot happen naturally. I forced a real one with a PowerPC Mach-O, which the kernel rejects with What held upThe three new ffprobe tests pass, and so do the other 94 once
What did notDragging an MP3 onto a build of this branch still produced the reported error verbatim:
Past that, the backend is never told which FFmpeg was settled on. It re-derives one from the data directory and asks only whether a file is there ( That last part also corrects something in the PR description. A wrong-architecture binary earlier on PATH does not shadow a working one: both zsh and Python's Pushed to the branch
Nothing under After it, the same launch rejects the pair, recovers, records both halves, and the import that failed before succeeds. Two notes
The recovery re-downloads from evermeet.cx, so on Apple Silicon it lands another Intel build. It verifies here only because Rosetta is present. Without Rosetta the launch now stops at the FFmpeg step with the architecture hint, which is the right outcome, but #642 is still what actually ends this for those users. |
The Linux Rust Check failed to compile the test target:
error[E0433]: cannot find module or crate `env` in this scope
`mod tests` imports `std::env` under `#[cfg(target_os = "macos")]`, so a
bare `env::` in a test compiles on exactly the one target I built and
nowhere else.
Fully qualifying it would have been enough to compile, but the test was
wrong in a second way worth fixing instead: it read and wrote the
process-wide PATH to steer resolve_on_path, and `cargo test` runs tests on
threads, so it raced every other test that spawns a child.
resolve_on_path is now a thin wrapper over resolve_in_paths, which takes
the paths to search. The test passes its own, asserts that an earlier
entry without the file is skipped rather than given up on, and that an
absent name comes back None instead of being handed back bare. No global
state, and it compiles wherever the module does.
The Linux target cannot be checked from a macOS host -- it needs the GTK
system libraries -- so this is verified by fmt, the workflow's clippy and
cargo test on macOS, 99 passing, plus a read of every env:: use in the
test module: one fully qualified, four inside a macOS-gated test.
Closes #637
Follow-up: #642
Thanks to @tsgo606 for the report, and for including the exact error text. That is what made this findable, because the message the app showed pointed at the wrong thing entirely.
What was happening
macOS FFmpeg comes from two sources. The primary publishes one build per architecture and is picked with
std::env::consts::ARCH. The fallback, evermeet.cx, is a single URL with a single hash used on both architectures, and it publishes Intel builds.When the primary is unreachable, an Apple Silicon Mac gets an Intel binary. Rosetta 2 normally hides that, but a bare
exec()never triggers Rosetta's install-on-demand prompt the way launching a GUI app does. On a Mac without it, the binary simply cannot run:Bad CPU type in executable (os error 86).That source gap is not fixed here. It needs an arm64 build to point at, and pinning one means downloading and verifying its hashes the way the existing four were. That is #642. Everything that turned the gap into a dead end is fixed here.
Why the issue title says ffprobe
Verification ran
ffmpegand only checked thatffprobeexisted. So a pair that disagreed about architecture passed setup and failed later, inside the Python pipeline, asCould not read file duration: [Errno 86].It gets worse than a missed check: the data directory is prepended to every child process's PATH, so a stale wrong-arch
ffprobesitting there shadows a working system one. ffprobe is now executed like ffmpeg is, andffprobeReadyreports whether it runs rather than whether the file exists. That flag had both cases backwards: a wrong-arch binary counted as ready, and a working ffprobe found on PATH did not.Why retrying never helped
resolve_existing_ffmpegonly asks whether the file exists, and a failed verification propagated straight out. A bad download therefore failed identically on every launch afterwards, forever, with nothing to do but delete the folder by hand. It now falls through to the system PATH and the download paths, which can replace it.An explicit
STEMDECK_FFMPEGoverride is exempt. That file is the user's own choice, usually outside the data directory, and replacing it is not the app's call.The message
The network/firewall hint was attached to every failure of the FFmpeg step, so an architecture mismatch was reported as a firewall block. It now recognises EBADARCH and offers the two things that actually work: install Rosetta, or point
STEMDECK_FFMPEG_URLat a matching build.Testing on an Apple Silicon Mac
Rosetta decides whether you can see the original bug at all, so check first:
Confirm the architecture, which works either way:
Reproduce the reported path:
rm -rf ~/Library/Application\ Support/StemDeck/ffmpeg(required on the old build, which is the point: nothing re-downloads on its own)127.0.0.1 github.com objects.githubusercontent.comto/etc/hoststo force the fallbackfilethe result to confirm it isx86_64What should now be different:
STEMDECK_FFMPEG_URL, instead of blaming your firewallTesting done here
cargo fmt,clippy --tests -D warningsandcargo testpass on Windows (96 tests) and on the Linux target through WSL (105). Two new unit tests cover ffprobe path resolution, including thePath::parent()trap where a bare file name yieldsSome("")rather thanNone.The hint classifier was checked against both of the reporter's verbatim error strings plus three controls.
desktop/uihas no test harness, so that check was a one-off rather than a committed test.The macOS
cfgblocks compile on neither host here, so PR CI's macOS job is the first thing to build them. I have no Mac, so the end-to-end behaviour above is unverified by me and needs a real Apple Silicon machine.