3 # Original is https://github.com/jimweirich/rake/
4 # Copyright (c) 2003 Jim Weirich
10 $rake_fiber_table = {}
16 return self.dup if ['.', '..'].include? self
18 newext = (newext =~ /^\./) ? newext : ("." + newext)
20 self.chomp(File.extname(self)) << newext
23 def pathmap(spec=nil, &block)
24 return self if spec.nil?
26 spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag|
29 result << File.basename(self)
31 result << File.basename(self).ext
33 result << File.dirname(self)
35 result << File.extname(self)
41 result << (File::ALT_SEPARATOR || File::SEPARATOR)
47 result << pathmap_partial($1.to_i)
48 when /^%\{([^}]*)\}(\d*[dpfnxX])/
49 patterns, operator = $1, $2
50 result << pathmap('%' + operator).pathmap_replace(patterns, &block)
52 fail ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'"
66 # List of prerequisites for a task.
67 attr_reader :prerequisites
69 # Source dependency for rule synthesized tasks. Nil if task was not
70 # sythesized from a rule.
73 # Create a task named +task_name+ with no actions or prerequisites..
74 # use +enhance+ to add actions and prerequisites.
75 def initialize(task_name)
81 # Enhance a task with prerequisites or actions. Returns self.
82 def enhance(deps=nil, &block)
83 @prerequisites |= deps if deps
84 @actions << block if block_given?
94 def running?; @running end
96 # Invoke the task if it is needed. Prerequisites are invoked first.
98 puts "Invoke #{name} (already=[#{@already_invoked}], needed=[#{needed?}])" if $trace
99 return if @already_invoked
100 prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten
101 prerequisites.each do |n|
104 return prerequisites.select{|v| v = Task[v]; v && (!v.done? || !v.running?) }
108 @already_invoked = true
115 $rake_root_fiber.transfer
125 # Execute the actions associated with this task.
127 puts "Execute #{name}" if $trace
128 self.class.enhance_with_matching_rule(name) if @actions.empty?
130 @actions.each { |act| act.call(self) }
136 # Is this task needed?
141 # Timestamp for this task. Basic tasks return the current time for
142 # their time stamp. Other tasks can be more sophisticated.
147 # Class Methods ----------------------------------------------------
151 # Clear the task list. This cause rake to immediately forget all
152 # the tasks that have been assigned. (Normally used in the unit
159 # List of all defined tasks.
161 TASKS.keys.sort.collect { |tn| Task[tn] }
164 # Return a task with the given name. If the task is not currently
165 # known, try to synthesize one from the defined rules. If no
166 # rules are found, but an existing file matches the task name,
167 # assume it is a file task with no dependencies or actions.
169 task_name = task_name.to_s
170 if task = TASKS[task_name]
173 if task = enhance_with_matching_rule(task_name)
176 if File.exist?(task_name)
177 return FileTask.define_task(task_name)
179 fail "Don't know how to rake #{task_name}"
182 # Define a task given +args+ and an option block. If a rule with
183 # the given name already exists, the prerequisites and actions are
184 # added to the existing task.
185 def define_task(args, &block)
186 task_name, deps = resolve_args(args)
187 lookup(task_name).enhance([deps].flatten, &block)
190 # Define a rule for synthesizing tasks.
191 def create_rule(args, &block)
192 pattern, deps = resolve_args(args)
193 pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern
194 RULES << [pattern, deps, block]
198 # Lookup a task. Return an existing task if found, otherwise
199 # create a task of the current type.
200 def lookup(task_name)
201 name = task_name.to_s
202 TASKS[name] ||= self.new(name)
205 # If a rule can be found that matches the task name, enhance the
206 # task with the prerequisites and actions from the rule. Set the
207 # source attribute of the task appropriately for the rule. Return
208 # the enhanced task or nil of no rule was found.
209 def enhance_with_matching_rule(task_name)
210 RULES.each do |pattern, extensions, block|
211 if pattern.match(task_name)
212 ext = extensions.first
213 deps = extensions[1..-1]
216 source = task_name.sub(/\.[^.]*$/, ext)
218 source = ext.call(task_name)
220 fail "Don't know how to handle rule dependent: #{ext.inspect}"
222 if File.exist?(source)
223 task = FileTask.define_task({task_name => [source]+deps}, &block)
234 # Resolve the arguments for a task/rule.
235 def resolve_args(args)
238 fail "Too Many Task Names: #{args.keys.join(' ')}" if args.size > 1
239 fail "No Task Name Given" if args.size < 1
240 task_name = args.keys[0]
241 deps = args[task_name]
242 deps = [deps] if (String===deps) || (Regexp===deps) || (Proc===deps)
253 ######################################################################
254 class FileTask < Task
255 # Is this file task needed? Yes if it doesn't exist, or if its time
256 # stamp is out of date.
258 return true unless File.exist?(name)
259 prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten
260 latest_prereq = prerequisites.collect{|n| Task[n].timestamp}.max
261 return false if latest_prereq.nil?
262 timestamp < latest_prereq
265 # Time stamp for file task.
267 return Time.at(0) unless File.exist?(name)
268 stat = File::stat(name.to_s)
269 stat.directory? ? Time.at(0) : stat.mtime
274 # Declare a basic task.
275 def task(args, &block)
276 MiniRake::Task.define_task(args, &block)
279 # Declare a file task.
280 def file(args, &block)
281 MiniRake::FileTask.define_task(args, &block)
284 # Declare a set of files tasks to create the given directories on
286 def directory(args, &block)
287 MiniRake::FileTask.define_task(args) do |t|
288 block.call(t) unless block.nil?
289 dir = args.is_a?(Hash) ? args.keys.first : args
290 (dir.split(File::SEPARATOR) + ['']).inject do |acc, part|
291 (acc + File::SEPARATOR).tap do |d|
292 Dir.mkdir(d) unless File.exists? d
298 # Declare a rule for auto-tasks.
299 def rule(args, &block)
300 MiniRake::Task.create_rule(args, &block)
303 # Write a message to standard out if $verbose is enabled.
305 print " " if $trace && $verbose
309 # Run the system command +cmd+.
313 if !$rake_root_fiber || Fiber.current == $rake_root_fiber
314 system(cmd) or fail "Command Failed: [#{cmd}]"
318 pid = Process.spawn(cmd)
319 $rake_fiber_table[pid] = {
320 fiber: Fiber.current,
322 process_waiter: Process.detach(pid)
324 $rake_root_fiber.transfer
336 ######################################################################
337 # Task Definition Functions ...
339 ######################################################################
340 # Rake main application object. When invoking +rake+ from the command
341 # line, a RakeApp object is created and run.
344 RAKEFILES = ['rakefile', 'Rakefile']
347 ['--dry-run', '-n', GetoptLong::NO_ARGUMENT,
348 "Do a dry run without executing actions."],
349 ['--help', '-H', GetoptLong::NO_ARGUMENT,
350 "Display this help message."],
351 ['--libdir', '-I', GetoptLong::REQUIRED_ARGUMENT,
352 "Include LIBDIR in the search path for required modules."],
353 ['--nosearch', '-N', GetoptLong::NO_ARGUMENT,
354 "Do not search parent directories for the Rakefile."],
355 ['--quiet', '-q', GetoptLong::NO_ARGUMENT,
356 "Do not log messages to standard output (default)."],
357 ['--rakefile', '-f', GetoptLong::REQUIRED_ARGUMENT,
358 "Use FILE as the rakefile."],
359 ['--require', '-r', GetoptLong::REQUIRED_ARGUMENT,
360 "Require MODULE before executing rakefile."],
361 ['--tasks', '-T', GetoptLong::NO_ARGUMENT,
362 "Display the tasks and dependencies, then exit."],
363 ['--pull-gems','-p', GetoptLong::NO_ARGUMENT,
364 "Pull all git mrbgems."],
365 ['--trace', '-t', GetoptLong::NO_ARGUMENT,
366 "Turn on invoke/execute tracing."],
367 ['--usage', '-h', GetoptLong::NO_ARGUMENT,
369 ['--verbose', '-v', GetoptLong::NO_ARGUMENT,
370 "Log message to standard output."],
371 ['--directory', '-C', GetoptLong::REQUIRED_ARGUMENT,
372 "Change executing directory of rakefiles."],
373 ['--jobs', '-j', GetoptLong::REQUIRED_ARGUMENT,
374 'Execute rake with parallel jobs.']
377 # Create a RakeApp object.
383 # True if one of the files in RAKEFILES is in the current directory.
384 # If a match is found, it is copied into @rakefile.
386 RAKEFILES.each do |fn|
395 # Display the program usage line.
397 puts "rake [-f rakefile] {options} targets..."
400 # Display the rake command line help.
404 puts "Options are ..."
406 OPTIONS.sort.each do |long, short, mode, desc|
407 if mode == GetoptLong::REQUIRED_ARGUMENT
408 if desc =~ /\b([A-Z]{2,})\b/
409 long = long + "=#{$1}"
412 printf " %-20s (%s)\n", long, short
417 # Display the tasks and dependencies.
419 MiniRake::Task.tasks.each do |t|
420 puts "#{t.class} #{t.name}"
421 t.prerequisites.each { |pre| puts " #{pre}" }
425 # Return a list of the command line options supported by the
427 def command_line_options
428 OPTIONS.collect { |lst| lst[0..-2] }
431 # Do the option defined by +opt+ and +value+.
432 def do_option(opt, value)
463 puts "rake, version #{RAKEVERSION}"
468 $rake_jobs = [value.to_i, 1].max
470 fail "Unknown option: #{opt}"
474 # Read and handle the command line options.
478 opts = GetoptLong.new(*command_line_options)
479 opts.each { |opt, value| do_option(opt, value) }
482 # Run the +rake+ application.
486 unless $rake_root_fiber
488 $rake_root_fiber = Fiber.current
493 while ! have_rakefile
495 if Dir.pwd == here || @nosearch
496 fail "No Rakefile found (looking for: #{RAKEFILES.join(', ')})"
501 ARGV.each do |task_name|
502 if /^(\w+)=(.*)/.match(task_name)
505 root_tasks << task_name
508 puts "(in #{Dir.pwd})"
509 $rakefile = @rakefile
514 root_tasks.push("default") if root_tasks.empty?
515 # revese tasks for popping
519 until root_tasks.empty?
520 root_name = root_tasks.pop
523 task_name = tasks.pop
524 t = MiniRake::Task[task_name]
527 # append additional tasks to task queue
533 unless f.kind_of? Fiber
534 tasks.insert 0, task_name unless t.done?
535 if root_name == task_name
541 wait_process while $rake_fiber_table.size >= $rake_jobs
547 wait_process until $rake_fiber_table.empty?
549 rescue Exception => e
552 wait_process until $rake_fiber_table.empty?
553 rescue Exception => next_e
559 return if $rake_failed.empty?
562 $rake_failed.each do |ex|
564 if $trace || $verbose
565 puts ex.backtrace.join("\n")
567 puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || ""
573 def wait_process(count = 0)
574 dur = [0.0001 * (10 ** count), 1].min
578 $rake_fiber_table.each do |pid, v|
579 exited << pid unless v[:process_waiter].alive?
583 ent = $rake_fiber_table.delete pid
584 st = ent[:process_waiter].value
586 # ignore process that isn't created by `sh` method
589 if st.exitstatus != 0
590 raise "Command Failed: [#{ent[:command]}]"
593 fail 'task scheduling bug!' if $rake_fiber_table.size >= $rake_jobs
598 wait_process(count + 1) if !$rake_fiber_table.empty? && exited.empty?
603 if __FILE__ == $0 then