Initial Blueprint commit.

Blueprint is a build system component that reads Blueprints files defining
modules to be built, and generates a Ninja build manifest that can be used to
perform all the build actions.  It does not dictate or implement much build
policy itself, but rather provides a framework to ease the process of defining
build logic in Go.

The "blueprint" and "blueprint/parser" Go packages contain the functionality
for reading Blueprint files and invoking build logic functions defined in other
Go packages.

The "blueprint/bootstrap" Go package contains just enough build logic to build
a binary that includes Blueprint and any pure-Go (i.e. no cgo) build logic
defined in external Go packages.  This can be used to create a minimal Ninja
file that's capable of bootstrapping a Blueprint-based build system from
source.

The "blueprint/bootstrap/minibp" Go package contains code for a minimal binary
that includes the build logic defined in the "blueprint/bootstrap" package.
This binary can then create the Ninja file for the bootstrapping process.

Change-Id: I8d8390042372a72d225785cda738525001b009f1
diff --git a/Blueprints b/Blueprints
new file mode 100644
index 0000000..5515e18
--- /dev/null
+++ b/Blueprints
@@ -0,0 +1,38 @@
+bootstrap_go_package {
+    name:    "blueprint",
+    deps:    ["blueprint-parser"],
+    pkgPath: "blueprint",
+    srcs:    ["blueprint/context.go",
+              "blueprint/globals.go",
+              "blueprint/live_tracker.go",
+              "blueprint/mangle.go",
+              "blueprint/module_ctx.go",
+              "blueprint/ninja_defs.go",
+              "blueprint/ninja_strings.go",
+              "blueprint/ninja_writer.go",
+              "blueprint/scope.go",
+              "blueprint/singleton_ctx.go",
+              "blueprint/unpack.go"],
+}
+
+bootstrap_go_package {
+    name:    "blueprint-parser",
+    pkgPath: "blueprint/parser",
+    srcs:    ["blueprint/parser/parser.go"],
+}
+
+bootstrap_go_package {
+    name:    "blueprint-bootstrap",
+    deps:    ["blueprint"],
+    pkgPath: "blueprint/bootstrap",
+    srcs:    ["blueprint/bootstrap/bootstrap.go",
+              "blueprint/bootstrap/command.go",
+              "blueprint/bootstrap/config.go",
+              "blueprint/bootstrap/doc.go"],
+}
+
+bootstrap_go_binary {
+    name: "minibp",
+    deps: ["blueprint", "blueprint-bootstrap"],
+    srcs: ["blueprint/bootstrap/minibp/main.go"],
+}
\ No newline at end of file
diff --git a/blueprint/bootstrap/bootstrap.go b/blueprint/bootstrap/bootstrap.go
new file mode 100644
index 0000000..5912524
--- /dev/null
+++ b/blueprint/bootstrap/bootstrap.go
@@ -0,0 +1,490 @@
+package bootstrap
+
+import (
+	"blueprint"
+	"fmt"
+	"path/filepath"
+	"strings"
+)
+
+var (
+	gcCmd   = blueprint.StaticVariable("gcCmd", "$goToolDir/${GoChar}g")
+	packCmd = blueprint.StaticVariable("packCmd", "$goToolDir/pack")
+	linkCmd = blueprint.StaticVariable("linkCmd", "$goToolDir/${GoChar}l")
+
+	gc = blueprint.StaticRule("gc",
+		blueprint.RuleParams{
+			Command: "GOROOT='$GoRoot' $gcCmd -o $out -p $pkgPath -complete " +
+				"$incFlags $in",
+			Description: "${GoChar}g $out",
+		},
+		"pkgPath", "incFlags")
+
+	pack = blueprint.StaticRule("pack",
+		blueprint.RuleParams{
+			Command:     "GOROOT='$GoRoot' $packCmd grcP $prefix $out $in",
+			Description: "pack $out",
+		},
+		"prefix")
+
+	link = blueprint.StaticRule("link",
+		blueprint.RuleParams{
+			Command:     "GOROOT='$GoRoot' $linkCmd -o $out $libDirFlags $in",
+			Description: "${GoChar}l $out",
+		},
+		"libDirFlags")
+
+	cp = blueprint.StaticRule("cp",
+		blueprint.RuleParams{
+			Command:     "cp $in $out",
+			Description: "cp $out",
+		})
+
+	bootstrap = blueprint.StaticRule("bootstrap",
+		blueprint.RuleParams{
+			Command:     "$Bootstrap $in $BootstrapManifest",
+			Description: "bootstrap $in",
+			Generator:   true,
+		})
+
+	rebootstrap = blueprint.StaticRule("rebootstrap",
+		blueprint.RuleParams{
+			// Ninja only re-invokes itself once when it regenerates a .ninja
+			// file.  For the re-bootstrap process we need that to happen twice,
+			// so we invoke ninja ourselves once from this.  Unfortunately this
+			// seems to cause "warning: bad deps log signature or version;
+			// starting over" messages from Ninja.  This warning can be avoided
+			// by having the bootstrap and non-bootstrap build manifests have a
+			// different builddir (so they use different log files).
+			//
+			// This workaround can be avoided entirely by making a simple change
+			// to Ninja that would allow it to rebuild the manifest twice rather
+			// than just once.
+			Command:     "$Bootstrap $in && ninja",
+			Description: "re-bootstrap $in",
+			Generator:   true,
+		})
+
+	minibp = blueprint.StaticRule("minibp",
+		blueprint.RuleParams{
+			Command: fmt.Sprintf("%s -d %s -o $out $in",
+				minibpFile, minibpDepFile),
+			Description: "minibp $out",
+			Generator:   true,
+			Restat:      true,
+			Depfile:     minibpDepFile,
+			Deps:        blueprint.DepsGCC,
+		})
+
+	// Work around a Ninja issue.  See https://github.com/martine/ninja/pull/634
+	phony = blueprint.StaticRule("phony",
+		blueprint.RuleParams{
+			Command:     "# phony $out",
+			Description: "phony $out",
+			Generator:   true,
+		},
+		"depfile")
+
+	goPackageModule = blueprint.MakeModuleType("goPackageModule", newGoPackage)
+	goBinaryModule  = blueprint.MakeModuleType("goBinaryModule", newGoBinary)
+
+	binDir        = filepath.Join("bootstrap", "bin")
+	minibpFile    = filepath.Join(binDir, "minibp")
+	minibpDepFile = filepath.Join("bootstrap", "bootstrap_manifest.d")
+)
+
+type goPackageProducer interface {
+	GoPkgRoot() string
+	GoPackageTarget() string
+}
+
+func isGoPackageProducer(module blueprint.Module) bool {
+	_, ok := module.(goPackageProducer)
+	return ok
+}
+
+func isBootstrapModule(module blueprint.Module) bool {
+	_, isPackage := module.(*goPackage)
+	_, isBinary := module.(*goBinary)
+	return isPackage || isBinary
+}
+
+func isBootstrapBinaryModule(module blueprint.Module) bool {
+	_, isBinary := module.(*goBinary)
+	return isBinary
+}
+
+func generatingBootstrapper(config blueprint.Config) bool {
+	bootstrapConfig, ok := config.(Config)
+	if ok {
+		return bootstrapConfig.GeneratingBootstrapper()
+	}
+	return false
+}
+
+// A goPackage is a module for building Go packages.
+type goPackage struct {
+	properties struct {
+		PkgPath string
+		Srcs    []string
+	}
+
+	// The root dir in which the package .a file is located.  The full .a file
+	// path will be "packageRoot/PkgPath.a"
+	pkgRoot string
+
+	// The path of the .a file that is to be built.
+	archiveFile string
+}
+
+var _ goPackageProducer = (*goPackage)(nil)
+
+func newGoPackage() (blueprint.Module, interface{}) {
+	module := &goPackage{}
+	return module, &module.properties
+}
+
+func (g *goPackage) GoPkgRoot() string {
+	return g.pkgRoot
+}
+
+func (g *goPackage) GoPackageTarget() string {
+	return g.archiveFile
+}
+
+func (g *goPackage) GenerateBuildActions(ctx blueprint.ModuleContext) {
+	name := ctx.ModuleName()
+
+	if g.properties.PkgPath == "" {
+		ctx.ModuleErrorf("module %s did not specify a valid pkgPath", name)
+		return
+	}
+
+	g.pkgRoot = packageRoot(ctx)
+	g.archiveFile = filepath.Clean(filepath.Join(g.pkgRoot,
+		filepath.FromSlash(g.properties.PkgPath)+".a"))
+
+	// We only actually want to build the builder modules if we're running as
+	// minibp (i.e. we're generating a bootstrap Ninja file).  This is to break
+	// the circular dependence that occurs when the builder requires a new Ninja
+	// file to be built, but building a new ninja file requires the builder to
+	// be built.
+	if generatingBootstrapper(ctx.Config()) {
+		buildGoPackage(ctx, g.pkgRoot, g.properties.PkgPath, g.archiveFile,
+			g.properties.Srcs)
+	} else {
+		phonyGoTarget(ctx, g.archiveFile, g.properties.Srcs)
+	}
+}
+
+// A goBinary is a module for building executable binaries from Go sources.
+type goBinary struct {
+	properties struct {
+		Srcs           []string
+		PrimaryBuilder bool
+	}
+}
+
+func newGoBinary() (blueprint.Module, interface{}) {
+	module := &goBinary{}
+	return module, &module.properties
+}
+
+func (g *goBinary) GenerateBuildActions(ctx blueprint.ModuleContext) {
+	var (
+		name        = ctx.ModuleName()
+		objDir      = objDir(ctx)
+		archiveFile = filepath.Join(objDir, name+".a")
+		aoutFile    = filepath.Join(objDir, "a.out")
+		binaryFile  = filepath.Join(binDir, name)
+	)
+
+	// We only actually want to build the builder modules if we're running as
+	// minibp (i.e. we're generating a bootstrap Ninja file).  This is to break
+	// the circular dependence that occurs when the builder requires a new Ninja
+	// file to be built, but building a new ninja file requires the builder to
+	// be built.
+	if generatingBootstrapper(ctx.Config()) {
+		buildGoPackage(ctx, objDir, name, archiveFile, g.properties.Srcs)
+
+		var libDirFlags []string
+		ctx.VisitDepsDepthFirstIf(isGoPackageProducer,
+			func(module blueprint.Module) {
+				dep := module.(goPackageProducer)
+				libDir := dep.GoPkgRoot()
+				libDirFlags = append(libDirFlags, "-L "+libDir)
+			})
+
+		linkArgs := map[string]string{}
+		if len(libDirFlags) > 0 {
+			linkArgs["libDirFlags"] = strings.Join(libDirFlags, " ")
+		}
+
+		ctx.Build(blueprint.BuildParams{
+			Rule:    link,
+			Outputs: []string{aoutFile},
+			Inputs:  []string{archiveFile},
+			Args:    linkArgs,
+		})
+
+		ctx.Build(blueprint.BuildParams{
+			Rule:    cp,
+			Outputs: []string{binaryFile},
+			Inputs:  []string{aoutFile},
+		})
+	} else {
+		phonyGoTarget(ctx, binaryFile, g.properties.Srcs)
+	}
+}
+
+func buildGoPackage(ctx blueprint.ModuleContext, pkgRoot string,
+	pkgPath string, archiveFile string, srcs []string) {
+
+	srcDir := srcDir(ctx)
+	srcFiles := PrefixPaths(srcs, srcDir)
+
+	objDir := objDir(ctx)
+	objFile := filepath.Join(objDir, "_go_.$GoChar")
+
+	var incFlags []string
+	var depTargets []string
+	ctx.VisitDepsDepthFirstIf(isGoPackageProducer,
+		func(module blueprint.Module) {
+			dep := module.(goPackageProducer)
+			incDir := dep.GoPkgRoot()
+			target := dep.GoPackageTarget()
+			incFlags = append(incFlags, "-I "+incDir)
+			depTargets = append(depTargets, target)
+		})
+
+	gcArgs := map[string]string{
+		"pkgPath": pkgPath,
+	}
+
+	if len(incFlags) > 0 {
+		gcArgs["incFlags"] = strings.Join(incFlags, " ")
+	}
+
+	ctx.Build(blueprint.BuildParams{
+		Rule:      gc,
+		Outputs:   []string{objFile},
+		Inputs:    srcFiles,
+		Implicits: depTargets,
+		Args:      gcArgs,
+	})
+
+	ctx.Build(blueprint.BuildParams{
+		Rule:    pack,
+		Outputs: []string{archiveFile},
+		Inputs:  []string{objFile},
+		Args: map[string]string{
+			"prefix": pkgRoot,
+		},
+	})
+}
+
+func phonyGoTarget(ctx blueprint.ModuleContext, target string, srcs []string) {
+	var depTargets []string
+	ctx.VisitDepsDepthFirstIf(isGoPackageProducer,
+		func(module blueprint.Module) {
+			dep := module.(goPackageProducer)
+			target := dep.GoPackageTarget()
+			depTargets = append(depTargets, target)
+		})
+
+	moduleDir := ctx.ModuleDir()
+	srcs = PrefixPaths(srcs, filepath.Join("$SrcDir", moduleDir))
+
+	ctx.Build(blueprint.BuildParams{
+		Rule:      phony,
+		Outputs:   []string{target},
+		Inputs:    srcs,
+		Implicits: depTargets,
+	})
+
+	// If one of the source files gets deleted or renamed that will prevent the
+	// re-bootstrapping happening because it depends on the missing source file.
+	// To get around this we add a build statement using the built-in phony rule
+	// for each source file, which will cause Ninja to treat it as dirty if its
+	// missing.
+	for _, src := range srcs {
+		ctx.Build(blueprint.BuildParams{
+			Rule:    blueprint.Phony,
+			Outputs: []string{src},
+		})
+	}
+}
+
+type singleton struct{}
+
+func newSingleton() *singleton {
+	return &singleton{}
+}
+
+func (s *singleton) GenerateBuildActions(ctx blueprint.SingletonContext) {
+	// Find the module that's marked as the "primary builder", which means it's
+	// creating the binary that we'll use to generate the non-bootstrap
+	// build.ninja file.
+	var primaryBuilders []*goBinary
+	ctx.VisitAllModulesIf(isBootstrapBinaryModule,
+		func(module blueprint.Module) {
+			binaryModule := module.(*goBinary)
+			if binaryModule.properties.PrimaryBuilder {
+				primaryBuilders = append(primaryBuilders, binaryModule)
+			}
+		})
+
+	var primaryBuilderName, primaryBuilderExtraFlags string
+	switch len(primaryBuilders) {
+	case 0:
+		// If there's no primary builder module then that means we'll use minibp
+		// as the primary builder.  We can trigger its primary builder mode with
+		// the -p flag.
+		primaryBuilderName = "minibp"
+		primaryBuilderExtraFlags = "-p"
+
+	case 1:
+		primaryBuilderName = ctx.ModuleName(primaryBuilders[0])
+
+	default:
+		ctx.Errorf("multiple primary builder modules present:")
+		for _, primaryBuilder := range primaryBuilders {
+			ctx.ModuleErrorf(primaryBuilder, "<-- module %s",
+				ctx.ModuleName(primaryBuilder))
+		}
+		return
+	}
+
+	primaryBuilderFile := filepath.Join(binDir, primaryBuilderName)
+
+	// Get the filename of the top-level Blueprints file to pass to minibp.
+	// This comes stored in a global variable that's set by Main.
+	topLevelBlueprints := filepath.Join("$SrcDir",
+		filepath.Base(topLevelBlueprintsFile))
+
+	tmpNinjaFile := filepath.Join("bootstrap", "build.ninja.in")
+	tmpNinjaDepFile := tmpNinjaFile + ".d"
+
+	if generatingBootstrapper(ctx.Config()) {
+		// We're generating a bootstrapper Ninja file, so we need to set things
+		// up to rebuild the build.ninja file using the primary builder.
+
+		// We generate the depfile here that includes the dependencies for all
+		// the Blueprints files that contribute to generating the big build
+		// manifest (build.ninja file).  This depfile will be used by the non-
+		// bootstrap build manifest to determine whether it should trigger a re-
+		// bootstrap.  Because the re-bootstrap rule's output is "build.ninja"
+		// we need to force the depfile to have that as its "make target"
+		// (recall that depfiles use a subset of the Makefile syntax).
+		bigbp := ctx.Rule("bigbp",
+			blueprint.RuleParams{
+				Command: fmt.Sprintf("%s %s -d %s -o $out $in",
+					primaryBuilderFile, primaryBuilderExtraFlags,
+					tmpNinjaDepFile),
+				Description: fmt.Sprintf("%s $out", primaryBuilderName),
+				Depfile:     tmpNinjaDepFile,
+			})
+
+		ctx.Build(blueprint.BuildParams{
+			Rule:      bigbp,
+			Outputs:   []string{tmpNinjaFile},
+			Inputs:    []string{topLevelBlueprints},
+			Implicits: []string{primaryBuilderFile},
+		})
+
+		// When the current build.ninja file is a bootstrapper, we always want
+		// to have it replace itself with a non-bootstrapper build.ninja.  To
+		// accomplish that we depend on a file that should never exist and
+		// "build" it using Ninja's built-in phony rule.
+		//
+		// We also need to add an implicit dependency on the minibp binary so
+		// that it actually gets built.  Nothing in the bootstrap build.ninja
+		// file actually requires minibp, but the non-bootstrap build.ninja
+		// requires that it have been built during the bootstrapping.
+		notAFile := filepath.Join("bootstrap", "notAFile")
+		ctx.Build(blueprint.BuildParams{
+			Rule:    blueprint.Phony,
+			Outputs: []string{notAFile},
+		})
+
+		ctx.Build(blueprint.BuildParams{
+			Rule:      bootstrap,
+			Outputs:   []string{"build.ninja"},
+			Inputs:    []string{tmpNinjaFile},
+			Implicits: []string{"$Bootstrap", notAFile, minibpFile},
+		})
+
+		// Because the non-bootstrap build.ninja file manually re-invokes Ninja,
+		// its builddir must be different than that of the bootstrap build.ninja
+		// file.  Otherwise we occasionally get "warning: bad deps log signature
+		// or version; starting over" messages from Ninja, presumably because
+		// two Ninja processes try to write to the same log concurrently.
+		ctx.SetBuildDir("bootstrap")
+	} else {
+		// We're generating a non-bootstrapper Ninja file, so we need to set it
+		// up to depend on the bootstrapper Ninja file.  The build.ninja target
+		// also has an implicit dependency on the primary builder, which will
+		// have a phony dependency on all its sources.  This will cause any
+		// changes to the primary builder's sources to trigger a re-bootstrap
+		// operation, which will rebuild the primary builder.
+		//
+		// On top of that we need to use the depfile generated by the bigbp
+		// rule.  We do this by depending on that file and then setting up a
+		// phony rule to generate it that uses the depfile.
+		ctx.Build(blueprint.BuildParams{
+			Rule:      rebootstrap,
+			Outputs:   []string{"build.ninja"},
+			Inputs:    []string{"$BootstrapManifest"},
+			Implicits: []string{"$Bootstrap", primaryBuilderFile, tmpNinjaFile},
+		})
+
+		ctx.Build(blueprint.BuildParams{
+			Rule:    phony,
+			Outputs: []string{tmpNinjaFile},
+			Inputs:  []string{topLevelBlueprints},
+			Args: map[string]string{
+				"depfile": tmpNinjaDepFile,
+			},
+		})
+
+		// Rebuild the bootstrap Ninja file using minibp, passing it all the
+		// Blueprint files that define a bootstrap_* module.
+		ctx.Build(blueprint.BuildParams{
+			Rule:      minibp,
+			Outputs:   []string{"$BootstrapManifest"},
+			Inputs:    []string{topLevelBlueprints},
+			Implicits: []string{minibpFile},
+		})
+	}
+}
+
+// packageRoot returns the module-specific package root directory path.  This
+// directory is where the final package .a files are output and where dependant
+// modules search for this package via -I arguments.
+func packageRoot(ctx blueprint.ModuleContext) string {
+	return filepath.Join("bootstrap", ctx.ModuleName(), "pkg")
+}
+
+// srcDir returns the path of the directory that all source file paths are
+// specified relative to.
+func srcDir(ctx blueprint.ModuleContext) string {
+	return filepath.Join("$SrcDir", ctx.ModuleDir())
+}
+
+// objDir returns the module-specific object directory path.
+func objDir(ctx blueprint.ModuleContext) string {
+	return filepath.Join("bootstrap", ctx.ModuleName(), "obj")
+}
+
+// PrefixPaths returns a list of paths consisting of prefix joined with each
+// element of paths.  The resulting paths are "clean" in the filepath.Clean
+// sense.
+//
+// TODO: This should probably go in a utility package.
+func PrefixPaths(paths []string, prefix string) []string {
+	result := make([]string, len(paths))
+	for i, path := range paths {
+		result[i] = filepath.Clean(filepath.Join(prefix, path))
+	}
+	return result
+}
diff --git a/blueprint/bootstrap/command.go b/blueprint/bootstrap/command.go
new file mode 100644
index 0000000..9a0c32e
--- /dev/null
+++ b/blueprint/bootstrap/command.go
@@ -0,0 +1,147 @@
+package bootstrap
+
+import (
+	"blueprint"
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+)
+
+var outFile string
+var depFile string
+var depTarget string
+
+// topLevelBlueprintsFile is set by Main as a way to pass this information on to
+// the bootstrap build manifest generators.  This information was not passed via
+// the Config object so as to allow the caller of Main to use whatever Config
+// object it wants.
+var topLevelBlueprintsFile string
+
+func init() {
+	flag.StringVar(&outFile, "o", "build.ninja.in", "the Ninja file to output")
+	flag.StringVar(&depFile, "d", "", "the dependency file to output")
+	flag.StringVar(&depTarget, "t", "", "the target name for the dependency "+
+		"file")
+}
+
+func Main(ctx *blueprint.Context, config blueprint.Config) {
+	if !flag.Parsed() {
+		flag.Parse()
+	}
+
+	ctx.RegisterModuleType("bootstrap_go_package", goPackageModule)
+	ctx.RegisterModuleType("bootstrap_go_binary", goBinaryModule)
+	ctx.RegisterSingleton("bootstrap", newSingleton())
+
+	if flag.NArg() != 1 {
+		fatalf("no Blueprints file specified")
+	}
+
+	topLevelBlueprintsFile = flag.Arg(0)
+
+	deps, errs := ctx.ParseBlueprintsFiles(topLevelBlueprintsFile)
+	if len(errs) > 0 {
+		fatalErrors(errs)
+	}
+
+	errs = ctx.PrepareBuildActions(config)
+	if len(errs) > 0 {
+		fatalErrors(errs)
+	}
+
+	buf := bytes.NewBuffer(nil)
+	err := ctx.WriteBuildFile(buf)
+	if err != nil {
+		fatalf("error generating Ninja file contents: %s", err)
+	}
+
+	err = writeFileIfChanged(outFile, buf.Bytes(), 0666)
+	if err != nil {
+		fatalf("error writing %s: %s", outFile, err)
+	}
+
+	if depFile != "" {
+		f, err := os.Create(depFile)
+		if err != nil {
+			fatalf("error creating depfile: %s", err)
+		}
+
+		target := depTarget
+		if target == "" {
+			target = outFile
+		}
+
+		_, err = fmt.Fprintf(f, "%s: \\\n %s\n", target,
+			strings.Join(deps, " \\\n "))
+		if err != nil {
+			fatalf("error writing depfile: %s", err)
+		}
+
+		f.Close()
+	}
+
+	os.Exit(0)
+}
+
+func fatalf(format string, args ...interface{}) {
+	fmt.Fprintf(os.Stderr, format, args...)
+	os.Exit(1)
+}
+
+func fatalErrors(errs []error) {
+	for _, err := range errs {
+		switch err.(type) {
+		case *blueprint.Error:
+			_, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+		default:
+			_, _ = fmt.Fprintf(os.Stderr, "internal error: %s\n", err)
+		}
+	}
+	os.Exit(1)
+}
+
+func writeFileIfChanged(filename string, data []byte, perm os.FileMode) error {
+	var isChanged bool
+
+	info, err := os.Stat(filename)
+	if err != nil {
+		if os.IsNotExist(err) {
+			// The file does not exist yet.
+			isChanged = true
+		} else {
+			return err
+		}
+	} else {
+		if info.Size() != int64(len(data)) {
+			isChanged = true
+		} else {
+			oldData, err := ioutil.ReadFile(filename)
+			if err != nil {
+				return err
+			}
+
+			if len(oldData) != len(data) {
+				isChanged = true
+			} else {
+				for i := range data {
+					if oldData[i] != data[i] {
+						isChanged = true
+						break
+					}
+				}
+			}
+		}
+	}
+
+	if isChanged {
+		err = ioutil.WriteFile(filename, data, perm)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/blueprint/bootstrap/config.go b/blueprint/bootstrap/config.go
new file mode 100644
index 0000000..2f41499
--- /dev/null
+++ b/blueprint/bootstrap/config.go
@@ -0,0 +1,29 @@
+package bootstrap
+
+import (
+	"blueprint"
+)
+
+var (
+	// These variables are the only only configuration needed by the boostrap
+	// modules.  They are always set to the variable name enclosed in "@@" so
+	// that their values can be easily replaced in the generated Ninja file.
+	SrcDir            = blueprint.StaticVariable("SrcDir", "@@SrcDir@@")
+	GoRoot            = blueprint.StaticVariable("GoRoot", "@@GoRoot@@")
+	GoOS              = blueprint.StaticVariable("GoOS", "@@GoOS@@")
+	GoArch            = blueprint.StaticVariable("GoArch", "@@GoArch@@")
+	GoChar            = blueprint.StaticVariable("GoChar", "@@GoChar@@")
+	Bootstrap         = blueprint.StaticVariable("Bootstrap", "@@Bootstrap@@")
+	BootstrapManifest = blueprint.StaticVariable("BootstrapManifest",
+		"@@BootstrapManifest@@")
+
+	goToolDir = blueprint.StaticVariable("goToolDir",
+		"$GoRoot/pkg/tool/${GoOS}_$GoArch")
+)
+
+type Config interface {
+	// GeneratingBootstrapper should return true if this build invocation is
+	// creating a build.ninja.in file to be used in a build bootstrapping
+	// sequence.
+	GeneratingBootstrapper() bool
+}
diff --git a/blueprint/bootstrap/doc.go b/blueprint/bootstrap/doc.go
new file mode 100644
index 0000000..bb7771c
--- /dev/null
+++ b/blueprint/bootstrap/doc.go
@@ -0,0 +1,29 @@
+/*
+
+The Blueprint bootstrapping mechanism is intended to enable building a source
+tree using a Blueprint-based build system that is embedded (as source) in that
+source tree.  The only prerequisites for performing such a build are:
+	1. A Ninja binary
+	2. A script interpreter (e.g. Bash or Python)
+	3. A Go toolchain
+
+The bootstrapping process is intended to be customized for the source tree in
+which it is embedded.  As an initial example, we'll examine the bootstrapping
+system used to build the core Blueprint packages.  Bootstrapping the core
+Blueprint packages involves two files:
+
+	bootstrap.bash:		When this script is run it initializes the current
+						working directory as a build output directory.  It does
+						this by first automatically determining the root source
+						directory and Go build environment.  It then uses those
+						values to do a simple string replacement over the
+						build.ninja.in file contents, and places the result into
+						the current working directory.
+
+	build.ninja.in:		This file is generated by passing all the Blueprint
+
+						files needed to construct the primary builder
+
+
+*/
+package bootstrap
diff --git a/blueprint/bootstrap/minibp/main.go b/blueprint/bootstrap/minibp/main.go
new file mode 100644
index 0000000..c19dd4f
--- /dev/null
+++ b/blueprint/bootstrap/minibp/main.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+	"blueprint"
+	"blueprint/bootstrap"
+	"flag"
+)
+
+var runAsPrimaryBuilder bool
+
+func init() {
+	flag.BoolVar(&runAsPrimaryBuilder, "p", false, "run as a primary builder")
+}
+
+type Config bool
+
+func (c Config) GeneratingBootstrapper() bool {
+	return bool(c)
+}
+
+func main() {
+	flag.Parse()
+
+	ctx := blueprint.NewContext()
+	if !runAsPrimaryBuilder {
+		ctx.SetIgnoreUnknownModuleTypes(true)
+	}
+
+	config := Config(!runAsPrimaryBuilder)
+
+	bootstrap.Main(ctx, config)
+}
diff --git a/blueprint/context.go b/blueprint/context.go
new file mode 100644
index 0000000..5af899f
--- /dev/null
+++ b/blueprint/context.go
@@ -0,0 +1,1363 @@
+package blueprint
+
+import (
+	"blueprint/parser"
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"reflect"
+	"sort"
+	"strings"
+	"text/scanner"
+	"text/template"
+)
+
+var ErrBuildActionsNotReady = errors.New("build actions are not ready")
+
+const maxErrors = 10
+
+type Context struct {
+	// set at instantiation
+	moduleTypes   map[string]ModuleType
+	modules       map[string]Module
+	moduleInfo    map[Module]*moduleInfo
+	singletonInfo map[string]*singletonInfo
+
+	dependenciesReady bool // set to true on a successful ResolveDependencies
+	buildActionsReady bool // set to true on a successful PrepareBuildActions
+
+	// set by SetIgnoreUnknownModuleTypes
+	ignoreUnknownModuleTypes bool
+
+	// set during PrepareBuildActions
+	pkgNames        map[*pkg]string
+	globalVariables map[Variable]*ninjaString
+	globalPools     map[Pool]*poolDef
+	globalRules     map[Rule]*ruleDef
+
+	// set during PrepareBuildActions
+	buildDir           *ninjaString // The builddir special Ninja variable
+	requiredNinjaMajor int          // For the ninja_required_version variable
+	requiredNinjaMinor int          // For the ninja_required_version variable
+	requiredNinjaMicro int          // For the ninja_required_version variable
+}
+
+// A Config contains build configuration information that can affect the
+// contents of the Ninja build file is that will be generated.  The specific
+// representation of this configuration information is not defined here.
+type Config interface{}
+
+type Error struct {
+	Err error
+	Pos scanner.Position
+}
+
+type localBuildActions struct {
+	variables []*localVariable
+	rules     []*localRule
+	buildDefs []*buildDef
+}
+
+type moduleInfo struct {
+	// set during Parse
+	typeName         string
+	typ              ModuleType
+	relBlueprintFile string
+	pos              scanner.Position
+	propertyPos      map[string]scanner.Position
+	properties       struct {
+		Name string
+		Deps []string
+	}
+
+	// set during ResolveDependencies
+	directDeps []Module
+
+	// set during PrepareBuildActions
+	actionDefs localBuildActions
+}
+
+type singletonInfo struct {
+	// set during RegisterSingleton
+	singleton Singleton
+
+	// set during PrepareBuildActions
+	actionDefs localBuildActions
+}
+
+func (e *Error) Error() string {
+
+	return fmt.Sprintf("%s: %s", e.Pos, e.Err)
+}
+
+func NewContext() *Context {
+	return &Context{
+		moduleTypes:   make(map[string]ModuleType),
+		modules:       make(map[string]Module),
+		moduleInfo:    make(map[Module]*moduleInfo),
+		singletonInfo: make(map[string]*singletonInfo),
+	}
+}
+
+func (c *Context) RegisterModuleType(name string, typ ModuleType) {
+	if _, present := c.moduleTypes[name]; present {
+		panic(errors.New("module type name is already registered"))
+	}
+	c.moduleTypes[name] = typ
+}
+
+func (c *Context) RegisterSingleton(name string, singleton Singleton) {
+	if _, present := c.singletonInfo[name]; present {
+		panic(errors.New("singleton name is already registered"))
+	}
+	if singletonPkgPath(singleton) == "" {
+		panic(errors.New("singleton types must be a named type"))
+	}
+	c.singletonInfo[name] = &singletonInfo{
+		singleton: singleton,
+	}
+}
+
+func singletonPkgPath(singleton Singleton) string {
+	typ := reflect.TypeOf(singleton)
+	for typ.Kind() == reflect.Ptr {
+		typ = typ.Elem()
+	}
+	return typ.PkgPath()
+}
+
+func singletonTypeName(singleton Singleton) string {
+	typ := reflect.TypeOf(singleton)
+	for typ.Kind() == reflect.Ptr {
+		typ = typ.Elem()
+	}
+	return typ.PkgPath() + "." + typ.Name()
+}
+
+func (c *Context) SetIgnoreUnknownModuleTypes(ignoreUnknownModuleTypes bool) {
+	c.ignoreUnknownModuleTypes = ignoreUnknownModuleTypes
+}
+
+func (c *Context) Parse(rootDir, filename string, r io.Reader) (subdirs []string,
+	errs []error) {
+
+	c.dependenciesReady = false
+
+	relBlueprintFile, err := filepath.Rel(rootDir, filename)
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	defs, errs := parser.Parse(filename, r)
+	if len(errs) > 0 {
+		for i, err := range errs {
+			if parseErr, ok := err.(*parser.ParseError); ok {
+				err = &Error{
+					Err: parseErr.Err,
+					Pos: parseErr.Pos,
+				}
+				errs[i] = err
+			}
+		}
+
+		// If there were any parse errors don't bother trying to interpret the
+		// result.
+		return nil, errs
+	}
+
+	for _, def := range defs {
+		var newErrs []error
+		switch def := def.(type) {
+		case *parser.Module:
+			newErrs = c.processModuleDef(def, relBlueprintFile)
+
+		case *parser.Assignment:
+			var newSubdirs []string
+			newSubdirs, newErrs = c.processAssignment(def)
+			if newSubdirs != nil {
+				subdirs = newSubdirs
+			}
+
+		default:
+			panic("unknown definition type")
+		}
+
+		if len(newErrs) > 0 {
+			errs = append(errs, newErrs...)
+			if len(errs) > maxErrors {
+				break
+			}
+		}
+	}
+
+	return subdirs, errs
+}
+
+func (c *Context) ParseBlueprintsFiles(rootFile string) (deps []string,
+	errs []error) {
+
+	rootDir := filepath.Dir(rootFile)
+
+	depsSet := map[string]bool{rootFile: true}
+	blueprints := []string{rootFile}
+
+	var file *os.File
+	defer func() {
+		if file != nil {
+			file.Close()
+		}
+	}()
+
+	var err error
+
+	for i := 0; i < len(blueprints); i++ {
+		if len(errs) > maxErrors {
+			return
+		}
+
+		filename := blueprints[i]
+		dir := filepath.Dir(filename)
+
+		file, err = os.Open(filename)
+		if err != nil {
+			errs = append(errs, &Error{Err: err})
+			continue
+		}
+
+		subdirs, newErrs := c.Parse(rootDir, filename, file)
+		if len(newErrs) > 0 {
+			errs = append(errs, newErrs...)
+			continue
+		}
+
+		err = file.Close()
+		if err != nil {
+			errs = append(errs, &Error{Err: err})
+			continue
+		}
+
+		// Add the subdirs to the list of directories to parse Blueprint files
+		// from.
+		for _, subdir := range subdirs {
+			subdir = filepath.Join(dir, subdir)
+			dirPart, filePart := filepath.Split(subdir)
+			dirPart = filepath.Clean(dirPart)
+
+			if filePart == "*" {
+				foundSubdirs, err := listSubdirs(dirPart)
+				if err != nil {
+					errs = append(errs, &Error{Err: err})
+					continue
+				}
+
+				for _, foundSubdir := range foundSubdirs {
+					subBlueprints := filepath.Join(dirPart, foundSubdir,
+						"Blueprints")
+
+					_, err := os.Stat(subBlueprints)
+					if os.IsNotExist(err) {
+						// There is no Blueprints file in this subdirectory.  We
+						// need to add the directory to the list of dependencies
+						// so that if someone adds a Blueprints file in the
+						// future we'll pick it up.
+						depsSet[filepath.Dir(subBlueprints)] = true
+					} else if !depsSet[subBlueprints] {
+						// We haven't seen this Blueprints file before, so add
+						// it to our list.
+						depsSet[subBlueprints] = true
+						blueprints = append(blueprints, subBlueprints)
+					}
+				}
+
+				// We now depend on the directory itself because if any new
+				// subdirectories get added or removed we need to rebuild the
+				// Ninja manifest.
+				depsSet[dirPart] = true
+			} else {
+				subBlueprints := filepath.Join(subdir, "Blueprints")
+				if !depsSet[subBlueprints] {
+					depsSet[subBlueprints] = true
+					blueprints = append(blueprints, subBlueprints)
+				}
+			}
+		}
+	}
+
+	for dep := range depsSet {
+		deps = append(deps, dep)
+	}
+
+	return
+}
+
+func listSubdirs(dir string) ([]string, error) {
+	d, err := os.Open(dir)
+	if err != nil {
+		return nil, err
+	}
+	defer d.Close()
+
+	infos, err := d.Readdir(-1)
+	if err != nil {
+		return nil, err
+	}
+
+	var subdirs []string
+	for _, info := range infos {
+		if info.IsDir() {
+			subdirs = append(subdirs, info.Name())
+		}
+	}
+
+	return subdirs, nil
+}
+
+func (c *Context) processAssignment(
+	assignment *parser.Assignment) (subdirs []string, errs []error) {
+
+	if assignment.Name == "subdirs" {
+		switch assignment.Value.Type {
+		case parser.List:
+			subdirs = make([]string, 0, len(assignment.Value.ListValue))
+
+			for _, value := range assignment.Value.ListValue {
+				if value.Type != parser.String {
+					// The parser should not produce this.
+					panic("non-string value found in list")
+				}
+
+				dirPart, filePart := filepath.Split(value.StringValue)
+				if (filePart != "*" && strings.ContainsRune(filePart, '*')) ||
+					strings.ContainsRune(dirPart, '*') {
+
+					errs = append(errs, &Error{
+						Err: fmt.Errorf("subdirs may only wildcard whole " +
+							"directories"),
+						Pos: value.Pos,
+					})
+
+					continue
+				}
+
+				subdirs = append(subdirs, value.StringValue)
+			}
+
+			if len(errs) > 0 {
+				subdirs = nil
+			}
+
+			return
+
+		case parser.Bool, parser.String:
+			errs = []error{
+				&Error{
+					Err: fmt.Errorf("subdirs must be a list of strings"),
+					Pos: assignment.Pos,
+				},
+			}
+
+			return
+
+		default:
+			panic(fmt.Errorf("unknown value type: %d", assignment.Value.Type))
+		}
+	}
+
+	return nil, []error{
+		&Error{
+			Err: fmt.Errorf("only 'subdirs' assignment is supported"),
+			Pos: assignment.Pos,
+		},
+	}
+}
+
+func (c *Context) processModuleDef(moduleDef *parser.Module,
+	relBlueprintFile string) []error {
+
+	typeName := moduleDef.Type
+	typ, ok := c.moduleTypes[typeName]
+	if !ok {
+		if c.ignoreUnknownModuleTypes {
+			return nil
+		}
+
+		err := fmt.Errorf("unrecognized module type %q", typeName)
+		return []error{err}
+	}
+
+	module, properties := typ.new()
+	info := &moduleInfo{
+		typeName:         typeName,
+		typ:              typ,
+		relBlueprintFile: relBlueprintFile,
+	}
+
+	errs := unpackProperties(moduleDef.Properties, &info.properties,
+		properties)
+	if len(errs) > 0 {
+		return errs
+	}
+
+	info.pos = moduleDef.Pos
+	info.propertyPos = make(map[string]scanner.Position)
+	for _, propertyDef := range moduleDef.Properties {
+		info.propertyPos[propertyDef.Name] = propertyDef.Pos
+	}
+
+	name := info.properties.Name
+	err := validateNinjaName(name)
+	if err != nil {
+		return []error{
+			&Error{
+				Err: fmt.Errorf("invalid module name %q: %s", err),
+				Pos: info.propertyPos["name"],
+			},
+		}
+	}
+
+	if first, present := c.modules[name]; present {
+		errs = append(errs, &Error{
+			Err: fmt.Errorf("module %q already defined", name),
+			Pos: moduleDef.Pos,
+		})
+		errs = append(errs, &Error{
+			Err: fmt.Errorf("<-- previous definition here"),
+			Pos: c.moduleInfo[first].pos,
+		})
+		if len(errs) >= maxErrors {
+			return errs
+		}
+	}
+
+	c.modules[name] = module
+	c.moduleInfo[module] = info
+
+	return nil
+}
+
+func (c *Context) ResolveDependencies() []error {
+	errs := c.resolveDependencies()
+	if len(errs) > 0 {
+		return errs
+	}
+
+	errs = c.checkForDependencyCycles()
+	if len(errs) > 0 {
+		return errs
+	}
+
+	c.dependenciesReady = true
+	return nil
+}
+
+// resolveDependencies populates the moduleInfo.directDeps list for every
+// module.  In doing so it checks for missing dependencies and self-dependant
+// modules.
+func (c *Context) resolveDependencies() (errs []error) {
+	for _, info := range c.moduleInfo {
+		depNames := info.properties.Deps
+		info.directDeps = make([]Module, 0, len(depNames))
+		depsPos := info.propertyPos["deps"]
+
+		for _, depName := range depNames {
+			if depName == info.properties.Name {
+				errs = append(errs, &Error{
+					Err: fmt.Errorf("%q depends on itself", depName),
+					Pos: depsPos,
+				})
+				continue
+			}
+
+			dep, ok := c.modules[depName]
+			if !ok {
+				errs = append(errs, &Error{
+					Err: fmt.Errorf("%q depends on undefined module %q",
+						info.properties.Name, depName),
+					Pos: depsPos,
+				})
+				continue
+			}
+
+			info.directDeps = append(info.directDeps, dep)
+		}
+	}
+
+	return
+}
+
+// checkForDependencyCycles recursively walks the module dependency graph and
+// reports errors when it encounters dependency cycles.  This should only be
+// called after resolveDependencies.
+func (c *Context) checkForDependencyCycles() (errs []error) {
+	visited := make(map[Module]bool)  // modules that were already checked
+	checking := make(map[Module]bool) // modules actively being checked
+
+	var check func(m Module) []Module
+
+	check = func(m Module) []Module {
+		info := c.moduleInfo[m]
+
+		visited[m] = true
+		checking[m] = true
+		defer delete(checking, m)
+
+		for _, dep := range info.directDeps {
+			if checking[dep] {
+				// This is a cycle.
+				return []Module{dep, m}
+			}
+
+			if !visited[dep] {
+				cycle := check(dep)
+				if cycle != nil {
+					if cycle[0] == m {
+						// We are the "start" of the cycle, so we're responsible
+						// for generating the errors.  The cycle list is in
+						// reverse order because all the 'check' calls append
+						// their own module to the list.
+						errs = append(errs, &Error{
+							Err: fmt.Errorf("encountered dependency cycle:"),
+							Pos: info.pos,
+						})
+
+						// Iterate backwards through the cycle list.
+						curInfo := info
+						for i := len(cycle) - 1; i >= 0; i-- {
+							nextInfo := c.moduleInfo[cycle[i]]
+							errs = append(errs, &Error{
+								Err: fmt.Errorf("    %q depends on %q",
+									curInfo.properties.Name,
+									nextInfo.properties.Name),
+								Pos: curInfo.propertyPos["deps"],
+							})
+							curInfo = nextInfo
+						}
+
+						// We can continue processing this module's children to
+						// find more cycles.  Since all the modules that were
+						// part of the found cycle were marked as visited we
+						// won't run into that cycle again.
+					} else {
+						// We're not the "start" of the cycle, so we just append
+						// our module to the list and return it.
+						return append(cycle, m)
+					}
+				}
+			}
+		}
+
+		return nil
+	}
+
+	for _, module := range c.modules {
+		if !visited[module] {
+			cycle := check(module)
+			if cycle != nil {
+				panic("inconceivable!")
+			}
+		}
+	}
+
+	return
+}
+
+func (c *Context) PrepareBuildActions(config Config) []error {
+	c.buildActionsReady = false
+
+	if !c.dependenciesReady {
+		errs := c.ResolveDependencies()
+		if len(errs) > 0 {
+			return errs
+		}
+	}
+
+	liveGlobals := newLiveTracker(config)
+
+	c.initSpecialVariables()
+
+	errs := c.generateModuleBuildActions(config, liveGlobals)
+	if len(errs) > 0 {
+		return errs
+	}
+
+	errs = c.generateSingletonBuildActions(config, liveGlobals)
+	if len(errs) > 0 {
+		return errs
+	}
+
+	if c.buildDir != nil {
+		liveGlobals.addNinjaStringDeps(c.buildDir)
+	}
+
+	pkgNames := c.makeUniquePackageNames(liveGlobals)
+
+	// This will panic if it finds a problem since it's a programming error.
+	c.checkForVariableReferenceCycles(liveGlobals.variables, pkgNames)
+
+	c.pkgNames = pkgNames
+	c.globalVariables = liveGlobals.variables
+	c.globalPools = liveGlobals.pools
+	c.globalRules = liveGlobals.rules
+
+	c.buildActionsReady = true
+
+	return nil
+}
+
+func (c *Context) initSpecialVariables() {
+	c.buildDir = nil
+	c.requiredNinjaMajor = 1
+	c.requiredNinjaMinor = 1
+	c.requiredNinjaMicro = 0
+}
+
+func (c *Context) generateModuleBuildActions(config Config,
+	liveGlobals *liveTracker) []error {
+
+	visited := make(map[Module]bool)
+
+	var errs []error
+
+	var walk func(module Module)
+	walk = func(module Module) {
+		visited[module] = true
+
+		info := c.moduleInfo[module]
+		for _, dep := range info.directDeps {
+			if !visited[dep] {
+				walk(dep)
+			}
+		}
+
+		mctx := &moduleContext{
+			context: c,
+			config:  config,
+			module:  module,
+			scope: newLocalScope(info.typ.pkg().scope,
+				moduleNamespacePrefix(info.properties.Name)),
+			info: info,
+		}
+
+		module.GenerateBuildActions(mctx)
+
+		if len(mctx.errs) > 0 {
+			errs = append(errs, mctx.errs...)
+			return
+		}
+
+		newErrs := c.processLocalBuildActions(&info.actionDefs,
+			&mctx.actionDefs, liveGlobals)
+		errs = append(errs, newErrs...)
+	}
+
+	for _, module := range c.modules {
+		if !visited[module] {
+			walk(module)
+		}
+	}
+
+	return errs
+}
+
+func (c *Context) generateSingletonBuildActions(config Config,
+	liveGlobals *liveTracker) []error {
+
+	var errs []error
+	for name, info := range c.singletonInfo {
+		// If the package to which the singleton type belongs has not defined
+		// any Ninja globals and has not called Import() then we won't have an
+		// entry for it in the pkgs map.  If that's the case then the
+		// singleton's scope's parent should just be nil.
+		var singletonScope *scope
+		if pkg := pkgs[singletonPkgPath(info.singleton)]; pkg != nil {
+			singletonScope = pkg.scope
+		}
+
+		sctx := &singletonContext{
+			context: c,
+			config:  config,
+			scope: newLocalScope(singletonScope,
+				singletonNamespacePrefix(name)),
+		}
+
+		info.singleton.GenerateBuildActions(sctx)
+
+		if len(sctx.errs) > 0 {
+			errs = append(errs, sctx.errs...)
+			if len(errs) > maxErrors {
+				break
+			}
+			continue
+		}
+
+		newErrs := c.processLocalBuildActions(&info.actionDefs,
+			&sctx.actionDefs, liveGlobals)
+		errs = append(errs, newErrs...)
+		if len(errs) > maxErrors {
+			break
+		}
+	}
+
+	return errs
+}
+
+func (c *Context) processLocalBuildActions(out, in *localBuildActions,
+	liveGlobals *liveTracker) []error {
+
+	var errs []error
+
+	// First we go through and add everything referenced by the module's
+	// buildDefs to the live globals set.  This will end up adding the live
+	// locals to the set as well, but we'll take them out after.
+	for _, def := range in.buildDefs {
+		err := liveGlobals.AddBuildDefDeps(def)
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+
+	out.buildDefs = in.buildDefs
+
+	// We use the now-incorrect set of live "globals" to determine which local
+	// definitions are live.  As we go through copying those live locals to the
+	// moduleInfo we remove them from the live globals set.
+	out.variables = nil
+	for _, v := range in.variables {
+		_, isLive := liveGlobals.variables[v]
+		if isLive {
+			out.variables = append(out.variables, v)
+			delete(liveGlobals.variables, v)
+		}
+	}
+
+	out.rules = nil
+	for _, r := range in.rules {
+		_, isLive := liveGlobals.rules[r]
+		if isLive {
+			out.rules = append(out.rules, r)
+			delete(liveGlobals.rules, r)
+		}
+	}
+
+	return nil
+}
+
+func (c *Context) visitDepsDepthFirst(module Module, visit func(Module)) {
+	visited := make(map[Module]bool)
+
+	var walk func(m Module)
+	walk = func(m Module) {
+		info := c.moduleInfo[m]
+		visited[m] = true
+		for _, dep := range info.directDeps {
+			if !visited[dep] {
+				walk(dep)
+			}
+		}
+		visit(m)
+	}
+
+	info := c.moduleInfo[module]
+	for _, dep := range info.directDeps {
+		if !visited[dep] {
+			walk(dep)
+		}
+	}
+}
+
+func (c *Context) visitDepsDepthFirstIf(module Module, pred func(Module) bool,
+	visit func(Module)) {
+
+	visited := make(map[Module]bool)
+
+	var walk func(m Module)
+	walk = func(m Module) {
+		info := c.moduleInfo[m]
+		visited[m] = true
+		if pred(m) {
+			for _, dep := range info.directDeps {
+				if !visited[dep] {
+					walk(dep)
+				}
+			}
+			visit(m)
+		}
+	}
+
+	info := c.moduleInfo[module]
+	for _, dep := range info.directDeps {
+		if !visited[dep] {
+			walk(dep)
+		}
+	}
+}
+
+func (c *Context) visitAllModules(visit func(Module)) {
+	for _, module := range c.modules {
+		visit(module)
+	}
+}
+
+func (c *Context) visitAllModulesIf(pred func(Module) bool,
+	visit func(Module)) {
+
+	for _, module := range c.modules {
+		if pred(module) {
+			visit(module)
+		}
+	}
+}
+
+func (c *Context) requireNinjaVersion(major, minor, micro int) {
+	if major != 1 {
+		panic("ninja version with major version != 1 not supported")
+	}
+	if c.requiredNinjaMinor < minor {
+		c.requiredNinjaMinor = minor
+		c.requiredNinjaMicro = micro
+	}
+	if c.requiredNinjaMinor == minor && c.requiredNinjaMicro < micro {
+		c.requiredNinjaMicro = micro
+	}
+}
+
+func (c *Context) setBuildDir(value *ninjaString) {
+	if c.buildDir != nil {
+		panic("buildDir set multiple times")
+	}
+	c.buildDir = value
+}
+
+func (c *Context) makeUniquePackageNames(
+	liveGlobals *liveTracker) map[*pkg]string {
+
+	pkgs := make(map[string]*pkg)
+	pkgNames := make(map[*pkg]string)
+	longPkgNames := make(map[*pkg]bool)
+
+	processPackage := func(pkg *pkg) {
+		if pkg == nil {
+			// This is a built-in rule and has no package.
+			return
+		}
+		if _, ok := pkgNames[pkg]; ok {
+			// We've already processed this package.
+			return
+		}
+
+		otherPkg, present := pkgs[pkg.shortName]
+		if present {
+			// Short name collision.  Both this package and the one that's
+			// already there need to use their full names.  We leave the short
+			// name in pkgNames for now so future collisions still get caught.
+			longPkgNames[pkg] = true
+			longPkgNames[otherPkg] = true
+		} else {
+			// No collision so far.  Tentatively set the package's name to be
+			// its short name.
+			pkgNames[pkg] = pkg.shortName
+		}
+	}
+
+	// We try to give all packages their short name, but when we get collisions
+	// we need to use the full unique package name.
+	for v, _ := range liveGlobals.variables {
+		processPackage(v.pkg())
+	}
+	for p, _ := range liveGlobals.pools {
+		processPackage(p.pkg())
+	}
+	for r, _ := range liveGlobals.rules {
+		processPackage(r.pkg())
+	}
+
+	// Add the packages that had collisions using their full unique names.  This
+	// will overwrite any short names that were added in the previous step.
+	for pkg := range longPkgNames {
+		pkgNames[pkg] = pkg.fullName
+	}
+
+	return pkgNames
+}
+
+func (c *Context) checkForVariableReferenceCycles(
+	variables map[Variable]*ninjaString, pkgNames map[*pkg]string) {
+
+	visited := make(map[Variable]bool)  // variables that were already checked
+	checking := make(map[Variable]bool) // variables actively being checked
+
+	var check func(v Variable) []Variable
+
+	check = func(v Variable) []Variable {
+		visited[v] = true
+		checking[v] = true
+		defer delete(checking, v)
+
+		value := variables[v]
+		for _, dep := range value.variables {
+			if checking[dep] {
+				// This is a cycle.
+				return []Variable{dep, v}
+			}
+
+			if !visited[dep] {
+				cycle := check(dep)
+				if cycle != nil {
+					if cycle[0] == v {
+						// We are the "start" of the cycle, so we're responsible
+						// for generating the errors.  The cycle list is in
+						// reverse order because all the 'check' calls append
+						// their own module to the list.
+						msgs := []string{"detected variable reference cycle:"}
+
+						// Iterate backwards through the cycle list.
+						curName := v.fullName(pkgNames)
+						curValue := value.Value(pkgNames)
+						for i := len(cycle) - 1; i >= 0; i-- {
+							next := cycle[i]
+							nextName := next.fullName(pkgNames)
+							nextValue := variables[next].Value(pkgNames)
+
+							msgs = append(msgs, fmt.Sprintf(
+								"    %q depends on %q", curName, nextName))
+							msgs = append(msgs, fmt.Sprintf(
+								"    [%s = %s]", curName, curValue))
+
+							curName = nextName
+							curValue = nextValue
+						}
+
+						// Variable reference cycles are a programming error,
+						// not the fault of the Blueprint file authors.
+						panic(strings.Join(msgs, "\n"))
+					} else {
+						// We're not the "start" of the cycle, so we just append
+						// our module to the list and return it.
+						return append(cycle, v)
+					}
+				}
+			}
+		}
+
+		return nil
+	}
+
+	for v := range variables {
+		if !visited[v] {
+			cycle := check(v)
+			if cycle != nil {
+				panic("inconceivable!")
+			}
+		}
+	}
+}
+
+func (c *Context) WriteBuildFile(w io.Writer) error {
+	if !c.buildActionsReady {
+		return ErrBuildActionsNotReady
+	}
+
+	nw := newNinjaWriter(w)
+
+	err := c.writeBuildFileHeader(nw)
+	if err != nil {
+		return err
+	}
+
+	err = c.writeNinjaRequiredVersion(nw)
+	if err != nil {
+		return err
+	}
+
+	// TODO: Group the globals by package.
+
+	err = c.writeGlobalVariables(nw)
+	if err != nil {
+		return err
+	}
+
+	err = c.writeGlobalPools(nw)
+	if err != nil {
+		return err
+	}
+
+	err = c.writeBuildDir(nw)
+	if err != nil {
+		return err
+	}
+
+	err = c.writeGlobalRules(nw)
+	if err != nil {
+		return err
+	}
+
+	err = c.writeAllModuleActions(nw)
+	if err != nil {
+		return err
+	}
+
+	err = c.writeAllSingletonActions(nw)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Context) writeBuildFileHeader(nw *ninjaWriter) error {
+	headerTemplate := template.New("fileHeader")
+	_, err := headerTemplate.Parse(fileHeaderTemplate)
+	if err != nil {
+		// This is a programming error.
+		panic(err)
+	}
+
+	type pkgAssociation struct {
+		PkgName string
+		PkgPath string
+	}
+
+	var pkgs []pkgAssociation
+	maxNameLen := 0
+	for pkg, name := range c.pkgNames {
+		pkgs = append(pkgs, pkgAssociation{
+			PkgName: name,
+			PkgPath: pkg.pkgPath,
+		})
+		if len(name) > maxNameLen {
+			maxNameLen = len(name)
+		}
+	}
+
+	for i := range pkgs {
+		pkgs[i].PkgName += strings.Repeat(" ", maxNameLen-len(pkgs[i].PkgName))
+	}
+
+	params := map[string]interface{}{
+		"Pkgs": pkgs,
+	}
+
+	buf := bytes.NewBuffer(nil)
+	err = headerTemplate.Execute(buf, params)
+	if err != nil {
+		return err
+	}
+
+	return nw.Comment(buf.String())
+}
+
+func (c *Context) writeNinjaRequiredVersion(nw *ninjaWriter) error {
+	value := fmt.Sprintf("%d.%d.%d", c.requiredNinjaMajor, c.requiredNinjaMinor,
+		c.requiredNinjaMicro)
+
+	err := nw.Assign("ninja_required_version", value)
+	if err != nil {
+		return err
+	}
+
+	return nw.BlankLine()
+}
+
+func (c *Context) writeBuildDir(nw *ninjaWriter) error {
+	if c.buildDir != nil {
+		err := nw.Assign("builddir", c.buildDir.Value(c.pkgNames))
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+type variableSorter struct {
+	pkgNames  map[*pkg]string
+	variables []Variable
+}
+
+func (v *variableSorter) Len() int {
+	return len(v.variables)
+}
+
+func (v *variableSorter) Less(i, j int) bool {
+	iName := v.variables[i].fullName(v.pkgNames)
+	jName := v.variables[j].fullName(v.pkgNames)
+	return iName < jName
+}
+
+func (v *variableSorter) Swap(i, j int) {
+	v.variables[i], v.variables[j] = v.variables[j], v.variables[i]
+}
+
+func (c *Context) writeGlobalVariables(nw *ninjaWriter) error {
+	visited := make(map[Variable]bool)
+
+	var walk func(v Variable) error
+	walk = func(v Variable) error {
+		visited[v] = true
+
+		// First visit variables on which this variable depends.
+		value := c.globalVariables[v]
+		for _, dep := range value.variables {
+			if !visited[dep] {
+				err := walk(dep)
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		err := nw.Assign(v.fullName(c.pkgNames), value.Value(c.pkgNames))
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+
+	globalVariables := make([]Variable, 0, len(c.globalVariables))
+	for v := range c.globalVariables {
+		globalVariables = append(globalVariables, v)
+	}
+
+	sort.Sort(&variableSorter{c.pkgNames, globalVariables})
+
+	for _, v := range globalVariables {
+		if !visited[v] {
+			err := walk(v)
+			if err != nil {
+				return nil
+			}
+		}
+	}
+
+	return nil
+}
+
+func (c *Context) writeGlobalPools(nw *ninjaWriter) error {
+	for pool, def := range c.globalPools {
+		name := pool.fullName(c.pkgNames)
+		err := def.WriteTo(nw, name)
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *Context) writeGlobalRules(nw *ninjaWriter) error {
+	for rule, def := range c.globalRules {
+		name := rule.fullName(c.pkgNames)
+		err := def.WriteTo(nw, name, c.pkgNames)
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *Context) writeAllModuleActions(nw *ninjaWriter) error {
+	headerTemplate := template.New("moduleHeader")
+	_, err := headerTemplate.Parse(moduleHeaderTemplate)
+	if err != nil {
+		// This is a programming error.
+		panic(err)
+	}
+
+	buf := bytes.NewBuffer(nil)
+
+	for _, info := range c.moduleInfo {
+		buf.Reset()
+		infoMap := map[string]interface{}{
+			"properties": info.properties,
+			"typeName":   info.typeName,
+			"goTypeName": info.typ.name(),
+			"pos":        info.pos,
+		}
+		err = headerTemplate.Execute(buf, infoMap)
+		if err != nil {
+			return err
+		}
+
+		err = nw.Comment(buf.String())
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+
+		err = c.writeLocalBuildActions(nw, &info.actionDefs)
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *Context) writeAllSingletonActions(nw *ninjaWriter) error {
+	headerTemplate := template.New("singletonHeader")
+	_, err := headerTemplate.Parse(singletonHeaderTemplate)
+	if err != nil {
+		// This is a programming error.
+		panic(err)
+	}
+
+	buf := bytes.NewBuffer(nil)
+
+	for name, info := range c.singletonInfo {
+		buf.Reset()
+		infoMap := map[string]interface{}{
+			"name":       name,
+			"goTypeName": singletonTypeName(info.singleton),
+		}
+		err = headerTemplate.Execute(buf, infoMap)
+		if err != nil {
+			return err
+		}
+
+		err = nw.Comment(buf.String())
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+
+		err = c.writeLocalBuildActions(nw, &info.actionDefs)
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *Context) writeLocalBuildActions(nw *ninjaWriter,
+	defs *localBuildActions) error {
+
+	// Write the local variable assignments.
+	for _, v := range defs.variables {
+		// A localVariable doesn't need the package names or config to
+		// determine its name or value.
+		name := v.fullName(nil)
+		value, err := v.value(nil)
+		if err != nil {
+			panic(err)
+		}
+		err = nw.Assign(name, value.Value(c.pkgNames))
+		if err != nil {
+			return err
+		}
+	}
+
+	if len(defs.variables) > 0 {
+		err := nw.BlankLine()
+		if err != nil {
+			return err
+		}
+	}
+
+	// Write the local rules.
+	for _, r := range defs.rules {
+		// A localRule doesn't need the package names or config to determine
+		// its name or definition.
+		name := r.fullName(nil)
+		def, err := r.def(nil)
+		if err != nil {
+			panic(err)
+		}
+
+		err = def.WriteTo(nw, name, c.pkgNames)
+		if err != nil {
+			return err
+		}
+
+		err = nw.BlankLine()
+		if err != nil {
+			return err
+		}
+	}
+
+	// Write the build definitions.
+	for _, buildDef := range defs.buildDefs {
+		err := buildDef.WriteTo(nw, c.pkgNames)
+		if err != nil {
+			return err
+		}
+
+		if len(buildDef.Args) > 0 {
+			err = nw.BlankLine()
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+var fileHeaderTemplate = `******************************************************************************
+***            This file is generated and should not be edited             ***
+******************************************************************************
+{{if .Pkgs}}
+This file contains variables, rules, and pools with name prefixes indicating
+they were generated by the following Go packages:
+{{range .Pkgs}}
+    {{.PkgName}} [from Go package {{.PkgPath}}]{{end}}{{end}}
+
+`
+
+var moduleHeaderTemplate = `# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
+Module:  {{.properties.Name}}
+Type:    {{.typeName}}
+GoType:  {{.goTypeName}}
+Defined: {{.pos}}
+`
+
+var singletonHeaderTemplate = `# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
+Singleton: {{.name}}
+GoType:    {{.goTypeName}}
+`
diff --git a/blueprint/context_test.go b/blueprint/context_test.go
new file mode 100644
index 0000000..2fda1b1
--- /dev/null
+++ b/blueprint/context_test.go
@@ -0,0 +1,91 @@
+package blueprint
+
+import (
+	"bytes"
+	"testing"
+)
+
+type fooModule struct {
+	properties struct {
+		Foo string
+	}
+}
+
+var FooModule = MakeModuleType("FooModule", newFooModule)
+
+func newFooModule() (Module, interface{}) {
+	m := &fooModule{}
+	return m, &m.properties
+}
+
+func (f *fooModule) GenerateBuildActions(ModuleContext) {
+}
+
+func (f *fooModule) Foo() string {
+	return f.properties.Foo
+}
+
+type barModule struct {
+	properties struct {
+		Bar bool
+	}
+}
+
+var BarModule = MakeModuleType("BarModule", newBarModule)
+
+func newBarModule() (Module, interface{}) {
+	m := &barModule{}
+	return m, &m.properties
+}
+
+func (b *barModule) GenerateBuildActions(ModuleContext) {
+}
+
+func (b *barModule) Bar() bool {
+	return b.properties.Bar
+}
+
+func TestContextParse(t *testing.T) {
+	ctx := NewContext()
+	ctx.RegisterModuleType("foo_module", FooModule)
+	ctx.RegisterModuleType("bar_module", BarModule)
+
+	r := bytes.NewBufferString(`
+		foo_module {
+			name: "MyFooModule",
+			deps: ["MyBarModule"],
+		}
+
+		bar_module {
+			name: "MyBarModule",
+		}
+	`)
+
+	_, errs := ctx.Parse(".", "Blueprint", r)
+	if len(errs) > 0 {
+		t.Errorf("unexpected parse errors:")
+		for _, err := range errs {
+			t.Errorf("  %s", err)
+		}
+		t.FailNow()
+	}
+
+	errs = ctx.resolveDependencies()
+	if len(errs) > 0 {
+		t.Errorf("unexpected dep errors:")
+		for _, err := range errs {
+			t.Errorf("  %s", err)
+		}
+		t.FailNow()
+	}
+
+	errs = ctx.checkForDependencyCycles()
+	if len(errs) > 0 {
+		t.Errorf("unexpected dep cycle errors:")
+		for _, err := range errs {
+			t.Errorf("  %s", err)
+		}
+		t.FailNow()
+	}
+
+}
diff --git a/blueprint/globals.go b/blueprint/globals.go
new file mode 100644
index 0000000..4a747df
--- /dev/null
+++ b/blueprint/globals.go
@@ -0,0 +1,533 @@
+package blueprint
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"runtime"
+	"strings"
+)
+
+type pkg struct {
+	fullName  string
+	shortName string
+	pkgPath   string
+	scope     *scope
+}
+
+var pkgs = map[string]*pkg{}
+
+var pkgRegexp = regexp.MustCompile(`(.*)\.init(·[0-9]+)?`)
+
+var Phony Rule = &builtinRule{
+	name_: "phony",
+}
+
+var errRuleIsBuiltin = errors.New("the rule is a built-in")
+
+// We make a Ninja-friendly name out of a Go package name by replaceing all the
+// '/' characters with '.'.  We assume the results are unique, though this is
+// not 100% guaranteed for Go package names that already contain '.' characters.
+// Disallowing package names with '.' isn't reasonable since many package names
+// contain the name of the hosting site (e.g. "code.google.com").  In practice
+// this probably isn't really a problem.
+func pkgPathToName(pkgPath string) string {
+	return strings.Replace(pkgPath, "/", ".", -1)
+}
+
+// callerPackage returns the pkg of the function that called the caller of
+// callerPackage.  The caller of callerPackage must have been called from an
+// init function of the package or callerPackage will panic.
+//
+// Looking for the package's init function on the call stack and using that to
+// determine its package name is unfortunately dependent upon Go runtime
+// implementation details.  However, it allows us to ensure that it's easy to
+// determine where a definition in a .ninja file came from.
+func callerPackage() *pkg {
+	var pc [1]uintptr
+	n := runtime.Callers(3, pc[:])
+	if n != 1 {
+		panic("unable to get caller pc")
+	}
+
+	f := runtime.FuncForPC(pc[0])
+	callerName := f.Name()
+
+	submatches := pkgRegexp.FindSubmatch([]byte(callerName))
+	if submatches == nil {
+		println(callerName)
+		panic("not called from an init func")
+	}
+
+	pkgPath := string(submatches[1])
+
+	pkgName := pkgPathToName(pkgPath)
+	err := validateNinjaName(pkgName)
+	if err != nil {
+		panic(err)
+	}
+
+	i := strings.LastIndex(pkgPath, "/")
+	shortName := pkgPath[i+1:]
+
+	p, ok := pkgs[pkgPath]
+	if !ok {
+		p = &pkg{
+			fullName:  pkgName,
+			shortName: shortName,
+			pkgPath:   pkgPath,
+			scope:     newScope(nil),
+		}
+		pkgs[pkgPath] = p
+	}
+
+	return p
+}
+
+func Import(pkgPath string) {
+	callerPkg := callerPackage()
+
+	importPkg, ok := pkgs[pkgPath]
+	if !ok {
+		panic(fmt.Errorf("package %q has no Blueprints definitions", pkgPath))
+	}
+
+	err := callerPkg.scope.AddImport(importPkg.shortName, importPkg.scope)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func ImportAs(as, pkgPath string) {
+	callerPkg := callerPackage()
+
+	importPkg, ok := pkgs[pkgPath]
+	if !ok {
+		panic(fmt.Errorf("package %q has no Blueprints definitions", pkgPath))
+	}
+
+	err := validateNinjaName(as)
+	if err != nil {
+		panic(err)
+	}
+
+	err = callerPkg.scope.AddImport(as, importPkg.scope)
+	if err != nil {
+		panic(err)
+	}
+}
+
+type staticVariable struct {
+	pkg_   *pkg
+	name_  string
+	value_ string
+}
+
+// StaticVariable returns a Variable that does not depend on any configuration
+// information.
+func StaticVariable(name, value string) Variable {
+	err := validateNinjaName(name)
+	if err != nil {
+		panic(err)
+	}
+
+	pkg := callerPackage()
+
+	v := &staticVariable{pkg, name, value}
+	err = pkg.scope.AddVariable(v)
+	if err != nil {
+		panic(err)
+	}
+
+	return v
+}
+
+func (v *staticVariable) pkg() *pkg {
+	return v.pkg_
+}
+
+func (v *staticVariable) name() string {
+	return v.name_
+}
+
+func (v *staticVariable) fullName(pkgNames map[*pkg]string) string {
+	return packageNamespacePrefix(pkgNames[v.pkg_]) + v.name_
+}
+
+func (v *staticVariable) value(Config) (*ninjaString, error) {
+	return parseNinjaString(v.pkg_.scope, v.value_)
+}
+
+type variableFunc struct {
+	pkg_   *pkg
+	name_  string
+	value_ func(Config) (string, error)
+}
+
+// VariableFunc returns a Variable whose value is determined by a function that
+// takes a Config object as input and returns either the variable value or an
+// error.
+func VariableFunc(name string, f func(Config) (string, error)) Variable {
+	err := validateNinjaName(name)
+	if err != nil {
+		panic(err)
+	}
+
+	pkg := callerPackage()
+
+	v := &variableFunc{pkg, name, f}
+	err = pkg.scope.AddVariable(v)
+	if err != nil {
+		panic(err)
+	}
+
+	return v
+}
+
+func (v *variableFunc) pkg() *pkg {
+	return v.pkg_
+}
+
+func (v *variableFunc) name() string {
+	return v.name_
+}
+
+func (v *variableFunc) fullName(pkgNames map[*pkg]string) string {
+	return packageNamespacePrefix(pkgNames[v.pkg_]) + v.name_
+}
+
+func (v *variableFunc) value(config Config) (*ninjaString, error) {
+	value, err := v.value_(config)
+	if err != nil {
+		return nil, err
+	}
+	return parseNinjaString(v.pkg_.scope, value)
+}
+
+// An argVariable is a Variable that exists only when it is set by a build
+// statement to pass a value to the rule being invoked.  It has no value, so it
+// can never be used to create a Ninja assignment statement.  It is inserted
+// into the rule's scope, which is used for name lookups within the rule and
+// when assigning argument values as part of a build statement.
+type argVariable struct {
+	name_ string
+}
+
+func (v *argVariable) pkg() *pkg {
+	panic("this should not be called")
+}
+
+func (v *argVariable) name() string {
+	return v.name_
+}
+
+func (v *argVariable) fullName(pkgNames map[*pkg]string) string {
+	return v.name_
+}
+
+var errVariableIsArg = errors.New("argument variables have no value")
+
+func (v *argVariable) value(config Config) (*ninjaString, error) {
+	return nil, errVariableIsArg
+}
+
+type staticPool struct {
+	pkg_   *pkg
+	name_  string
+	params PoolParams
+}
+
+func StaticPool(name string, params PoolParams) Pool {
+	err := validateNinjaName(name)
+	if err != nil {
+		panic(err)
+	}
+
+	pkg := callerPackage()
+
+	p := &staticPool{pkg, name, params}
+	err = pkg.scope.AddPool(p)
+	if err != nil {
+		panic(err)
+	}
+
+	return p
+}
+
+func (p *staticPool) pkg() *pkg {
+	return p.pkg_
+}
+
+func (p *staticPool) name() string {
+	return p.name_
+}
+
+func (p *staticPool) fullName(pkgNames map[*pkg]string) string {
+	return packageNamespacePrefix(pkgNames[p.pkg_]) + p.name_
+}
+
+func (p *staticPool) def(config Config) (*poolDef, error) {
+	def, err := parsePoolParams(p.pkg_.scope, &p.params)
+	if err != nil {
+		panic(fmt.Errorf("error parsing PoolParams for %s: %s", p.name_, err))
+	}
+	return def, nil
+}
+
+type poolFunc struct {
+	pkg_       *pkg
+	name_      string
+	paramsFunc func(Config) (PoolParams, error)
+}
+
+func PoolFunc(name string, f func(Config) (PoolParams, error)) Pool {
+	err := validateNinjaName(name)
+	if err != nil {
+		panic(err)
+	}
+
+	pkg := callerPackage()
+
+	p := &poolFunc{pkg, name, f}
+	err = pkg.scope.AddPool(p)
+	if err != nil {
+		panic(err)
+	}
+
+	return p
+}
+
+func (p *poolFunc) pkg() *pkg {
+	return p.pkg_
+}
+
+func (p *poolFunc) name() string {
+	return p.name_
+}
+
+func (p *poolFunc) fullName(pkgNames map[*pkg]string) string {
+	return packageNamespacePrefix(pkgNames[p.pkg_]) + p.name_
+}
+
+func (p *poolFunc) def(config Config) (*poolDef, error) {
+	params, err := p.paramsFunc(config)
+	if err != nil {
+		return nil, err
+	}
+	def, err := parsePoolParams(p.pkg_.scope, &params)
+	if err != nil {
+		panic(fmt.Errorf("error parsing PoolParams for %s: %s", p.name_, err))
+	}
+	return def, nil
+}
+
+type staticRule struct {
+	pkg_     *pkg
+	name_    string
+	params   RuleParams
+	argNames map[string]bool
+	scope_   *scope
+}
+
+func StaticRule(name string, params RuleParams, argNames ...string) Rule {
+	pkg := callerPackage()
+
+	err := validateNinjaName(name)
+	if err != nil {
+		panic(err)
+	}
+
+	err = validateArgNames(argNames)
+	if err != nil {
+		panic(fmt.Errorf("invalid argument name: %s", err))
+	}
+
+	argNamesSet := make(map[string]bool)
+	for _, argName := range argNames {
+		argNamesSet[argName] = true
+	}
+
+	ruleScope := (*scope)(nil) // This will get created lazily
+
+	r := &staticRule{pkg, name, params, argNamesSet, ruleScope}
+	err = pkg.scope.AddRule(r)
+	if err != nil {
+		panic(err)
+	}
+
+	return r
+}
+
+func (r *staticRule) pkg() *pkg {
+	return r.pkg_
+}
+
+func (r *staticRule) name() string {
+	return r.name_
+}
+
+func (r *staticRule) fullName(pkgNames map[*pkg]string) string {
+	return packageNamespacePrefix(pkgNames[r.pkg_]) + r.name_
+}
+
+func (r *staticRule) def(Config) (*ruleDef, error) {
+	def, err := parseRuleParams(r.scope(), &r.params)
+	if err != nil {
+		panic(fmt.Errorf("error parsing RuleParams for %s: %s", r.name_, err))
+	}
+	return def, nil
+}
+
+func (r *staticRule) scope() *scope {
+	// We lazily create the scope so that all the global variables get declared
+	// before the args are created.  Otherwise we could incorrectly shadow a
+	// global variable with an arg variable.
+	if r.scope_ == nil {
+		r.scope_ = makeRuleScope(r.pkg_.scope, r.argNames)
+	}
+	return r.scope_
+}
+
+func (r *staticRule) isArg(argName string) bool {
+	return r.argNames[argName]
+}
+
+type ruleFunc struct {
+	pkg_       *pkg
+	name_      string
+	paramsFunc func(Config) (RuleParams, error)
+	argNames   map[string]bool
+	scope_     *scope
+}
+
+func RuleFunc(name string, f func(Config) (RuleParams, error),
+	argNames ...string) Rule {
+
+	pkg := callerPackage()
+
+	err := validateNinjaName(name)
+	if err != nil {
+		panic(err)
+	}
+
+	err = validateArgNames(argNames)
+	if err != nil {
+		panic(fmt.Errorf("invalid argument name: %s", err))
+	}
+
+	argNamesSet := make(map[string]bool)
+	for _, argName := range argNames {
+		argNamesSet[argName] = true
+	}
+
+	ruleScope := (*scope)(nil) // This will get created lazily
+
+	r := &ruleFunc{pkg, name, f, argNamesSet, ruleScope}
+	err = pkg.scope.AddRule(r)
+	if err != nil {
+		panic(err)
+	}
+
+	return r
+}
+
+func (r *ruleFunc) pkg() *pkg {
+	return r.pkg_
+}
+
+func (r *ruleFunc) name() string {
+	return r.name_
+}
+
+func (r *ruleFunc) fullName(pkgNames map[*pkg]string) string {
+	return packageNamespacePrefix(pkgNames[r.pkg_]) + r.name_
+}
+
+func (r *ruleFunc) def(config Config) (*ruleDef, error) {
+	params, err := r.paramsFunc(config)
+	if err != nil {
+		return nil, err
+	}
+	def, err := parseRuleParams(r.scope(), &params)
+	if err != nil {
+		panic(fmt.Errorf("error parsing RuleParams for %s: %s", r.name_, err))
+	}
+	return def, nil
+}
+
+func (r *ruleFunc) scope() *scope {
+	// We lazily create the scope so that all the global variables get declared
+	// before the args are created.  Otherwise we could incorrectly shadow a
+	// global variable with an arg variable.
+	if r.scope_ == nil {
+		r.scope_ = makeRuleScope(r.pkg_.scope, r.argNames)
+	}
+	return r.scope_
+}
+
+func (r *ruleFunc) isArg(argName string) bool {
+	return r.argNames[argName]
+}
+
+type builtinRule struct {
+	name_  string
+	scope_ *scope
+}
+
+func (r *builtinRule) pkg() *pkg {
+	return nil
+}
+
+func (r *builtinRule) name() string {
+	return r.name_
+}
+
+func (r *builtinRule) fullName(pkgNames map[*pkg]string) string {
+	return r.name_
+}
+
+func (r *builtinRule) def(config Config) (*ruleDef, error) {
+	return nil, errRuleIsBuiltin
+}
+
+func (r *builtinRule) scope() *scope {
+	if r.scope_ == nil {
+		r.scope_ = makeRuleScope(nil, nil)
+	}
+	return r.scope_
+}
+
+func (r *builtinRule) isArg(argName string) bool {
+	return false
+}
+
+type ModuleType interface {
+	pkg() *pkg
+	name() string
+	new() (m Module, properties interface{})
+}
+
+type moduleTypeFunc struct {
+	pkg_  *pkg
+	name_ string
+	new_  func() (Module, interface{})
+}
+
+func MakeModuleType(name string,
+	new func() (m Module, properties interface{})) ModuleType {
+
+	pkg := callerPackage()
+	return &moduleTypeFunc{pkg, name, new}
+}
+
+func (m *moduleTypeFunc) pkg() *pkg {
+	return m.pkg_
+}
+
+func (m *moduleTypeFunc) name() string {
+	return m.pkg_.pkgPath + "." + m.name_
+}
+
+func (m *moduleTypeFunc) new() (Module, interface{}) {
+	return m.new_()
+}
diff --git a/blueprint/live_tracker.go b/blueprint/live_tracker.go
new file mode 100644
index 0000000..bad299a
--- /dev/null
+++ b/blueprint/live_tracker.go
@@ -0,0 +1,149 @@
+package blueprint
+
+// A liveTracker tracks the values of live variables, rules, and pools.  An
+// entity is made "live" when it is referenced directly or indirectly by a build
+// definition.  When an entity is made live its value is computed based on the
+// configuration.
+type liveTracker struct {
+	config Config // Used to evaluate variable, rule, and pool values.
+
+	variables map[Variable]*ninjaString
+	pools     map[Pool]*poolDef
+	rules     map[Rule]*ruleDef
+}
+
+func newLiveTracker(config Config) *liveTracker {
+	return &liveTracker{
+		config:    config,
+		variables: make(map[Variable]*ninjaString),
+		pools:     make(map[Pool]*poolDef),
+		rules:     make(map[Rule]*ruleDef),
+	}
+}
+
+func (l *liveTracker) AddBuildDefDeps(def *buildDef) error {
+	err := l.addRule(def.Rule)
+	if err != nil {
+		return err
+	}
+
+	err = l.addNinjaStringListDeps(def.Outputs)
+	if err != nil {
+		return err
+	}
+
+	err = l.addNinjaStringListDeps(def.Inputs)
+	if err != nil {
+		return err
+	}
+
+	err = l.addNinjaStringListDeps(def.Implicits)
+	if err != nil {
+		return err
+	}
+
+	err = l.addNinjaStringListDeps(def.OrderOnly)
+	if err != nil {
+		return err
+	}
+
+	for _, value := range def.Args {
+		err = l.addNinjaStringDeps(value)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (l *liveTracker) addRule(r Rule) error {
+	_, ok := l.rules[r]
+	if !ok {
+		def, err := r.def(l.config)
+		if err == errRuleIsBuiltin {
+			// No need to do anything for built-in rules.
+			return nil
+		}
+		if err != nil {
+			return err
+		}
+
+		if def.Pool != nil {
+			err = l.addPool(def.Pool)
+			if err != nil {
+				return err
+			}
+		}
+
+		for _, value := range def.Variables {
+			err = l.addNinjaStringDeps(value)
+			if err != nil {
+				return err
+			}
+		}
+
+		l.rules[r] = def
+	}
+
+	return nil
+}
+
+func (l *liveTracker) addPool(p Pool) error {
+	_, ok := l.pools[p]
+	if !ok {
+		def, err := p.def(l.config)
+		if err != nil {
+			return err
+		}
+
+		l.pools[p] = def
+	}
+
+	return nil
+}
+
+func (l *liveTracker) addVariable(v Variable) error {
+	_, ok := l.variables[v]
+	if !ok {
+		value, err := v.value(l.config)
+		if err == errVariableIsArg {
+			// This variable is a placeholder for an argument that can be passed
+			// to a rule.  It has no value and thus doesn't reference any other
+			// variables.
+			return nil
+		}
+		if err != nil {
+			return err
+		}
+
+		l.variables[v] = value
+
+		err = l.addNinjaStringDeps(value)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (l *liveTracker) addNinjaStringListDeps(list []*ninjaString) error {
+	for _, str := range list {
+		err := l.addNinjaStringDeps(str)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (l *liveTracker) addNinjaStringDeps(str *ninjaString) error {
+	for _, v := range str.variables {
+		err := l.addVariable(v)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/blueprint/mangle.go b/blueprint/mangle.go
new file mode 100644
index 0000000..22eec37
--- /dev/null
+++ b/blueprint/mangle.go
@@ -0,0 +1,13 @@
+package blueprint
+
+func packageNamespacePrefix(packageName string) string {
+	return "g." + packageName + "."
+}
+
+func moduleNamespacePrefix(moduleName string) string {
+	return "m." + moduleName + "."
+}
+
+func singletonNamespacePrefix(singletonName string) string {
+	return "s." + singletonName + "."
+}
diff --git a/blueprint/module_ctx.go b/blueprint/module_ctx.go
new file mode 100644
index 0000000..f8fe564
--- /dev/null
+++ b/blueprint/module_ctx.go
@@ -0,0 +1,116 @@
+package blueprint
+
+import (
+	"fmt"
+	"path/filepath"
+)
+
+type Module interface {
+	GenerateBuildActions(ModuleContext)
+}
+
+type ModuleContext interface {
+	ModuleName() string
+	ModuleDir() string
+	Config() Config
+
+	ModuleErrorf(fmt string, args ...interface{})
+	PropertyErrorf(property, fmt string, args ...interface{})
+
+	Variable(name, value string)
+	Rule(name string, params RuleParams) Rule
+	Build(params BuildParams)
+
+	VisitDepsDepthFirst(visit func(Module))
+	VisitDepsDepthFirstIf(pred func(Module) bool, visit func(Module))
+}
+
+var _ ModuleContext = (*moduleContext)(nil)
+
+type moduleContext struct {
+	context *Context
+	config  Config
+	module  Module
+	scope   *localScope
+	info    *moduleInfo
+
+	errs []error
+
+	actionDefs localBuildActions
+}
+
+func (m *moduleContext) ModuleName() string {
+	return m.info.properties.Name
+}
+
+func (m *moduleContext) ModuleDir() string {
+	return filepath.Dir(m.info.relBlueprintFile)
+}
+
+func (m *moduleContext) Config() Config {
+	return m.config
+}
+
+func (m *moduleContext) ModuleErrorf(format string, args ...interface{}) {
+	m.errs = append(m.errs, &Error{
+		Err: fmt.Errorf(format, args...),
+		Pos: m.info.pos,
+	})
+}
+
+func (m *moduleContext) PropertyErrorf(property, format string,
+	args ...interface{}) {
+
+	pos, ok := m.info.propertyPos[property]
+	if !ok {
+		panic(fmt.Errorf("property %q was not set for this module", property))
+	}
+
+	m.errs = append(m.errs, &Error{
+		Err: fmt.Errorf(format, args...),
+		Pos: pos,
+	})
+}
+
+func (m *moduleContext) Variable(name, value string) {
+	v, err := m.scope.AddLocalVariable(name, value)
+	if err != nil {
+		panic(err)
+	}
+
+	m.actionDefs.variables = append(m.actionDefs.variables, v)
+}
+
+func (m *moduleContext) Rule(name string, params RuleParams) Rule {
+	// TODO: Verify that params.Pool is accessible in this module's scope.
+
+	r, err := m.scope.AddLocalRule(name, &params)
+	if err != nil {
+		panic(err)
+	}
+
+	m.actionDefs.rules = append(m.actionDefs.rules, r)
+
+	return r
+}
+
+func (m *moduleContext) Build(params BuildParams) {
+	// TODO: Verify that params.Rule is accessible in this module's scope.
+
+	def, err := parseBuildParams(m.scope, &params)
+	if err != nil {
+		panic(err)
+	}
+
+	m.actionDefs.buildDefs = append(m.actionDefs.buildDefs, def)
+}
+
+func (m *moduleContext) VisitDepsDepthFirst(visit func(Module)) {
+	m.context.visitDepsDepthFirst(m.module, visit)
+}
+
+func (m *moduleContext) VisitDepsDepthFirstIf(pred func(Module) bool,
+	visit func(Module)) {
+
+	m.context.visitDepsDepthFirstIf(m.module, pred, visit)
+}
diff --git a/blueprint/ninja_defs.go b/blueprint/ninja_defs.go
new file mode 100644
index 0000000..3f3329d
--- /dev/null
+++ b/blueprint/ninja_defs.go
@@ -0,0 +1,299 @@
+package blueprint
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+type Deps int
+
+const (
+	DepsNone Deps = iota
+	DepsGCC
+	DepsMSVC
+)
+
+func (d Deps) String() string {
+	switch d {
+	case DepsNone:
+		return "none"
+	case DepsGCC:
+		return "gcc"
+	case DepsMSVC:
+		return "msvc"
+	default:
+		panic(fmt.Sprintf("unknown deps value: %d", d))
+	}
+}
+
+type PoolParams struct {
+	Comment string
+	Depth   int
+}
+
+type RuleParams struct {
+	Comment        string
+	Command        string
+	Depfile        string
+	Deps           Deps
+	Description    string
+	Generator      bool
+	Pool           Pool
+	Restat         bool
+	Rspfile        string
+	RspfileContent string
+}
+
+type BuildParams struct {
+	Rule      Rule
+	Outputs   []string
+	Inputs    []string
+	Implicits []string
+	OrderOnly []string
+	Args      map[string]string
+}
+
+// A poolDef describes a pool definition.  It does not include the name of the
+// pool.
+type poolDef struct {
+	Comment string
+	Depth   int
+}
+
+func parsePoolParams(scope variableLookup, params *PoolParams) (*poolDef,
+	error) {
+
+	def := &poolDef{
+		Comment: params.Comment,
+		Depth:   params.Depth,
+	}
+
+	return def, nil
+}
+
+func (p *poolDef) WriteTo(nw *ninjaWriter, name string) error {
+	if p.Comment != "" {
+		err := nw.Comment(p.Comment)
+		if err != nil {
+			return err
+		}
+	}
+
+	err := nw.Pool(name)
+	if err != nil {
+		return err
+	}
+
+	return nw.ScopedAssign("depth", strconv.Itoa(p.Depth))
+}
+
+// A ruleDef describes a rule definition.  It does not include the name of the
+// rule.
+type ruleDef struct {
+	Comment   string
+	Pool      Pool
+	Variables map[string]*ninjaString
+}
+
+func parseRuleParams(scope variableLookup, params *RuleParams) (*ruleDef,
+	error) {
+
+	r := &ruleDef{
+		Comment:   params.Comment,
+		Pool:      params.Pool,
+		Variables: make(map[string]*ninjaString),
+	}
+
+	if params.Command == "" {
+		return nil, fmt.Errorf("encountered rule params with no command " +
+			"specified")
+	}
+
+	value, err := parseNinjaString(scope, params.Command)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing Command param: %s", err)
+	}
+	r.Variables["command"] = value
+
+	if params.Depfile != "" {
+		value, err = parseNinjaString(scope, params.Depfile)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing Depfile param: %s", err)
+		}
+		r.Variables["depfile"] = value
+	}
+
+	if params.Deps != DepsNone {
+		r.Variables["deps"] = simpleNinjaString(params.Deps.String())
+	}
+
+	if params.Description != "" {
+		value, err = parseNinjaString(scope, params.Description)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing Description param: %s", err)
+		}
+		r.Variables["description"] = value
+	}
+
+	if params.Generator {
+		r.Variables["generator"] = simpleNinjaString("true")
+	}
+
+	if params.Restat {
+		r.Variables["restat"] = simpleNinjaString("true")
+	}
+
+	if params.Rspfile != "" {
+		value, err = parseNinjaString(scope, params.Rspfile)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing Rspfile param: %s", err)
+		}
+		r.Variables["rspfile"] = value
+	}
+
+	if params.RspfileContent != "" {
+		value, err = parseNinjaString(scope, params.RspfileContent)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing RspfileContent param: %s",
+				err)
+		}
+		r.Variables["rspfile_content"] = value
+	}
+
+	return r, nil
+}
+
+func (r *ruleDef) WriteTo(nw *ninjaWriter, name string,
+	pkgNames map[*pkg]string) error {
+
+	if r.Comment != "" {
+		err := nw.Comment(r.Comment)
+		if err != nil {
+			return err
+		}
+	}
+
+	err := nw.Rule(name)
+	if err != nil {
+		return err
+	}
+
+	if r.Pool != nil {
+		err = nw.ScopedAssign("pool", r.Pool.fullName(pkgNames))
+		if err != nil {
+			return err
+		}
+	}
+
+	for name, value := range r.Variables {
+		err = nw.ScopedAssign(name, value.Value(pkgNames))
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// A buildDef describes a build target definition.
+type buildDef struct {
+	Rule      Rule
+	Outputs   []*ninjaString
+	Inputs    []*ninjaString
+	Implicits []*ninjaString
+	OrderOnly []*ninjaString
+	Args      map[Variable]*ninjaString
+}
+
+func parseBuildParams(scope variableLookup, params *BuildParams) (*buildDef,
+	error) {
+
+	rule := params.Rule
+
+	b := &buildDef{
+		Rule: rule,
+	}
+
+	var err error
+	b.Outputs, err = parseNinjaStrings(scope, params.Outputs)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing Outputs param: %s", err)
+	}
+
+	b.Inputs, err = parseNinjaStrings(scope, params.Inputs)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing Inputs param: %s", err)
+	}
+
+	b.Implicits, err = parseNinjaStrings(scope, params.Implicits)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing Implicits param: %s", err)
+	}
+
+	b.OrderOnly, err = parseNinjaStrings(scope, params.OrderOnly)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing OrderOnly param: %s", err)
+	}
+
+	argNameScope := rule.scope()
+
+	if len(params.Args) > 0 {
+		b.Args = make(map[Variable]*ninjaString)
+		for name, value := range params.Args {
+			if !rule.isArg(name) {
+				return nil, fmt.Errorf("unknown argument %q", name)
+			}
+
+			argVar, err := argNameScope.LookupVariable(name)
+			if err != nil {
+				// This shouldn't happen.
+				return nil, fmt.Errorf("argument lookup error: %s", err)
+			}
+
+			ninjaValue, err := parseNinjaString(scope, value)
+			if err != nil {
+				return nil, fmt.Errorf("error parsing variable %q: %s", name,
+					err)
+			}
+
+			b.Args[argVar] = ninjaValue
+		}
+	}
+
+	return b, nil
+}
+
+func (b *buildDef) WriteTo(nw *ninjaWriter, pkgNames map[*pkg]string) error {
+	var (
+		rule          = b.Rule.fullName(pkgNames)
+		outputs       = valueList(b.Outputs, pkgNames, outputEscaper)
+		explicitDeps  = valueList(b.Inputs, pkgNames, inputEscaper)
+		implicitDeps  = valueList(b.Implicits, pkgNames, inputEscaper)
+		orderOnlyDeps = valueList(b.OrderOnly, pkgNames, inputEscaper)
+	)
+	err := nw.Build(rule, outputs, explicitDeps, implicitDeps, orderOnlyDeps)
+	if err != nil {
+		return err
+	}
+
+	for argVar, value := range b.Args {
+		name := argVar.fullName(pkgNames)
+		err = nw.ScopedAssign(name, value.Value(pkgNames))
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func valueList(list []*ninjaString, pkgNames map[*pkg]string,
+	escaper *strings.Replacer) []string {
+
+	result := make([]string, len(list))
+	for i, ninjaStr := range list {
+		result[i] = ninjaStr.ValueWithEscaper(pkgNames, escaper)
+	}
+	return result
+}
diff --git a/blueprint/ninja_strings.go b/blueprint/ninja_strings.go
new file mode 100644
index 0000000..035d084
--- /dev/null
+++ b/blueprint/ninja_strings.go
@@ -0,0 +1,281 @@
+package blueprint
+
+import (
+	"fmt"
+	"strings"
+)
+
+const EOF = -1
+
+var (
+	defaultEscaper = strings.NewReplacer(
+		"\n", "$\n")
+	inputEscaper = strings.NewReplacer(
+		"\n", "$\n",
+		" ", "$ ")
+	outputEscaper = strings.NewReplacer(
+		"\n", "$\n",
+		" ", "$ ",
+		":", "$:")
+)
+
+type ninjaString struct {
+	strings   []string
+	variables []Variable
+}
+
+type variableLookup interface {
+	LookupVariable(name string) (Variable, error)
+}
+
+func simpleNinjaString(str string) *ninjaString {
+	return &ninjaString{
+		strings: []string{str},
+	}
+}
+
+// parseNinjaString parses an unescaped ninja string (i.e. all $<something>
+// occurrences are expected to be variables or $$) and returns a list of the
+// variable names that the string references.
+func parseNinjaString(scope variableLookup, str string) (*ninjaString, error) {
+	type stateFunc func(int, rune) (stateFunc, error)
+	var (
+		stringState      stateFunc
+		dollarStartState stateFunc
+		dollarState      stateFunc
+		bracketsState    stateFunc
+	)
+
+	var stringStart, varStart int
+	var result ninjaString
+
+	stringState = func(i int, r rune) (stateFunc, error) {
+		switch {
+		case r == '$':
+			varStart = i + 1
+			return dollarStartState, nil
+
+		case r == EOF:
+			result.strings = append(result.strings, str[stringStart:i])
+			return nil, nil
+
+		default:
+			return stringState, nil
+		}
+	}
+
+	dollarStartState = func(i int, r rune) (stateFunc, error) {
+		switch {
+		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
+			r >= '0' && r <= '9', r == '_', r == '-':
+			// The beginning of a of the variable name.  Output the string and
+			// keep going.
+			result.strings = append(result.strings, str[stringStart:i-1])
+			return dollarState, nil
+
+		case r == '$':
+			// Just a "$$".  Go back to stringState without changing
+			// stringStart.
+			return stringState, nil
+
+		case r == '{':
+			// This is a bracketted variable name (e.g. "${blah.blah}").  Output
+			// the string and keep going.
+			result.strings = append(result.strings, str[stringStart:i-1])
+			varStart = i + 1
+			return bracketsState, nil
+
+		case r == EOF:
+			return nil, fmt.Errorf("unexpected end of string after '$'")
+
+		default:
+			// This was some arbitrary character following a dollar sign,
+			// which is not allowed.
+			return nil, fmt.Errorf("invalid character after '$' at byte "+
+				"offset %d", i)
+		}
+	}
+
+	dollarState = func(i int, r rune) (stateFunc, error) {
+		switch {
+		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
+			r >= '0' && r <= '9', r == '_', r == '-':
+			// A part of the variable name.  Keep going.
+			return dollarState, nil
+
+		case r == '$':
+			// A dollar after the variable name (e.g. "$blah$").  Output the
+			// variable we have and start a new one.
+			v, err := scope.LookupVariable(str[varStart:i])
+			if err != nil {
+				return nil, err
+			}
+
+			result.variables = append(result.variables, v)
+			varStart = i + 1
+			return dollarState, nil
+
+		case r == EOF:
+			// This is the end of the variable name.
+			v, err := scope.LookupVariable(str[varStart:i])
+			if err != nil {
+				return nil, err
+			}
+
+			result.variables = append(result.variables, v)
+
+			// We always end with a string, even if it's an empty one.
+			result.strings = append(result.strings, "")
+
+			return nil, nil
+
+		default:
+			// We've just gone past the end of the variable name, so record what
+			// we have.
+			v, err := scope.LookupVariable(str[varStart:i])
+			if err != nil {
+				return nil, err
+			}
+
+			result.variables = append(result.variables, v)
+			stringStart = i
+			return stringState, nil
+		}
+	}
+
+	bracketsState = func(i int, r rune) (stateFunc, error) {
+		switch {
+		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
+			r >= '0' && r <= '9', r == '_', r == '-', r == '.':
+			// A part of the variable name.  Keep going.
+			return bracketsState, nil
+
+		case r == '}':
+			if varStart == i {
+				// The brackets were immediately closed.  That's no good.
+				return nil, fmt.Errorf("empty variable name at byte offset %d",
+					i)
+			}
+
+			// This is the end of the variable name.
+			v, err := scope.LookupVariable(str[varStart:i])
+			if err != nil {
+				return nil, err
+			}
+
+			result.variables = append(result.variables, v)
+			stringStart = i + 1
+			return stringState, nil
+
+		case r == EOF:
+			return nil, fmt.Errorf("unexpected end of string in variable name")
+
+		default:
+			// This character isn't allowed in a variable name.
+			return nil, fmt.Errorf("invalid character in variable name at "+
+				"byte offset %d", i)
+		}
+	}
+
+	state := stringState
+	var err error
+	for i, r := range str {
+		state, err = state(i, r)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	_, err = state(len(str), EOF)
+	if err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+func parseNinjaStrings(scope variableLookup, strs []string) ([]*ninjaString,
+	error) {
+
+	if len(strs) == 0 {
+		return nil, nil
+	}
+	result := make([]*ninjaString, len(strs))
+	for i, str := range strs {
+		ninjaStr, err := parseNinjaString(scope, str)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing element %d: %s", i, err)
+		}
+		result[i] = ninjaStr
+	}
+	return result, nil
+}
+
+func (n *ninjaString) Value(pkgNames map[*pkg]string) string {
+	return n.ValueWithEscaper(pkgNames, defaultEscaper)
+}
+
+func (n *ninjaString) ValueWithEscaper(pkgNames map[*pkg]string,
+	escaper *strings.Replacer) string {
+
+	str := escaper.Replace(n.strings[0])
+	for i, v := range n.variables {
+		str += "${" + v.fullName(pkgNames) + "}"
+		str += escaper.Replace(n.strings[i+1])
+	}
+	return str
+}
+
+func validateNinjaName(name string) error {
+	for i, r := range name {
+		valid := (r >= 'a' && r <= 'z') ||
+			(r >= 'A' && r <= 'Z') ||
+			(r >= '0' && r <= '9') ||
+			(r == '_') ||
+			(r == '-') ||
+			(r == '.')
+		if !valid {
+			return fmt.Errorf("%q contains an invalid Ninja name character "+
+				"at byte offset %d", name, i)
+		}
+	}
+	return nil
+}
+
+var builtinRuleArgs = []string{"out", "in"}
+
+func validateArgName(argName string) error {
+	err := validateNinjaName(argName)
+	if err != nil {
+		return err
+	}
+
+	// We only allow globals within the rule's package to be used as rule
+	// arguments.  A global in another package can always be mirrored into
+	// the rule's package by defining a new variable, so this doesn't limit
+	// what's possible.  This limitation prevents situations where a Build
+	// invocation in another package must use the rule-defining package's
+	// import name for a 3rd package in order to set the rule's arguments.
+	if strings.ContainsRune(argName, '.') {
+		return fmt.Errorf("%q contains a '.' character", argName)
+	}
+
+	for _, builtin := range builtinRuleArgs {
+		if argName == builtin {
+			return fmt.Errorf("%q conflicts with Ninja built-in", argName)
+		}
+	}
+
+	return nil
+}
+
+func validateArgNames(argNames []string) error {
+	for _, argName := range argNames {
+		err := validateArgName(argName)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/blueprint/ninja_strings_test.go b/blueprint/ninja_strings_test.go
new file mode 100644
index 0000000..db47803
--- /dev/null
+++ b/blueprint/ninja_strings_test.go
@@ -0,0 +1,114 @@
+package blueprint
+
+import (
+	"reflect"
+	"testing"
+)
+
+var ninjaParseTestCases = []struct {
+	input  string
+	output []string
+	err    string
+}{
+	{
+		input:  "abc def $ghi jkl",
+		output: []string{"ghi"},
+	},
+	{
+		input:  "abc def $ghi$jkl",
+		output: []string{"ghi", "jkl"},
+	},
+	{
+		input:  "foo $012_-345xyz_! bar",
+		output: []string{"012_-345xyz_"},
+	},
+	{
+		input:  "foo ${012_-345xyz_} bar",
+		output: []string{"012_-345xyz_"},
+	},
+	{
+		input:  "foo ${012_-345xyz_} bar",
+		output: []string{"012_-345xyz_"},
+	},
+	{
+		input:  "foo $$ bar",
+		output: nil,
+	},
+	{
+		input: "foo $ bar",
+		err:   "invalid character after '$' at byte offset 5",
+	},
+	{
+		input: "foo $",
+		err:   "unexpected end of string after '$'",
+	},
+	{
+		input: "foo ${} bar",
+		err:   "empty variable name at byte offset 6",
+	},
+	{
+		input: "foo ${abc!} bar",
+		err:   "invalid character in variable name at byte offset 9",
+	},
+	{
+		input: "foo ${abc",
+		err:   "unexpected end of string in variable name",
+	},
+}
+
+func TestParseNinjaString(t *testing.T) {
+	for _, testCase := range ninjaParseTestCases {
+		scope := newLocalScope(nil, "namespace")
+		var expectedVars []Variable
+		for _, varName := range testCase.output {
+			v, err := scope.LookupVariable(varName)
+			if err != nil {
+				v, err = scope.AddLocalVariable(varName, "")
+				if err != nil {
+					t.Fatalf("error creating scope: %s", err)
+				}
+			}
+			expectedVars = append(expectedVars, v)
+		}
+
+		output, err := parseNinjaString(scope, testCase.input)
+		if err == nil && !reflect.DeepEqual(output.variables, expectedVars) {
+			t.Errorf("incorrect output:")
+			t.Errorf("     input: %q", testCase.input)
+			t.Errorf("  expected: %#v", testCase.output)
+			t.Errorf("       got: %#v", output)
+		}
+		var errStr string
+		if err != nil {
+			errStr = err.Error()
+		}
+		if err != nil && err.Error() != testCase.err {
+			t.Errorf("unexpected error:")
+			t.Errorf("     input: %q", testCase.input)
+			t.Errorf("  expected: %q", testCase.err)
+			t.Errorf("       got: %q", errStr)
+		}
+	}
+}
+
+func TestParseNinjaStringWithImportedVar(t *testing.T) {
+	ImpVar := &staticVariable{name_: "ImpVar"}
+	impScope := newScope(nil)
+	impScope.AddVariable(ImpVar)
+	scope := newScope(nil)
+	scope.AddImport("impPkg", impScope)
+
+	input := "abc def ${impPkg.ImpVar} ghi"
+	output, err := parseNinjaString(scope, input)
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	expect := []Variable{ImpVar}
+	if !reflect.DeepEqual(output.variables, expect) {
+		t.Errorf("incorrect output:")
+		t.Errorf("     input: %q", input)
+		t.Errorf("  expected: %#v", expect)
+		t.Errorf("       got: %#v", output)
+	}
+}
diff --git a/blueprint/ninja_writer.go b/blueprint/ninja_writer.go
new file mode 100644
index 0000000..0051969
--- /dev/null
+++ b/blueprint/ninja_writer.go
@@ -0,0 +1,244 @@
+package blueprint
+
+import (
+	"fmt"
+	"io"
+	"strings"
+	"unicode"
+)
+
+const (
+	indentWidth = 4
+	lineWidth   = 80
+)
+
+type ninjaWriter struct {
+	writer io.Writer
+
+	justDidBlankLine bool // true if the last operation was a BlankLine
+}
+
+func newNinjaWriter(writer io.Writer) *ninjaWriter {
+	return &ninjaWriter{
+		writer: writer,
+	}
+}
+
+func (n *ninjaWriter) Comment(comment string) error {
+	n.justDidBlankLine = false
+
+	const lineHeaderLen = len("# ")
+	const maxLineLen = lineWidth - lineHeaderLen
+
+	var lineStart, lastSplitPoint int
+	for i, r := range comment {
+		if unicode.IsSpace(r) {
+			// We know we can safely split the line here.
+			lastSplitPoint = i + 1
+		}
+
+		var line string
+		var writeLine bool
+		switch {
+		case r == '\n':
+			// Output the line without trimming the left so as to allow comments
+			// to contain their own indentation.
+			line = strings.TrimRightFunc(comment[lineStart:i], unicode.IsSpace)
+			writeLine = true
+
+		case (i-lineStart > maxLineLen) && (lastSplitPoint > lineStart):
+			// The line has grown too long and is splittable.  Split it at the
+			// last split point.
+			line = strings.TrimSpace(comment[lineStart:lastSplitPoint])
+			writeLine = true
+		}
+
+		if writeLine {
+			line = strings.TrimSpace("# "+line) + "\n"
+			_, err := io.WriteString(n.writer, line)
+			if err != nil {
+				return err
+			}
+			lineStart = lastSplitPoint
+		}
+	}
+
+	if lineStart != len(comment) {
+		line := strings.TrimSpace(comment[lineStart:])
+		_, err := fmt.Fprintf(n.writer, "# %s\n", line)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (n *ninjaWriter) Pool(name string) error {
+	n.justDidBlankLine = false
+	_, err := fmt.Fprintf(n.writer, "pool %s\n", name)
+	return err
+}
+
+func (n *ninjaWriter) Rule(name string) error {
+	n.justDidBlankLine = false
+	_, err := fmt.Fprintf(n.writer, "rule %s\n", name)
+	return err
+}
+
+func (n *ninjaWriter) Build(rule string, outputs, explicitDeps, implicitDeps,
+	orderOnlyDeps []string) error {
+
+	n.justDidBlankLine = false
+
+	const lineWrapLen = len(" $")
+	const maxLineLen = lineWidth - lineWrapLen
+
+	line := "build"
+
+	appendWithWrap := func(s string) (err error) {
+		if len(line)+len(s) > maxLineLen {
+			_, err = fmt.Fprintf(n.writer, "%s $\n", line)
+			line = strings.Repeat(" ", indentWidth*2)
+			s = strings.TrimLeftFunc(s, unicode.IsSpace)
+		}
+		line += s
+		return
+	}
+
+	for _, output := range outputs {
+		err := appendWithWrap(" " + output)
+		if err != nil {
+			return err
+		}
+	}
+
+	err := appendWithWrap(":")
+	if err != nil {
+		return err
+	}
+
+	err = appendWithWrap(" " + rule)
+	if err != nil {
+		return err
+	}
+
+	for _, dep := range explicitDeps {
+		err := appendWithWrap(" " + dep)
+		if err != nil {
+			return err
+		}
+	}
+
+	if len(implicitDeps) > 0 {
+		err := appendWithWrap(" |")
+		if err != nil {
+			return err
+		}
+
+		for _, dep := range implicitDeps {
+			err := appendWithWrap(" " + dep)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	if len(orderOnlyDeps) > 0 {
+		err := appendWithWrap(" ||")
+		if err != nil {
+			return err
+		}
+
+		for _, dep := range orderOnlyDeps {
+			err := appendWithWrap(" " + dep)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	_, err = fmt.Fprintln(n.writer, line)
+	return err
+}
+
+func (n *ninjaWriter) Assign(name, value string) error {
+	n.justDidBlankLine = false
+	_, err := fmt.Fprintf(n.writer, "%s = %s\n", name, value)
+	return err
+}
+
+func (n *ninjaWriter) ScopedAssign(name, value string) error {
+	n.justDidBlankLine = false
+	indent := strings.Repeat(" ", indentWidth)
+	_, err := fmt.Fprintf(n.writer, "%s%s = %s\n", indent, name, value)
+	return err
+}
+
+func (n *ninjaWriter) Default(targets ...string) error {
+	n.justDidBlankLine = false
+
+	const lineWrapLen = len(" $")
+	const maxLineLen = lineWidth - lineWrapLen
+
+	line := "default"
+
+	appendWithWrap := func(s string) (err error) {
+		if len(line)+len(s) > maxLineLen {
+			_, err = fmt.Fprintf(n.writer, "%s $\n", line)
+			line = strings.Repeat(" ", indentWidth*2)
+			s = strings.TrimLeftFunc(s, unicode.IsSpace)
+		}
+		line += s
+		return
+	}
+
+	for _, target := range targets {
+		err := appendWithWrap(" " + target)
+		if err != nil {
+			return err
+		}
+	}
+
+	_, err := fmt.Fprintln(n.writer, line)
+	return err
+}
+
+func (n *ninjaWriter) BlankLine() (err error) {
+	// We don't output multiple blank lines in a row.
+	if !n.justDidBlankLine {
+		n.justDidBlankLine = true
+		_, err = io.WriteString(n.writer, "\n")
+	}
+	return err
+}
+
+func writeAssignments(w io.Writer, indent int, assignments ...string) error {
+	var maxNameLen int
+	for i := 0; i < len(assignments); i += 2 {
+		name := assignments[i]
+		err := validateNinjaName(name)
+		if err != nil {
+			return err
+		}
+		if maxNameLen < len(name) {
+			maxNameLen = len(name)
+		}
+	}
+
+	indentStr := strings.Repeat(" ", indent*indentWidth)
+	extraIndentStr := strings.Repeat(" ", (indent+1)*indentWidth)
+	replacer := strings.NewReplacer("\n", "$\n"+extraIndentStr)
+
+	for i := 0; i < len(assignments); i += 2 {
+		name := assignments[i]
+		value := replacer.Replace(assignments[i+1])
+		_, err := fmt.Fprintf(w, "%s% *s = %s\n", indentStr, maxNameLen, name,
+			value)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/blueprint/ninja_writer_test.go b/blueprint/ninja_writer_test.go
new file mode 100644
index 0000000..24491e2
--- /dev/null
+++ b/blueprint/ninja_writer_test.go
@@ -0,0 +1,105 @@
+package blueprint
+
+import (
+	"bytes"
+	"testing"
+)
+
+func ck(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+var ninjaWriterTestCases = []struct {
+	input  func(w *ninjaWriter)
+	output string
+}{
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.Comment("foo"))
+		},
+		output: "# foo\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.Pool("foo"))
+		},
+		output: "pool foo\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.Rule("foo"))
+		},
+		output: "rule foo\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.Build("foo", []string{"o1", "o2"}, []string{"e1", "e2"},
+				[]string{"i1", "i2"}, []string{"oo1", "oo2"}))
+		},
+		output: "build o1 o2: foo e1 e2 | i1 i2 || oo1 oo2\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.Default("foo"))
+		},
+		output: "default foo\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.Assign("foo", "bar"))
+		},
+		output: "foo = bar\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.ScopedAssign("foo", "bar"))
+		},
+		output: "    foo = bar\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.BlankLine())
+		},
+		output: "\n",
+	},
+	{
+		input: func(w *ninjaWriter) {
+			ck(w.Pool("p"))
+			ck(w.ScopedAssign("depth", "3"))
+			ck(w.BlankLine())
+			ck(w.Comment("here comes a rule"))
+			ck(w.Rule("r"))
+			ck(w.ScopedAssign("command", "echo out: $out in: $in _arg: $_arg"))
+			ck(w.ScopedAssign("pool", "p"))
+			ck(w.BlankLine())
+			ck(w.Build("r", []string{"foo.o"}, []string{"foo.in"}, nil, nil))
+			ck(w.ScopedAssign("_arg", "arg value"))
+		},
+		output: `pool p
+    depth = 3
+
+# here comes a rule
+rule r
+    command = echo out: $out in: $in _arg: $_arg
+    pool = p
+
+build foo.o: r foo.in
+    _arg = arg value
+`,
+	},
+}
+
+func TestNinjaWriter(t *testing.T) {
+	for i, testCase := range ninjaWriterTestCases {
+		buf := bytes.NewBuffer(nil)
+		w := newNinjaWriter(buf)
+		testCase.input(w)
+		if buf.String() != testCase.output {
+			t.Errorf("incorrect output for test case %d", i)
+			t.Errorf("  expected: %q", testCase.output)
+			t.Errorf("       got: %q", buf.String())
+		}
+	}
+}
diff --git a/blueprint/parser/parser.go b/blueprint/parser/parser.go
new file mode 100644
index 0000000..626493a
--- /dev/null
+++ b/blueprint/parser/parser.go
@@ -0,0 +1,354 @@
+package parser
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+	"text/scanner"
+)
+
+var errTooManyErrors = errors.New("too many errors")
+
+const maxErrors = 1
+
+type ParseError struct {
+	Err error
+	Pos scanner.Position
+}
+
+func (e *ParseError) Error() string {
+	return fmt.Sprintf("%s: %s", e.Pos, e.Err)
+}
+
+func Parse(filename string, r io.Reader) (defs []Definition, errs []error) {
+	p := newParser(r)
+	p.scanner.Filename = filename
+
+	defer func() {
+		if r := recover(); r != nil {
+			if r == errTooManyErrors {
+				defs = nil
+				errs = p.errors
+				return
+			}
+			panic(r)
+		}
+	}()
+
+	defs = p.parseDefinitions()
+	p.accept(scanner.EOF)
+	errs = p.errors
+
+	return
+}
+
+type parser struct {
+	scanner scanner.Scanner
+	tok     rune
+	errors  []error
+}
+
+func newParser(r io.Reader) *parser {
+	p := &parser{}
+	p.scanner.Init(r)
+	p.scanner.Error = func(sc *scanner.Scanner, msg string) {
+		p.errorf(msg)
+	}
+	p.scanner.Mode = scanner.ScanIdents | scanner.ScanStrings |
+		scanner.ScanComments | scanner.SkipComments
+	p.next()
+	return p
+}
+
+func (p *parser) errorf(format string, args ...interface{}) {
+	pos := p.scanner.Position
+	if !pos.IsValid() {
+		pos = p.scanner.Pos()
+	}
+	err := &ParseError{
+		Err: fmt.Errorf(format, args...),
+		Pos: pos,
+	}
+	p.errors = append(p.errors, err)
+	if len(p.errors) >= maxErrors {
+		panic(errTooManyErrors)
+	}
+}
+
+func (p *parser) accept(toks ...rune) bool {
+	for _, tok := range toks {
+		if p.tok != tok {
+			p.errorf("expected %s, found %s", scanner.TokenString(tok),
+				scanner.TokenString(p.tok))
+			return false
+		}
+		if p.tok != scanner.EOF {
+			p.tok = p.scanner.Scan()
+		}
+	}
+	return true
+}
+
+func (p *parser) next() {
+	if p.tok != scanner.EOF {
+		p.tok = p.scanner.Scan()
+	}
+	return
+}
+
+func (p *parser) parseDefinitions() (defs []Definition) {
+	for {
+		switch p.tok {
+		case scanner.Ident:
+			ident := p.scanner.TokenText()
+			pos := p.scanner.Position
+
+			p.accept(scanner.Ident)
+
+			switch p.tok {
+			case '=':
+				defs = append(defs, p.parseAssignment(ident, pos))
+			case '{':
+				defs = append(defs, p.parseModule(ident, pos))
+			default:
+				p.errorf("expected \"=\" or \"{\", found %s",
+					scanner.TokenString(p.tok))
+			}
+		case scanner.EOF:
+			return
+		default:
+			p.errorf("expected assignment or module definition, found %s",
+				scanner.TokenString(p.tok))
+			return
+		}
+	}
+}
+
+func (p *parser) parseAssignment(name string,
+	pos scanner.Position) (assignment *Assignment) {
+
+	assignment = new(Assignment)
+
+	if !p.accept('=') {
+		return
+	}
+	value := p.parseValue()
+
+	assignment.Name = name
+	assignment.Value = value
+	assignment.Pos = pos
+
+	return
+}
+
+func (p *parser) parseModule(typ string,
+	pos scanner.Position) (module *Module) {
+
+	module = new(Module)
+
+	if !p.accept('{') {
+		return
+	}
+	properties := p.parsePropertyList()
+	p.accept('}')
+
+	module.Type = typ
+	module.Properties = properties
+	module.Pos = pos
+	return
+}
+
+func (p *parser) parsePropertyList() (properties []*Property) {
+	for p.tok == scanner.Ident {
+		property := p.parseProperty()
+		properties = append(properties, property)
+
+		if p.tok != ',' {
+			// There was no comma, so the list is done.
+			break
+		}
+
+		p.accept(',')
+	}
+
+	return
+}
+
+func (p *parser) parseProperty() (property *Property) {
+	property = new(Property)
+
+	name := p.scanner.TokenText()
+	pos := p.scanner.Position
+	if !p.accept(scanner.Ident, ':') {
+		return
+	}
+	value := p.parseValue()
+
+	property.Name = name
+	property.Value = value
+	property.Pos = pos
+
+	return
+}
+
+func (p *parser) parseValue() (value Value) {
+	switch p.tok {
+	case scanner.Ident:
+		return p.parseBoolValue()
+	case scanner.String:
+		return p.parseStringValue()
+	case '[':
+		return p.parseListValue()
+	default:
+		p.errorf("expected bool, list, or string value; found %s",
+			scanner.TokenString(p.tok))
+		return
+	}
+}
+
+func (p *parser) parseBoolValue() (value Value) {
+	value.Type = Bool
+	value.Pos = p.scanner.Position
+	switch text := p.scanner.TokenText(); text {
+	case "true":
+		value.BoolValue = true
+	case "false":
+		value.BoolValue = false
+	default:
+		p.errorf("expected true or false; found %q", text)
+		return
+	}
+	p.accept(scanner.Ident)
+	return
+}
+
+func (p *parser) parseStringValue() (value Value) {
+	value.Type = String
+	value.Pos = p.scanner.Position
+	str, err := strconv.Unquote(p.scanner.TokenText())
+	if err != nil {
+		p.errorf("couldn't parse string: %s", err)
+		return
+	}
+	value.StringValue = str
+	p.accept(scanner.String)
+	return
+}
+
+func (p *parser) parseListValue() (value Value) {
+	value.Type = List
+	value.Pos = p.scanner.Position
+	if !p.accept('[') {
+		return
+	}
+
+	var elements []Value
+	for p.tok == scanner.String {
+		elements = append(elements, p.parseStringValue())
+
+		if p.tok != ',' {
+			// There was no comma, so the list is done.
+			break
+		}
+
+		p.accept(',')
+	}
+
+	value.ListValue = elements
+
+	p.accept(']')
+	return
+}
+
+type ValueType int
+
+const (
+	Bool ValueType = iota
+	String
+	List
+)
+
+func (p ValueType) String() string {
+	switch p {
+	case Bool:
+		return "bool"
+	case String:
+		return "string"
+	case List:
+		return "list"
+	default:
+		panic(fmt.Errorf("unknown value type: %d", p))
+	}
+}
+
+type Definition interface {
+	String() string
+	definitionTag()
+}
+
+type Assignment struct {
+	Name  string
+	Value Value
+	Pos   scanner.Position
+}
+
+func (a *Assignment) String() string {
+	return fmt.Sprintf("%s@%d:%s: %s", a.Name, a.Pos.Offset, a.Pos, a.Value)
+}
+
+func (a *Assignment) definitionTag() {}
+
+type Module struct {
+	Type       string
+	Properties []*Property
+	Pos        scanner.Position
+}
+
+func (m *Module) String() string {
+	propertyStrings := make([]string, len(m.Properties))
+	for i, property := range m.Properties {
+		propertyStrings[i] = property.String()
+	}
+	return fmt.Sprintf("%s@%d:%s{%s}", m.Type, m.Pos.Offset, m.Pos,
+		strings.Join(propertyStrings, ", "))
+}
+
+func (m *Module) definitionTag() {}
+
+type Property struct {
+	Name  string
+	Value Value
+	Pos   scanner.Position
+}
+
+func (p *Property) String() string {
+	return fmt.Sprintf("%s@%d:%s: %s", p.Name, p.Pos.Offset, p.Pos, p.Value)
+}
+
+type Value struct {
+	Type        ValueType
+	BoolValue   bool
+	StringValue string
+	ListValue   []Value
+	Pos         scanner.Position
+}
+
+func (p Value) String() string {
+	switch p.Type {
+	case Bool:
+		return fmt.Sprintf("%t@%d:%s", p.BoolValue, p.Pos.Offset, p.Pos)
+	case String:
+		return fmt.Sprintf("%q@%d:%s", p.StringValue, p.Pos.Offset, p.Pos)
+	case List:
+		valueStrings := make([]string, len(p.ListValue))
+		for i, value := range p.ListValue {
+			valueStrings[i] = value.String()
+		}
+		return fmt.Sprintf("@%d:%s[%s]", p.Pos.Offset, p.Pos,
+			strings.Join(valueStrings, ", "))
+	default:
+		panic(fmt.Errorf("bad property type: %d", p.Type))
+	}
+}
diff --git a/blueprint/parser/parser_test.go b/blueprint/parser/parser_test.go
new file mode 100644
index 0000000..57a1966
--- /dev/null
+++ b/blueprint/parser/parser_test.go
@@ -0,0 +1,234 @@
+package parser
+
+import (
+	"bytes"
+	"reflect"
+	"strings"
+	"testing"
+	"text/scanner"
+)
+
+func mkpos(offset, line, column int) scanner.Position {
+	return scanner.Position{
+		Offset: offset,
+		Line:   line,
+		Column: column,
+	}
+}
+
+var validParseTestCases = []struct {
+	input  string
+	output []Definition
+}{
+	{`
+		foo {}
+		`,
+		[]Definition{
+			&Module{
+				Type: "foo",
+				Pos:  mkpos(3, 2, 3),
+			},
+		},
+	},
+
+	{`
+		foo {
+			name: "abc",
+		}
+		`,
+		[]Definition{
+			&Module{
+				Type: "foo",
+				Pos:  mkpos(3, 2, 3),
+				Properties: []*Property{
+					{
+						Name: "name",
+						Pos:  mkpos(12, 3, 4),
+						Value: Value{
+							Type:        String,
+							Pos:         mkpos(18, 3, 10),
+							StringValue: "abc",
+						},
+					},
+				},
+			},
+		},
+	},
+
+	{`
+		foo {
+			isGood: true,
+		}
+		`,
+		[]Definition{
+			&Module{
+				Type: "foo",
+				Pos:  mkpos(3, 2, 3),
+				Properties: []*Property{
+					{
+						Name: "isGood",
+						Pos:  mkpos(12, 3, 4),
+						Value: Value{
+							Type:      Bool,
+							Pos:       mkpos(20, 3, 12),
+							BoolValue: true,
+						},
+					},
+				},
+			},
+		},
+	},
+
+	{`
+		foo {
+			stuff: ["asdf", "jkl;", "qwert",
+				"uiop", "bnm,"]
+		}
+		`,
+		[]Definition{
+			&Module{
+				Type: "foo",
+				Pos:  mkpos(3, 2, 3),
+				Properties: []*Property{
+					{
+						Name: "stuff",
+						Pos:  mkpos(12, 3, 4),
+						Value: Value{
+							Type: List,
+							Pos:  mkpos(19, 3, 11),
+							ListValue: []Value{
+								Value{
+									Type:        String,
+									Pos:         mkpos(20, 3, 12),
+									StringValue: "asdf",
+								},
+								Value{
+									Type:        String,
+									Pos:         mkpos(28, 3, 20),
+									StringValue: "jkl;",
+								},
+								Value{
+									Type:        String,
+									Pos:         mkpos(36, 3, 28),
+									StringValue: "qwert",
+								},
+								Value{
+									Type:        String,
+									Pos:         mkpos(49, 4, 5),
+									StringValue: "uiop",
+								},
+								Value{
+									Type:        String,
+									Pos:         mkpos(57, 4, 13),
+									StringValue: "bnm,",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	},
+
+	{`
+		// comment
+		foo {
+			// comment
+			isGood: true,  // comment
+		}
+		`,
+		[]Definition{
+			&Module{
+				Type: "foo",
+				Pos:  mkpos(16, 3, 3),
+				Properties: []*Property{
+					{
+						Name: "isGood",
+						Pos:  mkpos(39, 5, 4),
+						Value: Value{
+							Type:      Bool,
+							Pos:       mkpos(47, 5, 12),
+							BoolValue: true,
+						},
+					},
+				},
+			},
+		},
+	},
+
+	{`
+		foo {
+			name: "abc",
+		}
+
+		bar {
+			name: "def",
+		}
+		`,
+		[]Definition{
+			&Module{
+				Type: "foo",
+				Pos:  mkpos(3, 2, 3),
+				Properties: []*Property{
+					{
+						Name: "name",
+						Pos:  mkpos(12, 3, 4),
+						Value: Value{
+							Type:        String,
+							Pos:         mkpos(18, 3, 10),
+							StringValue: "abc",
+						},
+					},
+				},
+			},
+			&Module{
+				Type: "bar",
+				Pos:  mkpos(32, 6, 3),
+				Properties: []*Property{
+					{
+						Name: "name",
+						Pos:  mkpos(41, 7, 4),
+						Value: Value{
+							Type:        String,
+							Pos:         mkpos(47, 7, 10),
+							StringValue: "def",
+						},
+					},
+				},
+			},
+		},
+	},
+}
+
+func defListString(defs []Definition) string {
+	defStrings := make([]string, len(defs))
+	for i, def := range defs {
+		defStrings[i] = def.String()
+	}
+
+	return strings.Join(defStrings, ", ")
+}
+
+func TestParseValidInput(t *testing.T) {
+	for _, testCase := range validParseTestCases {
+		r := bytes.NewBufferString(testCase.input)
+		defs, errs := Parse("", r)
+		if len(errs) != 0 {
+			t.Errorf("test case: %s", testCase.input)
+			t.Errorf("unexpected errors:")
+			for _, err := range errs {
+				t.Errorf("  %s", err)
+			}
+			t.FailNow()
+		}
+
+		if !reflect.DeepEqual(defs, testCase.output) {
+			t.Errorf("test case: %s", testCase.input)
+			t.Errorf("incorrect output:")
+			t.Errorf("  expected: %s", defListString(testCase.output))
+			t.Errorf("       got: %s", defListString(defs))
+		}
+	}
+}
+
+// TODO: Test error strings
diff --git a/blueprint/scope.go b/blueprint/scope.go
new file mode 100644
index 0000000..acb4598
--- /dev/null
+++ b/blueprint/scope.go
@@ -0,0 +1,320 @@
+package blueprint
+
+import (
+	"fmt"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+// A Variable represents a global Ninja variable definition that will be written
+// to the output .ninja file.  A variable may contain references to other global
+// Ninja variables, but circular variable references are not allowed.
+type Variable interface {
+	pkg() *pkg
+	name() string                             // "foo"
+	fullName(pkgNames map[*pkg]string) string // "pkg.foo" or "path/to/pkg.foo"
+	value(config Config) (*ninjaString, error)
+}
+
+// A Pool represents a Ninja pool that will be written to the output .ninja
+// file.
+type Pool interface {
+	pkg() *pkg
+	name() string                             // "foo"
+	fullName(pkgNames map[*pkg]string) string // "pkg.foo" or "path/to/pkg.foo"
+	def(config Config) (*poolDef, error)
+}
+
+// A Rule represents a Ninja build rule that will be written to the output
+// .ninja file.
+type Rule interface {
+	pkg() *pkg
+	name() string                             // "foo"
+	fullName(pkgNames map[*pkg]string) string // "pkg.foo" or "path/to/pkg.foo"
+	def(config Config) (*ruleDef, error)
+	scope() *scope
+	isArg(argName string) bool
+}
+
+type scope struct {
+	parent    *scope
+	variables map[string]Variable
+	pools     map[string]Pool
+	rules     map[string]Rule
+	imports   map[string]*scope
+}
+
+func newScope(parent *scope) *scope {
+	return &scope{
+		parent:    parent,
+		variables: make(map[string]Variable),
+		pools:     make(map[string]Pool),
+		rules:     make(map[string]Rule),
+		imports:   make(map[string]*scope),
+	}
+}
+
+func makeRuleScope(parent *scope, argNames map[string]bool) *scope {
+	scope := newScope(parent)
+	for argName := range argNames {
+		_, err := scope.LookupVariable(argName)
+		if err != nil {
+			arg := &argVariable{argName}
+			err = scope.AddVariable(arg)
+			if err != nil {
+				// This should not happen.  We should have already checked that
+				// the name is valid and that the scope doesn't have a variable
+				// with this name.
+				panic(err)
+			}
+		}
+	}
+
+	// We treat built-in variables like arguments for the purpose of this scope.
+	for _, builtin := range builtinRuleArgs {
+		arg := &argVariable{builtin}
+		err := scope.AddVariable(arg)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	return scope
+}
+
+func (s *scope) LookupVariable(name string) (Variable, error) {
+	dotIndex := strings.IndexRune(name, '.')
+	if dotIndex >= 0 {
+		// The variable name looks like "pkg.var"
+		if dotIndex+1 == len(name) {
+			return nil, fmt.Errorf("variable name %q ends with a '.'", name)
+		}
+		if strings.ContainsRune(name[dotIndex+1:], '.') {
+			return nil, fmt.Errorf("variable name %q contains multiple '.' "+
+				"characters", name)
+		}
+
+		pkgName := name[:dotIndex]
+		varName := name[dotIndex+1:]
+
+		first, _ := utf8.DecodeRuneInString(varName)
+		if !unicode.IsUpper(first) {
+			return nil, fmt.Errorf("cannot refer to unexported name %q", name)
+		}
+
+		importedScope, err := s.lookupImportedScope(pkgName)
+		if err != nil {
+			return nil, err
+		}
+
+		v, ok := importedScope.variables[varName]
+		if !ok {
+			return nil, fmt.Errorf("package %q does not contain variable %q",
+				pkgName, varName)
+		}
+
+		return v, nil
+	} else {
+		// The variable name has no package part; just "var"
+		for ; s != nil; s = s.parent {
+			v, ok := s.variables[name]
+			if ok {
+				return v, nil
+			}
+		}
+		return nil, fmt.Errorf("undefined variable %q", name)
+	}
+}
+
+func (s *scope) lookupImportedScope(pkgName string) (*scope, error) {
+	for ; s != nil; s = s.parent {
+		importedScope, ok := s.imports[pkgName]
+		if ok {
+			return importedScope, nil
+		}
+	}
+	return nil, fmt.Errorf("unknown imported package %q (missing call to "+
+		"blueprint.Import()?)", pkgName)
+}
+
+func (s *scope) AddImport(name string, importedScope *scope) error {
+	_, present := s.imports[name]
+	if present {
+		return fmt.Errorf("import %q is already defined in this scope", name)
+	}
+	s.imports[name] = importedScope
+	return nil
+}
+
+func (s *scope) AddVariable(v Variable) error {
+	name := v.name()
+	_, present := s.variables[name]
+	if present {
+		return fmt.Errorf("variable %q is already defined in this scope", name)
+	}
+	s.variables[name] = v
+	return nil
+}
+
+func (s *scope) AddPool(p Pool) error {
+	name := p.name()
+	_, present := s.pools[name]
+	if present {
+		return fmt.Errorf("pool %q is already defined in this scope", name)
+	}
+	s.pools[name] = p
+	return nil
+}
+
+func (s *scope) AddRule(r Rule) error {
+	name := r.name()
+	_, present := s.rules[name]
+	if present {
+		return fmt.Errorf("rule %q is already defined in this scope", name)
+	}
+	s.rules[name] = r
+	return nil
+}
+
+type localScope struct {
+	namePrefix string
+	scope      *scope
+}
+
+func newLocalScope(parent *scope, namePrefix string) *localScope {
+	return &localScope{
+		namePrefix: namePrefix,
+		scope:      newScope(parent),
+	}
+}
+
+func (s *localScope) LookupVariable(name string) (Variable, error) {
+	return s.scope.LookupVariable(name)
+}
+
+func (s *localScope) AddLocalVariable(name, value string) (*localVariable,
+	error) {
+
+	err := validateNinjaName(name)
+	if err != nil {
+		return nil, err
+	}
+
+	if strings.ContainsRune(name, '.') {
+		return nil, fmt.Errorf("local variable name %q contains '.'", name)
+	}
+
+	ninjaValue, err := parseNinjaString(s.scope, value)
+	if err != nil {
+		return nil, err
+	}
+
+	v := &localVariable{
+		namePrefix: s.namePrefix,
+		name_:      name,
+		value_:     ninjaValue,
+	}
+
+	err = s.scope.AddVariable(v)
+	if err != nil {
+		return nil, err
+	}
+
+	return v, nil
+}
+
+func (s *localScope) AddLocalRule(name string, params *RuleParams,
+	argNames ...string) (*localRule, error) {
+
+	err := validateNinjaName(name)
+	if err != nil {
+		return nil, err
+	}
+
+	err = validateArgNames(argNames)
+	if err != nil {
+		return nil, fmt.Errorf("invalid argument name: %s", err)
+	}
+
+	argNamesSet := make(map[string]bool)
+	for _, argName := range argNames {
+		argNamesSet[argName] = true
+	}
+
+	ruleScope := makeRuleScope(s.scope, argNamesSet)
+
+	def, err := parseRuleParams(ruleScope, params)
+	if err != nil {
+		return nil, err
+	}
+
+	r := &localRule{
+		namePrefix: s.namePrefix,
+		name_:      name,
+		def_:       def,
+		argNames:   argNamesSet,
+		scope_:     ruleScope,
+	}
+
+	err = s.scope.AddRule(r)
+	if err != nil {
+		return nil, err
+	}
+
+	return r, nil
+}
+
+type localVariable struct {
+	namePrefix string
+	name_      string
+	value_     *ninjaString
+}
+
+func (l *localVariable) pkg() *pkg {
+	return nil
+}
+
+func (l *localVariable) name() string {
+	return l.name_
+}
+
+func (l *localVariable) fullName(pkgNames map[*pkg]string) string {
+	return l.namePrefix + l.name_
+}
+
+func (l *localVariable) value(Config) (*ninjaString, error) {
+	return l.value_, nil
+}
+
+type localRule struct {
+	namePrefix string
+	name_      string
+	def_       *ruleDef
+	argNames   map[string]bool
+	scope_     *scope
+}
+
+func (l *localRule) pkg() *pkg {
+	return nil
+}
+
+func (l *localRule) name() string {
+	return l.name_
+}
+
+func (l *localRule) fullName(pkgNames map[*pkg]string) string {
+	return l.namePrefix + l.name_
+}
+
+func (l *localRule) def(Config) (*ruleDef, error) {
+	return l.def_, nil
+}
+
+func (r *localRule) scope() *scope {
+	return r.scope_
+}
+
+func (r *localRule) isArg(argName string) bool {
+	return r.argNames[argName]
+}
diff --git a/blueprint/singleton_ctx.go b/blueprint/singleton_ctx.go
new file mode 100644
index 0000000..549d079
--- /dev/null
+++ b/blueprint/singleton_ctx.go
@@ -0,0 +1,137 @@
+package blueprint
+
+import (
+	"fmt"
+	"path/filepath"
+)
+
+type Singleton interface {
+	GenerateBuildActions(SingletonContext)
+}
+
+type SingletonContext interface {
+	Config() Config
+
+	ModuleName(module Module) string
+	ModuleDir(module Module) string
+	BlueprintFile(module Module) string
+
+	ModuleErrorf(module Module, format string, args ...interface{})
+	Errorf(format string, args ...interface{})
+
+	Variable(name, value string)
+	Rule(name string, params RuleParams) Rule
+	Build(params BuildParams)
+	RequireNinjaVersion(major, minor, micro int)
+
+	// SetBuildDir sets the value of the top-level "builddir" Ninja variable
+	// that controls where Ninja stores its build log files.  This value can be
+	// set at most one time for a single build.  Setting it multiple times (even
+	// across different singletons) will result in a panic.
+	SetBuildDir(value string)
+
+	VisitAllModules(visit func(Module))
+	VisitAllModulesIf(pred func(Module) bool, visit func(Module))
+}
+
+var _ SingletonContext = (*singletonContext)(nil)
+
+type singletonContext struct {
+	context *Context
+	config  Config
+	scope   *localScope
+
+	errs []error
+
+	actionDefs localBuildActions
+}
+
+func (s *singletonContext) Config() Config {
+	return s.config
+}
+
+func (s *singletonContext) ModuleName(module Module) string {
+	info := s.context.moduleInfo[module]
+	return info.properties.Name
+}
+
+func (s *singletonContext) ModuleDir(module Module) string {
+	info := s.context.moduleInfo[module]
+	return filepath.Dir(info.relBlueprintFile)
+}
+
+func (s *singletonContext) BlueprintFile(module Module) string {
+	info := s.context.moduleInfo[module]
+	return info.relBlueprintFile
+}
+
+func (s *singletonContext) ModuleErrorf(module Module, format string,
+	args ...interface{}) {
+
+	info := s.context.moduleInfo[module]
+	s.errs = append(s.errs, &Error{
+		Err: fmt.Errorf(format, args...),
+		Pos: info.pos,
+	})
+}
+
+func (s *singletonContext) Errorf(format string, args ...interface{}) {
+	// TODO: Make this not result in the error being printed as "internal error"
+	s.errs = append(s.errs, fmt.Errorf(format, args...))
+}
+
+func (s *singletonContext) Variable(name, value string) {
+	v, err := s.scope.AddLocalVariable(name, value)
+	if err != nil {
+		panic(err)
+	}
+
+	s.actionDefs.variables = append(s.actionDefs.variables, v)
+}
+
+func (s *singletonContext) Rule(name string, params RuleParams) Rule {
+	// TODO: Verify that params.Pool is accessible in this module's scope.
+
+	r, err := s.scope.AddLocalRule(name, &params)
+	if err != nil {
+		panic(err)
+	}
+
+	s.actionDefs.rules = append(s.actionDefs.rules, r)
+
+	return r
+}
+
+func (s *singletonContext) Build(params BuildParams) {
+	// TODO: Verify that params.Rule is accessible in this module's scope.
+
+	def, err := parseBuildParams(s.scope, &params)
+	if err != nil {
+		panic(err)
+	}
+
+	s.actionDefs.buildDefs = append(s.actionDefs.buildDefs, def)
+}
+
+func (s *singletonContext) RequireNinjaVersion(major, minor, micro int) {
+	s.context.requireNinjaVersion(major, minor, micro)
+}
+
+func (s *singletonContext) SetBuildDir(value string) {
+	ninjaValue, err := parseNinjaString(s.scope, value)
+	if err != nil {
+		panic(err)
+	}
+
+	s.context.setBuildDir(ninjaValue)
+}
+
+func (s *singletonContext) VisitAllModules(visit func(Module)) {
+	s.context.visitAllModules(visit)
+}
+
+func (s *singletonContext) VisitAllModulesIf(pred func(Module) bool,
+	visit func(Module)) {
+
+	s.context.visitAllModulesIf(pred, visit)
+}
diff --git a/blueprint/unpack.go b/blueprint/unpack.go
new file mode 100644
index 0000000..98f0fe2
--- /dev/null
+++ b/blueprint/unpack.go
@@ -0,0 +1,196 @@
+package blueprint
+
+import (
+	"blueprint/parser"
+	"fmt"
+	"reflect"
+	"unicode"
+	"unicode/utf8"
+)
+
+type packedProperty struct {
+	property *parser.Property
+	unpacked bool
+}
+
+func unpackProperties(propertyDefs []*parser.Property,
+	propertiesStructs ...interface{}) (errs []error) {
+
+	propertyMap := make(map[string]*packedProperty)
+	for _, propertyDef := range propertyDefs {
+		name := propertyDef.Name
+		if first, present := propertyMap[name]; present {
+			errs = append(errs, &Error{
+				Err: fmt.Errorf("property %q already defined", name),
+				Pos: propertyDef.Pos,
+			})
+			errs = append(errs, &Error{
+				Err: fmt.Errorf("--> previous definition here"),
+				Pos: first.property.Pos,
+			})
+			if len(errs) >= maxErrors {
+				return errs
+			}
+			continue
+		}
+
+		propertyMap[name] = &packedProperty{
+			property: propertyDef,
+			unpacked: false,
+		}
+	}
+
+	for _, properties := range propertiesStructs {
+		propertiesValue := reflect.ValueOf(properties)
+		if propertiesValue.Kind() != reflect.Ptr {
+			panic("properties must be a pointer to a struct")
+		}
+
+		propertiesValue = propertiesValue.Elem()
+		if propertiesValue.Kind() != reflect.Struct {
+			panic("properties must be a pointer to a struct")
+		}
+
+		newErrs := unpackStruct(propertiesValue, propertyMap)
+		errs = append(errs, newErrs...)
+
+		if len(errs) >= maxErrors {
+			return errs
+		}
+	}
+
+	// Report any properties that didn't have corresponding struct fields as
+	// errors.
+	for name, packedProperty := range propertyMap {
+		if !packedProperty.unpacked {
+			err := &Error{
+				Err: fmt.Errorf("unrecognized property %q", name),
+				Pos: packedProperty.property.Pos,
+			}
+			errs = append(errs, err)
+		}
+	}
+
+	return errs
+}
+
+func unpackStruct(structValue reflect.Value,
+	propertyMap map[string]*packedProperty) []error {
+
+	structType := structValue.Type()
+
+	var errs []error
+	for i := 0; i < structValue.NumField(); i++ {
+		fieldValue := structValue.Field(i)
+		field := structType.Field(i)
+
+		if !fieldValue.CanSet() {
+			panic(fmt.Errorf("field %s is not settable", field.Name))
+		}
+
+		// To make testing easier we validate the struct field's type regardless
+		// of whether or not the property was specified in the parsed string.
+		switch kind := fieldValue.Kind(); kind {
+		case reflect.Bool, reflect.String:
+			// Do nothing
+		case reflect.Slice:
+			elemType := field.Type.Elem()
+			if elemType.Kind() != reflect.String {
+				panic(fmt.Errorf("field %s is a non-string slice", field.Name))
+			}
+		case reflect.Struct:
+			newErrs := unpackStruct(fieldValue, propertyMap)
+			errs = append(errs, newErrs...)
+			if len(errs) >= maxErrors {
+				return errs
+			}
+			continue // This field doesn't correspond to a specific property.
+		default:
+			panic(fmt.Errorf("unsupported kind for field %s: %s",
+				field.Name, kind))
+		}
+
+		// Get the property value if it was specified.
+		propertyName := propertyNameForField(field)
+		packedProperty, ok := propertyMap[propertyName]
+		if !ok {
+			// This property wasn't specified.
+			continue
+		}
+
+		packedProperty.unpacked = true
+
+		var newErrs []error
+		switch kind := fieldValue.Kind(); kind {
+		case reflect.Bool:
+			newErrs = unpackBool(fieldValue, packedProperty.property)
+		case reflect.String:
+			newErrs = unpackString(fieldValue, packedProperty.property)
+		case reflect.Slice:
+			newErrs = unpackSlice(fieldValue, packedProperty.property)
+		}
+		errs = append(errs, newErrs...)
+		if len(errs) >= maxErrors {
+			return errs
+		}
+	}
+
+	return errs
+}
+
+func unpackBool(boolValue reflect.Value, property *parser.Property) []error {
+	if property.Value.Type != parser.Bool {
+		return []error{
+			fmt.Errorf("%s: can't assign %s value to %s property %q",
+				property.Value.Pos, property.Value.Type, parser.Bool,
+				property.Name),
+		}
+	}
+	boolValue.SetBool(property.Value.BoolValue)
+	return nil
+}
+
+func unpackString(stringValue reflect.Value,
+	property *parser.Property) []error {
+
+	if property.Value.Type != parser.String {
+		return []error{
+			fmt.Errorf("%s: can't assign %s value to %s property %q",
+				property.Value.Pos, property.Value.Type, parser.String,
+				property.Name),
+		}
+	}
+	stringValue.SetString(property.Value.StringValue)
+	return nil
+}
+
+func unpackSlice(sliceValue reflect.Value, property *parser.Property) []error {
+	if property.Value.Type != parser.List {
+		return []error{
+			fmt.Errorf("%s: can't assign %s value to %s property %q",
+				property.Value.Pos, property.Value.Type, parser.List,
+				property.Name),
+		}
+	}
+
+	var list []string
+	for _, value := range property.Value.ListValue {
+		if value.Type != parser.String {
+			// The parser should not produce this.
+			panic("non-string value found in list")
+		}
+		list = append(list, value.StringValue)
+	}
+
+	sliceValue.Set(reflect.ValueOf(list))
+	return nil
+}
+
+func propertyNameForField(field reflect.StructField) string {
+	r, size := utf8.DecodeRuneInString(field.Name)
+	propertyName := string(unicode.ToLower(r))
+	if len(field.Name) > size {
+		propertyName += field.Name[size:]
+	}
+	return propertyName
+}
diff --git a/blueprint/unpack_test.go b/blueprint/unpack_test.go
new file mode 100644
index 0000000..91a9fbb
--- /dev/null
+++ b/blueprint/unpack_test.go
@@ -0,0 +1,124 @@
+package blueprint
+
+import (
+	"blueprint/parser"
+	"bytes"
+	"reflect"
+	"testing"
+)
+
+var validUnpackTestCases = []struct {
+	input  string
+	output interface{}
+}{
+	{`
+		m {
+			name: "abc",
+		}
+		`,
+		struct {
+			Name string
+		}{
+			Name: "abc",
+		},
+	},
+
+	{`
+		m {
+			isGood: true,
+		}
+		`,
+		struct {
+			IsGood bool
+		}{
+			IsGood: true,
+		},
+	},
+
+	{`
+		m {
+			stuff: ["asdf", "jkl;", "qwert",
+				"uiop", "bnm,"]
+		}
+		`,
+		struct {
+			Stuff []string
+		}{
+			Stuff: []string{"asdf", "jkl;", "qwert", "uiop", "bnm,"},
+		},
+	},
+
+	{`
+		m {
+			name: "abc",
+		}
+		`,
+		struct {
+			Nested struct {
+				Name string
+			}
+		}{
+			Nested: struct{ Name string }{
+				Name: "abc",
+			},
+		},
+	},
+
+	{`
+		m {
+			foo: "abc",
+			bar: false,
+			baz: ["def", "ghi"],
+		}
+		`,
+		struct {
+			Nested struct {
+				Foo string
+			}
+			Bar bool
+			Baz []string
+		}{
+			Nested: struct{ Foo string }{
+				Foo: "abc",
+			},
+			Bar: false,
+			Baz: []string{"def", "ghi"},
+		},
+	},
+}
+
+func TestUnpackProperties(t *testing.T) {
+	for _, testCase := range validUnpackTestCases {
+		r := bytes.NewBufferString(testCase.input)
+		defs, errs := parser.Parse("", r)
+		if len(errs) != 0 {
+			t.Errorf("test case: %s", testCase.input)
+			t.Errorf("unexpected parse errors:")
+			for _, err := range errs {
+				t.Errorf("  %s", err)
+			}
+			t.FailNow()
+		}
+
+		module := defs[0].(*parser.Module)
+		propertiesType := reflect.TypeOf(testCase.output)
+		properties := reflect.New(propertiesType)
+		errs = unpackProperties(module.Properties, properties.Interface())
+		if len(errs) != 0 {
+			t.Errorf("test case: %s", testCase.input)
+			t.Errorf("unexpected unpack errors:")
+			for _, err := range errs {
+				t.Errorf("  %s", err)
+			}
+			t.FailNow()
+		}
+
+		output := properties.Elem().Interface()
+		if !reflect.DeepEqual(output, testCase.output) {
+			t.Errorf("test case: %s", testCase.input)
+			t.Errorf("incorrect output:")
+			t.Errorf("  expected: %+v", testCase.output)
+			t.Errorf("       got: %+v", output)
+		}
+	}
+}
diff --git a/bootstrap.bash b/bootstrap.bash
new file mode 100755
index 0000000..f9ad8ad
--- /dev/null
+++ b/bootstrap.bash
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# SRCDIR should be set to the path of the root source directory.  It can be
+# either an absolute path or a path relative to the build directory.  Whether
+# its an absolute or relative path determines whether the build directory can be
+# moved relative to or along with the source directory without re-running the
+# bootstrap script.
+SRCDIR=`dirname "${BASH_SOURCE[0]}"`
+
+# BOOTSTRAP should be set to the path of this script.  It can be either an
+# absolute path or one relative to the build directory (which of these is used
+# should probably match what's used for SRCDIR).
+BOOTSTRAP="${BASH_SOURCE[0]}"
+
+# These variables should be set by auto-detecting or knowing a priori the Go
+# toolchain properties.
+GOROOT=`go env GOROOT`
+GOOS=`go env GOHOSTOS`
+GOARCH=`go env GOHOSTARCH`
+GOCHAR=`go env GOCHAR`
+
+case "$#" in
+    1) IN="$1";BOOTSTRAP_MANIFEST="$1";;
+	2) IN="$1";BOOTSTRAP_MANIFEST="$2";;
+    *) IN="${SRCDIR}/build.ninja.in";BOOTSTRAP_MANIFEST="$IN";;
+esac
+
+sed -e "s|@@SrcDir@@|$SRCDIR|g"                        \
+    -e "s|@@GoRoot@@|$GOROOT|g"                        \
+    -e "s|@@GoOS@@|$GOOS|g"                            \
+    -e "s|@@GoArch@@|$GOARCH|g"                        \
+    -e "s|@@GoChar@@|$GOCHAR|g"                        \
+    -e "s|@@Bootstrap@@|$BOOTSTRAP|g"                  \
+    -e "s|@@BootstrapManifest@@|$BOOTSTRAP_MANIFEST|g" \
+    $IN > build.ninja
\ No newline at end of file
diff --git a/build.ninja.in b/build.ninja.in
new file mode 100644
index 0000000..b7983ab
--- /dev/null
+++ b/build.ninja.in
@@ -0,0 +1,156 @@
+# ******************************************************************************
+# ***            This file is generated and should not be edited             ***
+# ******************************************************************************
+#
+# This file contains variables, rules, and pools with name prefixes indicating
+# they were generated by the following Go packages:
+#
+#     bootstrap [from Go package blueprint/bootstrap]
+#
+ninja_required_version = 1.1.0
+
+g.bootstrap.Bootstrap = @@Bootstrap@@
+
+g.bootstrap.BootstrapManifest = @@BootstrapManifest@@
+
+g.bootstrap.GoArch = @@GoArch@@
+
+g.bootstrap.GoChar = @@GoChar@@
+
+g.bootstrap.GoOS = @@GoOS@@
+
+g.bootstrap.GoRoot = @@GoRoot@@
+
+g.bootstrap.SrcDir = @@SrcDir@@
+
+g.bootstrap.goToolDir = ${g.bootstrap.GoRoot}/pkg/tool/${g.bootstrap.GoOS}_${g.bootstrap.GoArch}
+
+g.bootstrap.gcCmd = ${g.bootstrap.goToolDir}/${g.bootstrap.GoChar}g
+
+g.bootstrap.linkCmd = ${g.bootstrap.goToolDir}/${g.bootstrap.GoChar}l
+
+g.bootstrap.packCmd = ${g.bootstrap.goToolDir}/pack
+
+builddir = bootstrap
+
+rule g.bootstrap.gc
+    command = GOROOT='${g.bootstrap.GoRoot}' ${g.bootstrap.gcCmd} -o ${out} -p ${pkgPath} -complete ${incFlags} ${in}
+    description = ${g.bootstrap.GoChar}g ${out}
+
+rule g.bootstrap.pack
+    command = GOROOT='${g.bootstrap.GoRoot}' ${g.bootstrap.packCmd} grcP ${prefix} ${out} ${in}
+    description = pack ${out}
+
+rule g.bootstrap.link
+    command = GOROOT='${g.bootstrap.GoRoot}' ${g.bootstrap.linkCmd} -o ${out} ${libDirFlags} ${in}
+    description = ${g.bootstrap.GoChar}l ${out}
+
+rule g.bootstrap.cp
+    command = cp ${in} ${out}
+    description = cp ${out}
+
+rule g.bootstrap.bootstrap
+    command = ${g.bootstrap.Bootstrap} ${in} ${g.bootstrap.BootstrapManifest}
+    description = bootstrap ${in}
+    generator = true
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module:  blueprint
+# Type:    bootstrap_go_package
+# GoType:  blueprint/bootstrap.goPackageModule
+# Defined: ../Blueprints:1:1
+
+build bootstrap/blueprint/obj/_go_.${g.bootstrap.GoChar}: g.bootstrap.gc $
+        ${g.bootstrap.SrcDir}/blueprint/context.go $
+        ${g.bootstrap.SrcDir}/blueprint/globals.go $
+        ${g.bootstrap.SrcDir}/blueprint/live_tracker.go $
+        ${g.bootstrap.SrcDir}/blueprint/mangle.go $
+        ${g.bootstrap.SrcDir}/blueprint/module_ctx.go $
+        ${g.bootstrap.SrcDir}/blueprint/ninja_defs.go $
+        ${g.bootstrap.SrcDir}/blueprint/ninja_strings.go $
+        ${g.bootstrap.SrcDir}/blueprint/ninja_writer.go $
+        ${g.bootstrap.SrcDir}/blueprint/scope.go $
+        ${g.bootstrap.SrcDir}/blueprint/singleton_ctx.go $
+        ${g.bootstrap.SrcDir}/blueprint/unpack.go | $
+        bootstrap/blueprint-parser/pkg/blueprint/parser.a
+    pkgPath = blueprint
+    incFlags = -I bootstrap/blueprint-parser/pkg
+
+build bootstrap/blueprint/pkg/blueprint.a: g.bootstrap.pack $
+        bootstrap/blueprint/obj/_go_.${g.bootstrap.GoChar}
+    prefix = bootstrap/blueprint/pkg
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module:  blueprint-parser
+# Type:    bootstrap_go_package
+# GoType:  blueprint/bootstrap.goPackageModule
+# Defined: ../Blueprints:18:1
+
+build bootstrap/blueprint-parser/obj/_go_.${g.bootstrap.GoChar}: $
+        g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/parser/parser.go
+    pkgPath = blueprint/parser
+
+build bootstrap/blueprint-parser/pkg/blueprint/parser.a: g.bootstrap.pack $
+        bootstrap/blueprint-parser/obj/_go_.${g.bootstrap.GoChar}
+    prefix = bootstrap/blueprint-parser/pkg
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module:  blueprint-bootstrap
+# Type:    bootstrap_go_package
+# GoType:  blueprint/bootstrap.goPackageModule
+# Defined: ../Blueprints:24:1
+
+build bootstrap/blueprint-bootstrap/obj/_go_.${g.bootstrap.GoChar}: $
+        g.bootstrap.gc ${g.bootstrap.SrcDir}/blueprint/bootstrap/bootstrap.go $
+        ${g.bootstrap.SrcDir}/blueprint/bootstrap/command.go $
+        ${g.bootstrap.SrcDir}/blueprint/bootstrap/config.go $
+        ${g.bootstrap.SrcDir}/blueprint/bootstrap/doc.go | $
+        bootstrap/blueprint-parser/pkg/blueprint/parser.a $
+        bootstrap/blueprint/pkg/blueprint.a
+    pkgPath = blueprint/bootstrap
+    incFlags = -I bootstrap/blueprint-parser/pkg -I bootstrap/blueprint/pkg
+
+build bootstrap/blueprint-bootstrap/pkg/blueprint/bootstrap.a: $
+        g.bootstrap.pack $
+        bootstrap/blueprint-bootstrap/obj/_go_.${g.bootstrap.GoChar}
+    prefix = bootstrap/blueprint-bootstrap/pkg
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module:  minibp
+# Type:    bootstrap_go_binary
+# GoType:  blueprint/bootstrap.goBinaryModule
+# Defined: ../Blueprints:34:1
+
+build bootstrap/minibp/obj/_go_.${g.bootstrap.GoChar}: g.bootstrap.gc $
+        ${g.bootstrap.SrcDir}/blueprint/bootstrap/minibp/main.go | $
+        bootstrap/blueprint-parser/pkg/blueprint/parser.a $
+        bootstrap/blueprint/pkg/blueprint.a $
+        bootstrap/blueprint-bootstrap/pkg/blueprint/bootstrap.a
+    pkgPath = minibp
+    incFlags = -I bootstrap/blueprint-parser/pkg -I bootstrap/blueprint/pkg -I bootstrap/blueprint-bootstrap/pkg
+
+build bootstrap/minibp/obj/minibp.a: g.bootstrap.pack $
+        bootstrap/minibp/obj/_go_.${g.bootstrap.GoChar}
+    prefix = bootstrap/minibp/obj
+
+build bootstrap/minibp/obj/a.out: g.bootstrap.link $
+        bootstrap/minibp/obj/minibp.a
+    libDirFlags = -L bootstrap/blueprint-parser/pkg -L bootstrap/blueprint/pkg -L bootstrap/blueprint-bootstrap/pkg
+
+build bootstrap/bin/minibp: g.bootstrap.cp bootstrap/minibp/obj/a.out
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Singleton: bootstrap
+# GoType:    blueprint/bootstrap.singleton
+
+rule s.bootstrap.bigbp
+    command = bootstrap/bin/minibp -p -d bootstrap/build.ninja.in.d -o ${out} ${in}
+    depfile = bootstrap/build.ninja.in.d
+    description = minibp ${out}
+
+build bootstrap/build.ninja.in: s.bootstrap.bigbp $
+        ${g.bootstrap.SrcDir}/Blueprints | bootstrap/bin/minibp
+build bootstrap/notAFile: phony
+build build.ninja: g.bootstrap.bootstrap bootstrap/build.ninja.in | $
+        ${g.bootstrap.Bootstrap} bootstrap/notAFile bootstrap/bin/minibp
+