[270] | 1 | #!/usr/bin/env ruby
|
---|
| 2 |
|
---|
| 3 | # Original is https://github.com/jimweirich/rake/
|
---|
| 4 | # Copyright (c) 2003 Jim Weirich
|
---|
| 5 | # License: MIT-LICENSE
|
---|
| 6 |
|
---|
| 7 | require 'getoptlong'
|
---|
| 8 | require 'fileutils'
|
---|
| 9 |
|
---|
| 10 | class String
|
---|
| 11 | def ext(newext='')
|
---|
| 12 | return self.dup if ['.', '..'].include? self
|
---|
| 13 | if newext != ''
|
---|
| 14 | newext = (newext =~ /^\./) ? newext : ("." + newext)
|
---|
| 15 | end
|
---|
| 16 | self.chomp(File.extname(self)) << newext
|
---|
| 17 | end
|
---|
| 18 |
|
---|
| 19 | def pathmap(spec=nil, &block)
|
---|
| 20 | return self if spec.nil?
|
---|
| 21 | result = ''
|
---|
| 22 | spec.scan(/%\{[^}]*\}-?\d*[sdpfnxX%]|%-?\d+d|%.|[^%]+/) do |frag|
|
---|
| 23 | case frag
|
---|
| 24 | when '%f'
|
---|
| 25 | result << File.basename(self)
|
---|
| 26 | when '%n'
|
---|
| 27 | result << File.basename(self).ext
|
---|
| 28 | when '%d'
|
---|
| 29 | result << File.dirname(self)
|
---|
| 30 | when '%x'
|
---|
| 31 | result << File.extname(self)
|
---|
| 32 | when '%X'
|
---|
| 33 | result << self.ext
|
---|
| 34 | when '%p'
|
---|
| 35 | result << self
|
---|
| 36 | when '%s'
|
---|
| 37 | result << (File::ALT_SEPARATOR || File::SEPARATOR)
|
---|
| 38 | when '%-'
|
---|
| 39 | # do nothing
|
---|
| 40 | when '%%'
|
---|
| 41 | result << "%"
|
---|
| 42 | when /%(-?\d+)d/
|
---|
| 43 | result << pathmap_partial($1.to_i)
|
---|
| 44 | when /^%\{([^}]*)\}(\d*[dpfnxX])/
|
---|
| 45 | patterns, operator = $1, $2
|
---|
| 46 | result << pathmap('%' + operator).pathmap_replace(patterns, &block)
|
---|
| 47 | when /^%/
|
---|
| 48 | fail ArgumentError, "Unknown pathmap specifier #{frag} in '#{spec}'"
|
---|
| 49 | else
|
---|
| 50 | result << frag
|
---|
| 51 | end
|
---|
| 52 | end
|
---|
| 53 | result
|
---|
| 54 | end
|
---|
| 55 | end
|
---|
| 56 |
|
---|
| 57 | module MiniRake
|
---|
| 58 | class Task
|
---|
| 59 | TASKS = Hash.new
|
---|
| 60 | RULES = Array.new
|
---|
| 61 |
|
---|
| 62 | # List of prerequisites for a task.
|
---|
| 63 | attr_reader :prerequisites
|
---|
| 64 |
|
---|
| 65 | # Source dependency for rule synthesized tasks. Nil if task was not
|
---|
| 66 | # sythesized from a rule.
|
---|
| 67 | attr_accessor :source
|
---|
| 68 |
|
---|
| 69 | # Create a task named +task_name+ with no actions or prerequisites..
|
---|
| 70 | # use +enhance+ to add actions and prerequisites.
|
---|
| 71 | def initialize(task_name)
|
---|
| 72 | @name = task_name
|
---|
| 73 | @prerequisites = []
|
---|
| 74 | @actions = []
|
---|
| 75 | end
|
---|
| 76 |
|
---|
| 77 | # Enhance a task with prerequisites or actions. Returns self.
|
---|
| 78 | def enhance(deps=nil, &block)
|
---|
| 79 | @prerequisites |= deps if deps
|
---|
| 80 | @actions << block if block_given?
|
---|
| 81 | self
|
---|
| 82 | end
|
---|
| 83 |
|
---|
| 84 | # Name of the task.
|
---|
| 85 | def name
|
---|
| 86 | @name.to_s
|
---|
| 87 | end
|
---|
| 88 |
|
---|
| 89 | # Invoke the task if it is needed. Prerequites are invoked first.
|
---|
| 90 | def invoke
|
---|
| 91 | puts "Invoke #{name} (already=[#{@already_invoked}], needed=[#{needed?}])" if $trace
|
---|
| 92 | return if @already_invoked
|
---|
| 93 | @already_invoked = true
|
---|
| 94 | prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten
|
---|
| 95 | prerequisites.each { |n| Task[n].invoke }
|
---|
| 96 | execute if needed?
|
---|
| 97 | end
|
---|
| 98 |
|
---|
| 99 | # Execute the actions associated with this task.
|
---|
| 100 | def execute
|
---|
| 101 | puts "Execute #{name}" if $trace
|
---|
| 102 | self.class.enhance_with_matching_rule(name) if @actions.empty?
|
---|
| 103 | unless $dryrun
|
---|
| 104 | @actions.each { |act| act.call(self) }
|
---|
| 105 | end
|
---|
| 106 | end
|
---|
| 107 |
|
---|
| 108 | # Is this task needed?
|
---|
| 109 | def needed?
|
---|
| 110 | true
|
---|
| 111 | end
|
---|
| 112 |
|
---|
| 113 | # Timestamp for this task. Basic tasks return the current time for
|
---|
| 114 | # their time stamp. Other tasks can be more sophisticated.
|
---|
| 115 | def timestamp
|
---|
[331] | 116 | Time.now
|
---|
[270] | 117 | end
|
---|
| 118 |
|
---|
| 119 | # Class Methods ----------------------------------------------------
|
---|
| 120 |
|
---|
| 121 | class << self
|
---|
| 122 |
|
---|
| 123 | # Clear the task list. This cause rake to immediately forget all
|
---|
| 124 | # the tasks that have been assigned. (Normally used in the unit
|
---|
| 125 | # tests.)
|
---|
| 126 | def clear
|
---|
| 127 | TASKS.clear
|
---|
| 128 | RULES.clear
|
---|
| 129 | end
|
---|
| 130 |
|
---|
| 131 | # List of all defined tasks.
|
---|
| 132 | def tasks
|
---|
| 133 | TASKS.keys.sort.collect { |tn| Task[tn] }
|
---|
| 134 | end
|
---|
| 135 |
|
---|
| 136 | # Return a task with the given name. If the task is not currently
|
---|
| 137 | # known, try to synthesize one from the defined rules. If no
|
---|
| 138 | # rules are found, but an existing file matches the task name,
|
---|
[331] | 139 | # assume it is a file task with no dependencies or actions.
|
---|
| 140 | def [](task_name)
|
---|
| 141 | task_name = task_name.to_s
|
---|
| 142 | if task_name.end_with?(":")
|
---|
| 143 | task_name = task_name.slice(0, task_name.length - 1)
|
---|
| 144 | end
|
---|
| 145 | if task = TASKS[task_name]
|
---|
| 146 | return task
|
---|
| 147 | end
|
---|
[270] | 148 | if task = enhance_with_matching_rule(task_name)
|
---|
| 149 | return task
|
---|
| 150 | end
|
---|
| 151 | if File.exist?(task_name)
|
---|
| 152 | return FileTask.define_task(task_name)
|
---|
| 153 | end
|
---|
| 154 | fail "Don't know how to rake #{task_name}"
|
---|
| 155 | end
|
---|
| 156 |
|
---|
| 157 | # Define a task given +args+ and an option block. If a rule with
|
---|
| 158 | # the given name already exists, the prerequisites and actions are
|
---|
| 159 | # added to the existing task.
|
---|
| 160 | def define_task(args, &block)
|
---|
| 161 | task_name, deps = resolve_args(args)
|
---|
| 162 | lookup(task_name).enhance([deps].flatten, &block)
|
---|
| 163 | end
|
---|
| 164 |
|
---|
| 165 | # Define a rule for synthesizing tasks.
|
---|
| 166 | def create_rule(args, &block)
|
---|
| 167 | pattern, deps = resolve_args(args)
|
---|
| 168 | pattern = Regexp.new(Regexp.quote(pattern) + '$') if String === pattern
|
---|
| 169 | RULES << [pattern, deps, block]
|
---|
| 170 | end
|
---|
| 171 |
|
---|
| 172 |
|
---|
| 173 | # Lookup a task. Return an existing task if found, otherwise
|
---|
| 174 | # create a task of the current type.
|
---|
| 175 | def lookup(task_name)
|
---|
| 176 | name = task_name.to_s
|
---|
| 177 | TASKS[name] ||= self.new(name)
|
---|
| 178 | end
|
---|
| 179 |
|
---|
| 180 | # If a rule can be found that matches the task name, enhance the
|
---|
| 181 | # task with the prerequisites and actions from the rule. Set the
|
---|
| 182 | # source attribute of the task appropriately for the rule. Return
|
---|
| 183 | # the enhanced task or nil of no rule was found.
|
---|
| 184 | def enhance_with_matching_rule(task_name)
|
---|
| 185 | RULES.each do |pattern, extensions, block|
|
---|
| 186 | if pattern.match(task_name)
|
---|
| 187 | ext = extensions.first
|
---|
| 188 | deps = extensions[1..-1]
|
---|
| 189 | case ext
|
---|
| 190 | when String
|
---|
| 191 | source = task_name.sub(/\.[^.]*$/, ext)
|
---|
| 192 | when Proc
|
---|
| 193 | source = ext.call(task_name)
|
---|
| 194 | else
|
---|
| 195 | fail "Don't know how to handle rule dependent: #{ext.inspect}"
|
---|
| 196 | end
|
---|
| 197 | if File.exist?(source)
|
---|
| 198 | task = FileTask.define_task({task_name => [source]+deps}, &block)
|
---|
| 199 | task.source = source
|
---|
| 200 | return task
|
---|
| 201 | end
|
---|
| 202 | end
|
---|
| 203 | end
|
---|
| 204 | nil
|
---|
| 205 | end
|
---|
| 206 |
|
---|
| 207 | private
|
---|
| 208 |
|
---|
| 209 | # Resolve the arguments for a task/rule.
|
---|
| 210 | def resolve_args(args)
|
---|
| 211 | case args
|
---|
| 212 | when Hash
|
---|
| 213 | fail "Too Many Task Names: #{args.keys.join(' ')}" if args.size > 1
|
---|
| 214 | fail "No Task Name Given" if args.size < 1
|
---|
| 215 | task_name = args.keys[0]
|
---|
| 216 | deps = args[task_name]
|
---|
| 217 | deps = [deps] if (String===deps) || (Regexp===deps) || (Proc===deps)
|
---|
| 218 | else
|
---|
| 219 | task_name = args
|
---|
| 220 | deps = []
|
---|
| 221 | end
|
---|
| 222 | [task_name, deps]
|
---|
| 223 | end
|
---|
| 224 | end
|
---|
| 225 | end
|
---|
| 226 |
|
---|
| 227 |
|
---|
| 228 | ######################################################################
|
---|
| 229 | class FileTask < Task
|
---|
| 230 | # Is this file task needed? Yes if it doesn't exist, or if its time
|
---|
| 231 | # stamp is out of date.
|
---|
| 232 | def needed?
|
---|
| 233 | return true unless File.exist?(name)
|
---|
| 234 | prerequisites = @prerequisites.collect{ |n| n.is_a?(Proc) ? n.call(name) : n }.flatten
|
---|
| 235 | latest_prereq = prerequisites.collect{|n| Task[n].timestamp}.max
|
---|
| 236 | return false if latest_prereq.nil?
|
---|
| 237 | timestamp < latest_prereq
|
---|
| 238 | end
|
---|
| 239 |
|
---|
| 240 | # Time stamp for file task.
|
---|
| 241 | def timestamp
|
---|
[331] | 242 | return Time.at(0) unless File.exist?(name)
|
---|
[270] | 243 | stat = File::stat(name.to_s)
|
---|
| 244 | stat.directory? ? Time.at(0) : stat.mtime
|
---|
| 245 | end
|
---|
| 246 | end
|
---|
| 247 |
|
---|
| 248 | module DSL
|
---|
| 249 | # Declare a basic task.
|
---|
| 250 | def task(args, &block)
|
---|
| 251 | MiniRake::Task.define_task(args, &block)
|
---|
| 252 | end
|
---|
| 253 |
|
---|
| 254 | # Declare a file task.
|
---|
| 255 | def file(args, &block)
|
---|
| 256 | MiniRake::FileTask.define_task(args, &block)
|
---|
| 257 | end
|
---|
| 258 |
|
---|
| 259 | # Declare a set of files tasks to create the given directories on
|
---|
| 260 | # demand.
|
---|
| 261 | def directory(args, &block)
|
---|
| 262 | MiniRake::FileTask.define_task(args) do |t|
|
---|
| 263 | block.call(t) unless block.nil?
|
---|
| 264 | dir = args.is_a?(Hash) ? args.keys.first : args
|
---|
| 265 | (dir.split(File::SEPARATOR) + ['']).inject do |acc, part|
|
---|
| 266 | (acc + File::SEPARATOR).tap do |d|
|
---|
| 267 | Dir.mkdir(d) unless File.exists? d
|
---|
| 268 | end + part
|
---|
| 269 | end
|
---|
| 270 | end
|
---|
| 271 | end
|
---|
| 272 |
|
---|
| 273 | # Declare a rule for auto-tasks.
|
---|
| 274 | def rule(args, &block)
|
---|
| 275 | MiniRake::Task.create_rule(args, &block)
|
---|
| 276 | end
|
---|
| 277 |
|
---|
| 278 | # Write a message to standard out if $verbose is enabled.
|
---|
| 279 | def log(msg)
|
---|
| 280 | print " " if $trace && $verbose
|
---|
| 281 | puts msg if $verbose
|
---|
| 282 | end
|
---|
| 283 |
|
---|
| 284 | # Run the system command +cmd+.
|
---|
| 285 | def sh(cmd)
|
---|
| 286 | puts cmd if $verbose
|
---|
| 287 | system(cmd) or fail "Command Failed: [#{cmd}]"
|
---|
| 288 | end
|
---|
| 289 |
|
---|
| 290 | def desc(text)
|
---|
| 291 | end
|
---|
| 292 | end
|
---|
| 293 | end
|
---|
| 294 |
|
---|
| 295 | Rake = MiniRake
|
---|
| 296 | extend MiniRake::DSL
|
---|
| 297 |
|
---|
| 298 |
|
---|
| 299 | ######################################################################
|
---|
| 300 | # Task Definition Functions ...
|
---|
| 301 |
|
---|
| 302 | ######################################################################
|
---|
| 303 | # Rake main application object. When invoking +rake+ from the command
|
---|
| 304 | # line, a RakeApp object is created and run.
|
---|
| 305 | #
|
---|
| 306 | class RakeApp
|
---|
| 307 | RAKEFILES = ['rakefile', 'Rakefile']
|
---|
| 308 |
|
---|
| 309 | OPTIONS = [
|
---|
| 310 | ['--dry-run', '-n', GetoptLong::NO_ARGUMENT,
|
---|
| 311 | "Do a dry run without executing actions."],
|
---|
| 312 | ['--help', '-H', GetoptLong::NO_ARGUMENT,
|
---|
| 313 | "Display this help message."],
|
---|
| 314 | ['--libdir', '-I', GetoptLong::REQUIRED_ARGUMENT,
|
---|
| 315 | "Include LIBDIR in the search path for required modules."],
|
---|
| 316 | ['--nosearch', '-N', GetoptLong::NO_ARGUMENT,
|
---|
| 317 | "Do not search parent directories for the Rakefile."],
|
---|
| 318 | ['--quiet', '-q', GetoptLong::NO_ARGUMENT,
|
---|
| 319 | "Do not log messages to standard output (default)."],
|
---|
| 320 | ['--rakefile', '-f', GetoptLong::REQUIRED_ARGUMENT,
|
---|
| 321 | "Use FILE as the rakefile."],
|
---|
| 322 | ['--require', '-r', GetoptLong::REQUIRED_ARGUMENT,
|
---|
| 323 | "Require MODULE before executing rakefile."],
|
---|
| 324 | ['--tasks', '-T', GetoptLong::NO_ARGUMENT,
|
---|
| 325 | "Display the tasks and dependencies, then exit."],
|
---|
| 326 | ['--pull-gems','-p', GetoptLong::NO_ARGUMENT,
|
---|
| 327 | "Pull all git mrbgems."],
|
---|
| 328 | ['--trace', '-t', GetoptLong::NO_ARGUMENT,
|
---|
| 329 | "Turn on invoke/execute tracing."],
|
---|
| 330 | ['--usage', '-h', GetoptLong::NO_ARGUMENT,
|
---|
| 331 | "Display usage."],
|
---|
| 332 | ['--verbose', '-v', GetoptLong::NO_ARGUMENT,
|
---|
| 333 | "Log message to standard output."],
|
---|
| 334 | ['--directory', '-C', GetoptLong::REQUIRED_ARGUMENT,
|
---|
| 335 | "Change executing directory of rakefiles."]
|
---|
| 336 | ]
|
---|
| 337 |
|
---|
| 338 | # Create a RakeApp object.
|
---|
| 339 | def initialize
|
---|
| 340 | @rakefile = nil
|
---|
| 341 | @nosearch = false
|
---|
| 342 | end
|
---|
| 343 |
|
---|
| 344 | # True if one of the files in RAKEFILES is in the current directory.
|
---|
| 345 | # If a match is found, it is copied into @rakefile.
|
---|
| 346 | def have_rakefile
|
---|
| 347 | RAKEFILES.each do |fn|
|
---|
| 348 | if File.exist?(fn)
|
---|
| 349 | @rakefile = fn
|
---|
| 350 | return true
|
---|
| 351 | end
|
---|
| 352 | end
|
---|
| 353 | return false
|
---|
| 354 | end
|
---|
| 355 |
|
---|
| 356 | # Display the program usage line.
|
---|
| 357 | def usage
|
---|
| 358 | puts "rake [-f rakefile] {options} targets..."
|
---|
| 359 | end
|
---|
| 360 |
|
---|
| 361 | # Display the rake command line help.
|
---|
| 362 | def help
|
---|
| 363 | usage
|
---|
| 364 | puts
|
---|
| 365 | puts "Options are ..."
|
---|
| 366 | puts
|
---|
| 367 | OPTIONS.sort.each do |long, short, mode, desc|
|
---|
| 368 | if mode == GetoptLong::REQUIRED_ARGUMENT
|
---|
| 369 | if desc =~ /\b([A-Z]{2,})\b/
|
---|
| 370 | long = long + "=#{$1}"
|
---|
| 371 | end
|
---|
| 372 | end
|
---|
| 373 | printf " %-20s (%s)\n", long, short
|
---|
| 374 | printf " %s\n", desc
|
---|
| 375 | end
|
---|
| 376 | end
|
---|
| 377 |
|
---|
| 378 | # Display the tasks and dependencies.
|
---|
| 379 | def display_tasks
|
---|
| 380 | MiniRake::Task.tasks.each do |t|
|
---|
| 381 | puts "#{t.class} #{t.name}"
|
---|
| 382 | t.prerequisites.each { |pre| puts " #{pre}" }
|
---|
| 383 | end
|
---|
| 384 | end
|
---|
| 385 |
|
---|
| 386 | # Return a list of the command line options supported by the
|
---|
| 387 | # program.
|
---|
| 388 | def command_line_options
|
---|
| 389 | OPTIONS.collect { |lst| lst[0..-2] }
|
---|
| 390 | end
|
---|
| 391 |
|
---|
| 392 | # Do the option defined by +opt+ and +value+.
|
---|
| 393 | def do_option(opt, value)
|
---|
| 394 | case opt
|
---|
| 395 | when '--dry-run'
|
---|
| 396 | $dryrun = true
|
---|
| 397 | $trace = true
|
---|
| 398 | when '--help'
|
---|
| 399 | help
|
---|
| 400 | exit
|
---|
| 401 | when '--libdir'
|
---|
| 402 | $:.push(value)
|
---|
| 403 | when '--nosearch'
|
---|
| 404 | @nosearch = true
|
---|
| 405 | when '--quiet'
|
---|
| 406 | $verbose = false
|
---|
| 407 | when '--rakefile'
|
---|
| 408 | RAKEFILES.clear
|
---|
| 409 | RAKEFILES << value
|
---|
| 410 | when '--require'
|
---|
| 411 | require value
|
---|
| 412 | when '--tasks'
|
---|
| 413 | $show_tasks = true
|
---|
| 414 | when '--pull-gems'
|
---|
| 415 | $pull_gems = true
|
---|
| 416 | when '--trace'
|
---|
| 417 | $trace = true
|
---|
| 418 | when '--usage'
|
---|
| 419 | usage
|
---|
| 420 | exit
|
---|
| 421 | when '--verbose'
|
---|
| 422 | $verbose = true
|
---|
| 423 | when '--version'
|
---|
| 424 | puts "rake, version #{RAKEVERSION}"
|
---|
| 425 | exit
|
---|
| 426 | when '--directory'
|
---|
| 427 | Dir.chdir value
|
---|
| 428 | else
|
---|
| 429 | fail "Unknown option: #{opt}"
|
---|
| 430 | end
|
---|
| 431 | end
|
---|
| 432 |
|
---|
| 433 | # Read and handle the command line options.
|
---|
| 434 | def handle_options
|
---|
| 435 | $verbose = false
|
---|
| 436 | $pull_gems = false
|
---|
| 437 | opts = GetoptLong.new(*command_line_options)
|
---|
| 438 | opts.each { |opt, value| do_option(opt, value) }
|
---|
| 439 | end
|
---|
| 440 |
|
---|
| 441 | # Run the +rake+ application.
|
---|
| 442 | def run
|
---|
| 443 | handle_options
|
---|
| 444 | begin
|
---|
| 445 | here = Dir.pwd
|
---|
| 446 | while ! have_rakefile
|
---|
| 447 | Dir.chdir("..")
|
---|
| 448 | if Dir.pwd == here || @nosearch
|
---|
| 449 | fail "No Rakefile found (looking for: #{RAKEFILES.join(', ')})"
|
---|
| 450 | end
|
---|
| 451 | here = Dir.pwd
|
---|
| 452 | end
|
---|
| 453 | tasks = []
|
---|
| 454 | ARGV.each do |task_name|
|
---|
| 455 | if /^(\w+)=(.*)/.match(task_name)
|
---|
| 456 | ENV[$1] = $2
|
---|
| 457 | else
|
---|
| 458 | tasks << task_name
|
---|
| 459 | end
|
---|
| 460 | end
|
---|
| 461 | puts "(in #{Dir.pwd})"
|
---|
| 462 | $rakefile = @rakefile
|
---|
| 463 | load @rakefile
|
---|
| 464 | if $show_tasks
|
---|
| 465 | display_tasks
|
---|
| 466 | else
|
---|
| 467 | tasks.push("default") if tasks.size == 0
|
---|
| 468 | tasks.each do |task_name|
|
---|
| 469 | MiniRake::Task[task_name].invoke
|
---|
| 470 | end
|
---|
| 471 | end
|
---|
| 472 | rescue Exception => ex
|
---|
| 473 | puts "rake aborted!"
|
---|
| 474 | puts ex.message
|
---|
| 475 | if $trace
|
---|
| 476 | puts ex.backtrace.join("\n")
|
---|
| 477 | else
|
---|
| 478 | puts ex.backtrace.find {|str| str =~ /#{@rakefile}/ } || ""
|
---|
| 479 | end
|
---|
| 480 | exit 1
|
---|
| 481 | end
|
---|
| 482 | end
|
---|
| 483 | end
|
---|
| 484 |
|
---|
| 485 | if __FILE__ == $0 then
|
---|
| 486 | RakeApp.new.run
|
---|
| 487 | end
|
---|