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  	"errors"
    11  	"fmt"
    12  	"net"
    13  	"net/url"
    14  	"os"
    15  	"os/exec"
    16  	"os/signal"
    17  	"path/filepath"
    18  	"strings"
    19  )
    20  
    21  // pickUnusedPort finds an unused port by trying to listen on port 0
    22  // and letting the OS pick a port, then closing that connection and
    23  // returning that port number.
    24  // This is inherently racy.
    25  func pickUnusedPort() (int, error) {
    26  	l, err := net.Listen("tcp", "localhost:0")
    27  	if err != nil {
    28  		return 0, err
    29  	}
    30  	port := l.Addr().(*net.TCPAddr).Port
    31  	if err := l.Close(); err != nil {
    32  		return 0, err
    33  	}
    34  	return port, nil
    35  }
    36  
    37  func doPkgsite(urlPath, fragment string) error {
    38  	port, err := pickUnusedPort()
    39  	if err != nil {
    40  		return fmt.Errorf("failed to find port for documentation server: %v", err)
    41  	}
    42  	addr := fmt.Sprintf("localhost:%d", port)
    43  	path, err := url.JoinPath("http://"+addr, urlPath)
    44  	if err != nil {
    45  		return fmt.Errorf("internal error: failed to construct url: %v", err)
    46  	}
    47  	if fragment != "" {
    48  		path += "#" + fragment
    49  	}
    50  
    51  	// Turn off the default signal handler for SIGINT (and SIGQUIT on Unix)
    52  	// and instead wait for the child process to handle the signal and
    53  	// exit before exiting ourselves.
    54  	signal.Ignore(signalsToIgnore...)
    55  
    56  	// Prepend the local download cache to GOPROXY to get around deprecation checks.
    57  	env := os.Environ()
    58  	vars, err := runCmd(env, goCmd(), "env", "GOPROXY", "GOMODCACHE")
    59  	fields := strings.Fields(vars)
    60  	if err == nil && len(fields) == 2 {
    61  		goproxy, gomodcache := fields[0], fields[1]
    62  		gomodcache = filepath.Join(gomodcache, "cache", "download")
    63  		// Convert absolute path to file URL. pkgsite will not accept
    64  		// Windows absolute paths because they look like a host:path remote.
    65  		// TODO(golang.org/issue/32456): use url.FromFilePath when implemented.
    66  		if strings.HasPrefix(gomodcache, "/") {
    67  			gomodcache = "file://" + gomodcache
    68  		} else {
    69  			gomodcache = "file:///" + filepath.ToSlash(gomodcache)
    70  		}
    71  		env = append(env, "GOPROXY="+gomodcache+","+goproxy)
    72  	}
    73  
    74  	const version = "v0.0.0-20250714212547-01b046e81fe7"
    75  	cmd := exec.Command(goCmd(), "run", "golang.org/x/pkgsite/cmd/internal/doc@"+version,
    76  		"-gorepo", buildCtx.GOROOT,
    77  		"-http", addr,
    78  		"-open", path)
    79  	cmd.Env = env
    80  	cmd.Stdout = os.Stderr
    81  	cmd.Stderr = os.Stderr
    82  
    83  	if err := cmd.Run(); err != nil {
    84  		var ee *exec.ExitError
    85  		if errors.As(err, &ee) {
    86  			// Exit with the same exit status as pkgsite to avoid
    87  			// printing of "exit status" error messages.
    88  			// Any relevant messages have already been printed
    89  			// to stdout or stderr.
    90  			os.Exit(ee.ExitCode())
    91  		}
    92  		return err
    93  	}
    94  
    95  	return nil
    96  }
    97  

View as plain text