#!/usr/bin/env ruby # Original is https://github.com/jimweirich/rake/ # Copyright (c) 2003 Jim Weirich # License: MIT-LICENSE require 'getoptlong' require 'fileutils' class String def ext(newext='') return self.dup if ['.', '..'].include? self if newext != '' newext = (newext =~ /^\./) ? newext : ("." + newext) end self.chomp(File.extname(self)) << newext end 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 << File.extname(self) when '%X' result << self.ext 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 module MiniRake class Task TASKS = Hash.new RULES = Array.new # List of prerequisites for a task. attr_reader :prerequisites # Source dependency for rule synthesized tasks. Nil if task was not # sythesized from a rule. attr_accessor :source # Create a task named +task_name+ with no actions or prerequisites.. # use +enhance+ to add actions and prerequisites. def initialize(task_name) @name = task_name @prerequisites = [] @actions = [] 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. def name @name.to_s end # Invoke the task if it is needed. Prerequites are invoked first. def invoke puts "Invoke #{name} (already=[#{@already_invoked}], needed=[#{needed?}])" if $trace return if @already_invoked @already_invoked = true prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten prerequisites.each { |n| Task[n].invoke } execute if needed? end # Execute the actions associated with this task. def execute puts "Execute #{name}" if $trace self.class.enhance_with_matching_rule(name) if @actions.empty? unless $dryrun @actions.each { |act| act.call(self) } 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 Time.now end # Class 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 TASKS.clear RULES.clear end # List of all defined tasks. def tasks TASKS.keys.sort.collect { |tn| Task[tn] } 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) task_name = task_name.to_s if task_name.end_with?(":") task_name = task_name.slice(0, task_name.length - 1) end if task = TASKS[task_name] return task end if task = enhance_with_matching_rule(task_name) return task end if File.exist?(task_name) return FileTask.define_task(task_name) end fail "Don't know how to rake #{task_name}" 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. def define_task(args, &block) task_name, deps = resolve_args(args) lookup(task_name).enhance([deps].flatten, &block) end # Define a rule for synthesizing tasks. def create_rule(args, &block) pattern, deps = resolve_args(args) pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern RULES << [pattern, deps, block] end # Lookup a task. Return an existing task if found, otherwise # create a task of the current type. def lookup(task_name) name = task_name.to_s TASKS[name] ||= self.new(name) 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) RULES.each do |pattern, extensions, block| if pattern.match(task_name) ext = extensions.first deps = extensions[1..-1] case ext when String source = task_name.sub(/\.[^.]*$/, ext) when Proc source = ext.call(task_name) else fail "Don't know how to handle rule dependent: #{ext.inspect}" end if File.exist?(source) task = FileTask.define_task({task_name => [source]+deps}, &block) task.source = source return task end end end nil end private # Resolve the arguments for a task/rule. def resolve_args(args) case args when Hash fail "Too Many Task Names: #{args.keys.join(' ')}" if args.size > 1 fail "No Task Name Given" if args.size < 1 task_name = args.keys[0] deps = args[task_name] deps = [deps] if (String===deps) || (Regexp===deps) || (Proc===deps) else task_name = args deps = [] end [task_name, deps] end end end ###################################################################### 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) prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten latest_prereq = prerequisites.collect{|n| Task[n].timestamp}.max return false if latest_prereq.nil? timestamp < latest_prereq end # Time stamp for file task. def timestamp return Time.at(0) unless File.exist?(name) stat = File::stat(name.to_s) stat.directory? ? Time.at(0) : stat.mtime end end module DSL # Declare a basic task. def task(args, &block) MiniRake::Task.define_task(args, &block) end # Declare a file task. def file(args, &block) MiniRake::FileTask.define_task(args, &block) end # Declare a set of files tasks to create the given directories on # demand. def directory(args, &block) MiniRake::FileTask.define_task(args) do |t| block.call(t) unless block.nil? dir = args.is_a?(Hash) ? args.keys.first : args (dir.split(File::SEPARATOR) + ['']).inject do |acc, part| (acc + File::SEPARATOR).tap do |d| Dir.mkdir(d) unless File.exists? d end + part end end end # Declare a rule for auto-tasks. def rule(args, &block) MiniRake::Task.create_rule(args, &block) end # Write a message to standard out if $verbose is enabled. def log(msg) print " " if $trace && $verbose puts msg if $verbose end # Run the system command +cmd+. def sh(cmd) puts cmd if $verbose system(cmd) or fail "Command Failed: [#{cmd}]" end def desc(text) end end end Rake = MiniRake extend MiniRake::DSL ###################################################################### # Task Definition Functions ... ###################################################################### # Rake main application object. When invoking +rake+ from the command # line, a RakeApp object is created and run. # class RakeApp RAKEFILES = ['rakefile', 'Rakefile'] OPTIONS = [ ['--dry-run', '-n', GetoptLong::NO_ARGUMENT, "Do a dry run without executing actions."], ['--help', '-H', GetoptLong::NO_ARGUMENT, "Display this help message."], ['--libdir', '-I', GetoptLong::REQUIRED_ARGUMENT, "Include LIBDIR in the search path for required modules."], ['--nosearch', '-N', GetoptLong::NO_ARGUMENT, "Do not search parent directories for the Rakefile."], ['--quiet', '-q', GetoptLong::NO_ARGUMENT, "Do not log messages to standard output (default)."], ['--rakefile', '-f', GetoptLong::REQUIRED_ARGUMENT, "Use FILE as the rakefile."], ['--require', '-r', GetoptLong::REQUIRED_ARGUMENT, "Require MODULE before executing rakefile."], ['--tasks', '-T', GetoptLong::NO_ARGUMENT, "Display the tasks and dependencies, then exit."], ['--pull-gems','-p', GetoptLong::NO_ARGUMENT, "Pull all git mrbgems."], ['--trace', '-t', GetoptLong::NO_ARGUMENT, "Turn on invoke/execute tracing."], ['--usage', '-h', GetoptLong::NO_ARGUMENT, "Display usage."], ['--verbose', '-v', GetoptLong::NO_ARGUMENT, "Log message to standard output."], ['--directory', '-C', GetoptLong::REQUIRED_ARGUMENT, "Change executing directory of rakefiles."] ] # Create a RakeApp object. def initialize @rakefile = nil @nosearch = false 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) @rakefile = fn return true end end return false end # Display the program usage line. def usage puts "rake [-f rakefile] {options} targets..." end # Display the rake command line help. def help usage 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 MiniRake::Task.tasks.each do |t| puts "#{t.class} #{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 '--dry-run' $dryrun = true $trace = true when '--help' help exit when '--libdir' $:.push(value) when '--nosearch' @nosearch = true when '--quiet' $verbose = false when '--rakefile' RAKEFILES.clear RAKEFILES << value when '--require' require value when '--tasks' $show_tasks = true when '--pull-gems' $pull_gems = true when '--trace' $trace = true when '--usage' usage exit when '--verbose' $verbose = true when '--version' puts "rake, version #{RAKEVERSION}" exit when '--directory' Dir.chdir value else fail "Unknown option: #{opt}" end end # Read and handle the command line options. def handle_options $verbose = false $pull_gems = false opts = GetoptLong.new(*command_line_options) opts.each { |opt, value| do_option(opt, value) } end # Run the +rake+ application. def run handle_options begin here = Dir.pwd while ! have_rakefile Dir.chdir("..") if Dir.pwd == here || @nosearch fail "No Rakefile found (looking for: #{RAKEFILES.join(', ')})" end here = Dir.pwd end tasks = [] ARGV.each do |task_name| if /^(\w+)=(.*)/.match(task_name) ENV[$1] = $2 else tasks << task_name end end puts "(in #{Dir.pwd})" $rakefile = @rakefile load @rakefile if $show_tasks display_tasks else tasks.push("default") if tasks.size == 0 tasks.each do |task_name| MiniRake::Task[task_name].invoke end end rescue Exception => ex puts "rake aborted!" puts ex.message if $trace puts ex.backtrace.join("\n") else puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || "" end exit 1 end end end if __FILE__ == $0 then RakeApp.new.run end