#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 = (MACRO *) NULL;		/* root of macro-list */
FILENODE *froot = (FILENODE *) NULL;	/* root of filenode-list */
FILENODE *firstf = (FILENODE *) 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 (strchr (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")) == (FILE *) 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 = (FILENODE *)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 == (FILENODE *) 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 == (FILENODE *) 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 == (FILENODE *) 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])) == (FILENODE *) 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 == (NODE *) 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 != (NODE *) 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 != (NODE *) 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);
}
