From 6fd66c7cdf2f87083dc3fe5c10026a63d6136166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=F0=9F=95=B4=EF=B8=8F?= Date: Tue, 16 Dec 2025 18:45:15 -0500 Subject: [PATCH] cmd/dist: detect import cycles instead of deadlocking Previously, if there was an import cycle in the packages being built (e.g., package A imports B and B imports A), the build would deadlock with "all goroutines are asleep - deadlock!" because each package's goroutine would block waiting for the other to complete. Now we detect import cycles before blocking on each dependency by tracking an "install stack" of which package is waiting on which. Before waiting for a dependency, we walk the stack to check if that dependency is already waiting on us. When a cycle is detected, we report it with a clear message like "import cycle: A -> B -> A". Fixes #76439 --- src/cmd/dist/build.go | 46 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/cmd/dist/build.go b/src/cmd/dist/build.go index e4250e12de8975..e917cb6efae37e 100644 --- a/src/cmd/dist/build.go +++ b/src/cmd/dist/build.go @@ -678,6 +678,10 @@ var gentab = []struct { var installed = make(map[string]chan struct{}) var installedMu sync.Mutex +// waitingOn tracks the package that each installing package is blocked on. +// This forms the "install stack" used to detect import cycles. +var waitingOn = make(map[string]string) + func install(dir string) { <-startInstall(dir) } @@ -883,14 +887,54 @@ func runInstall(pkg string, ch chan struct{}) { } sort.Strings(sortedImports) + // Start all dependency installations and collect the list of deps. + deps := make([]string, 0, len(importMap)) for _, dep := range importMap { if dep == "C" { fatalf("%s imports C", pkg) } + deps = append(deps, dep) startInstall(dep) } - for _, dep := range importMap { + + // Wait for all dependencies, checking for import cycles. + // Before blocking on each dep, we check if it's already waiting on us + // (directly or transitively), which would indicate an import cycle. + for _, dep := range deps { + // Check for direct self-import. + if dep == pkg { + fatalf("import cycle: %s -> %s", pkg, dep) + } + + installedMu.Lock() + // Check for cycle: walk the waitingOn chain from dep. + // If we reach pkg, there's a cycle. + for p := dep; ; { + next, ok := waitingOn[p] + if !ok { + break + } + if next == pkg { + // Cycle found. Build the cycle path. + cycle := []string{pkg, dep} + for p2 := dep; p2 != pkg; { + next2 := waitingOn[p2] + cycle = append(cycle, next2) + p2 = next2 + } + installedMu.Unlock() + fatalf("import cycle: %s", strings.Join(cycle, " -> ")) + } + p = next + } + waitingOn[pkg] = dep + installedMu.Unlock() + install(dep) + + installedMu.Lock() + delete(waitingOn, pkg) + installedMu.Unlock() } if goos != gohostos || goarch != gohostarch {