module Scan # This class detects all kinds of default values class DetectValues # This is needed as these are more complex default values # Returns the finished config object def self.set_additional_default_values config = Scan.config # First, try loading the Scanfile from the current directory config.load_configuration_file(Scan.scanfile_name) # Detect the project FastlaneCore::Project.detect_projects(config) Scan.project = FastlaneCore::Project.new(config) # Go into the project's folder, as there might be a Snapfile there imported_path = File.expand_path(Scan.scanfile_name) Dir.chdir(File.expand_path("..", Scan.project.path)) do config.load_configuration_file(Scan.scanfile_name) unless File.expand_path(Scan.scanfile_name) == imported_path end Scan.project.select_scheme devices = Scan.config[:devices] || Array(Scan.config[:device]) # important to use Array(nil) for when the value is nil if devices.count > 0 detect_simulator(devices, '', '', '', nil) else if Scan.project.ios? # An iPhone 5s is a reasonably small and useful default for tests detect_simulator(devices, 'iOS', 'IPHONEOS_DEPLOYMENT_TARGET', 'iPhone 5s', nil) elsif Scan.project.tvos? detect_simulator(devices, 'tvOS', 'TVOS_DEPLOYMENT_TARGET', 'Apple TV 1080p', 'TV') end end detect_destination default_derived_data return config end def self.default_derived_data return unless Scan.config[:derived_data_path].to_s.empty? default_path = Scan.project.build_settings(key: "BUILT_PRODUCTS_DIR") # => /Users/.../Library/Developer/Xcode/DerivedData/app-bqrfaojicpsqnoglloisfftjhksc/Build/Products/Release-iphoneos # We got 3 folders up to point to ".../DerivedData/app-[random_chars]/" default_path = File.expand_path("../../..", default_path) UI.verbose("Detected derived data path '#{default_path}'") Scan.config[:derived_data_path] = default_path end def self.filter_simulators(simulators, operator = :greater_than_or_equal, deployment_target) deployment_target_version = Gem::Version.new(deployment_target) simulators.select do |s| sim_version = Gem::Version.new(s.os_version) if operator == :greater_than_or_equal sim_version >= deployment_target_version elsif operator == :equal sim_version == deployment_target_version else false # this will show an error message in the detect_simulator method end end end def self.regular_expression_for_split_on_whitespace_followed_by_parenthesized_version # %r{ # \s # a whitespace character # (?= # followed by -- using lookahead # \( # open parenthesis # [\d\.]+ # our version -- one or more digits or full stops # \) # close parenthesis # $ # end of line # ) # end of lookahead # } /\s(?=\([\d\.]+\)$)/ end def self.detect_simulator(devices, requested_os_type, deployment_target_key, default_device_name, simulator_type_descriptor) require 'set' deployment_target_version = Scan.project.build_settings(key: deployment_target_key) || '0' simulators = filter_simulators( FastlaneCore::DeviceManager.simulators(requested_os_type).tap do |array| if array.empty? UI.user_error!(['No', simulator_type_descriptor, 'simulators found on local machine'].reject(&:nil?).join(' ')) end end, :greater_than_or_equal, deployment_target_version ).tap do |sims| if sims.empty? UI.error("No simulators found that are greater than or equal to the version of deployment target (#{deployment_target_version})") end end # At this point we have all simulators for the given deployment target (or higher) # We create 2 lambdas, which we iterate over later on # If the first lambda `matches` found a simulator to use # we'll never call the second one matches = lambda do set_of_simulators = devices.inject( Set.new # of simulators ) do |set, device_string| pieces = device_string.split(regular_expression_for_split_on_whitespace_followed_by_parenthesized_version) selector = ->(sim) { pieces.count > 0 && sim.name == pieces.first } set + ( if pieces.count == 0 [] # empty array elsif pieces.count == 1 simulators .select(&selector) .reverse # more efficient, because `simctl` prints higher versions first .sort_by! { |sim| Gem::Version.new(sim.os_version) } .pop(1) else # pieces.count == 2 -- mathematically, because of the 'end of line' part of our regular expression version = pieces[1].tr('()', '') potential_emptiness_error = lambda do |sims| UI.error("No simulators found that are equal to the version " \ "of specifier (#{version}) and greater than or equal to the version " \ "of deployment target (#{deployment_target_version})") if sims.empty? end filter_simulators(simulators, :equal, version).tap(&potential_emptiness_error).select(&selector) end ).tap do |array| UI.error("Ignoring '#{device_string}', couldn’t find matching simulator") if array.empty? end end set_of_simulators.to_a end default = lambda do UI.error("Couldn't find any matching simulators for '#{devices}' - falling back to default simulator") result = Array( simulators .select { |sim| sim.name == default_device_name } .reverse # more efficient, because `simctl` prints higher versions first .sort_by! { |sim| Gem::Version.new(sim.os_version) } .last || simulators.first ) UI.error("Found simulator \"#{result.first.name} (#{result.first.os_version})\"") if result.first result end # grab the first unempty evaluated array Scan.devices = [matches, default].lazy.map { |x| arr = x.call arr unless arr.empty? }.reject(&:nil?).first end def self.min_xcode8? Helper.xcode_version.split(".").first.to_i >= 8 end def self.detect_destination if Scan.config[:destination] UI.important("It's not recommended to set the `destination` value directly") UI.important("Instead use the other options available in `scan --help`") UI.important("Using your value '#{Scan.config[:destination]}' for now") UI.important("because I trust you know what you're doing...") return end # building up the destination now if Scan.devices.count > 0 Scan.config[:destination] = Scan.devices.map { |d| "platform=#{d.os_type} Simulator,id=#{d.udid}" } else Scan.config[:destination] = min_xcode8? ? ["platform=macOS"] : ["platform=OS X"] end end end end