1require 'rake/invocation_exception_mixin'
2
3module Rake
4
5  # #########################################################################
6  # A Task is the basic unit of work in a Rakefile.  Tasks have associated
7  # actions (possibly more than one) and a list of prerequisites.  When
8  # invoked, a task will first ensure that all of its prerequisites have an
9  # opportunity to run and then it will execute its own actions.
10  #
11  # Tasks are not usually created directly using the new method, but rather
12  # use the +file+ and +task+ convenience methods.
13  #
14  class Task
15    # List of prerequisites for a task.
16    attr_reader :prerequisites
17
18    # List of actions attached to a task.
19    attr_reader :actions
20
21    # Application owning this task.
22    attr_accessor :application
23
24    # Comment for this task.  Restricted to a single line of no more than 50
25    # characters.
26    attr_reader :comment
27
28    # Full text of the (possibly multi-line) comment.
29    attr_reader :full_comment
30
31    # Array of nested namespaces names used for task lookup by this task.
32    attr_reader :scope
33
34    # File/Line locations of each of the task definitions for this
35    # task (only valid if the task was defined with the detect
36    # location option set).
37    attr_reader :locations
38
39    # Return task name
40    def to_s
41      name
42    end
43
44    def inspect
45      "<#{self.class} #{name} => [#{prerequisites.join(', ')}]>"
46    end
47
48    # List of sources for task.
49    attr_writer :sources
50    def sources
51      @sources ||= []
52    end
53
54    # List of prerequisite tasks
55    def prerequisite_tasks
56      prerequisites.collect { |pre| lookup_prerequisite(pre) }
57    end
58
59    def lookup_prerequisite(prerequisite_name)
60      application[prerequisite_name, @scope]
61    end
62    private :lookup_prerequisite
63
64    # First source from a rule (nil if no sources)
65    def source
66      @sources.first if defined?(@sources)
67    end
68
69    # Create a task named +task_name+ with no actions or prerequisites. Use
70    # +enhance+ to add actions and prerequisites.
71    def initialize(task_name, app)
72      @name = task_name.to_s
73      @prerequisites = []
74      @actions = []
75      @already_invoked = false
76      @full_comment = nil
77      @comment = nil
78      @lock = Monitor.new
79      @application = app
80      @scope = app.current_scope
81      @arg_names = nil
82      @locations = []
83    end
84
85    # Enhance a task with prerequisites or actions.  Returns self.
86    def enhance(deps=nil, &block)
87      @prerequisites |= deps if deps
88      @actions << block if block_given?
89      self
90    end
91
92    # Name of the task, including any namespace qualifiers.
93    def name
94      @name.to_s
95    end
96
97    # Name of task with argument list description.
98    def name_with_args # :nodoc:
99      if arg_description
100        "#{name}#{arg_description}"
101      else
102        name
103      end
104    end
105
106    # Argument description (nil if none).
107    def arg_description # :nodoc:
108      @arg_names ? "[#{arg_names.join(',')}]" : nil
109    end
110
111    # Name of arguments for this task.
112    def arg_names
113      @arg_names || []
114    end
115
116    # Reenable the task, allowing its tasks to be executed if the task
117    # is invoked again.
118    def reenable
119      @already_invoked = false
120    end
121
122    # Clear the existing prerequisites and actions of a rake task.
123    def clear
124      clear_prerequisites
125      clear_actions
126      clear_comments
127      self
128    end
129
130    # Clear the existing prerequisites of a rake task.
131    def clear_prerequisites
132      prerequisites.clear
133      self
134    end
135
136    # Clear the existing actions on a rake task.
137    def clear_actions
138      actions.clear
139      self
140    end
141
142    # Clear the existing comments on a rake task.
143    def clear_comments
144      @full_comment = nil
145      @comment = nil
146      self
147    end
148
149    # Invoke the task if it is needed.  Prerequisites are invoked first.
150    def invoke(*args)
151      task_args = TaskArguments.new(arg_names, args)
152      invoke_with_call_chain(task_args, InvocationChain::EMPTY)
153    end
154
155    # Same as invoke, but explicitly pass a call chain to detect
156    # circular dependencies.
157    def invoke_with_call_chain(task_args, invocation_chain) # :nodoc:
158      new_chain = InvocationChain.append(self, invocation_chain)
159      @lock.synchronize do
160        if application.options.trace
161          application.trace "** Invoke #{name} #{format_trace_flags}"
162        end
163        return if @already_invoked
164        @already_invoked = true
165        invoke_prerequisites(task_args, new_chain)
166        execute(task_args) if needed?
167      end
168    rescue Exception => ex
169      add_chain_to(ex, new_chain)
170      raise ex
171    end
172    protected :invoke_with_call_chain
173
174    def add_chain_to(exception, new_chain)
175      exception.extend(InvocationExceptionMixin) unless exception.respond_to?(:chain)
176      exception.chain = new_chain if exception.chain.nil?
177    end
178    private :add_chain_to
179
180    # Invoke all the prerequisites of a task.
181    def invoke_prerequisites(task_args, invocation_chain) # :nodoc:
182      if application.options.always_multitask
183        invoke_prerequisites_concurrently(task_args, invocation_chain)
184      else
185        prerequisite_tasks.each { |p|
186          prereq_args = task_args.new_scope(p.arg_names)
187          p.invoke_with_call_chain(prereq_args, invocation_chain)
188        }
189      end
190    end
191
192    # Invoke all the prerequisites of a task in parallel.
193    def invoke_prerequisites_concurrently(task_args, invocation_chain) # :nodoc:
194      futures = prerequisite_tasks.collect do |p|
195        prereq_args = task_args.new_scope(p.arg_names)
196        application.thread_pool.future(p) do |r|
197          r.invoke_with_call_chain(prereq_args, invocation_chain)
198        end
199      end
200      futures.each { |f| f.value }
201    end
202
203    # Format the trace flags for display.
204    def format_trace_flags
205      flags = []
206      flags << "first_time" unless @already_invoked
207      flags << "not_needed" unless needed?
208      flags.empty? ? "" : "(" + flags.join(", ") + ")"
209    end
210    private :format_trace_flags
211
212    # Execute the actions associated with this task.
213    def execute(args=nil)
214      args ||= EMPTY_TASK_ARGS
215      if application.options.dryrun
216        application.trace "** Execute (dry run) #{name}"
217        return
218      end
219      if application.options.trace
220        application.trace "** Execute #{name}"
221      end
222      application.enhance_with_matching_rule(name) if @actions.empty?
223      @actions.each do |act|
224        case act.arity
225        when 1
226          act.call(self)
227        else
228          act.call(self, args)
229        end
230      end
231    end
232
233    # Is this task needed?
234    def needed?
235      true
236    end
237
238    # Timestamp for this task.  Basic tasks return the current time for their
239    # time stamp.  Other tasks can be more sophisticated.
240    def timestamp
241      prerequisite_tasks.collect { |pre| pre.timestamp }.max || Time.now
242    end
243
244    # Add a description to the task.  The description can consist of an option
245    # argument list (enclosed brackets) and an optional comment.
246    def add_description(description)
247      return if ! description
248      comment = description.strip
249      add_comment(comment) if comment && ! comment.empty?
250    end
251
252    # Writing to the comment attribute is the same as adding a description.
253    def comment=(description)
254      add_description(description)
255    end
256
257    # Add a comment to the task.  If a comment already exists, separate
258    # the new comment with " / ".
259    def add_comment(comment)
260      if @full_comment
261        @full_comment << " / "
262      else
263        @full_comment = ''
264      end
265      @full_comment << comment
266      if @full_comment =~ /\A([^.]+?\.)( |$)/
267        @comment = $1
268      else
269        @comment = @full_comment
270      end
271    end
272    private :add_comment
273
274    # Set the names of the arguments for this task. +args+ should be
275    # an array of symbols, one for each argument name.
276    def set_arg_names(args)
277      @arg_names = args.map { |a| a.to_sym }
278    end
279
280    # Return a string describing the internal state of a task.  Useful for
281    # debugging.
282    def investigation
283      result = "------------------------------\n"
284      result << "Investigating #{name}\n"
285      result << "class: #{self.class}\n"
286      result <<  "task needed: #{needed?}\n"
287      result <<  "timestamp: #{timestamp}\n"
288      result << "pre-requisites: \n"
289      prereqs = prerequisite_tasks
290      prereqs.sort! {|a,b| a.timestamp <=> b.timestamp}
291      prereqs.each do |p|
292        result << "--#{p.name} (#{p.timestamp})\n"
293      end
294      latest_prereq = prerequisite_tasks.collect { |pre| pre.timestamp }.max
295      result <<  "latest-prerequisite time: #{latest_prereq}\n"
296      result << "................................\n\n"
297      return result
298    end
299
300    # ----------------------------------------------------------------
301    # Rake Module Methods
302    #
303    class << self
304
305      # Clear the task list.  This cause rake to immediately forget all the
306      # tasks that have been assigned.  (Normally used in the unit tests.)
307      def clear
308        Rake.application.clear
309      end
310
311      # List of all defined tasks.
312      def tasks
313        Rake.application.tasks
314      end
315
316      # Return a task with the given name.  If the task is not currently
317      # known, try to synthesize one from the defined rules.  If no rules are
318      # found, but an existing file matches the task name, assume it is a file
319      # task with no dependencies or actions.
320      def [](task_name)
321        Rake.application[task_name]
322      end
323
324      # TRUE if the task name is already defined.
325      def task_defined?(task_name)
326        Rake.application.lookup(task_name) != nil
327      end
328
329      # Define a task given +args+ and an option block.  If a rule with the
330      # given name already exists, the prerequisites and actions are added to
331      # the existing task.  Returns the defined task.
332      def define_task(*args, &block)
333        Rake.application.define_task(self, *args, &block)
334      end
335
336      # Define a rule for synthesizing tasks.
337      def create_rule(*args, &block)
338        Rake.application.create_rule(*args, &block)
339      end
340
341      # Apply the scope to the task name according to the rules for
342      # this kind of task.  Generic tasks will accept the scope as
343      # part of the name.
344      def scope_name(scope, task_name)
345        (scope + [task_name]).join(':')
346      end
347
348    end # class << Rake::Task
349  end # class Rake::Task
350end
351