diff options
Diffstat (limited to 'lib')
-rwxr-xr-x | lib/rake.rb | 2241 | ||||
-rw-r--r-- | lib/rake/classic_namespace.rb | 8 | ||||
-rw-r--r-- | lib/rake/clean.rb | 33 | ||||
-rw-r--r-- | lib/rake/gempackagetask.rb | 103 | ||||
-rw-r--r-- | lib/rake/loaders/makefile.rb | 40 | ||||
-rw-r--r-- | lib/rake/packagetask.rb | 184 | ||||
-rw-r--r-- | lib/rake/rake_test_loader.rb | 5 | ||||
-rw-r--r-- | lib/rake/rdoctask.rb | 147 | ||||
-rwxr-xr-x | lib/rake/ruby182_test_unit_fix.rb | 23 | ||||
-rw-r--r-- | lib/rake/runtest.rb | 23 | ||||
-rw-r--r-- | lib/rake/tasklib.rb | 18 | ||||
-rw-r--r-- | lib/rake/testtask.rb | 161 |
12 files changed, 2986 insertions, 0 deletions
diff --git a/lib/rake.rb b/lib/rake.rb new file mode 100755 index 000000000..799a1b2d8 --- /dev/null +++ b/lib/rake.rb @@ -0,0 +1,2241 @@ +#!/usr/bin/env ruby + +#-- + +# Copyright (c) 2003, 2004, 2005, 2006, 2007 Jim Weirich +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +#++ +# +# = Rake -- Ruby Make +# +# This is the main file for the Rake application. Normally it is referenced +# as a library via a require statement, but it can be distributed +# independently as an application. + +RAKEVERSION = '0.8.0' + +require 'rbconfig' +require 'ftools' +require 'getoptlong' +require 'fileutils' +require 'singleton' +require 'thread' +require 'ostruct' + +###################################################################### +# Rake extensions to Module. +# +class Module + # Check for an existing method in the current class before extending. IF + # the method already exists, then a warning is printed and the extension is + # not added. Otherwise the block is yielded and any definitions in the + # block will take effect. + # + # Usage: + # + # class String + # rake_extension("xyz") do + # def xyz + # ... + # end + # end + # end + # + def rake_extension(method) + if instance_methods.include?(method.to_s) || instance_methods.include?(method.to_sym) + $stderr.puts "WARNING: Possible conflict with Rake extension: #{self}##{method} already exists" + else + yield + end + end +end # module Module + + +###################################################################### +# User defined methods to be added to String. +# +class String + rake_extension("ext") do + # Replace the file extension with +newext+. If there is no extenson on + # the string, append the new extension to the end. If the new extension + # is not given, or is the empty string, remove any existing extension. + # + # +ext+ is a user added method for the String class. + def ext(newext='') + return self.dup if ['.', '..'].include? self + if newext != '' + newext = (newext =~ /^\./) ? newext : ("." + newext) + end + dup.sub!(%r(([^/\\])\.[^./\\]*$)) { $1 + newext } || self + newext + end + end + + rake_extension("pathmap") do + # Explode a path into individual components. Used by +pathmap+. + def pathmap_explode + head, tail = File.split(self) + return [self] if head == self + return [tail] if head == '.' || tail == '/' + return [head, tail] if head == '/' + return head.pathmap_explode + [tail] + end + protected :pathmap_explode + + # Extract a partial path from the path. Include +n+ directories from the + # front end (left hand side) if +n+ is positive. Include |+n+| + # directories from the back end (right hand side) if +n+ is negative. + def pathmap_partial(n) + dirs = File.dirname(self).pathmap_explode + partial_dirs = + if n > 0 + dirs[0...n] + elsif n < 0 + dirs.reverse[0...-n].reverse + else + "." + end + File.join(partial_dirs) + end + protected :pathmap_partial + + # Preform the pathmap replacement operations on the given path. The + # patterns take the form 'pat1,rep1;pat2,rep2...'. + def pathmap_replace(patterns, &block) + result = self + patterns.split(';').each do |pair| + pattern, replacement = pair.split(',') + pattern = Regexp.new(pattern) + if replacement == '*' && block_given? + result = result.sub(pattern, &block) + elsif replacement + result = result.sub(pattern, replacement) + else + result = result.sub(pattern, '') + end + end + result + end + protected :pathmap_replace + + # Map the path according to the given specification. The specification + # controls the details of the mapping. The following special patterns are + # recognized: + # + # * <b>%p</b> -- The complete path. + # * <b>%f</b> -- The base file name of the path, with its file extension, + # but without any directories. + # * <b>%n</b> -- The file name of the path without its file extension. + # * <b>%d</b> -- The directory list of the path. + # * <b>%x</b> -- The file extension of the path. An empty string if there + # is no extension. + # * <b>%X</b> -- Everything *but* the file extension. + # * <b>%s</b> -- The alternate file separater if defined, otherwise use + # the standard file separator. + # * <b>%%</b> -- A percent sign. + # + # The %d specifier can also have a numeric prefix (e.g. '%2d'). If the + # number is positive, only return (up to) +n+ directories in the path, + # starting from the left hand side. If +n+ is negative, return (up to) + # |+n+| directories from the right hand side of the path. + # + # Examples: + # + # 'a/b/c/d/file.txt'.pathmap("%2d") => 'a/b' + # 'a/b/c/d/file.txt'.pathmap("%-2d") => 'c/d' + # + # Also the %d, %p, $f, $n, %x, and %X operators can take a + # pattern/replacement argument to perform simple string substititions on a + # particular part of the path. The pattern and replacement are speparated + # by a comma and are enclosed by curly braces. The replacement spec comes + # after the % character but before the operator letter. (e.g. + # "%{old,new}d"). Muliple replacement specs should be separated by + # semi-colons (e.g. "%{old,new;src,bin}d"). + # + # Regular expressions may be used for the pattern, and back refs may be + # used in the replacement text. Curly braces, commas and semi-colons are + # excluded from both the pattern and replacement text (let's keep parsing + # reasonable). + # + # For example: + # + # "src/org/onestepback/proj/A.java".pathmap("%{^src,bin}X.class") + # + # returns: + # + # "bin/org/onestepback/proj/A.class" + # + # If the replacement text is '*', then a block may be provided to perform + # some arbitrary calculation for the replacement. + # + # For example: + # + # "/path/to/file.TXT".pathmap("%X%{.*,*}x") { |ext| + # ext.downcase + # } + # + # Returns: + # + # "/path/to/file.txt" + # + def pathmap(spec=nil, &block) + return self if spec.nil? + result = '' + spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag| + case frag + when '%f' + result << File.basename(self) + when '%n' + result << File.basename(self).ext + when '%d' + result << File.dirname(self) + when '%x' + result << $1 if self =~ /[^\/](\.[^.]+)$/ + when '%X' + if self =~ /^(.*[^\/])(\.[^.]+)$/ + result << $1 + else + result << self + end + when '%p' + result << self + when '%s' + result << (File::ALT_SEPARATOR || File::SEPARATOR) + when '%-' + # do nothing + when '%%' + result << "%" + when /%(-?\d+)d/ + result << pathmap_partial($1.to_i) + when /^%\{([^}]*)\}(\d*[dpfnxX])/ + patterns, operator = $1, $2 + result << pathmap('%' + operator).pathmap_replace(patterns, &block) + when /^%/ + fail ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'" + else + result << frag + end + end + result + end + end +end # class String + +############################################################################## +module Rake + + # -------------------------------------------------------------------------- + # Rake module singleton methods. + # + class << self + # Current Rake Application + def application + @application ||= Rake::Application.new + end + + # Set the current Rake application object. + def application=(app) + @application = app + end + + # Return the original directory where the Rake application was started. + def original_dir + application.original_dir + end + + end + + # ########################################################################## + # Mixin for creating easily cloned objects. + # + module Cloneable + # Clone an object by making a new object and setting all the instance + # variables to the same values. + def clone + sibling = self.class.new + instance_variables.each do |ivar| + value = self.instance_variable_get(ivar) + new_value = value.clone rescue value + sibling.instance_variable_set(ivar, new_value) + end + sibling + end + alias dup clone + end + + #################################################################### + # TaskAguments manage the arguments passed to a task. + # + class TaskArguments + include Enumerable + + attr_reader :names + + def initialize(names, values, parent=nil) + @names = names + @parent = parent + @hash = {} + names.each_with_index { |name, i| + @hash[name.to_sym] = values[i] + } + end + + # Create a new argument scope using the prerequisite argument + # names. + def new_scope(names) + values = names.collect { |n| self[n] } + self.class.new(names, values, self) + end + + # Find an argument value by name or index. + def [](index) + lookup(index.to_sym) + end + + def each(&block) + @hash.each(&block) + end + + def method_missing(sym, *args, &block) + lookup(sym.to_sym) + end + + def to_hash + @hash + end + + def to_s + @hash.inspect + end + + def inspect + to_s + end + + protected + + def lookup(name) + if @hash.has_key?(name) + @hash[name] + elsif ENV.has_key?(name.to_s) + ENV[name.to_s] + elsif ENV.has_key?(name.to_s.upcase) + ENV[name.to_s.upcase] + elsif @parent + @parent.lookup(name) + end + end + end + + #################################################################### + # InvocationChain tracks the chain of task invocations to detect + # circular dependencies. + class InvocationChain + def initialize(value, tail) + @value = value + @tail = tail + end + + def member?(obj) + @value == obj || @tail.member?(obj) + end + + def append(value) + if member?(value) + fail RuntimeError, "Circular dependency detected: #{to_s} => #{value}" + end + self.class.new(value, self) + end + + def to_s + "#{prefix}#{@value}" + end + + def self.append(value, chain) + chain.append(value) + end + + private + + def prefix + "#{@tail.to_s} => " + end + + class EmptyInvocationChain + def member?(obj) + false + end + def append(value) + InvocationChain.new(value, self) + end + def to_s + "TOP" + end + end + + EMPTY = EmptyInvocationChain.new + + end # class InvocationChain + +end # module Rake + +module Rake + + # ######################################################################### + # A Task is the basic unit of work in a Rakefile. Tasks have associated + # actions (possibly more than one) and a list of prerequisites. When + # invoked, a task will first ensure that all of its prerequisites have an + # opportunity to run and then it will execute its own actions. + # + # Tasks are not usually created directly using the new method, but rather + # use the +file+ and +task+ convenience methods. + # + class Task + # List of prerequisites for a task. + attr_reader :prerequisites + + # Application owning this task. + attr_accessor :application + + # Comment for this task. Restricted to a single line of no more than 50 + # characters. + attr_reader :comment + + # Full text of the (possibly multi-line) comment. + attr_reader :full_comment + + # Array of nested namespaces names used for task lookup by this task. + attr_reader :scope + + # Return task name + def to_s + name + end + + def inspect + "<#{self.class} #{name} => [#{prerequisites.join(', ')}]>" + end + + # List of sources for task. + attr_writer :sources + def sources + @sources ||= [] + end + + # First source from a rule (nil if no sources) + def source + @sources.first if defined?(@sources) + end + + # Create a task named +task_name+ with no actions or prerequisites. Use + # +enhance+ to add actions and prerequisites. + def initialize(task_name, app) + @name = task_name.to_s + @prerequisites = FileList[] + @actions = [] + @already_invoked = false + @full_comment = nil + @comment = nil + @lock = Mutex.new + @application = app + @scope = app.current_scope + @arg_names = nil + end + + # Enhance a task with prerequisites or actions. Returns self. + def enhance(deps=nil, &block) + @prerequisites |= deps if deps + @actions << block if block_given? + self + end + + # Name of the task, including any namespace qualifiers. + def name + @name.to_s + end + + # Name of task with argument list description. + def name_with_args # :nodoc: + if arg_description + "#{name}#{arg_description}" + else + name + end + end + + # Argument description (nil if none). + def arg_description # :nodoc: + @arg_names ? "[#{(arg_names || []).join(',')}]" : nil + end + + # Name of arguments for this task. + def arg_names + @arg_names || [] + end + + # Invoke the task if it is needed. Prerequites are invoked first. + def invoke(*args) + task_args = TaskArguments.new(arg_names, args) + invoke_with_call_chain(task_args, InvocationChain::EMPTY) + end + + # Same as invoke, but explicitly pass a call chain to detect + # circular dependencies. + def invoke_with_call_chain(task_args, invocation_chain) + new_chain = InvocationChain.append(self, invocation_chain) + @lock.synchronize do + if application.options.trace + puts "** Invoke #{name} #{format_trace_flags}" + end + return if @already_invoked + @already_invoked = true + invoke_prerequisites(task_args, new_chain) + execute(task_args) if needed? + end + end + protected :invoke_with_call_chain + + # Invoke all the prerequisites of a task. + def invoke_prerequisites(task_args, invocation_chain) + @prerequisites.each { |n| + prereq = application[n, @scope] + prereq_args = task_args.new_scope(prereq.arg_names) + prereq.invoke_with_call_chain(prereq_args, invocation_chain) + } + end + + # Format the trace flags for display. + def format_trace_flags + flags = [] + flags << "first_time" unless @already_invoked + flags << "not_needed" unless needed? + flags.empty? ? "" : "(" + flags.join(", ") + ")" + end + private :format_trace_flags + + # Execute the actions associated with this task. + def execute(args) + if application.options.dryrun + puts "** Execute (dry run) #{name}" + return + end + if application.options.trace + puts "** Execute #{name}" + end + application.enhance_with_matching_rule(name) if @actions.empty? + @actions.each do |act| + case act.arity + when 1 + act.call(self) + else + act.call(self, args) + end + end + end + + # Is this task needed? + def needed? + true + end + + # Timestamp for this task. Basic tasks return the current time for their + # time stamp. Other tasks can be more sophisticated. + def timestamp + @prerequisites.collect { |p| application[p].timestamp }.max || Time.now + end + + # Add a description to the task. The description can consist of an option + # argument list (enclosed brackets) and an optional comment. + def add_description(description) + return if ! description + comment = description.strip + add_comment(comment) if comment && ! comment.empty? + end + + # Writing to the comment attribute is the same as adding a description. + def comment=(description) + add_description(description) + end + + # Add a comment to the task. If a comment alread exists, separate + # the new comment with " / ". + def add_comment(comment) + if @full_comment + @full_comment << " / " + else + @full_comment = '' + end + @full_comment << comment + if @full_comment =~ /\A([^.]+?\.)( |$)/ + @comment = $1 + else + @comment = @full_comment + end + end + private :add_comment + + # Set the names of the arguments for this task. +args+ should be + # an array of symbols, one for each argument name. + def set_arg_names(args) + @arg_names = args.map { |a| a.to_sym } + end + + # Return a string describing the internal state of a task. Useful for + # debugging. + def investigation + result = "------------------------------\n" + result << "Investigating #{name}\n" + result << "class: #{self.class}\n" + result << "task needed: #{needed?}\n" + result << "timestamp: #{timestamp}\n" + result << "pre-requisites: \n" + prereqs = @prerequisites.collect {|name| application[name]} + prereqs.sort! {|a,b| a.timestamp <=> b.timestamp} + prereqs.each do |p| + result << "--#{p.name} (#{p.timestamp})\n" + end + latest_prereq = @prerequisites.collect{|n| application[n].timestamp}.max + result << "latest-prerequisite time: #{latest_prereq}\n" + result << "................................\n\n" + return result + end + + # ---------------------------------------------------------------- + # Rake Module Methods + # + class << self + + # Clear the task list. This cause rake to immediately forget all the + # tasks that have been assigned. (Normally used in the unit tests.) + def clear + Rake.application.clear + end + + # List of all defined tasks. + def tasks + Rake.application.tasks + end + + # Return a task with the given name. If the task is not currently + # known, try to synthesize one from the defined rules. If no rules are + # found, but an existing file matches the task name, assume it is a file + # task with no dependencies or actions. + def [](task_name) + Rake.application[task_name] + end + + # TRUE if the task name is already defined. + def task_defined?(task_name) + Rake.application.lookup(task_name) != nil + end + + # Define a task given +args+ and an option block. If a rule with the + # given name already exists, the prerequisites and actions are added to + # the existing task. Returns the defined task. + def define_task(*args, &block) + Rake.application.define_task(self, *args, &block) + end + + # Define a rule for synthesizing tasks. + def create_rule(*args, &block) + Rake.application.create_rule(*args, &block) + end + + # Apply the scope to the task name according to the rules for + # this kind of task. Generic tasks will accept the scope as + # part of the name. + def scope_name(scope, task_name) + (scope + [task_name]).join(':') + end + + end # class << Rake::Task + end # class Rake::Task + + + # ######################################################################### + # A FileTask is a task that includes time based dependencies. If any of a + # FileTask's prerequisites have a timestamp that is later than the file + # represented by this task, then the file must be rebuilt (using the + # supplied actions). + # + class FileTask < Task + + # Is this file task needed? Yes if it doesn't exist, or if its time stamp + # is out of date. + def needed? + return true unless File.exist?(name) + return true if out_of_date?(timestamp) + false + end + + # Time stamp for file task. + def timestamp + if File.exist?(name) + File.mtime(name.to_s) + else + Rake::EARLY + end + end + + private + + # Are there any prerequisites with a later time than the given time stamp? + def out_of_date?(stamp) + @prerequisites.any? { |n| application[n].timestamp > stamp} + end + + # ---------------------------------------------------------------- + # Task class methods. + # + class << self + # Apply the scope to the task name according to the rules for this kind + # of task. File based tasks ignore the scope when creating the name. + def scope_name(scope, task_name) + task_name + end + end + end # class Rake::FileTask + + # ######################################################################### + # A FileCreationTask is a file task that when used as a dependency will be + # needed if and only if the file has not been created. Once created, it is + # not re-triggered if any of its dependencies are newer, nor does trigger + # any rebuilds of tasks that depend on it whenever it is updated. + # + class FileCreationTask < FileTask + # Is this file task needed? Yes if it doesn't exist. + def needed? + ! File.exist?(name) + end + + # Time stamp for file creation task. This time stamp is earlier + # than any other time stamp. + def timestamp + Rake::EARLY + end + end + + # ######################################################################### + # Same as a regular task, but the immediate prerequisites are done in + # parallel using Ruby threads. + # + class MultiTask < Task + def invoke_prerequisites(args, invocation_chain) + threads = @prerequisites.collect { |p| + Thread.new(p) { |r| application[r].invoke_with_call_chain(args, invocation_chain) } + } + threads.each { |t| t.join } + end + end +end # module Rake + +# ########################################################################### +# Task Definition Functions ... + +# Declare a basic task. +# +# Example: +# task :clobber => [:clean] do +# rm_rf "html" +# end +# +def task(*args, &block) + Rake::Task.define_task(*args, &block) +end + + +# Declare a file task. +# +# Example: +# file "config.cfg" => ["config.template"] do +# open("config.cfg", "w") do |outfile| +# open("config.template") do |infile| +# while line = infile.gets +# outfile.puts line +# end +# end +# end +# end +# +def file(args, &block) + Rake::FileTask.define_task(args, &block) +end + +# Declare a file creation task. +# (Mainly used for the directory command). +def file_create(args, &block) + Rake::FileCreationTask.define_task(args, &block) +end + +# Declare a set of files tasks to create the given directories on demand. +# +# Example: +# directory "testdata/doc" +# +def directory(dir) + Rake.each_dir_parent(dir) do |d| + file_create d do |t| + mkdir_p t.name if ! File.exist?(t.name) + end + end +end + +# Declare a task that performs its prerequisites in parallel. Multitasks does +# *not* guarantee that its prerequisites will execute in any given order +# (which is obvious when you think about it) +# +# Example: +# multitask :deploy => [:deploy_gem, :deploy_rdoc] +# +def multitask(args, &block) + Rake::MultiTask.define_task(args, &block) +end + +# Create a new rake namespace and use it for evaluating the given block. +# Returns a NameSpace object that can be used to lookup tasks defined in the +# namespace. +# +# E.g. +# +# ns = namespace "nested" do +# task :run +# end +# task_run = ns[:run] # find :run in the given namespace. +# +def namespace(name=nil, &block) + Rake.application.in_namespace(name, &block) +end + +# Declare a rule for auto-tasks. +# +# Example: +# rule '.o' => '.c' do |t| +# sh %{cc -o #{t.name} #{t.source}} +# end +# +def rule(*args, &block) + Rake::Task.create_rule(*args, &block) +end + +# Describe the next rake task. +# +# Example: +# desc "Run the Unit Tests" +# task :test => [:build] +# runtests +# end +# +def desc(description) + Rake.application.last_description = description +end + +# Import the partial Rakefiles +fn+. Imported files are loaded _after_ the +# current file is completely loaded. This allows the import statement to +# appear anywhere in the importing file, and yet allowing the imported files +# to depend on objects defined in the importing file. +# +# A common use of the import statement is to include files containing +# dependency declarations. +# +# See also the --rakelibdir command line option. +# +# Example: +# import ".depend", "my_rules" +# +def import(*fns) + fns.each do |fn| + Rake.application.add_import(fn) + end +end + +# ########################################################################### +# This a FileUtils extension that defines several additional commands to be +# added to the FileUtils utility functions. +# +module FileUtils + RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) + + OPT_TABLE['sh'] = %w(noop verbose) + OPT_TABLE['ruby'] = %w(noop verbose) + + # Run the system command +cmd+. If multiple arguments are given the command + # is not run with the shell (same semantics as Kernel::exec and + # Kernel::system). + # + # Example: + # sh %{ls -ltr} + # + # sh 'ls', 'file with spaces' + # + # # check exit status after command runs + # sh %{grep pattern file} do |ok, res| + # if ! ok + # puts "pattern not found (status = #{res.exitstatus})" + # end + # end + # + def sh(*cmd, &block) + options = (Hash === cmd.last) ? cmd.pop : {} + unless block_given? + show_command = cmd.join(" ") + show_command = show_command[0,42] + "..." + # TODO code application logic heref show_command.length > 45 + block = lambda { |ok, status| + ok or fail "Command failed with status (#{status.exitstatus}): [#{show_command}]" + } + end + rake_check_options options, :noop, :verbose + rake_output_message cmd.join(" ") if options[:verbose] + unless options[:noop] + res = system(*cmd) + block.call(res, $?) + end + end + + # Run a Ruby interpreter with the given arguments. + # + # Example: + # ruby %{-pe '$_.upcase!' <README} + # + def ruby(*args,&block) + options = (Hash === args.last) ? args.pop : {} + if args.length > 1 then + sh(*([RUBY] + args + [options]), &block) + else + sh("#{RUBY} #{args.first}", options, &block) + end + end + + LN_SUPPORTED = [true] + + # Attempt to do a normal file link, but fall back to a copy if the link + # fails. + def safe_ln(*args) + unless LN_SUPPORTED[0] + cp(*args) + else + begin + ln(*args) + rescue StandardError, NotImplementedError => ex + LN_SUPPORTED[0] = false + cp(*args) + end + end + end + + # Split a file path into individual directory names. + # + # Example: + # split_all("a/b/c") => ['a', 'b', 'c'] + # + def split_all(path) + head, tail = File.split(path) + return [tail] if head == '.' || tail == '/' + return [head, tail] if head == '/' + return split_all(head) + [tail] + end +end + +# ########################################################################### +# RakeFileUtils provides a custom version of the FileUtils methods that +# respond to the <tt>verbose</tt> and <tt>nowrite</tt> commands. +# +module RakeFileUtils + include FileUtils + + class << self + attr_accessor :verbose_flag, :nowrite_flag + end + RakeFileUtils.verbose_flag = true + RakeFileUtils.nowrite_flag = false + + $fileutils_verbose = true + $fileutils_nowrite = false + + FileUtils::OPT_TABLE.each do |name, opts| + default_options = [] + if opts.include?('verbose') + default_options << ':verbose => RakeFileUtils.verbose_flag' + end + if opts.include?('noop') + default_options << ':noop => RakeFileUtils.nowrite_flag' + end + + next if default_options.empty? + module_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{name}( *args, &block ) + super( + *rake_merge_option(args, + #{default_options.join(', ')} + ), &block) + end + EOS + end + + # Get/set the verbose flag controlling output from the FileUtils utilities. + # If verbose is true, then the utility method is echoed to standard output. + # + # Examples: + # verbose # return the current value of the verbose flag + # verbose(v) # set the verbose flag to _v_. + # verbose(v) { code } # Execute code with the verbose flag set temporarily to _v_. + # # Return to the original value when code is done. + def verbose(value=nil) + oldvalue = RakeFileUtils.verbose_flag + RakeFileUtils.verbose_flag = value unless value.nil? + if block_given? + begin + yield + ensure + RakeFileUtils.verbose_flag = oldvalue + end + end + RakeFileUtils.verbose_flag + end + + # Get/set the nowrite flag controlling output from the FileUtils utilities. + # If verbose is true, then the utility method is echoed to standard output. + # + # Examples: + # nowrite # return the current value of the nowrite flag + # nowrite(v) # set the nowrite flag to _v_. + # nowrite(v) { code } # Execute code with the nowrite flag set temporarily to _v_. + # # Return to the original value when code is done. + def nowrite(value=nil) + oldvalue = RakeFileUtils.nowrite_flag + RakeFileUtils.nowrite_flag = value unless value.nil? + if block_given? + begin + yield + ensure + RakeFileUtils.nowrite_flag = oldvalue + end + end + oldvalue + end + + # Use this function to prevent protentially destructive ruby code from + # running when the :nowrite flag is set. + # + # Example: + # + # when_writing("Building Project") do + # project.build + # end + # + # The following code will build the project under normal conditions. If the + # nowrite(true) flag is set, then the example will print: + # DRYRUN: Building Project + # instead of actually building the project. + # + def when_writing(msg=nil) + if RakeFileUtils.nowrite_flag + puts "DRYRUN: #{msg}" if msg + else + yield + end + end + + # Merge the given options with the default values. + def rake_merge_option(args, defaults) + if Hash === args.last + defaults.update(args.last) + args.pop + end + args.push defaults + args + end + private :rake_merge_option + + # Send the message to the default rake output (which is $stderr). + def rake_output_message(message) + $stderr.puts(message) + end + private :rake_output_message + + # Check that the options do not contain options not listed in +optdecl+. An + # ArgumentError exception is thrown if non-declared options are found. + def rake_check_options(options, *optdecl) + h = options.dup + optdecl.each do |name| + h.delete name + end + raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless h.empty? + end + private :rake_check_options + + extend self +end + +# ########################################################################### +# Include the FileUtils file manipulation functions in the top level module, +# but mark them private so that they don't unintentionally define methods on +# other objects. + +include RakeFileUtils +private(*FileUtils.instance_methods(false)) +private(*RakeFileUtils.instance_methods(false)) + +###################################################################### +module Rake + + class RuleRecursionOverflowError < StandardError + def initialize(*args) + super + @targets = [] + end + + def add_target(target) + @targets << target + end + + def message + super + ": [" + @targets.reverse.join(' => ') + "]" + end + end + + # ######################################################################### + # A FileList is essentially an array with a few helper methods defined to + # make file manipulation a bit easier. + # + # FileLists are lazy. When given a list of glob patterns for possible files + # to be included in the file list, instead of searching the file structures + # to find the files, a FileList holds the pattern for latter use. + # + # This allows us to define a number of FileList to match any number of + # files, but only search out the actual files when then FileList itself is + # actually used. The key is that the first time an element of the + # FileList/Array is requested, the pending patterns are resolved into a real + # list of file names. + # + class FileList + + include Cloneable + + # == Method Delegation + # + # The lazy evaluation magic of FileLists happens by implementing all the + # array specific methods to call +resolve+ before delegating the heavy + # lifting to an embedded array object (@items). + # + # In addition, there are two kinds of delegation calls. The regular kind + # delegates to the @items array and returns the result directly. Well, + # almost directly. It checks if the returned value is the @items object + # itself, and if so will return the FileList object instead. + # + # The second kind of delegation call is used in methods that normally + # return a new Array object. We want to capture the return value of these + # methods and wrap them in a new FileList object. We enumerate these + # methods in the +SPECIAL_RETURN+ list below. + + # List of array methods (that are not in +Object+) that need to be + # delegated. + ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s } + + # List of additional methods that must be delegated. + MUST_DEFINE = %w[to_a inspect] + + # List of methods that should not be delegated here (we define special + # versions of them explicitly below). + MUST_NOT_DEFINE = %w[to_a to_ary partition *] + + # List of delegated methods that return new array values which need + # wrapping. + SPECIAL_RETURN = %w[ + map collect sort sort_by select find_all reject grep + compact flatten uniq values_at + + - & | + ] + + DELEGATING_METHODS = (ARRAY_METHODS + MUST_DEFINE - MUST_NOT_DEFINE).collect{ |s| s.to_s }.sort.uniq + + # Now do the delegation. + DELEGATING_METHODS.each_with_index do |sym, i| + if SPECIAL_RETURN.include?(sym) + ln = __LINE__+1 + class_eval %{ + def #{sym}(*args, &block) + resolve + result = @items.send(:#{sym}, *args, &block) + FileList.new.import(result) + end + }, __FILE__, ln + else + ln = __LINE__+1 + class_eval %{ + def #{sym}(*args, &block) + resolve + result = @items.send(:#{sym}, *args, &block) + result.object_id == @items.object_id ? self : result + end + }, __FILE__, ln + end + end + + # Create a file list from the globbable patterns given. If you wish to + # perform multiple includes or excludes at object build time, use the + # "yield self" pattern. + # + # Example: + # file_list = FileList.new('lib/**/*.rb', 'test/test*.rb') + # + # pkg_files = FileList.new('lib/**/*') do |fl| + # fl.exclude(/\bCVS\b/) + # end + # + def initialize(*patterns) + @pending_add = [] + @pending = false + @exclude_patterns = DEFAULT_IGNORE_PATTERNS.dup + @exclude_procs = DEFAULT_IGNORE_PROCS.dup + @exclude_re = nil + @items = [] + patterns.each { |pattern| include(pattern) } + yield self if block_given? + end + + # Add file names defined by glob patterns to the file list. If an array + # is given, add each element of the array. + # + # Example: + # file_list.include("*.java", "*.cfg") + # file_list.include %w( math.c lib.h *.o ) + # + def include(*filenames) + # TODO: check for pending + filenames.each do |fn| + if fn.respond_to? :to_ary + include(*fn.to_ary) + else + @pending_add << fn + end + end + @pending = true + self + end + alias :add :include + + # Register a list of file name patterns that should be excluded from the + # list. Patterns may be regular expressions, glob patterns or regular + # strings. In addition, a block given to exclude will remove entries that + # return true when given to the block. + # + # Note that glob patterns are expanded against the file system. If a file + # is explicitly added to a file list, but does not exist in the file + # system, then an glob pattern in the exclude list will not exclude the + # file. + # + # Examples: + # FileList['a.c', 'b.c'].exclude("a.c") => ['b.c'] + # FileList['a.c', 'b.c'].exclude(/^a/) => ['b.c'] + # + # If "a.c" is a file, then ... + # FileList['a.c', 'b.c'].exclude("a.*") => ['b.c'] + # + # If "a.c" is not a file, then ... + # FileList['a.c', 'b.c'].exclude("a.*") => ['a.c', 'b.c'] + # + def exclude(*patterns, &block) + patterns.each do |pat| + @exclude_patterns << pat + end + if block_given? + @exclude_procs << block + end + resolve_exclude if ! @pending + self + end + + + # Clear all the exclude patterns so that we exclude nothing. + def clear_exclude + @exclude_patterns = [] + @exclude_procs = [] + calculate_exclude_regexp if ! @pending + self + end + + # Define equality. + def ==(array) + to_ary == array + end + + # Return the internal array object. + def to_a + resolve + @items + end + + # Return the internal array object. + def to_ary + to_a + end + + # Lie about our class. + def is_a?(klass) + klass == Array || super(klass) + end + alias kind_of? is_a? + + # Redefine * to return either a string or a new file list. + def *(other) + result = @items * other + case result + when Array + FileList.new.import(result) + else + result + end + end + + # Resolve all the pending adds now. + def resolve + if @pending + @pending = false + @pending_add.each do |fn| resolve_add(fn) end + @pending_add = [] + resolve_exclude + end + self + end + + def calculate_exclude_regexp + ignores = [] + @exclude_patterns.each do |pat| + case pat + when Regexp + ignores << pat + when /[*?]/ + Dir[pat].each do |p| ignores << p end + else + ignores << Regexp.quote(pat) + end + end + if ignores.empty? + @exclude_re = /^$/ + else + re_str = ignores.collect { |p| "(" + p.to_s + ")" }.join("|") + @exclude_re = Regexp.new(re_str) + end + end + + def resolve_add(fn) + case fn + when %r{[*?\[\{]} + add_matching(fn) + else + self << fn + end + end + private :resolve_add + + def resolve_exclude + calculate_exclude_regexp + reject! { |fn| exclude?(fn) } + self + end + private :resolve_exclude + + # Return a new FileList with the results of running +sub+ against each + # element of the oringal list. + # + # Example: + # FileList['a.c', 'b.c'].sub(/\.c$/, '.o') => ['a.o', 'b.o'] + # + def sub(pat, rep) + inject(FileList.new) { |res, fn| res << fn.sub(pat,rep) } + end + + # Return a new FileList with the results of running +gsub+ against each + # element of the original list. + # + # Example: + # FileList['lib/test/file', 'x/y'].gsub(/\//, "\\") + # => ['lib\\test\\file', 'x\\y'] + # + def gsub(pat, rep) + inject(FileList.new) { |res, fn| res << fn.gsub(pat,rep) } + end + + # Same as +sub+ except that the oringal file list is modified. + def sub!(pat, rep) + each_with_index { |fn, i| self[i] = fn.sub(pat,rep) } + self + end + + # Same as +gsub+ except that the original file list is modified. + def gsub!(pat, rep) + each_with_index { |fn, i| self[i] = fn.gsub(pat,rep) } + self + end + + # Apply the pathmap spec to each of the included file names, returning a + # new file list with the modified paths. (See String#pathmap for + # details.) + def pathmap(spec=nil) + collect { |fn| fn.pathmap(spec) } + end + + # Return a new array with <tt>String#ext</tt> method applied to each + # member of the array. + # + # This method is a shortcut for: + # + # array.collect { |item| item.ext(newext) } + # + # +ext+ is a user added method for the Array class. + def ext(newext='') + collect { |fn| fn.ext(newext) } + end + + + # Grep each of the files in the filelist using the given pattern. If a + # block is given, call the block on each matching line, passing the file + # name, line number, and the matching line of text. If no block is given, + # a standard emac style file:linenumber:line message will be printed to + # standard out. + def egrep(pattern) + each do |fn| + open(fn) do |inf| + count = 0 + inf.each do |line| + count += 1 + if pattern.match(line) + if block_given? + yield fn, count, line + else + puts "#{fn}:#{count}:#{line}" + end + end + end + end + end + end + + # Return a new file list that only contains file names from the current + # file list that exist on the file system. + def existing + select { |fn| File.exist?(fn) } + end + + # Modify the current file list so that it contains only file name that + # exist on the file system. + def existing! + resolve + @items = @items.select { |fn| File.exist?(fn) } + self + end + + # FileList version of partition. Needed because the nested arrays should + # be FileLists in this version. + def partition(&block) # :nodoc: + resolve + result = @items.partition(&block) + [ + FileList.new.import(result[0]), + FileList.new.import(result[1]), + ] + end + + # Convert a FileList to a string by joining all elements with a space. + def to_s + resolve + self.join(' ') + end + + # Add matching glob patterns. + def add_matching(pattern) + Dir[pattern].each do |fn| + self << fn unless exclude?(fn) + end + end + private :add_matching + + # Should the given file name be excluded? + def exclude?(fn) + calculate_exclude_regexp unless @exclude_re + fn =~ @exclude_re || @exclude_procs.any? { |p| p.call(fn) } + end + + DEFAULT_IGNORE_PATTERNS = [ + /(^|[\/\\])CVS([\/\\]|$)/, + /(^|[\/\\])\.svn([\/\\]|$)/, + /\.bak$/, + /~$/ + ] + DEFAULT_IGNORE_PROCS = [ + proc { |fn| fn =~ /(^|[\/\\])core$/ && ! File.directory?(fn) } + ] +# @exclude_patterns = DEFAULT_IGNORE_PATTERNS.dup + + def import(array) + @items = array + self + end + + class << self + # Create a new file list including the files listed. Similar to: + # + # FileList.new(*args) + def [](*args) + new(*args) + end + end + end # FileList +end + +module Rake + class << self + + # Yield each file or directory component. + def each_dir_parent(dir) + old_length = nil + while dir != '.' && dir.length != old_length + yield(dir) + old_length = dir.length + dir = File.dirname(dir) + end + end + end +end # module Rake + +# Alias FileList to be available at the top level. +FileList = Rake::FileList + +# ########################################################################### +module Rake + + # Default Rakefile loader used by +import+. + class DefaultLoader + def load(fn) + Kernel.load(File.expand_path(fn)) + end + end + + # EarlyTime is a fake timestamp that occurs _before_ any other time value. + class EarlyTime + include Comparable + include Singleton + + def <=>(other) + -1 + end + + def to_s + "<EARLY TIME>" + end + end + + EARLY = EarlyTime.instance +end # module Rake + +# ########################################################################### +# Extensions to time to allow comparisons with an early time class. +# +class Time + alias rake_original_time_compare :<=> + def <=>(other) + if Rake::EarlyTime === other + - other.<=>(self) + else + rake_original_time_compare(other) + end + end +end # class Time + +module Rake + + #################################################################### + # The NameSpace class will lookup task names in the the scope + # defined by a +namespace+ command. + # + class NameSpace + + # Create a namespace lookup object using the given task manager + # and the list of scopes. + def initialize(task_manager, scope_list) + @task_manager = task_manager + @scope = scope_list.dup + end + + # Lookup a task named +name+ in the namespace. + def [](name) + @task_manager.lookup(name, @scope) + end + + # Return the list of tasks defined in this namespace. + def tasks + @task_manager.tasks + end + end # NameSpace + + + #################################################################### + # The TaskManager module is a mixin for managing tasks. + module TaskManager + # Track the last comment made in the Rakefile. + attr_accessor :last_description + alias :last_comment :last_description # Backwards compatibility + + def initialize + super + @tasks = Hash.new + @rules = Array.new + @scope = Array.new + @last_description = nil + end + + def create_rule(*args, &block) + pattern, arg_names, deps = resolve_args(args) + pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern + @rules << [pattern, deps, block] + end + + def define_task(task_class, *args, &block) + task_name, arg_names, deps = resolve_args(args) + task_name = task_class.scope_name(@scope, task_name) + deps = [deps] unless deps.respond_to?(:to_ary) + deps = deps.collect {|d| d.to_s } + task = intern(task_class, task_name) + task.set_arg_names(arg_names) unless arg_names.empty? + task.add_description(@last_description) + @last_description = nil + task.enhance(deps, &block) + task + end + + # Lookup a task. Return an existing task if found, otherwise + # create a task of the current type. + def intern(task_class, task_name) + @tasks[task_name.to_s] ||= task_class.new(task_name, self) + end + + # Find a matching task for +task_name+. + def [](task_name, scopes=nil) + task_name = task_name.to_s + self.lookup(task_name, scopes) or + enhance_with_matching_rule(task_name) or + synthesize_file_task(task_name) or + fail "Don't know how to build task '#{task_name}'" + end + + def synthesize_file_task(task_name) + return nil unless File.exist?(task_name) + define_task(Rake::FileTask, task_name) + end + + # Resolve the arguments for a task/rule. Returns a triplet of + # [task_name, arg_name_list, prerequisites]. + def resolve_args(args) + task_name = args.shift + arg_names = args #.map { |a| a.to_sym } + needs = [] + if task_name.is_a?(Hash) + hash = task_name + task_name = hash.keys[0] + needs = hash[task_name] + end + if arg_names.last.is_a?(Hash) + hash = arg_names.pop + needs = hash[:needs] + fail "Unrecognized keys in task hash: #{hash.keys.inspect}" if hash.size > 1 + end + needs = [needs] unless needs.respond_to?(:to_ary) + [task_name, arg_names, needs] + end + + # If a rule can be found that matches the task name, enhance the + # task with the prerequisites and actions from the rule. Set the + # source attribute of the task appropriately for the rule. Return + # the enhanced task or nil of no rule was found. + def enhance_with_matching_rule(task_name, level=0) + fail Rake::RuleRecursionOverflowError, + "Rule Recursion Too Deep" if level >= 16 + @rules.each do |pattern, extensions, block| + if md = pattern.match(task_name) + task = attempt_rule(task_name, extensions, block, level) + return task if task + end + end + nil + rescue Rake::RuleRecursionOverflowError => ex + ex.add_target(task_name) + fail ex + end + + # List of all defined tasks in this application. + def tasks + @tasks.values.sort_by { |t| t.name } + end + + # Clear all tasks in this application. + def clear + @tasks.clear + @rules.clear + end + + # Lookup a task, using scope and the scope hints in the task name. + # This method performs straight lookups without trying to + # synthesize file tasks or rules. Special scope names (e.g. '^') + # are recognized. If no scope argument is supplied, use the + # current scope. Return nil if the task cannot be found. + def lookup(task_name, initial_scope=nil) + initial_scope ||= @scope + task_name = task_name.to_s + if task_name =~ /^rake:/ + scopes = [] + task_name = task_name.sub(/^rake:/, '') + elsif task_name =~ /^(\^+)/ + scopes = initial_scope[0, initial_scope.size - $1.size] + task_name = task_name.sub(/^(\^+)/, '') + else + scopes = initial_scope + end + lookup_in_scope(task_name, scopes) + end + + # Lookup the task name + def lookup_in_scope(name, scope) + n = scope.size + while n >= 0 + tn = (scope[0,n] + [name]).join(':') + task = @tasks[tn] + return task if task + n -= 1 + end + nil + end + private :lookup_in_scope + + # Return the list of scope names currently active in the task + # manager. + def current_scope + @scope.dup + end + + # Evaluate the block in a nested namespace named +name+. Create + # an anonymous namespace if +name+ is nil. + def in_namespace(name) + name ||= generate_name + @scope.push(name) + ns = NameSpace.new(self, @scope) + yield(ns) + ns + ensure + @scope.pop + end + + private + + # Generate an anonymous namespace name. + def generate_name + @seed ||= 0 + @seed += 1 + "_anon_#{@seed}" + end + + # Attempt to create a rule given the list of prerequisites. + def attempt_rule(task_name, extensions, block, level) + sources = make_sources(task_name, extensions) + prereqs = sources.collect { |source| + if File.exist?(source) || Rake::Task.task_defined?(source) + source + elsif parent = enhance_with_matching_rule(sources.first, level+1) + parent.name + else + return nil + end + } + task = FileTask.define_task({task_name => prereqs}, &block) + task.sources = prereqs + task + end + + # Make a list of sources from the list of file name extensions / + # translation procs. + def make_sources(task_name, extensions) + extensions.collect { |ext| + case ext + when /%/ + task_name.pathmap(ext) + when %r{/} + ext + when /^\./ + task_name.ext(ext) + when String + ext + when Proc + if ext.arity == 1 + ext.call(task_name) + else + ext.call + end + else + fail "Don't know how to handle rule dependent: #{ext.inspect}" + end + }.flatten + end + + end # TaskManager + + ###################################################################### + # Rake main application object. When invoking +rake+ from the + # command line, a Rake::Application object is created and run. + # + class Application + include TaskManager + + # The name of the application (typically 'rake') + attr_reader :name + + # The original directory where rake was invoked. + attr_reader :original_dir + + # Name of the actual rakefile used. + attr_reader :rakefile + + # List of the top level task names (task names from the command line). + attr_reader :top_level_tasks + + DEFAULT_RAKEFILES = ['rakefile', 'Rakefile', 'rakefile.rb', 'Rakefile.rb'].freeze + + OPTIONS = [ # :nodoc: + ['--classic-namespace', '-C', GetoptLong::NO_ARGUMENT, + "Put Task and FileTask in the top level namespace"], + ['--describe', '-D', GetoptLong::OPTIONAL_ARGUMENT, + "Describe the tasks (matching optional PATTERN), then exit."], + ['--rakefile', '-f', GetoptLong::OPTIONAL_ARGUMENT, + "Use FILE as the rakefile."], + ['--help', '-h', '-H', GetoptLong::NO_ARGUMENT, + "Display this help message."], + ['--libdir', '-I', GetoptLong::REQUIRED_ARGUMENT, + "Include LIBDIR in the search path for required modules."], + ['--dry-run', '-n', GetoptLong::NO_ARGUMENT, + "Do a dry run without executing actions."], + ['--nosearch', '-N', GetoptLong::NO_ARGUMENT, + "Do not search parent directories for the Rakefile."], + ['--prereqs', '-P', GetoptLong::NO_ARGUMENT, + "Display the tasks and dependencies, then exit."], + ['--quiet', '-q', GetoptLong::NO_ARGUMENT, + "Do not log messages to standard output."], + ['--require', '-r', GetoptLong::REQUIRED_ARGUMENT, + "Require MODULE before executing rakefile."], + ['--rakelibdir', '-R', GetoptLong::REQUIRED_ARGUMENT, + "Auto-import any .rake files in RAKELIBDIR. (default is 'rakelib')"], + ['--silent', '-s', GetoptLong::NO_ARGUMENT, + "Like --quiet, but also suppresses the 'in directory' announcement."], + ['--tasks', '-T', GetoptLong::OPTIONAL_ARGUMENT, + "Display the tasks (matching optional PATTERN) with descriptions, then exit."], + ['--trace', '-t', GetoptLong::NO_ARGUMENT, + "Turn on invoke/execute tracing, enable full backtrace."], + ['--verbose', '-v', GetoptLong::NO_ARGUMENT, + "Log message to standard output (default)."], + ['--version', '-V', GetoptLong::NO_ARGUMENT, + "Display the program version."], + ] + + # Initialize a Rake::Application object. + def initialize + super + @name = 'rake' + @rakefiles = DEFAULT_RAKEFILES.dup + @rakefile = nil + @pending_imports = [] + @imported = [] + @loaders = {} + @default_loader = Rake::DefaultLoader.new + @original_dir = Dir.pwd + @top_level_tasks = [] + add_loader('rf', DefaultLoader.new) + add_loader('rake', DefaultLoader.new) + end + + # Run the Rake application. The run method performs the following three steps: + # + # * Initialize the command line options (+init+). + # * Define the tasks (+load_rakefile+). + # * Run the top level tasks (+run_tasks+). + # + # If you wish to build a custom rake command, you should call +init+ on your + # application. The define any tasks. Finally, call +top_level+ to run your top + # level tasks. + def run + standard_exception_handling do + init + load_rakefile + top_level + end + end + + # Initialize the command line parameters and app name. + def init(app_name='rake') + standard_exception_handling do + @name = app_name + handle_options + collect_tasks + end + end + + # Find the rakefile and then load it and any pending imports. + def load_rakefile + standard_exception_handling do + raw_load_rakefile + end + end + + # Run the top level tasks of a Rake application. + def top_level + standard_exception_handling do + if options.show_tasks + display_tasks_and_comments + elsif options.show_prereqs + display_prerequisites + else + top_level_tasks.each { |task_name| invoke_task(task_name) } + end + end + end + + # Add a loader to handle imported files ending in the extension + # +ext+. + def add_loader(ext, loader) + ext = ".#{ext}" unless ext =~ /^\./ + @loaders[ext] = loader + end + + # Application options from the command line + def options + @options ||= OpenStruct.new + end + + # private ---------------------------------------------------------------- + + def invoke_task(task_string) + name, args = parse_task_string(task_string) + t = self[name] + t.invoke(*args) + end + + def parse_task_string(string) + if string =~ /^([^\[]+)(\[(.*)\])$/ + name = $1 + args = $3.split(/\s*,\s*/) + else + name = string + args = [] + end + [name, args] + end + + # Provide standard execption handling for the given block. + def standard_exception_handling + begin + yield + rescue SystemExit => ex + # Exit silently with current status + exit(ex.status) + rescue SystemExit, GetoptLong::InvalidOption => ex + # Exit silently + exit(1) + rescue Exception => ex + # Exit with error message + $stderr.puts "rake aborted!" + $stderr.puts ex.message + if options.trace + $stderr.puts ex.backtrace.join("\n") + else + $stderr.puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || "" + $stderr.puts "(See full trace by running task with --trace)" + end + exit(1) + end + end + + # True if one of the files in RAKEFILES is in the current directory. + # If a match is found, it is copied into @rakefile. + def have_rakefile + @rakefiles.each do |fn| + if File.exist?(fn) || fn == '' + @rakefile = fn + return true + end + end + return false + end + + # Display the rake command line help. + def help + puts "rake [-f rakefile] {options} targets..." + puts + puts "Options are ..." + puts + OPTIONS.sort.each do |long, short, mode, desc| + if mode == GetoptLong::REQUIRED_ARGUMENT + if desc =~ /\b([A-Z]{2,})\b/ + long = long + "=#{$1}" + end + end + printf " %-20s (%s)\n", long, short + printf " %s\n", desc + end + end + + # Display the tasks and dependencies. + def display_tasks_and_comments + displayable_tasks = tasks.select { |t| + t.comment && t.name =~ options.show_task_pattern + } + if options.full_description + displayable_tasks.each do |t| + puts "rake #{t.name_with_args}" + t.full_comment.split("\n").each do |line| + puts " #{line}" + end + puts + end + else + width = displayable_tasks.collect { |t| t.name_with_args.length }.max || 10 + max_column = 80 - name.size - width - 7 + displayable_tasks.each do |t| + printf "#{name} %-#{width}s # %s\n", + t.name_with_args, truncate(t.comment, max_column) + end + end + end + + def truncate(string, width) + if string.length <= width + string + else + string[0, width-3] + "..." + end + end + + # Display the tasks and prerequisites + def display_prerequisites + tasks.each do |t| + puts "rake #{t.name}" + t.prerequisites.each { |pre| puts " #{pre}" } + end + end + + # Return a list of the command line options supported by the + # program. + def command_line_options + OPTIONS.collect { |lst| lst[0..-2] } + end + + # Do the option defined by +opt+ and +value+. + def do_option(opt, value) + case opt + when '--describe' + options.show_tasks = true + options.show_task_pattern = Regexp.new(value || '.') + options.full_description = true + when '--dry-run' + verbose(true) + nowrite(true) + options.dryrun = true + options.trace = true + when '--help' + help + exit + when '--libdir' + $:.push(value) + when '--nosearch' + options.nosearch = true + when '--prereqs' + options.show_prereqs = true + when '--quiet' + verbose(false) + when '--rakefile' + @rakefiles.clear + @rakefiles << value + when '--rakelibdir' + options.rakelib = value.split(':') + when '--require' + begin + require value + rescue LoadError => ex + begin + rake_require value + rescue LoadError => ex2 + raise ex + end + end + when '--silent' + verbose(false) + options.silent = true + when '--tasks' + options.show_tasks = true + options.show_task_pattern = Regexp.new(value || '.') + options.full_description = false + when '--trace' + options.trace = true + verbose(true) + when '--verbose' + verbose(true) + when '--version' + puts "rake, version #{RAKEVERSION}" + exit + when '--classic-namespace' + require 'rake/classic_namespace' + options.classic_namespace = true + end + end + + # Read and handle the command line options. + def handle_options + options.rakelib = ['rakelib'] + + opts = GetoptLong.new(*command_line_options) + opts.each { |opt, value| do_option(opt, value) } + + # If class namespaces are requested, set the global options + # according to the values in the options structure. + if options.classic_namespace + $show_tasks = options.show_tasks + $show_prereqs = options.show_prereqs + $trace = options.trace + $dryrun = options.dryrun + $silent = options.silent + end + rescue NoMethodError => ex + raise GetoptLong::InvalidOption, "While parsing options, error = #{ex.class}:#{ex.message}" + end + + # Similar to the regular Ruby +require+ command, but will check + # for .rake files in addition to .rb files. + def rake_require(file_name, paths=$LOAD_PATH, loaded=$") + return false if loaded.include?(file_name) + paths.each do |path| + fn = file_name + ".rake" + full_path = File.join(path, fn) + if File.exist?(full_path) + load full_path + loaded << fn + return true + end + end + fail LoadError, "Can't find #{file_name}" + end + + def raw_load_rakefile # :nodoc: + here = Dir.pwd + while ! have_rakefile + Dir.chdir("..") + if Dir.pwd == here || options.nosearch + fail "No Rakefile found (looking for: #{@rakefiles.join(', ')})" + end + here = Dir.pwd + end + puts "(in #{Dir.pwd})" unless options.silent + $rakefile = @rakefile + load File.expand_path(@rakefile) if @rakefile != '' + options.rakelib.each do |rlib| + Dir["#{rlib}/*.rake"].each do |name| add_import name end + end + load_imports + end + + # Collect the list of tasks on the command line. If no tasks are + # given, return a list containing only the default task. + # Environmental assignments are processed at this time as well. + def collect_tasks + @top_level_tasks = [] + ARGV.each do |arg| + if arg =~ /^(\w+)=(.*)$/ + ENV[$1] = $2 + else + @top_level_tasks << arg + end + end + @top_level_tasks.push("default") if @top_level_tasks.size == 0 + end + + # Add a file to the list of files to be imported. + def add_import(fn) + @pending_imports << fn + end + + # Load the pending list of imported files. + def load_imports + while fn = @pending_imports.shift + next if @imported.member?(fn) + if fn_task = lookup(fn) + fn_task.invoke + end + ext = File.extname(fn) + loader = @loaders[ext] || @default_loader + loader.load(fn) + @imported << fn + end + end + + # Warn about deprecated use of top level constant names. + def const_warning(const_name) + @const_warning ||= false + if ! @const_warning + $stderr.puts %{WARNING: Deprecated reference to top-level constant '#{const_name}' } + + %{found at: #{rakefile_location}} # ' + $stderr.puts %{ Use --classic-namespace on rake command} + $stderr.puts %{ or 'require "rake/classic_namespace"' in Rakefile} + end + @const_warning = true + end + + def rakefile_location + begin + fail + rescue RuntimeError => ex + ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || "" + end + end + end +end + + +class Module + # Rename the original handler to make it available. + alias :rake_original_const_missing :const_missing + + # Check for deprecated uses of top level (i.e. in Object) uses of + # Rake class names. If someone tries to reference the constant + # name, display a warning and return the proper object. Using the + # --classic-namespace command line option will define these + # constants in Object and avoid this handler. + def const_missing(const_name) + case const_name + when :Task + Rake.application.const_warning(const_name) + Rake::Task + when :FileTask + Rake.application.const_warning(const_name) + Rake::FileTask + when :FileCreationTask + Rake.application.const_warning(const_name) + Rake::FileCreationTask + when :RakeApp + Rake.application.const_warning(const_name) + Rake::Application + else + rake_original_const_missing(const_name) + end + end +end diff --git a/lib/rake/classic_namespace.rb b/lib/rake/classic_namespace.rb new file mode 100644 index 000000000..feb756996 --- /dev/null +++ b/lib/rake/classic_namespace.rb @@ -0,0 +1,8 @@ +# The following classes used to be in the top level namespace. +# Loading this file enables compatibility with older Rakefile that +# referenced Task from the top level. + +Task = Rake::Task +FileTask = Rake::FileTask +FileCreationTask = Rake::FileCreationTask +RakeApp = Rake::Application diff --git a/lib/rake/clean.rb b/lib/rake/clean.rb new file mode 100644 index 000000000..4ee2c5ac9 --- /dev/null +++ b/lib/rake/clean.rb @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby + +# The 'rake/clean' file defines two file lists (CLEAN and CLOBBER) and +# two rake tasks (:clean and :clobber). +# +# [:clean] Clean up the project by deleting scratch files and backup +# files. Add files to the CLEAN file list to have the :clean +# target handle them. +# +# [:clobber] Clobber all generated and non-source files in a project. +# The task depends on :clean, so all the clean files will +# be deleted as well as files in the CLOBBER file list. +# The intent of this task is to return a project to its +# pristine, just unpacked state. + +require 'rake' + +CLEAN = Rake::FileList["**/*~", "**/*.bak", "**/core"] +CLEAN.clear_exclude.exclude { |fn| + fn.pathmap("%f") == 'core' && File.directory?(fn) +} + +desc "Remove any temporary products." +task :clean do + CLEAN.each { |fn| rm_r fn rescue nil } +end + +CLOBBER = Rake::FileList.new + +desc "Remove any generated file." +task :clobber => [:clean] do + CLOBBER.each { |fn| rm_r fn rescue nil } +end diff --git a/lib/rake/gempackagetask.rb b/lib/rake/gempackagetask.rb new file mode 100644 index 000000000..a4e5cd190 --- /dev/null +++ b/lib/rake/gempackagetask.rb @@ -0,0 +1,103 @@ +#!/usr/bin/env ruby + +# Define a package task library to aid in the definition of GEM +# packages. + +require 'rubygems' +require 'rake' +require 'rake/packagetask' +require 'rubygems/user_interaction' +require 'rubygems/builder' + +begin + Gem.manage_gems +rescue NoMethodError => ex + # Using rubygems prior to 0.6.1 +end + +module Rake + + # Create a package based upon a Gem spec. Gem packages, as well as + # zip files and tar/gzipped packages can be produced by this task. + # + # In addition to the Rake targets generated by PackageTask, a + # GemPackageTask will also generate the following tasks: + # + # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.gem"</b>] + # Create a Ruby GEM package with the given name and version. + # + # Example using a Ruby GEM spec: + # + # require 'rubygems' + # + # spec = Gem::Specification.new do |s| + # s.platform = Gem::Platform::RUBY + # s.summary = "Ruby based make-like utility." + # s.name = 'rake' + # s.version = PKG_VERSION + # s.requirements << 'none' + # s.require_path = 'lib' + # s.autorequire = 'rake' + # s.files = PKG_FILES + # s.description = <<EOF + # Rake is a Make-like program implemented in Ruby. Tasks + # and dependencies are specified in standard Ruby syntax. + # EOF + # end + # + # Rake::GemPackageTask.new(spec) do |pkg| + # pkg.need_zip = true + # pkg.need_tar = true + # end + # + class GemPackageTask < PackageTask + # Ruby GEM spec containing the metadata for this package. The + # name, version and package_files are automatically determined + # from the GEM spec and don't need to be explicitly provided. + attr_accessor :gem_spec + + # Create a GEM Package task library. Automatically define the gem + # if a block is given. If no block is supplied, then +define+ + # needs to be called to define the task. + def initialize(gem_spec) + init(gem_spec) + yield self if block_given? + define if block_given? + end + + # Initialization tasks without the "yield self" or define + # operations. + def init(gem) + super(gem.name, gem.version) + @gem_spec = gem + @package_files += gem_spec.files if gem_spec.files + end + + # Create the Rake tasks and actions specified by this + # GemPackageTask. (+define+ is automatically called if a block is + # given to +new+). + def define + super + task :package => [:gem] + desc "Build the gem file #{gem_file}" + task :gem => ["#{package_dir}/#{gem_file}"] + file "#{package_dir}/#{gem_file}" => [package_dir] + @gem_spec.files do + when_writing("Creating GEM") { + Gem::Builder.new(gem_spec).build + verbose(true) { + mv gem_file, "#{package_dir}/#{gem_file}" + } + } + end + end + + def gem_file + if @gem_spec.platform == Gem::Platform::RUBY + "#{package_name}.gem" + else + "#{package_name}-#{@gem_spec.platform}.gem" + end + end + + end +end diff --git a/lib/rake/loaders/makefile.rb b/lib/rake/loaders/makefile.rb new file mode 100644 index 000000000..f66eb3b35 --- /dev/null +++ b/lib/rake/loaders/makefile.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby + +module Rake + + # Makefile loader to be used with the import file loader. + class MakefileLoader + + # Load the makefile dependencies in +fn+. + def load(fn) + buffer = '' + open(fn) do |mf| + mf.each do |line| + next if line =~ /^\s*#/ + buffer << line + if buffer =~ /\\$/ + buffer.sub!(/\\\n/, ' ') + state = :append + else + process_line(buffer) + buffer = '' + end + end + end + process_line(buffer) if buffer != '' + end + + private + + # Process one logical line of makefile data. + def process_line(line) + file_task, args = line.split(':') + return if args.nil? + dependents = args.split + file file_task => dependents + end + end + + # Install the handler + Rake.application.add_loader('mf', MakefileLoader.new) +end diff --git a/lib/rake/packagetask.rb b/lib/rake/packagetask.rb new file mode 100644 index 000000000..71b66a648 --- /dev/null +++ b/lib/rake/packagetask.rb @@ -0,0 +1,184 @@ +#!/usr/bin/env ruby + +# Define a package task libarary to aid in the definition of +# redistributable package files. + +require 'rake' +require 'rake/tasklib' + +module Rake + + # Create a packaging task that will package the project into + # distributable files (e.g zip archive or tar files). + # + # The PackageTask will create the following targets: + # + # [<b>:package</b>] + # Create all the requested package files. + # + # [<b>:clobber_package</b>] + # Delete all the package files. This target is automatically + # added to the main clobber target. + # + # [<b>:repackage</b>] + # Rebuild the package files from scratch, even if they are not out + # of date. + # + # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.tgz"</b>] + # Create a gzipped tar package (if <em>need_tar</em> is true). + # + # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.tar.gz"</b>] + # Create a gzipped tar package (if <em>need_tar_gz</em> is true). + # + # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.tar.bz2"</b>] + # Create a bzip2'd tar package (if <em>need_tar_bz2</em> is true). + # + # [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.zip"</b>] + # Create a zip package archive (if <em>need_zip</em> is true). + # + # Example: + # + # Rake::PackageTask.new("rake", "1.2.3") do |p| + # p.need_tar = true + # p.package_files.include("lib/**/*.rb") + # end + # + class PackageTask < TaskLib + # Name of the package (from the GEM Spec). + attr_accessor :name + + # Version of the package (e.g. '1.3.2'). + attr_accessor :version + + # Directory used to store the package files (default is 'pkg'). + attr_accessor :package_dir + + # True if a gzipped tar file (tgz) should be produced (default is false). + attr_accessor :need_tar + + # True if a gzipped tar file (tar.gz) should be produced (default is false). + attr_accessor :need_tar_gz + + # True if a bzip2'd tar file (tar.bz2) should be produced (default is false). + attr_accessor :need_tar_bz2 + + # True if a zip file should be produced (default is false) + attr_accessor :need_zip + + # List of files to be included in the package. + attr_accessor :package_files + + # Tar command for gzipped or bzip2ed archives. The default is 'tar'. + attr_accessor :tar_command + + # Zip command for zipped archives. The default is 'zip'. + attr_accessor :zip_command + + # Create a Package Task with the given name and version. + def initialize(name=nil, version=nil) + init(name, version) + yield self if block_given? + define unless name.nil? + end + + # Initialization that bypasses the "yield self" and "define" step. + def init(name, version) + @name = name + @version = version + @package_files = Rake::FileList.new + @package_dir = 'pkg' + @need_tar = false + @need_tar_gz = false + @need_tar_bz2 = false + @need_zip = false + @tar_command = 'tar' + @zip_command = 'zip' + end + + # Create the tasks defined by this task library. + def define + fail "Version required (or :noversion)" if @version.nil? + @version = nil if :noversion == @version + + desc "Build all the packages" + task :package + + desc "Force a rebuild of the package files" + task :repackage => [:clobber_package, :package] + + desc "Remove package products" + task :clobber_package do + rm_r package_dir rescue nil + end + + task :clobber => [:clobber_package] + + [ + [need_tar, tgz_file, "z"], + [need_tar_gz, tar_gz_file, "z"], + [need_tar_bz2, tar_bz2_file, "j"] + ].each do |(need, file, flag)| + if need + task :package => ["#{package_dir}/#{file}"] + file "#{package_dir}/#{file}" => [package_dir_path] + package_files do + chdir(package_dir) do + sh %{#{@tar_command} #{flag}cvf #{file} #{package_name}} + end + end + end + end + + if need_zip + task :package => ["#{package_dir}/#{zip_file}"] + file "#{package_dir}/#{zip_file}" => [package_dir_path] + package_files do + chdir(package_dir) do + sh %{#{@zip_command} -r #{zip_file} #{package_name}} + end + end + end + + directory package_dir + + file package_dir_path => @package_files do + mkdir_p package_dir rescue nil + @package_files.each do |fn| + f = File.join(package_dir_path, fn) + fdir = File.dirname(f) + mkdir_p(fdir) if !File.exist?(fdir) + if File.directory?(fn) + mkdir_p(f) + else + rm_f f + safe_ln(fn, f) + end + end + end + self + end + + def package_name + @version ? "#{@name}-#{@version}" : @name + end + + def package_dir_path + "#{package_dir}/#{package_name}" + end + + def tgz_file + "#{package_name}.tgz" + end + + def tar_gz_file + "#{package_name}.tar.gz" + end + + def tar_bz2_file + "#{package_name}.tar.bz2" + end + + def zip_file + "#{package_name}.zip" + end + end + +end diff --git a/lib/rake/rake_test_loader.rb b/lib/rake/rake_test_loader.rb new file mode 100644 index 000000000..8d7dad3c9 --- /dev/null +++ b/lib/rake/rake_test_loader.rb @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +# Load the test files from the command line. + +ARGV.each { |f| load f unless f =~ /^-/ } diff --git a/lib/rake/rdoctask.rb b/lib/rake/rdoctask.rb new file mode 100644 index 000000000..54adc6feb --- /dev/null +++ b/lib/rake/rdoctask.rb @@ -0,0 +1,147 @@ +#!/usr/bin/env ruby + +require 'rake' +require 'rake/tasklib' + +module Rake + + # Create a documentation task that will generate the RDoc files for + # a project. + # + # The RDocTask will create the following targets: + # + # [<b><em>rdoc</em></b>] + # Main task for this RDOC task. + # + # [<b>:clobber_<em>rdoc</em></b>] + # Delete all the rdoc files. This target is automatically + # added to the main clobber target. + # + # [<b>:re<em>rdoc</em></b>] + # Rebuild the rdoc files from scratch, even if they are not out + # of date. + # + # Simple Example: + # + # Rake::RDocTask.new do |rd| + # rd.main = "README.rdoc" + # rd.rdoc_files.include("README.rdoc", "lib/**/*.rb") + # end + # + # You may wish to give the task a different name, such as if you are + # generating two sets of documentation. For instance, if you want to have a + # development set of documentation including private methods: + # + # Rake::RDocTask.new(:rdoc_dev) do |rd| + # rd.main = "README.doc" + # rd.rdoc_files.include("README.rdoc", "lib/**/*.rb") + # rd.options << "--all" + # end + # + # The tasks would then be named :<em>rdoc_dev</em>, :clobber_<em>rdoc_dev</em>, and + # :re<em>rdoc_dev</em>. + # + class RDocTask < TaskLib + # Name of the main, top level task. (default is :rdoc) + attr_accessor :name + + # Name of directory to receive the html output files. (default is "html") + attr_accessor :rdoc_dir + + # Title of RDoc documentation. (default is none) + attr_accessor :title + + # Name of file to be used as the main, top level file of the + # RDoc. (default is none) + attr_accessor :main + + # Name of template to be used by rdoc. (default is 'html') + attr_accessor :template + + # List of files to be included in the rdoc generation. (default is []) + attr_accessor :rdoc_files + + # List of options to be passed rdoc. (default is []) + attr_accessor :options + + # Run the rdoc process as an external shell (default is false) + attr_accessor :external + + # Create an RDoc task named <em>rdoc</em>. Default task name is +rdoc+. + def initialize(name=:rdoc) # :yield: self + @name = name + @rdoc_files = Rake::FileList.new + @rdoc_dir = 'html' + @main = nil + @title = nil + @template = 'html' + @external = false + @options = [] + yield self if block_given? + define + end + + # Create the tasks defined by this task lib. + def define + if name.to_s != "rdoc" + desc "Build the RDOC HTML Files" + end + + desc "Build the #{name} HTML Files" + task name + + desc "Force a rebuild of the RDOC files" + task paste("re", name) => [paste("clobber_", name), name] + + desc "Remove rdoc products" + task paste("clobber_", name) do + rm_r rdoc_dir rescue nil + end + + task :clobber => [paste("clobber_", name)] + + directory @rdoc_dir + task name => [rdoc_target] + file rdoc_target => @rdoc_files + [$rakefile] do + rm_r @rdoc_dir rescue nil + args = option_list + @rdoc_files + if @external + argstring = args.join(' ') + sh %{ruby -Ivendor vender/rd #{argstring}} + else + require 'rdoc/rdoc' + RDoc::RDoc.new.document(args) + end + end + self + end + + def option_list + result = @options.dup + result << "-o" << @rdoc_dir + result << "--main" << quote(main) if main + result << "--title" << quote(title) if title + result << "-T" << quote(template) if template + result + end + + def quote(str) + if @external + "'#{str}'" + else + str + end + end + + def option_string + option_list.join(' ') + end + + private + + def rdoc_target + "#{rdoc_dir}/index.html" + end + + end +end diff --git a/lib/rake/ruby182_test_unit_fix.rb b/lib/rake/ruby182_test_unit_fix.rb new file mode 100755 index 000000000..f02c7879e --- /dev/null +++ b/lib/rake/ruby182_test_unit_fix.rb @@ -0,0 +1,23 @@ +module Test + module Unit + module Collector + class Dir + undef collect_file + def collect_file(name, suites, already_gathered) + # loadpath = $:.dup + dir = File.dirname(File.expand_path(name)) + $:.unshift(dir) unless $:.first == dir + if(@req) + @req.require(name) + else + require(name) + end + find_test_cases(already_gathered).each{|t| add_suite(suites, t.suite)} + ensure + # $:.replace(loadpath) + $:.delete_at $:.rindex(dir) + end + end + end + end +end diff --git a/lib/rake/runtest.rb b/lib/rake/runtest.rb new file mode 100644 index 000000000..3f1d20520 --- /dev/null +++ b/lib/rake/runtest.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +require 'test/unit' +require 'test/unit/assertions' + +module Rake + include Test::Unit::Assertions + + def run_tests(pattern='test/test*.rb', log_enabled=false) + Dir["#{pattern}"].each { |fn| + puts fn if log_enabled + begin + load fn + rescue Exception => ex + puts "Error in #{fn}: #{ex.message}" + puts ex.backtrace + assert false + end + } + end + + extend self +end diff --git a/lib/rake/tasklib.rb b/lib/rake/tasklib.rb new file mode 100644 index 000000000..465a58a0c --- /dev/null +++ b/lib/rake/tasklib.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +require 'rake' + +module Rake + + # Base class for Task Libraries. + class TaskLib + + include Cloneable + + # Make a symbol by pasting two strings together. + def paste(a,b) + (a.to_s + b.to_s).intern + end + end + +end diff --git a/lib/rake/testtask.rb b/lib/rake/testtask.rb new file mode 100644 index 000000000..79154e422 --- /dev/null +++ b/lib/rake/testtask.rb @@ -0,0 +1,161 @@ +#!/usr/bin/env ruby + +# Define a task library for running unit tests. + +require 'rake' +require 'rake/tasklib' + +module Rake + + # Create a task that runs a set of tests. + # + # Example: + # + # Rake::TestTask.new do |t| + # t.libs << "test" + # t.test_files = FileList['test/test*.rb'] + # t.verbose = true + # end + # + # If rake is invoked with a "TEST=filename" command line option, + # then the list of test files will be overridden to include only the + # filename specified on the command line. This provides an easy way + # to run just one test. + # + # If rake is invoked with a "TESTOPTS=options" command line option, + # then the given options are passed to the test process after a + # '--'. This allows Test::Unit options to be passed to the test + # suite. + # + # Examples: + # + # rake test # run tests normally + # rake test TEST=just_one_file.rb # run just one test file. + # rake test TESTOPTS="-v" # run in verbose mode + # rake test TESTOPTS="--runner=fox" # use the fox test runner + # + class TestTask < TaskLib + + # Name of test task. (default is :test) + attr_accessor :name + + # List of directories to added to $LOAD_PATH before running the + # tests. (default is 'lib') + attr_accessor :libs + + # True if verbose test output desired. (default is false) + attr_accessor :verbose + + # Test options passed to the test suite. An explicit + # TESTOPTS=opts on the command line will override this. (default + # is NONE) + attr_accessor :options + + # Request that the tests be run with the warning flag set. + # E.g. warning=true implies "ruby -w" used to run the tests. + attr_accessor :warning + + # Glob pattern to match test files. (default is 'test/test*.rb') + attr_accessor :pattern + + # Style of test loader to use. Options are: + # + # * :rake -- Rake provided test loading script (default). + # * :testrb -- Ruby provided test loading script. + # * :direct -- Load tests using command line loader. + # + attr_accessor :loader + + # Array of commandline options to pass to ruby when running test loader. + attr_accessor :ruby_opts + + # Explicitly define the list of test files to be included in a + # test. +list+ is expected to be an array of file names (a + # FileList is acceptable). If both +pattern+ and +test_files+ are + # used, then the list of test files is the union of the two. + def test_files=(list) + @test_files = list + end + + # Create a testing task. + def initialize(name=:test) + @name = name + @libs = ["lib"] + @pattern = nil + @options = nil + @test_files = nil + @verbose = false + @warning = false + @loader = :rake + @ruby_opts = [] + yield self if block_given? + @pattern = 'test/test*.rb' if @pattern.nil? && @test_files.nil? + define + end + + # Create the tasks defined by this task lib. + def define + lib_path = @libs.join(File::PATH_SEPARATOR) + desc "Run tests" + (@name==:test ? "" : " for #{@name}") + task @name do + run_code = '' + RakeFileUtils.verbose(@verbose) do + run_code = + case @loader + when :direct + "-e 'ARGV.each{|f| load f}'" + when :testrb + "-S testrb #{fix}" + when :rake + rake_loader + end + @ruby_opts.unshift( "-I#{lib_path}" ) + @ruby_opts.unshift( "-w" ) if @warning + ruby @ruby_opts.join(" ") + + " \"#{run_code}\" " + + file_list.collect { |fn| "\"#{fn}\"" }.join(' ') + + " #{option_list}" + end + end + self + end + + def option_list # :nodoc: + ENV['TESTOPTS'] || @options || "" + end + + def file_list # :nodoc: + if ENV['TEST'] + FileList[ ENV['TEST'] ] + else + result = [] + result += @test_files.to_a if @test_files + result += FileList[ @pattern ].to_a if @pattern + FileList[result] + end + end + + def fix # :nodoc: + case RUBY_VERSION + when '1.8.2' + find_file 'rake/ruby182_test_unit_fix' + else + nil + end || '' + end + + def rake_loader # :nodoc: + find_file('rake/rake_test_loader') or + fail "unable to find rake test loader" + end + + def find_file(fn) # :nodoc: + $LOAD_PATH.each do |path| + file_path = File.join(path, "#{fn}.rb") + return file_path if File.exist? file_path + end + nil + end + + end +end |