blob: a2398ef3970b9dad9aaf00b8b980dd49512e01c1 [file] [edit]
// 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 (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/load"
"cmd/go/internal/modload"
"cmd/go/internal/work"
)
// 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
}
// buildPkgsite builds a pkgsite binary whose build may be cached.
func buildPkgsite(ctx context.Context) string {
loader := modload.NewLoader()
// Set the builder to have no module root so we can build a pkg@version pattern.
loader.ForceUseModules = true
loader.RootMode = modload.NoRoot
loader.AllowMissingModuleImports()
modload.Init(loader)
work.BuildInit(loader)
b := work.NewBuilder("", loader.VendorDirOrEmpty)
defer func() {
if err := b.Close(); err != nil {
base.Fatal(err)
}
}()
const version = "v0.0.0-20251223195805-1a3bd3c788fe"
pkgVers := "golang.org/x/pkgsite/cmd/internal/doc@" + version
pkgOpts := load.PackageOpts{MainOnly: true}
pkgs, err := load.PackagesAndErrorsOutsideModule(loader, ctx, pkgOpts, []string{pkgVers})
if err != nil {
base.Fatal(err)
}
if len(pkgs) == 0 {
base.Fatalf("go: internal error: no packages loaded for %s", pkgVers)
}
if len(pkgs) > 1 {
base.Fatalf("go: internal error: pattern %s matches multiple packages", pkgVers)
}
p := pkgs[0]
p.Internal.OmitDebug = true
p.Internal.ExeName = p.DefaultExecName()
load.CheckPackageErrors([]*load.Package{p})
a := b.LinkAction(loader, work.ModeBuild, work.ModeBuild, p)
a.CacheExecutable = true
b.Do(ctx, a)
return a.BuiltTarget()
}
func doPkgsite(ctx context.Context, urlPath, fragment 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)
}
if fragment != "" {
path += "#" + fragment
}
if file := os.Getenv("TEST_GODOC_URL_FILE"); file != "" {
return os.WriteFile(file, []byte(path+"\n"), 0666)
}
// 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.
base.StartSigHandlers()
// 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)
}
pkgsite := buildPkgsite(ctx)
cmd := exec.Command(pkgsite, "-gorepo", cfg.GOROOT, "-http", addr, "-open", path)
cmd.Env = env
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if ee, ok := errors.AsType[*exec.ExitError](err); ok {
// 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
}