#!/scratchbox/tools/bin/python

# Copyright 2005  Movial
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# lib/build.py -- Builds and installs all dependencies of given packages
# recursively.

import sys, os, shutil, glob
import sb.config
import apt, util, parse, config

DEFAULT_PACKAGES = [
	"linux-kernel-headers",
	"libc6", "libc6-dev",
	"gcc-3.4-base", "libgcc1", "libstdc++6", "libstdc++6-dev",
	"libfakeroot"
]

class BuildError (Exception):
	"""Non-critical failures of external commands during the build
	   process."""

	def __init__(self, message):
		Exception.__init__(self, message)

def touch(path):
	"""Creates an empty file if PATH doesn't already exist.  Also
	   creates all leading directories if necessary."""

	dir = os.path.dirname(path)
	if not os.path.exists(dir):
		os.makedirs(dir)

	file = open(path, "w")
	file.close()

def get_epochless_version(version):
	"""Returns a copy of VERSION with the ":"-separated epoch
	   removed, if any."""

	list = version.split(":", 1)
	if len(list) == 1:
		return list[0]
	else:
		return list[1]

def get_upstream_version(version):
	"""Returns a copy of VERSION with epoch and Debian patch-level
	   removed."""

	epochless = get_epochless_version(version)

	list = epochless.split("-", 1)
	return list[0]

def get_deb_name(binary):
	"""Builds a filename based on the binary name, binary version and
	   Scratchbox's target architecture.  NOTE: This function returns
	   an incorrect filename if the binary package's version doesn't
	   correspond to the source package's version."""

	compiler = sb.config.get_compiler_info()
	version = get_epochless_version(binary.version)
	return "%s_%s_%s.deb" % (binary.package, version, compiler.arch)

def guess_deb_path(dirpath, binary):
	"""Finds the binary package file that provides the latest version
	   of BINARY in directory DIRNAME.  This can be used as a backup
	   measure if get_deb_name() fails.  NOTE: This problem should be
	   fixed in a proper way; this second-guessing is braindead."""

	compiler = sb.config.get_compiler_info()
	pattern = "%s/%s_*_%s.deb" % (dirpath, binary.package, compiler.arch)
	matches = glob.glob(pattern)
	if matches:
		matches.sort()
		return matches[-1]
	else:
		return None

def get_deb_depends(debpath):
	"""Extract the Depends and Pre-Depends fields from a deb file
	   DEBPATH (by calling "dpkg-deb") and return them as a list of
	   package names."""

	depends = []

	command = "dpkg-deb"
	file = os.popen("%s -f %s Depends Pre-Depends" % (command, debpath))

	while True:
		line = file.readline()
		if not line:
			break

		key, value = line.split(": ", 1)
		depends += map(lambda s: s.strip().split()[0], value.split(","))

	status = file.close()
	if status is not None:
		raise BuildError, "%s exited with status: %d" % (command, status)

	return depends

def get_src_build_depends(dirpath):
	"""Parse the Build-Depends field from an extracted source
	   package's control file.  DIRPATH is the source tree directory."""

	depends = []

	path = os.path.join(dirpath, "debian/control")
	file = open(path)

	while True:
		line = file.readline()
		if not line:
			break

		if line.lower().startswith("build-depends"):
			key, value = line.split(": ", 1)
			depends += parse.parse_depends(value)

	file.close()

	return depends

def get_install_plan(virtualname, plan):
	"""Find out which packages should be installed before the package
	   referred to by VIRTUALNAME can be installed and append all of
	   them to PLAN.  Attempt to build packages first if necessary."""

	binaryname = config.virtual.get(virtualname, virtualname)

	cookie = os.path.join("work/cookies/install", binaryname)
	if os.path.exists(cookie):
		print >>sys.stderr, "build:", binaryname, "is already installed"
		return

	sbox_list = parse.ScratchboxList.instance()
	binary = sbox_list.by_binary.get(binaryname)
	if binary:
		debname = get_deb_name(binary)

		compiler = sb.config.get_compiler_info()
		debpath = os.path.join(compiler.dir, "packages", debname)
	else:
		build(binaryname)

		source_list = parse.SourceList.instance()
		binary = source_list.by_binary.get(binaryname)
		if not binary:
			raise BuildError, "Binary package %s is unknown" % binaryname

		debname = get_deb_name(binary)

		debpath = os.path.join(apt.builddir, debname)
		if not os.path.exists(debpath):
			print >>sys.stderr, "build:", debname, "doesn't exist; guessing ..."

			debpath = guess_deb_path(apt.builddir, binaryname)
			if not debpath:
				raise BuildError, "Binary package %s not found" % binaryname

			debname = os.path.basename(debpath)
			print >>sys.stderr, "build: Guessed", debname, "..."

	for name in get_deb_depends(debpath):
		if name != binaryname:
			print >>sys.stderr, "build:", binaryname, "depends on", name
			get_install_plan(name, plan)

	plan.append((debpath, cookie))

def do_install_plan(plan):
	"""Install packages listed in PLAN (by calling "dpkg").  Creates
	   the corresponding cookies in "work/cookies/install"."""

	pretty_paths = ""
	for path, cookie in plan:
		pretty_paths += " " + os.path.basename(path)

	print >>sys.stderr, "build: Installing" + pretty_paths

	args = ["dpkg", "-i"] + map(plan, lambda pair: pair[0])

	if os.spawnvp(os.P_WAIT, args[0], args) != 0:
		raise BuildError, "Unable to install" + pretty_paths

	for path, cookie, in plan:
		touch(cookie)

def install(virtualname):
	"""Build and install the package referred to by VIRTUALNAME along
	   with all other required packages."""

	plan = []
	get_install_plan(virtualname, plan)

	if plan:
		do_install_plan(plan)

def build(binaryname):
	"""Build the source package of package BINARYNAME.  Attempt to
	   install all unsatisfied build-dependencies first."""

	source_list = parse.SourceList.instance()
	source = source_list.by_binary[binaryname]

	cookie = os.path.join("work/cookies/build", source.package)
	if os.path.exists(cookie):
		print >>sys.stderr, "build:", binaryname, "is already built"
		return

	upstream = get_upstream_version(source.version)

	dirname = "%s-%s" % (source.package, upstream)
	dirpath = os.path.join(apt.builddir, dirname)

	if os.path.exists(dirpath):
		shutil.rmtree(dirpath)

	print >>sys.stderr, "build: Fetching", source.package

	if apt.source(source.package, source.version) != 0:
		raise BuildError, "Unable to get sources for %s %s" % (source.package, source.version)

	patch = "patches/%s.patch" % source.package
	if os.path.exists(patch):
		print >>sys.stderr, "build: Applying", os.path.basename(patch)

		cwd = os.getcwd()
		abspatch = os.path.join(cwd, patch)

		args = ["patch", "-p1", "-d", dirpath, "-i", abspatch]
		if os.spawnvp(os.P_WAIT, args[0], args) != 0:
			raise Exception, "Unable to patch %s %s" % (source.package, source.version)

	build_depends = get_src_build_depends(dirpath)
	sbox_list = parse.ScratchboxList.instance()
	for name in build_depends:
		if name not in sbox_list.host_names:
			print >>sys.stderr, "build:", source.package, "build-depends on", name
			install(name)

	varfile = "variables/%s.env" % source.package
	if os.path.exists(varfile):
		print >>sys.stderr, "build: Setting", os.path.basename(varfile)

		file = open(varfile)
		lines = file.readlines()
		file.close()

		vars = {}
		for line in lines:
			key, value = line.split("=", 1)
			vars[key] = value
	else:
		vars = None

	print >>sys.stderr, "build: Building", source.package

	args = ["dpkg-buildpackage", "-rfakeroot", "-b", "-uc"]
	if util.spawn(args[0], args, dirpath, vars) != 0:
		raise BuildError, "Unable to build %s %s" % (source.package, source.version)

	touch(cookie)

def main(args):
	try:
		if "-h" in args or "--help" in args:
			print >>sys.stderr, "Usage: %s [--no-default] [--no-essential] [<packages>]" % sys.argv[0]
			sys.exit(1)

		if "--no-essential" in args:
			args.remove("--no-essential")
		else:
			binary_list = parse.BinaryList.instance()
			args = map(lambda p: p.package, binary_list.essential) + args

		if "--no-default" in args:
			args.remove("--no-default")
		else:
			args = DEFAULT_PACKAGES + args

		for name in args:
			install(name)

	except BuildError, e:
		print >>sys.stderr, "build:", e
		sys.exit(10)

	except KeyboardInterrupt:
		sys.exit(1)

if __name__ == "__main__":
	main(sys.argv[1:])

