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, ")") {