Merge pull request #29 from colincross/doc

Add self-documenting support
diff --git a/Blueprints b/Blueprints
index c22bb5a..46b5c84 100644
--- a/Blueprints
+++ b/Blueprints
@@ -73,6 +73,7 @@
         "blueprint",
         "blueprint-deptools",
         "blueprint-pathtools",
+        "blueprint-bootstrap-bpdoc",
     ],
     pkgPath = "github.com/google/blueprint/bootstrap",
     srcs = [
@@ -81,6 +82,19 @@
         "bootstrap/command.go",
         "bootstrap/config.go",
         "bootstrap/doc.go",
+        "bootstrap/writedocs.go",
+    ],
+)
+
+bootstrap_go_package(
+    name = "blueprint-bootstrap-bpdoc",
+    deps = [
+        "blueprint",
+        "blueprint-proptools",
+    ],
+    pkgPath = "github.com/google/blueprint/bootstrap/bpdoc",
+    srcs = [
+        "bootstrap/bpdoc/bpdoc.go",
     ],
 )
 
diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go
index ec6642e..c07f863 100644
--- a/bootstrap/bootstrap.go
+++ b/bootstrap/bootstrap.go
@@ -16,12 +16,13 @@
 
 import (
 	"fmt"
-	"github.com/google/blueprint"
-	"github.com/google/blueprint/pathtools"
 	"os"
 	"path/filepath"
 	"strconv"
 	"strings"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/pathtools"
 )
 
 const bootstrapDir = ".bootstrap"
@@ -116,6 +117,8 @@
 
 	BinDir     = filepath.Join(bootstrapDir, "bin")
 	minibpFile = filepath.Join(BinDir, "minibp")
+
+	docsDir = filepath.Join(bootstrapDir, "docs")
 )
 
 type goPackageProducer interface {
@@ -505,13 +508,13 @@
 	// creating the binary that we'll use to generate the non-bootstrap
 	// build.ninja file.
 	var primaryBuilders []*goBinary
-	var allGoBinaries []string
+	var rebootstrapDeps []string
 	ctx.VisitAllModulesIf(isBootstrapBinaryModule,
 		func(module blueprint.Module) {
 			binaryModule := module.(*goBinary)
 			binaryModuleName := ctx.ModuleName(binaryModule)
 			binaryModulePath := filepath.Join(BinDir, binaryModuleName)
-			allGoBinaries = append(allGoBinaries, binaryModulePath)
+			rebootstrapDeps = append(rebootstrapDeps, binaryModulePath)
 			if binaryModule.properties.PrimaryBuilder {
 				primaryBuilders = append(primaryBuilders, binaryModule)
 			}
@@ -552,6 +555,9 @@
 	mainNinjaFile := filepath.Join(bootstrapDir, "main.ninja.in")
 	mainNinjaDepFile := mainNinjaFile + ".d"
 	bootstrapNinjaFile := filepath.Join(bootstrapDir, "bootstrap.ninja.in")
+	docsFile := filepath.Join(docsDir, primaryBuilderName+".html")
+
+	rebootstrapDeps = append(rebootstrapDeps, docsFile)
 
 	if s.config.generatingBootstrapper {
 		// We're generating a bootstrapper Ninja file, so we need to set things
@@ -564,6 +570,24 @@
 		// two Ninja processes try to write to the same log concurrently.
 		ctx.SetBuildDir(pctx, bootstrapDir)
 
+		// Generate build system docs for the primary builder.  Generating docs reads the source
+		// files used to build the primary builder, but that dependency will be picked up through
+		// the dependency on the primary builder itself.  There are no dependencies on the
+		// Blueprints files, as any relevant changes to the Blueprints files would have caused
+		// a rebuild of the primary builder.
+		bigbpDocs := ctx.Rule(pctx, "bigbpDocs",
+			blueprint.RuleParams{
+				Command: fmt.Sprintf("%s %s --docs $out %s", primaryBuilderFile,
+					primaryBuilderExtraFlags, topLevelBlueprints),
+				Description: fmt.Sprintf("%s docs $out", primaryBuilderName),
+			})
+
+		ctx.Build(pctx, blueprint.BuildParams{
+			Rule:      bigbpDocs,
+			Outputs:   []string{docsFile},
+			Implicits: []string{primaryBuilderFile},
+		})
+
 		// 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-
@@ -584,7 +608,7 @@
 			Rule:      bigbp,
 			Outputs:   []string{mainNinjaFile},
 			Inputs:    []string{topLevelBlueprints},
-			Implicits: allGoBinaries,
+			Implicits: rebootstrapDeps,
 		})
 
 		// When the current build.ninja file is a bootstrapper, we always want
@@ -639,13 +663,12 @@
 			Args:      args,
 		})
 	} else {
-		var allGoTests []string
 		ctx.VisitAllModulesIf(isGoTestProducer,
 			func(module blueprint.Module) {
 				testModule := module.(goTestProducer)
 				target := testModule.GoTestTarget()
 				if target != "" {
-					allGoTests = append(allGoTests, target)
+					rebootstrapDeps = append(rebootstrapDeps, target)
 				}
 			})
 
@@ -661,8 +684,8 @@
 		// rule.  We do this by depending on that file and then setting up a
 		// phony rule to generate it that uses the depfile.
 		buildNinjaDeps := []string{"$bootstrapCmd", mainNinjaFile}
-		buildNinjaDeps = append(buildNinjaDeps, allGoBinaries...)
-		buildNinjaDeps = append(buildNinjaDeps, allGoTests...)
+		buildNinjaDeps = append(buildNinjaDeps, rebootstrapDeps...)
+
 		ctx.Build(pctx, blueprint.BuildParams{
 			Rule:      rebootstrap,
 			Outputs:   []string{"build.ninja"},
@@ -679,6 +702,12 @@
 			},
 		})
 
+		ctx.Build(pctx, blueprint.BuildParams{
+			Rule:      phony,
+			Outputs:   []string{docsFile},
+			Implicits: []string{primaryBuilderFile},
+		})
+
 		// If the bootstrap Ninja invocation caused a new bootstrapNinjaFile to be
 		// generated then that means we need to rebootstrap using it instead of
 		// the current bootstrap manifest.  We enable the Ninja "generator"
diff --git a/bootstrap/bpdoc/bpdoc.go b/bootstrap/bpdoc/bpdoc.go
new file mode 100644
index 0000000..50a87df
--- /dev/null
+++ b/bootstrap/bpdoc/bpdoc.go
@@ -0,0 +1,698 @@
+package bpdoc
+
+import (
+	"bytes"
+	"fmt"
+	"go/ast"
+	"go/doc"
+	"go/parser"
+	"go/token"
+	"io/ioutil"
+	"reflect"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"text/template"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+)
+
+type DocCollector struct {
+	pkgFiles map[string][]string // Map of package name to source files, provided by constructor
+
+	mutex   sync.Mutex
+	pkgDocs map[string]*doc.Package        // Map of package name to parsed Go AST, protected by mutex
+	docs    map[string]*PropertyStructDocs // Map of type name to docs, protected by mutex
+}
+
+func NewDocCollector(pkgFiles map[string][]string) *DocCollector {
+	return &DocCollector{
+		pkgFiles: pkgFiles,
+		pkgDocs:  make(map[string]*doc.Package),
+		docs:     make(map[string]*PropertyStructDocs),
+	}
+}
+
+// Return the PropertyStructDocs associated with a property struct type.  The type should be in the
+// format <package path>.<type name>
+func (dc *DocCollector) Docs(pkg, name string, defaults reflect.Value) (*PropertyStructDocs, error) {
+	docs := dc.getDocs(name)
+
+	if docs == nil {
+		pkgDocs, err := dc.packageDocs(pkg)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, t := range pkgDocs.Types {
+			if t.Name == name {
+				docs, err = newDocs(t)
+				if err != nil {
+					return nil, err
+				}
+				docs = dc.putDocs(name, docs)
+			}
+		}
+	}
+
+	if docs == nil {
+		return nil, fmt.Errorf("package %q type %q not found", pkg, name)
+	}
+
+	docs = docs.Clone()
+	docs.SetDefaults(defaults)
+
+	return docs, nil
+}
+
+func (dc *DocCollector) getDocs(name string) *PropertyStructDocs {
+	dc.mutex.Lock()
+	defer dc.mutex.Unlock()
+
+	return dc.docs[name]
+}
+
+func (dc *DocCollector) putDocs(name string, docs *PropertyStructDocs) *PropertyStructDocs {
+	dc.mutex.Lock()
+	defer dc.mutex.Unlock()
+
+	if dc.docs[name] != nil {
+		return dc.docs[name]
+	} else {
+		dc.docs[name] = docs
+		return docs
+	}
+}
+
+type PropertyStructDocs struct {
+	Name       string
+	Text       string
+	Properties []PropertyDocs
+}
+
+type PropertyDocs struct {
+	Name       string
+	OtherNames []string
+	Type       string
+	Tag        reflect.StructTag
+	Text       string
+	OtherTexts []string
+	Properties []PropertyDocs
+	Default    string
+}
+
+func (docs *PropertyStructDocs) Clone() *PropertyStructDocs {
+	ret := *docs
+	ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
+	for i, prop := range ret.Properties {
+		ret.Properties[i] = prop.Clone()
+	}
+
+	return &ret
+}
+
+func (docs *PropertyDocs) Clone() PropertyDocs {
+	ret := *docs
+	ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
+	for i, prop := range ret.Properties {
+		ret.Properties[i] = prop.Clone()
+	}
+
+	return ret
+}
+
+func (docs *PropertyDocs) Equal(other PropertyDocs) bool {
+	return docs.Name == other.Name && docs.Type == other.Type && docs.Tag == other.Tag &&
+		docs.Text == other.Text && docs.Default == other.Default &&
+		stringArrayEqual(docs.OtherNames, other.OtherNames) &&
+		stringArrayEqual(docs.OtherTexts, other.OtherTexts) &&
+		docs.SameSubProperties(other)
+}
+
+func (docs *PropertyStructDocs) SetDefaults(defaults reflect.Value) {
+	setDefaults(docs.Properties, defaults)
+}
+
+func setDefaults(properties []PropertyDocs, defaults reflect.Value) {
+	for i := range properties {
+		prop := &properties[i]
+		fieldName := proptools.FieldNameForProperty(prop.Name)
+		f := defaults.FieldByName(fieldName)
+		if (f == reflect.Value{}) {
+			panic(fmt.Errorf("property %q does not exist in %q", fieldName, defaults.Type()))
+		}
+
+		if reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) {
+			continue
+		}
+
+		if f.Type().Kind() == reflect.Interface {
+			f = f.Elem()
+		}
+
+		if f.Type().Kind() == reflect.Ptr {
+			f = f.Elem()
+		}
+
+		if f.Type().Kind() == reflect.Struct {
+			setDefaults(prop.Properties, f)
+		} else {
+			prop.Default = fmt.Sprintf("%v", f.Interface())
+		}
+	}
+}
+
+func stringArrayEqual(a, b []string) bool {
+	if len(a) != len(b) {
+		return false
+	}
+
+	for i := range a {
+		if a[i] != b[i] {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (docs *PropertyDocs) SameSubProperties(other PropertyDocs) bool {
+	if len(docs.Properties) != len(other.Properties) {
+		return false
+	}
+
+	for i := range docs.Properties {
+		if !docs.Properties[i].Equal(other.Properties[i]) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (docs *PropertyStructDocs) GetByName(name string) *PropertyDocs {
+	return getByName(name, "", &docs.Properties)
+}
+
+func getByName(name string, prefix string, props *[]PropertyDocs) *PropertyDocs {
+	for i := range *props {
+		if prefix+(*props)[i].Name == name {
+			return &(*props)[i]
+		} else if strings.HasPrefix(name, prefix+(*props)[i].Name+".") {
+			return getByName(name, prefix+(*props)[i].Name+".", &(*props)[i].Properties)
+		}
+	}
+	return nil
+}
+
+func (prop *PropertyDocs) Nest(nested *PropertyStructDocs) {
+	//prop.Name += "(" + nested.Name + ")"
+	//prop.Text += "(" + nested.Text + ")"
+	prop.Properties = append(prop.Properties, nested.Properties...)
+}
+
+func newDocs(t *doc.Type) (*PropertyStructDocs, error) {
+	typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
+	docs := PropertyStructDocs{
+		Name: t.Name,
+		Text: t.Doc,
+	}
+
+	structType, ok := typeSpec.Type.(*ast.StructType)
+	if !ok {
+		return nil, fmt.Errorf("type of %q is not a struct", t.Name)
+	}
+
+	var err error
+	docs.Properties, err = structProperties(structType)
+	if err != nil {
+		return nil, err
+	}
+
+	return &docs, nil
+}
+
+func structProperties(structType *ast.StructType) (props []PropertyDocs, err error) {
+	for _, f := range structType.Fields.List {
+		//fmt.Printf("%T %#v\n", f, f)
+		for _, n := range f.Names {
+			var name, typ, tag, text string
+			var innerProps []PropertyDocs
+			if n != nil {
+				name = proptools.PropertyNameForField(n.Name)
+			}
+			if f.Doc != nil {
+				text = f.Doc.Text()
+			}
+			if f.Tag != nil {
+				tag, err = strconv.Unquote(f.Tag.Value)
+				if err != nil {
+					return nil, err
+				}
+			}
+			switch a := f.Type.(type) {
+			case *ast.ArrayType:
+				typ = "list of strings"
+			case *ast.InterfaceType:
+				typ = "interface"
+			case *ast.Ident:
+				typ = a.Name
+			case *ast.StructType:
+				innerProps, err = structProperties(a)
+				if err != nil {
+					return nil, err
+				}
+			default:
+				typ = fmt.Sprintf("%T", f.Type)
+			}
+
+			props = append(props, PropertyDocs{
+				Name:       name,
+				Type:       typ,
+				Tag:        reflect.StructTag(tag),
+				Text:       text,
+				Properties: innerProps,
+			})
+		}
+	}
+
+	return props, nil
+}
+
+func (docs *PropertyStructDocs) ExcludeByTag(key, value string) {
+	filterPropsByTag(&docs.Properties, key, value, true)
+}
+
+func (docs *PropertyStructDocs) IncludeByTag(key, value string) {
+	filterPropsByTag(&docs.Properties, key, value, false)
+}
+
+func filterPropsByTag(props *[]PropertyDocs, key, value string, exclude bool) {
+	// Create a slice that shares the storage of props but has 0 length.  Appending up to
+	// len(props) times to this slice will overwrite the original slice contents
+	filtered := (*props)[:0]
+	for _, x := range *props {
+		tag := x.Tag.Get(key)
+		for _, entry := range strings.Split(tag, ",") {
+			if (entry == value) == !exclude {
+				filtered = append(filtered, x)
+			}
+		}
+	}
+
+	*props = filtered
+}
+
+// Package AST generation and storage
+func (dc *DocCollector) packageDocs(pkg string) (*doc.Package, error) {
+	pkgDocs := dc.getPackageDocs(pkg)
+	if pkgDocs == nil {
+		if files, ok := dc.pkgFiles[pkg]; ok {
+			var err error
+			pkgAST, err := NewPackageAST(files)
+			if err != nil {
+				return nil, err
+			}
+			pkgDocs = doc.New(pkgAST, pkg, doc.AllDecls)
+			pkgDocs = dc.putPackageDocs(pkg, pkgDocs)
+		} else {
+			return nil, fmt.Errorf("unknown package %q", pkg)
+		}
+	}
+	return pkgDocs, nil
+}
+
+func (dc *DocCollector) getPackageDocs(pkg string) *doc.Package {
+	dc.mutex.Lock()
+	defer dc.mutex.Unlock()
+
+	return dc.pkgDocs[pkg]
+}
+
+func (dc *DocCollector) putPackageDocs(pkg string, pkgDocs *doc.Package) *doc.Package {
+	dc.mutex.Lock()
+	defer dc.mutex.Unlock()
+
+	if dc.pkgDocs[pkg] != nil {
+		return dc.pkgDocs[pkg]
+	} else {
+		dc.pkgDocs[pkg] = pkgDocs
+		return pkgDocs
+	}
+}
+
+func NewPackageAST(files []string) (*ast.Package, error) {
+	asts := make(map[string]*ast.File)
+
+	fset := token.NewFileSet()
+	for _, file := range files {
+		ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
+		if err != nil {
+			return nil, err
+		}
+		asts[file] = ast
+	}
+
+	pkg, _ := ast.NewPackage(fset, asts, nil, nil)
+	return pkg, nil
+}
+
+func Write(filename string, pkgFiles map[string][]string,
+	moduleTypePropertyStructs map[string][]interface{}) error {
+
+	docSet := NewDocCollector(pkgFiles)
+
+	var moduleTypeList []*moduleTypeDoc
+	for moduleType, propertyStructs := range moduleTypePropertyStructs {
+		mtDoc, err := getModuleTypeDoc(docSet, moduleType, propertyStructs)
+		if err != nil {
+			return err
+		}
+		removeEmptyPropertyStructs(mtDoc)
+		collapseDuplicatePropertyStructs(mtDoc)
+		collapseNestedPropertyStructs(mtDoc)
+		combineDuplicateProperties(mtDoc)
+		moduleTypeList = append(moduleTypeList, mtDoc)
+	}
+
+	sort.Sort(moduleTypeByName(moduleTypeList))
+
+	buf := &bytes.Buffer{}
+
+	unique := 0
+
+	tmpl, err := template.New("file").Funcs(map[string]interface{}{
+		"unique": func() int {
+			unique++
+			return unique
+		}}).Parse(fileTemplate)
+	if err != nil {
+		return err
+	}
+
+	err = tmpl.Execute(buf, moduleTypeList)
+	if err != nil {
+		return err
+	}
+
+	err = ioutil.WriteFile(filename, buf.Bytes(), 0666)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func getModuleTypeDoc(docSet *DocCollector, moduleType string,
+	propertyStructs []interface{}) (*moduleTypeDoc, error) {
+	mtDoc := &moduleTypeDoc{
+		Name: moduleType,
+		//Text: docSet.ModuleTypeDocs(moduleType),
+	}
+
+	for _, s := range propertyStructs {
+		v := reflect.ValueOf(s).Elem()
+		t := v.Type()
+
+		// Ignore property structs with unexported or unnamed types
+		if t.PkgPath() == "" {
+			continue
+		}
+		psDoc, err := docSet.Docs(t.PkgPath(), t.Name(), v)
+		if err != nil {
+			return nil, err
+		}
+		psDoc.ExcludeByTag("blueprint", "mutated")
+
+		for nested, nestedValue := range nestedPropertyStructs(v) {
+			nestedType := nestedValue.Type()
+
+			// Ignore property structs with unexported or unnamed types
+			if nestedType.PkgPath() == "" {
+				continue
+			}
+			nestedDoc, err := docSet.Docs(nestedType.PkgPath(), nestedType.Name(), nestedValue)
+			if err != nil {
+				return nil, err
+			}
+			nestedDoc.ExcludeByTag("blueprint", "mutated")
+			nestPoint := psDoc.GetByName(nested)
+			if nestPoint == nil {
+				return nil, fmt.Errorf("nesting point %q not found", nested)
+			}
+
+			key, value, err := blueprint.HasFilter(nestPoint.Tag)
+			if err != nil {
+				return nil, err
+			}
+			if key != "" {
+				nestedDoc.IncludeByTag(key, value)
+			}
+
+			nestPoint.Nest(nestedDoc)
+		}
+		mtDoc.PropertyStructs = append(mtDoc.PropertyStructs, psDoc)
+	}
+
+	return mtDoc, nil
+}
+
+func nestedPropertyStructs(s reflect.Value) map[string]reflect.Value {
+	ret := make(map[string]reflect.Value)
+	var walk func(structValue reflect.Value, prefix string)
+	walk = func(structValue reflect.Value, prefix string) {
+		typ := structValue.Type()
+		for i := 0; i < structValue.NumField(); i++ {
+			field := typ.Field(i)
+			if field.PkgPath != "" {
+				// The field is not exported so just skip it.
+				continue
+			}
+
+			fieldValue := structValue.Field(i)
+
+			switch fieldValue.Kind() {
+			case reflect.Bool, reflect.String, reflect.Slice, reflect.Int, reflect.Uint:
+				// Nothing
+			case reflect.Struct:
+				walk(fieldValue, prefix+proptools.PropertyNameForField(field.Name)+".")
+			case reflect.Ptr, reflect.Interface:
+				if !fieldValue.IsNil() {
+					// We leave the pointer intact and zero out the struct that's
+					// pointed to.
+					elem := fieldValue.Elem()
+					if fieldValue.Kind() == reflect.Interface {
+						if elem.Kind() != reflect.Ptr {
+							panic(fmt.Errorf("can't get type of field %q: interface "+
+								"refers to a non-pointer", field.Name))
+						}
+						elem = elem.Elem()
+					}
+					if elem.Kind() != reflect.Struct {
+						panic(fmt.Errorf("can't get type of field %q: points to a "+
+							"non-struct", field.Name))
+					}
+					nestPoint := prefix + proptools.PropertyNameForField(field.Name)
+					ret[nestPoint] = elem
+					walk(elem, nestPoint+".")
+				}
+			default:
+				panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
+					field.Name, fieldValue.Kind()))
+			}
+		}
+
+	}
+
+	walk(s, "")
+	return ret
+}
+
+// Remove any property structs that have no exported fields
+func removeEmptyPropertyStructs(mtDoc *moduleTypeDoc) {
+	for i := 0; i < len(mtDoc.PropertyStructs); i++ {
+		if len(mtDoc.PropertyStructs[i].Properties) == 0 {
+			mtDoc.PropertyStructs = append(mtDoc.PropertyStructs[:i], mtDoc.PropertyStructs[i+1:]...)
+			i--
+		}
+	}
+}
+
+// Squashes duplicates of the same property struct into single entries
+func collapseDuplicatePropertyStructs(mtDoc *moduleTypeDoc) {
+	var collapsedDocs []*PropertyStructDocs
+
+propertyStructLoop:
+	for _, from := range mtDoc.PropertyStructs {
+		for _, to := range collapsedDocs {
+			if from.Name == to.Name {
+				collapseDuplicateProperties(&to.Properties, &from.Properties)
+				continue propertyStructLoop
+			}
+		}
+		collapsedDocs = append(collapsedDocs, from)
+	}
+	mtDoc.PropertyStructs = collapsedDocs
+}
+
+func collapseDuplicateProperties(to, from *[]PropertyDocs) {
+propertyLoop:
+	for _, f := range *from {
+		for i := range *to {
+			t := &(*to)[i]
+			if f.Name == t.Name {
+				collapseDuplicateProperties(&t.Properties, &f.Properties)
+				continue propertyLoop
+			}
+		}
+		*to = append(*to, f)
+	}
+}
+
+// Find all property structs that only contain structs, and move their children up one with
+// a prefixed name
+func collapseNestedPropertyStructs(mtDoc *moduleTypeDoc) {
+	for _, ps := range mtDoc.PropertyStructs {
+		collapseNestedProperties(&ps.Properties)
+	}
+}
+
+func collapseNestedProperties(p *[]PropertyDocs) {
+	var n []PropertyDocs
+
+	for _, parent := range *p {
+		var containsProperty bool
+		for j := range parent.Properties {
+			child := &parent.Properties[j]
+			if len(child.Properties) > 0 {
+				collapseNestedProperties(&child.Properties)
+			} else {
+				containsProperty = true
+			}
+		}
+		if containsProperty || len(parent.Properties) == 0 {
+			n = append(n, parent)
+		} else {
+			for j := range parent.Properties {
+				child := parent.Properties[j]
+				child.Name = parent.Name + "." + child.Name
+				n = append(n, child)
+			}
+		}
+	}
+	*p = n
+}
+
+func combineDuplicateProperties(mtDoc *moduleTypeDoc) {
+	for _, ps := range mtDoc.PropertyStructs {
+		combineDuplicateSubProperties(&ps.Properties)
+	}
+}
+
+func combineDuplicateSubProperties(p *[]PropertyDocs) {
+	var n []PropertyDocs
+propertyLoop:
+	for _, child := range *p {
+		if len(child.Properties) > 0 {
+			combineDuplicateSubProperties(&child.Properties)
+			for i := range n {
+				s := &n[i]
+				if s.SameSubProperties(child) {
+					s.OtherNames = append(s.OtherNames, child.Name)
+					s.OtherTexts = append(s.OtherTexts, child.Text)
+					continue propertyLoop
+				}
+			}
+		}
+		n = append(n, child)
+	}
+
+	*p = n
+}
+
+type moduleTypeByName []*moduleTypeDoc
+
+func (l moduleTypeByName) Len() int           { return len(l) }
+func (l moduleTypeByName) Less(i, j int) bool { return l[i].Name < l[j].Name }
+func (l moduleTypeByName) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
+
+type moduleTypeDoc struct {
+	Name            string
+	Text            string
+	PropertyStructs []*PropertyStructDocs
+}
+
+var (
+	fileTemplate = `
+<html>
+<head>
+<title>Build Docs</title>
+<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
+<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
+</head>
+<body>
+<h1>Build Docs</h1>
+<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
+  {{range .}}
+    {{ $collapseIndex := unique }}
+    <div class="panel panel-default">
+      <div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
+        <h2 class="panel-title">
+          <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
+             {{.Name}}
+          </a>
+        </h2>
+      </div>
+    </div>
+    <div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
+      <div class="panel-body">
+        <p>{{.Text}}</p>
+        {{range .PropertyStructs}}
+          <p>{{.Text}}</p>
+          {{template "properties" .Properties}}
+        {{end}}
+      </div>
+    </div>
+  {{end}}
+</div>
+</body>
+</html>
+
+{{define "properties"}}
+  <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
+    {{range .}}
+      {{$collapseIndex := unique}}
+      {{if .Properties}}
+        <div class="panel panel-default">
+          <div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
+            <h4 class="panel-title">
+              <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
+                 {{.Name}}{{range .OtherNames}}, {{.}}{{end}}
+              </a>
+            </h4>
+          </div>
+        </div>
+        <div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
+          <div class="panel-body">
+            <p>{{.Text}}</p>
+            {{range .OtherTexts}}<p>{{.}}</p>{{end}}
+            {{template "properties" .Properties}}
+          </div>
+        </div>
+      {{else}}
+        <div>
+          <h4>{{.Name}}{{range .OtherNames}}, {{.}}{{end}}</h4>
+          <p>{{.Text}}</p>
+          {{range .OtherTexts}}<p>{{.}}</p>{{end}}
+          <p><i>Type: {{.Type}}</i></p>
+          {{if .Default}}<p><i>Default: {{.Default}}</i></p>{{end}}
+        </div>
+      {{end}}
+    {{end}}
+  </div>
+{{end}}
+`
+)
diff --git a/bootstrap/command.go b/bootstrap/command.go
index 207d56e..932cfd7 100644
--- a/bootstrap/command.go
+++ b/bootstrap/command.go
@@ -33,6 +33,7 @@
 	depFile      string
 	checkFile    string
 	manifestFile string
+	docFile      string
 	cpuprofile   string
 	runGoTests   bool
 )
@@ -42,6 +43,7 @@
 	flag.StringVar(&depFile, "d", "", "the dependency file to output")
 	flag.StringVar(&checkFile, "c", "", "the existing file to check against")
 	flag.StringVar(&manifestFile, "m", "", "the bootstrap manifest file")
+	flag.StringVar(&docFile, "docs", "", "build documentation file to output")
 	flag.StringVar(&cpuprofile, "cpuprofile", "", "write cpu profile to file")
 	flag.BoolVar(&runGoTests, "t", false, "build and run go tests during bootstrap")
 }
@@ -90,6 +92,19 @@
 	// Add extra ninja file dependencies
 	deps = append(deps, extraNinjaFileDeps...)
 
+	errs = ctx.ResolveDependencies(config)
+	if len(errs) > 0 {
+		fatalErrors(errs)
+	}
+
+	if docFile != "" {
+		err := writeDocs(ctx, filepath.Dir(bootstrapConfig.topLevelBlueprintsFile), docFile)
+		if err != nil {
+			fatalErrors([]error{err})
+		}
+		return
+	}
+
 	extraDeps, errs := ctx.PrepareBuildActions(config)
 	if len(errs) > 0 {
 		fatalErrors(errs)
diff --git a/bootstrap/writedocs.go b/bootstrap/writedocs.go
new file mode 100644
index 0000000..868fd65
--- /dev/null
+++ b/bootstrap/writedocs.go
@@ -0,0 +1,59 @@
+package bootstrap
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/bootstrap/bpdoc"
+	"github.com/google/blueprint/pathtools"
+)
+
+func writeDocs(ctx *blueprint.Context, srcDir, filename string) error {
+	// 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
+	var minibp *goBinary
+	ctx.VisitAllModulesIf(isBootstrapBinaryModule,
+		func(module blueprint.Module) {
+			binaryModule := module.(*goBinary)
+			if binaryModule.properties.PrimaryBuilder {
+				primaryBuilders = append(primaryBuilders, binaryModule)
+			}
+			if ctx.ModuleName(binaryModule) == "minibp" {
+				minibp = binaryModule
+			}
+		})
+
+	if minibp == nil {
+		panic("missing minibp")
+	}
+
+	var primaryBuilder *goBinary
+	switch len(primaryBuilders) {
+	case 0:
+		// If there's no primary builder module then that means we'll use minibp
+		// as the primary builder.
+		primaryBuilder = minibp
+
+	case 1:
+		primaryBuilder = primaryBuilders[0]
+
+	default:
+		return fmt.Errorf("multiple primary builder modules present")
+	}
+
+	pkgFiles := make(map[string][]string)
+	ctx.VisitDepsDepthFirst(primaryBuilder, func(module blueprint.Module) {
+		switch m := module.(type) {
+		case (*goPackage):
+			pkgFiles[m.properties.PkgPath] = pathtools.PrefixPaths(m.properties.Srcs,
+				filepath.Join(srcDir, ctx.ModuleDir(m)))
+		default:
+			panic(fmt.Errorf("unknown dependency type %T", module))
+		}
+	})
+
+	return bpdoc.Write(filename, pkgFiles, ctx.ModuleTypePropertyStructs())
+}
diff --git a/build.ninja.in b/build.ninja.in
index 15e0886..3f43e21 100644
--- a/build.ninja.in
+++ b/build.ninja.in
@@ -84,18 +84,40 @@
         ${g.bootstrap.srcDir}/bootstrap/cleanup.go $
         ${g.bootstrap.srcDir}/bootstrap/command.go $
         ${g.bootstrap.srcDir}/bootstrap/config.go $
-        ${g.bootstrap.srcDir}/bootstrap/doc.go | ${g.bootstrap.gcCmd} $
+        ${g.bootstrap.srcDir}/bootstrap/doc.go $
+        ${g.bootstrap.srcDir}/bootstrap/writedocs.go | ${g.bootstrap.gcCmd} $
         .bootstrap/blueprint-parser/pkg/github.com/google/blueprint/parser.a $
         .bootstrap/blueprint-pathtools/pkg/github.com/google/blueprint/pathtools.a $
         .bootstrap/blueprint-proptools/pkg/github.com/google/blueprint/proptools.a $
         .bootstrap/blueprint/pkg/github.com/google/blueprint.a $
-        .bootstrap/blueprint-deptools/pkg/github.com/google/blueprint/deptools.a
-    incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-pathtools/pkg -I .bootstrap/blueprint-proptools/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-deptools/pkg
+        .bootstrap/blueprint-deptools/pkg/github.com/google/blueprint/deptools.a $
+        .bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a
+    incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-pathtools/pkg -I .bootstrap/blueprint-proptools/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-deptools/pkg -I .bootstrap/blueprint-bootstrap-bpdoc/pkg
     pkgPath = github.com/google/blueprint/bootstrap
 default $
         .bootstrap/blueprint-bootstrap/pkg/github.com/google/blueprint/bootstrap.a
 
 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# Module:  blueprint-bootstrap-bpdoc
+# Variant:
+# Type:    bootstrap_go_package
+# Factory: github.com/google/blueprint/bootstrap.func·002
+# Defined: Blueprints:89:1
+
+build $
+        .bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a $
+        : g.bootstrap.gc ${g.bootstrap.srcDir}/bootstrap/bpdoc/bpdoc.go | $
+        ${g.bootstrap.gcCmd} $
+        .bootstrap/blueprint-parser/pkg/github.com/google/blueprint/parser.a $
+        .bootstrap/blueprint-pathtools/pkg/github.com/google/blueprint/pathtools.a $
+        .bootstrap/blueprint-proptools/pkg/github.com/google/blueprint/proptools.a $
+        .bootstrap/blueprint/pkg/github.com/google/blueprint.a
+    incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-pathtools/pkg -I .bootstrap/blueprint-proptools/pkg -I .bootstrap/blueprint/pkg
+    pkgPath = github.com/google/blueprint/bootstrap/bpdoc
+default $
+        .bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a
+
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
 # Module:  blueprint-deptools
 # Variant:
 # Type:    bootstrap_go_package
@@ -159,7 +181,7 @@
 # Variant:
 # Type:    bootstrap_go_binary
 # Factory: github.com/google/blueprint/bootstrap.func·003
-# Defined: Blueprints:96:1
+# Defined: Blueprints:110:1
 
 build .bootstrap/bpfmt/obj/bpfmt.a: g.bootstrap.gc $
         ${g.bootstrap.srcDir}/bpfmt/bpfmt.go | ${g.bootstrap.gcCmd} $
@@ -181,7 +203,7 @@
 # Variant:
 # Type:    bootstrap_go_binary
 # Factory: github.com/google/blueprint/bootstrap.func·003
-# Defined: Blueprints:102:1
+# Defined: Blueprints:116:1
 
 build .bootstrap/bpmodify/obj/bpmodify.a: g.bootstrap.gc $
         ${g.bootstrap.srcDir}/bpmodify/bpmodify.go | ${g.bootstrap.gcCmd} $
@@ -203,7 +225,7 @@
 # Variant:
 # Type:    bootstrap_go_binary
 # Factory: github.com/google/blueprint/bootstrap.func·003
-# Defined: Blueprints:108:1
+# Defined: Blueprints:122:1
 
 build .bootstrap/gotestmain/obj/gotestmain.a: g.bootstrap.gc $
         ${g.bootstrap.srcDir}/gotestmain/gotestmain.go | ${g.bootstrap.gcCmd}
@@ -222,7 +244,7 @@
 # Variant:
 # Type:    bootstrap_go_binary
 # Factory: github.com/google/blueprint/bootstrap.func·003
-# Defined: Blueprints:87:1
+# Defined: Blueprints:101:1
 
 build .bootstrap/minibp/obj/minibp.a: g.bootstrap.gc $
         ${g.bootstrap.srcDir}/bootstrap/minibp/main.go | ${g.bootstrap.gcCmd} $
@@ -231,14 +253,15 @@
         .bootstrap/blueprint-proptools/pkg/github.com/google/blueprint/proptools.a $
         .bootstrap/blueprint/pkg/github.com/google/blueprint.a $
         .bootstrap/blueprint-deptools/pkg/github.com/google/blueprint/deptools.a $
+        .bootstrap/blueprint-bootstrap-bpdoc/pkg/github.com/google/blueprint/bootstrap/bpdoc.a $
         .bootstrap/blueprint-bootstrap/pkg/github.com/google/blueprint/bootstrap.a
-    incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-pathtools/pkg -I .bootstrap/blueprint-proptools/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-deptools/pkg -I .bootstrap/blueprint-bootstrap/pkg
+    incFlags = -I .bootstrap/blueprint-parser/pkg -I .bootstrap/blueprint-pathtools/pkg -I .bootstrap/blueprint-proptools/pkg -I .bootstrap/blueprint/pkg -I .bootstrap/blueprint-deptools/pkg -I .bootstrap/blueprint-bootstrap-bpdoc/pkg -I .bootstrap/blueprint-bootstrap/pkg
     pkgPath = minibp
 default .bootstrap/minibp/obj/minibp.a
 
 build .bootstrap/minibp/obj/a.out: g.bootstrap.link $
         .bootstrap/minibp/obj/minibp.a | ${g.bootstrap.linkCmd}
-    libDirFlags = -L .bootstrap/blueprint-parser/pkg -L .bootstrap/blueprint-pathtools/pkg -L .bootstrap/blueprint-proptools/pkg -L .bootstrap/blueprint/pkg -L .bootstrap/blueprint-deptools/pkg -L .bootstrap/blueprint-bootstrap/pkg
+    libDirFlags = -L .bootstrap/blueprint-parser/pkg -L .bootstrap/blueprint-pathtools/pkg -L .bootstrap/blueprint-proptools/pkg -L .bootstrap/blueprint/pkg -L .bootstrap/blueprint-deptools/pkg -L .bootstrap/blueprint-bootstrap-bpdoc/pkg -L .bootstrap/blueprint-bootstrap/pkg
 default .bootstrap/minibp/obj/a.out
 
 build .bootstrap/bin/minibp: g.bootstrap.cp .bootstrap/minibp/obj/a.out
@@ -248,6 +271,10 @@
 # Singleton: bootstrap
 # Factory:   github.com/google/blueprint/bootstrap.func·008
 
+rule s.bootstrap.bigbpDocs
+    command = .bootstrap/bin/minibp -p --docs ${out} ${g.bootstrap.srcDir}/Blueprints
+    description = minibp docs ${out}
+
 rule s.bootstrap.bigbp
     command = .bootstrap/bin/minibp -p -d .bootstrap/main.ninja.in.d -m ${g.bootstrap.bootstrapManifest} -o ${out} ${in}
     depfile = .bootstrap/main.ninja.in.d
@@ -259,10 +286,13 @@
     description = minibp ${out}
     generator = true
 
+build .bootstrap/docs/minibp.html: s.bootstrap.bigbpDocs | $
+        .bootstrap/bin/minibp
+default .bootstrap/docs/minibp.html
 build .bootstrap/main.ninja.in: s.bootstrap.bigbp $
         ${g.bootstrap.srcDir}/Blueprints | .bootstrap/bin/bpfmt $
         .bootstrap/bin/bpmodify .bootstrap/bin/gotestmain $
-        .bootstrap/bin/minibp
+        .bootstrap/bin/minibp .bootstrap/docs/minibp.html
 default .bootstrap/main.ninja.in
 build .bootstrap/notAFile: phony
 default .bootstrap/notAFile
diff --git a/context.go b/context.go
index 2599a2c..99e6421 100644
--- a/context.go
+++ b/context.go
@@ -1000,7 +1000,12 @@
 // objects via the Config method on the DynamicDependerModuleContext objects
 // passed to their DynamicDependencies method.
 func (c *Context) ResolveDependencies(config interface{}) []error {
-	errs := c.resolveDependencies(config)
+	errs := c.runEarlyMutators(config)
+	if len(errs) > 0 {
+		return errs
+	}
+
+	errs = c.resolveDependencies(config)
 	if len(errs) > 0 {
 		return errs
 	}
@@ -1361,11 +1366,6 @@
 func (c *Context) PrepareBuildActions(config interface{}) (deps []string, errs []error) {
 	c.buildActionsReady = false
 
-	errs = c.runEarlyMutators(config)
-	if len(errs) > 0 {
-		return nil, errs
-	}
-
 	if !c.dependenciesReady {
 		errs := c.ResolveDependencies(config)
 		if len(errs) > 0 {
@@ -2006,6 +2006,64 @@
 	return targets, nil
 }
 
+// ModuleTypePropertyStructs returns a mapping from module type name to a list of pointers to
+// property structs returned by the factory for that module type.
+func (c *Context) ModuleTypePropertyStructs() map[string][]interface{} {
+	ret := make(map[string][]interface{})
+	for moduleType, factory := range c.moduleFactories {
+		_, ret[moduleType] = factory()
+	}
+
+	return ret
+}
+
+func (c *Context) ModuleName(logicModule Module) string {
+	module := c.moduleInfo[logicModule]
+	return module.properties.Name
+}
+
+func (c *Context) ModuleDir(logicModule Module) string {
+	module := c.moduleInfo[logicModule]
+	return filepath.Dir(module.relBlueprintsFile)
+}
+
+func (c *Context) BlueprintFile(logicModule Module) string {
+	module := c.moduleInfo[logicModule]
+	return module.relBlueprintsFile
+}
+
+func (c *Context) ModuleErrorf(logicModule Module, format string,
+	args ...interface{}) error {
+
+	module := c.moduleInfo[logicModule]
+	return &Error{
+		Err: fmt.Errorf(format, args...),
+		Pos: module.pos,
+	}
+}
+
+func (c *Context) VisitAllModules(visit func(Module)) {
+	c.visitAllModules(visit)
+}
+
+func (c *Context) VisitAllModulesIf(pred func(Module) bool,
+	visit func(Module)) {
+
+	c.visitAllModulesIf(pred, visit)
+}
+
+func (c *Context) VisitDepsDepthFirst(module Module,
+	visit func(Module)) {
+
+	c.visitDepsDepthFirst(c.moduleInfo[module], visit)
+}
+
+func (c *Context) VisitDepsDepthFirstIf(module Module,
+	pred func(Module) bool, visit func(Module)) {
+
+	c.visitDepsDepthFirstIf(c.moduleInfo[module], pred, visit)
+}
+
 // WriteBuildFile writes the Ninja manifeset text for the generated build
 // actions to w.  If this is called before PrepareBuildActions successfully
 // completes then ErrBuildActionsNotReady is returned.
diff --git a/proptools/proptools.go b/proptools/proptools.go
index 2a71ea1..ebfe42a 100644
--- a/proptools/proptools.go
+++ b/proptools/proptools.go
@@ -30,6 +30,15 @@
 	return propertyName
 }
 
+func FieldNameForProperty(propertyName string) string {
+	r, size := utf8.DecodeRuneInString(propertyName)
+	fieldName := string(unicode.ToUpper(r))
+	if len(propertyName) > size {
+		fieldName += propertyName[size:]
+	}
+	return fieldName
+}
+
 func CloneProperties(structValue reflect.Value) reflect.Value {
 	result := reflect.New(structValue.Type())
 	CopyProperties(result.Elem(), structValue)
diff --git a/singleton_ctx.go b/singleton_ctx.go
index c9cfc8c..e982086 100644
--- a/singleton_ctx.go
+++ b/singleton_ctx.go
@@ -16,7 +16,6 @@
 
 import (
 	"fmt"
-	"path/filepath"
 )
 
 type Singleton interface {
@@ -71,28 +70,21 @@
 }
 
 func (s *singletonContext) ModuleName(logicModule Module) string {
-	module := s.context.moduleInfo[logicModule]
-	return module.properties.Name
+	return s.context.ModuleName(logicModule)
 }
 
 func (s *singletonContext) ModuleDir(logicModule Module) string {
-	module := s.context.moduleInfo[logicModule]
-	return filepath.Dir(module.relBlueprintsFile)
+	return s.context.ModuleDir(logicModule)
 }
 
 func (s *singletonContext) BlueprintFile(logicModule Module) string {
-	module := s.context.moduleInfo[logicModule]
-	return module.relBlueprintsFile
+	return s.context.BlueprintFile(logicModule)
 }
 
 func (s *singletonContext) ModuleErrorf(logicModule Module, format string,
 	args ...interface{}) {
 
-	module := s.context.moduleInfo[logicModule]
-	s.errs = append(s.errs, &Error{
-		Err: fmt.Errorf(format, args...),
-		Pos: module.pos,
-	})
+	s.errs = append(s.errs, s.context.ModuleErrorf(logicModule, format, args...))
 }
 
 func (s *singletonContext) Errorf(format string, args ...interface{}) {
@@ -153,25 +145,25 @@
 }
 
 func (s *singletonContext) VisitAllModules(visit func(Module)) {
-	s.context.visitAllModules(visit)
+	s.context.VisitAllModules(visit)
 }
 
 func (s *singletonContext) VisitAllModulesIf(pred func(Module) bool,
 	visit func(Module)) {
 
-	s.context.visitAllModulesIf(pred, visit)
+	s.context.VisitAllModulesIf(pred, visit)
 }
 
 func (s *singletonContext) VisitDepsDepthFirst(module Module,
 	visit func(Module)) {
 
-	s.context.visitDepsDepthFirst(s.context.moduleInfo[module], visit)
+	s.context.VisitDepsDepthFirst(module, visit)
 }
 
 func (s *singletonContext) VisitDepsDepthFirstIf(module Module,
 	pred func(Module) bool, visit func(Module)) {
 
-	s.context.visitDepsDepthFirstIf(s.context.moduleInfo[module], pred, visit)
+	s.context.VisitDepsDepthFirstIf(module, pred, visit)
 }
 
 func (s *singletonContext) AddNinjaFileDeps(deps ...string) {
diff --git a/unpack.go b/unpack.go
index 3e9fe00..83fcd32 100644
--- a/unpack.go
+++ b/unpack.go
@@ -230,7 +230,7 @@
 			fallthrough
 		case reflect.Struct:
 			localFilterKey, localFilterValue := filterKey, filterValue
-			if k, v, err := hasFilter(field); err != nil {
+			if k, v, err := HasFilter(field.Tag); err != nil {
 				errs = append(errs, err)
 				if len(errs) >= maxErrors {
 					return errs
@@ -337,8 +337,8 @@
 	return false
 }
 
-func hasFilter(field reflect.StructField) (k, v string, err error) {
-	tag := field.Tag.Get("blueprint")
+func HasFilter(field reflect.StructTag) (k, v string, err error) {
+	tag := field.Get("blueprint")
 	for _, entry := range strings.Split(tag, ",") {
 		if strings.HasPrefix(entry, "filter") {
 			if !strings.HasPrefix(entry, "filter(") || !strings.HasSuffix(entry, ")") {