diff --git a/first/ackbuilder.lua b/first/ackbuilder.lua
index 3a85c1e2c..72b4392a3 100644
--- a/first/ackbuilder.lua
+++ b/first/ackbuilder.lua
@@ -53,6 +53,25 @@ local function concatpath(...)
 	return p:gsub("/+", "/"):gsub("^%./", "")
 end
 
+local function basename(filename)
+	local _, _, b = filename:find("^.*/([^/]*)$")
+	if not b then
+		return ""
+	end
+	return b
+end
+
+local function basenames(collection)
+	local o = {}
+	for _, s in pairs(collection) do
+		local b = basename(s)
+		if (b ~= "") then
+			o[#o+1] = b
+		end
+	end
+	return o
+end
+
 local function dirname(filename)
 	local _, _, b = filename:find("^(.*)/[^/]*$")
 	if not b then
@@ -74,13 +93,15 @@ end
 
 local function filenamesof(results)
 	local f = {}
-	for _, r in pairs(results) do
-		if (type(r) == "string") then
-			f[#f+1] = r
-		elseif (type(r) == "table") then
-			if r.is and r.outs then
-				for _, o in pairs(r.outs) do
-					f[#f+1] = o
+	if results then
+		for _, r in pairs(results) do
+			if (type(r) == "string") then
+				f[#f+1] = r
+			elseif (type(r) == "table") then
+				if r.is and r.outs then
+					for _, o in pairs(r.outs) do
+						f[#f+1] = o
+					end
 				end
 			end
 		end
@@ -88,6 +109,27 @@ local function filenamesof(results)
 	return f
 end
 
+local function selectof(pattern, targets)
+	local o = {}
+	for k, v in pairs(targets) do
+		if v.is and v.outs then
+			local targetmatches = nil
+			for _, f in pairs(v.outs) do
+				local matches = not not f:find(pattern)
+				if (targetmatches == nil) then
+					targetmatches = matches
+				elseif (targetmatches ~= matches) then
+					error("selectof() is matching only part of a target")
+				end
+			end
+			if targetmatches then
+				o[#o+1] = v
+			end
+		end
+	end
+	return o
+end
+
 local function uniquify(collection)
 	local s = {}
 	local o = {}
@@ -238,6 +280,13 @@ local typeconverters = {
 		end
 		return i
 	end,
+
+	table = function(propname, i)
+		if (type(i) ~= "table") then
+			error(string.format("property '%s' must be a table", propname))
+		end
+		return i
+	end,
 }
 	
 local function definerule(rulename, types, cb)
@@ -258,7 +307,7 @@ local function definerule(rulename, types, cb)
 		local args = {}
 		for propname, typespec in pairs(types) do
 			if not e[propname] then
-				if not typespec.optional then
+				if not typespec.optional and not typespec.default then
 					error(string.format("missing mandatory property '%s'", propname))
 				end
 
@@ -325,19 +374,22 @@ definerule("simplerule",
 		outs = { type="strings" },
 		label = { type="string", optional=true },
 		commands = { type="strings" },
+		vars = { type="table", default={} },
 	},
 	function (e)
 		e.environment:rule(filenamesof(e.ins), e.outs)
 		e.environment:label(cwd..":"..e.name, " ", e.label or "")
 		e.environment:mkdirs(dirnames(filenamesof(e.outs)))
-		e.environment:exec(
-			templateexpand(e.commands,
-				{
-					ins = e.ins,
-					outs = e.outs
-				}
-			)
-		)
+
+		local vars = {
+			ins = e.ins,
+			outs = e.outs
+		}
+		for k, v in pairs(e.vars) do
+			vars[k] = v
+		end
+
+		e.environment:exec(templateexpand(e.commands, vars))
 		e.environment:endrule()
 
 		return {
@@ -352,11 +404,17 @@ definerule("simplerule",
 
 globals = {
 	asstring = asstring,
+	basename = basename,
+	basenames = basenames,
 	concatpath = concatpath,
 	cwd = cwd,
 	definerule = definerule,
+	dirname = dirname,
+	dirnames = dirnames,
 	emit = emit,
 	environment = environment,
+	filenamesof = filenamesof,
+	selectof = selectof,
 }
 setmetatable(globals,
 	{
diff --git a/first/build.lua b/first/build.lua
index 2ab1b3cc4..03077a414 100644
--- a/first/build.lua
+++ b/first/build.lua
@@ -4,6 +4,7 @@ definerule("normalrule",
 		outleaves = { type="strings" },
 		label = { type="string", optional=true },
 		commands = { type="strings" },
+		vars = { type="table", default={} },
 	},
 	function (e)
 		local objpath = "$(OBJDIR)/"..e.name
@@ -12,76 +13,76 @@ definerule("normalrule",
 			realouts[k] = concatpath(objpath, v)
 		end
 
-		return simplerule {
+		local result = simplerule {
 			name = e.name,
 			ins = e.ins,
 			outs = realouts,
 			label = e.label,
 			commands = e.commands,
+			vars = e.vars,
+		}
+		result.dir = objpath
+		return result
+	end
+)
+
+definerule("cfile",
+	{
+		srcs = { type="targets" },
+		hdrs = { type="targets", default={} },
+		commands = {
+			type="strings",
+			default={
+				"$CC -c -o %{outs[1]} %{ins[1]} %{hdrpaths}"
+			},
+		}
+	},
+	function (e)
+		if (#e.srcs ~= 1) then
+			error("you must have exactly one .c file")
+		end
+		
+		hdrpaths = {}
+		for _, t in pairs(e.hdrs) do
+			hdrpaths[#hdrpaths+1] = "-I"..t.dir
+		end
+
+		local outleaf = basename(filenamesof(e.srcs)[1]):gsub("%.c$", ".o")
+
+		return normalrule {
+			name = e.name,
+			ins = {e.srcs[1], unpack(e.hdrs)},
+			outleaves = {outleaf},
+			label = e.label,
+			commands = e.commands,
+			vars = {
+				hdrpaths = hdrpaths
+			}
 		}
 	end
 )
 
-
 normalrule {
-	name = "random",
+	name = "mkheader",
 	ins = {},
-	outleaves = {"out"},
+	outleaves = {"foo.h"},
 	commands = {
-		"dd if=/dev/random of=%{outs} bs=1024 count=1"
+		"echo 1 >> %{outs}"
 	}
 }
 
-normalrule {
-	name = "onetwo",
-	ins = {},
-	outleaves = {"one.txt", "two.txt"},
-	commands = {
-		"echo 1 >> %{outs[1]}",
-		"echo 2 >> %{outs[2]}",
-	}
+cfile {
+	name = "testfile-foo",
+	srcs = "foo.c",
 }
 
-normalrule {
-	name = "concat",
-	ins = {":onetwo"},
-	outleaves = {"result.txt"},
-	commands = {
-		"cat %{ins} > %{outs}"
-	}
-}
-
-simplerule {
-	name = "sorted",
-	ins = { ":random" },
-	outs = { "sorted" },
-	commands = {
-		"sort %{ins} > %{outs}"
-	}
+cfile {
+	name = "testfile-bar",
+	srcs = "bar.c",
+	hdrs = ":mkheader",
 }
 
 --[[
-function environment:cfileflags()
-	emit("$CFLAGS")
-end
-
-function environment:cfile(srcs, obj, includes)
-	emit("$CC -o", obj, srcs)
-	emit(ab.expand(includes, "-I%"))
-	self:cflags()
-	emit("\n")
-end
-
-function environment:clinkflags()
-	emit("$LDFLAGS")
-end
-
-function environment:clink(objs, exe, libraryFlags)
-	emit("$CC -o", exe, objs, libraryFlags)
-	self:clinkflags()
-	emit("\n")
-end
-
 --
 -- Targets:
 --