#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 MAXCMD 1024 #define MAXCMDS 32 /* Maximum number of commands across all files. */ #define MAXDEP 1024 #define MAXPATH 1024 #define MAXTGT 64 extern char *optarg; extern int optind; void cleandep(char **); void cleantgt(char **); char *dirname(char *); char *nextdep(char **); int main(int argc, char *argv[]) { char *b, buf[MAXBUF], *cmd[MAXCMDS], *d, *dep, *name, *tgt, wd[MAXPATH]; 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; /* * The original working directory is saved. (When acting on the * build information in a file, the program temporarily moves into * its directory.) */ getcwd(wd, MAXPATH); /* 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){ /* Return to original directory. */ chdir(wd); if(!(fp = fopen(argv[i], "r"))) err(1, "fopen(%s)", argv[i]); /* Read line by line, at most twenty. */ for(j = 0; j < 20 && fgets(buf, MAXBUF, fp); j++){ buf[strcspn(buf, "\n")] = 0; for(b = buf; *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){ chdir(dirname(argv[i])); goto build; } /* Trim shell meta-characters and whitespace. */ cleantgt(&tgt); cleandep(&dep); dd("%s: target '%s'\n", argv[i], tgt); /* Get information about source. */ if(stat(argv[i], &sb)) err(1, "stat(%s)", argv[i]); /* * Subsequent operations should take place inside the * directory of the source, as command lines and dependency * lines may refer to relative paths. */ chdir(dirname(argv[i])); /* Build immediately if source is newer than target. */ if(stat(tgt, &ssb)){ if(errno == ENOENT) goto build; err(1, "stat(%s)", 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, "stat(%s)", 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 * dirname(char *file) { char *dir; int i; if(!(dir = malloc(strlen(file)+1))) err(1, "malloc"); for(i = strlen(file); i >= 0; i--) if(file[i] == '/') break; for(dir[i--] = 0; i >= 0; i--) dir[i] = file[i]; return dir[0] ? dir : "."; } 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; }