-
Notifications
You must be signed in to change notification settings - Fork 5.6k
/
configuration.rb
338 lines (279 loc) 路 12.5 KB
/
configuration.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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
337
338
require_relative '../helper'
require_relative '../globals'
require_relative 'config_item'
require_relative 'commander_generator'
require_relative 'configuration_file'
module FastlaneCore
class Configuration
attr_accessor :available_options
attr_accessor :values
# @return [Array] An array of symbols which are all available keys
attr_reader :all_keys
# @return [String] The name of the configuration file (not the path). Optional!
attr_accessor :config_file_name
# @return [Hash] Options that were set from a config file using load_configuration_file. Optional!
attr_accessor :config_file_options
def self.create(available_options, values)
UI.user_error!("values parameter must be a hash") unless values.kind_of?(Hash)
v = values.dup
v.each do |key, val|
v[key] = val.dup if val.kind_of?(String) # this is necessary when fetching a value from an environment variable
end
if v.kind_of?(Hash) && available_options.kind_of?(Array) # we only want to deal with the new configuration system
# Now see if --verbose would be a valid input
# If not, it might be because it's an action and not a tool
unless available_options.find { |a| a.kind_of?(ConfigItem) && a.key == :verbose }
v.delete(:verbose) # as this is being processed by commander
end
end
Configuration.new(available_options, v)
end
#####################################################
# @!group Setting up the configuration
#####################################################
# collect sensitive strings
def self.sensitive_strings
@sensitive_strings ||= []
end
def initialize(available_options, values)
self.available_options = available_options || []
self.values = values || {}
self.config_file_options = {}
# used for pushing and popping values to provide nesting configuration contexts
@values_stack = []
# if we are in captured output mode - keep a array of sensitive option values
# those will be later - replaced by ####
if FastlaneCore::Globals.capture_output?
available_options.each do |element|
next unless element.sensitive
self.class.sensitive_strings << values[element.key]
end
end
verify_input_types
verify_value_exists
verify_no_duplicates
verify_conflicts
verify_default_value_matches_verify_block
end
def verify_input_types
UI.user_error!("available_options parameter must be an array of ConfigItems but is #{@available_options.class}") unless @available_options.kind_of?(Array)
@available_options.each do |item|
UI.user_error!("available_options parameter must be an array of ConfigItems. Found #{item.class}.") unless item.kind_of?(ConfigItem)
end
UI.user_error!("values parameter must be a hash") unless @values.kind_of?(Hash)
end
def verify_value_exists
# Make sure the given value keys exist
@values.each do |key, value|
next if key == :trace # special treatment
option = self.verify_options_key!(key)
@values[key] = option.auto_convert_value(value)
UI.deprecated("Using deprecated option: '--#{key}' (#{option.deprecated})") if option.deprecated
option.verify!(@values[key]) # Call the verify block for it too
end
end
def verify_no_duplicates
# Make sure a key was not used multiple times
@available_options.each do |current|
count = @available_options.count { |option| option.key == current.key }
UI.user_error!("Multiple entries for configuration key '#{current.key}' found!") if count > 1
unless current.short_option.to_s.empty?
count = @available_options.count { |option| option.short_option == current.short_option }
UI.user_error!("Multiple entries for short_option '#{current.short_option}' found!") if count > 1
end
end
end
def verify_conflicts
option_keys = @values.keys
option_keys.each do |current|
index = @available_options.find_index { |item| item.key == current }
current = @available_options[index]
# ignore conflicts because option value is nil
next if @values[current.key].nil?
next if current.conflicting_options.nil?
conflicts = current.conflicting_options & option_keys
next if conflicts.nil?
conflicts.each do |conflicting_option_key|
index = @available_options.find_index { |item| item.key == conflicting_option_key }
conflicting_option = @available_options[index]
# ignore conflicts because value of conflict option is nil
next if @values[conflicting_option.key].nil?
if current.conflict_block
begin
current.conflict_block.call(conflicting_option)
rescue => ex
UI.error("Error resolving conflict between options: '#{current.key}' and '#{conflicting_option.key}'")
raise ex
end
else
UI.user_error!("Unresolved conflict between options: '#{current.key}' and '#{conflicting_option.key}'")
end
end
end
end
# Verifies the default value is also valid
def verify_default_value_matches_verify_block
@available_options.each do |item|
next unless item.verify_block && item.default_value
begin
unless @values[item.key] # this is important to not verify if there already is a value there
item.verify_block.call(item.default_value)
end
rescue => ex
UI.error(ex)
UI.user_error!("Invalid default value for #{item.key}, doesn't match verify_block")
end
end
end
# This method takes care of parsing and using the configuration file as values
# Call this once you know where the config file might be located
# Take a look at how `gym` uses this method
#
# @param config_file_name [String] The name of the configuration file to use (optional)
# @param block_for_missing [Block] A ruby block that is called when there is an unknown method
# in the configuration file
def load_configuration_file(config_file_name = nil, block_for_missing = nil, skip_printing_values = false)
return unless config_file_name
self.config_file_name = config_file_name
path = FastlaneCore::Configuration.find_configuration_file_path(config_file_name: config_file_name)
return if path.nil?
begin
configuration_file = ConfigurationFile.new(self, path, block_for_missing, skip_printing_values)
options = configuration_file.options
rescue FastlaneCore::ConfigurationFile::ExceptionWhileParsingError => e
options = e.recovered_options
wrapped_exception = e.wrapped_exception
end
# Make sure all the values set in the config file pass verification
options.each do |key, val|
option = self.verify_options_key!(key)
option.verify!(val)
end
# Merge the new options into the old ones, keeping all previously set keys
self.config_file_options = options.merge(self.config_file_options)
verify_conflicts # important, since user can set conflicting options in configuration file
# Now that everything is verified, re-raise an exception that was raised in the config file
raise wrapped_exception unless wrapped_exception.nil?
configuration_file
end
def self.find_configuration_file_path(config_file_name: nil)
paths = []
paths += Dir["./fastlane/#{config_file_name}"]
paths += Dir["./.fastlane/#{config_file_name}"]
paths += Dir["./#{config_file_name}"]
paths += Dir["./fastlane_core/spec/fixtures/#{config_file_name}"] if Helper.test?
return nil if paths.count == 0
return paths.first
end
#####################################################
# @!group Actually using the class
#####################################################
# Returns the value for a certain key. fastlane_core tries to fetch the value from different sources
# if 'ask' is true and the value is not present, the user will be prompted to provide a value if optional
# if 'force_ask' is true, the option is not required to be optional to ask
# rubocop:disable Metrics/PerceivedComplexity
def fetch(key, ask: true, force_ask: false)
UI.crash!("Key '#{key}' must be a symbol. Example :#{key}") unless key.kind_of?(Symbol)
option = verify_options_key!(key)
# Same order as https://docs.fastlane.tools/advanced/#priorities-of-parameters-and-options
value = if @values.key?(key) && !@values[key].nil?
@values[key]
elsif (env_value = option.fetch_env_value)
env_value
elsif self.config_file_options.key?(key)
self.config_file_options[key]
else
option.default_value
end
value = option.auto_convert_value(value)
value = nil if value.nil? && !option.string? # by default boolean flags are false
return value unless value.nil? && (!option.optional || force_ask) && ask
# fallback to asking
if Helper.test? || !UI.interactive?
# Since we don't want to be asked on tests, we'll just call the verify block with no value
# to raise the exception that is shown when the user passes an invalid value
set(key, '')
# If this didn't raise an exception, just raise a default one
UI.user_error!("No value found for '#{key}'")
end
while value.nil?
UI.important("To not be asked about this value, you can specify it using '#{option.key}'") if ENV["FASTLANE_ONBOARDING_IN_PROCESS"].to_s.length == 0
value = option.sensitive ? UI.password("#{option.description}: ") : UI.input("#{option.description}: ")
# ConfigItem allows to specify a type for the item but UI.password and
# UI.input return String values. Try to convert the String input to
# the option's type before passing it along.
value = option.auto_convert_value(value)
# Also store this value to use it from now on
begin
set(key, value)
rescue => ex
UI.error(ex)
value = nil
end
end
# It's very, very important to use the self[:my_key] notation
# as this will make sure to use the `fetch` method
# that is responsible for auto converting the values into the right
# data type
# Found out via https://github.com/fastlane/fastlane/issues/11243
return self[key]
end
# rubocop:enable Metrics/PerceivedComplexity
# Overwrites or sets a new value for a given key
# @param key [Symbol] Must be a symbol
def set(key, value)
UI.crash!("Key '#{key}' must be a symbol. Example :#{key}.") unless key.kind_of?(Symbol)
option = option_for_key(key)
unless option
UI.user_error!("Could not find option '#{key}' in the list of available options: #{@available_options.collect(&:key).join(', ')}")
end
option.verify!(value)
@values[key] = value
true
end
# see fetch
def values(ask: true)
# As the user accesses all values, we need to iterate through them to receive all the values
@available_options.each do |option|
@values[option.key] = fetch(option.key, ask: ask) unless @values[option.key]
end
@values
end
# Direct access to the values, without iterating through all items
def _values
@values
end
# Clears away any current configuration values by pushing them onto a stack.
# Values set after calling push_values! will be merged with the previous
# values after calling pop_values!
#
# see: pop_values!
def push_values!
@values_stack.push(@values)
@values = {}
end
# Restores a previous set of configuration values by merging any current
# values on top of them
#
# see: push_values!
def pop_values!
return if @values_stack.empty?
@values = @values_stack.pop.merge(@values)
end
def all_keys
@available_options.collect(&:key)
end
# Returns the config_item object for a given key
def option_for_key(key)
@available_options.find { |o| o.key == key }
end
# Aliases `[key]` to `fetch(key)` because Ruby can do it.
alias [] fetch
alias []= set
def verify_options_key!(key)
option = option_for_key(key)
UI.user_error!("Could not find option '#{key}' in the list of available options: #{@available_options.collect(&:key).join(', ')}") unless option
option
end
end
end