1require 'shellwords' 2require 'optparse' 3 4require 'rake/task_manager' 5require 'rake/file_list' 6require 'rake/thread_pool' 7require 'rake/thread_history_display' 8require 'rake/trace_output' 9require 'rake/win32' 10 11module Rake 12 13 CommandLineOptionError = Class.new(StandardError) 14 15 ###################################################################### 16 # Rake main application object. When invoking +rake+ from the 17 # command line, a Rake::Application object is created and run. 18 # 19 class Application 20 include TaskManager 21 include TraceOutput 22 23 # The name of the application (typically 'rake') 24 attr_reader :name 25 26 # The original directory where rake was invoked. 27 attr_reader :original_dir 28 29 # Name of the actual rakefile used. 30 attr_reader :rakefile 31 32 # Number of columns on the terminal 33 attr_accessor :terminal_columns 34 35 # List of the top level task names (task names from the command line). 36 attr_reader :top_level_tasks 37 38 DEFAULT_RAKEFILES = ['rakefile', 'Rakefile', 'rakefile.rb', 'Rakefile.rb'].freeze 39 40 # Initialize a Rake::Application object. 41 def initialize 42 super 43 @name = 'rake' 44 @rakefiles = DEFAULT_RAKEFILES.dup 45 @rakefile = nil 46 @pending_imports = [] 47 @imported = [] 48 @loaders = {} 49 @default_loader = Rake::DefaultLoader.new 50 @original_dir = Dir.pwd 51 @top_level_tasks = [] 52 add_loader('rb', DefaultLoader.new) 53 add_loader('rf', DefaultLoader.new) 54 add_loader('rake', DefaultLoader.new) 55 @tty_output = STDOUT.tty? 56 @terminal_columns = ENV['RAKE_COLUMNS'].to_i 57 end 58 59 # Run the Rake application. The run method performs the following 60 # three steps: 61 # 62 # * Initialize the command line options (+init+). 63 # * Define the tasks (+load_rakefile+). 64 # * Run the top level tasks (+top_level+). 65 # 66 # If you wish to build a custom rake command, you should call 67 # +init+ on your application. Then define any tasks. Finally, 68 # call +top_level+ to run your top level tasks. 69 def run 70 standard_exception_handling do 71 init 72 load_rakefile 73 top_level 74 end 75 end 76 77 # Initialize the command line parameters and app name. 78 def init(app_name='rake') 79 standard_exception_handling do 80 @name = app_name 81 handle_options 82 collect_tasks 83 end 84 end 85 86 # Find the rakefile and then load it and any pending imports. 87 def load_rakefile 88 standard_exception_handling do 89 raw_load_rakefile 90 end 91 end 92 93 # Run the top level tasks of a Rake application. 94 def top_level 95 run_with_threads do 96 if options.show_tasks 97 display_tasks_and_comments 98 elsif options.show_prereqs 99 display_prerequisites 100 else 101 top_level_tasks.each { |task_name| invoke_task(task_name) } 102 end 103 end 104 end 105 106 # Run the given block with the thread startup and shutdown. 107 def run_with_threads 108 thread_pool.gather_history if options.job_stats == :history 109 110 yield 111 112 thread_pool.join 113 if options.job_stats 114 stats = thread_pool.statistics 115 puts "Maximum active threads: #{stats[:max_active_threads]}" 116 puts "Total threads in play: #{stats[:total_threads_in_play]}" 117 end 118 ThreadHistoryDisplay.new(thread_pool.history).show if options.job_stats == :history 119 end 120 121 # Add a loader to handle imported files ending in the extension 122 # +ext+. 123 def add_loader(ext, loader) 124 ext = ".#{ext}" unless ext =~ /^\./ 125 @loaders[ext] = loader 126 end 127 128 # Application options from the command line 129 def options 130 @options ||= OpenStruct.new 131 end 132 133 # Return the thread pool used for multithreaded processing. 134 def thread_pool # :nodoc: 135 @thread_pool ||= ThreadPool.new(options.thread_pool_size||FIXNUM_MAX) 136 end 137 138 # private ---------------------------------------------------------------- 139 140 def invoke_task(task_string) 141 name, args = parse_task_string(task_string) 142 t = self[name] 143 t.invoke(*args) 144 end 145 146 def parse_task_string(string) 147 if string =~ /^([^\[]+)(\[(.*)\])$/ 148 name = $1 149 args = $3.split(/\s*,\s*/) 150 else 151 name = string 152 args = [] 153 end 154 [name, args] 155 end 156 157 # Provide standard exception handling for the given block. 158 def standard_exception_handling 159 begin 160 yield 161 rescue SystemExit => ex 162 # Exit silently with current status 163 raise 164 rescue OptionParser::InvalidOption => ex 165 $stderr.puts ex.message 166 exit(false) 167 rescue Exception => ex 168 # Exit with error message 169 display_error_message(ex) 170 exit(false) 171 end 172 end 173 174 # Display the error message that caused the exception. 175 def display_error_message(ex) 176 trace "#{name} aborted!" 177 trace ex.message 178 if options.backtrace 179 trace ex.backtrace.join("\n") 180 else 181 trace Backtrace.collapse(ex.backtrace).join("\n") 182 end 183 trace "Tasks: #{ex.chain}" if has_chain?(ex) 184 trace "(See full trace by running task with --trace)" unless options.backtrace 185 end 186 187 # Warn about deprecated usage. 188 # 189 # Example: 190 # Rake.application.deprecate("import", "Rake.import", caller.first) 191 # 192 def deprecate(old_usage, new_usage, call_site) 193 return if options.ignore_deprecate 194 $stderr.puts "WARNING: '#{old_usage}' is deprecated. " + 195 "Please use '#{new_usage}' instead.\n" + 196 " at #{call_site}" 197 end 198 199 # Does the exception have a task invocation chain? 200 def has_chain?(exception) 201 exception.respond_to?(:chain) && exception.chain 202 end 203 private :has_chain? 204 205 # True if one of the files in RAKEFILES is in the current directory. 206 # If a match is found, it is copied into @rakefile. 207 def have_rakefile 208 @rakefiles.each do |fn| 209 if File.exist?(fn) 210 others = FileList.glob(fn, File::FNM_CASEFOLD) 211 return others.size == 1 ? others.first : fn 212 elsif fn == '' 213 return fn 214 end 215 end 216 return nil 217 end 218 219 # True if we are outputting to TTY, false otherwise 220 def tty_output? 221 @tty_output 222 end 223 224 # Override the detected TTY output state (mostly for testing) 225 def tty_output=( tty_output_state ) 226 @tty_output = tty_output_state 227 end 228 229 # We will truncate output if we are outputting to a TTY or if we've been 230 # given an explicit column width to honor 231 def truncate_output? 232 tty_output? || @terminal_columns.nonzero? 233 end 234 235 # Display the tasks and comments. 236 def display_tasks_and_comments 237 displayable_tasks = tasks.select { |t| 238 (options.show_all_tasks || t.comment) && t.name =~ options.show_task_pattern 239 } 240 case options.show_tasks 241 when :tasks 242 width = displayable_tasks.collect { |t| t.name_with_args.length }.max || 10 243 max_column = truncate_output? ? terminal_width - name.size - width - 7 : nil 244 245 displayable_tasks.each do |t| 246 printf "#{name} %-#{width}s # %s\n", 247 t.name_with_args, max_column ? truncate(t.comment, max_column) : t.comment 248 end 249 when :describe 250 displayable_tasks.each do |t| 251 puts "#{name} #{t.name_with_args}" 252 comment = t.full_comment || "" 253 comment.split("\n").each do |line| 254 puts " #{line}" 255 end 256 puts 257 end 258 when :lines 259 displayable_tasks.each do |t| 260 t.locations.each do |loc| 261 printf "#{name} %-30s %s\n",t.name_with_args, loc 262 end 263 end 264 else 265 fail "Unknown show task mode: '#{options.show_tasks}'" 266 end 267 end 268 269 def terminal_width 270 if @terminal_columns.nonzero? 271 result = @terminal_columns 272 else 273 result = unix? ? dynamic_width : 80 274 end 275 (result < 10) ? 80 : result 276 rescue 277 80 278 end 279 280 # Calculate the dynamic width of the 281 def dynamic_width 282 @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) 283 end 284 285 def dynamic_width_stty 286 %x{stty size 2>/dev/null}.split[1].to_i 287 end 288 289 def dynamic_width_tput 290 %x{tput cols 2>/dev/null}.to_i 291 end 292 293 def unix? 294 RbConfig::CONFIG['host_os'] =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i 295 end 296 297 def windows? 298 Win32.windows? 299 end 300 301 def truncate(string, width) 302 if string.nil? 303 "" 304 elsif string.length <= width 305 string 306 else 307 ( string[0, width-3] || "" ) + "..." 308 end 309 end 310 311 # Display the tasks and prerequisites 312 def display_prerequisites 313 tasks.each do |t| 314 puts "#{name} #{t.name}" 315 t.prerequisites.each { |pre| puts " #{pre}" } 316 end 317 end 318 319 def trace(*strings) 320 options.trace_output ||= $stderr 321 trace_on(options.trace_output, *strings) 322 end 323 324 def sort_options(options) 325 options.sort_by { |opt| 326 opt.select { |o| o =~ /^-/ }.map { |o| o.downcase }.sort.reverse 327 } 328 end 329 private :sort_options 330 331 # A list of all the standard options used in rake, suitable for 332 # passing to OptionParser. 333 def standard_rake_options 334 sort_options( 335 [ 336 ['--all', '-A', "Show all tasks, even uncommented ones", 337 lambda { |value| 338 options.show_all_tasks = value 339 } 340 ], 341 ['--backtrace=[OUT]', "Enable full backtrace. OUT can be stderr (default) or stdout.", 342 lambda { |value| 343 options.backtrace = true 344 select_trace_output(options, 'backtrace', value) 345 } 346 ], 347 ['--classic-namespace', '-C', "Put Task and FileTask in the top level namespace", 348 lambda { |value| 349 require 'rake/classic_namespace' 350 options.classic_namespace = true 351 } 352 ], 353 ['--comments', "Show commented tasks only", 354 lambda { |value| 355 options.show_all_tasks = !value 356 } 357 ], 358 ['--describe', '-D [PATTERN]', "Describe the tasks (matching optional PATTERN), then exit.", 359 lambda { |value| 360 select_tasks_to_show(options, :describe, value) 361 } 362 ], 363 ['--dry-run', '-n', "Do a dry run without executing actions.", 364 lambda { |value| 365 Rake.verbose(true) 366 Rake.nowrite(true) 367 options.dryrun = true 368 options.trace = true 369 } 370 ], 371 ['--execute', '-e CODE', "Execute some Ruby code and exit.", 372 lambda { |value| 373 eval(value) 374 exit 375 } 376 ], 377 ['--execute-print', '-p CODE', "Execute some Ruby code, print the result, then exit.", 378 lambda { |value| 379 puts eval(value) 380 exit 381 } 382 ], 383 ['--execute-continue', '-E CODE', 384 "Execute some Ruby code, then continue with normal task processing.", 385 lambda { |value| eval(value) } 386 ], 387 ['--jobs', '-j [NUMBER]', 388 "Specifies the maximum number of tasks to execute in parallel. (default:2)", 389 lambda { |value| options.thread_pool_size = [(value || 2).to_i,2].max } 390 ], 391 ['--job-stats [LEVEL]', 392 "Display job statistics. LEVEL=history displays a complete job list", 393 lambda { |value| 394 if value =~ /^history/i 395 options.job_stats = :history 396 else 397 options.job_stats = true 398 end 399 } 400 ], 401 ['--libdir', '-I LIBDIR', "Include LIBDIR in the search path for required modules.", 402 lambda { |value| $:.push(value) } 403 ], 404 ['--multitask', '-m', "Treat all tasks as multitasks.", 405 lambda { |value| options.always_multitask = true } 406 ], 407 ['--no-search', '--nosearch', '-N', "Do not search parent directories for the Rakefile.", 408 lambda { |value| options.nosearch = true } 409 ], 410 ['--prereqs', '-P', "Display the tasks and dependencies, then exit.", 411 lambda { |value| options.show_prereqs = true } 412 ], 413 ['--quiet', '-q', "Do not log messages to standard output.", 414 lambda { |value| Rake.verbose(false) } 415 ], 416 ['--rakefile', '-f [FILE]', "Use FILE as the rakefile.", 417 lambda { |value| 418 value ||= '' 419 @rakefiles.clear 420 @rakefiles << value 421 } 422 ], 423 ['--rakelibdir', '--rakelib', '-R RAKELIBDIR', 424 "Auto-import any .rake files in RAKELIBDIR. (default is 'rakelib')", 425 lambda { |value| options.rakelib = value.split(File::PATH_SEPARATOR) } 426 ], 427 ['--reduce-compat', "Remove DSL in Object; remove Module#const_missing which defines ::Task etc.", 428 # Load-time option. 429 # Handled in bin/rake where Rake::REDUCE_COMPAT is defined (or not). 430 lambda { |_| } 431 ], 432 ['--require', '-r MODULE', "Require MODULE before executing rakefile.", 433 lambda { |value| 434 begin 435 require value 436 rescue LoadError => ex 437 begin 438 rake_require value 439 rescue LoadError 440 raise ex 441 end 442 end 443 } 444 ], 445 ['--rules', "Trace the rules resolution.", 446 lambda { |value| options.trace_rules = true } 447 ], 448 ['--silent', '-s', "Like --quiet, but also suppresses the 'in directory' announcement.", 449 lambda { |value| 450 Rake.verbose(false) 451 options.silent = true 452 } 453 ], 454 ['--suppress-backtrace PATTERN', "Suppress backtrace lines matching regexp PATTERN. Ignored if --trace is on.", 455 lambda { |value| 456 options.suppress_backtrace_pattern = Regexp.new(value) 457 } 458 ], 459 ['--system', '-g', 460 "Using system wide (global) rakefiles (usually '~/.rake/*.rake').", 461 lambda { |value| options.load_system = true } 462 ], 463 ['--no-system', '--nosystem', '-G', 464 "Use standard project Rakefile search paths, ignore system wide rakefiles.", 465 lambda { |value| options.ignore_system = true } 466 ], 467 ['--tasks', '-T [PATTERN]', "Display the tasks (matching optional PATTERN) with descriptions, then exit.", 468 lambda { |value| 469 select_tasks_to_show(options, :tasks, value) 470 } 471 ], 472 ['--trace=[OUT]', '-t', "Turn on invoke/execute tracing, enable full backtrace. OUT can be stderr (default) or stdout.", 473 lambda { |value| 474 options.trace = true 475 options.backtrace = true 476 select_trace_output(options, 'trace', value) 477 Rake.verbose(true) 478 } 479 ], 480 ['--verbose', '-v', "Log message to standard output.", 481 lambda { |value| Rake.verbose(true) } 482 ], 483 ['--version', '-V', "Display the program version.", 484 lambda { |value| 485 puts "rake, version #{RAKEVERSION}" 486 exit 487 } 488 ], 489 ['--where', '-W [PATTERN]', "Describe the tasks (matching optional PATTERN), then exit.", 490 lambda { |value| 491 select_tasks_to_show(options, :lines, value) 492 options.show_all_tasks = true 493 } 494 ], 495 ['--no-deprecation-warnings', '-X', "Disable the deprecation warnings.", 496 lambda { |value| 497 options.ignore_deprecate = true 498 } 499 ], 500 ]) 501 end 502 503 def select_tasks_to_show(options, show_tasks, value) 504 options.show_tasks = show_tasks 505 options.show_task_pattern = Regexp.new(value || '') 506 Rake::TaskManager.record_task_metadata = true 507 end 508 private :select_tasks_to_show 509 510 def select_trace_output(options, trace_option, value) 511 value = value.strip unless value.nil? 512 case value 513 when 'stdout' 514 options.trace_output = $stdout 515 when 'stderr', nil 516 options.trace_output = $stderr 517 else 518 fail CommandLineOptionError, "Unrecognized --#{trace_option} option '#{value}'" 519 end 520 end 521 private :select_trace_output 522 523 # Read and handle the command line options. 524 def handle_options 525 options.rakelib = ['rakelib'] 526 options.trace_output = $stderr 527 528 OptionParser.new do |opts| 529 opts.banner = "rake [-f rakefile] {options} targets..." 530 opts.separator "" 531 opts.separator "Options are ..." 532 533 opts.on_tail("-h", "--help", "-H", "Display this help message.") do 534 puts opts 535 exit 536 end 537 538 standard_rake_options.each { |args| opts.on(*args) } 539 opts.environment('RAKEOPT') 540 end.parse! 541 542 # If class namespaces are requested, set the global options 543 # according to the values in the options structure. 544 if options.classic_namespace 545 $show_tasks = options.show_tasks 546 $show_prereqs = options.show_prereqs 547 $trace = options.trace 548 $dryrun = options.dryrun 549 $silent = options.silent 550 end 551 end 552 553 # Similar to the regular Ruby +require+ command, but will check 554 # for *.rake files in addition to *.rb files. 555 def rake_require(file_name, paths=$LOAD_PATH, loaded=$") 556 fn = file_name + ".rake" 557 return false if loaded.include?(fn) 558 paths.each do |path| 559 full_path = File.join(path, fn) 560 if File.exist?(full_path) 561 Rake.load_rakefile(full_path) 562 loaded << fn 563 return true 564 end 565 end 566 fail LoadError, "Can't find #{file_name}" 567 end 568 569 def find_rakefile_location 570 here = Dir.pwd 571 while ! (fn = have_rakefile) 572 Dir.chdir("..") 573 if Dir.pwd == here || options.nosearch 574 return nil 575 end 576 here = Dir.pwd 577 end 578 [fn, here] 579 ensure 580 Dir.chdir(Rake.original_dir) 581 end 582 583 def print_rakefile_directory(location) 584 $stderr.puts "(in #{Dir.pwd})" unless 585 options.silent or original_dir == location 586 end 587 588 def raw_load_rakefile # :nodoc: 589 rakefile, location = find_rakefile_location 590 if (! options.ignore_system) && 591 (options.load_system || rakefile.nil?) && 592 system_dir && File.directory?(system_dir) 593 print_rakefile_directory(location) 594 glob("#{system_dir}/*.rake") do |name| 595 add_import name 596 end 597 else 598 fail "No Rakefile found (looking for: #{@rakefiles.join(', ')})" if 599 rakefile.nil? 600 @rakefile = rakefile 601 Dir.chdir(location) 602 print_rakefile_directory(location) 603 $rakefile = @rakefile if options.classic_namespace 604 Rake.load_rakefile(File.expand_path(@rakefile)) if @rakefile && @rakefile != '' 605 options.rakelib.each do |rlib| 606 glob("#{rlib}/*.rake") do |name| 607 add_import name 608 end 609 end 610 end 611 load_imports 612 end 613 614 def glob(path, &block) 615 FileList.glob(path.gsub("\\", '/')).each(&block) 616 end 617 private :glob 618 619 # The directory path containing the system wide rakefiles. 620 def system_dir 621 @system_dir ||= 622 begin 623 if ENV['RAKE_SYSTEM'] 624 ENV['RAKE_SYSTEM'] 625 else 626 standard_system_dir 627 end 628 end 629 end 630 631 # The standard directory containing system wide rake files. 632 if Win32.windows? 633 def standard_system_dir #:nodoc: 634 Win32.win32_system_dir 635 end 636 else 637 def standard_system_dir #:nodoc: 638 File.join(File.expand_path('~'), '.rake') 639 end 640 end 641 private :standard_system_dir 642 643 # Collect the list of tasks on the command line. If no tasks are 644 # given, return a list containing only the default task. 645 # Environmental assignments are processed at this time as well. 646 def collect_tasks 647 @top_level_tasks = [] 648 ARGV.each do |arg| 649 if arg =~ /^(\w+)=(.*)$/ 650 ENV[$1] = $2 651 else 652 @top_level_tasks << arg unless arg =~ /^-/ 653 end 654 end 655 @top_level_tasks.push("default") if @top_level_tasks.size == 0 656 end 657 658 # Add a file to the list of files to be imported. 659 def add_import(fn) 660 @pending_imports << fn 661 end 662 663 # Load the pending list of imported files. 664 def load_imports 665 while fn = @pending_imports.shift 666 next if @imported.member?(fn) 667 if fn_task = lookup(fn) 668 fn_task.invoke 669 end 670 ext = File.extname(fn) 671 loader = @loaders[ext] || @default_loader 672 loader.load(fn) 673 @imported << fn 674 end 675 end 676 677 # Warn about deprecated use of top level constant names. 678 def const_warning(const_name) 679 @const_warning ||= false 680 if ! @const_warning 681 $stderr.puts %{WARNING: Deprecated reference to top-level constant '#{const_name}' } + 682 %{found at: #{rakefile_location}} # ' 683 $stderr.puts %{ Use --classic-namespace on rake command} 684 $stderr.puts %{ or 'require "rake/classic_namespace"' in Rakefile} 685 end 686 @const_warning = true 687 end 688 689 def rakefile_location(backtrace=caller) 690 backtrace.map { |t| t[/([^:]+):/,1] } 691 692 re = /^#{@rakefile}$/ 693 re = /#{re.source}/i if windows? 694 695 backtrace.find { |str| str =~ re } || '' 696 end 697 698 private 699 FIXNUM_MAX = (2**(0.size * 8 - 2) - 1) # :nodoc: 700 701 end 702end 703