public
Description: Remote multi-server automation tool
Homepage: http://www.capify.org
Clone URL: git://github.com/jamis/capistrano.git
Search Repo:
jamis (author)
Wed Feb 27 19:45:45 -0800 2008
commit  72a254d4221e37dce10e2e7e56b2abe36fc53452
tree    1682bd14df10907906f81378ee52d5c296f27edd
parent  e18662298b7f88d87f61c621492b709abd0dcb21
capistrano / lib / capistrano / configuration / loading.rb
100644 199 lines (175 sloc) 8.1 kb
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
module Capistrano
  class Configuration
    module Loading
      def self.included(base) #:nodoc:
        base.send :alias_method, :initialize_without_loading, :initialize
        base.send :alias_method, :initialize, :initialize_with_loading
        base.extend ClassMethods
      end
 
      module ClassMethods
        # Used by third-party task bundles to identify the capistrano
        # configuration that is loading them. Its return value is not reliable
        # in other contexts. If +require_config+ is not false, an exception
        # will be raised if the current configuration is not set.
        def instance(require_config=false)
          config = Thread.current[:capistrano_configuration]
          if require_config && config.nil?
            raise LoadError, "Please require this file from within a Capistrano recipe"
          end
          config
        end
 
        # Used internally by Capistrano to specify the current configuration
        # before loading a third-party task bundle.
        def instance=(config)
          Thread.current[:capistrano_configuration] = config
        end
 
        # Used internally by Capistrano to track which recipes have been loaded
        # via require, so that they may be successfully reloaded when require
        # is called again.
        def recipes_per_feature
          @recipes_per_feature ||= {}
        end
 
        # Used internally to determine what the current "feature" being
        # required is. This is used to track which files load which recipes
        # via require.
        def current_feature
          Thread.current[:capistrano_current_feature]
        end
 
        # Used internally to specify the current file being required, so that
        # any recipes loaded by that file can be remembered. This allows
        # recipes loaded via require to be correctly reloaded in different
        # Configuration instances in the same Ruby instance.
        def current_feature=(feature)
          Thread.current[:capistrano_current_feature] = feature
        end
      end
 
      # The load paths used for locating recipe files.
      attr_reader :load_paths
 
      def initialize_with_loading(*args) #:nodoc:
        initialize_without_loading(*args)
        @load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))]
        @loaded_features = []
      end
      private :initialize_with_loading
 
      # Load a configuration file or string into this configuration.
      #
      # Usage:
      #
      # load("recipe"):
      # Look for and load the contents of 'recipe.rb' into this
      # configuration.
      #
      # load(:file => "recipe"):
      # same as above
      #
      # load(:string => "set :scm, :subversion"):
      # Load the given string as a configuration specification.
      #
      # load { ... }
      # Load the block in the context of the configuration.
      def load(*args, &block)
        options = args.last.is_a?(Hash) ? args.pop : {}
 
        if block
          raise ArgumentError, "loading a block requires 0 arguments" unless options.empty? && args.empty?
          load(:proc => block)
 
        elsif args.any?
          args.each { |arg| load options.merge(:file => arg) }
 
        elsif options[:file]
          load_from_file(options[:file], options[:name])
 
        elsif options[:string]
          remember_load(options) unless options[:reloading]
          instance_eval(options[:string], options[:name] || "<eval>")
 
        elsif options[:proc]
          remember_load(options) unless options[:reloading]
          instance_eval(&options[:proc])
 
        else
          raise ArgumentError, "don't know how to load #{options.inspect}"
        end
      end
 
      # Require another file. This is identical to the standard require method,
      # with the exception that it sets the receiver as the "current" configuration
      # so that third-party task bundles can include themselves relative to
      # that configuration.
      #
      # This is a bit more complicated than an initial review would seem to
      # necessitate, but the use case that complicates things is this: An
      # advanced user wants to embed capistrano, and needs to instantiate
      # more than one capistrano configuration at a time. They also want each
      # configuration to require a third-party capistrano extension. Using a
      # naive require implementation, this would allow the first configuration
      # to successfully load the third-party extension, but the require would
      # fail for the second configuration because the extension has already
      # been loaded.
      #
      # To work around this, we do a few things:
      #
      # 1. Each time a 'require' is invoked inside of a capistrano recipe,
      # we remember the arguments (see "current_feature").
      # 2. Each time a 'load' is invoked inside of a capistrano recipe, and
      # "current_feature" is not nil (meaning we are inside of a pending
      # require) we remember the options (see "remember_load" and
      # "recipes_per_feature").
      # 3. Each time a 'require' is invoked inside of a capistrano recipe,
      # we check to see if this particular configuration has ever seen these
      # arguments to require (see @loaded_features), and if not, we proceed
      # as if the file had never been required. If the superclass' require
      # returns false (meaning, potentially, that the file has already been
      # required), then we look in the recipes_per_feature collection and
      # load any remembered recipes from there.
      #
      # It's kind of a bear, but it works, and works transparently. Note that
      # a simpler implementation would just muck with $", allowing files to be
      # required multiple times, but that will cause warnings (and possibly
      # errors) if the file to be required contains constant definitions and
      # such, alongside (or instead of) capistrano recipe definitions.
      def require(*args) #:nodoc:
        # look to see if this specific configuration instance has ever seen
        # these arguments to require before
        if !@loaded_features.include?(args)
          @loaded_features << args
 
          begin
            original_instance, self.class.instance = self.class.instance, self
            original_feature, self.class.current_feature = self.class.current_feature, args
 
            result = super
            if !result # file has been required previously, load up the remembered recipes
              list = self.class.recipes_per_feature[args] || []
              list.each { |options| load(options.merge(:reloading => true)) }
            end
 
            return result
          ensure
            # restore the original, so that require's can be nested
            self.class.instance = original_instance
            self.class.current_feature = original_feature
          end
        else
          return false
        end
      end
 
      private
 
        # Load a recipe from the named file. If +name+ is given, the file will
        # be reported using that name.
        def load_from_file(file, name=nil)
          file = find_file_in_load_path(file) unless file[0] == ?/
          load :string => File.read(file), :name => name || file
        end
 
        def find_file_in_load_path(file)
          load_paths.each do |path|
            ["", ".rb"].each do |ext|
              name = File.join(path, "#{file}#{ext}")
              return name if File.file?(name)
            end
          end
 
          raise LoadError, "no such file to load -- #{file}"
        end
 
        # If a file is being required, the options associated with loading a
        # recipe are remembered in the recipes_per_feature archive under the
        # name of the file currently being required.
        def remember_load(options)
          if self.class.current_feature
            list = (self.class.recipes_per_feature[self.class.current_feature] ||= [])
            list << options
          end
        end
    end
  end
end