blob: 0ecf92b987053d79c2bb4b1fc90e853bc609e810 [file] [log] [blame] [edit]
// Copyright 2024 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.
//go:build ignore
// The generate command reads all the GOROOT/api/go1.*.txt files and
// generates a single combined manifest.go file containing the Go
// standard library API symbols along with versions.
//
// It also runs "go list -deps std" and records the import graph. This
// information may be used, for example, to ensure that tools don't
// suggest fixes that import package P when analyzing one of P's
// dependencies.
package main
import (
"bytes"
"cmp"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"go/types"
"io/fs"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
)
func main() {
log.SetFlags(log.Lshortfile) // to identify the source of the log messages
dir := apidir()
manifest(dir)
deps()
}
// -- generate std manifest --
func manifest(apidir string) {
// find the signatures
cfg := packages.Config{
Mode: packages.LoadTypes,
Env: append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64"),
}
// find the source. This is not totally reliable: different
// systems may get different versions of unreleased APIs.
// The result depends on the toolchain.
// The x/tools release process regenerates the table
// with the canonical toolchain.
stdpkgs, err := packages.Load(&cfg, "std")
if err != nil {
log.Fatal(err)
}
signatures := make(map[string]map[string]string) // PkgPath->FuncName->signature
// signatures start with func and may contain type parameters
// "func[T comparable](value T) unique.Handle[T]"
for _, pkg := range stdpkgs {
if strings.HasPrefix(pkg.PkgPath, "vendor/") ||
strings.HasPrefix(pkg.PkgPath, "internal/") ||
strings.Contains(pkg.PkgPath, "/internal/") {
continue
}
for _, name := range pkg.Types.Scope().Names() {
fixer := func(p *types.Package) string {
// fn.Signature() would have produced
// "func(fi io/fs.FileInfo, link string) (*archive/tar.Header, error)"},
// This produces
// "func FileInfoHeader(fi fs.FileInfo, link string) (*Header, error)""
// Note that the function name is superfluous, so it is removed below
if p != pkg.Types {
return p.Name()
}
return ""
}
obj := pkg.Types.Scope().Lookup(name)
if fn, ok := obj.(*types.Func); ok {
mp, ok := signatures[pkg.PkgPath]
if !ok {
mp = make(map[string]string)
signatures[pkg.PkgPath] = mp
}
sig := types.ObjectString(fn, fixer)
// remove the space and function name introduced by fixer
sig = strings.Replace(sig, " "+name, "", 1)
mp[name] = sig
}
}
}
// read the api data
pkgs := make(map[string]map[string]symInfo) // package -> symbol -> info
symRE := regexp.MustCompile(`^pkg (\S+).*?, (var|func|type|const|method \([^)]*\)) ([\pL\p{Nd}_]+)(.*)`)
// parse parses symbols out of GOROOT/api/*.txt data, with the specified minor version.
// Errors are reported against filename.
parse := func(filename string, data []byte, minor int) {
for linenum, line := range strings.Split(string(data), "\n") {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
m := symRE.FindStringSubmatch(line)
if m == nil {
log.Fatalf("invalid input: %s:%d: %s", filename, linenum+1, line)
}
path, kind, sym, rest := m[1], m[2], m[3], m[4]
if _, recv, ok := strings.Cut(kind, "method "); ok {
// e.g. "method (*Func) Pos() token.Pos"
kind = "method" // (concrete)
recv := removeTypeParam(recv) // (*Foo[T]) -> (*Foo)
sym = recv + "." + sym // (*T).m
} else if method, ok := strings.CutPrefix(rest, " interface, "); ok && kind == "type" {
// e.g. "pkg reflect, type Type interface, Comparable() bool"
// or "pkg net, type Error interface, Temporary //deprecated"
kind = "method" // (abstract)
if strings.HasPrefix(method, "unexported methods") {
continue
}
if strings.Contains(method, " //deprecated") {
continue
}
name, _, ok := strings.Cut(method, "(")
if !ok {
log.Printf("unexpected: %s", line)
continue
}
sym = fmt.Sprintf("(%s).%s", sym, name) // (T).m
} else if field, ok := strings.CutPrefix(rest, " struct, "); ok && kind == "type" {
// e.g. "type ParenExpr struct, Lparen token.Pos"
kind = "field"
name, typ, _ := strings.Cut(field, " ")
// The api script uses the name
// "embedded" (ambiguously) for
// the name of an anonymous field.
if name == "embedded" {
// Strip "*pkg.T" down to "T".
typ = strings.TrimPrefix(typ, "*")
if _, after, ok := strings.Cut(typ, "."); ok {
typ = after
}
typ = removeTypeParam(typ) // embedded Foo[T] -> Foo
name = typ
}
sym += "." + name // T.f
}
symbols, ok := pkgs[path]
if !ok {
symbols = make(map[string]symInfo)
pkgs[path] = symbols
}
// Don't overwrite earlier entries:
// enums are redeclared in later versions
// as their encoding changes;
// deprecations count as updates too.
// TODO(adonovan): it would be better to mark
// deprecated as a boolean without changing the
// version.
if _, ok := symbols[sym]; !ok {
var sig string
if kind == "func" {
sig = signatures[path][sym]
}
symbols[sym] = symInfo{
kind: kind,
minor: minor,
signature: sig,
}
}
}
}
// Read and parse the GOROOT/api manifests.
for minor := 0; ; minor++ {
base := "go1.txt"
if minor > 0 {
base = fmt.Sprintf("go1.%d.txt", minor)
}
filename := filepath.Join(apidir, base)
data, err := os.ReadFile(filename)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// All caught up.
// Synthesize one final file from any api/next/*.txt fragments.
// (They are consolidated into a go1.%d file some time between
// the freeze and the first release candidate.)
filenames, err := filepath.Glob(filepath.Join(apidir, "next", "*.txt"))
if err != nil {
log.Fatal(err)
}
var next bytes.Buffer
for _, filename := range filenames {
data, err := os.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
next.Write(data)
}
parse(filename, next.Bytes(), minor) // (filename is a lie)
break
}
log.Fatal(err)
}
parse(filename, data, minor)
}
// The APIs of the syscall/js and unsafe packages need to be computed explicitly,
// because they're not included in the GOROOT/api/go1.*.txt files at this time.
pkgs["syscall/js"] = loadSymbols("syscall/js", "GOOS=js", "GOARCH=wasm")
pkgs["unsafe"] = exportedSymbols(types.Unsafe) // TODO(adonovan): set correct versions
// Write the combined manifest.
var buf bytes.Buffer
buf.WriteString(`// Copyright 2025 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.
// Code generated by generate.go. DO NOT EDIT.
package stdlib
var PackageSymbols = map[string][]Symbol{
`)
for _, path := range sortedKeys(pkgs) {
pkg := pkgs[path]
fmt.Fprintf(&buf, "\t%q: {\n", path)
for _, name := range sortedKeys(pkg) {
info := pkg[name]
fmt.Fprintf(&buf, "\t\t{%q, %s, %d, %q},\n",
name, strings.Title(info.kind), info.minor, info.signature)
}
fmt.Fprintln(&buf, "},")
}
fmt.Fprintln(&buf, "}")
fmtbuf, err := format.Source(buf.Bytes())
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("manifest.go", fmtbuf, 0o666); err != nil {
log.Fatal(err)
}
}
// find the api directory, In most situations it is in GOROOT/api, but not always.
// TODO(pjw): understand where it might be, and if there could be newer and older versions
func apidir() string {
stdout := new(bytes.Buffer)
cmd := exec.Command("go", "env", "GOROOT", "GOPATH")
cmd.Stdout = stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
// Prefer GOROOT/api over GOPATH/api.
for line := range strings.SplitSeq(stdout.String(), "\n") {
apidir := filepath.Join(line, "api")
info, err := os.Stat(apidir)
if err == nil && info.IsDir() {
return apidir
}
}
log.Fatal("could not find api dir")
return ""
}
type symInfo struct {
kind string // e.g. "func"
minor int // go1.%d
// for completion snippets
signature string // for Kind == stdlib.Func
}
// loadSymbols computes the exported symbols in the specified package
// by parsing and type-checking the current source.
func loadSymbols(pkg string, extraEnv ...string) map[string]symInfo {
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedTypes,
Env: append(os.Environ(), extraEnv...),
}, pkg)
if err != nil {
log.Fatalln(err)
} else if len(pkgs) != 1 {
log.Fatalf("got %d packages, want one package %q", len(pkgs), pkg)
}
return exportedSymbols(pkgs[0].Types)
}
func exportedSymbols(pkg *types.Package) map[string]symInfo {
symbols := make(map[string]symInfo)
for _, name := range pkg.Scope().Names() {
if obj := pkg.Scope().Lookup(name); obj.Exported() {
var kind string
switch obj.(type) {
case *types.Func, *types.Builtin:
kind = "func"
case *types.Const:
kind = "const"
case *types.Var:
kind = "var"
case *types.TypeName:
kind = "type"
// TODO(adonovan): expand fields and methods of syscall/js.*
default:
log.Fatalf("unexpected object type: %v", obj)
}
symbols[name] = symInfo{kind: kind, minor: 0} // pretend go1.0
}
}
return symbols
}
func sortedKeys[M ~map[K]V, K cmp.Ordered, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
slices.Sort(r)
return r
}
func removeTypeParam(s string) string {
i := strings.IndexByte(s, '[')
j := strings.LastIndexByte(s, ']')
if i > 0 && j > i {
s = s[:i] + s[j+len("["):]
}
return s
}
// -- generate dependency graph --
func deps() {
type Package struct {
// go list JSON output
ImportPath string // import path of package in dir
Imports []string // import paths used by this package
// encoding
index int
deps []int // indices of direct imports, sorted
}
pkgs := make(map[string]*Package)
var keys []string
for dec := json.NewDecoder(runGo("list", "-deps", "-json", "std")); dec.More(); {
var pkg Package
if err := dec.Decode(&pkg); err != nil {
log.Fatal(err)
}
pkgs[pkg.ImportPath] = &pkg
keys = append(keys, pkg.ImportPath)
}
// Sort and number the packages.
// There are 344 as of Mar 2025.
slices.Sort(keys)
for i, name := range keys {
pkgs[name].index = i
}
// Encode the dependencies.
for _, pkg := range pkgs {
for _, imp := range pkg.Imports {
if imp == "C" {
continue
}
pkg.deps = append(pkg.deps, pkgs[imp].index)
}
slices.Sort(pkg.deps)
}
// Emit the table.
var buf bytes.Buffer
buf.WriteString(`// Copyright 2025 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.
// Code generated by generate.go. DO NOT EDIT.
package stdlib
type pkginfo struct {
name string
deps string // list of indices of dependencies, as varint-encoded deltas
}
var deps = [...]pkginfo{
`)
for _, name := range keys {
prev := 0
var deps []int
for _, v := range pkgs[name].deps {
deps = append(deps, v-prev) // delta
prev = v
}
var data []byte
for _, v := range deps {
data = binary.AppendUvarint(data, uint64(v))
}
fmt.Fprintf(&buf, "\t{%q, %q},\n", name, data)
}
fmt.Fprintln(&buf, "}")
// Also write the list of bootstrap packages.
// (We can't use indices because it is not a subset of std.)
bootstrap, version := bootstrap()
minor := strings.Split(version, ".")[1] // "go1.2.3" -> "2"
buf.WriteString(`
// bootstrap is the list of bootstrap packages extracted from cmd/dist.
var bootstrap = map[string]bool{
`)
for _, pkg := range bootstrap {
fmt.Fprintf(&buf, "\t%q: true,\n", pkg)
}
fmt.Fprintf(&buf, `}
// BootstrapVersion is the minor version of Go used during toolchain
// bootstrapping. Packages for which [IsBootstrapPackage] must not use
// features of Go newer than this version.
const BootstrapVersion = Version(%s) // %s
`, minor, version)
// Format and update the dependencies file.
fmtbuf, err := format.Source(buf.Bytes())
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("deps.go", fmtbuf, 0o666); err != nil {
log.Fatal(err)
}
// Also generate the data for the test.
for _, t := range [...]struct{ flag, filename string }{
{"-deps=true", "testdata/nethttp.deps"},
{`-f={{join .Imports "\n"}}`, "testdata/nethttp.imports"},
} {
stdout := new(bytes.Buffer)
cmd := exec.Command("go", "list", t.flag, "net/http")
cmd.Stdout = stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64")
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
if err := os.WriteFile(t.filename, stdout.Bytes(), 0666); err != nil {
log.Fatal(err)
}
}
}
// bootstrap returns the list of bootstrap packages out of the
// source of the dist command, along with the minimum toolchain
// version.
//
// We assume it is "var bootstrapDirs []string" in buildtool.go, and
// is a list of string literals, either package names or "dir/...".
// TODO(adonovan): find a more robust solution.
func bootstrap() ([]string, string) {
fset := token.NewFileSet()
filename := strings.TrimSpace(runGo("list", "-f={{.Dir}}/buildtool.go", "cmd/dist").String())
f, err := parser.ParseFile(fset, filename, nil, 0)
if err != nil {
log.Fatalf("can't parse buildtool.go file in cmd/dist package: %v", err)
}
const bootstrapVarName = "bootstrapDirs"
var (
args = []string{"list"} // go list command to expand bootstrap packages
version string
)
for _, decl := range f.Decls {
decl, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
for _, spec := range decl.Specs {
spec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
if len(spec.Names) != 1 {
continue
}
switch spec.Names[0].Name {
case bootstrapVarName:
// var bootstrapDirs = []string{ ... }
if len(spec.Values) != 1 {
log.Fatalf("%s: %s var spec has %d values, want 1",
fset.Position(spec.Pos()), len(spec.Values))
}
value0 := spec.Values[0]
lit, ok := value0.(*ast.CompositeLit)
if !ok {
log.Fatalf("%s: %s assigned from %T, want slice literal",
fset.Position(value0.Pos()), value0)
}
// Construct a go list command from the package patterns.
for _, elt := range lit.Elts {
lit, ok := elt.(*ast.BasicLit)
if !ok {
log.Fatalf("%s: element is %T, want string literal",
fset.Position(elt.Pos()), elt)
}
pattern, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatalf("%s: %v", fset.Position(elt.Pos()), err)
}
args = append(args, pattern)
}
case "minBootstrap":
// const minBootstrap = "go1.2.3"
lit := spec.Values[0].(*ast.BasicLit)
version, _ = strconv.Unquote(lit.Value)
}
}
}
if len(args) < 2 {
log.Fatalf("can't find var %s in buildtool.go file in cmd/dist package: %v",
bootstrapVarName, err)
}
if version == "" {
log.Fatalf("can't find const minBootstrap version in buildtool.go file in cmd/dist package: %v",
err)
}
return strings.Split(strings.TrimSpace(runGo(args...).String()), "\n"), version
}
func runGo(args ...string) *bytes.Buffer {
cmd := exec.Command("go", args...)
cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux", "GOARCH=amd64")
stdout, err := cmd.Output()
if err != nil {
log.Fatalf("%s: failed: %v", cmd, err)
}
return bytes.NewBuffer(stdout)
}