#! /usr/bin/env python

# objgraph
#
# Read "nm -o" input (on IRIX: "nm -Bo") of a set of libraries or modules
# and print various interesting listings, such as:
#
# - which names are used but not defined in the set (and used where),
# - which names are defined in the set (and where),
# - which modules use which other modules,
# - which modules are used by which other modules.
#
# Usage: objgraph [-cdu] [file] ...
# -c: print callers per objectfile
# -d: print callees per objectfile
# -u: print usage of undefined symbols
# If none of -cdu is specified, all are assumed.
# Use "nm -o" to generate the input (on IRIX: "nm -Bo"),
# e.g.: nm -o /lib/libc.a | objgraph


import sys
import string
import os
import getopt
import regex

# Types of symbols.
#
definitions = 'TRGDSBAEC'
externals = 'UV'
ignore = 'Nntrgdsbavuc'

# Regular expression to parse "nm -o" output.
#
matcher = regex.compile('\(.*\):\t?........ \(.\) \(.*\)$')

# Store "item" in "dict" under "key".
# The dictionary maps keys to lists of items.
# If there is no list for the key yet, it is created.
#
def store(dict, key, item):
	if dict.has_key(key):
		dict[key].append(item)
	else:
		dict[key] = [item]

# Return a flattened version of a list of strings: the concatenation
# of its elements with intervening spaces.
#
def flat(list):
	s = ''
	for item in list:
		s = s + ' ' + item
	return s[1:]

# Global variables mapping defined/undefined names to files and back.
#
file2undef = {}
def2file = {}
file2def = {}
undef2file = {}

# Read one input file and merge the data into the tables.
# Argument is an open file.
#
def readinput(file):
	while 1:
		s = file.readline()
		if not s:
			break
		# If you get any output from this line,
		# it is probably caused by an unexpected input line:
		if matcher.search(s) < 0: s; continue # Shouldn't happen
		(ra, rb), (r1a, r1b), (r2a, r2b), (r3a, r3b) = matcher.regs[:4]
		fn, name, type = s[r1a:r1b], s[r3a:r3b], s[r2a:r2b]
		if type in definitions:
			store(def2file, name, fn)
			store(file2def, fn, name)
		elif type in externals:
			store(file2undef, fn, name)
			store(undef2file, name, fn)
		elif not type in ignore:
			print fn + ':' + name + ': unknown type ' + type

# Print all names that were undefined in some module and where they are
# defined.
#
def printcallee():
	flist = file2undef.keys()
	flist.sort()
	for file in flist:
		print file + ':'
		elist = file2undef[file]
		elist.sort()
		for ext in elist:
			if len(ext) >= 8:
				tabs = '\t'
			else:
				tabs = '\t\t'
			if not def2file.has_key(ext):
				print '\t' + ext + tabs + ' *undefined'
			else:
				print '\t' + ext + tabs + flat(def2file[ext])

# Print for each module the names of the other modules that use it.
#
def printcaller():
	files = file2def.keys()
	files.sort()
	for file in files:
		callers = []
		for label in file2def[file]:
			if undef2file.has_key(label):
				callers = callers + undef2file[label]
		if callers:
			callers.sort()
			print file + ':'
			lastfn = ''
			for fn in callers:
				if fn <> lastfn:
					print '\t' + fn
				lastfn = fn
		else:
			print file + ': unused'

# Print undefine names and where they are used.
#
def printundef():
	undefs = {}
	for file in file2undef.keys():
		for ext in file2undef[file]:
			if not def2file.has_key(ext):
				store(undefs, ext, file)
	elist = undefs.keys()
	elist.sort()
	for ext in elist:
		print ext + ':'
		flist = undefs[ext]
		flist.sort()
		for file in flist:
			print '\t' + file

# Print warning messages about names defined in more than one file.
#
def warndups():
	savestdout = sys.stdout
	sys.stdout = sys.stderr
	names = def2file.keys()
	names.sort()
	for name in names:
		if len(def2file[name]) > 1:
			print 'warning:', name, 'multiply defined:',
			print flat(def2file[name])
	sys.stdout = savestdout

# Main program
#
def main():
	try:
		optlist, args = getopt.getopt(sys.argv[1:], 'cdu')
	except getopt.error:
		sys.stdout = sys.stderr
		print 'Usage:', os.path.basename(sys.argv[0]),
		print           '[-cdu] [file] ...'
		print '-c: print callers per objectfile'
		print '-d: print callees per objectfile'
		print '-u: print usage of undefined symbols'
		print 'If none of -cdu is specified, all are assumed.'
		print 'Use "nm -o" to generate the input (on IRIX: "nm -Bo"),'
		print 'e.g.: nm -o /lib/libc.a | objgraph'
		return 1
	optu = optc = optd = 0
	for opt, void in optlist:
		if opt == '-u':
			optu = 1
		elif opt == '-c':
			optc = 1
		elif opt == '-d':
			optd = 1
	if optu == optc == optd == 0:
		optu = optc = optd = 1
	if not args:
		args = ['-']
	for file in args:
		if file == '-':
			readinput(sys.stdin)
		else:
			readinput(open(file, 'r'))
	#
	warndups()
	#
	more = (optu + optc + optd > 1)
	if optd:
		if more:
			print '---------------All callees------------------'
		printcallee()
	if optu:
		if more:
			print '---------------Undefined callees------------'
		printundef()
	if optc:
		if more:
			print '---------------All Callers------------------'
		printcaller()
	return 0

# Call the main program.
# Use its return value as exit status.
# Catch interrupts to avoid stack trace.
#
try:
	sys.exit(main())
except KeyboardInterrupt:
	sys.exit(1)
