From 6a053f59cd7ee77c529dd7906af8133f4171ec1c Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:24:40 -0700 Subject: [PATCH] fix(ios): keep system rsync paired during export --- apps/ios/fastlane/Fastfile | 37 +++++++++++-------- .../ios-release-fastlane-gates.test.ts | 15 ++++++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/ios/fastlane/Fastfile b/apps/ios/fastlane/Fastfile index e946efa26047..4541e458c333 100644 --- a/apps/ios/fastlane/Fastfile +++ b/apps/ios/fastlane/Fastfile @@ -1142,15 +1142,19 @@ def shell_join(parts) Shellwords.join(parts.compact) end -def xcodebuild_shell_join(parts) - # Keep caller-provided pinned Swift tools ahead of Homebrew. Replacing PATH - # here makes archive build phases bypass the versions verified by the caller. - xcode_path = [ - *ENV.fetch("PATH", "").split(File::PATH_SEPARATOR), +def xcodebuild_shell_join(parts, system_tools_first: false) + caller_path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR) + system_path = [ "/usr/bin", "/bin", "/usr/sbin", "/sbin", + ] + # Archive build phases need caller-pinned Swift tools ahead of Homebrew. + # Export needs Apple's rsync ahead of incompatible Homebrew rsync helpers. + preferred_path = system_tools_first ? [*system_path, *caller_path] : [*caller_path, *system_path] + xcode_path = [ + *preferred_path, "/opt/homebrew/bin", "/usr/local/bin", ].reject(&:empty?).uniq.join(File::PATH_SEPARATOR) @@ -1341,16 +1345,19 @@ def build_app_store_release(context) ) sh( - xcodebuild_shell_join([ - "xcodebuild", - "-exportArchive", - "-archivePath", - archive_path, - "-exportPath", - output_directory, - "-exportOptionsPlist", - export_options_path, - ]) + xcodebuild_shell_join( + [ + "xcodebuild", + "-exportArchive", + "-archivePath", + archive_path, + "-exportPath", + output_directory, + "-exportOptionsPlist", + export_options_path, + ], + system_tools_first: true, + ) ) exported_ipas = Dir[File.join(output_directory, "*.ipa")] diff --git a/test/scripts/ios-release-fastlane-gates.test.ts b/test/scripts/ios-release-fastlane-gates.test.ts index 025dfeddf4d9..a9fe940e6cac 100644 --- a/test/scripts/ios-release-fastlane-gates.test.ts +++ b/test/scripts/ios-release-fastlane-gates.test.ts @@ -99,15 +99,24 @@ describe("iOS Fastlane release upload gates", () => { it("preserves caller-pinned Swift tools in archive build PATH", () => { const fastfile = readFastfile(); const pathBuilder = functionBody(fastfile, "xcodebuild_shell_join"); - const callerPath = '*ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)'; + const callerPath = 'ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)'; expect(pathBuilder).toContain(callerPath); expect(pathBuilder).toContain(".reject(&:empty?).uniq.join(File::PATH_SEPARATOR)"); - expect(pathBuilder.indexOf(callerPath)).toBeLessThan( - pathBuilder.indexOf('"/opt/homebrew/bin"'), + expect(pathBuilder).toContain( + "system_tools_first ? [*system_path, *caller_path] : [*caller_path, *system_path]", ); }); + it("uses Apple's matched rsync pair when exporting the IPA", () => { + const fastfile = readFastfile(); + const builder = functionBody(fastfile, "build_app_store_release"); + const exportStart = builder.indexOf('"-exportArchive"'); + + expect(exportStart).toBeGreaterThanOrEqual(0); + expect(builder.slice(exportStart)).toContain("system_tools_first: true"); + }); + it("requires clean matching source before preparing and building release artifacts", () => { const fastfile = readFastfile(); const verifier = functionBody(fastfile, "verify_apple_release_source!");