Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 7389

Show
Ignore:
Timestamp:
09/01/07 14:35:31 (10 months ago)
Author:
minam
Message:

Allow independent configurations to require the same recipe file (closes #9367)

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • tools/capistrano/CHANGELOG

    r7388 r7389  
    11*SVN* 
     2 
     3* Allow independent configurations to require the same recipe file [Jamis Buck] 
    24 
    35* Set :shell to false to run a command without wrapping it in "sh -c" [Jamis Buck] 
  • tools/capistrano/lib/capistrano/configuration/loading.rb

    r7159 r7389  
    2626          Thread.current[:capistrano_configuration] = config 
    2727        end 
     28 
     29        # Used internally by Capistrano to track which recipes have been loaded 
     30        # via require, so that they may be successfully reloaded when require 
     31        # is called again. 
     32        def recipes_per_feature 
     33          @recipes_per_feature ||= {} 
     34        end 
     35 
     36        # Used internally to determine what the current "feature" being 
     37        # required is. This is used to track which files load which recipes 
     38        # via require. 
     39        def current_feature 
     40          Thread.current[:capistrano_current_feature] 
     41        end 
     42 
     43        # Used internally to specify the current file being required, so that 
     44        # any recipes loaded by that file can be remembered. This allows 
     45        # recipes loaded via require to be correctly reloaded in different 
     46        # Configuration instances in the same Ruby instance. 
     47        def current_feature=(feature) 
     48          Thread.current[:capistrano_current_feature] = feature 
     49        end 
    2850      end 
    2951 
     
    3456        initialize_without_loading(*args) 
    3557        @load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))] 
     58        @loaded_features = [] 
    3659      end 
    3760      private :initialize_with_loading 
     
    6790 
    6891        elsif options[:string] 
     92          remember_load(options) unless options[:reloading] 
    6993          instance_eval(options[:string], options[:name] || "<eval>") 
    7094 
    7195        elsif options[:proc] 
     96          remember_load(options) unless options[:reloading] 
    7297          instance_eval(&options[:proc]) 
    7398 
     
    81106      # so that third-party task bundles can include themselves relative to 
    82107      # that configuration. 
     108      # 
     109      # This is a bit more complicated than an initial review would seem to 
     110      # necessitate, but the use case that complicates things is this: An 
     111      # advanced user wants to embed capistrano, and needs to instantiate 
     112      # more than one capistrano configuration at a time. They also want each 
     113      # configuration to require a third-party capistrano extension. Using a 
     114      # naive require implementation, this would allow the first configuration 
     115      # to successfully load the third-party extension, but the require would 
     116      # fail for the second configuration because the extension has already 
     117      # been loaded. 
     118      # 
     119      # To work around this, we do a few things: 
     120      # 
     121      # 1. Each time a 'require' is invoked inside of a capistrano recipe, 
     122      #    we remember the arguments (see "current_feature"). 
     123      # 2. Each time a 'load' is invoked inside of a capistrano recipe, and 
     124      #    "current_feature" is not nil (meaning we are inside of a pending 
     125      #    require) we remember the options (see "remember_load" and 
     126      #    "recipes_per_feature"). 
     127      # 3. Each time a 'require' is invoked inside of a capistrano recipe, 
     128      #    we check to see if this particular configuration has ever seen these 
     129      #    arguments to require (see @loaded_features), and if not, we proceed 
     130      #    as if the file had never been required. If the superclass' require 
     131      #    returns false (meaning, potentially, that the file has already been 
     132      #    required), then we look in the recipes_per_feature collection and 
     133      #    load any remembered recipes from there. 
     134      # 
     135      # It's kind of a bear, but it works, and works transparently. Note that 
     136      # a simpler implementation would just muck with $", allowing files to be 
     137      # required multiple times, but that will cause warnings (and possibly 
     138      # errors) if the file to be required contains constant definitions and 
     139      # such, alongside (or instead of) capistrano recipe definitions. 
    83140      def require(*args) #:nodoc: 
    84         original, self.class.instance = self.class.instance, self 
    85         super 
    86       ensure 
    87         # restore the original, so that require's can be nested 
    88         self.class.instance = original 
     141        # look to see if this specific configuration instance has ever seen 
     142        # these arguments to require before 
     143        if !@loaded_features.include?(args) 
     144          @loaded_features << args 
     145 
     146          begin 
     147            original_instance, self.class.instance = self.class.instance, self 
     148            original_feature, self.class.current_feature = self.class.current_feature, args 
     149 
     150            result = super 
     151            if !result # file has been required previously, load up the remembered recipes 
     152              list = self.class.recipes_per_feature[args] || [] 
     153              list.each { |options| load(options.merge(:reloading => true)) } 
     154            end 
     155 
     156            return result 
     157          ensure 
     158            # restore the original, so that require's can be nested 
     159            self.class.instance = original_instance 
     160            self.class.current_feature = original_feature 
     161          end 
     162        else 
     163          return false 
     164        end 
    89165      end 
    90166 
     
    108184          raise LoadError, "no such file to load -- #{file}" 
    109185        end 
     186 
     187        # If a file is being required, the options associated with loading a 
     188        # recipe are remembered in the recipes_per_feature archive under the 
     189        # name of the file currently being required. 
     190        def remember_load(options) 
     191          if self.class.current_feature 
     192            list = (self.class.recipes_per_feature[self.class.current_feature] ||= []) 
     193            list << options 
     194          end 
     195        end 
    110196    end 
    111197  end 
  • tools/capistrano/test/configuration/loading_test.rb

    r6268 r7389  
    117117    end 
    118118  end 
     119 
     120  def test_require_in_multiple_instances_should_load_recipes_in_each_instance 
     121    config2 = MockConfig.new 
     122    @config.require "#{File.dirname(__FILE__)}/../fixtures/custom" 
     123    config2.require "#{File.dirname(__FILE__)}/../fixtures/custom" 
     124    assert_equal :custom, @config.ping 
     125    assert_equal :custom, config2.ping 
     126  end 
    119127end