summaryrefslogtreecommitdiffhomepage
path: root/tools/constraintutil/constraintutil.go
blob: fb3fbe5c221ffc478e1500c55068e36688dc02eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// Copyright 2021 The gVisor Authors.
//
// 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 constraintutil provides utilities for working with Go build
// constraints.
package constraintutil

import (
	"bufio"
	"bytes"
	"fmt"
	"go/build/constraint"
	"io"
	"os"
	"strings"
)

// FromReader extracts the build constraint from the Go source or assembly file
// whose contents are read by r.
func FromReader(r io.Reader) (constraint.Expr, error) {
	// See go/build.parseFileHeader() for the "official" logic that this is
	// derived from.
	const (
		slashStar     = "/*"
		starSlash     = "*/"
		gobuildPrefix = "//go:build"
	)
	s := bufio.NewScanner(r)
	var (
		inSlashStar = false // between /* and */
		haveGobuild = false
		e           constraint.Expr
	)
Lines:
	for s.Scan() {
		line := bytes.TrimSpace(s.Bytes())
		if !inSlashStar && constraint.IsGoBuild(string(line)) {
			if haveGobuild {
				return nil, fmt.Errorf("multiple go:build directives")
			}
			haveGobuild = true
			var err error
			e, err = constraint.Parse(string(line))
			if err != nil {
				return nil, err
			}
		}
	ThisLine:
		for len(line) > 0 {
			if inSlashStar {
				if i := bytes.Index(line, []byte(starSlash)); i >= 0 {
					inSlashStar = false
					line = bytes.TrimSpace(line[i+len(starSlash):])
					continue ThisLine
				}
				continue Lines
			}
			if bytes.HasPrefix(line, []byte("//")) {
				continue Lines
			}
			// Note that if /* appears in the line, but not at the beginning,
			// then the line is still non-empty, so skipping this and
			// terminating below is correct.
			if bytes.HasPrefix(line, []byte(slashStar)) {
				inSlashStar = true
				line = bytes.TrimSpace(line[len(slashStar):])
				continue ThisLine
			}
			// A non-empty non-comment line terminates scanning for go:build.
			break Lines
		}
	}
	return e, s.Err()
}

// FromString extracts the build constraint from the Go source or assembly file
// containing the given data. If no build constraint applies to the file, it
// returns nil.
func FromString(str string) (constraint.Expr, error) {
	return FromReader(strings.NewReader(str))
}

// FromFile extracts the build constraint from the Go source or assembly file
// at the given path. If no build constraint applies to the file, it returns
// nil.
func FromFile(path string) (constraint.Expr, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return FromReader(f)
}

// Combine returns a constraint.Expr that evaluates to true iff all expressions
// in es evaluate to true. If es is empty, Combine returns nil.
//
// Preconditions: All constraint.Exprs in es are non-nil.
func Combine(es []constraint.Expr) constraint.Expr {
	switch len(es) {
	case 0:
		return nil
	case 1:
		return es[0]
	default:
		a := &constraint.AndExpr{es[0], es[1]}
		for i := 2; i < len(es); i++ {
			a = &constraint.AndExpr{a, es[i]}
		}
		return a
	}
}

// CombineFromFiles returns a build constraint expression that evaluates to
// true iff the build constraints from all of the given Go source or assembly
// files evaluate to true. If no build constraints apply to any of the given
// files, it returns nil.
func CombineFromFiles(paths []string) (constraint.Expr, error) {
	var es []constraint.Expr
	for _, path := range paths {
		e, err := FromFile(path)
		if err != nil {
			return nil, fmt.Errorf("failed to read build constraints from %q: %v", path, err)
		}
		if e != nil {
			es = append(es, e)
		}
	}
	return Combine(es), nil
}

// Lines returns a string containing build constraint directives for the given
// constraint.Expr, including two trailing newlines, as appropriate for a Go
// source or assembly file. At least a go:build directive will be emitted; if
// the constraint is expressible using +build directives as well, then +build
// directives will also be emitted.
//
// If e is nil, Lines returns the empty string.
func Lines(e constraint.Expr) string {
	if e == nil {
		return ""
	}

	var b strings.Builder
	b.WriteString("//go:build ")
	b.WriteString(e.String())
	b.WriteByte('\n')

	if pblines, err := constraint.PlusBuildLines(e); err == nil {
		for _, line := range pblines {
			b.WriteString(line)
			b.WriteByte('\n')
		}
	}

	b.WriteByte('\n')
	return b.String()
}