#!/usr/bin/python
# Copyright 1999-2007 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
# $Id: quickpkg 8546 2007-11-20 19:36:37Z zmedico $

import errno, signal, stat, sys, os

def tar_contents(contents, root, tar, protect=None, onProgress=None):
	from portage_util import normalize_path
	import tarfile
	root = normalize_path(root).rstrip(os.path.sep) + os.path.sep
	id_strings = {}
	maxval = len(contents)
	curval = 0
	if onProgress:
		onProgress(maxval, 0)
	paths = contents.keys()
	paths.sort()
	for path in paths:
		curval += 1
		try:
			lst = os.lstat(path)
		except OSError, e:
			if e.errno != errno.ENOENT:
				raise
			del e
			if onProgress:
				onProgress(maxval, curval)
			continue
		contents_type = contents[path][0]
		if path.startswith(root):
			arcname = path[len(root):]
		else:
			raise ValueError("invalid root argument: '%s'" % root)
		live_path = path
		if 'dir' == contents_type and \
			not stat.S_ISDIR(lst.st_mode) and \
			os.path.isdir(live_path):
			# Even though this was a directory in the original ${D}, it exists
			# as a symlink to a directory in the live filesystem.  It must be
			# recorded as a real directory in the tar file to ensure that tar
			# can properly extract it's children.
			live_path = os.path.realpath(live_path)
		tarinfo = tar.gettarinfo(live_path, arcname)
		# store numbers instead of real names like tar's --numeric-owner
		tarinfo.uname = id_strings.setdefault(tarinfo.uid, str(tarinfo.uid))
		tarinfo.gname = id_strings.setdefault(tarinfo.gid, str(tarinfo.gid))

		if stat.S_ISREG(lst.st_mode):
			# break hardlinks due to bug #185305
			tarinfo.type = tarfile.REGTYPE
			if protect and protect(path):
				# Create an empty file as a place holder in order to avoid
				# potential collision-protect issues.
				tarinfo.size = 0
				tar.addfile(tarinfo)
			else:
				f = open(path)
				try:
					tar.addfile(tarinfo, f)
				finally:
					f.close()
		else:
			tar.addfile(tarinfo)
		if onProgress:
			onProgress(maxval, curval)

def quickpkg_main(options, args, eout):
	from portage import catsplit, dblink, dep_expand, flatten, isvalidatom, xpak
	from portage_dep import use_reduce, paren_reduce
	from portage_util import ConfigProtect, ensure_dirs
	from portage_exception import InvalidData, InvalidDependString
	import portage_exception
	from portage_checksum import perform_md5
	import tarfile
	import portage
	root = portage.settings["ROOT"]
	trees = portage.db[root]
	vartree = trees["vartree"]
	vardb = vartree.dbapi
	bintree = trees["bintree"]
	try:
		ensure_dirs(bintree.pkgdir)
	except portage_exception.PortageException:
		pass
	if not os.access(bintree.pkgdir, os.W_OK):
		eout.eerror("No write access to '%s'" % bintree.pkgdir)
		return errno.EACCES
	successes = []
	missing = []
	config_files_excluded = 0
	include_config = options.include_config == "y"
	include_unmodified_config = options.include_unmodified_config == "y"
	for arg in args:
		try:
			atom = dep_expand(arg, mydb=vardb, settings=vartree.settings)
		except ValueError, e:
			# Multiple matches thrown from cpv_expand
			eout.eerror("Please use a more specific atom: %s" % \
				" ".join(e.args[0]))
			del e
			missing.append(arg)
			continue
		except InvalidData, e:
			eout.eerror("Invalid atom: %s" % str(e))
			del e
			missing.append(arg)
			continue
		if not isvalidatom(atom):
			eout.eerror("Invalid atom: %s" % atom)
			missing.append(arg)
			continue
		matches = vardb.match(atom)
		pkgs_for_arg = 0
		for cpv in matches:
			excluded_config_files = []
			bintree.prevent_collision(cpv)
			cat, pkg = catsplit(cpv)
			dblnk = dblink(cat, pkg, root,
				vartree.settings, treetype="vartree",
				vartree=vartree)
			dblnk.lockdb()
			try:
				if not dblnk.exists():
					# unmerged by a concurrent process
					continue
				iuse, use, restrict = vardb.aux_get(cpv,
					["IUSE","USE","RESTRICT"])
				iuse = [ x.lstrip("+-") for x in iuse.split() ]
				use = use.split()
				try:
					restrict = flatten(use_reduce(
						paren_reduce(restrict), uselist=use))
				except InvalidDependString, e:
					eout.eerror("Invalid RESTRICT metadata " + \
						"for '%s': %s; skipping" % (cpv, str(e)))
					del e
					continue
				if "bindist" in iuse and "bindist" not in use:
					eout.ewarn("%s: package was emerged with USE=-bindist!" % cpv)
					eout.ewarn("%s: it may not be legal to redistribute this." % cpv)
				elif "bindist" in restrict:
					eout.ewarn("%s: package has RESTRICT=bindist!" % cpv)
					eout.ewarn("%s: it may not be legal to redistribute this." % cpv)
				eout.ebegin("Building package for %s" % cpv)
				pkgs_for_arg += 1
				contents = dblnk.getcontents()
				protect = None
				if not include_config:
					confprot = ConfigProtect(root,
						portage.settings.get("CONFIG_PROTECT","").split(),
						portage.settings.get("CONFIG_PROTECT_MASK","").split())
					def protect(filename):
						if not confprot.isprotected(filename):
							return False
						if include_unmodified_config:
							file_data = contents[filename]
							if file_data[0] == "obj":
								orig_md5 = file_data[2].lower()
								cur_md5 = perform_md5(filename, calc_prelink=1)
								if orig_md5 == cur_md5:
									return False
						excluded_config_files.append(filename)
						return True
				xpdata = xpak.xpak(dblnk.dbdir)
				binpkg_tmpfile = os.path.join(bintree.pkgdir,
					cpv + ".tbz2." + str(os.getpid()))
				ensure_dirs(os.path.dirname(binpkg_tmpfile))
				tar = tarfile.open(binpkg_tmpfile, "w:bz2")
				tar_contents(contents, root, tar, protect=protect)
				tar.close()
				xpak.tbz2(binpkg_tmpfile).recompose_mem(xpdata)
			finally:
				dblnk.unlockdb()
			binpkg_path = bintree.getname(cpv)
			ensure_dirs(os.path.dirname(binpkg_path))
			os.rename(binpkg_tmpfile, binpkg_path)
			bintree.inject(cpv)
			try:
				s = os.stat(binpkg_path)
			except OSError, e:
				# Sanity check, shouldn't happen normally.
				eout.eend(1)
				eout.eerror(str(e))
				del e
				eout.eerror("Failed to create package: '%s'" % binpkg_path)
			else:
				eout.eend(0)
				successes.append((cpv, s.st_size))
				config_files_excluded += len(excluded_config_files)
				for filename in excluded_config_files:
					eout.ewarn("Excluded config: '%s'" % filename)
		if not pkgs_for_arg:
			eout.eerror("Could not find anything " + \
				"to match '%s'; skipping" % arg)
			missing.append(arg)
	if not successes:
		eout.eerror("No packages found")
		return 1
	print
	eout.einfo("Packages now in '%s':" % bintree.pkgdir)
	import math
	units = {10:'K', 20:'M', 30:'G', 40:'T',
		50:'P', 60:'E', 70:'Z', 80:'Y'}
	for cpv, size in successes:
		if not size:
			# avoid OverflowError in math.log()
			size_str = "0"
		else:
			power_of_2 = math.log(size, 2)
			power_of_2 = 10*int(power_of_2/10)
			unit = units.get(power_of_2)
			if unit:
				size = float(size)/(2**power_of_2)
				size_str = "%.1f" % size
				if len(size_str) > 4:
					# emulate `du -h`, don't show too many sig figs
					size_str = str(int(size))
				size_str += unit
			else:
				size_str = str(size)
		eout.einfo("%s: %s" % (cpv, size_str))
	if config_files_excluded:
		print
		eout.ewarn("Excluded config files: %d" % config_files_excluded)
		eout.ewarn("See --help if you would like to include config files.")
	if missing:
		print
		eout.ewarn("The following packages could not be found:")
		eout.ewarn(" ".join(missing))
		return 2
	return os.EX_OK

if __name__ == "__main__":
	usage = "quickpkg [options] <list of package atoms>"
	from optparse import OptionParser
	parser = OptionParser(usage=usage)
	parser.add_option("--umask",
		default="0077",
		help="umask used during package creation (default is 0077)")
	parser.add_option("--ignore-default-opts",
		action="store_true",
		help="do not use the QUICKPKG_DEFAULT_OPTS environment variable")
	parser.add_option("--include-config",
		type="choice",
		choices=["y","n"],
		default="n",
		metavar="<y|n>",
		help="include all files protected by CONFIG_PROTECT (as a security precaution, default is 'n')")
	parser.add_option("--include-unmodified-config",
		type="choice",
		choices=["y","n"],
		default="n",
		metavar="<y|n>",
		help="include files protected by CONFIG_PROTECT that have not been modified since installation (as a security precaution, default is 'n')")
	options, args = parser.parse_args(sys.argv[1:])
	if not options.ignore_default_opts:
		from portage import settings
		default_opts = settings.get("QUICKPKG_DEFAULT_OPTS","").split()
		options, args = parser.parse_args(default_opts + sys.argv[1:])
	if not args:
		parser.error("no packages atoms given")
	try:
		umask = int(options.umask, 8)
	except ValueError:
		parser.error("invalid umask: %s" % options.umask)
	# We need to ensure a sane umask for the packages that will be created.
	old_umask = os.umask(umask)
	from output import get_term_size, EOutput
	eout = EOutput()
	def sigwinch_handler(signum, frame):
		lines, eout.term_columns = get_term_size()
	signal.signal(signal.SIGWINCH, sigwinch_handler)
	try:
		retval = quickpkg_main(options, args, eout)
	finally:
		os.umask(old_umask)
		signal.signal(signal.SIGWINCH, signal.SIG_DFL)
	sys.exit(retval)
