mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
328 lines
9.5 KiB
Ruby
328 lines
9.5 KiB
Ruby
|
require 'rake/invocation_exception_mixin'
|
||
|
|
||
|
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
|
||
|
|
||
|
# List of actions attached to a task.
|
||
|
attr_reader :actions
|
||
|
|
||
|
# 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
|
||
|
|
||
|
# File/Line locations of each of the task definitions for this
|
||
|
# task (only valid if the task was defined with the detect
|
||
|
# location option set).
|
||
|
attr_reader :locations
|
||
|
|
||
|
# 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
|
||
|
|
||
|
# List of prerequisite tasks
|
||
|
def prerequisite_tasks
|
||
|
prerequisites.collect { |pre| lookup_prerequisite(pre) }
|
||
|
end
|
||
|
|
||
|
def lookup_prerequisite(prerequisite_name)
|
||
|
application[prerequisite_name, @scope]
|
||
|
end
|
||
|
private :lookup_prerequisite
|
||
|
|
||
|
# 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 = []
|
||
|
@actions = []
|
||
|
@already_invoked = false
|
||
|
@full_comment = nil
|
||
|
@comment = nil
|
||
|
@lock = Monitor.new
|
||
|
@application = app
|
||
|
@scope = app.current_scope
|
||
|
@arg_names = nil
|
||
|
@locations = []
|
||
|
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
|
||
|
|
||
|
# Reenable the task, allowing its tasks to be executed if the task
|
||
|
# is invoked again.
|
||
|
def reenable
|
||
|
@already_invoked = false
|
||
|
end
|
||
|
|
||
|
# Clear the existing prerequisites and actions of a rake task.
|
||
|
def clear
|
||
|
clear_prerequisites
|
||
|
clear_actions
|
||
|
self
|
||
|
end
|
||
|
|
||
|
# Clear the existing prerequisites of a rake task.
|
||
|
def clear_prerequisites
|
||
|
prerequisites.clear
|
||
|
self
|
||
|
end
|
||
|
|
||
|
# Clear the existing actions on a rake task.
|
||
|
def clear_actions
|
||
|
actions.clear
|
||
|
self
|
||
|
end
|
||
|
|
||
|
# Invoke the task if it is needed. Prerequisites 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) # :nodoc:
|
||
|
new_chain = InvocationChain.append(self, invocation_chain)
|
||
|
@lock.synchronize do
|
||
|
if application.options.trace
|
||
|
$stderr.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
|
||
|
rescue Exception => ex
|
||
|
add_chain_to(ex, new_chain)
|
||
|
raise ex
|
||
|
end
|
||
|
protected :invoke_with_call_chain
|
||
|
|
||
|
def add_chain_to(exception, new_chain)
|
||
|
exception.extend(InvocationExceptionMixin) unless exception.respond_to?(:chain)
|
||
|
exception.chain = new_chain if exception.chain.nil?
|
||
|
end
|
||
|
private :add_chain_to
|
||
|
|
||
|
# Invoke all the prerequisites of a task.
|
||
|
def invoke_prerequisites(task_args, invocation_chain) # :nodoc:
|
||
|
prerequisite_tasks.each { |prereq|
|
||
|
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=nil)
|
||
|
args ||= EMPTY_TASK_ARGS
|
||
|
if application.options.dryrun
|
||
|
$stderr.puts "** Execute (dry run) #{name}"
|
||
|
return
|
||
|
end
|
||
|
if application.options.trace
|
||
|
$stderr.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
|
||
|
prerequisite_tasks.collect { |pre| pre.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 already 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 = prerequisite_tasks
|
||
|
prereqs.sort! {|a,b| a.timestamp <=> b.timestamp}
|
||
|
prereqs.each do |p|
|
||
|
result << "--#{p.name} (#{p.timestamp})\n"
|
||
|
end
|
||
|
latest_prereq = prerequisite_tasks.collect { |pre| pre.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
|
||
|
end
|