blob: be360dfb9d6c49ac0d2afeefc886ccfdc128e0e4 [file]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cache
import (
"bytes"
"errors"
"go/build"
"go/build/constraint"
"io"
"path/filepath"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/util/bug"
)
type port struct{ GOOS, GOARCH string }
var (
// preferredPorts holds GOOS/GOARCH combinations for which we dynamically
// create new Views, by setting GOOS=... and GOARCH=... on top of
// user-provided configuration when we detect that the default build
// configuration does not match an open file. Ports are matched in the order
// defined below, so that when multiple ports match a file we use the port
// occurring at a lower index in the slice. For that reason, we sort first
// class ports ahead of secondary ports, and (among first class ports) 64-bit
// ports ahead of the less common 32-bit ports.
preferredPorts = []port{
// First class ports, from https://go.dev/wiki/PortingPolicy.
{"darwin", "amd64"},
{"darwin", "arm64"},
{"linux", "amd64"},
{"linux", "arm64"},
{"windows", "amd64"},
{"linux", "arm"},
{"linux", "386"},
{"windows", "386"},
// Secondary ports, from GOROOT/src/internal/platform/zosarch.go.
// (First class ports are commented out.)
{"aix", "ppc64"},
{"dragonfly", "amd64"},
{"freebsd", "386"},
{"freebsd", "amd64"},
{"freebsd", "arm"},
{"freebsd", "arm64"},
{"illumos", "amd64"},
{"linux", "ppc64"},
{"linux", "ppc64le"},
{"linux", "mips"},
{"linux", "mipsle"},
{"linux", "mips64"},
{"linux", "mips64le"},
{"linux", "riscv64"},
{"linux", "s390x"},
{"android", "386"},
{"android", "amd64"},
{"android", "arm"},
{"android", "arm64"},
{"ios", "arm64"},
{"ios", "amd64"},
{"js", "wasm"},
{"netbsd", "386"},
{"netbsd", "amd64"},
{"netbsd", "arm"},
{"netbsd", "arm64"},
{"openbsd", "386"},
{"openbsd", "amd64"},
{"openbsd", "arm"},
{"openbsd", "arm64"},
{"openbsd", "mips64"},
{"plan9", "386"},
{"plan9", "amd64"},
{"plan9", "arm"},
{"solaris", "amd64"},
{"windows", "arm"},
{"windows", "arm64"},
{"aix", "ppc64"},
{"android", "386"},
{"android", "amd64"},
{"android", "arm"},
{"android", "arm64"},
// {"darwin", "amd64"},
// {"darwin", "arm64"},
{"dragonfly", "amd64"},
{"freebsd", "386"},
{"freebsd", "amd64"},
{"freebsd", "arm"},
{"freebsd", "arm64"},
{"freebsd", "riscv64"},
{"illumos", "amd64"},
{"ios", "amd64"},
{"ios", "arm64"},
{"js", "wasm"},
// {"linux", "386"},
// {"linux", "amd64"},
// {"linux", "arm"},
// {"linux", "arm64"},
{"linux", "loong64"},
{"linux", "mips"},
{"linux", "mips64"},
{"linux", "mips64le"},
{"linux", "mipsle"},
{"linux", "ppc64"},
{"linux", "ppc64le"},
{"linux", "riscv64"},
{"linux", "s390x"},
{"linux", "sparc64"},
{"netbsd", "386"},
{"netbsd", "amd64"},
{"netbsd", "arm"},
{"netbsd", "arm64"},
{"openbsd", "386"},
{"openbsd", "amd64"},
{"openbsd", "arm"},
{"openbsd", "arm64"},
{"openbsd", "mips64"},
{"openbsd", "ppc64"},
{"openbsd", "riscv64"},
{"plan9", "386"},
{"plan9", "amd64"},
{"plan9", "arm"},
{"solaris", "amd64"},
{"wasip1", "wasm"},
// {"windows", "386"},
// {"windows", "amd64"},
{"windows", "arm"},
{"windows", "arm64"},
}
)
// matches reports whether the port matches a file with the given absolute path
// and content.
//
// Note that this function accepts content rather than e.g. a file.Handle,
// because we trim content before matching for performance reasons, and
// therefore need to do this outside of matches when considering multiple ports.
func (p port) matches(path string, content []byte) bool {
ctxt := build.Default // make a copy
ctxt.UseAllFiles = false
path = filepath.Clean(path)
if !filepath.IsAbs(path) {
bug.Reportf("non-abs file path %q", path)
return false // fail closed
}
dir, name := filepath.Split(path)
// The only virtualized operation called by MatchFile is OpenFile.
ctxt.OpenFile = func(p string) (io.ReadCloser, error) {
if p != path {
return nil, bug.Errorf("unexpected file %q", p)
}
return io.NopCloser(bytes.NewReader(content)), nil
}
ctxt.GOOS = p.GOOS
ctxt.GOARCH = p.GOARCH
ok, err := ctxt.MatchFile(dir, name)
return err == nil && ok
}
// buildConstraintFile returns a minimal version of the given file (whose
// kind must be Go or assembly) containing only the same build
// constraints, if any, plus dummy syntax (e.g. package declaration).
//
// This is an unfortunate but necessary optimization, as matching build
// constraints using go/build has significant overhead, and involves parsing
// more than just the build constraint.
//
// It simulates the build constraint extraction performed by
// [go/build.Context.MatchFile] (see parseFileHeader in GOROOT/src/go/build/build.go).
//
// TestMatchingPortsConsistency enforces consistency by comparing results
// without trimming content.
func buildConstraintFile(kind file.Kind, content []byte) []byte {
trimmed, goBuild, err := parseFileHeader(content)
if err != nil {
return nil
}
var buf bytes.Buffer
if goBuild != nil {
buf.Write(goBuild)
buf.WriteByte('\n')
} else {
for line := range bytes.Lines(trimmed) {
line = bytes.TrimSpace(line)
if constraint.IsPlusBuild(string(line)) {
buf.Write(line)
buf.WriteByte('\n')
}
}
}
switch kind {
case file.Go:
// The package name does not matter, but go/build requires a
// package declaration, and +build lines require a blank line
// before the package declaration.
buf.WriteString("\npackage p")
case file.Asm:
if buf.Len() > 0 && goBuild == nil {
buf.WriteByte('\n') // legacy build tags require a trailing blank line
}
default:
panic("unsupported file kind: " + kind.String())
}
return buf.Bytes()
}
var (
slashSlash = []byte("//")
starSlash = []byte("*/")
slashStar = []byte("/*")
errMultipleGoBuild = errors.New("multiple //go:build comments")
)
// parseFileHeader is copied almost verbatim from GOROOT/src/go/build/build.go.
func parseFileHeader(content []byte) (trimmed, goBuild []byte, err error) {
end := 0
p := content
ended := false // found non-blank, non-// line, so stopped accepting //go:build lines
inSlashStar := false // in /* */ comment
Lines:
for len(p) > 0 {
line := p
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, p = line[:i], p[i+1:]
} else {
p = p[len(p):]
}
line = bytes.TrimSpace(line)
if len(line) == 0 && !ended { // Blank line
// Remember position of most recent blank line.
// When we find the first non-blank, non-// line,
// this "end" position marks the latest file position
// where a //go:build line can appear.
// (It must appear _before_ a blank line before the non-blank, non-// line.
// Yes, that's confusing, which is part of why we moved to //go:build lines.)
// Note that ended==false here means that inSlashStar==false,
// since seeing a /* would have set ended==true.
end = len(content) - len(p)
continue Lines
}
if !bytes.HasPrefix(line, slashSlash) { // Not comment line
ended = true
}
if !inSlashStar && constraint.IsGoBuild(string(line)) {
if goBuild != nil {
return nil, nil, errMultipleGoBuild
}
goBuild = line
}
Comments:
for len(line) > 0 {
if inSlashStar {
if i := bytes.Index(line, starSlash); i >= 0 {
inSlashStar = false
line = bytes.TrimSpace(line[i+len(starSlash):])
continue Comments
}
continue Lines
}
if bytes.HasPrefix(line, slashSlash) {
continue Lines
}
if bytes.HasPrefix(line, slashStar) {
inSlashStar = true
line = bytes.TrimSpace(line[len(slashStar):])
continue Comments
}
// Found non-comment text.
break Lines
}
}
return content[:end], goBuild, nil
}