AndyObtiva/glimmer-dsl-swt

View on GitHub
lib/glimmer/launcher.rb

Summary

Maintainability
A
3 hrs
Test Coverage
F
27%
# Copyright (c) 2007-2024 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

if ARGV.include?('--bundler') && File.exist?(File.expand_path('./Gemfile'))
  require 'bundler'
  Bundler.setup(:default)
end
require 'fileutils'
require 'os'

module Glimmer
  # Launcher of glimmer applications and main entry point for the `glimmer` command.
  class Launcher
    OPERATING_SYSTEMS_SUPPORTED = ["mac", "windows", "linux"]
    
    # TODO convert to a bash script to achieve faster startup time
    
    TEXT_USAGE = <<~MULTI_LINE_STRING
      Glimmer (JRuby Desktop Development GUI Framework) - JRuby Gem: glimmer-dsl-swt v#{File.read(File.expand_path('../../../VERSION', __FILE__))}
      Usage: glimmer [--bundler] [--pd] [--quiet] [--debug] [--log-level=VALUE] [[ENV_VAR=VALUE]...] [[-jruby-option]...] (application.rb or task[task_args])
    
      Runs Glimmer applications and tasks.
    
      When applications are specified, they are run using JRuby,
      automatically preloading the glimmer Ruby gem and SWT jar dependency.
    
      Optionally, extra Glimmer options, JRuby options, and/or environment variables may be passed in.
    
      Glimmer options:
      - "--bundler=GROUP"   : Activates gems in Bundler default group in Gemfile
      - "--pd=BOOLEAN"      : Requires puts_debuggerer to enable pd method
      - "--quiet=BOOLEAN"   : Does not announce file path of Glimmer application being launched
      - "--debug"           : Displays extra debugging information, passes "--debug" to JRuby, and enables debug logging
      - "--log-level=VALUE" : Sets Glimmer's Ruby logger level ("ERROR" / "WARN" / "INFO" / "DEBUG"; default is none)
    
      Tasks are run via rake. Some tasks take arguments in square brackets.
    
      Available tasks are below (if you do not see any, please add `require 'glimmer/rake_task'` to Rakefile and rerun or run rake -T):
      
    MULTI_LINE_STRING

    GLIMMER_LIB_LOCAL = File.expand_path(File.join('lib', 'glimmer-dsl-swt.rb'))
    GLIMMER_LIB_GEM = 'glimmer-dsl-swt'
    GLIMMER_OPTIONS = %w[--log-level --quiet --bundler --pd]
    GLIMMER_OPTION_ENV_VAR_MAPPING = {
      '--log-level' => 'GLIMMER_LOGGER_LEVEL'   ,
      '--bundler'   => 'GLIMMER_BUNDLER_SETUP'  ,
      '--pd'        => 'PD'  ,
    }
    REGEX_RAKE_TASK_WITH_ARGS = /^([^\[]+)\[?([^\]]*)\]?$/

    class << self
      def platform_os
        OPERATING_SYSTEMS_SUPPORTED.detect {|os| OS.send("#{os}?")}
      end
      
      def is_arm64?
        host_cpu = OS.host_cpu.downcase
        host_cpu.include?('aarch64') || host_cpu.include?('arm')
      end
      
      def special_cpu_architecture_suffix
        is_arm64? ? "_aarch64" : ''
      end

      def swt_jar_file
        @swt_jar_file ||= File.expand_path(File.join(__FILE__, '..', '..', '..', 'vendor', 'swt', platform_os + special_cpu_architecture_suffix, 'swt.jar'))
      end
 
      def jruby_os_specific_options
        OS.mac? ? "-J-XstartOnFirstThread" : ""
      end

      def glimmer_lib
        unless @glimmer_lib
          @glimmer_lib = GLIMMER_LIB_GEM
          if File.exists?(GLIMMER_LIB_LOCAL)
            @glimmer_lib = GLIMMER_LIB_LOCAL
            puts "[DEVELOPMENT MODE] (detected #{@glimmer_lib})"
          end
        end
        @glimmer_lib
      end
      
      def dev_mode?
        glimmer_lib == GLIMMER_LIB_LOCAL
      end

      def glimmer_option_env_vars(glimmer_options)
        GLIMMER_OPTION_ENV_VAR_MAPPING.reduce({}) do |hash, pair|
          glimmer_options[pair.first] ? hash.merge(GLIMMER_OPTION_ENV_VAR_MAPPING[pair.first] => glimmer_options[pair.first]) : hash
        end
      end

      def load_env_vars(env_vars)
        env_vars.each do |key, value|
          ENV[key] = value
        end
      end

      def launch(application, jruby_options: [], env_vars: {}, glimmer_options: {})
        jruby_options_string = jruby_options.join(' ') + ' ' if jruby_options.any?
        env_vars = env_vars.merge(glimmer_option_env_vars(glimmer_options))
        env_vars.each do |k,v|
          ENV[k] = v
        end
        the_glimmer_lib = glimmer_lib
        require 'puts_debuggerer' if the_glimmer_lib == GLIMMER_LIB_LOCAL
        is_rake_task = !application.end_with?('.rb')
        rake_tasks = []
        if is_rake_task
          load File.expand_path('./Rakefile') if File.exist?(File.expand_path('./Rakefile')) && caller.join("\n").include?('/bin/glimmer:')
          require_relative 'rake_task'
          rake_tasks = Rake.application.tasks.map(&:to_s).map {|t| t.sub('glimmer:', '')}
           
          # handle a bash quirk with calling package[msi] while there is a "packages" directory locally (it passes package[msi] as packages)
          application = 'package[msi]' if application == 'packages'
          
          potential_rake_task_parts = application.match(REGEX_RAKE_TASK_WITH_ARGS)
          application = potential_rake_task_parts[1]
          rake_task_args = potential_rake_task_parts[2].split(',')
        end
        if rake_tasks.include?(application)
          load_env_vars(glimmer_option_env_vars(glimmer_options))
          rake_task = "glimmer:#{application}"
          puts "Running Glimmer rake task: #{rake_task}" if jruby_options_string.to_s.include?('--debug')
          Rake::Task[rake_task].invoke(*rake_task_args)
        else
          puts "Launching Glimmer Application: #{application}" if jruby_options_string.to_s.include?('--debug') || glimmer_options['--quiet'].to_s.downcase != 'true'
          require the_glimmer_lib
          load File.expand_path(application)
        end
      end
    end
    
    attr_reader :application_paths
    attr_reader :env_vars
    attr_reader :glimmer_options
    attr_reader :jruby_options

    def initialize(raw_options)
      raw_options << '--quiet' if !caller.join("\n").include?('/bin/glimmer:') && !raw_options.join.include?('--quiet=')
      raw_options << '--log-level=DEBUG' if raw_options.join.include?('--debug') && !raw_options.join.include?('--log-level=')
      @application_path = extract_application_path(raw_options)
      @env_vars = extract_env_vars(raw_options)
      @glimmer_options = extract_glimmer_options(raw_options)
      @jruby_options = raw_options
    end

    def launch
      if @application_path.nil?
        display_usage
      else
        launch_application
      end
    end

    private

    def launch_application
      self.class.launch(
        @application_path,
        jruby_options: @jruby_options,
        env_vars: @env_vars,
        glimmer_options: @glimmer_options
      )
    end

    def display_usage
      puts TEXT_USAGE
      display_tasks
    end
    
    def display_tasks
      if OS.windows? || Launcher.is_arm64?
        require 'rake'
        Rake::TaskManager.record_task_metadata = true
        require_relative 'rake_task'
        tasks = Rake.application.tasks
        task_lines = tasks.reject do |task|
          task.comment.nil?
        end.map do |task|
          max_task_size = tasks.map(&:name_with_args).map(&:size).max + 1
          task_name = task.name_with_args.sub('glimmer:', '')
          line = "glimmer #{task_name.ljust(max_task_size)} # #{task.comment}"
        end
        puts task_lines.to_a
      else
        require 'rake-tui'
        require 'tty-screen'
        require_relative 'rake_task'
        Rake::TUI.run(branding_header: nil, prompt_question: 'Select a Glimmer task to run:') do |task, tasks|
          max_task_size = tasks.map(&:name_with_args).map(&:size).max + 1
          task_name = task.name_with_args.sub('glimmer:', '')
          line = "glimmer #{task_name.ljust(max_task_size)} # #{task.comment}"
          bound = TTY::Screen.width - 6
          line.size <= bound ? line : "#{line[0..(bound - 3)]}..."
        end
      end
    end

    # Extract application path (which can also be a rake task, basically a non-arg)
    def extract_application_path(options)
      application_path = options.detect do |option|
        !option.start_with?('-') && !option.include?('=')
      end.tap do
        options.delete(application_path)
      end
    end

    def extract_env_vars(options)
      options.select do |option|
        !option.start_with?('-') && option.include?('=')
      end.each do |env_var|
        options.delete(env_var)
      end.reduce({}) do |hash, env_var_string|
        match = env_var_string.match(/^([^=]+)=(.+)$/)
        hash.merge(match[1] => match[2])
      end
    end

    def extract_glimmer_options(options)
      options.select do |option|
        GLIMMER_OPTIONS.reduce(false) do |result, glimmer_option|
          result || option.include?(glimmer_option)
        end
      end.each do |glimmer_option|
        options.delete(glimmer_option)
      end.reduce({}) do |hash, glimmer_option_string|
        match = glimmer_option_string.match(/^([^=]+)=?(.+)?$/)
        hash.merge(match[1] => (match[2] || 'true'))
      end
    end
  end
end