Source file src/cmd/go/internal/doc/pkgsite.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build !cmd_go_bootstrap
     6  
     7  package doc
     8  
     9  import (
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"net"
    14  	"net/url"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"strings"
    19  
    20  	"cmd/go/internal/base"
    21  	"cmd/go/internal/cfg"
    22  	"cmd/go/internal/load"
    23  	"cmd/go/internal/modload"
    24  	"cmd/go/internal/work"
    25  )
    26  
    27  // pkgsiteCmdInternalDocVersion controls the version of the golang.org/x/pkgsite/cmd/internal/doc
    28  // module to build in buildPkgsite.
    29  //
    30  // This version is maintained, like other golang.org/x dependencies, via the go.dev/issue/36905 process.
    31  //
    32  // To make that process easier, the exact name and file location of this constant are known
    33  // to the [golang.org/x/build/cmd/updatestd] command. If this constant needs to be renamed
    34  // or moved to another .go file, update that code too.
    35  const pkgsiteCmdInternalDocVersion = "v0.0.0-20260722200430-7c7eca789259"
    36  
    37  // pickUnusedPort finds an unused port by trying to listen on port 0
    38  // and letting the OS pick a port, then closing that connection and
    39  // returning that port number.
    40  // This is inherently racy.
    41  func pickUnusedPort() (int, error) {
    42  	l, err := net.Listen("tcp", "localhost:0")
    43  	if err != nil {
    44  		return 0, err
    45  	}
    46  	port := l.Addr().(*net.TCPAddr).Port
    47  	if err := l.Close(); err != nil {
    48  		return 0, err
    49  	}
    50  	return port, nil
    51  }
    52  
    53  // buildPkgsite builds a pkgsite binary whose build may be cached.
    54  func buildPkgsite(ctx context.Context) string {
    55  	load.ClearPackageCache()
    56  	loader := modload.NewLoader()
    57  
    58  	// Set the builder to have no module root so we can build a pkg@version pattern.
    59  	loader.ForceUseModules = true
    60  	loader.RootMode = modload.NoRoot
    61  	loader.AllowMissingModuleImports()
    62  	modload.Init(loader)
    63  
    64  	work.BuildInit(loader)
    65  	b := work.NewBuilder("", loader.VendorDirOrEmpty)
    66  	defer func() {
    67  		if err := b.Close(); err != nil {
    68  			base.Fatal(err)
    69  		}
    70  	}()
    71  
    72  	version := pkgsiteCmdInternalDocVersion
    73  	if os.Getenv("TEST_GODOC_BUILD_ONLY") != "" {
    74  		version = "v0.1.0"
    75  	}
    76  	pkgVers := "golang.org/x/pkgsite/cmd/internal/doc@" + version
    77  	pkgOpts := load.PackageOpts{MainOnly: true}
    78  	pkgs, err := load.PackagesAndErrorsOutsideModule(loader, ctx, pkgOpts, []string{pkgVers})
    79  	if err != nil {
    80  		base.Fatal(err)
    81  	}
    82  	if len(pkgs) == 0 {
    83  		base.Fatalf("go: internal error: no packages loaded for %s", pkgVers)
    84  	}
    85  	if len(pkgs) > 1 {
    86  		base.Fatalf("go: internal error: pattern %s matches multiple packages", pkgVers)
    87  	}
    88  	p := pkgs[0]
    89  	p.Internal.OmitDebug = true
    90  	p.Internal.ExeName = p.DefaultExecName()
    91  	load.CheckPackageErrors([]*load.Package{p})
    92  
    93  	a := b.LinkAction(loader, work.ModeBuild, work.ModeBuild, p)
    94  	a.CacheExecutable = true
    95  	b.Do(ctx, a)
    96  
    97  	// Both paths return an executable in GOCACHE: CachedExecutable is set on
    98  	// fresh builds, while BuiltTarget is set on cache hits.
    99  	if cached := a.CachedExecutable(); cached != "" {
   100  		return cached
   101  	}
   102  	return a.BuiltTarget()
   103  }
   104  
   105  func doPkgsite(ctx context.Context, urlPath, fragment string) error {
   106  	port, err := pickUnusedPort()
   107  	if err != nil {
   108  		return fmt.Errorf("failed to find port for documentation server: %v", err)
   109  	}
   110  	addr := fmt.Sprintf("localhost:%d", port)
   111  	path, err := url.JoinPath("http://"+addr, urlPath)
   112  	if err != nil {
   113  		return fmt.Errorf("internal error: failed to construct url: %v", err)
   114  	}
   115  	if fragment != "" {
   116  		path += "#" + fragment
   117  	}
   118  
   119  	if file := os.Getenv("TEST_GODOC_URL_FILE"); file != "" {
   120  		return os.WriteFile(file, []byte(path+"\n"), 0666)
   121  	}
   122  
   123  	// Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
   124  	// and instead wait for the child process to handle the signal and
   125  	// exit before exiting ourselves.
   126  	base.StartSigHandlers()
   127  
   128  	// Prepend the local download cache to GOPROXY to get around deprecation checks.
   129  	env := os.Environ()
   130  	vars, err := runCmd(env, goCmd(), "env", "GOPROXY", "GOMODCACHE")
   131  	fields := strings.Fields(vars)
   132  	if err == nil && len(fields) == 2 {
   133  		goproxy, gomodcache := fields[0], fields[1]
   134  		gomodcache = filepath.Join(gomodcache, "cache", "download")
   135  		// Convert absolute path to file URL. pkgsite will not accept
   136  		// Windows absolute paths because they look like a host:path remote.
   137  		// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
   138  		if strings.HasPrefix(gomodcache, "/") {
   139  			gomodcache = "file://" + gomodcache
   140  		} else {
   141  			gomodcache = "file:///" + filepath.ToSlash(gomodcache)
   142  		}
   143  		env = append(env, "GOPROXY="+gomodcache+","+goproxy)
   144  	}
   145  
   146  	pkgsite := buildPkgsite(ctx)
   147  	if os.Getenv("TEST_GODOC_BUILD_ONLY") != "" {
   148  		if _, err := os.Stat(pkgsite); err != nil {
   149  			return fmt.Errorf("built pkgsite binary does not exist: %w", err)
   150  		}
   151  		return nil
   152  	}
   153  	cmd := exec.Command(pkgsite, "-gorepo", cfg.GOROOT, "-http", addr, "-open", path)
   154  	cmd.Env = env
   155  	cmd.Stdout = os.Stderr
   156  	cmd.Stderr = os.Stderr
   157  
   158  	if err := cmd.Run(); err != nil {
   159  		if ee, ok := errors.AsType[*exec.ExitError](err); ok {
   160  			// Exit with the same exit status as pkgsite to avoid
   161  			// printing of "exit status" error messages.
   162  			// Any relevant messages have already been printed
   163  			// to stdout or stderr.
   164  			os.Exit(ee.ExitCode())
   165  		}
   166  		return err
   167  	}
   168  
   169  	return nil
   170  }
   171  

View as plain text