lib/ridl/runner.rb



#--------------------------------------------------------------------
# run.rb - Standalone Ruby IDL compiler runner
#
# Author: Martin Corino
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the RIDL LICENSE which is
# included with this program.
#
# Copyright (c) Remedy IT Expertise BV
#--------------------------------------------------------------------
require 'stringio'
require 'ridl/optparse_ext'
require 'ridl/genfile'
require 'ridl/backend'
require 'ridl/options'

# -----------------------------------------------------------------------

module IDL
  OPTIONS = Options.new({
      outputdir: nil,
      includepaths: [],
      xincludepaths: [],
      verbose: (ENV['RIDL_VERBOSE'] || 0).to_i,
      debug: false,
      namespace: nil,
      search_incpath: false,
      backend: nil,
      macros: {
      }
  })
  CORE_OPTIONS = OPTIONS.keys

  class Engine
    class ProductionStack
      def initialize
        @stack = []
        @index = {}
      end

      def size
        @stack.size
      end

      def empty?
        @stack.empty?
      end

      def push(id, prod)
        @index[id.to_sym] = @stack.size
        @stack << [id.to_sym, prod]
      end

      def pop
        return nil if empty?

        id, prod = @stack.shift
        @index.delete(id)
        prod
      end

      def peek
        return nil if empty?

        id, _ = @stack.first
        id
      end

      def remove(id)
        return nil unless has?(id)

        i = @index.delete(id.to_sym)
        _, producer = @productionstack.delete(i)
        producer
      end

      def has?(id)
        @index.has_key?(id.to_sym)
      end

      def [](id)
        @stack[@index[id.to_sym]].last
      end
    end

    def initialize(backend, options)
      @backend = backend ? Backend.load(backend) : Backend.null_be
      @initopts = options.merge({
        backend: @backend.name,
        macros: options[:macros].merge({
           __RIDL__: "#{RIDL_VERSION}",
           __RIDLBE__: @backend.name.to_s,
           __RIDLBE_VER__: @backend.version
        })
      })
      @optparser = init_optparser
      @inputstack = []
      @productionstack = ProductionStack.new
      @options = nil
    end

    def backend
      @backend
    end

    def options
      @options || @initopts
    end

    # Input management

    def push_input(idlfile, opts)
      @inputstack << [idlfile, opts]
    end

    def pop_input
      @inputstack.shift
    end

    def peek_input
      @inputstack.first
    end

    def has_input?
      !@inputstack.empty?
    end

    # Production management

    def push_production(id, producer)
      raise "Producer #{id} already queued" if @productionstack.has?(id)

      @productionstack.push(id, producer)
    end

    def pop_production
      @productionstack.pop
    end

    def peek_production
      @productionstack.peek
    end

    def remove_production(id)
      @productionstack.remove(id)
    end

    def has_productions?
      !@productionstack.empty?
    end

    def has_production?(id)
      @productionstack.has?(id)
    end

    def production(id)
      @productionstack[id]
    end

    # Verbosity control

    def verbose_level
      options[:verbose]
    end

    def verbose_level=(l)
      options[:verbose] = l
    end

    def run(argv, runopts = {})

      # initialize options
      @options = @initopts.merge(runopts)

      # mark current (clean) state
      @options.mark

      # backup current engine (if any)
      cur_engine = Thread.current[:ridl_engine]
      # store currently running engine for current thread
      Thread.current[:ridl_engine] = self

      begin
        # parse arguments
        begin
          @optparser.parse!(argv)
        rescue ArgumentError => e
          IDL.error(e.inspect)
          IDL.error(e.backtrace.join("\n")) if IDL.verbose_level.positive?
          return false
        end

        if options[:preprocess]

          ## PREPROCESSING
          o = if options[:output].nil?
                $stdout
              else
                File.open(options[:output], 'w+')
              end
          options[:output] = o

          input_base = File.basename(argv.first)
          if input_base != argv.first
            options[:xincludepaths] << (File.dirname(argv.first) + '/')
          end

          return !parse("#include \"#{input_base}\"", options).nil?
        else
          ## collect input files from commandline
          collect_input(argv)

          ## CODE GENERATION
          while has_input?
            # get input from stack
            _idlfile, _opts = pop_input

            _fio = if IO === _idlfile || StringIO === _idlfile
                     _idlfile
                   else
                     File.open(_idlfile, 'r')
                   end
            raise 'cannot read from STDOUT' if $stdout == _fio

            # parse IDL source
            IDL.log(1, "RIDL - parsing #{IO === _idlfile ? 'from STDIN' : (StringIO === _idlfile ? 'from string' : _idlfile)}")

            unless _parser = parse(_fio, _opts)
              return false
            end

            # process parse result -> code generation
            IDL.log(2, 'RIDL - processing input')

            GenFile.transaction do
              begin

                backend.process_input(_parser, _opts)

                # handle productions
                while has_productions?
                  IDL.log(2, "RIDL - running production #{peek_production}")

                  # get next-in-line producer
                  producer = pop_production

                  # execute the producer
                  producer.run(_parser)
                end

              rescue Backend::ProcessStop
                IDL.log(2, "RIDL - processing #{IO === _idlfile ? 'from STDIN' : (StringIO === _idlfile ? 'from string' : _idlfile)} stopped with \"#{$!.message}\"")

              rescue => e
                IDL.error(e)
                IDL.error(e.backtrace.join("\n")) unless e.is_a? IDL::ParseError
                return false
              end
            end
          end
        end
      ensure
        # restore previous state
        Thread.current[:ridl_engine] = cur_engine
      end
      true
    end

    def parse(io, opts)
      # parse IDL source
      _parser = ::IDL::Parser.new(opts)
      _parser.yydebug = opts[:debug]

      begin
        _parser.parse(io)
      rescue => e
        IDL.error(e.inspect)
        IDL.error(e.backtrace.join("\n")) unless e.is_a? IDL::ParseError
        return nil
      ensure
        io.close unless String === io || io == $stdin
      end
      _parser
    end

    private

    def collect_input(argv)
      ## collect input files from commandline
      argv.each do |_arg|
        _opts = options.dup

        _opts[:idlfile] = _arg
        if _opts[:no_input]
          # do not parse specified file (only used as template for output names)
          # instead push an empty StringIO object
          _arg = StringIO.new('')
        else
          if _opts[:search_incpath]
            _fname = _arg
            _fpath = if File.file?(_fname) && File.readable?(_fname)
                       _fname
                     else
                       _fp = _opts[:includepaths].find do |_p|
                         _f = _p + _fname
                         File.file?(_f) && File.readable?(_f)
                       end
                       _opts[:outputdir] = _fp unless _fp.nil? || !_opts[:outputdir].nil?
                       _fp += '/' + _fname unless _fp.nil?
                       _fp
                     end
            _arg = _fpath unless _fpath.nil?
          end
          _opts[:xincludepaths] << (File.dirname(_arg) + '/')
        end

        _opts[:outputdir] ||= '.'

        push_input(_arg, _opts)
      end

      ## if no IDL input file specified read from STDIN
      unless has_input?
        _opts = options.dup
        _opts[:outputdir] ||= '.'
        push_input($stdin, _opts)
      end
    end

    def init_optparser
      script_name = File.basename($0, '.*')
      unless script_name =~ /ridlc/
        script_name = 'ruby ' + $0
      end

      # set up option parser with common options
      opts = OptionParser.new
      opts.banner = "Usage: #{script_name} [:backend] [options] [<idlfile> [<idlfile> ...]]\n\n" +
          "    backend\t\tSpecifies the IDL language mapping backend to use.\n" +
          "           \t\tDefault = :null\n\n" +
          "    Active language mapping = :#{backend.name}"
      opts.separator ''
      opts.on('-I PATH', '--include=PATH', String,
              'Adds include searchpath.',
              'Default: none') { |v|
        self.options[:includepaths] << (v.end_with?('\\', '/') ? v : v + '/')
      }
      opts.on('-Dmacro=[value]', String, 'defines preprocessor macro') { |v|
        name, value = v.split('=')
        self.options[:macros][name] = (value ? value : true)
      }
      opts.on('-n NAMESPACE', '--namespace=NAMESPACE', String,
              'Defines rootlevel enclosing namespace.',
              'Default: nil') { |v|
        self.options[:namespace] = v
      }
      opts.on('-v', '--verbose',
              'Set verbosity level. Repeat to increment.',
              'Default: 0') { |_|
        self.options[:verbose] += 1
      }
      opts.on('--debug',
              'Set parser debug mode. Do NOT do this at home!',
              'Default: off') { |_|
        self.options[:debug] = true
      }
      opts.on('--search-includepath',
              'Use include paths to find main IDL source.',
              'Default: off') { |_|
        self.options[:search_incpath] = true
      }
      opts.on('--no-input',
               'Do not parse specified file(s) as input IDL.',
               'Default: off') { |_|
        self.options[:no_input] = true
      }
      if @initopts[:preprocess]
        opts.on('--output=FILE', String,
                'Specifies filename to generate output in.',
                'Default: basename(idlfile)-\'.idl\'+<postfix>+<ext>') { |v|
          self.options[:output] = v
        }
      end

      # setup language mapping specific options
      be_options = OptionList.new
      @backend.setup_be(be_options, @initopts)
      be_options.to_option_parser(opts, self)

      opts.on('-V', '--version',
              'Show version information and exit.') {
        puts "RIDL compiler #{RIDL_VERSION}"
        puts RIDL_COPYRIGHT
        puts '---'
        @backend.print_version
        exit
      }

      opts.separator ""

      opts.on('-h', '--help',
              'Show this help message.') { puts opts
 puts
 exit }

      opts
    end
  end

  def IDL.engine?
    !Thread.current[:ridl_engine].nil?
  end

  def IDL.backend
    Thread.current[:ridl_engine] ? Thread.current[:ridl_engine].backend : nil
  end

  def IDL.push_input(idlfile, opts)
    Thread.current[:ridl_engine].push_input(idlfile, opts) if engine?
  end

  def IDL.pop_input
    return nil unless engine?

    Thread.current[:ridl_engine].pop_input
  end

  def IDL.peek_input
    return nil unless engine?

    Thread.current[:ridl_engine].peek_input
  end

  def IDL.has_input?
    engine? && Thread.current[:ridl_engine].has_input?
  end

  def IDL.push_production(id, producer)
    Thread.current[:ridl_engine].push_production(id, producer) if engine?
  end

  def IDL.pop_production
    return nil unless engine?

    Thread.current[:ridl_engine].pop_production
  end

  def IDL.remove_production(id)
    return nil unless engine?

    Thread.current[:ridl_engine].remove_production(id)
  end

  def IDL.has_productions?
    engine? && Thread.current[:ridl_engine].has_productions?
  end

  def IDL.has_production?(id)
    engine? && Thread.current[:ridl_engine].has_production?(id)
  end

  def IDL.production(id)
    return nil unless engine?

    Thread.current[:ridl_engine].production(id)
  end

  def IDL.verbose_level
    Thread.current[:ridl_engine] ? Thread.current[:ridl_engine].verbose_level : OPTIONS[:verbose]
  end

  def IDL.verbose_level=(l)
    if Thread.current[:ridl_engine]
      Thread.current[:ridl_engine].verbose_level = l.to_i
    else
      OPTIONS[:verbose] = l.to_i
    end
  end

  def IDL.log(level, message)
    STDERR.puts message if verbose_level >= level
  end

  def IDL.error(message)
    STDERR.puts(message)
  end

  def IDL.fatal(message)
    STDERR.puts(message, 'Exiting.')
    exit 1
  end

  def IDL.init(argv = ARGV)
    options = OPTIONS.dup

    # load config file(s) if any
    Options.load_config(options)

    IDL.log(2, "Configuration [#{options}]")

    # check commandline args for explicit language mapping backend
    if argv.first =~ /^:\S+/
      be_name = argv.shift.reverse.chop.reverse.to_sym
    elsif ENV['RIDL_BE_SELECT']   # or from environment
      be_name = ENV['RIDL_BE_SELECT'].to_sym
    elsif options[:backend]       # or from configuration
      be_name = options[:backend].to_sym
    end

    # add optional search paths for RIDL backends
    options[:be_path] ||= []
    options[:be_path].unshift(*ENV['RIDL_BE_PATH'].split(/#{File::PATH_SEPARATOR}/)) if ENV['RIDL_BE_PATH']
    options[:be_path].collect! { |p| p.gsub('\\', '/') } # cleanup to prevent mixed path separators
    $:.concat(options[:be_path]) unless options[:be_path].empty?

    # check for special bootstrapping switches
    if argv.first == '--preprocess'
      options[:preprocess] = true
      argv.shift
    elsif argv.first == '--ignore-pidl'
      options[:ignore_pidl] = true
      argv.shift
    end

    # create RIDL engine
    Thread.current[:ridl_engine] = Engine.new(be_name, options)
  end

  # main run method
  #
  def IDL.run(argv = ARGV)
    # run default engine if available
    if Thread.current[:ridl_engine]
      exit(1) unless Thread.current[:ridl_engine].run(argv)
    end
  end # IDL.run
end