Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def flutter_root
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
require File.expand_path('../../google_mlkit_commons/ios/scripts/apple_silicon_simulator', __dir__)

flutter_ios_podfile_setup

Expand Down Expand Up @@ -56,4 +57,6 @@ post_install do |installer|
end
end
end

mlkit_apple_silicon_simulator_patch(installer)
end
2 changes: 1 addition & 1 deletion packages/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,6 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SSZipArchive: 8a6ee5677c8e304bebc109e39cf0da91ccef22ea

PODFILE CHECKSUM: 810ea711de6d4d578877638350f293e7020676b9
PODFILE CHECKSUM: df61d3916884bb4fa8c7ec2fdffdf0dd0d3cec36

COCOAPODS: 1.16.2
27 changes: 27 additions & 0 deletions packages/google_mlkit_commons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@ end

Notice that the minimum `IPHONEOS_DEPLOYMENT_TARGET` is 15.5, you can set it to something newer but not older.

#### Apple Silicon iOS Simulator (iOS 26+)

Google's `GoogleMLKit/*` pods only ship `arm64-iphoneos` and `x86_64-iphonesimulator` slices and exclude `arm64` from simulator builds. On Apple Silicon Macs running iOS 26+ simulators (where Rosetta 2 is no longer the default for the iOS Simulator) this makes `flutter run` fail with `Unable to find a destination matching the provided destination specifier`. Issue tracked upstream by Google: https://issuetracker.google.com/issues/178965151.

Until proper `arm64-iphonesimulator` slices are published, this plugin ships an **opt-in** Podfile helper that, on every build, relabels the arm64 slice's `LC_BUILD_VERSION.platform` to match the target you are building for — iOS Simulator for simulator builds, iOS for device builds (the same swap used by the well-known [`arm64-to-sim`](https://github.com/bogo/arm64-to-sim) tool). It also strips `EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64` from the generated xcconfigs.

To enable it, add two lines to your iOS `Podfile`:

```ruby
# Near the top, after `require ... podhelper ...`:
require File.expand_path(
'.symlinks/plugins/google_mlkit_commons/ios/scripts/apple_silicon_simulator',
__dir__,
)

post_install do |installer|
# ...your existing post_install code...

# Add this line at the end:
mlkit_apple_silicon_simulator_patch(installer)
end
```

Then re-run `pod install`. The example app under `packages/example` is wired up this way.

Because the relabel runs per build and matches the target automatically, the same `pod install` works for **both** the simulator and physical devices — no manual revert is needed when you switch between them. The helper only touches the arm64 slice of the vendored ML Kit binaries inside `Pods/` and the `EXCLUDED_ARCHS` line in pod-generated xcconfigs. To fully revert, remove the two lines and reinstall the pods with `pod deintegrate && pod install` (a plain `pod install` re-adds the `EXCLUDED_ARCHS` line and removes the build phase, but does not restore the original platform label baked into the cached binaries by the last build).

### Android

- minSdkVersion: 21
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Opt-in Podfile helper that lets Google ML Kit pods build and run on Apple
# Silicon iOS 26+ simulators *and* physical devices from the same Pods install.
# A per-build script phase relabels the arm64 slice to match the target, so no
# manual revert is ever needed. See packages/google_mlkit_commons/README.md
# (iOS section). Upstream Google bug:
# https://issuetracker.google.com/issues/178965151

require 'fileutils'

MLKIT_STATE_DIR = 'MLKitAppleSiliconSimulator'.freeze
MLKIT_PATCHER = 'patch_arm64_simulator.py'.freeze
MLKIT_PHASE_NAME = '[ML Kit] Relabel arm64 slice for current platform'.freeze
MLKIT_EXCLUDED_RE = /^(\s*EXCLUDED_ARCHS\[sdk=iphonesimulator\*\]\s*=\s*)(.*?)\s*$/

def mlkit_apple_silicon_simulator_patch(installer)
pods_dir = File.expand_path(installer.sandbox.root.to_s)

framework_dirs = Dir.glob(File.join(pods_dir, '{MLKit*,MLImage*}'))
.select { |d| File.directory?(d) }
return if framework_dirs.empty?

_mlkit_copy_patcher(pods_dir)
_mlkit_strip_simulator_arm64_exclusion(pods_dir)
_mlkit_install_build_phase(installer)
installer.pods_project.save

Pod::UI.puts ''
Pod::UI.puts "[ml_kit] Apple Silicon simulator support enabled for " \
"#{framework_dirs.size} framework(s) (auto-toggles per build)."
rescue => e
raise "[ml_kit] failed to enable Apple Silicon simulator support: #{e.message}"
end

def _mlkit_copy_patcher(pods_dir)
state_dir = File.join(pods_dir, MLKIT_STATE_DIR)
FileUtils.rm_rf(state_dir)
FileUtils.mkdir_p(state_dir)
FileUtils.cp(File.expand_path(MLKIT_PATCHER, __dir__), state_dir)
end

def _mlkit_strip_simulator_arm64_exclusion(pods_dir)
Dir.glob(File.join(pods_dir, 'Target Support Files', '**', '*.xcconfig'))
.each do |xcconfig|
changed = false
new_text = File.read(xcconfig).each_line.map do |line|
match = line.match(MLKIT_EXCLUDED_RE)
next line unless match

tokens = match[2].split(/\s+/).reject(&:empty?)
next line unless tokens.include?('arm64')

changed = true
kept = tokens.reject { |t| t == 'arm64' }
kept.empty? ? '' : "#{match[1]}#{kept.join(' ')}\n"
end.join
File.write(xcconfig, new_text) if changed
end
end

def _mlkit_install_build_phase(installer)
script = <<~SH
set -euo pipefail
: "${PLATFORM_NAME:?PLATFORM_NAME is not set}"
: "${SRCROOT:?SRCROOT is not set}"
/usr/bin/env python3 "${SRCROOT}/#{MLKIT_STATE_DIR}/#{MLKIT_PATCHER}" \\
--platform "${PLATFORM_NAME}" \\
--pods-root "${SRCROOT}"
SH

installer.aggregate_targets.each do |aggregate|
target = installer.pods_project.targets.find { |t| t.name == aggregate.label }
next unless target

phase = target.shell_script_build_phases.find { |p| p.name == MLKIT_PHASE_NAME }
phase ||= target.new_shell_script_build_phase(MLKIT_PHASE_NAME)
phase.shell_path = '/bin/sh'
phase.shell_script = script
phase.always_out_of_date = '1' if phase.respond_to?(:always_out_of_date=)
end
end
185 changes: 185 additions & 0 deletions packages/google_mlkit_commons/ios/scripts/patch_arm64_simulator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Relabel the arm64 slice of Google ML Kit static frameworks to match the
build's target platform. Flips each arm64 Mach-O object's
LC_BUILD_VERSION.platform between 2 (iOS) and 7 (iOS Simulator) in place, so the
single arm64 slice Google ships works for whichever target is being built; no
instructions, symbols, sizes or offsets change. Idempotent and reversible. See
packages/google_mlkit_commons/README.md (iOS section).

Run from an Xcode build phase:
patch_arm64_simulator.py --platform "$PLATFORM_NAME" --pods-root "$SRCROOT"
"""

import argparse
import glob
import mmap
import os
import struct
import sys

FAT_MAGIC = 0xCAFEBABE
FAT_MAGIC_64 = 0xCAFEBABF
MH_MAGIC_64 = 0xFEEDFACF
LC_BUILD_VERSION = 0x32
PLATFORM_IOS = 2
PLATFORM_IOS_SIMULATOR = 7
CPU_TYPE_ARM64 = 0x0100000c

PLATFORM_BY_SDK = {
'iphoneos': PLATFORM_IOS,
'iphonesimulator': PLATFORM_IOS_SIMULATOR,
}


def _relabel_macho(buf, base, target_platform):
"""Flip LC_BUILD_VERSION.platform of the Mach-O object at ``base`` in place.
Returns True only when a byte was actually changed."""
if base + 32 > len(buf):
return False
if struct.unpack_from('<I', buf, base)[0] != MH_MAGIC_64:
return False
cputype, _cpusub, _filetype, ncmds, sizeofcmds, _flags, _reserved = \
struct.unpack_from('<iIIIIII', buf, base + 4)
if cputype != CPU_TYPE_ARM64:
return False
offset = base + 32
end = min(base + 32 + sizeofcmds, len(buf))
for _ in range(ncmds):
if offset + 8 > end:
break
cmd, cmdsize = struct.unpack_from('<II', buf, offset)
if cmdsize < 8 or offset + cmdsize > end:
break
if cmd == LC_BUILD_VERSION:
if offset + 12 <= end:
platform = struct.unpack_from('<I', buf, offset + 8)[0]
if platform in (PLATFORM_IOS, PLATFORM_IOS_SIMULATOR) \
and platform != target_platform:
struct.pack_into('<I', buf, offset + 8, target_platform)
return True
return False
offset += cmdsize
Comment thread
lucasdonordeste marked this conversation as resolved.
return False


def _relabel_archive_region(buf, start, size, target_platform):
"""Relabel every arm64 Mach-O member of the ``ar`` archive (or the single
Mach-O object) occupying ``buf[start:start+size]``. Returns the count."""
region_end = min(start + size, len(buf))
if buf[start:start + 8] != b'!<arch>\n':
return 1 if _relabel_macho(buf, start, target_platform) else 0
pos = start + 8
n = 0
while pos + 60 <= region_end:
try:
member_size = int(
bytes(buf[pos + 48:pos + 58]).decode('ascii', 'replace').strip())
except ValueError:
break
name = bytes(buf[pos:pos + 16]).rstrip()
body = pos + 60
obj = body
if name.startswith(b'#1/'): # BSD long-name extension
try:
obj = body + int(name[3:])
except ValueError:
obj = body
if _relabel_macho(buf, obj, target_platform):
n += 1
pos = body + member_size + (member_size & 1) # 2-byte alignment
return n


def _relabel_buffer(buf, target_platform):
if len(buf) < 8:
return 0
fat_magic = struct.unpack_from('>I', buf, 0)[0]
if fat_magic in (FAT_MAGIC, FAT_MAGIC_64):
nfat = struct.unpack_from('>I', buf, 4)[0]
is64 = fat_magic == FAT_MAGIC_64
entry = 8
n = 0
for _ in range(nfat):
if is64:
if entry + 32 > len(buf):
break
cputype, _cpusub, offset, size = struct.unpack_from('>iIQQ', buf, entry)
entry += 32
else:
if entry + 20 > len(buf):
break
cputype, _cpusub, offset, size, _align = \
struct.unpack_from('>iIIII', buf, entry)
entry += 20
if cputype == CPU_TYPE_ARM64:
n += _relabel_archive_region(buf, offset, size, target_platform)
return n
if struct.unpack_from('<I', buf, 0)[0] == MH_MAGIC_64 \
or buf[0:8] == b'!<arch>\n':
return _relabel_archive_region(buf, 0, len(buf), target_platform)
return 0


def _relabel_file(path, target_platform):
if os.path.getsize(path) == 0:
return 0
with open(path, 'r+b') as f:
with mmap.mmap(f.fileno(), 0) as buf:
n = _relabel_buffer(buf, target_platform)
if n:
buf.flush()
return n


def _find_framework_binary(pod_dir):
fw_dir = os.path.join(pod_dir, 'Frameworks')
if not os.path.isdir(fw_dir):
return None
for name in os.listdir(fw_dir):
if name.endswith('.framework'):
base = name[:-len('.framework')]
binary = os.path.join(fw_dir, name, base)
if os.path.isfile(binary):
return binary
return None


def _iter_framework_binaries(pods_root):
for pattern in ('MLKit*', 'MLImage*'):
for pod_dir in sorted(glob.glob(os.path.join(pods_root, pattern))):
if os.path.isdir(pod_dir):
binary = _find_framework_binary(pod_dir)
if binary:
yield binary


def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--platform', default=os.environ.get('PLATFORM_NAME', ''))
parser.add_argument('--pods-root', required=True)
args = parser.parse_args(argv)

target_platform = PLATFORM_BY_SDK.get(args.platform)
if target_platform is None:
print(f'[ml_kit] skipping arm64 relabel: platform {args.platform!r} is '
'not iphoneos/iphonesimulator', file=sys.stderr)
return 0

binaries = list(_iter_framework_binaries(args.pods_root))
if not binaries:
print('[ml_kit] ERROR: no ML Kit framework binaries found under '
f'{args.pods_root!r}; arm64 slice not relabeled', file=sys.stderr)
return 1

total = sum(_relabel_file(b, target_platform) for b in binaries)
if total:
print(f'[ml_kit] relabeled {total} arm64 object(s) for {args.platform}')
return 0


if __name__ == '__main__':
try:
sys.exit(main(sys.argv[1:]))
except Exception as exc: # surface a tagged, actionable build-phase error
print(f'[ml_kit] ERROR: arm64 relabel failed: {exc}', file=sys.stderr)
sys.exit(1)
Loading