Class: Fastlane::Helper::Xcspec
- Inherits:
-
Object
- Object
- Fastlane::Helper::Xcspec
- Defined in:
- lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb
Overview
Xcspec helper class.
Defined Under Namespace
Classes: Mapping
Evaluating Conditions collapse
- NO =
Value to use for ‘NO`.
false
- YES =
Value to use for ‘YES`.
true
Initialization collapse
-
#options ⇒ Array<Hash>
readonly
Xcspec options.
-
#path ⇒ Object
readonly
Path to xcspec file.
Mapping collapse
-
#map_args(option, args, value, build_settings) ⇒ String
Map command line arguments to build flags.
-
#map_build_setting_value(name, value, build_settings) ⇒ Array<Mapping>
Map single build setting value.
-
#map_build_settings(build_settings) ⇒ Hash
Map build settings to build flags.
-
#map_option(option, value, build_settings) ⇒ Array<Mapping>
Map the spec option to build settings using the value.
-
#map_option_scalar_value(option, value, build_settings) ⇒ Mapping
Map the spec option scalar value to build flags.
Initialization collapse
-
.load_plist(path) ⇒ Hash
Load plist as a dictionary.
-
#initialize(path, core_build_system_spec: nil) ⇒ Xcspec
constructor
Create new instance.
Helpers collapse
-
#find_option(name) ⇒ Hash
Find option by name.
Evaluating Conditions collapse
-
#check_condition(option, build_settings) ⇒ Boolean
Evaluate and check the condition.
Constructor Details
#initialize(path, core_build_system_spec: nil) ⇒ Xcspec
Create new instance.
58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 58 def initialize(path, core_build_system_spec: nil) UI.user_error!("No such file: #{path}") unless path && File.exist?(path) @path = path plist = Xcspec.load_plist(path) tools = plist.kind_of?(Array) ? plist : [plist] @options = tools.flat_map { |t| t["Options"] || t["Properties"] }.compact @options += core_build_system_spec. if core_build_system_spec end |
Instance Attribute Details
#options ⇒ Array<Hash> (readonly)
Returns Xcspec options.
53 54 55 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 53 def @options end |
#path ⇒ Object (readonly)
Path to xcspec file.
50 51 52 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 50 def path @path end |
Class Method Details
.load_plist(path) ⇒ Hash
Load plist as a dictionary.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 73 def self.load_plist(path) file_type = `file -b --mime-type #{path.shellescape}`.chomp if file_type == "text/xml" || file_type == "application/xml" xml_plist = path else if FastlaneCore::Helper.mac? xml_plist = Tempfile.new("xcspec.plist").path result = system("plutil -convert xml1 -o #{xml_plist.shellescape} #{path.shellescape}") UI.user_error!("Couldn't convert #{path} xcspec to XML plist") unless result else # There is plist-utils library, but it can only convert binary to XML, can't handle ASCII. UI.user_error!("Can't convert ASCII plists to XML on Linux or Windows platform") end end Nokogiri::PList(File.open(xml_plist)) end |
Instance Method Details
#check_condition(option, build_settings) ⇒ Boolean
Evaluate and check the condition. Conditions come in form like this: “$(COMPILER_INDEX_STORE_ENABLE) == YES || ( $(COMPILER_INDEX_STORE_ENABLE) == Default && $(GCC_OPTIMIZATION_LEVEL) == 0 )” Return true if there’s no condition to evaluate.
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 287 def check_condition(option, build_settings) condition = option["Condition"] return true unless condition # Need to resolve all $(VAR) references using build settings. # At this point using complete build settings, # which include default values for all known build settings. # Handy that read_xcconfig action already has a helper to resolve a value. # Just pass current build settings as parent config. resolved_condition = Fastlane::Actions::ReadXcconfigAction.resolve_value( condition, key: "condition", resolved: {}, parent: build_settings ) # Resolved condition is now a C-like expression, which also can be evaluated as Ruby code. # With small changes though. # All literals in condition can be treated as strings, except for YES/NO boolean literals, # which are replaced with `true` and `false`. # However, xcspecs are very inconsistent when it comes to strings. # Some values are used unquoted in the conditions, such as: # Default, mh_object, bitcode. # There's also use of '' and \"\" for empty string, the latter may cause issues. # Then "same-as-input" that may have to be resolved - do not handle for now. # Finally, $(variant) == profile - just leave it for now. # Ways to fix. # 1. Scan for all enums in xcspecs and define module vars for each enum value, # so when evaluated, is replaced with variable. # 2. Process resolved condition by wrapping all unwrapped entries in quotes. # Using approach 2 for now with insane regex. # In all cases replace \"\" with ''. resolved_condition.gsub!('"', "'") # Quote everything except YES and NO. resolved_condition = (" " + resolved_condition + " ").gsub(/\s((\w|\d|-|\+|\.)+?)\s/, " '\\1' ").gsub(/'(YES|NO)'/, '\\1') begin # rubocop:disable Security/Eval eval(resolved_condition) # rubocop:enable Security/Eval rescue SyntaxError # Values like USE_LLVM_TARGET_TRIPLES_FOR_CLANG are not defined anywhere. # Those will be resolved into nothing and result into condition that can't be evaluated. false end end |
#find_option(name) ⇒ Hash
Find option by name.
98 99 100 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 98 def find_option(name) @options.find { |o| o["Name"] == name } end |
#map_args(option, args, value, build_settings) ⇒ String
Map command line arguments to build flags.
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 249 def map_args(option, args, value, build_settings) return "" unless args if args.kind_of?(Hash) map_args(option, args[value] || args["<<otherwise>>"], value, build_settings) else # Args should be an array here, but can be just single string. resolved_args = args.kind_of?(Array) ? args.dup : [args] # Replacing $(value) is just one part, need to resolve any build settings present too. resolved_args = resolved_args.map { |a| a.gsub("$(value)", value) }.join(" ") Fastlane::Actions::ReadXcconfigAction.resolve_value( resolved_args, key: "resolved_args", resolved: {}, parent: build_settings ) end end |
#map_build_setting_value(name, value, build_settings) ⇒ Array<Mapping>
Map single build setting value.
142 143 144 145 146 147 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 142 def map_build_setting_value(name, value, build_settings) option = find_option(name) return nil unless option map_option(option, value, build_settings) end |
#map_build_settings(build_settings) ⇒ Hash
Map build settings to build flags.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 109 def map_build_settings(build_settings) # Some settings like ENABLE_TESTABILITY are used as default value for other build settings, # e.g. SWIFT_ENABLE_TESTABILITY. # So need to treat dependant (implicit) build settings as if they were defined in xcconfig # in order to get them properly resolved. implicit_build_settings = @options.reduce({}) do |memo, opt| name = opt["Name"] next memo if build_settings.key?(name) reference = build_settings.find { |k, _| opt["DefaultValue"].eql?("$(#{k})") } reference ? memo.merge({ name => reference.last }) : memo end build_settings = build_settings.merge(implicit_build_settings) # Build settings provided for mapping will not include all possible build settings. # Add default values for missing build settings. missing_build_settings = @options.reduce({}) do |memo, opt| name = opt["Name"] build_settings.key?(name) ? memo : memo.merge({ name => opt["DefaultValue"] }) end complete_build_settings = build_settings.merge(missing_build_settings) mappings = build_settings.flat_map do |setting, value| map_build_setting_value(setting, value, complete_build_settings) end.compact mappings.reduce(Mapping.new) { |memo, m| memo.join(m) } end |
#map_option(option, value, build_settings) ⇒ Array<Mapping>
Map the spec option to build settings using the value.
154 155 156 157 158 159 160 161 162 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 154 def map_option(option, value, build_settings) # Evaluate and check the 'Condition'. return nil unless check_condition(option, build_settings) # If type of the value is one of List types (StringList, PathList), # then split it into value list, else just use scalar value itself. scalar_values = option["Type"].downcase.end_with?("list") ? value.split : [value] scalar_values.flat_map { |v| map_option_scalar_value(option, v, build_settings) }.compact end |
#map_option_scalar_value(option, value, build_settings) ⇒ Mapping
Map the spec option scalar value to build flags.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/fastlane/plugin/xcconfig_actions/helper/xcspec.rb', line 169 def map_option_scalar_value(option, value, build_settings) # At this point we deal with scalar value. # Type = StringList # - CommandLineFlag and CommandLinePrefixFlag - map each string in the list. # - CommandLineArgs - map each string in the list. # - Finally there are entries with only Category = CustomFlags. # Those end up in compiler flags for sure, so looks like if category is CustomFlags, # then should use them as is. # There's only 3 of them: OTHER_CFLAGS, OTHER_CPLUSPLUSFLAGS and WARNING_CFLAGS. # PathList, that for all intents and purposes can be handed as StringList. # - CommandLineArgs # - If input is StringList, then map each entry from string list according to CommandLineArgs value # The value of CommandLineArgs can be an array, use flat map # Can have a switch, e.g. # CommandLineArgs = { # "" = (); # "<<otherwise>>" = ( # "-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$(value)", # ); # }; # All these switches, however, map to nothing for empty string and to something for non-empty string. # The vey same switch is used for enums. Switching for enums vs string is no different, # since switching happens on string values. # <<otherwise>> is for other values # - AdditionalLinkerArgs # Few build settings have them, collected separately, work just like CommandLineArgs for parsing. # - CommandLineFlag and CommandLinePrefixFlag # - If input is StringList then applied to each entry in the list, to each entry applied as to a scalar value: # - If input is scalar String value, then applied just once as -<flag> $(value) # - If input is boolean, then just the flag is used, e.g. -<flag>, no Prefix option supported for boolean. # - While for prefix it appears there's no space and it is -<prefix>$(value) # - An exception(!) is MACH_O_TYPE, where no value is appended but just the flag is used. # It is also the only enum where list of values has command line flag for each value. # Type = Enumeration # Most of them have CommandLineArgs with a switch. # Some also come with AdditionalLinkerArgs, which is mapped in similar way. # Then there's MACH_O_TYPE, which has CommandLineFlag under Values... # But only when used for linker flags: # Value = "mh_dylib"; # CommandLineFlag = "-dynamiclib"; # -dynamiclib is a flag that doesn't take input, so only specified as is. flags = "" if (cli_args = option["CommandLineArgs"]) flags = map_args(option, cli_args, value, build_settings) elsif (cli_flag = option["CommandLineFlag"]) # If Boolean and YES, then use CommandLineFlag value. if option["Type"] == "Boolean" flags = cli_flag if value == "YES" else flags = [cli_flag, value].join(" ") end elsif (cli_prefix_flag = option["CommandLinePrefixFlag"]) flags = [cli_prefix_flag, value].join # Join with no space in between. elsif option["Category"] == "CustomFlags" flags = value else # Nothing to map to, except for when it's MACH_O_TYPE, when it has list of dictionaries: # Values = ( { Value = "v", CommandLineFlag = "f", ... } ) match = (option["Values"] || []).find { |v| v["Value"] == value } || {} flags = match["CommandLineFlag"] || "" end linker_flags = "" if (linker_args = option["AdditionalLinkerArgs"]) linker_flags = map_args(option, linker_args, value, build_settings) end Mapping.new(flags, linker_flags) end |