Class: Fastlane::PluginManager

Inherits:
Object
  • Object
show all
Defined in:
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://github.com/fastlane/fastlane/blob/master/fastlane/docs/PluginsTroubleshooting.md"

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



235
236
237
238
239
240
241
242
243
# File 'lib/fastlane/plugins/plugin_manager.rb', line 235

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

.fetch_gem_info_from_rubygems(gem_name) ⇒ Object



147
148
149
150
151
152
153
154
155
156
# File 'lib/fastlane/plugins/plugin_manager.rb', line 147

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(open(url).read)
  rescue
    nil
  end
end

.plugin_prefixObject



44
45
46
# File 'lib/fastlane/plugins/plugin_manager.rb', line 44

def self.plugin_prefix
  FASTLANE_PLUGIN_PREFIX
end

.to_gem_name(plugin_name) ⇒ Object



48
49
50
# File 'lib/fastlane/plugins/plugin_manager.rb', line 48

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



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

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

    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



132
133
134
135
136
137
138
139
140
141
# File 'lib/fastlane/plugins/plugin_manager.rb', line 132

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



53
54
55
56
57
# File 'lib/fastlane/plugins/plugin_manager.rb', line 53

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)



61
62
63
64
65
# File 'lib/fastlane/plugins/plugin_manager.rb', line 61

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

#ensure_plugins_attached!Object



250
251
252
253
# File 'lib/fastlane/plugins/plugin_manager.rb', line 250

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)



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/fastlane/plugins/plugin_manager.rb', line 107

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 = UI.select(
    "Seems like the plugin is not available on RubyGems, what do you want to do?",
    [selection_git_url, selection_path, selection_rubygems]
  )

  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 ""
  else
    UI.user_error!("Unknown input #{selection}")
  end
end

#gemfile_contentObject



32
33
34
# File 'lib/fastlane/plugins/plugin_manager.rb', line 32

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

#gemfile_pathObject



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

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



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/fastlane/plugins/plugin_manager.rb', line 164

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 $verbose
    cmd << " && echo 'Successfully installed plugins'"
    UI.command(cmd) if $verbose
    exec(cmd)
  end
end

#load_pluginsObject

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



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

def load_plugins
  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)

      require gem_name.tr("-", "/") # from "fastlane-plugin-xcversion" to "fastlane/plugin/xcversion"
      store_plugin_reference(gem_name)
      loaded_plugins = true
    rescue => ex
      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?

  # 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



336
337
338
# File 'lib/fastlane/plugins/plugin_manager.rb', line 336

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:

  • (Boolean)


69
70
71
72
# File 'lib/fastlane/plugins/plugin_manager.rb', line 69

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]
}


331
332
333
# File 'lib/fastlane/plugins/plugin_manager.rb', line 331

def plugin_references
  @plugin_references ||= {}
end

#pluginfile_contentObject



36
37
38
# File 'lib/fastlane/plugins/plugin_manager.rb', line 36

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

#pluginfile_pathObject



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

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

#plugins_attached?Boolean

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

Returns:

  • (Boolean)


246
247
248
# File 'lib/fastlane/plugins/plugin_manager.rb', line 246

def plugins_attached?
  gemfile_path && gemfile_content.include?(self.class.code_to_attach)
end

Prints a table all the plugins that were loaded



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/fastlane/plugins/plugin_manager.rb', line 303

def print_plugin_information(references)
  rows = references.collect do |current|
    if current[1][:actions].empty?
      # Something is wrong with this plugin, no available actions
      [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

  puts Terminal::Table.new({
    rows: rows,
    title: "Used plugins".green,
    headings: ["Plugin", "Version", "Action"]
  })
  puts ""
end

#setupObject



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/fastlane/plugins/plugin_manager.rb', line 210

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 neccessary 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



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/fastlane/plugins/plugin_manager.rb', line 340

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).end_with?("/actions") # we only want to match actions

    File.basename(path).gsub("_action", "").gsub(".rb", "").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



179
180
181
182
183
184
185
186
187
188
189
# File 'lib/fastlane/plugins/plugin_manager.rb', line 179

def update_dependencies!
  puts "Updating plugin dependencies..."
  ensure_plugins_attached!
  with_clean_bundler_env do
    cmd = "bundle update"
    cmd << " --quiet" unless $verbose
    cmd << " && echo 'Successfully updated plugins'"
    UI.command(cmd) if $verbose
    exec(cmd)
  end
end

#with_clean_bundler_envObject



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

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_clean_env do
    yield if block_given?
  end
end