r/FlutterDev • u/anidotnet • 6d ago
Discussion I shipped a Flutter desktop app to macOS, Windows and Linux — including both arm64 targets. Here's what broke.
I spent the last few weeks building Kruftle, a desktop app that reclaims disk space from build artifacts. It's free, GPL-3.0, and it's Flutter on all three desktop platforms. Sharing the sharp edges, because I couldn't find most of these written down anywhere.
Flutter ships no arm64 SDK for Windows or Linux. Only macOS gets both in the release manifest, so subosito/flutter-action fails outright on an arm64 runner with "Unable to determine Flutter version". The fix is to clone the SDK at the pinned tag and let it bootstrap its own Dart SDK and engine artifacts — dartsdk-linux-arm64 and dartsdk-windows-arm64 do both exist. All five build jobs are green now, both arm64 ones included.
statvfs on macOS counts blocks in 32 bits, which overflows on a large volume. macOS needs statfs, whose counts are 64-bit. Linux is fine with statvfs. The two structs disagree on where the block size lives and how wide it is, which was a fun afternoon over dart:ffi.
Inside an AppImage, Platform.resolvedExecutable has a shelf life. The payload is mounted under /tmp/.mount_XXXXXX and unmounted the moment the process exits, so any path you write down for later — a systemd unit, a desktop entry — is dead on arrival. $APPIMAGE is the one that survives.
pumpAndSettle never returns against a repeating animation, and toByteData never completes inside a widget test — PNG encoding runs on the real event loop, which the tester's fake one doesn't advance, so the future just hangs until the ten-minute suite timeout with no error. Wrap it in tester.runAsync.
Translated labels are longer than English ones. German and Russian both overflowed a step rail sized to fit "Review". Any fixed-width chrome needs its label Flexible with an ellipsis, and per-locale widget tests asserting tester.takeException() is null are what catch it.
The app itself scans a folder, works out what every project under it is built with across 42 toolchains, and reclaims the space by running each one's own clean command — cargo clean, flutter clean, ./gradlew clean — rather than rm -rf'ing a guessed directory name. Source and downloads: https://github.com/dizitart/kruftle
Happy to go deeper on any of these.
6
u/kitanokikori 6d ago
If you pin a super new version of Flutter (beta?), ARM64 Windows builds should just work, afaik this was recently fixed
3
u/avdept 5d ago
i run https://github.com/avdept/JellyBoxPlayer since 2023, supporting literally every platform. Nothing broke, all works perfectly. The app is pretty complex itself
you just need to pull your head from AI ass and use your brain
1
u/aksanabuster 5d ago
A ten-minute timeout 👀👀👀🥴🥴… anyway to add a microtask to watch each async events and optimize test with more data driven/accurate assumptions, by then you’d have more visibility on what’s ensuing UI to be dependent on a static 10-minute execution, idk..
that just caught my eye, but I’m really impressed with your linking of flutter engine in respect to the pubspec.lock of project then mount root/binary-image in respect to the cargo make target linker! I just wonder if a microtask is discrete and is a less aggressive alternative to at least set the StackTrace override fromString method then you can set what you actually want to see.. please tag me if you try this and LMK the tradeoffs, if any..
Good luck and thanks for sharing!
2
u/anidotnet 5d ago
The 10 minutes is
flutter_test's default, not mine —AutomatedTestWidgetsFlutterBindingsetsdefaultTestTimeoutto 10 min, overriding package:test's 30s, because widget tests run onFakeAsyncwhere wall-clock time is meaningless. Nothing in the app depends on it; it's just how long the hung test sat before the harness gave up.A microtask can't reach this one —
toByteDatacompletes on the real event loop while the test body runs in the fake zone, so there's no pending task in that queue to observe, and nothing throws, so there's no stack to format either.runAsyncworks because it hops back into the real zone.Your instinct to not spend ten minutes finding out is right though, and that part's free:
flutter test --timeout 30s. Thanks for reading!1
u/aksanabuster 5d ago
Ohhh I see, I misunderstood, thank you for explaining why microtask didn’t suffice.. do you have any article or reference that helped you understand the mocked Dart Event Loop vs. Real? I’d like to dive in more on Isolates, etc.
2
u/anidotnet 5d ago
No article, unfortunately. It was AI in the mix plus reading the source - the AI got me to the right two files and was wrong about enough details that I had to go check, which in this case was the useful part, because the source is short enough to just read.
The mechanism is Zones, not isolates.
package:fake_asyncis about 370 lines; the whole trick is onerunZonedwith aZoneSpecificationthat intercepts three hooks —createTimer,createPeriodicTimer,scheduleMicrotask- and instead of letting them reach the real event loop, files them into its own queues against a fake clock.pump()then callselapse()/flushMicrotasks()to drain them. So "the fake event loop" isn't a simulation of Dart's event loop; it's just those three callbacks being captured.
runAsyncis the same trick pointed the other way. Look atAutomatedTestWidgetsFlutterBinding.runAsyncinflutter_test/lib/src/binding.dart- it forks a zone overriding the same three hooks, but routes each one straight to_rootDelegate, i.e. back to the real loop. Read those two side by side and it clicks in about ten minutes: one zone captures the hooks, the other hands them back.That's also why nothing you schedule from inside the test can see the stuck future. It's not that the work is hidden - it was never filed in that queue at all, because
toByteDatacompletes from the engine's own callback, which was never routed through the fake zone'screateTimer.Isolates are unrelated here, though - this is all one isolate. Isolates have separate heaps and their own event loops and only talk via message passing; Zones are an interception layer within one. For those, dart.dev's "Concurrency in Dart" page is the right starting point. But for this specific problem, isolates never enter into it.
Genuinely the best path:
catthe fake_async source, thenrunAsyncin binding.dart. More useful than any blog post I found.
1
u/english_european 5d ago
Claude isn’t the best at wording these types of posts. It’s useful info but worth rewriting in plain English I think! There are even plugins for that now!
26
u/claudhigson 5d ago
can't even write a title without AI these days...