#include #include #include #include #include #include #include #include #include #define USAGE "usage: %s file [...]\n", name #define d(...) do { if (dflag > 0) \ fprintf(stderr, __VA_ARGS__); } while (0); #define dd(...) do { if (dflag > 1) \ fprintf(stderr, __VA_ARGS__); } while (0); #define ddd(...) do { if (dflag > 2) \ fprintf(stderr, __VA_ARGS__); } while (0); #define MAXBUF 1024 #define MAXCMDS 32 /* Maximum number of commands across all files. */ #define MAXCMD 1024 #define MAXDEP 1024 #define MAXTGT 64 extern char *optarg; extern int optind; void cleandep(char **); void cleantgt(char **); char *nextdep(char **); int main(int argc, char *argv[]) { char *b, buf[MAXBUF], *cmd[MAXCMDS], *d, *dep, *name, *tgt; FILE *fp; int c, dflag, fflag, i, icmd, j, s; struct stat sb, ssb; /* Allocate memory. */ for (i = 0; i < MAXCMDS; i++) if (!(cmd[i] = malloc(MAXCMD))) err(1, "malloc"); if (!(dep = malloc(MAXDEP))) err(1, "malloc"); if (!(tgt = malloc(MAXTGT))) err(1, "malloc"); tgt[0] = dep[0] = 0; /* Process command-line flags (debug, force). */ name = argv[0]; dflag = fflag = 0; while ((c = getopt(argc, argv, "df")) != -1) switch (c) { case 'd': dflag++; break; case 'f': fflag = 1; break; default: fprintf(stderr, USAGE); return 1; } argc -= optind; argv += optind; if (argc == 0) { fprintf(stderr, USAGE); return 1; } /* Process dependencies and commands in each file. */ for (i = icmd = 0; i < argc; i++, icmd = 0) { if (!(fp = fopen(argv[i], "r"))) err(1, "fopen"); /* Read line by line, at most twenty. */ for (j = 0; j < 20 && fgets(buf, MAXBUF, fp); j++) { buf[strcspn(buf, "\n")] = 0; b = buf; for (; *b; b++) { /* Find command line. */ if (strncmp(b, " $ ", 3) == 0 || strncmp(b, " $ ", 3) == 0) { strncpy(cmd[icmd++], b+3, MAXBUF-1); /* Find target inside command. */ for (b = b+3; *b; b++) { if (!(*b+1)) continue; if (*b != '>') continue; strncpy(tgt, b+1, MAXTGT-1); } ddd("%s: command line '%s'\n", argv[i], buf); continue; } /* Find dependency line. */ if (strncmp(b, " % ", 3) == 0 || strncmp(b, " % ", 3) == 0) { strncpy(dep, b+3, MAXDEP-1); ddd("%s: dependency line '%s'\n", argv[i], buf); continue; } } } if (!icmd) { fprintf(stderr, "%s: no command line found\n", argv[i]); goto next; } /* Build immediately if forced or no target found. */ if (fflag || !*tgt) goto build; /* Trim shell meta-characters and whitespace. */ cleantgt(&tgt); cleandep(&dep); /* Build immediately if source is newer than target. */ dd("%s: target '%s'\n", argv[i], tgt); if (stat(argv[i], &sb)) err(1, argv[i]); if (stat(tgt, &ssb)) { if (errno == ENOENT) goto build; err(1, tgt); } if (sb.st_mtime > ssb.st_mtime) { d("%s: %s is modified, building\n", argv[i], argv[i]); goto build; } /* * If target is newer than source and there are no * dependencies, the target is up-to-date. */ if (!*dep) goto uptodate; /* Build immediately if any dependency is newer than target. */ while (d = nextdep(&dep)) { dd("%s: depend '%s'\n", argv[i], d); if (stat(d, &sb)) { if (errno == ENOENT) { fprintf(stderr, "%s: dependency %s " "does not exist\n", argv[i], d); continue; } err(1, d); } if (sb.st_mtime > ssb.st_mtime) { d("%s: %s is modified, building\n", argv[i], d); free(d); goto build; } free(d); } uptodate: /* * As neither the source or the dependencies are newer * than the target, the target is up-to-date. */ fprintf(stderr, "%s: already up-to-date\n", argv[i]); goto next; build: /* * All commands found in the file are concatenated to a * single string, separated by newlines, which is passed * to system(1). The -e option makes the shell exit * whenever a command fails. The -x option makes the shell * print executed commands. */ buf[0] = 0; strcat(buf, "set -ex\n"); for (j = 0; j < icmd; j++) { strncat(buf, cmd[j], MAXBUF-1); strncat(buf, "\n", MAXBUF-1); } s = system(buf); /* Process next file if shell command succeeded. */ if (s == 0) goto next; /* * As the shell command was unsuccessful, the return value * of system(1) is processed using the macros defined in * and printed to the user. The program exits * with a positive status. */ if (WIFEXITED(s)) { fprintf(stderr, "%s: exited with %d\n", argv[i], WEXITSTATUS(s)); exit(WEXITSTATUS(s)); } else if (WIFSIGNALED(s)) { fprintf(stderr, "%s: terminated by signal %d\n", argv[i], WTERMSIG(s)); exit(1); } next: fclose(fp); } } void cleandep(char **dep) { for (; **dep; ++*dep) if (!isspace(**dep)) break; } void cleantgt(char **tgt) { char *t; for (; **tgt; ++*tgt) if (!isspace(**tgt)) break; for (t = *tgt; *t; t++) if (isspace(*t) || *t == '|' || *t == '&' || *t == ';' || *t == ')') { *t = 0; break; } } char * nextdep(char **dep) { char *d; int i; /* Read dependency string character-by-character. */ for (i = 0; (*dep)[i]; i++) { /* * Upon encountering a space or the final character, the * hitherto gathered string is stored in a copy. */ if (isspace((*dep)[i]) || (*dep)[i+1] == 0 && i++) { d = strdup(*dep); d[i] = 0; goto found; } } /* * The dependency string has a length of zero, meaning that the * processing is completed. */ return NULL; found: /* * The original dependency string is incremented until the next * dependency. */ for (; (*dep)[i]; i++) if (!isspace((*dep)[i])) break; *dep += i; return d; }