// Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package blueprint import ( "errors" "fmt" "reflect" "regexp" "runtime" "strings" "sync" ) // A PackageContext provides a way to create package-scoped Ninja pools, // rules, and variables. A Go package should create a single unexported // package-scoped PackageContext variable that it uses to create all package- // scoped Ninja object definitions. This PackageContext object should then be // passed to all calls to define module- or singleton-specific Ninja // definitions. For example: // // package blah // // import ( // "blueprint" // ) // // var ( // pctx = NewPackageContext("path/to/blah") // // myPrivateVar = pctx.StaticVariable("myPrivateVar", "abcdef") // MyExportedVar = pctx.StaticVariable("MyExportedVar", "$myPrivateVar 123456!") // // SomeRule = pctx.StaticRule(...) // ) // // // ... // // func (m *MyModule) GenerateBuildActions(ctx blueprint.Module) { // ctx.Build(pctx, blueprint.BuildParams{ // Rule: SomeRule, // Outputs: []string{"$myPrivateVar"}, // }) // } type PackageContext interface { Import(pkgPath string) ImportAs(as, pkgPath string) StaticVariable(name, value string) Variable VariableFunc(name string, f func(ctx VariableFuncContext, config interface{}) (string, error)) Variable VariableConfigMethod(name string, method interface{}) Variable StaticPool(name string, params PoolParams) Pool PoolFunc(name string, f func(interface{}) (PoolParams, error)) Pool StaticRule(name string, params RuleParams, argNames ...string) Rule RuleFunc(name string, f func(interface{}) (RuleParams, error), argNames ...string) Rule AddNinjaFileDeps(deps ...string) getScope() *basicScope } type packageContext struct { fullName string shortName string pkgPath string scope *basicScope ninjaFileDeps []string } var _ PackageContext = (*packageContext)(nil) func (p *packageContext) getScope() *basicScope { return p.scope } var packageContexts = map[string]*packageContext{} // NewPackageContext creates a PackageContext object for a given package. The // pkgPath argument should always be set to the full path used to import the // package. This function may only be called from a Go package's init() // function or as part of a package-scoped variable initialization. func NewPackageContext(pkgPath string) PackageContext { checkCalledFromInit() if _, present := packageContexts[pkgPath]; present { panic(fmt.Errorf("package %q already has a package context", pkgPath)) } pkgName := pkgPathToName(pkgPath) err := validateNinjaName(pkgName) if err != nil { panic(err) } i := strings.LastIndex(pkgPath, "/") shortName := pkgPath[i+1:] p := &packageContext{ fullName: pkgName, shortName: shortName, pkgPath: pkgPath, scope: newScope(nil), } packageContexts[pkgPath] = p return p } var Phony Rule = NewBuiltinRule("phony") var Console Pool = NewBuiltinPool("console") var errRuleIsBuiltin = errors.New("the rule is a built-in") var errPoolIsBuiltin = errors.New("the pool is a built-in") var errVariableIsArg = errors.New("argument variables have no value") // checkCalledFromInit panics if a Go package's init function is not on the // call stack. func checkCalledFromInit() { for skip := 3; ; skip++ { _, funcName, ok := callerName(skip) if !ok { panic("not called from an init func") } if funcName == "init" || strings.HasPrefix(funcName, "init·") || funcName == "init.ializers" || strings.HasPrefix(funcName, "init.") { return } } } // A regex to find a package path within a function name. It finds the shortest string that is // followed by '.' and doesn't have any '/'s left. var pkgPathRe = regexp.MustCompile(`^(.*?)\.([^/]+)$`) // callerName returns the package path and function name of the calling // function. The skip argument has the same meaning as the skip argument of // runtime.Callers. func callerName(skip int) (pkgPath, funcName string, ok bool) { var pc [1]uintptr n := runtime.Callers(skip+1, pc[:]) if n != 1 { return "", "", false } frames := runtime.CallersFrames(pc[:]) frame, _ := frames.Next() f := frame.Function s := pkgPathRe.FindStringSubmatch(f) if len(s) < 3 { panic(fmt.Errorf("failed to extract package path and function name from %q", f)) } return s[1], s[2], true } // pkgPathToName makes a Ninja-friendly name out of a Go package name by // replaceing all the '/' characters with '.'. We assume the results are // unique, though this is not 100% guaranteed for Go package names that // already contain '.' characters. Disallowing package names with '.' isn't // reasonable since many package names contain the name of the hosting site // (e.g. "code.google.com"). In practice this probably isn't really a // problem. func pkgPathToName(pkgPath string) string { return strings.Replace(pkgPath, "/", ".", -1) } // Import enables access to the exported Ninja pools, rules, and variables // that are defined at the package scope of another Go package. Go's // visibility rules apply to these references - capitalized names indicate // that something is exported. It may only be called from a Go package's // init() function. The Go package path passed to Import must have already // been imported into the Go package using a Go import statement. The // imported variables may then be accessed from Ninja strings as // "${pkg.Variable}", while the imported rules can simply be accessed as // exported Go variables from the package. For example: // // import ( // "blueprint" // "foo/bar" // ) // // var pctx = NewPackagePath("blah") // // func init() { // pctx.Import("foo/bar") // } // // ... // // func (m *MyModule) GenerateBuildActions(ctx blueprint.Module) { // ctx.Build(pctx, blueprint.BuildParams{ // Rule: bar.SomeRule, // Outputs: []string{"${bar.SomeVariable}"}, // }) // } // // Note that the local name used to refer to the package in Ninja variable names // is derived from pkgPath by extracting the last path component. This differs // from Go's import declaration, which derives the local name from the package // clause in the imported package. By convention these names are made to match, // but this is not required. func (p *packageContext) Import(pkgPath string) { checkCalledFromInit() importPkg, ok := packageContexts[pkgPath] if !ok { panic(fmt.Errorf("package %q has no context", pkgPath)) } err := p.scope.AddImport(importPkg.shortName, importPkg.scope) if err != nil { panic(err) } } // ImportAs provides the same functionality as Import, but it allows the local // name that will be used to refer to the package to be specified explicitly. // It may only be called from a Go package's init() function. func (p *packageContext) ImportAs(as, pkgPath string) { checkCalledFromInit() importPkg, ok := packageContexts[pkgPath] if !ok { panic(fmt.Errorf("package %q has no context", pkgPath)) } err := validateNinjaName(as) if err != nil { panic(err) } err = p.scope.AddImport(as, importPkg.scope) if err != nil { panic(err) } } type staticVariable struct { pctx *packageContext name_ string value_ string } // StaticVariable returns a Variable whose value does not depend on any // configuration information. It may only be called during a Go package's // initialization - either from the init() function or as part of a package- // scoped variable's initialization. // // This function is usually used to initialize a package-scoped Go variable that // represents a Ninja variable that will be output. The name argument should // exactly match the Go variable name, and the value string may reference other // Ninja variables that are visible within the calling Go package. func (p *packageContext) StaticVariable(name, value string) Variable { checkCalledFromInit() err := validateNinjaName(name) if err != nil { panic(err) } v := &staticVariable{ pctx: p, name_: name, value_: value, } err = p.scope.AddVariable(v) if err != nil { panic(err) } return v } func (v *staticVariable) packageContext() *packageContext { return v.pctx } func (v *staticVariable) name() string { return v.name_ } func (v *staticVariable) fullName(pkgNames map[*packageContext]string) string { return packageNamespacePrefix(pkgNames[v.pctx]) + v.name_ } func (v *staticVariable) value(VariableFuncContext, interface{}) (*ninjaString, error) { ninjaStr, err := parseNinjaString(v.pctx.scope, v.value_) if err != nil { err = fmt.Errorf("error parsing variable %s value: %s", v, err) panic(err) } return ninjaStr, nil } func (v *staticVariable) String() string { return v.pctx.pkgPath + "." + v.name_ } type variableFunc struct { pctx *packageContext name_ string value_ func(VariableFuncContext, interface{}) (string, error) } // VariableFuncContext is passed to VariableFunc functions. type VariableFuncContext interface { // GlobWithDeps returns a list of files and directories that match the // specified pattern but do not match any of the patterns in excludes. // Any directories will have a '/' suffix. It also adds efficient // dependencies to rerun the primary builder whenever a file matching // the pattern as added or removed, without rerunning if a file that // does not match the pattern is added to a searched directory. GlobWithDeps(globPattern string, excludes []string) ([]string, error) } type variableFuncContext struct { context *Context } func (v *variableFuncContext) GlobWithDeps(pattern string, excludes []string) ([]string, error) { return v.context.glob(pattern, excludes) } // VariableFunc returns a Variable whose value is determined by a function that // takes a config object as input and returns either the variable value or an // error. It may only be called during a Go package's initialization - either // from the init() function or as part of a package-scoped variable's // initialization. // // This function is usually used to initialize a package-scoped Go variable that // represents a Ninja variable that will be output. The name argument should // exactly match the Go variable name, and the value string returned by f may // reference other Ninja variables that are visible within the calling Go // package. func (p *packageContext) VariableFunc(name string, f func(ctx VariableFuncContext, config interface{}) (string, error)) Variable { checkCalledFromInit() err := validateNinjaName(name) if err != nil { panic(err) } v := &variableFunc{ pctx: p, name_: name, value_: f, } err = p.scope.AddVariable(v) if err != nil { panic(err) } return v } // VariableConfigMethod returns a Variable whose value is determined by calling // a method on the config object. The method must take no arguments and return // a single string that will be the variable's value. It may only be called // during a Go package's initialization - either from the init() function or as // part of a package-scoped variable's initialization. // // This function is usually used to initialize a package-scoped Go variable that // represents a Ninja variable that will be output. The name argument should // exactly match the Go variable name, and the value string returned by method // may reference other Ninja variables that are visible within the calling Go // package. func (p *packageContext) VariableConfigMethod(name string, method interface{}) Variable { checkCalledFromInit() err := validateNinjaName(name) if err != nil { panic(err) } methodValue := reflect.ValueOf(method) validateVariableMethod(name, methodValue) fun := func(ctx VariableFuncContext, config interface{}) (string, error) { result := methodValue.Call([]reflect.Value{reflect.ValueOf(config)}) resultStr := result[0].Interface().(string) return resultStr, nil } v := &variableFunc{ pctx: p, name_: name, value_: fun, } err = p.scope.AddVariable(v) if err != nil { panic(err) } return v } func (v *variableFunc) packageContext() *packageContext { return v.pctx } func (v *variableFunc) name() string { return v.name_ } func (v *variableFunc) fullName(pkgNames map[*packageContext]string) string { return packageNamespacePrefix(pkgNames[v.pctx]) + v.name_ } func (v *variableFunc) value(ctx VariableFuncContext, config interface{}) (*ninjaString, error) { value, err := v.value_(ctx, config) if err != nil { return nil, err } ninjaStr, err := parseNinjaString(v.pctx.scope, value) if err != nil { err = fmt.Errorf("error parsing variable %s value: %s", v, err) panic(err) } return ninjaStr, nil } func (v *variableFunc) String() string { return v.pctx.pkgPath + "." + v.name_ } func validateVariableMethod(name string, methodValue reflect.Value) { methodType := methodValue.Type() if methodType.Kind() != reflect.Func { panic(fmt.Errorf("method given for variable %s is not a function", name)) } if n := methodType.NumIn(); n != 1 { panic(fmt.Errorf("method for variable %s has %d inputs (should be 1)", name, n)) } if n := methodType.NumOut(); n != 1 { panic(fmt.Errorf("method for variable %s has %d outputs (should be 1)", name, n)) } if kind := methodType.Out(0).Kind(); kind != reflect.String { panic(fmt.Errorf("method for variable %s does not return a string", name)) } } // An argVariable is a Variable that exists only when it is set by a build // statement to pass a value to the rule being invoked. It has no value, so it // can never be used to create a Ninja assignment statement. It is inserted // into the rule's scope, which is used for name lookups within the rule and // when assigning argument values as part of a build statement. type argVariable struct { name_ string } func (v *argVariable) packageContext() *packageContext { panic("this should not be called") } func (v *argVariable) name() string { return v.name_ } func (v *argVariable) fullName(pkgNames map[*packageContext]string) string { return v.name_ } func (v *argVariable) value(ctx VariableFuncContext, config interface{}) (*ninjaString, error) { return nil, errVariableIsArg } func (v *argVariable) String() string { return ":" + v.name_ } type staticPool struct { pctx *packageContext name_ string params PoolParams } // StaticPool returns a Pool whose value does not depend on any configuration // information. It may only be called during a Go package's initialization - // either from the init() function or as part of a package-scoped Go variable's // initialization. // // This function is usually used to initialize a package-scoped Go variable that // represents a Ninja pool that will be output. The name argument should // exactly match the Go variable name, and the params fields may reference other // Ninja variables that are visible within the calling Go package. func (p *packageContext) StaticPool(name string, params PoolParams) Pool { checkCalledFromInit() err := validateNinjaName(name) if err != nil { panic(err) } pool := &staticPool{ pctx: p, name_: name, params: params, } err = p.scope.AddPool(pool) if err != nil { panic(err) } return pool } func (p *staticPool) packageContext() *packageContext { return p.pctx } func (p *staticPool) name() string { return p.name_ } func (p *staticPool) fullName(pkgNames map[*packageContext]string) string { return packageNamespacePrefix(pkgNames[p.pctx]) + p.name_ } func (p *staticPool) def(config interface{}) (*poolDef, error) { def, err := parsePoolParams(p.pctx.scope, &p.params) if err != nil { panic(fmt.Errorf("error parsing PoolParams for %s: %s", p, err)) } return def, nil } func (p *staticPool) String() string { return p.pctx.pkgPath + "." + p.name_ } type poolFunc struct { pctx *packageContext name_ string paramsFunc func(interface{}) (PoolParams, error) } // PoolFunc returns a Pool whose value is determined by a function that takes a // config object as input and returns either the pool parameters or an error. It // may only be called during a Go package's initialization - either from the // init() function or as part of a package-scoped variable's initialization. // // This function is usually used to initialize a package-scoped Go variable that // represents a Ninja pool that will be output. The name argument should // exactly match the Go variable name, and the string fields of the PoolParams // returned by f may reference other Ninja variables that are visible within the // calling Go package. func (p *packageContext) PoolFunc(name string, f func(interface{}) (PoolParams, error)) Pool { checkCalledFromInit() err := validateNinjaName(name) if err != nil { panic(err) } pool := &poolFunc{ pctx: p, name_: name, paramsFunc: f, } err = p.scope.AddPool(pool) if err != nil { panic(err) } return pool } func (p *poolFunc) packageContext() *packageContext { return p.pctx } func (p *poolFunc) name() string { return p.name_ } func (p *poolFunc) fullName(pkgNames map[*packageContext]string) string { return packageNamespacePrefix(pkgNames[p.pctx]) + p.name_ } func (p *poolFunc) def(config interface{}) (*poolDef, error) { params, err := p.paramsFunc(config) if err != nil { return nil, err } def, err := parsePoolParams(p.pctx.scope, ¶ms) if err != nil { panic(fmt.Errorf("error parsing PoolParams for %s: %s", p, err)) } return def, nil } func (p *poolFunc) String() string { return p.pctx.pkgPath + "." + p.name_ } type builtinPool struct { name_ string } func (p *builtinPool) packageContext() *packageContext { return nil } func (p *builtinPool) name() string { return p.name_ } func (p *builtinPool) fullName(pkgNames map[*packageContext]string) string { return p.name_ } func (p *builtinPool) def(config interface{}) (*poolDef, error) { return nil, errPoolIsBuiltin } // NewBuiltinPool returns a Pool object that refers to a pool name created outside of Blueprint func NewBuiltinPool(name string) Pool { return &builtinPool{ name_: name, } } func (p *builtinPool) String() string { return ":" + p.name_ } type staticRule struct { pctx *packageContext name_ string params RuleParams argNames map[string]bool scope_ *basicScope sync.Mutex // protects scope_ during lazy creation } // StaticRule returns a Rule whose value does not depend on any configuration // information. It may only be called during a Go package's initialization - // either from the init() function or as part of a package-scoped Go variable's // initialization. // // This function is usually used to initialize a package-scoped Go variable that // represents a Ninja rule that will be output. The name argument should // exactly match the Go variable name, and the params fields may reference other // Ninja variables that are visible within the calling Go package. // // The argNames arguments list Ninja variables that may be overridden by Ninja // build statements that invoke the rule. These arguments may be referenced in // any of the string fields of params. Arguments can shadow package-scoped // variables defined within the caller's Go package, but they may not shadow // those defined in another package. Shadowing a package-scoped variable // results in the package-scoped variable's value being used for build // statements that do not override the argument. For argument names that do not // shadow package-scoped variables the default value is an empty string. func (p *packageContext) StaticRule(name string, params RuleParams, argNames ...string) Rule { checkCalledFromInit() err := validateNinjaName(name) if err != nil { panic(err) } err = validateArgNames(argNames) if err != nil { panic(fmt.Errorf("invalid argument name: %s", err)) } argNamesSet := make(map[string]bool) for _, argName := range argNames { argNamesSet[argName] = true } ruleScope := (*basicScope)(nil) // This will get created lazily r := &staticRule{ pctx: p, name_: name, params: params, argNames: argNamesSet, scope_: ruleScope, } err = p.scope.AddRule(r) if err != nil { panic(err) } return r } func (r *staticRule) packageContext() *packageContext { return r.pctx } func (r *staticRule) name() string { return r.name_ } func (r *staticRule) fullName(pkgNames map[*packageContext]string) string { return packageNamespacePrefix(pkgNames[r.pctx]) + r.name_ } func (r *staticRule) def(interface{}) (*ruleDef, error) { def, err := parseRuleParams(r.scope(), &r.params) if err != nil { panic(fmt.Errorf("error parsing RuleParams for %s: %s", r, err)) } return def, nil } func (r *staticRule) scope() *basicScope { // We lazily create the scope so that all the package-scoped variables get // declared before the args are created. Otherwise we could incorrectly // shadow a package-scoped variable with an arg variable. r.Lock() defer r.Unlock() if r.scope_ == nil { r.scope_ = makeRuleScope(r.pctx.scope, r.argNames) } return r.scope_ } func (r *staticRule) isArg(argName string) bool { return r.argNames[argName] } func (r *staticRule) String() string { return r.pctx.pkgPath + "." + r.name_ } type ruleFunc struct { pctx *packageContext name_ string paramsFunc func(interface{}) (RuleParams, error) argNames map[string]bool scope_ *basicScope sync.Mutex // protects scope_ during lazy creation } // RuleFunc returns a Rule whose value is determined by a function that takes a // config object as input and returns either the rule parameters or an error. It // may only be called during a Go package's initialization - either from the // init() function or as part of a package-scoped variable's initialization. // // This function is usually used to initialize a package-scoped Go variable that // represents a Ninja rule that will be output. The name argument should // exactly match the Go variable name, and the string fields of the RuleParams // returned by f may reference other Ninja variables that are visible within the // calling Go package. // // The argNames arguments list Ninja variables that may be overridden by Ninja // build statements that invoke the rule. These arguments may be referenced in // any of the string fields of the RuleParams returned by f. Arguments can // shadow package-scoped variables defined within the caller's Go package, but // they may not shadow those defined in another package. Shadowing a package- // scoped variable results in the package-scoped variable's value being used for // build statements that do not override the argument. For argument names that // do not shadow package-scoped variables the default value is an empty string. func (p *packageContext) RuleFunc(name string, f func(interface{}) (RuleParams, error), argNames ...string) Rule { checkCalledFromInit() err := validateNinjaName(name) if err != nil { panic(err) } err = validateArgNames(argNames) if err != nil { panic(fmt.Errorf("invalid argument name: %s", err)) } argNamesSet := make(map[string]bool) for _, argName := range argNames { argNamesSet[argName] = true } ruleScope := (*basicScope)(nil) // This will get created lazily rule := &ruleFunc{ pctx: p, name_: name, paramsFunc: f, argNames: argNamesSet, scope_: ruleScope, } err = p.scope.AddRule(rule) if err != nil { panic(err) } return rule } func (r *ruleFunc) packageContext() *packageContext { return r.pctx } func (r *ruleFunc) name() string { return r.name_ } func (r *ruleFunc) fullName(pkgNames map[*packageContext]string) string { return packageNamespacePrefix(pkgNames[r.pctx]) + r.name_ } func (r *ruleFunc) def(config interface{}) (*ruleDef, error) { params, err := r.paramsFunc(config) if err != nil { return nil, err } def, err := parseRuleParams(r.scope(), ¶ms) if err != nil { panic(fmt.Errorf("error parsing RuleParams for %s: %s", r, err)) } return def, nil } func (r *ruleFunc) scope() *basicScope { // We lazily create the scope so that all the global variables get declared // before the args are created. Otherwise we could incorrectly shadow a // global variable with an arg variable. r.Lock() defer r.Unlock() if r.scope_ == nil { r.scope_ = makeRuleScope(r.pctx.scope, r.argNames) } return r.scope_ } func (r *ruleFunc) isArg(argName string) bool { return r.argNames[argName] } func (r *ruleFunc) String() string { return r.pctx.pkgPath + "." + r.name_ } type builtinRule struct { name_ string scope_ *basicScope sync.Mutex // protects scope_ during lazy creation } func (r *builtinRule) packageContext() *packageContext { return nil } func (r *builtinRule) name() string { return r.name_ } func (r *builtinRule) fullName(pkgNames map[*packageContext]string) string { return r.name_ } func (r *builtinRule) def(config interface{}) (*ruleDef, error) { return nil, errRuleIsBuiltin } func (r *builtinRule) scope() *basicScope { r.Lock() defer r.Unlock() if r.scope_ == nil { r.scope_ = makeRuleScope(nil, nil) } return r.scope_ } func (r *builtinRule) isArg(argName string) bool { return false } func (r *builtinRule) String() string { return ":" + r.name_ } // NewBuiltinRule returns a Rule object that refers to a rule that was created outside of Blueprint func NewBuiltinRule(name string) Rule { return &builtinRule{ name_: name, } } func (p *packageContext) AddNinjaFileDeps(deps ...string) { p.ninjaFileDeps = append(p.ninjaFileDeps, deps...) }