#!/usr/bin/python

from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from os.path import exists, dirname, join, isdir, split, abspath, relpath, basename
import sys, os, imp, re, subprocess, shutil, shlex
import zipfile
import tempfile

# Terribly useful debugging tweaks
import cgitb
import traceback
import pdb

cgitb.enable(display=True, format='text')
def excepthook(etype, value, tb):
	if tb:
		traceback.print_exception(etype, value, tb)
	pdb.post_mortem(tb)
sys.excepthook = excepthook

def entryPoint():
	return split(abspath(__file__))[0]
sys.path.append(entryPoint()+'/tools')
import githubUpload
import lint

# These are the kinds of files we replace all instances of "%%VERSION%%" with
# the current version.
TXT_FILE_EXTENSIONS = [ '.js' ]

RESOURCE_FOLDER = 'tnoodle_resources'
RESOURCE_FOLDER_SRC = 'src_%s' % RESOURCE_FOLDER
RESOURCE_FOLDER_BIN = 'bin_%s' % RESOURCE_FOLDER

def windowsOrCygwin():
	return sys.platform == 'win32' or sys.platform == 'cygwin'

if windowsOrCygwin():
	def windowsSymlink(src, linkName):
		parentDir, fileName = split(linkName)
		assert isdir(parentDir)
		src = src.replace('/', '\\')
		assert 0 == os.system("cmd /C \"cd %s && mklink /D %s %s\"" % ( parentDir, fileName, src ) )
	def islink(path):
		return readlink(path) != None
	def readlink(path):
		parentDir, fileName = split(path)
		p = subprocess.Popen([ 'dir', parentDir ], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout, stderr = p.communicate()
		assert p.returncode == 0
		files = stdout.split('\n')
		symlinkRe = re.compile(r'.*SYMLINKD.*%s.*\[(.*)\].*' % fileName) # lol, this is crazy fragile
		for file in files:
			match = symlinkRe.match(file)
			if match:
				return match.group(1).replace('\\', '/')
		return None
	oldUnlink = os.unlink
	def unlink(path):
		if islink(path):
			# For some reason, Windows refuses to delete a symlink when using
			# the hacked version of islink I've create above.
			assert 0 == os.system('rmdir %s' % path)
		else:
			oldUnlink(path)
	
	os.unlink = unlink
	os.symlink = windowsSymlink
	os.path.islink = islink
	os.readlink = readlink
	
def rmtree(dir):
	# This method recursively deletes a directory, but doesn't follow any symlinks
	if not os.path.exists(dir) and not os.path.islink(dir):
		# If this path doesn't exist and is not a broken symlink,
		# then we're done!
		return
	def onerror(func, path, excinfo):
		if func == os.path.islink:
			os.unlink(path)
		else:
			raise excinfo[1]
	shutil.rmtree(dir, onerror=onerror)

def createSymlinkIfNotExistsOrStale(target, name):
	if ( not os.path.exists(name) or
		 not os.path.islink(name) or
		 not os.readlink(name) == target ):
		print "Setting up symlink %s -> %s" % ( name, target )
		rmtree(name)
		os.symlink(target, name)

PROJECT_FILE_NAME = 'tmtproject'
DEFAULT_PROJECT_NAME = 'all'

def dfs(node, path=()):
	assert node not in path, "Not a dag!"
	visted = [ node, ]
	path += ( node, )
	for child in node.getDependencies():
		visted += dfs(child, path)
	return visted

def rightPrune(nodes):
	prunedNodes = []
	for node in reversed(nodes):
		if node not in prunedNodes:
			prunedNodes.insert(0, node)
	return prunedNodes

def isTopologicalSort(sortedNodes):
	for i, node in enumerate(sortedNodes):
		for child in node.getDependencies():
			childIndex = sortedNodes.index(child)
			assert childIndex != i
			if childIndex < i:
				return False
	return True

def topologicalSort(rootProject):
	visitedNodes = dfs(rootProject)
	visitedNodes = rightPrune(visitedNodes)
	assert isTopologicalSort(visitedNodes)
	return visitedNodes

class tmt:
	def _cdEntryPoint(self):
		os.chdir(entryPoint())
		assert exists('README.textile')

		p = subprocess.Popen([ 'git', 'describe' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout, stderr = p.communicate()
		assert p.returncode == 0
		self.VERSION = stdout.strip()
		versionPrefix = "v"
		assert self.VERSION.startswith(versionPrefix)
		self.VERSION = self.VERSION[len(versionPrefix):]

		# Check that the git hooks folder is all set up
		gitHooksFolder = '.git/hooks'
		target = relpath(abspath('tools/git-hooks'), split(gitHooksFolder)[0])
		createSymlinkIfNotExistsOrStale(target, gitHooksFolder)

	def _loadTmtProjects(self):
		self._cdEntryPoint()
		projectFiles = ( join(dir, PROJECT_FILE_NAME) for dir in os.listdir('.') )
		for projectFile in sorted(filter(exists, projectFiles)):
			projectName = dirname(projectFile)
			imp.load_source(projectFile, projectFile)
			assert dirname(projectFile) in tmt.TmtProject.projects
		
		assert DEFAULT_PROJECT_NAME in tmt.TmtProject.projects, tmt.TmtProject.projects.keys()
		defaultProject = tmt.TmtProject.projects[ DEFAULT_PROJECT_NAME ]

		for projectName, project in sorted(tmt.TmtProject.projects.iteritems()):
			project.afterProjects()

		tmt.TmtProject.sortedProjects = topologicalSort(defaultProject)
		for project in reversed(tmt.TmtProject.sortedProjects):
			project.configure()

	def _main(self):
		self._loadTmtProjects()
		self.args = tmt.parser.parse_args()
		self.args.func()

	def _graph(self):
		if tmt.args.file:
			# TODO look at tmt.args.file and generate a dotty file or something?
			assert False, "not yet implemented"
		else:
			print tmt.TmtProject.projects[tmt.args.project].prettyDependencies()
	
	def _lint(self):
		lsFilesCmd = None
		if tmt.args.all:
			# Check every file under version control.
			lsFilesCmd = "git ls-files"
		else:
			# Check the files that git believes have been edited.
			lsFilesCmd = "git diff --cached --name-only"
		files = os.popen(lsFilesCmd).read().split('\n')

		failures = lint.lint(files)
		if failures:
			print
			print "*********%s errors found**********" % len(failures)
			print
			sys.exit('\n'.join(failures))

	def _make(self, projectName=None, command=None):
		if projectName == None:
			projectName = tmt.args.project
		if command == None:
			command = tmt.args.command
		project = tmt.TmtProject.projects[projectName]
		recurse = not tmt.args.no_recursive
		dist = False
		if recurse:
			noRecurseCommands = [ 'run', 'dist' ]
			if command in noRecurseCommands:
				print "WARNING: recurse makes no sense with the following commands: %s, so I'm ignoring it" % noRecurseCommands
				recurse = False
		if recurse:
			projects = project.getRecursiveDependenciesTopoSorted()
		else:
			projects = [ project ]

		projects.reverse()
		for project in projects:
			commandFunc = getattr(project, command, None)
			assert commandFunc, 'No %s target in %s' % ( command, project )
			commandFunc()

	def __init__(self):
		self.parser = ArgumentParser(
			description='tmt, the Tnoodle Make Tools',
			formatter_class=ArgumentDefaultsHelpFormatter)
		subparsers = self.parser.add_subparsers(help='Available commands')

		desc = 'Make stuff'
		parser_make = subparsers.add_parser(
			'make',
			help=desc, description=desc,
			formatter_class=ArgumentDefaultsHelpFormatter)
		parser_make.add_argument(
			'--project', '-p',
			default=DEFAULT_PROJECT_NAME,
			type=str,
			help='the project to build')
		parser_make.add_argument(
			'--no-recursive', '-nr',
			default=False,
			action='store_true',
			help='do not apply command recursively to dependencies')
		parser_make.add_argument(
			'command',
			nargs='?',
			choices=['compile', 'dist', 'release', 'run', 'check', 'clean'],
			default='compile',
			help='command!')
		parser_make.add_argument(
			'--args', '-a',
			default='',
			help='Command line arguments (only valid with the "run" command)')
		parser_make.add_argument(
			'--debug', '-d',
			choices=['jdb', 'attach'],
			default=None,
			help='Run with jdb or as an attacheable vm (only valid with the "run" command)')
		parser_make.set_defaults(func=self._make)

		desc = 'Generate dependency graph'
		parser_graph = subparsers.add_parser(
			'graph',
			help=desc, description=desc,
			formatter_class=ArgumentDefaultsHelpFormatter)
		parser_graph.add_argument(
			'--file', '-f',
			default='', type=str,
			help='')
		parser_graph.add_argument(
			'--project', '-p',
			default=DEFAULT_PROJECT_NAME, type=str,
			help='the project to build')
		parser_graph.add_argument(
			'--timestamps', '-t',
			action="store_true", default=False,
			help='Show project timestamps for debugging purposes')
		parser_graph.add_argument(
			'--no-prune', '-np',
			action="store_true", default=False,
			help='Do not prune duplicate projects')
		parser_graph.set_defaults(func=self._graph)

		desc = "Run JSLint on some (or all) js and html files. Also checks " +\
			   "for appearances of characters we don't allow in the repository"
		parser_graph = subparsers.add_parser(
			'lint',
			help=desc, description=desc,
			formatter_class=ArgumentDefaultsHelpFormatter)
		parser_graph.add_argument(
			'--all', '-a',
			action="store_true", default=False,
			help='Lint all files, even if they have been edited')
		parser_graph.set_defaults(func=self._lint)
	
	def memoize(self, func):
		d = {}
		evaluating = set()
		def memoed(*args):
			if not args in d:
				assert not args in evaluating, "Memoized functions may not have cycles!"
				evaluating.add(args)
				d[args] = func(*args)
				evaluating.remove(args)
			return d[args]
		return memoed

	def projectName(self):
		import traceback
		projectName, projectFileName = traceback.extract_stack()[-2][0].split(os.sep)
		assert isdir(projectName)
		assert projectFileName == PROJECT_FILE_NAME
		return projectName

	def runCmd(self, argv):
		p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout, stderr = p.communicate()
		return p.returncode, stdout, stderr

	def timestamp(self, f):
		"""
			If f is a directory, returns the timestamp of the newest file 
			found anywhere under the given directory f.
			If f is a file, simply returns the timestamp of the given file.
			Empty/non existent directories have timestamp 0.
		"""
		if not exists(f):
			return 0

		if os.path.islink(f):
			# We don't follow symbolic links because we don't want them
			# to cause a recompile of any project that uses us.
			return os.lstat(f).st_mtime
		if not isdir(f):
			try:
				return os.lstat(f).st_mtime
			except:
				import traceback
				traceback.print_exc()
				import pdb; pdb.set_trace()

		m = 0
		for ff in os.listdir(f):
			m = max(m, self.timestamp(join(f, ff)))
		return m
	
	def glob(self, folder, pattern, relativeTo=None):
		matches = []
		pattern = re.compile(pattern)
		if relativeTo is None:
			relativeTo = '.'
		for root, dirs, files in os.walk(folder):
			matches += [ relpath(join(root, f), relativeTo) for f in files if pattern.match(f) ]
		return matches

	def java(self, main, classpath='', args=[], debug=None):
		command = []
		assert debug in [ None, 'jdb', 'attach' ]
		if debug == 'jdb':
			command.append('jdb')
			# TODO - passing -ea to jdb doesn't work! I HATE JAVA
		else:
			command.append('java')
			if debug == 'attach':
				command.append('-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n') # TODO - configurable port?
			command.append('-ea') # TODO - should turn on assertions at runtime
		if classpath:
			command.append('-classpath')
			command.append(classpath)
		command.append(main)
		command += args
		print " ".join(command)
		p = subprocess.Popen(command)
		return p.wait()

tmt = tmt() # We only want one instance of Tmt
sys.modules['tmt'] = tmt

class TmtProject(object):
	projects = {}
	def __init__(self, name):
		self.name = name
		assert not name in TmtProject.projects, name
		TmtProject.projects[name] = self
	def __str__(self, timestamps=False):
		return self.name
	# The configure method of each project gets called in reverse
	# dependency order. That is, if a project A uses a project B,
	# B.configure() will be called before A.configure().
	def configure(self):
		pass
	# The afterProjects method gets called for each project after all the projects
	# have been loaded, in no particular order.
	def afterProjects(self):
		pass
	def compile(self):
		assert False
	def dist(self):
		assert False
	def release(self):
		pass
	def run(self):
		assert False
	def clean(self):
		assert False
	def getDependencies(self):
		assert False
	def getRecursiveDependenciesTopoSorted(self):
		assert self in tmt.TmtProject.sortedProjects
		projectIndex = tmt.TmtProject.sortedProjects.index(self)
		projects = tmt.TmtProject.sortedProjects[projectIndex:]
		return projects
	def prettyDependencies(self, level=0, printingLevels=None, seen=None):
		if printingLevels == None:
			assert level == 0
			printingLevels = set([0])
		else:
			printingLevels = printingLevels.copy()
		if seen == None:
			assert level == 0
			seen = set()
		indent = ''
		for i in range(level):
			if i == level-1:
				if not i in printingLevels:
					indent += '`-- '
				else:
					indent += '|-- '
			else:
				if i in printingLevels:
					indent += '|   '
				else:
					indent += '    '

		if not tmt.args.no_prune and self in seen:
			# If we're pruning, and we've seen this node before,
			# then we don't recurse
			s = indent + self.__str__(timestamps=tmt.args.timestamps) + ' (seen)\n'
		else:
			s = indent + self.__str__(timestamps=tmt.args.timestamps) + '\n'
			printingLevels.add(level)
			dependencies = self.getDependencies()
			for p in dependencies:
				if p == dependencies[-1]:
					printingLevels.remove(level)
				s += p.prettyDependencies(level+1, printingLevels, seen)
				seen.add(p)
		return s
tmt.TmtProject = TmtProject

@tmt.memoize
def createJarDependency(jarFile):
	return JarDependency(jarFile)

class JarDependency(TmtProject):
	def __init__(self, jarFile):
		self.jarFile = jarFile
		TmtProject.__init__(self, basename(jarFile))
	def check(self):
		return
	def getDependencies(self):
		return []
	def compile(self):
		pass
	def clean(self):
		pass
	def __str__(self, timestamps=False):
		s = self.name
		if timestamps:
			s += ' %s' % tmt.timestamp(self.jarFile)
		return s

class EclipseProject(TmtProject):
	from xml.sax import make_parser, saxutils

	class EclipseClasspathHandler(saxutils.handler.ContentHandler):
		def startDocument(self):
			self.jarFiles = []
			self.projects = []

		def startElement(self, name, attrs):
			if name == 'classpathentry':
				kind = attrs.get('kind')
				path = attrs.get('path')
				if kind == 'output':
					assert path == 'bin'
				elif kind == 'src':
					if path.startswith('/'):
						self.projects.append(path[1:])
					else:
						assert path == 'src' or path == RESOURCE_FOLDER_SRC
				elif kind == 'lib':
					assert path.startswith('/')
					self.jarFiles.append(abspath(path[1:]))
				elif kind == 'con':
					pass
				else:
					assert False, 'Unrecognized kind: %s' % kind
	classpathParser = make_parser()
	classpathHandler = EclipseClasspathHandler()
	classpathParser.setContentHandler(classpathHandler)

	def __init__(self, name, main=None, argv=None, tests=None):
		self.main = main
		self.argv = argv
		self.nonJavaSrcDeps = []
		self.tests = tests
		TmtProject.__init__(self, name)

	def getDependencies(self):
		return self.jars + self.projects

	def getClasspathEntities(self, includeResources=True):
		classpath = set()
		classpath |= set(relpath(jar.jarFile) for jar in self.jars)
		classpath.add(self.bin)
		if includeResources:
			classpath.add(self.binResource)

		# TODO - the src folder only needs to be in the classpath so
		# jdb can discover the source code
		# TODO - i thought this was harmless, but it's making "tmt make run"
		# behave differently than "tmt make dist". DirectoryHandler
		# searches for dependencies using 
		#  getClass().getResourceAsStream()
		# If the src directory is in the classpath, dependencies that should be
		# copied to the bin directory are being found in src directory.
		# Bugs like this wouldn't show up until someone does a "tmt make dist",
		# and who wants to do that?
		#classpath.add(self.src)
		# TODO - I hate the world
		# Uncomment & replace the following with the path to your jre
		# if you're trying to use jbd inside of a java library method.
		#classpath.add('/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0.x86_64/src')

		for project in self.projects:
			classpath |= project.getClasspathEntities(includeResources=includeResources)
		return classpath

	def getClasspath(self, includeResources=True):
		separator = None
		if windowsOrCygwin():
			separator = ';'
		else:
			separator = ':'
		return separator.join(self.getClasspathEntities(includeResources=includeResources))

	def afterProjects(self):
		dependencyFile = join(self.name, '.classpath')
		assert exists(dependencyFile), dependencyFile
		EclipseProject.classpathParser.parse(dependencyFile)
		self.jars = [ createJarDependency(jar) for jar in EclipseProject.classpathHandler.jarFiles ]
		self.projects = [ tmt.TmtProject.projects[p] for p in EclipseProject.classpathHandler.projects ]
		self.bin = join(self.name, 'bin')
		self.distDir = join(self.name, 'dist')
		self.src = join(self.name, 'src')
		self.srcResource = join(self.name, RESOURCE_FOLDER_SRC)
		self.binResource = join(self.name, RESOURCE_FOLDER_BIN)
		self.fullName = 'TNoodle%s' % ( self.name[0].upper() + self.name[1:] )
	
		if not isdir(self.bin):
			os.mkdir(self.bin)
		if not isdir(self.binResource):
			os.mkdir(self.binResource)
		if not isdir(self.distDir):
			os.mkdir(self.distDir)
	
	def distJarFile(self, includeVersion=True):
		# This is a function rather than a static attribute because
		# tmt.VERSION can change (see release target).
		if includeVersion:
			fileName = '%s-%s.jar' % ( self.fullName, tmt.VERSION )
		else:
			fileName = '%s.jar' % self.fullName
		return join(self.distDir, fileName)

	def compile(self):
		# Note that we assume that if the resources of a project we depend on change,
		# we don't need to recompile. This is the way resources should behave.
		lastCompiled = tmt.timestamp(self.bin)
		depsLastTouched = ( ( tmt.timestamp(dep), dep ) for dep in self.getClasspathEntities(includeResources=False) )
		changedDeps = filter( lambda ( ts, dep ): ts > lastCompiled, depsLastTouched )
		srcLastTouched = max(tmt.timestamp(self.src), tmt.timestamp(self.srcResource))
		if lastCompiled > srcLastTouched and len(changedDeps) == 0:
			print "%s is up to date, not recompiling" % self.name
			return

		self._compile(self.src, self.bin)
		self._compile(self.srcResource, self.binResource)

		for nonJavaSrcDep in self.nonJavaSrcDeps:
			srcPath = join(self.srcResource, nonJavaSrcDep)
			binPath = join(self.binResource, nonJavaSrcDep)
			print "Copying %s -> %s" % ( srcPath, binPath )
			if isdir(srcPath):
				# If this dependency is a directory, we symlink to it
				# this way, we can get some changes without recompiling.
				parentDir = dirname(dirname(binPath))
				if not isdir(parentDir):
					os.makedirs(parentDir)
				if binPath.endswith('/'):
					binPath = binPath[:-1]
				srcPath = abspath(srcPath)
				srcPath = relpath(srcPath, parentDir)
				os.symlink(srcPath, binPath)
			else:
				assert not isdir(binPath)
				parentDir = dirname(binPath)
				if not isdir(parentDir):
					os.makedirs(parentDir)
				shutil.copy(srcPath, binPath)

		os.symlink(os.path.relpath(self.binResource, self.bin), join(self.bin, RESOURCE_FOLDER))


	def _compile(self, src, bin):
		head, tail = os.path.split(bin)
		tempBin = join(head, "." + tail)

		print 'Compiling: %s' % src

		rmtree(bin)
		if isdir(tempBin):
			rmtree(tempBin)
		os.mkdir(tempBin)

		javaFiles = tmt.glob(src, r'.*\.java$')
		if len(javaFiles) > 0:
			args = [ 'javac' ]
			args += [ '-d', tempBin ]
			classpath = self.getClasspath()
			if classpath:
				args += [ '-classpath', classpath ]
			args += javaFiles
			p = subprocess.Popen(args)
			print " ".join(args)
			assert p.wait() == 0, 'javac failed!'

		print 'Compiled successfully! Renaming %s -> %s' % ( tempBin, bin )
		os.rename(tempBin, bin)

	def release(self):
		# TODO - this method is getting called for every project in the dependency heirarchy
		if tmt.args.project == self.name:
			retVal, stdout, stderr = tmt.runCmd(['git', 'ls-files', '--other', '--exclude-standard'])
			assert retVal == 0
			if stdout != '':
				print "Untracked files found, please deal with them before releasing:"
				print stdout
				return

			retVal, stdout, stderr = tmt.runCmd(['git', 'diff', '--name-only'])
			assert retVal == 0
			if stdout != '':
				print "Edited files found, please commit them before releasing:"
				print stdout
				return

			# We do a compile here even though we're going to do dist later
			# in order to catch compile errors before we make a tag.
			tmt._make(projectName=self.name, command='compile')

			print "Releasing", self.name

			assert exists('VERSION')
			versionFileContents = file('VERSION').read().split('\n')
			tmt.VERSION = None
			for line in versionFileContents:
				if not line.startswith("#"):
					tmt.VERSION = line
					break
			assert tmt.VERSION
			tag = "v%s" % tmt.VERSION
			tagMessage = "version %s" % tmt.VERSION
			tagCommand = "git tag -a %s -m '%s'" % (tag, tagMessage)
			print tagCommand
			assert 0 == os.system(tagCommand)
			print "Successfully created tag %s" % tag

			# We can't run a dist until after we create the tag. This way
			# the version number of the resulting file will be correct.
			self.dist()

			githubUpload.githubConnect()
			if not githubUpload.githubUpload(self.distJarFile()):
				print "Failed to upload %s, see above for details" % self.distJarFile()
				print "Deleting tag %s" % tag
				return

			pushTagsCommand = 'git push --tags'
			print pushTagsCommand
			assert 0 == os.system(pushTagsCommand)
	
	def dist(self):
		tmt._make(projectName=self.name, command='compile')
		jar = zipfile.ZipFile(self.distJarFile(), "w", compression=zipfile.ZIP_DEFLATED)

		jarDeps = []
		javaDeps = []
		for project in self.getRecursiveDependenciesTopoSorted():
			if hasattr(project, 'jarFile'):
				jarDeps.append(project)
			else:
				javaDeps.append(project)
		if jarDeps:
			extractedLibDir = tempfile.mkdtemp()
			for jarDep in jarDeps:
				jarLib = zipfile.ZipFile(jarDep.jarFile)
				jarLib.extractall(path=extractedLibDir)
			for dirpath, dirnames, filenames in os.walk(extractedLibDir):
				for name in filenames:
					path = os.path.normpath(os.path.join(dirpath, name))
					arcPath = os.path.relpath(path, extractedLibDir)
					if os.path.isfile(path):
						jar.write(path, arcPath)
			rmtree(extractedLibDir)

		for project in javaDeps:
			for dirpath, dirnames, filenames in os.walk(project.bin, followlinks=True):
				for name in filenames:
					path = os.path.normpath(os.path.join(dirpath, name))
					arcPath = os.path.relpath(path, project.bin)
					if os.path.isfile(path):
						if os.path.splitext(path)[1] in TXT_FILE_EXTENSIONS:
							txt = file(path).read()
							# This might be a pretty heavy hammer, but who in the world
							# would legitimately want to use the string "%%VERSION%%"?
							# Famous last words?
							txt = txt.replace("%%VERSION%%", tmt.VERSION)
							jar.writestr(arcPath, txt)
						else:
							jar.write(path, arcPath)

		manifest = """\
Manifest-Version: 1.0
Main-Class: %s
Implementation-Title: %s
Implementation-Version: %s
""" % ( self.main, self.fullName, tmt.VERSION )
		jar.writestr('META-INF/MANIFEST.MF', manifest)

		jar.close()

		noVersionPath = self.distJarFile(includeVersion=False)

		createSymlinkIfNotExistsOrStale(basename(self.distJarFile()), noVersionPath)

		print 'Successfully created %s with main class %s' % ( self.distJarFile(), self.main )

	def run(self):
		assert self.main, 'Main class not defined'
		assert tmt.args.args is not None # shlex reads from stdin if you pass None, lol
		tmt._make(projectName=self.name, command='compile')
		# TODO propogate return code up
		args = shlex.split(tmt.args.args)
		if self.argv:
			args += self.argv
		tmt.java(
			main=self.main,
			classpath=self.getClasspath(includeResources=False),
			args=args,
			debug=tmt.args.debug)
	
	def check(self):
		if not self.tests:
			return

		tmt._make(projectName=self.name, command='compile')

		for testClass in self.tests:
			assert 0 == tmt.java(main=testClass,
								 classpath=self.getClasspath())

	def clean(self):
		print 'Cleaning: %s' % self.name
		if exists(self.bin):
			assert isdir(self.bin)
			rmtree(self.bin)
		if exists(self.distDir):
			assert isdir(self.distDir)
			rmtree(self.distDir)
		tempBin = join(self.name, '.bin')
		if isdir(tempBin):
			rmtree(tempBin)
	
	def __str__(self, timestamps=False):
		s = self.name
		if timestamps:
			binTs = tmt.timestamp(self.bin)
			srcTs = tmt.timestamp(self.src)
			s += ' bin: %s src: %s' % ( binTs, srcTs )
		return s

tmt.EclipseProject = EclipseProject

if __name__ == "__main__":
	tmt._main()
