module Sprinkle # Installers are where the bulk of the work in Sprinkle happens. Installers are # the building blocks of packages. Typically each unique type of install # command has it's own installer class. # module Installers # The base class which all installers must subclass, this class makes # sure all installers share some general features, which are outlined # below. # # = Pre/Post Installation Hooks # # With all installation methods you have the ability to specify multiple # pre/post installation hooks. This gives you the ability to specify # commands to run before and after an installation takes place. # There are three ways to specify a pre/post hook. # Note about sudo: # When using the Capistrano actor all commands by default are run using # sudo (unless your Capfile includes "set :use_sudo, false"). If you wish # to use sudo periodically with "set :user_sudo, false" or with an actor # other than Capistrano then you can just append it to your command. Some # installers (transfer) also support a :sudo option, so check each # installer for details. # # First, a single command: # # pre :install, 'echo "Hello, World!"' # post :install, 'rm -rf /' # # Second, an array of commands: # # commands = ['echo "First"', 'echo "Then Another"'] # pre :install, commands # post :install, commands # # Third, a block which returns either a single or multiple commands: # # pre :install do # amount = 7 * 3 # "echo 'Before we install, lets plant #{amount} magic beans...'" # end # post :install do # ['echo "Now... let's hope they sprout!", 'echo "Indeed they have!"'] # end # # = Other Pre/Post Hooks # # Some installation methods actually grant you more fine grained # control of when commands are run rather than a blanket pre :install # or post :install. If this is the case, it will be documented on # the installation method's corresponding documentation page. class Installer include Sprinkle::Attributes include Sprinkle::Sudo delegate :version, :to => :package attr_accessor :delivery, :package, :options, :pre, :post #:nodoc: def initialize(package, options = {}, &block) #:nodoc: @package = package @options = options || {} @pre = {}; @post = {} @delivery = nil self.instance_eval(&block) if block end attributes :prefix, :archives, :builds class << self def subclasses @subclasses ||= [] end def api(&block) Sprinkle::Package::Package.add_api(&block) end def verify_api(&block) Sprinkle::Verify.class_eval(&block) end def inherited(base) subclasses << base end end def escape_shell_arg(str) str.gsub("'", "'\\\\''").gsub("\n", '\n') end def pre(stage, *commands, &block) @pre[stage] ||= [] @pre[stage] += commands @pre[stage] << defer(block) if block_given? @pre[stage] end def post(stage, *commands, &block) @post[stage] ||= [] @post[stage] += commands @post[stage] << defer(block) if block_given? @post[stage] end # defer execution of command block until the package is being # processed def defer(block) p = Proc.new { self.commands_from_block(block) } end def commands_from_block(block) return [] unless block out = nil diff = @package.with_private_install_queue { out = block.call } diff.each {|x| x.delivery = self.delivery } diff.empty? ? out : diff.map {|x| x.install_sequence } end def method_missing(method, *args, &block) if package.class.installer_methods.include?(method) @package.send(method, *args, &block) else super(method, *args, &block) end end def per_host? return false end # Called right after processing, can be used for local cleanup such # as removing any temporary files created on the local system, etc def post_process; end # Called right before an installer is exected, can be used for logging # and announcing what is about to happen def announce; end def process(roles) #:nodoc: if logger.debug? sequence = install_sequence; sequence = sequence.join('; ') if sequence.is_a? Array logger.debug "#{@package.name} install sequence: #{sequence} for roles: #{roles}\n" end unless Sprinkle::OPTIONS[:testing] logger.debug " --> Running #{self.class.name} for roles: #{roles}" @delivery.install(self, roles, :per_host => per_host?) end post_process end # More complicated installers that have different stages, and require pre/post commands # within stages can override install_sequence and take complete control of the install # command sequence construction (eg. source based installer). def install_sequence commands = pre_commands(:install) + [ install_commands ] + post_commands(:install) flatten commands end protected def log(t, level=:info) #:nodoc: logger.send(level, t) end def flatten(commands) #:nodoc: commands.flatten.map {|c| c.is_a?(Proc) ? c.call : c }.flatten end # A concrete installer (subclass of this virtual class) must override this method # and return the commands it needs to run as either a string or an array. # # Overriding this method is required. def install_commands raise 'Concrete installers implement this to specify commands to run to install their respective packages' end def pre_commands(stage) #:nodoc: dress @pre[stage] || [], :pre end def post_commands(stage) #:nodoc: dress @post[stage] || [], :post end # Concrete installers (subclasses of this virtual class) can override this method to # specify stage-specific (pre-installation, post-installation, etc.) modifications # of commands. # # An example usage of overriding this would be to prefix all commands for a # certain stage to change to a certain directory. An example is given below: # # def dress(commands, stage) # commands.collect { |x| "cd #{magic_beans_path} && #{x}" } # end # # By default, no modifications are made to the commands. def dress(commands, stage); commands; end end end end