#!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -wKU
# == Synopsis
#
# gen_build: create build.ninja based on target files
#
# == Usage
#
# --help:
#    show help.
#
# --output/-o «file»:
#    the «file» to write build info into.
#
# --build-directory/-C «directory»:
#    where build files should go.
#
# --define/-d «name»=«value»:
#    define a variable that ends up in build.ninja.

require 'getoptlong'
require 'rdoc/usage'
require 'shellwords'
require 'set'
require 'pp'

GLOB_KEYS     = %w{ CP_Resources CP_SharedSupport CP_PlugIns EXPORT HTML_FOOTER HTML_HEADER MARKDOWN_HEADER MARKDOWN_FOOTER PRELUDE SOURCES TARGETS TEST_SOURCES }
STRING_KEYS   = %w{ TARGET_NAME BUNDLE_EXTENSION FLAGS C_FLAGS CXX_FLAGS OBJC_FLAGS OBJCXX_FLAGS LN_FLAGS PLIST_FLAGS BUILD }

COMPILER_INFO = {
  'c'  => { :rule => 'build_c',   :compiler => '$CC',  :flags => '$C_FLAGS',      },
  'm'  => { :rule => 'build_m',   :compiler => '$CC',  :flags => '$OBJC_FLAGS',   },
  'cc' => { :rule => 'build_cc',  :compiler => '$CXX', :flags => '$CXX_FLAGS',    },
  'mm' => { :rule => 'build_mm',  :compiler => '$CXX', :flags => '$OBJCXX_FLAGS', },
}

class Targets
  include Enumerable

  attr_reader :root
  attr_reader :dependencies

  def initialize(path)
    targets = all_targets(path)
    linked  = Set.new(targets.collect { |target| target['LINK'].to_a }.flatten)

    @root = targets.first
    @all, @dependencies = [ ], [ ]

    targets.each do |target|
      next if target['SOURCES'].to_a.empty?
      @all << target
      if linked.include?(target[:name]) || target['BUILD'] == 'lib'
        target[:type] = :library
      else
        has_info_plist = target['CP_Resources'].to_a.find { |path| path =~ /\bInfo\.plist$/ }
        target[:type] = has_info_plist ? :bundle : :executable
      end
    end

    paths = [ ]
    targets.each do |target|
      paths << target[:full_path]
      target.each do |key, value|
        paths << value.reject { |path| path =~ /^@/ }.map do |path|
          File.dirname(File.join(target[:base], path))
        end if GLOB_KEYS.include?(key) && key != 'PRELUDE'
      end
    end
    @dependencies = paths.flatten.sort.uniq
  end

  def target_named(name)
    self.find { |target| target[:name] == name }
  end

  def each(&block)
    @all.each(&block)
  end

  private

  def parse(path, root_dir, parent = { })
    base = File.dirname(path)
    dict = parent.merge({ :name => File.basename(base), :base => base, :path => base.sub(/^#{Regexp.escape root_dir}\//, ''), :full_path => path })
    dict.delete('TARGETS')

    data = File.read(path)
    assignments = data.scan(/^(\w+)\s*(\+=|-=|=)[ \t]*(.*)$/)
    assignments.each do |arr|
      key, op, value = *arr
      value = Shellwords.shellwords(value) unless STRING_KEYS.include? key

      if GLOB_KEYS.include?(key)
        globs   = value.reject { |v| v =~ /^@/ }
        targets = value.reject { |v| v !~ /^@/ }

        Dir.chdir(File.dirname(path)) do
          value = Dir.glob(globs)
          value = value.concat(targets)
        end
      end

      unless op != '+=' || dict[key].nil?
        if STRING_KEYS.include? key
          value = "#{dict[key]} #{value}"
        else
          value = dict[key].dup.concat(value)
        end
      end

      dict[key] = value
    end

    if dict.has_key?('TARGET_NAME')
      dict[:name] = dict['TARGET_NAME']
      dict.delete('TARGET_NAME')
    end

    dict
  end

  def recursive_parse(target, root_dir)
    res = [ target ]
    target["TARGETS"].to_a.each do |subtarget|
      res << recursive_parse(parse(File.join(target[:base], subtarget), root_dir, target), root_dir)
    end
    res
  end

  def all_targets(path)
    recursive_parse(parse(path, File.dirname(path)), File.dirname(path)).flatten
  end
end

def esc_path(path)
  if path.class == String
    return path if path =~ /^\$builddir/
    path.gsub(/[ :$]/, '$\\&')
  else
    path.map { |obj| esc_path(obj) }
  end
end

def build_path(path)
  "$builddir/#{path.gsub(/[ :$]/, '$\\&')}"
end

def prepend(prefix, array, sep = ' ')
  array.map do |item|
    item =~ %r{/} ? item : esc_path("#{prefix}#{item}")
  end.join(sep)
end

def flags_for_target(target)
  res = ""
  target.each do |key, value|
    res << "  #{key} = #{value}\n" unless key.class != String || key !~ /FLAGS$/ || value == TARGETS.root[key]
  end
  res
end

def compiler_for(source)
  COMPILER_INFO[source[/\b[a-z]+$/]][:rule]
end

def pch_for(src, target)
  target['PRELUDE'].each do |pch|
    return "#{build_path(pch)}" if src[/\b[a-z]+$/] == pch[/\b[a-z]+$/]
  end
  abort "*** no pre-compiled header for #{src} (target: #{target[:name]})"
end

def all_values_for_key(target, key)
  all_targets = { }
  new_targets = [ target ]
  until new_targets.empty?
    target = new_targets.pop
    unless all_targets.include? target[:base]
      all_targets[target[:base]] = target
      target['LINK'].to_a.each do |name|
        TARGETS.each { |target| new_targets << target if name == target[:name] }
      end
    end
  end

  all_targets.map { |path, target| target[key].to_a }.flatten.sort.uniq
end

def headers(target)
  res = ''
  return res if target['EXPORT'].to_s.empty?
  headers = [ ]
  target['EXPORT'].to_a.each do |path|
    src = File.join(target[:path], path)
    dst = build_path("include/#{target[:name]}/#{File.basename(path)}")
    res << "build #{dst}: cp #{esc_path src}\n"
    headers << dst
  end
  headers = headers.concat(target['LINK'].map { |lib| "#{esc_path lib}/headers" }) unless target['LINK'].nil?
  res << "build #{target[:name]}/headers: phony | #{headers.join(' ')}\n"
  res
end

def sources(target)
  res = ''
  objects = [ ]
  headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
  target['SOURCES'].each do |path|
    src = File.join(target[:path], path)
    case src
      when /^(.*)\.(c|cc|m|mm)$/ then
        base, ext = $1, $2
        cc  = compiler_for(src)
        dst = build_path("#{base}.o")
        res << "build #{dst}: #{cc} #{esc_path src} | #{pch_for(src, target)}.gch || #{esc_path(headers).join(' ')}\n"
        res << "  depfile = #{dst}.d\n"
        res << "  RAVE_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(src, target)}\n"
        res << flags_for_target(target)

        objects << dst

      when /^(.*)\.(rl)$/ then
        base, ext = $1, $2
        dst = build_path("#{base}.cc")
        res << "build #{dst}: gen_ragel #{esc_path src} || #{esc_path(headers).join(' ')}\n"

        ext = 'cc'
        cc  = compiler_for(dst)
        obj = build_path("#{base}.o")
        res << "build #{obj}: #{cc} #{dst} | #{pch_for(dst, target)}.gch || #{esc_path(headers).join(' ')}\n"
        res << "  depfile = #{obj}.d\n"
        res << "  RAVE_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(dst, target)} -iquote#{esc_path File.dirname(src)}\n"
        res << flags_for_target(target)

        objects << obj

      else
        STDERR << "*** unhandled source type ‘#{path}’\n"
    end
  end
  target[:objects] = objects

  res
end

def clean(target)
  res = ''
  dst = build_path(File.join(target[:path]))
  res << "build #{dst}/#{target[:name]}.clean: clean\n"
  res << "  path = #{dst}\n"
  res << "build #{target[:name]}/clean: phony #{dst}/#{target[:name]}.clean\n"
end

def tests(target)
  res = ''
  unless target['TEST_SOURCES'].to_s.empty?
    headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
    src = target['TEST_SOURCES'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
    dst = build_path("#{target[:path]}/test_#{target[:name]}")
    ext = target['TEST_SOURCES'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'

    res << "build #{dst}.#{ext}: gen_test #{src}\n"
    res << "build #{dst}.o: #{compiler_for("#{dst}.#{ext}")} #{dst}.#{ext} | #{pch_for(src, target)}.gch || #{target[:name]}/headers\n"
    res << "  depfile = #{dst}.o.d\n"
    res << "  RAVE_FLAGS = -Ibin/CxxTest -include #{pch_for(src, target)}\n"
    res << "build #{dst}: link_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
    res << flags_for_target(target)

    fws  = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
    libs = prepend('-l', all_values_for_key(target, 'LIBS'))
    res << "  RAVE_FLAGS = #{fws} #{libs}\n"

    res << "build #{dst}.run: run_test #{dst}\n"
    res << "build #{dst}.coerce: skip_test #{dst}\n"
    res << "  seal = #{dst}.run\n"

    res << "build #{target[:name]}:        phony #{dst}.run\n"
    res << "build #{target[:name]}/coerce: phony #{dst}.coerce\n"

    target[:test] = "#{dst}.run"
  end
  res
end

def resources_helper(target, key, symbol)
  res = ''
  rsrc = [ ]
  all_resources = [ ]

  target[key].to_a.each do |path|
    if path =~ /^@/
      src = TARGETS.target_named($')
      abort "*** target does not exist: ‘#{path}’.\nKnown targets:\n - #{TARGETS.map { |target| target[:name] }.join("\n - ")}\n" if src.nil?
      abort "*** no resources in target: ‘#{path}’ (from target: #{target[:name]}).\n" unless src.has_key?(:rsrc_info)
      src[:rsrc_info].each do |info|
        rsrc << { :src => info[:src], :dst => esc_path(info[:install_name]) }
      end
    else
      src = File.join(target[:path], path)
      abs = File.join(target[:base], path)
      if File.directory?(abs) && !File.symlink?(abs) && abs !~ /\.xcdatamodeld$/
        Dir.chdir(abs) { Dir.glob("**/*") }.each do |file|
          all_resources << { :path => File.dirname(src), :file => File.join(File.basename(src), file) } unless File.directory?(File.join(abs, file))
        end
      else
        all_resources << { :path => File.dirname(src), :file => File.basename(src) }
      end
    end
  end

  all_resources.each do |dict|
    path, file = dict[:path], dict[:file]
    src = File.join(path, file)

    case file
      when /(.+)\.xib$/ then
        dst = build_path(File.join(path, "#$1.nib"))
        res << "build #{dst}: compile_xib #{esc_path src}\n"
        rsrc << { :src => dst, :dst => esc_path("#$1.nib") }
      when /(.+)\.xcdatamodeld$/ then
        dst = build_path(File.join(path, "#$1.mom"))
        res << "build #{dst}: compile_mom #{esc_path src}\n"
        rsrc << { :src => dst, :dst => esc_path("#$1.mom") }
      when /\bInfo\.plist$/ then
        dst = build_path(File.join(path, file))
        res << "build #{dst}: process_plist #{esc_path src}\n"
        res << "  RAVE_FLAGS = -dTARGET_NAME='#{target[:name]}'\n"
        res << flags_for_target(target)
        target[:info_plist] = dst
      when /(.+)\.md(own)?$/ then
        dst = build_path(File.join(path, "#$1.html"))
        prefix, suffix = target['MARKDOWN_HEADER'].to_a, target['MARKDOWN_FOOTER'].to_a
        res << "build #{dst}: markdown #{prefix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')} #{esc_path src} #{suffix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')}\n"
        header, footer = target['HTML_HEADER'], target['HTML_FOOTER']
        flags = ''
        flags << " -h #{esc_path(File.join(target[:path], header.first))}" if header.to_a.size == 1
        flags << " -f #{esc_path(File.join(target[:path], footer.first))}" if footer.to_a.size == 1
        res << "  md_flags =#{flags}\n"
        rsrc << { :src => dst, :dst => esc_path("#$1.html") }
      else
        rsrc << { :src => esc_path(src), :dst => esc_path(file) }
    end
  end

  help_files = rsrc.find_all { |dict| dict[:src] =~ %r{[^/]+ Help/.+\.html$} }
  unless help_files.empty?
    src = "#$&/#{target[:name]}.helpindex" if help_files.first[:src] =~ /.* Help(?=\/)/
    dst = "#$&/#{target[:name]}.helpindex" if help_files.first[:dst] =~ /.* Help(?=\/)/

    res << "build #{src}: index_help | #{help_files.map { |dict| dict[:src] }.join(' ')}\n"
    res << "  HELP_BOOK = #{File.dirname(src)}\n"

    rsrc << { :src => src, :dst => dst }
  end

  target[symbol] = rsrc

  res
end

def resources(target)
  res = ''
  target.keys.each do |key|
    res << resources_helper(target, key, key.to_sym) if key.class == String && key =~ /^CP_/
  end
  res
end

def object_archive(target)
  dst = build_path("#{target[:path]}/lib#{target[:name]}.a")
  target[:lib] = dst
  res = ''
  res << "build #{dst}: link_static #{target[:objects].join(' ')} | #{target[:test]}\n"
  res << flags_for_target(target)
end

def dynamic_lib(target)
  dst = target[:dylib] = build_path("#{target[:path]}/#{target[:name]}.dylib")

  if target[:CP_Resources].to_a.empty?
    install_name = "@executable_path/../Frameworks/#{target[:name]}.dylib"
  else
    install_name = "@executable_path/../Frameworks/#{target[:name]}.framework/Versions/A/#{target[:name]}"
  end

  link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }

  res = ''
  res << "build #{dst}: link_dynamic #{target[:objects].join(' ')} | #{target[:test]} #{link_with.join(' ')}\n"
  res << flags_for_target(target)
  fws  = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
  libs = prepend('-l', all_values_for_key(target, 'LIBS'))
  res << "  RAVE_FLAGS = -install_name #{esc_path install_name} #{link_with.join(' ')} #{fws} #{libs}\n"
end

def framework_bundle(target, dst_dir = nil)
  res = ''
  dst = File.join(dst_dir || build_path("#{target[:path]}"), "#{target[:name]}.framework")

  deps = [ ]

  target.each do |key, files|
    if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
      install_dir = $1
      files.each do |rsrc|
        deps << "#{dst}/Versions/A/#{install_dir}/#{rsrc[:dst]}"
        res << "build #{deps.last}: cp #{rsrc[:src]}\n"
      end
    end
  end

  deps << "#{dst}/Versions/A/Resources/Info.plist"
  res << "build #{deps.last}: cp #{target[:info_plist]}\n"

  deps << "#{dst}/Versions/A/#{target[:name]}"
  res << "build #{deps.last}: cp #{target[:dylib]}\n"

  deps << "#{dst}/Versions/Current"
  res << "build #{deps.last}: ln\n  link = A\n"
  deps << "#{dst}/Resources"
  res << "build #{deps.last}: ln\n  link = Versions/Current/Resources\n"
  deps << "#{dst}/#{target[:name]}"
  res << "build #{deps.last}: ln\n  link = Versions/Current/#{target[:name]}\n"

  res << "build #{dst}: phony | #{deps.join(' ')}\n"
end

def static_executable(target)
   objects = [ ]
   all_values_for_key(target, 'LINK').each do |name|
     objects << TARGETS.target_named(name)[:lib]
   end
   objects << target[:objects]

   res = ''

   dst = build_path("#{target[:path]}/#{target[:name]}")
   target[:static_executable] = dst
   target[:rsrc_info] = [ { :install_name => "#{target[:name]}", :src => dst } ]

   res << "build #{dst}: link_executable #{objects.flatten.join(' ')}\n"
   res << flags_for_target(target)
   fws  = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
   libs = prepend('-l', all_values_for_key(target, 'LIBS'))
   res << "  RAVE_FLAGS = #{fws} #{libs}\n"

   res << "build #{dst}.run: run_executable #{dst}\n"

   res << "build #{target[:name]}:     phony #{dst}\n"
   res << "build #{target[:name]}/run: phony #{dst}.run\n"

   res
end

def dynamic_executable(target)
   res = ''

   link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }

   dst = build_path("#{target[:path]}/#{target[:name]}.dyn")
   target[:dynamic_executable] = dst

   res << "build #{dst}: link_executable #{target[:objects].join(' ')} | #{link_with.join(' ')}\n"
   res << flags_for_target(target)
   fws  = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
   libs = prepend('-l', all_values_for_key(target, 'LIBS'))
   res << "  RAVE_FLAGS = #{link_with.join(' ')} #{fws} #{libs}\n"

   res
end

def app_bundle(target)
  res = ''
  deps = [ ]
  all_files = [ ]
  ext = target['BUNDLE_EXTENSION'] || 'app'
  dst = build_path("#{target[:path]}/#{target[:name]}.#{ext}")
  target[:bundle] = dst

  fw_dst = File.join(dst, "Contents/Frameworks")
  all_values_for_key(target, 'LINK').each do |name|
    tmp = TARGETS.target_named(name)
    if tmp[:CP_Resources].to_a.empty?
      res << "build #{fw_dst}/#{tmp[:name]}.dylib: cp #{tmp[:dylib]}\n"
      deps << "#{fw_dst}/#{tmp[:name]}.dylib"
    else
      res << framework_bundle(tmp, fw_dst)
      deps << "#{fw_dst}/#{tmp[:name]}.framework"
    end
  end

  target.each do |key, files|
    if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
      install_dir = $1
      files.each do |rsrc|
        deps << "#{dst}/Contents/#{install_dir}/#{rsrc[:dst]}"
        res << "build #{deps.last}: cp #{rsrc[:src]}\n"
        all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/#{install_dir}/#{rsrc[:dst]}", :src => rsrc[:src] }
      end
    end
  end

  deps << "#{dst}/Contents/Info.plist"
  res << "build #{deps.last}: cp #{target[:info_plist]}\n"
  all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/Info.plist", :src => target[:info_plist] }

  deps << "#{dst}/Contents/MacOS/#{target[:name]}"
  res << "build #{deps.last}: cp #{target[:dynamic_executable]}\n"
  all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/MacOS/#{target[:name]}", :src => target[:dynamic_executable] }

  res << "build #{dst}: phony | #{deps.join(' ')}\n"

  res << "build #{dst}.sign: sign_executable #{dst}\n"
  res << "build #{dst}.run: run_application #{dst} | #{dst}.sign\n"
  res << "  appname = #{target[:name]}\n"

  res << "build #{target[:name]}:     phony #{dst}.sign\n"
  res << "build #{target[:name]}/run: phony #{dst}.run\n"

  target[:rsrc_info] = all_files

  res
end

def deploy(target)
  res = ''
  base = target[:bundle].sub(/\.[^.]+$/, '_r${APP_REVISION}')

  res << "build #{base}-dSYM.tbz: dsym #{target[:bundle]}\n"
  res << "build #{base}.tbz:      tbz_archive #{target[:bundle]} | #{target[:bundle]}.sign\n"
  res << "build #{base}.upload:   upload #{base}.tbz\n"
  res << "build #{base}.deploy:   deploy #{base}.upload | #{base}-dSYM.tbz\n"
  res << "build #{base}.bump:     bump_revision $builddir/build.ninja | #{base}.deploy\n"

  res << "build #{target[:name]}/dsym:   phony #{base}-dSYM.tbz\n"
  res << "build #{target[:name]}/tbz:    phony #{base}.tbz\n"
  res << "build #{target[:name]}/deploy: phony #{base}.bump\n"
end

# =======================
# = Program Starts Here =
# =======================

opts = GetoptLong.new(
  [ '--help',            '-h', GetoptLong::NO_ARGUMENT       ],
  [ '--output',          '-o', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--build-directory', '-C', GetoptLong::REQUIRED_ARGUMENT ],
  [ '--define',          '-d', GetoptLong::REQUIRED_ARGUMENT ]
)

outfile, builddir = '/dev/stdout', File.expand_path('~/build')
variables = [ ]

opts.each do |opt, arg|
  case opt
    when '--help'
      RDoc::usage
    when '--output'
      outfile = arg
    when '--build-directory'
      builddir = arg
    when '--define'
      variables << [ $1, $2 ] if arg =~ /^(\w+)\s*=\s*(.*)$/
  end
end

abort "No root target file specified" if ARGV.empty?
manifest = ARGV[0]
TARGETS = Targets.new(manifest)

open(outfile, "w") do |io|
  io << "###########################################\n"
  io << "# AUTOMATICALLY GENERATED -- DO NOT EDIT! #\n"
  io << "###########################################\n"
  io << "\n"
  io << "builddir = #{builddir}\n"
  io << "subninja $builddir/build.ninja\n"
end

open("#{builddir}/build.ninja.d", "w") do |io|
  deps = TARGETS.dependencies.map { |path| path.gsub(/ /, '\\ ') }
  io << outfile << ": " << deps.join(" \\\n  ") << "\n"
end

open("#{builddir}/build.ninja", "w") do |io|
  user_variables = TARGETS.root.to_a.reject { |arr| arr[0].class != String || GLOB_KEYS.include?(arr[0]) }.sort { |lhs, rhs| lhs[0] <=> rhs[0] }
  tmp = variables.dup.concat(user_variables) 
  width = tmp.map { |arr| arr[0].length }.max { |lhs, rhs| lhs <=> rhs }

  io << variables.map { |arr| format("%-#{width}s = %s\n", *arr) }
  io << "\n"

  io << user_variables.map { |arr| format("%-#{width}s = %s\n", *arr) }
  io << "\n"

  args = variables.map { |key, _| "-d#{key}='$#{key}'" }.join(' ')
  io << "rule configure\n"
  io << "  command = bin/gen_build -C \"$builddir\" #{args} -o $out $in\n"
  io << "  depfile = $builddir/$out.d\n"
  io << "  generator = 1\n"
  io << "\n"
  io << "build build.ninja: configure #{manifest} | bin/gen_build\n"
  io << "\n"

  io << DATA.read << "\n"

  COMPILER_INFO.each do |key, value|
    io << "rule #{value[:rule]}\n"
    io << "  command = '#{value[:compiler]}' $FLAGS $RAVE_FLAGS #{value[:flags]} -o $out -MMD -MF $out.d -I$builddir/include $in\n"
    io << "  depfile = $depfile\n"
    io << "  description = Compile ‘$in’…\n\n"
  end

  TARGETS.root['PRELUDE'].each do |src|
    type = { 'c' => '-x c-header', 'm' => '-x objective-c-header', 'cc' => '-x c++-header', 'mm' => '-x objective-c++-header' }
    io << "build #{pch_for(src, TARGETS.root)}.gch: #{compiler_for(src)} #{src}\n"
    io << "  depfile = #{pch_for(src, TARGETS.root)}.gch.d\n"
    io << "  RAVE_FLAGS = #{type[src[/\b[a-z]+$/]]}\n\n"
  end

  # =======================================================
  # = Do a topological sort based on the dependency graph =
  # =======================================================

  targets = { } # name → target

  dag = ''
  TARGETS.each do |target|
    target['LINK'].each { |dep| dag << "#{target[:name]}\t#{dep}\n" } unless target['LINK'].nil?
    target.each do |key, value|
      value.each { |rsrc| dag << "#{target[:name]}\t#$'\n" if rsrc =~ /^@/ } if key =~ /^CP_/
    end
    targets[target[:name]] = target
  end
  dag = open('|/usr/bin/tsort -q', 'r+') { |tsort| tsort << dag; tsort.close_write; tsort.read }

  # =========================================================================
  # = Now iterate the targets (in the sorted order) to generate build rules =
  # =========================================================================

  dag.scan(/.+/).reverse_each do |name|
    target = targets[name]

    io << clean(target)
    io << sources(target)
    io << resources(target)

    if target[:type] == :library
      io << headers(target)
      io << object_archive(target)
      io << tests(target)
      io << dynamic_lib(target)
    elsif target[:type] == :executable
      io << static_executable(target)
    else
      io << dynamic_executable(target)
      io << app_bundle(target)
      io << deploy(target)
    end
  end

  io << "\n"
  io << "build run:    phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/run\n"
  io << "build deploy: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/deploy\n"
  io << "default run\n"
end

# =======================

__END__
rule cp
  command = cp -p $in $out
  description = Copy ‘$in’…

rule ln
  command = ln -fs $link $out
  description = Link ‘$out’…

rule gen_ragel
  command = ragel -o $out $in
  description = Generate source from ‘$in’…

rule gen_test
  command = bin/CxxTest/bin/cxxtestgen --have-std -o $out --runner=unix $in
  description = Generate test ‘$out’…

rule link_test
  command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
  description = Link test ‘$out’…

rule run_test
  command = $in && touch $out
  description = Run test ‘$in’…

rule skip_test
  command = touch "$seal"
  description = Skip test ‘$in’…

rule link_static
  command = rm -f $out && ar -cqs $out $in
  description = Archive objects ‘$out’…

rule link_dynamic
  command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS -dynamiclib -current_version 1.0.1 -compatibility_version 1.0.0
  description = Link dynamic library ‘$out’…

rule link_executable
  command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
  description = Link executable ‘$out’…

rule run_executable
  command = $in
  description = Run ‘$in’…

rule run_application
  command = {                        $
    if pgrep -q "$appname"; then     $
      if [[ -x "$$DIALOG" && $$("$$DIALOG" < /dev/null alert --title "Relaunch $appname?" --body "Would you like to quit $appname and start the newly built version?" --button1 Relaunch --button2 Cancel|pl) != *"buttonClicked = 0"* ]]; $
        then exit;                   $
      fi;                            $
      pkill "$appname";              $
      while pgrep -q "$appname"; do  $
        if (( ++n == 10 )); then     $
          test -x "$$DIALOG" && "$$DIALOG" </dev/null alert --title "Relaunch Timed Out" --body "Unable to exit $appname." --button1 OK; $
          exit;                      $
        fi;                          $
        sleep .2;                    $
      done;                          $
    fi;                              $
    open $in;                        $
  } &>/dev/null &
  description = Run ‘$in’…

rule sign_executable
  command = codesign -fs "$identity" $in && touch $out
  description = Sign ‘$in’…

rule tbz_archive
  command = tar $bzip2_flag -cf $out -C "$$(dirname $in)" "$$(basename $in)"
  description = Archive ‘$in’…

rule upload
  command = bin/upload $in > $out~ && mv $out~ $out
  description = Upload ‘$in’…

rule deploy
  command = curl -sfnd @$in '${rest_api}/releases/nightly' && touch $out
  description = Deploy…

rule bump_revision
  command = ruby -pe '$$_.gsub!(/^(APP_REVISION\s*=\s*)(\d+)(\s*)$$/) { "#$$1#{$$2.to_i + 1}#$$3" }' $in > $in~ && mv $in~ $in && touch $out
  description = Increment version number…

rule clean
  command = rm -r '$path'

rule dsym
  command = $
    DST=$$(mktemp -dt dsym); $
    find $in -name rmate -or -type f -perm +0111 -print|while read line; $
    do dsymutil --flat --out "$$DST/$$(basename "$$line" .dylib).dSYM" "$$line"; $
    done && tar $bzip2_flag -cf $out -C "$$DST" . && rm -rf "$$DST"
  description = Archive dSYM info for ‘$in’…

rule compile_xib
  command = "$xcodedir/usr/bin/ibtool" --errors --warnings --notices --output-format human-readable-text --compile $out $in
  description = Compile xib ‘$in’…

rule process_plist
  command = bin/process_plist > $out~ $in $PLIST_FLAGS $RAVE_FLAGS && mv $out~ $out
  description = Process plist ‘$in’…

rule markdown
  command = bin/gen_html > $out~ $md_flags $in && mv $out~ $out
  description = Generate ‘$out’…

rule index_help
  command = /usr/bin/hiutil -Caf $out "$HELP_BOOK"
  description = Index help book ‘$HELP_BOOK’…

rule compile_mom
  command = "$xcodedir/usr/bin/momc" $in $out
  description = Generate ‘$out’…
