#!/usr/bin/env python
# $Id: makedepend.py,v 1.6 2006/05/01 05:07:31 devel Exp $
# Author:   Jim Brooks  http://www.jimbrooks.org
# Desc:     makedepend replacement written in Python.
#           This was written because makedepend will omit include files
#           at deep levels of inclusion which is intolerable because if
#           dependencies are incomplete, bad executables can result.
#
# Args:     [-Idir] [-target-dir dir] [files]
#
#           [-target-dir dir] causes dir to be prepended to the target of a makefile rule.
#           If the target had a directory component, it is replaced by dir.
#
#           [-long] for debugging, output longer human-readable make targets.
#
# LEGAL:    COPYRIGHT (C) JIM E. BROOKS 2005
#           THIS SOURCE CODE IS RELEASED UNDER THE TERMS
#           OF THE GNU GENERAL PUBLIC LICENSE (GPL).
# ==============================================================================

import sys, os, os.path, traceback, copy, re
from string import *

#-------------------------------------------------------------------------------

def Unique( l ):
    """ Remove duplicates from a list. """
    d = {}
    for k in l:
        d[k] = 1
    return d.keys()

def ChangeSuffix( fname, suffixNew ):
    """ Return filename/pathname with a different suffix. """ 
    (path,suffixOld) = os.path.splitext( fname )
    return path + suffixNew

#-------------------------------------------------------------------------------

class Makedepend:
    
    mSrcFileList     = []         # filenames from cmd-line
    mIncDirList      = []         # include dirs from -I args
    mTargetDir       = ""         # optionally, target of rule prepended with dir
    mObjFileSuffix   = ".o"
    mLongMakeTargets = False      # if True, output is more human-readable (slows make)

    # Handle '#include' special cases such as whitespace in middle or commented-out.
    regexInclude            = re.compile( '#[ \t]*include "' )
    regexIncludeCommentOut1 = re.compile( '\/\*.*#[ \t]*include "' )
    regexIncludeCommentOut2 = re.compile( '//.*#[ \t]*include "' )

    def __init__( s ):

        # Process cmd-line args.
        ignoreNextArg = True   # initially true to ignore argv[0]
        i = -1
        for arg in sys.argv:
            i += 1

            if ignoreNextArg:
                ignoreNextArg = False
                continue

            # Arg starts with "-"?
            if arg[0] == "-":
                # -Idir
                if len(arg) > 2 and arg[:2] == "-I":
                    # Append include dir to list.
                    dirName = arg[2:]    # after "-I"
                    # Does the dir exist?
                    if os.path.isdir( dirName ):
                        s.mIncDirList.append( dirName )
                    else:
                        print "WARNING[makedepend.py]: '" + dirName + "' isn't a directory."
                # -target-dir
                elif arg == "-target-dir":
                    s.mTargetDir = sys.argv[i+1]
                    ignoreNextArg = True
                elif arg == "-long":
                    s.mLongMakeTargets = True
            else:
                # Assume this is a src file for now.
                s.mSrcFileList.append( arg )

        # For each source file given.
        for fname in s.mSrcFileList:

            # Read this src file.
            # Build a list of header files that it includes (its dependencies).
            depList = s.MakeDeps( fname, {} )
            depList = Unique( depList )

            # If it includes anything.
            if len(depList) > 0:
                fnameObj = ChangeSuffix( fname, s.mObjFileSuffix )

                # Optionally, prepend a directory to the target of the rule.
                # Eg, if s.mTargetDir = "out"
                # main.o: header.h
                # out/main.o: header.h
                if s.mTargetDir:
                    fnameObj = s.mTargetDir + os.sep + os.path.basename( fnameObj )
                    
                # Print this src file's dependencies as a makefile rule, eg:
                # main.o: header.h
                if s.mLongMakeTargets:
                    # Long format: more human-readable but will slow "make".
                    for dep in depList:
                        print fnameObj + ": " + dep
                else:
                    # Short format: print target and all deps on one line.
                    print fnameObj + ":",
                    for dep in depList:
                        print dep,
                    print

    def GetIncludeFilename( s, line ):
        """ Return filename specified in #include (dquote) statement. """
        l = line.split( '"' )
        return l[1]

    def FindSrcFile( s, fname ):
        """ Return filename of source file.  May try appending include dirs given by -I arg. """
        dirList = [ "" ] + s.mIncDirList    # try CWD first
        for dir in dirList:
            if dir:
                pathname = dir + os.sep + fname
            else:
                pathname = fname
            if os.access( pathname, os.R_OK ):
                return pathname
        raise IOError( "[makedepend.py]: can't find source file '" + fname + "'" )

    def MakeDeps( s, fname, includeDict ):
        """ Recursively read source files for #include (dquote) statements.
            Returns a list of included files (dependencies). """

        depList = []

        # Open this source file.
        # May need to prepend an include dir to the filename given in #include "*".
        fname = s.FindSrcFile( fname )
        ifile = open( fname, "r" )

        # For each line in this src file.
        for line in ifile:

            # Does the line contain #include "" which isn't commented-out?
            # Handle '#include' special cases such as whitespace in middle or commented-out.
            if s.regexInclude.search( line ) \
            and not s.regexIncludeCommentOut1.search( line ) \
            and not s.regexIncludeCommentOut2.search( line ):

                # Extract include filename, find its dependencies.
                # But don't re-read the same include file.
                includeFname = s.GetIncludeFilename( line )             # get what's inside #include "*"
                try:
                    includeFname = s.FindSrcFile( includeFname )
                except:
                    raise IOError( "[makedepend.py]: can't file include file " + includeFname + " included by " + fname )
            
                if not includeDict.has_key( includeFname ):             # if it was never read before
                    includeDict[includeFname] = 1                       # remember this include file was read
                    depList.append( includeFname )                      # append this include file to list
                    depList += s.MakeDeps( includeFname, includeDict )  # recursively add include files
            
        ifile.close()
        return depList

#-------------------------------------------------------------------------------

Makedepend()
