Skip to content

Commit 56c774b

Browse files
committed
CLJS-2055: Circular dependencies with parallel build cause the compiler get stuck
Add circular dep helper that works on compiler inputs, call before starting parallel build
1 parent 233b423 commit 56c774b

3 files changed

Lines changed: 69 additions & 30 deletions

File tree

src/main/clojure/cljs/closure.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,7 @@
947947
(recur ns-info)))))))
948948

949949
(defn parallel-compile-sources [inputs compiler-stats opts]
950+
(module-graph/validate-inputs inputs)
950951
(let [deque (LinkedBlockingDeque. inputs)
951952
input-set (atom (into #{} (comp (remove nil?) (map :ns)) inputs))
952953
cnt (+ 2 (.. Runtime getRuntime availableProcessors))

src/main/clojure/cljs/module_graph.cljc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,28 @@
111111
provides))
112112
{} inputs))
113113

114+
(defn validate-inputs*
115+
[indexed path seen]
116+
(let [ns (peek path)
117+
{:keys [requires]} (get indexed ns)]
118+
(doseq [ns' requires]
119+
(if (contains? seen ns')
120+
(throw
121+
(ex-info
122+
(str "Circular dependency detected "
123+
(apply str (interpose " -> " (conj path ns'))))
124+
{:cljs.closure/error :invalid-inputs}))
125+
(validate-inputs* indexed (conj path ns') (conj seen ns'))))))
126+
127+
(defn validate-inputs
128+
"Throws on the presence of circular dependencies"
129+
([inputs]
130+
(validate-inputs inputs [] #{}))
131+
([inputs path seen]
132+
(let [indexed (index-inputs inputs)]
133+
(doseq [[ns] (seq indexed)]
134+
(validate-inputs* indexed (conj path ns) (conj seen ns))))))
135+
114136
(defn ^:dynamic deps-for
115137
"Return all dependencies for x in a graph using deps-key."
116138
[x graph deps-key]

src/test/clojure/cljs/module_graph_tests.clj

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
(:require [clojure.test :as test :refer [deftest is testing]]
1111
[cljs.closure :as closure]
1212
[cljs.util :as util]
13-
[cljs.module-graph :as module-graph]))
13+
[cljs.module-graph :as module-graph])
14+
(:import [clojure.lang ExceptionInfo]))
1415

1516
(def opts {:output-dir "out"})
1617

@@ -24,35 +25,38 @@
2425
:depends-on [:shared]
2526
:output-to (str output-dir "/page2.js")}})
2627

27-
(defn inputs [{:keys [output-dir] :as opts}]
28-
[{:provides '[goog]
29-
:out-file (str output-dir "/goog/base.js")}
30-
{:provides '[cljs.core]
31-
:out-file (str output-dir "/cljs/core.js")}
32-
{:provides ["cljs.reader"]
33-
:requires ["cljs.core"]
34-
:out-file (str output-dir "/cljs/reader.js")}
35-
{:provides '[events "event.types"]
36-
:requires ["cljs.core"]
37-
:out-file (str output-dir "/events.js")}
38-
{:provides '[shared.a]
39-
:requires ["cljs.core"]
40-
:out-file (str output-dir "/shared/a.js")}
41-
{:provides '[shared.b]
42-
:requires '[cljs.core]
43-
:out-file (str output-dir "/shared/b.js")}
44-
{:provides ["page1.a"]
45-
:requires ["cljs.core" "cljs.reader" "events" 'shared.a]
46-
:out-file (str output-dir "/page1/a.js")}
47-
{:provides ["page1.b"]
48-
:requires '[cljs.core shared.b]
49-
:out-file (str output-dir "/page1/b.js")}
50-
{:provides ["page2.a"]
51-
:requires ["cljs.core" "events" 'shared.a]
52-
:out-file (str output-dir "/page2/a.js")}
53-
{:provides ["page2.b"]
54-
:requires ['cljs.core 'shared.b]
55-
:out-file (str output-dir "/page2/b.js")}])
28+
(defn inputs
29+
([]
30+
(inputs {:output-dir "out"}))
31+
([{:keys [output-dir] :as opts}]
32+
[{:provides '[goog]
33+
:out-file (str output-dir "/goog/base.js")}
34+
{:provides '[cljs.core]
35+
:out-file (str output-dir "/cljs/core.js")}
36+
{:provides ["cljs.reader"]
37+
:requires ["cljs.core"]
38+
:out-file (str output-dir "/cljs/reader.js")}
39+
{:provides '[events "event.types"]
40+
:requires ["cljs.core"]
41+
:out-file (str output-dir "/events.js")}
42+
{:provides '[shared.a]
43+
:requires ["cljs.core"]
44+
:out-file (str output-dir "/shared/a.js")}
45+
{:provides '[shared.b]
46+
:requires '[cljs.core]
47+
:out-file (str output-dir "/shared/b.js")}
48+
{:provides ["page1.a"]
49+
:requires ["cljs.core" "cljs.reader" "events" 'shared.a]
50+
:out-file (str output-dir "/page1/a.js")}
51+
{:provides ["page1.b"]
52+
:requires '[cljs.core shared.b]
53+
:out-file (str output-dir "/page1/b.js")}
54+
{:provides ["page2.a"]
55+
:requires ["cljs.core" "events" 'shared.a]
56+
:out-file (str output-dir "/page2/a.js")}
57+
{:provides ["page2.b"]
58+
:requires ['cljs.core 'shared.b]
59+
:out-file (str output-dir "/page2/b.js")}]))
5660

5761
(deftest test-add-cljs-base
5862
(is (true? (contains? (module-graph/add-cljs-base (modules opts)) :cljs-base))))
@@ -151,3 +155,15 @@
151155
(deftest test-module-for
152156
(is (= :page1 (module-graph/module-for 'page1.a (modules opts))))
153157
(is (= :page1 (module-graph/module-for "page1.a" (modules opts)))))
158+
159+
(def circular-inputs
160+
[{:provides ["foo.core"]
161+
:requires ["bar.core"]}
162+
{:provides ["bar.core"]
163+
:requires ["baz.core"]}
164+
{:provides ["baz.core"]
165+
:requires ["foo.core"]}])
166+
167+
(deftest test-circular-deps
168+
(is (nil? (module-graph/validate-inputs (inputs))))
169+
(is (thrown? ExceptionInfo (module-graph/validate-inputs circular-inputs))))

0 commit comments

Comments
 (0)