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+}