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