Skip to content

Commit 6fd66c7

Browse files
committed
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
1 parent 65b71c1 commit 6fd66c7

File tree

1 file changed

+45
-1
lines changed

1 file changed

+45
-1
lines changed

src/cmd/dist/build.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,10 @@ var gentab = []struct {
678678
var installed = make(map[string]chan struct{})
679679
var installedMu sync.Mutex
680680

681+
// waitingOn tracks the package that each installing package is blocked on.
682+
// This forms the "install stack" used to detect import cycles.
683+
var waitingOn = make(map[string]string)
684+
681685
func install(dir string) {
682686
<-startInstall(dir)
683687
}
@@ -883,14 +887,54 @@ func runInstall(pkg string, ch chan struct{}) {
883887
}
884888
sort.Strings(sortedImports)
885889

890+
// Start all dependency installations and collect the list of deps.
891+
deps := make([]string, 0, len(importMap))
886892
for _, dep := range importMap {
887893
if dep == "C" {
888894
fatalf("%s imports C", pkg)
889895
}
896+
deps = append(deps, dep)
890897
startInstall(dep)
891898
}
892-
for _, dep := range importMap {
899+
900+
// Wait for all dependencies, checking for import cycles.
901+
// Before blocking on each dep, we check if it's already waiting on us
902+
// (directly or transitively), which would indicate an import cycle.
903+
for _, dep := range deps {
904+
// Check for direct self-import.
905+
if dep == pkg {
906+
fatalf("import cycle: %s -> %s", pkg, dep)
907+
}
908+
909+
installedMu.Lock()
910+
// Check for cycle: walk the waitingOn chain from dep.
911+
// If we reach pkg, there's a cycle.
912+
for p := dep; ; {
913+
next, ok := waitingOn[p]
914+
if !ok {
915+
break
916+
}
917+
if next == pkg {
918+
// Cycle found. Build the cycle path.
919+
cycle := []string{pkg, dep}
920+
for p2 := dep; p2 != pkg; {
921+
next2 := waitingOn[p2]
922+
cycle = append(cycle, next2)
923+
p2 = next2
924+
}
925+
installedMu.Unlock()
926+
fatalf("import cycle: %s", strings.Join(cycle, " -> "))
927+
}
928+
p = next
929+
}
930+
waitingOn[pkg] = dep
931+
installedMu.Unlock()
932+
893933
install(dep)
934+
935+
installedMu.Lock()
936+
delete(waitingOn, pkg)
937+
installedMu.Unlock()
894938
}
895939

896940
if goos != gohostos || goarch != gohostarch {

0 commit comments

Comments
 (0)