mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-29 23:33:45 +00:00

Make the OS-specific staticcheck jobs only test stuff that's specialized for that OS. Do that using a new ./tool/listpkgs program that's a fancy 'go list' with more filtering flags. Updates tailscale/corp#28679 Change-Id: I790be2e3a0b42b105bd39f68c4b20e217a26de60 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
207 lines
5.1 KiB
Go
207 lines
5.1 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
// listpkgs prints the import paths that match the Go package patterns
|
||
// given on the command line and conditionally filters them in various ways.
|
||
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"flag"
|
||
"fmt"
|
||
"go/build/constraint"
|
||
"log"
|
||
"os"
|
||
"slices"
|
||
"strings"
|
||
"sync"
|
||
|
||
"golang.org/x/tools/go/packages"
|
||
)
|
||
|
||
var (
|
||
ignore3p = flag.Bool("ignore-3p", false, "ignore third-party packages forked/vendored into Tailscale")
|
||
goos = flag.String("goos", "", "GOOS to use for loading packages (default: current OS)")
|
||
goarch = flag.String("goarch", "", "GOARCH to use for loading packages (default: current architecture)")
|
||
withTagsAllStr = flag.String("with-tags-all", "", "if non-empty, a comma-separated list of builds tags to require (a package will only be listed if it contains all of these build tags)")
|
||
withoutTagsAnyStr = flag.String("without-tags-any", "", "if non-empty, a comma-separated list of build constraints to exclude (a package will be omitted if it contains any of these build tags)")
|
||
shard = flag.String("shard", "", "if non-empty, a string of the form 'N/M' to only print packages in shard N of M (e.g. '1/3', '2/3', '3/3/' for different thirds of the list)")
|
||
)
|
||
|
||
func main() {
|
||
flag.Parse()
|
||
|
||
patterns := flag.Args()
|
||
if len(patterns) == 0 {
|
||
flag.Usage()
|
||
os.Exit(1)
|
||
}
|
||
|
||
cfg := &packages.Config{
|
||
Mode: packages.LoadFiles,
|
||
Env: os.Environ(),
|
||
}
|
||
if *goos != "" {
|
||
cfg.Env = append(cfg.Env, "GOOS="+*goos)
|
||
}
|
||
if *goarch != "" {
|
||
cfg.Env = append(cfg.Env, "GOARCH="+*goarch)
|
||
}
|
||
|
||
pkgs, err := packages.Load(cfg, patterns...)
|
||
if err != nil {
|
||
log.Fatalf("loading packages: %v", err)
|
||
}
|
||
|
||
var withoutAny []string
|
||
if *withoutTagsAnyStr != "" {
|
||
withoutAny = strings.Split(*withoutTagsAnyStr, ",")
|
||
}
|
||
var withAll []string
|
||
if *withTagsAllStr != "" {
|
||
withAll = strings.Split(*withTagsAllStr, ",")
|
||
}
|
||
|
||
seen := map[string]bool{}
|
||
matches := 0
|
||
Pkg:
|
||
for _, pkg := range pkgs {
|
||
if pkg.PkgPath == "" { // malformed (shouldn’t happen)
|
||
continue
|
||
}
|
||
if seen[pkg.PkgPath] {
|
||
continue // suppress duplicates when patterns overlap
|
||
}
|
||
seen[pkg.PkgPath] = true
|
||
|
||
pkgPath := pkg.PkgPath
|
||
|
||
if *ignore3p && isThirdParty(pkgPath) {
|
||
continue
|
||
}
|
||
if withAll != nil {
|
||
for _, t := range withAll {
|
||
if !hasBuildTag(pkg, t) {
|
||
continue Pkg
|
||
}
|
||
}
|
||
}
|
||
for _, t := range withoutAny {
|
||
if hasBuildTag(pkg, t) {
|
||
continue Pkg
|
||
}
|
||
}
|
||
matches++
|
||
|
||
if *shard != "" {
|
||
var n, m int
|
||
if _, err := fmt.Sscanf(*shard, "%d/%d", &n, &m); err != nil || n < 1 || m < 1 {
|
||
log.Fatalf("invalid shard format %q; expected 'N/M'", *shard)
|
||
}
|
||
if m > 0 && (matches-1)%m != n-1 {
|
||
continue // not in this shard
|
||
}
|
||
}
|
||
fmt.Println(pkgPath)
|
||
}
|
||
|
||
// If any package had errors (e.g. missing deps) report them via packages.PrintErrors.
|
||
// This mirrors `go list` behaviour when -e is *not* supplied.
|
||
if packages.PrintErrors(pkgs) > 0 {
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func isThirdParty(pkg string) bool {
|
||
return strings.HasPrefix(pkg, "tailscale.com/tempfork/")
|
||
}
|
||
|
||
// hasBuildTag reports whether any source file in pkg mentions `tag`
|
||
// in a //go:build constraint.
|
||
func hasBuildTag(pkg *packages.Package, tag string) bool {
|
||
all := slices.Concat(pkg.CompiledGoFiles, pkg.OtherFiles, pkg.IgnoredFiles)
|
||
suffix := "_" + tag + ".go"
|
||
for _, name := range all {
|
||
if strings.HasSuffix(name, suffix) {
|
||
return true
|
||
}
|
||
ok, err := fileMentionsTag(name, tag)
|
||
if err != nil {
|
||
log.Printf("reading %s: %v", name, err)
|
||
continue
|
||
}
|
||
if ok {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// tagSet is a set of build tags.
|
||
// The values are always true. We avoid non-std set types
|
||
// to make this faster to "go run" on empty caches.
|
||
type tagSet map[string]bool
|
||
|
||
var (
|
||
mu sync.Mutex
|
||
fileTags = map[string]tagSet{} // abs path -> set of build tags mentioned in file
|
||
)
|
||
|
||
func getFileTags(filename string) (tagSet, error) {
|
||
mu.Lock()
|
||
tags, ok := fileTags[filename]
|
||
mu.Unlock()
|
||
if ok {
|
||
return tags, nil
|
||
}
|
||
|
||
f, err := os.Open(filename)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer f.Close()
|
||
|
||
ts := make(tagSet)
|
||
s := bufio.NewScanner(f)
|
||
for s.Scan() {
|
||
line := s.Text()
|
||
if strings.TrimSpace(line) == "" {
|
||
continue // still in leading blank lines
|
||
}
|
||
if !strings.HasPrefix(line, "//") {
|
||
// hit real code – done with header comments
|
||
// TODO(bradfitz): care about /* */ comments?
|
||
break
|
||
}
|
||
if !strings.HasPrefix(line, "//go:build") {
|
||
continue // some other comment
|
||
}
|
||
expr, err := constraint.Parse(line)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parsing %q: %w", line, err)
|
||
}
|
||
// Call Eval to populate ts with the tags mentioned in the expression.
|
||
// We don't care about the result, just the side effect of populating ts.
|
||
expr.Eval(func(tag string) bool {
|
||
ts[tag] = true
|
||
return true // arbitrary
|
||
})
|
||
}
|
||
if err := s.Err(); err != nil {
|
||
return nil, fmt.Errorf("reading %s: %w", filename, err)
|
||
}
|
||
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
fileTags[filename] = ts
|
||
return tags, nil
|
||
}
|
||
|
||
func fileMentionsTag(filename, tag string) (bool, error) {
|
||
tags, err := getFileTags(filename)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return tags[tag], nil
|
||
}
|