#!/usr/bin/env python
#
#	Standard interface for compiling occam-pi libraries and programs
#	Copyright (C) 2007 University of Kent
#	Copyright (C) 2008 Adam Sampson <ats@offog.org>
#
#	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.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program; if not, write to the Free Software
#	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#

import sys, os, getopt, re

VERSION = "$Revision: 4473 $"[11:-2]
CONTACT = "kroc-bugs@kent.ac.uk"
autogen_message = "Automatically generated by occbuild %s; do not edit" % VERSION

std_libs = True
verbose = False
programs = {}
search_path = []
occ21_opts = []
linker_opts = []
needs = []
includes_before = []
includes_after = []
static_link = False
prefix = None
destdir = ""
target_os = "@KROC_HOST_OS@"
patterns = None
build_shared = True

def warn(*s):
	print >>sys.stderr, "occbuild: " + "".join(map(str, s))

def die(*s):
	warn(*s)
	sys.exit(1)

def run_command(cmd):
	"""Run a program with arguments and return the exit code.
	Equivalent to subprocess.call, but works on Python 2.3 when os.spawnvp
	is available."""
	try:
		import subprocess
		return subprocess.call(cmd)
	except ImportError:
		return os.spawnvp(os.P_WAIT, cmd[0], cmd)

def safe_sorted(l):
	l = l[:]
	l.sort()
	return l

def tidy_spaces(string):
	"""Remove leading and trailing whitespace, and collapse all interior
	whitespace into single spaces."""
	return re.sub('\s+', ' ', string.strip())

def split_options(opts):
	"""Split an argument we've been given into a list of arguments to pass
	to another program."""
	return opts.strip().split()

def split_ext(file):
	"""Given a filename, return its basename and extension."""
	file = os.path.basename(file)
	i = file.rfind(".")
	if i == -1:
		return (file, "")
	else:
		return (file[:i], file[i + 1:])

def run(cmd):
	"""Run a command, and check that it succeeded."""
	if verbose:
		warn("Running command: ", " ".join(cmd))
	rc = run_command(cmd)
	if rc != 0:
		die("Command failed: ", " ".join(cmd))

def capture(cmd):
	"""Run a command, checking that it succeeded, and return its output."""
	if verbose:
		warn("Running command: ", " ".join(cmd))

	(pr, pw) = os.pipe()
	pid = os.fork()
	if pid == 0:
		os.close(pr)
		os.dup2(pw, 1)
		os.execvp(cmd[0], cmd)
		os._exit(1)

	os.close(pw)
	data = ""
	while 1:
		s = os.read(pr, 4096)
		if s == "":
			break
		data += s
	os.close(pr)

	(pid, status) = os.waitpid(pid, 0)
	if status != 0:
		die("Command failed: ", " ".join(cmd))

	return data

def find_in_path(file, path):
	"""Search for file in all the directories listed in path."""
	for dir in path:
		fn = dir + "/" + file
		if exists(fn):
			return fn
	return None

def make_static(library, objs):
	"""Link a bunch of native objects into a static library."""

	run(programs["ar"] + ["rc", library] + objs)
	run(programs["ranlib"] + [library])

def make_shared(library, objs, extra_opts = []):
	"""Link a bunch of native objects into a shared library."""

	opts = []
	if target_os.startswith("darwin"):
		opts = ["-dynamiclib", "-read_only_relocs", "suppress"]
	else:
		opts = ["-shared"]

	run(programs["cc"] + opts + ["-o", library] + objs + linker_opts + extra_opts)

def make_module(name, custom):
	"""Generate a .module file, calling the provided function to fill in
	any toolchain-specific lines."""

	ofn = name + ".module"
	f = open(ofn, "w")
	f.write('-- Include file for "%s" module\n' % name)
	f.write('-- %s\n' % autogen_message)
	f.write("\n")

	guard = ofn.replace("_", ".").upper()
	f.write("#IF NOT (DEFINED (%s))\n" % guard)
	f.write("#DEFINE %s\n" % guard)
	f.write("\n")

	def do_list(format, list):
		for item in list:
			f.write(format % item)
		if list != []:
			f.write('\n')

	do_list('#INCLUDE "%s.module"\n', needs)
	do_list('#INCLUDE "%s"\n', includes_before)
	custom(f)
	do_list('#INCLUDE "%s"\n', includes_after)

	f.write("#ENDIF\n")
	f.close()

def exists(file):
	"""Does a file or directory exist?"""
	return os.access(file, os.F_OK)

def make_stub(file):
	"""Make a stub file to serve as a placeholder."""
	f = open(file, "w")
	f.close()

def is_stub(file):
	"""Is a file an empty stub?"""
	return os.stat(file).st_size == 0

def delete(file):
	"""Delete a file, if it exists."""
	if exists(file):
		os.unlink(file)

def install(file, dir, is_exec):
	"""Install a file (if it exists) into a directory. Make the directory
	if it doesn't already exist. If is_exec, then make the file
	executable."""
	if not exists(file):
		return
	if not exists(dir):
		try:
			os.makedirs(dir)
		except OSError:
			die("Unable to create directory: ", dir)
	cmd = programs["install"] + []
	if is_exec:
		cmd += ["-m755"]
	else:
		cmd += ["-m644"]
	cmd += [file, "%s/%s" % (dir, os.path.basename(file))]
	run(cmd)

def get_install_dir(default, default_suffix):
	"""Figure out the location of an installation directory, based on how
	occbuild was originally configured."""

	default_prefix = "@prefix@"
	if prefix is None or prefix == default_prefix:
		dir = default
	elif default.startswith(default_prefix):
		dir = prefix + default[len(default_prefix):]
	else:
		dir = prefix + default_suffix

	return destdir + dir

class KRoCToolchain:
	def kroc_cmd(self):
		cmd = programs["kroc"] + []
		cmd += ["--oc-opts", " ".join(occ21_opts + ["-DEF", "OCCBUILD.KROC"])]
		if static_link:
			cmd += ["--cc-opts", "-static"]
		return cmd

	def object(self, source):
		run(self.kroc_cmd() + ["-c", source])

	def library(self, occam_objs, native_objs, base):
		def custom(f):
			if occam_objs != []:
				f.write('#USE "%s.lib"\n' % base)
			if occam_objs != [] or native_objs != []:
				f.write('#PRAGMA COMMENT ".USELIB %s"\n'
				        % base)
				f.write('-- occbuild:has-native-library\n')
			if linker_opts != []:
				f.write('#PRAGMA COMMENT ".LDFLAGS %s"\n'
					% ' '.join(linker_opts))
			f.write('\n')
		make_module(base, custom)

		need_mods = needs[:]
		if std_libs:
			need_mods.append("forall")

		# Look for all the other modules we need.
		full_path = search_path + capture(programs["kroc"] + ["--incpath"]).strip().split(":")
		lib_opts = []
		for need in need_mods:
			fn = find_in_path("%s.module" % need, full_path)
			if fn is None:
				die("Cannot find module ", need, " in search path ", ":".join(full_path))

			f = open(fn)
			data = f.read()
			f.close()

			# If this module uses a native library, we must link
			# against it when building a shared library.
			if data.find('occbuild:has-native-library') != -1:
				lib_opts.append(patterns["link"] % need)

		if std_libs:
			lib_opts.append(patterns["rtlink"] % "krocif")
		lib_opts.append(patterns["rtlink"] % "ccsp")

		objs = [split_ext(o)[0] + ".o" for o in occam_objs] + native_objs
		if objs != []:
			make_static(patterns["a"] % base, objs)

			if build_shared:
				kroc_ldflags = split_options(capture(programs["kroc"] + ["--ldflags"]))
				cclibpath = split_options(capture(programs["kroc"] + ["--cclibpath"]))
				make_shared(patterns["so"] % base, objs, kroc_ldflags + cclibpath + lib_opts)

		if occam_objs == []:
			make_stub(base + ".lib")
		else:
			run(programs["ilibr"] + ["-o", base + ".lib"] + occam_objs)

	def program(self, occam_objs, native_objs, base):
		run(self.kroc_cmd() + [base + ".occ"] + [split_ext(o)[0] + ".o" for o in occam_objs] + native_objs)

	def install(self, file):
		bindir = get_install_dir("@KROC_ARCHBINDIR@", "/bin")
		libdir = get_install_dir("@KROC_ARCHLIBDIR@", "/lib")
		includedir = get_install_dir("@KROC_ARCHINCDIR@", "/include/kroc")
		krocdir = get_install_dir("@KROC_KROCDIR@", "/share/kroc")
		vtlibdir = krocdir + "/vtlib"
		vtincludedir = krocdir + "/vtinclude"

		(base, ext) = split_ext(file)
		if ext == "lib":
			if not is_stub(file):
				install(file, vtlibdir, False)
			install(patterns["a"] % base, libdir, True)
			if build_shared:
				install(patterns["so"] % base, libdir, True)
			install(base + ".module", vtincludedir, False)
		elif ext == "inc":
			install(file, vtincludedir, False)
		elif ext == "h":
			install(file, includedir, False)
		elif ext == "":
			install(file, bindir, True)

	def install_examples(self, package, file):
		examplesdir = "%s/%s" % (get_install_dir("@KROC_EXAMPLESDIR@", "/lib/kroc/examples"), package)

		(base, ext) = split_ext(file)
		if ext == "":
			install(file, examplesdir, True)

	def clean(self, file):
		delete(file)
		(base, ext) = split_ext(file)
		if ext == "tce":
			delete(base + ".o")
			delete(base + ".etc")
			delete(base + ".s")
		elif ext == "lib":
			delete(base + ".lbb")
			delete(patterns["a"] % base)
			delete(patterns["so"] % base)
			delete(base + ".module")
		elif ext == "":
			self.clean(base + ".tce")

	def cflags(self):
		cflags = "-DOCCBUILD_KROC"
		cflags += " " + capture(programs["kroc"] + ["--cflags"])
		cflags += " " + capture(programs["kroc"] + ["--ccincpath"])
		print tidy_spaces(cflags)

class TVMToolchain:
	def skroc_cmd(self):
		cmd = programs["skroc"] + []
		cmd += ["--occ21-opts", " ".join(occ21_opts + ["-DEF", "OCCBUILD.TVM"])]
		return cmd

	def object(self, source):
		run(self.skroc_cmd() + ["--tce", source])

	def library(self, occam_objs, native_objs, base):
		def custom(f):
			if occam_objs != []:
				f.write('#USE "%s.lib"\n' % base)
				f.write("#PRAGMA COMMENT \"(spragma (uselib %s))\"\n" % base)
			if native_objs != []:
				f.write("#PRAGMA COMMENT \"(spragma (dynlib %s))\"\n"
				        % (patterns["libname"] % base))
			f.write('\n')
		make_module(base, custom)

		if native_objs != []:
			make_shared(patterns["so"] % base, native_objs)

		if occam_objs == []:
			make_stub(base + ".lib")
		else:
			run(programs["skroc"] + ["-f", base, "--library"] + occam_objs)

	def program(self, occam_objs, native_objs, base):
		if native_objs != []:
			die("Native objects in a Transterpreter program are not yet supported; build a library instead")
			# You can *almost* get there by building an FFI library
			# and a stub that invokes the right #PRAGMA here, but
			# the slinker currently won't do the right thing if you
			# use a .tce file that you haven't actually #USEd.
		run(self.skroc_cmd() + [base + ".occ"] + occam_objs)

		# Generate a wrapper script (because we have to produce
		# something with the target name, and it might as well be
		# useful).
		f = open(base, "w")
		f.write("#!/bin/sh\n")
		f.write("# %s\n" % autogen_message)
		f.write("exec %s $0.tbc \"$@\"\n" % (" ".join(programs["tvm"])))
		f.close()
		os.chmod(base, 0755)

	def install(self, file):
		bindir = get_install_dir("@TVM_BINDIR@", "/bin")
		tvmdir = get_install_dir("@TVM_LIBDIR@", "/lib/transterpreter")

		(base, ext) = split_ext(file)
		if ext == "lib":
			if not is_stub(file):
				install(file, tvmdir, False)
				install(base + ".precomp", tvmdir, False)
			install(patterns["so"] % base, tvmdir, True)
			install(base + ".module", tvmdir, False)
		elif ext == "inc":
			install(file, tvmdir, False)
		elif ext == "h":
			install(file, tvmdir, False)
		elif ext == "":
			install(file, bindir, True)
			install(base + ".tbc", bindir, False)

	def install_examples(self, package, file):
		examplesdir = "%s/%s" % (get_install_dir("@TVM_EXAMPLESDIR@", "/lib/transterpreter/examples"), package)

		(base, ext) = split_ext(file)
		if ext == "":
			install(file, examplesdir, True)

	def clean(self, file):
		delete(file)
		(base, ext) = split_ext(file)
		if ext == "tce":
			pass
		elif ext == "lib":
			delete(base + ".lbb")
			delete(base + ".precomp")
			delete(patterns["so"] % base)
			delete(base + ".module")
		elif ext == "":
			self.clean(base + ".tce")
			delete(base + ".tbc")

	def cflags(self):
		print "-DOCCBUILD_TVM"
		# FIXME: skroc doesn't currently have a cflags mode

def usage(f):
	print >>f, """occbuild, version """ + VERSION + """
Compile occam-pi libraries and programs
Usage:

  occbuild [OPTIONS] --object NAME.occ
    Compile source file NAME.occ to produce NAME.tce

  occbuild [OPTIONS] --library LIBNAME.lib [OBJECT ...]
    Link object files (.tce or .o) to produce LIBNAME.lib

  occbuild [OPTIONS] --program PROGNAME.occ [OBJECT ...]
    Compile PROGNAME.occ with additional OBJECTs to produce PROGNAME

  occbuild [OPTIONS] --install FILE ...
    Install files (.inc, .lib, .h or programs) to system locations

  occbuild [OPTIONS] --install-examples PACKAGE PROGRAM ...
    Install PROGRAMs as examples for PACKAGE

  occbuild [OPTIONS] --clean FILE ...
    Remove generated files

  occbuild [OPTIONS] --cflags
    Print CFLAGS necessary to build FFI objects to stdout

Options for all modes:
  -v, --verbose       Print progress reports to stderr
  --help              Show usage
  --toolchain CHAIN   Use toolchain CHAIN (kroc or tvm, default kroc)
  --search DIR        Add DIR to search path for include files and libraries
  --target-os OS      Override OS target for binaries, e.g. Darwin or Linux

For each program prog of """ + ", ".join(safe_sorted(programs.keys())) + """:
  --prog PATH         Path to prog (or set PROG=PATH in the environment)
  --prog-opts OPTS    Additional options for prog

Options for --object mode:
  --occ21-opts OPTS   Additional options for occ21
  -D DEFINE           Passed through to occ21 as -DEF

Options for --library mode:
  -l LIBRARY          } Passed through to native linker
  -L PATH             }
  -R PATH             }
  -W OPTION           }
  -m OPTION           }
  -p OPTION           }
  --need MODULE       Module depends on other module MODULE
  --no-std-libs       Do not link module against standard libraries
  --include INCLUDE   Module uses include file INCLUDE
  --include-after INCLUDE
                      Module uses include file INCLUDE after library is loaded

Options for --program mode:
  --static            Force program to be statically linked

Options for --install/--install-examples mode:
  --prefix PREFIX     Select installation prefix PREFIX (mandatory)
  --destdir DESTDIR   Use staging directory DESTDIR

Report bugs to <""" + CONTACT + """>."""

def main(args):
	# Get default program locations and arguments from the environment.
	for prog in ("cc", "ar", "ranlib", "kroc", "ilibr", "install", "skroc", "tvm"):
		programs[prog] = split_options(os.getenv(prog.upper(), prog))

	long_opts = [
		"object", "library=", "program=",
		"install", "install-examples=", "clean", "cflags",
		"help", "no-std-libs", "verbose",
		"toolchain=", "search=",
		"occ21-opts=", "need=", "include=", "include-after=",
		"static",
		"prefix=", "destdir=", "target-os="
		]
	for prog in programs.keys():
		long_opts += [prog + "=", prog + "-opts="]
	try:
		opts, args = getopt.getopt(args, "vD:l:L:R:W:m:p:", long_opts)
	except getopt.GetoptError:
		usage(sys.stderr)
		sys.exit(1)

	toolchains = {
		"kroc": KRoCToolchain,
		"tvm": TVMToolchain,
		}

	mode = None
	target = None
	toolchain = None
	global std_libs, verbose, search_path, occ21_opts, linker_opts, needs
	global includes_before, includes_after, static_link
	global prefix, destdir, examples, target_os
	for (o, a) in opts:
		if o in ("--object", "--library", "--program", "--install",
		         "--install-examples", "--clean", "--cflags"):
			if mode is not None:
				die("Multiple modes specified")
			mode = o[2:]
			target = a
		elif o == "--help":
			usage(sys.stdout)
			sys.exit(0)
		elif o in ("-v", "--verbose"):
			verbose = True
		elif o == "--toolchain":
			if toolchain is not None:
				die("Multiple toolchains specified")
			if a not in toolchains:
				die("Unknown toolchain specified: ", a)
			toolchain = toolchains[a]()
		elif o == "--search":
			search_path.append(a)
		elif o == "--occ21-opts":
			occ21_opts += split_options(a)
		elif o == "-D":
			occ21_opts += ["-DEF", a]
		elif o in ("-l", "-L", "-R", "-W", "-m", "-p"):
			linker_opts.append("%s%s" % (o, a))
		elif o == "--need":
			needs.append(a)
		elif o == "--include":
			includes_before.append(a)
		elif o == "--include-after":
			includes_after.append(a)
		elif o == "--static":
			static_link = True
		elif o == "--prefix":
			prefix = a
		elif o == "--destdir":
			destdir = a
		elif o == "--no-std-libs":
			std_libs = False
		elif o == "--target-os":
			target_os = a
		else:
			for prog in programs.keys():
				if o == "--" + prog:
					programs[prog][0] = a
				elif o == "--" + prog + "-opts":
					programs[prog] += split_options(a)

	if mode is None:
		usage(sys.stderr)
		sys.exit(1)

	if toolchain is None:
		toolchain = toolchains["kroc"]()

	for dir in search_path:
		programs["cc"] += ["-L" + dir]
		programs["kroc"] += ["-I" + dir, "-L" + dir]
		programs["skroc"] += ["-L", dir]
	
	global patterns, build_shared
	patterns = {
		"libname": "occam_%s",
		"link": "-loccam_%s",
		"rtlink": "-l%s",
		"so": "liboccam_%s.so",
		"a": "liboccam_%s.a",
		}
	if target_os.startswith("cygwin"):
		build_shared = False
	elif target_os.startswith("darwin"):
		patterns["so"] = "liboccam_%s.dylib"
		build_shared = False

	if mode == "object":
		if args == []:
			die("No source files specified")
		for source in args:
			if not source.endswith(".occ"):
				die("Source filenames must end in .occ: ", source)
			toolchain.object(source)
	elif mode == "library":
		occam_objs = []
		native_objs = []
		for object in args:
			(_, ext) = split_ext(object)
			if ext == "tce":
				occam_objs.append(object)
			elif ext == "o":
				native_objs.append(object)
			else:
				die("Object filename must end in .tce or .o: ", object)
		(base, ext) = split_ext(target)
		if ext != "lib":
			die("Library filename must end in .lib: ", target)
		toolchain.library(occam_objs, native_objs, base)
	elif mode == "program":
		occam_objs = []
		native_objs = []
		for object in args:
			(_, ext) = split_ext(object)
			if ext == "tce":
				occam_objs.append(object)
			elif ext == "o":
				native_objs.append(object)
			else:
				die("Object filename must end in .tce or .o: ", object)
		(base, ext) = split_ext(target)
		if ext != "occ":
			die("Source filename must end in .occ: ", target)
		toolchain.program(occam_objs, native_objs, base)
	elif mode == "install":
		if prefix is None:
			die("When using --install mode, you must give --prefix")
		for file in args:
			toolchain.install(file)
	elif mode == "install-examples":
		if prefix is None:
			die("When using --install-examples mode, you must give --prefix")
		for file in args:
			toolchain.install_examples(target, file)
	elif mode == "clean":
		for file in args:
			toolchain.clean(file)
	elif mode == "cflags":
		if args != []:
			die("Unexpected arguments specified")
		toolchain.cflags()
	else:
		die("Mode ", mode, " not implemented")

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

