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>main
parent
42f71e959d
commit
d37e8d0bfa
@ -0,0 +1,206 @@ |
||||
// 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 |
||||
} |
||||
Loading…
Reference in new issue