blob: 659fcdcf5c6063345ed6dfc7a1af59e412ddfe71 [file] [log] [blame]
// Copyright 2021 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.
// Pkgsite extracts and generates documentation for Go programs.
// It runs as a web server and presents the documentation as a
// web page.
//
// To install, run `go install ./cmd/pkgsite` from the pkgsite repo root.
//
// With no arguments, pkgsite will serve docs for the module in the current
// directory, which must have a go.mod file:
//
// cd ~/repos/cue && pkgsite
//
// This form will also serve all of the module's required modules at their
// required versions. You can disable serving the required modules by passing
// -list=false.
//
// You can also serve docs from your module cache, directly from the proxy
// (it uses the GOPROXY environment variable), or both:
//
// pkgsite -cache -proxy
//
// With either -cache or -proxy, pkgsite won't look for a module in the current
// directory. You can still provide modules on the local filesystem by listing
// their paths:
//
// pkgsite -cache -proxy ~/repos/cue some/other/module
//
// Although standard library packages will work by default, the docs can take a
// while to appear the first time because the Go repo must be cloned and
// processed. If you clone the repo yourself (https://go.googlesource.com/go),
// you can provide its location with the -gorepo flag to save a little time.
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/google/safehtml/template"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/fetch"
"golang.org/x/pkgsite/internal/fetchdatasource"
"golang.org/x/pkgsite/internal/frontend"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware"
"golang.org/x/pkgsite/internal/proxy"
"golang.org/x/pkgsite/internal/source"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/static"
thirdparty "golang.org/x/pkgsite/third_party"
)
const defaultAddr = "localhost:8080" // default webserver address
var (
gopathMode = flag.Bool("gopath_mode", false, "assume that local modules' paths are relative to GOPATH/src")
httpAddr = flag.String("http", defaultAddr, "HTTP service address to listen for incoming requests on")
useCache = flag.Bool("cache", false, "fetch from the module cache")
cacheDir = flag.String("cachedir", "", "module cache directory (defaults to `go env GOMODCACHE`)")
useProxy = flag.Bool("proxy", false, "fetch from GOPROXY if not found locally")
goRepoPath = flag.String("gorepo", "", "path to Go repo on local filesystem")
useListedMods = flag.Bool("list", true, "for each path, serve all modules in build list")
)
func main() {
flag.Usage = func() {
out := flag.CommandLine.Output()
fmt.Fprintf(out, "usage: %s [flags] [PATHS ...]\n", os.Args[0])
fmt.Fprintf(out, " where each PATHS is a single path or a comma-separated list\n")
fmt.Fprintf(out, " (default is current directory if neither -cache nor -proxy is provided)\n")
flag.PrintDefaults()
}
flag.Parse()
ctx := context.Background()
paths := collectPaths(flag.Args())
if len(paths) == 0 && !*useCache && !*useProxy {
paths = []string{"."}
}
var modCacheDir string
if *useCache || *useListedMods {
modCacheDir = *cacheDir
if modCacheDir == "" {
var err error
modCacheDir, err = defaultCacheDir()
if err != nil {
die("%v", err)
}
if modCacheDir == "" {
die("empty value for GOMODCACHE")
}
}
}
if *useCache || *useProxy {
fmt.Fprintf(os.Stderr, "BYPASSING LICENSE CHECKING: MAY DISPLAY NON-REDISTRIBUTABLE INFORMATION\n")
}
var prox *proxy.Client
if *useProxy {
url := os.Getenv("GOPROXY")
if url == "" {
die("GOPROXY environment variable is not set")
}
var err error
prox, err = proxy.New(url)
if err != nil {
die("connecting to proxy: %s", err)
}
}
if *goRepoPath != "" {
stdlib.SetGoRepoPath(*goRepoPath)
}
var cacheMods []internal.Modver
if *useListedMods && !*useCache {
var err error
paths, cacheMods, err = listModsForPaths(paths, modCacheDir)
if err != nil {
die("listing mods (consider passing -list=false): %v", err)
}
}
getters, err := buildGetters(ctx, paths, *gopathMode, modCacheDir, cacheMods, prox)
if err != nil {
die("%s", err)
}
server, err := newServer(getters, prox)
if err != nil {
die("%s", err)
}
router := http.NewServeMux()
server.Install(router.Handle, nil, nil)
mw := middleware.Timeout(54 * time.Second)
log.Infof(ctx, "Listening on addr http://%s", *httpAddr)
die("%v", http.ListenAndServe(*httpAddr, mw(router)))
}
func collectPaths(args []string) []string {
var paths []string
for _, arg := range args {
paths = append(paths, strings.Split(arg, ",")...)
}
return paths
}
func buildGetters(ctx context.Context, paths []string, gopathMode bool, downloadDir string, cacheMods []internal.Modver, prox *proxy.Client) ([]fetch.ModuleGetter, error) {
getters := buildPathGetters(ctx, paths, gopathMode)
if downloadDir != "" {
g, err := fetch.NewFSProxyModuleGetter(downloadDir, cacheMods)
if err != nil {
return nil, err
}
getters = append(getters, g)
}
if prox != nil {
getters = append(getters, fetch.NewProxyModuleGetter(prox, source.NewClient(time.Second)))
}
return getters, nil
}
func buildPathGetters(ctx context.Context, paths []string, gopathMode bool) []fetch.ModuleGetter {
var getters []fetch.ModuleGetter
loaded := len(paths)
for _, path := range paths {
var (
mg fetch.ModuleGetter
err error
)
if gopathMode {
mg, err = fetchdatasource.NewGOPATHModuleGetter(path)
} else {
mg, err = fetch.NewDirectoryModuleGetter("", path)
}
if err != nil {
log.Error(ctx, err)
loaded--
} else {
getters = append(getters, mg)
}
}
if loaded == 0 && len(paths) > 0 {
die("failed to load module(s) at %v", paths)
}
return getters
}
func newServer(getters []fetch.ModuleGetter, prox *proxy.Client) (*frontend.Server, error) {
lds := fetchdatasource.Options{
Getters: getters,
ProxyClientForLatest: prox,
BypassLicenseCheck: true,
}.New()
server, err := frontend.NewServer(frontend.ServerConfig{
DataSourceGetter: func(context.Context) internal.DataSource { return lds },
TemplateFS: template.TrustedFSFromEmbed(static.FS),
StaticFS: static.FS,
ThirdPartyFS: thirdparty.FS,
})
if err != nil {
return nil, err
}
for _, g := range getters {
p, fsys := g.SourceFS()
if p != "" {
server.InstallFS(p, fsys)
}
}
return server, nil
}
func defaultCacheDir() (string, error) {
out, err := runGo("", "env", "GOMODCACHE")
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// listedMod has a subset of the fields written by `go list -m`.
type listedMod struct {
internal.Modver
GoMod string // absolute path to go.mod file; in download cache or replaced
Indirect bool
}
var listModules = _listModules
func _listModules(dir string) ([]listedMod, error) {
out, err := runGo(dir, "list", "-json", "-m", "all")
if err != nil {
return nil, err
}
d := json.NewDecoder(bytes.NewReader(out))
var ms []listedMod
for d.More() {
var m listedMod
if err := d.Decode(&m); err != nil {
return nil, err
}
ms = append(ms, m)
}
return ms, nil
}
func runGo(dir string, args ...string) ([]byte, error) {
cmd := exec.Command("go", args...)
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("running go with %q: %v: %s", args, err, out)
}
return out, nil
}
func listModsForPaths(paths []string, cacheDir string) ([]string, []internal.Modver, error) {
var outPaths []string
var cacheMods []internal.Modver
for _, p := range paths {
lms, err := listModules(p)
if err != nil {
return nil, nil, err
}
for _, lm := range lms {
// Ignore indirect modules.
if lm.Indirect {
continue
}
if lm.GoMod == "" {
return nil, nil, errors.New("empty GoMod: please file a pkgsite bug at https://go.dev/issues/new")
}
if strings.HasPrefix(lm.GoMod, cacheDir) {
cacheMods = append(cacheMods, lm.Modver)
} else { // probably the result of a replace directive
outPaths = append(outPaths, filepath.Dir(lm.GoMod))
}
}
}
return outPaths, cacheMods, nil
}
func die(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
os.Exit(1)
}