Skip to content

Regression of #147142: read-only SDK builds fail on macOS 15.4+ — the rsync --chmod fix from #148580 is now a silent no-op #189645

Closed

Description

@sun617

Regression of #147142: read-only SDK builds fail again on macOS 15.4+ — the rsync --chmod fix from #148580 is now a silent no-op

Summary

This is a silent regression of #147142, which #148580 fixed in May 2024.

That PR made copyFramework pass --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r so its copy of Flutter.framework would be writable before thinFramework rewrites it in place with lipo. The fix was correct and worked at the time.

macOS 15.4 (March 2025) removed GNU rsync. /usr/bin/rsync is now Apple's openrsync fork, whose --chmod is accepted, validated, and then never applied. So the fix stopped working with no code change on either side, and #147142's exact failure is back on any read-only SDK.

The unit tests added by #148580 assert that the flag is passed (ios_test.dart / macos_test.dart match the argument list), not that the resulting mode is writable — so this regression is invisible to CI.

If the SDK's bin/cache is read-only, the copy therefore inherits r-xr-xr-x and the in-place lipo fails:

Target debug_unpack_ios failed: Exception: Failed to extract architectures "arm64 x86_64" for
  …/build/ios/Debug-iphonesimulator/Flutter.framework/Flutter.

stderr:
fatal error: lipo: can't create temporary output file:
  …/build/ios/Debug-iphonesimulator/Flutter.framework/Flutter.lipo (Permission denied)

#0  UnpackDarwin.thinFramework (package:flutter_tools/src/build_system/targets/darwin.dart:115:7)
#1  UnpackIOS.build (package:flutter_tools/src/build_system/targets/ios.dart:300:5)

rsync exits 0, so Flutter cannot detect the failure; it surfaces later as a confusing permissions error pointing at a build output directory.

A normally-installed SDK (~/flutter, Homebrew) is writable, so --chmod doing nothing is harmless and nobody notices. It is fatal for any read-only SDK — Nix, Bazel, a shared/root-owned install, or a CI image that hardens the toolchain.

Why this is newly broken (and why it looks like "works on my machine")

Apple's transition had two steps, which matters for triage:

macOS /usr/bin/rsync Does Flutter's --chmod work?
≤ 14 Sonoma GNU rsync 2.6.9 ✅ yes
15.0 – 15.3 dispatcher: openrsync + GNU fallback ✅ yes — --chmod is registered only in rsync_samba_longopts, so it fell through to GNU rsync
15.4+ (incl. 26 Tahoe) openrsync only, no fallback ❌ no

Apple's own shipped source confirms the dispatcher: rsync.wrapper.c @ rsync-91.0.6 has wrapper_apps[0] = rsync_openrsync (default) and wrapper_apps[1] = rsync_samba, with --chmod present only in the samba longopts table. The wrapper and GNU rsync were removed between tags rsync-91.81.3 (2025-02-26, still has it) and rsync-146 (2025-04-25, gone) — bracketing macOS 15.4 / 15.4.1.

So Flutter's --chmod silently stopped working at macOS 15.4 with no code change on either side. Apple documented none of this — the macOS 15.4 security content never mentions rsync, and no release note or deprecation notice covers it.

Root cause in Apple's openrsync

--chmod is an Apple-fork-only addition — upstream OpenBSD's openrsync(1) documents no --chmod at all. Apple added the flag and a man page entry, but the parser doesn't implement what the page promises.

openrsync/main.c, chmod_parse() states its grammar in a comment:

 *   mode    ::= clause [, clause ...]
 *   clause  ::= [which] [who ...] [action ...] action
 *   action  ::= op [perm ...]
 *   which   ::= D | F
 *   who     ::= a | u | g | o
 *   op      ::= + | - | =
 *   perm    ::= r | s | t | w | x | X

There is no numeric/octal production. 755 matches no case, hits default: rc = EINVAL, and surfaces as --chmod=755: invalid argument — while Apple's shipped man page says "The MODE string can be a comma separated list of literal (755) or relative (g+w) permissions."

The relative forms parse into a mask, but the mode never lands on disk (see matrix below).

Minimal repro (no Flutter, no Nix)

mkdir -p /tmp/rs/src/F.framework && echo hi > /tmp/rs/src/F.framework/bin
chmod -R a-w /tmp/rs/src/F.framework            # read-only source, like a nix store / hardened SDK
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
rsync exit dir file
/usr/bin/rsync (openrsync, macOS 26.5) 0 dr-xr-xr-x ❌ -r--r--r-- ❌
GNU rsync 3.4.4 0 drwxr-xr-x ✅ -rw-r--r-- ✅

Same flags, same source. Putting GNU rsync ahead of /usr/bin/rsync on PATH makes flutter build ios pass with no other change — isolating --chmod as the single variable.

--chmod is non-functional in every documented form

Tested with --perms explicit (openrsync documents "Only has an effect if --perms is specified as well") and a fresh destination (so all files are new — the condition under which openrsync does apply modes):

--perms --chmod=u+w              → exit 0, dr-xr-xr-x  (unchanged)
-av --perms --chmod=u+w          → exit 0, dr-xr-xr-x  (unchanged)
-av --perms --chmod=Fu+w         → exit 0, dr-xr-xr-x  (unchanged)
-av --perms --chmod=Du=rwx,Fu=rw → exit 0, dr-xr-xr-x  (unchanged)
-av --perms --chmod=u=rwx        → exit 0, dr-xr-xr-x  (unchanged)
-av --perms --chmod=a+w          → exit 0, dr-xr-xr-x  (unchanged)
-av --chmod=755                  → exit 1: "rsync: --chmod=755: invalid argument"

The documented caveat does not explain this: --perms is passed and the destination files are new. I've found no existing report of this in Apple's or kristapsdz's trackers — filing with Apple via Feedback Assistant separately, but Flutter can't rely on --chmod on macOS regardless, and can't detect its failure since rsync exits 0.

Where Flutter is affected

packages/flutter_tools/lib/src/build_system/targets/darwin.dart — copyFramework:

final ProcessResult result = await environment.processManager.run(<String>[
  'rsync',
  '-av',
  '--delete',
  '--filter', '- .DS_Store/',
  '--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r',   // ← no-op on macOS 15.4+
  basePath,
  environment.outputDir.path,
]);

…then thinFramework rewrites that copy in place (same path as input and -output):

final ProcessResult extractResult = await environment.processManager.run(<String>[
  'lipo', '-output', frameworkBinaryPath,
  for (final String arch in archList) ...<String>['-extract', arch],
  frameworkBinaryPath,
]);

packages/flutter_tools/bin/xcode_backend.dart's runRsync (-av, no --chmod) has the same exposure for add-to-app flows, since -a implies -p and preserves the source's read-only mode.

Proposed fix

Don't depend on rsync --chmod. Options, in the order I'd suggest:

  1. chmod from Dart after copyFramework — ensure the copied framework is writable before thinFramework runs. Implementation-independent, works on any rsync, removes a silent-failure mode.
  2. Thin to a temp path and move into place — avoids needing the copy writable at all.
  3. At minimum, diagnose it — have thinFramework check writability before invoking lipo and report "the copied framework at X is not writable" rather than surfacing lipo's raw error against a build directory, which sends people looking in entirely the wrong place.

Whichever is chosen, the test gap is worth closing too: ios_test.dart / macos_test.dart currently assert the --chmod argument is present in the rsync invocation. That passes regardless of whether the mode is actually applied, which is why this regressed silently for ~10 months. A test asserting the post-copy mode is writable would have caught it — and would catch the next implementation swap.

Happy to open a PR for whichever the team prefers.

Environment

Flutter 3.44.3 (stable), Dart 3.12.2
macOS 26.5.2 (25F84), darwin-arm64
Xcode 26.6 (17F113)
/usr/bin/rsync → openrsync: protocol version 29 / "rsync version 2.6.9 compatible"
                 man page: OPENRSYNC(1), "written by Kristaps Dzonsons"
SDK installed via Nix (read-only /nix/store)

Reproduced consistently; fixed 100% of the time by putting GNU rsync on PATH.

Related issues

Why this keeps getting closed as "environment-specific"

Three reports of this now (#147142, #167823, this one) have arrived with Nix in the title, which reads like a niche packaging quirk. It isn't. Nix is just the most common way to end up with a read-only bin/cache — Bazel, root-owned/shared installs, and hardened CI images all produce it. Flutter already decided to support that configuration when it merged #148580. The only thing special about Nix users is that they're the ones who notice when that support breaks.

Activity

  1. added
    c: regressionIt was better in the past than it is now
    platform-macosBuilding on or for macOS specifically
    team-macosOwned by the macOS platform team
    toolAffects the "flutter" command-line tool. See also t: labels.
    on Jul 17, 2026
  2. mbcorona commented on Jul 17, 2026

    @mbcorona
    Member

    Thanks for the detailed report and for submitting a PR. Routing to the macOS team for evaluation.

  3. added
    P2Important issues not at the top of the work list
    triaged-macosTriaged by the macOS platform team
    on Jul 23, 2026
  4. added a commit that references this issue on Aug 19, 2026
    9204165
  5. github-actions commented on Sep 2, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  6. locked as resolved and limited conversation to collaborators on Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: regressionIt was better in the past than it is nowfound in release: 3.44Found to occur in 3.44platform-macosBuilding on or for macOS specificallyteam-macosOwned by the macOS platform teamtoolAffects the "flutter" command-line tool. See also t: labels.triaged-macosTriaged by the macOS platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions