1
0
Fork 0
mirror of https://github.com/capistrano/capistrano synced 2023-03-27 23:21:18 -04:00

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

git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@7389 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Jamis Buck 2007-09-01 14:35:31 +00:00
parent ee78acbca6
commit 8bfb81eac9
3 changed files with 101 additions and 5 deletions

View file

@ -1,5 +1,7 @@
*SVN*
* Allow independent configurations to require the same recipe file [Jamis Buck]
* Set :shell to false to run a command without wrapping it in "sh -c" [Jamis Buck]
* Don't request a pty by default [Jamis Buck]

View file

@ -25,6 +25,28 @@ module Capistrano
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.
@ -33,6 +55,7 @@ module Capistrano
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
@ -66,9 +89,11 @@ module Capistrano
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
@ -80,12 +105,63 @@ module Capistrano
# 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:
original, self.class.instance = self.class.instance, self
super
ensure
# restore the original, so that require's can be nested
self.class.instance = original
# 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
@ -107,6 +183,16 @@ module Capistrano
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

View file

@ -116,4 +116,12 @@ class ConfigurationLoadingTest < Test::Unit::TestCase
require "#{File.dirname(__FILE__)}/../fixtures/custom"
end
end
def test_require_in_multiple_instances_should_load_recipes_in_each_instance
config2 = MockConfig.new
@config.require "#{File.dirname(__FILE__)}/../fixtures/custom"
config2.require "#{File.dirname(__FILE__)}/../fixtures/custom"
assert_equal :custom, @config.ping
assert_equal :custom, config2.ping
end
end