DEV Community

Cover image for Patches Welcome #1: Apple Quietly Broke Flutter Builds, and Nobody Noticed for 10 Months
Sagar Ghag
Sagar Ghag

Posted on Edited on

Patches Welcome #1: Apple Quietly Broke Flutter Builds, and Nobody Noticed for 10 Months

flutter build ios started failing on my machine with a permissions error pointing at a build output directory. The actual problem was three layers deeper: Apple silently removed GNU rsync from macOS 15.4, its replacement ignores a flag Flutter depends on, and the existing fix had been a no-op for almost a year.

How I found it

I run the Flutter SDK through Nix. The SDK lives in /nix/store, which is read-only by design. That's a supported configuration. Flutter merged PR #148580 in May 2024 specifically to handle read-only SDK paths by passing --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r to rsync during framework copies.

Worked fine until one day it didn't. The error:

fatal error: lipo: can't create temporary output file:
  …/Flutter.framework/Flutter.lipo (Permission denied)
Enter fullscreen mode Exit fullscreen mode

That's lipo complaining it can't write to a file in the build output directory. Not the SDK. The build output. I spent a while staring at that before realising: the output directory inherited the source's read-only permissions because the copy step silently failed to set them.

The mechanism

I traced it to packages/flutter_tools/lib/src/build_system/targets/darwin.dart. The copyFramework function runs rsync with --chmod to make the copied framework writable before thinFramework rewrites it in place with lipo. The flag is there. It's correct. It does nothing.

macOS 15.4 removed GNU rsync. /usr/bin/rsync is now Apple's openrsync fork, and openrsync accepts --chmod, validates it, and then never applies it. Exit code 0. No warning.

I dug into Apple's open source distributions to confirm. The dispatcher source at rsync-91.0.6 shows two backends: rsync_openrsync (default) and rsync_samba (GNU). The --chmod flag exists only in the samba longopts table. Between tags rsync-91.81.3 (2025-02-26) and rsync-146 (2025-04-25), Apple removed the GNU fallback entirely. That brackets macOS 15.4.

Apple documented none of this. The macOS 15.4 security content doesn't mention rsync. The openrsync man page still says --chmod supports literal modes like 755. It doesn't. chmod_parse() in openrsync/main.c has no octal production; 755 returns EINVAL. The relative forms parse but the mode never reaches disk.

I wrote a minimal repro that takes Flutter out of the equation entirely:

mkdir -p /tmp/rs/src/F.framework && echo hi > /tmp/rs/src/F.framework/bin
chmod -R a-w /tmp/rs/src/F.framework
mkdir -p /tmp/rs/dst
/usr/bin/rsync -av --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \
  /tmp/rs/src/F.framework /tmp/rs/dst/
ls -ld /tmp/rs/dst/F.framework
# dr-xr-xr-x (--chmod did nothing, exit was 0)
Enter fullscreen mode Exit fullscreen mode

Same flags, same source. GNU rsync 3.4.4 produces drwxr-xr-x. Apple's openrsync produces dr-xr-xr-x. That single variable explains the entire failure.

The fix

I filed issue #189645 with the full root cause analysis, the Apple source audit, the minimal repro, and a proposed fix. Then I opened PR #189658.

The fix is one idea: don't trust rsync --chmod. Run chmod -R u+w on the copied framework path after every rsync completes. 120 lines added across darwin.dart, build_swift_package.dart, and three test files. Zero lines deleted.

The review took two rounds with two Google engineers. Chris Bracken (@cbracken) caught that I was using printTrace on failure and suggested ToolExit instead. Victoria Ashworth (@vashworth) pushed back on that: since chmod is only needed for read-only SDK environments, a build-breaking ToolExit would punish normal installs. She pointed me to _utils.logger.printWarning for a non-fatal warning. Both reviewers approved. Auto-submit merged it.

I learned something from that exchange. My instinct was to fail loudly. Vashworth's instinct was to fail proportionally. The chmod path matters on Nix and Bazel and hardened CI. On a regular ~/flutter install, it's a no-op that should stay quiet. She was right.

What I didn't fix

The existing tests assert that --chmod is present in the rsync argument list. They don't check that the resulting files are actually writable. That's the test gap that let this regression hide for 10 months across three separate bug reports (#147142, #167823, #189645). I didn't close that gap. My PR tests that chmod runs after rsync, which is better, but a true end-to-end assertion on post-copy file modes would be stronger. I left that for a follow-up.

I also filed the openrsync --chmod bug with Apple via Feedback Assistant. No response as of writing. The --chmod flag is documented in Apple's man page, parses without error, and does nothing. That's been true since March 2025.

Contributing to a 170k-star repo

The issue mattered more than the PR. I spent a full day writing #189645: reading Apple's dispatcher source, testing every documented --chmod form, building the macOS version matrix, linking the three prior issues that got closed as environment-specific. The PR was 120 lines. The issue was the argument that those 120 lines needed to exist.

Flutter's review process is human. Two reviewers, each with a different concern. One focused on error visibility, the other on blast radius. The CLA bot, the test checker bot, and gemini-code-assist all showed up too (the bot suggested wrapping chmod in try-catch, which was actually reasonable). But the decisions were made by two engineers who understood the trade-offs I didn't see from the outside.

If you're thinking about contributing to a large framework: file the issue first, with a repro that removes your specific setup from the equation. The Nix angle got the previous two reports closed as niche. My repro used nothing but mkdir, chmod, and rsync. The problem stopped being about Nix and started being about macOS.

PR #189658 · Issue #189645 · flutter/flutter

Top comments (0)