From 44d73dfb4e5f80998dc0276e239e3e860c8d9e91 Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Wed, 23 Jul 2025 11:45:01 -0400 Subject: [PATCH] cmd/go/internal/doc: clean up after merge with cmd/internal/doc This is done in a separate CL to reduce the diffs from the previous CL. Merge the main.go and doc.go files, and isolate the bootstrap-tagged code to one file. For #74667 Change-Id: I11bf0aa18beeb898937135f49f473c1ba1b7e756 Reviewed-on: https://go-review.googlesource.com/c/go/+/689875 Reviewed-by: Michael Matloob Auto-Submit: Michael Matloob LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Pratt --- src/cmd/go/internal/doc/dirs.go | 2 - src/cmd/go/internal/doc/doc.go | 431 ++++++++++++++- src/cmd/go/internal/doc/main.go | 521 ------------------ src/cmd/go/internal/doc/pkg.go | 2 - src/cmd/go/internal/doc/pkgsite.go | 93 ++++ ...{doc_bootstrap.go => pkgsite_bootstrap.go} | 6 +- 6 files changed, 524 insertions(+), 531 deletions(-) delete mode 100644 src/cmd/go/internal/doc/main.go create mode 100644 src/cmd/go/internal/doc/pkgsite.go rename src/cmd/go/internal/doc/{doc_bootstrap.go => pkgsite_bootstrap.go} (61%) diff --git a/src/cmd/go/internal/doc/dirs.go b/src/cmd/go/internal/doc/dirs.go index 350c386587..8b1670f61c 100644 --- a/src/cmd/go/internal/doc/dirs.go +++ b/src/cmd/go/internal/doc/dirs.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !cmd_go_bootstrap - package doc import ( diff --git a/src/cmd/go/internal/doc/doc.go b/src/cmd/go/internal/doc/doc.go index ed3c191dc3..37501065fe 100644 --- a/src/cmd/go/internal/doc/doc.go +++ b/src/cmd/go/internal/doc/doc.go @@ -2,18 +2,26 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !cmd_go_bootstrap - // Package doc implements the “go doc” command. package doc import ( + "bytes" "context" "flag" + "fmt" + "go/build" + "go/token" + "io" "log" "os" + "os/exec" + "path" + "path/filepath" + "strings" "cmd/go/internal/base" + "cmd/internal/telemetry/counter" ) var CmdDoc = &base.Command{ @@ -148,3 +156,422 @@ func runDoc(ctx context.Context, cmd *base.Command, args []string) { log.Fatal(err) } } + +var ( + unexported bool // -u flag + matchCase bool // -c flag + chdir string // -C flag + showAll bool // -all flag + showCmd bool // -cmd flag + showSrc bool // -src flag + short bool // -short flag + serveHTTP bool // -http flag +) + +// usage is a replacement usage function for the flags package. +func usage(flagSet *flag.FlagSet) { + fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n") + fmt.Fprintf(os.Stderr, "\tgo doc\n") + fmt.Fprintf(os.Stderr, "\tgo doc \n") + fmt.Fprintf(os.Stderr, "\tgo doc [.]\n") + fmt.Fprintf(os.Stderr, "\tgo doc [.][.]\n") + fmt.Fprintf(os.Stderr, "\tgo doc [.][.]\n") + fmt.Fprintf(os.Stderr, "\tgo doc [.]\n") + fmt.Fprintf(os.Stderr, "For more information run\n") + fmt.Fprintf(os.Stderr, "\tgo help doc\n\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + flagSet.PrintDefaults() + os.Exit(2) +} + +// do is the workhorse, broken out of runDoc to make testing easier. +func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) { + flagSet.Usage = func() { usage(flagSet) } + unexported = false + matchCase = false + flagSet.StringVar(&chdir, "C", "", "change to `dir` before running command") + flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported") + flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)") + flagSet.BoolVar(&showAll, "all", false, "show all documentation for package") + flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command") + flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol") + flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol") + flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP") + flagSet.Parse(args) + counter.CountFlags("doc/flag:", *flag.CommandLine) + if chdir != "" { + if err := os.Chdir(chdir); err != nil { + return err + } + } + if serveHTTP { + // Special case: if there are no arguments, try to go to an appropriate page + // depending on whether we're in a module or workspace. The pkgsite homepage + // is often not the most useful page. + if len(flagSet.Args()) == 0 { + mod, err := runCmd(append(os.Environ(), "GOWORK=off"), "go", "list", "-m") + if err == nil && mod != "" && mod != "command-line-arguments" { + // If there's a module, go to the module's doc page. + return doPkgsite(mod) + } + gowork, err := runCmd(nil, "go", "env", "GOWORK") + if err == nil && gowork != "" { + // Outside a module, but in a workspace, go to the home page + // with links to each of the modules' pages. + return doPkgsite("") + } + // Outside a module or workspace, go to the documentation for the standard library. + return doPkgsite("std") + } + + // If args are provided, we need to figure out which page to open on the pkgsite + // instance. Run the logic below to determine a match for a symbol, method, + // or field, but don't actually print the documentation to the output. + writer = io.Discard + } + var paths []string + var symbol, method string + // Loop until something is printed. + dirs.Reset() + for i := 0; ; i++ { + buildPackage, userPath, sym, more := parseArgs(flagSet, flagSet.Args()) + if i > 0 && !more { // Ignore the "more" bit on the first iteration. + return failMessage(paths, symbol, method) + } + if buildPackage == nil { + return fmt.Errorf("no such package: %s", userPath) + } + + // The builtin package needs special treatment: its symbols are lower + // case but we want to see them, always. + if buildPackage.ImportPath == "builtin" { + unexported = true + } + + symbol, method = parseSymbol(flagSet, sym) + pkg := parsePackage(writer, buildPackage, userPath) + paths = append(paths, pkg.prettyPath()) + + defer func() { + pkg.flush() + e := recover() + if e == nil { + return + } + pkgError, ok := e.(PackageError) + if ok { + err = pkgError + return + } + panic(e) + }() + + var found bool + switch { + case symbol == "": + pkg.packageDoc() // The package exists, so we got some output. + found = true + case method == "": + if pkg.symbolDoc(symbol) { + found = true + } + case pkg.printMethodDoc(symbol, method): + found = true + case pkg.printFieldDoc(symbol, method): + found = true + } + if found { + if serveHTTP { + path, err := objectPath(userPath, pkg, symbol, method) + if err != nil { + return err + } + return doPkgsite(path) + } + return nil + } + } +} + +func runCmd(env []string, cmdline ...string) (string, error) { + var stdout, stderr strings.Builder + cmd := exec.Command(cmdline[0], cmdline[1:]...) + cmd.Env = env + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("go doc: %s: %v\n%s\n", strings.Join(cmdline, " "), err, stderr.String()) + } + return strings.TrimSpace(stdout.String()), nil +} + +func objectPath(userPath string, pkg *Package, symbol, method string) (string, error) { + var err error + path := pkg.build.ImportPath + if path == "." { + // go/build couldn't determine the import path, probably + // because this was a relative path into a module. Use + // go list to get the import path. + path, err = runCmd(nil, "go", "list", userPath) + if err != nil { + return "", err + } + } + + object := symbol + if symbol != "" && method != "" { + object = symbol + "." + method + } + if object != "" { + path = path + "#" + object + } + return path, nil +} + +// failMessage creates a nicely formatted error message when there is no result to show. +func failMessage(paths []string, symbol, method string) error { + var b bytes.Buffer + if len(paths) > 1 { + b.WriteString("s") + } + b.WriteString(" ") + for i, path := range paths { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(path) + } + if method == "" { + return fmt.Errorf("no symbol %s in package%s", symbol, &b) + } + return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b) +} + +// parseArgs analyzes the arguments (if any) and returns the package +// it represents, the part of the argument the user used to identify +// the path (or "" if it's the current package) and the symbol +// (possibly with a .method) within that package. +// parseSymbol is used to analyze the symbol itself. +// The boolean final argument reports whether it is possible that +// there may be more directories worth looking at. It will only +// be true if the package path is a partial match for some directory +// and there may be more matches. For example, if the argument +// is rand.Float64, we must scan both crypto/rand and math/rand +// to find the symbol, and the first call will return crypto/rand, true. +func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) { + wd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + if len(args) == 0 { + // Easy: current directory. + return importDir(wd), "", "", false + } + arg := args[0] + // We have an argument. If it is a directory name beginning with . or .., + // use the absolute path name. This discriminates "./errors" from "errors" + // if the current directory contains a non-standard errors package. + if isDotSlash(arg) { + arg = filepath.Join(wd, arg) + } + switch len(args) { + default: + usage(flagSet) + case 1: + // Done below. + case 2: + // Package must be findable and importable. + pkg, err := build.Import(args[0], wd, build.ImportComment) + if err == nil { + return pkg, args[0], args[1], false + } + for { + packagePath, ok := findNextPackage(arg) + if !ok { + break + } + if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil { + return pkg, arg, args[1], true + } + } + return nil, args[0], args[1], false + } + // Usual case: one argument. + // If it contains slashes, it begins with either a package path + // or an absolute directory. + // First, is it a complete package path as it is? If so, we are done. + // This avoids confusion over package paths that have other + // package paths as their prefix. + var importErr error + if filepath.IsAbs(arg) { + pkg, importErr = build.ImportDir(arg, build.ImportComment) + if importErr == nil { + return pkg, arg, "", false + } + } else { + pkg, importErr = build.Import(arg, wd, build.ImportComment) + if importErr == nil { + return pkg, arg, "", false + } + } + // Another disambiguator: If the argument starts with an upper + // case letter, it can only be a symbol in the current directory. + // Kills the problem caused by case-insensitive file systems + // matching an upper case name as a package name. + if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) { + pkg, err := build.ImportDir(".", build.ImportComment) + if err == nil { + return pkg, "", arg, false + } + } + // If it has a slash, it must be a package path but there is a symbol. + // It's the last package path we care about. + slash := strings.LastIndex(arg, "/") + // There may be periods in the package path before or after the slash + // and between a symbol and method. + // Split the string at various periods to see what we find. + // In general there may be ambiguities but this should almost always + // work. + var period int + // slash+1: if there's no slash, the value is -1 and start is 0; otherwise + // start is the byte after the slash. + for start := slash + 1; start < len(arg); start = period + 1 { + period = strings.Index(arg[start:], ".") + symbol := "" + if period < 0 { + period = len(arg) + } else { + period += start + symbol = arg[period+1:] + } + // Have we identified a package already? + pkg, err := build.Import(arg[0:period], wd, build.ImportComment) + if err == nil { + return pkg, arg[0:period], symbol, false + } + // See if we have the basename or tail of a package, as in json for encoding/json + // or ivy/value for robpike.io/ivy/value. + pkgName := arg[:period] + for { + path, ok := findNextPackage(pkgName) + if !ok { + break + } + if pkg, err = build.ImportDir(path, build.ImportComment); err == nil { + return pkg, arg[0:period], symbol, true + } + } + dirs.Reset() // Next iteration of for loop must scan all the directories again. + } + // If it has a slash, we've failed. + if slash >= 0 { + // build.Import should always include the path in its error message, + // and we should avoid repeating it. Unfortunately, build.Import doesn't + // return a structured error. That can't easily be fixed, since it + // invokes 'go list' and returns the error text from the loaded package. + // TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages + // instead of go/build. + importErrStr := importErr.Error() + if strings.Contains(importErrStr, arg[:period]) { + log.Fatal(importErrStr) + } else { + log.Fatalf("no such package %s: %s", arg[:period], importErrStr) + } + } + // Guess it's a symbol in the current directory. + return importDir(wd), "", arg, false +} + +// dotPaths lists all the dotted paths legal on Unix-like and +// Windows-like file systems. We check them all, as the chance +// of error is minute and even on Windows people will use ./ +// sometimes. +var dotPaths = []string{ + `./`, + `../`, + `.\`, + `..\`, +} + +// isDotSlash reports whether the path begins with a reference +// to the local . or .. directory. +func isDotSlash(arg string) bool { + if arg == "." || arg == ".." { + return true + } + for _, dotPath := range dotPaths { + if strings.HasPrefix(arg, dotPath) { + return true + } + } + return false +} + +// importDir is just an error-catching wrapper for build.ImportDir. +func importDir(dir string) *build.Package { + pkg, err := build.ImportDir(dir, build.ImportComment) + if err != nil { + log.Fatal(err) + } + return pkg +} + +// parseSymbol breaks str apart into a symbol and method. +// Both may be missing or the method may be missing. +// If present, each must be a valid Go identifier. +func parseSymbol(flagSet *flag.FlagSet, str string) (symbol, method string) { + if str == "" { + return + } + elem := strings.Split(str, ".") + switch len(elem) { + case 1: + case 2: + method = elem[1] + default: + log.Printf("too many periods in symbol specification") + usage(flagSet) + } + symbol = elem[0] + return +} + +// isExported reports whether the name is an exported identifier. +// If the unexported flag (-u) is true, isExported returns true because +// it means that we treat the name as if it is exported. +func isExported(name string) bool { + return unexported || token.IsExported(name) +} + +// findNextPackage returns the next full file name path that matches the +// (perhaps partial) package path pkg. The boolean reports if any match was found. +func findNextPackage(pkg string) (string, bool) { + if filepath.IsAbs(pkg) { + if dirs.offset == 0 { + dirs.offset = -1 + return pkg, true + } + return "", false + } + if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name. + return "", false + } + pkg = path.Clean(pkg) + pkgSuffix := "/" + pkg + for { + d, ok := dirs.Next() + if !ok { + return "", false + } + if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) { + return d.dir, true + } + } +} + +var buildCtx = build.Default + +// splitGopath splits $GOPATH into a list of roots. +func splitGopath() []string { + return filepath.SplitList(buildCtx.GOPATH) +} diff --git a/src/cmd/go/internal/doc/main.go b/src/cmd/go/internal/doc/main.go deleted file mode 100644 index ee04f017fc..0000000000 --- a/src/cmd/go/internal/doc/main.go +++ /dev/null @@ -1,521 +0,0 @@ -// Copyright 2015 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 !cmd_go_bootstrap - -package doc - -import ( - "bytes" - "errors" - "flag" - "fmt" - "go/build" - "go/token" - "io" - "log" - "net" - "net/url" - "os" - "os/exec" - "os/signal" - "path" - "path/filepath" - "strings" - - "cmd/internal/telemetry/counter" -) - -var ( - unexported bool // -u flag - matchCase bool // -c flag - chdir string // -C flag - showAll bool // -all flag - showCmd bool // -cmd flag - showSrc bool // -src flag - short bool // -short flag - serveHTTP bool // -http flag -) - -// usage is a replacement usage function for the flags package. -func usage(flagSet *flag.FlagSet) { - fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n") - fmt.Fprintf(os.Stderr, "\tgo doc\n") - fmt.Fprintf(os.Stderr, "\tgo doc \n") - fmt.Fprintf(os.Stderr, "\tgo doc [.]\n") - fmt.Fprintf(os.Stderr, "\tgo doc [.][.]\n") - fmt.Fprintf(os.Stderr, "\tgo doc [.][.]\n") - fmt.Fprintf(os.Stderr, "\tgo doc [.]\n") - fmt.Fprintf(os.Stderr, "For more information run\n") - fmt.Fprintf(os.Stderr, "\tgo help doc\n\n") - fmt.Fprintf(os.Stderr, "Flags:\n") - flagSet.PrintDefaults() - os.Exit(2) -} - -// do is the workhorse, broken out of main to make testing easier. -func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) { - flagSet.Usage = func() { usage(flagSet) } - unexported = false - matchCase = false - flagSet.StringVar(&chdir, "C", "", "change to `dir` before running command") - flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported") - flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)") - flagSet.BoolVar(&showAll, "all", false, "show all documentation for package") - flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command") - flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol") - flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol") - flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP") - flagSet.Parse(args) - counter.CountFlags("doc/flag:", *flag.CommandLine) - if chdir != "" { - if err := os.Chdir(chdir); err != nil { - return err - } - } - if serveHTTP { - // Special case: if there are no arguments, try to go to an appropriate page - // depending on whether we're in a module or workspace. The pkgsite homepage - // is often not the most useful page. - if len(flagSet.Args()) == 0 { - mod, err := runCmd(append(os.Environ(), "GOWORK=off"), "go", "list", "-m") - if err == nil && mod != "" && mod != "command-line-arguments" { - // If there's a module, go to the module's doc page. - return doPkgsite(mod) - } - gowork, err := runCmd(nil, "go", "env", "GOWORK") - if err == nil && gowork != "" { - // Outside a module, but in a workspace, go to the home page - // with links to each of the modules' pages. - return doPkgsite("") - } - // Outside a module or workspace, go to the documentation for the standard library. - return doPkgsite("std") - } - - // If args are provided, we need to figure out which page to open on the pkgsite - // instance. Run the logic below to determine a match for a symbol, method, - // or field, but don't actually print the documentation to the output. - writer = io.Discard - } - var paths []string - var symbol, method string - // Loop until something is printed. - dirs.Reset() - for i := 0; ; i++ { - buildPackage, userPath, sym, more := parseArgs(flagSet, flagSet.Args()) - if i > 0 && !more { // Ignore the "more" bit on the first iteration. - return failMessage(paths, symbol, method) - } - if buildPackage == nil { - return fmt.Errorf("no such package: %s", userPath) - } - - // The builtin package needs special treatment: its symbols are lower - // case but we want to see them, always. - if buildPackage.ImportPath == "builtin" { - unexported = true - } - - symbol, method = parseSymbol(flagSet, sym) - pkg := parsePackage(writer, buildPackage, userPath) - paths = append(paths, pkg.prettyPath()) - - defer func() { - pkg.flush() - e := recover() - if e == nil { - return - } - pkgError, ok := e.(PackageError) - if ok { - err = pkgError - return - } - panic(e) - }() - - var found bool - switch { - case symbol == "": - pkg.packageDoc() // The package exists, so we got some output. - found = true - case method == "": - if pkg.symbolDoc(symbol) { - found = true - } - case pkg.printMethodDoc(symbol, method): - found = true - case pkg.printFieldDoc(symbol, method): - found = true - } - if found { - if serveHTTP { - path, err := objectPath(userPath, pkg, symbol, method) - if err != nil { - return err - } - return doPkgsite(path) - } - return nil - } - } -} - -func runCmd(env []string, cmdline ...string) (string, error) { - var stdout, stderr strings.Builder - cmd := exec.Command(cmdline[0], cmdline[1:]...) - cmd.Env = env - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("go doc: %s: %v\n%s\n", strings.Join(cmdline, " "), err, stderr.String()) - } - return strings.TrimSpace(stdout.String()), nil -} - -func objectPath(userPath string, pkg *Package, symbol, method string) (string, error) { - var err error - path := pkg.build.ImportPath - if path == "." { - // go/build couldn't determine the import path, probably - // because this was a relative path into a module. Use - // go list to get the import path. - path, err = runCmd(nil, "go", "list", userPath) - if err != nil { - return "", err - } - } - - object := symbol - if symbol != "" && method != "" { - object = symbol + "." + method - } - if object != "" { - path = path + "#" + object - } - return path, nil -} - -func doPkgsite(urlPath string) error { - port, err := pickUnusedPort() - if err != nil { - return fmt.Errorf("failed to find port for documentation server: %v", err) - } - addr := fmt.Sprintf("localhost:%d", port) - path, err := url.JoinPath("http://"+addr, urlPath) - if err != nil { - return fmt.Errorf("internal error: failed to construct url: %v", err) - } - - // Turn off the default signal handler for SIGINT (and SIGQUIT on Unix) - // and instead wait for the child process to handle the signal and - // exit before exiting ourselves. - signal.Ignore(signalsToIgnore...) - - // Prepend the local download cache to GOPROXY to get around deprecation checks. - env := os.Environ() - vars, err := runCmd(env, goCmd(), "env", "GOPROXY", "GOMODCACHE") - fields := strings.Fields(vars) - if err == nil && len(fields) == 2 { - goproxy, gomodcache := fields[0], fields[1] - gomodcache = filepath.Join(gomodcache, "cache", "download") - // Convert absolute path to file URL. pkgsite will not accept - // Windows absolute paths because they look like a host:path remote. - // TODO(golang.org/issue/32456): use url.FromFilePath when implemented. - if strings.HasPrefix(gomodcache, "/") { - gomodcache = "file://" + gomodcache - } else { - gomodcache = "file:///" + filepath.ToSlash(gomodcache) - } - env = append(env, "GOPROXY="+gomodcache+","+goproxy) - } - - const version = "v0.0.0-20250714212547-01b046e81fe7" - cmd := exec.Command(goCmd(), "run", "golang.org/x/pkgsite/cmd/internal/doc@"+version, - "-gorepo", buildCtx.GOROOT, - "-http", addr, - "-open", path) - cmd.Env = env - cmd.Stdout = os.Stderr - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - var ee *exec.ExitError - if errors.As(err, &ee) { - // Exit with the same exit status as pkgsite to avoid - // printing of "exit status" error messages. - // Any relevant messages have already been printed - // to stdout or stderr. - os.Exit(ee.ExitCode()) - } - return err - } - - return nil -} - -// pickUnusedPort finds an unused port by trying to listen on port 0 -// and letting the OS pick a port, then closing that connection and -// returning that port number. -// This is inherently racy. -func pickUnusedPort() (int, error) { - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - return 0, err - } - port := l.Addr().(*net.TCPAddr).Port - if err := l.Close(); err != nil { - return 0, err - } - return port, nil -} - -// failMessage creates a nicely formatted error message when there is no result to show. -func failMessage(paths []string, symbol, method string) error { - var b bytes.Buffer - if len(paths) > 1 { - b.WriteString("s") - } - b.WriteString(" ") - for i, path := range paths { - if i > 0 { - b.WriteString(", ") - } - b.WriteString(path) - } - if method == "" { - return fmt.Errorf("no symbol %s in package%s", symbol, &b) - } - return fmt.Errorf("no method or field %s.%s in package%s", symbol, method, &b) -} - -// parseArgs analyzes the arguments (if any) and returns the package -// it represents, the part of the argument the user used to identify -// the path (or "" if it's the current package) and the symbol -// (possibly with a .method) within that package. -// parseSymbol is used to analyze the symbol itself. -// The boolean final argument reports whether it is possible that -// there may be more directories worth looking at. It will only -// be true if the package path is a partial match for some directory -// and there may be more matches. For example, if the argument -// is rand.Float64, we must scan both crypto/rand and math/rand -// to find the symbol, and the first call will return crypto/rand, true. -func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) { - wd, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - if len(args) == 0 { - // Easy: current directory. - return importDir(wd), "", "", false - } - arg := args[0] - // We have an argument. If it is a directory name beginning with . or .., - // use the absolute path name. This discriminates "./errors" from "errors" - // if the current directory contains a non-standard errors package. - if isDotSlash(arg) { - arg = filepath.Join(wd, arg) - } - switch len(args) { - default: - usage(flagSet) - case 1: - // Done below. - case 2: - // Package must be findable and importable. - pkg, err := build.Import(args[0], wd, build.ImportComment) - if err == nil { - return pkg, args[0], args[1], false - } - for { - packagePath, ok := findNextPackage(arg) - if !ok { - break - } - if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil { - return pkg, arg, args[1], true - } - } - return nil, args[0], args[1], false - } - // Usual case: one argument. - // If it contains slashes, it begins with either a package path - // or an absolute directory. - // First, is it a complete package path as it is? If so, we are done. - // This avoids confusion over package paths that have other - // package paths as their prefix. - var importErr error - if filepath.IsAbs(arg) { - pkg, importErr = build.ImportDir(arg, build.ImportComment) - if importErr == nil { - return pkg, arg, "", false - } - } else { - pkg, importErr = build.Import(arg, wd, build.ImportComment) - if importErr == nil { - return pkg, arg, "", false - } - } - // Another disambiguator: If the argument starts with an upper - // case letter, it can only be a symbol in the current directory. - // Kills the problem caused by case-insensitive file systems - // matching an upper case name as a package name. - if !strings.ContainsAny(arg, `/\`) && token.IsExported(arg) { - pkg, err := build.ImportDir(".", build.ImportComment) - if err == nil { - return pkg, "", arg, false - } - } - // If it has a slash, it must be a package path but there is a symbol. - // It's the last package path we care about. - slash := strings.LastIndex(arg, "/") - // There may be periods in the package path before or after the slash - // and between a symbol and method. - // Split the string at various periods to see what we find. - // In general there may be ambiguities but this should almost always - // work. - var period int - // slash+1: if there's no slash, the value is -1 and start is 0; otherwise - // start is the byte after the slash. - for start := slash + 1; start < len(arg); start = period + 1 { - period = strings.Index(arg[start:], ".") - symbol := "" - if period < 0 { - period = len(arg) - } else { - period += start - symbol = arg[period+1:] - } - // Have we identified a package already? - pkg, err := build.Import(arg[0:period], wd, build.ImportComment) - if err == nil { - return pkg, arg[0:period], symbol, false - } - // See if we have the basename or tail of a package, as in json for encoding/json - // or ivy/value for robpike.io/ivy/value. - pkgName := arg[:period] - for { - path, ok := findNextPackage(pkgName) - if !ok { - break - } - if pkg, err = build.ImportDir(path, build.ImportComment); err == nil { - return pkg, arg[0:period], symbol, true - } - } - dirs.Reset() // Next iteration of for loop must scan all the directories again. - } - // If it has a slash, we've failed. - if slash >= 0 { - // build.Import should always include the path in its error message, - // and we should avoid repeating it. Unfortunately, build.Import doesn't - // return a structured error. That can't easily be fixed, since it - // invokes 'go list' and returns the error text from the loaded package. - // TODO(golang.org/issue/34750): load using golang.org/x/tools/go/packages - // instead of go/build. - importErrStr := importErr.Error() - if strings.Contains(importErrStr, arg[:period]) { - log.Fatal(importErrStr) - } else { - log.Fatalf("no such package %s: %s", arg[:period], importErrStr) - } - } - // Guess it's a symbol in the current directory. - return importDir(wd), "", arg, false -} - -// dotPaths lists all the dotted paths legal on Unix-like and -// Windows-like file systems. We check them all, as the chance -// of error is minute and even on Windows people will use ./ -// sometimes. -var dotPaths = []string{ - `./`, - `../`, - `.\`, - `..\`, -} - -// isDotSlash reports whether the path begins with a reference -// to the local . or .. directory. -func isDotSlash(arg string) bool { - if arg == "." || arg == ".." { - return true - } - for _, dotPath := range dotPaths { - if strings.HasPrefix(arg, dotPath) { - return true - } - } - return false -} - -// importDir is just an error-catching wrapper for build.ImportDir. -func importDir(dir string) *build.Package { - pkg, err := build.ImportDir(dir, build.ImportComment) - if err != nil { - log.Fatal(err) - } - return pkg -} - -// parseSymbol breaks str apart into a symbol and method. -// Both may be missing or the method may be missing. -// If present, each must be a valid Go identifier. -func parseSymbol(flagSet *flag.FlagSet, str string) (symbol, method string) { - if str == "" { - return - } - elem := strings.Split(str, ".") - switch len(elem) { - case 1: - case 2: - method = elem[1] - default: - log.Printf("too many periods in symbol specification") - usage(flagSet) - } - symbol = elem[0] - return -} - -// isExported reports whether the name is an exported identifier. -// If the unexported flag (-u) is true, isExported returns true because -// it means that we treat the name as if it is exported. -func isExported(name string) bool { - return unexported || token.IsExported(name) -} - -// findNextPackage returns the next full file name path that matches the -// (perhaps partial) package path pkg. The boolean reports if any match was found. -func findNextPackage(pkg string) (string, bool) { - if filepath.IsAbs(pkg) { - if dirs.offset == 0 { - dirs.offset = -1 - return pkg, true - } - return "", false - } - if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name. - return "", false - } - pkg = path.Clean(pkg) - pkgSuffix := "/" + pkg - for { - d, ok := dirs.Next() - if !ok { - return "", false - } - if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) { - return d.dir, true - } - } -} - -var buildCtx = build.Default - -// splitGopath splits $GOPATH into a list of roots. -func splitGopath() []string { - return filepath.SplitList(buildCtx.GOPATH) -} diff --git a/src/cmd/go/internal/doc/pkg.go b/src/cmd/go/internal/doc/pkg.go index 989301d909..953b0d9a28 100644 --- a/src/cmd/go/internal/doc/pkg.go +++ b/src/cmd/go/internal/doc/pkg.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !cmd_go_bootstrap - package doc import ( diff --git a/src/cmd/go/internal/doc/pkgsite.go b/src/cmd/go/internal/doc/pkgsite.go new file mode 100644 index 0000000000..6769536ca5 --- /dev/null +++ b/src/cmd/go/internal/doc/pkgsite.go @@ -0,0 +1,93 @@ +// 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. + +//go:build !cmd_go_bootstrap + +package doc + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" +) + +// pickUnusedPort finds an unused port by trying to listen on port 0 +// and letting the OS pick a port, then closing that connection and +// returning that port number. +// This is inherently racy. +func pickUnusedPort() (int, error) { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return 0, err + } + port := l.Addr().(*net.TCPAddr).Port + if err := l.Close(); err != nil { + return 0, err + } + return port, nil +} + +func doPkgsite(urlPath string) error { + port, err := pickUnusedPort() + if err != nil { + return fmt.Errorf("failed to find port for documentation server: %v", err) + } + addr := fmt.Sprintf("localhost:%d", port) + path, err := url.JoinPath("http://"+addr, urlPath) + if err != nil { + return fmt.Errorf("internal error: failed to construct url: %v", err) + } + + // Turn off the default signal handler for SIGINT (and SIGQUIT on Unix) + // and instead wait for the child process to handle the signal and + // exit before exiting ourselves. + signal.Ignore(signalsToIgnore...) + + // Prepend the local download cache to GOPROXY to get around deprecation checks. + env := os.Environ() + vars, err := runCmd(env, goCmd(), "env", "GOPROXY", "GOMODCACHE") + fields := strings.Fields(vars) + if err == nil && len(fields) == 2 { + goproxy, gomodcache := fields[0], fields[1] + gomodcache = filepath.Join(gomodcache, "cache", "download") + // Convert absolute path to file URL. pkgsite will not accept + // Windows absolute paths because they look like a host:path remote. + // TODO(golang.org/issue/32456): use url.FromFilePath when implemented. + if strings.HasPrefix(gomodcache, "/") { + gomodcache = "file://" + gomodcache + } else { + gomodcache = "file:///" + filepath.ToSlash(gomodcache) + } + env = append(env, "GOPROXY="+gomodcache+","+goproxy) + } + + const version = "v0.0.0-20250714212547-01b046e81fe7" + cmd := exec.Command(goCmd(), "run", "golang.org/x/pkgsite/cmd/internal/doc@"+version, + "-gorepo", buildCtx.GOROOT, + "-http", addr, + "-open", path) + cmd.Env = env + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + var ee *exec.ExitError + if errors.As(err, &ee) { + // Exit with the same exit status as pkgsite to avoid + // printing of "exit status" error messages. + // Any relevant messages have already been printed + // to stdout or stderr. + os.Exit(ee.ExitCode()) + } + return err + } + + return nil +} diff --git a/src/cmd/go/internal/doc/doc_bootstrap.go b/src/cmd/go/internal/doc/pkgsite_bootstrap.go similarity index 61% rename from src/cmd/go/internal/doc/doc_bootstrap.go rename to src/cmd/go/internal/doc/pkgsite_bootstrap.go index 8be95dc9a6..c909d6184a 100644 --- a/src/cmd/go/internal/doc/doc_bootstrap.go +++ b/src/cmd/go/internal/doc/pkgsite_bootstrap.go @@ -4,10 +4,8 @@ //go:build cmd_go_bootstrap -// Don't build cmd/doc into go_bootstrap because it depends on net. +// Don't build the pkgsite code into go_bootstrap because it depends on net. package doc -import "cmd/go/internal/base" - -var CmdDoc = &base.Command{} +func doPkgsite(string) error { return nil } -- 2.51.0