rake/helpers.rb
author Michael Granger <mgranger@laika.com>
Wed, 07 May 2008 18:22:04 +0000
changeset 1 1d3cfd4837a8
permissions -rw-r--r--
Filled out the project, added Ezmlm module + spec.

#####################################################################
###	G L O B A L   H E L P E R   F U N C T I O N S
#####################################################################

require 'pathname'
require 'readline'
require 'open3'

# Set some ANSI escape code constants (Shamelessly stolen from Perl's
# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
ANSI_ATTRIBUTES = {
	'clear'      => 0,
	'reset'      => 0,
	'bold'       => 1,
	'dark'       => 2,
	'underline'  => 4,
	'underscore' => 4,
	'blink'      => 5,
	'reverse'    => 7,
	'concealed'  => 8,

	'black'      => 30,   'on_black'   => 40, 
	'red'        => 31,   'on_red'     => 41, 
	'green'      => 32,   'on_green'   => 42, 
	'yellow'     => 33,   'on_yellow'  => 43, 
	'blue'       => 34,   'on_blue'    => 44, 
	'magenta'    => 35,   'on_magenta' => 45, 
	'cyan'       => 36,   'on_cyan'    => 46, 
	'white'      => 37,   'on_white'   => 47
}


CLEAR_TO_EOL       = "\e[K"
CLEAR_CURRENT_LINE = "\e[2K"


### Output a logging message
def log( *msg )
	output = colorize( msg.flatten.join(' '), 'cyan' )
	$deferr.puts( output )
end


### Output a logging message if tracing is on
def trace( *msg )
	return unless $trace
	output = colorize( msg.flatten.join(' '), 'yellow' )
	$deferr.puts( output )
end


### Run the specified command +cmd+ with system(), failing if the execution
### fails.
def run( *cmd )
	cmd.flatten!

	log( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
	if $dryrun
		$deferr.puts "(dry run mode)"
	else
		system( *cmd )
		unless $?.success?
			fail "Command failed: [%s]" % [cmd.join(' ')]
		end
	end
end


### Open a pipe to a process running the given +cmd+ and call the given block with it.
def pipeto( *cmd )
	$DEBUG = true

	cmd.flatten!
	log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
	if $dryrun
		$deferr.puts "(dry run mode)"
	else
		open( '|-', 'w+' ) do |io|
		
			# Parent
			if io
				yield( io )

			# Child
			else
				exec( *cmd )
				fail "Command failed: [%s]" % [cmd.join(' ')]
			end
		end
	end
end


### Download the file at +sourceuri+ via HTTP and write it to +targetfile+.
def download( sourceuri, targetfile )
	oldsync = $defout.sync
	$defout.sync = true
	require 'net/http'
	require 'uri'

	targetpath = Pathname.new( targetfile )

	log "Downloading %s to %s" % [sourceuri, targetfile]
	targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh|
	
		url = URI.parse( sourceuri )
		downloaded = false
		limit = 5
		
		until downloaded or limit.zero?
			Net::HTTP.start( url.host, url.port ) do |http|
				req = Net::HTTP::Get.new( url.path )

				http.request( req ) do |res|
					if res.is_a?( Net::HTTPSuccess )
						log "Downloading..."
						res.read_body do |buf|
							ofh.print( buf )
						end
						downloaded = true
						puts "done."
		
					elsif res.is_a?( Net::HTTPRedirection )
						url = URI.parse( res['location'] )
						log "...following redirection to: %s" % [ url ]
						limit -= 1
						sleep 0.2
						next
				
					else
						res.error!
					end
				end
			end
		end
	end
	
	return targetpath
ensure
	$defout.sync = oldsync
end


### Return the fully-qualified path to the specified +program+ in the PATH.
def which( program )
	ENV['PATH'].split(/:/).
		collect {|dir| Pathname.new(dir) + program }.
		find {|path| path.exist? && path.executable? }
end


### Create a string that contains the ANSI codes specified and return it
def ansi_code( *attributes )
	attributes.flatten!
	attributes.collect! {|at| at.to_s }
	# $deferr.puts "Returning ansicode for TERM = %p: %p" %
	# 	[ ENV['TERM'], attributes ]
	return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
	attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')

	# $deferr.puts "  attr is: %p" % [attributes]
	if attributes.empty? 
		return ''
	else
		return "\e[%sm" % attributes
	end
end


### Colorize the given +string+ with the specified +attributes+ and return it, handling 
### line-endings, color reset, etc.
def colorize( *args )
	string = ''
	
	if block_given?
		string = yield
	else
		string = args.shift
	end
	
	ending = string[/(\s)$/] || ''
	string = string.rstrip

	return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
end


### Output the specified <tt>msg</tt> as an ANSI-colored error message
### (white on red).
def error_message( msg, details='' )
	$deferr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details
end
alias :error :error_message


### Highlight and embed a prompt control character in the given +string+ and return it.
def make_prompt_string( string )
	return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' }
end


### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
### return the user's input with leading and trailing spaces removed.  If a
### test is provided, the prompt will repeat until the test returns true.
### An optional failure message can also be passed in.
def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
	prompt_string.chomp!
	prompt_string << ":" unless /\W$/.match( prompt_string )
	response = nil

	begin
		prompt = make_prompt_string( prompt_string )
		response = Readline.readline( prompt ) || ''
		response.strip!
		if block_given? && ! yield( response ) 
			error_message( failure_msg + "\n\n" )
			response = nil
		end
	end while response.nil?

	return response
end


### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
### substituting the given <tt>default</tt> if the user doesn't input
### anything.  If a test is provided, the prompt will repeat until the test
### returns true.  An optional failure message can also be passed in.
def prompt_with_default( prompt_string, default, failure_msg="Try again." )
	response = nil

	begin
		response = prompt( "%s [%s]" % [ prompt_string, default ] )
		response = default if response.empty?

		if block_given? && ! yield( response ) 
			error_message( failure_msg + "\n\n" )
			response = nil
		end
	end while response.nil?

	return response
end


### Display a description of a potentially-dangerous task, and prompt
### for confirmation. If the user answers with anything that begins
### with 'y', yield to the block, else raise with an error.
def ask_for_confirmation( description )
	puts description

	answer = prompt_with_default( "Continue?", 'n' ) do |input|
		input =~ /^[yn]/i
	end

	case answer
	when /^y/i
		yield
	else
		error "Aborted."
		fail
	end
end


### Search line-by-line in the specified +file+ for the given +regexp+, returning the
### first match, or nil if no match was found. If the +regexp+ has any capture groups,
### those will be returned in an Array, else the whole matching line is returned.
def find_pattern_in_file( regexp, file )
	rval = nil
	
	File.open( file, 'r' ).each do |line|
		if (( match = regexp.match(line) ))
			rval = match.captures.empty? ? match[0] : match.captures
			break
		end
	end

	return rval
end


### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
### returning the first match, or nil if no match was found. If the +regexp+ has any 
### capture groups, those will be returned in an Array, else the whole matching line
### is returned.
def find_pattern_in_pipe( regexp, *cmd )
	output = []
	
	Open3.popen3( *cmd ) do |stdin, stdout, stderr|
		stdin.close

		output << stdout.gets until stdout.eof?
		output << stderr.gets until stderr.eof?
	end
	
	result = output.find { |line| regexp.match(line) } 
	return $1 || result
end


### Extract all the non Rake-target arguments from ARGV and return them.
def get_target_args
	args = ARGV.reject {|arg| Rake::Task.task_defined?(arg) }
	return args
end



require 'rubygems/dependency_installer'
require 'rubygems/source_index'
require 'rubygems/requirement'
require 'rubygems/doc_manager'

### Install the specified +gems+ if they aren't already installed.
def install_gems( *gems )
	gems.flatten!
	
	defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
		:generate_rdoc     => true,
		:generate_ri       => true,
		:install_dir       => Gem.dir,
		:format_executable => false,
		:test              => false,
		:version           => Gem::Requirement.default,
	  })
    
	# Check for root
	if Process.euid != 0
		$stderr.puts "This probably won't work, as you aren't root, but I'll try anyway"
	end

	gemindex = Gem::SourceIndex.from_installed_gems

	gems.each do |gemname|
		if (( specs = gemindex.search(gemname) )) && ! specs.empty?
			log "Version %s of %s is already installed; skipping..." % 
				[ specs.first.version, specs.first.name ]
			next
		end

		log "Trying to install #{gemname.inspect}..."
		installer = Gem::DependencyInstaller.new
		installer.install( gemname )

		installer.installed_gems.each do |spec|
			log "Installed: %s" % [ spec.full_name ]
		end

	end
end