| 1 | # |
|---|
| 2 | # Install/distribution utility functions |
|---|
| 3 | # $Id$ |
|---|
| 4 | # |
|---|
| 5 | # Copyright (c) 2001-2005, 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 | BEGIN { |
|---|
| 13 | require 'rbconfig' |
|---|
| 14 | require 'uri' |
|---|
| 15 | require 'find' |
|---|
| 16 | require 'pp' |
|---|
| 17 | |
|---|
| 18 | begin |
|---|
| 19 | require 'readline' |
|---|
| 20 | include Readline |
|---|
| 21 | rescue LoadError => e |
|---|
| 22 | $stderr.puts "Faking readline..." |
|---|
| 23 | def readline( prompt ) |
|---|
| 24 | $stderr.print prompt.chomp |
|---|
| 25 | return $stdin.gets.chomp |
|---|
| 26 | end |
|---|
| 27 | end |
|---|
| 28 | |
|---|
| 29 | begin |
|---|
| 30 | require 'yaml' |
|---|
| 31 | $yaml = true |
|---|
| 32 | rescue LoadError => e |
|---|
| 33 | $stderr.puts "No YAML; try() will use PrettyPrint instead." |
|---|
| 34 | $yaml = false |
|---|
| 35 | end |
|---|
| 36 | } |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | module UtilityFunctions |
|---|
| 40 | include Config |
|---|
| 41 | |
|---|
| 42 | # The list of regexen that eliminate files from the MANIFEST |
|---|
| 43 | ANTIMANIFEST = [ |
|---|
| 44 | /makedist\.rb/, |
|---|
| 45 | /\bCVS\b/, |
|---|
| 46 | /~$/, |
|---|
| 47 | /^#/, |
|---|
| 48 | %r{docs/html}, |
|---|
| 49 | %r{docs/man}, |
|---|
| 50 | /\bTEMPLATE\.\w+\.tpl\b/, |
|---|
| 51 | /\.cvsignore/, |
|---|
| 52 | /\.s?o$/, |
|---|
| 53 | ] |
|---|
| 54 | |
|---|
| 55 | # Set some ANSI escape code constants (Shamelessly stolen from Perl's |
|---|
| 56 | # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com> |
|---|
| 57 | AnsiAttributes = { |
|---|
| 58 | 'clear' => 0, |
|---|
| 59 | 'reset' => 0, |
|---|
| 60 | 'bold' => 1, |
|---|
| 61 | 'dark' => 2, |
|---|
| 62 | 'underline' => 4, |
|---|
| 63 | 'underscore' => 4, |
|---|
| 64 | 'blink' => 5, |
|---|
| 65 | 'reverse' => 7, |
|---|
| 66 | 'concealed' => 8, |
|---|
| 67 | |
|---|
| 68 | 'black' => 30, 'on_black' => 40, |
|---|
| 69 | 'red' => 31, 'on_red' => 41, |
|---|
| 70 | 'green' => 32, 'on_green' => 42, |
|---|
| 71 | 'yellow' => 33, 'on_yellow' => 43, |
|---|
| 72 | 'blue' => 34, 'on_blue' => 44, |
|---|
| 73 | 'magenta' => 35, 'on_magenta' => 45, |
|---|
| 74 | 'cyan' => 36, 'on_cyan' => 46, |
|---|
| 75 | 'white' => 37, 'on_white' => 47 |
|---|
| 76 | } |
|---|
| 77 | |
|---|
| 78 | ErasePreviousLine = "\033[A\033[K" |
|---|
| 79 | |
|---|
| 80 | ManifestHeader = (<<-"EOF").gsub( /^\t+/, '' ) |
|---|
| 81 | # |
|---|
| 82 | # Distribution Manifest |
|---|
| 83 | # Created: #{Time::now.to_s} |
|---|
| 84 | # |
|---|
| 85 | |
|---|
| 86 | EOF |
|---|
| 87 | |
|---|
| 88 | ############### |
|---|
| 89 | module_function |
|---|
| 90 | ############### |
|---|
| 91 | |
|---|
| 92 | # Create a string that contains the ANSI codes specified and return it |
|---|
| 93 | def ansiCode( *attributes ) |
|---|
| 94 | return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM'] |
|---|
| 95 | attr = attributes.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';') |
|---|
| 96 | if attr.empty? |
|---|
| 97 | return '' |
|---|
| 98 | else |
|---|
| 99 | return "\e[%sm" % attr |
|---|
| 100 | end |
|---|
| 101 | end |
|---|
| 102 | |
|---|
| 103 | # Test for the presence of the specified <tt>library</tt>, and output a |
|---|
| 104 | # message describing the test using <tt>nicename</tt>. If <tt>nicename</tt> |
|---|
| 105 | # is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default. |
|---|
| 106 | def testForLibrary( library, nicename=nil, progress=false ) |
|---|
| 107 | nicename ||= library |
|---|
| 108 | message( "Testing for the #{nicename} library..." ) if progress |
|---|
| 109 | if $LOAD_PATH.detect {|dir| |
|---|
| 110 | File.exists?(File.join(dir,"#{library}.rb")) || |
|---|
| 111 | File.exists?(File.join(dir,"#{library}.#{CONFIG['DLEXT']}")) |
|---|
| 112 | } |
|---|
| 113 | message( "found.\n" ) if progress |
|---|
| 114 | return true |
|---|
| 115 | else |
|---|
| 116 | message( "not found.\n" ) if progress |
|---|
| 117 | return false |
|---|
| 118 | end |
|---|
| 119 | end |
|---|
| 120 | |
|---|
| 121 | # Test for the presence of the specified <tt>library</tt>, and output a |
|---|
| 122 | # message describing the problem using <tt>nicename</tt>. If |
|---|
| 123 | # <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used |
|---|
| 124 | # to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are |
|---|
| 125 | # specified, they are also use to build a message describing how to find the |
|---|
| 126 | # required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library |
|---|
| 127 | # will cause the program to abort. |
|---|
| 128 | def testForRequiredLibrary( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true ) |
|---|
| 129 | nicename ||= library |
|---|
| 130 | unless testForLibrary( library, nicename ) |
|---|
| 131 | msgs = [ "You are missing the required #{nicename} library.\n" ] |
|---|
| 132 | msgs << "RAA: #{raaUrl}\n" if raaUrl |
|---|
| 133 | msgs << "Download: #{downloadUrl}\n" if downloadUrl |
|---|
| 134 | if fatal |
|---|
| 135 | abort msgs.join('') |
|---|
| 136 | else |
|---|
| 137 | errorMessage msgs.join('') |
|---|
| 138 | end |
|---|
| 139 | end |
|---|
| 140 | return true |
|---|
| 141 | end |
|---|
| 142 | |
|---|
| 143 | ### Output <tt>msg</tt> as a ANSI-colored program/section header (white on |
|---|
| 144 | ### blue). |
|---|
| 145 | def header( msg ) |
|---|
| 146 | msg.chomp! |
|---|
| 147 | $stderr.puts ansiCode( 'bold', 'white', 'on_blue' ) + msg + ansiCode( 'reset' ) |
|---|
| 148 | $stderr.flush |
|---|
| 149 | end |
|---|
| 150 | |
|---|
| 151 | ### Output <tt>msg</tt> to STDERR and flush it. |
|---|
| 152 | def message( *msgs ) |
|---|
| 153 | $stderr.print( msgs.join("\n") ) |
|---|
| 154 | $stderr.flush |
|---|
| 155 | end |
|---|
| 156 | |
|---|
| 157 | ### Output +msg+ to STDERR and flush it if $VERBOSE is true. |
|---|
| 158 | def verboseMsg( msg ) |
|---|
| 159 | msg.chomp! |
|---|
| 160 | message( msg + "\n" ) if $VERBOSE |
|---|
| 161 | end |
|---|
| 162 | |
|---|
| 163 | ### Output the specified <tt>msg</tt> as an ANSI-colored error message |
|---|
| 164 | ### (white on red). |
|---|
| 165 | def errorMessage( msg ) |
|---|
| 166 | message ansiCode( 'bold', 'white', 'on_red' ) + msg + ansiCode( 'reset' ) |
|---|
| 167 | end |
|---|
| 168 | |
|---|
| 169 | ### Output the specified <tt>msg</tt> as an ANSI-colored debugging message |
|---|
| 170 | ### (yellow on blue). |
|---|
| 171 | def debug_msg( msg ) |
|---|
| 172 | return unless $DEBUG |
|---|
| 173 | msg.chomp! |
|---|
| 174 | $stderr.puts ansiCode( 'bold', 'yellow', 'on_blue' ) + ">>> #{msg}" + ansiCode( 'reset' ) |
|---|
| 175 | $stderr.flush |
|---|
| 176 | end |
|---|
| 177 | |
|---|
| 178 | ### Erase the previous line (if supported by your terminal) and output the |
|---|
| 179 | ### specified <tt>msg</tt> instead. |
|---|
| 180 | def replaceMessage( msg ) |
|---|
| 181 | $stderr.print ErasePreviousLine |
|---|
| 182 | message( msg ) |
|---|
| 183 | end |
|---|
| 184 | |
|---|
| 185 | ### Output a divider made up of <tt>length</tt> hyphen characters. |
|---|
| 186 | def divider( length=75 ) |
|---|
| 187 | $stderr.puts "\r" + ("-" * length ) |
|---|
| 188 | end |
|---|
| 189 | alias :writeLine :divider |
|---|
| 190 | |
|---|
| 191 | |
|---|
| 192 | ### Output the specified <tt>msg</tt> colored in ANSI red and exit with a |
|---|
| 193 | ### status of 1. |
|---|
| 194 | def abort( msg ) |
|---|
| 195 | print ansiCode( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansiCode( 'reset' ) + "\n\n" |
|---|
| 196 | Kernel.exit!( 1 ) |
|---|
| 197 | end |
|---|
| 198 | |
|---|
| 199 | |
|---|
| 200 | ### Output the specified <tt>promptString</tt> as a prompt (in green) and |
|---|
| 201 | ### return the user's input with leading and trailing spaces removed. If a |
|---|
| 202 | ### test is provided, the prompt will repeat until the test returns true. |
|---|
| 203 | ### An optional failure message can also be passed in. |
|---|
| 204 | def prompt( promptString, failure_msg="Try again." ) # :yields: response |
|---|
| 205 | promptString.chomp! |
|---|
| 206 | response = nil |
|---|
| 207 | |
|---|
| 208 | begin |
|---|
| 209 | response = readline( ansiCode('bold', 'green') + |
|---|
| 210 | "#{promptString}: " + ansiCode('reset') ).strip |
|---|
| 211 | if block_given? && ! yield( response ) |
|---|
| 212 | errorMessage( failure_msg + "\n\n" ) |
|---|
| 213 | response = nil |
|---|
| 214 | end |
|---|
| 215 | end until response |
|---|
| 216 | |
|---|
| 217 | return response |
|---|
| 218 | end |
|---|
| 219 | |
|---|
| 220 | |
|---|
| 221 | ### Prompt the user with the given <tt>promptString</tt> via #prompt, |
|---|
| 222 | ### substituting the given <tt>default</tt> if the user doesn't input |
|---|
| 223 | ### anything. If a test is provided, the prompt will repeat until the test |
|---|
| 224 | ### returns true. An optional failure message can also be passed in. |
|---|
| 225 | def promptWithDefault( promptString, default, failure_msg="Try again." ) |
|---|
| 226 | response = nil |
|---|
| 227 | |
|---|
| 228 | begin |
|---|
| 229 | response = prompt( "%s [%s]" % [ promptString, default ] ) |
|---|
| 230 | response = default if response.empty? |
|---|
| 231 | |
|---|
| 232 | if block_given? && ! yield( response ) |
|---|
| 233 | errorMessage( failure_msg + "\n\n" ) |
|---|
| 234 | response = nil |
|---|
| 235 | end |
|---|
| 236 | end until response |
|---|
| 237 | |
|---|
| 238 | return response |
|---|
| 239 | end |
|---|
| 240 | |
|---|
| 241 | |
|---|
| 242 | $programs = {} |
|---|
| 243 | |
|---|
| 244 | ### Search for the program specified by the given <tt>progname</tt> in the |
|---|
| 245 | ### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if |
|---|
| 246 | ### no such program is in the path. |
|---|
| 247 | def findProgram( progname ) |
|---|
| 248 | unless $programs.key?( progname ) |
|---|
| 249 | ENV['PATH'].split(File::PATH_SEPARATOR).each {|d| |
|---|
| 250 | file = File.join( d, progname ) |
|---|
| 251 | if File.executable?( file ) |
|---|
| 252 | $programs[ progname ] = file |
|---|
| 253 | break |
|---|
| 254 | end |
|---|
| 255 | } |
|---|
| 256 | end |
|---|
| 257 | |
|---|
| 258 | return $programs[ progname ] |
|---|
| 259 | end |
|---|
| 260 | |
|---|
| 261 | |
|---|
| 262 | ### Search for the release version for the project in the specified |
|---|
| 263 | ### +directory+. |
|---|
| 264 | def extractVersion( directory='.' ) |
|---|
| 265 | release = nil |
|---|
| 266 | |
|---|
| 267 | Dir::chdir( directory ) do |
|---|
| 268 | if File::directory?( "CVS" ) |
|---|
| 269 | verboseMsg( "Project is versioned via CVS. Searching for RELEASE_*_* tags..." ) |
|---|
| 270 | |
|---|
| 271 | if (( cvs = findProgram('cvs') )) |
|---|
| 272 | revs = [] |
|---|
| 273 | output = %x{cvs log} |
|---|
| 274 | output.scan( /RELEASE_(\d+(?:_\d\w+)*)/ ) {|match| |
|---|
| 275 | rev = $1.split(/_/).collect {|s| Integer(s) rescue 0} |
|---|
| 276 | verboseMsg( "Found %s...\n" % rev.join('.') ) |
|---|
| 277 | revs << rev |
|---|
| 278 | } |
|---|
| 279 | |
|---|
| 280 | release = revs.sort.last |
|---|
| 281 | end |
|---|
| 282 | |
|---|
| 283 | elsif File::directory?( '.svn' ) |
|---|
| 284 | verboseMsg( "Project is versioned via Subversion" ) |
|---|
| 285 | |
|---|
| 286 | if (( svn = findProgram('svn') )) |
|---|
| 287 | output = %x{svn pg project-version}.chomp |
|---|
| 288 | unless output.empty? |
|---|
| 289 | verboseMsg( "Using 'project-version' property: %p" % output ) |
|---|
| 290 | release = output.split( /[._]/ ).collect {|s| Integer(s) rescue 0} |
|---|
| 291 | end |
|---|
| 292 | end |
|---|
| 293 | end |
|---|
| 294 | end |
|---|
| 295 | |
|---|
| 296 | return release |
|---|
| 297 | end |
|---|
| 298 | |
|---|
| 299 | |
|---|
| 300 | ### Find the current release version for the project in the specified |
|---|
| 301 | ### +directory+ and return its successor. |
|---|
| 302 | def extractNextVersion( directory='.' ) |
|---|
| 303 | version = extractVersion( directory ) || [0,0,0] |
|---|
| 304 | version.compact! |
|---|
| 305 | version[-1] += 1 |
|---|
| 306 | |
|---|
| 307 | return version |
|---|
| 308 | end |
|---|
| 309 | |
|---|
| 310 | |
|---|
| 311 | # Pattern for extracting the name of the project from a Subversion URL |
|---|
| 312 | SVNUrlPath = %r{ |
|---|
| 313 | .*/ # Skip all but the last bit |
|---|
| 314 | (\w+) # $1 = project name |
|---|
| 315 | / # Followed by / + |
|---|
| 316 | (?: |
|---|
| 317 | trunk | # 'trunk' |
|---|
| 318 | ( |
|---|
| 319 | branches | # ...or branches/branch-name |
|---|
| 320 | tags # ...or tags/tag-name |
|---|
| 321 | )/\w |
|---|
| 322 | ) |
|---|
| 323 | $ # bound to the end |
|---|
| 324 | }ix |
|---|
| 325 | |
|---|
| 326 | ### Extract the project name (CVS Repository name) for the given +directory+. |
|---|
| 327 | def extractProjectName( directory='.' ) |
|---|
| 328 | name = nil |
|---|
| 329 | |
|---|
| 330 | Dir::chdir( directory ) do |
|---|
| 331 | |
|---|
| 332 | # CVS-controlled |
|---|
| 333 | if File::directory?( "CVS" ) |
|---|
| 334 | verboseMsg( "Project is versioned via CVS. Using repository name." ) |
|---|
| 335 | name = File.open( "CVS/Repository", "r").readline.chomp |
|---|
| 336 | name.sub!( %r{.*/}, '' ) |
|---|
| 337 | |
|---|
| 338 | # Subversion-controlled |
|---|
| 339 | elsif File::directory?( '.svn' ) |
|---|
| 340 | verboseMsg( "Project is versioned via Subversion" ) |
|---|
| 341 | |
|---|
| 342 | # If the machine has the svn tool, try to get the project name |
|---|
| 343 | if (( svn = findProgram( 'svn' ) )) |
|---|
| 344 | |
|---|
| 345 | # First try an explicit property |
|---|
| 346 | output = shellCommand( svn, 'pg', 'project-name' ) |
|---|
| 347 | if !output.empty? |
|---|
| 348 | verboseMsg( "Using 'project-name' property: %p" % output ) |
|---|
| 349 | name = output.first.chomp |
|---|
| 350 | |
|---|
| 351 | # If that doesn't work, try to figure it out from the URL |
|---|
| 352 | elsif (( uri = getSvnUri() )) |
|---|
| 353 | name = uri.path.sub( SVNUrlPath ) { $1 } |
|---|
| 354 | end |
|---|
| 355 | end |
|---|
| 356 | end |
|---|
| 357 | |
|---|
| 358 | # Fall back to guessing based on the directory name |
|---|
| 359 | unless name |
|---|
| 360 | name = File::basename(File::dirname( File::expand_path(__FILE__) )) |
|---|
| 361 | end |
|---|
| 362 | end |
|---|
| 363 | |
|---|
| 364 | return name |
|---|
| 365 | end |
|---|
| 366 | |
|---|
| 367 | |
|---|
| 368 | ### Extract the Subversion URL from the specified directory and return it as |
|---|
| 369 | ### a URI object. |
|---|
| 370 | def getSvnUri( directory='.' ) |
|---|
| 371 | uri = nil |
|---|
| 372 | |
|---|
| 373 | Dir::chdir( directory ) do |
|---|
| 374 | output = %x{svn info} |
|---|
| 375 | debug_msg( "Using info: %p" % output ) |
|---|
| 376 | |
|---|
| 377 | if /^URL: \s* ( .* )/xi.match( output ) |
|---|
| 378 | uri = URI::parse( $1 ) |
|---|
| 379 | end |
|---|
| 380 | end |
|---|
| 381 | |
|---|
| 382 | return uri |
|---|
| 383 | end |
|---|
| 384 | |
|---|
| 385 | |
|---|
| 386 | ### (Re)make a manifest file in the specified +path+. |
|---|
| 387 | def makeManifest( path="MANIFEST" ) |
|---|
| 388 | if File::exists?( path ) |
|---|
| 389 | reply = promptWithDefault( "Replace current '#{path}'? [yN]", "n" ) |
|---|
| 390 | return false unless /^y/i.match( reply ) |
|---|
| 391 | |
|---|
| 392 | verboseMsg "Replacing manifest at '#{path}'" |
|---|
| 393 | else |
|---|
| 394 | verboseMsg "Creating new manifest at '#{path}'" |
|---|
| 395 | end |
|---|
| 396 | |
|---|
| 397 | files = [] |
|---|
| 398 | verboseMsg( "Finding files...\n" ) |
|---|
| 399 | Find::find( Dir::pwd ) do |f| |
|---|
| 400 | Find::prune if File::directory?( f ) && |
|---|
| 401 | /^\./.match( File::basename(f) ) |
|---|
| 402 | verboseMsg( " found: #{f}\n" ) |
|---|
| 403 | files << f.sub( %r{^#{Dir::pwd}/?}, '' ) |
|---|
| 404 | end |
|---|
| 405 | files = vetManifest( files ) |
|---|
| 406 | |
|---|
| 407 | verboseMsg( "Writing new manifest to #{path}..." ) |
|---|
| 408 | File::open( path, File::WRONLY|File::CREAT|File::TRUNC ) do |ofh| |
|---|
| 409 | ofh.puts( ManifestHeader ) |
|---|
| 410 | ofh.puts( files ) |
|---|
| 411 | end |
|---|
| 412 | verboseMsg( "done." ) |
|---|
| 413 | end |
|---|
| 414 | |
|---|
| 415 | |
|---|
| 416 | ### Read the specified <tt>manifestFile</tt>, which is a text file |
|---|
| 417 | ### describing which files to package up for a distribution. The manifest |
|---|
| 418 | ### should consist of one or more lines, each containing one filename or |
|---|
| 419 | ### shell glob pattern. |
|---|
| 420 | def readManifest( manifestFile="MANIFEST" ) |
|---|
| 421 | verboseMsg "Building manifest..." |
|---|
| 422 | raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile |
|---|
| 423 | |
|---|
| 424 | manifest = IO::readlines( manifestFile ).collect {|line| |
|---|
| 425 | line.chomp |
|---|
| 426 | }.select {|line| |
|---|
| 427 | line !~ /^(\s*(#.*)?)?$/ |
|---|
| 428 | } |
|---|
| 429 | |
|---|
| 430 | filelist = [] |
|---|
| 431 | for pat in manifest |
|---|
| 432 | verboseMsg "Adding files that match '#{pat}' to the file list" |
|---|
| 433 | filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)} |
|---|
| 434 | end |
|---|
| 435 | |
|---|
| 436 | verboseMsg "found #{filelist.length} files.\n" |
|---|
| 437 | return filelist |
|---|
| 438 | end |
|---|
| 439 | |
|---|
| 440 | |
|---|
| 441 | ### Given a <tt>filelist</tt> like that returned by #readManifest, remove |
|---|
| 442 | ### the entries therein which match the Regexp objects in the given |
|---|
| 443 | ### <tt>antimanifest</tt> and return the resultant Array. |
|---|
| 444 | def vetManifest( filelist, antimanifest=ANTIMANIFEST ) |
|---|
| 445 | origLength = filelist.length |
|---|
| 446 | verboseMsg "Vetting manifest..." |
|---|
| 447 | |
|---|
| 448 | for regex in antimanifest |
|---|
| 449 | verboseMsg "\n\tPattern /#{regex.source}/ removed: " + |
|---|
| 450 | filelist.find_all {|file| regex.match(file)}.join(', ') |
|---|
| 451 | filelist.delete_if {|file| regex.match(file)} |
|---|
| 452 | end |
|---|
| 453 | |
|---|
| 454 | verboseMsg "removed #{origLength - filelist.length} files from the list.\n" |
|---|
| 455 | return filelist |
|---|
| 456 | end |
|---|
| 457 | |
|---|
| 458 | |
|---|
| 459 | ### Combine a call to #readManifest with one to #vetManifest. |
|---|
| 460 | def getVettedManifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST ) |
|---|
| 461 | vetManifest( readManifest(manifestFile), antimanifest ) |
|---|
| 462 | end |
|---|
| 463 | |
|---|
| 464 | |
|---|
| 465 | ### Given a documentation <tt>catalogFile</tt>, extract the title, if |
|---|
| 466 | ### available, and return it. Otherwise generate a title from the name of |
|---|
| 467 | ### the CVS module. |
|---|
| 468 | def findRdocTitle( catalogFile="docs/CATALOG" ) |
|---|
| 469 | |
|---|
| 470 | # Try extracting it from the CATALOG file from a line that looks like: |
|---|
| 471 | # Title: Foo Bar Module |
|---|
| 472 | title = findCatalogKeyword( 'title', catalogFile ) |
|---|
| 473 | |
|---|
| 474 | # If that doesn't work for some reason, use the name of the project. |
|---|
| 475 | title = extractProjectName() |
|---|
| 476 | |
|---|
| 477 | return title |
|---|
| 478 | end |
|---|
| 479 | |
|---|
| 480 | |
|---|
| 481 | ### Given a documentation <tt>catalogFile</tt>, extract the name of the file |
|---|
| 482 | ### to use as the initally displayed page. If extraction fails, the |
|---|
| 483 | ### +default+ will be used if it exists. Returns +nil+ if there is no main |
|---|
| 484 | ### file to be found. |
|---|
| 485 | def findRdocMain( catalogFile="docs/CATALOG", default="README" ) |
|---|
| 486 | |
|---|
| 487 | # Try extracting it from the CATALOG file from a line that looks like: |
|---|
| 488 | # Main: Foo Bar Module |
|---|
| 489 | main = findCatalogKeyword( 'main', catalogFile ) |
|---|
| 490 | |
|---|
| 491 | # Try to make some educated guesses if that doesn't work |
|---|
| 492 | if main.nil? |
|---|
| 493 | basedir = File::dirname( __FILE__ ) |
|---|
| 494 | basedir = File::dirname( basedir ) if /docs$/ =~ basedir |
|---|
| 495 | |
|---|
| 496 | if File::exists?( File::join(basedir, default) ) |
|---|
| 497 | main = default |
|---|
| 498 | end |
|---|
| 499 | end |
|---|
| 500 | |
|---|
| 501 | return main |
|---|
| 502 | end |
|---|
| 503 | |
|---|
| 504 | |
|---|
| 505 | ### Given a documentation <tt>catalogFile</tt>, extract an upload URL for |
|---|
| 506 | ### RDoc. |
|---|
| 507 | def findRdocUpload( catalogFile="docs/CATALOG" ) |
|---|
| 508 | findCatalogKeyword( 'upload', catalogFile ) |
|---|
| 509 | end |
|---|
| 510 | |
|---|
| 511 | |
|---|
| 512 | ### Given a documentation <tt>catalogFile</tt>, extract a CVS web frontend |
|---|
| 513 | ### URL for RDoc. |
|---|
| 514 | def findRdocCvsURL( catalogFile="docs/CATALOG" ) |
|---|
| 515 | findCatalogKeyword( 'webcvs', catalogFile ) |
|---|
| 516 | end |
|---|
| 517 | |
|---|
| 518 | |
|---|
| 519 | ### Given a documentation <tt>catalogFile</tt>, try extracting the given |
|---|
| 520 | ### +keyword+'s value from it. Keywords are lines that look like: |
|---|
| 521 | ### # <keyword>: <value> |
|---|
| 522 | ### Returns +nil+ if the catalog file was unreadable or didn't contain the |
|---|
| 523 | ### specified +keyword+. |
|---|
| 524 | def findCatalogKeyword( keyword, catalogFile="docs/CATALOG" ) |
|---|
| 525 | val = nil |
|---|
| 526 | |
|---|
| 527 | if File::exists? catalogFile |
|---|
| 528 | verboseMsg "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile |
|---|
| 529 | File::foreach( catalogFile ) {|line| |
|---|
| 530 | debug_msg( "Examining line #{line.inspect}..." ) |
|---|
| 531 | val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i =~ line |
|---|
| 532 | } |
|---|
| 533 | end |
|---|
| 534 | |
|---|
| 535 | return val |
|---|
| 536 | end |
|---|
| 537 | |
|---|
| 538 | |
|---|
| 539 | ### Given a documentation <tt>catalogFile</tt>, which is in the same format |
|---|
| 540 | ### as that described by #readManifest, read and expand it, and then return |
|---|
| 541 | ### a list of those files which appear to have RDoc documentation in |
|---|
| 542 | ### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST |
|---|
| 543 | ### file is used instead. |
|---|
| 544 | def findRdocableFiles( catalogFile="docs/CATALOG" ) |
|---|
| 545 | startlist = [] |
|---|
| 546 | if File.exists? catalogFile |
|---|
| 547 | verboseMsg "Using CATALOG file (%s).\n" % catalogFile |
|---|
| 548 | startlist = getVettedManifest( catalogFile ) |
|---|
| 549 | else |
|---|
| 550 | verboseMsg "Using default MANIFEST\n" |
|---|
| 551 | startlist = getVettedManifest() |
|---|
| 552 | end |
|---|
| 553 | |
|---|
| 554 | verboseMsg "Looking for RDoc comments in:\n" |
|---|
| 555 | startlist.select {|fn| |
|---|
| 556 | verboseMsg " #{fn}: " |
|---|
| 557 | found = false |
|---|
| 558 | File::open( fn, "r" ) {|fh| |
|---|
| 559 | fh.each {|line| |
|---|
| 560 | if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*} |
|---|
| 561 | found = true |
|---|
| 562 | break |
|---|
| 563 | end |
|---|
| 564 | } |
|---|
| 565 | } |
|---|
| 566 | |
|---|
| 567 | verboseMsg( (found ? "yes" : "no") + "\n" ) |
|---|
| 568 | found |
|---|
| 569 | } |
|---|
| 570 | end |
|---|
| 571 | |
|---|
| 572 | ### Open a file and filter each of its lines through the given block a |
|---|
| 573 | ### <tt>line</tt> at a time. The return value of the block is used as the |
|---|
| 574 | ### new line, or omitted if the block returns <tt>nil</tt> or |
|---|
| 575 | ### <tt>false</tt>. |
|---|
| 576 | def editInPlace( file, testMode=false ) # :yields: line |
|---|
| 577 | raise "No block specified for editing operation" unless block_given? |
|---|
| 578 | |
|---|
| 579 | tempName = "#{file}.#{$$}" |
|---|
| 580 | File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile| |
|---|
| 581 | File::open( file, File::RDONLY ) {|fh| |
|---|
| 582 | fh.each {|line| |
|---|
| 583 | newline = yield( line ) or next |
|---|
| 584 | tempfile.print( newline ) |
|---|
| 585 | $deferr.puts "%p -> %p" % [ line, newline ] if |
|---|
| 586 | line != newline |
|---|
| 587 | } |
|---|
| 588 | } |
|---|
| 589 | } |
|---|
| 590 | |
|---|
| 591 | if testMode |
|---|
| 592 | File::unlink( tempName ) |
|---|
| 593 | else |
|---|
| 594 | File::rename( tempName, file ) |
|---|
| 595 | end |
|---|
| 596 | end |
|---|
| 597 | |
|---|
| 598 | ### Execute the specified shell <tt>command</tt>, read the results, and |
|---|
| 599 | ### return them. Like a %x{} that returns an Array instead of a String. |
|---|
| 600 | def shellCommand( *command ) |
|---|
| 601 | raise "Empty command" if command.empty? |
|---|
| 602 | |
|---|
| 603 | cmdpipe = IO::popen( command.join(' '), 'r' ) |
|---|
| 604 | return cmdpipe.readlines |
|---|
| 605 | end |
|---|
| 606 | |
|---|
| 607 | ### Execute a block with $VERBOSE set to +false+, restoring it to its |
|---|
| 608 | ### previous value before returning. |
|---|
| 609 | def verboseOff |
|---|
| 610 | raise LocalJumpError, "No block given" unless block_given? |
|---|
| 611 | |
|---|
| 612 | thrcrit = Thread.critical |
|---|
| 613 | oldverbose = $VERBOSE |
|---|
| 614 | begin |
|---|
| 615 | Thread.critical = true |
|---|
| 616 | $VERBOSE = false |
|---|
| 617 | yield |
|---|
| 618 | ensure |
|---|
| 619 | $VERBOSE = oldverbose |
|---|
| 620 | Thread.critical = false |
|---|
| 621 | end |
|---|
| 622 | end |
|---|
| 623 | |
|---|
| 624 | |
|---|
| 625 | ### Try the specified code block, printing the given |
|---|
| 626 | def try( msg, bind=TOPLEVEL_BINDING ) |
|---|
| 627 | result = '' |
|---|
| 628 | if msg =~ /^to\s/ |
|---|
| 629 | message "Trying #{msg}...\n" |
|---|
| 630 | else |
|---|
| 631 | message msg + "\n" |
|---|
| 632 | end |
|---|
| 633 | |
|---|
| 634 | begin |
|---|
| 635 | rval = nil |
|---|
| 636 | if block_given? |
|---|
| 637 | rval = yield |
|---|
| 638 | else |
|---|
| 639 | file, line = caller(1)[0].split(/:/,2) |
|---|
| 640 | rval = eval( msg, bind, file, line.to_i ) |
|---|
| 641 | end |
|---|
| 642 | |
|---|
| 643 | if $yaml |
|---|
| 644 | result = rval.to_yaml |
|---|
| 645 | else |
|---|
| 646 | PP.pp( rval, result ) |
|---|
| 647 | end |
|---|
| 648 | |
|---|
| 649 | rescue Exception => err |
|---|
| 650 | if err.backtrace |
|---|
| 651 | nicetrace = err.backtrace.delete_if {|frame| |
|---|
| 652 | /in `(try|eval)'/ =~ frame |
|---|
| 653 | }.join("\n\t") |
|---|
| 654 | else |
|---|
| 655 | nicetrace = "Exception had no backtrace" |
|---|
| 656 | end |
|---|
| 657 | |
|---|
| 658 | result = err.message + "\n\t" + nicetrace |
|---|
| 659 | ensure |
|---|
| 660 | divider |
|---|
| 661 | message result + "\n" |
|---|
| 662 | divider |
|---|
| 663 | $deferr.puts |
|---|
| 664 | end |
|---|
| 665 | end |
|---|
| 666 | end |
|---|
| 667 | |
|---|
| 668 | |
|---|
| 669 | if __FILE__ == $0 |
|---|
| 670 | # $DEBUG = true |
|---|
| 671 | include UtilityFunctions |
|---|
| 672 | |
|---|
| 673 | projname = extractProjectName() |
|---|
| 674 | header "Project: #{projname}" |
|---|
| 675 | |
|---|
| 676 | ver = extractVersion() || [0,0,1] |
|---|
| 677 | puts "Version: %s\n" % ver.join('.') |
|---|
| 678 | |
|---|
| 679 | if File::directory?( "docs" ) |
|---|
| 680 | puts "Rdoc:", |
|---|
| 681 | " Title: " + findRdocTitle(), |
|---|
| 682 | " Main: " + findRdocMain(), |
|---|
| 683 | " Upload: " + findRdocUpload(), |
|---|
| 684 | " SCCS URL: " + findRdocCvsURL() |
|---|
| 685 | end |
|---|
| 686 | |
|---|
| 687 | puts "Manifest:", |
|---|
| 688 | " " + getVettedManifest().join("\n ") |
|---|
| 689 | end |
|---|