#!/usr/bin/ruby

require 'ftools'
require 'optparse'
require 'rpm/spec'
require 'tiny-buildfarm/vine'
require 'tiny-buildfarm/setup'

base_dir     = nil
system_type  = TinyBuildFarm::Vine26
initial_pkgs = []
extra_pkgs   = []
output_dir   = '.'
rebuild_mode = false

if File.basename($0) == 'chroot-rebuild'
  rebuild_mode = true
end

ARGV.options do |q|
  q.banner = "usage: #{$0} [options] [rpm-options] srpm\n"
  q.on
  q.on('options:')

  q.on_tail('-h', '--help', 'show this message') {
    puts q
    exit(0)
  }
  q.on_tail
  q.on_tail('subsystems:')
  q.on_tail('    ' + 
	    TinyBuildFarm::SubSystems.collect{|x|
	      x.to_s.split(/::/).last}.sort.join(', '))

  q.on('-dBASEDIR', '--basedir=BASEDIR', 'base directory') {|base|
    base_dir = File.expand_path(base) if base
  }

  q.on('-tTYPE', '--type=TYPE', 'type of subsystem') {|type|
    system_type = type if type
    system_type = TinyBuildFarm::subsystem(system_type)

    raise OptionParser::InvalidArgument, 
      'unknown subsystem name' unless system_type
  }

  q.on('-iPKG,PKG', '--initial=PKG,PKG', Array, 
       'initial installed packages') {|pkgs|
    initial_pkgs = pkgs if pkgs
  }

  q.on('-ePKG,PKG', '--extra=PKG,PKG', Array, 
       'extra packages which is removed after doing task') {|pkgs|
    extra_pkgs = pkgs if pkgs
  }

  if rebuild_mode
    q.on('-oDIR', '--output-dir=DIR', 
	 'directory, generated packages are putted into there') {|dir|
      if dir
	raise OptionParser::InvalidArgument, 
	  'no such directory' unless FileTest.directory?(dir)
	output_dir = dir
      end
    }
  end

  q.parse!
end or exit(1)

unless system_type && base_dir && initial_pkgs
  $stderr.puts "#{$0}: base directory must be specified, try --help"
  exit(1)
end

unless Process.uid == 0
  $stderr.puts "#{$0}: required root privilege"
  exit(1)
end

if rebuild_mode
  if ARGV.empty?
    $stderr.puts "#{$0}: srpm must be specified, try --help"
    exit(1)
  end

  unless FileTest.file?(ARGV.last)
    $stderr.puts "#{$0}: #{ARGV.last} not exist"
    exit(1)
  end
end

def cleaning_rpmdirs(topdir)
  dirs = []
  ['BUILD', 'SPECS', 'RPMS', 'SRPMS', 'SOURCES'].each do |x|
    dirs << File.join(topdir, x)
  end
  system('rm', '-rf', *dirs)
  File.makedirs(*dirs)
end

def srpm_info(srpm)
  qf = "p,%{packager}\nv,%{vendor}\n[f,%{FILEFLAGS:fflags},%{FILENAMES}\n]"

  pr = IO::pipe
  pid = fork do
    pr[0].close
    STDOUT.reopen(pr[1])
    pr[1].close

    sleep 0.1 # XXX
    exec(TinyBuildFarm::RPM_CMD, '-qp', '--queryformat=' + qf, srpm)
    exit!
  end

  pr[1].close
  ret = pr[0].read

  rc = Process.waitpid2(pid).last
  raise RPMError, "could not get information: #{srpm}" if rc != 0

  packager = vendor = spec = nil
  source = []
  ret.split("\n").collect do |x|
    t, v = x.split(',', 2)
    v = nil if v == '(none)'
    case t
    when 'p'
      packager = v
    when 'v'
      vendor = v
    when 'f'
      ft, fn = v.split(',', 2)
      if ft == 's'
	spec = fn
      else
	source << fn
      end
    end
  end

  return packager, vendor, spec, source
end

def package_path(specinfo, package)
  if package.arch == 'src' || package.arch == 'nosrc'
    dir = specinfo.source_package_dir
  else
    dir = File.join(specinfo.package_dir, package.arch)
  end

  sprintf('%s/%s-%s-%s.%s.rpm', 
	  dir,
	  package.name, 
	  package.version,
	  package.release,
	  package.arch)
end

build_deps = nil
begin
  ss = system_type.new(base_dir)
  ss.extend(TinyBuildFarm::Setup)
  ss.extend(TinyBuildFarm::Updateable)
  ss.extend(TinyBuildFarm::Installable)

  ss.setup
  ss.update

  initial_pkgs = ss.essential if initial_pkgs.empty?

  task = nil

  initial_pkgs += ss.build_essential

  if rebuild_mode
    srpm = ARGV.pop
    build_deps = ss.requires(srpm)
    build_deps.delete('rpmlib(VersionedDependencies)')
    extra_pkgs += build_deps.keys
  end

  puts "initial packages: #{initial_pkgs.join(', ')}"
  ss.install(initial_pkgs, true)

  puts "extra packages: #{extra_pkgs.join(', ')}"
  ss.install(extra_pkgs)

  if build_deps
    build_deps.reject! do |pkg, cond|
      if cond
	ss.installed?(pkg, cond[0], cond[1])
      else
	ss.installed?(pkg)
      end
    end

    unless build_deps.empty?
      tmp = build_deps.collect{|pkg, cond|
	if cond
	  "#{pkg} (#{cond.join(' ')})"
	else
	  pkg
	end
      }.join(', ')
      raise TinyBuildFarm::UnrecoverableError,
	"could not satisfy BuildRequires and BuildPreReq: #{tmp}"
    end
  end

  ['/etc/hosts', '/etc/resolv.conf'].each do |f|
    File.cp(f, File.join(ss.root_dir, f)) if FileTest.exist?(f)
  end

  if rebuild_mode
    packager, vendor, specfile, source = srpm_info(srpm)
    tmp_srpm = File.join(ss.root_dir, 'tmp', File.basename(srpm))
    File.cp(srpm, tmp_srpm)

    task = proc do |opts, pw|
      puts 'chroot environment are built.'

      tmp_srpm.sub!(/^#{Regexp.quote(ss.root_dir)}/, '')

      RPM.setup
      cleaning_rpmdirs(RPM.expand('%{_topdir}'))
      Dir.chdir(RPM.expand('%{_topdir}'))

      system(TinyBuildFarm::RPM_CMD, '-ivh', tmp_srpm)
      if $? != 0
	puts "#{tmp_srpm} installation failed"

      else
	# parse spec
	spec = File.join('SPECS', specfile)
	info = RPM::Spec.dump(spec)

	# build
	rpm_opts = opts.dup
	rpm_opts << ['--define', "packager #{packager}"] if packager
	rpm_opts << ['--define', "vendor #{vendor}"]     if vendor
	rpm_opts << spec
	system(TinyBuildFarm::RPM_CMD, '-ba', *rpm_opts.flatten)

	if $? != 0
	  puts "rebuild failed"

	else
	  pkgs = []
	  info.packages.each do |arch, packages|
	    packages.each do |package|
	      pkg = package_path(info, package)
	      File.mv(pkg, '..')
	      pkgs << File.basename(pkg)
	    end
	  end
	  srcpkg = package_path(info, info.source_package)
	  File.mv(srcpkg, '..')
	  pkgs << File.basename(srcpkg)

	  cleaning_rpmdirs(RPM.expand('%{_topdir}'))
	  puts "rebuild succeeded"
	  pw.print pkgs.join("\n")
	end
      end
    end # task = proc

  else # if rebuild_mode
    tmp_srpm = nil
    if ARGV.empty?
      puts 'entering /bin/sh under chroot environment...'
      task = proc {|opts, pw| exec(['/bin/sh', '-sh'])}

    else
      puts 'doing tasks: ' + ARGV.join(' ')
      task = proc {|opts, pw| exec(*opts)}
    end
  end # if rebuild_mode

  begin
    ret = ss.chroot{|pw| task.call(ARGV, pw)}
    if rebuild_mode
      ret.each do |pkg|
	pkg.chomp!
	file = File.join(ss.root_dir, 
	     		 TinyBuildFarm::FarmerHome, pkg).gsub(/\/+/, '/')
	File.mv(file, output_dir)
      end
    end

  ensure
    File.unlink(tmp_srpm) if tmp_srpm
  end

rescue TinyBuildFarm::APTError
  $stderr.puts "#{$0}: error occurred on apt\n#{$!}"
  exit(1)

rescue TinyBuildFarm::UnrecoverableError
  $stderr.puts "#{$0}: error occurred\n#{$!}"
  exit(1)

ensure
  removed = ss.clean
  puts "removing #{removed.join(', ')}..." if removed.size > 0
end
