#! /usr/bin/env awk -f
#  __   _
#  |_) /|  Copyright (C) 2000  |  richard@
#  | \/¯|  Richard Atterer     |  atterer.org
#  ¯ '` ¯
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License, version 2. See
#  the file COPYING for details.

# Syntax: awk -f depend.awk srcdir subdir1 subdir2... - file1 file2...

# Reads all the files and looks for #include directives. If any file
# being included can be found in srcdir/subdir or ./subdir, it is also
# recursively scanned. Finally, writes a list of dependencies for each
# file to srcdir+"Makedeps" (can also append the list of dependencies
# to Makefile and Makefile.in; see output = "..." below)

# Supports my two-level or three-level #include hierarchy:
#   - in "fh" (forward decl) files, may only include other "fh" files
#   - in "hh" (normal hdr) files, may include "fh" and "hh" files
#   - in "ih" (inline defs) files, may include "fh", "hh" and "ih" files
# Furthermore, depending on whether NOINLINE has been #define'd,
# INLINE functions are either compiled inline or non-inline. The
# latter is useful during development, to avoid excessive
# recompilation after changes to "hh" headers. The idea is that the
# implementations in "ih" will need many more other headers than the
# decls in "hh". IMPORTANT: The generated dependencies assume that you
# *have* defined NOINLINE during development.

# Differences to "makedepend" behaviour: ignores ".ih" files unless
# included from ".cc" files; never takes #ifdef and friends into
# account; does not scan include dirs in specified order, but instead
# scans all include dirs for each file; Untested behaviour if the same
# source filename appears more than once in different source
# directories.

# Another extension: If a source file contains "#makefile" at the
# start of the line (whitespace allowed), everything after the
# space/tab following this string is written to the Makefile verbatim.

# Furthermore, if the source file contains "#test-deps" followed by a
# (possibly empty) list of object files, an appropriate Makefile entry
# is added to build the unit test's executable. A "#test-ldflags" line
# can specify flags to pass to the linker, e.g. "$(GTKFLAGS)"
#______________________________________________________________________

# recursively find dependencies for specified file.
function makeDeps(dir, file) {
  finalDeps = makeDeps2(dir, file);
  return substr(finalDeps, 2, length(finalDeps) - 2);
}

function makeDeps2(dir, file, l_deps, l_dir, l_recurseFile, l_line, l_exists,
                   l_lineNr, l_fileType, l_includedType,
                   l_depsLine, l_depsLineNr, l_depsLdflags) {
  if (deps[file] == "-") return ""; # avoid infinite recursion
  else if (deps[file] != "") return deps[file]; # already know about that
  deps[file] = "-";

  l_deps = " "; l_lineNr = 0;
  l_fileType = substr(file, length(file) - 1); # e.g. "cc" or ".h"
  l_depsLine = ""; l_depsLdflags = "";
  while ((getline l_line < (dir file)) == 1) { # read each line of file
    exists[file]; # have read at least 1 line, so the file is there
    ++l_lineNr;
    if (l_line ~ /^[ \t]*\#[ \t]*makefile[ \t]/) {
      sub(/^[ \t]*\#[ \t]*makefile[ \t]/, "", l_line);
      inlinedMakefile = inlinedMakefile "# " file ":" l_lineNr "\n" \
          l_line "\n";
      continue;
    }
    if (l_line ~ /^[ \t]*\#[ \t]*test-deps([ \t]|$)/) {
      sub(/^[ \t]*\#[ \t]*test-deps[ \t]?/, "", l_line);
      l_depsLine = l_depsLine " " l_line;
      l_depsLineNr = l_lineNr;
      continue;
    }
    if (l_line ~ /^[ \t]*\#[ \t]*test-ldflags([ \t]|$)/) {
      sub(/^[ \t]*\#[ \t]*test-ldflags[ \t]?/, "", l_line);
      l_depsLdflags = l_depsLdflags " " l_line;
      continue;
    }
    if (l_line !~ /^[ \t]*\#[ \t]*include[ \t]+["<][a-zA-Z0-9.-]+[">]/)
      continue;
    # found #include line
    match(l_line, /["<][a-zA-Z0-9.-]+[">]/);
    l_includedType = substr(l_line, RSTART + RLENGTH - 3, 2);

    l_exists = 0;
    # skip for loop to ignore .ih files except when included from .cc
    if (l_fileType == "cc" || l_includedType != "ih") {
      for (l_dir in includeDir) { # try each in include path
        l_recurseFile = substr(l_line, RSTART + 1, RLENGTH - 2);
        split(makeDeps2(dir, l_dir l_recurseFile), newDeps); # recurse
        # eliminate duplicates by only adding new files to l_deps
        for (i in newDeps) {
          if (index(l_deps, " " newDeps[i] " ") == 0)
            l_deps = l_deps newDeps[i] " ";
        }
        newDep = includeDir[l_dir] l_recurseFile;
        if ((l_dir l_recurseFile) in exists) {
          l_exists = 1;
          if (index(l_deps, " " newDep " ") == 0) l_deps = l_deps newDep " ";
        }
      }
    }
    # maybe complain about wrong includes
    if (l_line !~ /\/\* *NOINLINE/ && l_exists \
        && type[l_fileType] < type[l_includedType])
      printf("%s:%d: #including file `%s' violates policy\n",
             file, l_lineNr, l_recurseFile);
  }
  close((dir file));

  if (l_depsLine != "") {
    base = file; sub(/^(.\/)*/, "", base); sub(/\.(c|cc|cpp|C)$/, "",base);
    l_depsLine = base".o $(TEST-DEFAULTOBJS)" l_depsLine;
    inlinedMakefile = inlinedMakefile "# " file ":" l_depsLineNr "\n" \
      base "$(EXE): " l_depsLine "\n" \
      "\t$(LD) -o "base"$(EXE) " l_depsLine " $(TEST-LDFLAGS)" \
      l_depsLdflags "\n";
  }

  deps[file] = l_deps;
  return l_deps;
}
#______________________________________________________________________

# Read file until separator line found, set depContent to rest
function readSepFile(file, l_line, l_ret) {
  l_ret = "";
  while ((getline l_line < file) == 1) {
    l_ret = l_ret l_line "\n";
    if (l_line == "# DO NOT DELETE THIS LINE -- make depend depends on it."){
      depContent = "";
      while ((getline l_line < file) == 1)
        depContent = depContent l_line "\n";
      close(file);
      return l_ret;
    }
  }
  print "depend.awk: No separator line found in `" file "'!?";
  exit(1);
}

# Read entire file contents into depContent
function readFile(file, l_RS) {
  l_RS = RS;
  RS = "\x7f";
  getline depContent < file;
  close(file);
  RS = l_RS;
  return "";
}
#______________________________________________________________________

BEGIN {
  # Output filename. Special case: output="" means: Append
  # dependencies to Makefile and Makefile.in
  output = "Makedeps";

  # lower number => may include all with higher number
  type["cc"] = type[".c"] = 4;
  type["ih"] = 3;
  type["hh"] = type[".h"] = 2;
  type["fh"] = 1;

  arg = 0;
  srcDir = ARGV[++arg]; # first arg is directory containing sources
  if (substr(srcDir, length(srcDir)) != "/") srcDir = srcDir "/";
  if (substr(srcDir, 1, 2) == "./") srcDir = substr(srcDir, 3);

  includeDir[srcDir] = ""; # look in source dir
  includeDir[""] = ""; # also look in current dir and its subdirs
  while (ARGV[++arg] != "-") { # read subdirectories until "-"
    includeDir[srcDir ARGV[arg] "/"] = ARGV[arg] "/";
    includeDir[ARGV[arg] "/"] = ARGV[arg] "/";
  }

  if (output) {
    md = srcDir output;
    readFile(md);
    newDepContent = "";
  } else {
    # Makefile/Makefile.in
    mf1 = srcDir "Makefile.in";
    mf2 = "Makefile";
    makeFileInContent = readSepFile(mf1);
    newDepContent = "\n";
  }

  # build dependencies
  inlinedMakefile = "";
  while (++arg < ARGC) {
    f = ARGV[arg]; sub(/^\.\//, "", f);
    base = f; sub(/\.(c|cc|cpp|C)$/, "", base);
    target = base ".o";
    newDepContent = newDepContent \
                    sprintf("%s: %s %s\n", target, f, makeDeps(srcDir, f));
  }
  newDepContent = inlinedMakefile "\n" newDepContent;

  # write files
  if (depContent == newDepContent) {
    print "No dependency changes";
  } else {
    if (output) {
      system("mv -f " md " " md ".bak");
      printf("%s", newDepContent) > md;
      print "Updated `" md "'";
    } else {
      # Makefile/Makefile.in
      makeFileContent = readSepFile(mf2);
      system("mv -f " mf1 " " mf1 ".bak");
      printf("%s%s", makeFileInContent, newDepContent) > mf1;
      printf("%s%s", makeFileContent, newDepContent) > mf2;
      print "Updated `" srcDir "Makefile.in' and `Makefile'";
    }
  }
}
