Class: Fastlane::PluginManager

Inherits:
Object
  • Object
show all
Defined in:
fastlane/lib/fastlane/plugins/plugin_manager.rb

Constant Summary collapse

PLUGINFILE_NAME =
"Pluginfile".freeze
DEFAULT_GEMFILE_PATH =
"Gemfile".freeze
AUTOGENERATED_LINE =
"# Autogenerated by fastlane\n#\n# Ensure this file is checked in to source control!\n\n"
GEMFILE_SOURCE_LINE =
"source \"https://rubygems.org\"\n"
FASTLANE_PLUGIN_PREFIX =
"fastlane-plugin-"
TROUBLESHOOTING_URL =
"https://docs.fastlane.tools/plugins/plugins-troubleshooting/"

Reading the files and their paths collapse

Helpers collapse

Modifying dependencies collapse

Accessing RubyGems collapse

Installing and updating dependencies collapse

Initial setup collapse

Requiring the plugins collapse

Reference between plugins to actions collapse

Class Method Details

.code_to_attachObject

The code required to load the Plugins file


252
253
254
255
256
257
258
259
260
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 252

def self.code_to_attach
  if FastlaneCore::FastlaneFolder.path
    fastlane_folder_name = File.basename(FastlaneCore::FastlaneFolder.path)
  else
    fastlane_folder_name = "fastlane"
  end
  "plugins_path = File.join(File.dirname(__FILE__), '#{fastlane_folder_name}', '#{PluginManager::PLUGINFILE_NAME}')\n" \
  "eval_gemfile(plugins_path) if File.exist?(plugins_path)"
end

.fetch_gem_info_from_rubygems(gem_name) ⇒ Object


157
158
159
160
161
162
163
164
165
166
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 157

def self.fetch_gem_info_from_rubygems(gem_name)
  require 'open-uri'
  require 'json'
  url = "https://rubygems.org/api/v1/gems/#{gem_name}.json"
  begin
    JSON.parse(URI.open(url).read)
  rescue
    nil
  end
end

.plugin_prefixObject


46
47
48
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 46

def self.plugin_prefix
  FASTLANE_PLUGIN_PREFIX
end

.to_gem_name(plugin_name) ⇒ Object


50
51
52
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 50

def self.to_gem_name(plugin_name)
  plugin_name.start_with?(plugin_prefix) ? plugin_name : (plugin_prefix + plugin_name)
end

Instance Method Details

#add_dependency(plugin_name) ⇒ Object


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
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 80

def add_dependency(plugin_name)
  UI.user_error!("fastlane is not setup for this project, make sure you have a fastlane folder") unless pluginfile_path
  plugin_name = self.class.plugin_prefix + plugin_name unless plugin_name.start_with?(self.class.plugin_prefix)

  if plugin_name.gsub(self.class.plugin_prefix, '').include?("-")
    # e.g. "fastlane-plugin-ya_tu-sabes" (which is invalid)
    UI.user_error!("Plugin name must not contain a '-', did you mean '_'?")
  end

  unless plugin_is_added_as_dependency?(plugin_name)
    content = pluginfile_content || AUTOGENERATED_LINE

    unless content.end_with?("\n")
      content += "\n"
    end

    line_to_add = "gem '#{plugin_name}'"
    line_to_add += gem_dependency_suffix(plugin_name)
    UI.verbose("Adding line: #{line_to_add}")

    content += "#{line_to_add}\n"
    File.write(pluginfile_path, content)
    UI.success("Plugin '#{plugin_name}' was added to '#{pluginfile_path}'")
  end

  # We do this *after* creating the Plugin file
  # Since `bundle exec` would be broken if something fails on the way
  ensure_plugins_attached!

  true
end

#attach_plugins_to_gemfile!(path_to_gemfile) ⇒ Object

Modify the user's Gemfile to load the plugins


142
143
144
145
146
147
148
149
150
151
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 142

def attach_plugins_to_gemfile!(path_to_gemfile)
  content = gemfile_content || (AUTOGENERATED_LINE + GEMFILE_SOURCE_LINE)

  # We have to make sure fastlane is also added to the Gemfile, since we now use
  # bundler to run fastlane
  content += "\ngem 'fastlane'\n" unless available_gems.include?('fastlane')
  content += "\n#{self.class.code_to_attach}\n"

  File.write(path_to_gemfile, content)
end

#available_gemsObject

Returns an array of gems that are added to the Gemfile or Pluginfile


55
56
57
58
59
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 55

def available_gems
  return [] unless gemfile_path
  dsl = Bundler::Dsl.evaluate(gemfile_path, nil, true)
  return dsl.dependencies.map(&:name)
end

#available_pluginsObject

Returns an array of fastlane plugins that are added to the Gemfile or Pluginfile The returned array contains the string with their prefixes (e.g. fastlane-plugin-xcversion)


63
64
65
66
67
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 63

def available_plugins
  available_gems.keep_if do |current|
    current.start_with?(self.class.plugin_prefix)
  end
end

#ensure_plugins_attached!Object


267
268
269
270
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 267

def ensure_plugins_attached!
  return if plugins_attached?
  self.setup
end

#gem_dependency_suffix(plugin_name) ⇒ Object

Get a suffix (e.g. `path` or `git` for the gem dependency)


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
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 113

def gem_dependency_suffix(plugin_name)
  return "" unless self.class.fetch_gem_info_from_rubygems(plugin_name).nil?

  selection_git_url = "Git URL"
  selection_path = "Local Path"
  selection_rubygems = "RubyGems.org ('#{plugin_name}' seems to not be available there)"
  selection_gem_server = "Other Gem Server"
  selection = UI.select(
    "Seems like the plugin is not available on RubyGems, what do you want to do?",
    [selection_git_url, selection_path, selection_rubygems, selection_gem_server]
  )

  if selection == selection_git_url
    git_url = UI.input('Please enter the URL to the plugin, including the protocol (e.g. https:// or git://)')
    return ", git: '#{git_url}'"
  elsif selection == selection_path
    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')
    return ", path: '#{path}'"
  elsif selection == selection_rubygems
    return ""
  elsif selection == selection_gem_server
    source_url = UI.input('Please enter the gem source URL which hosts the plugin you want to use, including the protocol (e.g. https:// or git://)')
    return ", source: '#{source_url}'"
  else
    UI.user_error!("Unknown input #{selection}")
  end
end

#gemfile_contentObject


34
35
36
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 34

def gemfile_content
  File.read(gemfile_path) if gemfile_path && File.exist?(gemfile_path)
end

#gemfile_pathObject


18
19
20
21
22
23
24
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 18

def gemfile_path
  # This is pretty important, since we don't know what kind of
  # Gemfile the user has (e.g. Gemfile, gems.rb, or custom env variable)
  Bundler::SharedHelpers.default_gemfile.to_s
rescue Bundler::GemfileNotFound
  nil
end

#install_dependencies!Object

Warning: This will exec out This is necessary since the user might be prompted for their password


174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 174

def install_dependencies!
  # Using puts instead of `UI` to have the same style as the `echo`
  puts("Installing plugin dependencies...")
  ensure_plugins_attached!
  with_clean_bundler_env do
    cmd = "bundle install"
    cmd << " --quiet" unless FastlaneCore::Globals.verbose?
    cmd << " && bundle exec fastlane generate_swift" if FastlaneCore::FastlaneFolder.swift?
    cmd << " && echo 'Successfully installed plugins'"
    UI.command(cmd) if FastlaneCore::Globals.verbose?
    exec(cmd)
  end
end

#load_plugins(print_table: true) ⇒ Object

Iterate over all available plugins which follow the naming convention

fastlane-plugin-[plugin_name]

This will make sure to load the action and all its helpers


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
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 281

def load_plugins(print_table: true)
  UI.verbose("Checking if there are any plugins that should be loaded...")

  loaded_plugins = false
  available_plugins.each do |gem_name|
    UI.verbose("Loading '#{gem_name}' plugin")
    begin
      # BEFORE requiring the gem, we get a list of loaded actions
      # This way we can check inside `store_plugin_reference` if
      # any actions were overwritten
      self.loaded_fastlane_actions.concat(Fastlane::Actions.constants)

      FastlaneRequire.install_gem_if_needed(gem_name: gem_name, require_gem: true)

      store_plugin_reference(gem_name)
      loaded_plugins = true
    rescue StandardError, ScriptError => ex # some errors, like ScriptError are not caught unless explicitly
      UI.error("Error loading plugin '#{gem_name}': #{ex}")

      # We'll still add it to the table, to make the error
      # much more visible and obvious
      self.plugin_references[gem_name] = {
        version_number: Fastlane::ActionCollector.determine_version(gem_name),
        actions: []
      }
    end
  end

  if !loaded_plugins && self.pluginfile_content.to_s.include?(PluginManager.plugin_prefix)
    UI.error("It seems like you wanted to load some plugins, however they couldn't be loaded")
    UI.error("Please follow the troubleshooting guide: #{TROUBLESHOOTING_URL}")
  end

  skip_print_plugin_info = self.plugin_references.empty? || CLIToolsDistributor.running_version_command? || !print_table

  # We want to avoid printing output other than the version number if we are running `fastlane -v`
  print_plugin_information(self.plugin_references) unless skip_print_plugin_info
end

#loaded_fastlane_actionsObject

Contains an array of symbols for the action classes


365
366
367
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 365

def loaded_fastlane_actions
  @fastlane_actions ||= []
end

#plugin_is_added_as_dependency?(plugin_name) ⇒ Boolean

Check if a plugin is added as dependency to either the Gemfile or the Pluginfile

Returns:


71
72
73
74
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 71

def plugin_is_added_as_dependency?(plugin_name)
  UI.user_error!("fastlane plugins must start with '#{self.class.plugin_prefix}' string") unless plugin_name.start_with?(self.class.plugin_prefix)
  return available_plugins.include?(plugin_name)
end

#plugin_referencesObject

Connection between plugins and their actions Example value of plugin_references

> => {

     version_number: "0.1.0",
     actions: [:rspec, :rubocop]
}

360
361
362
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 360

def plugin_references
  @plugin_references ||= {}
end

#pluginfile_contentObject


38
39
40
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 38

def pluginfile_content
  File.read(pluginfile_path) if pluginfile_path && File.exist?(pluginfile_path)
end

#pluginfile_pathObject


26
27
28
29
30
31
32
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 26

def pluginfile_path
  if FastlaneCore::FastlaneFolder.path
    return File.join(FastlaneCore::FastlaneFolder.path, PLUGINFILE_NAME)
  else
    return nil
  end
end

#plugins_attached?Boolean

Makes sure, the user's Gemfile actually loads the Plugins file

Returns:


263
264
265
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 263

def plugins_attached?
  gemfile_path && gemfile_content.include?(PluginManager::PLUGINFILE_NAME)
end

Prints a table all the plugins that were loaded


321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 321

def print_plugin_information(references)
  no_action_found = false

  rows = references.collect do |current|
    if current[1][:actions].empty?
      # Something is wrong with this plugin, no available actions
      no_action_found = true
      [current[0].red, current[1][:version_number], "No actions found".red]
    else
      [current[0], current[1][:version_number], current[1][:actions].join("\n")]
    end
  end

  require 'terminal-table'
  puts(Terminal::Table.new({
    rows: FastlaneCore::PrintTable.transform_output(rows),
    title: "Used plugins".green,
    headings: ["Plugin", "Version", "Action"]
  }))
  puts("")

  if no_action_found
    puts("[!] No actions were found while loading one or more plugins".red)
    puts("    Please use `bundle exec fastlane` with plugins".red)
    puts("    More info - https://docs.fastlane.tools/plugins/using-plugins/#run-with-plugins".red)
    puts("")
  end
end

#setupObject


227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 227

def setup
  UI.important("It looks like fastlane plugins are not yet set up for this project.")

  path_to_gemfile = gemfile_path || DEFAULT_GEMFILE_PATH

  if gemfile_content.to_s.length > 0
    UI.important("fastlane will modify your existing Gemfile at path '#{path_to_gemfile}'")
  else
    UI.important("fastlane will create a new Gemfile at path '#{path_to_gemfile}'")
  end

  UI.important("This change is necessary for fastlane plugins to work")

  unless UI.confirm("Should fastlane modify the Gemfile at path '#{path_to_gemfile}' for you?")
    UI.important("Please add the following code to '#{path_to_gemfile}':")
    puts("")
    puts(self.class.code_to_attach.magenta) # we use `puts` instead of `UI` to make it easier to copy and paste
    UI.user_error!("Please update '#{path_to_gemfile} and run fastlane again")
  end

  attach_plugins_to_gemfile!(path_to_gemfile)
  UI.success("Successfully modified '#{path_to_gemfile}'")
end

#store_plugin_reference(gem_name) ⇒ Object


369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 369

def store_plugin_reference(gem_name)
  module_name = gem_name.gsub(PluginManager.plugin_prefix, '').fastlane_class
  # We store a collection of the imported plugins
  # This way we can tell which action came from what plugin
  # (a plugin may contain any number of actions)
  version_number = Fastlane::ActionCollector.determine_version(gem_name)
  references = Fastlane.const_get(module_name).all_classes.collect do |path|
    next unless File.dirname(path).include?("/actions") # we only want to match actions

    File.basename(path).gsub(".rb", "").gsub(/_action$/, '').to_sym # the _action is optional
  end
  references.compact!

  # Check if this overwrites a built-in action and
  # show a warning if that's the case
  references.each do |current_ref|
    # current_ref is a symbol, e.g. :emoji_fetcher
    class_name = (current_ref.to_s.fastlane_class + 'Action').to_sym

    if self.loaded_fastlane_actions.include?(class_name)
      UI.important("Plugin '#{module_name}' overwrites already loaded action '#{current_ref}'")
    end
  end

  self.plugin_references[gem_name] = {
    version_number: version_number,
    actions: references
  }
end

#update_dependencies!Object

Warning: This will exec out This is necessary since the user might be prompted for their password


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 190

def update_dependencies!
  puts("Updating plugin dependencies...")
  ensure_plugins_attached!
  plugins = available_plugins
  if plugins.empty?
    UI.user_error!("No plugins are installed")
  end
  with_clean_bundler_env do
    cmd = "bundle update"
    cmd << " #{plugins.join(' ')}"
    cmd << " --quiet" unless FastlaneCore::Globals.verbose?
    cmd << " && bundle exec fastlane generate_swift" if FastlaneCore::FastlaneFolder.swift?
    cmd << " && echo 'Successfully updated plugins'"
    UI.command(cmd) if FastlaneCore::Globals.verbose?
    exec(cmd)
  end
end

#with_clean_bundler_envObject


208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'fastlane/lib/fastlane/plugins/plugin_manager.rb', line 208

def with_clean_bundler_env
  # There is an interesting problem with using exec to call back into Bundler
  # The `bundle ________` command that we exec, inherits all of the Bundler
  # state we'd already built up during this run. That was causing the command
  # to fail, telling us to install the Gem we'd just introduced, even though
  # that is exactly what we are trying to do!
  #
  # Bundler.with_clean_env solves this problem by resetting Bundler state before the
  # exec'd call gets merged into this process.

  Bundler.with_original_env do
    yield if block_given?
  end
end