shithub: mc

Download patch

ref: a2ff9547b25b4c44d8e9095659bd282a2b4e160e
parent: cc99dfdb5dbb27ac3fed40c796a01a4269d307c2
author: Ori Bernstein <ori@eigenstate.org>
date: Sun Jul 23 19:28:34 EDT 2017

New parallel mbld.

--- /dev/null
+++ b/lib/thread/ncpu.myr
@@ -1,0 +1,10 @@
+use std
+use sys
+
+pkg thread =
+	const ncpu	: (-> int)
+;;
+
+const ncpu = {
+	-> 1
+}
--- a/mbld/bld.sub
+++ b/mbld/bld.sub
@@ -1,8 +1,8 @@
-# the mbld binary
 bin mbld =
 	build.myr
 	clean.myr
 	deps.myr
+	libs.myr
 	install.myr
 	main.myr
 	opts.myr
--- a/mbld/build.myr
+++ b/mbld/build.myr
@@ -1,532 +1,134 @@
 use std
 
 use "config"
-use "deps"
 use "opts"
-use "parse"
 use "types"
 use "util"
 
 pkg bld =
-	const buildall	: (b : build# -> bool)
-	const genall	: (b : build# -> bool)
-	const buildtarg	: (b : build#, target : byte[:] -> bool)
-	const buildbin	: (b : build#, bt : myrtarg#, addsrc : bool -> bool)
-	const buildlib	: (b : build#, lt : myrtarg# -> bool)
+	const buildtarg	: (b : build#, targ : byte[:] -> bool)
+	const clean	: (b : build# -> bool)
 ;;
 
-const buildall = {b
-	for tn : b.all
-		if std.hthas(b.built, tn)
-			continue
-		;;
-		std.htput(b.built, tn, true)
-		match gettarg(b.targs, tn)
-		| `Bin bt:
-			if !bt.istest
-				buildbin(b, bt, false)
-			;;
-		| `Lib lt:	buildlib(b, lt)
-		| `Gen gt:	genfiles(b, gt)
-		| `Data _:	/* nothing needed */
-		| `Man _:	/* nothing needed */
-		| `Cmd _:	/* these are for manual commands or tests */
-		;;
+const buildtarg = {b, targ
+	var g
+
+	g = b.deps
+	var outs = std.htgetv(g.targs, targ, [][:])
+	for o : outs
+		mark(b, o)
 	;;
-	setdir(b, "")
+	build(b, g)
 	-> true
 }
 
-const buildtarg = {b, targ
-	var depset
-
-	depset = std.mkht(std.strhash, std.streq)
-	addeps(b, targ, depset)
-	for tn : b.all
-		if std.hthas(b.built, tn) || !std.hthas(depset, tn)
+const clean = {b
+	for n : b.deps.nodes
+		if n.durable
 			continue
 		;;
-		if std.sleq(tn, targ)
-			break
-		;;
-		std.htput(b.built, tn, true)
-		match gettarg(b.targs, tn)
-		| `Bin bt:
-			if !bt.istest
-				buildbin(b, bt, false)
+		for g : n.gen
+			if std.remove(g)
+				mbldput("\tclean {}\n", g)
 			;;
-		| `Lib lt:	buildlib(b, lt)
-		| `Gen gt:	genfiles(b, gt)
-		| `Data _:	/* nothing needed */
-		| `Man _:	/* nothing needed */
-		| `Cmd _:	/* these are for manual commands or tests */
 		;;
 	;;
-	build(b, targ)
-	std.htfree(depset)
 	-> true
 }
 
-const addeps = {b, targ, depset
-	if std.hthas(depset, targ)
+const mark = {b, o
+	if o.want
 		-> void
 	;;
 
-	std.htput(depset, targ, true)
-	match gettarg(b.targs, targ)
-	| `Bin bt:
-		for (dir, lib, targname) : bt.libdeps
-			addeps(b, targname, depset)
-		;;
-	| `Lib lt:
-		for (dir, lib, targname) : lt.libdeps
-			addeps(b, targname, depset)
-		;;
-	| _:
+	o.want = true
+	for n : o.ndep
+		mark(b, n)
 	;;
 }
 
-const genall = {b
-	for tn : b.all
-		match gettarg(b.targs, tn)
-		| `Gen gt:	runin(b, gt.cmd, gt.dir)
-		| _:	/* skip */
+const build = {b, g
+	for n : g.leaves
+		if n.want
+			stale(b, n)
+			unblock(b, n)
 		;;
 	;;
-	/* genfiles will exit if the build fails; always return true */
-	-> true
-}
 
-const build = {b, targ
-	match std.htget(b.targs, targ)
-	| `std.Some (`Bin bt):	buildbin(b, bt, false)
-	| `std.Some (`Lib lt):	buildlib(b, lt)
-	| `std.Some (`Gen gt):	runin(b, gt.cmd, gt.dir)
-	| `std.Some (`Cmd ct):	runin(b, ct.cmd, ct.dir)
-	| `std.Some (`Data _):	/* nothing needed */
-	| `std.Some (`Man _):	/* nothing needed */
-	| `std.None:	std.fatal("invalid target {}\n", targ)
-	;;
-	-> true
-}
-
-const runin = {b, cmd, dir
-	setdir(b, dir)
-	run(cmd)
-}
-
-const buildbin = {b, targ, addsrc
-	var dg, src, path
-	var libpath, incs
-
-	setdir(b, targ.dir)
-	addincludes(b, targ)
-	path = std.pathcat(b.curdir, targ.name)
-	mbldput("{}...\n", path)
-	std.slfree(path)
-	dg = myrdeps(b, targ, false, addsrc)
-	if !std.hthas(dg.deps, targ.name)
-		std.fatal("no input files for {}\n", targ.name)
-	;;
-	if builddep(b, dg, targ.name, targ.incpath) || !freshlibs(targ, targ.name, dg.libs)
-		src = std.htkeys(dg.sources)
-
-		incs = std.sldup(targ.incpath)
-		if opt_instbase.len > 0 && !std.sleq(opt_instbase, "none")
-			libpath = std.pathcat(bld.opt_instbase, config.Libpath)
-			std.slpush(&incs, libpath)
+	while std.htcount(b.proc) != 0 || b.queue.len != 0
+		while std.htcount(b.proc) < opt_maxproc && b.queue.len > 0
+			launch(b, std.slpop(&b.queue))
 		;;
-		linkbin(dg, targ.name, src, targ.ldscript, targ.runtime, incs, targ.libdeps)
-		std.slfree(incs)
-		std.slfree(src)
+		wait(b)
 	;;
-	-> true
 }
 
-const buildlib = {b, targ
-	var archive, usefile
-	var u, l
-	var dg
-	var lib, src
+const launch = {b, n
+	var pid
 
-	setdir(b, targ.dir)
-	addincludes(b, targ)
-	lib = targ.name
-	mbldput("{}/lib{}.a...\n", b.curdir, lib)
-	archive = std.fmt("lib{}.a", lib)
-	usefile = std.fmt("lib{}.use", lib)
-	dg = myrdeps(b, targ, false, false)
-	if !std.hthas(dg.deps, lib)
-		std.fatal("no target declared for {}\n", lib)
-	;;
-	u = builddep(b, dg, usefile, targ.incpath)
-	l = builddep(b, dg, archive, targ.incpath)
-	if  u || l || !freshlibs(targ, usefile, dg.libs)
-		src = std.htkeys(dg.sources)
-		mergeuse(dg, lib, src, targ.incpath)
-		archivelib(dg, lib, src, targ.incpath)
-		std.slfree(src)
-	;;
-	std.slfree(archive)
-	-> true
-}
-
-const genfiles = {b, gt
-	setdir(b, gt.dir)
-	for out : gt.out
-		if !std.fexists(out) || !allfresh(gt.deps, out)
-			run(gt.cmd)
-			break
-		;;
-	;;
-}
-
-const addincludes = {b, targ
-	for (inc, lib, subtarg) : targ.libdeps
-		if !hasinc(targ.incpath, inc)
-			std.slput(&targ.incpath, 0, inc)
-		;;
-	;;
-}
-
-const hasinc = {path, t
-	for e : path
-		if std.sleq(e, t)
-			-> true
-		;;
-	;;
-	-> false
-}
-
-
-const builddep = {b, dg, out, incs
-	var stale
-
-	stale = false
-	/* short circuit walking the dep tree if we've already built this. */
-	if std.htgetv(dg.updated, out, false)
-		-> false
-	;;
-
-	match std.htget(dg.deps, out)
-	| `std.Some deps:
-		for d : deps
-			if std.sleq(out, d)
-				/*
-				if an input generates itself (eg, object files), we
-				shouldn't recurse here, because that would be infinite.
-
-				The generation rule will be invoked from the target that
-				consumes this file.
-				*/
-				continue
-			;;
-			if std.sleq(out, d) || builddep(b, dg, d, incs)
-				stale = true
-			;;
-			match std.htget(b.gensrc, d)
-			| `std.Some gt:	
-				if !std.fexists(d) || !allfresh(gt.deps, d)
-					run(gt.cmd)
-				;;
-			| `std.None:
-				if !std.fexists(d)
-					std.fatal("no input file {}\n", d)
-				;;
-			;;
-			if !isfresh(d, out)
-				stale = true
-			;;
-		;;
-	| `std.None:
-	;;
-
-	match std.htget(dg.input, out)
-	| `std.Some src:
-		if stale
-			compile(dg, src, incs)
-		;;
-		std.htput(dg.updated, out, true)
-	| `std.None:
-	;;
-	-> stale
-}
-
-const compile = {dg, src, incs
-	var o
-	var cmd
-
-	cmd = [][:]
-	if std.hassuffix(src, ".myr")
-		std.slpush(&cmd, opt_mc)
-		for inc : incs 
-			std.slpush(&cmd, "-I")
-			std.slpush(&cmd, inc)
-		;;
-		if opt_genasm
-			std.slpush(&cmd, "-S")
-		;;
-		std.slpush(&cmd, src)
-		run(cmd)
-		std.slfree(cmd)
-	elif std.hassuffix(src, ".s")
-		o = srcswapsuffix(src, config.Objsuffix)
-		for c : config.Ascmd
-			std.slpush(&cmd, c)
-		;;
-		std.slpush(&cmd,"-o")
-		std.slpush(&cmd, o)
-		std.slpush(&cmd, src)
-		run(cmd)
-		std.slfree(o)
-	elif std.hassuffix(src, ".glue.c")
-		o = srcswapsuffix(src, config.Objsuffix)
-		std.slpush(&cmd, "cc")
-		std.slpush(&cmd,"-c")
-		std.slpush(&cmd,"-o")
-		std.slpush(&cmd, o)
-		std.slpush(&cmd, src)
-		for flg : std.htgetv(dg.cflags, src, [][:])
-			std.slpush(&cmd, flg)
-		;;
-		run(cmd)
-	elif std.hassuffix(src, ".o") || std.hassuffix(src, ".6")
-		/* nothing to do with object files*/
+	if stale(b, n)
+		pid = run(n.cmd, "")
+		std.htput(b.proc, pid, n)
 	else
-		std.fatal("Unknown file type for {}\n", src)
+		unblock(b, n)
 	;;
 }
 
-const linkbin = {dg, bin, srcfiles, ldscript, rt, incs, extlibs
-	var cmd
+const wait = {b
+	var pp : std.pid
 
-	cmd = [][:]
-
-	/* ld -o bin */
-	for c : config.Linkcmd
-		std.slpush(&cmd, std.sldup(c))
+	if std.htcount(b.proc) == 0
+		-> void
 	;;
-	std.slpush(&cmd, std.sldup(bin))
-
-	/* [-T script] */
-	if ldscript.len > 0
-		std.slpush(&cmd, std.sldup("-T"))
-		std.slpush(&cmd, std.sldup(ldscript))
-	;;
-
-	if rt.len != 0
-		if !std.sleq(rt, "none")
-			std.slpush(&cmd, std.sldup(rt))
+	match std.waitany()
+	| (p, `std.Wfailure):	std.fatal("FAIL: {j= }\n", proclbl(b, p))
+	| (p, `std.Wsignalled):	std.fatal("CRASH: {j= }\n", proclbl(b, p))
+	| (p, `std.Waiterror):	std.fatal("WAT: {j= }\n", proclbl(b, p))
+	| (p, `std.Wsuccess):
+		pp = p
+		match std.htget(b.proc, p)
+		| `std.None:
+			std.fatal("followed home by stray pid {}\n", p)
+		| `std.Some n:
+			n.mtime = std.now()
+			std.htdel(b.proc, p)
+			unblock(b, n)
 		;;
-	else
-		std.slpush(&cmd, std.sldup(opt_runtime))
 	;;
-
-	/* input.o list.o... */
-	for f : srcfiles
-		std.slpush(&cmd, srcswapsuffix(f, config.Objsuffix))
-	;;
-
-	/* -L path -l lib... */
-	cmd = addlibs(cmd, dg.libs, incs)
-
-
-	/* add extra libs */
-	for l : dg.extlibs
-		std.slpush(&cmd, std.fmt("-l{}", l))
-	;;
-
-	/* special for OSX: it warns if we don't add this */
-	if std.sleq(opt_sys, "osx")
-		std.slpush(&cmd, std.sldup("-macosx_version_min"))
-		std.slpush(&cmd, std.sldup("10.6"))
-	elif std.sleq(opt_sys, "linux") && dg.dynamic
-		std.slpush(&cmd, std.sldup("-dynamic-linker"))
-		std.slpush(&cmd, std.sldup("/lib64/ld-linux-x86-64.so.2"))
-	;;
-
-	run(cmd)
-	strlistfree(cmd)
 }
 
-const archivelib = {dg, lib, files, incs
-	var cmd
-	var obj
-
-	cmd = [][:]
-	for c : config.Arcmd
-		std.slpush(&cmd, std.sldup(c))
+const proclbl = {b, pid
+	match std.htget(b.proc, pid)
+	| `std.Some n:	-> n.cmd
+	| `std.None:	-> std.sldup(["?unknown?"][:])
 	;;
-	std.slpush(&cmd, std.fmt("lib{}.a", lib))
-	for f : files
-		obj = srcswapsuffix(f, config.Objsuffix)
-		std.slpush(&cmd, obj)
-	;;
-	run(cmd)
-	strlistfree(cmd)
 }
 
-const mergeuse = {dg, lib, files, incs
-	var cmd
-
-	cmd = [][:]
-	std.slpush(&cmd, std.sldup(opt_muse))
-	std.slpush(&cmd, std.sldup("-o"))
-	std.slpush(&cmd, std.fmt("lib{}.use", lib))
-	std.slpush(&cmd, std.sldup("-p"))
-	std.slpush(&cmd, std.sldup(lib))
-	for f : files
-		if std.hassuffix(f, ".myr")
-			std.slpush(&cmd, srcswapsuffix(f, ".use"))
-		elif !std.hassuffix(f, ".s") && !std.hassuffix(f, ".glue.c")
-			std.fatal("unknown file type for {}\n", f)
+const unblock = {b, n
+	for g : n.ngen
+		std.assert(g.nblock != 0, "bogus unblock {} from {}\n", g.lbl, n.lbl)
+		g.nblock--
+		if g.nblock == 0 && g.want
+			std.slpush(&b.queue, g)
 		;;
 	;;
-	for l : dg.extlibs
-		std.slpush(&cmd, std.fmt("-l{}", l))
-	;;
-	run(cmd)
-	strlistfree(cmd)
-}
 
-const addlibs = {cmd, libgraph, incs
-	var looped : std.htab(byte[:], bool)#
-	var marked : std.htab(byte[:], bool)#
-	var libs
-	var head
-
-	/* -L incpath... */
-	if !config.Directlib
-		for inc : incs
-			std.slpush(&cmd, std.fmt("-L{}", inc))
-		;;
-	;;
-
-	libs = std.htkeys(libgraph)
-	looped = std.mkht(std.strhash, std.streq)
-	marked = std.mkht(std.strhash, std.streq)
-	head = cmd.len
-
-	for lib : libs
-		cmd = visit(cmd, head, libgraph, lib, looped, marked, incs)
-	;;
-
-	-> cmd
 }
 
-const visit = {cmd, head, g, lib, looped, marked, incs
-	if std.hthas(looped, lib)
-		std.fatal("cycle in library graph involving \"{}\"\n", lib)
-	elif std.hthas(marked, lib)
-		-> cmd
-	;;
+const stale = {b, n
+	var staletime
 
-	std.htput(looped, lib, true)
-	for dep : std.htgetv(g, lib, [][:])
-		cmd = visit(cmd, head, g, dep, looped, marked, incs)
+	staletime = 0
+	for d : n.ndep
+		staletime = std.max(staletime, d.mtime)
 	;;
-	std.htdel(looped, lib)
-	std.htput(marked, lib, true)
-	-> putlib(cmd, head, lib, incs)
-}
 
-const putlib = {cmd, head, lib, incs
-	var sep
-
-	if !config.Directlib
-		-> std.slput(&cmd, head, std.fmt("-l{}", lib))
-	else
-		match findlib(lib, incs)
-		| `std.None:
-			mbldput("in path: ")
-			sep = ""
-			for inc : incs
-				mbldput("\t{}{}\n", sep, inc)
-				sep = ", "
-			;;
-			std.fatal("could not find library lib{}.a.\n", lib)
-		| `std.Some p:
-			-> std.slput(&cmd, head, p)
+	n.mtime = std.now()
+	for g : n.gen
+		match std.fmtime(g)
+		| `std.Ok tm:	n.mtime = std.min(n.mtime, tm)
+		| `std.Err e:	n.mtime = 0
 		;;
 	;;
+	-> staletime > n.mtime
 }
-
-const findlib = {lib, incs
-	var buf : byte[512]
-	var sl, p
-
-	sl = std.bfmt(buf[:], "lib{}.a", lib)
-	for i : incs
-		p = std.pathcat(i, sl)
-		if std.fexists(p)
-			-> `std.Some p
-		;;
-		std.slfree(p)
-	;;
-
-	p = std.pathjoin([opt_instbase, config.Libpath, sl][:])
-	if std.fexists(p)
-		-> `std.Some p
-	;;
-	std.slfree(p)
-	-> `std.None
-}
-
-const freshlibs = {targ, output, libgraph
-	var libs
-
-	libs = std.htkeys(libgraph)
-	for l : libs
-		match findlib(l, targ.incpath)
-		| `std.Some lib:
-			if !isfresh(lib, output)
-				std.slfree(lib)
-				-> false
-			;;
-			std.slfree(lib)
-		| `std.None:
-			std.fput(1, "{}: could not find library lib{}.a\n", targ.name, l)
-			std.fput(1, "searched:\n")
-			for inc : targ.incpath
-				std.fput(1, "\t{}\n", inc)
-			;;
-			std.fput(1, "\t{}/{}\n", opt_instbase, config.Libpath)
-			std.exit(1)
-		;;
-	;;
-	std.slfree(libs)
-	-> true
-}
-
-const allfresh = {deps, out
-	for d : deps
-		if !isfresh(d, out)
-			-> false
-		;;
-	;;
-	-> true
-}
-
-const isfresh = {src, dst
-	var srcmt, dstmt
-
-	/*
-	OSX only has single second resolution on modification
-	times. Since most builds happen within one second of each
-	other, if we treat equal times as outdated, we do a lot of
-	spurious rebuilding.
-
-	So, we treat times where both secs and nsecs are equal as
-	up to date.
-	*/
-	match std.fmtime(src)
-	| `std.Ok mt:	srcmt = mt
-	| `std.Err e:	std.fatal("could not stat {}: {}\n", src, e)
-	;;
-	match std.fmtime(dst)
-	| `std.Ok mt:	dstmt = mt
-	| `std.Err e:	-> false
-	;;
-	-> srcmt <= dstmt
-}
-
--- a/mbld/clean.myr
+++ b/mbld/clean.myr
@@ -1,93 +1,8 @@
 use std
 
-use "config"
-use "deps"
 use "opts"
-use "parse"
 use "types"
 use "util"
 
 pkg bld =
-	const cleanall	: (b : build# -> bool)
-	const clean	: (b : build#, targ : byte[:] -> bool)
-	const cleanmyr	: (b : build#, mt : myrtarg# -> void)
 ;;
-
-const cleanall = {b
-	for tn : b.all
-		match gettarg(b.targs, tn)
-		| `Bin bt:
-			cleanup(b, bt, bt.inputs)
-		| `Lib lt:
-			cleanup(b, lt, lt.inputs)
-		| `Gen gt:
-			for f : gt.out
-				if !gt.durable && std.remove(f)
-					mbldput("\tclean {}\n", f)
-				;;
-			;;
-		| `Data _:	/* nothing to do */
-		| `Cmd _:	/* nothing to do */
-		| `Man _:	/* nothing to do */
-		;;
-	;;
-	-> true
-}
-
-const clean = {b, targ
-	for tn : b.all
-		match gettarg(b.targs, tn)
-		| `Bin bt:
-			if std.sleq(bt.name, targ)
-				cleanup(b, bt, bt.inputs)
-			;;
-		| `Lib lt:
-			if std.sleq(lt.name, targ)
-				cleanup(b, lt, lt.inputs)
-			;;
-		| `Gen gt:
-			for f : gt.out
-				if !gt.durable && std.remove(f)
-					mbldput("\tclean {}\n", f)
-				;;
-			;;
-		| `Data _:	/* nothing to do */
-		| `Cmd _:	/* nothing to do */
-		| `Man _:	/* nothing to do */
-		;;
-	;;
-	-> true
-}
-
-const cleanmyr = {b, targ
-	cleanup(b, targ, targ.inputs)
-}
-
-const cleanup = {b, targ, leaves
-	var mchammer_files /* cant touch this */
-	var keys, dg, test
-
-	/*
-	we want to automatically add 'clean' sources since otherwise,
-	mbld won't be able to clean code after changing a build file.
-	*/
-	setdir(b, targ.dir)
-	dg = myrdeps(b, targ, true, true)
-	mchammer_files = std.mkht(std.strhash, std.streq)
-	for l : leaves
-		std.htput(mchammer_files, l, true)
-	;;
-
-	keys = std.htkeys(dg.deps)
-	for k : keys
-		if !std.htgetv(mchammer_files, k, false) && std.remove(k)
-			mbldput("\tclean {}\n", k)
-		;;
-		test = std.pathcat("test", k)
-		if std.remove(test)
-			mbldput("\tclean {}\n", test)
-		;;
-		std.slfree(test)
-	;;
-}
-
--- a/mbld/deps.myr
+++ b/mbld/deps.myr
@@ -6,11 +6,19 @@
 use "opts"
 use "types"
 use "util"
+use "libs"
 
 pkg bld =
-	const myrdeps	: (b : build#, mt : myrtarg#, doclean : bool, addsrc : bool -> depgraph#)
+	const deps	: (b : build# -> void)
+	const testdeps	: (b : build# -> void)
+	const resolve	: (b : build# -> void)
 ;;
 
+type dep = union
+	`Xdep	byte[:]
+	`Ldep	byte[:]
+;;
+
 const Abiversion = 13
 
 var usepat	: regex.regex#
@@ -23,413 +31,568 @@
 	clibpat = std.try(regex.compile("/\\*\\s*LIBS:\\s*(.*)\\s*\\*/"))
 }
 
+const deps = {b
+	for (name, `Gen gt) : std.byhtkeyvals(b.targs)
+		cmddeps(b, name, gt)
+	;;
+	for name : b.all
+		match gettarg(b.targs, name)
+		| `Bin bt:	myrdeps(b, name, bt)
+		| `Lib lt:	myrdeps(b, name, lt)
+		| `Cmd ct:	cmddeps(b, name, ct)
+		| `Man mt:	mandeps(b, name, mt)
+		| `Data dt:	datdeps(b, name, dt)
+		| `Gen gt:	/* gen was dealt with earlier */
+		;;
+	;;
+}
 
-type dep = union
-	`Local	(byte[:], int)
-	`Lib	(byte[:], int)
-;;
+const testdeps = {b
+	for name : b.all
+		match gettarg(b.targs, name)
+		| `Bin bt:	addtests(b, name, bt)
+		| `Lib lt:	addtests(b, name, lt)
+		| _:		/* skip */
+		;;
+	;;
+}
 
-type depscan = struct
-	doclean	: bool
-	addsrc	: bool
-	tagsel	: std.htab(byte[:], byte[:])#
-	targ	: myrtarg#
-	incs	: byte[:][:]
-	depstk	: byte[:][:]
-;;
+const myrdeps = {b, name, mt
+	var p, o, u, n, to, tu
+	var libs, dynlibs
+	var cflags, ll
+	var g, a, deps
+	var gu, go
 
-const myrdeps = {b, mt, doclean, addsrc
-	var objs, uses, srcs
-	var cflags, libs 
-	var out, useout
-	var dg : depgraph#
-	var ds : depscan
-	var i
+	g = b.deps
+	if mt.islib
+		u = std.fmt("lib{}.use", mt.name)
+		o = std.fmt("lib{}.a", mt.name)
+		tu = std.pathcat(mt.dir, u)
+		to = std.pathcat(mt.dir, o)
 
-	dg = std.mk([
-		.deps = std.mkht(std.strhash, std.streq),
-		.libs = std.mkht(std.strhash, std.streq),
-		.input = std.mkht(std.strhash, std.streq),
-		.sources = std.mkht(std.strhash, std.streq),
-		.updated = std.mkht(std.strhash, std.streq),
-		.seen = std.mkht(std.strhash, std.streq),
-		.done = std.mkht(std.strhash, std.streq),
-		.cflags = std.mkht(std.strhash, std.streq),
-		.extlibs = [][:],
-		.dynamic = false,
-	])
+		gu = node(g, tu)
+		go = node(g, to)
+		go.instdir = config.Libpath
+		gu.instdir = config.Libpath
+		go.instmod = 0o644
+		gu.instmod = 0o644
+		generates(g, gu, tu)
+		generates(g, go, to)
 
-	/* direct dependencies of binary */
-	if mt.islib
-		out = std.fmt("lib{}.a", mt.name)
-		useout = std.sldup(mt.name)
+		a = std.htgetv(g.targs, "all", [][:])
+		std.slpush(&a, gu)
+		std.slpush(&a, go)
+		std.htput(g.targs, to, std.sldup([gu, go][:]))
+		std.htput(g.targs, "all", a)
+
+		std.slfree(o)
+		std.slfree(u)
 	else
-		out = std.sldup(mt.name)
-		useout = ""
-	;;
+		u = std.fmt("{}.use", mt.name)
+		tu = std.pathcat(mt.dir, u)
+		to = std.pathcat(mt.dir, mt.name)
 
-	ds = [
-		.doclean = doclean,
-		.addsrc = addsrc,
-		.incs = mt.incpath,
-		.targ = mt,
-		.depstk = [][:],
-	]
-	srcs = mt.inputs
-	objs = swapall(srcs, config.Objsuffix)
-	uses = swapall(srcs, ".use")
-	for i = 0; i < srcs.len; i++
-		std.htput(dg.input, objs[i], srcs[i])
-		std.htput(dg.sources, srcs[i], true)
-		pushdep(dg, srcs[i], objs[i])
-		if std.hassuffix(srcs[i], ".myr")
-			std.htput(dg.input, uses[i], srcs[i])
-			pushdep(dg, srcs[i], uses[i])
-		elif std.hassuffix(srcs[i], ".glue.c")
-			(cflags, libs) = scrapecflags(b, dg, srcs[i])
-			std.htput(dg.cflags, srcs[i], cflags)
-			std.sljoin(&dg.extlibs, libs)
-			dg.dynamic = true
+		go = node(g, to)
+		gu = node(g, tu)
+
+		generates(g, go, to)
+		std.htput(g.targs, to, std.sldup([go][:]))
+		if mt.istest
+			n = node(g, mt.name)
+			depends(g, n, to)
+			addnode(g, "test", n)
+
+			n.wdir = std.sldup(mt.dir)
+			std.slpush(&n.cmd, std.fmt("./{}", mt.name))
+		else
+			addnode(g, "all", go)
+			go.instdir = config.Binpath
+			go.instmod = 0o755
 		;;
+
+		std.slfree(u)
 	;;
 
-	for i = 0; i < srcs.len; i++
-		pushdep(dg, objs[i], out)
-		if !std.hassuffix(srcs[i], ".myr")
-			continue
+	libs = [][:]
+	dynlibs = [][:]
+	for f : mt.inputs
+		p = std.pathcat(mt.dir, f)
+		leaf(g, p)
+		if std.hassuffix(f, ".myr")
+			o = changesuffix(p, config.Objsuffix)
+			u = changesuffix(p, ".use")
+			depends(g, go, o)
+			depends(g, gu, u)
+
+			n = node(g, o)
+			generates(g, n, o)
+			generates(g, n, u)
+			depends(g, n, p)
+
+			deps = scrapedeps(b, mt, p)
+			for `Ldep d : deps
+				depends(g, n, d)
+			;;
+			for `Xdep d : deps
+				scrapelib(b, d, mt.incpath)
+				xdepends(b, g, n, d)
+				std.slpush(&libs, d)
+			;;
+			myrcmd(b, n, mt, p, false)
+			std.slfree(deps)
+		elif std.hassuffix(f, ".s")
+			o = changesuffix(p, config.Objsuffix)
+			depends(g, go, o)
+			n = node(g, o)
+			generates(g, n, o)
+			depends(g, n, p)
+			ascmd(b, n, mt, o, p)
+		elif std.hassuffix(f, ".glue.c")
+			(cflags, ll) = scrapecflags(b, mt, p)
+			std.put("cflags: {}, ll: {}\n", cflags, ll)
+			o = changesuffix(p, config.Objsuffix)
+			depends(g, go, o)
+			n = node(g, o)
+			generates(g, n, o)
+			depends(g, n, p)
+			ccmd(b, n, mt, o, p, cflags)
+			for l : ll
+				std.slpush(&dynlibs, l)
+			;;
+		elif std.hassuffix(f, config.Objsuffix)
+			/* handled by leaf */
+		else
+			std.fatal("don't know how to build {}/{}\n", mt.dir, f)
 		;;
-		if mt.islib
-			pushdep(dg, uses[i], useout)
-		;;
-		srcdeps(b, &ds, dg, srcs[i], objs[i], uses[i])
 	;;
 
-	dumpgraph(dg)
-	-> dg
+	if mt.islib
+		arcmd(b, go, mt, to)
+		musecmd(b, gu, mt, tu)
+		builtlib(b, mt, libs, dynlibs)
+	else
+		linkcmd(b, go, mt, to, libs, dynlibs, false)
+		std.slfree(libs)
+	;;
 }
 
-const swapall = {srcs, suff
-	var sl
+const cmddeps = {b, name, ct
+	var n, a, p, gen, pid
 
-	sl = [][:]
-	for s : srcs
-		std.slpush(&sl, srcswapsuffix(s, suff))
+	n = node(b.deps, std.strjoin(ct.cmd, " "))
+	n.wdir = std.sldup(ct.dir)
+	n.durable = ct.durable
+
+	for c : ct.cmd
+		std.slpush(&n.cmd, std.sldup(c))
 	;;
-	-> sl
-}
 
-const dumpgraph = {dg
-	var keys
+	if ct.deps.len == 0
+		std.slpush(&b.deps.leaves, n)
+	else
+		for d : ct.deps
+			leaf(b.deps, d)
+			depends(b.deps, n, d)
+		;;
+	;;
 
-	if !opt_debug
-		-> void
+	gen = false
+	for g : ct.gen
+		p = std.pathcat(ct.dir, g)
+		gen = gen || !std.fexists(p)
+		generates(b.deps, n, p)
 	;;
-	keys = std.htkeys(dg.deps)
-	mbldput("digraph dg {{\n")
-	for k : keys
-		for v : std.htgetv(dg.deps, k, ["WTFUNKNOWN!"][:])
-			mbldput("\t\"{}\" -> \"{}\";\n", k, v)
+
+	if ct.istest
+		a = std.htgetv(b.deps.targs, "test", [][:])
+		std.slpush(&a, n)
+		std.htput(b.deps.targs, "test", a)
+	elif gen 
+		pid = run(ct.cmd, ct.dir)
+		match std.wait(pid)
+		| `std.Wfailure:	std.fatal("FAIL: {j= }\n", ct.cmd)
+		| `std.Wsignalled:	std.fatal("CRASH: {j= }\n", ct.cmd)
+		| `std.Waiterror:	std.fatal("WAT: {j= }\n", ct.cmd)
+		| `std.Wsuccess:	/* ok */
 		;;
 	;;
-	mbldput("}\n")
 }
 
-const srcdeps = {b, ds, g, path, obj, usefile
-	var deps
+const mandeps = {b, name, mt
+	var p, r, n
 
-	if std.hthas(g.done, path)
-		-> void
-	;;
-
-	std.slpush(&ds.depstk, path)
-	if std.htgetv(g.seen, path, false)
-		std.fput(1, "dependency loop involving {}:\n", path)
-		for d : ds.depstk
-			std.fput(1, "\t{}\n", d)
+	for pg : mt.pages
+		p = std.pathcat(mt.dir, pg)
+		n = leaf(b.deps, p)
+		match std.strfind(pg, ".")
+		| `std.None:	std.fatal("manpage {} missing section\n", pg)
+		| `std.Some i:	r = std.pathcat(config.Manpath, pg[i + 1:])
 		;;
-		std.exit(1)
+		n.instdir = r
+		n.instmod = 0o644
+		addnode(b.deps, "all", n)
 	;;
-	deps = getdeps(b, ds, path, std.dirname(path))
-	std.htput(g.seen, path, true)
-	for d : deps
-		match d
-		| `Lib (lib, lnum):
-			/*
-			If we're cleaning, we don't care about libraries; at best, this does nothing. At
-			worst, this will cause failure if the library is a local library that gets cleand.
-			*/
-			if !ds.doclean
-				scrapelibs(g, lib, ds.incs)
-			;;
-		| `Local (l, lnum):
-			if !std.hassuffix(l, ".use")
-				std.sljoin(&l, ".use")
-			;;
-			if obj.len != 0
-				pushdep(g, l, obj)
-			;;
-			if usefile.len != 0
-				pushdep(g, l, usefile)
-			;;
-			addusedep(b, ds, g, path, l, lnum)
-		;;
-	;;
-	std.slgrow(&ds.depstk, ds.depstk.len - 1)
-	std.htput(g.seen, path, false)
-	std.htput(g.done, path, true)
 }
 
-const addusedep = {b, ds, g, f, usefile, line
-	var src
+const datdeps = {b, name, dt
+	var p, n
 
-	if std.hthas(g.done, usefile)
-		if opt_debug
-			mbldput("already loaded deps for {}\n", usefile)
+	for db : dt.blobs
+		p = std.pathcat(dt.dir, db)
+		n = leaf(b.deps, p)
+		if dt.path.len == 0
+			n.instdir = config.Sharepath
+		else
+			n.instdir = dt.path
 		;;
-		-> void
+		n.instmod = 0o644
+		addnode(b.deps, "all", n)
 	;;
-	match std.htget(g.input, usefile)
-	| `std.Some path:
-		src = std.sldup(path)
-	| `std.None:
-		src = swapsuffix(usefile, ".use", ".myr")
-		if ds.addsrc
-			std.htput(g.sources, src, true)
-		elif !std.hthas(g.input, usefile)
-			std.fatal("{}:{}: source file {} not listed in bldfile\n", f, line, src)
-		;;
+}
+
+const addtests = {b, name, mt
+	for f : mt.inputs
+		addtest(b, mt, f)
 	;;
-	pushdep(g, src, usefile)
-	std.htput(g.input, usefile, src)
-	srcdeps(b, ds, g, src, "", usefile)
-	std.htput(g.done, usefile, true)
 }
 
-const scrapecflags = {b, ds, path
-	var cflags, libs, lnum
-	var f
+const addtest = {b, mt, f
+	var libs, deps
+	var sp, tp, op
+	var s, t, o
+	var g, n
+	var testinc
 
-	lnum = 0
-	cflags = [][:]
+	/*
+	change of suffix is to support testing assembly,
+	C glue, and foo+sys.myr forms.
+	*/
+	g = b.deps
+	s = changesuffix(f, ".myr")
+	sp = std.pathjoin([mt.dir, "test", s][:])
+	std.slfree(s)
+	if !std.fexists(sp)
+		std.slfree(sp)
+		-> void
+	;;
+
 	libs = [][:]
-	f = opensrc(b, path)
-	while true
-		lnum++
-		match bio.readln(f)
-		| `bio.Err e:	std.fatal("unable to read {}: {}\n", path, e)
-		| `bio.Eof:	break
-		| `bio.Ok ln:
-			(cflags, libs) = getcflags(ln, cflags, libs)
-			std.slfree(ln)
-		;;
+	leaf(g, sp)
+
+	t = changesuffix(f, "")
+	tp = std.pathjoin([mt.dir, "test", t][:])
+	std.slfree(t)
+
+	o = changesuffix(f, config.Objsuffix)
+	op = std.pathjoin([mt.dir, "test", o][:])
+	std.slfree(o)
+
+	n = node(g, sp)
+	generates(g, n, op)
+	depends(g, n, sp)
+	testinc = [][:]
+	std.slpush(&testinc, mt.dir)
+	std.sljoin(&testinc, mt.incpath)
+	deps = scrapedeps(b, mt, sp)
+	for `Ldep d : deps
+		depends(g, n, d)
 	;;
-	bio.close(f)
-	-> (cflags, libs)
+	for `Xdep d : deps
+		scrapelib(b, d, mt.incpath)
+		xdepends(b, g, n, d)
+		std.slpush(&libs, d)
+	;;
+	myrcmd(b, n, mt, sp, true)
+	std.slfree(mt.incpath)
+
+	n = node(g, tp)
+	generates(g, n, tp)
+	depends(g, n, op)
+	linkcmd(b, n, mt, tp, libs, [][:], true)
+	std.slfree(libs)
+
+	n = node(g, tp)
+	depends(g, n, tp)
+	n.wdir = std.sldup(std.dirname(std.dirname(tp)))
+	std.slpush(&n.cmd, std.fmt("./test/{}", std.basename(tp)))
+
+	addnode(g, "test", n)
 }
 
-const getcflags = {ln, cflags, libs
-	var flags
+const resolve = {b
+	var visited, looped, stk
+	var g
 
-	match regex.exec(cflagpat, ln)
-	| `std.None:
-	| `std.Some m:
-		flags = std.strtok(m[1])
-		for fl : flags
-			std.slpush(&cflags, std.sldup(fl))
+	g = b.deps
+	for n : g.nodes
+		for e : n.dep
+			edge(g, n, e)
 		;;
-		std.slfree(flags)
-		regex.matchfree(m)
 	;;
 
-	match regex.exec(clibpat, ln)
+	stk = [][:]
+	visited = std.mkht(std.ptrhash, std.ptreq)
+	looped = std.mkht(std.ptrhash, std.ptreq)
+	for n : g.nodes
+		checkloop(g, n, visited, looped, &stk)
+	;;
+	std.htfree(visited)
+	std.htfree(looped)
+}
+
+const edge = {g, n, e
+	match std.htget(g.gen, e)
 	| `std.None:
-	| `std.Some m:
-		flags = std.strtok(m[1])
-		for fl : flags
-			std.slpush(&libs, std.sldup(fl))
-		;;
-		std.slfree(flags)
-		regex.matchfree(m)
+		std.fatal("nothing satisfies {} for {}\n", e, n.lbl)
+	| `std.Some d:	
+		std.slpush(&n.ndep, d)
+		std.slpush(&d.ngen, n)
+		n.nblock++
 	;;
-	-> (cflags, libs)
 }
 
-const getdeps = {b, ds, path, dir
-	var deps, lnum
-	var f
-
-	lnum = 0
-	deps = [][:]
-	f = opensrc(b, path)
-	while true
-		lnum++
-		match bio.readln(f)
-		| `bio.Err e:	std.fatal("unable to read {}: {}\n", path, e)
-		| `bio.Eof:	break
-		| `bio.Ok ln:
-			deps = depname(deps, ln, lnum, dir)
-			std.slfree(ln)
-		;;
+const checkloop = {g, n, visited, looped, stk
+	if std.hthas(looped, n)
+		std.slpush(stk, n.lbl)
+		std.fatal("dependency loop: {j= -> }\n", stk#)
 	;;
-	bio.close(f)
-	-> deps
+	if std.hthas(visited, n)
+		-> void
+	;;
+	std.slpush(stk, n.lbl)
+	std.htput(visited, n, void)
+	std.htput(looped, n, void)
+	for d : n.ndep
+		checkloop(g, d, visited, looped, stk)
+	;;
+	std.slpop(stk)
+	std.htdel(looped, n)
 }
 
-const opensrc = {b, path
-	if !std.fexists(path)
-		match std.htget(b.gensrc, path)
-		| `std.Some gt:	run(gt.cmd)
-		| `std.None:	std.fatal("no input file {}\n", path)
-		;;
+const musecmd = {b, n, mt, mu
+	std.slpush(&n.cmd, std.sldup(opt_muse))
+	std.slpush(&n.cmd, std.sldup("-o"))
+	std.slpush(&n.cmd, std.sldup(mu))
+	std.slpush(&n.cmd, std.sldup("-p"))
+	std.slpush(&n.cmd, std.sldup(mt.name))
+	for u : n.dep
+		std.slpush(&n.cmd, std.sldup(u))
 	;;
+}
 
-	match bio.open(path, bio.Rd)
-	| `std.Err m:	std.fatal("could not open {}: {}\n", path, m)
-	| `std.Ok f:	-> f
+const arcmd = {b, n, mt, ar
+	for c : config.Arcmd
+		std.slpush(&n.cmd, std.sldup(c))
 	;;
+	std.slpush(&n.cmd, std.sldup(ar))
+	for obj : n.dep
+		std.slpush(&n.cmd, std.sldup(obj))
+	;;
 }
 
-const depname = {deps, ln, lnum, dir
-	var p
+const linkcmd = {b, n, mt, bin, libs, dynlibs, istest
+	for c : config.Linkcmd
+		std.slpush(&n.cmd, std.sldup(c))
+	;;
+	std.slpush(&n.cmd, std.sldup(bin))
+	if mt.ldscript.len > 0
+		std.slpush(&n.cmd, std.sldup("-T"))
+		std.slpush(&n.cmd, std.sldup(mt.ldscript))
+	;;
 
-	/*
-	the regex pattern does some contortions to either grab
-	an unquoted path and put it into uses[4], or a quoted
-	path, and put it (minus the quotes) into uses[2]
-	*/
-	match regex.exec(usepat, ln)
-	| `std.Some uses:
-		if uses[2].len > 0
-			std.slpush(&deps, `Lib (std.sldup(uses[2]), lnum))
-		else
-			p = std.pathcat(dir, std.sldup(uses[3]))
-			std.slpush(&deps, `Local (p, lnum))
-		;;
-		regex.matchfree(uses)
-	| `std.None:
-		/* nothing to do */
+	if mt.runtime.len == 0 || std.sleq(mt.runtime, "none")
+		std.slpush(&n.cmd, std.sldup(opt_runtime))
+	else
+		std.slpush(&n.cmd, std.sldup(mt.runtime))
 	;;
-	-> deps
-}
 
-const scrapelibs = {dg, lib, incs
-	var deps, d
-	var f
-	var done
+	for o : n.dep
+		std.slpush(&n.cmd, std.sldup(o))
+	;;
 
-	if std.hthas(dg.libs, lib)
-		-> void
+	addlibs(b, &n.cmd, libs, mt.incpath)
+	for l : dynlibs
+		std.slpush(&n.cmd, std.fmt("-l{}", l))
 	;;
 
-	f = openlib(lib, incs)
-	match bio.getc(f)
-	| `bio.Ok 'U': /* nothing */
-	| `bio.Ok _:	std.fatal("library {}: corrupt or invalid usefile\n", lib)
-	| `bio.Err e:	std.fatal("library {}: could not read usefile: {}\n", lib, e)
-	| `bio.Eof:	std.fatal("library {}: could not read usefile\n", lib)
+	/* OSX warns if we don't add a version */
+	if std.sleq(opt_sys, "osx")
+		std.slpush(&n.cmd, std.sldup("-macosx_version_min"))
+		std.slpush(&n.cmd, std.sldup("10.6"))
+	elif std.sleq(opt_sys, "linux") && dynlibs.len != 0
+		std.slpush(&n.cmd, std.sldup("-dynamic-linker"))
+		std.slpush(&n.cmd, std.sldup("/lib64/ld-linux-x86-64.so.2"))
 	;;
-	match bio.getbe32(f)
-	| `bio.Ok Abiversion:	/* nothing: version matches. */
-	| `bio.Ok v:	
-		if v < Abiversion
-			std.fput(1, "library {}: warning: old abi version {}\n", lib, v)
-		else
-			std.fput(1, "library {}: usefile version {} unknown\n", lib, v)
+
+}
+
+const myrcmd = {b, n, mt, src, istest
+	std.slpush(&n.cmd, opt_mc)
+	for inc : mt.incpath[:mt.incpath.len - 1]
+		pushinc(&n.cmd, "-I", inc)
+	;;
+	if istest
+		for (dir, _, _) : mt.tstdeps
+			pushinc(&n.cmd, "-I", dir)
 		;;
-	| `bio.Err e:	std.fatal("library {}: error reading usefile: {}\n", lib, e)
-	| `bio.Eof:	std.fatal("library {}: corrupt or truncated usefile\n", lib)
+		pushinc(&n.cmd, "-I", mt.dir)
 	;;
-	std.slfree(rdstr(f))
+	for (dir, _, _) : mt.libdeps
+		pushinc(&n.cmd, "-I", dir)
+	;;
+	if opt_genasm
+		std.slpush(&n.cmd, "-S")
+	;;
+	std.slpush(&n.cmd, src)
+}
 
-	done = false
-	deps = [][:]
-	while !done
-		match bio.getc(f)
-		| `bio.Ok 'L':
-			d = rdstr(f)
-			std.slpush(&deps, d)
-		| `bio.Ok 'X':
-			d = rdstr(f)
-			std.slpush(&dg.extlibs, d)
-                        dg.dynamic = true
-		| `bio.Ok _:	done = true
-		| `bio.Eof:	done = true
-		| `bio.Err e:	std.fatal("io error reading {}: {}", lib, e)
-		;;
+const ascmd = {b, n, mt, out, src
+	for c : config.Ascmd
+		std.slpush(&n.cmd, c)
 	;;
-	bio.close(f)
-	std.htput(dg.libs, lib, deps)
-	for dep : deps
-		scrapelibs(dg, dep, incs)
+	std.slpush(&n.cmd,"-o")
+	std.slpush(&n.cmd, out)
+	std.slpush(&n.cmd, src)
+}
+
+const ccmd = {b, n, mt, out, src, cflags
+	std.slpush(&n.cmd, "cc")
+	std.slpush(&n.cmd,"-c")
+	std.slpush(&n.cmd,"-o")
+	std.slpush(&n.cmd, out)
+	std.slpush(&n.cmd, src)
+	for flg : cflags
+		std.slpush(&n.cmd, flg)
 	;;
 }
 
-const openlib = {lib, incs
-	var path, libname
+const scrapedeps = {b : build#, mt, path
+	var p, f, l
 
-	for p : incs
-		libname = std.fmt("lib{}.use", lib)
-		path = std.pathjoin([p, libname][:])
-		std.slfree(libname)
-		if std.fisreg(path)
-			goto found
-		;;
-		std.slfree(path)
+	match bio.open(path, bio.Rd)
+	| `std.Ok fd:	f = fd
+	| `std.Err e:	std.fatal("error opening {}: {}\n", path, e)
+	;;
 
-		path = std.pathjoin([p, lib][:])
-		if std.fisreg(path)
-			goto found
+	l = [][:]
+	for ln : bio.byline(f)
+		match regex.exec(usepat, ln)
+		| `std.None:
+		| `std.Some uses:
+			if uses[2].len > 0
+				/* external library */
+				p = std.sldup(uses[2])
+				std.slpush(&l, `Xdep p)
+			else
+				/* internal library */
+				p = std.pathcat(mt.dir, uses[3])
+				std.sljoin(&p, ".use")
+				std.slpush(&l, `Ldep p)
+			;;
+			regex.matchfree(uses)
 		;;
-		std.slfree(path)
 	;;
+	bio.close(f)
+	-> l
+}
 
-	libname = std.fmt("lib{}.use", lib)
-	path = std.pathjoin([opt_instbase, config.Libpath, libname][:])
-	if !std.fisreg(path)
-		std.slfree(path)
-		path = std.pathjoin([opt_instbase, config.Libpath, lib][:])
+const scrapecflags = {b, mt, path
+	var f, fl, cflags, libs
+
+	match bio.open(path, bio.Rd)
+	| `std.Ok fd:	f = fd
+	| `std.Err e:	std.fatal("error opening {}: {}\n", path, e)
 	;;
 
-	std.slfree(libname)
-:found
-	match  bio.open(path, bio.Rd)
-	| `std.Ok file:
-		std.slfree(path)
-		-> file
-	| `std.Err m:
+	cflags = [][:]
+	libs = [][:]
+	for ln : bio.byline(f)
+		match regex.exec(cflagpat, ln)
+		| `std.None:	/* skip */
+		| `std.Some m:
+			fl = std.strtok(m[1])
+			for s : fl
+				std.slpush(&cflags, std.sldup(s))
+			;;
+			std.slfree(fl)
+		;;
+		match regex.exec(clibpat, ln)
+		| `std.None:	/* skip */
+		| `std.Some m:
+			fl = std.strtok(m[1])
+			for s : fl
+				std.slpush(&libs, std.sldup(s))
+			;;
+			std.slfree(fl)
+		;;
 	;;
-	std.fput(std.Err, "could not find library {}\n", lib)
-	std.fput(std.Err, "search path is:\n")
-	for p : incs
-		std.fput(std.Err, "\t{}\n", p)
-	;;
-	std.fput(std.Err, "\t{}\n", config.Libpath)
-	std.exit(1)
+	-> (cflags, libs)
 }
 
-/* pushes a dep into the dependency list */
-const pushdep = {dg, src, dst
-	var sl
+const generates = {g, n, dep
+	std.slpush(&n.gen, dep)
+	std.htput(g.gen, dep, n)
+}
 
-	if opt_debug
-		mbldput("{} <= {}\n", dst, src)
+const depends = {g, n, dep
+	std.slpush(&n.dep, dep)
+}
+
+const xdepends = {b, g, n, dep
+	match std.htget(b.libs, dep)
+	| `std.Some ldep:
+		if ldep.genuse.len != 0
+			depends(g, n, ldep.genuse)
+			depends(g, n, ldep.genar)
+		;;
+		std.slpush(&n.xdep, dep)
+	| `std.None:
+		std.fatal("unknown xdep {} (known: {})\n", dep, std.htkeys(b.libs))
 	;;
-	sl = std.htgetv(dg.deps, dst, [][:])
-	std.slpush(&sl, src)
-	std.htput(dg.deps, dst, sl)
 }
 
-const rdstr = {f
-	var len
-	var sl
+const leaf = {g, f
+	var nod
 
-	match bio.getbe32(f)
-	| `bio.Ok l:
-		len = l
-		sl = std.slalloc(len)
-	| `bio.Eof:	std.fatal("end of file while reading string")
-	| `bio.Err e:	std.fatal("error while reading string: {}", e)
+	match std.htget(g.gen, f)
+	| `std.Some n:
+		-> n
+	| `std.None:	
+		nod = node(g, f)
+		nod.durable = true
+		generates(g, nod, f)
+		std.slpush(&g.leaves, nod)
+		-> nod
 	;;
-	bio.read(f, sl)
-	-> sl
+}
+
+const addnode = {g, targ, n
+	var nl
+
+	nl = std.htgetv(g.targs, targ, [][:])
+	std.slpush(&nl, n)
+	std.htput(g.targs, targ, nl)
+}
+
+const pushinc = {lst, pfx, dir
+	std.slpush(lst, std.sldup(pfx))
+	std.slpush(lst, std.pathnorm(dir))
+}
+
+const node = {g, lbl
+	var nod
+
+	nod = std.mk([
+		.lbl=lbl,
+		.cmd=[][:],
+		.gen=[][:],
+		.dep=[][:],
+		.xdep=[][:],
+
+		.nblock=0,
+		.mtime=0,
+	])
+	std.slpush(&g.nodes, nod)
+	-> nod
 }
--- a/mbld/install.myr
+++ b/mbld/install.myr
@@ -1,9 +1,7 @@
 use std
 
 use "config"
-use "deps"
 use "opts"
-use "parse"
 use "types"
 use "util"
 use "build"
@@ -14,7 +12,7 @@
 ;;
 
 const install = {b
-	buildall(b)
+	buildtarg(b, "all")
 	-> movetargs(b, false)
 }
 
@@ -23,54 +21,28 @@
 }
 
 const movetargs = {b, rm
-	var libarchive, libuse
-	var pfx
+	var inst
 
-	for tn : b.all
-		match gettarg(b.targs, tn)
-		| `Bin bt:
-			if bt.install && !bt.istest
-				movefile(b, rm, bt.dir, bt.name, config.Binpath, 0o755)
+	inst = std.htgetv(b.deps.targs, "all", [][:])
+	for n : inst
+		if n.instdir.len == 0
+			continue
+		;;
+		for g : n.gen
+			if !movefile(b, rm, g, n.instdir, n.instmod)
+				-> false
 			;;
-		| `Lib lt:
-			if lt.install && !lt.istest
-				libuse = std.fmt("lib{}.use", lt.name)
-				movefile(b, rm, lt.dir, libuse, config.Libpath, 0o644)
-				libarchive = std.fmt("lib{}.a", lt.name)
-				movefile(b, rm, lt.dir, libarchive, config.Libpath, 0o644)
-				std.slfree(libarchive)
-				std.slfree(libuse)
-			;;
-		| `Data dt:
-			for blob : dt.blobs
-				if dt.path.len == 0
-					pfx = std.pathcat(config.Sharepath, dt.name)
-					movefile(b, rm, dt.dir, blob, pfx, 0o644)
-					std.slfree(pfx)
-				else
-					movefile(b, rm, dt.dir, blob, dt.path, 0o644)
-				;;
-			;;
-		| `Gen gt:
-			/* nothing to do */
-		| `Cmd ct:
-			/* nothing to do */
-		| `Man mt:
-			/* FIXME: figure out man section by number */
-			for m : mt.pages
-				moveman(b, rm, mt.dir, m)
-			;;
 		;;
 	;;
 	-> true
+
 }
 
-const movefile = {b, rm, dir, file, prefix, perm
-	var path
+const movefile = {b, rm, file, prefix, perm
+	var path, ok
 
-	setdir(b, dir)
-	makepath(prefix)
-	path = std.pathjoin([opt_destdir, opt_instbase, prefix, file][:])
+	ok = true
+	path = std.pathjoin([opt_destdir, opt_instbase, prefix, std.basename(file)][:])
 	if rm
 		mbldput("\trm {}\n", path)
 		if !std.remove(path)
@@ -78,42 +50,18 @@
 		;;
 	else
 		std.remove(path)
+		std.mkpath(std.dirname(path))
 		match std.slurp(file)
 		| `std.Err m:	std.fatal("could not open {} for reading\n", file)
 		| `std.Ok buf:
-			if std.blat(path, buf, perm)
-				mbldput("\t{} => {}\n", file, path)
-			else
+			mbldput("\t{} => {}\n", file, path)
+			if !std.blat(path, buf, perm)
 				mbldput("could not write {}\n", file)
+				ok = false
 			;;
 		;;
 	;;
 	std.slfree(path)
+	-> ok
 }
 
-const moveman = {b, rm, dir, man
-	var sect, manrel
-
-	match std.strrfind(man, ".")
-	| `std.None:
-		std.fatal("manpage {} has no section\n", man)
-	| `std.Some s:
-		sect = s + 1
-		if s + 1 == man.len
-			std.fatal("manpage {} missing suffix\n", man)
-		;;
-	;;
-
-	manrel = std.fmt("{}{}", config.Manpath, man[sect:])
-	makepath(manrel)
-	movefile(b, rm, dir, man, manrel, 0o644)
-	std.slfree(manrel)
-}
-
-const makepath = {prefix
-	var p
-	
-	p = std.pathjoin([opt_destdir, opt_instbase, prefix][:])
-	std.mkpath(p)
-	std.slfree(p)
-}
--- /dev/null
+++ b/mbld/libs.myr
@@ -1,0 +1,187 @@
+use std
+use bio
+
+use "config"
+use "opts"
+use "types"
+
+pkg bld =
+	const addlibs	: (b : build#, \
+		sl : byte[:][:]#, \
+		libs : byte[:][:], \
+		incs : byte[:][:] -> void)
+
+	const builtlib	: (b : build#, \
+		lt : myrtarg#, \
+		dep : byte[:][:], \
+		dyndep : byte[:][:] -> void)
+
+	const scrapelib : (b : build#, libs : byte[:], incs : byte[:][:] -> void)
+;;
+
+const Abiversion = 13
+
+const builtlib = {b, mt, dep, dyndep
+	var ldep, l, u
+
+	l = std.fmt("lib{}.a", mt.name)
+	u = std.fmt("lib{}.use", mt.name)
+	ldep = std.mk([
+		.name=mt.name,
+		.dir=mt.dir,
+		.dep=dep,
+		.dyndep=dyndep,
+		.genuse=std.pathjoin([b.objdir, mt.dir, u][:]),
+		.genar=std.pathjoin([b.objdir, mt.dir, l][:]),
+	])
+	std.slfree(l)
+	std.slfree(u)
+	for d : dep
+		scrapelib(b, d, mt.incpath)
+	;;
+	std.htput(b.libs, mt.name, ldep)
+}
+
+const scrapelib = {b, lib, incs
+	var dep, dyndep, ldep
+	var f, dir
+
+	if std.hthas(b.libs, lib)
+		-> void
+	;;
+
+	(f, dir) = openlib(lib, incs)
+	match bio.getc(f)
+	| `bio.Ok 'U':	/* ok */
+	| `bio.Ok _:	std.fput(1, "{}: not a usefile\n", lib)
+	| `bio.Err e:	std.fatal("{}: error reading: {}\n", lib, e)
+	| `bio.Eof:	std.fatal("{}: truncated\n", lib)
+	;;
+	match bio.getbe32(f)
+	| `bio.Ok Abiversion:	/* nothing: version matches. */
+	| `bio.Ok v:	std.fput(1, "{}: mismatched abi {}\n", lib, v)
+	| `bio.Err e:	std.fatal("{}: error reading: {}\n", lib, e)
+	| `bio.Eof:	std.fatal("{}: truncated\n", lib)
+	;;
+	std.slfree(rdstr(f))
+
+	dep = [][:]
+	dyndep = [][:]
+	while true
+		match bio.getc(f)
+		| `bio.Ok 'L':	std.slpush(&dep, rdstr(f))
+		| `bio.Ok 'X':	std.slpush(&dyndep, rdstr(f))
+		| `bio.Err e:	std.fatal("{}: error reading {}", lib, e)
+		| _:		break;
+		;;
+	;;
+	bio.close(f)
+	ldep = std.mk([
+		.name=lib,
+		.dir=dir,
+		.dep=dep,
+		.dyndep=dyndep,
+		.genuse="",
+		.genar="",
+	])
+	std.htput(b.libs, lib, ldep)
+
+	for d : dep
+		scrapelib(b, d, incs)
+	;;
+}
+
+const openlib = {lib, incs : byte[:][:]
+	var path, libname
+
+	path = ""
+	libname = ""
+	for p : incs
+		libname = std.fmt("lib{}.use", lib)
+		path = std.pathjoin([p, libname][:])
+		std.slfree(libname)
+		if std.fisreg(path)
+			match  bio.open(path, bio.Rd)
+			| `std.Ok file:
+				std.slfree(path)
+				-> (file, p)
+			| `std.Err m:
+			;;
+			std.fatal("{} does not exist in {j=, }\n", lib, incs)
+		;;
+		std.slfree(path)
+	;;
+	std.fatal("{} does not exist in {j= }\n", lib, incs)
+}
+
+const addlibs = {b, sl, libs, incs
+	var added, diradded, looped
+	var lo
+
+	added  = std.mkht(std.strhash, std.streq)
+	looped  = std.mkht(std.strhash, std.streq)
+	diradded  = std.mkht(std.strhash, std.streq)
+
+	lo = sl#.len
+	for l : libs
+		addlib(b, sl, l, added, diradded, looped)
+	;;
+	for var i = 0; i < sl#[lo:].len/2; i++
+		std.swap(&sl#[lo+i], &sl#[sl#.len - i - 1])
+	;;
+
+	std.htfree(diradded)
+	std.htfree(looped)
+	std.htfree(added)
+}
+
+const addlib = {b, sl, lib, added, diradded, looped
+	var ar
+
+	if std.hthas(looped, lib)
+		std.fatal("{}: loop in deps\n", lib)
+	;;
+	std.htput(looped, lib, void)
+	match std.htget(b.libs, lib)
+	| `std.None:
+		std.slpush(sl, std.fmt("-l{}", lib))
+	| `std.Some ld:
+		for l : ld.dep
+			addlib(b, sl, l, added, diradded, looped)
+		;;
+		for l : ld.dyndep
+			addlib(b, sl, l, added, diradded, looped)
+		;;
+		if !std.hthas(added, lib)
+			if config.Directlib
+				ar = std.fmt("lib{}.a", lib)
+				std.slpush(sl, std.pathcat(ld.dir, ar))
+				std.slfree(ar)
+			else
+				std.slpush(sl, std.fmt("-l{}", lib))
+			;;
+			std.htput(added, lib, void)
+		;;
+		if !config.Directlib && !std.hthas(diradded, ld.dir)
+			std.htput(diradded, ld.dir, void)
+			std.slpush(sl, std.fmt("-L{}", ld.dir))
+		;;
+	;;
+	std.htdel(looped, lib)
+}
+
+
+const rdstr = {f -> byte[:]
+	var len
+	var sl
+
+	match bio.getbe32(f)
+	| `bio.Ok l:
+		len = l
+		sl = std.slalloc(len)
+	| `bio.Eof:	std.fatal("end of file while reading string")
+	| `bio.Err e:	std.fatal("error while reading string: {}", e)
+	;;
+	bio.read(f, sl)
+	-> sl
+}
--- a/mbld/main.myr
+++ b/mbld/main.myr
@@ -1,8 +1,8 @@
 use std
 use regex
+use thread
 
 use "build"
-use "clean"
 use "config"
 use "deps"
 use "install"
@@ -18,9 +18,11 @@
 	var bintarg
 	var targname
 	var runsrc
+	var path
 	var tmp
 	var cmd 
 	var tags
+	var pid
 	var ok, r
 
 	cmd = std.optparse(args, &[
@@ -35,8 +37,6 @@
 			[.opt='b', .arg="bin", .desc="compile binary named 'bin' from inputs"],
 			[.opt='l', .arg="lib", .desc="compile lib named 'lib' from inputs"],
 			[.opt='r', .arg="rt", .desc="link against runtime 'rt' instead of default"],
-			[.opt='C', .arg="mc", .desc="compile with 'mc' instead of the default compiler"],
-			[.opt='M', .arg="mu", .desc="merge uses with 'mu' instead of the default muse"],
 		][:]
 	])
 
@@ -46,7 +46,7 @@
 	ok = true
 
 	bld.initopts()
-	for opt : cmd.opts
+	for opt in cmd.opts
 		match opt
 		| ('S', ""):	bld.opt_genasm = true
 		| ('I', arg):	std.slpush(&bld.opt_incpaths, arg)
@@ -70,41 +70,46 @@
 		building with an uninstalled compiler.
 		*/
 		| ('d', arg): bld.opt_debug = true
-		| ('C', arg): bld.opt_mc = arg
-		| ('M', arg): bld.opt_muse = arg
 		| _:	std.die("unreachable\n")
 
 		;;
 	;;
+	path = std.pathcat(bld.opt_instbase, config.Libpath)
+	std.slpush(&bld.opt_incpaths, path)
 
-	for (e, v) : config.Env
+	for (e, v) in config.Env
 		std.setenv(e, v)
 	;;
 
 	b = mkbuild(tags)
 	if targname.len != 0
-		buildimm(b, targname, cmd.args, bintarg)
+		buildimm(b, targname, cmd.args)
 	elif runsrc.len != 0
 		bld.opt_silent = true
 		tmp = std.mktemppath("runmyr")
-		ok = buildimm(b, tmp, [runsrc][:], true)
+		ok = buildimm(b, tmp, [runsrc][:])
 		if ok
-			ok = runcmd(tmp, cmd.args)
+			pid = runcmd(tmp, cmd.args)
+			match std.wait(pid)
+			| `std.Wsuccess:	ok = true
+			| _:			ok = false
+			;;
 		;;
 		std.remove(tmp)
 	else
-		findproj(b, "bld.proj")
+		findproj(b)
 		bld.load(b)
+		bld.deps(b)
+		bld.testdeps(b)
+		bld.resolve(b)
 		/*bld.configure()*/
 		/* default: buildall */
 		if cmd.args.len == 0
-			bld.buildall(b)
+			bld.buildtarg(b, "all")
 		else
-			for c : cmd.args
+			for c in cmd.args
 				match c
-				| "all":	r = bld.buildall(b)
-				| "gen":	r = bld.genall(b)
-				| "clean":	r = bld.cleanall(b)
+				| "clean":	r = bld.clean(b)
 				| "install":	r = bld.install(b)
 				| "uninstall":	r = bld.uninstall(b)
 				| "test":	r = bld.test(b)
@@ -119,7 +124,7 @@
 	;;
 }
 
-const buildimm = {b, targ, inputs, bintarg -> bool
+const buildimm = {b, targ, inputs
 	var mt : bld.myrtarg
 
 	mt = [
@@ -129,11 +134,11 @@
 		.incpath=bld.opt_incpaths,
 		.libdeps=[][:]
 	]
-	if bintarg
-		-> bld.buildbin(b, &mt, true)
-	else
-		-> bld.buildlib(b, &mt)
-	;;
+	std.slpush(&b.all, "__out__")
+	std.htput(b.targs, "__out__", `bld.Bin &mt)
+	bld.deps(b)
+	bld.resolve(b)
+	-> bld.buildtarg(b, "all")
 }
 
 const runcmd = {bin, args
@@ -140,7 +145,7 @@
 	var sl
 
 	sl = std.sldup([bin][:])
-	-> bld.run(std.sljoin(&sl, args))
+	-> bld.run(std.sljoin(&sl, args), "")
 }
 
 const mkbuild = {tags
@@ -147,37 +152,33 @@
 	var b
 
 	b = std.zalloc()
+	b.libs = std.mkht(std.strhash, std.streq)
+	b.proc = std.mkht(std.inthash, std.inteq)
 	b.targs = std.mkht(std.strhash, std.streq)
-	b.gensrc = std.mkht(std.strhash, std.streq)
-	b.built = std.mkht(std.strhash, std.streq)
 	b.tags = std.mkht(std.strhash, std.streq)
+	b.deps = std.mk([
+		.targs = std.mkht(std.strhash, std.streq),
+		.gen = std.mkht(std.strhash, std.streq),
+		.leaves = [][:],
+		.nodes = [][:],
+	])
 	bld.addsysattrs(b, tags)
 	-> b
 }
 
-const findproj = {b, bldfile
-	if !findbase(b, bldfile) || !std.chdir(b.basedir)
-		std.fatal("could not find {}\n", bldfile)
-	;;
-	bld.setdir(b, "")
-}
+const findproj = {b
+	var dir
 
-const findbase = {b, file
-	var p, bld, dir
-
 	dir = std.getcwd()
-	while !std.sleq(dir, "/")
-		bld = std.pathcat(dir, file)
-		if std.fexists(bld)
-			bld.mbldput("project base {}:\n", dir)
+	while dir.len > 0 && !std.sleq(dir, "/")
+		if std.chdir(dir) && std.fexists("bld.proj")
 			b.basedir = dir
-			b.bldfile = bld
-			-> true
+			break
 		;;
-		p = std.pathcat(dir, "..")
-		std.slfree(dir)
-		dir = p
+		dir = std.dirname(dir)
 	;;
-	-> false
+	if dir.len > 0 && std.sleq(b.basedir, "/")
+		std.fatal("could not find bld.proj\n")
+	;;
 }
 
--- a/mbld/opts.myr
+++ b/mbld/opts.myr
@@ -1,4 +1,5 @@
 use std
+use thread
 
 use "config"
 
@@ -12,6 +13,7 @@
 	var opt_instbase	: byte[:]
 	var opt_destdir	: byte[:]
 	var opt_outdir	: byte[:]
+	var opt_maxproc : std.size
 	var opt_debug	: bool
 	var opt_silent	: bool
 
@@ -44,6 +46,7 @@
 var opt_ar	= "ar"
 var opt_genasm  = false
 var opt_silent	= false
+var opt_maxproc = 1
 
 const initopts = {
 	var si
@@ -67,6 +70,7 @@
 	| unknown:	std.fatal("unknown architecture \"{}\"\n", unknown)
 	;;
 
+	opt_maxproc = 2*(thread.ncpu() : std.size)
 	opt_incpaths = [][:]
 	opt_instbase = config.Instroot
 	opt_destdir = std.getenvv("DESTDIR", "")
@@ -84,7 +88,7 @@
 
 	i = 0
 	a = [0, 0, 0]
-	for e : std.bysplit(v, ".")
+	for e in std.bysplit(v, ".")
 		match std.intparse(e)
 		| `std.Some n:	a[i++] = n
 		| `std.None:	continue
--- a/mbld/parse.myr
+++ b/mbld/parse.myr
@@ -40,7 +40,7 @@
 	var targs
 
 	sel = mksyssel(b, "mbld", 0, "mbld")
-	ok = loadall(b, b.bldfile, "", sel)
+	ok = loadall(b, "bld.proj", "", sel)
 
 	targs = sysselfin(sel)
 	for (name, targ) : targs
@@ -55,6 +55,56 @@
 	-> ok
 }
 
+const sortdeps = {b
+	var looped
+	var marked
+	var all
+
+	all = [][:]
+	looped = std.mkht(std.strhash, std.streq)
+	marked = std.mkht(std.strhash, std.streq)
+	for dep : b.all
+		match gettarg(b.targs, dep)
+		| `Bin _:	all = visit(all, b, "all", dep, looped, marked)
+		| `Lib _:	all = visit(all, b, "all", dep, looped, marked)
+		| targ:		std.slpush(&all, dep)
+		;;
+	;;
+	std.slfree(b.all)
+	b.all = all
+	-> true
+}
+
+const visit = {all, b, parent, targ, looped, marked
+	if std.hthas(looped, targ)
+		std.fatal("cycle in build depgraph involving {}\n", targ)
+	elif std.hthas(marked, targ)
+		-> all
+	;;
+
+	std.htput(looped, targ, true)
+	for (dir, lib, dep) : getdeps(b, parent, targ)
+		all = visit(all, b, targ, dep, looped, marked)
+	;;
+	std.htdel(looped, targ)
+	std.htput(marked, targ, true)
+	-> std.slpush(&all, targ)
+}
+
+const getdeps = {b, parent, targname
+	match std.htget(b.targs, targname)
+	| `std.Some targ:
+		match targ
+		| `Bin t:	-> t.libdeps
+		| `Lib t:	-> t.libdeps
+		| _:	std.fatal("{} depends on non-library target", parent)
+		;;
+	| `std.None:
+		std.fatal("{}: could not find dependency {}\n", parent, targname)
+	;;
+}
+
+/* recursively load all bld.{proj,sub} files in project */
 const loadall = {b, path, dir, sel
 	var p : parser#
 	var subbld, subproj, ok
@@ -62,8 +112,12 @@
 
 	p = mkparser(path, dir, b.basedir, sel)
 	ok = bld.parse(b, p, "")
-	for t : p.targs
-		setopts(p, t)
+	for targ : p.targs
+		match targ
+		| `Bin t:	setmyropt(p, t)
+		| `Lib t:	setmyropt(p, t)
+		| _:	/* only myr targets have settable defaults so far */
+		;;
 	;;
 
 	for sub : p.subdirs
@@ -95,14 +149,6 @@
 	-> ok
 }
 
-const setopts = {p, targ
-	match targ
-	| `Bin t:	setmyropt(p, t)
-	| `Lib t:	setmyropt(p, t)
-	| _:	/* only myr targets have settable defaults so far */
-	;;
-}
-
 const setmyropt = {p, t
 	var libdep, tstdep
 
@@ -127,7 +173,6 @@
 
 const parse = {b, p, path
 	while true
-		skipspace(p)
 		if !target(b, p)
 			break
 		;;
@@ -198,55 +243,6 @@
 	std.free(p)
 }
 
-const sortdeps = {b
-	var looped
-	var marked
-	var all
-
-	all = [][:]
-	looped = std.mkht(std.strhash, std.streq)
-	marked = std.mkht(std.strhash, std.streq)
-	for dep : b.all
-		match gettarg(b.targs, dep)
-		| `Bin _:	all = visit(all, b, "all", dep, looped, marked)
-		| `Lib _:	all = visit(all, b, "all", dep, looped, marked)
-		| targ:		std.slpush(&all, dep)
-		;;
-	;;
-	std.slfree(b.all)
-	b.all = all
-	-> true
-}
-
-const visit = {all, b, parent, targ, looped, marked
-	if std.hthas(looped, targ)
-		std.fatal("cycle in build depgraph involving {}\n", targ)
-	elif std.hthas(marked, targ)
-		-> all
-	;;
-
-	std.htput(looped, targ, true)
-	for (dir, lib, dep) : getdeps(b, parent, targ)
-		all = visit(all, b, targ, dep, looped, marked)
-	;;
-	std.htdel(looped, targ)
-	std.htput(marked, targ, true)
-	-> std.slpush(&all, targ)
-}
-
-const getdeps = {b, parent, targname
-	match std.htget(b.targs, targname)
-	| `std.Some targ:
-		match targ
-		| `Bin t:	-> t.libdeps
-		| `Lib t:	-> t.libdeps
-		| _:	std.fatal("{} depends on non-library target", parent)
-		;;
-	| `std.None:
-		std.fatal("{}: could not find dependency {}\n", parent, targname)
-	;;
-}
-
 /* <name>: '=' wordlist ';;' */
 const incpath = {b, p, words, name
 	skipspace(p)
@@ -332,14 +328,9 @@
 		outlist = out
 	;;
 
-	skipspace(p)
-	if matchc(p, '{')
-		match attrlist(p)
-		| `std.Some al:	attrs = al
-		| `std.None:	failparse(p, "invalid attr list for {}\n", outlist[0])
-		;;
-	else
-		attrs = [][:]
+	match attrlist(p)
+	| `std.Some al:	attrs = al
+	| `std.None:	failparse(p, "invalid attr list for {}\n", outlist[0])
 	;;
 
 	skipspace(p)
@@ -377,7 +368,7 @@
 
 	gt = std.mk([
 		.dir=std.sldup(p.fdir),
-		.out=outlist,
+		.gen=outlist,
 		.durable=durable,
 		.istest=istest,
 		.deps=deplist,
@@ -388,7 +379,6 @@
 		if iscmd
 			addtarg(p, b, o, gt.tags, `Cmd gt)
 		else
-			std.htput(b.gensrc, o, gt)
 			addtarg(p, b, o, gt.tags, `Gen gt)
 		;;
 	;;
@@ -400,8 +390,7 @@
 */
 const myrtarget = {b, p, targ
 	var ldscript, runtime, install, incpath, tags
-	var libdeps, tstdeps
-	var name, inputs, attrs
+	var libdeps, name, inputs, attrs
 	var istest
 	var fsel
 
@@ -410,14 +399,9 @@
 	| `std.None:	failparse(p, "expected target name after '{}'\n", targ)
 	;;
 
-	skipspace(p)
-	if matchc(p, '{')
-		match attrlist(p)
-		| `std.Some al:	attrs = al
-		| `std.None:	failparse(p, "invalid attr list for {} {}\n", targ, name)
-		;;
-	else
-		attrs = [][:]
+	match attrlist(p)
+	| `std.Some al:	attrs = al
+	| `std.None:	failparse(p, "invalid attr list for {} {}\n", targ, name)
 	;;
 
 	skipspace(p)
@@ -448,7 +432,6 @@
 	incpath = [][:]
 	tags = [][:]
 	istest = false
-	tstdeps = [][:]
 	for elt : attrs
 		match elt
 		| ("ldscript", lds):	ldscript = std.sldup(lds)
@@ -459,12 +442,12 @@
 		| ("noinst", ""):	install = false
 		| ("test", ""):		istest = true
 		| ("notest", ""):	istest = false
-		| (invalid, ""):	std.fatal("{}: got invalid attr '{}'\n", targ, invalid)
-		| (invalid, attr):	std.fatal("{}: got invalid attr '{} = {}'\n", targ, invalid, attr)
+		| (invalid, ""):	std.fatal("{}: invalid attr '{}'\n", targ, invalid)
+		| (invalid, attr):	std.fatal("{}: invalid attr '{} = {}'\n", targ, invalid, attr)
 		;;
 	;;
+
 	std.sljoin(&incpath, bld.opt_incpaths)
-	std.sljoin(&incpath, bld.opt_incpaths)
 	-> std.mk([
 		/* target */
 		.dir=std.sldup(p.fdir),
@@ -471,7 +454,6 @@
 		.name=name,
 		.inputs=inputs,
 		.libdeps=libdeps,
-		.tstdeps=tstdeps,
 		.islib=false,
 		.istest=istest,
 		/* attrs */
@@ -493,13 +475,11 @@
 	;;
 
 	skipspace(p)
-	if matchc(p, '{')
-		match attrlist(p)
-		| `std.Some al:	attrs = al
-		| `std.None:	failparse(p, "invalid attr list for {} {}\n", targ, name)
-		;;
-	else
-		attrs = [][:]
+	attrs = [][:]
+
+	match attrlist(p)
+	| `std.Some al:	attrs = al
+	| `std.None:	failparse(p, "invalid attr list for {} {}\n", targ, name)
 	;;
 
 	skipspace(p)
@@ -508,14 +488,13 @@
 	;;
 
 	match wordlist(p)
-	| `std.Some wl: 
-		blobs = wl
-	| `std.None: failparse(p, "expected list of file names after '{} {}'\n", targ, name)
+	| `std.Some wl:	blobs = wl
+	| `std.None:	failparse(p, "expected file list after '{} {}'\n", targ, name)
 	;;
 
 	skipspace(p)
 	if !matchc(p, ';') || !matchc(p, ';')
-		failparse(p, "expected ';;' terminating input list, got {}\n", peekc(p))
+		failparse(p, "expected ';;' after input list, got {}\n", peekc(p))
 	;;
 
 	tags = [][:]
@@ -523,10 +502,10 @@
 		match elt
 		| ("tag", tag):		std.slpush(&tags, tag)
 		| ("path", pathdir):	path = pathdir
-		| (invalid, ""):
-			std.fatal("{}: got invalid attr '{}'\n", targ, invalid)
-		| (invalid, attr):
-			std.fatal("{}: got invalid attr '{} = {}'\n", targ, invalid, attr)
+		| (tag, ""):
+			std.fatal("{}: invalid attr '{}'\n", targ, tag)
+		| (tag, attr):
+			std.fatal("{}: invalid attr '{}={}'\n", targ, tag, attr)
 		;;
 	;;
 
@@ -550,8 +529,8 @@
 	;;
 
 	match wordlist(p)
-	| `std.None:	failparse(p, "expected list of file names after '{}' target\n", targ)
 	| `std.Some wl:	inputs = wl
+	| `std.None:	failparse(p, "expected file list after '{}' target\n", targ)
 	;;
 	skipspace(p)
 	if !matchc(p, ';') || !matchc(p, ';')
@@ -561,7 +540,7 @@
 }
 
 /*
-attrlist: attrs '}'
+attrlist: EMPTY |'{' attrs '}'
 
 attrs	: EMPTY
 	| attrs attr
@@ -573,10 +552,13 @@
 	var al
 
 	al = [][:]
+	skipspace(p)
+	if !matchc(p, '{')
+		-> `std.Some al
+	;;
 	while true
 		match word(p)
 		| `std.Some k:
-			skipspace(p)
 			if matchc(p, '=')
 				match word(p)
 				| `std.Some v:
@@ -661,38 +643,21 @@
 	var start
 
 	skipspace(p)
-
 	c = peekc(p)
-	if c == '"'
-		n = 0
-		getc(p)
-		start = p.rest
-		while p.rest.len > 0
-			c = peekc(p)
-			if c == '"'
-				getc(p)
-				goto done
-			elif c == '\\'
-				c = getc(p)
-			;;
+	n = 0
+	start = p.rest
+	while p.rest.len > 0
+		c = peekc(p)
+		if c == '\\'
+			c = getc(p)
+		;;
+		if wordchar(c)
 			getc(p)
 			n += std.charlen(c)
+		else
+			break
 		;;
-		failparse(p, "input ended within quoted word\n")
-	else
-		n = 0
-		start = p.rest
-		while p.rest.len > 0
-			c = peekc(p)
-			if wordchar(c)
-				getc(p)
-				n += std.charlen(c)
-			else
-				break
-			;;
-		;;
 	;;
-:done
 	if n > 0
 		-> `std.Some std.sldup(start[:n])
 	else
@@ -733,9 +698,7 @@
 const matchc = {p, c
 	var chr, s
 
-	if p.rest.len == 0
-		-> false
-	;;
+	/* safe to use at eof: strstep returns (-1, "") */
 	(chr, s) = std.strstep(p.rest)
 	if c == chr
 		p.rest = s
@@ -770,7 +733,7 @@
 
 	match std.strrfind(libpath, ":")
 	| `std.None:
-		dir = std.sldup(".")
+		dir = std.sldup(p.fdir)
 		lib = std.sldup(libpath)
 		targ = std.fmt("{}:{}", p.fdir, lib)
 	| `std.Some idx:
@@ -779,12 +742,12 @@
 		;;
 		/* absolute path */
 		if std.hasprefix(libpath, "@/") || std.hasprefix(libpath, "@:")
-			dir = std.pathcat(p.basedir, libpath[2:idx])
+			dir = std.sldup(libpath[2:idx])
 			lib = std.sldup(libpath[idx+1:])
 			targ = std.sldup(libpath[2:])
 		/* relative path */
 		else
-			dir = std.sldup(libpath[:idx])
+			dir = std.pathcat(p.fdir, libpath[:idx])
 			lib = std.sldup(libpath[idx+1:])
 			targ = std.pathcat(p.fdir, libpath)
 			if std.hasprefix(targ, "../")
--- a/mbld/subtest.myr
+++ b/mbld/subtest.myr
@@ -75,7 +75,7 @@
 	curtest = ""
 	nresults = 0
 	mbldput("\n")
-	for ln : bio.byline(f)
+	for ln in bio.byline(f)
 		ln = std.strstrip(ln)
 		match testhead(ln)
 		| `std.None:
@@ -127,7 +127,7 @@
 
 const starttest = {curtest, t
 	if curtest#.len != 0
-		std.fatal("malformed input in test {} nested : {}\n", t, curtest)
+		std.fatal("malformed input: test {} nested in {}\n", t, curtest)
 	;;
 	mbldput("\trun {}:\t", std.strstrip(t))
 	curtest# = t
@@ -148,7 +148,7 @@
 		else
 			mbldput("FAIL\n")
 		;;
-		p = std.pathjoin([b.curdir, cmd, curtest#][:])
+		p = std.pathcat(cmd, curtest#)
 		std.slpush(failed, p)
 	;;
 
--- a/mbld/syssel.myr
+++ b/mbld/syssel.myr
@@ -120,83 +120,46 @@
 }
 
 const addsysattrs = {b, tags
-	var tagfile
-
 	std.htput(b.tags, opt_sys, opt_sysvers)
+
 	match opt_sys
-	| "freebsd":	std.htput(b.tags, "posixy", (-1, -1, -1))
-	| "netbsd":	std.htput(b.tags, "posixy", (-1, -1, -1))
-	| "openbsd":	std.htput(b.tags, "posixy", (-1, -1, -1))
-	| "osx":	std.htput(b.tags, "posixy", (-1, -1, -1))
-	| "linux":	std.htput(b.tags, "posixy", (-1, -1, -1))
+	| "freebsd":	tag(b, "posixy")
+	| "netbsd":	tag(b, "posixy")
+	| "openbsd":	tag(b, "posixy")
+	| "osx":	tag(b, "posixy")
+	| "linux":	tag(b, "posixy")
 	| "plan9":
 	| unknown:	std.fatal("unknown system \"{}\"\n", unknown)
 	;;
 
 	match opt_arch
-	| "x64":	tag(b.tags, ["x64"][:])
+	| "x64":	tag(b, "x64")
 	| unknown:	std.fatal("unknown architecture {}\n", unknown)
 	;;
-	tag(b.tags, tags)
-
-	tagfile = std.pathcat(b.basedir, " bld.tag")
-	if std.fexists(tagfile)
-		loadtagfile(b, tagfile)
+	for t : tags
+		tag(b, t)
 	;;
-	std.slfree(tagfile)
+
+	loadtagfile(b, "bld.tag")
 }
 
 const loadtagfile = {b, tagfile
-	var data, tag
+	var data, sp
 
+	if !std.fexists(tagfile)
+		-> void
+	;;
 	data = std.try(std.slurp(tagfile))
 	while true
-		data = skipspace(data)
-		(tag, data) = word(data) 
-		match tag
-		| `std.Some w:
-			std.htput(b.tags, w, (-1, -1, -1))
-		| `std.None:
-			if data.len > 0
-				std.fatal("junk character near '{}'\n", trailing(data, 10))
-			else
-				break
-			;;
+		sp = std.strtok(data)
+		for t : sp
+			tag(b, t)
 		;;
+		std.slfree(sp)
 	;;
+	std.slfree(data)
 }
 
-const skipspace = {data
-	var c
-
-	c = std.decode(data)
-	while std.isspace(c)
-		data = data[std.charlen(c):]
-	;;
-	-> data
-}
-
-const word = {data
-	var c, split
-
-	split = 0
-	c = std.decode(data[:])
-	while std.isalnum(c) || c == '.' || c == '_' || c == '$'
-		split += std.charlen(c)
-	;;
-	if split > 0
-		-> (`std.Some data[:split], data[split:])
-	else
-		-> (`std.None, data)
-	;;
-}
-
-const tag  = {sa, tags
-	for t : tags
-		std.htput(sa, t, (-1, -1, -1))
-	;;
-}
-
-const trailing = {str, len
-	-> str[:std.min(len, str.len)]
+const tag  = {b, tag
+	std.htput(b.tags, std.sldup(tag), (-1, -1, -1))
 }
--- a/mbld/test.myr
+++ b/mbld/test.myr
@@ -1,13 +1,9 @@
 use std
 
 use "build"
-use "clean"
-use "deps"
 use "opts"
-use "parse"
 use "types"
 use "util"
-use "syssel"
 use "subtest"
 
 use "config"
@@ -17,66 +13,26 @@
 ;;
 
 const test = {b
-	var tests : (byte[:][:], byte[:])[:]
 	var failed, ok
-	var bincmd
+	var tests
 
-	/* build with the test tag */
-	addsysattrs(b, ["test"][:])
-	/* no implicit tests to run */
-	tests = [][:]
-	buildall(b)
-	setdir(b, "")
-        for tn : b.all
-		match gettarg(b.targs, tn)
-		| `Bin bt:      std.sljoin(&tests, buildtests(b, bt))
-		| `Lib lt:	std.sljoin(&tests, buildtests(b, lt))
-		| _:	/* nothing */
-		;;
-        ;;
-	for tn : b.all
-		match gettarg(b.targs, tn)
-		| `Bin t:
-			if !t.istest
-				continue
-			;;
-			if t.incpath.len == 0 || !std.sleq(t.incpath[0], ".")
-				std.slput(&t.incpath, 0, std.sldup("."))
-			;;
-			buildbin(b, t, false)
-			bincmd = std.sldup([std.strcat("./", t.name)][:])
-			std.slpush(&tests, (bincmd, std.sldup(t.dir)))
-		| `Cmd t:
-			if !t.istest
-				continue
-			;;
-			std.slpush(&tests, (dupcmd(t.cmd), std.sldup(t.dir)))
-		| _:
-			/* skip */
-		;;
-	;;
+	buildtarg(b, "test")
+	tests = std.htgetv(b.deps.targs, "test", [][:])
 
 	ok = true
 	failed = [][:]
-	for (c, dir) : tests
-		setdir(b, dir)
-		if !runtest(b, c, &failed)
+	for t in tests
+		if !runtest(b, t, &failed)
 			ok = false
 		;;
-
 	;;
-	for (bin, dir) : tests
-		freecmd(bin)
-		std.slfree(dir)
-	;;
+	std.chdir(b.basedir)
+
 	if tests.len == 0
 		-> true
 	;;
 
 	printfailed(failed)
-	for f : failed
-		std.slfree(f)
-	;;
 	if ok
 		mbldput("TESTS PASSED\n")
 	else
@@ -83,7 +39,6 @@
 		mbldput("TESTS FAILED\n")
 	;;
 	std.slfree(failed)
-	std.slfree(tests)
 	-> ok
 }
 
@@ -90,81 +45,26 @@
 const printfailed = {failed
 	if failed.len > 0
 		mbldput("FAILURES: {}\n", failed.len)
-		for t : failed
+		for t in failed
 			mbldput("\t{}\n", t)
 		;;
 	;;
 }
 
-const dupcmd = {cmd
-	var ret
-
-	ret = [][:]
-	for c : cmd
-		std.slpush(&ret, std.sldup(c))
-	;;
-	-> ret
-}
-
-const freecmd = {cmd
-	for c : cmd
-		std.slfree(c)
-	;;
-	std.slfree(cmd)
-}
-
-const buildtests = {b, targ
-	var tt, bin, tests, incpath, libdeps
-	var cmd
-
-	tests = [][:]
-	setdir(b, targ.dir)
-	for s : targ.inputs
-		match testpath(s)
-		| `std.None: /* nothing to do */
-		| `std.Some path:
-			bin = srcswapsuffix(path, "")
-			incpath = std.sldup(targ.incpath)
-			libdeps = std.sldup(targ.libdeps)
-			std.sljoin(&libdeps, targ.tstdeps)
-			tt = [
-				.name = bin,
-				.dir = targ.dir,
-				.inputs = [path][:],
-				.install = false,
-				.libdeps = libdeps,
-				.incpath = std.slput(&incpath, 0, "."),
-			]
-
-			buildbin(b, &tt, true)
-			cmd = std.sldup([std.strcat("./", bin)][:])
-			std.slpush(&tests, (cmd, std.sldup(targ.dir)))
-			std.slfree(tt.libdeps)
-			std.slfree(tt.incpath)
-			std.slfree(path)
-		;;
-	;;
-	-> tests
-}
-
-const runtest = {b, cmd, failed
-	var res, log, logfd, p
+const runtest = {b, n, failed
+	var dir, res, log, logfd
 	var sub
 
-	mbldput("run")
-	for c : cmd
-		p = std.pathcat(b.curdir, c)
-		mbldput(" {}", p)
-		std.slfree(p)
-	;;
-	mbldput(":\t")
-	match std.spork(cmd)
+	dir = std.pathcat(b.basedir, n.wdir)
+	mbldput("run {}:\t", n.lbl)
+	std.chdir(dir)
+	match std.spork(n.cmd)
 	| `std.Err m:
 		std.fatal("\nunable to run test: {}\n", m)
 	| `std.Ok (pid, infd, outfd):
-		log = std.strcat(cmd[0], ".log")
+		log = std.strcat(std.basename(n.lbl), ".log")
 		logfd = std.try(std.openmode(log, std.Owronly | std.Ocreat, 0o644))
-		sub = showsub(b, cmd[0], outfd, logfd, failed)
+		sub = showsub(b, n.lbl, outfd, logfd, failed)
 		std.slfree(log)
 		std.close(infd)
 		std.close(outfd)
@@ -171,6 +71,7 @@
 
 		res = false
 		match std.wait(pid)
+		| `std.Waiterror:	mbldput("FUCK pid {}\n", pid)
 		| `std.Wfailure:	mbldput("FAIL\n")
 		| `std.Wsignalled:	mbldput("CRASH\n")
 		| `std.Wsuccess:
@@ -180,33 +81,12 @@
 			| `std.Some r:	res = r
 			| `std.None:	mbldput("PASS\n")
 			;;
-		| `std.Waiterror:	mbldput("failed waiting for pid {}\n", pid)
 		;;
 		if !res
-			p = std.pathcat(b.curdir, cmd[0])
-			std.slpush(failed, p)
+			std.slpush(failed, n.lbl)
 		;;
 	;;
+	std.slfree(dir)
 	-> res
-}
-
-const testpath = {s
-	var path, file
-
-	path = std.pathcat("./test", s)
-	if std.fexists(path)
-		-> `std.Some path
-	;;
-	match std.strfind(s, "+")
-	| `std.None:
-	| `std.Some idx:
-		file = std.strcat(s[:idx], ".myr")
-		path = std.pathcat("./test", file)
-		std.slfree(file)
-		if std.fexists(path)
-			-> `std.Some path
-		;;
-	;;
-	-> `std.None
 }
 
--- a/mbld/types.myr
+++ b/mbld/types.myr
@@ -4,19 +4,21 @@
 	type build = struct
 		/* build state */
 		basedir	: byte[:]
-		bldfile	: byte[:]
-		curdir	: byte[:]
-		built	: std.htab(byte[:], bool)#	/* set of targets built in this run */
 		tags	: std.htab(byte[:], (int, int, int))#
+		libs	: std.htab(byte[:], libdep#)#
+		deps	: depgraph#
 
+		/* in flight builds */
+		queue	: node#[:]
+		proc	: std.htab(std.pid, node#)#
+
 		/* build params */
-		all	: byte[:][:]			/* targets in reverse topological order */
-		targs	: std.htab(byte[:], targ)#	/* name => target mapping */
-		tdeps	: std.htab(byte[:], byte[:][:])	/* targname => depname[:][:] */
-		gensrc	: std.htab(byte[:], cmdtarg#)#	/* generated => generator mapping */
+		all	: byte[:][:]
+		targs	: std.htab(byte[:], targ)#
 		prefix	: byte[:]
 		system	: byte[:]
 		arch	: byte[:]
+
 	;;
 
 	type targ = union
@@ -29,32 +31,30 @@
 	;;
 
 	type myrtarg = struct
-		file	: byte[:]
-		line	: int
-
-		islib	: bool
-		istest	: bool
-		dir	: byte[:]
 		name	: byte[:]
+		dir	: byte[:]
+
 		inputs	: byte[:][:]
 		libdeps	: (byte[:], byte[:], byte[:])[:]	/* dir, lib, targname */
 		tstdeps	: (byte[:], byte[:], byte[:])[:]	/* dir, lib, targname */
-		install	: bool
 		runtime	: byte[:]
 		incpath	: byte[:][:]
 		tags	: byte[:][:]
 		ldscript	: byte[:]
+
+		islib	: bool
+		istest	: bool
+		install	: bool
+
 	;;
 
 	type cmdtarg = struct
 		dir	: byte[:]
-		out	: byte[:][:]
+		gen	: byte[:][:]
 		cmd	: byte[:][:] 
 		deps	: byte[:][:] 
 		tags	: byte[:][:]
 		durable	: bool
-		/* we can have multiple outputs, but we only want to run once for each */
-		done	: bool
 		istest	: bool
 	;;
 
@@ -72,18 +72,49 @@
 		blobs	: byte[:][:]
 	;;
 
+	type libdep = struct
+		dir	: byte[:]
+		name	: byte[:]
+		dep	: byte[:][:]
+		dyndep	: byte[:][:]
+		genuse	: byte[:]
+		genar	: byte[:]
+	;;
+
 	type depgraph = struct
-		deps	: std.htab(byte[:], byte[:][:])#
-		libs	: std.htab(byte[:], byte[:][:])#
-		input	: std.htab(byte[:], byte[:])#
-		sources	: std.htab(byte[:], bool)#
-		updated	: std.htab(byte[:], bool)#
-		seen	: std.htab(byte[:], bool)#
-		done	: std.htab(byte[:], bool)#
+		/* the edges of the graph from both ends */
+		targs	: std.htab(byte[:], node#[:])#
+		gen	: std.htab(byte[:], node#)#
 
-		/* used for linking C */
-		extlibs	: byte[:][:]
-		cflags	: std.htab(byte[:], byte[:][:])#
-		dynamic	: bool
+		nodes	: node#[:]
+		leaves	: node#[:]
+	;;
+
+	type node = struct
+		/* for debugging, "primary output" */
+		lbl	: byte[:]
+
+		/* run this command in this dir to 'execute' the node */
+		cmd	: byte[:][:]
+		wdir	: byte[:]
+
+		/* install info */
+		instdir	: byte[:]
+		instmod	: int64
+
+		/* dependency names */
+		gen	: byte[:][:]
+		dep	: byte[:][:]
+		xdep	: byte[:][:]
+
+		/* resolved dependency nodes */
+		ndep	: node#[:]
+		ngen	: node#[:]
+
+		/* build state */
+		nblock	: int32
+		mtime	: std.time
+		durable	: bool	/* keep outputs when cleaning */
+		want	: bool
 	;;
 ;;
--- a/mbld/util.myr
+++ b/mbld/util.myr
@@ -4,38 +4,30 @@
 use "types"
 
 pkg bld =
-	const run	: (cmd : byte[:][:] -> bool)
-	const printcmd	: (cmd : byte[:][:] -> void)
+	const run	: (cmd : byte[:][:], dir : byte[:] -> std.pid)
 	const mbldput	: (fmt : byte[:], args : ... -> void)
 	const srcsplit	: (src : byte[:] -> (byte[:], byte[:], byte[:]))
-	const swapsuffix	: (f : byte[:], suff : byte[:], newsuff : byte[:] -> byte[:])
-	const srcswapsuffix	: (f : byte[:], newsuff : byte[:] -> byte[:])
-	const strlistfree	: (sl : byte[:][:] -> void)
+	const changesuffix	: (f : byte[:], newsuff : byte[:] -> byte[:])
 	const gettarg	: (tab : std.htab(byte[:], targ)#, n : byte[:] -> targ)
-	const setdir	: (b : build#, dir : byte[:] -> void)
 ;;
 
-const run = {cmd
+const run = {cmd, dir
 	var pid
 
-	printcmd(cmd)
+	std.assert(cmd.len != 0, "empty command\n")
+	mbldput("\t{j= }\n", cmd);
 	pid = std.fork()
 	if pid == -1
 		std.fatal("could not fork command\n")
-		-> false
 	elif pid == 0
+		if dir.len != 0
+			std.chdir(dir)
+		;;
 		if std.execvp(cmd[0], cmd) < 0
 			std.fatal("failed to exec {}\n", cmd[0])
 		;;
-		-> false
-	else
-		match std.wait(pid)
-		| `std.Wsuccess:	-> true
-		| `std.Wfailure:	std.fatal("FAIL: \"{}\"\n", std.strjoin(cmd, " "))
-		| `std.Wsignalled:	std.fatal("CRASH: \"{}\"\n", std.strjoin(cmd, " "))
-		| `std.Waiterror:	std.fatal("WAT: \"{}\"\n", std.strjoin(cmd, " "))
-		;;
 	;;
+	-> pid
 }
 
 const mbldput = {fmt, args
@@ -47,17 +39,6 @@
 	;;
 }
 
-const printcmd = {lst
-	if lst.len > 0
-		mbldput("\t")
-		mbldput("{}\t", lst[0])
-		for l : lst[1:]
-			mbldput("{} ", l)
-		;;
-	;;
-	mbldput("\n")
-}
-
 const srcsplit = {src
 	var platf, fbase, suff
 
@@ -69,31 +50,22 @@
 	| `std.None:	fbase = 0
 	;;
 	match std.strfind(src[fbase:], ".")
+	| `std.None: /* no suffix to trim */
 	| `std.Some i:
 		suff = src[fbase+i:]
 		src = src[:fbase+i]
-	| `std.None:
-		/* no suffix to trim */
 	;;
 
 	match std.strrfind(src[fbase:], "+")
+	| `std.None: /* no platform to trim */
 	| `std.Some i:
 		platf = src[fbase+i:]
 		src = src[:fbase+i]
-	| `std.None:
-		/* no platform to trim */
 	;;
 	-> (src, platf, suff)
 }
 
-const swapsuffix = {f, suff, newsuff
-	if std.hassuffix(f, suff)
-		f = f[:f.len - suff.len]
-	;;
-	-> std.fmt("{}{}", f, newsuff)
-}
-
-const srcswapsuffix = {src, new
+const changesuffix = {src, new
 	var base, platf, suff
 
 	(base, platf, suff) = srcsplit(src)
@@ -110,30 +82,10 @@
 	;;
 }
 
-const strlistfree = {sl
-	for s : sl
-		std.slfree(s)
-	;;
-	std.slfree(sl)
-}
-
 const gettarg = {tab, n
 	match std.htget(tab, n)
 	| `std.None:	std.fatal("unknown target '{}'\n", n)
 	| `std.Some t:	-> t
-	;;
-}
-
-const setdir = {b, dir
-	var p
-
-	if !std.sleq(b.curdir, dir)
-		p = std.pathcat(b.basedir, dir)
-		if !std.chdir(p)
-			std.fatal("could not cd into {}\n", p)
-		;;
-		b.curdir = dir
-		std.slfree(p)
 	;;
 }