fastlane-plugin-xcconfig_actions 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1de54217087690915ba20ee2e6cb210e198d0663696dcb59314aab235391142b
4
- data.tar.gz: 4cce95907529eb88ffff000abadbe3bcbf7a0cc119418fd8ac09996a8809b571
3
+ metadata.gz: 1ff5347f5b29ae989604b428568e7a6f6f76a782699a2d6f0afdb1696f9d41ce
4
+ data.tar.gz: ccabe7e926c527995f7d648916ff5113d510cb9099ea9d312fd49a8db3c08573
5
5
  SHA512:
6
- metadata.gz: 4ca5c56e3317e7407d37f2175ec367bf392ad010377191fe56b65c6c07a28302f87ec1253021182fa88449220c8e6fdd3ba6baaf60e45336d7298e0400e0d5e1
7
- data.tar.gz: c5e29e943f531cb921c7a1f11ea542293ab4296e757379510cce3d4759f19bd195f6b5c62cca971c9f022e2372767b778780285248197db1511346b38d7e77e5
6
+ metadata.gz: a5cf33cdd535fe366170caafe3b6d9a46262dcf9273f14b4c3fa3cebdefd84989d60f59ee59e5e189f394186b5d98666def29db384f753f1e8c62919060dfd66
7
+ data.tar.gz: 510bed17f4306c2d6aaac5c48992554846ecb19d724574a701a0244fdf32c1fd0d13c3a0baf79bb714459683ee9a643c7f1309ce51353b031185c7c40df75dc9
data/README.md CHANGED
@@ -34,6 +34,41 @@ Things **not supported** at the moment:
34
34
  - Use of `<DEVELOPER_DIR>` in include paths
35
35
  - Use of curly braces in variable references, e.g. `${VAR}`
36
36
 
37
+ The build settings are also saved as a dictionary under `SharedValues::XCCONFIG_ACTIONS_BUILD_SETTINGS` key in current `lane_context`.
38
+
39
+ ### build_settings_to_flags
40
+
41
+ Map build settings to Clang Cxx/Objective-C compiler, Swift compiler and Linker flags.
42
+
43
+ This action is useful when you plan to reuse xcconfigs with other tools, such as [Buck](https://buckbuild.com/), and you want to translate xcconfigs into the compiler/linker flags.
44
+
45
+ Build flags can be printed to standard output or saved to file.
46
+ The flags are also available via lane context as `lane_context[SharedValues::XCCONFIG_ACTIONS_BUILD_FLAGS]`.
47
+
48
+ The result is a dictionary with following keys:
49
+
50
+ - `compiler_flags` for Clang CXX/Objective-C compiler.
51
+ - `swift_compiler_flags` for Swift compiler.
52
+ - `linker_flags` for Clang linker.
53
+
54
+ <!-- TODO: Add info on how it works. -->
55
+
56
+ References:
57
+
58
+ - [Xcode Build Settings](https://help.apple.com/xcode/mac/10.2/#/itcaec37c2a6)
59
+ - [LLVM Clang Command Line Options](https://clang.llvm.org/docs/ClangCommandLineReference.html)
60
+ - [LLVM Clang Diagnostics](https://clang.llvm.org/docs/DiagnosticsReference.html)
61
+ - [GCC Warning Options](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html)
62
+ - https://gist.github.com/fabiopelosin/4560417
63
+ - https://pewpewthespells.com/blog/xcode_build_system.html
64
+
65
+ #### Known Issues
66
+
67
+ Flags like `-sdk iphoneos` and `-isysroot iphoneos` may not be suitable for all uses, so may have to remove them from all flags.
68
+
69
+ The flag like `-std=gnu++14` is added to `compiler_flags` but it's not applicable for Objective-C code.
70
+ Most tools have differentiation between C flags (C and Objective-C) and Cxx flags (C++/Objective-C++).
71
+
37
72
  ### validate_xcconfig
38
73
 
39
74
  Validate xcconfig using set of very opinionated rules:
@@ -96,6 +131,9 @@ Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plu
96
131
  # Read xcconfig example.
97
132
  bundle exec fastlane read
98
133
 
134
+ # Map build settings to build flags.
135
+ bundle exec fastlane build_flags
136
+
99
137
  # Validate xcconfig example.
100
138
  bundle exec fastlane validate
101
139
  ```
@@ -0,0 +1,122 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/xcconfig_actions_helper'
3
+ require_relative '../helper/xcspec'
4
+
5
+ module Fastlane
6
+ module Actions
7
+ ActionHelper = Fastlane::Helper::XcconfigActionsHelper
8
+ Xcspec = Fastlane::Helper::Xcspec
9
+
10
+ module SharedValues
11
+ XCCONFIG_ACTIONS_BUILD_FLAGS = :XCCONFIG_ACTIONS_BUILD_FLAGS
12
+ end
13
+
14
+ class BuildSettingsToFlagsAction < Action
15
+ def self.run(params)
16
+ build_settings = params[:build_settings] || Actions.lane_context[SharedValues::XCCONFIG_ACTIONS_BUILD_SETTINGS]
17
+ UI.user_error!("Missing build settings input") unless build_settings
18
+ xcode = params[:xcode]
19
+
20
+ clang_spec = load_tool_spec("Clang*", xcode: xcode)
21
+ swift_spec = load_tool_spec("Swift*", xcode: xcode)
22
+ linker_spec = load_tool_spec("Ld*", xcode: xcode)
23
+
24
+ clang_mapping = clang_spec.map_build_settings(build_settings)
25
+ swift_mapping = swift_spec.map_build_settings(build_settings)
26
+ linker_mapping = linker_spec.map_build_settings(build_settings)
27
+
28
+ flags = {
29
+ "compiler_flags" => clang_mapping.flags,
30
+ "swift_compiler_flags" => swift_mapping.flags,
31
+ "linker_flags" => [
32
+ linker_mapping.flags,
33
+ linker_mapping.linker_flags,
34
+ clang_mapping.linker_flags,
35
+ swift_mapping.linker_flags
36
+ ].reject(&:empty?).join(" ")
37
+ }
38
+
39
+ Actions.lane_context[SharedValues::XCCONFIG_ACTIONS_BUILD_FLAGS] = flags
40
+
41
+ if params[:output_path]
42
+ File.open(params[:output_path], "w") { |f| f.puts(flags.to_json) }
43
+ else
44
+ return flags
45
+ end
46
+ end
47
+
48
+ ###
49
+ # @!group Xcspecs
50
+ ###
51
+
52
+ def self.load_tool_spec(tool, xcode:)
53
+ core_spec_path = ActionHelper.find_xcspec("CoreBuildSystem*", xcode: xcode)
54
+ standard_spec = Xcspec.new(core_spec_path, id: "com.apple.buildsettings.standard")
55
+ core_spec = Xcspec.new(core_spec_path, id: "com.apple.build-system.core")
56
+
57
+ Xcspec.new(
58
+ ActionHelper.find_xcspec(tool, xcode: xcode),
59
+ standard_spec: standard_spec,
60
+ core_spec: core_spec
61
+ )
62
+ end
63
+
64
+ ###
65
+ # @!group Info and Options
66
+ ###
67
+
68
+ def self.description
69
+ "Map xcconfig build settings to compiler and linker build flags"
70
+ end
71
+
72
+ def self.authors
73
+ ["Maksym Grebenets"]
74
+ end
75
+
76
+ def self.return_value
77
+ "Build flags dictionary"
78
+ end
79
+
80
+ def self.details
81
+ [
82
+ "Build flags keys:",
83
+ "- compiler_flags: CXX compiler flags for clang compiler",
84
+ "- swift_compiler_flags: Compiler flags for Swift compiler",
85
+ "- linker_flags: Linker flags for clang linker (Cxx and Swift)"
86
+ ].join("\n")
87
+ end
88
+
89
+ def self.category
90
+ :building
91
+ end
92
+
93
+ def self.available_options
94
+ [
95
+ FastlaneCore::ConfigItem.new(key: :build_settings,
96
+ env_name: "XCCONFIG_ACTIONS_BUILD_FLAGS_BUILD_SETTINGS",
97
+ description: "Build settings to convert to build flags",
98
+ optional: true,
99
+ type: Hash,
100
+ verify_block: proc do |value|
101
+ UI.user_error!("Missing build settings") if value.nil?
102
+ end),
103
+ FastlaneCore::ConfigItem.new(key: :xcode,
104
+ env_name: "XCCONFIG_ACTIONS_BUILD_FLAGS_XCODE",
105
+ description: "Xcode version of path to Xcode.app",
106
+ optional: true,
107
+ default_value: "10.2",
108
+ type: String),
109
+ FastlaneCore::ConfigItem.new(key: :output_path,
110
+ env_name: "XCCONFIG_ACTIONS_BUILD_FLAGS_OUTPUT_PATH",
111
+ description: "Output path to save build settings JSON",
112
+ optional: true,
113
+ type: String)
114
+ ]
115
+ end
116
+
117
+ def self.is_supported?(platform)
118
+ [:ios, :mac].include?(platform)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -4,6 +4,10 @@ require_relative '../helper/xcconfig_actions_helper'
4
4
 
5
5
  module Fastlane
6
6
  module Actions
7
+ module SharedValues
8
+ XCCONFIG_ACTIONS_BUILD_SETTINGS = :XCCONFIG_ACTIONS_BUILD_SETTINGS
9
+ end
10
+
7
11
  class ReadXcconfigAction < Action
8
12
  def self.run(params)
9
13
  path = params[:path]
@@ -13,9 +17,7 @@ module Fastlane
13
17
 
14
18
  config = read_config(path)
15
19
 
16
- if params[:no_resolve]
17
- json = config.to_json
18
- else
20
+ unless params[:no_resolve]
19
21
  parent_config = read_config(parent)
20
22
 
21
23
  parent_config["SRCROOT"] = srcroot
@@ -24,20 +26,22 @@ module Fastlane
24
26
  if Helper::XcconfigActionsHelper.command_exist?("xcodebuild")
25
27
  # Set value of XCODE_VERSION_MAJOR not available when reading xcconfigs directly.
26
28
  xcode_version = `xcodebuild -version | head -n1 | cut -d' ' -f2 | xargs`.strip
27
- xcode_version_majon_padded = xcode_version.split(".").first.rjust(2, "0") + "00"
28
- parent_config["XCODE_VERSION_MAJOR"] = xcode_version_majon_padded
29
+ xcode_version_major_padded = xcode_version.split(".").first.rjust(2, "0") + "00"
30
+ parent_config["XCODE_VERSION_MAJOR"] = xcode_version_major_padded
29
31
  end
30
32
 
31
33
  resolved_parent_config = resolve_config(parent_config)
32
34
  resolved_config = resolve_config(config, parent: resolved_parent_config)
33
35
 
34
- json = resolved_parent_config.merge(resolved_config).to_json
36
+ config = resolved_parent_config.merge(resolved_config)
35
37
  end
36
38
 
39
+ Actions.lane_context[SharedValues::XCCONFIG_ACTIONS_BUILD_SETTINGS] = config
40
+
37
41
  if params[:output_path]
38
- File.open(params[:output_path], "w") { |f| f.puts(json) }
42
+ File.open(params[:output_path], "w") { |f| f.puts(config.to_json) }
39
43
  else
40
- return json
44
+ return config
41
45
  end
42
46
  end
43
47
 
@@ -96,6 +100,7 @@ module Fastlane
96
100
  def self.resolve_value(value, key:, resolved: {}, parent: {})
97
101
  matches = value.scan(/(\$\([^$\)]*\))/)
98
102
 
103
+ mutable_value = value.dup # Prevent unwanted side-effect of input modification.
99
104
  matches.each do |group|
100
105
  group.each do |match|
101
106
  var_name = match.delete("$()")
@@ -105,13 +110,13 @@ module Fastlane
105
110
  else
106
111
  resolved[var_name] || parent[var_name]
107
112
  end
108
- value.gsub!(match, var_value || "")
109
- resolved[key] = value
113
+ mutable_value.gsub!(match, var_value || "")
114
+ resolved[key] = mutable_value
110
115
  end
111
116
  end
112
117
 
113
118
  # If there are still variables, keep resolving then.
114
- value.include?("$(") ? resolve_value(value, key: key, resolved: resolved, parent: parent) : value
119
+ mutable_value.include?("$(") ? resolve_value(mutable_value, key: key, resolved: resolved, parent: parent) : mutable_value
115
120
  end
116
121
 
117
122
  # Resolve xcconfig values using parent config.
@@ -147,6 +152,10 @@ module Fastlane
147
152
  ""
148
153
  end
149
154
 
155
+ def self.category
156
+ :building
157
+ end
158
+
150
159
  def self.available_options
151
160
  [
152
161
  FastlaneCore::ConfigItem.new(key: :path,
@@ -190,7 +199,7 @@ module Fastlane
190
199
  end
191
200
 
192
201
  def self.is_supported?(platform)
193
- true
202
+ [:ios, :mac].include?(platform)
194
203
  end
195
204
  end
196
205
  end
@@ -91,6 +91,10 @@ module Fastlane
91
91
  ].join("\n")
92
92
  end
93
93
 
94
+ def self.category
95
+ :linting
96
+ end
97
+
94
98
  def self.available_options
95
99
  [
96
100
  FastlaneCore::ConfigItem.new(key: :path,
@@ -113,7 +117,7 @@ module Fastlane
113
117
  end
114
118
 
115
119
  def self.is_supported?(platform)
116
- true
120
+ [:ios, :mac].include?(platform)
117
121
  end
118
122
  end
119
123
  end
@@ -0,0 +1,18 @@
1
+ from bs4 import BeautifulSoup
2
+ import re
3
+
4
+ soup = BeautifulSoup(open("page.html"), "html.parser")
5
+
6
+ entries = soup.find_all("div", id=re.compile("dev.*"))
7
+
8
+ title_re = re.compile(".*\((.*)\)")
9
+ settings = []
10
+ for e in entries:
11
+ text = e.find("h2", class_="Name").text
12
+ m = title_re.match(text)
13
+ if m == None:
14
+ settings.append(text)
15
+ else:
16
+ settings.append(m.group(1))
17
+
18
+ print settings
@@ -0,0 +1,23 @@
1
+ # https://stackoverflow.com/questions/8049520/web-scraping-javascript-page-with-python
2
+ from selenium import webdriver
3
+ from selenium.webdriver.support.ui import WebDriverWait
4
+ from selenium.webdriver.support import expected_conditions
5
+ from selenium.webdriver.common.by import By
6
+
7
+ options = webdriver.FirefoxOptions()
8
+ options.headless = True
9
+ driver = webdriver.Firefox(firefox_options=options)
10
+
11
+ id = "itcaec37c2a6"
12
+ url = "https://help.apple.com/xcode/mac/10.2/#/{}".format(id)
13
+ driver.get(url)
14
+
15
+ # https://stackoverflow.com/questions/26566799/wait-until-page-is-loaded-with-selenium-webdriver-for-python
16
+ WebDriverWait(driver, 10).until(
17
+ expected_conditions.text_to_be_present_in_element(
18
+ (By.ID, id),
19
+ 'Build settings reference'
20
+ )
21
+ )
22
+
23
+ print driver.page_source.encode('utf-8').strip()
@@ -42,6 +42,16 @@ module Fastlane
42
42
  includes: includes
43
43
  }
44
44
  end
45
+
46
+ def self.find_xcspec(name, xcode:)
47
+ search_path = File.exist?(xcode) ? File.join(xcode, "Contents/Plugins") : File.join(File.dirname(__FILE__), "xcspecs", xcode)
48
+ UI.user_error!("Can't find app path of xcspecs folder for xcode: #{xcode}") unless File.exist?(search_path)
49
+
50
+ query = File.join(search_path, "**", name + ".xcspec")
51
+ xcspec = Dir[query].first
52
+ UI.user_error!("Can't find xcspec with name: #{name}") unless File.exist?(xcspec)
53
+ xcspec
54
+ end
45
55
  end
46
56
  end
47
57
  end
@@ -0,0 +1,259 @@
1
+ require "plist"
2
+ require "nokogiri-plist"
3
+
4
+ module Fastlane
5
+ UI = UI unless Fastlane.const_defined?("UI")
6
+
7
+ module Helper
8
+ # Xcspec helper class.
9
+ class Xcspec
10
+ ###
11
+ # @!group Mapping
12
+ ###
13
+ class Mapping
14
+ attr_reader :flags, :linker_flags
15
+
16
+ def initialize(flags = "", linker_flags = "")
17
+ @flags = flags
18
+ @linker_flags = linker_flags
19
+ end
20
+
21
+ # Join with other mapping and return new mapping.
22
+ def join(other)
23
+ return self if other.nil?
24
+
25
+ joined_flags = [flags, other.flags].reject(&:empty?).join(" ")
26
+ joined_linker_flags = [linker_flags, other.linker_flags].reject(&:empty?).join(" ")
27
+
28
+ Mapping.new(joined_flags, joined_linker_flags)
29
+ end
30
+ end
31
+
32
+ ###
33
+ # @!group Initialization
34
+ ###
35
+
36
+ attr_reader :tool
37
+
38
+ def initialize(path, id: nil, standard_spec: nil, core_spec: nil)
39
+ UI.user_error!("No such file: #{path}") unless path && File.exist?(path)
40
+
41
+ @plist = Xcspec.load_plist(path)
42
+ @tool = find_tool(id)
43
+
44
+ all_tools = [@tool]
45
+ all_tools << standard_spec.tool if standard_spec
46
+ all_tools << core_spec.tool if core_spec
47
+
48
+ @all_options = all_tools.flat_map { |t| t["Options"] }
49
+ end
50
+
51
+ def self.load_plist(path)
52
+ file_type = `file -b --mime-type #{path.shellescape}`.chomp
53
+ if file_type == "text/xml" || file_type == "application/xml"
54
+ xml_plist = path
55
+ else
56
+ if FastlaneCore::Helper.mac?
57
+ xml_plist = Tempfile.new("xcspec.plist").path
58
+ result = system("plutil -convert xml1 -o #{xml_plist.shellescape} #{path.shellescape}")
59
+ UI.user_error!("Couldn't convert #{path} xcspec to XML plist") unless result
60
+ else
61
+ # There is plist-utils library, but it can only convert binary to XML, can't handle ASCII.
62
+ UI.user_error!("Can't convert ASCII plists to XML on Linux or Windows platform")
63
+ end
64
+ end
65
+
66
+ Nokogiri::PList(File.open(xml_plist))
67
+ end
68
+
69
+ def find_tool(id)
70
+ return @plist unless @plist.kind_of?(Array)
71
+ id ? @plist.find { |t| t["Identifier"] == id } : @plist.first
72
+ end
73
+
74
+ ###
75
+ # @!group Mapping
76
+ ###
77
+
78
+ def map_build_settings(build_settings)
79
+ # Build settings provided for mapping will not include all possible build settings.
80
+ # Add default values for missing build settings.
81
+ missing_build_settings = @all_options.reduce({}) do |memo, opt|
82
+ name = opt["Name"]
83
+ build_settings.key?(name) ? memo : memo.merge({ name => opt["DefaultValue"] })
84
+ end
85
+ complete_build_settings = build_settings.merge(missing_build_settings)
86
+
87
+ mappings = build_settings.flat_map do |setting, value|
88
+ map_build_setting_value(setting, value, complete_build_settings)
89
+ end.compact
90
+
91
+ mappings.reduce(Mapping.new) { |memo, m| memo.join(m) }
92
+ end
93
+
94
+ def map_build_setting_value(name, value, build_settings)
95
+ option = @tool["Options"].find { |o| o["Name"] == name }
96
+ return nil unless option
97
+
98
+ map_option(option, value, build_settings)
99
+ end
100
+
101
+ def map_option(option, value, build_settings)
102
+ # Evaluate and check the 'Condition'.
103
+ return nil unless check_condition(option, build_settings)
104
+
105
+ # If type of the value is one of List types (StringList, PathList),
106
+ # then split it into value list, else just use scalar value itself.
107
+ scalar_values = option["Type"].downcase.end_with?("list") ? value.split : [value]
108
+ scalar_values.flat_map { |v| map_option_scalar_value(option, v, build_settings) }.compact
109
+ end
110
+
111
+ def map_option_scalar_value(option, value, build_settings)
112
+ # At this point we deal with scalar value.
113
+
114
+ # Type = StringList
115
+ # - CommandLineFlag and CommandLinePrefixFlag - map each string in the list.
116
+ # - CommandLineArgs - map each string in the list.
117
+ # - Finally there are entries with only Category = CustomFlags.
118
+ # Those end up in compiler flags for sure, so looks like if category is CustomFlags,
119
+ # then should use them as is.
120
+ # There's only 3 of them: OTHER_CFLAGS, OTHER_CPLUSPLUSFLAGS and WARNING_CFLAGS.
121
+ # PathList, that for all intents and purposes can be handed as StringList.
122
+
123
+ # - CommandLineArgs
124
+ # - If input is StringList, then map each entry from string list according to CommandLineArgs value
125
+ # The value of CommandLineArgs can be an array, use flat map
126
+ # Can have a switch, e.g.
127
+ # CommandLineArgs = {
128
+ # "" = ();
129
+ # "<<otherwise>>" = (
130
+ # "-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$(value)",
131
+ # );
132
+ # };
133
+ # All these switches, however, map to nothing for empty string and to something for non-empty string.
134
+ # The vey same switch is used for enums. Switching for enums vs string is no different,
135
+ # since switching happens on string values.
136
+ # <<otherwise>> is for other values
137
+ # - AdditionalLinkerArgs
138
+ # Few build settings have them, collected separately, work just like CommandLineArgs for parsing.
139
+ # - CommandLineFlag and CommandLinePrefixFlag
140
+ # - If input is StringList then applied to each entry in the list, to each entry applied as to a scalar value:
141
+ # - If input is scalar String value, then applied just once as -<flag> $(value)
142
+ # - If input is boolean, then just the flag is used, e.g. -<flag>, no Prefix option supported for boolean.
143
+ # - While for prefix it appears there's no space and it is -<prefix>$(value)
144
+ # - An exception(!) is MACH_O_TYPE, where no value is appended but just the flag is used.
145
+ # It is also the only enum where list of values has command line flag for each value.
146
+
147
+ # Type = Enumeration
148
+ # Most of them have CommandLineArgs with a switch.
149
+ # Some also come with AdditionalLinkerArgs, which is mapped in similar way.
150
+ # Then there's MACH_O_TYPE, which has CommandLineFlag under Values...
151
+ # But only when used for linker flags:
152
+ # Value = "mh_dylib";
153
+ # CommandLineFlag = "-dynamiclib";
154
+ # -dynamiclib is a flag that doesn't take input, so only specified as is.
155
+
156
+ flags = ""
157
+ if (cli_args = option["CommandLineArgs"])
158
+ flags = map_args(option, cli_args, value, build_settings)
159
+ elsif (cli_flag = option["CommandLineFlag"])
160
+ # If Boolean and YES, then use CommandLineFlag value.
161
+ if option["Type"] == "Boolean"
162
+ flags = cli_flag if value == "YES"
163
+ else
164
+ flags = [cli_flag, value].join(" ")
165
+ end
166
+ elsif (cli_prefix_flag = option["CommandLinePrefixFlag"])
167
+ flags = [cli_prefix_flag, value].join # Join with no space in between.
168
+ elsif option["Category"] == "CustomFlags"
169
+ flags = value
170
+ else
171
+ # Nothing to map to, except for when it's MACH_O_TYPE, when it has list of dictionaries:
172
+ # Values = ( { Value = "v", CommandLineFlag = "f", ... } )
173
+ match = (option["Values"] || []).find { |v| v["Value"] == value } || {}
174
+ flags = match["CommandLineFlag"] || ""
175
+ end
176
+
177
+ linker_flags = ""
178
+ if (linker_args = option["AdditionalLinkerArgs"])
179
+ linker_flags = map_args(option, linker_args, value, build_settings)
180
+ end
181
+
182
+ Mapping.new(flags, linker_flags)
183
+ end
184
+
185
+ def map_args(option, args, value, build_settings)
186
+ return "" unless args
187
+
188
+ if args.kind_of?(Hash)
189
+ map_args(option, args[value] || args["<<otherwise>>"], value, build_settings)
190
+ else
191
+ # Args should be an array here, but can be just single string.
192
+
193
+ resolved_args = args.kind_of?(Array) ? args.dup : [args]
194
+ # Replacing $(value) is just one part, need to resolve any build settings present too.
195
+ resolved_args = resolved_args.map { |a| a.gsub("$(value)", value) }.join(" ")
196
+ Fastlane::Actions::ReadXcconfigAction.resolve_value(
197
+ resolved_args,
198
+ key: "resolved_args",
199
+ resolved: {},
200
+ parent: build_settings
201
+ )
202
+ end
203
+ end
204
+
205
+ # Constants for condition evaluation.
206
+ NO = false
207
+ YES = true
208
+
209
+ # Evaluate and check the condition.
210
+ # Conditions come in form like this:
211
+ # "$(COMPILER_INDEX_STORE_ENABLE) == YES || ( $(COMPILER_INDEX_STORE_ENABLE) == Default && $(GCC_OPTIMIZATION_LEVEL) == 0 )"
212
+ # Return true if there's no condition to evaluate.
213
+ def check_condition(option, build_settings)
214
+ condition = option["Condition"]
215
+ return true unless condition
216
+
217
+ # Need to resolve all $(VAR) references using build settings.
218
+ # At this point using complete build settings,
219
+ # which include default values for all known build settings.
220
+
221
+ # Handy that read_xcconfig action already has a helper to resolve a value.
222
+ # Just pass current build settings as parent config.
223
+ resolved_condition = Fastlane::Actions::ReadXcconfigAction.resolve_value(
224
+ condition,
225
+ key: "condition",
226
+ resolved: {},
227
+ parent: build_settings
228
+ )
229
+
230
+ # Resolved condition is now a C-like expression, which also can be evaluated as Ruby code.
231
+ # With small changes though.
232
+ # All literals in condition can be treated as strings, except for YES/NO boolean literals,
233
+ # which are replaced with `true` and `false`.
234
+ # However, xcspecs are very inconsistent when it comes to strings.
235
+ # Some values are used unquoted in the conditions, such as:
236
+ # Defatult, mh_object, bitcode.
237
+ # There's also use of '' and \"\" for empty string, the latter may cause issues.
238
+ # Then "same-as-input" that may have to be resolved - do not handle for now.
239
+ # Finally, $(variant) == profile - just leave it for now.
240
+
241
+ # Ways to fix.
242
+ # 1. Scan for all enums in xcspecs and define module vars for each enum value,
243
+ # so when evaluated, is replaced with variable.
244
+ # 2. Process resolved condition by wrapping all unwrapped entries in quotes.
245
+ # Using approach 2 for now with insane regex.
246
+
247
+ # In all cases replace \"\" with ''.
248
+ resolved_condition.gsub!('"', "'")
249
+
250
+ # Quote everything except YES and NO.
251
+ resolved_condition = (" " + resolved_condition + " ").gsub(/\s((\w|\d|-|\+|\.)+?)\s/, " '\\1' ").gsub(/'(YES|NO)'/, '\\1')
252
+
253
+ # rubocop:disable Security/Eval
254
+ eval(resolved_condition)
255
+ # rubocop:enable Security/Eval
256
+ end
257
+ end
258
+ end
259
+ end