|
1 # |
|
2 # Install/distribution utility functions |
|
3 # $Id$ |
|
4 # |
|
5 # Copyright (c) 2001-2008, The FaerieMUD Consortium. |
|
6 # |
|
7 # All rights reserved. |
|
8 # |
|
9 # Redistribution and use in source and binary forms, with or without modification, are |
|
10 # permitted provided that the following conditions are met: |
|
11 # |
|
12 # * Redistributions of source code must retain the above copyright notice, this |
|
13 # list of conditions and the following disclaimer. |
|
14 # |
|
15 # * Redistributions in binary form must reproduce the above copyright notice, this |
|
16 # list of conditions and the following disclaimer in the documentation and/or |
|
17 # other materials provided with the distribution. |
|
18 # |
|
19 # * Neither the name of LAIKA, nor the names of its contributors may be used to |
|
20 # endorse or promote products derived from this software without specific prior |
|
21 # written permission. |
|
22 # |
|
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
26 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|
27 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|
28 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|
29 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
30 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
31 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
32 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
33 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
34 # |
|
35 |
|
36 BEGIN { |
|
37 require 'rbconfig' |
|
38 require 'uri' |
|
39 require 'find' |
|
40 require 'pp' |
|
41 require 'irb' |
|
42 |
|
43 begin |
|
44 require 'readline' |
|
45 include Readline |
|
46 rescue LoadError => e |
|
47 $stderr.puts "Faking readline..." |
|
48 def readline( prompt ) |
|
49 $stderr.print prompt.chomp |
|
50 return $stdin.gets.chomp |
|
51 end |
|
52 end |
|
53 |
|
54 } |
|
55 |
|
56 |
|
57 ### Command-line utility functions |
|
58 module UtilityFunctions |
|
59 include Config |
|
60 |
|
61 # The list of regexen that eliminate files from the MANIFEST |
|
62 ANTIMANIFEST = [ |
|
63 /makedist\.rb/, |
|
64 /\bCVS\b/, |
|
65 /~$/, |
|
66 /^#/, |
|
67 %r{docs/html}, |
|
68 %r{docs/man}, |
|
69 /\bTEMPLATE\.\w+\.tpl\b/, |
|
70 /\.cvsignore/, |
|
71 /\.s?o$/, |
|
72 ] |
|
73 |
|
74 # Set some ANSI escape code constants (Shamelessly stolen from Perl's |
|
75 # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com> |
|
76 AnsiAttributes = { |
|
77 'clear' => 0, |
|
78 'reset' => 0, |
|
79 'bold' => 1, |
|
80 'dark' => 2, |
|
81 'underline' => 4, |
|
82 'underscore' => 4, |
|
83 'blink' => 5, |
|
84 'reverse' => 7, |
|
85 'concealed' => 8, |
|
86 |
|
87 'black' => 30, 'on_black' => 40, |
|
88 'red' => 31, 'on_red' => 41, |
|
89 'green' => 32, 'on_green' => 42, |
|
90 'yellow' => 33, 'on_yellow' => 43, |
|
91 'blue' => 34, 'on_blue' => 44, |
|
92 'magenta' => 35, 'on_magenta' => 45, |
|
93 'cyan' => 36, 'on_cyan' => 46, |
|
94 'white' => 37, 'on_white' => 47 |
|
95 } |
|
96 |
|
97 ErasePreviousLine = "\033[A\033[K" |
|
98 |
|
99 ManifestHeader = (<<-"EOF").gsub( /^\t+/, '' ) |
|
100 # |
|
101 # Distribution Manifest |
|
102 # Created: #{Time::now.to_s} |
|
103 # |
|
104 |
|
105 EOF |
|
106 |
|
107 ############### |
|
108 module_function |
|
109 ############### |
|
110 |
|
111 # Create a string that contains the ANSI codes specified and return it |
|
112 def ansi_code( *attributes ) |
|
113 attributes.flatten! |
|
114 # $stderr.puts "Returning ansicode for TERM = %p: %p" % |
|
115 # [ ENV['TERM'], attributes ] |
|
116 return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM'] |
|
117 attributes = AnsiAttributes.values_at( *attributes ).compact.join(';') |
|
118 |
|
119 # $stderr.puts " attr is: %p" % [attributes] |
|
120 if attributes.empty? |
|
121 return '' |
|
122 else |
|
123 return "\e[%sm" % attributes |
|
124 end |
|
125 end |
|
126 |
|
127 |
|
128 ### Colorize the given +string+ with the specified +attributes+ and return it, handling line-endings, etc. |
|
129 def colorize( string, *attributes ) |
|
130 ending = string[/(\s)$/] || '' |
|
131 string = string.rstrip |
|
132 return ansi_code( attributes.flatten ) + string + ansi_code( 'reset' ) + ending |
|
133 end |
|
134 |
|
135 |
|
136 # Test for the presence of the specified <tt>library</tt>, and output a |
|
137 # message describing the test using <tt>nicename</tt>. If <tt>nicename</tt> |
|
138 # is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default. |
|
139 def test_for_library( library, nicename=nil, progress=false ) |
|
140 nicename ||= library |
|
141 message( "Testing for the #{nicename} library..." ) if progress |
|
142 if $LOAD_PATH.detect {|dir| |
|
143 File.exists?(File.join(dir,"#{library}.rb")) || |
|
144 File.exists?(File.join(dir,"#{library}.#{CONFIG['DLEXT']}")) |
|
145 } |
|
146 message( "found.\n" ) if progress |
|
147 return true |
|
148 else |
|
149 message( "not found.\n" ) if progress |
|
150 return false |
|
151 end |
|
152 end |
|
153 |
|
154 # Test for the presence of the specified <tt>library</tt>, and output a |
|
155 # message describing the problem using <tt>nicename</tt>. If |
|
156 # <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used |
|
157 # to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are |
|
158 # specified, they are also use to build a message describing how to find the |
|
159 # required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library |
|
160 # will cause the program to abort. |
|
161 def test_for_required_library( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true ) |
|
162 nicename ||= library |
|
163 unless test_for_library( library, nicename ) |
|
164 msgs = [ "You are missing the required #{nicename} library.\n" ] |
|
165 msgs << "RAA: #{raaUrl}\n" if raaUrl |
|
166 msgs << "Download: #{downloadUrl}\n" if downloadUrl |
|
167 if fatal |
|
168 abort msgs.join('') |
|
169 else |
|
170 error_message msgs.join('') |
|
171 end |
|
172 end |
|
173 return true |
|
174 end |
|
175 |
|
176 ### Output <tt>msg</tt> as a ANSI-colored program/section header (white on |
|
177 ### blue). |
|
178 def header( msg ) |
|
179 msg.chomp! |
|
180 $stderr.puts ansi_code( 'bold', 'white', 'on_blue' ) + msg + ansi_code( 'reset' ) |
|
181 $stderr.flush |
|
182 end |
|
183 |
|
184 ### Output <tt>msg</tt> to STDERR and flush it. |
|
185 def message( *msgs ) |
|
186 $stderr.print( msgs.join("\n") ) |
|
187 $stderr.flush |
|
188 end |
|
189 |
|
190 ### Output +msg+ to STDERR and flush it if $VERBOSE is true. |
|
191 def verbose_msg( msg ) |
|
192 msg.chomp! |
|
193 message( msg + "\n" ) if $VERBOSE |
|
194 end |
|
195 |
|
196 ### Output the specified <tt>msg</tt> as an ANSI-colored error message |
|
197 ### (white on red). |
|
198 def error_msg( msg ) |
|
199 message ansi_code( 'bold', 'white', 'on_red' ) + msg + ansi_code( 'reset' ) |
|
200 end |
|
201 alias :error_message :error_msg |
|
202 |
|
203 ### Output the specified <tt>msg</tt> as an ANSI-colored debugging message |
|
204 ### (yellow on blue). |
|
205 def debug_msg( msg ) |
|
206 return unless $DEBUG |
|
207 msg.chomp! |
|
208 $stderr.puts ansi_code( 'yellow' ) + ">>> #{msg}" + ansi_code( 'reset' ) |
|
209 $stderr.flush |
|
210 end |
|
211 |
|
212 ### Erase the previous line (if supported by your terminal) and output the |
|
213 ### specified <tt>msg</tt> instead. |
|
214 def replace_msg( msg ) |
|
215 $stderr.puts |
|
216 $stderr.print ErasePreviousLine |
|
217 message( msg ) |
|
218 end |
|
219 alias :replace_message :replace_msg |
|
220 |
|
221 ### Output a divider made up of <tt>length</tt> hyphen characters. |
|
222 def divider( length=75 ) |
|
223 $stderr.puts "\r" + ("-" * length ) |
|
224 end |
|
225 alias :writeLine :divider |
|
226 |
|
227 |
|
228 ### Output the specified <tt>msg</tt> colored in ANSI red and exit with a |
|
229 ### status of 1. |
|
230 def abort( msg ) |
|
231 print ansi_code( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansi_code( 'reset' ) + "\n\n" |
|
232 Kernel.exit!( 1 ) |
|
233 end |
|
234 |
|
235 |
|
236 ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and |
|
237 ### return the user's input with leading and trailing spaces removed. If a |
|
238 ### test is provided, the prompt will repeat until the test returns true. |
|
239 ### An optional failure message can also be passed in. |
|
240 def prompt( prompt_string, failure_msg="Try again." ) # :yields: response |
|
241 prompt_string.chomp! |
|
242 prompt_string << ":" unless /\W$/.match( prompt_string ) |
|
243 response = nil |
|
244 |
|
245 begin |
|
246 response = readline( ansi_code('bold', 'green') + |
|
247 "#{prompt_string} " + ansi_code('reset') ) || '' |
|
248 response.strip! |
|
249 if block_given? && ! yield( response ) |
|
250 error_message( failure_msg + "\n\n" ) |
|
251 response = nil |
|
252 end |
|
253 end until response |
|
254 |
|
255 return response |
|
256 end |
|
257 |
|
258 |
|
259 ### Prompt the user with the given <tt>prompt_string</tt> via #prompt, |
|
260 ### substituting the given <tt>default</tt> if the user doesn't input |
|
261 ### anything. If a test is provided, the prompt will repeat until the test |
|
262 ### returns true. An optional failure message can also be passed in. |
|
263 def prompt_with_default( prompt_string, default, failure_msg="Try again." ) |
|
264 response = nil |
|
265 |
|
266 begin |
|
267 response = prompt( "%s [%s]" % [ prompt_string, default ] ) |
|
268 response = default if response.empty? |
|
269 |
|
270 if block_given? && ! yield( response ) |
|
271 error_message( failure_msg + "\n\n" ) |
|
272 response = nil |
|
273 end |
|
274 end until response |
|
275 |
|
276 return response |
|
277 end |
|
278 |
|
279 |
|
280 $programs = {} |
|
281 |
|
282 ### Search for the program specified by the given <tt>progname</tt> in the |
|
283 ### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if |
|
284 ### no such program is in the path. |
|
285 def find_program( progname ) |
|
286 unless $programs.key?( progname ) |
|
287 ENV['PATH'].split(File::PATH_SEPARATOR).each {|d| |
|
288 file = File.join( d, progname ) |
|
289 if File.executable?( file ) |
|
290 $programs[ progname ] = file |
|
291 break |
|
292 end |
|
293 } |
|
294 end |
|
295 |
|
296 return $programs[ progname ] |
|
297 end |
|
298 |
|
299 |
|
300 ### Search for the release version for the project in the specified |
|
301 ### +directory+. |
|
302 def extract_version( directory='.' ) |
|
303 release = nil |
|
304 |
|
305 Dir::chdir( directory ) do |
|
306 if File::directory?( "CVS" ) |
|
307 verbose_msg( "Project is versioned via CVS. Searching for RELEASE_*_* tags..." ) |
|
308 |
|
309 if (( cvs = find_program('cvs') )) |
|
310 revs = [] |
|
311 output = %x{cvs log} |
|
312 output.scan( /RELEASE_(\d+(?:_\d\w+)*)/ ) {|match| |
|
313 rev = $1.split(/_/).collect {|s| Integer(s) rescue 0} |
|
314 verbose_msg( "Found %s...\n" % rev.join('.') ) |
|
315 revs << rev |
|
316 } |
|
317 |
|
318 release = revs.sort.last |
|
319 end |
|
320 |
|
321 elsif File::directory?( '.svn' ) |
|
322 verbose_msg( "Project is versioned via Subversion" ) |
|
323 |
|
324 if (( svn = find_program('svn') )) |
|
325 output = %x{svn pg project-version}.chomp |
|
326 unless output.empty? |
|
327 verbose_msg( "Using 'project-version' property: %p" % output ) |
|
328 release = output.split( /[._]/ ).collect {|s| Integer(s) rescue 0} |
|
329 end |
|
330 end |
|
331 end |
|
332 end |
|
333 |
|
334 return release |
|
335 end |
|
336 |
|
337 |
|
338 ### Find the current release version for the project in the specified |
|
339 ### +directory+ and return its successor. |
|
340 def extract_next_version( directory='.' ) |
|
341 version = extract_version( directory ) || [0,0,0] |
|
342 version.compact! |
|
343 version[-1] += 1 |
|
344 |
|
345 return version |
|
346 end |
|
347 |
|
348 |
|
349 # Pattern for extracting the name of the project from a Subversion URL |
|
350 SVNUrlPath = %r{ |
|
351 .*/ # Skip all but the last bit |
|
352 ([^/]+) # $1 = project name |
|
353 / # Followed by / + |
|
354 (?: |
|
355 trunk | # 'trunk' |
|
356 ( |
|
357 branches | # ...or branches/branch-name |
|
358 tags # ...or tags/tag-name |
|
359 )/\w |
|
360 ) |
|
361 $ # bound to the end |
|
362 }ix |
|
363 |
|
364 ### Extract the project name (CVS Repository name) for the given +directory+. |
|
365 def extract_project_name( directory='.' ) |
|
366 name = nil |
|
367 |
|
368 Dir::chdir( directory ) do |
|
369 |
|
370 # CVS-controlled |
|
371 if File::directory?( "CVS" ) |
|
372 verbose_msg( "Project is versioned via CVS. Using repository name." ) |
|
373 name = File.open( "CVS/Repository", "r").readline.chomp |
|
374 name.sub!( %r{.*/}, '' ) |
|
375 |
|
376 # Subversion-controlled |
|
377 elsif File::directory?( '.svn' ) |
|
378 verbose_msg( "Project is versioned via Subversion" ) |
|
379 |
|
380 # If the machine has the svn tool, try to get the project name |
|
381 if (( svn = find_program( 'svn' ) )) |
|
382 |
|
383 # First try an explicit property |
|
384 output = shell_command( svn, 'pg', 'project-name' ) |
|
385 if !output.empty? |
|
386 verbose_msg( "Using 'project-name' property: %p" % output ) |
|
387 name = output.first.chomp |
|
388 |
|
389 # If that doesn't work, try to figure it out from the URL |
|
390 elsif (( uri = get_svn_uri() )) |
|
391 name = uri.path.sub( SVNUrlPath ) { $1 } |
|
392 end |
|
393 end |
|
394 end |
|
395 |
|
396 # Fall back to guessing based on the directory name |
|
397 unless name |
|
398 name = File::basename(File::dirname( File::expand_path(__FILE__) )) |
|
399 end |
|
400 end |
|
401 |
|
402 return name |
|
403 end |
|
404 |
|
405 |
|
406 ### Extract the Subversion URL from the specified directory and return it as |
|
407 ### a URI object. |
|
408 def get_svn_uri( directory='.' ) |
|
409 uri = nil |
|
410 |
|
411 Dir::chdir( directory ) do |
|
412 output = %x{svn info} |
|
413 debug_msg( "Using info: %p" % output ) |
|
414 |
|
415 if /^URL: \s* ( .* )/xi.match( output ) |
|
416 uri = URI::parse( $1 ) |
|
417 end |
|
418 end |
|
419 |
|
420 return uri |
|
421 end |
|
422 |
|
423 |
|
424 ### (Re)make a manifest file in the specified +path+. |
|
425 def make_manifest( path="MANIFEST" ) |
|
426 if File::exists?( path ) |
|
427 reply = prompt_with_default( "Replace current '#{path}'? [yN]", "n" ) |
|
428 return false unless /^y/i.match( reply ) |
|
429 |
|
430 verbose_msg "Replacing manifest at '#{path}'" |
|
431 else |
|
432 verbose_msg "Creating new manifest at '#{path}'" |
|
433 end |
|
434 |
|
435 files = [] |
|
436 verbose_msg( "Finding files...\n" ) |
|
437 Find::find( Dir::pwd ) do |f| |
|
438 Find::prune if File::directory?( f ) && |
|
439 /^\./.match( File::basename(f) ) |
|
440 verbose_msg( " found: #{f}\n" ) |
|
441 files << f.sub( %r{^#{Dir::pwd}/?}, '' ) |
|
442 end |
|
443 files = vet_manifest( files ) |
|
444 |
|
445 verbose_msg( "Writing new manifest to #{path}..." ) |
|
446 File::open( path, File::WRONLY|File::CREAT|File::TRUNC ) do |ofh| |
|
447 ofh.puts( ManifestHeader ) |
|
448 ofh.puts( files ) |
|
449 end |
|
450 verbose_msg( "done." ) |
|
451 end |
|
452 |
|
453 |
|
454 ### Read the specified <tt>manifestFile</tt>, which is a text file |
|
455 ### describing which files to package up for a distribution. The manifest |
|
456 ### should consist of one or more lines, each containing one filename or |
|
457 ### shell glob pattern. |
|
458 def read_manifest( manifestFile="MANIFEST" ) |
|
459 verbose_msg "Building manifest..." |
|
460 raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile |
|
461 |
|
462 manifest = IO::readlines( manifestFile ).collect {|line| |
|
463 line.chomp |
|
464 }.select {|line| |
|
465 line !~ /^(\s*(#.*)?)?$/ |
|
466 } |
|
467 |
|
468 filelist = [] |
|
469 for pat in manifest |
|
470 verbose_msg "Adding files that match '#{pat}' to the file list" |
|
471 filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)} |
|
472 end |
|
473 |
|
474 verbose_msg "found #{filelist.length} files.\n" |
|
475 return filelist |
|
476 end |
|
477 |
|
478 |
|
479 ### Given a <tt>filelist</tt> like that returned by #read_manifest, remove |
|
480 ### the entries therein which match the Regexp objects in the given |
|
481 ### <tt>antimanifest</tt> and return the resultant Array. |
|
482 def vet_manifest( filelist, antimanifest=ANTIMANIFEST ) |
|
483 origLength = filelist.length |
|
484 verbose_msg "Vetting manifest..." |
|
485 |
|
486 for regex in antimanifest |
|
487 verbose_msg "\n\tPattern /#{regex.source}/ removed: " + |
|
488 filelist.find_all {|file| regex.match(file)}.join(', ') |
|
489 filelist.delete_if {|file| regex.match(file)} |
|
490 end |
|
491 |
|
492 verbose_msg "removed #{origLength - filelist.length} files from the list.\n" |
|
493 return filelist |
|
494 end |
|
495 |
|
496 |
|
497 ### Combine a call to #read_manifest with one to #vet_manifest. |
|
498 def get_vetted_manifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST ) |
|
499 vet_manifest( read_manifest(manifestFile), antimanifest ) |
|
500 end |
|
501 |
|
502 |
|
503 ### Given a documentation <tt>catalogFile</tt>, extract the title, if |
|
504 ### available, and return it. Otherwise generate a title from the name of |
|
505 ### the CVS module. |
|
506 def find_rdoc_title( catalogFile="docs/CATALOG" ) |
|
507 |
|
508 # Try extracting it from the CATALOG file from a line that looks like: |
|
509 # Title: Foo Bar Module |
|
510 title = find_catalog_keyword( 'title', catalogFile ) |
|
511 |
|
512 # If that doesn't work for some reason, use the name of the project. |
|
513 title = extract_project_name() |
|
514 |
|
515 return title |
|
516 end |
|
517 |
|
518 |
|
519 ### Given a documentation <tt>catalogFile</tt>, extract the name of the file |
|
520 ### to use as the initally displayed page. If extraction fails, the |
|
521 ### +default+ will be used if it exists. Returns +nil+ if there is no main |
|
522 ### file to be found. |
|
523 def find_rdoc_main( catalogFile="docs/CATALOG", default="README" ) |
|
524 |
|
525 # Try extracting it from the CATALOG file from a line that looks like: |
|
526 # Main: Foo Bar Module |
|
527 main = find_catalog_keyword( 'main', catalogFile ) |
|
528 |
|
529 # Try to make some educated guesses if that doesn't work |
|
530 if main.nil? |
|
531 basedir = File::dirname( __FILE__ ) |
|
532 basedir = File::dirname( basedir ) if /docs$/ =~ basedir |
|
533 |
|
534 if File::exists?( File::join(basedir, default) ) |
|
535 main = default |
|
536 end |
|
537 end |
|
538 |
|
539 return main |
|
540 end |
|
541 |
|
542 |
|
543 ### Given a documentation <tt>catalogFile</tt>, extract an upload URL for |
|
544 ### RDoc. |
|
545 def find_rdoc_upload( catalogFile="docs/CATALOG" ) |
|
546 find_catalog_keyword( 'upload', catalogFile ) |
|
547 end |
|
548 |
|
549 |
|
550 ### Given a documentation <tt>catalogFile</tt>, extract a CVS web frontend |
|
551 ### URL for RDoc. |
|
552 def find_rdoc_cvs_url( catalogFile="docs/CATALOG" ) |
|
553 find_catalog_keyword( 'webcvs', catalogFile ) |
|
554 end |
|
555 |
|
556 |
|
557 ### Find one or more 'accessor' directives in the catalog if they exist and |
|
558 ### return an Array of them. |
|
559 def find_rdoc_accessors( catalogFile="docs/CATALOG" ) |
|
560 accessors = [] |
|
561 in_attr_section = false |
|
562 indent = '' |
|
563 |
|
564 if File::exists?( catalogFile ) |
|
565 verbose_msg "Extracting accessors from CATALOG file (%s).\n" % catalogFile |
|
566 |
|
567 # Read lines from the catalog |
|
568 File::foreach( catalogFile ) do |line| |
|
569 debug_msg( " Examining line #{line.inspect}..." ) |
|
570 |
|
571 # Multi-line accessors |
|
572 if in_attr_section |
|
573 if /^#\s+([a-z0-9_]+(?:\s*=\s*.*)?)$/i.match( line ) |
|
574 debug_msg( " Found accessor: #$1" ) |
|
575 accessors << $1 |
|
576 next |
|
577 end |
|
578 |
|
579 debug_msg( " End of accessors section." ) |
|
580 in_attr_section = false |
|
581 |
|
582 # Single-line accessor |
|
583 elsif /^#\s*Accessors:\s*(\S+)$/i.match( line ) |
|
584 debug_msg( " Found single accessors line: #$1" ) |
|
585 vals = $1.split(/,/).collect {|val| val.strip } |
|
586 accessors.replace( vals ) |
|
587 |
|
588 # Multi-line accessor header |
|
589 elsif /^#\s*Accessors:\s*$/i.match( line ) |
|
590 debug_msg( " Start of accessors section." ) |
|
591 in_attr_section = true |
|
592 end |
|
593 |
|
594 end |
|
595 end |
|
596 |
|
597 debug_msg( "Found accessors: %s" % accessors.join(",") ) |
|
598 return accessors |
|
599 end |
|
600 |
|
601 |
|
602 ### Given a documentation <tt>catalogFile</tt>, try extracting the given |
|
603 ### +keyword+'s value from it. Keywords are lines that look like: |
|
604 ### # <keyword>: <value> |
|
605 ### Returns +nil+ if the catalog file was unreadable or didn't contain the |
|
606 ### specified +keyword+. |
|
607 def find_catalog_keyword( keyword, catalogFile="docs/CATALOG" ) |
|
608 val = nil |
|
609 |
|
610 if File::exists? catalogFile |
|
611 verbose_msg "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile |
|
612 File::foreach( catalogFile ) do |line| |
|
613 debug_msg( "Examining line #{line.inspect}..." ) |
|
614 val = $1.strip and break if /^#\s*#{keyword}:\s*(.*)$/i.match( line ) |
|
615 end |
|
616 end |
|
617 |
|
618 return val |
|
619 end |
|
620 |
|
621 |
|
622 ### Given a documentation <tt>catalogFile</tt>, which is in the same format |
|
623 ### as that described by #read_manifest, read and expand it, and then return |
|
624 ### a list of those files which appear to have RDoc documentation in |
|
625 ### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST |
|
626 ### file is used instead. |
|
627 def find_rdocable_files( catalogFile="docs/CATALOG" ) |
|
628 startlist = [] |
|
629 if File.exists? catalogFile |
|
630 verbose_msg "Using CATALOG file (%s).\n" % catalogFile |
|
631 startlist = get_vetted_manifest( catalogFile ) |
|
632 else |
|
633 verbose_msg "Using default MANIFEST\n" |
|
634 startlist = get_vetted_manifest() |
|
635 end |
|
636 |
|
637 verbose_msg "Looking for RDoc comments in:\n" |
|
638 startlist.select {|fn| |
|
639 verbose_msg " #{fn}: " |
|
640 found = false |
|
641 File::open( fn, "r" ) {|fh| |
|
642 fh.each {|line| |
|
643 if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*} |
|
644 found = true |
|
645 break |
|
646 end |
|
647 } |
|
648 } |
|
649 |
|
650 verbose_msg( (found ? "yes" : "no") + "\n" ) |
|
651 found |
|
652 } |
|
653 end |
|
654 |
|
655 |
|
656 ### Open a file and filter each of its lines through the given block a |
|
657 ### <tt>line</tt> at a time. The return value of the block is used as the |
|
658 ### new line, or omitted if the block returns <tt>nil</tt> or |
|
659 ### <tt>false</tt>. |
|
660 def edit_in_place( file, testMode=false ) # :yields: line |
|
661 raise "No block specified for editing operation" unless block_given? |
|
662 |
|
663 tempName = "#{file}.#{$$}" |
|
664 File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile| |
|
665 File::open( file, File::RDONLY ) {|fh| |
|
666 fh.each {|line| |
|
667 newline = yield( line ) or next |
|
668 tempfile.print( newline ) |
|
669 $stderr.puts "%p -> %p" % [ line, newline ] if |
|
670 line != newline |
|
671 } |
|
672 } |
|
673 } |
|
674 |
|
675 if testMode |
|
676 File::unlink( tempName ) |
|
677 else |
|
678 File::rename( tempName, file ) |
|
679 end |
|
680 end |
|
681 |
|
682 |
|
683 ### Execute the specified shell <tt>command</tt>, read the results, and |
|
684 ### return them. Like a %x{} that returns an Array instead of a String. |
|
685 def shell_command( *command ) |
|
686 raise "Empty command" if command.empty? |
|
687 |
|
688 cmdpipe = IO::popen( command.join(' '), 'r' ) |
|
689 return cmdpipe.readlines |
|
690 end |
|
691 |
|
692 |
|
693 ### Execute a block with $VERBOSE set to +false+, restoring it to its |
|
694 ### previous value before returning. |
|
695 def verbose_off |
|
696 raise LocalJumpError, "No block given" unless block_given? |
|
697 |
|
698 thrcrit = Thread.critical |
|
699 oldverbose = $VERBOSE |
|
700 begin |
|
701 Thread.critical = true |
|
702 $VERBOSE = false |
|
703 yield |
|
704 ensure |
|
705 $VERBOSE = oldverbose |
|
706 Thread.critical = false |
|
707 end |
|
708 end |
|
709 |
|
710 |
|
711 ### Try the specified code block, printing the given |
|
712 def try( msg, bind=TOPLEVEL_BINDING ) |
|
713 result = '' |
|
714 if msg =~ /^to\s/ |
|
715 message "Trying #{msg}...\n" |
|
716 else |
|
717 message msg + "\n" |
|
718 end |
|
719 |
|
720 begin |
|
721 rval = nil |
|
722 if block_given? |
|
723 rval = yield |
|
724 else |
|
725 file, line = caller(1)[0].split(/:/,2) |
|
726 rval = eval( msg, bind, file, line.to_i ) |
|
727 end |
|
728 |
|
729 PP.pp( rval, result ) |
|
730 |
|
731 rescue Exception => err |
|
732 if err.backtrace |
|
733 nicetrace = err.backtrace.delete_if {|frame| |
|
734 /in `(try|eval)'/ =~ frame |
|
735 }.join("\n\t") |
|
736 else |
|
737 nicetrace = "Exception had no backtrace" |
|
738 end |
|
739 |
|
740 result = err.message + "\n\t" + nicetrace |
|
741 |
|
742 ensure |
|
743 divider |
|
744 message result.chomp + "\n" |
|
745 divider |
|
746 $stderr.puts |
|
747 end |
|
748 end |
|
749 |
|
750 |
|
751 ### Start an IRB session with the specified binding +b+ as the current scope. |
|
752 def start_irb_session( b ) |
|
753 IRB.setup(nil) |
|
754 |
|
755 workspace = IRB::WorkSpace.new( b ) |
|
756 |
|
757 if IRB.conf[:SCRIPT] |
|
758 irb = IRB::Irb.new( workspace, IRB.conf[:SCRIPT] ) |
|
759 else |
|
760 irb = IRB::Irb.new( workspace ) |
|
761 end |
|
762 |
|
763 IRB.conf[:IRB_RC].call( irb.context ) if IRB.conf[:IRB_RC] |
|
764 IRB.conf[:MAIN_CONTEXT] = irb.context |
|
765 |
|
766 trap("SIGINT") do |
|
767 irb.signal_handle |
|
768 end |
|
769 |
|
770 catch(:IRB_EXIT) do |
|
771 irb.eval_input |
|
772 end |
|
773 end |
|
774 |
|
775 end # module UtilityFunctions |
|
776 |
|
777 |
|
778 |
|
779 if __FILE__ == $0 |
|
780 # $DEBUG = true |
|
781 include UtilityFunctions |
|
782 |
|
783 projname = extract_project_name() |
|
784 header "Project: #{projname}" |
|
785 |
|
786 ver = extract_version() || [0,0,1] |
|
787 puts "Version: %s\n" % ver.join('.') |
|
788 |
|
789 if File::directory?( "docs" ) |
|
790 puts "Rdoc:", |
|
791 " Title: " + find_rdoc_title(), |
|
792 " Main: " + find_rdoc_main(), |
|
793 " Upload: " + find_rdoc_upload(), |
|
794 " SCCS URL: " + find_rdoc_cvs_url(), |
|
795 " Accessors: " + find_rdoc_accessors().join(",") |
|
796 end |
|
797 |
|
798 puts "Manifest:", |
|
799 " " + get_vetted_manifest().join("\n ") |
|
800 end |