#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018-2024 Free Software Foundation, Inc.
# Contributed by Bernhard Reutner-Fischer <aldot@gcc.gnu.org>
# Inspired by bloat-o-meter from busybox.

# This software may be used and distributed according to the terms and
# conditions of the GNU General Public License as published by the Free
# Software Foundation.

# For a set of object-files, determine symbols that are
#  - public but should be static

# Examples:
# unused_functions.py ./gcc/fortran
# unused_functions.py gcc/c  gcc/c-family/ gcc/*-c.o | grep -v "'gt_"
# unused_functions.py gcc/cp gcc/c-family/ gcc/*-c.o | grep -v "'gt_"

import sys, os
from tempfile import mkdtemp
from subprocess import Popen, PIPE

def usage():
    sys.stderr.write("usage: %s [-v] [dirs | files] [-- <readelf options>]\n"
                        % sys.argv[0])
    sys.stderr.write("\t-v\tVerbose output\n");
    sys.exit(1)

(odir, sym_args, tmpd, verbose) = (set(), "", None, False)

for i in range(1, len(sys.argv)):
    f = sys.argv[i]
    if f == '--': # sym_args
        sym_args = ' '.join(sys.argv[i + 1:])
        break
    if f == '-v':
        verbose = True
        continue
    if not os.path.exists(f):
        sys.stderr.write("Error: No such file or directory '%s'\n" % f)
        usage()
    else:
        if f.endswith('.a') and tmpd is None:
            tmpd = mkdtemp(prefix='unused_fun')
        odir.add(f)

def dbg(args):
    if not verbose: return
    print(args)

def get_symbols(file):
    syms = {}
    rargs = "readelf -W -s %s %s" % (sym_args, file)
    p0 = Popen((a for a in rargs.split(' ') if a.strip() != ''), stdout=PIPE)
    p1 = Popen(["c++filt"], stdin=p0.stdout, stdout=PIPE,
            universal_newlines=True)
    lines = p1.communicate()[0]
    for l in lines.split('\n'):
        l = l.strip()
        if not len(l) or not l[0].isdigit(): continue
        larr = l.split()
        if len(larr) != 8: continue
        num, value, size, typ, bind, vis, ndx, name = larr
        if typ == 'SECTION' or typ == 'FILE': continue
        # I don't think we have many aliases in gcc, re-instate the addr
        # lut otherwise.
        if vis != 'DEFAULT': continue
        #value = int(value, 16)
        #size = int(size, 16) if size.startswith('0x') else int(size)
        defined = ndx != 'UND'
        globl = bind == 'GLOBAL'
        # c++ RID_FUNCTION_NAME dance. FORNOW: Handled as local use
        # Is that correct?
        if name.endswith('::__FUNCTION__') and typ == 'OBJECT':
            name = name[0:(len(name) - len('::__FUNCTION__'))]
            if defined: defined = False
        if defined and not globl: continue
        syms.setdefault(name, {})
        syms[name][['use','def'][defined]] = True
        syms[name][['local','global'][globl]] = True
    # Note: we could filter out e.g. debug_* symbols by looking for
    # value in the debug_macro sections.
    if p1.returncode != 0:
        print("Warning: Reading file '%s' exited with %r|%r"
            % (file, p0.returncode, p1.returncode))
    p0.kill()
    return syms

(oprog, nprog) = ({}, {})

def walker(paths):
    def ar_x(archive):
        dbg("Archive %s" % path)
        f = os.path.abspath(archive)
        f = os.path.splitdrive(f)[1]
        d = tmpd + os.path.sep + f
        d = os.path.normpath(d)
        owd = os.getcwd()
        try:
            os.makedirs(d)
            os.chdir(d)
            p0 = Popen(["ar", "x", "%s" % os.path.join(owd, archive)],
                    stderr=PIPE, universal_newlines=True)
            p0.communicate()
            if p0.returncode > 0: d = None # assume thin archive
        except:
            dbg("ar x: Error: %s: %s" % (archive, sys.exc_info()[0]))
            os.chdir(owd)
            raise
        os.chdir(owd)
        if d: dbg("Extracted to %s" % (d))
        return (archive, d)

    def ar_t(archive):
        dbg("Thin archive, using existing files:")
        try:
            p0 = Popen(["ar", "t", "%s" % archive], stdout=PIPE,
                    universal_newlines=True)
            ret = p0.communicate()[0]
            return ret.split('\n')
        except:
            dbg("ar t: Error: %s: %s" % (archive, sys.exc_info()[0]))
            raise

    prog = {}
    for path in paths:
        if os.path.isdir(path):
            for r, dirs, files in os.walk(path):
                if files: dbg("Files %s" % ", ".join(files))
                if dirs: dbg("Dirs  %s" % ", ".join(dirs))
                prog.update(walker([os.path.join(r, f) for f in files]))
                prog.update(walker([os.path.join(r, d) for d in dirs]))
        else:
            if path.endswith('.a'):
                if ar_x(path)[1] is not None: continue # extract worked
                prog.update(walker(ar_t(path)))
            if not path.endswith('.o'): continue
            dbg("Reading symbols from %s" % (path))
            prog[os.path.normpath(path)] = get_symbols(path)
    return prog

def resolve(prog):
    x = prog.keys()
    use = set()
    # for each unique pair of different files
    for (f, g) in ((f,g) for f in x for g in x if f != g):
        refs = set()
        # for each defined symbol
        for s in (s for s in prog[f] if prog[f][s].get('def') and s in prog[g]):
            if prog[g][s].get('use'):
                refs.add(s)
        for s in refs:
            # Prune externally referenced symbols as speed optimization only
            for i in (i for i in x if s in prog[i]): del prog[i][s]
        use |= refs
    return use

try:
    oprog = walker(odir)
    if tmpd is not None:
        oprog.update(walker([tmpd]))
    oused = resolve(oprog)
finally:
    try:
        p0 = Popen(["rm", "-r", "-f", "%s" % (tmpd)], stderr=PIPE, stdout=PIPE)
        p0.communicate()
        if p0.returncode != 0: raise "rm '%s' didn't work out" % (tmpd)
    except:
        from shutil import rmtree
        rmtree(tmpd, ignore_errors=True)

for (i,s) in ((i,s) for i in oprog.keys() for s in oprog[i] if oprog[i][s]):
    if oprog[i][s].get('def') and not oprog[i][s].get('use'):
        print("%s: Symbol '%s' declared extern but never referenced externally"
            % (i,s))


