fastlane 1.92.0 → 1.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/assets/AvailablePlugins.md.erb +24 -0
  3. data/lib/fastlane.rb +13 -6
  4. data/lib/fastlane/action_collector.rb +35 -2
  5. data/lib/fastlane/actions/actions_helper.rb +4 -0
  6. data/lib/fastlane/commands_generator.rb +61 -1
  7. data/lib/fastlane/lane.rb +1 -1
  8. data/lib/fastlane/lane_manager.rb +6 -2
  9. data/lib/fastlane/one_off.rb +7 -1
  10. data/lib/fastlane/plugins/plugin_fetcher.rb +59 -0
  11. data/lib/fastlane/plugins/plugin_generator.rb +86 -0
  12. data/lib/fastlane/plugins/plugin_generator_ui.rb +19 -0
  13. data/lib/fastlane/plugins/plugin_info.rb +47 -0
  14. data/lib/fastlane/plugins/plugin_info_collector.rb +150 -0
  15. data/lib/fastlane/plugins/plugin_manager.rb +358 -0
  16. data/lib/fastlane/plugins/plugin_search.rb +46 -0
  17. data/lib/fastlane/plugins/plugins.rb +11 -0
  18. data/lib/fastlane/plugins/template/%gem_name%.gemspec.erb +26 -0
  19. data/lib/fastlane/plugins/template/Gemfile +3 -0
  20. data/lib/fastlane/plugins/template/LICENSE.erb +21 -0
  21. data/lib/fastlane/plugins/template/README.md.erb +31 -0
  22. data/lib/fastlane/plugins/template/Rakefile +1 -0
  23. data/lib/fastlane/plugins/template/lib/fastlane/plugin/%plugin_name%.rb.erb +16 -0
  24. data/lib/fastlane/plugins/template/lib/fastlane/plugin/%plugin_name%/actions/%plugin_name%_action.rb.erb +35 -0
  25. data/lib/fastlane/plugins/template/lib/fastlane/plugin/%plugin_name%/helper/%plugin_name%_helper.rb.erb +12 -0
  26. data/lib/fastlane/plugins/template/lib/fastlane/plugin/%plugin_name%/version.rb.erb +5 -0
  27. data/lib/fastlane/plugins/template/spec/%plugin_name%_action_spec.rb.erb +9 -0
  28. data/lib/fastlane/plugins/template/spec/spec_helper.rb.erb +10 -0
  29. data/lib/fastlane/runner.rb +34 -12
  30. data/lib/fastlane/version.rb +1 -1
  31. metadata +60 -27
  32. data/lib/fastlane/actions/xcake.rb +0 -35
@@ -0,0 +1,150 @@
1
+ module Fastlane
2
+ class PluginInfoCollector
3
+ def initialize(ui = PluginGeneratorUI.new)
4
+ @ui = ui
5
+ end
6
+
7
+ def collect_info(initial_name = nil)
8
+ plugin_name = collect_plugin_name(initial_name)
9
+ author = collect_author(detect_author)
10
+ email = collect_email(detect_email)
11
+ summary = collect_summary
12
+
13
+ PluginInfo.new(plugin_name, author, email, summary)
14
+ end
15
+
16
+ #
17
+ # Plugin name
18
+ #
19
+
20
+ def collect_plugin_name(initial_name = nil)
21
+ plugin_name = initial_name
22
+ first_try = true
23
+
24
+ loop do
25
+ if !first_try || plugin_name.to_s.empty?
26
+ plugin_name = @ui.input("\nWhat would you like to be the name of your plugin?")
27
+ end
28
+ first_try = false
29
+
30
+ unless plugin_name_valid?(plugin_name)
31
+ fixed_name = fix_plugin_name(plugin_name)
32
+
33
+ if plugin_name_valid?(fixed_name)
34
+ plugin_name = fixed_name if @ui.confirm("\nWould '#{fixed_name}' be okay to use for your plugin name?")
35
+ end
36
+ end
37
+
38
+ break if plugin_name_valid?(plugin_name)
39
+
40
+ if plugin_name_taken?(plugin_name)
41
+ # Plugin name is already taken on RubyGems
42
+ @ui.message("\nPlugin name '#{plugin_name}' is already taken on RubyGems, please choose a different one.")
43
+ else
44
+ # That's a naming error
45
+ @ui.message("\nPlugin names can only contain lower case letters, numbers, and underscores")
46
+ @ui.message("and should not contain 'fastlane' or 'plugin'.")
47
+ end
48
+ end
49
+
50
+ plugin_name
51
+ end
52
+
53
+ def plugin_name_valid?(name)
54
+ # Only lower case letters, numbers and underscores allowed
55
+ /^[a-z0-9_]+$/ =~ name &&
56
+ # Does not contain the words 'fastlane' or 'plugin' since those will become
57
+ # part of the gem name
58
+ [/fastlane/, /plugin/].none? { |regex| regex =~ name } &&
59
+ # Plugin name isn't taken on RubyGems yet
60
+ !plugin_name_taken?(name)
61
+ end
62
+
63
+ # Checks if the plugin name is still free on RubyGems
64
+ def plugin_name_taken?(name)
65
+ require 'open-uri'
66
+ require 'json'
67
+ url = "https://rubygems.org/api/v1/gems/#{name}.json"
68
+ response = JSON.parse(open(url).read)
69
+ return !!response['version']
70
+ rescue
71
+ false
72
+ end
73
+
74
+ # Applies a series of replacement rules to turn the requested plugin name into one
75
+ # that is acceptable, returning that suggestion
76
+ def fix_plugin_name(name)
77
+ name = name.to_s.downcase
78
+ fixes = {
79
+ /[\- ]/ => '_', # dashes and spaces become underscores
80
+ /[^a-z0-9_]/ => '', # anything other than lower case letters, numbers and underscores is removed
81
+ /fastlane[_]?/ => '', # 'fastlane' or 'fastlane_' is removed
82
+ /plugin[_]?/ => '' # 'plugin' or 'plugin_' is removed
83
+ }
84
+ fixes.each do |regex, replacement|
85
+ name = name.gsub(regex, replacement)
86
+ end
87
+ name
88
+ end
89
+
90
+ #
91
+ # Author
92
+ #
93
+
94
+ def detect_author
95
+ git_name = Helper.backticks('git config --get user.name', print: $verbose).strip
96
+ return git_name.empty? ? nil : git_name
97
+ end
98
+
99
+ def collect_author(initial_author = nil)
100
+ return initial_author if author_valid?(initial_author)
101
+ author = nil
102
+ loop do
103
+ author = @ui.input("\nWhat is the plugin author's name?")
104
+ break if author_valid?(author)
105
+
106
+ @ui.message('An author name is required.')
107
+ end
108
+
109
+ author
110
+ end
111
+
112
+ def author_valid?(author)
113
+ !author.to_s.strip.empty?
114
+ end
115
+
116
+ #
117
+ # Email
118
+ #
119
+
120
+ def detect_email
121
+ git_email = Helper.backticks('git config --get user.email', print: $verbose).strip
122
+ return git_email.empty? ? nil : git_email
123
+ end
124
+
125
+ def collect_email(initial_email = nil)
126
+ return initial_email || @ui.input("\nWhat is the plugin author's email address?")
127
+ end
128
+
129
+ #
130
+ # Summary
131
+ #
132
+
133
+ def collect_summary
134
+ summary = nil
135
+ loop do
136
+ summary = @ui.input("\nPlease enter a short summary of this fastlane plugin:")
137
+ break if summary_valid?(summary)
138
+
139
+ @ui.message('A summary is required.')
140
+ end
141
+
142
+ summary
143
+ end
144
+
145
+ def summary_valid?(summary)
146
+ !summary.to_s.strip.empty?
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,358 @@
1
+ module Fastlane
2
+ class PluginManager
3
+ require "bundler"
4
+
5
+ PLUGINFILE_NAME = "Pluginfile".freeze
6
+ DEFAULT_GEMFILE_PATH = "Gemfile".freeze
7
+ AUTOGENERATED_LINE = "# Autogenerated by fastlane\n#\n# Ensure this file is checked in to source control!\n\n"
8
+ GEMFILE_SOURCE_LINE = "source \"https://rubygems.org\"\n"
9
+ FASTLANE_PLUGIN_PREFIX = "fastlane-plugin-"
10
+ TROUBLESHOOTING_URL = "https://github.com/fastlane/fastlane/blob/master/fastlane/docs/PluginsTroubleshooting.md"
11
+
12
+ #####################################################
13
+ # @!group Reading the files and their paths
14
+ #####################################################
15
+
16
+ def gemfile_path
17
+ # This is pretty important, since we don't know what kind of
18
+ # Gemfile the user has (e.g. Gemfile, gems.rb, or custom env variable)
19
+ Bundler::SharedHelpers.default_gemfile.to_s
20
+ rescue Bundler::GemfileNotFound
21
+ nil
22
+ end
23
+
24
+ def pluginfile_path
25
+ if FastlaneFolder.path
26
+ return File.join(FastlaneFolder.path, PLUGINFILE_NAME)
27
+ else
28
+ return nil
29
+ end
30
+ end
31
+
32
+ def gemfile_content
33
+ File.read(gemfile_path) if gemfile_path && File.exist?(gemfile_path)
34
+ end
35
+
36
+ def pluginfile_content
37
+ File.read(pluginfile_path) if pluginfile_path && File.exist?(pluginfile_path)
38
+ end
39
+
40
+ #####################################################
41
+ # @!group Helpers
42
+ #####################################################
43
+
44
+ def self.plugin_prefix
45
+ FASTLANE_PLUGIN_PREFIX
46
+ end
47
+
48
+ # Returns an array of gems that are added to the Gemfile or Pluginfile
49
+ def available_gems
50
+ return [] unless gemfile_path
51
+ dsl = Bundler::Dsl.evaluate(gemfile_path, nil, true)
52
+ return dsl.dependencies.map(&:name)
53
+ end
54
+
55
+ # Returns an array of fastlane plugins that are added to the Gemfile or Pluginfile
56
+ # The returned array contains the string with their prefixes (e.g. fastlane-plugin-xcversion)
57
+ def available_plugins
58
+ available_gems.keep_if do |current|
59
+ current.start_with?(self.class.plugin_prefix)
60
+ end
61
+ end
62
+
63
+ # Check if a plugin is added as dependency to either the
64
+ # Gemfile or the Pluginfile
65
+ def plugin_is_added_as_dependency?(plugin_name)
66
+ UI.user_error!("fastlane plugins must start with '#{self.class.plugin_prefix}' string") unless plugin_name.start_with?(self.class.plugin_prefix)
67
+ return available_plugins.include?(plugin_name)
68
+ end
69
+
70
+ #####################################################
71
+ # @!group Modifying dependencies
72
+ #####################################################
73
+
74
+ def add_dependency(plugin_name)
75
+ UI.user_error!("fastlane is not setup for this project, make sure you have a fastlane folder") unless pluginfile_path
76
+ plugin_name = self.class.plugin_prefix + plugin_name unless plugin_name.start_with?(self.class.plugin_prefix)
77
+
78
+ unless plugin_is_added_as_dependency?(plugin_name)
79
+ content = pluginfile_content || AUTOGENERATED_LINE
80
+
81
+ line_to_add = "gem '#{plugin_name}'"
82
+ line_to_add += gem_dependency_suffix(plugin_name)
83
+ UI.verbose("Adding line: #{line_to_add}")
84
+
85
+ content += "#{line_to_add}\n"
86
+ File.write(pluginfile_path, content)
87
+ UI.success("Plugin '#{plugin_name}' was added to '#{pluginfile_path}'")
88
+ end
89
+
90
+ # We do this *after* creating the Plugin file
91
+ # Since `bundle exec` would be broken if something fails on the way
92
+ ensure_plugins_attached!
93
+
94
+ true
95
+ end
96
+
97
+ # Get a suffix (e.g. `path` or `git` for the gem dependency)
98
+ def gem_dependency_suffix(plugin_name)
99
+ return "" unless self.class.fetch_gem_info_from_rubygems(plugin_name).nil?
100
+
101
+ selection_git_url = "Git URL"
102
+ selection_path = "Local Path"
103
+ selection_rubygems = "RubyGems.org ('#{plugin_name}' seems to not be available there)"
104
+ selection = UI.select(
105
+ "Seems like the plugin is not available on RubyGems, what do you want to do?",
106
+ [selection_git_url, selection_path, selection_rubygems]
107
+ )
108
+
109
+ if selection == selection_git_url
110
+ git_url = UI.input('Please enter the URL to the plugin, including the protocol (e.g. https:// or git://)')
111
+ return ", git: '#{git_url}'"
112
+ elsif selection == selection_path
113
+ path = UI.input('Please enter the relative path to the plugin you want to use. It has to point to the directory containing the .gemspec file')
114
+ return ", path: '#{path}'"
115
+ elsif selection == selection_rubygems
116
+ return ""
117
+ else
118
+ UI.user_error!("Unknown input #{selection}")
119
+ end
120
+ end
121
+
122
+ # Modify the user's Gemfile to load the plugins
123
+ def attach_plugins_to_gemfile!(path_to_gemfile)
124
+ content = gemfile_content || (AUTOGENERATED_LINE + GEMFILE_SOURCE_LINE)
125
+
126
+ # We have to make sure fastlane is also added to the Gemfile, since we now use
127
+ # bundler to run fastlane
128
+ content += "\ngem 'fastlane'\n" unless available_gems.include?('fastlane')
129
+ content += "\n#{self.class.code_to_attach}\n"
130
+
131
+ File.write(path_to_gemfile, content)
132
+ end
133
+
134
+ #####################################################
135
+ # @!group Accessing RubyGems
136
+ #####################################################
137
+
138
+ def self.fetch_gem_info_from_rubygems(gem_name)
139
+ require 'open-uri'
140
+ require 'json'
141
+ url = "https://rubygems.org/api/v1/gems/#{gem_name}.json"
142
+ begin
143
+ JSON.parse(open(url).read)
144
+ rescue
145
+ nil
146
+ end
147
+ end
148
+
149
+ #####################################################
150
+ # @!group Installing and updating dependencies
151
+ #####################################################
152
+
153
+ # Warning: This will exec out
154
+ # This is necessary since the user might be prompted for their password
155
+ def install_dependencies!
156
+ # Using puts instead of `UI` to have the same style as the `echo`
157
+ puts "Installing plugin dependencies..."
158
+ ensure_plugins_attached!
159
+ with_clean_bundler_env do
160
+ cmd = "bundle install"
161
+ cmd << " --quiet" unless $verbose
162
+ cmd << " && echo 'Successfully installed plugins'"
163
+ UI.command(cmd) if $verbose
164
+ exec(cmd)
165
+ end
166
+ end
167
+
168
+ # Warning: This will exec out
169
+ # This is necessary since the user might be prompted for their password
170
+ def update_dependencies!
171
+ puts "Updating plugin dependencies..."
172
+ ensure_plugins_attached!
173
+ with_clean_bundler_env do
174
+ cmd = "bundle update"
175
+ cmd << " --quiet" unless $verbose
176
+ cmd << " && echo 'Successfully updated plugins'"
177
+ UI.command(cmd) if $verbose
178
+ exec(cmd)
179
+ end
180
+ end
181
+
182
+ def with_clean_bundler_env
183
+ # There is an interesting problem with using exec to call back into Bundler
184
+ # The `bundle ________` command that we exec, inherits all of the Bundler
185
+ # state we'd already built up during this run. That was causing the command
186
+ # to fail, telling us to install the Gem we'd just introduced, even though
187
+ # that is exactly what we are trying to do!
188
+ #
189
+ # Bundler.with_clean_env solves this problem by resetting Bundler state before the
190
+ # exec'd call gets merged into this process.
191
+
192
+ Bundler.with_clean_env do
193
+ yield if block_given?
194
+ end
195
+ end
196
+
197
+ #####################################################
198
+ # @!group Initial setup
199
+ #####################################################
200
+
201
+ def setup
202
+ UI.important("It looks like fastlane plugins are not yet set up for this project.")
203
+
204
+ path_to_gemfile = gemfile_path || DEFAULT_GEMFILE_PATH
205
+
206
+ if gemfile_content.to_s.length > 0
207
+ UI.important("fastlane will modify your existing Gemfile at path '#{path_to_gemfile}'")
208
+ else
209
+ UI.important("fastlane will create a new Gemfile at path '#{path_to_gemfile}'")
210
+ end
211
+
212
+ UI.important("This change is neccessary for fastlane plugins to work")
213
+
214
+ unless UI.confirm("Should fastlane modify the Gemfile at path '#{path_to_gemfile}' for you?")
215
+ UI.important("Please add the following code to '#{path_to_gemfile}':")
216
+ puts ""
217
+ puts self.class.code_to_attach.magenta # we use `puts` instead of `UI` to make it easier to copy and paste
218
+ UI.user_error!("Please update '#{path_to_gemfile} and run fastlane again")
219
+ end
220
+
221
+ attach_plugins_to_gemfile!(path_to_gemfile)
222
+ UI.success("Successfully modified '#{path_to_gemfile}'")
223
+ end
224
+
225
+ # The code required to load the Plugins file
226
+ def self.code_to_attach
227
+ if FastlaneFolder.path
228
+ fastlane_folder_name = File.basename(FastlaneFolder.path)
229
+ else
230
+ fastlane_folder_name = "fastlane"
231
+ end
232
+ "plugins_path = File.join(File.dirname(__FILE__), '#{fastlane_folder_name}', '#{PluginManager::PLUGINFILE_NAME}')\n" \
233
+ "eval(File.read(plugins_path), binding) if File.exist?(plugins_path)"
234
+ end
235
+
236
+ # Makes sure, the user's Gemfile actually loads the Plugins file
237
+ def plugins_attached?
238
+ gemfile_path && gemfile_content.include?(self.class.code_to_attach)
239
+ end
240
+
241
+ def ensure_plugins_attached!
242
+ return if plugins_attached?
243
+ self.setup
244
+ end
245
+
246
+ #####################################################
247
+ # @!group Requiring the plugins
248
+ #####################################################
249
+
250
+ # Iterate over all available plugins
251
+ # which follow the naming convention
252
+ # fastlane-plugin-[plugin_name]
253
+ # This will make sure to load the action
254
+ # and all its helpers
255
+ def load_plugins
256
+ UI.verbose("Checking if there are any plugins that should be loaded...")
257
+
258
+ loaded_plugins = false
259
+ available_plugins.each do |gem_name|
260
+ UI.verbose("Loading '#{gem_name}' plugin")
261
+ begin
262
+ # BEFORE requiring the gem, we get a list of loaded actions
263
+ # This way we can check inside `store_plugin_reference` if
264
+ # any actions were overwritten
265
+ self.loaded_fastlane_actions.concat(Fastlane::Actions.constants)
266
+
267
+ require gem_name.tr("-", "/") # from "fastlane-plugin-xcversion" to "fastlane/plugin/xcversion"
268
+ store_plugin_reference(gem_name)
269
+ loaded_plugins = true
270
+ rescue => ex
271
+ UI.error("Error loading plugin '#{gem_name}': #{ex}")
272
+
273
+ # We'll still add it to the table, to make the error
274
+ # much more visible and obvious
275
+ self.plugin_references[gem_name] = {
276
+ version_number: Fastlane::ActionCollector.determine_version(gem_name),
277
+ actions: []
278
+ }
279
+ end
280
+ end
281
+
282
+ if !loaded_plugins && self.pluginfile_content.to_s.include?(PluginManager.plugin_prefix)
283
+ UI.error("It seems like you wanted to load some plugins, however they couldn't be loaded")
284
+ UI.error("Please follow the troubleshooting guide: #{TROUBLESHOOTING_URL}")
285
+ end
286
+
287
+ print_plugin_information(self.plugin_references) unless self.plugin_references.empty?
288
+ end
289
+
290
+ # Prints a table all the plugins that were loaded
291
+ def print_plugin_information(references)
292
+ rows = references.collect do |current|
293
+ if current[1][:actions].empty?
294
+ # Something is wrong with this plugin, no available actions
295
+ [current[0].red, current[1][:version_number], "No actions found".red]
296
+ else
297
+ [current[0], current[1][:version_number], current[1][:actions].join("\n")]
298
+ end
299
+ end
300
+
301
+ puts Terminal::Table.new({
302
+ rows: rows,
303
+ title: "Used plugins".green,
304
+ headings: ["Plugin", "Version", "Action"]
305
+ })
306
+ puts ""
307
+ end
308
+
309
+ #####################################################
310
+ # @!group Reference between plugins to actions
311
+ #####################################################
312
+
313
+ # Connection between plugins and their actions
314
+ # Example value of plugin_references
315
+ # => {"fastlane-plugin-ruby" => {
316
+ # version_number: "0.1.0",
317
+ # actions: [:rspec, :rubocop]
318
+ # }}
319
+ def plugin_references
320
+ @plugin_references ||= {}
321
+ end
322
+
323
+ # Contains an array of symbols for the action classes
324
+ def loaded_fastlane_actions
325
+ @fastlane_actions ||= []
326
+ end
327
+
328
+ def store_plugin_reference(gem_name)
329
+ module_name = gem_name.gsub(PluginManager.plugin_prefix, '').fastlane_class
330
+ # We store a collection of the imported plugins
331
+ # This way we can tell which action came from what plugin
332
+ # (a plugin may contain any number of actions)
333
+ version_number = Fastlane::ActionCollector.determine_version(gem_name)
334
+ references = Fastlane.const_get(module_name).all_classes.collect do |path|
335
+ next unless File.dirname(path).end_with?("/actions") # we only want to match actions
336
+
337
+ File.basename(path).gsub("_action", "").gsub(".rb", "").to_sym # the _action is optional
338
+ end
339
+ references.compact!
340
+
341
+ # Check if this overwrites a built-in action and
342
+ # show a warning if that's the case
343
+ references.each do |current_ref|
344
+ # current_ref is a symbol, e.g. :emoji_fetcher
345
+ class_name = (current_ref.to_s.fastlane_class + 'Action').to_sym
346
+
347
+ if self.loaded_fastlane_actions.include?(class_name)
348
+ UI.important("Plugin '#{module_name}' overwrites already loaded action '#{current_ref}'")
349
+ end
350
+ end
351
+
352
+ self.plugin_references[gem_name] = {
353
+ version_number: version_number,
354
+ actions: references
355
+ }
356
+ end
357
+ end
358
+ end