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