diff --git a/buildtags/buildtags.go b/buildtags/buildtags.go index fc03634..8fd61e1 100644 --- a/buildtags/buildtags.go +++ b/buildtags/buildtags.go @@ -1,3 +1,24 @@ +// Package buildtags provides types for representing and manipulating build constraints. +// +// In Go, build constraints are represented as comments in source code together with file naming conventions. For example +// +// // +build linux,386 darwin,!cgo +// // +build !purego +// +// Any terms provided in the filename can be thought of as an implicit extra +// constraint comment line. Collectively, these are referred to as +// ``constraints''. Each line is a ``constraint''. Within each constraint the +// space-separated terms are ``options'', and within that the comma-separated +// items are ``terms'' which may be negated with at most one exclaimation mark. +// +// These represent a boolean formulae. The constraints are evaluated as the AND +// of constraint lines; a constraint is evaluated as the OR of its options and +// an option is evaluated as the AND of its terms. Overall build constraints are +// a boolean formula that is an AND of ORs of ANDs. +// +// This level of complexity is rarely used in Go programs. Therefore this +// package aims to provide access to all these layers of nesting if required, +// but make it easy to forget about for basic use cases too. package buildtags import ( @@ -31,6 +52,7 @@ import ( // // (linux OR darwin) AND 386 // +// Interface represents a build constraint. type Interface interface { ConstraintsConvertable fmt.GoStringer @@ -38,27 +60,37 @@ type Interface interface { Validate() error } +// ConstraintsConvertable can be converted to a Constraints object. type ConstraintsConvertable interface { ToConstraints() Constraints } +// ConstraintConvertable can be converted to a Constraint. type ConstraintConvertable interface { ToConstraint() Constraint } +// OptionConvertable can be converted to an Option. type OptionConvertable interface { ToOption() Option } -type ( - Constraints []Constraint - Constraint []Option - Option []Term - Term string -) +// Constraints represents the AND of a list of Constraint lines. +type Constraints []Constraint +// And builds Constraints that will be true if all of its constraints are true. +func And(cs ...ConstraintConvertable) Constraints { + constraints := Constraints{} + for _, c := range cs { + constraints = append(constraints, c.ToConstraint()) + } + return constraints +} + +// ToConstraints returns cs. func (cs Constraints) ToConstraints() Constraints { return cs } +// Validate validates the constraints set. func (cs Constraints) Validate() error { for _, c := range cs { if err := c.Validate(); err != nil { @@ -68,6 +100,8 @@ func (cs Constraints) Validate() error { return nil } +// Evaluate the boolean formula represented by cs under the given assignment of +// tag values. This is the AND of the values of the constituent Constraints. func (cs Constraints) Evaluate(v map[string]bool) bool { r := true for _, c := range cs { @@ -76,6 +110,7 @@ func (cs Constraints) Evaluate(v map[string]bool) bool { return r } +// GoString represents Constraints as +build comment lines. func (cs Constraints) GoString() string { s := "" for _, c := range cs { @@ -84,9 +119,38 @@ func (cs Constraints) GoString() string { return s } -func (c Constraint) ToConstraints() Constraints { return Constraints{c} } -func (c Constraint) ToConstraint() Constraint { return c } +// Constraint represents the OR of a list of Options. +type Constraint []Option +// Any builds a Constraint that will be true if any of its options are true. +func Any(opts ...OptionConvertable) Constraint { + c := Constraint{} + for _, opt := range opts { + c = append(c, opt.ToOption()) + } + return c +} + +// ParseConstraint parses a space-separated list of options. +func ParseConstraint(expr string) (Constraint, error) { + c := Constraint{} + for _, field := range strings.Fields(expr) { + opt, err := ParseOption(field) + if err != nil { + return c, err + } + c = append(c, opt) + } + return c, nil +} + +// ToConstraints returns the list of constraints containing just c. +func (c Constraint) ToConstraints() Constraints { return Constraints{c} } + +// ToConstraint returns c. +func (c Constraint) ToConstraint() Constraint { return c } + +// Validate validates the constraint. func (c Constraint) Validate() error { for _, o := range c { if err := o.Validate(); err != nil { @@ -96,6 +160,8 @@ func (c Constraint) Validate() error { return nil } +// Evaluate the boolean formula represented by c under the given assignment of +// tag values. This is the OR of the values of the constituent Options. func (c Constraint) Evaluate(v map[string]bool) bool { r := false for _, o := range c { @@ -104,6 +170,7 @@ func (c Constraint) Evaluate(v map[string]bool) bool { return r } +// GoString represents the Constraint as one +build comment line. func (c Constraint) GoString() string { s := "// +build" for _, o := range c { @@ -112,10 +179,33 @@ func (c Constraint) GoString() string { return s + "\n" } -func (o Option) ToConstraints() Constraints { return o.ToConstraint().ToConstraints() } -func (o Option) ToConstraint() Constraint { return Constraint{o} } -func (o Option) ToOption() Option { return o } +// Option represents the AND of a list of Terms. +type Option []Term +// Opt builds an Option from the list of Terms. +func Opt(terms ...Term) Option { + return Option(terms) +} + +// ParseOption parses a comma-separated list of terms. +func ParseOption(expr string) (Option, error) { + opt := Option{} + for _, t := range strings.Split(expr, ",") { + opt = append(opt, Term(t)) + } + return opt, opt.Validate() +} + +// ToConstraints returns Constraints containing just this option. +func (o Option) ToConstraints() Constraints { return o.ToConstraint().ToConstraints() } + +// ToConstraint returns a Constraint containing just this option. +func (o Option) ToConstraint() Constraint { return Constraint{o} } + +// ToOption returns o. +func (o Option) ToOption() Option { return o } + +// Validate validates o. func (o Option) Validate() error { for _, t := range o { if err := t.Validate(); err != nil { @@ -125,6 +215,8 @@ func (o Option) Validate() error { return nil } +// Evaluate the boolean formula represented by o under the given assignment of +// tag values. This is the AND of the values of the constituent Terms. func (o Option) Evaluate(v map[string]bool) bool { r := true for _, t := range o { @@ -133,6 +225,7 @@ func (o Option) Evaluate(v map[string]bool) bool { return r } +// GoString represents the Option as a comma-separated list of terms. func (o Option) GoString() string { var ts []string for _, t := range o { @@ -141,16 +234,32 @@ func (o Option) GoString() string { return strings.Join(ts, ",") } -func (t Term) ToConstraints() Constraints { return t.ToOption().ToConstraints() } -func (t Term) ToConstraint() Constraint { return t.ToOption().ToConstraint() } -func (t Term) ToOption() Option { return Option{t} } +// Term is an atomic term in a build constraint: an identifier or its negation. +type Term string +// Not returns a term for the negation of ident. +func Not(ident string) Term { + return Term("!" + ident) +} + +// ToConstraints returns Constraints containing just this term. +func (t Term) ToConstraints() Constraints { return t.ToOption().ToConstraints() } + +// ToConstraint returns a Constraint containing just this term. +func (t Term) ToConstraint() Constraint { return t.ToOption().ToConstraint() } + +// ToOption returns an Option containing just this term. +func (t Term) ToOption() Option { return Option{t} } + +// IsNegated reports whether t is the negation of an identifier. func (t Term) IsNegated() bool { return strings.HasPrefix(string(t), "!") } +// Name returns the identifier for this term. func (t Term) Name() string { return strings.TrimPrefix(string(t), "!") } +// Validate the term. func (t Term) Validate() error { // Reference: https://github.com/golang/go/blob/204a8f55dc2e0ac8d27a781dab0da609b98560da/src/cmd/go/internal/imports/build.go#L110-L112 // @@ -185,60 +294,19 @@ func (t Term) Validate() error { return nil } +// Evaluate the term under the given set of identifier values. func (t Term) Evaluate(v map[string]bool) bool { return (t.Validate() == nil) && (v[t.Name()] == !t.IsNegated()) } +// GoString returns t. func (t Term) GoString() string { return string(t) } -func Not(ident string) Term { - return Term("!" + ident) -} - -func And(cs ...ConstraintConvertable) Constraints { - constraints := Constraints{} - for _, c := range cs { - constraints = append(constraints, c.ToConstraint()) - } - return constraints -} - -func Any(opts ...OptionConvertable) Constraint { - c := Constraint{} - for _, opt := range opts { - c = append(c, opt.ToOption()) - } - return c -} - -func Opt(terms ...Term) Option { - return Option(terms) -} - -func ParseOption(expr string) (Option, error) { - opt := Option{} - for _, t := range strings.Split(expr, ",") { - opt = append(opt, Term(t)) - } - return opt, opt.Validate() -} - -func ParseConstraint(expr string) (Constraint, error) { - c := Constraint{} - for _, field := range strings.Fields(expr) { - opt, err := ParseOption(field) - if err != nil { - return c, err - } - c = append(c, opt) - } - return c, nil -} - -func SetTags(names ...string) map[string]bool { +// SetTags builds a set where the given list of identifiers are true. +func SetTags(idents ...string) map[string]bool { v := map[string]bool{} - for _, n := range names { - v[n] = true + for _, ident := range idents { + v[ident] = true } return v } diff --git a/buildtags/examples_test.go b/buildtags/examples_test.go new file mode 100644 index 0000000..121f406 --- /dev/null +++ b/buildtags/examples_test.go @@ -0,0 +1,16 @@ +package buildtags_test + +import ( + "fmt" + + "github.com/mmcloughlin/avo/buildtags" +) + +func ExampleParseConstraint() { + c, err := buildtags.ParseConstraint("a,!b c") + fmt.Print(c.GoString()) + fmt.Println(err) + // Output: + // // +build a,!b c + // +}