rake/helpers.rb
changeset 9 143e61e24c08
parent 8 308f7dc97753
child 10 389e66b0d38e
equal deleted inserted replaced
8:308f7dc97753 9:143e61e24c08
     1 #####################################################################
       
     2 ###	G L O B A L   H E L P E R   F U N C T I O N S
       
     3 #####################################################################
       
     4 
       
     5 require 'pathname'
       
     6 require 'readline'
       
     7 require 'open3'
       
     8 
       
     9 # Set some ANSI escape code constants (Shamelessly stolen from Perl's
       
    10 # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
       
    11 ANSI_ATTRIBUTES = {
       
    12 	'clear'      => 0,
       
    13 	'reset'      => 0,
       
    14 	'bold'       => 1,
       
    15 	'dark'       => 2,
       
    16 	'underline'  => 4,
       
    17 	'underscore' => 4,
       
    18 	'blink'      => 5,
       
    19 	'reverse'    => 7,
       
    20 	'concealed'  => 8,
       
    21 
       
    22 	'black'      => 30,   'on_black'   => 40, 
       
    23 	'red'        => 31,   'on_red'     => 41, 
       
    24 	'green'      => 32,   'on_green'   => 42, 
       
    25 	'yellow'     => 33,   'on_yellow'  => 43, 
       
    26 	'blue'       => 34,   'on_blue'    => 44, 
       
    27 	'magenta'    => 35,   'on_magenta' => 45, 
       
    28 	'cyan'       => 36,   'on_cyan'    => 46, 
       
    29 	'white'      => 37,   'on_white'   => 47
       
    30 }
       
    31 
       
    32 
       
    33 CLEAR_TO_EOL       = "\e[K"
       
    34 CLEAR_CURRENT_LINE = "\e[2K"
       
    35 
       
    36 
       
    37 ### Output a logging message
       
    38 def log( *msg )
       
    39 	output = colorize( msg.flatten.join(' '), 'cyan' )
       
    40 	$deferr.puts( output )
       
    41 end
       
    42 
       
    43 
       
    44 ### Output a logging message if tracing is on
       
    45 def trace( *msg )
       
    46 	return unless $trace
       
    47 	output = colorize( msg.flatten.join(' '), 'yellow' )
       
    48 	$deferr.puts( output )
       
    49 end
       
    50 
       
    51 
       
    52 ### Run the specified command +cmd+ with system(), failing if the execution
       
    53 ### fails.
       
    54 def run( *cmd )
       
    55 	cmd.flatten!
       
    56 
       
    57 	log( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
       
    58 	if $dryrun
       
    59 		$deferr.puts "(dry run mode)"
       
    60 	else
       
    61 		system( *cmd )
       
    62 		unless $?.success?
       
    63 			fail "Command failed: [%s]" % [cmd.join(' ')]
       
    64 		end
       
    65 	end
       
    66 end
       
    67 
       
    68 
       
    69 ### Open a pipe to a process running the given +cmd+ and call the given block with it.
       
    70 def pipeto( *cmd )
       
    71 	$DEBUG = true
       
    72 
       
    73 	cmd.flatten!
       
    74 	log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} ) 
       
    75 	if $dryrun
       
    76 		$deferr.puts "(dry run mode)"
       
    77 	else
       
    78 		open( '|-', 'w+' ) do |io|
       
    79 		
       
    80 			# Parent
       
    81 			if io
       
    82 				yield( io )
       
    83 
       
    84 			# Child
       
    85 			else
       
    86 				exec( *cmd )
       
    87 				fail "Command failed: [%s]" % [cmd.join(' ')]
       
    88 			end
       
    89 		end
       
    90 	end
       
    91 end
       
    92 
       
    93 
       
    94 ### Download the file at +sourceuri+ via HTTP and write it to +targetfile+.
       
    95 def download( sourceuri, targetfile )
       
    96 	oldsync = $defout.sync
       
    97 	$defout.sync = true
       
    98 	require 'net/http'
       
    99 	require 'uri'
       
   100 
       
   101 	targetpath = Pathname.new( targetfile )
       
   102 
       
   103 	log "Downloading %s to %s" % [sourceuri, targetfile]
       
   104 	targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh|
       
   105 	
       
   106 		url = URI.parse( sourceuri )
       
   107 		downloaded = false
       
   108 		limit = 5
       
   109 		
       
   110 		until downloaded or limit.zero?
       
   111 			Net::HTTP.start( url.host, url.port ) do |http|
       
   112 				req = Net::HTTP::Get.new( url.path )
       
   113 
       
   114 				http.request( req ) do |res|
       
   115 					if res.is_a?( Net::HTTPSuccess )
       
   116 						log "Downloading..."
       
   117 						res.read_body do |buf|
       
   118 							ofh.print( buf )
       
   119 						end
       
   120 						downloaded = true
       
   121 						puts "done."
       
   122 		
       
   123 					elsif res.is_a?( Net::HTTPRedirection )
       
   124 						url = URI.parse( res['location'] )
       
   125 						log "...following redirection to: %s" % [ url ]
       
   126 						limit -= 1
       
   127 						sleep 0.2
       
   128 						next
       
   129 				
       
   130 					else
       
   131 						res.error!
       
   132 					end
       
   133 				end
       
   134 			end
       
   135 		end
       
   136 	end
       
   137 	
       
   138 	return targetpath
       
   139 ensure
       
   140 	$defout.sync = oldsync
       
   141 end
       
   142 
       
   143 
       
   144 ### Return the fully-qualified path to the specified +program+ in the PATH.
       
   145 def which( program )
       
   146 	ENV['PATH'].split(/:/).
       
   147 		collect {|dir| Pathname.new(dir) + program }.
       
   148 		find {|path| path.exist? && path.executable? }
       
   149 end
       
   150 
       
   151 
       
   152 ### Create a string that contains the ANSI codes specified and return it
       
   153 def ansi_code( *attributes )
       
   154 	attributes.flatten!
       
   155 	attributes.collect! {|at| at.to_s }
       
   156 	# $deferr.puts "Returning ansicode for TERM = %p: %p" %
       
   157 	# 	[ ENV['TERM'], attributes ]
       
   158 	return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
       
   159 	attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
       
   160 
       
   161 	# $deferr.puts "  attr is: %p" % [attributes]
       
   162 	if attributes.empty? 
       
   163 		return ''
       
   164 	else
       
   165 		return "\e[%sm" % attributes
       
   166 	end
       
   167 end
       
   168 
       
   169 
       
   170 ### Colorize the given +string+ with the specified +attributes+ and return it, handling 
       
   171 ### line-endings, color reset, etc.
       
   172 def colorize( *args )
       
   173 	string = ''
       
   174 	
       
   175 	if block_given?
       
   176 		string = yield
       
   177 	else
       
   178 		string = args.shift
       
   179 	end
       
   180 	
       
   181 	ending = string[/(\s)$/] || ''
       
   182 	string = string.rstrip
       
   183 
       
   184 	return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
       
   185 end
       
   186 
       
   187 
       
   188 ### Output the specified <tt>msg</tt> as an ANSI-colored error message
       
   189 ### (white on red).
       
   190 def error_message( msg, details='' )
       
   191 	$deferr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details
       
   192 end
       
   193 alias :error :error_message
       
   194 
       
   195 
       
   196 ### Highlight and embed a prompt control character in the given +string+ and return it.
       
   197 def make_prompt_string( string )
       
   198 	return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' }
       
   199 end
       
   200 
       
   201 
       
   202 ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
       
   203 ### return the user's input with leading and trailing spaces removed.  If a
       
   204 ### test is provided, the prompt will repeat until the test returns true.
       
   205 ### An optional failure message can also be passed in.
       
   206 def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
       
   207 	prompt_string.chomp!
       
   208 	prompt_string << ":" unless /\W$/.match( prompt_string )
       
   209 	response = nil
       
   210 
       
   211 	begin
       
   212 		prompt = make_prompt_string( prompt_string )
       
   213 		response = Readline.readline( prompt ) || ''
       
   214 		response.strip!
       
   215 		if block_given? && ! yield( response ) 
       
   216 			error_message( failure_msg + "\n\n" )
       
   217 			response = nil
       
   218 		end
       
   219 	end while response.nil?
       
   220 
       
   221 	return response
       
   222 end
       
   223 
       
   224 
       
   225 ### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
       
   226 ### substituting the given <tt>default</tt> if the user doesn't input
       
   227 ### anything.  If a test is provided, the prompt will repeat until the test
       
   228 ### returns true.  An optional failure message can also be passed in.
       
   229 def prompt_with_default( prompt_string, default, failure_msg="Try again." )
       
   230 	response = nil
       
   231 
       
   232 	begin
       
   233 		response = prompt( "%s [%s]" % [ prompt_string, default ] )
       
   234 		response = default if response.empty?
       
   235 
       
   236 		if block_given? && ! yield( response ) 
       
   237 			error_message( failure_msg + "\n\n" )
       
   238 			response = nil
       
   239 		end
       
   240 	end while response.nil?
       
   241 
       
   242 	return response
       
   243 end
       
   244 
       
   245 
       
   246 ### Display a description of a potentially-dangerous task, and prompt
       
   247 ### for confirmation. If the user answers with anything that begins
       
   248 ### with 'y', yield to the block, else raise with an error.
       
   249 def ask_for_confirmation( description )
       
   250 	puts description
       
   251 
       
   252 	answer = prompt_with_default( "Continue?", 'n' ) do |input|
       
   253 		input =~ /^[yn]/i
       
   254 	end
       
   255 
       
   256 	case answer
       
   257 	when /^y/i
       
   258 		yield
       
   259 	else
       
   260 		error "Aborted."
       
   261 		fail
       
   262 	end
       
   263 end
       
   264 
       
   265 
       
   266 ### Search line-by-line in the specified +file+ for the given +regexp+, returning the
       
   267 ### first match, or nil if no match was found. If the +regexp+ has any capture groups,
       
   268 ### those will be returned in an Array, else the whole matching line is returned.
       
   269 def find_pattern_in_file( regexp, file )
       
   270 	rval = nil
       
   271 	
       
   272 	File.open( file, 'r' ).each do |line|
       
   273 		if (( match = regexp.match(line) ))
       
   274 			rval = match.captures.empty? ? match[0] : match.captures
       
   275 			break
       
   276 		end
       
   277 	end
       
   278 
       
   279 	return rval
       
   280 end
       
   281 
       
   282 
       
   283 ### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
       
   284 ### returning the first match, or nil if no match was found. If the +regexp+ has any 
       
   285 ### capture groups, those will be returned in an Array, else the whole matching line
       
   286 ### is returned.
       
   287 def find_pattern_in_pipe( regexp, *cmd )
       
   288 	output = []
       
   289 	
       
   290 	Open3.popen3( *cmd ) do |stdin, stdout, stderr|
       
   291 		stdin.close
       
   292 
       
   293 		output << stdout.gets until stdout.eof?
       
   294 		output << stderr.gets until stderr.eof?
       
   295 	end
       
   296 	
       
   297 	result = output.find { |line| regexp.match(line) } 
       
   298 	return $1 || result
       
   299 end
       
   300 
       
   301 
       
   302 ### Extract all the non Rake-target arguments from ARGV and return them.
       
   303 def get_target_args
       
   304 	args = ARGV.reject {|arg| Rake::Task.task_defined?(arg) }
       
   305 	return args
       
   306 end
       
   307 
       
   308 
       
   309 
       
   310 require 'rubygems/dependency_installer'
       
   311 require 'rubygems/source_index'
       
   312 require 'rubygems/requirement'
       
   313 require 'rubygems/doc_manager'
       
   314 
       
   315 ### Install the specified +gems+ if they aren't already installed.
       
   316 def install_gems( *gems )
       
   317 	gems.flatten!
       
   318 	
       
   319 	defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
       
   320 		:generate_rdoc     => true,
       
   321 		:generate_ri       => true,
       
   322 		:install_dir       => Gem.dir,
       
   323 		:format_executable => false,
       
   324 		:test              => false,
       
   325 		:version           => Gem::Requirement.default,
       
   326 	  })
       
   327     
       
   328 	# Check for root
       
   329 	if Process.euid != 0
       
   330 		$stderr.puts "This probably won't work, as you aren't root, but I'll try anyway"
       
   331 	end
       
   332 
       
   333 	gemindex = Gem::SourceIndex.from_installed_gems
       
   334 
       
   335 	gems.each do |gemname|
       
   336 		if (( specs = gemindex.search(gemname) )) && ! specs.empty?
       
   337 			log "Version %s of %s is already installed; skipping..." % 
       
   338 				[ specs.first.version, specs.first.name ]
       
   339 			next
       
   340 		end
       
   341 
       
   342 		log "Trying to install #{gemname.inspect}..."
       
   343 		installer = Gem::DependencyInstaller.new
       
   344 		installer.install( gemname )
       
   345 
       
   346 		installer.installed_gems.each do |spec|
       
   347 			log "Installed: %s" % [ spec.full_name ]
       
   348 		end
       
   349 
       
   350 	end
       
   351 end
       
   352 
       
   353