Changeset 237

Show
Ignore:
Timestamp:
06/20/05 08:56:59 (3 years ago)
Author:
ged
Message:

- Synced with the project-utils version.

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • trunk/utils.rb

    r233 r237  
    1010# 
    1111 
    12  
    1312BEGIN { 
     13    require 'rbconfig' 
     14    require 'uri' 
     15    require 'find' 
     16 
    1417    begin 
    1518        require 'readline' 
     
    3235} 
    3336 
    34 require 'rbconfig' 
    35 include Config 
    36 require 'pp' 
    3737 
    3838module UtilityFunctions 
     39    include Config 
    3940 
    4041    # The list of regexen that eliminate files from the MANIFEST 
     
    4647        %r{docs/html}, 
    4748        %r{docs/man}, 
    48         /^TEMPLATE/, 
     49        /\bTEMPLATE\.\w+\.tpl\b/, 
    4950        /\.cvsignore/, 
    50         /\.s?o$/ 
     51        /\.s?o$/, 
    5152    ] 
    5253 
     
    7677    ErasePreviousLine = "\033[A\033[K" 
    7778 
     79    ManifestHeader = (<<-"EOF").gsub( /^\t+/, '' ) 
     80        # 
     81        # Distribution Manifest 
     82        # Created: #{Time::now.to_s} 
     83        #  
     84 
     85    EOF 
    7886 
    7987    ############### 
     
    8391    # Create a string that contains the ANSI codes specified and return it 
    8492    def ansiCode( *attributes ) 
    85         return '' unless /(?:vt10[03]|xterm(?:-color)?|linux)/i =~ ENV['TERM'] 
     93        return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM'] 
    8694        attr = attributes.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';') 
    8795        if attr.empty?  
     
    92100    end 
    93101 
    94     ### Return the given +prompt+ with the specified +attributes+ turned on and 
    95     ### a reset at the end. 
    96     def colored( prompt, *attributes ) 
    97         return ansiCode( *(attributes.flatten) ) + prompt + ansiCode( 'reset' ) 
    98     end 
    99  
    100  
    101102    # Test for the presence of the specified <tt>library</tt>, and output a 
    102103    # message describing the test using <tt>nicename</tt>. If <tt>nicename</tt> 
    103104    # is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default. 
    104     def testForLibrary( library, nicename=nil ) 
     105    def testForLibrary( library, nicename=nil, progress=false ) 
    105106        nicename ||= library 
    106         message( "Testing for the #{nicename} library..." ) 
     107        message( "Testing for the #{nicename} library..." ) if progress 
    107108        if $LOAD_PATH.detect {|dir| 
    108109                File.exists?(File.join(dir,"#{library}.rb")) || 
    109110                File.exists?(File.join(dir,"#{library}.#{CONFIG['DLEXT']}")) 
    110111            } 
    111             message( "found.\n" ) 
     112            message( "found.\n" ) if progress 
    112113            return true 
    113114        else 
    114             message( "not found.\n" ) 
     115            message( "not found.\n" ) if progress 
    115116            return false 
    116117        end 
     
    148149 
    149150    ### Output <tt>msg</tt> to STDERR and flush it. 
    150     def message( msg ) 
    151         $stderr.print ansiCode( 'cyan' ) + msg + ansiCode( 'reset' ) 
     151    def message( *msgs ) 
     152        $stderr.print( msgs.join("\n") ) 
    152153        $stderr.flush 
     154    end 
     155 
     156    ### Output +msg+ to STDERR and flush it if $VERBOSE is true. 
     157    def verboseMsg( msg ) 
     158        msg.chomp! 
     159        message( msg + "\n" ) if $VERBOSE 
    153160    end 
    154161 
     
    181188    alias :writeLine :divider 
    182189 
     190 
    183191    ### Output the specified <tt>msg</tt> colored in ANSI red and exit with a 
    184192    ### status of 1. 
     
    188196    end 
    189197 
     198 
    190199    ### Output the specified <tt>promptString</tt> as a prompt (in green) and 
    191200    ### return the user's input with leading and trailing spaces removed.  If a 
    192201    ### test is provided, the prompt will repeat until the test returns true. 
    193202    ### An optional failure message can also be passed in. 
    194     def prompt( promptString, failure_msg="Try again.", &test ) 
     203    def prompt( promptString, failure_msg="Try again." ) # :yields: response 
    195204        promptString.chomp! 
    196         response = readline( ansiCode('bold', 'green') + 
    197             "#{promptString}: " + ansiCode('reset') ).strip 
    198         until test.call(response) 
    199             errorMessage(failure_msg) 
    200             message("\n") 
    201             response = prompt( promptString ) 
    202         end if test 
     205        promptString << ":" unless /\W$/.match( promptString ) 
     206        response = nil 
     207 
     208        begin 
     209            response = readline( ansiCode('bold', 'green') + 
     210                "#{promptString} " + ansiCode('reset') ) || '' 
     211            response.strip! 
     212            if block_given? && ! yield( response )  
     213                errorMessage( failure_msg + "\n\n" ) 
     214                response = nil 
     215            end 
     216        end until response 
     217 
    203218        return response 
    204219    end 
     220 
    205221 
    206222    ### Prompt the user with the given <tt>promptString</tt> via #prompt, 
     
    208224    ### anything.  If a test is provided, the prompt will repeat until the test 
    209225    ### returns true.  An optional failure message can also be passed in. 
    210     def promptWithDefault( promptString, default, failure_msg="Try again.", &test ) 
    211         response = prompt( "%s [%s]" % [ promptString, default ] ) 
    212         response = default if response.empty? 
    213         until test.call(response) 
    214             errorMessage(faiure_msg) 
    215             message("\n") 
    216             response = promptWithDefault( promptString, default ) 
    217         end if test 
     226    def promptWithDefault( promptString, default, failure_msg="Try again." ) 
     227        response = nil 
     228         
     229        begin 
     230            response = prompt( "%s [%s]" % [ promptString, default ] ) 
     231            response = default if response.empty? 
     232 
     233            if block_given? && ! yield( response )  
     234                errorMessage( failure_msg + "\n\n" ) 
     235                response = nil 
     236            end 
     237        end until response 
     238 
    218239        return response 
    219240    end 
     241 
     242 
     243    $programs = {} 
    220244 
    221245    ### Search for the program specified by the given <tt>progname</tt> in the 
     
    223247    ### no such program is in the path. 
    224248    def findProgram( progname ) 
    225         ENV['PATH'].split(File::PATH_SEPARATOR).each {|d| 
    226             file = File.join( d, progname ) 
    227             return file if File.executable?( file ) 
    228         } 
    229         return nil 
    230     end 
    231  
    232     ### Using the CVS log for the given <tt>file</tt> attempt to guess what the 
    233     ### next release version might be. This only works if releases are tagged 
    234     ### with tags like 'RELEASE_x_y'. 
    235     def extractNextVersionFromTags( file ) 
    236         message "Attempting to extract next release version from CVS tags for #{file}...\n" 
    237         raise RuntimeError, "No such file '#{file}'" unless File.exists?( file ) 
    238         cvsPath = findProgram( 'cvs' ) or 
    239             raise RuntimeError, "Cannot find the 'cvs' program. Aborting." 
    240  
    241         output = %x{#{cvsPath} log #{file}} 
    242         release = [ 0, 0 ] 
    243         output.scan( /RELEASE_(\d+)_(\d+)/ ) {|match| 
    244             if $1.to_i > release[0] || $2.to_i > release[1] 
    245                 release = [ $1.to_i, $2.to_i ] 
    246                 replaceMessage( "Found %d.%02d...\n" % release ) 
    247             end 
    248         } 
    249  
    250         if release[1] >= 99 
    251             release[0] += 1 
    252             release[1] = 1 
     249        unless $programs.key?( progname ) 
     250            ENV['PATH'].split(File::PATH_SEPARATOR).each {|d| 
     251                file = File.join( d, progname ) 
     252                if File.executable?( file ) 
     253                    $programs[ progname ] = file  
     254                    break 
     255                end 
     256            } 
     257        end 
     258 
     259        return $programs[ progname ] 
     260    end 
     261 
     262 
     263    ### Search for the release version for the project in the specified 
     264    ### +directory+. 
     265    def extractVersion( directory='.' ) 
     266        release = nil 
     267 
     268        Dir::chdir( directory ) do 
     269            if File::directory?( "CVS" ) 
     270                verboseMsg( "Project is versioned via CVS. Searching for RELEASE_*_* tags..." ) 
     271 
     272                if (( cvs = findProgram('cvs') )) 
     273                    revs = [] 
     274                    output = %x{cvs log} 
     275                    output.scan( /RELEASE_(\d+(?:_\d\w+)*)/ ) {|match| 
     276                        rev = $1.split(/_/).collect {|s| Integer(s) rescue 0} 
     277                        verboseMsg( "Found %s...\n" % rev.join('.') ) 
     278                        revs << rev 
     279                    } 
     280 
     281                    release = revs.sort.last 
     282                end 
     283 
     284            elsif File::directory?( '.svn' ) 
     285                verboseMsg( "Project is versioned via Subversion" ) 
     286 
     287                if (( svn = findProgram('svn') )) 
     288                    output = %x{svn pg project-version}.chomp 
     289                    unless output.empty? 
     290                        verboseMsg( "Using 'project-version' property: %p" % output ) 
     291                        release = output.split( /[._]/ ).collect {|s| Integer(s) rescue 0} 
     292                    end 
     293                end 
     294            end 
     295        end 
     296 
     297        return release 
     298    end 
     299 
     300 
     301    ### Find the current release version for the project in the specified 
     302    ### +directory+ and return its successor. 
     303    def extractNextVersion( directory='.' ) 
     304        version = extractVersion( directory ) || [0,0,0] 
     305        version.compact! 
     306        version[-1] += 1 
     307 
     308        return version 
     309    end 
     310 
     311 
     312    # Pattern for extracting the name of the project from a Subversion URL 
     313    SVNUrlPath = %r{ 
     314        .*/                     # Skip all but the last bit 
     315        (\w+)                   # $1 = project name 
     316        /                       # Followed by / + 
     317        (?: 
     318            trunk |             # 'trunk' 
     319            ( 
     320                branches |      # ...or branches/branch-name 
     321                tags            # ...or tags/tag-name 
     322            )/\w     
     323        ) 
     324        $                       # bound to the end 
     325    }ix 
     326 
     327    ### Extract the project name (CVS Repository name) for the given +directory+. 
     328    def extractProjectName( directory='.' ) 
     329        name = nil 
     330 
     331        Dir::chdir( directory ) do 
     332 
     333            # CVS-controlled 
     334            if File::directory?( "CVS" ) 
     335                verboseMsg( "Project is versioned via CVS. Using repository name." ) 
     336                name = File.open( "CVS/Repository", "r").readline.chomp 
     337                name.sub!( %r{.*/}, '' ) 
     338 
     339            # Subversion-controlled 
     340            elsif File::directory?( '.svn' ) 
     341                verboseMsg( "Project is versioned via Subversion" ) 
     342 
     343                # If the machine has the svn tool, try to get the project name 
     344                if (( svn = findProgram( 'svn' ) )) 
     345 
     346                    # First try an explicit property 
     347                    output = shellCommand( svn, 'pg', 'project-name' ) 
     348                    if !output.empty? 
     349                        verboseMsg( "Using 'project-name' property: %p" % output ) 
     350                        name = output.first.chomp 
     351 
     352                    # If that doesn't work, try to figure it out from the URL 
     353                    elsif (( uri = getSvnUri() )) 
     354                        name = uri.path.sub( SVNUrlPath ) { $1 } 
     355                    end 
     356                end 
     357            end 
     358 
     359            # Fall back to guessing based on the directory name 
     360            unless name 
     361                name = File::basename(File::dirname( File::expand_path(__FILE__) )) 
     362            end 
     363        end 
     364 
     365        return name 
     366    end 
     367 
     368 
     369    ### Extract the Subversion URL from the specified directory and return it as 
     370    ### a URI object. 
     371    def getSvnUri( directory='.' ) 
     372        uri = nil 
     373 
     374        Dir::chdir( directory ) do 
     375            output = %x{svn info} 
     376            debugMsg( "Using info: %p" % output ) 
     377 
     378            if /^URL: \s* ( .* )/xi.match( output ) 
     379                uri = URI::parse( $1 ) 
     380            end 
     381        end 
     382 
     383        return uri 
     384    end 
     385 
     386 
     387    ### (Re)make a manifest file in the specified +path+. 
     388    def makeManifest( path="MANIFEST" ) 
     389        if File::exists?( path ) 
     390            reply = promptWithDefault( "Replace current '#{path}'? [yN]", "n" ) 
     391            return false unless /^y/i.match( reply ) 
     392 
     393            verboseMsg "Replacing manifest at '#{path}'" 
    253394        else 
    254             release[1] += 1 
    255         end 
    256  
    257         return "%d.%02d" % release 
    258     end 
    259  
    260     ### Extract the project name (CVS Repository name) for the given directory. 
    261     def extractProjectName 
    262         File.open( "CVS/Repository", "r").readline.chomp 
    263     end 
     395            verboseMsg "Creating new manifest at '#{path}'" 
     396        end 
     397 
     398        files = [] 
     399        verboseMsg( "Finding files...\n" ) 
     400        Find::find( Dir::pwd ) do |f| 
     401            Find::prune if File::directory?( f ) && 
     402                /^\./.match( File::basename(f) ) 
     403            verboseMsg( "  found: #{f}\n" ) 
     404            files << f.sub( %r{^#{Dir::pwd}/?}, '' ) 
     405        end 
     406        files = vetManifest( files ) 
     407         
     408        verboseMsg( "Writing new manifest to #{path}..." ) 
     409        File::open( path, File::WRONLY|File::CREAT|File::TRUNC ) do |ofh| 
     410            ofh.puts( ManifestHeader ) 
     411            ofh.puts( files ) 
     412        end 
     413        verboseMsg( "done." ) 
     414    end 
     415 
    264416 
    265417    ### Read the specified <tt>manifestFile</tt>, which is a text file 
     
    268420    ### shell glob pattern. 
    269421    def readManifest( manifestFile="MANIFEST" ) 
    270         message "Building manifest..." 
     422        verboseMsg "Building manifest..." 
    271423        raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile 
    272424 
     
    279431        filelist = [] 
    280432        for pat in manifest 
    281             $stderr.puts "Adding files that match '#{pat}' to the file list" if $VERBOSE 
     433            verboseMsg "Adding files that match '#{pat}' to the file list" 
    282434            filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)} 
    283435        end 
    284436 
    285         message "found #{filelist.length} files.\n" 
     437        verboseMsg "found #{filelist.length} files.\n" 
    286438        return filelist 
    287439    end 
     440 
    288441 
    289442    ### Given a <tt>filelist</tt> like that returned by #readManifest, remove 
    290443    ### the entries therein which match the Regexp objects in the given 
    291444    ### <tt>antimanifest</tt> and return the resultant Array. 
    292     def vetManifest( filelist, antimanifest=ANITMANIFEST ) 
     445    def vetManifest( filelist, antimanifest=ANTIMANIFEST ) 
    293446        origLength = filelist.length 
    294         message "Vetting manifest..." 
     447        verboseMsg "Vetting manifest..." 
    295448 
    296449        for regex in antimanifest 
    297             if $VERBOSE 
    298                 message "\n\tPattern /#{regex.source}/ removed: " + 
    299                     filelist.find_all {|file| regex.match(file)}.join(', ') 
    300             end 
     450            verboseMsg "\n\tPattern /#{regex.source}/ removed: " + 
     451                filelist.find_all {|file| regex.match(file)}.join(', ') 
    301452            filelist.delete_if {|file| regex.match(file)} 
    302453        end 
    303454 
    304         message "removed #{origLength - filelist.length} files from the list.\n" 
     455        verboseMsg "removed #{origLength - filelist.length} files from the list.\n" 
    305456        return filelist 
    306457    end 
     458 
    307459 
    308460    ### Combine a call to #readManifest with one to #vetManifest. 
     
    310462        vetManifest( readManifest(manifestFile), antimanifest ) 
    311463    end 
     464 
    312465 
    313466    ### Given a documentation <tt>catalogFile</tt>, extract the title, if 
     
    320473        title = findCatalogKeyword( 'title', catalogFile ) 
    321474 
    322         # If that doesn't work for some reason, try grabbing the name of the CVS 
    323         # repository the directory belongs to. 
    324         if title.nil? && File::directory?( "CVS" ) && 
    325                 File::exists?( "CVS/Repository" ) 
    326             title = File::read( "CVS/Repository" ).chomp 
    327         end 
    328  
    329         # As a last resort, use the name of the project directory 
    330         if title.nil? 
    331             distdir = File::dirname( __FILE__ ) 
    332             distdir = File::dirname( distdir ) if /docs$/ =~ distdir 
    333             title = File::basename( distdir ) 
    334         end 
     475        # If that doesn't work for some reason, use the name of the project. 
     476        title = extractProjectName() 
    335477 
    336478        return title 
    337479    end 
     480 
    338481 
    339482    ### Given a documentation <tt>catalogFile</tt>, extract the name of the file 
     
    384527 
    385528        if File::exists? catalogFile 
    386             message "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile 
     529            verboseMsg "Extracting '#{keyword}' from CATALOG file (%s).\n" % catalogFile 
    387530            File::foreach( catalogFile ) {|line| 
    388531                debugMsg( "Examining line #{line.inspect}..." ) 
     
    403546        startlist = [] 
    404547        if File.exists? catalogFile 
    405             message "Using CATALOG file (%s).\n" % catalogFile 
     548            verboseMsg "Using CATALOG file (%s).\n" % catalogFile 
    406549            startlist = getVettedManifest( catalogFile ) 
    407550        else 
    408             message "Using default MANIFEST\n" 
     551            verboseMsg "Using default MANIFEST\n" 
    409552            startlist = getVettedManifest() 
    410553        end 
    411554 
    412         message "Looking for RDoc comments in:\n" if $VERBOSE 
     555        verboseMsg "Looking for RDoc comments in:\n" 
    413556        startlist.select {|fn| 
    414             message "  #{fn}: " if $VERBOSE 
     557            verboseMsg "  #{fn}: " 
    415558            found = false 
    416559            File::open( fn, "r" ) {|fh| 
     
    423566            } 
    424567 
    425             message( (found ? "yes" : "no") + "\n" ) if $VERBOSE 
     568            verboseMsg( (found ? "yes" : "no") + "\n" ) 
    426569            found 
    427570        } 
     
    483626    ### Try the specified code block, printing the given  
    484627    def try( msg, bind=nil ) 
    485         result = nil 
    486         message "Trying #{msg}...\n" 
    487  
     628        result = '' 
     629        if msg =~ /^to\s/ 
     630            message = "Trying #{msg}..." 
     631        else 
     632            message = msg 
     633        end 
     634             
    488635        begin 
    489636            rval = nil 
     
    498645                result = rval.to_yaml 
    499646            else 
    500                 result = '' 
    501647                PP.pp( rval, result ) 
    502648            end 
     649 
    503650        rescue Exception => err 
    504             nicetrace = err.backtrace.delete_if {|frame| 
    505                 /in `(try|eval)'/ =~ frame 
    506             }.join("\n\t") 
     651            if err.backtrace 
     652                nicetrace = err.backtrace.delete_if {|frame| 
     653                    /in `(try|eval)'/ =~ frame 
     654                }.join("\n\t") 
     655            else 
     656                nicetrace = "Exception had no backtrace" 
     657            end 
     658 
    507659            result = err.message + "\n\t" + nicetrace 
    508660        ensure 
     
    514666    end 
    515667end 
     668 
     669 
     670if __FILE__ == $0 
     671    # $DEBUG = true 
     672    include UtilityFunctions 
     673 
     674    projname = extractProjectName() 
     675    header "Project: #{projname}" 
     676 
     677    ver = extractVersion() || [0,0,1] 
     678    puts "Version: %s\n" % ver.join('.') 
     679 
     680    if File::directory?( "docs" ) 
     681        puts "Rdoc:", 
     682            "  Title: " + findRdocTitle(), 
     683            "  Main: " + findRdocMain(), 
     684            "  Upload: " + findRdocUpload(), 
     685            "  SCCS URL: " + findRdocCvsURL() 
     686    end 
     687 
     688    puts "Manifest:", 
     689        "  " + getVettedManifest().join("\n  ") 
     690end