class Bootsnap::CLI

def cache_dir=(dir)

def cache_dir=(dir)
  @cache_dir = File.expand_path(File.join(dir, "bootsnap/compile-cache"))
end

def exclude_pattern(pattern)

def exclude_pattern(pattern)
  (@exclude_patterns ||= []) << Regexp.new(pattern)
  self.exclude = Regexp.union(@exclude_patterns)
end

def fix_default_encoding

def fix_default_encoding
  if Encoding.default_external == Encoding::US_ASCII
    Encoding.default_external = Encoding::UTF_8
    begin
      yield
    ensure
      Encoding.default_external = Encoding::US_ASCII
    end
  else
    yield
  end
end

def initialize(argv)

def initialize(argv)
  @argv = argv
  self.cache_dir = ENV.fetch("BOOTSNAP_CACHE_DIR", "tmp/cache")
  self.compile_gemfile = false
  self.exclude = nil
  self.verbose = false
  self.jobs = Etc.nprocessors
  self.iseq = true
  self.yaml = true
  self.json = true
end

def invalid_usage!(message)

def invalid_usage!(message)
  STDERR.puts message
  STDERR.puts
  STDERR.puts parser
  1
end

def list_files(path, pattern)

def list_files(path, pattern)
  if File.directory?(path)
    Dir[File.join(path, pattern), sort: false]
  elsif File.exist?(path)
    [path]
  else
    []
  end
end

def list_files(path, pattern)

def list_files(path, pattern)
  if File.directory?(path)
    Dir[File.join(path, pattern)]
  elsif File.exist?(path)
    [path]
  else
    []
  end
end

def parser

def parser
  @parser ||= OptionParser.new do |opts|
    opts.banner = "Usage: bootsnap COMMAND [ARGS]"
    opts.separator ""
    opts.separator "GLOBAL OPTIONS"
    opts.separator ""
    help = <<~HELP
      Path to the bootsnap cache directory. Defaults to tmp/cache
    HELP
    opts.on("--cache-dir DIR", help.strip) do |dir|
      self.cache_dir = dir
    end
    help = <<~HELP
      Print precompiled paths.
    HELP
    opts.on("--verbose", "-v", help.strip) do
      self.verbose = true
    end
    help = <<~HELP
      Number of workers to use. Default to number of processors, set to 0 to disable multi-processing.
    HELP
    opts.on("--jobs JOBS", "-j", help.strip) do |jobs|
      self.jobs = Integer(jobs)
    end
    opts.separator ""
    opts.separator "COMMANDS"
    opts.separator ""
    opts.separator "    precompile [DIRECTORIES...]: Precompile all .rb files in the passed directories"
    help = <<~HELP
      Precompile the gems in Gemfile
    HELP
    opts.on("--gemfile", help) { self.compile_gemfile = true }
    help = <<~HELP
      Path pattern to not precompile. e.g. --exclude 'aws-sdk|google-api'
    HELP
    opts.on("--exclude PATTERN", help) { |pattern| exclude_pattern(pattern) }
    help = <<~HELP
      Disable ISeq (.rb) precompilation.
    HELP
    opts.on("--no-iseq", help) { self.iseq = false }
    help = <<~HELP
      Disable YAML precompilation.
    HELP
    opts.on("--no-yaml", help) { self.yaml = false }
    help = <<~HELP
      Disable JSON precompilation.
    HELP
    opts.on("--no-json", help) { self.json = false }
  end
end

def precompile_command(*sources)

def precompile_command(*sources)
  require "bootsnap/compile_cache/iseq"
  require "bootsnap/compile_cache/yaml"
  require "bootsnap/compile_cache/json"
  fix_default_encoding do
    Bootsnap::CompileCache::ISeq.cache_dir = cache_dir
    Bootsnap::CompileCache::YAML.init!
    Bootsnap::CompileCache::YAML.cache_dir = cache_dir
    Bootsnap::CompileCache::JSON.init!
    Bootsnap::CompileCache::JSON.cache_dir = cache_dir
    @work_pool = WorkerPool.create(size: jobs, jobs: {
      ruby: method(:precompile_ruby),
      yaml: method(:precompile_yaml),
      json: method(:precompile_json),
    })
    @work_pool.spawn
    main_sources = sources.map { |d| File.expand_path(d) }
    precompile_ruby_files(main_sources)
    precompile_yaml_files(main_sources)
    precompile_json_files(main_sources)
    if compile_gemfile
      # Some gems embed their tests, they're very unlikely to be loaded, so not worth precompiling.
      gem_exclude = Regexp.union([exclude, "/spec/", "/test/"].compact)
      precompile_ruby_files($LOAD_PATH.map { |d| File.expand_path(d) }, exclude: gem_exclude)
      # Gems that include JSON or YAML files usually don't put them in `lib/`.
      # So we look at the gem root.
      gem_pattern = %r{^#{Regexp.escape(Bundler.bundle_path.to_s)}/?(?:bundler/)?gems\/[^/]+}
      gem_paths = $LOAD_PATH.map { |p| p[gem_pattern] }.compact.uniq
      precompile_yaml_files(gem_paths, exclude: gem_exclude)
      precompile_json_files(gem_paths, exclude: gem_exclude)
    end
    if (exitstatus = @work_pool.shutdown)
      exit(exitstatus)
    end
  end
  0
end

def precompile_json(*json_files)

def precompile_json(*json_files)
  Array(json_files).each do |json_file|
    if CompileCache::JSON.precompile(json_file)
      STDERR.puts(json_file) if verbose
    end
  end
end

def precompile_json_files(load_paths, exclude: self.exclude)

def precompile_json_files(load_paths, exclude: self.exclude)
  return unless json
  load_paths.each do |path|
    if !exclude || !exclude.match?(path)
      list_files(path, "**/*.json").each do |json_file|
        # We ignore hidden files to not match the various .config.json files
        if !File.basename(json_file).start_with?(".") && (!exclude || !exclude.match?(json_file))
          @work_pool.push(:json, json_file)
        end
      end
    end
  end
end

def precompile_ruby(*ruby_files)

def precompile_ruby(*ruby_files)
  Array(ruby_files).each do |ruby_file|
    if CompileCache::ISeq.precompile(ruby_file)
      STDERR.puts(ruby_file) if verbose
    end
  end
end

def precompile_ruby_files(load_paths, exclude: self.exclude)

def precompile_ruby_files(load_paths, exclude: self.exclude)
  return unless iseq
  load_paths.each do |path|
    if !exclude || !exclude.match?(path)
      list_files(path, "**/{*.rb,*.rake,Rakefile}").each do |ruby_file|
        if !exclude || !exclude.match?(ruby_file)
          @work_pool.push(:ruby, ruby_file)
        end
      end
    end
  end
end

def precompile_yaml(*yaml_files)

def precompile_yaml(*yaml_files)
  Array(yaml_files).each do |yaml_file|
    if CompileCache::YAML.precompile(yaml_file)
      STDERR.puts(yaml_file) if verbose
    end
  end
end

def precompile_yaml_files(load_paths, exclude: self.exclude)

def precompile_yaml_files(load_paths, exclude: self.exclude)
  return unless yaml
  load_paths.each do |path|
    if !exclude || !exclude.match?(path)
      list_files(path, "**/*.{yml,yaml}").each do |yaml_file|
        # We ignore hidden files to not match the various .ci.yml files
        if !File.basename(yaml_file).start_with?(".") && (!exclude || !exclude.match?(yaml_file))
          @work_pool.push(:yaml, yaml_file)
        end
      end
    end
  end
end

def run

def run
  parser.parse!(argv)
  command = argv.shift
  method = "#{command}_command"
  if respond_to?(method)
    public_send(method, *argv)
  else
    invalid_usage!("Unknown command: #{command}")
  end
end