commit 57ba0f9
xplshn
·
2026-07-07 23:49:38 +0000 UTC
parent 4bb01f6
I'm stupid Signed-off-by: xplshn <anto@xplshn.com.ar>
2 files changed,
+431,
-0
+78,
-0
1@@ -0,0 +1,78 @@
2+#include "ninqu.h"
3+#include "wexec.h"
4+
5+#include <ctype.h>
6+#include <limits.h>
7+#include <string.h>
8+
9+/* one entry per generator format. adding a backend means writing its
10+ * own emit fn elsewhere and adding a row here, nothing else in ninqu
11+ * needs to change */
12+static const struct Backend backends[] = {
13+ {"ninja", "NINJA_TRY", "build.ninja", emit_ninja},
14+};
15+#define NBACKENDS (int)(sizeof backends / sizeof backends[0])
16+
17+const struct Backend *
18+backend_by_name(const char *name)
19+{
20+ int i;
21+
22+ for (i = 0; i < NBACKENDS; i++)
23+ if (strcmp(backends[i].name, name) == 0)
24+ return &backends[i];
25+ return NULL;
26+}
27+
28+/* try_key is a comma list of binary names, tried in order, first
29+ * one found on PATH wins. bin is set to that binarys own path */
30+const struct Backend *
31+backend_resolve(char **bin)
32+{
33+ int i;
34+
35+ for (i = 0; i < NBACKENDS; i++) {
36+ const char *p = kv_get_or(backends[i].try_key, "");
37+
38+ while (*p) {
39+ char name[PATH_MAX];
40+ const char *start;
41+ char *found;
42+ size_t len;
43+
44+ while (*p == ',' || isspace((unsigned char)*p))
45+ p++;
46+ start = p;
47+ while (*p && *p != ',')
48+ p++;
49+ len = (size_t)(p - start);
50+ while (len && isspace((unsigned char)start[len - 1]))
51+ len--;
52+ if (len && len < sizeof name) {
53+ memcpy(name, start, len);
54+ name[len] = '\0';
55+ found = wwhich(name);
56+ if (found) {
57+ *bin = found;
58+ return &backends[i];
59+ }
60+ }
61+ }
62+ }
63+ return NULL;
64+}
65+
66+void
67+backend_exec(const struct Backend *be, const char *bin, struct StrList *wanted)
68+{
69+ struct StrList argv = {0};
70+ int i;
71+
72+ sl_push(&argv, base_of(bin));
73+ sl_push(&argv, "-f");
74+ sl_push(&argv, be->file);
75+ for (i = 0; i < wanted->n; i++)
76+ sl_push(&argv, wanted->v[i]);
77+
78+ wexecv_self(bin, sl_argv(&argv));
79+}
+353,
-0
1@@ -0,0 +1,353 @@
2+#include "ninqu.h"
3+
4+#include <stdio.h>
5+#include <stdlib.h>
6+#include <string.h>
7+
8+/* ninjas own tokenizer treats '$', ':' and ' ' as special outside
9+ * of a rules variables, so any path used in a build statement
10+ * (outputs, inputs, deps) needs those three escaped with a leading
11+ * '$'. this is a different escaping pass from shell quoting below,
12+ * it runs on ninjas file syntax, not on the command it configures */
13+static char *
14+ninja_path_escape(const char *s)
15+{
16+ size_t i, j = 0;
17+ size_t len = strlen(s);
18+ char *out = emalloc(len * 2 + 1);
19+
20+ for (i = 0; i < len; i++) {
21+ if (s[i] == '$' || s[i] == ':' || s[i] == ' ')
22+ out[j++] = '$';
23+ out[j++] = s[i];
24+ }
25+ out[j] = '\0';
26+ return out;
27+}
28+
29+/* ninqus argv is already fully expanded ($(VAR) substitution
30+ * happened at parse time), so the ninja rules command is a plain
31+ * literal string, never $in/$out. write one argv word as a single
32+ * shell-quoted token: close/escape/reopen for an embedded quote,
33+ * and double any '$' so ninjas own preprocessing pass on the
34+ * "command" value hands the shell back exactly one '$' */
35+static void
36+fprint_shell_word_plain(FILE *f, const char *s)
37+{
38+ fputc('\'', f);
39+ for (; *s; s++) {
40+ if (*s == '\'')
41+ fputs("'\\''", f);
42+ else if (*s == '$')
43+ fputs("$$", f);
44+ else
45+ fputc(*s, f);
46+ }
47+ fputc('\'', f);
48+}
49+
50+/* ninja reads a "command = ..." value to the end of the physical
51+ * line: a literal newline byte terminates the statement, and $\n is
52+ * a continuation that strips the newline rather than preserving it,
53+ * so neither lets a real newline survive inside the value. POSIX
54+ * single quotes cannot help either, there is no escape processing
55+ * inside them, so a backslash-n written between quotes stays two
56+ * literal characters, not a newline, once the shell reads it back.
57+ *
58+ * the fix is to not carry the newline in quoted text at all: emit
59+ * a command substitution that runs the word through `printf '%b'`,
60+ * whose %b directive is POSIX-specified to expand backslash escapes
61+ * (including \n) in its argument at runtime. the argument to printf
62+ * is itself single-quoted using the same rules as the plain case,
63+ * plus doubling any backslash so it survives as data rather than
64+ * being read as an escape by the quoting layer itself, and the `$(`
65+ * that opens the substitution needs its own `$` doubled so ninjas
66+ * pass just picks it out unmodified for the shell to see */
67+static void
68+fprint_shell_word(FILE *f, const char *s)
69+{
70+ const char *p;
71+ int has_nl = 0;
72+
73+ for (p = s; *p; p++) {
74+ if (*p == '\n') {
75+ has_nl = 1;
76+ break;
77+ }
78+ }
79+
80+ if (!has_nl) {
81+ fprint_shell_word_plain(f, s);
82+ return;
83+ }
84+
85+ /* wrapped in double quotes so the shell treats the substitutions
86+ * result as one word instead of splitting it on whitespace, which
87+ * is exactly what an unquoted $(...) would do to a multi-line
88+ * result */
89+ fputs("\"$$(printf '%b' '", f);
90+ for (; *s; s++) {
91+ if (*s == '\'')
92+ fputs("'\\''", f);
93+ else if (*s == '$')
94+ fputs("$$", f);
95+ else if (*s == '\\')
96+ fputs("\\\\", f);
97+ else if (*s == '\n')
98+ fputs("\\n", f);
99+ else
100+ fputc(*s, f);
101+ }
102+ fputs("')\"", f);
103+}
104+
105+/* an instance with no (out) has no on-disk path of its own (a
106+ * (phony) rule, or a pure grouping rule). ninja still needs a
107+ * target name to hang a build statement off, so one is made up
108+ * from the rule name and instance index. it is never a real file,
109+ * so ninja will always find it missing and re-run it, the same
110+ * "always stale" behaviour inst_stale() gives phony instances in
111+ * the internal engine. the name is a single flat path component
112+ * (no '/'), so ninja never mkdir -p's a directory for it: ninja
113+ * only creates the parent directory of a build outputs path, and a
114+ * name with no slash has no parent beyond the working directory */
115+static const char *
116+inst_target(struct Inst *inst, int idx, char *buf, size_t bufsz)
117+{
118+ if (inst->out[0])
119+ return inst->out;
120+ snprintf(buf, bufsz, "__ninqu_phony__%s_%d", rules[inst->rule_idx].name, idx);
121+ return buf;
122+}
123+
124+/* does this instance look like "cc ... -c ... -o foo.o"? if so its
125+ * a real compile step and ninja can track its header dependencies
126+ * precisely via a gcc-style depfile instead of only the coarse
127+ * order-only edges dep_inst gives every instance. matched by
128+ * basename so whatever CC the manifest actually resolved (gcc,
129+ * clang, a cross compiler under some path) is what gets the extra
130+ * flags, nothing here hardcodes a specific compiler binary; the
131+ * check is on -c and a .o output, not on the compiler name meaning
132+ * anything beyond "probably understands -MMD -MF" */
133+static int
134+is_cc_compile(struct Inst *inst)
135+{
136+ static const char *const compilers[] = {"cc", "gcc", "clang", "g++", "c++", NULL};
137+ const char *prog, *slash;
138+ size_t len;
139+ int k, has_c;
140+
141+ if (inst->phony || !inst->out[0] || inst->cmd.is_pipe || inst->cmd.argv.n < 2)
142+ return 0;
143+
144+ len = strlen(inst->out);
145+ if (len < 2 || strcmp(inst->out + len - 2, ".o") != 0)
146+ return 0;
147+
148+ prog = inst->cmd.argv.v[0];
149+ slash = strrchr(prog, '/');
150+ if (slash)
151+ prog = slash + 1;
152+
153+ for (k = 0; compilers[k]; k++)
154+ if (strcmp(prog, compilers[k]) == 0)
155+ break;
156+ if (!compilers[k])
157+ return 0;
158+
159+ for (k = 1, has_c = 0; k < inst->cmd.argv.n; k++)
160+ if (strcmp(inst->cmd.argv.v[k], "-c") == 0)
161+ has_c = 1;
162+
163+ return has_c;
164+}
165+
166+/* the command a rule runs: an optional workdir cd, the argv or
167+ * piped stages shell-quoted word by word, and an optional stdout
168+ * redirect into (out). mirrors spawn_inst()/spawn_pipe() exactly,
169+ * just written out as text instead of forked and execd. a compile
170+ * step (is_cc_compile()) gets "-MMD -MF <out>.d" spliced in right
171+ * after the compiler name, paired with the depfile/deps lines
172+ * emit_ninja() writes into that instances rule block */
173+static void
174+emit_command(FILE *f, struct Inst *inst)
175+{
176+ int is_cc = is_cc_compile(inst);
177+ int i;
178+
179+ if (inst->workdir[0]) {
180+ fputs("cd ", f);
181+ fprint_shell_word(f, inst->workdir);
182+ fputs(" && ", f);
183+ }
184+
185+ if (inst->cmd.is_pipe) {
186+ for (i = 0; i < inst->cmd.nstages; i++) {
187+ int k;
188+ if (i > 0)
189+ fputs(" | ", f);
190+ for (k = 0; k < inst->cmd.stages[i]->n; k++) {
191+ if (k > 0)
192+ fputc(' ', f);
193+ fprint_shell_word(f, inst->cmd.stages[i]->v[k]);
194+ }
195+ }
196+ } else {
197+ for (i = 0; i < inst->cmd.argv.n; i++) {
198+ if (i > 0)
199+ fputc(' ', f);
200+ fprint_shell_word(f, inst->cmd.argv.v[i]);
201+ if (i == 0 && is_cc) {
202+ fputs(" -MMD -MF ", f);
203+ fprint_shell_word(f, inst->out);
204+ fputs(".d", f);
205+ }
206+ }
207+ }
208+
209+ if (inst->redirect) {
210+ fputs(" > ", f);
211+ fprint_shell_word(f, inst->out);
212+ }
213+}
214+
215+/* write every instance currently in insts[] (the same closure the
216+ * internal engine would have built, wanted targets plus their
217+ * transitive deps) out as a build.ninja. one rule per instance
218+ * (ninqu already resolved everything to a literal command line, so
219+ * there is no shared rule template to factor out), one build
220+ * statement per instance. dep_inst edges become order-only ("||"):
221+ * inst_stale() never consults dep_inst for staleness either, only
222+ * (in)/(stale_extra), so mirroring that as a plain (not order-only)
223+ * prerequisite list is what makes ninjas own mtime check agree
224+ * with ninqus.
225+ *
226+ * ninja only knows build outputs, never ninqus rule/group/meta
227+ * names, so `ninja LIBUTIL` (what the Makefile forwards $@ as) has
228+ * nothing to resolve against on its own. wanted carries the literal
229+ * command-line words ninqu itself was invoked with, one phony alias
230+ * per word makes that name buildable too. when more than one target
231+ * was requested in the same invocation every alias points at the
232+ * whole combined closure rather than just its own slice, since
233+ * insts[] no longer remembers which instance came from which wanted
234+ * name, thats fine for the Makefiles own use (always one target
235+ * per invocation), just slightly conservative for a multi-target
236+ * `ninqu -G ninja a b` run */
237+void
238+emit_ninja(const char *path, struct StrList *wanted)
239+{
240+ FILE *f = fopen(path, "w");
241+ int i, k;
242+
243+ if (!f)
244+ eprintf("emit_ninja: open %s:", path);
245+
246+ fputs("# autogenerated by `ninqu -G ninja`, do not edit\n", f);
247+ fputs("ninja_required_version = 1.3\n\n", f);
248+
249+ for (i = 0; i < ninsts; i++) {
250+ struct Inst *inst = &insts[i];
251+ fprintf(f, "rule r%d\n", i);
252+ fprintf(
253+ f,
254+ " description = %s %s\n",
255+ rules[inst->rule_idx].name,
256+ inst->out[0] ? inst->out : "(phony)"
257+ );
258+ if (is_cc_compile(inst)) {
259+ char *esc = ninja_path_escape(inst->out);
260+ fprintf(f, " depfile = %s.d\n", esc);
261+ fputs(" deps = gcc\n", f);
262+ free(esc);
263+ }
264+ fputs(" command = ", f);
265+ emit_command(f, inst);
266+ fputs("\n\n", f);
267+ }
268+
269+ for (i = 0; i < ninsts; i++) {
270+ struct Inst *inst = &insts[i];
271+ char buf[1024];
272+ const char *out = inst_target(inst, i, buf, sizeof buf);
273+ char *esc = ninja_path_escape(out);
274+
275+ fprintf(f, "build %s: r%d", esc, i);
276+ free(esc);
277+
278+ for (k = 0; k < inst->in.n; k++) {
279+ esc = ninja_path_escape(inst->in.v[k]);
280+ fprintf(f, " %s", esc);
281+ free(esc);
282+ }
283+ for (k = 0; k < inst->stale_extra.n; k++) {
284+ esc = ninja_path_escape(inst->stale_extra.v[k]);
285+ fprintf(f, " %s", esc);
286+ free(esc);
287+ }
288+
289+ if (inst->n_dep > 0) {
290+ int printed_bar = 0;
291+ for (k = 0; k < inst->n_dep; k++) {
292+ struct Inst *dep = &insts[inst->dep_inst[k]];
293+ char depbuf[1024];
294+ const char *depout = inst_target(dep, inst->dep_inst[k], depbuf, sizeof depbuf);
295+ /* a rule ref that is also a real (dep ...) input already
296+ * forces both order and staleness, listing it again as
297+ * order-only is pure noise */
298+ if (sl_has(&inst->in, depout) || sl_has(&inst->stale_extra, depout))
299+ continue;
300+ if (!printed_bar) {
301+ fputs(" ||", f);
302+ printed_bar = 1;
303+ }
304+ esc = ninja_path_escape(depout);
305+ fprintf(f, " %s", esc);
306+ free(esc);
307+ }
308+ }
309+ fputc('\n', f);
310+ }
311+
312+ fputs("\ndefault", f);
313+ for (i = 0; i < ninsts; i++) {
314+ struct Inst *inst = &insts[i];
315+ char buf[1024];
316+ const char *out = inst_target(inst, i, buf, sizeof buf);
317+ char *esc = ninja_path_escape(out);
318+ fprintf(f, " %s", esc);
319+ free(esc);
320+ }
321+ fputc('\n', f);
322+
323+ for (i = 0; i < wanted->n; i++) {
324+ const char *name = wanted->v[i];
325+ int dup = 0;
326+
327+ for (k = 0; k < ninsts && !dup; k++) {
328+ char buf[1024];
329+ const char *out = inst_target(&insts[k], k, buf, sizeof buf);
330+ if (strcmp(out, name) == 0)
331+ dup = 1;
332+ }
333+ if (dup)
334+ continue;
335+
336+ {
337+ char *esc = ninja_path_escape(name);
338+ fprintf(f, "build %s: phony", esc);
339+ free(esc);
340+ }
341+ for (k = 0; k < ninsts; k++) {
342+ char buf[1024];
343+ const char *out = inst_target(&insts[k], k, buf, sizeof buf);
344+ char *esc = ninja_path_escape(out);
345+ fprintf(f, " %s", esc);
346+ free(esc);
347+ }
348+ fputc('\n', f);
349+ }
350+
351+ if (fshut(f, path))
352+ eprintf("emit_ninja: %s:", path);
353+ printf(" GEN %s\n", path);
354+}