MediumHow I Shipped Flutter to Windows from a Mac with No Windows Hardware and $0 | by Altbozon | Medium
Sitemap
Altbozon

Altbozon

How I Shipped Flutter to Windows from a Mac with No Windows Hardware and $0

6 min readMay 20, 2026

--

I had a Friday deadline, an Apple Silicon MacBook, no Windows PC, and no budget for cloud VMs or code-signing certs. I needed a real x64 Windows .exe — with a bundled data file — in a friend's hands by end of week.

This is the three-day path that got there.

Press enter or click to view image in full size

The constraint stack

Most Flutter Windows tutorials assume you own both platforms. I don’t. The standard advice — “just build it on Windows” — wasn’t available. Neither was Codemagic or Bitrise for the desktop case (they solve iOS from Mac, not Windows from Mac).

What I had: a private GitHub repo, a Mac, and UTM. That turned out to be enough.

The workflow has four stations. Each one has at least one gotcha nobody documents.

The four-station workflow

The workflow has four stations, each with a distinct job:

  1. Mac — edit and iterate. All Dart code, hot reload, business logic, FFI bindings. Your inner loop stays fast and native.
  2. GitHub Actions — your build server. Compiles the native x64 Windows binary on a windows-latest runner without you maintaining anything.
  3. UTM + Prism — sanity check. Drag the x64 .exe into an ARM Windows VM, confirm it launches and runs before sending it to a human.
  4. Real Windows user — truth. Native x64 performance, real DPI, real Defender, real workflows. Nothing else substitutes for this.

Total cost: $0. No certs, no hardware, no subscriptions.

Station 1 — Edit on Mac

Flutter hot reload works fine on macOS. The macOS build is your inner loop — fast, native, no emulation. This part is normal.

The non-obvious trap comes later, but it’s caused here: flutter create on macOS doesn't generate a windows/ folder. Your project silently won't build on Windows CI until you run:

flutter create --platforms=windows .

Commit that scaffold. Without it, GitHub Actions will give you a cryptic build failure that points nowhere near the real cause.

Station 2 — GitHub Actions builds the x64 artifact

Push to your private repo. A windows-latest runner builds the x64 binary. GitHub Actions gives you 2,000 base minutes/month for free on private repos — but Windows runners cost 2×, so effectively 1,000 Windows minutes. A typical Flutter Windows build is 5–10 minutes, which means 100+ builds/month before you pay anything.

name: Build Windows
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: flutter build windows --release
- uses: actions/upload-artifact@v4
with:
name: app-windows-x64
path: build/windows/x64/runner/Release/
retention-days: 7

To pull the artifact without going to the browser:

brew install gh && gh auth login
gh run download --name app-windows-x64 -D /tmp/app-windows

The performance lie your Mac is telling you. During development on macOS, ONNX Runtime silently uses the CoreML Execution Provider — meaning inference runs on the Neural Engine. When the same code runs on a friend’s Intel laptop, it falls back to CPU. A large bundled model that felt snappy on your Mac can be 5–10× slower on real Windows hardware. You won’t know this until Station 4. Profile on the target hardware, not the dev machine.

Station 3 — UTM Windows VM for verification

You need to run the binary before shipping it. UTM with a Windows 11 ARM VM is the free answer.

Get Altbozon’s stories in your inbox

Join Medium for free to get updates from this writer.

Download the official ARM64 ISO from microsoft.com/software-download/windows11arm64. No key required — it runs indefinitely with a watermark. Use UTM’s Windows wizard.

The gotcha that wastes two hours: the Windows installer shows a “Press any key to boot from CD” prompt for about two seconds. UTM’s display lags on Apple Silicon. Miss it and you fall silently into the EFI shell with no indication of what went wrong. The fix: mash the spacebar from the moment you click Play. Not a tap — sustained mashing.

Once Windows is running, x64 binaries work via Prism emulation. Drop your artifact into the shared folder and run it.

The shared folder cap. UTM uses SPICE WebDAV for the shared folder, which has a 50 MB single-file transfer limit by default. A large bundled file will exceed this. The fix is one registry command on the Windows side (run as Admin):

reg add "HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters" /v 
FileSizeLimitInBytes /t REG_DWORD /d 4294967295 /f

Reboot the VM. Limit is now 4 GB.

The zip extraction gotcha. Windows refuses to extract zip files directly from the Z: network drive. Copy the zip to the Desktop first, then extract from there. Extracting from Z: silently fails or errors with no clear message.

RAM and the pause button. UTM’s pause button freezes the VM but doesn’t release RAM — the full allocation stays pinned. Close the window with the macOS red-X instead. UTM auto-saves state and resumes in ~5 seconds.

Station 4 — The real machine

Hand the artifact to your friend. This is where you find out what you actually shipped.

SmartScreen. Without a code-signing cert, Windows will show a “Windows protected your PC” warning. Users can click “More info → Run anyway,” but most won’t. EV certs used to bypass SmartScreen immediately — they no longer do since March 2024. OV certs are the right answer when you’re ready to sign: cheaper, and reputation builds over time just like EV now does. For a beta to a known friend, the warning is fine. For a public release, factor signing into the plan before launch day.

DPI scaling. UTM runs at 100% DPI, so you won’t see Flutter’s font rendering at 125% or 150% — which is what most Windows users actually have set. Real users surface this. Test on the real machine before claiming the UI is done.

Model size vs. real hardware. If Station 4 surfaces the performance gap — it probably will — now you know what to tune. The variant that felt right on Apple Silicon may need to drop to a smaller version for x86 CPU, offered as a default with the larger one as opt-in.

What this doesn’t solve

  • Native x64 performance — UTM’s Prism emulation is 3× slower than native. Use it for “does it work,” not “is it fast enough.” For real perf numbers, borrow a real x86 Windows laptop or rent a cloud x86 VM ($5 on Azure F-series for an hour).
  • Code signing — your .exe triggers SmartScreen warnings. Fine for friend/alpha; not fine for public release.
  • Hot reload on Windows — not available. Windows side is verification only. All iteration happens on macOS via flutter run -d macos.
  • DPI scaling reality — UTM at 100% DPI doesn’t catch real-user display settings.

Things that will bite you

A few things will bite you before the workflow clicks:

  • GitHub Actions build fails on first push. The Windows platform scaffold is missing — flutter create on macOS doesn't generate it. Run flutter create --platforms=windows ., commit, push.
  • File transfer to the UTM VM fails. SPICE WebDAV has a 50 MB single-file cap. Apply the registry tweak in Station 3 and reboot the VM.
  • Zip extraction fails from the Z: drive. Windows refuses to extract zips directly from network drives. Copy the zip to the Desktop first, then extract from there.
  • App runs in UTM but crashes on your friend’s machine. You built it locally on ARM instead of using the GitHub Actions artifact. Always send the CI-built binary.
  • Defender flags the .exe on download. Expected for an unsigned binary. Instruct your friend to click "More info → Run anyway."

The bigger frame

This is CI-as-build-server applied to Flutter desktop. The same logic that lets indie iOS devs ship without owning a Mac works in reverse — a Mac-only dev can target Windows by treating GitHub Actions as their build machine.

The four-station loop (edit → CI build → VM verify → real device) is the minimum viable cross-platform workflow. Each station catches something the previous one can’t: CI catches build failures, the VM catches runtime crashes and dependency issues, the real machine catches performance and UX on actual hardware.

Total cost: $0. Total time to first binary in a friend’s hands: three days, most of it looking up things I didn’t know yet.

--

--