#!/usr/bin/python

# makegen makefile generator for gcc and icc
# Copyright (c) 2008, Tobias Hammer <tobi (at) der - hammer (dot) info>
# 
# 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 3, 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., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.


import sys
import os
import glob


##############################################################################

gMakefileName = "Makefile"
gAutoIncludes = True

##############################################################################


def extractBlocks( lines ):
	state = 0
	
	blockList = []
	curBlockName = ""
	curBlockLines = []
	
	for l in lines:
		curLine = l[:-1]

		# State 0: search block start, ignore comments
		if state == 0:
			if curLine.find( '{' ) == -1:
				commentPos = curLine.find( '#' )
				if commentPos != -1:
					curLine = curLine[:commentPos]
				curLine = curLine.strip()
				if curLine != '':
					print "Expected comment or empty line but found '%s'" % l[:-1]
					sys.exit( -1 )
			else:
				curBlockName = curLine[:curLine.find( '{' )].strip()
				state = 1
				
		# State 1: search block end, save every line in block storage
		elif state == 1:
			if len(curLine) > 0 and curLine[0] == '}':
				blockList.append( ( curBlockName, curBlockLines ) )
				curBlockLines = []
				state = 0
			else:
				curBlockLines.append( curLine )

	if state == 1:
		print "Expected } to end block '%s'" % curBlockName
		sys.exit( -1 )

	return blockList


##############################################################################

def parseConfigSection( lines ):
	target = ""
	compiler = ""
	subdir = []
	exclude = []
	includes = ""

	for l in lines:
		if l.find( '#' ) != -1:
			l = l[:l.find( '#' )]
		l = l.strip()

		if l == "":
			continue

		split = l.split( '=', 1 )
		if len(split) < 2:
			print "No '='-character found in line '%s'" % l
			sys.exit( -1 )

		split[0] = split[0].strip()
		split[1] = split[1].strip()

		if split[0] == "" or split[1] == "":
			print "Invalid line '%s'" % l
			sys.exit( -1 )

		if split[0] == "target":
			if target != "":
				print "Duplicate target-configuration"
				sys.exit( -1 )
			target = split[1]

		elif split[0] == "subdir":
			subdir.append( split[1] )
		
		elif split[0] == "exclude":
			exclude.append( split[1] )

		elif split[0] == "include_paths":
			includes += " -I" + " -I".join([ x.strip() for x in split[1].split('|')])

		elif split[0] == "include_expr":
			includes += " " + split[1]

		elif split[0] == "compiler":
			if compiler != "":
				print "Duplicate compiler statement found"
				sys.exit( -1 )
			compiler = split[1]

		else:
			print "Expected target, subdir or exclude but found '%s'" % split[0]
			sys.exit( -1 )

	if target == "":
		print "No target specified"
		sys.exit( -1 )

	if compiler == "":
		print "Warning: No compiler statement, assuming g++"
		compiler = "g++"

	return ( target, compiler, subdir, exclude, includes )


##############################################################################

def checkDuplicates( items, name ):
	items = items[:]
	items.sort()
	last = ""

	for i in items:
		if i == last:
			print "Duplicate %s-statement '%s' found" % (name, i)
			sys.exit( -1 )
		else:
			last = i


##############################################################################

def collectFiles( dir, exclude ):
	flist = glob.glob( os.path.join( dir, "*.c" ) )
	flist += glob.glob( os.path.join( dir, "*.cpp" ) )

	res = []
	for f in flist:
		if f in exclude:
			exclude[ f ] += 1
		else:
			res.append( f )
	return res


##############################################################################

def generateAutoInclude( dir ):
	if dir == "":
		return "-I./"

	count = 0
	while dir != "":
		dir = os.path.split( dir )[0]
		count += 1
	res = "".join( [ '..'+os.sep for i in range( count ) ] )

	return "-I" + res

##############################################################################

def writeMakefile( dir, target, vars, moreTargets, deps, src, obj, compiler, includes, cfgFile ):
	if gAutoIncludes:
		includes += " " + generateAutoInclude( dir )
	
	# Compose makefile content
	buf = "\n\n"

	buf += "CPP=" + compiler + "\n\n"

	if 	target != "":
		buf += "TARGET=" + target + "\n"
	buf += "INCLUDE=" + includes + "\n\n"

	buf += "# ### User variables ###\n"
	buf += vars + "\n\n"
	buf += "########################\n\n"

	buf += "CFLAGS+=$(INCLUDE)\n\n\n"

	buf += "SRC=" + src + "\n\n"
	buf += "OBJ=" + obj + "\n\n\n"

	buf += ".SUFFIXES: .cpp .o\n"
	buf += ".SUFFIXES: .c .o\n\n"

	buf += "%.o: %.cpp\n"
	buf += "	$(CPP) $(CFLAGS) -c $< -o $@\n"
	buf += "%.o: %.c\n"
	buf += "	$(CPP) $(CFLAGS) -c $< -o $@\n"

	if target != "":
		buf += "$(TARGET): $(OBJ)\n"
		buf += "	$(CPP) $(LDFLAGS) $(OBJ) -o $(TARGET)\n"

	buf += "\n\n"
	buf += "objects: $(OBJ)\n\n"

	buf += "clean:\n"
	buf += "	rm -rf $(OBJ) "

	if target != "":
		buf += "$(TARGET)\n\n"
		buf += "depend:\n"
		buf += "	%s %s\n\n" % ( sys.argv[0], cfgFile )
	else:
		buf += "\n\n"

	buf += "\n"
	buf += "# ### User targets ###\n"
	buf += moreTargets + "\n\n"
	buf += "######################\n\n\n"

	buf += "# ### Auto generated dependencies ###\n\n"
	buf += deps

	# Write file to directory dir
	fname =  os.path.join( dir, gMakefileName )

	print "Generate", fname, "...", 

	fd = open( fname, 'w' )
	fd.write( buf )
	fd.close()

	print "done"
	

##############################################################################

def getDependencies( compiler, includes, file, dir ):
	fd = os.popen( compiler + " -MM " + includes + " -I./ " + file, 'r' )
	dep = os.path.join( os.path.dirname(file), fd.read() )
	fd.close()
	return dep


##############################################################################

def createObjects( files ):
	res = []
	for f in files:
		if f.endswith( ".c" ):
			res.append( f[:-2] + ".o" )
		elif f.endswith( ".cpp" ):
			res.append( f[:-4] + ".o" )
		else:
			print "Warning: File '%s' has unknown ending" % f
	return res


##############################################################################

def generateMakefiles( fileDict, target, compiler, includes, vars, moreTargets, cfgFile ):
	baseDep = ""
	baseSrc = ""
	baseObj = ""

	for dir in fileDict:
		if dir == "":
			continue

		flist = fileDict[ dir ]
		flist_base = [ os.path.basename(x) for x in flist ]
		localDep = ""
		localSrc = " ".join( flist_base )
		localObj = " ".join( createObjects( flist_base ) )

		for f in flist:
			localDep += getDependencies( compiler, includes, f, dir ) + "\n"
		
		writeMakefile( dir, "", vars, moreTargets, localDep, localSrc, localObj, compiler, includes, cfgFile )

		baseDep += localDep
		baseSrc += " ".join( flist ) + " "
		baseObj += " ".join( createObjects( flist ) ) + " "

	# main makefile
	flist = fileDict[""]

	for f in flist:
		baseDep += getDependencies( compiler, includes, f, "" ) + "\n"

	baseSrc += " ".join( flist )
	baseObj += " ".join( createObjects( flist ) )

	writeMakefile( "", target, vars, moreTargets, baseDep, baseSrc, baseObj, compiler, includes, cfgFile )


##############################################################################


cfgFile = ""

if len(sys.argv) == 1:
	if os.path.exists( "Makefile.conf" ):
		cfgFile = "Makefile.conf"
	elif os.path.exists( "makefile.conf" ):
		cfgFile = "makefile.conf"
	else:
		print "Usage: %s [<config file>]" % sys.argv[0]
		sys.exit( -1 )

elif len(sys.argv) == 2:
	if os.path.exists( sys.argv[1] ):
		cfgFile = sys.argv[1]
	else:
		print "Configuration file '%s' not found" % sys.argv[1]
		print "Usage: %s [<config file>]" % sys.argv[0]
		sys.exit( -1 )

else:
	print "Usage: %s [<config file>]" % sys.argv[0]
	sys.exit( -1 )


fd = open( cfgFile )

try:
	lines = fd.readlines()

	blockList = extractBlocks( lines )

	# vars
	cfgTarget = ""
	cfgCompiler = ""
	cfgSubdirs = []
	cfgExcludes = []
	cfgIncludes = ""
	hadConfig = False

	varBlock = ""
	targetBlock = ""

	for b in blockList:
		if b[0] not in ( 'config', 'variables', 'targets' ):
			print "Unknown section '%s' (expect code, variables or targets onyl)" % b[0]
			sys.exit( -1 )

		if b[0] == "config":
			if hadConfig:
				print "Only one 'config'-section allowed"
				sys.exit( -1 )
			hadConfig = True
			(cfgTarget, cfgCompiler, cfgSubdirs, cfgExcludes, cfgIncludes) = parseConfigSection( b[1] )

		elif b[0] == "variables":
			if varBlock != "":
				print "Only one 'variables'-section allowed"
				sys.exit( -1 )
			varBlock = "\n".join( b[1] )

		elif b[0] == "targets":
			if targetBlock != "":
				print "Only one 'targets'-section allowed"
				sys.exit( -1 )
			targetBlock = "\n".join( b[1] )
	

	if not hadConfig:
		print "Config section not found"
		sys.exit( -1 )

	
	checkDuplicates( cfgSubdirs, "subdir" )
	checkDuplicates( cfgExcludes, "exclude" )

	cfgSubdirs.append( "" )
	
	excludeDict = dict( zip( cfgExcludes, [ 0 for x in range(len(cfgExcludes)) ] ) )

	fileDict = {}
	for sub in cfgSubdirs:
		fileDict[ sub ] = collectFiles( sub, excludeDict )

	for key in excludeDict:
		if excludeDict[ key ] == 0:
			print "Warning: No match for exclude '%s'" % key

	generateMakefiles( fileDict, cfgTarget, cfgCompiler, cfgIncludes, varBlock, targetBlock, cfgFile )

finally:
	fd.close()


