commit 47989f3

shrub  ·  2026-07-03 14:01:55 +0000 UTC
parent 3025f65
handle multiple conflicting submake branches by picking the wanted branch in cases where there is a conflict
9 files changed,  +236, -18
+71, -7
  1@@ -127,11 +127,12 @@ exportenv(const struct Target *t)
  2 static char *
  3 normalizebody(const char *s)
  4 {
  5-	size_t i, n, j;
  6+	size_t i, n, j, cap;
  7 	char *out;
  8 
  9 	n = strlen(s);
 10-	out = xmalloc(n + 1);
 11+	cap = n * 2 + 1;
 12+	out = xmalloc(cap);
 13 	j = 0;
 14 	for (i = 0; i < n; i++) {
 15 		if (s[i] == '\\' && i + 1 < n && s[i + 1] == '\n') {
 16@@ -209,6 +210,60 @@ findrule(struct Rule *rules, size_t n, const char *cmd)
 17 	return 0;
 18 }
 19 
 20+static const struct Target *
 21+wanteddefault(const struct Graph *graph, const char *prefix)
 22+{
 23+	const struct Target *t;
 24+	size_t i;
 25+
 26+	t = defaulttarget(graph, prefix);
 27+	if (t && t->wanted)
 28+		return t;
 29+	for (i = 0; i < graph->n; i++) {
 30+		if (!targetownedby(&graph->v[i], prefix))
 31+			continue;
 32+		if (graph->v[i].wanted)
 33+			return &graph->v[i];
 34+	}
 35+	return 0;
 36+}
 37+
 38+static int
 39+emitgraphsub(const struct Graph *graph, size_t idx)
 40+{
 41+	size_t i;
 42+	int saw_conflict, saw_wanted;
 43+
 44+	saw_conflict = 0;
 45+	saw_wanted = 0;
 46+	for (i = 0; i < graph->nsubs; i++) {
 47+		if (i == idx)
 48+			continue;
 49+		if (strcmp(graph->subs[i].prefix, graph->subs[idx].prefix) != 0)
 50+			continue;
 51+		saw_conflict = 1;
 52+		if (graph->subs[i].wanted)
 53+			saw_wanted = 1;
 54+	}
 55+	if (!saw_conflict)
 56+		return 1;
 57+	if (graph->subs[idx].wanted) {
 58+		for (i = 0; i < idx; i++) {
 59+			if (strcmp(graph->subs[i].prefix, graph->subs[idx].prefix) == 0 &&
 60+			    graph->subs[i].wanted)
 61+				return 0;
 62+		}
 63+		return 1;
 64+	}
 65+	if (saw_wanted)
 66+		return 0;
 67+	for (i = 0; i < idx; i++) {
 68+		if (strcmp(graph->subs[i].prefix, graph->subs[idx].prefix) == 0)
 69+			return 0;
 70+	}
 71+	return 1;
 72+}
 73+
 74 static int
 75 genninjafile(const struct Graph *graph, const char *path, const char *prefix, int root)
 76 {
 77@@ -219,6 +274,8 @@ genninjafile(const struct Graph *graph, const char *path, const char *prefix, in
 78 	size_t nrules;
 79 
 80 	for (i = 0; i < graph->nsubs; i++) {
 81+		if (!emitgraphsub(graph, i))
 82+			continue;
 83 		char *childpath;
 84 
 85 		childpath = joinpath(graph->subs[i].cwd, "build.ninja");
 86@@ -238,10 +295,17 @@ genninjafile(const struct Graph *graph, const char *path, const char *prefix, in
 87 	ruleid = 0;
 88 	rules = 0;
 89 	nrules = 0;
 90-	for (i = 0; i < graph->nsubs; i++)
 91+	for (i = 0; i < graph->nsubs; i++) {
 92+		if (!emitgraphsub(graph, i))
 93+			continue;
 94 		fprintf(fp, "subninja %s/build.ninja\n", graph->subs[i].prefix);
 95-	if (graph->nsubs)
 96-		fputc('\n', fp);
 97+	}
 98+	for (i = 0; i < graph->nsubs; i++) {
 99+		if (emitgraphsub(graph, i)) {
100+			fputc('\n', fp);
101+			break;
102+		}
103+	}
104 	for (i = 0; i < graph->n; i++) {
105 		if (!targetownedby(&graph->v[i], prefix))
106 			continue;
107@@ -295,8 +359,8 @@ genninjafile(const struct Graph *graph, const char *path, const char *prefix, in
108 			fprintf(fp, "\n\n");
109 		}
110 	}
111-	if (root && defaulttarget(graph, prefix))
112-		fprintf(fp, "default %s\n", defaulttarget(graph, prefix)->name);
113+	if (root && wanteddefault(graph, prefix))
114+		fprintf(fp, "default %s\n", wanteddefault(graph, prefix)->name);
115 
116 	fclose(fp);
117 	for (i = 0; i < nrules; i++)
+1, -0
1@@ -431,6 +431,7 @@ main(int argc, char **argv)
2 	if (env_override)
3 		addstr(&sg.flags, "-e");
4 	sg.mode = mode;
5+	sg.wanted = 1;
6 	free(assigns);
7 	free(goals);
8 	assigns = 0;
+18, -2
 1@@ -184,20 +184,29 @@ issubstref(const char *s, size_t n, size_t *colon, size_t *eq)
 2 static ptrdiff_t
 3 findargcomma(const char *s, size_t n)
 4 {
 5-	size_t i, depth;
 6+	size_t i, depth, rawdepth;
 7 
 8 	depth = 0;
 9+	rawdepth = 0;
10 	for (i = 0; i < n; i++) {
11 		if (s[i] == '$' && i + 1 < n && (s[i + 1] == '(' || s[i + 1] == '{')) {
12 			depth++;
13 			i++;
14 			continue;
15 		}
16+		if (s[i] == '(') {
17+			rawdepth++;
18+			continue;
19+		}
20+		if (s[i] == ')' && rawdepth > 0) {
21+			rawdepth--;
22+			continue;
23+		}
24 		if ((s[i] == ')' || s[i] == '}') && depth > 0) {
25 			depth--;
26 			continue;
27 		}
28-		if (s[i] == ',' && depth == 0)
29+		if (s[i] == ',' && depth == 0 && rawdepth == 0)
30 			return (ptrdiff_t)i;
31 	}
32 	return -1;
33@@ -207,15 +216,22 @@ static size_t
34 findclose(const char *s, size_t i, size_t n, char close)
35 {
36 	size_t j, inner;
37+	char open;
38 
39 	j = i + 2;
40 	inner = 1;
41+	open = close == ')' ? '(' : '{';
42 	while (j < n && inner) {
43 		if (s[j] == '$' && j + 1 < n && (s[j + 1] == '(' || s[j + 1] == '{')) {
44 			inner++;
45 			j += 2;
46 			continue;
47 		}
48+		if (s[j] == open) {
49+			inner++;
50+			j++;
51+			continue;
52+		}
53 		if (s[j] == close)
54 			inner--;
55 		j++;
+29, -2
 1@@ -10,6 +10,7 @@
 2 /* implementations of gnu make builtin functions*/
 3 
 4 static char *trimspacesdup(const char *s);
 5+static char *shellquote(const char *s);
 6 
 7 static int
 8 matchword(const char *patterns, const char *word, size_t nword)
 9@@ -71,6 +72,29 @@ matchword(const char *patterns, const char *word, size_t nword)
10 	return 0;
11 }
12 
13+static char *
14+shellquote(const char *s)
15+{
16+	size_t i, len, cap;
17+	char *out;
18+
19+	cap = strlen(s) * 4 + 3;
20+	out = xmalloc(cap);
21+	len = 0;
22+	out[len++] = '\'';
23+	for (i = 0; s[i]; i++) {
24+		if (s[i] == '\'') {
25+			memcpy(out + len, "'\\''", 4);
26+			len += 4;
27+		} else {
28+			out[len++] = s[i];
29+		}
30+	}
31+	out[len++] = '\'';
32+	out[len] = 0;
33+	return out;
34+}
35+
36 char *
37 fnwildcard(const char *patterns)
38 {
39@@ -532,12 +556,15 @@ fninfo(struct EvalCtx *ctx, const char *args)
40 
41 	text = expandstr(ctx, args);
42 	if (ctx->avoid_io && ctx->side_effects) {
43-		char *cmd;
44+		char *cmd, *quoted;
45 
46 		/* this is kinda a hack, but it gets more tests to pass:
47 		 * we tag delayed $(info) output so the test wrapper can distinguish
48 		   synthetic info side effects from real normal echo-having recipes. */
49-		cmd = cat3("echo ", text, " # __shin_info__");
50+		quoted = shellquote(text);
51+		cmd = xmalloc(strlen("printf '%s\\n'  # __shin_info__") + strlen(quoted) + 1);
52+		sprintf(cmd, "printf '%%s\\n' %s # __shin_info__", quoted);
53+		free(quoted);
54 		addstr(ctx->side_effects, cmd);
55 		free(cmd);
56 	} else {
+94, -7
  1@@ -170,6 +170,24 @@ markreachable(const struct Graph *graph, const char *name, unsigned char *seen)
  2 		markreachable(graph, t->order_only.v[i], seen);
  3 }
  4 
  5+static int
  6+goalreaches(const struct Graph *graph, const char *goal, const char *name)
  7+{
  8+	const struct Target *t;
  9+	unsigned char *seen;
 10+	int ok;
 11+
 12+	t = findctarget(graph, name);
 13+	if (!t)
 14+		return 0;
 15+	seen = xmalloc(graph->n ? graph->n : 1);
 16+	memset(seen, 0, graph->n ? graph->n : 1);
 17+	markreachable(graph, goal, seen);
 18+	ok = seen[(size_t)(t - graph->v)] != 0;
 19+	free(seen);
 20+	return ok;
 21+}
 22+
 23 static int
 24 allsubmake(const struct Target *t)
 25 {
 26@@ -351,6 +369,7 @@ mergetarget(struct Graph *graph, const struct Target *src)
 27 	if (!dst->defined && src->defined)
 28 		dst->dcolon = src->dcolon;
 29 	dst->defined |= src->defined;
 30+	dst->wanted |= src->wanted;
 31 	if (src->defined)
 32 		dst->dcolon = src->dcolon;
 33 	addwords(&dst->prereqs, &src->prereqs);
 34@@ -394,18 +413,77 @@ subgraphorder(struct Graph *graph, const char *owner, const struct StrList *prev
 35 	}
 36 }
 37 
 38+static int
 39+sameinvocation(const struct SubGraph *a, const struct SubGraph *b);
 40+
 41 static struct SubGraph *
 42-findgraphsub(struct Graph *graph, const char *prefix)
 43+findsubgraph(struct Graph *graph, const struct SubGraph *want)
 44 {
 45 	size_t i;
 46 
 47 	for (i = 0; i < graph->nsubs; i++) {
 48-		if (sameprefix(graph->subs[i].prefix, prefix))
 49+		if (!sameprefix(graph->subs[i].prefix, want->prefix))
 50+			continue;
 51+		if (sameinvocation(&graph->subs[i], want))
 52 			return &graph->subs[i];
 53 	}
 54 	return 0;
 55 }
 56 
 57+static int
 58+haswantedclash(const struct Graph *graph, const struct SubGraph *want)
 59+{
 60+	size_t i;
 61+
 62+	for (i = 0; i < graph->nsubs; i++) {
 63+		const struct SubGraph *sg;
 64+
 65+		sg = &graph->subs[i];
 66+		if (!sameprefix(sg->prefix, want->prefix))
 67+			continue;
 68+		if (sameinvocation(sg, want))
 69+			continue;
 70+		if (sg->wanted && want->wanted)
 71+			return 1;
 72+	}
 73+	return 0;
 74+}
 75+
 76+static void
 77+markwanted(struct SubGraph *sg)
 78+{
 79+	struct StrList roots;
 80+	unsigned char *reachable;
 81+	size_t i, n;
 82+
 83+	n = sg->graph.n;
 84+	if (!sg->wanted) {
 85+		for (i = 0; i < n; i++)
 86+			sg->graph.v[i].wanted = 0;
 87+		return;
 88+	}
 89+	if (resolvegoals(sg, &roots) < 0)
 90+		return;
 91+	reachable = xmalloc(n ? n : 1);
 92+	memset(reachable, 0, n ? n : 1);
 93+	for (i = 0; i < roots.n; i++)
 94+		markreachable(&sg->graph, roots.v[i], reachable);
 95+	for (i = 0; i < n; i++)
 96+		sg->graph.v[i].wanted = reachable[i] != 0;
 97+	free(reachable);
 98+	freestrs(&roots);
 99+}
100+
101+static void
102+markwantedtree(struct SubGraph *sg)
103+{
104+	size_t i;
105+
106+	markwanted(sg);
107+	for (i = 0; i < sg->graph.nsubs; i++)
108+		markwantedtree(&sg->graph.subs[i]);
109+}
110+
111 static int
112 sameinvocation(const struct SubGraph *a, const struct SubGraph *b)
113 {
114@@ -452,13 +530,13 @@ mergechildgraph(struct SubGraph *parent,
115 	struct Target *t;
116 	size_t i;
117 
118-	known = sameprefix(parent->prefix, child->prefix) ? 0 : findgraphsub(&parent->graph, child->prefix);
119-	if (known && !sameinvocation(known, child)) {
120+	known = sameprefix(parent->prefix, child->prefix) ? 0 : findsubgraph(&parent->graph, child);
121+	if (!sameprefix(parent->prefix, child->prefix) && haswantedclash(&parent->graph, child)) {
122 		fprintf(stderr, "multiple incompatible subgraphs map to %s/build.ninja\n",
123 		        child->prefix ? child->prefix : ".");
124 		return -1;
125 	}
126-	if (!known) {
127+	if (!known && child->wanted) {
128 		reachable = xmalloc(child->graph.n ? child->graph.n : 1);
129 		memset(reachable, 0, child->graph.n ? child->graph.n : 1);
130 		for (i = 0; i < goals->n; i++)
131@@ -479,6 +557,11 @@ mergechildgraph(struct SubGraph *parent,
132 		return -1;
133 	}
134 	for (i = 0; i < goals->n; i++) {
135+		/* the child graph might have a default goal like "lib/all" to build
136+		 * the parent. if we add that goal back as a prerequisite of
137+		 * the parent target would create a synthetic cycle; dont do that. */
138+		if (goalreaches(&child->graph, goals->v[i], tname))
139+			continue;
140 		if (!hasword(&t->prereqs, goals->v[i]))
141 			addstr(&t->prereqs, goals->v[i]);
142 	}
143@@ -512,6 +595,7 @@ expandsubgraphs(struct SubGraph *sg, struct SubGraphStack *stack)
144 	unsigned char *reachable;
145 	size_t i, n;
146 
147+	markwanted(sg);
148 	if (resolvegoals(sg, &roots) < 0)
149 		return -1;
150 	for (i = 0; i < sg->graph.n; i++) {
151@@ -554,6 +638,7 @@ expandsubgraphs(struct SubGraph *sg, struct SubGraphStack *stack)
152 			child.cwd = r->sm.dir ? joinpath(sg->cwd, r->sm.dir) : xstrdup(sg->cwd);
153 			child.prefix = joinprefix(sg->prefix, r->sm.dir);
154 			child.mode = sg->mode;
155+			child.wanted = sg->graph.v[i].wanted;
156 			addstr(&child.parents, tname);
157 			if (r->sm.makefile)
158 				child.makefile = xstrdup(r->sm.makefile);
159@@ -581,7 +666,7 @@ expandsubgraphs(struct SubGraph *sg, struct SubGraphStack *stack)
160 			if (rc > 0) {
161 				struct SubGraph *known;
162 
163-				known = sameprefix(sg->prefix, child.prefix) ? 0 : findgraphsub(&sg->graph, child.prefix);
164+				known = sameprefix(sg->prefix, child.prefix) ? 0 : findsubgraph(&sg->graph, &child);
165 				if (known && sameinvocation(known, &child) && child.goals.n > 0) {
166 					size_t gi;
167 					struct Target *pt;
168@@ -722,7 +807,7 @@ buildsubgraph0(struct SubGraph *sg, struct SubGraphStack *stack)
169 	rc = eval(path, &ast, sg->assigns.n ? &pre : 0, envoverride, sg->mode, &rs);
170 	if (rc < 0)
171 		goto out;
172-	rc = buildgraph(&rs, &sg->goals, &sg->graph, sg->mode);
173+	rc = buildgraph(&rs, sg->wanted ? &sg->goals : 0, &sg->graph, sg->mode);
174 	if (rc < 0)
175 		goto out;
176 	rc = expandgraph(&sg->graph, sg->mode);
177@@ -730,6 +815,8 @@ buildsubgraph0(struct SubGraph *sg, struct SubGraphStack *stack)
178 		goto out;
179 	scopegraph(&sg->graph, sg->prefix);
180 	rc = expandsubgraphs(sg, stack);
181+	if (rc >= 0)
182+		markwantedtree(sg);
183 out:
184 	freeruleset(&rs);
185 	freeast(&pre);
+2, -0
 1@@ -225,6 +225,7 @@ struct Target {
 2 	int dcolon;
 3 	int defined;
 4 	int phony;
 5+	int wanted;
 6 };
 7 
 8 struct SubGraph;
 9@@ -247,6 +248,7 @@ struct SubGraph {
10 	struct StrList goals;
11 	struct Graph graph;
12 	enum ShinMode mode;
13+	int wanted;
14 };
15 
16 int preproc(const char *path, struct Pre *pre, enum ShinMode mode);
+4, -0
1@@ -0,0 +1,4 @@
2+X = $(if ,ok,echo "(skip)")
3+
4+all:
5+	@echo $(X)
+1, -0
1@@ -0,0 +1 @@
2+echo (skip)
+16, -0
 1@@ -0,0 +1,16 @@
 2+{
 3+  "case": "t004",
 4+  "category": "variables",
 5+  "compare_output": true,
 6+  "description": "function arguments preserve balanced literal parentheses",
 7+  "details": "",
 8+  "env": {},
 9+  "expected_exit": 0,
10+  "options": "",
11+  "options_mode": "argv",
12+  "output_mode": "exact",
13+  "setup": [],
14+  "stdin": "",
15+  "suite": "shin",
16+  "timeout_seconds": 60
17+}