#include <stdio.h>
#include <ctype.h>
#include "make.h"

/*
 *    MAKE - Maintain separate source files
 *
 *    SYNOPSIS
 *        MK [-f file] [-a] [-n] [-d] [-i] [-k] [name] ...
 *           f: use 'file' instead of default makefile
 *           a: assume all modules are obsolete (recompile everything)
 *           n: don't recompile, just list steps to recompile
 *           d: debugging (print tree, file info)
 *          i: ignore return statuses from execution
 *          k: if errors occur, propagate error status up tree; continue.
 *           name: module name to recompile
 *
 *    AUTHOR
 *        Landon M. Dyer, Atari Inc.
 *
 *    INCREDIBLY HACKED OVER BY
 *        Eric C. Brown University of Utah.
 *       Fred Fish, UniSoft Systems Inc  (for Commodore AMIGA)
 *
 *    HACKS
 *        Added object library support (dummy names that inherit dates)
 *       Added source library support (real parents)
 *       Added direct execution capability.
 *       Removed script file.
 *       Added support for my macro based debugging package (fnf)
 *       Ran through "indent" to change formatting (fnf)
 */

#define INIT   "~INIT"         /* initialization macro */
#define DEINIT "~DEINIT"       /* de-init macro */
#define BEFORE "~BEFORE"       /* the per-root 'startup' method */
#define AFTER  "~AFTER"        /* the per-root 'wrapup' method */

char *mfiles[] = {             /* default makefiles */
    "makefile",
    "Makefile",

#ifdef VAXVMS
    "[-]makefile",
    "sys$login:makefile",
#endif

#ifdef MSDOS
    "..\makefile",
#endif
    ""
};

MACRO *mroot = NULL;         /* root of macro-list */
FILENODE *froot = NULL;   /* root of filenode-list */
FILENODE *firstf = NULL;  /* the very first filenode */

char *modnames[MAXMODS];       /* module-names mentioned in commandline */
int execstat = 0;              /* nonzero if started executing */
int modcount = 0;              /* #of module-names */
int debug = 0;                 /* nonzero: turn on debugging */
int obsolete = 0;              /* nonzero: every file should be recompiled */
int noscript = 0;              /* nonzero: print methods on stdout */
int ignore_errors = 0;         /* nonzero: ignore error return codes */
int prop_errors = 0;           /* nonzero: propagate error status up tree */
DATE bigbang;                  /* a date, the very earliest possible */
DATE endoftime;                        /* a date, the very last possible */

/*
 *     The following are used to save macro definitions given on the
 *     command line.  In the unix make, it is very common to override
 *     a definition in the makefile via a definition on the command line.
 *     However, this make expands macros in each line as they are read,
 *     which conflicts with the need to wait until the entire file is read
 *     before adding command line macros to the table (to override those
 *     from the file).  Thus the code needs to be modified to delay macro
 *     expansions until the last minute, before shipping the line off to
 *     be executed. The alternative is to implement a macro locking or
 *     precedence scheme.  Either solution requires more work than I can
 *     do at the moment.  Fred Fish  28-Nov-85
 */
char *cmdmacros[MAXCMDMACS];   /* Macro defs given on command line */
int cmdmcount = 0;             /* Number of macro defs on command line */

static void fparse ();
static void yankdependents ();
static void addmanydepend ();
static void determ ();
static void recomp ();

main (argc, argv)
int argc;
char *argv[];
{
    register int arg;
    register int i;
    register char *mfile = NULL;
    extern DATE adate ();
    extern void initrootdates ();
    extern void prtree ();

    DBUG_ENTER ("main");
    ENABLE_ABORT;
    initrootdates ();
    for (arg = 1; arg < argc; ++arg) {
       if (*argv[arg] == '-') {
           switch (tolower (argv[arg][1])) {
               case '#':
                   DBUG_PUSH (&argv[arg][2]);
                   break;
               case 'f': 
                   if (++arg >= argc) {
                       fputs ("-f needs filename argument.\n", stderr);
                       DBUG_RETURN (1);
                   }
                   mfile = argv[arg];
                   break;
               case 'a': 
                   obsolete = 1;
                   break;
               case 'n': 
                   noscript = 1;
                   break;
               case 'd': 
                   debug = 1;
                   break;
               case 'i': 
                   ignore_errors = 1;
                   break;
               case 'k': 
                   prop_errors = 1;
                   break;
               default: 
                   fputs ("Unknown switch: ", stderr);
                   fputc (argv[arg][1], stderr);
                   fputc ('\n', stderr);
                   break;
           }
       } else {
           if (index (argv[arg], '=') != NULL) {
               if (cmdmcount < MAXCMDMACS) {
                   cmdmacros[cmdmcount++] = argv[arg];
               } else {
                   fputs ("Too many command line macros.\n", stderr);
                   DBUG_RETURN (1);
               }
           } else {
               if (modcount < MAXMODS) {
                   modnames[modcount++] = argv[arg];
               } else {
                   fputs ("Too many module names.\n", stderr);
                   DBUG_RETURN (1);
               }
           }
       }
    }
    if (mfile != NULL) {
       if (fmake (mfile) == -1) {
           fputs ("Cannot open makefile '", stderr);
           fputs (mfile, stderr);
           fputs ("'.\n", stderr);
       }
    } else {
       for (i = 0; *mfiles[i]; ++i) {
           if (fmake (mfiles[i]) != -1) {
               break;
           }
       }
       if (!*mfiles[i]) {
           fputs ("Cannot open makefile.\n", stderr);
       }
    }
    if (debug) {
       prtree ();
    }
    DBUG_RETURN (0);
}


/*
 * Construct dependency tree from the makefile 'fn'.
 * Figure out what has to be recompiled, and write a script file to do that.
 */

fmake (fn)
char *fn;
{
    FILE * fp;

    DBUG_ENTER ("fmake");
    if ((fp = fopen (fn, "r")) == NULL) {
       DBUG_RETURN (-1);
    }
    fparse (fp);
    determ ();
    fclose (fp);
    DBUG_RETURN (0);
}


/*
 * Parse the input file, defining macros and building the dependency tree.
 */

static void fparse (fp)
FILE *fp;
{
    auto char ibuf[STRSIZ];
    auto char ebuf[STRSIZ];
    auto char *strp;
    register char *tok1;
    register char *tok2;
    register char *s;
    extern char *fgets ();
    register FILENODE *lastf = NULL;
    extern FILENODE *addfile ();
    extern void defmac ();
    extern void AddToLibrary ();
    extern void escape ();
    extern void addmeth ();

    DBUG_ENTER ("fparse");
    for (;;) {
       if (fgets (ibuf, STRSIZ, fp) == NULL) {
           break;
       }
       DBUG_3 ("inline", "got line '%s'", ibuf);
       mexpand (ibuf, ebuf, STRSIZ, MACCHAR);
       escape (ebuf, COMCHAR);
       s = ebuf + strlen (ebuf) - 1;   /* clobber last newline in string */
       if (s >= ebuf && *s == '\n') {
           *s = '\0';
       }
       DBUG_3 ("inline2", "after macro and excape processing is '%s'", ebuf);
       if (ebuf[0] == '\t' || ebuf[0] == ' ') {
           DBUG_2 ("meth", "looks like a method line to me");
           addmeth (lastf, ebuf);
           continue;
       }
       strp = ebuf;
       if ((tok1 = token (&strp)) == NULL) {
           continue;
       }
       if ((tok2 = token (&strp)) != NULL) {
           if (STRSAME (tok2, DEFMAC)) {
               DBUG_2 ("mac", "looks like a macro definition to me");
               if (*strp) {
                   defmac (tok1, strp);
               }
#if VAXVMS || MSDOS
               /* Preserve old behavior.  Unix and amiga behavior is */
               /* to never undefine any macros, in the sense used here. */
               /* This also allows macros with null expansions, which */
               /* are very useful.  Fred Fish */
               if (!*strp) {
                   if (undefmac (tok1) < 0) {
                       fputs ("Can't undefine macro '", stderr);
                       fputs (tok1, stderr);
                       fputs ("'.\n", stderr);
                   }
               }
#endif
               continue;
           } else if (STRSAME (tok2, DEPEND)) {
               DBUG_2 ("mac", "looks like a dependency line to me");
               addmeth (lastf, gmacro (AFTER)); /* terminate last mETHOD */
               lastf = filenode (tok1);        /* init lastf */
               if (firstf == NULL) {
                   firstf = lastf;
               }
               lastf -> fmake = NULL;
               addmeth (lastf, gmacro (BEFORE));
               lastf -> fflag |= ROOTP;
               addmanydepend (strp, lastf);
               continue;
#ifndef FUNNYLIBS
           } else if (STRSAME (tok2, ISLIB)) {
               addmeth (lastf, gmacro (AFTER));
               lastf = filenode (tok1);
               if (firstf == NULL) {
                   firstf = lastf;
               }
               lastf -> fmake = NULL;
               addmeth (lastf, gmacro (BEFORE));
               lastf -> fflag |= LIBRARY;
               lastf -> fflag |= ROOTP;

               AddToLibrary (lastf);
               /* no archives here */
               /* archives and libraries are mutually exclusive */
               while ((tok1 = token (&strp)) != NULL) {
                   (void) addfile (lastf, tok1);
               }
               continue;
#endif
           } else {
               DBUG_2 ("uh", "what kinda line is this?");
               addmanydepend (strp, lastf);
           }
       }
    }
    addmeth (lastf, gmacro (AFTER));
    DBUG_VOID_RETURN;
}

/*
 * scan tokens from strbuf and search for libraries and archives.
 * libraries look like foo [ bar baz mumble ]
 * archives look like foo ( bar baz mumble )
 * in either case, bar, baz, and mumble have parents of foo.
 * foo is added to the parentlist, if not already on the list.
 * bar, baz, and mumble are added to the dependency list of depend.
 * the command *cannot* be split across newlines without causing errors.
 * if you don't like that, well, life's a bitch and then you die.
 */

static void addmanydepend (strbuf, depend)
char *strbuf;
FILENODE *depend;
{
    register char *tok1;
    register char *tok2;
    register FILENODE *parent;
    register FILENODE *child;
    extern FILENODE *addfile ();
    extern FILENODE *addparent ();
    extern void exit ();

    DBUG_ENTER ("addmanydepend");
    DBUG_4 ("dep", "add dependencies '%s' to '%s'", strbuf, depend -> fname);
    tok1 = token (&strbuf);
    if (tok1 == NULL) {
       DBUG_VOID_RETURN;
    }
    tok2 = token (&strbuf);
    while (tok2 != NULL) {
#ifdef FUNNYLIBS
       if (STRSAME (tok2, BGNLIB)) {
           parent = addparent (tok1);  /* add tok1 to parent list */
           for (tok1 = token (&strbuf);        /* skip over token in tok2 */
                tok1 != NULL && strcmp (tok1, ENDLIB); /* go eol or end */
                tok1 = token (&strbuf)) {      /* get next token */
               if (tok1 == NULL) {
                   fputs ("MAKE: Error in library defn.\n", stderr);
                   exit (2);
               }
               child = addfile (depend, tok1);
               child -> fflag = LIBRARY;
               child -> parent = parent;
           }                   /* for */
           tok1 = token (&strbuf);
           tok2 = token (&strbuf);
           continue;           /* the while */
       }                       /* if islib */
#endif
       if (STRSAME (tok2, BGNARC)) {
           parent = addparent (tok1);          /* add tok1 to parent list */
           for (tok1 = token (&strbuf);        /* skip over token in tok2 */
                tok1 != NULL && strcmp (tok1, ENDARC); /* go eol or end */
                tok1 = token (&strbuf)) {      /* get next token */
               if (tok1 == NULL) {
                   fputs ("MAKE: Error in archive defn.\n", stderr);
                   exit (2);
               }
               child = addfile (depend, tok1);
               child -> fflag = ARCHIVE;
               child -> parent = parent;
           }                   /* for */
           tok1 = token (&strbuf);/* get current token */
           tok2 = token (&strbuf);/* get lookahead token */
           continue;           /* the while */
       }                       /* if isarc */
       else {                  /* nothing special -- */
           (void) addfile (depend, tok1);/* add dependency */
           tok1 = tok2;        /* shift token */
           tok2 = token (&strbuf);
       }
    }                          /* while */
    if (tok2 == NULL && tok1 != NULL) {                /* last token = not special */
       (void) addfile (depend, tok1);
    }
    DBUG_VOID_RETURN;
}

/*
 * Determine sequence of recompiles from the creation dates.
 * If have anything to recompile, then create a script file full of commands.
 */

static void determ ()
{
    register FILENODE *f;
    register int i;
    register char *m;
    extern void cleanuparchives ();

    DBUG_ENTER ("determ");
    if (firstf == NULL) {         /* empty tree */
       puts ("No changes.");
       DBUG_VOID_RETURN;
    }
    if (modcount == 0) {
       examine (firstf, endoftime);
    } else {
       for (i = 0; i < modcount; ++i) {
           if ((f = gfile (modnames[i])) == NULL) {
               fputs ("Don't know how to make ", stderr);
               fputs (modnames[i], stderr);
               fputs (".\n", stderr);
               continue;
           }
           if ((f -> fflag & ROOTP) == 0) {
               fputc ('\'', stderr);
               fputs (f -> fname, stderr);
               fputs ("' is not a root!\n", stderr);
               continue;
           }
           examine (f, endoftime);
       }
    }
    if (execstat) {
       if ((m = gmacro (DEINIT)) != NULL) {
           execute (m, noscript);
       }
       cleanuparchives ();
    } else {
       puts ("No changes.");
    }
    DBUG_VOID_RETURN;
}


/*
 * Examine filenode 'fnd' and see if it has to be recompiled.
 * 'date' is the last-touched date of the node's father
 * (or 'endoftime' if its a root file.)
 * Root files with NO dependencies are assumed not to be up to date.
 */

examine (fnd, date)
FILENODE *fnd;
DATE date;
{
    register int rebuildp = 0;
    register int rval;
    register int errcode = 0;
    register NODE *n;
    extern void getdate ();
    extern char *printdate ();

    DBUG_ENTER ("examine");
    DBUG_3 ("ex", "parent node date '%s'", printdate (date));
    DBUG_3 ("ex", "examine node '%s'", fnd -> fname);
    getdate (fnd);
    DBUG_3 ("ex", "modification date '%s'", printdate (fnd -> fdate));
    DBUG_3 ("ex", "parent node date '%s'", printdate (date));
    if (fnd -> fnode == NULL && fnd -> fflag & ROOTP) {
       DBUG_2 ("root", "node, is rootnode with no dependents, rebuild");
       rebuildp = 1;
    } else {                   /* see if dependents need to be recompiled */
       for (n = fnd -> fnode; n != NULL; n = n -> nnext) {
           if ((rval = examine (n -> nfile, fnd -> fdate)) != 0) {
               if (rval == ERROR) {
                   errcode = ERROR;/* if error occurred, propagate up */
                   fnd -> fflag |= ERROR;
                   fputs ("Couldn't remake ", stderr);
                   fputs (fnd -> fname, stderr);
                   fputs (" because of errors.\n", stderr);
               }
               rebuildp = 1;
           }
       }
    }
    DBUG_3 ("ex", "parent node date '%s'", printdate (date));
    DBUG_3 ("rebuildp", "rebuild flag is %d", rebuildp);
    /* if ancestor recompiled or root, recompile, */
    /* but not if error in ancestor */
    if (rebuildp && (fnd -> fflag & ERROR) == 0) {
       DBUG_3 ("rebuild", "'%s' needs remaking", fnd -> fname);
       recomp (fnd);
       if (fnd -> fflag & ERROR) {
           DBUG_3 ("err", "got an error remaking %s", fnd -> fname);
           DBUG_RETURN (ERROR);
       }
    }
    DBUG_3 ("ex", "current node date now '%s'", printdate (fnd -> fdate));
    DBUG_3 ("ex", "parent node date '%s'", printdate (date));
    if (obsolete || laterdt (fnd -> fdate, date) >= 0) {
       DBUG_2 ("date", "looks like parent needs remaking now");
       rebuildp = 1;
    }
    if (errcode) {
       DBUG_RETURN (errcode);
    } else {
       DBUG_RETURN (rebuildp);
    }
}

/*
 * Make sure a filenode gets recompiled.
 */

static void recomp (f)
FILENODE *f;
{
    register char *m;

    DBUG_ENTER ("recomp");
    if (!execstat) {
       execstat = 1;
       if ((m = gmacro (INIT)) != NULL) {
           execute (m, noscript);
       }
    }
    if (f -> fflag & REBUILT) {
       DBUG_VOID_RETURN;
    }
    if (!noscript) {           /* don't extract if printing steps */
       yankdependents (f);
    }
    if (f -> fmake != NULL) {
       if (execute (f -> fmake, noscript) != 0) {
           if (!ignore_errors && !prop_errors) {
               exit (2);
           } else if (prop_errors) {
               f -> fflag |= ERROR;
           }
       }
    }
    f -> fflag |= REBUILT;
    DBUG_VOID_RETURN;
}

static void yankdependents (fnd)
FILENODE *fnd;
{
    register NODE *n;
    extern int extract ();

    DBUG_ENTER ("yankdependents");
    for (n = fnd -> fnode; n != NULL; n = n -> nnext) {
#ifdef YANKDESCENDANTS
       yankdependents (n -> nfile);
#endif
       DBUG_3 ("dep", "yanking %s", n -> nfile -> fname);
       DBUG_3 ("dep", "flags %d", n -> nfile -> fflag);
       if ((n -> nfile -> fflag & ARCHIVE) && ((n -> nfile -> fflag & EXTRACT) == 0)) {
           /* if archived and not extracted */
           fputs ("Extracting ", stdout);
           puts (n -> nfile -> fname);
           if (!noscript) {
#ifdef LAR
               if (extract (n -> nfile) == FAILURE) {
                   fputs ("Extract failed -- I think I'll die now.\n", stderr);
                   exit (1);
               }
#else
               fputs ("No support for archives, bye!\n", stderr);
               exit (1);
#endif
           }
           n -> nfile -> fflag |= EXTRACT;
       }
    }
    DBUG_VOID_RETURN;
}

/*
 * Complain about being out of memory, and then die.
 */

allerr ()
{
    fputs ("Can't alloc -- no space left (I give up!)\n", stderr);
    exit (1);
}
