summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2026-02-25 17:45:51 +0000
committerBrad Fitzpatrick <brad@danga.com>2026-02-25 11:41:33 -0800
commit7370c24eb4989ca82f83009a0d36395bab4ea8c0 (patch)
tree3d92f02483fed471c88b17af8bf5b63805c0971f
parentfd2ebcd5bdf5a166513e7b86114dcbcb5d8c67e3 (diff)
downloadtailscale-7370c24eb4989ca82f83009a0d36395bab4ea8c0.tar.xz
tailscale-7370c24eb4989ca82f83009a0d36395bab4ea8c0.zip
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 <bradfitz@tailscale.com>
-rw-r--r--tool/listpkgs/listpkgs.go81
1 files 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) {