Run AXe against an Xcode that ships SimulatorKit

AXe dlopens the private SimulatorKit.framework from
$DEVELOPER_DIR/Library/PrivateFrameworks. Xcode 27 relocated that
framework to Contents/SharedFrameworks, so under a selected Xcode 27
every axe command fails with "Failed to load essential private
frameworks" and all UI assertions fail uniformly.

Detect when the active Xcode lacks SimulatorKit at the path AXe expects
and inject DEVELOPER_DIR pointing at the first installed Xcode that has
it, for every axe subprocess. No global xcode-select switch required.

Claude-Session: https://claude.ai/code/session_01Aqq5bSGrYfjQNvkTEzWcva
This commit is contained in:
Arkadiusz Fal
2026-06-30 09:35:32 +02:00
parent eb551e7641
commit a4b0c60a3e

View File

@@ -155,7 +155,7 @@ module UITest
# Type text # Type text
# @param text [String] Text to type # @param text [String] Text to type
def type(text) def type(text)
output, status = Open3.capture2e('axe', 'type', '--stdin', '--udid', @udid, stdin_data: text) output, status = Open3.capture2e(self.class.axe_env, 'axe', 'type', '--stdin', '--udid', @udid, stdin_data: text)
output = output.dup.force_encoding('UTF-8') if output.is_a?(String) output = output.dup.force_encoding('UTF-8') if output.is_a?(String)
raise AxeError, "type failed: #{output}" unless status.success? raise AxeError, "type failed: #{output}" unless status.success?
end end
@@ -202,11 +202,53 @@ module UITest
private private
def run_axe(*) def run_axe(*)
output, status = Open3.capture2e('axe', *, '--udid', @udid) output, status = Open3.capture2e(self.class.axe_env, 'axe', *, '--udid', @udid)
output = output.dup.force_encoding('UTF-8') if output.is_a?(String) output = output.dup.force_encoding('UTF-8') if output.is_a?(String)
[output, status] [output, status]
end end
class << self
# Environment overrides for every `axe` invocation.
#
# AXe loads Xcode's private SimulatorKit.framework at runtime. Some Xcode
# betas ship without it (e.g. Xcode 27 beta), which makes every `axe`
# command fail with "Failed to load essential private frameworks". When
# the active Xcode is missing SimulatorKit, fall back to any installed
# Xcode that has it by overriding DEVELOPER_DIR for the axe subprocess.
# @return [Hash] Environment hash to pass to Open3
def axe_env
return @axe_env if defined?(@axe_env)
@axe_env = simulatorkit?(active_developer_dir) ? {} : fallback_env
end
private
def fallback_env
dir = usable_developer_dir
dir ? { 'DEVELOPER_DIR' => dir } : {}
end
# Path to the active Xcode's Developer directory (xcode-select -p)
def active_developer_dir
@active_developer_dir ||= `xcode-select -p 2>/dev/null`.strip
end
# First installed Xcode Developer directory that ships SimulatorKit
def usable_developer_dir
Dir.glob('/Applications/Xcode*.app/Contents/Developer').find do |dir|
simulatorkit?(dir)
end
end
# Whether a Developer directory ships the SimulatorKit private framework
def simulatorkit?(developer_dir)
return false if developer_dir.nil? || developer_dir.empty?
File.exist?(File.join(developer_dir, 'Library', 'PrivateFrameworks', 'SimulatorKit.framework'))
end
end
# Wait for a file to exist and have non-zero size # Wait for a file to exist and have non-zero size
# Helps avoid race conditions where screenshot isn't fully written # Helps avoid race conditions where screenshot isn't fully written
# @param path [String] Path to the file # @param path [String] Path to the file