require 'getoptlong'
require 'fileutils'
$rake_fiber_table = {}
$rake_jobs = 1
$rake_failed = []
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 '%-'
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
attr_reader :prerequisites
attr_accessor :source
def initialize(task_name)
@name = task_name
@prerequisites = []
@actions = []
end
def enhance(deps=nil, &block)
@prerequisites |= deps if deps
@actions << block if block_given?
self
end
def name
@name.to_s
end
def done?; @done end
def running?; @running end
def invoke
puts "Invoke #{name} (already=[#{@already_invoked}], needed=[#{needed?}])" if $trace
return if @already_invoked
prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten
prerequisites.each do |n|
t = Task[n]
unless t.done?
return prerequisites.select{|v| v = Task[v]; v && (!v.done? || !v.running?) }
end
end
@already_invoked = true
if needed?
@running = true
if $rake_root_fiber
return Fiber.new do
self.execute
$rake_root_fiber.transfer
end
else
self.execute
end
end
@done = true
end
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
@done = true
@running = false
end
def needed?
true
end
def timestamp
Time.now
end
class << self
def clear
TASKS.clear
RULES.clear
end
def tasks
TASKS.keys.sort.collect { |tn| Task[tn] }
end
def [](task_name)
task_name = task_name.to_s
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
def define_task(args, &block)
task_name, deps = resolve_args(args)
lookup(task_name).enhance([deps].flatten, &block)
end
def create_rule(args, &block)
pattern, deps = resolve_args(args)
pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern
RULES << [pattern, deps, block]
end
def lookup(task_name)
name = task_name.to_s
TASKS[name] ||= self.new(name)
end
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
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
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
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
def task(args, &block)
MiniRake::Task.define_task(args, &block)
end
def file(args, &block)
MiniRake::FileTask.define_task(args, &block)
end
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
def rule(args, &block)
MiniRake::Task.create_rule(args, &block)
end
def log(msg)
print " " if $trace && $verbose
puts msg if $verbose
end
def sh(cmd)
puts cmd if $verbose
if !$rake_root_fiber || Fiber.current == $rake_root_fiber
system(cmd) or fail "Command Failed: [#{cmd}]"
return
end
pid = Process.spawn(cmd)
$rake_fiber_table[pid] = {
fiber: Fiber.current,
command: cmd,
process_waiter: Process.detach(pid)
}
$rake_root_fiber.transfer
end
def desc(text)
end
end
end
Rake = MiniRake
extend MiniRake::DSL
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."],
['--jobs', '-j', GetoptLong::REQUIRED_ARGUMENT,
'Execute rake with parallel jobs.']
]
def initialize
@rakefile = nil
@nosearch = false
end
def have_rakefile
RAKEFILES.each do |fn|
if File.exist?(fn)
@rakefile = fn
return true
end
end
return false
end
def usage
puts "rake [-f rakefile] {options} targets..."
end
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
def display_tasks
MiniRake::Task.tasks.each do |t|
puts "#{t.class} #{t.name}"
t.prerequisites.each { |pre| puts " #{pre}" }
end
end
def command_line_options
OPTIONS.collect { |lst| lst[0..-2] }
end
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
when '--jobs'
$rake_jobs = [value.to_i, 1].max
else
fail "Unknown option: #{opt}"
end
end
def handle_options
$verbose = false
$pull_gems = false
opts = GetoptLong.new(*command_line_options)
opts.each { |opt, value| do_option(opt, value) }
end
def run
handle_options
unless $rake_root_fiber
require 'fiber'
$rake_root_fiber = Fiber.current
end
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
root_tasks = []
ARGV.each do |task_name|
if /^(\w+)=(.*)/.match(task_name)
ENV[$1] = $2
else
root_tasks << task_name
end
end
puts "(in #{Dir.pwd})"
$rakefile = @rakefile
load @rakefile
if $show_tasks
display_tasks
else
root_tasks.push("default") if root_tasks.empty?
root_tasks.reverse!
tasks = []
until root_tasks.empty?
root_name = root_tasks.pop
tasks << root_name
until tasks.empty?
task_name = tasks.pop
t = MiniRake::Task[task_name]
f = t.invoke
if f.kind_of?(Array)
tasks.push(*f)
tasks.uniq!
end
unless f.kind_of? Fiber
tasks.insert 0, task_name unless t.done?
if root_name == task_name
wait_process
end
next
end
wait_process while $rake_fiber_table.size >= $rake_jobs
f.transfer
end
end
wait_process until $rake_fiber_table.empty?
end
rescue Exception => e
begin
$rake_failed << e
wait_process until $rake_fiber_table.empty?
rescue Exception => next_e
e = next_e
retry
end
end
return if $rake_failed.empty?
puts "rake aborted!"
$rake_failed.each do |ex|
puts ex.message
if $trace || $verbose
puts ex.backtrace.join("\n")
else
puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || ""
end
end
exit 1
end
def wait_process(count = 0)
dur = [0.0001 * (10 ** count), 1].min
sleep dur
exited = []
$rake_fiber_table.each do |pid, v|
exited << pid unless v[:process_waiter].alive?
end
exited.each do |pid|
ent = $rake_fiber_table.delete pid
st = ent[:process_waiter].value
return if ent.nil?
if st.exitstatus != 0
raise "Command Failed: [#{ent[:command]}]"
end
fail 'task scheduling bug!' if $rake_fiber_table.size >= $rake_jobs
ent[:fiber].transfer
end
wait_process(count + 1) if !$rake_fiber_table.empty? && exited.empty?
end
end
if __FILE__ == $0 then
RakeApp.new.run
end