summaryrefslogtreecommitdiff
path: root/lib/bundler/plugin.rb
blob: fb577ad6bc151a52d3b658a6e41537766af78f1f (plain)
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
# frozen_string_literal: true

module Bundler
  class Plugin
    autoload :Api,        "bundler/plugin/api"
    autoload :Dsl,        "bundler/plugin/dsl"
    autoload :Index,      "bundler/plugin/index"
    autoload :Installer,  "bundler/plugin/installer"
    autoload :SourceList, "bundler/plugin/source_list"

    PLUGIN_FILE_NAME = "plugin.rb".freeze

    @commands = {}

    class << self
      # Installs a new plugin by the given name
      #
      # @param [String] name the name of plugin to be installed
      # @param [Hash] options various parameters as described in description
      # @option options [String] :source rubygems source to fetch the plugin gem from
      # @option options [String] :version (optional) the version of the plugin to install
      def install(name, options)
        plugin_path = Pathname.new Installer.new.install(name, options)

        validate_plugin! plugin_path

        register_plugin name, plugin_path

        Bundler.ui.info "Installed plugin #{name}"
      rescue StandardError => e
        Bundler.rm_rf(plugin_path) if plugin_path
        Bundler.ui.error "Failed to install plugin #{name}: #{e.message}\n  #{e.backtrace.join("\n  ")}"
      end

      # Evaluates the Gemfile with a limited DSL and installs the plugins
      # specified by plugin method
      #
      # @param [Pathname] gemfile path
      def eval_gemfile(gemfile)
        definition = Dsl.evaluate(gemfile, nil, {})
        return unless definition.dependencies.any?

        plugins = Installer.new.install_definition(definition)

        plugins.each do |name, path|
          path = Pathname.new path
          validate_plugin! path
          register_plugin name, path
          Bundler.ui.info "Installed plugin #{name}"
        end
      end

      # The index object used to store the details about the plugin
      def index
        @index ||= Index.new
      end

      # The directory root to all plugin related data
      def root
        @root ||= Bundler.user_bundle_path.join("plugin")
      end

      # The cache directory for plugin stuffs
      def cache
        @cache ||= root.join("cache")
      end

      # To be called via the Api to register to handle a command
      def add_command(command, cls)
        @commands[command] = cls
      end

      # Checks if any plugins handles the command
      def command?(command)
        !index.command_plugin(command).nil?
      end

      # To be called from Cli class to pass the command and argument to
      # approriate plugin class
      def exec_command(command, args)
        raise "Command #{command} not found" unless command? command

        load_plugin index.command_plugin(command) unless @commands.key? command

        @commands[command].new.exec(command, args)
      end

    private

      # Checks if the gem is good to be a plugin
      #
      # At present it only checks whether it contains plugin.rb file
      #
      # @param [Pathname] plugin_path the path plugin is installed at
      #
      # @raise [Error] if plugin.rb file is not found
      def validate_plugin!(plugin_path)
        plugin_file = plugin_path.join(PLUGIN_FILE_NAME)
        raise "#{PLUGIN_FILE_NAME} was not found in the plugin!" unless plugin_file.file?
      end

      # Runs the plugin.rb file in an isolated namespace, records the plugin
      # actions it registers for and then passes the data to index to be stored.
      #
      # @param [String] name the name of the plugin
      # @param [Pathname] path the path where the plugin is installed at
      def register_plugin(name, path)
        commands = @commands

        @commands = {}

        load path.join(PLUGIN_FILE_NAME), true

        index.register_plugin name, path.to_s, @commands.keys
      ensure
        @commands = commands
      end

      def load_plugin(name)
        # Need to ensure before this that plugin root where the rest of gems
        # are installed to be on load path to support plugin deps. Currently not
        # done to avoid conflicts
        path = index.plugin_path(name)

        load path.join(PLUGIN_FILE_NAME)
      end
    end
  end
end