1364 lines
30 KiB
Go
1364 lines
30 KiB
Go
|
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}}
|
||
|
`
|