From 48089d025d59e862d1bb4595706a85cad11f895b Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Tue, 1 Sep 2026 09:35:16 -0700 Subject: [PATCH 1/3] fix(mobile): isolate notification extension linker flags Signed-off-by: Tom Brow Co-authored-by: Codex Ai-assisted: true --- mobile/ios/Runner.xcodeproj/project.pbxproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 739d8730751..3061f92b6a8 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -938,6 +938,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).NotificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -962,6 +963,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).NotificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -986,6 +988,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; + OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).NotificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; From f5486b468e129bd36acaa3b6293678b0c87f5426 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Tue, 1 Sep 2026 10:09:25 -0700 Subject: [PATCH 2/3] test(mobile): guard iOS linker isolation Signed-off-by: Tom Brow Co-authored-by: Codex Ai-assisted: true --- .github/workflows/ci.yml | 11 +++- scripts/test-ios-linker-isolation.rb | 92 ++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 scripts/test-ios-linker-isolation.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0f3b0e4b2a..50e011f0602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1001,17 +1001,26 @@ jobs: mobile-swift: name: Mobile Swift runs-on: macos-latest - timeout-minutes: 10 + timeout-minutes: 30 needs: [changes] if: needs.changes.outputs.mobile == 'true' steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Install Flutter dependencies + run: cd mobile && flutter pub get + - name: Install iOS pods + run: cd mobile/ios && pod install + - name: Verify notification extension linker isolation + run: ruby scripts/test-ios-linker-isolation.rb - name: Build run: swift build --package-path mobile/ios/BuzzPushKit - name: Build release run: swift build -c release --package-path mobile/ios/BuzzPushKit - name: Test run: swift test --package-path mobile/ios/BuzzPushKit + - name: Build complete unsigned iOS release + run: cd mobile && flutter build ios --release --no-codesign --no-pub security: name: Security runs-on: ubuntu-latest diff --git a/scripts/test-ios-linker-isolation.rb b/scripts/test-ios-linker-isolation.rb new file mode 100644 index 00000000000..f2d1fdb6587 --- /dev/null +++ b/scripts/test-ios-linker-isolation.rb @@ -0,0 +1,92 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "fileutils" +require "json" +require "open3" +require "shellwords" + +ROOT = File.expand_path("..", __dir__) +IOS_DIR = File.join(ROOT, "mobile", "ios") +SOURCE_PROJECT = File.join(IOS_DIR, "Runner.xcodeproj") +PROJECT = File.join(IOS_DIR, ".Runner-linker-isolation-#{Process.pid}.xcodeproj") +PODS_SUPPORT = File.join( + ROOT, + "mobile", + "ios", + "Pods", + "Target Support Files", + "Pods-Runner" +) +CONFIGURATIONS = %w[Debug Release Profile].freeze + +abort "Temporary project already exists: #{PROJECT}" if File.exist?(PROJECT) + +FileUtils.cp_r(SOURCE_PROJECT, PROJECT) +at_exit { FileUtils.rm_rf(PROJECT) } + +def build_settings(target, configuration) + stdout, stderr, status = Open3.capture3( + "xcodebuild", + "-project", + PROJECT, + "-target", + target, + "-configuration", + configuration, + "-showBuildSettings", + "-json" + ) + + unless status.success? + warn stderr + abort "xcodebuild failed for #{target} #{configuration}" + end + + result = JSON.parse(stdout).find { |entry| entry.fetch("target") == target } + abort "xcodebuild returned no settings for #{target} #{configuration}" unless result + + result.fetch("buildSettings") +rescue JSON::ParserError => error + warn stderr + abort "Could not parse build settings for #{target} #{configuration}: #{error.message}" +end + +def linker_flags(settings) + value = settings.fetch("OTHER_LDFLAGS", "") + value.is_a?(Array) ? value.join(" ") : value.to_s +end + +def expected_runner_flags(configuration) + path = File.join(PODS_SUPPORT, "Pods-Runner.#{configuration.downcase}.xcconfig") + abort "Missing CocoaPods support file: #{path}" unless File.file?(path) + + assignment = File.foreach(path).find { |line| line.start_with?("OTHER_LDFLAGS =") } + abort "Missing OTHER_LDFLAGS in #{path}" unless assignment + + Shellwords.split(assignment.split("=", 2).fetch(1)).reject { |flag| flag == "$(inherited)" } +end + +CONFIGURATIONS.each do |configuration| + extension_flags = Shellwords.split( + linker_flags(build_settings("NotificationService", configuration)) + ) + unless extension_flags.empty? + abort( + "NotificationService #{configuration} inherited linker flags: " \ + "#{extension_flags.join(" ")}" + ) + end + + runner_flags = Shellwords.split(linker_flags(build_settings("Runner", configuration))) + expected_flags = expected_runner_flags(configuration) + unless runner_flags == expected_flags + abort( + "Runner #{configuration} did not retain its CocoaPods linker flags.\n" \ + "Expected: #{expected_flags.join(" ")}\n" \ + "Actual: #{runner_flags.join(" ")}" + ) + end + + puts "#{configuration}: NotificationService isolated; Runner CocoaPods flags retained" +end From f0a3c7b32503eb188d3e47c133aa0ef25ff18248 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Tue, 1 Sep 2026 10:45:13 -0700 Subject: [PATCH 3/3] test(mobile): rely on full iOS release build Signed-off-by: Tom Brow Co-authored-by: Codex Ai-assisted: true --- .github/workflows/ci.yml | 4 -- scripts/test-ios-linker-isolation.rb | 92 ---------------------------- 2 files changed, 96 deletions(-) delete mode 100644 scripts/test-ios-linker-isolation.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50e011f0602..fca5a7dcc7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1009,10 +1009,6 @@ jobs: - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - name: Install Flutter dependencies run: cd mobile && flutter pub get - - name: Install iOS pods - run: cd mobile/ios && pod install - - name: Verify notification extension linker isolation - run: ruby scripts/test-ios-linker-isolation.rb - name: Build run: swift build --package-path mobile/ios/BuzzPushKit - name: Build release diff --git a/scripts/test-ios-linker-isolation.rb b/scripts/test-ios-linker-isolation.rb deleted file mode 100644 index f2d1fdb6587..00000000000 --- a/scripts/test-ios-linker-isolation.rb +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "fileutils" -require "json" -require "open3" -require "shellwords" - -ROOT = File.expand_path("..", __dir__) -IOS_DIR = File.join(ROOT, "mobile", "ios") -SOURCE_PROJECT = File.join(IOS_DIR, "Runner.xcodeproj") -PROJECT = File.join(IOS_DIR, ".Runner-linker-isolation-#{Process.pid}.xcodeproj") -PODS_SUPPORT = File.join( - ROOT, - "mobile", - "ios", - "Pods", - "Target Support Files", - "Pods-Runner" -) -CONFIGURATIONS = %w[Debug Release Profile].freeze - -abort "Temporary project already exists: #{PROJECT}" if File.exist?(PROJECT) - -FileUtils.cp_r(SOURCE_PROJECT, PROJECT) -at_exit { FileUtils.rm_rf(PROJECT) } - -def build_settings(target, configuration) - stdout, stderr, status = Open3.capture3( - "xcodebuild", - "-project", - PROJECT, - "-target", - target, - "-configuration", - configuration, - "-showBuildSettings", - "-json" - ) - - unless status.success? - warn stderr - abort "xcodebuild failed for #{target} #{configuration}" - end - - result = JSON.parse(stdout).find { |entry| entry.fetch("target") == target } - abort "xcodebuild returned no settings for #{target} #{configuration}" unless result - - result.fetch("buildSettings") -rescue JSON::ParserError => error - warn stderr - abort "Could not parse build settings for #{target} #{configuration}: #{error.message}" -end - -def linker_flags(settings) - value = settings.fetch("OTHER_LDFLAGS", "") - value.is_a?(Array) ? value.join(" ") : value.to_s -end - -def expected_runner_flags(configuration) - path = File.join(PODS_SUPPORT, "Pods-Runner.#{configuration.downcase}.xcconfig") - abort "Missing CocoaPods support file: #{path}" unless File.file?(path) - - assignment = File.foreach(path).find { |line| line.start_with?("OTHER_LDFLAGS =") } - abort "Missing OTHER_LDFLAGS in #{path}" unless assignment - - Shellwords.split(assignment.split("=", 2).fetch(1)).reject { |flag| flag == "$(inherited)" } -end - -CONFIGURATIONS.each do |configuration| - extension_flags = Shellwords.split( - linker_flags(build_settings("NotificationService", configuration)) - ) - unless extension_flags.empty? - abort( - "NotificationService #{configuration} inherited linker flags: " \ - "#{extension_flags.join(" ")}" - ) - end - - runner_flags = Shellwords.split(linker_flags(build_settings("Runner", configuration))) - expected_flags = expected_runner_flags(configuration) - unless runner_flags == expected_flags - abort( - "Runner #{configuration} did not retain its CocoaPods linker flags.\n" \ - "Expected: #{expected_flags.join(" ")}\n" \ - "Actual: #{runner_flags.join(" ")}" - ) - end - - puts "#{configuration}: NotificationService isolated; Runner CocoaPods flags retained" -end