/*
 * make.c	An imitation of the Unix MAKE facility
 *
 * 88-10-01 v1.0	created by greg yachuk, placed in the public domain
 * 88-10-06 v1.1	changed prerequisite list handling
 *
 */

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <memory.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef	MSDOS
#include <stdlib.h>
#include <process.h>
#include <dos.h>
#else
#include <sys/errno.h>
#include <sys/wait.h>
#endif

#include "make.h"
#include "tstring.h"
#include "decl.h"


#ifdef	MSDOS
#define	PATH_SEPARATOR	";"
#define	FILE_SEPARATOR	"/\\"
#else
#define	PATH_SEPARATOR	":"
#define	FILE_SEPARATOR	"/"
#endif

#define MAKEINI "default.mk"

char   *shell_cmds[] = {
#ifdef	MSDOS
	"dir", "type", "rem", "pause", "date", "time", "ren", "rename",
	"ver", "vol", "break", "verify", "mkdir", "md", "exit", "ctty",
	"echo", "if", "cls", "chdir", "cd", "rmdir", "rd", "copy", "del",
	"erase",
#endif
	NULL,
};


targptr target_list;		/* list of target nodes */
fileptr file_list;		/* list of file nodes */
symptr  symbol_list;		/* list of symbol nodes */
shellptr shell_list;		/* list of shell nodes */

targptr first_targ = NULL;	/* first target, in case nothing explicit */
targptr suffix_targ = NULL;	/* .SUFFIXES target pointer */

char  **tlist = NULL;		/* command line targets */
char  **flist = NULL;		/* command line make files */
char  **mlist = NULL;		/* command line macros */

int	tmax = 0;		/* max size of tlist */
int	fmax = 0;		/* max size of flist */
int	mmax = 0;		/* max size of mlist */

int     depend = 0;		/* -d option */
int	display = 0;		/* -D option */
int     ignore = 0;		/* -i option */
int     noexec = 0;		/* -n option */
int     readdef = 1;		/* -r option */
int     silent = 0;		/* -s option */
int	touch = 0;		/* -t option */

int	dispcount = 0;		/* used for -D option */

long	now;			/* time at startup */


main(argc, argv)
int     argc;
char  **argv;
{
	REGISTER int i;
	REGISTER targptr targp;

	/* initialize the various global lists */

	target_list = NULL;
	file_list = NULL;
	symbol_list = NULL;
	shell_list = NULL;

	/* allocate space for command line targets, files and macros */

	tlist = grow_list(NULL, &tmax);
	flist = grow_list(NULL, &fmax);
	mlist = grow_list(NULL, &mmax);

	make_args(argc, argv);		/* process command line options */

	if (noexec)
		silent = touch = 0;	/* -n always displays; never touches */

	if (dispcount > 1)
		display = 1;		/* display `default.mk' on -DD */


	first_targ = NULL;		/* used in parse() */

	if (readdef)
		parse(fopenp(MAKEINI, "r"));	/* read in `default.mk' */

	if (dispcount > 0)
		display = 1;		/* display makefile's on -D */

	first_targ = NULL;		/* get first target in `makefile' */

	/* parse the makefiles given on command line */
	for(i = 0; flist[i] != NULL; i++)
		parse(fopen(flist[i], "r"));

	/* no makefiles specified, so use "makefile" */
	if (i == 0)
		parse(fopen("makefile", "r"));

	tfree(flist);			/* all done with makefile's */

	for(i = 0; mlist[i] != NULL; i++)
		add_macro(mlist[i]);	/* add command line macros */

	tfree(mlist);			/* all done with macros */

	if ((targp = get_target(".INIT")) != NULL)
		build(targp->tshell);	/* process the .INIT rule */

	for (i = 0; tlist[i] != NULL; i++)
		make(tlist[i], 1);	/* process command line targets */

	tfree(tlist);			/* all done with targets */

	/* if no targets specified, make the first one */
	if (i == 0 && first_targ)
		make(first_targ->tfile->fname, 1);

	if ((targp = get_target(".DONE")) != NULL)
		build(targp->tshell);	/* process the .DONE rule */

	return (0);			/* not exit(); see new_make() */
}


make_args(argc, argv)
int	argc;
char  **argv;
{
	REGISTER int tlen;
	REGISTER int flen;
	REGISTER int mlen;

	/* get current date & time */
	now = curr_time();

	tlen = flen = mlen = 0;

	while (++argv, --argc)
	{
		/* look for macros and targets */

		if (**argv != '-')
		{
			if (strchr(*argv, '='))
			{		/* store as a macro */
				if (mlen == mmax)
					mlist = grow_list(mlist, &mmax);
				mlist[mlen++] = *argv;
			}
			else
			{		/* store as a target */
				if (tlen == tmax)
					tlist = grow_list(tlist, &tmax);
				tlist[tlen++] = *argv;
			}
			continue;
		}

		while (*argv && *++*argv)
		{
			switch (**argv)
			{
			case 'd':		/* show dependencies */
				depend++;
				break;

			case 'D':		/* display makefiles */
				dispcount++;
				break;

			case 'f':		/* new makefile name */
				if (argc < 2)
					usage();
				if (flen == fmax)
					flist = grow_list(flist, &fmax);
				++argv, --argc;
				flist[flen++] = *argv;
				*argv = NULL;
				break;

			case 'i':		/* ignore errors */
				ignore = 1;
				break;

			case 'n':		/* don't execute commands */
				noexec = 1;
				break;

			case 'r':		/* don't read default.mk */
				readdef = 0;
				break;

			case 's':		/* don't echo commands */
				silent = 1;
				break;

			case 't':		/* touch files, don't build */
				touch = 1;
				break;

			default:
				usage();
			}
		}
	}

	/* terminate all lists with a NULL pointer */

	tlist[tlen] = NULL;
	flist[flen] = NULL;
	mlist[mlen] = NULL;
}


/*
 * grow_list	- expand the list of pointers by a factor of two
 */
char  **
grow_list(list, len)
char  **list;
int    *len;
{
	REGISTER int l;

	l = *len;		/* get current length */

	/* if list is NULL, start off with a default list */

	if (list == NULL)
		list = (char **) talloc(((l=1)+1) * sizeof(char *));
	else
		list = (char **) trealloc((char *)list,
					  ((l <<=1)+1)*sizeof(char *));

	if (list == NULL)
		terror(1, "too many options");

	/* if we are initially allocating it, set first pointer to NULL */

	if (l == 1)
		*list = NULL;

	*len = l;		/* update current length */
	return (list);
}


/*
 * fopenp	- open file in current directory or along PATH
 */
FILE   *
fopenp(fname, type)
char   *fname;
char   *type;
{
	REGISTER int len;
	REGISTER char *fpath;
	FILE   *fd;
	char   *path;
	char   *tp;

	/* if the filename is -, use stdin */

	if (equal(fname, "-"))
		return (stdin);

	/* try to open file relative to current directory */
	if ((fd = fopen(fname, type)) != NULL)
		return (fd);

	/* didn't work, search along path */

	if ((path = getenv("PATH")) == NULL)
		return (NULL);

	path = tstrcpy(path);		/* allocate string and copy */
	fpath = talloc(strlen(path) + strlen(fname) + 2);

	/* look for tokens separated by semi-colons (;) or colons (:) */

	tp = token(path, PATH_SEPARATOR);
	while (tp != NULL)
	{
		strcpy(fpath, tp);
		len = strlen(fpath) - 1;

		/* make sure there is a separator between path and filename */

		if (!strchr(FILE_SEPARATOR, fpath[len]))
			fpath[++len] = '/';

		strcpy(&fpath[len+1], fname);
		if ((fd = fopen(fpath, type)) != NULL)
			break;

		tp = token(NULL, PATH_SEPARATOR);
	}

	tfree(path);
	tfree(fpath);

	return (fd);
}


/*
 * make		- guts of the make command
 *		- make all pre-requisites, and if necessary, build target
 *
 *	returns	-1 target was already up to date w.r.t. pre-requisites
 *		 0 target has not been built
 *		 1 target is now built (and up to date)
 */
make(targname, worry)
char   *targname;
int	worry;		/* if set, it is an error to NOT build this */
{
	REGISTER targptr targp;
	REGISTER fileptr *preqp;
	fileptr	filep;
	long	targtime;
	long	preqtime;

	/* if recorded time of file is not default, we've already built it */
	filep = get_file(targname);
	if (filep && filep->ftime != MAXNEGTIME)
		return (-1);

	targp = get_target(targname);	/* find the target node */
	if (targp == NULL)
		return (default_rule(targname, worry, 0));

	targtime = file_time(targname);	/* keep actual time of current target */

	/* must build non-existant files, even with no pre-requisites */
	preqtime = MAXNEGTIME + 1;

	/* make all pre-requisites */
	preqp = targp->tpreq;
	while (*preqp)
	{
		make((*preqp)->fname, worry);

		/* keep track of newest pre-requisite */
		if (preqtime < (*preqp)->ftime)
			preqtime = (*preqp)->ftime;

		/* display as necessary */
		if (depend > 1 || (depend && (*preqp)->ftime > targtime))
		{
			display_prereq(targname, targtime, (*preqp)->fname,
				       (*preqp)->ftime);
		}

		++preqp;
	}

	if (targp->tshell == NULL)	/* try default rules anyway */
		return (default_rule(targname, 0, preqtime > targtime));
	else if (preqtime > targtime)
	{
		if (touch)		/* won't be set when `noexec' */
			touch_file(targname);
		else
		{
			add_metas("", "", targname);
			build(targp->tshell);
		}

		targp->tfile->ftime = (noexec) ? now : file_time(targname);
		return (1);
	}

	targp->tfile->ftime = targtime;

	return (-1);
}


/*
 * default_rule	- try the .SUFFIXES when we don't have an explicit target
 *		- if `worry' is set, it is an ERROR to NOT build this target
 *		- `mustbuild' is set if make() has out-of-date prereq's
 *		   but no explicit shell rules
 */
default_rule(targname, worry, mustbuild)
char   *targname;
int	worry;
int	mustbuild;
{
	REGISTER targptr targp;
	REGISTER fileptr *preqp;
	fileptr	filep;
	char   *ext;
	char   *basename;
	char   *preqname;
	long	targtime;
	long	preqtime;
	int	built;
	char	suffrule[80];

	/* find the extension */
	ext = strrchr(targname, '.');
	if (ext == NULL)
		ext = targname + strlen(targname);

	/* find the base name */
	basename = tstrncpy(targname, ext - targname);

	targtime = file_time(targname);

	/* suffix_targ is used to (slightly) speed up this function */
	preqp = suffix_targ ? suffix_targ->tpreq : NULL;
	built = 0;

	while (*preqp && !built)
	{
		/* look for a default rule from SUFFIX to `ext' */
		strcat(strcpy(suffrule, (*preqp)->fname), ext);
		targp = get_target(suffrule);	/* e.g. `.c.o' */

		if (targp != NULL)
		{
			/* found a rule; see if file exists */
			preqname = tstrcat(basename, (*preqp)->fname);
			preqtime = file_time(preqname);

			/*
			 * don't bother recursive makes unless necessary
			 * e.g. we have .c.o and .l.c, but also .l.o!
			 * we want to use .l.o if a .c file does not exist
			 */
			if (preqtime != MAXNEGTIME || mustbuild)
				built = make(preqname, 0);

			/* check if pre-req file exists and is newer */
			preqtime = file_time(preqname);
			if (preqtime > targtime || (mustbuild && built))
			{
				if (depend)
				{
					display_prereq(targname, targtime,
						       preqname, preqtime);
				}

				if (touch)	/* won't be set when `noexec' */
					touch_file(targname);
				else
				{
					add_metas(basename, preqname, targname);
					build(targp->tshell);
				}
				built = 1;
			}
			else if (depend > 1 && preqtime != MAXNEGTIME)
			{
				display_prereq(targname, targtime,
					       preqname, preqtime);
			}

			tfree(preqname);
		}

		++preqp;		/* try next .SUFFIXES rule */
	}


	if (!built)
	{
		/* didn't find anything; try the default rule */
		targp = get_target(".DEFAULT");
		if (targp != NULL)
		{
			add_metas(basename, "", targname);
			build(targp->tshell);
			built = 1;
		}
		else if (targtime == MAXNEGTIME && worry)
			terror(1, tstrcat("Don't know how to make ", targname));
	}

	tfree(basename);

	/* record the current file time */
	if ((filep = get_file(targname)) != NULL)
	{
		filep->ftime = (built == 1 && noexec) ? now
						      : file_time(targname);
	}

	return (built ? built : ((targtime == MAXNEGTIME) ? 0 : -1));
}


/*
 * add_metas	- add symbols for $*, $< and $@
 */
add_metas(basename, preqname, targname)
char   *basename;
char   *preqname;
char   *targname;
{
	add_symbol("*", basename);
	add_symbol("<", preqname);
	add_symbol("@", targname);
}


/*
 * touch_file	- set the MODIFICATION time of the file to NOW
 */
touch_file(targname)
char   *targname;
{
	REGISTER int handle;
#ifndef	MSDOS
	time_t	timep[2];

	time(&timep[0]);
	timep[1] = timep[0];
	handle = utime(targname, timep);
#else
	handle = utime(targname, NULL);
#endif
	fputs("touch ", stdout);
	puts(targname);

	if (handle == 0)
		return;

	/* create the file, if it did not exist */
	if (errno == ENOENT)
	{
		handle = open(targname, O_CREAT | O_TRUNC, S_IWRITE);
		if (handle != -1)
		{
			close(handle);
			return;
		}
	}

	perror("make");
	exit (1);
}


/*
 * build	- process the shell commands
 */
build(shellp)
REGISTER shellptr *shellp;
{
	int	runst;		/* exec return status */
	char   *scmd;		/* command with symbols broken out */
	char   *tcmd;		/* copy of scmd for `tokenize' */
	REGISTER char **argv;	/* argified version of scmd */
	char  **shcp;		/* pointer to shell command list */

	for (;*shellp != NULL;
	     tfree(scmd), tfree(tcmd), tfree(argv), ++shellp)
	{
		/* breakout runtime symbols (e.g. $* $@ $<) */
		scmd = breakout((*shellp)->scmd);

		if (!(*shellp)->s_silent && !silent)
			puts(scmd);

		/* make a copy because tokenize litters '\0's */
		tcmd = tstrcpy(scmd);
		argv = tokenize(tcmd);

		if (noexec)
			continue;

		if (equal(argv[0], "make"))
		{
			/* call ourselves recursively */
			new_make(argv);
			continue;
		}

		/* any indirection MUST be handled by the shell */
		if (!(*shellp)->s_shell && strpbrk(scmd, "<|>"))
			(*shellp)->s_shell = 1;
#ifdef	MSDOS
		/* likewise, check for COMMAND.COM builtin commands */
		shcp = shell_cmds;
		for (; !(*shellp)->s_shell && *shcp; ++shcp)
		{
			if (equal(*shcp, argv[0]))
			{
				(*shellp)->s_shell = 1;
				break;
			}
		}
#endif
		/* run without COMMAND.COM if possible, 'cause it uses RAM */
		if ((*shellp)->s_shell)
			runst = system(scmd);
		else
			runst = spawnvp(P_WAIT, argv[0], argv); 

		if (runst == 0)
			continue;

		/* uh-oh, an error */
		if (runst == -1)
			perror("make");
#ifdef	MSDOS
		terror(0, tstrcat("\007*** Error code ",itoa(runst, scmd, 10)));
#else
		sprintf(scmd, "\007*** Error code %d\n", runst);
		terror(0, scmd);
#endif
		if (!ignore && !(*shellp)->s_ignore)
			exit(1);
	}
}


/*
 * new_make	- save current environment
 *		- call make() recursively (actually main())
 *		- clean up new environment
 *		- restore environment
 */
new_make(argv)
char  **argv;
{
	targptr	thead, tnext;
	fileptr	fhead, fnext;
	symptr	shead, snext;
	shellptr shhead, shnext;
	char  **ttlist;
	long	tnow;
	int	i;

	/* save all the globals */
	thead = target_list;
	fhead = file_list;
	shead = symbol_list;
	shhead = shell_list;
	ttlist = tlist;
	tnow = now;

	/* count the arguments */
	for (i = 0; argv[i]; ++i);

	/* call ourselves recursively; this inherits flags */
	main(i, argv);

	/* we're back, so gotta clean up and dispose of a few things */
	while (target_list)
	{
		tnext = target_list->tnext;
		if (target_list->tpreq)
			tfree(target_list->tpreq);
		if (target_list->tshell)
			tfree(target_list->tshell);
		tfree(target_list);
		target_list = tnext;
	}

	while (file_list)
	{
		fnext = file_list->fnext;
		tfree(file_list->fname);
		tfree(file_list);
		file_list = fnext;
	}

	while (symbol_list)
	{
		snext = symbol_list->snext;
		tfree(symbol_list->sname);
		tfree(symbol_list->svalue);
		tfree(symbol_list);
		symbol_list = snext;
	}

	while (shell_list)
	{
		shnext = shell_list->slink;
		tfree(shell_list->scmd);
		tfree(shell_list);
		shell_list = shnext;
	}

	/* restore our original globals */
	target_list = thead;
	file_list = fhead;
	symbol_list = shead;
	shell_list = shhead;
	tlist = ttlist;
	now = tnow;
}


usage()
{
	puts("make [-f filename] [-dDinrst] [target ...] [macro = value ...]");
	exit(1);
}


display_prereq(targname, targtime, preqname, preqtime)
char   *targname;
long	targtime;
char   *preqname;
long	preqtime;
{
#ifdef	MSDOS
	char	chtime[10];

	fputs(targname, stdout);
	fputs(" (", stdout);
	fputs(ltoa(targtime, chtime, 16), stdout);
	fputs((targtime<=preqtime) ? ") older than ":") newer than ", stdout);
	fputs(preqname, stdout);
	fputs(" (", stdout);
	fputs(ltoa(preqtime, chtime, 16), stdout);
	puts(")");
#else
	printf("%s (%08lx) %s than %s (%08lx)\n",
		targname, targtime, 
		(targtime < preqtime) ? "older" : "newer",
		preqname, preqtime);
#endif
}


long
file_time(fname)
char   *fname;
{
	REGISTER int handle;
#ifdef	MSDOS
	long	ltime;
	unsigned date;
	unsigned time;
#else
	struct stat sbuf;
#endif
	handle = open(fname, O_RDONLY);
	if (handle == -1)
		return (MAXNEGTIME);
#ifdef	MSDOS
	if (_dos_getftime(handle, &date, &time))
		ltime = MAXNEGTIME;
	else
		ltime = (((unsigned long) date) << 15) | (time >> 1);
#else
	fstat(handle, &sbuf);
#endif
	close(handle);
#ifdef	MSDOS
	return (ltime);
#else
	return (sbuf.st_mtime);
#endif
}


/*
 * curr_time	- return current time in same format as file_time()
 *		- used with the `-n' option
 */
long
curr_time()
{
#ifndef	MSDOS
	return (time(NULL));
#else
	struct dosdate_t date;
	struct dostime_t time;

	_dos_getdate(&date);
	_dos_gettime(&time);

	return ((((long) (date.year-1980)<<9 | date.month<<5 | date.day) << 15)
	       | (time.hour << 11 | time.minute << 5 | time.second >> 1) >> 1);
#endif
}

#ifndef	MSDOS
int
spawnvp(mode, path, args)
int	mode;
char   *path;
char  **args;
{
	int	pid = 0;
	union wait waitword;

	if (mode != P_OVERLAY)
		pid = fork();

	if (pid == 0)
		execvp(path, args);

	wait(&waitword);
	return (waitword.w_retcode);
}
#endif
