| 1 | # |
|---|
| 2 | # Install/distribution utility functions |
|---|
| 3 | # $Id: utils.rb,v 1.1 2003/07/09 14:49:58 deveiant Exp $ |
|---|
| 4 | # |
|---|
| 5 | # Copyright (c) 2001-2003, The FaerieMUD Consortium. |
|---|
| 6 | # |
|---|
| 7 | # This is free software. You may use, modify, and/or redistribute this |
|---|
| 8 | # software under the terms of the Perl Artistic License. (See |
|---|
| 9 | # http://language.perl.com/misc/Artistic.html) |
|---|
| 10 | # |
|---|
| 11 | |
|---|
| 12 | |
|---|
| 13 | BEGIN { |
|---|
| 14 | begin |
|---|
| 15 | require 'readline' |
|---|
| 16 | include Readline |
|---|
| 17 | rescue LoadError => e |
|---|
| 18 | $stderr.puts "Faking readline..." |
|---|
| 19 | def readline( prompt ) |
|---|
| 20 | $stderr.print prompt.chomp |
|---|
| 21 | return $stdin.gets.chomp |
|---|
| 22 | end |
|---|
| 23 | end |
|---|
| 24 | } |
|---|
| 25 | |
|---|
| 26 | module UtilityFunctions |
|---|
| 27 | |
|---|
| 28 | # The list of regexen that eliminate files from the MANIFEST |
|---|
| 29 | ANTIMANIFEST = [ |
|---|
| 30 | /makedist\.rb/, |
|---|
| 31 | /\bCVS\b/, |
|---|
| 32 | /~$/, |
|---|
| 33 | /^#/, |
|---|
| 34 | %r{docs/html}, |
|---|
| 35 | %r{docs/man}, |
|---|
| 36 | /^TEMPLATE/, |
|---|
| 37 | /\.cvsignore/, |
|---|
| 38 | /\.s?o$/ |
|---|
| 39 | ] |
|---|
| 40 | |
|---|
| 41 | # Set some ANSI escape code constants (Shamelessly stolen from Perl's |
|---|
| 42 | # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com> |
|---|
| 43 | AnsiAttributes = { |
|---|
| 44 | 'clear' => 0, |
|---|
| 45 | 'reset' => 0, |
|---|
| 46 | 'bold' => 1, |
|---|
| 47 | 'dark' => 2, |
|---|
| 48 | 'underline' => 4, |
|---|
| 49 | 'underscore' => 4, |
|---|
| 50 | 'blink' => 5, |
|---|
| 51 | 'reverse' => 7, |
|---|
| 52 | 'concealed' => 8, |
|---|
| 53 | |
|---|
| 54 | 'black' => 30, 'on_black' => 40, |
|---|
| 55 | 'red' => 31, 'on_red' => 41, |
|---|
| 56 | 'green' => 32, 'on_green' => 42, |
|---|
| 57 | 'yellow' => 33, 'on_yellow' => 43, |
|---|
| 58 | 'blue' => 34, 'on_blue' => 44, |
|---|
| 59 | 'magenta' => 35, 'on_magenta' => 45, |
|---|
| 60 | 'cyan' => 36, 'on_cyan' => 46, |
|---|
| 61 | 'white' => 37, 'on_white' => 47 |
|---|
| 62 | } |
|---|
| 63 | |
|---|
| 64 | ErasePreviousLine = "\033[A\033[K" |
|---|
| 65 | |
|---|
| 66 | |
|---|
| 67 | ############### |
|---|
| 68 | module_function |
|---|
| 69 | ############### |
|---|
| 70 | |
|---|
| 71 | # Create a string that contains the ANSI codes specified and return it |
|---|
| 72 | def ansiCode( *attributes ) |
|---|
| 73 | attr = attributes.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';') |
|---|
| 74 | if attr.empty? |
|---|
| 75 | return '' |
|---|
| 76 | else |
|---|
| 77 | return "\e[%sm" % attr |
|---|
| 78 | end |
|---|
| 79 | end |
|---|
| 80 | |
|---|
| 81 | # Test for the presence of the specified <tt>library</tt>, and output a |
|---|
| 82 | # message describing the test using <tt>nicename</tt>. If <tt>nicename</tt> |
|---|
| 83 | # is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default. |
|---|
| 84 | def testForLibrary( library, nicename=nil ) |
|---|
| 85 | nicename ||= library |
|---|
| 86 | message( "Testing for the #{nicename} library..." ) |
|---|
| 87 | if $:.detect {|dir| File.exists?(File.join(dir,"#{library}.rb")) || File.exists?(File.join(dir,"#{library}.so"))} |
|---|
| 88 | message( "found.\n" ) |
|---|
| 89 | return true |
|---|
| 90 | else |
|---|
| 91 | message( "not found.\n" ) |
|---|
| 92 | return false |
|---|
| 93 | end |
|---|
| 94 | end |
|---|
| 95 | |
|---|
| 96 | # Test for the presence of the specified <tt>library</tt>, and output a |
|---|
| 97 | # message describing the problem using <tt>nicename</tt>. If |
|---|
| 98 | # <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used |
|---|
| 99 | # to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are |
|---|
| 100 | # specified, they are also use to build a message describing how to find the |
|---|
| 101 | # required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library |
|---|
| 102 | # will cause the program to abort. |
|---|
| 103 | def testForRequiredLibrary( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true ) |
|---|
| 104 | nicename ||= library |
|---|
| 105 | unless testForLibrary( library, nicename ) |
|---|
| 106 | msgs = [ "You are missing the required #{nicename} library.\n" ] |
|---|
| 107 | msgs << "RAA: #{raaUrl}\n" if raaUrl |
|---|
| 108 | msgs << "Download: #{downloadUrl}\n" if downloadUrl |
|---|
| 109 | if fatal |
|---|
| 110 | abort msgs.join('') |
|---|
| 111 | else |
|---|
| 112 | errorMessage msgs.join('') |
|---|
| 113 | end |
|---|
| 114 | end |
|---|
| 115 | return true |
|---|
| 116 | end |
|---|
| 117 | |
|---|
| 118 | ### Output <tt>msg</tt> as a ANSI-colored program/section header (white on |
|---|
| 119 | ### blue). |
|---|
| 120 | def header( msg ) |
|---|
| 121 | msg.chomp! |
|---|
| 122 | $stderr.puts ansiCode( 'bold', 'white', 'on_blue' ) + msg + ansiCode( 'reset' ) |
|---|
| 123 | $stderr.flush |
|---|
| 124 | end |
|---|
| 125 | |
|---|
| 126 | ### Output <tt>msg</tt> to STDERR and flush it. |
|---|
| 127 | def message( msg ) |
|---|
| 128 | $stderr.print msg |
|---|
| 129 | $stderr.flush |
|---|
| 130 | end |
|---|
| 131 | |
|---|
| 132 | ### Output the specified <tt>msg</tt> as an ANSI-colored error message |
|---|
| 133 | ### (white on red). |
|---|
| 134 | def errorMessage( msg ) |
|---|
| 135 | message ansiCode( 'bold', 'white', 'on_red' ) + msg + ansiCode( 'reset' ) |
|---|
| 136 | end |
|---|
| 137 | |
|---|
| 138 | ### Output the specified <tt>msg</tt> as an ANSI-colored debugging message |
|---|
| 139 | ### (yellow on blue). |
|---|
| 140 | def debugMsg( msg ) |
|---|
| 141 | return unless $DEBUG |
|---|
| 142 | msg.chomp! |
|---|
| 143 | $stderr.puts ansiCode( 'bold', 'yellow', 'on_blue' ) + ">>> #{msg}" + ansiCode( 'reset' ) |
|---|
| 144 | $stderr.flush |
|---|
| 145 | end |
|---|
| 146 | |
|---|
| 147 | ### Erase the previous line (if supported by your terminal) and output the |
|---|
| 148 | ### specified <tt>msg</tt> instead. |
|---|
| 149 | def replaceMessage( msg ) |
|---|
| 150 | print ErasePreviousLine |
|---|
| 151 | message( msg ) |
|---|
| 152 | end |
|---|
| 153 | |
|---|
| 154 | ### Output a divider made up of <tt>length</tt> hyphen characters. |
|---|
| 155 | def divider( length=75 ) |
|---|
| 156 | puts "\r" + ("-" * length ) |
|---|
| 157 | end |
|---|
| 158 | alias :writeLine :divider |
|---|
| 159 | |
|---|
| 160 | ### Output the specified <tt>msg</tt> colored in ANSI red and exit with a |
|---|
| 161 | ### status of 1. |
|---|
| 162 | def abort( msg ) |
|---|
| 163 | print ansiCode( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansiCode( 'reset' ) + "\n\n" |
|---|
| 164 | Kernel.exit!( 1 ) |
|---|
| 165 | end |
|---|
| 166 | |
|---|
| 167 | ### Output the specified <tt>promptString</tt> as a prompt (in green) and |
|---|
| 168 | ### return the user's input with leading and trailing spaces removed. |
|---|
| 169 | def prompt( promptString ) |
|---|
| 170 | promptString.chomp! |
|---|
| 171 | promptString += ": " unless /:\s*$/ =~ promptString |
|---|
| 172 | promptString = ansiCode('bold', 'green') + promptString + ansiCode('reset') |
|---|
| 173 | rval = readline( promptString ) || '' |
|---|
| 174 | return rval.strip |
|---|
| 175 | end |
|---|
| 176 | |
|---|
| 177 | ### Prompt the user with the given <tt>promptString</tt> via #prompt, |
|---|
| 178 | ### substituting the given <tt>default</tt> if the user doesn't input |
|---|
| 179 | ### anything. |
|---|
| 180 | def promptWithDefault( promptString, default ) |
|---|
| 181 | response = prompt( "%s [%s]" % [ promptString, default ] ) |
|---|
| 182 | if response.empty? |
|---|
| 183 | return default |
|---|
| 184 | else |
|---|
| 185 | return response |
|---|
| 186 | end |
|---|
| 187 | end |
|---|
| 188 | |
|---|
| 189 | ### Search for the program specified by the given <tt>progname</tt> in the |
|---|
| 190 | ### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if |
|---|
| 191 | ### no such program is in the path. |
|---|
| 192 | def findProgram( progname ) |
|---|
| 193 | ENV['PATH'].split(File::PATH_SEPARATOR).each {|d| |
|---|
| 194 | file = File.join( d, progname ) |
|---|
| 195 | return file if File.executable?( file ) |
|---|
| 196 | } |
|---|
| 197 | return nil |
|---|
| 198 | end |
|---|
| 199 | |
|---|
| 200 | ### Using the CVS log for the given <tt>file</tt> attempt to guess what the |
|---|
| 201 | ### next release version might be. This only works if releases are tagged |
|---|
| 202 | ### with tags like 'RELEASE_x_y'. |
|---|
| 203 | def extractNextVersionFromTags( file ) |
|---|
| 204 | message "Attempting to extract next release version from CVS tags for #{file}...\n" |
|---|
| 205 | raise RuntimeError, "No such file '#{file}'" unless File.exists?( file ) |
|---|
| 206 | cvsPath = findProgram( 'cvs' ) or |
|---|
| 207 | raise RuntimeError, "Cannot find the 'cvs' program. Aborting." |
|---|
| 208 | |
|---|
| 209 | output = %x{#{cvsPath} log #{file}} |
|---|
| 210 | release = [ 0, 0 ] |
|---|
| 211 | output.scan( /RELEASE_(\d+)_(\d+)/ ) {|match| |
|---|
| 212 | if $1.to_i > release[0] || $2.to_i > release[1] |
|---|
| 213 | release = [ $1.to_i, $2.to_i ] |
|---|
| 214 | replaceMessage( "Found %d.%02d...\n" % release ) |
|---|
| 215 | end |
|---|
| 216 | } |
|---|
| 217 | |
|---|
| 218 | if release[1] >= 99 |
|---|
| 219 | release[0] += 1 |
|---|
| 220 | release[1] = 1 |
|---|
| 221 | else |
|---|
| 222 | release[1] += 1 |
|---|
| 223 | end |
|---|
| 224 | |
|---|
| 225 | return "%d.%02d" % release |
|---|
| 226 | end |
|---|
| 227 | |
|---|
| 228 | ### Extract the project name (CVS Repository name) for the given directory. |
|---|
| 229 | def extractProjectName |
|---|
| 230 | File.open( "CVS/Repository", "r").readline.chomp |
|---|
| 231 | end |
|---|
| 232 | |
|---|
| 233 | ### Read the specified <tt>manifestFile</tt>, which is a text file |
|---|
| 234 | ### describing which files to package up for a distribution. The manifest |
|---|
| 235 | ### should consist of one or more lines, each containing one filename or |
|---|
| 236 | ### shell glob pattern. |
|---|
| 237 | def readManifest( manifestFile="MANIFEST" ) |
|---|
| 238 | message "Building manifest..." |
|---|
| 239 | raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile |
|---|
| 240 | |
|---|
| 241 | manifest = IO::readlines( manifestFile ).collect {|line| |
|---|
| 242 | line.chomp |
|---|
| 243 | }.select {|line| |
|---|
| 244 | line !~ /^(\s*(#.*)?)?$/ |
|---|
| 245 | } |
|---|
| 246 | |
|---|
| 247 | filelist = [] |
|---|
| 248 | for pat in manifest |
|---|
| 249 | $stderr.puts "Adding files that match '#{pat}' to the file list" if $VERBOSE |
|---|
| 250 | filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)} |
|---|
| 251 | end |
|---|
| 252 | |
|---|
| 253 | message "found #{filelist.length} files.\n" |
|---|
| 254 | return filelist |
|---|
| 255 | end |
|---|
| 256 | |
|---|
| 257 | ### Given a <tt>filelist</tt> like that returned by #readManifest, remove |
|---|
| 258 | ### the entries therein which match the Regexp objects in the given |
|---|
| 259 | ### <tt>antimanifest</tt> and return the resultant Array. |
|---|
| 260 | def vetManifest( filelist, antimanifest=ANITMANIFEST ) |
|---|
| 261 | origLength = filelist.length |
|---|
| 262 | message "Vetting manifest..." |
|---|
| 263 | |
|---|
| 264 | for regex in antimanifest |
|---|
| 265 | if $VERBOSE |
|---|
| 266 | message "\n\tPattern /#{regex.source}/ removed: " + |
|---|
| 267 | filelist.find_all {|file| regex.match(file)}.join(', ') |
|---|
| 268 | end |
|---|
| 269 | filelist.delete_if {|file| regex.match(file)} |
|---|
| 270 | end |
|---|
| 271 | |
|---|
| 272 | message "removed #{origLength - filelist.length} files from the list.\n" |
|---|
| 273 | return filelist |
|---|
| 274 | end |
|---|
| 275 | |
|---|
| 276 | ### Combine a call to #readManifest with one to #vetManifest. |
|---|
| 277 | def getVettedManifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST ) |
|---|
| 278 | vetManifest( readManifest(manifestFile), antimanifest ) |
|---|
| 279 | end |
|---|
| 280 | |
|---|
| 281 | ### Given a documentation <tt>catalogFile</tt>, which is in the same format |
|---|
| 282 | ### as that described by #readManifest, read and expand it, and then return |
|---|
| 283 | ### a list of those files which appear to have RDoc documentation in |
|---|
| 284 | ### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST |
|---|
| 285 | ### file is used instead. |
|---|
| 286 | def findRdocableFiles( catalogFile="docs/CATALOG" ) |
|---|
| 287 | startlist = [] |
|---|
| 288 | if File.exists? catalogFile |
|---|
| 289 | message "Using CATALOG file (%s).\n" % catalogFile |
|---|
| 290 | startlist = getVettedManifest( catalogFile ) |
|---|
| 291 | else |
|---|
| 292 | message "Using default MANIFEST\n" |
|---|
| 293 | startlist = getVettedManifest() |
|---|
| 294 | end |
|---|
| 295 | |
|---|
| 296 | message "Looking for RDoc comments in:\n" if $VERBOSE |
|---|
| 297 | startlist.select {|fn| |
|---|
| 298 | message " #{fn}: " if $VERBOSE |
|---|
| 299 | found = false |
|---|
| 300 | File::open( fn, "r" ) {|fh| |
|---|
| 301 | fh.each {|line| |
|---|
| 302 | if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*} |
|---|
| 303 | found = true |
|---|
| 304 | break |
|---|
| 305 | end |
|---|
| 306 | } |
|---|
| 307 | } |
|---|
| 308 | |
|---|
| 309 | message( (found ? "yes" : "no") + "\n" ) if $VERBOSE |
|---|
| 310 | found |
|---|
| 311 | } |
|---|
| 312 | end |
|---|
| 313 | |
|---|
| 314 | ### Open a file and filter each of its lines through the given block a |
|---|
| 315 | ### <tt>line</tt> at a time. The return value of the block is used as the |
|---|
| 316 | ### new line, or omitted if the block returns <tt>nil</tt> or |
|---|
| 317 | ### <tt>false</tt>. |
|---|
| 318 | def editInPlace( file ) # :yields: line |
|---|
| 319 | raise "No block specified for editing operation" unless block_given? |
|---|
| 320 | |
|---|
| 321 | tempName = "#{file}.#{$$}" |
|---|
| 322 | File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile| |
|---|
| 323 | File::unlink( tempName ) |
|---|
| 324 | File::open( file, File::RDONLY ) {|fh| |
|---|
| 325 | fh.each {|line| |
|---|
| 326 | newline = yield( line ) or next |
|---|
| 327 | tempfile.print( newline ) |
|---|
| 328 | } |
|---|
| 329 | } |
|---|
| 330 | |
|---|
| 331 | tempfile.seek(0) |
|---|
| 332 | |
|---|
| 333 | File::open( file, File::TRUNC|File::WRONLY, 0644 ) {|newfile| |
|---|
| 334 | newfile.print( tempfile.read ) |
|---|
| 335 | } |
|---|
| 336 | } |
|---|
| 337 | end |
|---|
| 338 | |
|---|
| 339 | ### Execute the specified shell <tt>command</tt>, read the results, and |
|---|
| 340 | ### return them. Like a %x{} that returns an Array instead of a String. |
|---|
| 341 | def shellCommand( *command ) |
|---|
| 342 | raise "Empty command" if command.empty? |
|---|
| 343 | |
|---|
| 344 | cmdpipe = IO::popen( command.join(' '), 'r' ) |
|---|
| 345 | return cmdpipe.readlines |
|---|
| 346 | end |
|---|
| 347 | |
|---|
| 348 | ### Execute a block with $VERBOSE set to +false+, restoring it to its |
|---|
| 349 | ### previous value before returning. |
|---|
| 350 | def verboseOff |
|---|
| 351 | raise LocalJumpError, "No block given" unless block_given? |
|---|
| 352 | |
|---|
| 353 | thrcrit = Thread.critical |
|---|
| 354 | oldverbose = $VERBOSE |
|---|
| 355 | begin |
|---|
| 356 | Thread.critical = true |
|---|
| 357 | $VERBOSE = false |
|---|
| 358 | yield |
|---|
| 359 | ensure |
|---|
| 360 | $VERBOSE = oldverbose |
|---|
| 361 | Thread.critical = false |
|---|
| 362 | end |
|---|
| 363 | end |
|---|
| 364 | |
|---|
| 365 | end |
|---|