From 7370c24eb4989ca82f83009a0d36395bab4ea8c0 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 25 Feb 2026 17:45:51 +0000 Subject: [PATCH] tool/listpkgs: add --affected-by-tag For paring back build tag variant CI runs' set of packages to test. Updates tailscale/corp#28679 Change-Id: Iba46fd1f58c1eaee1f7888ef573bc8b14fa73208 Signed-off-by: Brad Fitzpatrick --- tool/listpkgs/listpkgs.go | 81 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/tool/listpkgs/listpkgs.go b/tool/listpkgs/listpkgs.go index e2c286efc..1c2dda257 100644 --- a/tool/listpkgs/listpkgs.go +++ b/tool/listpkgs/listpkgs.go @@ -26,6 +26,7 @@ var ( 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)") + affectedByTag = flag.String("affected-by-tag", "", "if non-empty, only list packages whose test binary would be affected by the presence or absence of this build tag") ) func main() { @@ -41,6 +42,10 @@ func main() { Mode: packages.LoadFiles, Env: os.Environ(), } + if *affectedByTag != "" { + cfg.Mode |= packages.NeedImports + cfg.Tests = true + } if *goos != "" { cfg.Env = append(cfg.Env, "GOOS="+*goos) } @@ -62,6 +67,11 @@ func main() { withAll = strings.Split(*withTagsAllStr, ",") } + var affected map[string]bool // PkgPath → true + if *affectedByTag != "" { + affected = computeAffected(pkgs, *affectedByTag) + } + seen := map[string]bool{} matches := 0 Pkg: @@ -69,6 +79,17 @@ Pkg: if pkg.PkgPath == "" { // malformed (shouldn’t happen) continue } + if affected != nil { + // Skip synthetic packages created by Tests: true: + // - for-test variants like "foo [foo.test]" (ID != PkgPath) + // - test binary packages like "foo.test" (PkgPath ends in ".test") + if pkg.ID != pkg.PkgPath || strings.HasSuffix(pkg.PkgPath, ".test") { + continue + } + if !affected[pkg.PkgPath] { + continue + } + } if seen[pkg.PkgPath] { continue // suppress duplicates when patterns overlap } @@ -96,7 +117,7 @@ Pkg: 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) + log.Fatalf("invalid shard format %q; expected ‘N/M’", *shard) } if m > 0 && (matches-1)%m != n-1 { continue // not in this shard @@ -112,6 +133,62 @@ Pkg: } } +// computeAffected returns the set of package paths whose test binaries would +// differ with vs without the given build tag. It finds packages that directly +// mention the tag, then propagates transitively via reverse dependencies. +func computeAffected(pkgs []*packages.Package, tag string) map[string]bool { + // Build a map from package ID to package for quick lookup. + byID := make(map[string]*packages.Package, len(pkgs)) + for _, pkg := range pkgs { + byID[pkg.ID] = pkg + } + + // First pass: find directly affected package IDs. + directlyAffected := make(map[string]bool) + for _, pkg := range pkgs { + if hasBuildTag(pkg, tag) { + directlyAffected[pkg.ID] = true + } + } + + // Build reverse dependency graph: importedID → []importingID. + reverseDeps := make(map[string][]string) + for _, pkg := range pkgs { + for _, imp := range pkg.Imports { + reverseDeps[imp.ID] = append(reverseDeps[imp.ID], pkg.ID) + } + } + + // BFS from directly affected packages through reverse deps. + affectedIDs := make(map[string]bool) + queue := make([]string, 0, len(directlyAffected)) + for id := range directlyAffected { + affectedIDs[id] = true + queue = append(queue, id) + } + for len(queue) > 0 { + id := queue[0] + queue = queue[1:] + for _, rdep := range reverseDeps[id] { + if !affectedIDs[rdep] { + affectedIDs[rdep] = true + queue = append(queue, rdep) + } + } + } + + // Map affected IDs back to PkgPaths. For-test variants like + // "foo [foo.test]" share the same PkgPath as "foo", so the + // result naturally deduplicates. + affected := make(map[string]bool) + for id := range affectedIDs { + if pkg, ok := byID[id]; ok { + affected[pkg.PkgPath] = true + } + } + return affected +} + func isThirdParty(pkg string) bool { return strings.HasPrefix(pkg, "tailscale.com/tempfork/") } @@ -194,7 +271,7 @@ func getFileTags(filename string) (tagSet, error) { mu.Lock() defer mu.Unlock() fileTags[filename] = ts - return tags, nil + return ts, nil } func fileMentionsTag(filename, tag string) (bool, error) {