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