class Fastlane::PluginManager

Constants

AUTOGENERATED_LINE
DEFAULT_GEMFILE_PATH
FASTLANE_PLUGIN_PREFIX
GEMFILE_SOURCE_LINE
PLUGINFILE_NAME
TROUBLESHOOTING_URL

Public Class Methods

code_to_attach() click to toggle source

The code required to load the Plugins file

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 251
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) click to toggle source

@!group Accessing RubyGems

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 157
def self.fetch_gem_info_from_rubygems(gem_name)
  require 'json'
  url = "https://rubygems.org/api/v1/gems/#{gem_name}.json"
  begin
    JSON.parse(FastlaneCore::Helper.open_uri(url).read)
  rescue
    nil
  end
end
plugin_prefix() click to toggle source

@!group Helpers

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 46
def self.plugin_prefix
  FASTLANE_PLUGIN_PREFIX
end
to_gem_name(plugin_name) click to toggle source
# 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

Public Instance Methods

add_dependency(plugin_name) click to toggle source

@!group Modifying dependencies

# 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) click to toggle source

Modify the user's Gemfile to load the plugins

# 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_gems() click to toggle source

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

# 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_plugins() click to toggle source

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)

# 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!() click to toggle source
# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 266
def ensure_plugins_attached!
  return if plugins_attached?
  self.setup
end
gem_dependency_suffix(plugin_name) click to toggle source

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

# 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_content() click to toggle source
# 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_path() click to toggle source

@!group Reading the files and their paths

# 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!() click to toggle source

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

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 173
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) click to toggle source

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

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 280
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_actions() click to toggle source

Contains an array of symbols for the action classes

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 364
def loaded_fastlane_actions
  @fastlane_actions ||= []
end
plugin_is_added_as_dependency?(plugin_name) click to toggle source

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

# 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_references() click to toggle source

Connection between plugins and their actions Example value of plugin_references

> {“fastlane-plugin-ruby” => {

     version_number: "0.1.0",
     actions: [:rspec, :rubocop]
}}
# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 359
def plugin_references
  @plugin_references ||= {}
end
pluginfile_content() click to toggle source
# 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_path() click to toggle source
# 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?() click to toggle source

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

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 262
def plugins_attached?
  gemfile_path && gemfile_content.include?(PluginManager::PLUGINFILE_NAME)
end
print_plugin_information(references) click to toggle source

Prints a table all the plugins that were loaded

setup() click to toggle source

@!group Initial setup

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 226
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) click to toggle source
# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 368
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!() click to toggle source

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

# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 189
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_env() { || ... } click to toggle source
# File fastlane/lib/fastlane/plugins/plugin_manager.rb, line 207
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