#!/usr/bin/env ruby
STDOUT.sync = true

$:.unshift File.join(File.dirname(__FILE__), *%w{ .. lib })

require 'optparse'
require 'iesd'

begin
  raise "iESD is only supported on OS X." unless Kernel.system('[ "$(/usr/bin/uname)" = "Darwin" ]')
rescue Exception => e
  opoo e.message
  abort
end

options = {
  :input => nil,
  :output => nil,
  :type => nil,
  :interactive => false,
  :hdiutil => {
    :resize => {
      :grow => 0,
      :shrink => false
    }
  },
  :mach_kernel => nil,
  :extensions => {
    :install => [],
    :uninstall=> [],
    :kextcache => nil,
    :postinstall => true
  }
}

optparse = OptionParser.new do |opts|
  opts.banner = "Usage: #{File.basename $0} -i <inputfile> -o <outputfile> [options]"

  opts.separator ""
  opts.separator "Specific options:"

  opts.on("-i", "--input file",
          "Specify the input dmg or app.") do |input|
    options[:input] = File.absolute_path input
  end

  opts.on("-o", "--output file",
          "Specify the output dmg.") do |output|
    output << ".dmg" if File.extname(output).downcase != ".dmg"
    options[:output] = File.absolute_path output
  end

  opts.on("-t", "--type type", [:BaseSystem, :InstallESD],
          "Specify the output type.  Type could be BaseSystem or InstallESD.") do |type|
    options[:type] = type
  end

  opts.on("-s", "--[no-]interactive-shell", "Open #{ENV["SHELL"]} inside the temporary mount directory.") do |interactive|
    options[:interactive] = interactive
  end

  opts.separator ""
  opts.separator "HDIUtil options:"

  opts.on("--grow sectors", OptionParser::DecimalInteger,
          "Specify the size of the image to grow in 512-byte sectors.") do |sectors|
    options[:hdiutil][:resize][:grow] = sectors
  end

  opts.on("--[no-]shrink", "Do [not] shrink the output image.") do |s|
    options[:hdiutil][:resize][:shrink] = s
  end

  opts.separator ""
  opts.separator "Boot options:"

  opts.on("--[no-]fallback-kernel", "Do [not] fallback to mach_kernel when kernelcache fails to boot.") do |k|
    options[:mach_kernel] = k
  end

  opts.separator ""
  opts.separator "Extensions options:"

  opts.on("--install-extension kext",
          "Add the kext to the list of the extensions to be installed.") do |kext|
    options[:extensions][:install].push File.absolute_path kext
  end

  opts.on("--uninstall-extension kext",
          "Add the kext to the list of the extensions to be uninstalled.") do |kext|
    options[:extensions][:uninstall].push kext
  end

  opts.on("--[no-]postinstall-extensions", "Do [not] patch OSInstall.pkg to postinstall extensions.") do |p|
    options[:extensions][:postinstall] = p
  end

  opts.on("--[no-]update-kernelcache", "Do [not] rebuild the startup kernelcache.") do |c|
    options[:extensions][:kextcache] = c
  end

  opts.separator ""
  opts.separator "Common options:"

  opts.on("--[no-]verbose", "Run verbosely") do |v|
    ENV['VERBOSE'] = '1'
  end

  opts.on_tail("-h", "--help", "Show this message") do
    puts opts
    exit
  end
end

begin
  optparse.parse!
  missing_options = [:input, :output].select { |param| options[param].nil? }
  raise "missing options: #{(missing_options.map { |param| "-#{param.to_s[0]}" }).join(', ')}" unless missing_options.empty?
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
  onoe $!.to_s
  puts optparse
  abort
rescue SystemExit
  raise
rescue Exception => e
  onoe e.message
  puts optparse
  abort
end

begin
  raise "input file does not exist" unless File.exist? options[:input]
  raise "output file already exists" if File.exist? options[:output]
  options[:extensions][:uninstall].select { |kext|
    if File.extname(kext) == ".kext"
      true
    else
      opoo "invalid kext: #{kext}"
      false
    end
  }
  options[:extensions][:install].each { |kext|
    raise "invalid kext: #{kext}" unless File.exist? File.join(kext, *%w{ Contents MacOS }, File.basename(kext, ".kext"))
  }
  if (iesd = IESD.new options[:input])
    iesd_type = iesd.class.name.split("::").last
    types = [nil, :BaseSystem, :InstallESD, :InstallOSX]
    if types.index(options[:type]) > types.index(iesd_type.to_sym)
      raise "invalid output type #{options[:type].to_s} for input type #{iesd_type}"
    end
    iesd.export options
    puts "\xF0\x9F\x8D\xBA  #{options[:output]}"
  else
    raise "invalid input file"
  end
rescue SignalException => e
  abort
rescue StandardError => e
  onoe e.message
  abort
end

