commit 4f51eb3

shrub  ·  2026-04-20 18:31:41 +0000 UTC
parent 088da53
keep target env so we can do lazy recipe expansion, and lower info into proper ninja commands. also some wrapper plumbing to preserve gmake semantics. +380 test passes
11 files changed,  +211, -58
+1, -1
1@@ -175,7 +175,7 @@ genninjafile(const struct Graph *graph, const char *path, const char *prefix, in
2 					fprintf(fp, " %s", graph->v[i].order_only.v[j]);
3 			}
4 			fprintf(fp, "\n\n");
5-		} else if (graph->v[i].prereqs.n > 0 || graph->v[i].order_only.n > 0) {
6+		} else if (graph->v[i].defined || graph->v[i].prereqs.n > 0 || graph->v[i].order_only.n > 0) {
7 			fprintf(fp, "build %s: phony", graph->v[i].name);
8 			for (j = 0; j < graph->v[i].prereqs.n; j++)
9 				fprintf(fp, " %s", graph->v[i].prereqs.v[j]);
+6, -0
 1@@ -357,6 +357,12 @@ main(int argc, char **argv)
 2 		freesubgraph(&sg);
 3 		return 1;
 4 	}
 5+	if (expandgraph(&sg.graph) < 0) {
 6+		fprintf(stderr, "expand error in %s\n", path ? path : "(default)");
 7+		free(pathbuf);
 8+		freesubgraph(&sg);
 9+		return 1;
10+	}
11 	if (dump_graph)
12 		dumpgraph(&sg.graph);
13 	if (gen == GEN_DOT) {
+1, -1
1@@ -325,7 +325,7 @@ static const struct func funcs[] = {
2     {"wildcard", FNEXP1, {.f1 = fnwildcard}},
3     {"shell", FNEXP1, {.f1 = fnshell}},
4     {"sort", FNEXP1, {.f1 = fnsort}},
5-    {"info", FNEXP1, {.f1 = fninfo}},
6+    {"info", FNCTX, {.ctx = fninfo}},
7     {"notdir", FNEXP1, {.f1 = fnnotdir}},
8     {"dir", FNEXP1, {.f1 = fndir}},
9     {"basename", FNEXP1, {.f1 = fnbasename}},
+15, -3
 1@@ -421,10 +421,22 @@ fnsort(const char *text)
 2 }
 3 
 4 char *
 5-fninfo(const char *text)
 6+fninfo(struct EvalCtx *ctx, const char *args)
 7 {
 8-	fputs(text, stderr);
 9-	fputc('\n', stderr);
10+	char *text;
11+
12+	text = expandstr(ctx, args);
13+	if (ctx->avoid_io && ctx->side_effects) {
14+		char *cmd;
15+
16+		cmd = cat3("echo ", text, "");
17+		addstr(ctx->side_effects, cmd);
18+		free(cmd);
19+	} else {
20+		fputs(text, stdout);
21+		fputc('\n', stdout);
22+	}
23+	free(text);
24 	return xstrdup("");
25 }
26 
+4, -6
 1@@ -192,13 +192,9 @@ instpatrule(const struct PatRules *rules, const struct Graph *graph, struct Targ
 2 			t->order_only.v[t->order_only.n++] = s;
 3 		}
 4 		for (j = 0; j < rules->v[i].recipes.n; j++) {
 5-			char *s, *vars, *exp;
 6+			char *exp;
 7 
 8-			s = applystem(rules->v[i].recipes.v[j].body, stem);
 9-			vars = expandstr(ctx, s);
10-			exp = expandstem(vars, stem);
11-			free(vars);
12-			free(s);
13+			exp = expandstem(rules->v[i].recipes.v[j].body, stem);
14 			if (!exp[0]) {
15 				free(exp);
16 				continue;
17@@ -212,6 +208,8 @@ instpatrule(const struct PatRules *rules, const struct Graph *graph, struct Targ
18 			copysubmake(&t->recipes.v[t->recipes.n].sm, &rules->v[i].recipes.v[j].sm);
19 			t->recipes.n++;
20 		}
21+		freeenv(&t->env);
22+		copyenv(&t->env, ctx->env);
23 		free(stem);
24 		return 1;
25 	}
+75, -24
  1@@ -14,6 +14,7 @@ struct TAssign {
  2 	char *lhs;
  3 	char *rhs;
  4 	enum AssignOp op;
  5+	enum Origin origin;
  6 };
  7 
  8 struct GraphState {
  9@@ -70,29 +71,6 @@ targetmatches(const char *pat, const char *name)
 10 	return patmatches(pat, name);
 11 }
 12 
 13-static void
 14-addexprecipes(struct RecipeList *dest, const struct RecipeList *src, struct EvalCtx *ctx)
 15-{
 16-	size_t i;
 17-
 18-	for (i = 0; i < src->n; i++) {
 19-		char *exp;
 20-
 21-		exp = expandstr(ctx, src->v[i].body);
 22-		if (!exp[0]) {
 23-			free(exp);
 24-			continue;
 25-		}
 26-		dest->v = xrealloc(dest->v, (dest->n + 1) * sizeof(dest->v[0]));
 27-		dest->v[dest->n].body = exp;
 28-		dest->v[dest->n].silent = src->v[i].silent;
 29-		dest->v[dest->n].ignore = src->v[i].ignore;
 30-		dest->v[dest->n].recursive = src->v[i].recursive;
 31-		dest->v[dest->n].submake = src->v[i].submake;
 32-		copysubmake(&dest->v[dest->n].sm, &src->v[i].sm);
 33-		dest->n++;
 34-	}
 35-}
 36 
 37 /* we apply target assignments from the graphstate (gs.tas) to those specific
 38  * targets, for some target like
 39@@ -114,6 +92,7 @@ applytassigns(struct GraphState *gs, struct EvalCtx *ctx, const char *name)
 40 			in.lhs = gs->tas[i].lhs;
 41 			in.rhs = gs->tas[i].rhs;
 42 			in.op = gs->tas[i].op;
 43+			in.origin = gs->tas[i].origin;
 44 			evalassign(ctx, &in);
 45 		}
 46 	}
 47@@ -156,7 +135,9 @@ addrule(struct GraphState *gs, const char *name, const struct RuleNode *rule)
 48 	addwords(&t->prereqs, &rule->prereqs);
 49 	addwords(&t->order_only, &rule->order_only);
 50 	targetenv(gs, &ctx, name);
 51-	addexprecipes(&t->recipes, &rule->recipes, &ctx);
 52+	addrecipes(&t->recipes, &rule->recipes);
 53+	freeenv(&t->env);
 54+	copyenv(&t->env, &env);
 55 	freeenv(&env);
 56 
 57 	for (i = 0; i < rule->prereqs.n; i++) {
 58@@ -182,6 +163,7 @@ addtassign(struct GraphState *gs, const struct AssignNode *assign)
 59 	ta->lhs = xstrdup(assign->lhs);
 60 	ta->rhs = xstrdup(assign->rhs);
 61 	ta->op = assign->op;
 62+	ta->origin = assign->origin;
 63 }
 64 
 65 /*generate the graph from the evaluated rule set
 66@@ -295,6 +277,74 @@ buildgraph(const struct RuleSet *ruleset, struct Graph *graph)
 67 	return ctx.errors ? -1 : 0;
 68 }
 69 
 70+int
 71+expandgraph(struct Graph *graph)
 72+{
 73+	size_t i, k, j;
 74+
 75+	for (i = 0; i < graph->n; i++) {
 76+		struct Target *t = &graph->v[i];
 77+		struct EvalCtx ctx;
 78+		struct StrList side_effects;
 79+		struct RecipeList new_recipes;
 80+
 81+		if (!t->env.n) {
 82+			freeenv(&t->env);
 83+			continue;
 84+		}
 85+		memset(&ctx, 0, sizeof(ctx));
 86+		memset(&side_effects, 0, sizeof(side_effects));
 87+		memset(&new_recipes, 0, sizeof(new_recipes));
 88+		ctx.env = &t->env;
 89+		ctx.avoid_io = 1;
 90+		ctx.side_effects = &side_effects;
 91+
 92+		for (k = 0; k < t->recipes.n; k++) {
 93+			struct Recipe *r = &t->recipes.v[k];
 94+			char *exp;
 95+
 96+			freestrs(&side_effects);
 97+			exp = expandstr(&ctx, r->body);
 98+			for (j = 0; j < side_effects.n; j++) {
 99+				struct Recipe *nr;
100+
101+				new_recipes.v = xrealloc(new_recipes.v,
102+				    (new_recipes.n + 1) * sizeof(new_recipes.v[0]));
103+				nr = &new_recipes.v[new_recipes.n++];
104+				memset(nr, 0, sizeof(*nr));
105+				nr->body = xstrdup(side_effects.v[j]);
106+				nr->silent = r->silent;
107+				nr->ignore = r->ignore;
108+			}
109+			if (exp[0]) {
110+				struct Recipe *nr;
111+
112+				new_recipes.v = xrealloc(new_recipes.v,
113+				    (new_recipes.n + 1) * sizeof(new_recipes.v[0]));
114+				nr = &new_recipes.v[new_recipes.n++];
115+				*nr = *r;
116+				nr->body = exp;
117+				free(r->body);
118+				r->body = 0;
119+				memset(&r->sm, 0, sizeof(r->sm));
120+			} else {
121+				free(exp);
122+			}
123+		}
124+
125+		freestrs(&side_effects);
126+		freerecipes(&t->recipes);
127+		t->recipes = new_recipes;
128+		freeenv(&t->env);
129+	}
130+
131+	for (i = 0; i < graph->nsubs; i++) {
132+		if (expandgraph(&graph->subs[i].graph) < 0)
133+			return -1;
134+	}
135+	return 0;
136+}
137+
138 void
139 freegraph(struct Graph *graph)
140 {
141@@ -308,6 +358,7 @@ freegraph(struct Graph *graph)
142 		freestrs(&graph->v[i].prereqs);
143 		freestrs(&graph->v[i].order_only);
144 		freerecipes(&graph->v[i].recipes);
145+		freeenv(&graph->v[i].env);
146 	}
147 	for (i = 0; i < graph->nsubs; i++)
148 		freesubgraph(&graph->subs[i]);
+3, -13
 1@@ -3,18 +3,6 @@
 2 
 3 #include "shinobi.h"
 4 
 5-struct Var {
 6-	char *name;
 7-	char *val;
 8-	int simple;
 9-	enum Origin origin;
10-};
11-
12-struct Env {
13-	struct Var *v;
14-	size_t n;
15-};
16-
17 struct CallFrame {
18 	char **args;
19 	size_t nargs;
20@@ -26,6 +14,8 @@ struct EvalCtx {
21 	struct RuleSet *out;
22 	struct CallFrame *call;
23 	int errors;
24+	int avoid_io;
25+	struct StrList *side_effects;
26 };
27 
28 void *xmalloc(size_t n);
29@@ -72,7 +62,7 @@ char *fnfilterout(const char *patterns, const char *text);
30 char *fnaddprefix(const char *prefix, const char *names);
31 char *fnaddsuffix(const char *suffix, const char *names);
32 char *fnsort(const char *text);
33-char *fninfo(const char *text);
34+char *fninfo(struct EvalCtx *ctx, const char *args);
35 char *fnnotdir(const char *names);
36 char *fndir(const char *names);
37 char *fnbasename(const char *names);
+13, -2
 1@@ -733,10 +733,21 @@ parseline(const struct PreLine *line)
 2 	if (colon >= 0 && as.ok && (size_t)colon < as.pos) {
 3 		/* some inline rule like 'all: ; @echo hi' */
 4 		size_t off = dcolon ? 2 : 1;
 5+		size_t base = (size_t)colon + off;
 6+		int assign_override = is_override;
 7 		ptrdiff_t semi = findtop(trim + colon + off, as.pos - (size_t)colon - off, ';');
 8+
 9+		while (base < n && isspace((unsigned char)trim[base]))
10+			base++;
11+		if (haskw(trim + base, "override")) {
12+			base += 8;
13+			while (base < n && isspace((unsigned char)trim[base]))
14+				base++;
15+			assign_override = 1;
16+		}
17 		if (semi < 0) {
18-			state = parseassign(line, trim, n, (size_t)colon + 1, as, 1, (size_t)colon);
19-			if (is_override)
20+			state = parseassign(line, trim, n, base, as, 1, (size_t)colon);
21+			if (assign_override)
22 				state.data.assign.origin = ORIGIN_OVERRIDE;
23 			free(trim);
24 			return state;
+8, -8
 1@@ -424,11 +424,9 @@ instsufrule(const struct SufRules *rules,
 2 				t->prereqs.v[0] = src;
 3 				t->prereqs.n++;
 4 				for (k = 0; k < rules->sufs[r].recipes.n; k++) {
 5-					char *vars, *exp;
 6+					char *exp;
 7 
 8-					vars = expandstr(ctx, rules->sufs[r].recipes.v[k].body);
 9-					exp = expandauto(vars, t, stem);
10-					free(vars);
11+					exp = expandauto(rules->sufs[r].recipes.v[k].body, t, stem);
12 					if (!exp[0]) {
13 						free(exp);
14 						continue;
15@@ -442,6 +440,8 @@ instsufrule(const struct SufRules *rules,
16 					copysubmake(&t->recipes.v[t->recipes.n].sm, &rules->sufs[r].recipes.v[k].sm);
17 					t->recipes.n++;
18 				}
19+				freeenv(&t->env);
20+				copyenv(&t->env, ctx->env);
21 				free(stem);
22 				return 1;
23 			}
24@@ -468,11 +468,9 @@ instsufrule(const struct SufRules *rules,
25 			t->prereqs.v[0] = src;
26 			t->prereqs.n++;
27 			for (k = 0; k < rules->singlesuf[j].recipes.n; k++) {
28-				char *vars, *exp;
29+				char *exp;
30 
31-				vars = expandstr(ctx, rules->singlesuf[j].recipes.v[k].body);
32-				exp = expandauto(vars, t, t->name);
33-				free(vars);
34+				exp = expandauto(rules->singlesuf[j].recipes.v[k].body, t, t->name);
35 				if (!exp[0]) {
36 					free(exp);
37 					continue;
38@@ -486,6 +484,8 @@ instsufrule(const struct SufRules *rules,
39 				copysubmake(&t->recipes.v[t->recipes.n].sm, &rules->singlesuf[j].recipes.v[k].sm);
40 				t->recipes.n++;
41 			}
42+			freeenv(&t->env);
43+			copyenv(&t->env, ctx->env);
44 			return 1;
45 		}
46 	}
+14, -0
 1@@ -159,12 +159,25 @@ struct RuleSet {
 2 	size_t nrules;
 3 };
 4 
 5+struct Var {
 6+	char *name;
 7+	char *val;
 8+	int simple;
 9+	enum Origin origin;
10+};
11+
12+struct Env {
13+	struct Var *v;
14+	size_t n;
15+};
16+
17 struct Target {
18 	char *name;
19 	char *owner;
20 	struct StrList prereqs;
21 	struct StrList order_only;
22 	struct RecipeList recipes;
23+	struct Env env;
24 	int dcolon;
25 	int defined;
26 };
27@@ -197,6 +210,7 @@ int parse(const char *path, const char *src, struct Ast *ast);
28 void freeast(struct Ast *ast);
29 void freeruleset(struct RuleSet *ruleset);
30 int buildgraph(const struct RuleSet *ruleset, struct Graph *graph);
31+int expandgraph(struct Graph *graph);
32 void freegraph(struct Graph *graph);
33 int buildsubgraph(struct SubGraph *sg);
34 void freesubgraph(struct SubGraph *sg);
+71, -0
 1@@ -63,6 +63,35 @@ probemk() {
 2 	return 1
 3 }
 4 
 5+default_target() {
 6+	sed -n 's/^default //p' build.ninja 2>/dev/null | sed -n '1p'
 7+}
 8+
 9+uptodate_target() {
10+	if [ -n "$targets" ]; then
11+		printf '%s\n' "$targets" | sed -n '1p'
12+	else
13+		default_target
14+	fi
15+}
16+
17+delayed_only() {
18+	printf '%s\n' "$1" | while IFS= read -r line; do
19+		case $line in
20+		'') continue ;;
21+		'['*'] '*) line=${line#*] } ;;
22+		esac
23+		case $line in
24+		echo\ *|echo\ -e\ *)
25+			:
26+			;;
27+		*)
28+			return 1
29+			;;
30+		esac
31+	done
32+}
33+
34 while [ "$#" -gt 0 ]; do
35 	case "$1" in
36 	-v|--version)
37@@ -185,4 +214,46 @@ if [ -n "$targets" ]; then
38 	IFS=$oldifs
39 fi
40 
41+if [ "$dryrun" -eq 0 ]; then
42+	probe_cmd="$ninja_bin -f build.ninja"
43+	if [ -n "${jobs}" ]; then
44+		probe_cmd="$probe_cmd -j $jobs"
45+	fi
46+	oldifs=$IFS
47+	IFS='
48+'
49+	for target in $targets; do
50+		probe_cmd="$probe_cmd '$target'"
51+	done
52+	IFS=$oldifs
53+	probe_out=$(eval "$probe_cmd -n" 2>&1)
54+	probe_status=$?
55+	if [ "$probe_status" -ne 0 ]; then
56+		printf '%s' "$probe_out"
57+		exit "$probe_status"
58+	fi
59+	if [ -z "$probe_out" ]; then
60+		target=$(uptodate_target)
61+		if [ -n "$target" ]; then
62+			printf "%s: '%s' is up to date.\n" "${0##*/}" "$target"
63+			exit 0
64+		fi
65+	fi
66+	if delayed_only "$probe_out"; then
67+		run_out=$("$@" 2>&1)
68+		run_status=$?
69+		if [ -n "$run_out" ]; then
70+			printf '%s\n' "$run_out"
71+		fi
72+		if [ "$run_status" -ne 0 ]; then
73+			exit "$run_status"
74+		fi
75+		target=$(uptodate_target)
76+		if [ -n "$target" ]; then
77+			printf "%s: '%s' is up to date.\n" "${0##*/}" "$target"
78+		fi
79+		exit 0
80+	fi
81+fi
82+
83 NINJA_STATUS= exec "$@"