Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 13
- Log:
Initial import of Typo 2.6.0 sources from a downloaded Tarball.
Typo is a Ruby On Rails based blog engine.
- Author:
- adh
- Date:
- Sat Jul 22 22:25:02 +0100 2006
- Size:
- 15954 Bytes
1 | # |
2 | # Install/distribution utility functions |
3 | # $Id: utils.rb,v 1.5 2004/01/18 19:15:18 deveiant Exp $ |
4 | # |
5 | # Copyright (c) 2001-2004, 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 | require 'find' |
15 | |
16 | begin |
17 | require 'readline' |
18 | include Readline |
19 | rescue LoadError => e |
20 | $stderr.puts "Faking readline..." |
21 | def readline( prompt ) |
22 | $stderr.print prompt.chomp |
23 | return $stdin.gets.chomp |
24 | end |
25 | end |
26 | } |
27 | |
28 | class File |
29 | Win32Exts = %w{.exe .com .bat} |
30 | |
31 | def self::which( prog, path=ENV['PATH'] ) |
32 | path.split(File::PATH_SEPARATOR).each {|dir| |
33 | # If running under Windows, look for prog + extensions |
34 | if File::ALT_SEPARATOR |
35 | ext = Win32Exts.find_all {|ext| |
36 | f = File::join(dir, prog+ext) |
37 | File::executable?(f) && !File::directory?(f) |
38 | } |
39 | ext.each {|f| |
40 | f = File::join( dir, prog + f ).gsub(%r:/:,'\\') |
41 | if block_given? then yield( f ) else return f end |
42 | } |
43 | else |
44 | f = File::join( dir, prog ) |
45 | if File::executable?( f ) && ! File::directory?( f ) |
46 | if block_given? then yield(f) else return f end |
47 | end |
48 | end |
49 | } |
50 | end |
51 | |
52 | end |
53 | |
54 | |
55 | module UtilityFunctions |
56 | |
57 | # The list of regexen that eliminate files from the MANIFEST |
58 | ANTIMANIFEST = [ |
59 | /makedist\.rb/, |
60 | /\bCVS\b/, |
61 | /~$/, |
62 | /^#/, |
63 | %r{docs/html}, |
64 | %r{docs/man}, |
65 | /\bTEMPLATE\.\w+\.tpl\b/, |
66 | /\.cvsignore/, |
67 | /\.s?o$/, |
68 | ] |
69 | AMRegexp = Regexp::union( *ANTIMANIFEST ) |
70 | |
71 | # Set some ANSI escape code constants (Shamelessly stolen from Perl's |
72 | # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com> |
73 | AnsiAttributes = { |
74 | 'clear' => 0, |
75 | 'reset' => 0, |
76 | 'bold' => 1, |
77 | 'dark' => 2, |
78 | 'underline' => 4, |
79 | 'underscore' => 4, |
80 | 'blink' => 5, |
81 | 'reverse' => 7, |
82 | 'concealed' => 8, |
83 | |
84 | 'black' => 30, 'on_black' => 40, |
85 | 'red' => 31, 'on_red' => 41, |
86 | 'green' => 32, 'on_green' => 42, |
87 | 'yellow' => 33, 'on_yellow' => 43, |
88 | 'blue' => 34, 'on_blue' => 44, |
89 | 'magenta' => 35, 'on_magenta' => 45, |
90 | 'cyan' => 36, 'on_cyan' => 46, |
91 | 'white' => 37, 'on_white' => 47 |
92 | } |
93 | |
94 | ErasePreviousLine = "\033[A\033[K" |
95 | |
96 | |
97 | ############### |
98 | module_function |
99 | ############### |
100 | |
101 | # Create a string that contains the ANSI codes specified and return it |
102 | def ansiCode( *attributes ) |
103 | return '' unless /(?:vt10[03]|xterm(?:-color)?|linux)/i =~ ENV['TERM'] |
104 | attr = attributes.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';') |
105 | if attr.empty? |
106 | return '' |
107 | else |
108 | return "\e[%sm" % attr |
109 | end |
110 | end |
111 | |
112 | |
113 | # Test for the presence of the specified <tt>library</tt>, and output a |
114 | # message describing the test using <tt>nicename</tt>. If <tt>nicename</tt> |
115 | # is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default. |
116 | def testForLibrary( library, nicename=nil ) |
117 | nicename ||= library |
118 | message( "Testing for the #{nicename} library..." ) |
119 | found = false |
120 | |
121 | begin |
122 | require library |
123 | rescue LoadError => err |
124 | message "no found (%s)\n" % err.message |
125 | else |
126 | message "found\n" |
127 | found = true |
128 | end |
129 | |
130 | return found |
131 | end |
132 | |
133 | |
134 | # Test for the presence of the specified <tt>library</tt>, and output a |
135 | # message describing the problem using <tt>nicename</tt>. If |
136 | # <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used |
137 | # to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are |
138 | # specified, they are also use to build a message describing how to find the |
139 | # required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library |
140 | # will cause the program to abort. |
141 | def testForRequiredLibrary( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true ) |
142 | nicename ||= library |
143 | unless testForLibrary( library, nicename ) |
144 | msgs = [ "You are missing the required #{nicename} library.\n" ] |
145 | msgs << "RAA: #{raaUrl}\n" if raaUrl |
146 | msgs << "Download: #{downloadUrl}\n" if downloadUrl |
147 | if fatal |
148 | abort msgs.join('') |
149 | else |
150 | errorMessage msgs.join('') |
151 | end |
152 | end |
153 | return true |
154 | end |
155 | |
156 | ### Output <tt>msg</tt> as a ANSI-colored program/section header (white on |
157 | ### blue). |
158 | def header( msg ) |
159 | msg.chomp! |
160 | $stderr.puts ansiCode( 'bold', 'white', 'on_blue' ) + msg + ansiCode( 'reset' ) |
161 | $stderr.flush |
162 | end |
163 | |
164 | ### Output <tt>msg</tt> to STDERR and flush it. |
165 | def message( msg ) |
166 | $stderr.print ansiCode( 'cyan' ) + msg + ansiCode( 'reset' ) |
167 | $stderr.flush |
168 | end |
169 | |
170 | ### Output the specified <tt>msg</tt> as an ANSI-colored error message |
171 | ### (white on red). |
172 | def errorMessage( msg ) |
173 | message ansiCode( 'bold', 'white', 'on_red' ) + msg + ansiCode( 'reset' ) |
174 | end |
175 | |
176 | ### Output the specified <tt>msg</tt> as an ANSI-colored debugging message |
177 | ### (yellow on blue). |
178 | def debugMsg( msg ) |
179 | return unless $DEBUG |
180 | msg.chomp! |
181 | $stderr.puts ansiCode( 'bold', 'yellow', 'on_blue' ) + ">>> #{msg}" + ansiCode( 'reset' ) |
182 | $stderr.flush |
183 | end |
184 | |
185 | ### Erase the previous line (if supported by your terminal) and output the |
186 | ### specified <tt>msg</tt> instead. |
187 | def replaceMessage( msg ) |
188 | print ErasePreviousLine |
189 | message( msg ) |
190 | end |
191 | |
192 | ### Output a divider made up of <tt>length</tt> hyphen characters. |
193 | def divider( length=75 ) |
194 | puts "\r" + ("-" * length ) |
195 | end |
196 | alias :writeLine :divider |
197 | |
198 | ### Output the specified <tt>msg</tt> colored in ANSI red and exit with a |
199 | ### status of 1. |
200 | def abort( msg ) |
201 | print ansiCode( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansiCode( 'reset' ) + "\n\n" |
202 | Kernel.exit!( 1 ) |
203 | end |
204 | |
205 | ### Output the specified <tt>promptString</tt> as a prompt (in green) and |
206 | ### return the user's input with leading and trailing spaces removed. |
207 | def prompt( promptString ) |
208 | promptString.chomp! |
209 | return readline( ansiCode('bold', 'green') + "#{promptString}: " + ansiCode('reset') ).strip |
210 | end |
211 | |
212 | ### Prompt the user with the given <tt>promptString</tt> via #prompt, |
213 | ### substituting the given <tt>default</tt> if the user doesn't input |
214 | ### anything. |
215 | def promptWithDefault( promptString, default ) |
216 | response = prompt( "%s [%s]" % [ promptString, default ] ) |
217 | if response.empty? |
218 | return default |
219 | else |
220 | return response |
221 | end |
222 | end |
223 | |
224 | ### Search for the program specified by the given <tt>progname</tt> in the |
225 | ### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if |
226 | ### no such program is in the path. |
227 | def findProgram( progname ) |
228 | ENV['PATH'].split(File::PATH_SEPARATOR).each {|d| |
229 | file = File.join( d, progname ) |
230 | return file if File.executable?( file ) |
231 | } |
232 | return nil |
233 | end |
234 | |
235 | ### Using the CVS log for the given <tt>file</tt> attempt to guess what the |
236 | ### next release version might be. This only works if releases are tagged |
237 | ### with tags like 'RELEASE_x_y'. |
238 | def extractNextVersionFromTags( file ) |
239 | message "Attempting to extract next release version from CVS tags for #{file}...\n" |
240 | raise RuntimeError, "No such file '#{file}'" unless File.exists?( file ) |
241 | cvsPath = findProgram( 'cvs' ) or |
242 | raise RuntimeError, "Cannot find the 'cvs' program. Aborting." |
243 | |
244 | output = %x{#{cvsPath} log #{file}} |
245 | release = [ 0, 0 ] |
246 | output.scan( /RELEASE_(\d+)_(\d+)/ ) {|match| |
247 | if $1.to_i > release[0] || $2.to_i > release[1] |
248 | release = [ $1.to_i, $2.to_i ] |
249 | replaceMessage( "Found %d.%02d...\n" % release ) |
250 | end |
251 | } |
252 | |
253 | if release[1] >= 99 |
254 | release[0] += 1 |
255 | release[1] = 1 |
256 | else |
257 | release[1] += 1 |
258 | end |
259 | |
260 | return "%d.%02d" % release |
261 | end |
262 | |
263 | |
264 | ### Write a new manifest file with the given +named+, moving any current one |
265 | ### aside with an ".old" suffix if +backup+ is true. |
266 | def makeManifest( name="MANIFEST", backup=true ) |
267 | message "Making manifest file '#{name}'" |
268 | |
269 | # Move an old one aside if a backup is desired |
270 | if backup and File::exists?( name ) |
271 | File::rename( name, name + ".old" ) |
272 | end |
273 | |
274 | File::open( name, File::WRONLY|File::TRUNC|File::CREAT ) {|ofh| |
275 | Find::find( "." ) do |file| |
276 | Find.prune if AMRegexp =~ file |
277 | Find.prune if %r{/\.} =~ file |
278 | Find.prune if /TEMPLATE/ =~ file |
279 | next if File::directory?( file ) |
280 | |
281 | ofh.puts file |
282 | end |
283 | } |
284 | end |
285 | |
286 | |
287 | ### Read the specified <tt>manifestFile</tt>, which is a text file |
288 | ### describing which files to package up for a distribution. The manifest |
289 | ### should consist of one or more lines, each containing one filename or |
290 | ### shell glob pattern. |
291 | def readManifest( manifestFile="MANIFEST" ) |
292 | message "Reading manifest..." |
293 | raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile |
294 | |
295 | manifest = IO::readlines( manifestFile ).collect {|line| |
296 | line.chomp |
297 | }.select {|line| |
298 | line !~ /^(\s*(#.*)?)?$/ |
299 | } |
300 | |
301 | filelist = [] |
302 | for pat in manifest |
303 | $stderr.puts "Adding files that match '#{pat}' to the file list" if $VERBOSE |
304 | filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)} |
305 | end |
306 | |
307 | message "found #{filelist.length} files.\n" |
308 | return filelist |
309 | end |
310 | |
311 | |
312 | ### Given a <tt>filelist</tt> like that returned by #readManifest, remove |
313 | ### the entries therein which match the Regexp objects in the given |
314 | ### <tt>antimanifest</tt> and return the resultant Array. |
315 | def vetManifest( filelist, antimanifest=ANITMANIFEST ) |
316 | origLength = filelist.length |
317 | message "Vetting manifest..." |
318 | |
319 | for regex in antimanifest |
320 | if $VERBOSE |
321 | message "\n\tPattern /#{regex.source}/ removed: " + |
322 | filelist.find_all {|file| regex.match(file)}.join(', ') |
323 | end |
324 | filelist.delete_if {|file| regex.match(file)} |
325 | end |
326 | |
327 | message "removed #{origLength - filelist.length} files from the list.\n" |
328 | return filelist |
329 | end |
330 | |
331 | ### Combine a call to #readManifest with one to #vetManifest. |
332 | def getVettedManifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST ) |
333 | vetManifest( readManifest(manifestFile), antimanifest ) |
334 | end |
335 | |
336 | ### Given a documentation <tt>catalogFile</tt>, extract the title, if |
337 | ### available, and return it. Otherwise generate a title from the name of |
338 | ### the CVS module. |
339 | def findRdocTitle( catalogFile="docs/CATALOG" ) |
340 | |
341 | # Try extracting it from the CATALOG file from a line that looks like: |
342 | # Title: Foo Bar Module |
343 | title = findCatalogKeyword( 'title', catalogFile ) |
344 | |
345 | # If that doesn't work for some reason, try grabbing the name of the CVS |
346 | # repository the directory belongs to. |
347 | if title.nil? && File::directory?( "CVS" ) && |
348 | File::exists?( "CVS/Repository" ) |
349 | title = File::read( "CVS/Repository" ).chomp |
350 | end |
351 | |
352 | # As a last resort, use the name of the project directory |
353 | if title.nil? |
354 | distdir = File::dirname( __FILE__ ) |
355 | distdir = File::dirname( distdir ) if /docs$/ =~ distdir |
356 | title = File::basename( distdir ) |
357 | end |
358 | |
359 | return title |
360 | end |
361 | |
362 | ### Given a documentation <tt>catalogFile</tt>, extract the name of the file |
363 | ### to use as the initally displayed page. If extraction fails, the |
364 | ### +default+ will be used if it exists. Returns +nil+ if there is no main |
365 | ### file to be found. |
366 | def findRdocMain( catalogFile="docs/CATALOG", default="README" ) |
367 | |
368 | # Try extracting it from the CATALOG file from a line that looks like: |
369 | # Main: Foo Bar Module |
370 | main = findCatalogKeyword( 'main', catalogFile ) |
371 | |
372 | # Try to make some educated guesses if that doesn't work |
373 | if main.nil? |
374 | basedir = File::dirname( __FILE__ ) |
375 | basedir = File::dirname( basedir ) if /docs$/ =~ basedir |
376 | |
377 | if File::exists?( File::join(basedir, default) ) |
378 | main = default |
379 | end |
380 | end |
381 | |
382 | return main |
383 | end |
384 | |
385 | |
386 | ### Given a documentation <tt>catalogFile</tt>, extract an upload URL for |
387 | ### RDoc. |
388 | def findRdocUpload( catalogFile="docs/CATALOG" ) |
389 | findCatalogKeyword( 'upload', catalogFile ) |
390 | end |
391 | |
392 | |
393 | ### Given a documentation <tt>catalogFile</tt>, extract a CVS web frontend |
394 | ### URL for RDoc. |
395 | def findRdocCvsURL( catalogFile="docs/CATALOG" ) |
396 | findCatalogKeyword( 'webcvs', catalogFile ) |
397 | end |
398 | |
399 | |
400 | ### Given a documentation <tt>catalogFile</tt>, try extracting the given |
401 | ### +keyword+'s value from it. Keywords are lines that look like: |
402 | ### # <keyword>: <value> |
403 | ### Returns +nil+ if the catalog file was unreadable or didn't contain the |
404 | ### specified +keyword+. |
405 | def findCatalogKeyword( keyword, catalogFile="docs/CATALOG" ) |
406 | val = nil |
407 | |
408 | if File::exists? catalogFile |
409 | message "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile |
410 | File::foreach( catalogFile ) {|line| |
411 | debugMsg( "Examining line #{line.inspect}..." ) |
412 | val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i =~ line |
413 | } |
414 | end |
415 | |
416 | return val |
417 | end |
418 | |
419 | |
420 | ### Given a documentation <tt>catalogFile</tt>, which is in the same format |
421 | ### as that described by #readManifest, read and expand it, and then return |
422 | ### a list of those files which appear to have RDoc documentation in |
423 | ### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST |
424 | ### file is used instead. |
425 | def findRdocableFiles( catalogFile="docs/CATALOG" ) |
426 | startlist = [] |
427 | if File.exists? catalogFile |
428 | message "Using CATALOG file (%s).\n" % catalogFile |
429 | startlist = getVettedManifest( catalogFile ) |
430 | else |
431 | message "Using default MANIFEST\n" |
432 | startlist = getVettedManifest() |
433 | end |
434 | |
435 | message "Looking for RDoc comments in:\n" if $VERBOSE |
436 | startlist.select {|fn| |
437 | message " #{fn}: " if $VERBOSE |
438 | found = false |
439 | File::open( fn, "r" ) {|fh| |
440 | fh.each {|line| |
441 | if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*} |
442 | found = true |
443 | break |
444 | end |
445 | } |
446 | } |
447 | |
448 | message( (found ? "yes" : "no") + "\n" ) if $VERBOSE |
449 | found |
450 | } |
451 | end |
452 | |
453 | ### Open a file and filter each of its lines through the given block a |
454 | ### <tt>line</tt> at a time. The return value of the block is used as the |
455 | ### new line, or omitted if the block returns <tt>nil</tt> or |
456 | ### <tt>false</tt>. |
457 | def editInPlace( file ) # :yields: line |
458 | raise "No block specified for editing operation" unless block_given? |
459 | |
460 | tempName = "#{file}.#{$$}" |
461 | File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile| |
462 | File::unlink( tempName ) |
463 | File::open( file, File::RDONLY ) {|fh| |
464 | fh.each {|line| |
465 | newline = yield( line ) or next |
466 | tempfile.print( newline ) |
467 | } |
468 | } |
469 | |
470 | tempfile.seek(0) |
471 | |
472 | File::open( file, File::TRUNC|File::WRONLY, 0644 ) {|newfile| |
473 | newfile.print( tempfile.read ) |
474 | } |
475 | } |
476 | end |
477 | |
478 | ### Execute the specified shell <tt>command</tt>, read the results, and |
479 | ### return them. Like a %x{} that returns an Array instead of a String. |
480 | def shellCommand( *command ) |
481 | raise "Empty command" if command.empty? |
482 | |
483 | cmdpipe = IO::popen( command.join(' '), 'r' ) |
484 | return cmdpipe.readlines |
485 | end |
486 | |
487 | ### Execute a block with $VERBOSE set to +false+, restoring it to its |
488 | ### previous value before returning. |
489 | def verboseOff |
490 | raise LocalJumpError, "No block given" unless block_given? |
491 | |
492 | thrcrit = Thread.critical |
493 | oldverbose = $VERBOSE |
494 | begin |
495 | Thread.critical = true |
496 | $VERBOSE = false |
497 | yield |
498 | ensure |
499 | $VERBOSE = oldverbose |
500 | Thread.critical = false |
501 | end |
502 | end |
503 | |
504 | |
505 | ### Try the specified code block, printing the given |
506 | def try( msg, bind=nil ) |
507 | result = nil |
508 | if msg =~ /^to\s/ |
509 | message = "Trying #{msg}..." |
510 | else |
511 | message = msg |
512 | end |
513 | |
514 | begin |
515 | rval = nil |
516 | if block_given? |
517 | rval = yield |
518 | else |
519 | file, line = caller(1)[0].split(/:/,2) |
520 | rval = eval( msg, bind, file, line.to_i ) |
521 | end |
522 | |
523 | result = rval.inspect |
524 | rescue Exception => err |
525 | if err.backtrace |
526 | nicetrace = err.backtrace.delete_if {|frame| |
527 | /in `(try|eval)'/ =~ frame |
528 | }.join("\n\t") |
529 | else |
530 | nicetrace = "Exception had no backtrace" |
531 | end |
532 | |
533 | result = err.message + "\n\t" + nicetrace |
534 | ensure |
535 | puts result |
536 | end |
537 | end |
538 | |
539 | def time |
540 | start = Time::now |
541 | stimes = Process::times |
542 | rval = yield |
543 | etimes = Process::times |
544 | $stderr.puts "Time elapsed: %0.5f user, %0.5f system (%0.5f wall clock seconds)" % [ |
545 | etimes.utime - stimes.utime, |
546 | etimes.stime - stimes.stime, |
547 | Time::now.to_f - start.to_f, |
548 | ] |
549 | |
550 | return rval |
551 | end |
552 | |
553 | end |