root/tags/ALPHA/utils.rb

Revision 1, 10.7 KB (checked in by deveiant, 6 years ago)

Initial revision

Line 
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
13BEGIN {
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
26module 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
365end
Note: See TracBrowser for help on using the browser.