commit b52592b

xplshn  ·  2026-07-06 15:10:30 +0000 UTC
parent c9e91a9
Get rif of Make/Makefile.sh and a bunch of bullshit

Signed-off-by: xplshn <anto@xplshn.com.ar>
25 files changed,  +4569, -2366
M README
+76, -847
  1@@ -1,848 +1,77 @@
  2--include scripts/mk/config.mk
  3-
  4-.SUFFIXES: .y .o .c
  5-HDR =\
  6-	shared/arg.h\
  7-	shared/compat.h\
  8-	shared/config.h\
  9-	shared/crypt.h\
 10-	shared/wexec.h\
 11-	shared/fs.h\
 12-	shared/md5.h\
 13-	shared/queue.h\
 14-	shared/sha1.h\
 15-	shared/sha224.h\
 16-	shared/sha256.h\
 17-	shared/sha384.h\
 18-	shared/sha512.h\
 19-	shared/sha512-224.h\
 20-	shared/sha512-256.h\
 21-	shared/text.h\
 22-	shared/utf.h\
 23-	shared/util.h\
 24-	shared/passwd.h\
 25-	shared/paths.h\
 26-	shared/reboot.h\
 27-	shared/rtc.h\
 28-	shared/proc.h\
 29-	shared/tls.h
 30-
 31-LIBUTFOBJ =\
 32-	shared/libutf/fgetrune.o\
 33-	shared/libutf/fputrune.o\
 34-	shared/libutf/isalnumrune.o\
 35-	shared/libutf/isalpharune.o\
 36-	shared/libutf/isblankrune.o\
 37-	shared/libutf/iscntrlrune.o\
 38-	shared/libutf/isdigitrune.o\
 39-	shared/libutf/isgraphrune.o\
 40-	shared/libutf/isprintrune.o\
 41-	shared/libutf/ispunctrune.o\
 42-	shared/libutf/isspacerune.o\
 43-	shared/libutf/istitlerune.o\
 44-	shared/libutf/isxdigitrune.o\
 45-	shared/libutf/lowerrune.o\
 46-	shared/libutf/rune.o\
 47-	shared/libutf/runetype.o\
 48-	shared/libutf/upperrune.o\
 49-	shared/libutf/utf.o\
 50-	shared/libutf/utftorunestr.o
 51-
 52-LIBUTILOBJ =\
 53-	shared/libutil/concat.o\
 54-	shared/libutil/cp.o\
 55-	shared/libutil/crypt.o\
 56-	shared/libutil/confirm.o\
 57-	shared/libutil/diffutil.o\
 58-	shared/libutil/wexec.o\
 59-	shared/libutil/ealloc.o\
 60-	shared/libutil/enmasse.o\
 61-	shared/libutil/eprintf.o\
 62-	shared/libutil/eregcomp.o\
 63-	shared/libutil/estrtod.o\
 64-	shared/libutil/fnck.o\
 65-	shared/libutil/fshut.o\
 66-	shared/libutil/getlines.o\
 67-	shared/libutil/human.o\
 68-	shared/libutil/linecmp.o\
 69-	shared/libutil/md5.o\
 70-	shared/libutil/memmem.o\
 71-	shared/libutil/mkdirp.o\
 72-	shared/libutil/mode.o\
 73-	shared/libutil/parseoffset.o\
 74-	shared/libutil/putword.o\
 75-	shared/libutil/reallocarray.o\
 76-	shared/libutil/recurse.o\
 77-	shared/libutil/rm.o\
 78-	shared/libutil/sha1.o\
 79-	shared/libutil/sha224.o\
 80-	shared/libutil/sha256.o\
 81-	shared/libutil/sha384.o\
 82-	shared/libutil/sha512.o\
 83-	shared/libutil/sha512-224.o\
 84-	shared/libutil/sha512-256.o\
 85-	shared/libutil/strcasestr.o\
 86-	shared/libutil/strlcat.o\
 87-	shared/libutil/strlcpy.o\
 88-	shared/libutil/strsep.o\
 89-	shared/libutil/strnsubst.o\
 90-	shared/libutil/strtonum.o\
 91-	shared/libutil/writeall.o\
 92-	shared/libutil/unescape.o\
 93-	shared/libutil/agetcwd.o\
 94-	shared/libutil/agetline.o\
 95-	shared/libutil/apathmax.o\
 96-	shared/libutil/estrtol.o\
 97-	shared/libutil/estrtoul.o\
 98-	shared/libutil/explicit_bzero.o\
 99-	shared/libutil/passwd.o\
100-	shared/libutil/proc.o\
101-	shared/libutil/tty.o\
102-	shared/libutil/fconcat.o\
103-	shared/libutil/recurse_dir.o\
104-	shared/libutil/sig.o\
105-	shared/libutil/net.o\
106-	shared/libutil/sysinfo.o\
107-	shared/libutil/tls.o\
108-	shared/libutil/diskutil.o
109-
110-LIBREDLINEOBJ =\
111-	shared/libredline/redline.o
112-
113-LIB = shared/libredline/libredline.a shared/libutil/libutil.a shared/libutf/libutf.a
114-
115-POSIX_BIN_ALL =\
116-	cmd/posix/basename\
117-	cmd/posix/cal\
118-	cmd/posix/cat\
119-	cmd/posix/chgrp\
120-	cmd/posix/chmod\
121-	cmd/posix/chown\
122-	cmd/posix/cksum\
123-	cmd/posix/cmp\
124-	cmd/posix/comm\
125-	cmd/posix/cp\
126-	cmd/posix/cut\
127-	cmd/posix/date\
128-	cmd/posix/dd\
129-	cmd/posix/df\
130-	cmd/posix/diff\
131-	cmd/posix/patch\
132-	cmd/posix/dirname\
133-	cmd/posix/du\
134-	cmd/posix/echo\
135-	cmd/posix/ed\
136-	cmd/posix/env\
137-	cmd/posix/expand\
138-	cmd/posix/expr\
139-	cmd/posix/false\
140-	cmd/posix/find\
141-	cmd/posix/fold\
142-	cmd/posix/getconf\
143-	cmd/posix/grep\
144-	cmd/posix/head\
145-	cmd/posix/id\
146-	cmd/posix/join\
147-	cmd/posix/kill\
148-	cmd/posix/link\
149-	cmd/posix/ln\
150-	cmd/posix/logger\
151-	cmd/posix/logname\
152-	cmd/posix/ls\
153-	cmd/posix/mesg\
154-	cmd/posix/mkdir\
155-	cmd/posix/mkfifo\
156-	cmd/posix/mv\
157-	cmd/posix/nice\
158-	cmd/posix/nl\
159-	cmd/posix/nohup\
160-	cmd/posix/od\
161-	cmd/posix/paste\
162-	cmd/posix/pathchk\
163-	cmd/posix/printf\
164-	cmd/posix/ps\
165-	cmd/posix/pwd\
166-	cmd/posix/readlink\
167-	cmd/posix/renice\
168-	cmd/posix/rm\
169-	cmd/posix/rmdir\
170-	cmd/posix/sed\
171-	cmd/posix/sleep\
172-	cmd/posix/sort\
173-	cmd/posix/split\
174-	cmd/posix/tail\
175-	cmd/posix/tee\
176-	cmd/posix/test\
177-	cmd/posix/time\
178-	cmd/posix/touch\
179-	cmd/posix/tr\
180-	cmd/posix/true\
181-	cmd/posix/tsort\
182-	cmd/posix/tty\
183-	cmd/posix/uname\
184-	cmd/posix/unexpand\
185-	cmd/posix/uniq\
186-	cmd/posix/unlink\
187-	cmd/posix/uudecode\
188-	cmd/posix/uuencode\
189-	cmd/posix/wc\
190-	cmd/posix/who\
191-	cmd/posix/xargs\
192-	cmd/posix/awk/awk\
193-	cmd/posix/sh/sh\
194-	cmd/posix/pax\
195-	cmd/posix/make/make\
196-	cmd/posix/bc
197-
198-LINUX_BIN_ALL =\
199-	cmd/linux/blkdiscard\
200-	cmd/linux/chvt\
201-	cmd/linux/ctrlaltdel\
202-	cmd/linux/eject\
203-	cmd/linux/freeramdisk\
204-	cmd/linux/fsfreeze\
205-	cmd/linux/hwclock\
206-	cmd/linux/insmod\
207-	cmd/linux/lsmod\
208-	cmd/linux/lsusb\
209-	cmd/linux/modprobe\
210-	cmd/linux/depmod\
211-	cmd/linux/mkswap\
212-	cmd/linux/mount\
213-	cmd/linux/mountpoint\
214-	cmd/linux/pivot_root\
215-	cmd/linux/readahead\
216-	cmd/linux/rmmod\
217-	cmd/linux/swaplabel\
218-	cmd/linux/swapoff\
219-	cmd/linux/swapon\
220-	cmd/linux/switch_root\
221-	cmd/linux/sysctl\
222-	cmd/linux/tunctl\
223-	cmd/linux/umount\
224-	cmd/linux/unshare\
225-	cmd/linux/vtallow
226-
227-NET_BIN_ALL =\
228-	cmd/net/netcat\
229-	cmd/net/tftp\
230-	cmd/net/wget\
231-	cmd/net/ping\
232-	cmd/net/sdhcp\
233-	cmd/net/ifconfig\
234-	cmd/net/host\
235-	cmd/net/httpd\
236-	cmd/net/ip
237-
238-XSI_BIN_ALL =\
239-	cmd/xsi/mknod\
240-	cmd/xsi/passwd\
241-	cmd/xsi/su
242-
243-PSEUDO_BIN_ALL =\
244-	cmd/pseudo/chroot\
245-	cmd/pseudo/clear\
246-	cmd/pseudo/cols\
247-	cmd/pseudo/cron\
248-	cmd/pseudo/flock\
249-	cmd/pseudo/getty\
250-	cmd/pseudo/halt\
251-	cmd/pseudo/hostname\
252-	cmd/pseudo/killall5\
253-	cmd/pseudo/last\
254-	cmd/pseudo/lastlog\
255-	cmd/pseudo/login\
256-	cmd/pseudo/md5sum\
257-	cmd/pseudo/mktemp\
258-	cmd/pseudo/nologin\
259-	cmd/pseudo/pagesize\
260-	cmd/pseudo/printenv\
261-	cmd/pseudo/respawn\
262-	cmd/pseudo/rev\
263-	cmd/pseudo/seq\
264-	cmd/pseudo/setsid\
265-	cmd/pseudo/sha1sum\
266-	cmd/pseudo/sha224sum\
267-	cmd/pseudo/sha256sum\
268-	cmd/pseudo/sha384sum\
269-	cmd/pseudo/sha512sum\
270-	cmd/pseudo/sha512-224sum\
271-	cmd/pseudo/sha512-256sum\
272-	cmd/pseudo/sponge\
273-	cmd/pseudo/stat\
274-	cmd/pseudo/tar\
275-	cmd/pseudo/truncate\
276-	cmd/pseudo/watch\
277-	cmd/pseudo/which\
278-	cmd/pseudo/whoami\
279-	cmd/pseudo/xinstall\
280-	cmd/pseudo/yes\
281-	cmd/pseudo/base64\
282-	cmd/pseudo/mkpasswd\
283-	cmd/extra/b3sum\
284-	cmd/extra/blkid\
285-	cmd/extra/lsblk\
286-	cmd/extra/fdisk\
287-	cmd/extra/sync\
288-	cmd/extra/yap/yap\
289-	cmd/dev/ar/ar\
290-	cmd/dev/as/as\
291-	cmd/dev/ld/ld\
292-	cmd/dev/cc/cc\
293-	cmd/pseudo/dmesg\
294-	cmd/pseudo/fallocate\
295-	cmd/pseudo/free\
296-	cmd/pseudo/pidof\
297-	cmd/pseudo/pwdx\
298-	cmd/pseudo/uptime\
299-	cmd/pseudo/diff3
300-
301-MAKEOBJ =\
302-	cmd/posix/make/defaults.o\
303-	cmd/posix/make/main.o\
304-	cmd/posix/make/parser.o\
305-	cmd/posix/make/posix.o\
306-	cmd/posix/make/rules.o
307-
308-
309-
310-BIN_basename_1 = cmd/posix/basename
311-BIN_cal_1 = cmd/posix/cal
312-BIN_cat_1 = cmd/posix/cat
313-BIN_chgrp_1 = cmd/posix/chgrp
314-BIN_chmod_1 = cmd/posix/chmod
315-BIN_chown_1 = cmd/posix/chown
316-BIN_cksum_1 = cmd/posix/cksum
317-BIN_cmp_1 = cmd/posix/cmp
318-BIN_comm_1 = cmd/posix/comm
319-BIN_cp_1 = cmd/posix/cp
320-BIN_cut_1 = cmd/posix/cut
321-BIN_date_1 = cmd/posix/date
322-BIN_dd_1 = cmd/posix/dd
323-BIN_df_1 = cmd/posix/df
324-BIN_diff_1 = cmd/posix/diff
325-BIN_patch_1 = cmd/posix/patch
326-BIN_dirname_1 = cmd/posix/dirname
327-BIN_du_1 = cmd/posix/du
328-BIN_echo_1 = cmd/posix/echo
329-BIN_ed_1 = cmd/posix/ed
330-BIN_env_1 = cmd/posix/env
331-BIN_expand_1 = cmd/posix/expand
332-BIN_expr_1 = cmd/posix/expr
333-BIN_false_1 = cmd/posix/false
334-BIN_find_1 = cmd/posix/find
335-BIN_fold_1 = cmd/posix/fold
336-BIN_getconf_1 = cmd/posix/getconf
337-BIN_grep_1 = cmd/posix/grep
338-BIN_head_1 = cmd/posix/head
339-BIN_id_1 = cmd/posix/id
340-BIN_join_1 = cmd/posix/join
341-BIN_kill_1 = cmd/posix/kill
342-BIN_link_1 = cmd/posix/link
343-BIN_ln_1 = cmd/posix/ln
344-BIN_logger_1 = cmd/posix/logger
345-BIN_logname_1 = cmd/posix/logname
346-BIN_ls_1 = cmd/posix/ls
347-BIN_mesg_1 = cmd/posix/mesg
348-BIN_mkdir_1 = cmd/posix/mkdir
349-BIN_mkfifo_1 = cmd/posix/mkfifo
350-BIN_mv_1 = cmd/posix/mv
351-BIN_nice_1 = cmd/posix/nice
352-BIN_nl_1 = cmd/posix/nl
353-BIN_nohup_1 = cmd/posix/nohup
354-BIN_od_1 = cmd/posix/od
355-BIN_paste_1 = cmd/posix/paste
356-BIN_pathchk_1 = cmd/posix/pathchk
357-BIN_printf_1 = cmd/posix/printf
358-BIN_ps_1 = cmd/posix/ps
359-BIN_pwd_1 = cmd/posix/pwd
360-BIN_readlink_1 = cmd/posix/readlink
361-BIN_renice_1 = cmd/posix/renice
362-BIN_rm_1 = cmd/posix/rm
363-BIN_rmdir_1 = cmd/posix/rmdir
364-BIN_sed_1 = cmd/posix/sed
365-BIN_sleep_1 = cmd/posix/sleep
366-BIN_sort_1 = cmd/posix/sort
367-BIN_split_1 = cmd/posix/split
368-BIN_tail_1 = cmd/posix/tail
369-BIN_tee_1 = cmd/posix/tee
370-BIN_test_1 = cmd/posix/test
371-BIN_time_1 = cmd/posix/time
372-BIN_touch_1 = cmd/posix/touch
373-BIN_tr_1 = cmd/posix/tr
374-BIN_true_1 = cmd/posix/true
375-BIN_tsort_1 = cmd/posix/tsort
376-BIN_tty_1 = cmd/posix/tty
377-BIN_uname_1 = cmd/posix/uname
378-BIN_unexpand_1 = cmd/posix/unexpand
379-BIN_uniq_1 = cmd/posix/uniq
380-BIN_unlink_1 = cmd/posix/unlink
381-BIN_uudecode_1 = cmd/posix/uudecode
382-BIN_uuencode_1 = cmd/posix/uuencode
383-BIN_wc_1 = cmd/posix/wc
384-BIN_who_1 = cmd/posix/who
385-BIN_xargs_1 = cmd/posix/xargs
386-BIN_awk_1 = cmd/posix/awk/awk
387-BIN_sh_1 = cmd/posix/sh/sh
388-BIN_pax_1 = cmd/posix/pax
389-BIN_make_1 = cmd/posix/make/make
390-
391-BIN_blkdiscard_1 = cmd/linux/blkdiscard
392-BIN_chvt_1 = cmd/linux/chvt
393-BIN_ctrlaltdel_1 = cmd/linux/ctrlaltdel
394-BIN_eject_1 = cmd/linux/eject
395-BIN_freeramdisk_1 = cmd/linux/freeramdisk
396-BIN_fsfreeze_1 = cmd/linux/fsfreeze
397-BIN_hwclock_1 = cmd/linux/hwclock
398-BIN_insmod_1 = cmd/linux/insmod
399-BIN_lsmod_1 = cmd/linux/lsmod
400-BIN_lsusb_1 = cmd/linux/lsusb
401-BIN_modprobe_1 = cmd/linux/modprobe
402-BIN_depmod_1 = cmd/linux/depmod
403-BIN_mkswap_1 = cmd/linux/mkswap
404-BIN_mount_1 = cmd/linux/mount
405-BIN_mountpoint_1 = cmd/linux/mountpoint
406-BIN_pivot_root_1 = cmd/linux/pivot_root
407-BIN_readahead_1 = cmd/linux/readahead
408-BIN_rmmod_1 = cmd/linux/rmmod
409-BIN_swaplabel_1 = cmd/linux/swaplabel
410-BIN_swapoff_1 = cmd/linux/swapoff
411-BIN_swapon_1 = cmd/linux/swapon
412-BIN_switch_root_1 = cmd/linux/switch_root
413-BIN_sysctl_1 = cmd/linux/sysctl
414-BIN_tunctl_1 = cmd/linux/tunctl
415-BIN_umount_1 = cmd/linux/umount
416-BIN_unshare_1 = cmd/linux/unshare
417-BIN_vtallow_1 = cmd/linux/vtallow
418-
419-BIN_netcat_1 = cmd/net/netcat
420-BIN_tftp_1 = cmd/net/tftp
421-BIN_wget_1 = cmd/net/wget
422-BIN_ping_1 = cmd/net/ping
423-BIN_sdhcp_1 = cmd/net/sdhcp
424-BIN_ifconfig_1 = cmd/net/ifconfig
425-BIN_host_1 = cmd/net/host
426-BIN_httpd_1 = cmd/net/httpd
427-BIN_ip_1 = cmd/net/ip
428-
429-BIN_mknod_1 = cmd/xsi/mknod
430-BIN_passwd_1 = cmd/xsi/passwd
431-BIN_su_1 = cmd/xsi/su
432-
433-BIN_chroot_1 = cmd/pseudo/chroot
434-BIN_clear_1 = cmd/pseudo/clear
435-BIN_cols_1 = cmd/pseudo/cols
436-BIN_cron_1 = cmd/pseudo/cron
437-BIN_flock_1 = cmd/pseudo/flock
438-BIN_getty_1 = cmd/pseudo/getty
439-BIN_halt_1 = cmd/pseudo/halt
440-BIN_hostname_1 = cmd/pseudo/hostname
441-BIN_killall5_1 = cmd/pseudo/killall5
442-BIN_last_1 = cmd/pseudo/last
443-BIN_lastlog_1 = cmd/pseudo/lastlog
444-BIN_login_1 = cmd/pseudo/login
445-BIN_md5sum_1 = cmd/pseudo/md5sum
446-BIN_mktemp_1 = cmd/pseudo/mktemp
447-BIN_nologin_1 = cmd/pseudo/nologin
448-BIN_pagesize_1 = cmd/pseudo/pagesize
449-BIN_printenv_1 = cmd/pseudo/printenv
450-BIN_respawn_1 = cmd/pseudo/respawn
451-BIN_rev_1 = cmd/pseudo/rev
452-BIN_seq_1 = cmd/pseudo/seq
453-BIN_setsid_1 = cmd/pseudo/setsid
454-BIN_sha1sum_1 = cmd/pseudo/sha1sum
455-BIN_sha224sum_1 = cmd/pseudo/sha224sum
456-BIN_sha256sum_1 = cmd/pseudo/sha256sum
457-BIN_sha384sum_1 = cmd/pseudo/sha384sum
458-BIN_sha512sum_1 = cmd/pseudo/sha512sum
459-BIN_sha512_224sum_1 = cmd/pseudo/sha512-224sum
460-BIN_sha512_256sum_1 = cmd/pseudo/sha512-256sum
461-BIN_sponge_1 = cmd/pseudo/sponge
462-BIN_stat_1 = cmd/pseudo/stat
463-BIN_tar_1 = cmd/pseudo/tar
464-BIN_truncate_1 = cmd/pseudo/truncate
465-BIN_watch_1 = cmd/pseudo/watch
466-BIN_which_1 = cmd/pseudo/which
467-BIN_whoami_1 = cmd/pseudo/whoami
468-BIN_xinstall_1 = cmd/pseudo/xinstall
469-BIN_yes_1 = cmd/pseudo/yes
470-BIN_base64_1 = cmd/pseudo/base64
471-BIN_mkpasswd_1 = cmd/pseudo/mkpasswd
472-BIN_bc_1 = cmd/posix/bc
473-BIN_b3sum_1 = cmd/extra/b3sum
474-BIN_blkid_1 = cmd/extra/blkid
475-BIN_lsblk_1 = cmd/extra/lsblk
476-BIN_fdisk_1 = cmd/extra/fdisk
477-BIN_sync_1 = cmd/extra/sync
478-BIN_yap_1 = cmd/extra/yap/yap
479-BIN_diff3_1 = cmd/pseudo/diff3
480-BIN_ar_1 = cmd/dev/ar/ar
481-BIN_as_1 = cmd/dev/as/as
482-BIN_ld_1 = cmd/dev/ld/ld
483-BIN_cc_1 = cmd/dev/cc/cc
484-BIN_dmesg_1 = cmd/pseudo/dmesg
485-BIN_fallocate_1 = cmd/pseudo/fallocate
486-BIN_free_1 = cmd/pseudo/free
487-BIN_pidof_1 = cmd/pseudo/pidof
488-BIN_pwdx_1 = cmd/pseudo/pwdx
489-BIN_uptime_1 = cmd/pseudo/uptime
490-
491-POSIX_BIN = $(POSIX_BIN_CONFIG)
492-
493-LINUX_BIN = $(LINUX_BIN_CONFIG)
494-
495-NET_BIN = $(NET_BIN_CONFIG)
496-
497-XSI_BIN = $(XSI_BIN_CONFIG)
498-
499-PSEUDO_BIN = $(PSEUDO_BIN_CONFIG) $(EXTRA_BIN_CONFIG) $(DEV_BIN_CONFIG)
500-
501-
502-OBJ = $(LIBUTFOBJ) $(LIBUTILOBJ) $(MAKEOBJ)
503-
504-all: $(LIB) $(POSIX_BIN) $(LINUX_BIN) $(NET_BIN) $(XSI_BIN) $(PSEUDO_BIN)
505-
506--include deps.mk
507--include scripts/mk/rules.mk
508-
509-$(POSIX_BIN_ALL) $(LINUX_BIN_ALL) $(NET_BIN_ALL) $(XSI_BIN_ALL) $(PSEUDO_BIN_ALL): $(LIB)
510-
511-$(OBJ) $(POSIX_BIN_ALL) $(LINUX_BIN_ALL) $(NET_BIN_ALL) $(XSI_BIN_ALL) $(PSEUDO_BIN_ALL): $(HDR)
512-
513-.o:
514-	$(CC) $(LDFLAGS) -o $@ $< $(LIB) $(LDLIBS)
515-
516-.c.o:
517-	$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ -c $<
518-
519-.c:
520-	$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIB) $(LDLIBS)
521-
522-cmd/posix/bc.c: cmd/posix/bc.y
523-	YACC='$(YACC)' sh scripts/genconfig.sh bc
524-
525-cmd/posix/bc: cmd/posix/bc.c
526-	$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ cmd/posix/bc.c $(LIB) $(LDLIBS)
527-
528-$(MAKEOBJ): cmd/posix/make/make.h
529-
530-cmd/posix/make/make: $(MAKEOBJ) $(LIB)
531-	$(CC) $(LDFLAGS) -o $@ $(MAKEOBJ) $(LIB) $(LDLIBS)
532-
533-cmd/posix/diff: cmd/posix/diff.c $(LIB)
534-	$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ cmd/posix/diff.c $(LIB) $(LDLIBS) -lm
535-
536-shared/libutf/libutf.a: $(LIBUTFOBJ)
537-	$(AR) $(ARFLAGS) $@ $?
538-	$(RANLIB) $@
539-
540-shared/libredline/libredline.a: $(LIBREDLINEOBJ)
541-	$(AR) $(ARFLAGS) $@ $?
542-	$(RANLIB) $@
543-
544-shared/libutil/libutil.a: $(LIBUTILOBJ)
545-	$(AR) $(ARFLAGS) $@ $?
546-	$(RANLIB) $@
547-
548-cmd/posix/getconf: cmd/posix/getconf.h
549-
550-cmd/posix/getconf.h:
551-	sh scripts/genconfig.sh getconf
552-
553-box: $(LIB)
554-	CC='$(CC)' CPPFLAGS='$(CPPFLAGS)' CFLAGS='$(CFLAGS)' \
555-	LDFLAGS='$(LDFLAGS)' LDLIBS='$(LDLIBS)' OBJCOPY='$(OBJCOPY)' \
556-	scripts/mkbox
557-
558-.PHONY: man clean install regen help
559-
560-scripts/mkman/mkman: scripts/mkman/main.go scripts/mkman/page.go scripts/mkman/parse.go scripts/mkman/mdoc.go
561-	cd scripts/mkman && go build -o mkman .
562-
563-man: scripts/mkman/mkman
564-	find cmd -type f -name '*.c' | while read -r src; do \
565-		if [ -f "$$src" ] && grep -qE '!man|\?man' "$$src"; then \
566-			sub=$${src#cmd/}; \
567-			cat=$${sub%%/*}; \
568-			base=$${src##*/}; \
569-			base=$${base%.c}; \
570-			name=$${sub#*/}; \
571-			if [ "$${name}" != "$${base}.c" ]; then \
572-				name=$${name%%/*}; \
573-			else \
574-				name=$$base; \
575-			fi; \
576-			u_group=$$(echo "$$cat" | tr 'a-z' 'A-Z'); \
577-			u_tool=$$(echo "$$name" | tr 'a-z-' 'A-Z_'); \
578-			var_name="BUILD_$${u_group}_$${u_tool}"; \
579-			val=""; \
580-			if [ -f scripts/mk/config.kv ]; then \
581-				val=$$(grep "^$${var_name}=" scripts/mk/config.kv | cut -d= -f2 | tr -d '"'); \
582-			fi; \
583-			if [ "$$val" != "1" ]; then \
584-				continue; \
585-			fi; \
586-			case "$$cat" in \
587-				linux|net|xsi) sec=8 ;; \
588-				*)             sec=1 ;; \
589-			esac; \
590-			mkdir -p "man/man$${sec}"; \
591-			scripts/mkman/mkman -fmt mdoc -config scripts/mk/config.kv -section "$${sec}" "$$src" > "man/man$${sec}/$$name.$${sec}"; \
592-			scripts/mkman/mkman -fmt txt -config scripts/mk/config.kv -section "$${sec}" "$$src" > "man/man$${sec}/$$name.$${sec}.txt"; \
593-		fi; \
594-	done
595-
596-regen: scripts/mk/config.mk cmd/posix/getconf.h cmd/posix/bc.c cmd/posix/awk/awkgram.tab.c cmd/posix/awk/proctab.c cmd/posix/sh/token.h cmd/posix/sh/syntax.c cmd/posix/sh/nodes.c cmd/posix/sh/builtins.c
597-
598-help:
599-	@echo "Available targets:"
600-	@echo "  all       - Build all enabled utilities"
601-	@echo "  clean     - Clean build artifacts"
602-	@echo "  install   - Install binaries and man pages"
603-	@echo "  box       - Build the multicall binary (aruu-box)"
604-	@echo "  man       - Generate manual pages"
605-	@echo "  regen     - Regenerate build system files and config"
606-	@echo "  help      - Show this help message"
607-
608-install: all man
609-	@mkdir -p $(PREFIX)/bin $(MANPREFIX)/man1 $(MANPREFIX)/man8
610-	@find cmd -type f ! -name '*.*' -perm -100 | while read -r bin; do \
611-		printf "  INSTALL %s/bin/%s\n" "$(PREFIX)" "$${bin##*/}"; \
612-		cp "$$bin" "$(PREFIX)/bin/$${bin##*/}"; \
613-		chmod 755 "$(PREFIX)/bin/$${bin##*/}"; \
614-	done
615-	@find man/man1 man/man8 -type f -name '*.[18]' 2>/dev/null | while read -r f; do \
616-		sec=$${f%/*}; sec=$${sec##*/}; \
617-		cp "$$f" "$(MANPREFIX)/$${sec}/$${f##*/}"; \
618-	done
619-
620-clean:
621-	@printf "  CLEAN object files\n"
622-	@find shared cmd -name "*.o" -exec rm -f {} +
623-	@printf "  CLEAN static libraries\n"
624-	@rm -f $(LIB)
625-	@printf "  CLEAN compiled binaries\n"
626-	@rm -f $(POSIX_BIN_ALL) $(LINUX_BIN_ALL) $(NET_BIN_ALL) $(XSI_BIN_ALL) $(PSEUDO_BIN_ALL)
627-	@sh scripts/genconfig.sh clean
628-	@printf "  CLEAN build tools\n"
629-	@rm -f scripts/mkman/mkman
630-	@rm -f scripts/mk/config.kv
631-	@rm -f scripts/mk/config.mk
632-	@rm -f scripts/mk/rules.mk
633-
634-	@printf "  CLEAN dev artifacts\n"
635-	@rm -f cmd/dev/cc/cc1 cmd/dev/cc/cpp cmd/dev/as/as cmd/dev/ld/ld cmd/dev/ar/ar shared/libaruuelf.so
636-	@rm -f cmd/dev/config.h cmd/dev/cc/config.h cmd/dev/version.h
637-	@printf "  CLEAN misc\n"
638-	@rm -rf aruu-box .box man/man1 man/man8
639-	@printf "  CLEAN done\n"
640-AWKOBJ =\
641-	cmd/posix/awk/b.o\
642-	cmd/posix/awk/main.o\
643-	cmd/posix/awk/parse.o\
644-	cmd/posix/awk/proctab.o\
645-	cmd/posix/awk/tran.o\
646-	cmd/posix/awk/lib.o\
647-	cmd/posix/awk/run.o\
648-	cmd/posix/awk/lex.o\
649-	cmd/posix/awk/math.o\
650-	cmd/posix/awk/awkgram.tab.o
651-
652-SH_GENHDRS =\
653-	cmd/posix/sh/syntax.h\
654-	cmd/posix/sh/nodes.h\
655-	cmd/posix/sh/builtins.h\
656-	cmd/posix/sh/token.h
657-
658-SHOBJ =\
659-	cmd/posix/sh/alias.o\
660-	cmd/posix/sh/arith_yacc.o\
661-	cmd/posix/sh/arith_yylex.o\
662-	cmd/posix/sh/cd.o\
663-	cmd/posix/sh/echo.o\
664-	cmd/posix/sh/error.o\
665-	cmd/posix/sh/easter.o\
666-	cmd/posix/sh/eval.o\
667-	cmd/posix/sh/exec.o\
668-	cmd/posix/sh/expand.o\
669-	cmd/posix/sh/lineedit.o\
670-	cmd/posix/sh/input.o\
671-	cmd/posix/sh/jobs.o\
672-	cmd/posix/sh/kill.o\
673-	cmd/posix/sh/mail.o\
674-	cmd/posix/sh/main.o\
675-	cmd/posix/sh/memalloc.o\
676-	cmd/posix/sh/miscbltin.o\
677-	cmd/posix/sh/mystring.o\
678-	cmd/posix/sh/options.o\
679-	cmd/posix/sh/output.o\
680-	cmd/posix/sh/parser.o\
681-	cmd/posix/sh/printf.o\
682-	cmd/posix/sh/redir.o\
683-	cmd/posix/sh/show.o\
684-	cmd/posix/sh/test.o\
685-	cmd/posix/sh/trap.o\
686-	cmd/posix/sh/var.o\
687-	cmd/posix/sh/builtins.o\
688-	cmd/posix/sh/nodes.o\
689-	cmd/posix/sh/syntax.o
690-
691-YAPOBJ =\
692-	cmd/extra/yap/ansi.o\
693-	cmd/extra/yap/commands.o\
694-	cmd/extra/yap/display.o\
695-	cmd/extra/yap/getcomm.o\
696-	cmd/extra/yap/getline.o\
697-	cmd/extra/yap/help.o\
698-	cmd/extra/yap/keys.o\
699-	cmd/extra/yap/machine.o\
700-	cmd/extra/yap/main.o\
701-	cmd/extra/yap/options.o\
702-	cmd/extra/yap/output.o\
703-	cmd/extra/yap/pattern.o\
704-	cmd/extra/yap/process.o\
705-	cmd/extra/yap/prompt.o\
706-	cmd/extra/yap/term.o
707-
708-cmd/posix/awk/awkgram.tab.c: cmd/posix/awk/awkgram.y cmd/posix/awk/maketab.c
709-	CC='$(CC)' CFLAGS='$(CFLAGS)' YACC='$(YACC)' sh scripts/genconfig.sh awk
710-
711-cmd/posix/awk/awkgram.tab.h: cmd/posix/awk/awkgram.tab.c ;
712-cmd/posix/awk/maketab: cmd/posix/awk/awkgram.tab.c ;
713-cmd/posix/awk/proctab.c: cmd/posix/awk/awkgram.tab.c ;
714-
715-$(AWKOBJ): cmd/posix/awk/awk.h cmd/posix/awk/awkgram.tab.h cmd/posix/awk/proto.h
716-
717-cmd/posix/awk/awk: $(AWKOBJ) $(LIB)
718-	$(CC) $(LDFLAGS) -o $@ $(AWKOBJ) $(LIB) $(LDLIBS) -lm
719-
720-cmd/posix/sh/builtins.c: cmd/posix/sh/mknodes.c cmd/posix/sh/mksyntax.c cmd/posix/sh/nodetypes cmd/posix/sh/nodes.c.pat cmd/posix/sh/mkbuiltins cmd/posix/sh/builtins.def cmd/posix/sh/shell.h cmd/posix/sh/mktokens
721-	CC='$(CC)' CFLAGS='$(CFLAGS)' CPPFLAGS='$(CPPFLAGS)' sh scripts/genconfig.sh sh
722-
723-cmd/posix/sh/syntax.c: cmd/posix/sh/builtins.c ;
724-cmd/posix/sh/syntax.h: cmd/posix/sh/builtins.c ;
725-cmd/posix/sh/nodes.c: cmd/posix/sh/builtins.c ;
726-cmd/posix/sh/nodes.h: cmd/posix/sh/builtins.c ;
727-cmd/posix/sh/builtins.h: cmd/posix/sh/builtins.c ;
728-cmd/posix/sh/token.h: cmd/posix/sh/builtins.c ;
729-cmd/posix/sh/mknodes: cmd/posix/sh/builtins.c ;
730-cmd/posix/sh/mksyntax: cmd/posix/sh/builtins.c ;
731-
732-$(SHOBJ): $(SH_GENHDRS)
733-
734-cmd/posix/sh/sh: $(SHOBJ) $(LIB)
735-	$(CC) $(LDFLAGS) -o $@ $(SHOBJ) $(LIB) $(LDLIBS)
736-
737-cmd/extra/yap/yap: $(YAPOBJ) $(LIB)
738-	$(CC) $(LDFLAGS) -o $@ $(YAPOBJ) $(LIB) $(LDLIBS) -ltinfo
739-
740-cmd/extra/diff3/diff3: cmd/extra/diff3/diff3.o cmd/posix/diff/xmalloc.o $(LIB)
741-	$(CC) $(LDFLAGS) -o $@ cmd/extra/diff3/diff3.o cmd/posix/diff/xmalloc.o $(LIB) $(LDLIBS)
742-
743-cmd/net/wget: cmd/net/wget.o $(LIB)
744-	$(CC) $(LDFLAGS) -o $@ cmd/net/wget.o $(LIB) $(LDLIBS) $(LDLIBS_TLS)
745-
746-cmd/dev/ar/ar: cmd/dev/ar/ar.o $(LIB)
747-	$(CC) $(LDFLAGS) -o $@ cmd/dev/ar/ar.o $(LIB) $(LDLIBS)
748-
749-LD_OBJ =\
750-	cmd/dev/xcutil/util.o\
751-	cmd/dev/xcutil/table.o\
752-	cmd/dev/xcutil/elfutil.o\
753-	cmd/dev/xcutil/archive.o\
754-	cmd/dev/ld/ld.o\
755-	cmd/dev/ld/elfobj.o
756-
757-cmd/dev/ld/ld: $(LD_OBJ) $(LIB)
758-	$(CC) $(LDFLAGS) -o $@ $(LD_OBJ) $(LIB) $(LDLIBS)
759-
760-AS_OBJ =\
761-	cmd/dev/xcutil/util.o\
762-	cmd/dev/xcutil/table.o\
763-	cmd/dev/xcutil/elfutil.o\
764-	cmd/dev/xcutil/archive.o\
765-	cmd/dev/as/as.o\
766-	cmd/dev/as/as_util.o\
767-	cmd/dev/as/emit_elf.o\
768-	cmd/dev/as/emit_macho.o\
769-	cmd/dev/as/ir_asm.o\
770-	cmd/dev/as/parse_asm.o\
771-	cmd/dev/as/arch/x64/asm_code.o\
772-	cmd/dev/as/arch/x64/ir_asm_x64.o\
773-	cmd/dev/as/arch/x64/parse_x64.o
774-
775-AS_HDRS =\
776-	cmd/dev/as/parse_asm.h\
777-	cmd/dev/as/ir_asm.h\
778-	cmd/dev/as/as_util.h\
779-	cmd/dev/as/asm_code.h\
780-	cmd/dev/as/arch/x64/inst.h
781-
782-cmd/dev/as/as: $(AS_OBJ) $(LIB)
783-	$(CC) $(LDFLAGS) -o $@ $(AS_OBJ) $(LIB) $(LDLIBS)
784-
785-cmd/dev/cc/driver.o: cmd/dev/cc/config.h
786-
787-cmd/dev/cc/cc: cmd/dev/cc/driver.o cmd/dev/cc/util.o $(LIB) cmd/dev/cc/cc1 cmd/dev/cc/cpp cmd/dev/as/as cmd/dev/ld/ld
788-	$(CC) $(LDFLAGS) -o $@ cmd/dev/cc/driver.o cmd/dev/cc/util.o $(LIB) $(LDLIBS)
789-
790-CC1_OBJ =\
791-	cmd/dev/cc/attr.o\
792-	cmd/dev/cc/decl.o\
793-	cmd/dev/cc/eval.o\
794-	cmd/dev/cc/expr.o\
795-	cmd/dev/cc/init.o\
796-	cmd/dev/cc/cc1.o\
797-	cmd/dev/cc/map.o\
798-	cmd/dev/cc/pp.o\
799-	cmd/dev/cc/qbe.o\
800-	cmd/dev/cc/scan.o\
801-	cmd/dev/cc/scope.o\
802-	cmd/dev/cc/stmt.o\
803-	cmd/dev/cc/targ.o\
804-	cmd/dev/cc/token.o\
805-	cmd/dev/cc/tree.o\
806-	cmd/dev/cc/type.o\
807-	cmd/dev/cc/utf.o\
808-	cmd/dev/cc/util.o
809-
810-CPP_OBJ =\
811-	cmd/dev/cc/attr.o\
812-	cmd/dev/cc/decl.o\
813-	cmd/dev/cc/eval.o\
814-	cmd/dev/cc/expr.o\
815-	cmd/dev/cc/init.o\
816-	cmd/dev/cc/map.o\
817-	cmd/dev/cc/pp.o\
818-	cmd/dev/cc/qbe.o\
819-	cmd/dev/cc/scan.o\
820-	cmd/dev/cc/scope.o\
821-	cmd/dev/cc/stmt.o\
822-	cmd/dev/cc/targ.o\
823-	cmd/dev/cc/token.o\
824-	cmd/dev/cc/tree.o\
825-	cmd/dev/cc/type.o\
826-	cmd/dev/cc/utf.o\
827-	cmd/dev/cc/util.o
828-
829-cmd/dev/cc/cc1: $(CC1_OBJ) $(LIB)
830-	$(CC) $(LDFLAGS) -o $@ $(CC1_OBJ) $(LIB) $(LDLIBS)
831-
832-cmd/dev/cc/cpp: cmd/dev/cc/cpp.o $(CPP_OBJ) $(LIB)
833-	$(CC) $(LDFLAGS) -o $@ cmd/dev/cc/cpp.o $(CPP_OBJ) $(LIB) $(LDLIBS)
834-
835-cmd/dev/config.h: cmd/dev/configure
836-	sh cmd/dev/configure
837-
838-cmd/dev/cc/config.h: cmd/dev/config.h ;
839-cmd/dev/version.h: cmd/dev/config.h ;
840-
841-$(AS_OBJ) $(LD_OBJ) $(CC1_OBJ) $(CPP_OBJ) cmd/dev/ar/ar.o cmd/dev/cc/cpp.o: cmd/dev/config.h
842-
843-scripts/mk/config.mk: build.cfg scripts/genconfig.sh
844-	sh scripts/genconfig.sh
845-
846-scripts/mk/config.kv: scripts/mk/config.mk ;
847-scripts/mk/rules.mk: scripts/mk/config.mk ;
848-
849+CC       ?= cc -static
850+CFLAGS   ?= -std=c99 -Wall -Wextra -pedantic
851+CPPFLAGS ?= -Ishared -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809L \
852+            -DFEATURE_NOFORK=0 -DFEATURE_NOEXEC=0
853+# NOEXEC cannot be on the bootstrap compile, but it can
854+# be on when one compiles aruu-box with ninqu included
855+
856+NINQU_LIB_SRC =\
857+        shared/libutil/ealloc.c\
858+        shared/libutil/eprintf.c\
859+        shared/libutil/strlcpy.c\
860+        shared/libutil/strlcat.c\
861+        shared/libutil/strtonum.c\
862+        shared/libutil/fshut.c\
863+        shared/libutil/mkdirp.c\
864+        shared/libutil/recurse_dir.c
865+
866+NINQU_SRC =\
867+        cmd/dev/ninqu/main.c\
868+        cmd/dev/ninqu/strlist.c\
869+        cmd/dev/ninqu/kv.c\
870+        cmd/dev/ninqu/sexpr.c\
871+        cmd/dev/ninqu/gate.c\
872+        cmd/dev/ninqu/registry.c\
873+        cmd/dev/ninqu/fs.c\
874+        cmd/dev/ninqu/exec.c\
875+        cmd/dev/ninqu/parse.c\
876+        cmd/dev/ninqu/expand.c\
877+        cmd/dev/ninqu/schedule.c\
878+        cmd/dev/ninqu/query.c\
879+        cmd/dev/ninqu/resolve.c
880+
881+# map make's -j to ninqu's -j flag
882+NINQU ?= cmd/dev/ninqu/ninqu
883+JOBS  ?= $(filter -j%,$(MAKEFLAGS))
884+
885+# rebuilds config.set first so a build.cfg edit is picked up before
886+# ninqu compiles, ninqu also does this lazily on its own via
887+# (set-file), this just avoids the extra shell startup on every run
888+$(NINQU): $(NINQU_SRC) cmd/dev/ninqu/ninqu.h scripts/mk/config.set $(NINQU_LIB_SRC)
889+	$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(NINQU_SRC) $(NINQU_LIB_SRC)
890+
891+# FORCE has no rule, thus making any pattern
892+# that depends on it be always outdated
893+.PHONY: FORCE
894+FORCE:
895+
896+%: $(NINQU) FORCE
897+	$(NINQU) $(JOBS) $@
898+
899+clean: $(NINQU)
900+	$(NINQU) clean
901+	rm -f $(NINQU)
902+
903+scripts/mk/config.set: build.cfg scripts/genconfig.sh
904+	sh scripts/genconfig.sh config
905+
906+# stops the % pattern rule above from trying to rebuild these as if
907+# they were stale build targets
908 Makefile: ;
909+ninqu.rules: ;
910+cmd/dev/ninqu/ninqu.h: ;
911+cmd/dev/ninqu/main.c: ;
912+cmd/dev/ninqu/strlist.c: ;
913+cmd/dev/ninqu/kv.c: ;
914+cmd/dev/ninqu/sexpr.c: ;
915+cmd/dev/ninqu/gate.c: ;
916+cmd/dev/ninqu/registry.c: ;
917+cmd/dev/ninqu/fs.c: ;
918+cmd/dev/ninqu/exec.c: ;
919+cmd/dev/ninqu/parse.c: ;
920+cmd/dev/ninqu/expand.c: ;
921+cmd/dev/ninqu/schedule.c: ;
922+cmd/dev/ninqu/query.c: ;
923+cmd/dev/ninqu/resolve.c: ;
924+build.cfg: ;
925+scripts/genconfig.sh: ;
+0, -537
  1@@ -1,537 +0,0 @@
  2-#!/bin/sh
  3-# drives the build without needing a working make to bootstrap
  4-#
  5-set -e
  6-
  7-# per-run work directory, cleaned up on exit
  8-#
  9-WORKDIR="${TMPDIR:-/tmp}/aruu.$$"
 10-mkdir -p "$WORKDIR"
 11-trap 'rm -rf "$WORKDIR"' EXIT INT TERM HUP
 12-
 13-JOBS=1
 14-LOAD=0
 15-TARGET=all
 16-
 17-while [ $# -gt 0 ]; do
 18-        case "$1" in
 19-                -j)  shift; JOBS="${1:?-j requires a number}" ;;
 20-                -j*) JOBS="${1#-j}" ;;
 21-                -l)  shift; LOAD="${1:?-l requires a number}" ;;
 22-                -l*) LOAD="${1#-l}" ;;
 23-                -*)  printf 'unknown flag: %s\n' "$1" >&2; exit 1 ;;
 24-                *)   TARGET="$1" ;;
 25-        esac
 26-        shift
 27-done
 28-
 29-# timestamp comparison
 30-#
 31-mtime() {
 32-        stat -c '%Y' "$1" 2>/dev/null || stat -f '%m' "$1" 2>/dev/null || echo 0
 33-}
 34-newer_than() {
 35-        local s t
 36-        [ -e "$2" ] || return 0
 37-        s=$(mtime "$1"); t=$(mtime "$2")
 38-        [ "$s" -gt "$t" ]
 39-}
 40-any_newer_than() {
 41-        local target="$1"; shift
 42-        for src do
 43-                newer_than "$src" "$target" && return 0
 44-        done
 45-        return 1
 46-}
 47-
 48-[ -f build.cfg ] || { printf 'build.cfg not found; see the repository README\n' >&2; exit 1; }
 49-
 50-# generate config files if missing or build.cfg is newer
 51-if [ ! -f scripts/mk/config.kv ] || newer_than build.cfg scripts/mk/config.kv; then
 52-        sh scripts/genconfig.sh
 53-fi
 54-
 55-# shellcheck disable=SC1091
 56-. ./build.cfg
 57-
 58-# load config.kv if it exists to override and provide detected TLS settings
 59-if [ -f scripts/mk/config.kv ]; then
 60-        # shellcheck disable=SC1091
 61-        . ./scripts/mk/config.kv
 62-fi
 63-
 64-# cppflags derived from build.cfg; any feature_* added there is picked up automatically
 65-_feature_flags=""
 66-while IFS= read -r _line; do
 67-        _trimmed=$(printf '%s' "$_line" | tr -d ' \t')
 68-        case "$_trimmed" in
 69-                FEATURE_*=*)
 70-                        _key=${_trimmed%%=*}
 71-                        eval "_val=\$$_key"
 72-                        _feature_flags="$_feature_flags -D${_key}=${_val}"
 73-                        ;;
 74-        esac
 75-done <<EOF
 76-$(tr ';' '\n' < build.cfg)
 77-EOF
 78-unset _line _key _val _trimmed
 79-
 80-CPPFLAGS="-Ishared -DPREFIX=\\\"\$PREFIX\\\" -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_NETBSD_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_FILE_OFFSET_BITS=64$_feature_flags $CPPFLAGS_TLS"
 81-LDFLAGS="${LDFLAGS:-} $LDFLAGS_TLS"
 82-TLS_LDLIBS="$LDLIBS_TLS"
 83-
 84-CC=${CC:-cc}
 85-AR=${AR:-ar}
 86-ARFLAGS=${ARFLAGS:-rc}
 87-RANLIB=${RANLIB:-ranlib}
 88-PREFIX=${PREFIX:-/usr/local}
 89-MANPREFIX=${MANPREFIX:-$PREFIX/share/man}
 90-
 91-# shared headers, most objects depend on these
 92-HDR=$(echo shared/*.h)
 93-LIB="shared/libredline/libredline.a shared/libutil/libutil.a shared/libutf/libutf.a"
 94-
 95-# set by multi-file builders for generated headers, cleared after use
 96-EXTRA_HDR=
 97-
 98-# parallel job queue
 99-# objs_for must not run in a subshell, enqueue writes _qn/_queue in the current shell
100-#
101-_qn=0
102-_queue=""
103-_objs=""
104-# wraps arg in single quotes, escaping embedded ones, for safe eval
105-shquote() {
106-        printf "'"
107-        printf '%s' "$1" | sed "s/'/'\\\\''/g"
108-        printf "'"
109-}
110-enqueue() {
111-        local desc="$1" cmd="$2"
112-        _qn=$((_qn + 1))
113-        local s="$WORKDIR/j${_qn}.sh"
114-        {
115-                printf '#!/bin/sh\n'
116-                printf 'printf "%%s\\n" %s\n' "$(shquote "$desc")"
117-                printf 'eval %s\n'            "$(shquote "$cmd")"
118-        } > "$s"
119-        _queue="$_queue $_qn"
120-}
121-# silently skipped on systems without /proc/loadavg
122-_wait_for_load() {
123-        [ "$LOAD" -eq 0 ] && return 0
124-        local avg
125-        while true; do
126-                avg=$(awk '{printf "%d", int($1 + 0.5)}' /proc/loadavg 2>/dev/null) || return 0
127-                [ "$avg" -le "$LOAD" ] && return 0
128-                sleep 1
129-        done
130-}
131-drain() {
132-        [ -z "$_queue" ] && return 0
133-        local n
134-        _wait_for_load
135-        if [ "$JOBS" -le 1 ]; then
136-                for n in $_queue; do
137-                        sh "$WORKDIR/j${n}.sh" || { printf 'error: build step failed\n' >&2; exit 1; }
138-                done
139-        else
140-                # xargs -P (POSIX 2024), -n 1 so each path becomes its own sh invocation
141-                for n in $_queue; do
142-                        printf '%s\n' "$WORKDIR/j${n}.sh"
143-                done | xargs -P "$JOBS" -n 1 sh || { printf 'error: build step failed\n' >&2; exit 1; }
144-        fi
145-        _queue=""
146-}
147-
148-# compile and link routines
149-#
150-compile_c() {
151-        local src="$1" obj="$2"; shift 2
152-        # shellcheck disable=SC2086
153-        any_newer_than "$obj" "$src" $HDR $EXTRA_HDR || return 0
154-        enqueue "  CC  $obj" "$CC $* $CPPFLAGS $CFLAGS -o $obj -c $src"
155-}
156-link_bin() {
157-        local bin="$1" objs="" libs="" sep=0; shift
158-        for a do
159-                if   [ "$a" = "--" ]; then sep=1
160-                elif [ "$sep" = 0  ]; then objs="$objs $a"
161-                else                       libs="$libs $a"
162-                fi
163-        done
164-        drain
165-        # shellcheck disable=SC2086
166-        any_newer_than "$bin" $objs || return 0
167-        printf '  LD  %s\n' "$bin"
168-        # shellcheck disable=SC2086
169-        if [ "${bin##*/}" = "wget" ]; then
170-                eval "$CC $LDFLAGS -o $bin $objs $libs $LDLIBS $TLS_LDLIBS"
171-        else
172-                eval "$CC $LDFLAGS -o $bin $objs $libs $LDLIBS"
173-        fi
174-}
175-# writes to _objs rather than stdout so the caller avoids a subshell that
176-# would discard enqueued jobs
177-objs_for() {
178-        local dir="$1" skip=" $2 " flags="$3" src base
179-        _objs=""
180-        for src in "$dir"/*.c; do
181-                [ -f "$src" ] || continue
182-                base=${src##*/}
183-                case "$skip" in *" $base "*) continue ;; esac
184-                compile_c "$src" "${src%.c}.o" $flags
185-                _objs="$_objs ${src%.c}.o"
186-        done
187-}
188-
189-# static libraries
190-# both directories enqueued before the single drain so they compile in parallel
191-#
192-build_lib() {
193-        local utf_objs util_objs redline_objs
194-        objs_for shared/libutf "" ""
195-        utf_objs="$_objs"
196-        objs_for shared/libutil "" ""
197-        util_objs="$_objs"
198-        objs_for shared/libredline "" ""
199-        redline_objs="$_objs"
200-        drain
201-        # shellcheck disable=SC2086
202-        any_newer_than shared/libutf/libutf.a $utf_objs && {
203-                printf '  AR  shared/libutf/libutf.a\n'
204-                $AR $ARFLAGS shared/libutf/libutf.a $utf_objs
205-                $RANLIB shared/libutf/libutf.a
206-        }
207-        # shellcheck disable=SC2086
208-        any_newer_than shared/libutil/libutil.a $util_objs && {
209-                printf '  AR  shared/libutil/libutil.a\n'
210-                $AR $ARFLAGS shared/libutil/libutil.a $util_objs
211-                $RANLIB shared/libutil/libutil.a
212-        }
213-        # shellcheck disable=SC2086
214-        any_newer_than shared/libredline/libredline.a $redline_objs && {
215-                printf '  AR  shared/libredline/libredline.a\n'
216-                $AR $ARFLAGS shared/libredline/libredline.a $redline_objs
217-                $RANLIB shared/libredline/libredline.a
218-        }
219-        return 0
220-}
221-
222-# per-tool category builder
223-#
224-cfgvar() {
225-        local cat="$1" base="$2"
226-        printf 'BUILD_%s_%s' "$cat" "$base" | tr 'a-z-' 'A-Z_'
227-}
228-
229-# individual flag overrides group flag
230-cfg_enabled() {
231-        local _var="$1" _v
232-        while [ -n "$_var" ]; do
233-                eval "_v=\${${_var}-UNSET}"
234-                if [ "$_v" = "1" ]; then
235-                        return 0
236-                elif [ "$_v" = "0" ]; then
237-                        return 1
238-                fi
239-                case "$_var" in
240-                        *_[!_]*) _var=$(printf '%s' "$_var" | sed 's/_[^_]*$//') ;;
241-                        *)       break ;;
242-                esac
243-        done
244-        return 1
245-}
246-
247-build_simple_tools() {
248-        local cat="$1" src base var
249-        for src in cmd/"$cat"/*.c; do
250-                [ -f "$src" ] || continue
251-                base=${src##*/}; base=${base%.c}
252-                var=$(cfgvar "$cat" "$base")
253-                cfg_enabled "$var" || continue
254-                local bin="cmd/$cat/$base"
255-                # shellcheck disable=SC2086
256-                any_newer_than "$bin" "$src" $HDR $EXTRA_HDR $LIB || continue
257-                if [ "$base" = "diff" ]; then
258-                        enqueue "  CC  $bin" "$CC $src $CPPFLAGS $CFLAGS $LDFLAGS -o $bin $LIB -lm $LDLIBS"
259-                else
260-                        enqueue "  CC  $bin" "$CC $src $CPPFLAGS $CFLAGS $LDFLAGS -o $bin $LIB $LDLIBS"
261-                fi
262-        done
263-        drain
264-}
265-
266-build_awk() {
267-        cfg_enabled BUILD_POSIX_AWK || return 0
268-        local dir=cmd/posix/awk
269-        CC="$CC" CFLAGS="$CFLAGS" YACC="$YACC" sh scripts/genconfig.sh awk
270-        EXTRA_HDR="$dir/awk.h $dir/awkgram.tab.h $dir/proto.h"
271-        objs_for "$dir" maketab.c "-I$dir"
272-        EXTRA_HDR=
273-        link_bin "$dir/awk" $_objs -- $LIB -lm
274-}
275-
276-build_sh() {
277-        cfg_enabled BUILD_POSIX_SH || return 0
278-        local dir=cmd/posix/sh
279-        CC="$CC" CFLAGS="$CFLAGS" CPPFLAGS="$CPPFLAGS" sh scripts/genconfig.sh sh
280-        EXTRA_HDR="$dir/syntax.h $dir/nodes.h $dir/builtins.h $dir/token.h"
281-        objs_for "$dir" "mknodes.c mksyntax.c" "-DSHELL -I$dir"
282-        EXTRA_HDR=
283-        link_bin "$dir/sh" $_objs -- $LIB
284-}
285-
286-build_make() {
287-        cfg_enabled BUILD_POSIX_MAKE || return 0
288-        local dir=cmd/posix/make
289-        EXTRA_HDR="$dir/make.h"
290-        objs_for "$dir" "" ""
291-        EXTRA_HDR=
292-        link_bin "$dir/make" $_objs -- $LIB
293-}
294-
295-build_bc() {
296-        cfg_enabled BUILD_POSIX_BC || return 0
297-        local src=cmd/posix/bc.c
298-        YACC="$YACC" sh scripts/genconfig.sh bc
299-        # shellcheck disable=SC2086
300-        any_newer_than cmd/posix/bc "$src" $HDR $EXTRA_HDR $LIB || return 0
301-        printf '  CC  cmd/posix/bc\n'
302-        eval "$CC $src $CPPFLAGS $CFLAGS $LDFLAGS -o cmd/posix/bc $LIB $LDLIBS"
303-}
304-
305-build_posix() {
306-        cfg_enabled BUILD_POSIX_GETCONF && sh scripts/genconfig.sh getconf
307-        build_simple_tools posix
308-        build_awk
309-        build_bc
310-        build_sh
311-        build_make
312-}
313-
314-# multi-file dev toolchain
315-#
316-build_ar() {
317-        cfg_enabled BUILD_DEV_AR || return 0
318-        local dir=cmd/dev/ar
319-        objs_for "$dir" tinyar.c "-I$dir"
320-        link_bin "$dir/ar" $_objs -- $LIB
321-}
322-
323-build_as() {
324-        cfg_enabled BUILD_DEV_AS || return 0
325-        local dir="cmd/dev/as"
326-        local flags="-Icmd/dev/xcutil -I$dir -Ishared"
327-        local objs=""
328-        objs_for "cmd/dev/xcutil" "" "$flags"
329-        objs="$objs $_objs"
330-        objs_for "$dir" "" "$flags"
331-        objs="$objs $_objs"
332-        objs_for "$dir/arch/x64" "" "-Icmd/dev/xcutil -I$dir -I$dir/arch/x64 -Ishared"
333-        objs="$objs $_objs"
334-        link_bin "$dir/as" $objs -- $LIB
335-}
336-
337-build_ld() {
338-        cfg_enabled BUILD_DEV_LD || return 0
339-        local dir="cmd/dev/ld"
340-        local flags="-Icmd/dev/xcutil -I$dir"
341-        local objs=""
342-        objs_for "cmd/dev/xcutil" "" "$flags"
343-        objs="$objs $_objs"
344-        objs_for "$dir" "" "$flags"
345-        objs="$objs $_objs"
346-        link_bin "$dir/ld" $objs -- $LIB
347-}
348-
349-build_cc() {
350-        cfg_enabled BUILD_DEV_CC || return 0
351-        local dir="cmd/dev/cc"
352-        local cflags="-I$dir"
353-        local common
354-
355-        # compiled once, linked into both cc1 and cpp
356-        objs_for "$dir" "driver.c cc1.c cpp.c" "$cflags"
357-        common="$_objs"
358-        drain
359-
360-        compile_c "$dir/cc1.c"    "$dir/cc1.o"    $cflags
361-        compile_c "$dir/cpp.c"    "$dir/cpp.o"    $cflags
362-        compile_c "$dir/driver.c" "$dir/driver.o" $cflags
363-        drain
364-
365-        link_bin "$dir/cc1" "$dir/cc1.o"    $common -- $LIB
366-        link_bin "$dir/cpp" "$dir/cpp.o"    $common -- $LIB
367-        link_bin "$dir/cc"  "$dir/driver.o" "$dir/util.o" -- $LIB
368-}
369-
370-build_dev() {
371-        [ -f cmd/dev/configure ] && sh cmd/dev/configure
372-        build_ar
373-        build_as
374-        build_ld
375-        build_cc
376-}
377-
378-# manual page generation
379-#
380-man_section() {
381-        case "$1" in
382-                */linux/*|*/net/*|*/xsi/*) printf '8\n' ;;
383-                *)                         printf '1\n' ;;
384-        esac
385-}
386-
387-build_man_for() {
388-        local var="$1" src="$2" name="$3" sec out_mdoc out_txt
389-        cfg_enabled "$var" || return 0
390-        [ -x scripts/mkman/mkman ] || { printf 'error: mkman not built\n' >&2; exit 1; }
391-
392-        grep -qE '!man|\?man' "$src" || return 0
393-
394-        sec=$(man_section "$src")
395-
396-        mkdir -p "man/man${sec}"
397-        out_mdoc="man/man${sec}/${name}.${sec}"
398-        out_txt="man/man${sec}/${name}.${sec}.txt"
399-        if any_newer_than "$out_mdoc" "$src" scripts/mk/config.kv scripts/mkman/mkman; then
400-                printf '  MAN   %s\n' "$out_mdoc"
401-                scripts/mkman/mkman -fmt mdoc -config scripts/mk/config.kv -section "$sec" "$src" > "$out_mdoc"
402-        fi
403-        if any_newer_than "$out_txt" "$src" scripts/mk/config.kv scripts/mkman/mkman; then
404-                printf '  MAN   %s\n' "$out_txt"
405-                scripts/mkman/mkman -fmt txt -config scripts/mk/config.kv -section "$sec" "$src" > "$out_txt"
406-        fi
407-}
408-
409-build_man() {
410-        local src cat base sub name var
411-        if [ ! -x scripts/mkman/mkman ] || any_newer_than scripts/mkman/mkman scripts/mkman/main.go scripts/mkman/page.go scripts/mkman/parse.go scripts/mkman/mdoc.go; then
412-                printf '  GO    scripts/mkman/mkman\n'
413-                (cd scripts/mkman && go build -o mkman .)
414-        fi
415-        find cmd -type f -name '*.c' | while read -r src; do
416-                sub=${src#cmd/}
417-                cat=${sub%%/*}
418-                base=${src##*/}
419-                base=${base%.c}
420-                name=${sub#*/}
421-                if [ "${name}" != "${base}.c" ]; then
422-                        name=${name%%/*}
423-                else
424-                        name=$base
425-                fi
426-                var=$(cfgvar "$cat" "$name")
427-                build_man_for "$var" "$src" "$name"
428-        done
429-}
430-
431-# install and clean
432-#
433-do_install() {
434-        local bin f sec
435-        mkdir -p "$PREFIX/bin" "$MANPREFIX/man1" "$MANPREFIX/man8"
436-        find cmd -type f ! -name '*.*' -perm -100 | while read -r bin; do
437-                printf '  INSTALL %s/bin/%s\n' "$PREFIX" "${bin##*/}"
438-                cp "$bin" "$PREFIX/bin/${bin##*/}"
439-                chmod 755 "$PREFIX/bin/${bin##*/}"
440-        done
441-        find man/man1 man/man8 -type f -name '*.[18]' 2>/dev/null | while read -r f; do
442-                sec=${f%/*}; sec=${sec##*/}
443-                cp "$f" "$MANPREFIX/${sec}/${f##*/}"
444-        done
445-}
446-
447-do_regen() {
448-        # 1. regenerate config
449-        sh scripts/genconfig.sh
450-
451-        # 2. re-read the new config.kv
452-        if [ -f scripts/mk/config.kv ]; then
453-                # shellcheck disable=SC1091
454-                . scripts/mk/config.kv
455-        fi
456-
457-        # 3. clean and generate files using scripts/genconfig.sh
458-        CC="$CC" CFLAGS="$CFLAGS" CPPFLAGS="$CPPFLAGS" YACC="$YACC" sh scripts/genconfig.sh clean
459-        CC="$CC" CFLAGS="$CFLAGS" CPPFLAGS="$CPPFLAGS" YACC="$YACC" sh scripts/genconfig.sh all
460-}
461-
462-do_clean() {
463-        printf '  CLEAN object files\n'
464-        find shared cmd -name '*.o' -exec rm -f {} +
465-        printf '  CLEAN static libraries\n'
466-        find shared -name '*.a' -exec rm -f {} +
467-        printf '  CLEAN compiled binaries\n'
468-        find cmd -type f ! -name '*.*' -perm -100 -exec rm -f {} +
469-        sh scripts/genconfig.sh clean
470-        rm -f scripts/mk/config.mk scripts/mk/config.kv scripts/mk/rules.mk shared/libutil/nofork_list.h
471-        printf '  CLEAN build tools\n'
472-        rm -f scripts/mkman/mkman
473-        printf '  CLEAN dev artifacts\n'
474-        rm -f cmd/dev/cc/cc1 cmd/dev/cc/cpp cmd/dev/as/as cmd/dev/ld/ld cmd/dev/ar/ar
475-        rm -f shared/libaruuelf.so
476-        rm -f cmd/dev/config.h cmd/dev/cc/config.h cmd/dev/version.h
477-        printf '  CLEAN misc\n'
478-        rm -rf man/man1 man/man8
479-        rm -rf aruu-box .box
480-        printf '  CLEAN done\n'
481-}
482-
483-case "$TARGET" in
484-        all)
485-                build_lib
486-                build_posix
487-                build_dev
488-        build_simple_tools linux
489-        build_simple_tools net
490-        build_simple_tools xsi
491-        build_simple_tools pseudo
492-        build_simple_tools extra
493-                ;;
494-        lib)             build_lib ;;
495-        posix)           build_lib; build_posix ;;
496-        dev)             build_lib; build_dev ;;
497-        make)            build_lib; build_make ;;
498-        linux|net|xsi|pseudo|extra)
499-                         build_lib; build_simple_tools "$TARGET" ;;
500-        regen)           do_regen ;;
501-        clean)           do_clean ;;
502-        man)             build_man ;;
503-        install)
504-                build_lib
505-                build_posix
506-                build_dev
507-                build_simple_tools linux
508-                build_simple_tools net
509-                build_simple_tools xsi
510-                build_simple_tools pseudo
511-                build_simple_tools extra
512-                build_man
513-                do_install
514-                ;;
515-        help)
516-                printf 'usage: sh Makefile.sh [-jN] [-lN] [all|clean|install|man|lib|posix|linux|net|xsi|pseudo|extra|dev|make|regen|help]\n'
517-                printf '\nTargets:\n'
518-                printf '  all       Build all enabled utilities\n'
519-                printf '  clean     Clean built objects and binaries\n'
520-                printf '  install   Install binaries and man pages\n'
521-                printf '  man       Generate man pages\n'
522-                printf '  lib       Build core libraries\n'
523-                printf '  posix     Build POSIX utilities\n'
524-                printf '  linux     Build Linux utilities\n'
525-                printf '  net       Build Network utilities\n'
526-                printf '  xsi       Build XSI utilities\n'
527-                printf '  pseudo    Build Pseudo utilities\n'
528-                printf '  extra     Build Extra utilities\n'
529-                printf '  dev       Build Dev toolchain\n'
530-                printf '  make      Build POSIX make\n'
531-                printf '  regen     Regenerate configuration and all generated source files\n'
532-                printf '  help      Show this help message\n'
533-                ;;
534-        *)
535-                printf 'usage: sh Makefile.sh [-jN] [-lN] [all|clean|install|man|lib|posix|linux|net|xsi|pseudo|extra|dev|make|regen|help]\n' >&2
536-                exit 1
537-                ;;
538-esac
M README
+42, -17
  1@@ -9,7 +9,8 @@ on their toybox applet equivalent.
  2 The codebase has diverged a lot since the project started. And we have
  3 a myriad of utilities which were written specifically for ARUU.
  4 We also have greatly simplified ports of components from other systems
  5-and projects, such as FreeBSD sh, minix yap (pager), onetrueawk, etc.
  6+and projects, such as FreeBSD sh, minix yap (pager (thanks shrub)),
  7+onetrueawk, etc.
  8 
  9 OVERVIEW
 10 ========
 11@@ -19,10 +20,11 @@ aruu builds two things:
 12   1. individual tool binaries (default 'make all')
 13   2. aruu-box: a multi-call binary containing every tool ('make box')
 14 
 15-The multi-call binary is built by scripts/mkbox. It uses objcopy to
 16-rename each tool's 'main' symbol, generates a C dispatch table, then
 17-links everything into a single statically-dispatched binary. Invoke
 18-tools by symlinking aruu-box to the tool name, or by running:
 19+The multi-call binary is built by ninqu's box group (BOX_OBJ, BOX_DISPATCH,
 20+and BOX in ninqu.rules). Each enabled tool gets objcopied so only its
 21+renamed main stays global, scripts/genbox.sh turns the resulting object
 22+paths into a C dispatch table, and everything links into one binary.
 23+Invoke tools by symlinking aruu-box to the tool name, or by running:
 24 
 25     aruu-box <command> [args...]
 26 
 27@@ -43,6 +45,23 @@ Optional:
 28 
 29   yacc     only needed if rebuilding bc from bc.y
 30 
 31+HOW THE BUILD WORKS
 32+====================
 33+
 34+make builds cmd/dev/ninqu/ninqu, then forwards whatever target you
 35+gave it (make all, make box, make clean, ...) straight to that binary.
 36+
 37+ninqu reads ninqu.rules, which pulls in scripts/mk/config.set. That
 38+file is generated by scripts/genconfig.sh, which reads build.cfg,
 39+resolves every preset and per-tool override, and writes config.set
 40+(read by ninqu), config.kv (shell-sourced by genbox.sh), and
 41+scripts/mk/features.h (the resolved FEATURE_* set, pulled into every
 42+compile with -include).
 43+
 44+To change what gets built, or see what a preset turns on or off,
 45+edit or read build.cfg. That is the one file genconfig.sh expands
 46+from, everything downstream of it is generated and safe to delete.
 47+
 48 BUILDING
 49 ========
 50 
 51@@ -58,7 +77,8 @@ Both targets can be combined:
 52 
 53     make all box
 54 
 55-Configuration is in config.mk. You can override compile flags via make
 56+Configuration is in build.cfg. You can override compile flags via make
 57+or with ninqu's -D flag (see HOW THE BUILD WORKS above).
 58 
 59 INSTALL
 60 =======
 61@@ -69,14 +89,18 @@ INSTALL
 62 SOURCE LAYOUT
 63 =============
 64 
 65-    Makefile               pdpmake-compatible build rules
 66+    Makefile          builds cmd/dev/ninqu/ninqu, forwards to it
 67+    ninqu.rules       all real build knowledge, read by ninqu
 68+    build.cfg         presets and per-tool overrides
 69     scripts/
 70-      mkbox                builds aruu-box from compiled tool objects
 71-      getconf.sh           generates cmd/posix/getconf.h at build time
 72+      genconfig.sh    build.cfg -> config.set/config.kv/features.h
 73+      genbox.sh       writes aruu-box's dispatch table
 74+      getconf.sh      generates cmd/posix/getconf.h at build time
 75+      mkbox           legacy, superseded by ninqu's box group
 76     shared/
 77-      *.h                  project headers (util.h, arg.h, utf.h, ...)
 78-      libutil/             utility library (eprintf, ealloc, recurse, ...)
 79-      libutf/              UTF-8/rune library
 80+      *.h             project headers (util.h, arg.h, utf.h, ...)
 81+      libutil/        utility library (eprintf, ealloc, recurse, ...)
 82+      libutf/         UTF-8/rune library
 83     cmd/
 84       posix/          POSIX.1-2008 tools (sh, make, grep, sed, pax, ...)
 85       linux/          Linux-specific tools (mount, dmesg, insmod, ...)
 86@@ -84,8 +108,9 @@ SOURCE LAYOUT
 87       pseudo/         widely-used non-POSIX tools (tar, stat, seq, ...)
 88       xsi/            XSI-extension tools (mknod, su, passwd)
 89       extra/          third-party-flavored extras (yap, b3sum, fdisk, ...)
 90-      dev/            small toolchain (as, cc, ld, ar); not built by
 91-                      default, see PRESET_UTILS in build.cfg
 92+      dev/            small toolchain tooling (ninqu, in the future: as, cc, ld, ar)
 93+
 94+      See PRESET_UTILS in build.cfg to understand, en/dis/able the build of a set
 95 
 96 TOOLS
 97 =====
 98@@ -125,9 +150,9 @@ extra:
 99 
100 TODOs / PENDING RELEASE
101 =======================
102-init - situation/sctl rewrite
103-troff - neatroff
104-man - unsure
105+init - vendor cowmonk's
106+troff - neatroff? create cmd/9?
107+man - unsure, this is not a fun task
108 vendor and publish my compiler toolchain (small binutils + cproc), even if incomplete - unsure
109 
110 LICENSE
+19, -14
 1@@ -161,7 +161,7 @@ FEATURE_SH_HISTEDIT=1
 2 
 3 # FEATURE_NOEXEC: when set to 1, wexec/wpopen/wsystem can execute registered
 4 # tool mains directly inside the forked child process (skipping execve)
 5-# This is equivalent to Busybox noexec applet support.
 6+# This is equivalent to Busybox noexec applet support
 7 # This applies to ALL commands contained within aruu-box, you may also
 8 # specify if some specific command is not an attack vector to also be nofork
 9 # via enabling FEATURE_NOFORK=1 and specifying those commands in NOFORK_LIST
10@@ -177,7 +177,7 @@ FEATURE_SH_HISTEDIT=1
11 # (like cat, echo, pwd, test) directly in the current process without spawning
12 # a subprocess (noexec+nofork). This is equivalent to Busybox nofork applet support
13 #
14-FEATURE_NOFORK=1
15+#FEATURE_NOFORK=1
16 #
17 # FEATURE_NOFORK_OVERRIDE: only meaningful when FEATURE_NOFORK=1
18 # when set to 1, ARUU_NOFORK=0 in the environment forces fork for nofork commands
19@@ -192,25 +192,30 @@ NOFORK_LIST="true false echo pwd test [ basename dirname cat clear yes"
20 #
21 
22 BUILD_EXTRA_YAP=0
23+if pkg-config --exists tinfo 2>/dev/null; then
24+    BUILD_EXTRA_YAP=1
25+fi
26 
27 # tls backend: set exactly one to 1 to enable tls
28 #
29 FEATURE_USE_BEARSSL=0
30-FEATURE_USE_LIBRESSL=0
31+FEATURE_USE_LIBTLS=0
32 FEATURE_USE_OPENSSL=0
33-FEATURE_USE_SSL=0
34+FEATURE_USE_SSL=1
35 #
36-if [ "$FEATURE_USE_SSL" = 1 ]; then
37-    if [ "$FEATURE_USE_BEARSSL" = 0 ] && [ "$FEATURE_USE_LIBRESSL" = 0 ] && [ "$FEATURE_USE_OPENSSL" = 0 ]; then
38-        if pkg-config --exists libbearssl 2>/dev/null || pkg-config --exists bearssl 2>/dev/null; then
39-            FEATURE_USE_BEARSSL=1
40-        elif pkg-config --exists libtls 2>/dev/null; then
41-            FEATURE_USE_LIBRESSL=1
42-        elif pkg-config --exists openssl 2>/dev/null; then
43-            FEATURE_USE_OPENSSL=1
44-        fi
45+if [ "$FEATURE_USE_SSL" = 1 ] &&
46+   [ "$FEATURE_USE_BEARSSL$FEATURE_USE_LIBTLS$FEATURE_USE_OPENSSL" = "000" ]
47+then
48+    if pkg-config --exists libbearssl 2>/dev/null || pkg-config --exists bearssl 2>/dev/null; then
49+        FEATURE_USE_BEARSSL=1
50+    elif pkg-config --exists libtls 2>/dev/null; then
51+        FEATURE_USE_LIBTLS=1
52+    elif pkg-config --exists openssl 2>/dev/null; then
53+        FEATURE_USE_OPENSSL=1
54     fi
55 fi
56+
57+unset FEATURE_USE_SSL
58 # auto-detect TLS/SSL flags based on selected backend
59 CPPFLAGS_TLS="" LDFLAGS_TLS="" LDLIBS_TLS="" PKG_NAME=""
60 if [ "$FEATURE_USE_BEARSSL" = 1 ]; then
61@@ -219,7 +224,7 @@ if [ "$FEATURE_USE_BEARSSL" = 1 ]; then
62     elif pkg-config --exists bearssl 2>/dev/null; then
63         PKG_NAME="bearssl"
64     fi
65-elif [ "$FEATURE_USE_LIBRESSL" = 1 ]; then
66+elif [ "$FEATURE_USE_LIBTLS" = 1 ]; then
67     if pkg-config --exists libtls 2>/dev/null; then
68         PKG_NAME="libtls"
69     else
+294, -0
  1@@ -0,0 +1,294 @@
  2+#include "ninqu.h"
  3+
  4+#include <errno.h>
  5+#include <libgen.h>
  6+#include <limits.h>
  7+#include <stdio.h>
  8+#include <stdlib.h>
  9+#include <string.h>
 10+#include <sys/wait.h>
 11+#include <unistd.h>
 12+
 13+int jobs_n = 1;
 14+int summary_mode;
 15+int failed_any;
 16+
 17+/* fork, chdir, redirect, exec. the child _exit on error so the
 18+ * parent never sees a stale pid */
 19+pid_t
 20+spawn_inst(struct Inst *inst, struct StrList *argv)
 21+{
 22+  pid_t pid = fork();
 23+  if (pid < 0)
 24+    eprintf("fork:");
 25+  if (pid != 0)
 26+    return pid;
 27+
 28+  if (inst->workdir[0] && chdir(inst->workdir) != 0)
 29+    eprintf("chdir %s:", inst->workdir);
 30+  if (inst->redirect) {
 31+    FILE *f = fopen(inst->out, "w");
 32+    if (!f)
 33+      eprintf("open %s:", inst->out);
 34+    if (dup2(fileno(f), STDOUT_FILENO) < 0)
 35+      _exit(127);
 36+    fclose(f);
 37+  }
 38+  child_execvp(sl_argv(argv));
 39+}
 40+
 41+/* fork a supervisor that chains each stage stdout into the next
 42+ * stdin. the parent waits on the supervisor, so the whole pipeline
 43+ * has one exit status */
 44+pid_t
 45+spawn_pipe(struct Inst *inst)
 46+{
 47+  pid_t pid = fork();
 48+  if (pid < 0)
 49+    eprintf("fork:");
 50+  if (pid != 0)
 51+    return pid;
 52+
 53+  if (inst->workdir[0] && chdir(inst->workdir) != 0)
 54+    eprintf("chdir %s:", inst->workdir);
 55+
 56+  {
 57+    int k, prev_read = -1;
 58+    for (k = 0; k < inst->cmd.nstages; k++) {
 59+      int   p[2] = {-1, -1};
 60+      pid_t sp;
 61+      if (k < inst->cmd.nstages - 1) {
 62+        if (pipe(p) != 0)
 63+          _exit(127);
 64+      }
 65+      sp = fork();
 66+      if (sp < 0)
 67+        _exit(127);
 68+      if (sp == 0) {
 69+        if (prev_read >= 0) {
 70+          if (dup2(prev_read, STDIN_FILENO) < 0)
 71+            _exit(127);
 72+          close(prev_read);
 73+        }
 74+        if (k < inst->cmd.nstages - 1) {
 75+          close(p[0]);
 76+          if (dup2(p[1], STDOUT_FILENO) < 0)
 77+            _exit(127);
 78+          close(p[1]);
 79+        }
 80+        child_execvp(sl_argv(inst->cmd.stages[k]));
 81+      }
 82+      if (prev_read >= 0)
 83+        close(prev_read);
 84+      if (k < inst->cmd.nstages - 1) {
 85+        close(p[1]);
 86+        prev_read = p[0];
 87+      }
 88+    }
 89+    for (k = 0; k < inst->cmd.nstages; k++)
 90+      wait(NULL);
 91+    _exit(0);
 92+  }
 93+}
 94+
 95+/* mkdir -p the directory an instance writes into, so a rule (out)
 96+ * can point at a path that does not exist yet */
 97+void
 98+mk_out_dir(const char *out)
 99+{
100+  char  tmp[PATH_MAX];
101+  char *dir;
102+
103+  if (!out || !out[0])
104+    return;
105+  estrlcpy(tmp, out, sizeof tmp);
106+  dir = dirname(tmp);
107+  if (strcmp(dir, ".") == 0 || strcmp(dir, "/") == 0)
108+    return;
109+  mkdirp(dir, 0777, 0777);
110+}
111+
112+/* print the rule line and flush before the child runs, so the
113+ * announcement appears before the child own output */
114+void
115+announce_inst(struct Inst *inst)
116+{
117+  mk_out_dir(inst->out);
118+  printf("  %-10s %s\n", rules[inst->rule_idx].name, inst->out[0] ? inst->out : "(phony)");
119+  fflush(stdout);
120+}
121+
122+/* run one instance synchronously. exits on failure since a missing
123+ * producer is unrecoverable */
124+void
125+run_inst_sync(int idx)
126+{
127+  struct Inst *inst = &insts[idx];
128+  pid_t        pid;
129+  int          status;
130+
131+  if (!inst_stale(inst))
132+    return;
133+  announce_inst(inst);
134+  pid = inst->cmd.is_pipe ? spawn_pipe(inst) : spawn_inst(inst, &inst->cmd.argv);
135+  if (waitpid(pid, &status, 0) < 0)
136+    eprintf("waitpid:");
137+  if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0))
138+    eprintf("manifest: producer failed (rule %s)\n", rules[inst->rule_idx].name);
139+}
140+
141+/* run a batch of independent instances concurrently, at most jobs_n
142+ * at a time. all instances in the batch are at the same kahn level */
143+void
144+run_batch(int *idxs, int n)
145+{
146+  int    next = 0, running = 0;
147+  pid_t *pids = emalloc((size_t)(n > 0 ? n : 1) * sizeof *pids);
148+  int   *slot = emalloc((size_t)(n > 0 ? n : 1) * sizeof *slot);
149+
150+  while (next < n || running > 0) {
151+    while (running < jobs_n && next < n) {
152+      struct Inst *inst = &insts[idxs[next]];
153+      pid_t        pid;
154+
155+      if (!inst_stale(inst)) {
156+        next++;
157+        continue;
158+      }
159+      announce_inst(inst);
160+
161+      pid           = inst->cmd.is_pipe ? spawn_pipe(inst) : spawn_inst(inst, &inst->cmd.argv);
162+      pids[running] = pid;
163+      slot[running] = idxs[next];
164+      running++;
165+      next++;
166+    }
167+    if (running > 0) {
168+      int   status;
169+      pid_t done = wait(&status);
170+      int   i;
171+      if (done < 0)
172+        eprintf("wait:");
173+      for (i = 0; i < running; i++) {
174+        if (pids[i] == done) {
175+          if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) {
176+            char *cmd = sl_join(&insts[slot[i]].cmd.argv);
177+            weprintf("rule %s failed: %s\n", rules[insts[slot[i]].rule_idx].name, cmd);
178+            free(cmd);
179+            failed_any = 1;
180+          }
181+          pids[i] = pids[running - 1];
182+          slot[i] = slot[running - 1];
183+          running--;
184+          break;
185+        }
186+      }
187+    }
188+  }
189+  free(pids);
190+  free(slot);
191+}
192+
193+/* run an instance and all its transitive deps in order, so a
194+ * producer like LIBUTIL does not archive .o files that have not
195+ * been compiled yet */
196+void
197+run_inst_with_deps(int idx)
198+{
199+  struct Inst *inst = &insts[idx];
200+  int          i;
201+
202+  for (i = 0; i < inst->dep_rule_names.n; i++) {
203+    struct Rule *dep = rule_find(inst->dep_rule_names.v[i]);
204+    int          k;
205+    if (!dep)
206+      continue;
207+    for (k = 0; k < dep->n_inst; k++)
208+      dep_push(inst, dep->inst_idx[k]);
209+  }
210+  for (i = 0; i < inst->n_dep; i++)
211+    run_inst_with_deps(inst->dep_inst[i]);
212+  run_inst_sync(idx);
213+}
214+
215+/* bootstrap: run the producer for path only if path does not exist.
216+ * after that the normal dependency graph takes over */
217+void
218+materialize_if_missing(const char *path)
219+{
220+  struct Rule *r;
221+
222+  if (file_exists(path))
223+    return;
224+  r = rule_find_output(path);
225+  if (!r)
226+    r = rule_find_produces(base_of(path));
227+  if (!r)
228+    return;
229+  expand_rule(r->name);
230+  if (r->n_inst > 0) {
231+    int k;
232+    for (k = 0; k < r->n_inst; k++)
233+      run_inst_with_deps(r->inst_idx[k]);
234+  }
235+}
236+
237+/* run argv synchronously, return its stdout as a string. used by
238+ * (set NAME (capture (exec ...))) at parse time */
239+char *
240+capture_argv(struct StrList *argv)
241+{
242+  int    fds[2];
243+  pid_t  pid;
244+  char  *buf;
245+  size_t cap, len;
246+  int    status;
247+
248+  if (argv->n == 0)
249+    eprintf("capture: empty argv\n");
250+  if (pipe(fds) != 0)
251+    eprintf("capture: pipe:");
252+
253+  pid = fork();
254+  if (pid < 0)
255+    eprintf("capture: fork:");
256+  if (pid == 0) {
257+    close(fds[0]);
258+    if (dup2(fds[1], STDOUT_FILENO) < 0)
259+      _exit(127);
260+    close(fds[1]);
261+    child_execvp(sl_argv(argv));
262+  }
263+  close(fds[1]);
264+
265+  cap = 4096;
266+  len = 0;
267+  buf = emalloc(cap);
268+  for (;;) {
269+    ssize_t n;
270+    if (len + 4096 > cap) {
271+      cap *= 2;
272+      buf = erealloc(buf, cap);
273+    }
274+    n = read(fds[0], buf + len, cap - len - 1);
275+    if (n < 0) {
276+      if (errno == EINTR)
277+        continue;
278+      eprintf("capture: read:");
279+    }
280+    if (n == 0)
281+      break;
282+    len += (size_t)n;
283+  }
284+  buf[len] = '\0';
285+  close(fds[0]);
286+
287+  while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r'))
288+    buf[--len] = '\0';
289+
290+  if (waitpid(pid, &status, 0) < 0)
291+    eprintf("capture: waitpid:");
292+  if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0))
293+    eprintf("capture: command failed\n");
294+  return buf;
295+}
+355, -0
  1@@ -0,0 +1,355 @@
  2+#include "ninqu.h"
  3+
  4+#include <stdio.h>
  5+#include <stdlib.h>
  6+#include <string.h>
  7+
  8+void
  9+dep_push(struct Inst *inst, int idx)
 10+{
 11+  int i;
 12+  for (i = 0; i < inst->n_dep; i++)
 13+    if (inst->dep_inst[i] == idx)
 14+      return;
 15+  if (inst->n_dep >= inst->cap_dep) {
 16+    inst->cap_dep  = inst->cap_dep ? inst->cap_dep * 2 : 4;
 17+    inst->dep_inst = erealloc(inst->dep_inst, (size_t)inst->cap_dep * sizeof *inst->dep_inst);
 18+  }
 19+  inst->dep_inst[inst->n_dep++] = idx;
 20+}
 21+
 22+void
 23+resolve_deps(void)
 24+{
 25+  int i, j, k;
 26+  for (i = 0; i < ninsts; i++) {
 27+    struct Inst *inst = &insts[i];
 28+    for (j = 0; j < inst->dep_rule_names.n; j++) {
 29+      struct Rule *dep = rule_find(inst->dep_rule_names.v[j]);
 30+      if (!dep)
 31+        continue;
 32+      for (k = 0; k < dep->n_inst; k++)
 33+        dep_push(inst, dep->inst_idx[k]);
 34+    }
 35+  }
 36+}
 37+
 38+int
 39+inst_stale(struct Inst *inst)
 40+{
 41+  int i;
 42+  if (inst->phony)
 43+    return 1;
 44+  if (!file_exists(inst->out))
 45+    return 1;
 46+  for (i = 0; i < inst->in.n; i++)
 47+    if (mtime_of(inst->in.v[i]) > mtime_of(inst->out))
 48+      return 1;
 49+  for (i = 0; i < inst->stale_extra.n; i++)
 50+    if (file_exists(inst->stale_extra.v[i])
 51+        && mtime_of(inst->stale_extra.v[i]) > mtime_of(inst->out))
 52+      return 1;
 53+  return 0;
 54+}
 55+
 56+/* a pure $(NAME) splits on whitespace so $(CFLAGS) becomes many
 57+ * flags, while -I$(INCDIR) stays one token */
 58+static int
 59+is_pure_ref(const char *tok)
 60+{
 61+  size_t      len;
 62+  int         depth;
 63+  const char *p;
 64+
 65+  if (tok[0] != '$' || tok[1] != '(')
 66+    return 0;
 67+  len = strlen(tok);
 68+  if (tok[len - 1] != ')')
 69+    return 0;
 70+  depth = 1;
 71+  for (p = tok + 2; *p; p++) {
 72+    if (p[0] == '$' && p[1] == '(') {
 73+      depth++;
 74+      p++;
 75+      continue;
 76+    }
 77+    if (*p == ')') {
 78+      depth--;
 79+      if (depth == 0)
 80+        return (size_t)(p - tok) == len - 1;
 81+    }
 82+  }
 83+  return 0;
 84+}
 85+
 86+static void
 87+expand_cmd_token(struct StrList *out, const char *raw)
 88+{
 89+  char *exp = kv_expand(raw);
 90+  if (is_pure_ref(raw)) {
 91+    struct StrList pieces = {0};
 92+    int            j;
 93+    sl_split(&pieces, exp);
 94+    for (j = 0; j < pieces.n; j++)
 95+      sl_push(out, pieces.v[j]);
 96+  } else {
 97+    sl_push(out, exp);
 98+  }
 99+  free(exp);
100+}
101+
102+static void
103+build_cmd(struct Inst *inst, struct Rule *r)
104+{
105+  int i;
106+  if (r->cmd.is_pipe) {
107+    inst->cmd.is_pipe = 1;
108+    inst->cmd.nstages = r->cmd.nstages;
109+    inst->cmd.stages  = emalloc((size_t)r->cmd.nstages * sizeof *inst->cmd.stages);
110+    for (i = 0; i < r->cmd.nstages; i++) {
111+      int             j;
112+      struct StrList *slot = emalloc(sizeof *slot);
113+      memset(slot, 0, sizeof *slot);
114+      for (j = 0; j < r->cmd.stages[i]->n; j++)
115+        expand_cmd_token(slot, r->cmd.stages[i]->v[j]);
116+      inst->cmd.stages[i] = slot;
117+    }
118+    for (i = 0; i < inst->cmd.nstages; i++) {
119+      char *joined = sl_join(inst->cmd.stages[i]);
120+      if (i > 0)
121+        sl_push(&inst->cmd.argv, "|");
122+      sl_push(&inst->cmd.argv, joined);
123+      free(joined);
124+    }
125+  } else {
126+    for (i = 0; i < r->cmd.argv.n; i++)
127+      expand_cmd_token(&inst->cmd.argv, r->cmd.argv.v[i]);
128+  }
129+}
130+
131+/* @NAME resolves to a rule ref. glob:PREFIX goes to glob_expand.
132+ * anything else is a path: if it does not exist, try to find a
133+ * producer and run it first */
134+static void
135+resolve_input_token(int inst_idx, const char *raw, int only_dep)
136+{
137+  char *exp = kv_expand(raw);
138+
139+  if (exp[0] == '@') {
140+    const char     *rulename = exp + 1;
141+    struct Rule    *dep;
142+    struct KvStore *saved = local_overlay;
143+    struct Inst    *inst;
144+    int             i;
145+
146+    /* expand_rule may set/clear local_overlay for its own loop */
147+    local_overlay = NULL;
148+    expand_rule(rulename);
149+    local_overlay = saved;
150+
151+    /* insts[] may have moved during expand_rule */
152+    inst = &insts[inst_idx];
153+    dep  = rule_find(rulename);
154+    if (!dep)
155+      eprintf(
156+          "manifest: '%s' references unknown rule '@%s'\n", rules[inst->rule_idx].name, rulename
157+      );
158+    if (!sl_has(&inst->dep_rule_names, rulename))
159+      sl_push(&inst->dep_rule_names, rulename);
160+    if (!only_dep)
161+      for (i = 0; i < dep->n_inst; i++)
162+        sl_push(&inst->in, insts[dep->inst_idx[i]].out);
163+  } else if (strncmp(exp, "glob:", 5) == 0) {
164+    struct StrList matches = {0};
165+    int            i;
166+    glob_expand(exp + 5, &matches);
167+    for (i = 0; i < matches.n; i++) {
168+      if (only_dep)
169+        sl_push(&insts[inst_idx].stale_extra, matches.v[i]);
170+      else
171+        sl_push(&insts[inst_idx].in, matches.v[i]);
172+    }
173+  } else {
174+    struct StrList pieces = {0};
175+    int            i;
176+    sl_split(&pieces, exp);
177+    for (i = 0; i < pieces.n; i++) {
178+      const char *path = pieces.v[i];
179+      if (!file_exists(path))
180+        materialize_if_missing(path);
181+      if (only_dep)
182+        sl_push(&insts[inst_idx].stale_extra, path);
183+      else
184+        sl_push(&insts[inst_idx].in, path);
185+    }
186+  }
187+  free(exp);
188+}
189+
190+static void
191+setup_inst(struct Inst *inst, struct Rule *r)
192+{
193+  inst->phony    = r->phony;
194+  inst->redirect = r->redirect;
195+  if (r->workdir[0]) {
196+    char *w = kv_expand(r->workdir);
197+    strlcpy(inst->workdir, w, sizeof inst->workdir);
198+    free(w);
199+  }
200+}
201+
202+/* resolve deps, expand (out), populate IN/OUT on the overlay, build
203+ * the cmd. caller must have local_overlay set if needed */
204+static void
205+finalize_inst(int idx, struct Rule *r, struct KvStore *ov)
206+{
207+  struct Inst *inst = &insts[idx];
208+  int          k;
209+
210+  for (k = 0; k < r->in.n; k++)
211+    resolve_input_token(idx, r->in.v[k], 0);
212+  for (k = 0; k < r->extra_deps.n; k++)
213+    resolve_input_token(idx, r->extra_deps.v[k], 1);
214+
215+  inst = &insts[idx]; /* may have moved */
216+  {
217+    char *o = kv_expand(r->out);
218+    strlcpy(inst->out, o, sizeof inst->out);
219+    free(o);
220+  }
221+  kv_set_raw(ov, "IN", sl_join(&inst->in));
222+  kv_set_raw(ov, "OUT", inst->out);
223+  build_cmd(inst, r);
224+}
225+
226+void
227+expand_rule(const char *name)
228+{
229+  struct Rule *r = rule_find(name);
230+  int          i;
231+
232+  if (!r)
233+    eprintf("manifest: unknown rule '%s'\n", name);
234+  if (r->expanded)
235+    return;
236+  r->expanded = 1;
237+
238+  /* a gate with $(...) is per-instance, a static gate is checked
239+   * once and if false the rule produces zero instances */
240+  if (r->gate && !gate_has_dyn(r->gate) && !gate_eval(r->gate))
241+    return;
242+
243+  if (r->require_file[0] && r->globs.n == 0) {
244+    char *rf = kv_expand(r->require_file);
245+    int   ok = file_exists(rf);
246+    free(rf);
247+    if (!ok)
248+      return;
249+  }
250+
251+  if (r->globs.n > 0) {
252+    struct StrList matches = {0};
253+    int            j;
254+    int            r_literal = rule_is_literal(r);
255+
256+    /* run order-only @rule producers before globbing, so a codegen
257+     * step that generates sources finishes before the glob runs.
258+     * skipped in summary mode since -S must not execute */
259+    if (!summary_mode) {
260+      for (j = 0; j < r->extra_deps.n; j++) {
261+        char *tok = kv_expand(r->extra_deps.v[j]);
262+        if (tok[0] == '@') {
263+          struct Rule *dep = rule_find(tok + 1);
264+          if (dep) {
265+            expand_rule(dep->name);
266+            if (dep->n_inst > 0) {
267+              int k;
268+              for (k = 0; k < dep->n_inst; k++)
269+                run_inst_with_deps(dep->inst_idx[k]);
270+            }
271+          }
272+        }
273+        free(tok);
274+      }
275+    }
276+
277+    glob_all(r, &matches);
278+
279+    for (i = 0; i < matches.n; i++) {
280+      const char    *match = matches.v[i];
281+      const char    *base  = base_of(match);
282+      char           dir[1024], stem[512], basestem[512], ext[64], unused[64];
283+      char           ident[512];
284+      struct KvStore ov;
285+      int            idx;
286+      struct Inst   *inst;
287+
288+      if (sl_has(&r->skip, base))
289+        continue;
290+      if (!r_literal && path_claimed_elsewhere(match, r))
291+        continue;
292+      if (r->require_marker[0] && !file_contains(match, r->require_marker))
293+        continue;
294+
295+      if (strrchr(match, '/')) {
296+        size_t dl = (size_t)(strrchr(match, '/') - match);
297+        if (dl >= sizeof dir)
298+          dl = sizeof dir - 1;
299+        memcpy(dir, match, dl);
300+        dir[dl] = '\0';
301+      } else {
302+        strlcpy(dir, ".", sizeof dir);
303+      }
304+      splitext(base, basestem, sizeof basestem, ext, sizeof ext);
305+      splitext(match, stem, sizeof stem, unused, sizeof unused);
306+      to_ident(basestem, ident, sizeof ident);
307+
308+      memset(&ov, 0, sizeof ov);
309+      kv_set_raw(&ov, "MATCH", match);
310+      kv_set_raw(&ov, "DIR", dir);
311+      kv_set_raw(&ov, "BASE", base);
312+      kv_set_raw(&ov, "EXT", ext);
313+      kv_set_raw(&ov, "STEM", stem);
314+      kv_set_raw(&ov, "BASESTEM", basestem);
315+      kv_set_raw(&ov, "IDENT", ident);
316+      local_overlay = &ov;
317+
318+      if (r->gate && gate_has_dyn(r->gate)) {
319+        struct Gate *mg = gate_materialize(r->gate);
320+        int          ok = gate_eval(mg);
321+        gate_free(mg);
322+        if (!ok) {
323+          local_overlay = NULL;
324+          continue;
325+        }
326+      }
327+      if (r->require_file[0]) {
328+        char *rf = kv_expand(r->require_file);
329+        int   ok = file_exists(rf);
330+        free(rf);
331+        if (!ok) {
332+          local_overlay = NULL;
333+          continue;
334+        }
335+      }
336+
337+      idx  = inst_new((int)(r - rules));
338+      inst = &insts[idx];
339+      setup_inst(inst, r);
340+      finalize_inst(idx, r, &ov);
341+      local_overlay = NULL;
342+    }
343+  } else {
344+    int            idx  = inst_new((int)(r - rules));
345+    struct Inst   *inst = &insts[idx];
346+    struct KvStore ov;
347+
348+    /* non-glob rules have no MATCH/DIR/BASE to expose, so the
349+     * overlay only carries IN/OUT for build_cmd */
350+    setup_inst(inst, r);
351+    memset(&ov, 0, sizeof ov);
352+    local_overlay = &ov;
353+    finalize_inst(idx, r, &ov);
354+    local_overlay = NULL;
355+  }
356+}
+193, -0
  1@@ -0,0 +1,193 @@
  2+#include "ninqu.h"
  3+
  4+#include <ctype.h>
  5+#include <fnmatch.h>
  6+#include <glob.h>
  7+#include <stdio.h>
  8+#include <stdlib.h>
  9+#include <string.h>
 10+#include <sys/stat.h>
 11+
 12+long
 13+mtime_of(const char *path)
 14+{
 15+  struct stat st;
 16+  if (stat(path, &st) != 0)
 17+    return -1;
 18+  return (long)st.st_mtime;
 19+}
 20+
 21+int
 22+file_exists(const char *path)
 23+{
 24+  return mtime_of(path) >= 0;
 25+}
 26+
 27+int
 28+file_contains(const char *path, const char *needle)
 29+{
 30+  FILE  *fp;
 31+  char   buf[8192];
 32+  size_t n, needlen = strlen(needle), taillen = 0;
 33+  char   tail[256];
 34+  int    found = 0;
 35+
 36+  if (!needle[0])
 37+    return 1;
 38+  fp = fopen(path, "r");
 39+  if (!fp)
 40+    return 0;
 41+  tail[0] = '\0';
 42+  while (!found && (n = fread(buf, 1, sizeof buf - 1, fp)) > 0) {
 43+    char joined[8192 + 256];
 44+    buf[n] = '\0';
 45+    snprintf(joined, sizeof joined, "%s%s", tail, buf);
 46+    if (strstr(joined, needle))
 47+      found = 1;
 48+    taillen = strlen(buf);
 49+    if (taillen > needlen)
 50+      taillen = needlen - 1;
 51+    snprintf(tail, sizeof tail, "%s", buf + strlen(buf) - taillen);
 52+  }
 53+  fclose(fp);
 54+  return found;
 55+}
 56+
 57+/* recurse_dir has no userdata slot, so the pattern and output list
 58+ * travel through file-scope statics for the span of one call */
 59+static const char     *rg_suffix;
 60+static struct StrList *rg_out;
 61+
 62+static void
 63+rg_visit(const char *path)
 64+{
 65+  struct stat st;
 66+
 67+  if (lstat(path, &st) != 0)
 68+    return;
 69+  if (S_ISDIR(st.st_mode)) {
 70+    recurse_dir(path, rg_visit);
 71+    return;
 72+  }
 73+  if (S_ISREG(st.st_mode) && fnmatch(rg_suffix, base_of(path), 0) == 0)
 74+    sl_push(rg_out, path);
 75+}
 76+
 77+static void
 78+recurse_glob(const char *dir, const char *suffix, struct StrList *out)
 79+{
 80+  rg_suffix = suffix;
 81+  rg_out    = out;
 82+  recurse_dir(dir, rg_visit);
 83+}
 84+
 85+/* a double-star-slash pattern recurses from the dir before it,
 86+ * matching the suffix against each basename. anything else goes to
 87+ * glob(3) as-is */
 88+void
 89+glob_expand(const char *pattern, struct StrList *out)
 90+{
 91+  const char *rec = strstr(pattern, "**/");
 92+  if (rec) {
 93+    char   prefix[1024], suffix[1024];
 94+    size_t plen = (size_t)(rec - pattern);
 95+    if (plen >= sizeof prefix)
 96+      plen = sizeof prefix - 1;
 97+    memcpy(prefix, pattern, plen);
 98+    prefix[plen] = '\0';
 99+    if (plen > 0 && prefix[plen - 1] == '/')
100+      prefix[plen - 1] = '\0';
101+    strlcpy(suffix, rec + 3, sizeof suffix);
102+    recurse_glob(prefix[0] ? prefix : ".", suffix, out);
103+  } else {
104+    glob_t g;
105+    size_t i;
106+    if (glob(pattern, 0, NULL, &g) == 0)
107+      for (i = 0; i < g.gl_pathc; i++)
108+        sl_push(out, g.gl_pathv[i]);
109+    globfree(&g);
110+  }
111+}
112+
113+void
114+glob_all(struct Rule *r, struct StrList *out)
115+{
116+  int i;
117+  for (i = 0; i < r->globs.n; i++) {
118+    char *pat = kv_expand(r->globs.v[i]);
119+    glob_expand(pat, out);
120+    free(pat);
121+  }
122+}
123+
124+int
125+is_wildcard_pattern(const char *pattern)
126+{
127+  return strpbrk(pattern, "*?[") != NULL;
128+}
129+
130+/* true if every glob on r is a literal path. a rule built entirely
131+ * from literals is the override side of path_claimed_elsewhere */
132+int
133+rule_is_literal(struct Rule *r)
134+{
135+  int i;
136+  if (r->globs.n == 0)
137+    return 0;
138+  for (i = 0; i < r->globs.n; i++)
139+    if (is_wildcard_pattern(r->globs.v[i]))
140+      return 0;
141+  return 1;
142+}
143+
144+/* a broad wildcard rule steps aside for any file another rule names
145+ * outright, so a per-file override needs no (skip) on the broad rule */
146+int
147+path_claimed_elsewhere(const char *path, struct Rule *self)
148+{
149+  int i, j;
150+  for (i = 0; i < nrules; i++) {
151+    struct Rule *other = &rules[i];
152+    if (other == self || !rule_is_literal(other))
153+      continue;
154+    for (j = 0; j < other->globs.n; j++) {
155+      char *exp = kv_expand(other->globs.v[j]);
156+      int   hit = strcmp(exp, path) == 0;
157+      free(exp);
158+      if (hit)
159+        return 1;
160+    }
161+  }
162+  return 0;
163+}
164+
165+void
166+splitext(const char *base, char *stem, size_t stemsz, char *ext, size_t extsz)
167+{
168+  const char *dot = strrchr(base, '.');
169+  if (dot && dot != base) {
170+    size_t sl = (size_t)(dot - base);
171+    if (sl >= stemsz)
172+      sl = stemsz - 1;
173+    memcpy(stem, base, sl);
174+    stem[sl] = '\0';
175+    strlcpy(ext, dot, extsz);
176+  } else {
177+    strlcpy(stem, base, stemsz);
178+    ext[0] = '\0';
179+  }
180+}
181+
182+/* a basestem like sha512-224sum has a hyphen, but a config toggle
183+ * cannot. normalize so a gate keying off BASESTEM matches the form
184+ * genconfig writes
185+ * FIXME: I do not like our current genconfig.sh
186+ */
187+void
188+to_ident(const char *s, char *out, size_t outsz)
189+{
190+  size_t i;
191+  for (i = 0; s[i] && i + 1 < outsz; i++)
192+    out[i] = (s[i] == '-' || s[i] == '.') ? '_' : s[i];
193+  out[i] = '\0';
194+}
+139, -0
  1@@ -0,0 +1,139 @@
  2+#include "ninqu.h"
  3+
  4+#include <stdio.h>
  5+#include <stdlib.h>
  6+#include <string.h>
  7+
  8+struct Gate *
  9+gate_new(enum GKind kind)
 10+{
 11+  struct Gate *g = ecalloc(1, sizeof *g);
 12+  g->kind        = kind;
 13+  return g;
 14+}
 15+
 16+void
 17+gate_free(struct Gate *g)
 18+{
 19+  int i;
 20+  if (!g)
 21+    return;
 22+  for (i = 0; i < g->nkids; i++)
 23+    gate_free(g->kids[i]);
 24+  free(g->kids);
 25+  free(g->var);
 26+  free(g);
 27+}
 28+
 29+struct Gate *
 30+gate_parse(struct SNode *expr)
 31+{
 32+  const char   *h;
 33+  struct Gate **kids;
 34+  int           i, n;
 35+
 36+  if (!expr)
 37+    return gate_new(G_TRUE);
 38+
 39+  if (expr->kind == S_ATOM) {
 40+    struct Gate *g = gate_new(G_VAR);
 41+    g->var         = estrdup(expr->atom);
 42+    return g;
 43+  }
 44+
 45+  h = s_head(expr);
 46+  if (!h)
 47+    eprintf("manifest: empty () in gate\n");
 48+
 49+  if (strcmp(h, "not") == 0) {
 50+    struct Gate *g;
 51+    if (expr->nkids != 2)
 52+      eprintf("manifest: (not X) takes one operand\n");
 53+    g          = gate_new(G_NOT);
 54+    g->kids    = emalloc(sizeof *g->kids);
 55+    g->kids[0] = gate_parse(expr->kids[1]);
 56+    g->nkids   = 1;
 57+    return g;
 58+  }
 59+  if (strcmp(h, "and") == 0 || strcmp(h, "or") == 0) {
 60+    struct Gate *g = gate_new(h[0] == 'a' ? G_AND : G_OR);
 61+    if (expr->nkids < 2)
 62+      eprintf("manifest: (%s ...) needs one operand\n", h);
 63+    n    = expr->nkids - 1;
 64+    kids = emalloc((size_t)n * sizeof *kids);
 65+    for (i = 0; i < n; i++)
 66+      kids[i] = gate_parse(expr->kids[i + 1]);
 67+    g->kids  = kids;
 68+    g->nkids = n;
 69+    return g;
 70+  }
 71+  eprintf("manifest: unknown gate operator '%s'\n", h);
 72+  return NULL;
 73+}
 74+
 75+/* true if the gate contains any $(...) that needs per-instance
 76+ * expansion before it can be evaluated */
 77+int
 78+gate_has_dyn(struct Gate *g)
 79+{
 80+  int i;
 81+  if (!g)
 82+    return 0;
 83+  if (g->kind == G_VAR)
 84+    return strstr(g->var, "$(") != NULL;
 85+  for (i = 0; i < g->nkids; i++)
 86+    if (gate_has_dyn(g->kids[i]))
 87+      return 1;
 88+  return 0;
 89+}
 90+
 91+int
 92+gate_eval(struct Gate *g)
 93+{
 94+  int i, v;
 95+  if (!g)
 96+    return 1;
 97+  switch (g->kind) {
 98+    case G_TRUE:
 99+      return 1;
100+    case G_VAR: {
101+      char *vraw = kv_get(g->var);
102+      return vraw && *vraw && strcmp(vraw, "0") != 0;
103+    }
104+    case G_NOT:
105+      return !gate_eval(g->kids[0]);
106+    case G_AND:
107+      v = 1;
108+      for (i = 0; i < g->nkids; i++)
109+        v = v && gate_eval(g->kids[i]);
110+      return v;
111+    case G_OR:
112+      v = 0;
113+      for (i = 0; i < g->nkids; i++)
114+        v = v || gate_eval(g->kids[i]);
115+      return v;
116+  }
117+  return 0;
118+}
119+
120+/* expand every $(...) so a gate like build_$(BASESTEM) resolves
121+ * against the active overlay before evaluation */
122+struct Gate *
123+gate_materialize(struct Gate *g)
124+{
125+  int          i;
126+  struct Gate *out;
127+
128+  if (!g)
129+    return NULL;
130+  out = gate_new(g->kind);
131+  if (g->var)
132+    out->var = kv_expand(g->var);
133+  if (g->nkids) {
134+    out->kids = emalloc((size_t)g->nkids * sizeof *out->kids);
135+    for (i = 0; i < g->nkids; i++)
136+      out->kids[i] = gate_materialize(g->kids[i]);
137+    out->nkids = g->nkids;
138+  }
139+  return out;
140+}
+182, -0
  1@@ -0,0 +1,182 @@
  2+#include "ninqu.h"
  3+
  4+#include <stdio.h>
  5+#include <stdlib.h>
  6+#include <string.h>
  7+
  8+struct KvStore  kv;
  9+struct KvStore *local_overlay;
 10+struct StrList  kv_locked;
 11+struct Map     *maps;
 12+int             nmaps, mapscap;
 13+
 14+char *
 15+kv_get_raw(struct KvStore *s, const char *key)
 16+{
 17+  int i;
 18+  for (i = 0; i < s->n; i++)
 19+    if (strcmp(s->v[i].key, key) == 0)
 20+      return s->v[i].val;
 21+  return NULL;
 22+}
 23+
 24+void
 25+kv_set_raw(struct KvStore *s, const char *key, const char *val)
 26+{
 27+  int i;
 28+  for (i = 0; i < s->n; i++) {
 29+    if (strcmp(s->v[i].key, key) == 0) {
 30+      free(s->v[i].val);
 31+      s->v[i].val = estrdup(val);
 32+      return;
 33+    }
 34+  }
 35+  GROW(s->v, s->n, s->cap, 16);
 36+  s->v[s->n].key = estrdup(key);
 37+  s->v[s->n].val = estrdup(val);
 38+  s->n++;
 39+}
 40+
 41+/* overlay beats global beats environ, so a per-match $(BASESTEM) can
 42+ * shadow a (set BASESTEM ...) without clobbering it for other matches */
 43+char *
 44+kv_get(const char *key)
 45+{
 46+  char *v;
 47+  if (local_overlay) {
 48+    v = kv_get_raw(local_overlay, key);
 49+    if (v)
 50+      return v;
 51+  }
 52+  v = kv_get_raw(&kv, key);
 53+  if (v)
 54+    return v;
 55+  return getenv(key);
 56+}
 57+
 58+const char *
 59+kv_get_or(const char *key, const char *def)
 60+{
 61+  char *v = kv_get(key);
 62+  return v ? v : def;
 63+}
 64+
 65+void
 66+kv_set(const char *key, const char *val)
 67+{
 68+  kv_set_raw(&kv, key, val);
 69+}
 70+
 71+/* -D on the cli locks the name so a later (set) in the file cannot
 72+ * overwrite it */
 73+void
 74+kv_set_manifest(const char *key, const char *val)
 75+{
 76+  if (sl_has(&kv_locked, key))
 77+    return;
 78+  kv_set(key, val);
 79+}
 80+
 81+void
 82+kv_set_cli(const char *key, const char *val)
 83+{
 84+  kv_set(key, val);
 85+  sl_push(&kv_locked, key);
 86+}
 87+
 88+struct Map *
 89+map_find(const char *name)
 90+{
 91+  int i;
 92+  for (i = 0; i < nmaps; i++)
 93+    if (strcmp(maps[i].name, name) == 0)
 94+      return &maps[i];
 95+  return NULL;
 96+}
 97+
 98+struct Map *
 99+map_new(const char *name)
100+{
101+  struct Map *m;
102+  GROW(maps, nmaps, mapscap, 8);
103+  m = &maps[nmaps++];
104+  memset(m, 0, sizeof *m);
105+  strlcpy(m->name, name, sizeof m->name);
106+  return m;
107+}
108+
109+/* $(NAME) and $(NAME:KEY). KEY is expanded first, the map value is
110+ * re-expanded once so $(LDLIBS_TLS) inside a map entry resolves. the
111+ * result is not recursively expanded, matching simply-expanded vars */
112+char *
113+kv_expand(const char *s)
114+{
115+  size_t      cap = strlen(s) * 2 + 64, len = 0;
116+  char       *out = emalloc(cap);
117+  const char *p   = s;
118+
119+  while (*p) {
120+    if (p[0] == '$' && p[1] == '(') {
121+      const char *inner        = p + 2;
122+      const char *close        = NULL;
123+      char       *val          = NULL;
124+      char       *expanded_val = NULL;
125+
126+      close = find_dollar_close(inner);
127+
128+      if (close) {
129+        char   name[4096];
130+        size_t nl = (size_t)(close - inner);
131+        char  *colon;
132+
133+        if (nl >= sizeof name)
134+          nl = sizeof name - 1;
135+        memcpy(name, inner, nl);
136+        name[nl] = '\0';
137+
138+        colon = strchr(name, ':');
139+        if (colon) {
140+          char        mapname[128];
141+          char       *keypart;
142+          struct Map *m;
143+          size_t      mnl = (size_t)(colon - name);
144+
145+          if (mnl >= sizeof mapname)
146+            mnl = sizeof mapname - 1;
147+          memcpy(mapname, name, mnl);
148+          mapname[mnl] = '\0';
149+          m            = map_find(mapname);
150+          keypart      = kv_expand(colon + 1);
151+          if (m) {
152+            char *raw = kv_get_raw(&m->entries, keypart);
153+            if (raw)
154+              expanded_val = kv_expand(raw);
155+          }
156+          free(keypart);
157+          val = expanded_val;
158+        } else {
159+          val = kv_get(name);
160+        }
161+
162+        if (!val)
163+          val = "";
164+        while (len + strlen(val) + 1 >= cap) {
165+          cap *= 2;
166+          out = erealloc(out, cap);
167+        }
168+        memcpy(out + len, val, strlen(val));
169+        len += strlen(val);
170+        free(expanded_val);
171+        p = close + 1;
172+        continue;
173+      }
174+    }
175+    if (len + 2 >= cap) {
176+      cap *= 2;
177+      out = erealloc(out, cap);
178+    }
179+    out[len++] = *p++;
180+  }
181+  out[len] = '\0';
182+  return out;
183+}
+112, -0
  1@@ -0,0 +1,112 @@
  2+#include "ninqu.h"
  3+
  4+#include <limits.h>
  5+#include <stdio.h>
  6+#include <stdlib.h>
  7+#include <string.h>
  8+#include <unistd.h>
  9+
 10+static void
 11+usage(void)
 12+{
 13+  eprintf(
 14+      "usage: %s [-jN] [-f manifest] [-C dir] [-Dvar=val]... [-S] [-F] [target|meta.target ...]\n"
 15+      "       %s query (features|groups|metas|rules|graph) [arg]\n",
 16+      argv0,
 17+      argv0
 18+  );
 19+}
 20+
 21+int
 22+main(int argc, char *argv[])
 23+{
 24+  const char    *manifest      = "ninqu.rules";
 25+  int            features_mode = 0;
 26+  int            query_mode    = 0;
 27+  const char    *query_sub     = NULL;
 28+  const char    *query_arg     = NULL;
 29+  int            i;
 30+  struct StrList wanted = {0};
 31+
 32+  /* detect `query` before ARGBEGIN so it is not parsed as a target */
 33+  if (argc >= 2 && strcmp(argv[1], "query") == 0) {
 34+    query_mode = 1;
 35+    if (argc < 3)
 36+      usage();
 37+    query_sub = argv[2];
 38+    query_arg = (argc >= 4) ? argv[3] : NULL;
 39+    for (i = 3; i < argc; i++) {
 40+      if (argv[i][0] == '-' && argv[i][1] == 'f')
 41+        manifest = argv[i][2] ? argv[i] + 2 : argv[++i];
 42+      else if (argv[i][0] == '-' && argv[i][1] == 'C') {
 43+        if (chdir(argv[i][2] ? argv[i] + 2 : argv[++i]) != 0)
 44+          eprintf("chdir:");
 45+      } else if (argv[i][0] == '-' && argv[i][1] == 'D') {
 46+        char *spec = argv[i][2] ? argv[i] + 2 : argv[++i];
 47+        char *eq   = strchr(spec, '=');
 48+        if (!eq)
 49+          usage();
 50+        *eq = '\0';
 51+        kv_set_cli(spec, eq + 1);
 52+      }
 53+    }
 54+  } else {
 55+    ARGBEGIN
 56+    {
 57+      case 'j':
 58+        jobs_n = (int)estrtonum(EARGF(usage()), 1, INT_MAX);
 59+        break;
 60+      case 'f':
 61+        manifest = EARGF(usage());
 62+        break;
 63+      case 'C':
 64+        if (chdir(EARGF(usage())) != 0)
 65+          eprintf("chdir:");
 66+        break;
 67+      case 'D': {
 68+        char *spec = EARGF(usage());
 69+        char *eq   = strchr(spec, '=');
 70+        if (!eq)
 71+          usage();
 72+        *eq = '\0';
 73+        kv_set_cli(spec, eq + 1);
 74+        break;
 75+      }
 76+      case 'S':
 77+        summary_mode = 1;
 78+        break;
 79+      case 'F':
 80+        features_mode = 1;
 81+        break;
 82+      default:
 83+        usage();
 84+    }
 85+    ARGEND
 86+
 87+    for (i = 0; i < argc; i++)
 88+      sl_push(&wanted, argv[i]);
 89+  }
 90+
 91+  load_file(manifest);
 92+  resolve_all_bareword_deps();
 93+
 94+  if (query_mode) {
 95+    do_query(query_sub, query_arg);
 96+    return 0;
 97+  }
 98+  if (features_mode) {
 99+    do_features();
100+    return 0;
101+  }
102+
103+  if (wanted.n == 0)
104+    build_default_group();
105+  else
106+    for (i = 0; i < wanted.n; i++)
107+      resolve_wanted(wanted.v[i]);
108+
109+  resolve_deps();
110+  schedule_and_run();
111+
112+  return failed_any ? 1 : 0;
113+}
+256, -0
  1@@ -0,0 +1,256 @@
  2+#if !defined(_DEFAULT_SOURCE)
  3+#define _DEFAULT_SOURCE
  4+#endif
  5+#if !defined(_POSIX_C_SOURCE)
  6+#define _POSIX_C_SOURCE 200809L
  7+#endif
  8+
  9+#include <sys/types.h>
 10+
 11+#include "arg.h"
 12+#include "util.h"
 13+
 14+/* grow a struct-array when full. n/cap doubles each time */
 15+#define GROW(arr, n, cap, init)                                                                    \
 16+  do {                                                                                             \
 17+    if ((n) >= (cap)) {                                                                            \
 18+      (cap) = (cap) ? (cap) * 2 : (init);                                                          \
 19+      (arr) = erealloc((arr), (size_t)(cap) * sizeof *(arr));                                      \
 20+    }                                                                                              \
 21+  } while (0)
 22+
 23+struct StrList {
 24+  char **v;
 25+  int    n;
 26+  int    cap;
 27+};
 28+
 29+void        sl_push(struct StrList *sl, const char *s);
 30+int         sl_has(struct StrList *sl, const char *s);
 31+void        sl_split(struct StrList *sl, const char *s);
 32+char       *sl_join(struct StrList *sl);
 33+char      **sl_argv(struct StrList *sl);
 34+const char *base_of(const char *path);
 35+void        child_execvp(char **av) __attribute__((noreturn));
 36+
 37+struct Kv {
 38+  char *key;
 39+  char *val;
 40+};
 41+
 42+struct KvStore {
 43+  struct Kv *v;
 44+  int        n, cap;
 45+};
 46+
 47+struct Map {
 48+  char           name[128];
 49+  struct KvStore entries;
 50+};
 51+
 52+char       *kv_get(const char *key);
 53+const char *kv_get_or(const char *key, const char *def);
 54+void        kv_set(const char *key, const char *val);
 55+void        kv_set_manifest(const char *key, const char *val);
 56+void        kv_set_cli(const char *key, const char *val);
 57+char       *kv_expand(const char *s);
 58+char       *kv_get_raw(struct KvStore *s, const char *key);
 59+void        kv_set_raw(struct KvStore *s, const char *key, const char *val);
 60+struct Map *map_find(const char *name);
 61+struct Map *map_new(const char *name);
 62+
 63+extern struct KvStore  kv;
 64+extern struct KvStore *local_overlay;
 65+extern struct StrList  kv_locked;
 66+extern struct Map     *maps;
 67+extern int             nmaps, mapscap;
 68+
 69+enum SKind { S_ATOM, S_LIST };
 70+
 71+struct SNode {
 72+  enum SKind     kind;
 73+  char          *atom;
 74+  struct SNode **kids;
 75+  int            nkids;
 76+  int            line;
 77+};
 78+
 79+struct SLex {
 80+  const char *src;
 81+  size_t      pos;
 82+  int         line;
 83+};
 84+
 85+struct SNode *snode_atom(const char *s, int line);
 86+struct SNode *snode_list(int line);
 87+void          snode_push(struct SNode *list, struct SNode *kid);
 88+void          snode_free(struct SNode *n);
 89+struct SNode *snode_clone(struct SNode *n);
 90+struct SNode *snode_parse(struct SLex *lx);
 91+int           s_is_list(struct SNode *n);
 92+const char   *s_head(struct SNode *n);
 93+char         *subst_tmpl(const char *s, struct StrList *params, char **vals);
 94+void          snode_subst(struct SNode *n, struct StrList *params, char **vals);
 95+void          lex_swallow_dollar(struct SLex *lx, char *buf, size_t *blen, size_t bsz);
 96+void          lex_string(struct SLex *lx, char *buf, size_t bsz);
 97+const char   *find_dollar_close(const char *inner);
 98+
 99+/* gate: (not X) | (and X...) | (or X...) | bare var */
100+enum GKind { G_TRUE, G_VAR, G_NOT, G_AND, G_OR };
101+
102+struct Gate {
103+  enum GKind    kind;
104+  char         *var;
105+  struct Gate **kids;
106+  int           nkids;
107+};
108+
109+struct Gate *gate_new(enum GKind kind);
110+void         gate_free(struct Gate *g);
111+struct Gate *gate_parse(struct SNode *expr);
112+int          gate_has_dyn(struct Gate *g);
113+int          gate_eval(struct Gate *g);
114+struct Gate *gate_materialize(struct Gate *g);
115+
116+struct Cmd {
117+  struct StrList   argv;
118+  int              is_pipe;
119+  struct StrList **stages;
120+  int              nstages;
121+};
122+
123+struct Rule {
124+  char           name[128];
125+  struct StrList globs;
126+  struct StrList skip;
127+  char           require_marker[1024];
128+  char           require_file[1024];
129+  struct StrList in;
130+  struct StrList extra_deps;
131+  char           out[1024];
132+  struct Cmd     cmd;
133+  struct Gate   *gate;
134+  char           workdir[1024];
135+  int            phony;
136+  int            redirect;
137+  char           produces[128];
138+  char           description[256];
139+  char           group_name[128];
140+  char           member_of[128];
141+  char           member_alias[128];
142+  struct StrList features;
143+
144+  int *inst_idx;
145+  int  n_inst, cap_inst;
146+  int  expanded;
147+};
148+
149+struct Group {
150+  char           name[128];
151+  struct StrList refs;
152+};
153+
154+struct Meta {
155+  char           name[128];
156+  struct StrList keys;
157+  struct StrList vals;
158+};
159+
160+struct Template {
161+  char           name[128];
162+  struct StrList params;
163+  struct SNode  *body;
164+};
165+
166+struct Rule     *rule_find(const char *name);
167+struct Rule     *rule_find_output(const char *path);
168+struct Rule     *rule_find_produces(const char *name);
169+struct Rule     *rule_new(const char *name);
170+struct Group    *group_find(const char *name);
171+struct Group    *group_new(const char *name);
172+struct Meta     *meta_find(const char *name);
173+struct Meta     *meta_new(const char *name);
174+struct Template *template_find(const char *name);
175+struct Template *template_new(const char *name);
176+
177+extern struct Rule     *rules;
178+extern int              nrules, rulescap;
179+extern struct Group    *groups;
180+extern int              ngroups, groupscap;
181+extern struct Meta     *metas;
182+extern int              nmetas, metascap;
183+extern struct Template *templates;
184+extern int              ntemplates, templatescap;
185+
186+struct Inst {
187+  int            rule_idx;
188+  struct StrList in;
189+  struct StrList stale_extra;
190+  char           out[1024];
191+  struct Cmd     cmd;
192+  char           workdir[1024];
193+  int            phony;
194+  int            redirect;
195+
196+  struct StrList dep_rule_names;
197+  int           *dep_inst;
198+  int            n_dep, cap_dep;
199+  int            will_build;
200+};
201+
202+int  inst_new(int rule_idx);
203+int  inst_stale(struct Inst *inst);
204+void dep_push(struct Inst *inst, int idx);
205+void resolve_deps(void);
206+
207+extern struct Inst *insts;
208+extern int          ninsts, instscap;
209+
210+long mtime_of(const char *path);
211+int  file_exists(const char *path);
212+int  file_contains(const char *path, const char *needle);
213+void glob_expand(const char *pattern, struct StrList *out);
214+void glob_all(struct Rule *r, struct StrList *out);
215+int  is_wildcard_pattern(const char *pattern);
216+int  rule_is_literal(struct Rule *r);
217+int  path_claimed_elsewhere(const char *path, struct Rule *self);
218+void splitext(const char *base, char *stem, size_t stemsz, char *ext, size_t extsz);
219+void to_ident(const char *s, char *out, size_t outsz);
220+
221+pid_t spawn_inst(struct Inst *inst, struct StrList *argv);
222+pid_t spawn_pipe(struct Inst *inst);
223+void  mk_out_dir(const char *out);
224+void  announce_inst(struct Inst *inst);
225+void  run_inst_sync(int idx);
226+void  run_batch(int *idxs, int n);
227+void  run_inst_with_deps(int idx);
228+void  materialize_if_missing(const char *path);
229+char *capture_argv(struct StrList *argv);
230+
231+extern int jobs_n;
232+extern int summary_mode;
233+extern int failed_any;
234+
235+void        load_file(const char *path);
236+void        load_manifest(struct SNode *root);
237+void        dispatch_top_form(struct SNode *form);
238+void        parse_exec_clause(struct SNode *clause, struct Cmd *cmd);
239+void        parse_shell_clause(struct SNode *clause, struct Cmd *cmd);
240+void        parse_pipe_clause(struct SNode *clause, struct Cmd *cmd);
241+void        meta_apply(struct Meta *m);
242+const char *first_atom(struct SNode *node, const char *msg);
243+const char *sole_atom(struct SNode *node, const char *msg);
244+
245+extern struct StrList files_read;
246+
247+void expand_rule(const char *name);
248+
249+void schedule_and_run(void);
250+
251+void do_features(void);
252+void do_query(const char *sub, const char *arg);
253+
254+void include_ref(const char *name);
255+void build_default_group(void);
256+void resolve_wanted(const char *w);
257+void resolve_all_bareword_deps(void);
+650, -0
  1@@ -0,0 +1,650 @@
  2+#include "ninqu.h"
  3+
  4+#include <ctype.h>
  5+#include <limits.h>
  6+#include <stdio.h>
  7+#include <stdlib.h>
  8+#include <string.h>
  9+
 10+struct StrList files_read;
 11+
 12+/* second child must be an atom, more children may follow */
 13+const char *
 14+first_atom(struct SNode *node, const char *msg)
 15+{
 16+  if (node->nkids < 2 || node->kids[1]->kind != S_ATOM)
 17+    eprintf("manifest: %s\n", msg);
 18+  return node->kids[1]->atom;
 19+}
 20+
 21+/* second child must be the only other child, and an atom */
 22+const char *
 23+sole_atom(struct SNode *node, const char *msg)
 24+{
 25+  if (node->nkids != 2 || node->kids[1]->kind != S_ATOM)
 26+    eprintf("manifest: %s\n", msg);
 27+  return node->kids[1]->atom;
 28+}
 29+
 30+/* a dep operand is a bare atom, a (rule NAME) stored as @NAME, or
 31+ * an (after X...) whose operands are order-only */
 32+static void
 33+parse_dep_item(struct SNode *item, struct StrList *hard, struct StrList *ord, int allow_after)
 34+{
 35+  const char *h;
 36+
 37+  if (item->kind == S_ATOM) {
 38+    sl_push(hard, item->atom);
 39+    return;
 40+  }
 41+  h = s_head(item);
 42+  if (!h)
 43+    eprintf("manifest: empty () in dep\n");
 44+  if (strcmp(h, "rule") == 0) {
 45+    char buf[256];
 46+    snprintf(buf, sizeof buf, "@%s", sole_atom(item, "(rule NAME) takes one name"));
 47+    sl_push(hard, buf);
 48+    return;
 49+  }
 50+  if (strcmp(h, "after") == 0) {
 51+    int i;
 52+    if (!allow_after)
 53+      eprintf("manifest: (after ...) not allowed here\n");
 54+    for (i = 1; i < item->nkids; i++)
 55+      parse_dep_item(item->kids[i], ord, ord, 0);
 56+    return;
 57+  }
 58+  eprintf("manifest: unknown dep operand '%s'\n", h);
 59+}
 60+
 61+static void
 62+parse_dep_clause(struct SNode *clause, struct StrList *hard, struct StrList *ord)
 63+{
 64+  int i;
 65+  for (i = 1; i < clause->nkids; i++)
 66+    parse_dep_item(clause->kids[i], hard, ord, 1);
 67+}
 68+
 69+void
 70+parse_exec_clause(struct SNode *clause, struct Cmd *cmd)
 71+{
 72+  int i;
 73+  memset(cmd, 0, sizeof *cmd);
 74+  for (i = 1; i < clause->nkids; i++) {
 75+    if (clause->kids[i]->kind != S_ATOM)
 76+      eprintf("manifest: (exec ...) operands must be atoms\n");
 77+    sl_push(&cmd->argv, clause->kids[i]->atom);
 78+  }
 79+  if (cmd->argv.n == 0)
 80+    eprintf("manifest: (exec ...) needs one operand\n");
 81+}
 82+
 83+/* desugar to (exec sh -c STR). sh is the posix shell, overridable
 84+ * by writing (exec ...) directly */
 85+void
 86+parse_shell_clause(struct SNode *clause, struct Cmd *cmd)
 87+{
 88+  memset(cmd, 0, sizeof *cmd);
 89+  sl_push(&cmd->argv, "sh");
 90+  sl_push(&cmd->argv, "-c");
 91+  sl_push(&cmd->argv, sole_atom(clause, "(shell \"...\") takes one string"));
 92+}
 93+
 94+static void
 95+parse_pipe_stage(struct SNode *stage, struct StrList *argv)
 96+{
 97+  const char *h = s_head(stage);
 98+  int         j;
 99+
100+  if (!h)
101+    eprintf("manifest: (pipe ...) stage must be a form\n");
102+  if (strcmp(h, "exec") == 0) {
103+    for (j = 1; j < stage->nkids; j++) {
104+      if (stage->kids[j]->kind != S_ATOM)
105+        eprintf("manifest: (exec ...) operands must be atoms\n");
106+      sl_push(argv, stage->kids[j]->atom);
107+    }
108+  } else if (strcmp(h, "shell") == 0) {
109+    sl_push(argv, "sh");
110+    sl_push(argv, "-c");
111+    sl_push(argv, sole_atom(stage, "(shell \"...\") takes one string"));
112+  } else {
113+    eprintf("manifest: (pipe) stage must be exec or shell, got '%s'\n", h);
114+  }
115+  if (argv->n == 0)
116+    eprintf("manifest: (pipe) stage cannot be empty\n");
117+}
118+
119+void
120+parse_pipe_clause(struct SNode *clause, struct Cmd *cmd)
121+{
122+  int i, j;
123+  memset(cmd, 0, sizeof *cmd);
124+  cmd->is_pipe = 1;
125+  for (i = 1; i < clause->nkids; i++) {
126+    struct StrList  argv = {0};
127+    struct StrList *slot;
128+
129+    parse_pipe_stage(clause->kids[i], &argv);
130+
131+    slot = emalloc(sizeof *slot);
132+    memset(slot, 0, sizeof *slot);
133+    for (j = 0; j < argv.n; j++)
134+      sl_push(slot, argv.v[j]);
135+    if (cmd->nstages == 0)
136+      cmd->stages = emalloc(8 * sizeof *cmd->stages);
137+    else if (cmd->nstages >= 8)
138+      cmd->stages = erealloc(cmd->stages, (size_t)cmd->nstages * 2 * sizeof *cmd->stages);
139+    cmd->stages[cmd->nstages++] = slot;
140+  }
141+  if (cmd->nstages < 2)
142+    eprintf("manifest: (pipe ...) needs two stages\n");
143+  for (i = 0; i < cmd->nstages; i++) {
144+    char *joined = sl_join(cmd->stages[i]);
145+    if (i > 0)
146+      sl_push(&cmd->argv, "|");
147+    sl_push(&cmd->argv, joined);
148+    free(joined);
149+  }
150+}
151+
152+static void apply_rule_clause(struct Rule *r, const char *name, struct SNode *clause);
153+
154+static void
155+process_rule_body(struct Rule *r, const char *name, struct SNode *form, int start)
156+{
157+  int i;
158+  for (i = start; i < form->nkids; i++)
159+    apply_rule_clause(r, name, form->kids[i]);
160+}
161+
162+/* (if EXPR clause...) splices its clauses in when EXPR is true.
163+ * reuses the gate grammar so there is one condition language.
164+ * a $(...) in the condition is rejected: splicing per glob match
165+ * would mean deferring cmd/dep construction, which no rule needs.
166+ * use (gate ...) on a separate rule for a per-instance condition */
167+static void
168+apply_if_clause(struct Rule *r, const char *name, struct SNode *clause)
169+{
170+  struct Gate *cond;
171+
172+  if (clause->nkids < 2)
173+    eprintf("manifest: rule %s: (if EXPR clause...) needs a condition\n", name);
174+  cond = gate_parse(clause->kids[1]);
175+  if (gate_has_dyn(cond))
176+    eprintf(
177+        "manifest: rule %s: (if ...) condition cannot reference $(...), "
178+        "only (gate ...) on the whole rule supports a per-instance condition\n",
179+        name
180+    );
181+  if (gate_eval(cond))
182+    process_rule_body(r, name, clause, 2);
183+  gate_free(cond);
184+}
185+
186+static void
187+apply_rule_clause(struct Rule *r, const char *name, struct SNode *clause)
188+{
189+  const char *h = s_head(clause);
190+  if (!h)
191+    eprintf("manifest: rule %s: empty () clause\n", name);
192+
193+  if (strcmp(h, "if") == 0) {
194+    apply_if_clause(r, name, clause);
195+  } else if (strcmp(h, "glob") == 0) {
196+    int j;
197+    for (j = 1; j < clause->nkids; j++) {
198+      if (clause->kids[j]->kind != S_ATOM)
199+        eprintf("manifest: (glob ...) operands must be atoms\n");
200+      sl_push(&r->globs, clause->kids[j]->atom);
201+    }
202+  } else if (strcmp(h, "skip") == 0) {
203+    int j;
204+    for (j = 1; j < clause->nkids; j++) {
205+      if (clause->kids[j]->kind != S_ATOM)
206+        eprintf("manifest: (skip ...) operands must be atoms\n");
207+      sl_push(&r->skip, clause->kids[j]->atom);
208+    }
209+  } else if (strcmp(h, "mark") == 0) {
210+    strlcpy(
211+        r->require_marker,
212+        sole_atom(clause, "(mark STR) takes one string"),
213+        sizeof r->require_marker
214+    );
215+  } else if (strcmp(h, "req") == 0) {
216+    strlcpy(
217+        r->require_file, sole_atom(clause, "(req PATH) takes one path"), sizeof r->require_file
218+    );
219+  } else if (strcmp(h, "gate") == 0) {
220+    if (clause->nkids != 2)
221+      eprintf("manifest: (gate EXPR) takes one expression\n");
222+    r->gate = gate_parse(clause->kids[1]);
223+  } else if (strcmp(h, "dep") == 0) {
224+    parse_dep_clause(clause, &r->in, &r->extra_deps);
225+  } else if (strcmp(h, "after") == 0) {
226+    parse_dep_clause(clause, &r->extra_deps, &r->extra_deps);
227+  } else if (strcmp(h, "out") == 0) {
228+    strlcpy(r->out, sole_atom(clause, "(out TEMPLATE) takes one template"), sizeof r->out);
229+  } else if (strcmp(h, "exec") == 0) {
230+    parse_exec_clause(clause, &r->cmd);
231+  } else if (strcmp(h, "shell") == 0) {
232+    parse_shell_clause(clause, &r->cmd);
233+  } else if (strcmp(h, "pipe") == 0) {
234+    parse_pipe_clause(clause, &r->cmd);
235+  } else if (strcmp(h, "dir") == 0) {
236+    strlcpy(r->workdir, sole_atom(clause, "(dir PATH) takes one path"), sizeof r->workdir);
237+  } else if (strcmp(h, "phony") == 0) {
238+    r->phony = 1;
239+  } else if (strcmp(h, "redirect") == 0) {
240+    r->redirect = 1;
241+  } else if (strcmp(h, "produces") == 0) {
242+    strlcpy(r->produces, sole_atom(clause, "(produces NAME) takes one name"), sizeof r->produces);
243+  } else if (strcmp(h, "description") == 0) {
244+    strlcpy(
245+        r->description,
246+        sole_atom(clause, "(description STR) takes one string"),
247+        sizeof r->description
248+    );
249+  } else if (strcmp(h, "group") == 0) {
250+    /* display-only: shows up in (query rules) output, no effect on
251+     * resolution. (member ...) below is the enforcing one. the two
252+     * are separate because several rules used (group ...) this way
253+     * before namespacing existed */
254+    strlcpy(r->group_name, sole_atom(clause, "(group NAME) takes one name"), sizeof r->group_name);
255+  } else if (strcmp(h, "member") == 0) {
256+    /* (member GROUP [ALIAS]) hides the bare name from the command
257+     * line. ALIAS defaults to the rule own name */
258+    if (clause->nkids < 2 || clause->nkids > 3 || clause->kids[1]->kind != S_ATOM)
259+      eprintf("manifest: rule %s: (member GROUP [ALIAS]) needs a group name\n", name);
260+    strlcpy(r->member_of, clause->kids[1]->atom, sizeof r->member_of);
261+    if (clause->nkids == 3) {
262+      if (clause->kids[2]->kind != S_ATOM)
263+        eprintf("manifest: rule %s: (member GROUP ALIAS) alias must be an atom\n", name);
264+      strlcpy(r->member_alias, clause->kids[2]->atom, sizeof r->member_alias);
265+    }
266+  } else if (strcmp(h, "feature") == 0) {
267+    sl_push(&r->features, sole_atom(clause, "(feature NAME) takes one name"));
268+  } else {
269+    eprintf("manifest: rule %s: unknown clause '%s'\n", name, h);
270+  }
271+}
272+
273+static void
274+parse_rule(struct SNode *form)
275+{
276+  struct Rule *r;
277+  const char  *name;
278+
279+  name = first_atom(form, "(rule NAME ...) needs a name");
280+  r    = rule_new(name);
281+  process_rule_body(r, name, form, 2);
282+}
283+
284+static void
285+parse_group(struct SNode *form)
286+{
287+  struct Group *g;
288+  int           i;
289+  const char   *name;
290+
291+  name = first_atom(form, "(group NAME ...) needs a name");
292+  g    = group_new(name);
293+
294+  for (i = 2; i < form->nkids; i++) {
295+    struct SNode *ref = form->kids[i];
296+    const char   *h;
297+
298+    if (ref->kind == S_ATOM) {
299+      sl_push(&g->refs, ref->atom);
300+      continue;
301+    }
302+    h = s_head(ref);
303+    if (!h)
304+      eprintf("manifest: group %s: empty () ref\n", name);
305+    if ((strcmp(h, "rule") == 0 || strcmp(h, "group") == 0) && ref->nkids == 2
306+        && ref->kids[1]->kind == S_ATOM) {
307+      sl_push(&g->refs, ref->kids[1]->atom);
308+      continue;
309+    }
310+    eprintf("manifest: group %s: unknown ref '%s'\n", name, h);
311+  }
312+}
313+
314+/* stores K/V verbatim, unexpanded. expansion happens in meta_apply
315+ * at target-resolution time, so a value picks up whatever its vars
316+ * hold then, not what they held at load time */
317+static void
318+parse_meta(struct SNode *form)
319+{
320+  struct Meta *m;
321+  int          i;
322+  const char  *name;
323+
324+  name = first_atom(form, "(meta NAME ...) needs a name");
325+  m    = meta_new(name);
326+
327+  for (i = 2; i < form->nkids; i++) {
328+    struct SNode *clause = form->kids[i];
329+    const char   *h      = s_head(clause);
330+    if (!h || strcmp(h, "set") != 0)
331+      eprintf("manifest: meta %s: only (set NAME VAL) clauses are allowed\n", name);
332+    if (clause->nkids != 3 || clause->kids[1]->kind != S_ATOM || clause->kids[2]->kind != S_ATOM)
333+      eprintf("manifest: meta %s: (set NAME VAL) needs a name and an atom value\n", name);
334+    sl_push(&m->keys, clause->kids[1]->atom);
335+    sl_push(&m->vals, clause->kids[2]->atom);
336+  }
337+}
338+
339+void
340+meta_apply(struct Meta *m)
341+{
342+  int i;
343+  for (i = 0; i < m->keys.n; i++) {
344+    char *exp = kv_expand(m->vals.v[i]);
345+    kv_set_manifest(m->keys.v[i], exp);
346+    free(exp);
347+  }
348+}
349+
350+static void
351+parse_set(struct SNode *form)
352+{
353+  char buf[8192];
354+
355+  first_atom(form, "(set NAME VAL) needs a name");
356+  if (form->nkids != 3)
357+    eprintf("manifest: (set NAME VAL) takes one value\n");
358+
359+  if (form->kids[2]->kind == S_LIST) {
360+    const char *h = s_head(form->kids[2]);
361+    if (h && strcmp(h, "capture") == 0) {
362+      struct SNode *cap = form->kids[2];
363+      struct Cmd    cmd;
364+      char         *out;
365+
366+      if (cap->nkids != 2)
367+        eprintf("manifest: (capture (exec ...)) takes one form\n");
368+      h = s_head(cap->kids[1]);
369+      if (!h)
370+        eprintf("manifest: capture operand must be a form\n");
371+      if (strcmp(h, "exec") == 0)
372+        parse_exec_clause(cap->kids[1], &cmd);
373+      else if (strcmp(h, "shell") == 0)
374+        parse_shell_clause(cap->kids[1], &cmd);
375+      else
376+        eprintf("manifest: capture operand must be exec or shell\n");
377+      out = capture_argv(&cmd.argv);
378+      strlcpy(buf, out, sizeof buf);
379+      free(out);
380+      kv_set_manifest(form->kids[1]->atom, buf);
381+      return;
382+    }
383+    eprintf("manifest: (set NAME VAL): value must be atom or (capture ...)\n");
384+  }
385+  kv_set_manifest(form->kids[1]->atom, form->kids[2]->atom);
386+}
387+
388+/* (list) and (append) keep a space-joined string in the kv store.
389+ * a $(NAME) alone in a command splits back into argv entries */
390+static void
391+parse_list_or_append(struct SNode *form, int append)
392+{
393+  const char *name;
394+  char        buf[8192];
395+  char        msg[64];
396+  size_t      len = 0;
397+  int         i;
398+  char       *prev;
399+
400+  snprintf(msg, sizeof msg, "(%s NAME tok...) needs a name", form->kids[0]->atom);
401+  name   = first_atom(form, msg);
402+  buf[0] = '\0';
403+
404+  if (append) {
405+    prev = kv_get(name);
406+    if (prev)
407+      len = strlcpy(buf, prev, sizeof buf);
408+  }
409+  for (i = 2; i < form->nkids; i++) {
410+    struct SNode *tok = form->kids[i];
411+    char         *exp;
412+    if (tok->kind != S_ATOM)
413+      eprintf("manifest: (%s NAME tok...): tokens must be atoms\n", form->kids[0]->atom);
414+    if (len > 0 && len + 1 < sizeof buf)
415+      buf[len++] = ' ';
416+    exp = kv_expand(tok->atom);
417+    len += strlcpy(buf + len, exp, sizeof buf - len);
418+    free(exp);
419+  }
420+  kv_set_manifest(name, buf);
421+}
422+
423+static void
424+parse_map(struct SNode *form)
425+{
426+  struct Map *m;
427+  int         i;
428+  const char *name;
429+
430+  name = first_atom(form, "(map NAME (k v)...) needs a name");
431+  m    = map_find(name);
432+  if (!m)
433+    m = map_new(name);
434+
435+  for (i = 2; i < form->nkids; i++) {
436+    struct SNode *pair = form->kids[i];
437+    if (!s_is_list(pair) || pair->nkids != 2 || pair->kids[0]->kind != S_ATOM
438+        || pair->kids[1]->kind != S_ATOM)
439+      eprintf("manifest: (map %s ...) entries must be (key value)\n", name);
440+    kv_set_raw(&m->entries, pair->kids[0]->atom, pair->kids[1]->atom);
441+  }
442+}
443+
444+static void
445+parse_import(struct SNode *form)
446+{
447+  char        rp[PATH_MAX];
448+  const char *path;
449+
450+  path = sole_atom(form, "(import PATH) takes one path");
451+
452+  if (realpath(path, rp) && sl_has(&files_read, rp))
453+    return;
454+
455+  materialize_if_missing(path);
456+  if (!file_exists(path))
457+    eprintf("manifest: (import %s): missing and no rule produces it\n", path);
458+  load_file(path);
459+}
460+
461+static void
462+parse_set_file(struct SNode *form)
463+{
464+  const char *path;
465+  FILE       *fp;
466+  char        line[8192];
467+  char        rp[PATH_MAX];
468+
469+  path = sole_atom(form, "(set-file PATH) takes one path");
470+
471+  materialize_if_missing(path);
472+  if (!file_exists(path))
473+    eprintf("manifest: (set-file %s): missing and no rule produces it\n", path);
474+
475+  fp = fopen(path, "r");
476+  if (!fp)
477+    eprintf("manifest: (set-file %s):", path);
478+  if (realpath(path, rp))
479+    sl_push(&files_read, rp);
480+
481+  while (fgets(line, sizeof line, fp)) {
482+    char *s = line, *eq, *end;
483+    while (isspace((unsigned char)*s))
484+      s++;
485+    if (*s == '\0' || *s == '#')
486+      continue;
487+    eq = strchr(s, '=');
488+    if (!eq)
489+      continue;
490+    *eq = '\0';
491+    end = eq - 1;
492+    while (end > s && isspace((unsigned char)*end))
493+      *end-- = '\0';
494+    eq++;
495+    while (isspace((unsigned char)*eq))
496+      eq++;
497+    end = eq + strlen(eq);
498+    while (end > eq && (isspace((unsigned char)end[-1]) || end[-1] == '\n'))
499+      *--end = '\0';
500+    kv_set_manifest(s, eq);
501+  }
502+  fclose(fp);
503+}
504+
505+/* (template NAME (PARAM...) FORM...): the body is cloned so it stays
506+ * valid regardless of when or how many times (use ...) instantiates
507+ * it */
508+static void
509+parse_template(struct SNode *form)
510+{
511+  struct Template *t;
512+  struct SNode    *params;
513+  const char      *name;
514+  int              i;
515+
516+  name = first_atom(form, "(template NAME (PARAM...) FORM...) needs a name");
517+  if (form->nkids < 3 || form->kids[2]->kind != S_LIST)
518+    eprintf("manifest: template %s: needs a (PARAM...) list\n", name);
519+  params = form->kids[2];
520+
521+  t = template_new(name);
522+  for (i = 0; i < params->nkids; i++) {
523+    if (params->kids[i]->kind != S_ATOM)
524+      eprintf("manifest: template %s: param names must be atoms\n", name);
525+    sl_push(&t->params, params->kids[i]->atom);
526+  }
527+  t->body = snode_list(form->line);
528+  for (i = 3; i < form->nkids; i++)
529+    snode_push(t->body, snode_clone(form->kids[i]));
530+}
531+
532+/* clone the template body, substitute each $(PARAM), then dispatch
533+ * every substituted form as a normal top-level form */
534+static void
535+parse_use(struct SNode *form)
536+{
537+  struct Template *t;
538+  struct SNode    *clone;
539+  const char      *name;
540+  char           **vals;
541+  int              i, nargs;
542+
543+  name = first_atom(form, "(use NAME arg...) needs a name");
544+  t    = template_find(name);
545+  if (!t)
546+    eprintf("manifest: (use %s ...): no such template\n", name);
547+
548+  nargs = form->nkids - 2;
549+  if (nargs != t->params.n)
550+    eprintf("manifest: (use %s ...): takes %d argument(s), got %d\n", name, t->params.n, nargs);
551+
552+  vals = emalloc((size_t)(nargs ? nargs : 1) * sizeof *vals);
553+  for (i = 0; i < nargs; i++) {
554+    if (form->kids[i + 2]->kind != S_ATOM)
555+      eprintf("manifest: (use %s ...): arguments must be atoms\n", name);
556+    vals[i] = form->kids[i + 2]->atom;
557+  }
558+
559+  clone = snode_clone(t->body);
560+  snode_subst(clone, &t->params, vals);
561+  for (i = 0; i < clone->nkids; i++)
562+    dispatch_top_form(clone->kids[i]);
563+  snode_free(clone);
564+  free(vals);
565+}
566+
567+void
568+dispatch_top_form(struct SNode *form)
569+{
570+  const char *h = s_head(form);
571+  if (!h)
572+    eprintf("manifest: top-level form has no head\n");
573+  if (strcmp(h, "set") == 0)
574+    parse_set(form);
575+  else if (strcmp(h, "list") == 0)
576+    parse_list_or_append(form, 0);
577+  else if (strcmp(h, "append") == 0)
578+    parse_list_or_append(form, 1);
579+  else if (strcmp(h, "map") == 0)
580+    parse_map(form);
581+  else if (strcmp(h, "rule") == 0)
582+    parse_rule(form);
583+  else if (strcmp(h, "group") == 0)
584+    parse_group(form);
585+  else if (strcmp(h, "meta") == 0)
586+    parse_meta(form);
587+  else if (strcmp(h, "import") == 0)
588+    parse_import(form);
589+  else if (strcmp(h, "set-file") == 0)
590+    parse_set_file(form);
591+  else if (strcmp(h, "template") == 0)
592+    parse_template(form);
593+  else if (strcmp(h, "use") == 0)
594+    parse_use(form);
595+  else
596+    eprintf("manifest: unknown top-level form '%s'\n", h);
597+}
598+
599+void
600+load_manifest(struct SNode *root)
601+{
602+  int i;
603+  for (i = 0; i < root->nkids; i++)
604+    dispatch_top_form(root->kids[i]);
605+}
606+
607+void
608+load_file(const char *path)
609+{
610+  FILE         *fp;
611+  char         *buf;
612+  size_t        cap, len;
613+  char          rp[PATH_MAX];
614+  struct SLex   lx;
615+  struct SNode *root;
616+
617+  if (realpath(path, rp)) {
618+    if (sl_has(&files_read, rp))
619+      return;
620+    sl_push(&files_read, rp);
621+  }
622+
623+  fp = fopen(path, "r");
624+  if (!fp)
625+    eprintf("cannot open manifest %s:", path);
626+
627+  cap = 65536;
628+  len = 0;
629+  buf = emalloc(cap);
630+  for (;;) {
631+    size_t n;
632+    if (len + 65536 > cap) {
633+      cap *= 2;
634+      buf = erealloc(buf, cap);
635+    }
636+    n = fread(buf + len, 1, cap - len - 1, fp);
637+    len += n;
638+    if (n == 0)
639+      break;
640+  }
641+  buf[len] = '\0';
642+  fclose(fp);
643+
644+  lx.src  = buf;
645+  lx.pos  = 0;
646+  lx.line = 1;
647+  root    = snode_parse(&lx);
648+  free(buf);
649+  load_manifest(root);
650+  snode_free(root);
651+}
+203, -0
  1@@ -0,0 +1,203 @@
  2+#include "ninqu.h"
  3+
  4+#include <stdio.h>
  5+#include <stdlib.h>
  6+#include <string.h>
  7+
  8+/* walk every gate tree, collect the static var names and the rules
  9+ * each one gates. skips $(...) vars since those are per-instance */
 10+static void
 11+collect_gate_vars(struct StrList *out, struct StrList *affects, struct StrList *rules_affected)
 12+{
 13+  int i;
 14+  for (i = 0; i < nrules; i++) {
 15+    struct Rule   *r    = &rules[i];
 16+    struct Gate   *g    = r->gate;
 17+    struct StrList seen = {0};
 18+    struct Gate  **stack;
 19+    int            sp = 0;
 20+
 21+    if (!g)
 22+      continue;
 23+    stack       = emalloc(64 * sizeof *stack);
 24+    stack[sp++] = g;
 25+    while (sp > 0) {
 26+      struct Gate *cur = stack[--sp];
 27+      int          k;
 28+      if (cur->kind == G_VAR) {
 29+        if (strstr(cur->var, "$("))
 30+          continue;
 31+        if (!sl_has(&seen, cur->var)) {
 32+          sl_push(&seen, cur->var);
 33+          if (!sl_has(out, cur->var))
 34+            sl_push(out, cur->var);
 35+          sl_push(affects, cur->var);
 36+          sl_push(rules_affected, r->name);
 37+        }
 38+        continue;
 39+      }
 40+      for (k = 0; k < cur->nkids; k++)
 41+        stack[sp++] = cur->kids[k];
 42+    }
 43+    free(stack);
 44+  }
 45+}
 46+
 47+/* (feature NAME) tags, separate from gate vars. a rule can carry
 48+ * more than one, so the pairing is flattened */
 49+static void
 50+collect_named_features(struct StrList *out, struct StrList *owns, struct StrList *rules_tagged)
 51+{
 52+  int i, j;
 53+  for (i = 0; i < nrules; i++) {
 54+    struct Rule *r = &rules[i];
 55+    for (j = 0; j < r->features.n; j++) {
 56+      const char *feat = r->features.v[j];
 57+      if (!sl_has(out, feat))
 58+        sl_push(out, feat);
 59+      sl_push(owns, feat);
 60+      sl_push(rules_tagged, r->name);
 61+    }
 62+  }
 63+}
 64+
 65+void
 66+do_features(void)
 67+{
 68+  struct StrList gatevars = {0}, affects = {0}, affected = {0};
 69+  struct StrList featnames = {0}, feat_owns = {0}, feat_rules = {0};
 70+  int            i, j;
 71+
 72+  printf("variables (override with -Dname=value):\n");
 73+  for (i = 0; i < kv.n; i++)
 74+    printf(
 75+        "  %s=%s%s\n",
 76+        kv.v[i].key,
 77+        kv.v[i].val,
 78+        sl_has(&kv_locked, kv.v[i].key) ? "  [locked by -D]" : ""
 79+    );
 80+
 81+  collect_gate_vars(&gatevars, &affects, &affected);
 82+  printf("\nfeature gates (override with -Dname=0 or -Dname=1):\n");
 83+  for (i = 0; i < gatevars.n; i++) {
 84+    printf("  %s=%s\n", gatevars.v[i], kv_get_or(gatevars.v[i], ""));
 85+    for (j = 0; j < affects.n; j++)
 86+      if (strcmp(affects.v[j], gatevars.v[i]) == 0)
 87+        printf("      gates rule: %s\n", affected.v[j]);
 88+  }
 89+
 90+  collect_named_features(&featnames, &feat_owns, &feat_rules);
 91+  if (featnames.n > 0) {
 92+    printf("\nnamed features (declared with (feature NAME)):\n");
 93+    for (i = 0; i < featnames.n; i++) {
 94+      printf("  %s\n", featnames.v[i]);
 95+      for (j = 0; j < feat_owns.n; j++)
 96+        if (strcmp(feat_owns.v[j], featnames.v[i]) == 0)
 97+          printf("      rule: %s\n", feat_rules.v[j]);
 98+    }
 99+  }
100+
101+  printf("\ngroups:\n");
102+  for (i = 0; i < ngroups; i++)
103+    printf("  %s\n", groups[i].name);
104+
105+  printf("\nrules:\n");
106+  for (i = 0; i < nrules; i++) {
107+    struct Rule *r = &rules[i];
108+    printf("  %s%s\n", r->name, r->globs.n > 0 ? "  (pattern rule)" : "");
109+    if (r->description[0])
110+      printf("      # %s\n", r->description);
111+    if (r->group_name[0])
112+      printf("      group: %s\n", r->group_name);
113+    if (r->member_of[0])
114+      printf("      member: %s/%s\n", r->member_of, r->member_alias[0] ? r->member_alias : r->name);
115+    for (j = 0; j < r->features.n; j++)
116+      printf("      feature: %s\n", r->features.v[j]);
117+  }
118+}
119+
120+void
121+do_query(const char *sub, const char *arg)
122+{
123+  int i, j;
124+
125+  if (strcmp(sub, "features") == 0) {
126+    do_features();
127+    return;
128+  }
129+  if (strcmp(sub, "groups") == 0) {
130+    for (i = 0; i < ngroups; i++) {
131+      printf("%s\n", groups[i].name);
132+      for (j = 0; j < groups[i].refs.n; j++)
133+        printf("  %s\n", groups[i].refs.v[j]);
134+    }
135+    return;
136+  }
137+  if (strcmp(sub, "metas") == 0) {
138+    for (i = 0; i < nmetas; i++) {
139+      printf("%s\n", metas[i].name);
140+      for (j = 0; j < metas[i].keys.n; j++)
141+        printf("  %s=%s\n", metas[i].keys.v[j], metas[i].vals.v[j]);
142+    }
143+    return;
144+  }
145+  if (strcmp(sub, "rules") == 0) {
146+    for (i = 0; i < nrules; i++) {
147+      printf("%s", rules[i].name);
148+      if (rules[i].globs.n > 0) {
149+        printf("  glob:");
150+        for (j = 0; j < rules[i].globs.n; j++)
151+          printf(" %s", rules[i].globs.v[j]);
152+      }
153+      if (rules[i].produces[0])
154+        printf("  produces=%s", rules[i].produces);
155+      if (rules[i].group_name[0])
156+        printf("  group=%s", rules[i].group_name);
157+      if (rules[i].member_of[0])
158+        printf(
159+            "  member=%s/%s",
160+            rules[i].member_of,
161+            rules[i].member_alias[0] ? rules[i].member_alias : rules[i].name
162+        );
163+      for (j = 0; j < rules[i].features.n; j++)
164+        printf("  feature=%s", rules[i].features.v[j]);
165+      if (rules[i].description[0])
166+        printf("  # %s", rules[i].description);
167+      printf("\n");
168+    }
169+    return;
170+  }
171+  if (strcmp(sub, "graph") == 0) {
172+    printf("digraph ninqu {\n");
173+    if (arg) {
174+      struct Group *g = group_find(arg);
175+      if (!g)
176+        eprintf("query graph: no such group '%s'\n", arg);
177+      for (i = 0; i < g->refs.n; i++) {
178+        struct Rule *r = rule_find(g->refs.v[i]);
179+        if (!r)
180+          continue;
181+        for (j = 0; j < r->in.n; j++)
182+          if (r->in.v[j][0] == '@')
183+            printf("  \"%s\" -> \"%s\";\n", r->in.v[j] + 1, r->name);
184+        for (j = 0; j < r->extra_deps.n; j++)
185+          if (r->extra_deps.v[j][0] == '@')
186+            printf("  \"%s\" -> \"%s\" [style=dashed];\n", r->extra_deps.v[j] + 1, r->name);
187+      }
188+    } else {
189+      for (i = 0; i < nrules; i++) {
190+        for (j = 0; j < rules[i].in.n; j++)
191+          if (rules[i].in.v[j][0] == '@')
192+            printf("  \"%s\" -> \"%s\";\n", rules[i].in.v[j] + 1, rules[i].name);
193+        for (j = 0; j < rules[i].extra_deps.n; j++)
194+          if (rules[i].extra_deps.v[j][0] == '@')
195+            printf(
196+                "  \"%s\" -> \"%s\" [style=dashed];\n", rules[i].extra_deps.v[j] + 1, rules[i].name
197+            );
198+      }
199+    }
200+    printf("}\n");
201+    return;
202+  }
203+  eprintf("query: unknown subcommand '%s' (try features, groups, metas, rules, graph)\n", sub);
204+}
+167, -0
  1@@ -0,0 +1,167 @@
  2+#include "ninqu.h"
  3+
  4+#include <stdio.h>
  5+#include <stdlib.h>
  6+#include <string.h>
  7+
  8+struct Rule     *rules;
  9+int              nrules, rulescap;
 10+struct Group    *groups;
 11+int              ngroups, groupscap;
 12+struct Meta     *metas;
 13+int              nmetas, metascap;
 14+struct Template *templates;
 15+int              ntemplates, templatescap;
 16+struct Inst     *insts;
 17+int              ninsts, instscap;
 18+
 19+struct Rule *
 20+rule_find(const char *name)
 21+{
 22+  int i;
 23+  for (i = 0; i < nrules; i++)
 24+    if (strcmp(rules[i].name, name) == 0)
 25+      return &rules[i];
 26+  return NULL;
 27+}
 28+
 29+/* match (out) against path, expanding $(...) as needed. used to
 30+ * find a producer for a file that does not exist yet */
 31+struct Rule *
 32+rule_find_output(const char *path)
 33+{
 34+  int i;
 35+  for (i = 0; i < nrules; i++) {
 36+    if (!rules[i].out[0])
 37+      continue;
 38+    if (strchr(rules[i].out, '$')) {
 39+      char *exp = kv_expand(rules[i].out);
 40+      int   ok  = strcmp(exp, path) == 0;
 41+      free(exp);
 42+      if (ok)
 43+        return &rules[i];
 44+    } else if (strcmp(rules[i].out, path) == 0) {
 45+      return &rules[i];
 46+    }
 47+  }
 48+  return NULL;
 49+}
 50+
 51+struct Rule *
 52+rule_find_produces(const char *name)
 53+{
 54+  int i;
 55+  for (i = 0; i < nrules; i++)
 56+    if (rules[i].produces[0] && strcmp(rules[i].produces, name) == 0)
 57+      return &rules[i];
 58+  return NULL;
 59+}
 60+
 61+struct Rule *
 62+rule_new(const char *name)
 63+{
 64+  struct Rule *r;
 65+  if (rule_find(name))
 66+    eprintf("manifest: duplicate rule '%s'\n", name);
 67+  if (group_find(name))
 68+    eprintf("manifest: name '%s' already a group\n", name);
 69+  GROW(rules, nrules, rulescap, 16);
 70+  r = &rules[nrules++];
 71+  memset(r, 0, sizeof *r);
 72+  strlcpy(r->name, name, sizeof r->name);
 73+  return r;
 74+}
 75+
 76+struct Group *
 77+group_find(const char *name)
 78+{
 79+  int i;
 80+  for (i = 0; i < ngroups; i++)
 81+    if (strcmp(groups[i].name, name) == 0)
 82+      return &groups[i];
 83+  return NULL;
 84+}
 85+
 86+struct Group *
 87+group_new(const char *name)
 88+{
 89+  struct Group *g;
 90+  if (group_find(name))
 91+    eprintf("manifest: duplicate group '%s'\n", name);
 92+  if (rule_find(name))
 93+    eprintf("manifest: name '%s' already a rule\n", name);
 94+  GROW(groups, ngroups, groupscap, 16);
 95+  g = &groups[ngroups++];
 96+  memset(g, 0, sizeof *g);
 97+  strlcpy(g->name, name, sizeof g->name);
 98+  return g;
 99+}
100+
101+struct Meta *
102+meta_find(const char *name)
103+{
104+  int i;
105+  for (i = 0; i < nmetas; i++)
106+    if (strcmp(metas[i].name, name) == 0)
107+      return &metas[i];
108+  return NULL;
109+}
110+
111+struct Meta *
112+meta_new(const char *name)
113+{
114+  struct Meta *m;
115+  if (meta_find(name))
116+    eprintf("manifest: duplicate meta '%s'\n", name);
117+  if (rule_find(name))
118+    eprintf("manifest: name '%s' already a rule\n", name);
119+  if (group_find(name))
120+    eprintf("manifest: name '%s' already a group\n", name);
121+  GROW(metas, nmetas, metascap, 8);
122+  m = &metas[nmetas++];
123+  memset(m, 0, sizeof *m);
124+  strlcpy(m->name, name, sizeof m->name);
125+  return m;
126+}
127+
128+struct Template *
129+template_find(const char *name)
130+{
131+  int i;
132+  for (i = 0; i < ntemplates; i++)
133+    if (strcmp(templates[i].name, name) == 0)
134+      return &templates[i];
135+  return NULL;
136+}
137+
138+struct Template *
139+template_new(const char *name)
140+{
141+  struct Template *t;
142+  if (template_find(name))
143+    eprintf("manifest: duplicate template '%s'\n", name);
144+  GROW(templates, ntemplates, templatescap, 8);
145+  t = &templates[ntemplates++];
146+  memset(t, 0, sizeof *t);
147+  strlcpy(t->name, name, sizeof t->name);
148+  return t;
149+}
150+
151+int
152+inst_new(int rule_idx)
153+{
154+  struct Inst *in;
155+  struct Rule *r = &rules[rule_idx];
156+
157+  GROW(insts, ninsts, instscap, 32);
158+  in = &insts[ninsts];
159+  memset(in, 0, sizeof *in);
160+  in->rule_idx = rule_idx;
161+
162+  if (r->n_inst >= r->cap_inst) {
163+    r->cap_inst = r->cap_inst ? r->cap_inst * 2 : 4;
164+    r->inst_idx = erealloc(r->inst_idx, (size_t)r->cap_inst * sizeof *r->inst_idx);
165+  }
166+  r->inst_idx[r->n_inst++] = ninsts;
167+  return ninsts++;
168+}
+195, -0
  1@@ -0,0 +1,195 @@
  2+#include "ninqu.h"
  3+
  4+#include <stdio.h>
  5+#include <stdlib.h>
  6+#include <string.h>
  7+
  8+/* pull in every rule a group names, recursively */
  9+static struct StrList visited_groups;
 10+
 11+void
 12+include_ref(const char *name)
 13+{
 14+  struct Group *g = group_find(name);
 15+  int           i;
 16+
 17+  if (g) {
 18+    if (sl_has(&visited_groups, name))
 19+      return;
 20+    sl_push(&visited_groups, name);
 21+    for (i = 0; i < g->refs.n; i++)
 22+      include_ref(g->refs.v[i]);
 23+    return;
 24+  }
 25+  if (rule_find(name)) {
 26+    expand_rule(name);
 27+    return;
 28+  }
 29+  eprintf("manifest: '%s' is neither a known group nor a known rule\n", name);
 30+}
 31+
 32+void
 33+build_default_group(void)
 34+{
 35+  struct Group *def = group_find("default");
 36+  int           i;
 37+  if (!def)
 38+    eprintf("no group given, and manifest defines no (group default)\n");
 39+  for (i = 0; i < def->refs.n; i++)
 40+    include_ref(def->refs.v[i]);
 41+}
 42+
 43+/* match on ALIAS when the rule gave one, otherwise on the rule own
 44+ * bare name, so a member with no alias is still reachable under its
 45+ * existing name */
 46+static struct Rule *
 47+rule_find_member(const char *group, const char *alias)
 48+{
 49+  int i;
 50+  for (i = 0; i < nrules; i++) {
 51+    struct Rule *r = &rules[i];
 52+    if (!r->member_of[0] || strcmp(r->member_of, group) != 0)
 53+      continue;
 54+    if (r->member_alias[0]) {
 55+      if (strcmp(r->member_alias, alias) == 0)
 56+        return r;
 57+    } else if (strcmp(r->name, alias) == 0) {
 58+      return r;
 59+    }
 60+  }
 61+  return NULL;
 62+}
 63+
 64+/* GROUP/ALIAS qualified addressing. leading segments before the
 65+ * last two are ignored, so "posix/bc" and "cmd/posix/bc" resolve
 66+ * the same rule. returns NULL when w has no '/', so callers can
 67+ * fall through to plain group/rule resolution. a qualified path
 68+ * that does not resolve is a hard error, not a silent fallthrough */
 69+static struct Rule *
 70+resolve_member_path(const char *w)
 71+{
 72+  const char  *last_slash;
 73+  const char  *alias;
 74+  const char  *group_start;
 75+  const char  *p;
 76+  char         group[128];
 77+  size_t       glen;
 78+  struct Rule *r;
 79+
 80+  last_slash = strrchr(w, '/');
 81+  if (!last_slash)
 82+    return NULL;
 83+  alias = last_slash + 1;
 84+  if (!*alias)
 85+    eprintf("manifest: '%s' ends in '/', expected GROUP/ALIAS\n", w);
 86+
 87+  group_start = w;
 88+  for (p = w; p < last_slash; p++)
 89+    if (*p == '/')
 90+      group_start = p + 1;
 91+  glen = (size_t)(last_slash - group_start);
 92+  if (glen == 0 || glen >= sizeof group)
 93+    eprintf("manifest: '%s': bad GROUP/ALIAS path\n", w);
 94+  memcpy(group, group_start, glen);
 95+  group[glen] = '\0';
 96+
 97+  if (!group_find(group))
 98+    eprintf("manifest: '%s': no such group '%s'\n", w, group);
 99+  r = rule_find_member(group, alias);
100+  if (!r)
101+    eprintf("manifest: '%s' names no member of group '%s'\n", alias, group);
102+  return r;
103+}
104+
105+/* META.TARGET applies META overrides then resolves TARGET.
106+ * META alone applies META then falls back to (group default).
107+ * anything else is a plain group or rule name */
108+void
109+resolve_wanted(const char *w)
110+{
111+  const char  *dot = strchr(w, '.');
112+  struct Meta *m;
113+  char         meta_name[128];
114+  struct Rule *member;
115+
116+  if (strchr(w, '/')) {
117+    member = resolve_member_path(w);
118+    expand_rule(member->name);
119+    return;
120+  }
121+
122+  if (dot) {
123+    size_t len = (size_t)(dot - w);
124+    if (len >= sizeof meta_name)
125+      eprintf("manifest: meta name too long in '%s'\n", w);
126+    memcpy(meta_name, w, len);
127+    meta_name[len] = '\0';
128+    m              = meta_find(meta_name);
129+    if (!m)
130+      eprintf("manifest: '%s' names no known meta-target\n", meta_name);
131+    meta_apply(m);
132+    include_ref(dot + 1);
133+    return;
134+  }
135+
136+  m = meta_find(w);
137+  if (m) {
138+    meta_apply(m);
139+    build_default_group();
140+    return;
141+  }
142+
143+  /* a rule that declared (member ...) does not resolve by bare name
144+   * from the command line, only through GROUP/ALIAS or transitively
145+   * via its owning group ref list */
146+  {
147+    struct Rule *br = rule_find(w);
148+    if (br && br->member_of[0])
149+      eprintf(
150+          "manifest: '%s' is a member of group '%s', not a top-level target; "
151+          "use '%s/%s'\n",
152+          w,
153+          br->member_of,
154+          br->member_of,
155+          br->member_alias[0] ? br->member_alias : br->name
156+      );
157+  }
158+
159+  include_ref(w);
160+}
161+
162+/* a plain token in (dep) or (after) that names an existing rule
163+ * means the same as (rule NAME). resolved once after the whole
164+ * manifest loads, so a rule declared later can still be named
165+ * earlier. a token with $, glob metachars, /, or . is left alone
166+ * so this never shadows a real file dependency */
167+static int
168+looks_like_path(const char *s)
169+{
170+  return strpbrk(s, "$*?/.") != NULL;
171+}
172+
173+static void
174+resolve_bareword_deps(struct StrList *sl)
175+{
176+  int i;
177+  for (i = 0; i < sl->n; i++) {
178+    char *tok = sl->v[i];
179+    char  buf[256];
180+    if (tok[0] == '@' || looks_like_path(tok) || !rule_find(tok))
181+      continue;
182+    snprintf(buf, sizeof buf, "@%s", tok);
183+    free(sl->v[i]);
184+    sl->v[i] = estrdup(buf);
185+  }
186+}
187+
188+void
189+resolve_all_bareword_deps(void)
190+{
191+  int i;
192+  for (i = 0; i < nrules; i++) {
193+    resolve_bareword_deps(&rules[i].in);
194+    resolve_bareword_deps(&rules[i].extra_deps);
195+  }
196+}
+93, -0
 1@@ -0,0 +1,93 @@
 2+#include "ninqu.h"
 3+
 4+#include <stdio.h>
 5+#include <stdlib.h>
 6+
 7+/* kahn topo sort, one ready level at a time so each level
 8+ * runs in parallel. failures are reported but do not stop the
 9+ * batch, so one broken file does not block the rest */
10+void
11+schedule_and_run(void)
12+{
13+  int  *indeg      = ecalloc((size_t)ninsts, sizeof *indeg);
14+  int **dependents = emalloc((size_t)ninsts * sizeof *dependents);
15+  int  *ndep       = ecalloc((size_t)ninsts, sizeof *ndep);
16+  int  *capdep     = ecalloc((size_t)ninsts, sizeof *capdep);
17+  int  *level      = emalloc((size_t)ninsts * sizeof *level);
18+  int   levn = 0, done_count = 0;
19+  int   i, j;
20+
21+  for (i = 0; i < ninsts; i++) {
22+    indeg[i]      = insts[i].n_dep;
23+    dependents[i] = NULL;
24+  }
25+  for (i = 0; i < ninsts; i++) {
26+    for (j = 0; j < insts[i].n_dep; j++) {
27+      int d = insts[i].dep_inst[j];
28+      if (ndep[d] >= capdep[d]) {
29+        capdep[d]     = capdep[d] ? capdep[d] * 2 : 4;
30+        dependents[d] = erealloc(dependents[d], (size_t)capdep[d] * sizeof *dependents[d]);
31+      }
32+      dependents[d][ndep[d]++] = i;
33+    }
34+  }
35+  for (i = 0; i < ninsts; i++)
36+    if (indeg[i] == 0)
37+      level[levn++] = i;
38+
39+  while (levn > 0) {
40+    int *next_level = emalloc((size_t)ninsts * sizeof *next_level);
41+    int  nextn      = 0;
42+
43+    if (summary_mode) {
44+      for (i = 0; i < levn; i++) {
45+        struct Inst *inst = &insts[level[i]];
46+        int          will = inst_stale(inst);
47+        int          d;
48+        for (d = 0; d < inst->n_dep && !will; d++)
49+          if (insts[inst->dep_inst[d]].will_build)
50+            will = 1;
51+        inst->will_build = will;
52+        printf(
53+            "%-4s %-12s %s\n",
54+            will ? "BUILD" : "OK",
55+            rules[inst->rule_idx].name,
56+            inst->out[0] ? inst->out : "(phony)"
57+        );
58+        if (rules[inst->rule_idx].description[0])
59+          printf("       # %s\n", rules[inst->rule_idx].description);
60+        if (will) {
61+          char *cmd = sl_join(&inst->cmd.argv);
62+          printf("       cmd: %s\n", cmd);
63+          free(cmd);
64+        }
65+      }
66+    } else {
67+      run_batch(level, levn);
68+    }
69+
70+    done_count += levn;
71+    for (i = 0; i < levn; i++) {
72+      int u = level[i];
73+      for (j = 0; j < ndep[u]; j++) {
74+        int v = dependents[u][j];
75+        if (--indeg[v] == 0)
76+          next_level[nextn++] = v;
77+      }
78+    }
79+    free(level);
80+    level = next_level;
81+    levn  = nextn;
82+  }
83+
84+  if (done_count < ninsts)
85+    eprintf("manifest: dependency cycle among %d instances\n", ninsts - done_count);
86+
87+  free(level);
88+  free(indeg);
89+  for (i = 0; i < ninsts; i++)
90+    free(dependents[i]);
91+  free(dependents);
92+  free(ndep);
93+  free(capdep);
94+}
+344, -0
  1@@ -0,0 +1,344 @@
  2+#include "ninqu.h"
  3+
  4+#include <ctype.h>
  5+#include <stdio.h>
  6+#include <stdlib.h>
  7+#include <string.h>
  8+
  9+/* track nesting so $(EXTRA_LIBS:$(BASESTEM)) does not stop at the
 10+ * inner close paren. shared by kv_expand and subst_tmpl */
 11+const char *
 12+find_dollar_close(const char *inner)
 13+{
 14+  const char *scan  = inner;
 15+  int         depth = 1;
 16+
 17+  while (*scan) {
 18+    if (scan[0] == '$' && scan[1] == '(') {
 19+      depth++;
 20+      scan += 2;
 21+      continue;
 22+    }
 23+    if (*scan == ')') {
 24+      if (--depth == 0)
 25+        return scan;
 26+    }
 27+    scan++;
 28+  }
 29+  return NULL;
 30+}
 31+
 32+struct SNode *
 33+snode_atom(const char *s, int line)
 34+{
 35+  struct SNode *n = ecalloc(1, sizeof *n);
 36+  n->kind         = S_ATOM;
 37+  n->atom         = estrdup(s);
 38+  n->line         = line;
 39+  return n;
 40+}
 41+
 42+struct SNode *
 43+snode_list(int line)
 44+{
 45+  struct SNode *n = ecalloc(1, sizeof *n);
 46+  n->kind         = S_LIST;
 47+  n->line         = line;
 48+  return n;
 49+}
 50+
 51+void
 52+snode_push(struct SNode *list, struct SNode *kid)
 53+{
 54+  if (list->nkids == 0) {
 55+    list->kids = emalloc(8 * sizeof *list->kids);
 56+  } else {
 57+    int cap = 8;
 58+    while (cap < list->nkids)
 59+      cap *= 2;
 60+    if (list->nkids == cap)
 61+      list->kids = erealloc(list->kids, (size_t)cap * 2 * sizeof *list->kids);
 62+  }
 63+  list->kids[list->nkids++] = kid;
 64+}
 65+
 66+void
 67+snode_free(struct SNode *n)
 68+{
 69+  int i;
 70+  if (!n)
 71+    return;
 72+  if (n->kind == S_LIST) {
 73+    for (i = 0; i < n->nkids; i++)
 74+      snode_free(n->kids[i]);
 75+    free(n->kids);
 76+  }
 77+  free(n->atom);
 78+  free(n);
 79+}
 80+
 81+/* deep copy. a template body is parsed from a file that gets freed
 82+ * once load_manifest returns, so the stored copy cannot point back
 83+ * into that tree */
 84+struct SNode *
 85+snode_clone(struct SNode *n)
 86+{
 87+  struct SNode *out;
 88+  int           i;
 89+
 90+  if (!n)
 91+    return NULL;
 92+  if (n->kind == S_ATOM)
 93+    return snode_atom(n->atom, n->line);
 94+  out = snode_list(n->line);
 95+  for (i = 0; i < n->nkids; i++)
 96+    snode_push(out, snode_clone(n->kids[i]));
 97+  return out;
 98+}
 99+
100+/* replace each $(PARAM) with its argument text. any other $(...) is
101+ * copied verbatim for the later kv_expand pass, so a template body
102+ * can reference $(BASESTEM) the same way a hand-written rule does */
103+char *
104+subst_tmpl(const char *s, struct StrList *params, char **vals)
105+{
106+  size_t      cap = strlen(s) * 2 + 64, len = 0;
107+  char       *out = emalloc(cap);
108+  const char *p   = s;
109+
110+  while (*p) {
111+    if (p[0] == '$' && p[1] == '(') {
112+      const char *inner = p + 2;
113+      const char *close = find_dollar_close(inner);
114+
115+      if (close) {
116+        char   name[256];
117+        size_t nl = (size_t)(close - inner);
118+        size_t tokl;
119+        int    pi;
120+
121+        if (nl >= sizeof name)
122+          nl = sizeof name - 1;
123+        memcpy(name, inner, nl);
124+        name[nl] = '\0';
125+
126+        for (pi = 0; pi < params->n; pi++)
127+          if (strcmp(params->v[pi], name) == 0)
128+            break;
129+
130+        tokl = (pi < params->n) ? strlen(vals[pi]) : (size_t)(close - p) + 1;
131+        while (len + tokl + 1 >= cap) {
132+          cap *= 2;
133+          out = erealloc(out, cap);
134+        }
135+        memcpy(out + len, pi < params->n ? vals[pi] : p, tokl);
136+        len += tokl;
137+        p = close + 1;
138+        continue;
139+      }
140+    }
141+    if (len + 2 >= cap) {
142+      cap *= 2;
143+      out = erealloc(out, cap);
144+    }
145+    out[len++] = *p++;
146+  }
147+  out[len] = '\0';
148+  return out;
149+}
150+
151+void
152+snode_subst(struct SNode *n, struct StrList *params, char **vals)
153+{
154+  int i;
155+  if (!n)
156+    return;
157+  if (n->kind == S_ATOM) {
158+    char *out = subst_tmpl(n->atom, params, vals);
159+    free(n->atom);
160+    n->atom = out;
161+    return;
162+  }
163+  for (i = 0; i < n->nkids; i++)
164+    snode_subst(n->kids[i], params, vals);
165+}
166+
167+/* swallow a balanced $(...) inside a bare atom */
168+void
169+lex_swallow_dollar(struct SLex *lx, char *buf, size_t *blen, size_t bsz)
170+{
171+  int depth = 1;
172+
173+  if (*blen + 2 < bsz) {
174+    buf[(*blen)++] = '$';
175+    buf[(*blen)++] = '(';
176+  }
177+  lx->pos += 2;
178+  while (depth > 0) {
179+    int e = lx->src[lx->pos];
180+    if (e == '\0')
181+      eprintf("manifest: unterminated $(...) on line %d\n", lx->line);
182+    if (e == '\n')
183+      lx->line++;
184+    if (e == '$' && lx->src[lx->pos + 1] == '(') {
185+      depth++;
186+      if (*blen + 2 < bsz) {
187+        buf[(*blen)++] = e;
188+        buf[(*blen)++] = lx->src[lx->pos + 1];
189+      }
190+      lx->pos += 2;
191+      continue;
192+    }
193+    if (*blen < bsz - 1)
194+      buf[(*blen)++] = e;
195+    if (e == ')')
196+      depth--;
197+    lx->pos++;
198+  }
199+}
200+
201+/* \" \\ \n \t \r \$ are recognized. anything else keeps the backslash
202+ * so shell escapes like \( survive */
203+void
204+lex_string(struct SLex *lx, char *buf, size_t bsz)
205+{
206+  int    line = lx->line;
207+  size_t blen = 0;
208+
209+  lx->pos++;
210+  while (lx->src[lx->pos] && lx->src[lx->pos] != '"') {
211+    int e = lx->src[lx->pos];
212+
213+    if (e == '\\' && lx->src[lx->pos + 1]) {
214+      int esc = lx->src[lx->pos + 1];
215+      switch (esc) {
216+        case 'n':
217+          e = '\n';
218+          break;
219+        case 't':
220+          e = '\t';
221+          break;
222+        case 'r':
223+          e = '\r';
224+          break;
225+        case '"':
226+        case '\\':
227+        case '$':
228+          e = esc;
229+          break;
230+        default:
231+          if (blen < bsz - 1)
232+            buf[blen++] = '\\';
233+          e = esc;
234+          break;
235+      }
236+      lx->pos += 2;
237+    } else {
238+      lx->pos++;
239+    }
240+    if (e == '\n')
241+      lx->line++;
242+    if (blen < bsz - 1)
243+      buf[blen++] = (char)e;
244+  }
245+  if (lx->src[lx->pos] != '"')
246+    eprintf("manifest: unterminated string on line %d\n", line);
247+  lx->pos++;
248+  buf[blen] = '\0';
249+}
250+
251+struct SNode *
252+snode_parse(struct SLex *lx)
253+{
254+  char           buf[8192];
255+  size_t         blen;
256+  int            line;
257+  struct SNode  *root  = snode_list(lx->line);
258+  struct SNode **stack = emalloc(16 * sizeof *stack);
259+  int            sp = 0, scap = 16;
260+
261+  stack[sp++] = root;
262+
263+  for (;;) {
264+    int c = lx->src[lx->pos];
265+
266+    if (c == '\0')
267+      break;
268+    if (c == '\n') {
269+      lx->line++;
270+      lx->pos++;
271+      continue;
272+    }
273+    if (isspace((unsigned char)c)) {
274+      lx->pos++;
275+      continue;
276+    }
277+    if (c == ';') {
278+      while (lx->src[lx->pos] && lx->src[lx->pos] != '\n')
279+        lx->pos++;
280+      continue;
281+    }
282+    if (c == '(') {
283+      struct SNode *lst = snode_list(lx->line);
284+      snode_push(stack[sp - 1], lst);
285+      if (sp >= scap) {
286+        scap *= 2;
287+        stack = erealloc(stack, (size_t)scap * sizeof *stack);
288+      }
289+      stack[sp++] = lst;
290+      lx->pos++;
291+      continue;
292+    }
293+    if (c == ')') {
294+      if (sp <= 1)
295+        eprintf("manifest: stray ')' on line %d\n", lx->line);
296+      sp--;
297+      lx->pos++;
298+      continue;
299+    }
300+    if (c == '"') {
301+      line = lx->line;
302+      lex_string(lx, buf, sizeof buf);
303+      snode_push(stack[sp - 1], snode_atom(buf, line));
304+      continue;
305+    }
306+
307+    /* bare atom */
308+    line = lx->line;
309+    blen = 0;
310+    while (lx->src[lx->pos]) {
311+      int d = lx->src[lx->pos];
312+      if (d == '(' || d == ')' || d == '"' || d == ';' || isspace((unsigned char)d))
313+        break;
314+      if (d == '$' && lx->src[lx->pos + 1] == '(') {
315+        lex_swallow_dollar(lx, buf, &blen, sizeof buf);
316+        continue;
317+      }
318+      if (blen < sizeof buf - 1)
319+        buf[blen++] = d;
320+      lx->pos++;
321+    }
322+    buf[blen] = '\0';
323+    snode_push(stack[sp - 1], snode_atom(buf, line));
324+  }
325+
326+  if (sp != 1)
327+    eprintf("manifest: %d unclosed '(' at end of file\n", sp - 1);
328+
329+  free(stack);
330+  return root;
331+}
332+
333+int
334+s_is_list(struct SNode *n)
335+{
336+  return n && n->kind == S_LIST;
337+}
338+
339+const char *
340+s_head(struct SNode *n)
341+{
342+  if (!s_is_list(n) || n->nkids == 0 || n->kids[0]->kind != S_ATOM)
343+    return NULL;
344+  return n->kids[0]->atom;
345+}
+103, -0
  1@@ -0,0 +1,103 @@
  2+#include "ninqu.h"
  3+
  4+#include <ctype.h>
  5+#include <errno.h>
  6+#include <stdio.h>
  7+#include <string.h>
  8+#include <unistd.h>
  9+
 10+void
 11+sl_push(struct StrList *sl, const char *s)
 12+{
 13+  GROW(sl->v, sl->n, sl->cap, 8);
 14+  sl->v[sl->n++] = estrdup(s);
 15+}
 16+
 17+int
 18+sl_has(struct StrList *sl, const char *s)
 19+{
 20+  int i;
 21+  for (i = 0; i < sl->n; i++)
 22+    if (strcmp(sl->v[i], s) == 0)
 23+      return 1;
 24+  return 0;
 25+}
 26+
 27+void
 28+sl_split(struct StrList *sl, const char *s)
 29+{
 30+  const char *p = s;
 31+
 32+  sl->n = 0;
 33+  while (*p) {
 34+    const char *start;
 35+    char        buf[4096];
 36+    size_t      len;
 37+
 38+    while (*p && isspace((unsigned char)*p))
 39+      p++;
 40+    if (!*p)
 41+      break;
 42+    start = p;
 43+    while (*p && !isspace((unsigned char)*p))
 44+      p++;
 45+    len = (size_t)(p - start);
 46+    if (len >= sizeof buf)
 47+      len = sizeof buf - 1;
 48+    memcpy(buf, start, len);
 49+    buf[len] = '\0';
 50+    sl_push(sl, buf);
 51+  }
 52+}
 53+
 54+char *
 55+sl_join(struct StrList *sl)
 56+{
 57+  size_t cap = 64, len = 0;
 58+  char  *out = emalloc(cap);
 59+  int    i;
 60+
 61+  out[0] = '\0';
 62+  for (i = 0; i < sl->n; i++) {
 63+    size_t addlen = strlen(sl->v[i]) + 1;
 64+    while (len + addlen + 1 > cap) {
 65+      cap *= 2;
 66+      out = erealloc(out, cap);
 67+    }
 68+    if (i > 0)
 69+      out[len++] = ' ';
 70+    memcpy(out + len, sl->v[i], strlen(sl->v[i]));
 71+    len += strlen(sl->v[i]);
 72+    out[len] = '\0';
 73+  }
 74+  return out;
 75+}
 76+
 77+const char *
 78+base_of(const char *path)
 79+{
 80+  const char *p = strrchr(path, '/');
 81+  return p ? p + 1 : path;
 82+}
 83+
 84+/* null-terminate the array so execvp stops scanning */
 85+char **
 86+sl_argv(struct StrList *sl)
 87+{
 88+  char **av = emalloc((size_t)(sl->n + 1) * sizeof *av);
 89+  int    i;
 90+  for (i = 0; i < sl->n; i++)
 91+    av[i] = sl->v[i];
 92+  av[sl->n] = NULL;
 93+  return av;
 94+}
 95+
 96+/* noreturn so callers do not need to guard the dead path after the
 97+ * fork child calls this */
 98+void
 99+child_execvp(char **av)
100+{
101+  execvp(av[0], av);
102+  fprintf(stderr, "%s: exec %s: %s\n", argv0, av[0], strerror(errno));
103+  _exit(127);
104+}
+678, -0
  1@@ -0,0 +1,678 @@
  2+; genconfig.sh config produces four files from build.cfg:
  3+; config.set (read by ninqu), config.kv (sourced by genbox.sh),
  4+; nofork_list.h (included by wexec.c), and features.h (-include'd
  5+; into every compile). each gets its own rule so ninqu can find a
  6+; producer for any of them
  7+;
  8+(rule CONFIG_SET
  9+  (out "scripts/mk/config.set")
 10+  (dep "build.cfg" "scripts/genconfig.sh")
 11+  (exec sh scripts/genconfig.sh config))
 12+
 13+(rule NOFORK_LIST_H
 14+  (out "shared/libutil/nofork_list.h")
 15+  (dep "build.cfg" "scripts/genconfig.sh")
 16+  (exec sh scripts/genconfig.sh config))
 17+
 18+(rule FEATURES_H
 19+  (out "scripts/mk/features.h")
 20+  (dep "build.cfg" "scripts/genconfig.sh")
 21+  (exec sh scripts/genconfig.sh config))
 22+
 23+(set-file "scripts/mk/config.set")
 24+
 25+; static libraries: compile every .c in each lib dir then AR
 26+;
 27+(rule LIBUTF_OBJ
 28+  (glob "shared/libutf/*.c")
 29+  (dep $(MATCH))
 30+  (out "$(STEM).o")
 31+  (exec $(CC) $(CPPFLAGS) -Ishared/libutf $(CFLAGS) -c -o $(OUT) $(MATCH)))
 32+
 33+(rule LIBUTF
 34+  (dep LIBUTF_OBJ)
 35+  (out "shared/libutf/libutf.a")
 36+  (description "utf-8 and rune handling, linked into every aruu tool")
 37+  (feature core-lib)
 38+  (exec $(AR) $(ARFLAGS) $(OUT) $(IN)))
 39+
 40+(rule LIBUTIL_OBJ
 41+  (glob "shared/libutil/*.c")
 42+  (dep $(MATCH))
 43+  (after NOFORK_LIST_H FEATURES_H)
 44+  (out "$(STEM).o")
 45+  (exec $(CC) $(CPPFLAGS_LIBUTIL) -Ishared/libutil $(CFLAGS) -c -o $(OUT) $(MATCH)))
 46+
 47+(rule LIBUTIL
 48+  (dep LIBUTIL_OBJ)
 49+  (out "shared/libutil/libutil.a")
 50+  (description "allocation, error reporting, and string helpers shared by every tool")
 51+  (feature core-lib)
 52+  (exec $(AR) $(ARFLAGS) $(OUT) $(IN)))
 53+
 54+(rule LIBREDLINE_OBJ
 55+  (glob "shared/libredline/*.c")
 56+  (dep $(MATCH))
 57+  (out "$(STEM).o")
 58+  (exec $(CC) $(CPPFLAGS) -Ishared/libredline $(CFLAGS) -c -o $(OUT) $(MATCH)))
 59+
 60+(rule LIBREDLINE
 61+  (dep LIBREDLINE_OBJ)
 62+  (out "shared/libredline/libredline.a")
 63+  (description "line editing used by sh and any other interactive tool")
 64+  (feature core-lib)
 65+  (exec $(AR) $(ARFLAGS) $(OUT) $(IN)))
 66+
 67+(group lib LIBUTF LIBUTIL LIBREDLINE)
 68+
 69+; perfile: every *.c directly inside one of these dirs becomes its
 70+; own binary of the same name, individually gated by
 71+; build_<category>_<ident> (lowercase, hyphens and dots turned to
 72+; underscores, see the $(IDENT) comment above BOX_OBJ below for why)
 73+; (skip) excludes files that belong to a multi-file tool below instead
 74+;
 75+(map EXTRA_LIBS
 76+  (diff "-lm")
 77+  (wget $(LDLIBS_TLS)))
 78+
 79+; Every .c file in a category builds into its own binary. PERFILE captures
 80+; that shared rule. Only EXTRA_AFTER differs cuz POSIX waits for GETCONF_H
 81+; the rest pass "". cmd/posix/bc.c overrides this with its own yacc-based
 82+; rule, so no need for PERFILE to have that as a special case
 83+;
 84+(template PERFILE (NAME CATEGORY DESC EXTRA_AFTER)
 85+  (rule $(NAME)
 86+    (glob "cmd/$(CATEGORY)/*.c")
 87+    (gate build_$(CATEGORY)_$(IDENT))
 88+    (dep $(MATCH))
 89+    (after LIBUTIL LIBUTF $(EXTRA_AFTER))
 90+    (out $(STEM))
 91+    (description $(DESC))
 92+    (feature perfile)
 93+    (exec $(CC) $(CPPFLAGS) $(CFLAGS) -o $(OUT) $(MATCH) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS) $(EXTRA_LIBS:$(BASESTEM)))))
 94+
 95+(use PERFILE POSIX_BIN posix "one POSIX2024 utility per source file under cmd/posix" GETCONF_H)
 96+(use PERFILE LINUX_BIN linux "Linux-only utility, one source file per binary under cmd/linux" "")
 97+(use PERFILE NET_BIN net "networking utility, one source file per binary under cmd/net" "")
 98+(use PERFILE XSI_BIN xsi "XSI-specified utility, one source file per binary under cmd/xsi" "")
 99+(use PERFILE PSEUDO_BIN pseudo "common but undefined utility, one source file per binary under cmd/pseudo" "")
100+(use PERFILE EXTRA_BIN extra "extra utility outside POSIX/XSI scope, one source file per binary under cmd/extra" "")
101+
102+(group posix lib POSIX_BIN POSIX_BC POSIX_AWK POSIX_SH POSIX_MAKE)
103+(group linux lib LINUX_BIN)
104+(group net lib NET_BIN)
105+(group xsi lib XSI_BIN)
106+(group pseudo lib PSEUDO_BIN)
107+(group extra lib EXTRA_BIN EXTRA_YAP)
108+
109+; codegen: generate sources consumed by the build rules below. Ungated
110+; generators always run; conditional ones use (req) so they only run
111+; when their inputs exist in this checkout
112+;
113+(rule GETCONF_H
114+  (gate build_posix_getconf)
115+  (out "cmd/posix/getconf.h")
116+  (dep "scripts/getconf.sh")
117+  (exec sh scripts/genconfig.sh getconf))
118+
119+(rule POSIX_BC_C
120+  (gate build_posix_bc)
121+  (req "cmd/posix/bc.y")
122+  (out "cmd/posix/bc.c")
123+  (dep "cmd/posix/bc.y")
124+  (exec sh scripts/genconfig.sh bc))
125+
126+(rule POSIX_AWK_C
127+  (gate build_posix_awk)
128+  (out "cmd/posix/awk/awkgram.tab.c")
129+  (dep "cmd/posix/awk/awkgram.y")
130+  (exec sh scripts/genconfig.sh awk))
131+
132+(rule POSIX_SH_C
133+  (gate build_posix_sh)
134+  (out "cmd/posix/sh/syntax.c")
135+  (dep "cmd/posix/sh/mksyntax.c" "cmd/posix/sh/mknodes.c" "cmd/posix/sh/mkbuiltins" "cmd/posix/sh/mktokens")
136+  (exec sh scripts/genconfig.sh sh))
137+
138+(rule DEVCONF
139+  (req "cmd/dev/configure")
140+  (out "cmd/dev/config.h")
141+  (dep "cmd/dev/configure")
142+  (phony)
143+  (exec sh cmd/dev/configure))
144+
145+(group codegen GETCONF_H POSIX_BC_C POSIX_AWK_C POSIX_SH_C DEVCONF)
146+
147+; multi: one binary from all .c files in one or more dirs (minus skip).
148+; codegen must run first so generated sources are present before these
149+; globs collect them
150+;
151+(rule POSIX_BC_OBJ
152+  (glob "cmd/posix/bc.c")
153+  (gate build_posix_bc)
154+  (dep $(MATCH))
155+  (after POSIX_BC_C)
156+  (out "$(STEM).o")
157+  (exec $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH)))
158+
159+(rule POSIX_BC
160+  (gate build_posix_bc)
161+  (member posix bc)
162+  (dep POSIX_BC_OBJ)
163+  (after LIBUTIL LIBUTF)
164+  (out "cmd/posix/bc")
165+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS) -lm))
166+
167+(rule POSIX_AWK_OBJ
168+  (glob "cmd/posix/awk/*.c")
169+  (gate build_posix_awk)
170+  (skip "maketab.c")
171+  (dep $(MATCH))
172+  (after POSIX_AWK_C)
173+  (out "$(STEM).o")
174+  (exec $(CC) $(CPPFLAGS) -Icmd/posix/awk $(CFLAGS) -c -o $(OUT) $(MATCH)))
175+
176+(rule POSIX_AWK
177+  (gate build_posix_awk)
178+  (member posix awk)
179+  (dep POSIX_AWK_OBJ)
180+  (after LIBUTIL LIBUTF)
181+  (out "cmd/posix/awk/awk")
182+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS) -lm))
183+
184+(rule POSIX_SH_OBJ
185+  (glob "cmd/posix/sh/*.c")
186+  (gate build_posix_sh)
187+  (skip "mknodes.c" "mksyntax.c")
188+  (dep $(MATCH))
189+  (after POSIX_SH_C)
190+  (out "$(STEM).o")
191+  (exec $(CC) $(CPPFLAGS) -DSHELL -Icmd/posix/sh $(CFLAGS) -c -o $(OUT) $(MATCH)))
192+
193+(rule POSIX_SH
194+  (gate build_posix_sh)
195+  (member posix sh)
196+  (dep POSIX_SH_OBJ)
197+  (after LIBUTIL LIBUTF LIBREDLINE)
198+  (out "cmd/posix/sh/sh")
199+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a shared/libredline/libredline.a $(LDFLAGS) $(LDLIBS)))
200+
201+(rule POSIX_MAKE_OBJ
202+  (glob "cmd/posix/make/*.c")
203+  (gate build_posix_make)
204+  (dep $(MATCH))
205+  (out "$(STEM).o")
206+  (exec $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH)))
207+
208+(rule POSIX_MAKE
209+  (gate build_posix_make)
210+  (member posix make)
211+  (dep POSIX_MAKE_OBJ)
212+  (after LIBUTIL LIBUTF)
213+  (out "cmd/posix/make/make")
214+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS)))
215+
216+(rule EXTRA_YAP_OBJ
217+  (glob "cmd/extra/yap/*.c")
218+  (gate build_extra_yap)
219+  (dep $(MATCH))
220+  (out "$(STEM).o")
221+  (exec $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH)))
222+
223+(rule EXTRA_YAP
224+  (gate build_extra_yap)
225+  (member extra yap)
226+  (dep EXTRA_YAP_OBJ)
227+  (after LIBUTIL LIBUTF)
228+  (out "cmd/extra/yap/yap")
229+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS) -ltinfo))
230+
231+; (member GROUP ALIAS) gives a rule a qualified user-facing name
232+; (e.g. posix/bc) while keeping its internal name (POSIX_BC) for deps,
233+; gates, and other references
234+;
235+
236+; dev: the aruu toolchain. ar/as/ld are standalone and cc
237+; shares one object pool linked into three frontends
238+;
239+(rule AR_OBJ
240+  (glob "cmd/dev/ar/*.c")
241+  (gate build_dev_ar)
242+  (skip "tinyar.c")
243+  (dep $(MATCH))
244+  (out "$(STEM).o")
245+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/ar $(CFLAGS) -c -o $(OUT) $(MATCH)))
246+
247+(rule AR
248+  (gate build_dev_ar)
249+  (dep AR_OBJ)
250+  (after LIBUTIL LIBUTF)
251+  (out "cmd/dev/ar/ar")
252+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS)))
253+
254+(rule XCUTIL_OBJ
255+  (glob "cmd/dev/xcutil/*.c")
256+  (gate build_dev_cc)
257+  (dep $(MATCH))
258+  (out "$(STEM).o")
259+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/xcutil $(CFLAGS) -c -o $(OUT) $(MATCH)))
260+
261+(rule AS_OBJ
262+  (glob "cmd/dev/as/*.c" "cmd/dev/as/arch/x64/*.c")
263+  (gate build_dev_cc)
264+  (dep $(MATCH))
265+  (out "$(STEM).o")
266+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/xcutil -Icmd/dev/as -Icmd/dev/as/arch/x64 -Ishared $(CFLAGS) -c -o $(OUT) $(MATCH)))
267+
268+(rule AS
269+  (gate build_dev_cc)
270+  (dep AS_OBJ)
271+  (after XCUTIL_OBJ LIBUTIL LIBUTF)
272+  (out "cmd/dev/as/as")
273+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS)))
274+
275+(rule LD_OBJ
276+  (glob "cmd/dev/ld/*.c")
277+  (gate build_dev_ld)
278+  (dep $(MATCH))
279+  (out "$(STEM).o")
280+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/xcutil -Icmd/dev/ld $(CFLAGS) -c -o $(OUT) $(MATCH)))
281+
282+(rule LD
283+  (gate build_dev_ld)
284+  (dep LD_OBJ)
285+  (after XCUTIL_OBJ LIBUTIL LIBUTF)
286+  (out "cmd/dev/ld/ld")
287+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS)))
288+
289+; CCPOOL_OBJ globs the whole directory. The entry points below use
290+; literal globs, so the override mechanism excludes them automatically
291+(rule CCPOOL_OBJ
292+  (glob "cmd/dev/cc/*.c")
293+  (gate build_dev_cc)
294+  (dep $(MATCH))
295+  (out "$(STEM).o")
296+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/cc $(CFLAGS) -c -o $(OUT) $(MATCH)))
297+
298+(rule CC1_OBJ
299+  (glob "cmd/dev/cc/cc1.c")
300+  (gate build_dev_cc)
301+  (dep $(MATCH))
302+  (out "$(STEM).o")
303+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/cc $(CFLAGS) -c -o $(OUT) $(MATCH)))
304+
305+(rule CC1
306+  (gate build_dev_cc)
307+  (dep CC1_OBJ CCPOOL_OBJ)
308+  (after LIBUTIL LIBUTF)
309+  (out "cmd/dev/cc/cc1")
310+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS)))
311+
312+(rule CPP_OBJ
313+  (glob "cmd/dev/cc/cpp.c")
314+  (gate build_dev_cc)
315+  (dep $(MATCH))
316+  (out "$(STEM).o")
317+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/cc $(CFLAGS) -c -o $(OUT) $(MATCH)))
318+
319+(rule CPP
320+  (gate build_dev_cc)
321+  (dep CPP_OBJ CCPOOL_OBJ)
322+  (after LIBUTIL LIBUTF)
323+  (out "cmd/dev/cc/cpp")
324+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS)))
325+
326+(rule CCDRV_OBJ
327+  (glob "cmd/dev/cc/driver.c" "cmd/dev/cc/util.c")
328+  (gate build_dev_cc)
329+  (dep $(MATCH))
330+  (out "$(STEM).o")
331+  (exec $(CC) $(CPPFLAGS) -Icmd/dev/cc $(CFLAGS) -c -o $(OUT) $(MATCH)))
332+
333+(rule CCDRV
334+  (gate build_dev_cc)
335+  (dep CCDRV_OBJ)
336+  (after LIBUTIL LIBUTF)
337+  (out "cmd/dev/cc/cc")
338+  (exec $(CC) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a $(LDFLAGS) $(LDLIBS)))
339+
340+; ninqu itself lives at cmd/dev/ninqu/ninqu and is built by the
341+; Makefile directly, since it has to exist before this file can be
342+; parsed. a rule here producing the same path would just race that
343+; bootstrap compile, so only the box variant below belongs to ninqu
344+;
345+; Like BOX_OBJ: rename main and hide other globals so ninqu can link
346+; into aruu-box. It is still not reentrant, so it stays out of
347+; NOFORK_LIST
348+;
349+(rule NINQU_BOX_OBJ
350+  (glob "cmd/dev/ninqu/*.c")
351+  (gate build_dev_ninqu)
352+  (dep $(MATCH))
353+  (after LIBUTIL LIBUTF LIBREDLINE NOFORK_LIST_H FEATURES_H GETCONF_H)
354+  (out ".box/cmd/dev/$(BASESTEM).o")
355+  (description "ninqu as a box builtin, main renamed for the box dispatch table")
356+  (shell "$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH) && sym=`echo $(BASESTEM) | tr '.-' '__'`_main && $(OBJCOPY) --redefine-sym main=$sym $(OUT) && $(OBJCOPY) --keep-global-symbol=$sym $(OUT)")
357+  (group box))
358+
359+(group dev lib DEVCONF AR AS LD CC1 CPP CCDRV)
360+
361+; man pages: build mdoc and text pages from ?man-marked sources.
362+;
363+(rule MKMAN
364+  (dir "scripts/mkman")
365+  (dep "glob:scripts/mkman/*.go")
366+  (out "scripts/mkman/mkman")
367+  (exec go build -o mkman .))
368+
369+(rule MAN_MDOC
370+  (glob "cmd/**/*.c")
371+  (mark "?man")
372+  (dep (after MKMAN))
373+  (out "man/man$(SEC_OF:$(DIR))/$(BASESTEM).$(SEC_OF:$(DIR))")
374+  (exec scripts/mkman/mkman -fmt mdoc -config scripts/mk/config.kv -src $(MATCH))
375+  (redirect))
376+
377+(rule MAN_TXT
378+  (glob "cmd/**/*.c")
379+  (mark "?man")
380+  (dep (after MKMAN))
381+  (out "man/man$(SEC_OF:$(DIR))/$(BASESTEM).$(SEC_OF:$(DIR)).txt")
382+  (exec scripts/mkman/mkman -fmt txt -config scripts/mk/config.kv -src $(MATCH))
383+  (redirect))
384+
385+(map SEC_OF
386+  (cmd/posix 1)
387+  (cmd/linux 8)
388+  (cmd/net 8)
389+  (cmd/xsi 1)
390+  (cmd/pseudo 1)
391+  (cmd/extra 1)
392+  (cmd/dev 1))
393+
394+(group man MAN_MDOC MAN_TXT)
395+
396+(rule CLEAN
397+  (phony)
398+  (shell "
399+    printf '  CLEAN object files\n'
400+    find shared cmd -name '*.o' -exec rm -f {} +
401+
402+    printf '  CLEAN static libraries\n'
403+    find shared -name '*.a' -exec rm -f {} +
404+
405+    printf '  CLEAN compiled binaries\n'
406+    find cmd -type f ! -name '*.*' -perm -100 -exec rm -f {} +
407+
408+    sh scripts/genconfig.sh clean
409+    rm -f scripts/mk/config.mk scripts/mk/config.kv scripts/mk/rules.mk shared/libutil/nofork_list.h
410+
411+    printf '  CLEAN build tools\n'
412+    rm -f scripts/mkman/mkman
413+
414+    printf '  CLEAN dev artifacts\n'
415+    rm -f cmd/dev/cc/cc1 cmd/dev/cc/cpp cmd/dev/as/as cmd/dev/ld/ld cmd/dev/ar/ar
416+    rm -f shared/libaruuelf.so
417+    rm -f cmd/dev/config.h cmd/dev/cc/config.h cmd/dev/version.h
418+
419+    printf '  CLEAN misc\n'
420+    rm -rf man/man1 man/man8
421+    rm -rf aruu-box .box
422+
423+    printf '  CLEAN done\n'
424+  "))
425+
426+(group clean CLEAN)
427+
428+; install every built executable and generated man page
429+(rule INSTALL
430+  (phony)
431+  (after ALL_BINS MAN_MDOC MAN_TXT)
432+  (shell "
433+    mkdir -p \"$(PREFIX)/bin\" \"$(MANPREFIX)/man1\" \"$(MANPREFIX)/man8\"
434+
435+    find cmd -type f ! -name '*.*' -perm -100 | while read -r bin; do
436+      name=${bin##*/}
437+      printf '  INSTALL %s/bin/%s\n' '$(PREFIX)' \"$name\"
438+      cp \"$bin\" \"$(PREFIX)/bin/$name\"
439+      chmod 755 \"$(PREFIX)/bin/$name\"
440+    done
441+
442+    find man/man1 man/man8 -type f -name '*.[18]' 2>/dev/null | while read -r f; do
443+      sec=${f%/*}; sec=${sec##*/}
444+      name=${f##*/}
445+      cp \"$f\" \"$(MANPREFIX)/$sec/$name\"
446+    done
447+  "))
448+
449+(group install INSTALL)
450+
451+; remove exactly what install would have copied
452+(rule REMOVE
453+  (phony)
454+  (shell "
455+    find cmd -type f ! -name '*.*' -perm -100 | while read -r bin; do
456+      name=${bin##*/}
457+      printf '  REMOVE %s/bin/%s\n' '$(PREFIX)' \"$name\"
458+      rm -f \"$(PREFIX)/bin/$name\"
459+    done
460+
461+    find man/man1 man/man8 -type f -name '*.[18]' 2>/dev/null | while read -r f; do
462+      sec=${f%/*}; sec=${sec##*/}
463+      name=${f##*/}
464+      printf '  REMOVE %s/%s/%s\n' '$(MANPREFIX)' \"$sec\" \"$name\"
465+      rm -f \"$(MANPREFIX)/$sec/$name\"
466+    done
467+  "))
468+
469+(group remove REMOVE)
470+(group uninstall REMOVE)
471+
472+; box: the multi-call binary containing every enabled tool in a single executable
473+; with a dispatch table. This includes multi-file tools like POSIX_BC, POSIX_AWK,
474+; POSIX_SH, POSIX_MAKE, and EXTRA_YAP. A single-file tool has its main function
475+; renamed directly by BOX_OBJ. The multi-file tools use a partial link (ld -r)
476+; in their respective rules to combine their objects before renaming main,
477+; preventing helper symbols from colliding. This replaces scripts/mkbox
478+;
479+; Object files are stored under .box/ to mirror the cmd/ layout and prevent
480+; collisions with standalone objects. Since standalone and box objects use
481+; different symbol names, they cannot share a path
482+;
483+; Ninqu determines which tools enter the box using the gate on BOX_OBJ.
484+; BOX_DISPATCH reads the object paths passed on argv rather than consulting
485+; build.cfg or config.set directly
486+;
487+
488+; CATEGORY_OF maps a directory path to its category name, matching the toggles
489+; in build.cfg. Since BOX_OBJ globs multiple directories, its gate evaluates the
490+; category dynamically based on the match directory
491+;
492+; Gates evaluate $(IDENT) instead of $(BASESTEM) because toggle names must be
493+; valid shell variables. Genconfig.sh replaces hyphens and periods with
494+; underscores, and $(IDENT) applies the same normalization so they match
495+;
496+(map CATEGORY_OF
497+  (cmd/posix  posix)
498+  (cmd/linux  linux)
499+  (cmd/net    net)
500+  (cmd/pseudo pseudo)
501+  (cmd/xsi    xsi)
502+  (cmd/extra  extra))
503+
504+; Tool basestems match their C symbols using a shell transform (tr '.-' '__'
505+; and appending _main) in both the objcopy step and in genbox.sh. A static map
506+; is unnecessary because no current tools require custom overrides, and
507+; genbox.sh only receives paths via argv
508+;
509+
510+; The match directory determines the gate and output path, allowing one rule
511+; to handle six categories. The multi-file tool bc.c inside cmd/posix is
512+; automatically excluded via the literal-glob override mechanism
513+;
514+(rule BOX_OBJ
515+  (glob "cmd/posix/*.c" "cmd/linux/*.c" "cmd/net/*.c" "cmd/pseudo/*.c"
516+        "cmd/xsi/*.c" "cmd/extra/*.c")
517+  (gate build_$(CATEGORY_OF:$(DIR))_$(IDENT))
518+  (dep $(MATCH))
519+  (after LIBUTIL LIBUTF LIBREDLINE NOFORK_LIST_H FEATURES_H GETCONF_H)
520+  (out ".box/$(DIR)/$(BASESTEM).o")
521+  (description "one object per enabled tool, main renamed for the box dispatch table")
522+  (shell "$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH) && sym=`echo $(BASESTEM) | tr '.-' '__'`_main && $(OBJCOPY) --redefine-sym main=$sym $(OUT) && $(OBJCOPY) --keep-global-symbol=$sym $(OUT)")
523+  (group box))
524+
525+; Multi-file tools like POSIX_BC, POSIX_AWK, POSIX_SH, POSIX_MAKE, and
526+; EXTRA_YAP cannot use the single-file objcopy step because their global helper
527+; symbols would collide when linked into a single binary. Combining these
528+; objects first with a partial link (ld -r) resolves internal references, making
529+; it safe to isolate the renamed main function and hide remaining symbols
530+;
531+; Combined objects use the layout .box/cmd/<category>/<tool>.o so genbox.sh
532+; can parse them without structural modifications
533+;
534+(rule POSIX_BC_BOX_OBJ
535+  (glob "cmd/posix/bc.c")
536+  (gate build_posix_bc)
537+  (dep $(MATCH))
538+  (after POSIX_BC_C)
539+  (out ".box/parts/$(STEM).o")
540+  (exec $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH)))
541+
542+(rule POSIX_BC_BOX
543+  (gate build_posix_bc)
544+  (dep POSIX_BC_BOX_OBJ)
545+  (out ".box/cmd/posix/bc.o")
546+  (description "bc's objects combined into one relocatable object, main renamed for the box dispatch table")
547+  (shell "$(LD) -r -o $(OUT) $(IN) && $(OBJCOPY) --redefine-sym main=bc_main $(OUT) && $(OBJCOPY) --keep-global-symbol=bc_main $(OUT)")
548+  (group box))
549+
550+(rule POSIX_AWK_BOX_OBJ
551+  (glob "cmd/posix/awk/*.c")
552+  (gate build_posix_awk)
553+  (skip "maketab.c")
554+  (dep $(MATCH))
555+  (after POSIX_AWK_C)
556+  (out ".box/parts/$(STEM).o")
557+  (exec $(CC) $(CPPFLAGS) -Icmd/posix/awk $(CFLAGS) -c -o $(OUT) $(MATCH)))
558+
559+(rule POSIX_AWK_BOX
560+  (gate build_posix_awk)
561+  (dep POSIX_AWK_BOX_OBJ)
562+  (out ".box/cmd/posix/awk.o")
563+  (description "awk's objects combined into one relocatable object, main renamed for the box dispatch table")
564+  (shell "$(LD) -r -o $(OUT) $(IN) && $(OBJCOPY) --redefine-sym main=awk_main $(OUT) && $(OBJCOPY) --keep-global-symbol=awk_main $(OUT)")
565+  (group box))
566+
567+(rule POSIX_SH_BOX_OBJ
568+  (glob "cmd/posix/sh/*.c")
569+  (gate build_posix_sh)
570+  (skip "mknodes.c" "mksyntax.c")
571+  (dep $(MATCH))
572+  (after POSIX_SH_C)
573+  (out ".box/parts/$(STEM).o")
574+  (exec $(CC) $(CPPFLAGS) -DSHELL -Icmd/posix/sh $(CFLAGS) -c -o $(OUT) $(MATCH)))
575+
576+(rule POSIX_SH_BOX
577+  (gate build_posix_sh)
578+  (dep POSIX_SH_BOX_OBJ)
579+  (out ".box/cmd/posix/sh.o")
580+  (description "sh's objects combined into one relocatable object, main renamed for the box dispatch table")
581+  (shell "$(LD) -r -o $(OUT) $(IN) && $(OBJCOPY) --redefine-sym main=sh_main $(OUT) && $(OBJCOPY) --keep-global-symbol=sh_main $(OUT)")
582+  (group box))
583+
584+(rule POSIX_MAKE_BOX_OBJ
585+  (glob "cmd/posix/make/*.c")
586+  (gate build_posix_make)
587+  (dep $(MATCH))
588+  (out ".box/parts/$(STEM).o")
589+  (exec $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH)))
590+
591+(rule POSIX_MAKE_BOX
592+  (gate build_posix_make)
593+  (dep POSIX_MAKE_BOX_OBJ)
594+  (out ".box/cmd/posix/make.o")
595+  (description "make's objects combined into one relocatable object, main renamed for the box dispatch table")
596+  (shell "$(LD) -r -o $(OUT) $(IN) && $(OBJCOPY) --redefine-sym main=make_main $(OUT) && $(OBJCOPY) --keep-global-symbol=make_main $(OUT)")
597+  (group box))
598+
599+(rule EXTRA_YAP_BOX_OBJ
600+  (glob "cmd/extra/yap/*.c")
601+  (gate build_extra_yap)
602+  (dep $(MATCH))
603+  (out ".box/parts/$(STEM).o")
604+  (exec $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $(OUT) $(MATCH)))
605+
606+(rule EXTRA_YAP_BOX
607+  (gate build_extra_yap)
608+  (dep EXTRA_YAP_BOX_OBJ)
609+  (out ".box/cmd/extra/yap.o")
610+  (description "yap's objects combined into one relocatable object, main renamed for the box dispatch table")
611+  (shell "$(LD) -r -o $(OUT) $(IN) && $(OBJCOPY) --redefine-sym main=yap_main $(OUT) && $(OBJCOPY) --keep-global-symbol=yap_main $(OUT)")
612+  (group box))
613+
614+; Genbox.sh relies on arguments passed by ninqu, where each argument matches
615+; an output from BOX_OBJ or a custom box rule. The script derives categories
616+; and tool names directly from these paths and outputs the dispatch code
617+;
618+(rule BOX_DISPATCH
619+  (dep BOX_OBJ POSIX_BC_BOX POSIX_AWK_BOX POSIX_SH_BOX POSIX_MAKE_BOX EXTRA_YAP_BOX NINQU_BOX_OBJ)
620+  (out ".box/aruu-box.c")
621+  (description "dispatch table and main() for aruu-box, generated from the enabled tool list")
622+  (exec scripts/genbox.sh $(IN))
623+  (redirect)
624+  (group box))
625+
626+; Libm is linked unconditionally because bc requires it, and unused library
627+; references are safe. Modern toolchains omit unreferenced archive components.
628+; Yap requires terminfo, which is an optional system dependency. Linking it
629+; unconditionally breaks builds on systems without ncurses-dev when yap is
630+; disabled. Genconfig.sh exports BOX_EXTRA_LIBS as "-ltinfo" or an empty string
631+; based on whether yap is enabled
632+;
633+(rule BOX
634+  (dep BOX_DISPATCH BOX_OBJ POSIX_BC_BOX POSIX_AWK_BOX POSIX_SH_BOX POSIX_MAKE_BOX EXTRA_YAP_BOX NINQU_BOX_OBJ)
635+  (after LIBUTIL LIBUTF LIBREDLINE)
636+  (out "aruu-box")
637+  (description "single multi-call binary, one dispatch table for every enabled tool")
638+;  (shell "$(CC) $(CPPFLAGS) $(CFLAGS) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a shared/libredline/libredline.a $(LDFLAGS) $(LDLIBS) -lm $(BOX_EXTRA_LIBS)")
639+  (shell "$(CC) $(CPPFLAGS) $(CFLAGS) -o $(OUT) $(IN) shared/libutil/libutil.a shared/libutf/libutf.a shared/libredline/libredline.a $(LDFLAGS) $(LDLIBS) $(BOX_EXTRA_LIBS)")
640+  (group box))
641+
642+(group box BOX)
643+
644+; -------------------------------------------------------------------
645+; top level. (group all) contains every build target except clean
646+; and box. box is separate because it produces a different binary
647+; with different .o files, and mixing it with standalone builds
648+; would cause confusion. clean is never in all because it removes
649+; what all builds
650+; -------------------------------------------------------------------
651+
652+(rule ALL_BINS
653+  (phony)
654+  (after LIBUTIL LIBUTF LIBREDLINE
655+         POSIX_BIN LINUX_BIN NET_BIN XSI_BIN
656+         PSEUDO_BIN EXTRA_BIN
657+         POSIX_BC POSIX_AWK POSIX_SH POSIX_MAKE EXTRA_YAP
658+         AR AS LD CC1 CPP CCDRV)
659+  (exec true))
660+
661+(group all ALL_BINS)
662+
663+(group default all)
664+
665+; -------------------------------------------------------------------
666+; meta-targets: apply an override, then build a normal target under
667+; it. invoke as `ninqu META.TARGET` (`ninqu debug.posix`) or bare
668+; `ninqu META` to apply the override and build (group default). the
669+; $(CFLAGS) inside each value is expanded against whatever CFLAGS
670+; already holds (the config.set default, or a -DCFLAGS=... override)
671+; at the moment the meta is applied, so these only append, they
672+; never have to repeat -std=c99 -Wall -Wextra -pedantic themselves
673+; -------------------------------------------------------------------
674+
675+(meta debug
676+  (set CFLAGS "$(CFLAGS) -g -O0"))
677+
678+(meta release
679+  (set CFLAGS "$(CFLAGS) -O2 -DNDEBUG"))
+0, -98
 1@@ -1,98 +0,0 @@
 2-#!/bin/sh
 3-# https://srcdump.net/shrub/shrubtools/tree/main/item/deps.html
 4-#
 5-# emit makefile dependencies for a list of C sources using the compiler's .d output. having a bunch of
 6-# autogenerated .d files and -include's is flaky and ugly, and might not work on all make
 7-# implementations. this way you can generate the deps once and ship the makefile.
 8-# it takes a list of files from stdin and prints to stdout. you might also want to append
 9-# you CFLAGS/CPPFLAGS if that will change the deps.
10-#
11-# example:
12-#     find . -type f -name '*.c' | CPPFLAGS='-Iinclude' deps >> Makefile
13-#
14-set -eu
15-
16-cc=${CC:-cc}
17-cppflags=${CPPFLAGS:-}
18-cflags=${CFLAGS:-}
19-tmpbase=${TMPDIR:-/tmp}
20-
21-cleanup() {
22-	rm -rf "$tmpdir"
23-}
24-
25-usage() {
26-	printf 'usage: %s [file.c ...]\n' "${0##*/}" >&2
27-	printf '       %s < file-list\n' "${0##*/}" >&2
28-	exit 2
29-}
30-
31-list() {
32-	if [ "$#" -gt 0 ]; then
33-		for src do
34-			case $src in
35-			*.c) printf '%s\n' "$src" ;;
36-			*) printf '%s: not a .c file: %s\n' "${0##*/}" "$src" >&2; exit 2 ;;
37-			esac
38-		done
39-		return
40-	fi
41-
42-	while IFS= read -r src; do
43-		[ -n "$src" ] || continue
44-		case $src in
45-		*.c) printf '%s\n' "$src" ;;
46-		*) printf '%s: not a .c file: %s\n' "${0##*/}" "$src" >&2; exit 2 ;;
47-		esac
48-	done
49-}
50-
51-if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
52-	usage
53-fi
54-
55-tmpdir=$(mktemp -d "$tmpbase/deps.XXXXXX")
56-trap cleanup EXIT HUP INT TERM
57-
58-count=0
59-srcs=$tmpdir/sources
60-list "$@" > "$srcs"
61-
62-if [ ! -s "$srcs" ]; then
63-	printf '%s: no .c files provided\n' "${0##*/}" >&2
64-	exit 1
65-fi
66-
67-printf '\n'
68-printf '#AUTOGENERATED\n'
69-
70-while IFS= read -r src; do
71-	dep=$tmpdir/$count.d
72-	obj=${src%.c}.o
73-	"$cc" $cppflags $cflags -MMD -MF "$dep" -MT "$obj" -E -o /dev/null "$src"
74-	#flatten the block with one target: dep line per prereq
75-	awk '
76-	BEGIN {
77-		rule = ""
78-	}
79-	{
80-		line = $0
81-		sub(/\\$/, "", line)
82-		rule = rule line " "
83-		if ($0 !~ /\\$/) {
84-			sub(/^[[:space:]]*/, "", rule)
85-			split(rule, parts, ":")
86-			target = parts[1]
87-			deps = substr(rule, length(target) + 2)
88-			n = split(deps, items, /[[:space:]]+/)
89-			for (i = 1; i <= n; ++i) {
90-				if (items[i] == "")
91-					continue
92-				print target ": " items[i]
93-			}
94-			rule = ""
95-		}
96-	}
97-	' "$dep"
98-	count=$((count + 1))
99-done < "$srcs"
+186, -0
  1@@ -0,0 +1,186 @@
  2+#!/bin/sh
  3+# genbox.sh - generate the aruu-box dispatch table
  4+#
  5+# does one thing: given a list of object file paths on argv, each one
  6+# of BOX_OBJ's own outputs (".box/cmd/<category>/<tool>.o"), emit the C
  7+# source for aruu-box's dispatch table and main() on stdout
  8+#
  9+set -e
 10+
 11+[ $# -gt 0 ] || {
 12+        printf "genbox: no object paths given\n" >&2
 13+        exit 1
 14+}
 15+
 16+# derive category, tool basestem, and C symbol from one BOX_OBJ output
 17+# path. the symbol transform (tr '.-' '__', append _main) must match
 18+# BOX_OBJ's own objcopy step exactly, see the CATEGORY_OF/symbol
 19+# comment in ninqu.rules for why this is a shell transform and not a
 20+# map
 21+path_category() {
 22+        rest=${1#.box/cmd/}
 23+        printf "%s\n" "${rest%%/*}"
 24+}
 25+
 26+path_tool() {
 27+        rest=${1##*/}
 28+        printf "%s\n" "${rest%.o}"
 29+}
 30+
 31+path_symbol() {
 32+        tool=$(path_tool "$1")
 33+        printf "%s\n" "$(echo "$tool" | tr '.-' '__')_main"
 34+}
 35+
 36+cat << 'HEAD'
 37+/* this file is autogenerated by genbox.sh. do not edit */
 38+
 39+#include "wexec.h"
 40+#include "util.h"
 41+
 42+#include <stdio.h>
 43+#include <stdlib.h>
 44+#include <string.h>
 45+#include <unistd.h>
 46+
 47+HEAD
 48+
 49+# extern decls, in the order ninqu passed the objects in (BOX_OBJ's
 50+# own glob order, one category at a time)
 51+for obj in "$@"; do
 52+        sym=$(path_symbol "$obj")
 53+        echo "int ${sym}(int, char **);"
 54+done
 55+
 56+cat << 'MID'
 57+
 58+struct Cmd {
 59+        const char *name;
 60+        int (*fn)(int, char **);
 61+        const char *category;
 62+};
 63+
 64+static struct Cmd cmds[] = {
 65+MID
 66+
 67+categories=$(
 68+        for obj in "$@"; do
 69+                path_category "$obj"
 70+        done | sort -u
 71+)
 72+
 73+for category in $categories; do
 74+        for obj in "$@"; do
 75+                [ "$(path_category "$obj")" = "$category" ] || continue
 76+                tool=$(path_tool "$obj")
 77+                sym=$(path_symbol "$obj")
 78+                printf '\t{ "%s", %s, "%s" },\n' "$tool" "$sym" "$category"
 79+                if [ "$tool" = "test" ]; then
 80+                        printf '\t{ "[", %s, "%s" },\n' "$sym" "$category"
 81+                fi
 82+                if [ "$tool" = "xinstall" ]; then
 83+                        printf '\t{ "install", %s, "%s" },\n' "$sym" "$category"
 84+                fi
 85+        done
 86+done
 87+
 88+cat << 'TAIL'
 89+        { NULL, NULL, NULL }
 90+};
 91+
 92+static void
 93+usage(void)
 94+{
 95+        struct Cmd *c;
 96+        const char *last;
 97+        int col;
 98+
 99+        last = NULL;
100+        col = 0;
101+        fprintf(stderr, "usage: %s [-i path] [command [args...]]\n", argv0);
102+        for (c = cmds; c->name; c++) {
103+                if (!last || strcmp(c->category, last) != 0) {
104+                        if (last)
105+                                fprintf(stderr, "\n\n");
106+                        last = c->category;
107+                        fprintf(stderr, "%s:\n", last);
108+                        col = 0;
109+                }
110+                if (col == 0) {
111+                        fprintf(stderr, "\t%s", c->name);
112+                        col = 8 + strlen(c->name);
113+                } else if (col + 2 + strlen(c->name) >= 78) {
114+                        fprintf(stderr, ",\n\t%s", c->name);
115+                        col = 8 + strlen(c->name);
116+                } else {
117+                        fprintf(stderr, ", %s", c->name);
118+                        col += 2 + strlen(c->name);
119+                }
120+        }
121+        if (last)
122+                fputc('\n', stderr);
123+        exit(1);
124+}
125+
126+static void
127+x_install(const char *path)
128+{
129+        struct Cmd *c;
130+        char buf[4096];
131+        int r;
132+
133+        for (c = cmds; c->name; c++) {
134+                r = snprintf(buf, sizeof buf, "%s/%s", path, c->name);
135+                if (r < 0 || r >= (int)sizeof buf)
136+                        eprintf("install: '%s/%s': path too long\n", path, c->name);
137+                remove(buf);
138+                if (symlink("aruu-box", buf) < 0)
139+                        eprintf("install: symlink '%s':", buf);
140+        }
141+}
142+
143+int
144+main(int argc, char *argv[])
145+{
146+        struct Cmd *c;
147+        char *s;
148+
149+        argv0 = argv[0];
150+
151+        for (c = cmds; c->name; c++)
152+                wexec_register(c->name, c->fn);
153+
154+        s = strrchr(argv[0], '/');
155+        if (s)
156+                s++;
157+        else
158+                s = argv[0];
159+
160+        if (strcmp(s, "aruu-box") == 0) {
161+                if (argc < 2)
162+                        usage();
163+                argc--;
164+                argv++;
165+                if (strcmp(argv[0], "-i") == 0) {
166+                        if (argc < 2)
167+                                eprintf("usage: aruu-box -i path\n");
168+                        x_install(argv[1]);
169+                        return 0;
170+                }
171+                s = strrchr(argv[0], '/');
172+                if (s)
173+                        s++;
174+                else
175+                        s = argv[0];
176+        }
177+        for (c = cmds; c->name; c++) {
178+                if (strcmp(c->name, s) == 0) {
179+                        argv0 = (char *)c->name;
180+                        argv[0] = (char *)c->name;
181+                        return c->fn(argc, argv);
182+                }
183+        }
184+        enprintf(127, "%s: not found\n", s);
185+        return 1;
186+}
187+TAIL
+272, -332
  1@@ -1,11 +1,36 @@
  2 #!/bin/sh
  3-# genconfig.sh - generate config.mk and config.kv from build.cfg
  4-
  5+# genconfig.sh generates build configuration from build.cfg
  6+#
  7+# outputs four files:
  8+#
  9+#   scripts/mk/config.set   ninqu reads this via (set-file). flat
 10+#                           KEY=VAL, unquoted. per-tool BUILD_CATEGORY_TOOL
 11+#                           toggles get a lowercase alias here, since
 12+#                           (gate ...) in ninqu.rules only ever checks
 13+#                           the lowercase form
 14+#
 15+#   scripts/mk/config.kv    shell-quoted KEY="VAL" format. sourced by
 16+#                           read via the -config flag by scripts/mkman.
 17+#                           nothing in the ninqu build itself sources
 18+#                           this file: genbox.sh takes its input on argv
 19+#                           only, see the BOX section of ninqu.rules
 20+#
 21+#   shared/libutil/nofork_list.h
 22+#                           wexec.c includes this at compile time.
 23+#                           generated from NOFORK_LIST in build.cfg
 24+#
 25+#   scripts/mk/features.h   every resolved FEATURE_* as a #define
 26+#                           CPPFLAGS pulls this in with -include
 27+#
 28+# the codegen subcommands (getconf, bc, awk, sh) are also here so
 29+# ninqu.rules can call (exec sh scripts/genconfig.sh <step>) for
 30+# each one. they check mtimes internally and only regenerate when
 31+# their input changed
 32+#
 33 set -e
 34 
 35 target=${1:-config}
 36 
 37-# load build.cfg
 38 if [ -f build.cfg ]; then
 39         . ./build.cfg
 40 else
 41@@ -13,196 +38,275 @@ else
 42         exit 1
 43 fi
 44 
 45-# helper to check if source is newer than target
 46 newer_than() {
 47-  [ -e "$2" ] || return 0
 48-  local s t
 49-  s=$(stat -c '%Y' "$1" 2>/dev/null || stat -f '%m' "$1" 2>/dev/null || echo 0)
 50-  t=$(stat -c '%Y' "$2" 2>/dev/null || stat -f '%m' "$2" 2>/dev/null || echo 0)
 51-  [ "$s" -gt "$t" ]
 52+        [ -e "$2" ] || return 0
 53+        local s t
 54+        s=$(stat -c '%Y' "$1" 2>/dev/null || stat -f '%m' "$1" 2>/dev/null || echo 0)
 55+        t=$(stat -c '%Y' "$2" 2>/dev/null || stat -f '%m' "$2" 2>/dev/null || echo 0)
 56+        [ "$s" -gt "$t" ]
 57 }
 58 
 59 gen_getconf() {
 60-  if newer_than scripts/getconf.sh cmd/posix/getconf.h; then
 61-    printf "  GEN   cmd/posix/getconf.h\n"
 62-    scripts/getconf.sh > cmd/posix/getconf.h || { rm -f cmd/posix/getconf.h; exit 1; }
 63-  fi
 64+        if newer_than scripts/getconf.sh cmd/posix/getconf.h; then
 65+                printf "  GEN   cmd/posix/getconf.h\n"
 66+                scripts/getconf.sh > cmd/posix/getconf.h || { rm -f cmd/posix/getconf.h; exit 1; }
 67+        fi
 68 }
 69 
 70 gen_bc() {
 71-  if newer_than cmd/posix/bc.y cmd/posix/bc.c; then
 72-    printf "  GEN   cmd/posix/bc.c\n"
 73-    CC=${CC:-cc}
 74-    YACC=${YACC:-}
 75-    if [ -z "$YACC" ]; then
 76-      if command -v yacc >/dev/null 2>&1; then
 77-        YACC="yacc"
 78-      elif command -v bison >/dev/null 2>&1; then
 79-        YACC="bison -y"
 80-      else
 81-        YACC="yacc"
 82-      fi
 83-    fi
 84-    if [ "$YACC" = "bison -y" ]; then
 85-      bison -o cmd/posix/bc.c cmd/posix/bc.y
 86-    else
 87-      $YACC -o cmd/posix/bc.c cmd/posix/bc.y
 88-    fi
 89-  fi
 90+        if newer_than cmd/posix/bc.y cmd/posix/bc.c; then
 91+                printf "  GEN   cmd/posix/bc.c\n"
 92+                CC=${CC:-cc}
 93+                YACC=${YACC:-}
 94+                if [ -z "$YACC" ]; then
 95+                        if command -v yacc >/dev/null 2>&1; then
 96+                                YACC="yacc"
 97+                        elif command -v bison >/dev/null 2>&1; then
 98+                                YACC="bison -y"
 99+                        else
100+                                YACC="yacc"
101+                        fi
102+                fi
103+                if [ "$YACC" = "bison -y" ]; then
104+                        bison -o cmd/posix/bc.c cmd/posix/bc.y
105+                else
106+                        $YACC -o cmd/posix/bc.c cmd/posix/bc.y
107+                fi
108+        fi
109 }
110 
111 gen_awk() {
112-  local dir="cmd/posix/awk"
113-  CC=${CC:-cc}
114-  YACC=${YACC:-}
115-  if [ -z "$YACC" ]; then
116-    if command -v yacc >/dev/null 2>&1; then
117-      YACC="yacc"
118-    elif command -v bison >/dev/null 2>&1; then
119-      YACC="bison -y"
120-    else
121-      YACC="yacc"
122-    fi
123-  fi
124-
125-  if newer_than "$dir/awkgram.y" "$dir/awkgram.tab.c" || newer_than "$dir/awkgram.y" "$dir/awkgram.tab.h"; then
126-    printf "  YACC  $dir/awkgram.tab.c\n"
127-    if [ "$YACC" = "bison -y" ]; then
128-      bison -d -o "$dir/awkgram.tab.c" "$dir/awkgram.y"
129-    else
130-      $YACC -d -o "$dir/awkgram.tab.c" "$dir/awkgram.y"
131-      if [ ! -f "$dir/awkgram.tab.h" ]; then
132-        if [ -f y.tab.h ]; then
133-          mv y.tab.h "$dir/awkgram.tab.h"
134-        elif [ -f "$dir/y.tab.h" ]; then
135-          mv "$dir/y.tab.h" "$dir/awkgram.tab.h"
136+        local dir="cmd/posix/awk"
137+        CC=${CC:-cc}
138+        YACC=${YACC:-}
139+        if [ -z "$YACC" ]; then
140+                if command -v yacc >/dev/null 2>&1; then
141+                        YACC="yacc"
142+                elif command -v bison >/dev/null 2>&1; then
143+                        YACC="bison -y"
144+                else
145+                        YACC="yacc"
146+                fi
147+        fi
148+
149+        if newer_than "$dir/awkgram.y" "$dir/awkgram.tab.c" || newer_than "$dir/awkgram.y" "$dir/awkgram.tab.h"; then
150+                printf "  YACC  $dir/awkgram.tab.c\n"
151+                if [ "$YACC" = "bison -y" ]; then
152+                        bison -d -o "$dir/awkgram.tab.c" "$dir/awkgram.y"
153+                else
154+                        $YACC -d -o "$dir/awkgram.tab.c" "$dir/awkgram.y"
155+                        if [ ! -f "$dir/awkgram.tab.h" ]; then
156+                                if [ -f y.tab.h ]; then
157+                                        mv y.tab.h "$dir/awkgram.tab.h"
158+                                elif [ -f "$dir/y.tab.h" ]; then
159+                                        mv "$dir/y.tab.h" "$dir/awkgram.tab.h"
160+                                fi
161+                        fi
162+                fi
163+        fi
164+
165+        if newer_than "$dir/maketab.c" "$dir/maketab" || newer_than "$dir/awkgram.tab.h" "$dir/maketab"; then
166+                printf "  CC    $dir/maketab\n"
167+                $CC -o "$dir/maketab" "$dir/maketab.c"
168+        fi
169+
170+        if newer_than "$dir/maketab" "$dir/proctab.c" || newer_than "$dir/awkgram.tab.h" "$dir/proctab.c"; then
171+                printf "  GEN   $dir/proctab.c\n"
172+                "$dir/maketab" "$dir/awkgram.tab.h" > "$dir/proctab.c"
173         fi
174-      fi
175-    fi
176-  fi
177-
178-  if newer_than "$dir/maketab.c" "$dir/maketab" || newer_than "$dir/awkgram.tab.h" "$dir/maketab"; then
179-    printf "  CC    $dir/maketab\n"
180-    $CC -o "$dir/maketab" "$dir/maketab.c"
181-  fi
182-
183-  if newer_than "$dir/maketab" "$dir/proctab.c" || newer_than "$dir/awkgram.tab.h" "$dir/proctab.c"; then
184-    printf "  GEN   $dir/proctab.c\n"
185-    "$dir/maketab" "$dir/awkgram.tab.h" > "$dir/proctab.c"
186-  fi
187 }
188 
189 gen_sh() {
190-  local dir="cmd/posix/sh"
191-  CC=${CC:-cc}
192-
193-  if newer_than "$dir/mknodes.c" "$dir/mknodes"; then
194-    printf "  CC    $dir/mknodes\n"
195-    $CC -o "$dir/mknodes" "$dir/mknodes.c"
196-  fi
197-
198-  if newer_than "$dir/mksyntax.c" "$dir/mksyntax"; then
199-    printf "  CC    $dir/mksyntax\n"
200-    $CC -Ishared -I"$dir" -o "$dir/mksyntax" "$dir/mksyntax.c"
201-  fi
202-
203-  if newer_than "$dir/mktokens" "$dir/token.h"; then
204-    printf "  GEN   $dir/token.h\n"
205-    (cd "$dir" && sh mktokens)
206-  fi
207-
208-  if newer_than "$dir/mksyntax" "$dir/syntax.c" || newer_than "$dir/mksyntax" "$dir/syntax.h"; then
209-    printf "  GEN   $dir/syntax.c\n"
210-    (cd "$dir" && ./mksyntax)
211-  fi
212-
213-  if newer_than "$dir/mknodes" "$dir/nodes.c" || newer_than "$dir/nodetypes" "$dir/nodes.c" || newer_than "$dir/nodes.c.pat" "$dir/nodes.c"; then
214-    printf "  GEN   $dir/nodes.c\n"
215-    (cd "$dir" && ./mknodes nodetypes nodes.c.pat)
216-  fi
217-
218-  if newer_than "$dir/mkbuiltins" "$dir/builtins.c" || newer_than "$dir/builtins.def" "$dir/builtins.c" || newer_than "$dir/shell.h" "$dir/builtins.c" || [ ! -f "$dir/builtins.c" ]; then
219-    printf "  GEN   $dir/builtins.c\n"
220-    (cd "$dir" && sh mkbuiltins .)
221-  fi
222+        local dir="cmd/posix/sh"
223+        CC=${CC:-cc}
224+
225+        if newer_than "$dir/mknodes.c" "$dir/mknodes"; then
226+                printf "  CC    $dir/mknodes\n"
227+                $CC -o "$dir/mknodes" "$dir/mknodes.c"
228+        fi
229+
230+        if newer_than "$dir/mksyntax.c" "$dir/mksyntax"; then
231+                printf "  CC    $dir/mksyntax\n"
232+                $CC -Ishared -I"$dir" -o "$dir/mksyntax" "$dir/mksyntax.c"
233+        fi
234+
235+        if newer_than "$dir/mktokens" "$dir/token.h"; then
236+                printf "  GEN   $dir/token.h\n"
237+                (cd "$dir" && sh mktokens)
238+        fi
239+
240+        if newer_than "$dir/mksyntax" "$dir/syntax.c" || newer_than "$dir/mksyntax" "$dir/syntax.h"; then
241+                printf "  GEN   $dir/syntax.c\n"
242+                (cd "$dir" && ./mksyntax)
243+        fi
244+
245+        if newer_than "$dir/mknodes" "$dir/nodes.c" || newer_than "$dir/nodetypes" "$dir/nodes.c" || newer_than "$dir/nodes.c.pat" "$dir/nodes.c"; then
246+                printf "  GEN   $dir/nodes.c\n"
247+                (cd "$dir" && ./mknodes nodetypes nodes.c.pat)
248+        fi
249+
250+        if newer_than "$dir/mkbuiltins" "$dir/builtins.c" || newer_than "$dir/builtins.def" "$dir/builtins.c" || newer_than "$dir/shell.h" "$dir/builtins.c" || [ ! -f "$dir/builtins.c" ]; then
251+                printf "  GEN   $dir/builtins.c\n"
252+                (cd "$dir" && sh mkbuiltins .)
253+        fi
254 }
255 
256 clean() {
257-  printf "  CLEAN generated files\n"
258-  rm -f cmd/posix/getconf.h
259-  rm -f cmd/posix/bc.c cmd/posix/bc.h
260-  rm -f cmd/posix/awk/awkgram.tab.c cmd/posix/awk/awkgram.tab.h cmd/posix/awk/proctab.c cmd/posix/awk/maketab
261-  rm -f cmd/posix/sh/mknodes cmd/posix/sh/mksyntax cmd/posix/sh/token.h cmd/posix/sh/syntax.c cmd/posix/sh/syntax.h cmd/posix/sh/nodes.c cmd/posix/sh/nodes.h cmd/posix/sh/builtins.c cmd/posix/sh/builtins.h
262-  rm -f scripts/mk/config.mk scripts/mk/config.kv scripts/mk/rules.mk shared/libutil/nofork_list.h
263+        printf "  CLEAN generated files\n"
264+        rm -f cmd/posix/getconf.h
265+        rm -f cmd/posix/bc.c cmd/posix/bc.h
266+        rm -f cmd/posix/awk/awkgram.tab.c cmd/posix/awk/awkgram.tab.h cmd/posix/awk/proctab.c cmd/posix/awk/maketab
267+        rm -f cmd/posix/sh/mknodes cmd/posix/sh/mksyntax cmd/posix/sh/token.h cmd/posix/sh/syntax.c cmd/posix/sh/syntax.h cmd/posix/sh/nodes.c cmd/posix/sh/nodes.h cmd/posix/sh/builtins.c cmd/posix/sh/builtins.h
268+        rm -f scripts/mk/config.set scripts/mk/config.kv scripts/mk/features.h shared/libutil/nofork_list.h
269 }
270 
271-# dynamic tool discovery function
272-list_tools() {
273-        local catg="$1" dirs="" files="" d f b
274-        [ -d "cmd/$catg" ] || return 0
275-        # find all subdirectories
276-        for d in "cmd/$catg"/*; do
277-                if [ -d "$d" ]; then
278-                        dirs="$dirs ${d##*/}"
279-                fi
280-        done
281-        # find all .c and .y files
282-        for f in "cmd/$catg"/*.c "cmd/$catg"/*.y; do
283-                [ -f "$f" ] || continue
284-                b=${f##*/}
285-                b=${b%.c}
286-                b=${b%.y}
287-                case "$b" in
288-                        # exclude intermediate files or helper generators
289-                        maketab|mknodes|mksyntax|y.tab|*gram.tab) continue ;;
290-                esac
291-                files="$files $b"
292+# config: generate config.set, config.kv, nofork_list.h
293+#
294+# config.set is what ninqu reads. config.kv is what mkbox and
295+# genbox.sh source. both contain the same data in different formats:
296+# config.kv is shell-quoted (KEY="VAL"), config.set is unquoted
297+# (KEY=VAL) since ninqu's (set-file) takes the rest of the line
298+# verbatim
299+
300+config() {
301+        mkdir -p scripts/mk
302+
303+        kv_file="scripts/mk/config.kv"
304+        set_file="scripts/mk/config.set"
305+
306+        : > "${kv_file}"
307+        : > "${set_file}"
308+
309+        emit() {
310+                local k="$1" v="$2"
311+                echo "${k}=\"${v}\"" >> "${kv_file}"
312+                echo "${k}=${v}" >> "${set_file}"
313+        }
314+
315+        emit VERSION   "${VERSION}"
316+        emit PREFIX    "${PREFIX}"
317+        emit MANPREFIX "${MANPREFIX:-/usr/local/share/man}"
318+        emit RANLIB    "${RANLIB:-ranlib}"
319+        emit AR        "${AR:-ar}"
320+        emit ARFLAGS   "${ARFLAGS:-rc}"
321+        emit CC        "${CC:-cc}"
322+        emit CFLAGS    "${CFLAGS:--std=c99 -Wall -Wextra -pedantic}"
323+        emit LDFLAGS   "${LDFLAGS:-} ${LDFLAGS_TLS:-}"
324+        emit LDLIBS    "${LDLIBS:-} ${LDLIBS_TLS:-}"
325+        emit OBJCOPY   "${OBJCOPY:-objcopy}"
326+        emit LD        "${LD:-ld}"
327+
328+        emit CPPFLAGS_TLS "${CPPFLAGS_TLS:-}"
329+        emit LDFLAGS_TLS  "${LDFLAGS_TLS:-}"
330+        emit LDLIBS_TLS   "${LDLIBS_TLS:-}"
331+
332+        # these seven are only consulted per-tool below (build_CATEGORY_TOOL,
333+        # build_CATEGORY_$(BASESTEM)), the bare category toggle has no gate
334+        # anywhere in ninqu.rules, (group CATEGORY ...) already does that
335+        # job. config.kv still gets the uppercase copy since genbox.sh
336+        # and mkbox source it directly
337+        for g in BUILD_POSIX BUILD_XSI BUILD_NET BUILD_PSEUDO BUILD_EXTRA BUILD_LINUX BUILD_DEV; do
338+                eval "val=\${$g:-0}"
339+                echo "${g}=\"${val}\"" >> "${kv_file}"
340         done
341 
342-        # output sorted unique list of tool names
343-        # shellcheck disable=SC2086
344-        printf '%s %s\n' $dirs $files | tr ' ' '\n' | sort -u | grep -v '^$'
345-}
346+        # per-tool toggles: scan cmd/<category>/ for tools. a tool is
347+        # either a .c/.y file directly in the category dir (perfile)
348+        # or a subdirectory (multi-file tool). emit BUILD_<CATEGORY>_<TOOL>
349+        # plus lowercase alias for each
350+        for category in posix xsi net pseudo extra linux dev; do
351+                [ -d "cmd/$category" ] || continue
352+                u_category=$(echo "$category" | tr '[:lower:]' '[:upper:]')
353+
354+                tools=""
355+                for d in "cmd/$category"/*; do
356+                        [ -d "$d" ] && tools="$tools ${d##*/}"
357+                done
358+                for f in "cmd/$category"/*.c "cmd/$category"/*.y; do
359+                        [ -f "$f" ] || continue
360+                        b=${f##*/}
361+                        b=${b%.c}
362+                        b=${b%.y}
363+                        case "$b" in
364+                                maketab|mknodes|mksyntax|y.tab|*gram.tab) continue ;;
365+                        esac
366+                        tools="$tools $b"
367+                done
368 
369-write_subtarget() {
370-        group="$1"
371-        tool="$2"
372-        u_group=$(echo "$group" | tr '[:lower:]' '[:upper:]')
373-        u_tool=$(echo "$tool" | tr 'a-z-' 'A-Z_')
374-        var_name="BUILD_${u_group}_${u_tool}"
375+                tools=$(echo "$tools" | tr ' ' '\n' | sort -u | grep -v '^$' || true)
376 
377-        eval "has_override=\${$var_name+yes}"
378-        eval "val=\${$var_name:-}"
379+                for b in $tools; do
380+                        u_tool=$(echo "$b" | tr 'a-z-' 'A-Z_')
381+                        var_name="BUILD_${u_category}_${u_tool}"
382 
383-        eval "group_val=\$BUILD_${u_group}"
384+                        #if [ "$category" = "dev" ] && [ "$u_tool" = "AS" ]; then
385+                        #        var_name="BUILD_DEV_CC"
386+                        #fi
387 
388-        if [ "$has_override" = "yes" ]; then
389-                echo "${var_name} = ${val}" >> "${mk_file}"
390-                echo "${var_name}=\"${val}\"" >> "${kv_file}"
391-        else
392-                echo "${var_name} = \$(BUILD_${u_group})" >> "${mk_file}"
393-                echo "${var_name}=\"${group_val}\"" >> "${kv_file}"
394-        fi
395-}
396+                        eval "has_override=\${$var_name+yes}"
397+                        if [ "$has_override" = "yes" ]; then
398+                                eval "val=\${$var_name}"
399+                        else
400+                                eval "val=\$BUILD_${u_category}"
401+                        fi
402 
403-gen_dir_rules() {
404-        local dir="$1" flags="$2" extra_files="$3" f obj files uniq_files
405-        [ -d "$dir" ] || return 0
406-        files=""
407-        for f in "$dir"/*.c; do
408-                if [ -f "$f" ]; then
409-                        files="$files $f"
410-                fi
411+                        echo "${var_name}=\"${val}\"" >> "${kv_file}"
412+                        lk=$(echo "$var_name" | tr '[:upper:]' '[:lower:]')
413+                        echo "${lk}=${val}" >> "${set_file}"
414+                done
415         done
416-        for f in ${extra_files}; do
417-                files="$files $f"
418+
419+        # box's own link line has no per-tool composition the way a
420+        # standalone PERFILE binary's EXTRA_LIBS map does (box links
421+        # every enabled tool's object into one binary in one shot), so
422+        # a tool that needs a library nothing else in the box needs
423+        # (yap needs terminfo) can only get it by ninqu.rules reading
424+        # a value genconfig.sh already resolved, same as every other
425+        # build_* toggle. this is still data, not policy: which
426+        # library to add for which tool is decided here, not what to
427+        # do with that decision
428+        box_extra_libs=""
429+        [ "${yap_val:-0}" != "0" ] && box_extra_libs="-ltinfo"
430+        emit BOX_EXTRA_LIBS "${box_extra_libs}"
431+
432+        features=$(set | grep '^FEATURE_[A-Za-z0-9_]*=' | cut -d= -f1 | sort)
433+        for f in $features; do
434+                eval "val=\$$f"
435+                emit "$f" "$val"
436         done
437-        uniq_files=$(echo "$files" | tr ' ' '\n' | sort -u | grep -v '^$')
438-        for f in $uniq_files; do
439-                obj="${f%.c}.o"
440-                echo "${obj}: ${f}" >> "${rules_file}"
441-                echo "	\$(CC) \$(CPPFLAGS) ${flags} \$(CFLAGS) -o \$@ -c ${f}" >> "${rules_file}"
442+
443+        # every resolved FEATURE_* becomes a #define in one generated
444+        # header. config.h's own #ifndef guards still apply to anything
445+        # not here (there is nothing not here), and any file gets the
446+        # real values just by being compiled with -include, whether or
447+        # not it bothers to #include "config.h" itself.
448+        features_h="scripts/mk/features.h"
449+        {
450+                echo "/* autogenerated by genconfig.sh, do not edit */"
451+                for f in $features; do
452+                        eval "fval=\$$f"
453+                        echo "#define ${f} ${fval}"
454+                done
455+        } > "${features_h}"
456+
457+        cppflags_common="-DPREFIX=\"${PREFIX}\" -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_NETBSD_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_FILE_OFFSET_BITS=64 -include ${features_h} ${CPPFLAGS_TLS:-}"
458+        cppflags_set="-Ishared ${cppflags_common}"
459+        echo "CPPFLAGS=\"${cppflags_set}\"" >> "${kv_file}"
460+        echo "CPPFLAGS=${cppflags_set}" >> "${set_file}"
461+
462+        # shared/tls.h would conflict with the system's tls.h
463+        # if we passed -Ishared to build libutil
464+        echo "CPPFLAGS_LIBUTIL=\"${cppflags_common}\"" >> "${kv_file}"
465+        echo "CPPFLAGS_LIBUTIL=${cppflags_common}" >> "${set_file}"
466+
467+        nofork_h="shared/libutil/nofork_list.h"
468+        echo "/* autogenerated by genconfig.sh */" > "${nofork_h}"
469+        echo "static const char *nofork_list[] = {" >> "${nofork_h}"
470+        for app in ${NOFORK_LIST}; do
471+                echo "  \"${app}\"," >> "${nofork_h}"
472         done
473+        echo "  NULL" >> "${nofork_h}"
474+        echo "};" >> "${nofork_h}"
475 }
476 
477 case "$target" in
478@@ -210,181 +314,17 @@ case "$target" in
479         bc)      gen_bc ;;
480         awk)     gen_awk ;;
481         sh)      gen_sh ;;
482-        clean)   clean ;;
483         all)
484                 gen_getconf
485                 gen_bc
486                 gen_awk
487                 gen_sh
488                 ;;
489-        config)
490-                mkdir -p scripts/mk
491-
492-                mk_file="scripts/mk/config.mk"
493-                kv_file="scripts/mk/config.kv"
494-
495-                # clear existing files
496-                : > "${mk_file}"
497-                : > "${kv_file}"
498-
499-                # write version prefix manprefix
500-                echo "VERSION = ${VERSION}" >> "${mk_file}"
501-                echo "VERSION=\"${VERSION}\"" >> "${kv_file}"
502-
503-                echo "PREFIX = ${PREFIX}" >> "${mk_file}"
504-                echo "PREFIX=\"${PREFIX}\"" >> "${kv_file}"
505-
506-                # default manprefix to prefix/share/man if empty
507-                man_pref="${MANPREFIX:-/usr/local/share/man}"
508-                echo "MANPREFIX = ${man_pref}" >> "${mk_file}"
509-                echo "MANPREFIX=\"${man_pref}\"" >> "${kv_file}"
510-
511-                # toolchain defaults
512-                echo "RANLIB = ${RANLIB:-ranlib}" >> "${mk_file}"
513-                echo "RANLIB=\"${RANLIB:-ranlib}\"" >> "${kv_file}"
514-
515-                echo "AR = ${AR:-ar}" >> "${mk_file}"
516-                echo "AR=\"${AR:-ar}\"" >> "${kv_file}"
517-
518-                echo "ARFLAGS = ${ARFLAGS:-rc}" >> "${mk_file}"
519-                echo "ARFLAGS=\"${ARFLAGS:-rc}\"" >> "${kv_file}"
520-
521-                # build toggles
522-                echo "" >> "${mk_file}"
523-                echo "# build toggles" >> "${mk_file}"
524-                for g in BUILD_POSIX BUILD_XSI BUILD_NET BUILD_PSEUDO BUILD_EXTRA BUILD_LINUX BUILD_DEV; do
525-                        eval "val=\${$g:-0}"
526-                        echo "$g = $val" >> "${mk_file}"
527-                        echo "$g=\"$val\"" >> "${kv_file}"
528-                done
529-
530-                # write subtargets for each category dynamically
531-                for catg in posix xsi net pseudo extra linux dev; do
532-                        echo "" >> "${mk_file}"
533-                        echo "# ${catg} subtargets" >> "${mk_file}"
534-                        tools=$(list_tools "$catg")
535-                        for t in $tools; do
536-                                write_subtarget "$catg" "$t"
537-                        done
538-
539-                        # build the enabled list using lowercase tools
540-                        enabled=""
541-                        for t in $tools; do
542-                                u_group=$(echo "$catg" | tr '[:lower:]' '[:upper:]')
543-                                u_tool=$(echo "$t" | tr 'a-z-' 'A-Z_')
544-
545-                                # special cases for dev tools mapping to different variables
546-                                if [ "$catg" = "dev" ] && [ "$u_tool" = "AS" ]; then
547-                                        var_name="BUILD_DEV_CC"
548-                                else
549-                                        var_name="BUILD_${u_group}_${u_tool}"
550-                                fi
551-
552-                                eval "has_override=\${$var_name+yes}"
553-                                eval "val=\${$var_name:-}"
554-                                if [ "$has_override" != "yes" ]; then
555-                                        if [ "$catg" = "dev" ] && [ "$u_tool" = "AS" ]; then
556-                                                eval "val=\$BUILD_DEV"
557-                                        else
558-                                                eval "val=\$BUILD_${u_group}"
559-                                        fi
560-                                fi
561-
562-                                if [ "$val" = "1" ]; then
563-                                        # map to the correct path
564-                                        case "$catg/$t" in
565-                                                posix/awk)  path="cmd/posix/awk/awk" ;;
566-                                                posix/sh)   path="cmd/posix/sh/sh" ;;
567-                                                posix/make) path="cmd/posix/make/make" ;;
568-                                                extra/yap)  path="cmd/extra/yap/yap" ;;
569-                                                dev/ar)     path="cmd/dev/ar/ar" ;;
570-                                                dev/as)     path="cmd/dev/as/as" ;;
571-                                                dev/ld)     path="cmd/dev/ld/ld" ;;
572-                                                dev/cc)     path="cmd/dev/cc/cc" ;;
573-                                                *)          path="cmd/$catg/$t" ;;
574-                                        esac
575-                                        enabled="$enabled $path"
576-                                fi
577-                        done
578-                        u_catg=$(echo "$catg" | tr '[:lower:]' '[:upper:]')
579-                        # write the list of enabled binaries
580-                        echo "${u_catg}_BIN_CONFIG =${enabled}" >> "${mk_file}"
581-                done
582-
583-                # write feature flags
584-                echo "" >> "${mk_file}"
585-                echo "# feature flags" >> "${mk_file}"
586-                features=$(set | grep '^FEATURE_[A-Za-z0-9_]*=' | cut -d= -f1 | sort)
587-                for f in $features; do
588-                        eval "val=\$$f"
589-                        echo "$f = $val" >> "${mk_file}"
590-                        echo "$f=\"$val\"" >> "${kv_file}"
591-                done
592-
593-                # write TLS variables to config files
594-                echo "" >> "${mk_file}"
595-                echo "CPPFLAGS_TLS = ${CPPFLAGS_TLS}" >> "${mk_file}"
596-                echo "CPPFLAGS_TLS=\"${CPPFLAGS_TLS}\"" >> "${kv_file}"
597-
598-                echo "LDFLAGS_TLS = ${LDFLAGS_TLS}" >> "${mk_file}"
599-                echo "LDFLAGS_TLS=\"${LDFLAGS_TLS}\"" >> "${kv_file}"
600-
601-                echo "LDLIBS_TLS = ${LDLIBS_TLS}" >> "${mk_file}"
602-                echo "LDLIBS_TLS=\"${LDLIBS_TLS}\"" >> "${kv_file}"
603-
604-                # write CPPFLAGS with all feature macros
605-                echo "" >> "${mk_file}"
606-                echo "CPPFLAGS =\\" >> "${mk_file}"
607-                echo "        -Ishared\\" >> "${mk_file}"
608-                echo "        -DPREFIX=\\\"\\\$(PREFIX)\\\"\\" >> "${mk_file}"
609-                echo "        -D_DEFAULT_SOURCE\\" >> "${mk_file}"
610-                echo "        -D_GNU_SOURCE\\" >> "${mk_file}"
611-                echo "        -D_NETBSD_SOURCE\\" >> "${mk_file}"
612-                echo "        -D_BSD_SOURCE\\" >> "${mk_file}"
613-                echo "        -D_XOPEN_SOURCE=700\\" >> "${mk_file}"
614-                echo "        -D_FILE_OFFSET_BITS=64\\" >> "${mk_file}"
615-                for f in $features; do
616-                        if [ "$f" = "FEATURE_USE_SSL" ]; then
617-                                continue
618-                        fi
619-                        echo "        -D$f=\$($f)\\" >> "${mk_file}"
620-                done
621-                echo "        \$(CPPFLAGS_TLS)" >> "${mk_file}"
622-
623-                # write toolchain configuration
624-                echo "" >> "${mk_file}"
625-                echo "CC = ${CC:-cc}" >> "${mk_file}"
626-                echo "CFLAGS = ${CFLAGS:-}" >> "${mk_file}"
627-                echo "LDFLAGS = ${LDFLAGS:-} \$(LDFLAGS_TLS)" >> "${mk_file}"
628-                echo "LDLIBS = ${LDLIBS:-} \$(LDLIBS_TLS)" >> "${mk_file}"
629-
630-                # write compilation rules for subdirectories that need special flags or nested structures
631-                rules_file="scripts/mk/rules.mk"
632-                echo "# autogenerated compilation rules" > "${rules_file}"
633-
634-                echo "" >> "${rules_file}"
635-                gen_dir_rules "cmd/posix/awk" "-Icmd/posix/awk" "cmd/posix/awk/awkgram.tab.c cmd/posix/awk/proctab.c"
636-                gen_dir_rules "cmd/posix/sh" "-DSHELL -Icmd/posix/sh" "cmd/posix/sh/syntax.c cmd/posix/sh/nodes.c cmd/posix/sh/builtins.c"
637-                gen_dir_rules "cmd/extra/yap" "" ""
638-                gen_dir_rules "cmd/dev/ar" "-Icmd/dev/ar" ""
639-                gen_dir_rules "cmd/dev/xcutil" "-Icmd/dev/xcutil" ""
640-                gen_dir_rules "cmd/dev/ld" "-Icmd/dev/xcutil -Icmd/dev/ld" ""
641-                gen_dir_rules "cmd/dev/as" "-Icmd/dev/xcutil -Icmd/dev/as" ""
642-                gen_dir_rules "cmd/dev/as/arch/x64" "-Icmd/dev/xcutil -Icmd/dev/as -Icmd/dev/as/arch/x64" ""
643-                gen_dir_rules "cmd/dev/cc" "-Icmd/dev/cc" ""
644-
645-                # generate nofork_list.h
646-                nofork_h="shared/libutil/nofork_list.h"
647-                echo "/* autogenerated by genconfig.sh */" > "${nofork_h}"
648-                echo "static const char *nofork_list[] = {" >> "${nofork_h}"
649-                for app in ${NOFORK_LIST}; do
650-                        echo "  \"${app}\"," >> "${nofork_h}"
651-                done
652-                echo "  NULL" >> "${nofork_h}"
653-                echo "};" >> "${nofork_h}"
654-                ;;
655+        config)  config ;;
656+        clean)   clean ;;
657         *)
658-                printf "unknown target: %s\n" "$target" >&2
659+                printf "genconfig: unknown target: %s\n" "$target" >&2
660+                printf "usage: %s (config|getconf|bc|awk|sh|all|clean)\n" "$0" >&2
661                 exit 1
662                 ;;
663 esac
+0, -511
  1@@ -1,511 +0,0 @@
  2-#!/bin/sh
  3-# mkbox - build aruu-box, the aruu multi-call binary
  4-#
  5-# expects CC CPPFLAGS CFLAGS LDFLAGS LDLIBS OBJCOPY YACC from the environment
  6-# defaults match config.kv but can be overridden
  7-#
  8-# run from the source root (the Makefile does this automatically)
  9-
 10-set -e
 11-
 12-ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
 13-
 14-# load key-value config if it exists
 15-if [ -f "${ROOT}/scripts/mk/config.kv" ]; then
 16-        . "${ROOT}/scripts/mk/config.kv"
 17-fi
 18-
 19-: "${CC:=cc}"
 20-: "${CFLAGS:=-std=c99 -Wall -Wextra -pedantic}"
 21-: "${LDFLAGS:=}"
 22-: "${LDLIBS:=-lcrypt -lresolv}"
 23-: "${OBJCOPY:=objcopy}"
 24-: "${YACC:=yacc}"
 25-
 26-# build feature flag string from loaded config.kv variables
 27-_feature_flags=""
 28-for _f in $(set | grep '^FEATURE_[A-Za-z0-9_]*=' | cut -d= -f1 | sort); do
 29-        eval "_v=\$$_f"
 30-        _feature_flags="$_feature_flags -D${_f}=${_v}"
 31-done
 32-unset _f _v
 33-
 34-CPPFLAGS="-Ishared -DPREFIX=\\\"${PREFIX:-/usr/local}\\\" -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_NETBSD_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_FILE_OFFSET_BITS=64${_feature_flags} ${CPPFLAGS_TLS:-}"
 35-
 36-# -lm must be included if diff or awk is built
 37-if [ "${BUILD_POSIX_DIFF:-0}" = "1" ] || [ "${BUILD_POSIX_AWK:-0}" = "1" ]; then
 38-        case "$LDLIBS" in
 39-                *-lm*) ;;
 40-                *) LDLIBS="${LDLIBS} -lm" ;;
 41-        esac
 42-fi
 43-# -ltinfo must be included if yap is built
 44-if [ "${BUILD_EXTRA_YAP:-0}" = "1" ]; then
 45-        case "$LDLIBS" in
 46-                *-ltinfo*) ;;
 47-                *) LDLIBS="${LDLIBS} -ltinfo" ;;
 48-        esac
 49-fi
 50-
 51-BDIR="${ROOT}/.box"
 52-BOX="${ROOT}/aruu-box"
 53-LIBREDLINE="${ROOT}/shared/libredline/libredline.a"
 54-LIBUTIL="${ROOT}/shared/libutil/libutil.a"
 55-LIBUTF="${ROOT}/shared/libutf/libutf.a"
 56-
 57-die() {
 58-        printf 'mkbox: %s\n' "$*" >&2
 59-        exit 1
 60-}
 61-
 62-# Clean up build directory on exit or signal termination
 63-trap 'rm -rf "${BDIR}"' EXIT INT QUIT TERM
 64-
 65-[ -f "${LIBREDLINE}" ] || die "libredline.a not found; run 'make' first"
 66-[ -f "${LIBUTIL}" ] || die "libutil.a not found; run 'make' first"
 67-[ -f "${LIBUTF}" ]  || die "libutf.a not found; run 'make' first"
 68-
 69-[ -f "${ROOT}/cmd/posix/getconf.h" ] || {
 70-        printf 'mkbox: generating getconf.h\n'
 71-        "${ROOT}/scripts/getconf.sh" > "${ROOT}/cmd/posix/getconf.h" ||
 72-                die "scripts/getconf.sh failed"
 73-}
 74-
 75-rm -rf "${BDIR}"
 76-mkdir -p "${BDIR}"
 77-
 78-OBJS=""
 79-DECL_F="${BDIR}/decls.inc"
 80-: > "${DECL_F}"
 81-for _c in posix linux net pseudo xsi extra; do
 82-        : > "${BDIR}/entries_${_c}.inc"
 83-done
 84-unset _c
 85-
 86-alias_entry() {
 87-        printf '\t{ "%s", %s_main, "%s" },\n' "$1" "$2" "$3" \
 88-                >> "${BDIR}/entries_${3}.inc"
 89-}
 90-
 91-compile_tool() {
 92-        src="$1"
 93-        name="$2"
 94-        fn="$3"
 95-        catg="$4"
 96-        extra="${5:-}"
 97-        out="${BDIR}/${fn}.o"
 98-        symf="${BDIR}/${fn}.syms"
 99-
100-        printf 'mkbox: cc %s\n' "${name}"
101-        eval "${CC} ${CPPFLAGS} ${extra} ${CFLAGS} -c \"\$src\" -o \"\$out\"" ||
102-                die "compile failed: ${src}"
103-        printf 'main %s_main\n' "${fn}" > "${symf}"
104-        ${OBJCOPY} --redefine-syms="${symf}" "${out}" ||
105-                die "objcopy failed: ${name}"
106-        ${OBJCOPY} --keep-global-symbol="${fn}_main" "${out}" ||
107-                die "objcopy localize failed: ${name}"
108-        rm -f "${symf}"
109-        OBJS="${OBJS} ${out}"
110-        printf 'int %s_main(int, char **);\n' "${fn}" >> "${DECL_F}"
111-        printf '\t{ "%s", %s_main, "%s" },\n' "${name}" "${fn}" "${catg}" \
112-                >> "${BDIR}/entries_${catg}.inc"
113-}
114-
115-compile_make() {
116-        mdir="${ROOT}/cmd/posix/make"
117-        symf="${BDIR}/make.syms"
118-        printf 'mkbox: cc make (multi)\n'
119-        make_objs=""
120-        for s in defaults parser posix rules; do
121-                out="${BDIR}/make_${s}.o"
122-                eval "${CC} ${CPPFLAGS} ${CFLAGS} -I\"\$mdir\" -c \"\$mdir/\$s.c\" -o \"\$out\"" ||
123-                        die "compile failed: make/${s}.c"
124-                make_objs="${make_objs} ${out}"
125-        done
126-        out_main="${BDIR}/make_main.o"
127-        eval "${CC} ${CPPFLAGS} ${CFLAGS} -I\"\$mdir\" -c \"\$mdir/main.c\" -o \"\${out_main}\"" ||
128-                die "compile failed: make/main.c"
129-        make_objs="${make_objs} ${out_main}"
130-
131-        # Combined incremental link to resolve internal symbols cleanly
132-        combined="${BDIR}/make_combined.o"
133-        ${LD:-ld} -r -o "${combined}" ${make_objs} ||
134-                die "ld -r failed: make"
135-
136-        printf 'main make_main\n' > "${symf}"
137-        ${OBJCOPY} --redefine-syms="${symf}" "${combined}" ||
138-                die "objcopy failed: make"
139-        ${OBJCOPY} --keep-global-symbol=make_main "${combined}" ||
140-                die "objcopy localize failed: make"
141-        rm -f "${symf}" ${make_objs}
142-
143-        OBJS="${OBJS} ${combined}"
144-        printf 'int make_main(int, char **);\n' >> "${DECL_F}"
145-        printf '\t{ "make", make_main, "posix" },\n' >> "${BDIR}/entries_posix.inc"
146-}
147-
148-
149-compile_yap() {
150-        ydir="${ROOT}/cmd/extra/yap"
151-        symf="${BDIR}/yap.syms"
152-        yap_objs=""
153-
154-        [ -d "${ydir}" ] || return 0
155-        printf 'mkbox: cc yap (multi)\n'
156-        for src in "${ydir}"/*.c; do
157-                [ -f "${src}" ] || continue
158-                s=${src##*/}
159-                s=${s%.c}
160-                out="${BDIR}/yap_${s}.o"
161-                eval "${CC} ${CPPFLAGS} ${CFLAGS} -c \"\$src\" -o \"\$out\"" ||
162-                        die "compile failed: yap/${s}.c"
163-                yap_objs="${yap_objs} ${out}"
164-        done
165-
166-        combined="${BDIR}/yap_combined.o"
167-        ${LD:-ld} -r -o "${combined}" ${yap_objs} ||
168-                die "ld -r failed: yap"
169-
170-        printf 'main yap_main\n' > "${symf}"
171-        ${OBJCOPY} --redefine-syms="${symf}" "${combined}" ||
172-                die "objcopy failed: yap"
173-        ${OBJCOPY} --keep-global-symbol=yap_main "${combined}" ||
174-                die "objcopy localize failed: yap"
175-        rm -f "${symf}" ${yap_objs}
176-
177-        OBJS="${OBJS} ${combined}"
178-        printf 'int yap_main(int, char **);\n' >> "${DECL_F}"
179-        printf '\t{ "yap", yap_main, "extra" },\n' >> "${BDIR}/entries_extra.inc"
180-}
181-
182-compile_bc() {
183-        bdir="${ROOT}/cmd/posix"
184-        symf="${BDIR}/bc.syms"
185-
186-        printf 'mkbox: yacc bc/bc.y\n'
187-        ${YACC} -o "${bdir}/bc.c" "${bdir}/bc.y" ||
188-                die "yacc failed: bc/bc.y"
189-
190-        out="${BDIR}/bc.o"
191-        eval "${CC} ${CPPFLAGS} ${CFLAGS} -c \"${bdir}/bc.c\" -o \"${out}\"" ||
192-                die "compile failed: bc/bc.c"
193-
194-        printf 'main bc_main\n' > "${symf}"
195-        ${OBJCOPY} --redefine-syms="${symf}" "${out}" ||
196-                die "objcopy failed: bc"
197-        ${OBJCOPY} --keep-global-symbol=bc_main "${out}" ||
198-                die "objcopy localize failed: bc"
199-        rm -f "${symf}" "${bdir}/bc.c"
200-
201-        OBJS="${OBJS} ${out}"
202-        printf 'int bc_main(int, char **);\n' >> "${DECL_F}"
203-        printf '\t{ "bc", bc_main, "posix" },\n' >> "${BDIR}/entries_posix.inc"
204-}
205-
206-compile_awk() {
207-        adir="${ROOT}/cmd/posix/awk"
208-        symf="${BDIR}/awk.syms"
209-        awk_objs=""
210-
211-        printf 'mkbox: yacc awk/awkgram.y\n'
212-        ${YACC} -d -o "${adir}/awkgram.tab.c" "${adir}/awkgram.y" ||
213-                die "yacc failed: awk/awkgram.y"
214-
215-        # maketab is a host code-generator, only needs CFLAGS, no CPPFLAGS
216-        printf 'mkbox: cc awk/maketab\n'
217-        ${CC} ${CFLAGS} -o "${BDIR}/awk_maketab" "${adir}/maketab.c" ||
218-                die "compile failed: awk/maketab.c"
219-
220-        printf 'mkbox: gen awk/proctab.c\n'
221-        "${BDIR}/awk_maketab" "${adir}/awkgram.tab.h" > "${adir}/proctab.c" ||
222-                die "maketab failed"
223-
224-        for s in b main parse proctab tran lib run lex math; do
225-                out="${BDIR}/awk_${s}.o"
226-                eval "${CC} ${CPPFLAGS} -I\"\$adir\" ${CFLAGS} -c \"\$adir/${s}.c\" -o \"\$out\"" ||
227-                        die "compile failed: awk/${s}.c"
228-                awk_objs="${awk_objs} ${out}"
229-        done
230-        out="${BDIR}/awk_awkgram.tab.o"
231-        eval "${CC} ${CPPFLAGS} -I\"\$adir\" ${CFLAGS} -c \"\$adir/awkgram.tab.c\" -o \"\$out\"" ||
232-                die "compile failed: awk/awkgram.tab.c"
233-        awk_objs="${awk_objs} ${out}"
234-
235-        combined="${BDIR}/awk_combined.o"
236-        ${LD:-ld} -r -o "${combined}" ${awk_objs} ||
237-                die "ld -r failed: awk"
238-
239-        printf 'main awk_main\n' > "${symf}"
240-        ${OBJCOPY} --redefine-syms="${symf}" "${combined}" ||
241-                die "objcopy failed: awk"
242-        ${OBJCOPY} --keep-global-symbol=awk_main "${combined}" ||
243-                die "objcopy localize failed: awk"
244-        rm -f "${symf}" ${awk_objs}
245-
246-        OBJS="${OBJS} ${combined}"
247-        printf 'int awk_main(int, char **);\n' >> "${DECL_F}"
248-        printf '\t{ "awk", awk_main, "posix" },\n' >> "${BDIR}/entries_posix.inc"
249-}
250-
251-compile_sh() {
252-        sdir="${ROOT}/cmd/posix/sh"
253-        symf="${BDIR}/sh.syms"
254-        sh_objs=""
255-
256-        # mknodes and mksyntax are host code-generators, not part of the shell
257-        printf 'mkbox: cc sh/mknodes\n'
258-        ${CC} ${CFLAGS} -o "${BDIR}/sh_mknodes" "${sdir}/mknodes.c" ||
259-                die "compile failed: sh/mknodes.c"
260-
261-        printf 'mkbox: cc sh/mksyntax\n'
262-        ${CC} ${CPPFLAGS} -I"${sdir}" ${CFLAGS} -o "${BDIR}/sh_mksyntax" "${sdir}/mksyntax.c" ||
263-                die "compile failed: sh/mksyntax.c"
264-
265-        printf 'mkbox: gen sh/nodes.{c,h}\n'
266-        (cd "${sdir}" && "${BDIR}/sh_mknodes" nodetypes nodes.c.pat) ||
267-                die "mknodes failed"
268-
269-        printf 'mkbox: gen sh/syntax.{c,h}\n'
270-        (cd "${sdir}" && "${BDIR}/sh_mksyntax") ||
271-                die "mksyntax failed"
272-
273-        printf 'mkbox: gen sh/builtins.{c,h}\n'
274-        (cd "${sdir}" && sh mkbuiltins .) ||
275-                die "mkbuiltins failed"
276-
277-        printf 'mkbox: gen sh/token.h\n'
278-        (cd "${sdir}" && sh mktokens) ||
279-                die "mktokens failed"
280-
281-        for s in alias arith_yacc arith_yylex cd easter echo error eval exec \
282-                  expand lineedit input jobs kill mail main memalloc \
283-                  miscbltin mystring options output parser printf redir \
284-                  show test trap var builtins nodes syntax; do
285-                out="${BDIR}/sh_${s}.o"
286-                eval "${CC} ${CPPFLAGS} -DSHELL -I\"\$sdir\" ${CFLAGS} -c \"\$sdir/${s}.c\" -o \"\$out\"" ||
287-                        die "compile failed: sh/${s}.c"
288-                sh_objs="${sh_objs} ${out}"
289-        done
290-
291-        combined="${BDIR}/sh_combined.o"
292-        ${LD:-ld} -r -o "${combined}" ${sh_objs} ||
293-                die "ld -r failed: sh"
294-
295-        printf 'main sh_main\n' > "${symf}"
296-        ${OBJCOPY} --redefine-syms="${symf}" "${combined}" ||
297-                die "objcopy failed: sh"
298-        ${OBJCOPY} --keep-global-symbol=sh_main "${combined}" ||
299-                die "objcopy localize failed: sh"
300-        rm -f "${symf}" ${sh_objs}
301-
302-        OBJS="${OBJS} ${combined}"
303-        printf 'int sh_main(int, char **);\n' >> "${DECL_F}"
304-        printf '\t{ "sh", sh_main, "posix" },\n' >> "${BDIR}/entries_posix.inc"
305-}
306-
307-process_dir() {
308-        dir="$1"
309-        catg="$2"
310-        extra="${3:-}"
311-        for src in "${dir}"/*.c; do
312-                [ -f "${src}" ] || continue
313-                name=${src##*/}
314-                name=${name%.c}
315-
316-                # bc.c is generated by compile_bc() (see below); it lands in
317-                # this same directory but must not be picked up here too, or
318-                # bc_main ends up defined twice and the final link fails
319-                [ "${catg}" = "posix" ] && [ "${name}" = "bc" ] && continue
320-
321-                u_catg=$(printf '%s' "${catg}" | tr '[:lower:]' '[:upper:]')
322-                u_name=$(printf '%s' "${name}" | tr '[:lower:].-' '[:upper:]__')
323-                var_name="BUILD_${u_catg}_${u_name}"
324-                eval "val=\${$var_name:-0}"
325-                [ "$val" = "1" ] || continue
326-
327-                fn=$(printf '%s' "${name}" | tr '.-' '__')
328-                compile_tool "${src}" "${name}" "${fn}" "${catg}" "${extra}"
329-                if [ "${name}" = "test" ]; then
330-                        alias_entry "[" "test" "${catg}"
331-                fi
332-                if [ "${name}" = "xinstall" ]; then
333-                        alias_entry "install" "xinstall" "${catg}"
334-                fi
335-        done
336-}
337-
338-process_dir "${ROOT}/cmd/posix" "posix"
339-[ "${BUILD_POSIX_AWK:-0}" = "1" ] && compile_awk
340-[ "${BUILD_POSIX_BC:-0}" = "1" ] && compile_bc
341-[ "${BUILD_POSIX_SH:-0}" = "1" ] && compile_sh
342-[ "${BUILD_POSIX_MAKE:-0}" = "1" ] && compile_make
343-process_dir "${ROOT}/cmd/linux" "linux"
344-process_dir "${ROOT}/cmd/net" "net"
345-process_dir "${ROOT}/cmd/pseudo" "pseudo"
346-process_dir "${ROOT}/cmd/xsi" "xsi"
347-process_dir "${ROOT}/cmd/extra" "extra"
348-[ "${BUILD_EXTRA_YAP:-0}" = "1" ] && compile_yap
349-
350-DISP="${BDIR}/aruu-box.c"
351-printf 'mkbox: generating dispatch\n'
352-
353-escaped_license=$(tr -d '\r' < "${ROOT}/LICENSE" | sed 's/\\/\\\\/g; s/"/\\"/g; s/^/\"/; s/$/\\n\"/')
354-
355-cat > "${DISP}" << DISPATCH_HEAD
356-/* this software is released into the public domain, or
357-   registered under the isc license, see ./license for details
358-   2026 xplshn anto@xplshn.com.ar */
359-
360-/* routes argv0 to the appropriate tool main */
361-#include "wexec.h"
362-#include "util.h"
363-
364-#include <stdio.h>
365-#include <stdlib.h>
366-#include <string.h>
367-#include <unistd.h>
368-
369-static const char license[] =
370-${escaped_license}
371-;
372-
373-extern char *argv0;
374-
375-DISPATCH_HEAD
376-
377-cat "${DECL_F}" >> "${DISP}"
378-
379-cat >> "${DISP}" << 'DISPATCH_STRUCT'
380-
381-struct Cmd {
382-        const char *name;
383-        int (*fn)(int, char **);
384-        const char *cat;
385-};
386-
387-static struct Cmd cmds[] = {
388-DISPATCH_STRUCT
389-
390-# sort each category alphabetically by tool name at build time;
391-# entries have the form: \t{ "name", fn_main, "cat" },
392-# splitting on " makes field 2 the tool name
393-for _c in posix linux net pseudo xsi extra; do
394-        sort -t'"' -k2,2 "${BDIR}/entries_${_c}.inc"
395-done >> "${DISP}"
396-unset _c
397-
398-cat >> "${DISP}" << 'DISPATCH_TAIL'
399-        { NULL, NULL, NULL }
400-};
401-
402-static void
403-usage(void)
404-{
405-        struct Cmd *c;
406-        const char *last;
407-        int col;
408-
409-        last = NULL;
410-        col = 0;
411-        fprintf(stderr, "usage: %s [-i path] [--license | -l] [command [args...]]\n", argv0);
412-        for (c = cmds; c->name; c++) {
413-                if (!last || strcmp(c->cat, last) != 0) {
414-                        if (last)
415-                                fprintf(stderr, "\n\n");
416-                        last = c->cat;
417-                        fprintf(stderr, "%s:\n", last);
418-                        col = 0;
419-                }
420-                if (col == 0) {
421-                        fprintf(stderr, "\t%s", c->name);
422-                        col = 8 + strlen(c->name);
423-                } else if (col + 2 + strlen(c->name) >= 78) {
424-                        fprintf(stderr, ",\n\t%s", c->name);
425-                        col = 8 + strlen(c->name);
426-                } else {
427-                        fprintf(stderr, ", %s", c->name);
428-                        col += 2 + strlen(c->name);
429-                }
430-        }
431-        if (last)
432-                fputc('\n', stderr);
433-        exit(1);
434-}
435-
436-static void
437-x_install(const char *path)
438-{
439-        struct Cmd *c;
440-        char buf[4096];
441-        int r;
442-
443-        for (c = cmds; c->name; c++) {
444-                r = snprintf(buf, sizeof buf, "%s/%s", path, c->name);
445-                if (r < 0 || r >= (int)sizeof buf)
446-                        eprintf("install: '%s/%s': path too long\n", path, c->name);
447-                remove(buf);
448-                if (symlink("aruu-box", buf) < 0)
449-                        eprintf("install: symlink '%s':", buf);
450-        }
451-}
452-
453-int
454-main(int argc, char *argv[])
455-{
456-        struct Cmd *c;
457-        char *s;
458-
459-        argv0 = argv[0];
460-
461-        for (c = cmds; c->name; c++)
462-                wexec_register(c->name, c->fn);
463-
464-        s = strrchr(argv[0], '/');
465-        if (s)
466-                s++;
467-        else
468-                s = argv[0];
469-
470-        if (strcmp(s, "aruu-box") == 0) {
471-                if (argc < 2)
472-                        usage();
473-                if (strcmp(argv[1], "--license") == 0 || strcmp(argv[1], "-l") == 0) {
474-                        printf("%s", license);
475-                        return 0;
476-                }
477-                argc--;
478-                argv++;
479-                if (strcmp(argv[0], "-i") == 0) {
480-                        if (argc < 2)
481-                                eprintf("usage: aruu-box -i path\n");
482-                        x_install(argv[1]);
483-                        return 0;
484-                }
485-                s = strrchr(argv[0], '/');
486-                if (s)
487-                        s++;
488-                else
489-                        s = argv[0];
490-        }
491-        for (c = cmds; c->name; c++) {
492-                if (strcmp(c->name, s) == 0) {
493-                        argv0 = (char *)c->name;
494-                        argv[0] = (char *)c->name;
495-                        return c->fn(argc, argv);
496-                }
497-        }
498-        enprintf(127, "%s: not found\n", s);
499-        return 1;
500-}
501-DISPATCH_TAIL
502-
503-printf 'mkbox: cc dispatch\n'
504-eval "${CC} ${CPPFLAGS} ${CFLAGS} -c \"\$DISP\" -o \"\${BDIR}/aruu-box.o\"" ||
505-        die "compile failed: dispatch"
506-OBJS="${OBJS} ${BDIR}/aruu-box.o"
507-
508-printf 'mkbox: ld aruu-box\n'
509-eval "${CC} ${LDFLAGS} -o \"\$BOX\" \$OBJS \"\$LIBREDLINE\" \"\$LIBUTIL\" \"\$LIBUTF\" \${LDLIBS}" ||
510-        die "link failed"
511-
512-printf 'mkbox: done => %s\n' "${BOX}"
+8, -8
 1@@ -10,7 +10,7 @@
 2 #include <string.h>
 3 #include <unistd.h>
 4 
 5-#if FEATURE_USE_LIBRESSL
 6+#if FEATURE_USE_LIBTLS
 7 #include <tls.h>
 8 
 9 struct TlsSocket {
10@@ -298,12 +298,12 @@ tls_connect(int fd, const char *host, int check_cert, int is_tls)
11 
12   if (!is_tls)
13     return s;
14-#if !FEATURE_USE_LIBRESSL && !FEATURE_USE_BEARSSL && !FEATURE_USE_OPENSSL
15+
16+  /* not every backend below uses these */
17   (void)host;
18   (void)check_cert;
19-#endif
20 
21-#if FEATURE_USE_LIBRESSL
22+#if FEATURE_USE_LIBTLS
23   {
24     struct tls_config *cfg;
25 
26@@ -369,7 +369,7 @@ tls_connect(int fd, const char *host, int check_cert, int is_tls)
27   }
28 #else
29   weprintf(
30-      "TLS not supported, compile with FEATURE_USE_LIBRESSL or "
31+      "TLS not supported, compile with FEATURE_USE_LIBTLS or "
32       "FEATURE_USE_BEARSSL\n"
33   );
34   free(s);
35@@ -390,7 +390,7 @@ tls_read(struct TlsSocket *s, void *buf, size_t len)
36       return r;
37     }
38   }
39-#if FEATURE_USE_LIBRESSL
40+#if FEATURE_USE_LIBTLS
41   for (;;) {
42     ssize_t r = tls_read(s->ctx, buf, len);
43     if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT)
44@@ -415,7 +415,7 @@ tls_write(struct TlsSocket *s, const void *buf, size_t len)
45       return r;
46     }
47   }
48-#if FEATURE_USE_LIBRESSL
49+#if FEATURE_USE_LIBTLS
50   for (;;) {
51     ssize_t r = tls_write(s->ctx, buf, len);
52     if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT)
53@@ -440,7 +440,7 @@ void
54 tls_close(struct TlsSocket *s, int close_fd)
55 {
56   if (s->is_tls) {
57-#if FEATURE_USE_LIBRESSL
58+#if FEATURE_USE_LIBTLS
59     tls_close(s->ctx);
60     tls_free(s->ctx);
61 #elif FEATURE_USE_BEARSSL
+2, -2
 1@@ -6,12 +6,12 @@
 2 
 3 struct TlsSocket;
 4 
 5-/* tls_connect takes an already connected socket fd and upgrades it to tls
 6+/* tlssock_connect takes an already connected socket fd and upgrades it to tls
 7    if check_cert is 0 certificate validation is bypassed
 8    returns a new tls socket or null on error */
 9 struct TlsSocket *tls_connect(int fd, const char *host, int check_cert, int is_tls);
10 
11-/* standard read and write wrapping either libressl bearssl or plain fallback */
12+/* standard read and write wrapping either libtls, bearssl or plain fallback */
13 ssize_t tls_read(struct TlsSocket *s, void *buf, size_t len);
14 ssize_t tls_write(struct TlsSocket *s, const void *buf, size_t len);
15