commit a98ab69
shrub
·
2026-05-06 09:04:26 +0000 UTC
parent 75ca578
handle 'export var' and fix test wrapper regression
11 files changed,
+262,
-95
M
README
+1,
-1
1@@ -17,6 +17,6 @@ if you want to help out, there's lots to do. feel free to make a
2 contibution. some high-priority tasks are listed in the TODO file.
3
4 it is implemented in C99. the perl in this repo is just the gnu make
5-test suite (of which 2613/3865 tests pass, at the time of writing)
6+test suite (of which 2606/3865 tests pass, at the time of writing)
7
8 this is public domain software
+23,
-2
1@@ -5,7 +5,23 @@
2 #include <stdlib.h>
3 #include <string.h>
4
5-/* ninja backend */
6+/* check so we dont export special vars */
7+static int
8+isvarname(const char *s)
9+{
10+ size_t i;
11+
12+ if (!s || !s[0])
13+ return 0;
14+ for (i = 0; s[i]; i++) {
15+ if (!(('a' <= s[i] && s[i] <= 'z') ||
16+ ('A' <= s[i] && s[i] <= 'Z') ||
17+ ('0' <= s[i] && s[i] <= '9') ||
18+ s[i] == '_'))
19+ return 0;
20+ }
21+ return 1;
22+}
23
24 struct Rule {
25 char *cmd;
26@@ -58,6 +74,8 @@ exportenv(const struct Target *t)
27
28 if (!t->env.v[i].exported)
29 continue;
30+ if (!isvarname(t->env.v[i].name))
31+ continue;
32 quoted = shellquote(t->env.v[i].val);
33 nname = strlen(t->env.v[i].name);
34 nquoted = strlen(quoted);
35@@ -93,7 +111,10 @@ joinrecipes(const struct Target *t)
36 for (i = 0; i < t->recipes.n; i++) {
37 char *body;
38
39- body = cat3("( ", t->recipes.v[i].body, " )");
40+ if (t->recipes.n > 1)
41+ body = cat3("( ", t->recipes.v[i].body, " )");
42+ else
43+ body = xstrdup(t->recipes.v[i].body);
44 bodies.v = xrealloc(bodies.v, (bodies.n + 1) * sizeof(bodies.v[0]));
45 bodies.v[bodies.n++] = body;
46 }
+64,
-64
1@@ -242,12 +242,12 @@ main(int argc, char **argv)
2 } else if (strcmp(argv[i], "-g") == 0) {
3 dump_graph = 1;
4 } else if (strcmp(argv[i], "-G") == 0) {
5- if (i + 1 >= argc) {
6- fprintf(stderr, "specify a generator\n\n");
7- usage(stderr, argv[0]);
8- free(assigns);
9- return 2;
10- }
11+ if (i + 1 >= argc) {
12+ fprintf(stderr, "specify a generator\n\n");
13+ usage(stderr, argv[0]);
14+ free(assigns);
15+ return 2;
16+ }
17 ++i;
18 if (strcmp(argv[i], "ninja") == 0) {
19 gen = GEN_NINJA;
20@@ -255,61 +255,61 @@ main(int argc, char **argv)
21 gen = GEN_DOT;
22 } else if (strcmp(argv[i], "compcmd") == 0) {
23 gen = GEN_COMPCMD;
24- } else {
25- fprintf(stderr, "unknown generator: %s\n\n", argv[i]);
26- usage(stderr, argv[0]);
27- free(assigns);
28- return 2;
29- }
30+ } else {
31+ fprintf(stderr, "unknown generator: %s\n\n", argv[i]);
32+ usage(stderr, argv[0]);
33+ free(assigns);
34+ return 2;
35+ }
36 } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
37 usage(stdout, argv[0]);
38 free(assigns);
39 return 0;
40- } else if (strcmp(argv[i], "-C") == 0) {
41- if (i + 1 >= argc) {
42- fprintf(stderr, "specify a directory\n\n");
43- usage(stderr, argv[0]);
44- free(assigns);
45- return 2;
46- }
47+ } else if (strcmp(argv[i], "-C") == 0) {
48+ if (i + 1 >= argc) {
49+ fprintf(stderr, "specify a directory\n\n");
50+ usage(stderr, argv[0]);
51+ free(assigns);
52+ return 2;
53+ }
54 ++i;
55
56- if (chdir(argv[i]) != 0) {
57- fprintf(stderr, "failed to chdir to %s", argv[i]);
58- free(assigns);
59- return 2;
60- }
61- } else if (strcmp(argv[i], "-f") == 0) {
62- if (i + 1 >= argc) {
63- fprintf(stderr, "specify a file\n\n");
64- usage(stderr, argv[0]);
65- free(assigns);
66- return 2;
67- }
68+ if (chdir(argv[i]) != 0) {
69+ fprintf(stderr, "failed to chdir to %s", argv[i]);
70+ free(assigns);
71+ return 2;
72+ }
73+ } else if (strcmp(argv[i], "-f") == 0) {
74+ if (i + 1 >= argc) {
75+ fprintf(stderr, "specify a file\n\n");
76+ usage(stderr, argv[0]);
77+ free(assigns);
78+ return 2;
79+ }
80 ++i;
81 path = argv[i];
82 } else if (strcmp(argv[i], "-e") == 0) {
83 env_override = 1;
84- } else if (isvarassign(argv[i])) {
85- char **tmp;
86+ } else if (isvarassign(argv[i])) {
87+ char **tmp;
88
89- tmp = realloc(assigns, (nassigns + 1) * sizeof(assigns[0]));
90- if (!tmp) {
91- fprintf(stderr, "out of memory\n");
92- free(assigns);
93- return 2;
94- }
95- assigns = tmp;
96- assigns[nassigns++] = argv[i];
97- } else if (argv[i][0] == '-') {
98- usage(stderr, argv[0]);
99- free(assigns);
100- return 2;
101- } else {
102- usage(stderr, argv[0]);
103+ tmp = realloc(assigns, (nassigns + 1) * sizeof(assigns[0]));
104+ if (!tmp) {
105+ fprintf(stderr, "out of memory\n");
106 free(assigns);
107 return 2;
108 }
109+ assigns = tmp;
110+ assigns[nassigns++] = argv[i];
111+ } else if (argv[i][0] == '-') {
112+ usage(stderr, argv[0]);
113+ free(assigns);
114+ return 2;
115+ } else {
116+ usage(stderr, argv[0]);
117+ free(assigns);
118+ return 2;
119+ }
120 }
121 for (i = 0; i < (int)nassigns; i++)
122 addstr(&sg.assigns, assigns[i]);
123@@ -317,15 +317,15 @@ main(int argc, char **argv)
124 char *mkpath;
125
126 mkpath = 0;
127- if (loadmakefile(path, &mkpath, &src) < 0) {
128- if (path)
129- fprintf(stderr, "could not read %s\n", path);
130- else
131- fprintf(stderr, "could not find a makefile\n");
132- free(assigns);
133- freesubgraph(&sg);
134- return 2;
135- }
136+ if (loadmakefile(path, &mkpath, &src) < 0) {
137+ if (path)
138+ fprintf(stderr, "could not read %s\n", path);
139+ else
140+ fprintf(stderr, "could not find a makefile\n");
141+ free(assigns);
142+ freesubgraph(&sg);
143+ return 2;
144+ }
145 path = mkpath;
146 pathbuf = mkpath;
147 src = appendassigns(src, &sg.assigns);
148@@ -353,14 +353,14 @@ main(int argc, char **argv)
149 }
150 }
151 }
152- if (eval(path, &ast, 0, env_override, &rs) < 0) {
153- free(assigns);
154- freeast(&ast);
155- free(pathbuf);
156- free(src);
157- freesubgraph(&sg);
158- return 2;
159- }
160+ if (eval(path, &ast, 0, env_override, &rs) < 0) {
161+ free(assigns);
162+ freeast(&ast);
163+ free(pathbuf);
164+ free(src);
165+ freesubgraph(&sg);
166+ return 2;
167+ }
168 dumpruleset(&rs);
169 freeruleset(&rs);
170 freeast(&ast);
+102,
-6
1@@ -103,6 +103,35 @@ runshellassign(const char *cmd)
2 return out;
3 }
4
5+static void
6+removeexportname(struct StrList *list, const char *name)
7+{
8+ size_t i;
9+
10+ for (i = 0; i < list->n; i++) {
11+ if (strcmp(list->v[i], name) != 0)
12+ continue;
13+ free(list->v[i]);
14+ memmove(&list->v[i], &list->v[i + 1], (list->n - i - 1) * sizeof(list->v[0]));
15+ list->n--;
16+ return;
17+ }
18+}
19+
20+static int
21+assignexported(const struct EvalCtx *ctx, const struct AssignNode *in)
22+{
23+ if (in->exported > 0)
24+ return 1;
25+ if (in->exported < 0)
26+ return 0;
27+ if (hasword(&ctx->unexports, in->lhs))
28+ return 0;
29+ if (hasword(&ctx->exports, in->lhs))
30+ return 1;
31+ return ctx->export_all;
32+}
33+
34 void
35 evalassign(struct EvalCtx *ctx, const struct AssignNode *in)
36 {
37@@ -110,19 +139,21 @@ evalassign(struct EvalCtx *ctx, const struct AssignNode *in)
38 struct Var *v;
39 char *rhs, *joined;
40 enum Origin o;
41+ int exported;
42
43 env = ctx->env;
44 o = in->origin ? in->origin : ORIGIN_FILE;
45+ exported = assignexported(ctx, in);
46 switch (in->op) {
47 case ASSIGN_EQ:
48- envsetvar(env, in->lhs, xstrdup(in->rhs), 0, o, in->exported);
49+ envsetvar(env, in->lhs, xstrdup(in->rhs), 0, o, exported);
50 break;
51 case ASSIGN_COLON_EQ:
52- envsetvar(env, in->lhs, expandstr(ctx, in->rhs), 1, o, in->exported);
53+ envsetvar(env, in->lhs, expandstr(ctx, in->rhs), 1, o, exported);
54 break;
55 case ASSIGN_QMARK_EQ:
56 if (!findvar(env, in->lhs))
57- envsetvar(env, in->lhs, xstrdup(in->rhs), 0, o, in->exported);
58+ envsetvar(env, in->lhs, xstrdup(in->rhs), 0, o, exported);
59 break;
60 case ASSIGN_PLUS_EQ:
61 v = findvar(env, in->lhs);
62@@ -138,18 +169,68 @@ evalassign(struct EvalCtx *ctx, const struct AssignNode *in)
63 free(v->val);
64 v->val = joined;
65 v->origin = o;
66- if (in->exported)
67+ if (exported)
68 v->exported = 1;
69 break;
70 case ASSIGN_BANG_EQ:
71 rhs = expandstr(ctx, in->rhs);
72 joined = runshellassign(rhs);
73 free(rhs);
74- envsetvar(env, in->lhs, joined, 1, o, in->exported);
75+ envsetvar(env, in->lhs, joined, 1, o, exported);
76 break;
77 }
78 }
79
80+static void
81+evalexport(struct EvalCtx *ctx, const struct ExportNode *exp)
82+{
83+ struct StrList names;
84+ struct Var *v;
85+ size_t i, j;
86+
87+ if (exp->all) {
88+ ctx->export_all = exp->exported;
89+ for (i = 0; i < ctx->env->n; i++)
90+ ctx->env->v[i].exported = exp->exported;
91+ return;
92+ }
93+ memset(&names, 0, sizeof(names));
94+ for (i = 0; i < exp->names.n; i++) {
95+ char *s;
96+
97+ s = expandstr(ctx, exp->names.v[i]);
98+ splitwords(&names, s, strlen(s));
99+ free(s);
100+ }
101+ for (i = 0; i < names.n; i++) {
102+ if (exp->exported) {
103+ removeexportname(&ctx->unexports, names.v[i]);
104+ if (!hasword(&ctx->exports, names.v[i]))
105+ addstr(&ctx->exports, names.v[i]);
106+ } else {
107+ removeexportname(&ctx->exports, names.v[i]);
108+ if (!hasword(&ctx->unexports, names.v[i]))
109+ addstr(&ctx->unexports, names.v[i]);
110+ }
111+ v = findvar(ctx->env, names.v[i]);
112+ if (v)
113+ v->exported = exp->exported;
114+ }
115+ for (j = 0; j < ctx->out->nvars; j++) {
116+ if (hasword(&ctx->exports, ctx->out->vars[j].lhs))
117+ ctx->out->vars[j].exported = 1;
118+ if (hasword(&ctx->unexports, ctx->out->vars[j].lhs))
119+ ctx->out->vars[j].exported = 0;
120+ }
121+ for (j = 0; j < ctx->out->ntvars; j++) {
122+ if (hasword(&ctx->exports, ctx->out->tvars[j].lhs))
123+ ctx->out->tvars[j].exported = 1;
124+ if (hasword(&ctx->unexports, ctx->out->tvars[j].lhs))
125+ ctx->out->tvars[j].exported = 0;
126+ }
127+ freestrs(&names);
128+}
129+
130 static int
131 testcond(struct EvalCtx *ctx, const struct CondNode *cond)
132 {
133@@ -203,7 +284,7 @@ addrulesetassign(struct AssignNode **vec, size_t *n, const struct Node *src, str
134 dst->lhs = xstrdup(src->data.assign.lhs);
135 dst->op = src->data.assign.op;
136 dst->origin = src->data.assign.origin;
137- dst->exported = src->data.assign.exported;
138+ dst->exported = assignexported(ctx, &src->data.assign);
139 dst->tspec = src->data.assign.tspec;
140 if (src->data.assign.op == ASSIGN_COLON_EQ || src->data.assign.op == ASSIGN_BANG_EQ)
141 dst->rhs = expandstr(ctx, src->data.assign.rhs);
142@@ -309,7 +390,15 @@ evalnodes(const struct NodeList *in, struct RuleSet *out, struct EvalCtx *ctx)
143 evalassign(ctx, &src->data.assign);
144 }
145 break;
146+ case NODE_EXPORT:
147+ evalexport(ctx, &src->data.export);
148+ break;
149 case NODE_RULE:
150+ if (src->data.rule.targets.n == 1 &&
151+ strcmp(src->data.rule.targets.v[0], ".EXPORT_ALL_VARIABLES") == 0) {
152+ ctx->export_all = 1;
153+ break;
154+ }
155 if (handlespecialrule(&targets, &src->data.rule))
156 break;
157 addrulesetrule(out, &src->data.rule, ctx);
158@@ -318,6 +407,8 @@ evalnodes(const struct NodeList *in, struct RuleSet *out, struct EvalCtx *ctx)
159 }
160 if (out)
161 addwords(&out->phony, &targets.phony);
162+ if (out)
163+ out->export_all = targets.export_all;
164 if (out)
165 out->posix = targets.posix;
166 freestrs(&targets.phony);
167@@ -365,6 +456,8 @@ eval(const char *path, const struct Ast *ast, const struct Ast *pre, int envover
168 }
169 rc = evalnodes((const struct NodeList *)ast, out, &ctx);
170 freeenv(&env);
171+ freestrs(&ctx.exports);
172+ freestrs(&ctx.unexports);
173 if (rc == 0 && ctx.errors)
174 return -1;
175 return rc;
176@@ -424,6 +517,9 @@ freenodes(struct NodeList *list)
177 case NODE_RAW:
178 case NODE_INCLUDE:
179 break;
180+ case NODE_EXPORT:
181+ free(list->v[i].data.export.names.v);
182+ break;
183 case NODE_ASSIGN:
184 free(list->v[i].data.assign.targets.v);
185 break;
+4,
-0
1@@ -132,6 +132,10 @@ resolvegoals(const struct SubGraph *sg, struct StrList *out)
2 return -1;
3 }
4
5+/* when expanding a submake, start from a target name and walk the graph backwards
6+ * only for that target. if we expand the entire subgraph from the beginning,
7+ * we get a lot of cases where the subgraph will recurse and try to create a new
8+ * subgraph of itself */
9 static void
10 markreachable(const struct Graph *graph, const char *name, unsigned char *seen)
11 {
+6,
-2
1@@ -21,12 +21,16 @@ struct EvalCtx {
2 struct StrList *side_effects;
3 const char *cur_path;
4 int cur_line;
5+ struct StrList exports;
6+ struct StrList unexports;
7+ int export_all;
8 };
9
10 struct SpecialTargets {
11 int posix;
12 struct StrList phony;
13 char recipeprefix;
14+ int export_all;
15 };
16
17 void *xmalloc(size_t n);
18@@ -34,11 +38,11 @@ void dielikemake(const char *path, int line, const char *msg, const char *detail
19 void *xrealloc(void *p, size_t n);
20 char *xstrndup(const char *s, size_t n);
21 char *xstrdup(const char *s);
22-void arena_init(struct Arena *a, size_t block_size);
23+void arena_init(struct Arena *a, size_t block_size);
24 void *arena_alloc(struct Arena *a, size_t n);
25 char *arena_strndup(struct Arena *a, const char *s, size_t n);
26 char *arena_strdup(struct Arena *a, const char *s);
27-void arena_free(struct Arena *a);
28+void arena_free(struct Arena *a);
29 const char *intern(const char *s);
30 char *readfile(const char *path);
31 int loadmakefile(const char *path, char **path_out, char **src_out);
+46,
-18
1@@ -90,7 +90,6 @@ parsewarn(const struct PreLine *line, const char *msg)
2 }
3
4 static const char *const unsupported_kws[] = {
5- "unexport",
6 "undefine", "vpath", "private", "load", 0};
7
8 static char *
9@@ -578,20 +577,20 @@ preprocfile0(const char *path, const char *src_override, struct Pre *pre, struct
10 opt = haskw(trim, "-include") || haskw(trim, "sinclude");
11 kwlen = haskw(trim, "-include") || haskw(trim, "sinclude") ? 8 : 7;
12 incarg = trimdup(trim + kwlen, strlen(trim + kwlen));
13- if (isplainpath(incarg)) {
14- free(trim);
15- rc = preprocinclude(dir, incarg, pre, inc, targets);
16- if (rc < 0 && !opt)
17- parseerr(&line, "include not found", 0);
18- free(incarg);
19- free(line.path);
20- free(line.text);
21- if (rc < 0 && !opt) {
22- free(dir);
23- free(src);
24- popinc(inc);
25- return -1;
26- }
27+ if (isplainpath(incarg)) {
28+ free(trim);
29+ rc = preprocinclude(dir, incarg, pre, inc, targets);
30+ if (rc < 0 && !opt)
31+ parseerr(&line, "include not found", 0);
32+ free(incarg);
33+ free(line.path);
34+ free(line.text);
35+ if (rc < 0 && !opt) {
36+ free(dir);
37+ free(src);
38+ popinc(inc);
39+ return -1;
40+ }
41 continue;
42 }
43 free(incarg);
44@@ -855,6 +854,22 @@ parseexpr(const struct PreLine *line, const char *s)
45 return state;
46 }
47
48+static struct Node
49+parseexport(const struct PreLine *line, const char *s, int exported)
50+{
51+ struct Node state;
52+
53+ memset(&state, 0, sizeof(state));
54+ state.kind = NODE_EXPORT;
55+ state.loc.line0 = line->line0;
56+ state.loc.line1 = line->line1;
57+ state.data.export.exported = exported;
58+ state.data.export.all = *s == 0;
59+ if (!state.data.export.all)
60+ astsplitwords(&state.data.export.names, s, strlen(s));
61+ return state;
62+}
63+
64 /* unclassified line, treat as unsupported syntax */
65 static struct Node
66 parseraw(const struct PreLine *line, const char *s, char recipeprefix)
67@@ -992,12 +1007,14 @@ parseline(const struct PreLine *line, const struct SpecialTargets *targets)
68 int dcolon;
69 int is_override;
70 int is_export;
71+ int is_unexport;
72 size_t n;
73 ptrdiff_t colon;
74
75 trim = trimdup(line->text, strlen(line->text));
76 is_override = 0;
77 is_export = 0;
78+ is_unexport = 0;
79 if (haskw(trim, "override")) {
80 char *rest;
81
82@@ -1014,6 +1031,14 @@ parseline(const struct PreLine *line, const struct SpecialTargets *targets)
83 trim = rest;
84 is_export = 1;
85 }
86+ if (haskw(trim, "unexport")) {
87+ char *rest;
88+
89+ rest = trimdup(trim + 8, strlen(trim + 8));
90+ free(trim);
91+ trim = rest;
92+ is_unexport = 1;
93+ }
94 n = strlen(trim);
95
96 if (!n) {
97@@ -1087,6 +1112,8 @@ parseline(const struct PreLine *line, const struct SpecialTargets *targets)
98 state.data.assign.origin = ORIGIN_OVERRIDE;
99 if (is_export)
100 state.data.assign.exported = 1;
101+ if (is_unexport)
102+ state.data.assign.exported = -1;
103 free(trim);
104 return state;
105 }
106@@ -1107,12 +1134,13 @@ parseline(const struct PreLine *line, const struct SpecialTargets *targets)
107 state.data.assign.origin = ORIGIN_OVERRIDE;
108 if (is_export)
109 state.data.assign.exported = 1;
110+ if (is_unexport)
111+ state.data.assign.exported = -1;
112 free(trim);
113 return state;
114 }
115- if (is_export) {
116- parseerr(line, "directive", "export");
117- state = blanknode(line);
118+ if (is_export || is_unexport) {
119+ state = parseexport(line, trim, is_export && !is_unexport);
120 free(trim);
121 return state;
122 }
+9,
-0
1@@ -25,6 +25,7 @@ enum NodeKind {
2 NODE_RULE,
3 NODE_INCLUDE,
4 NODE_COND,
5+ NODE_EXPORT,
6 NODE_RAW,
7 };
8
9@@ -130,6 +131,12 @@ struct RawNode {
10 char *text;
11 };
12
13+struct ExportNode {
14+ int all;
15+ int exported;
16+ struct StrList names;
17+};
18+
19 struct Node;
20
21 struct NodeList {
22@@ -155,6 +162,7 @@ struct Node {
23 struct RuleNode rule;
24 struct IncludeNode include;
25 struct CondNode cond;
26+ struct ExportNode export;
27 struct RawNode raw;
28 } data;
29 };
30@@ -174,6 +182,7 @@ struct RuleSet {
31 struct RuleNode *rules;
32 size_t nrules;
33 struct StrList phony;
34+ int export_all;
35 int posix;
36 int envoverride;
37 };
+1,
-1
1@@ -239,7 +239,7 @@ parsesubmake(struct SubMake *dst, const char *cmd)
2 if (strcmp(ntok, "&&") == 0) {
3 free(ntok);
4 break;
5- }
6+ }
7 if ((strcmp(ntok, "-C") == 0 || strcmp(ntok, "--directory") == 0) && i + 1 < toks.n) {
8 addstr(&dst->flags, ntok);
9 free(dst->dir);
+5,
-0
1@@ -9,6 +9,7 @@ initspecialtargets(struct SpecialTargets *targets)
2 targets->posix = 0;
3 memset(&targets->phony, 0, sizeof(targets->phony));
4 targets->recipeprefix = '\t';
5+ targets->export_all = 0;
6 }
7
8 void
9@@ -40,5 +41,9 @@ handlespecialrule(struct SpecialTargets *targets, const struct RuleNode *rule)
10 }
11 return 1;
12 }
13+ if (rule->targets.n == 1 && strcmp(rule->targets.v[0], ".EXPORT_ALL_VARIABLES") == 0) {
14+ targets->export_all = 1;
15+ return 1;
16+ }
17 return 0;
18 }
+1,
-1
1@@ -276,7 +276,7 @@ if [ "$dryrun" -eq 0 ]; then
2 probe_cmd="$probe_cmd '$target'"
3 done
4 IFS=$oldifs
5- probe_out=$(eval "$probe_cmd -n" 2>&1)
6+ probe_out=$(eval "$probe_cmd -v -n" 2>&1)
7 probe_status=$?
8 if [ "$probe_status" -ne 0 ]; then
9 if ! rewrite_ninja_error "$probe_out"; then