From 4a0a308cb94f33dba6e7dcda954ebe98b0e40f82 Mon Sep 17 00:00:00 2001 From: Robin Bate Boerop Date: Thu, 23 Apr 2026 13:39:07 -0700 Subject: [PATCH 1/2] test: add single-module consumer unreferenced-dep regression guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a blackbox test in which a single-module consumer library has a dependency library whose module it does not reference. When the dependency's module is edited (no signature change), the consumer module must not be recompiled. On current main, the cmi is hash-stable under this edit, so the observed rebuild target count is zero; the test guards against regressions of that behaviour. The test also anchors the per-module-lib-deps surface for future tightening work — when tighter per-module filtering lands (https://github.com/ocaml/dune/issues/4572), this scenario remains at zero rebuilds but for a different reason (consumer has no declared dep on the module). See: https://github.com/ocaml/dune/pull/14116#issuecomment-4286949811 Signed-off-by: Robin Bate Boerop --- .../single-module-unreferenced-lib.t | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t b/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t new file mode 100644 index 00000000000..43a51146bdc --- /dev/null +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t @@ -0,0 +1,35 @@ +Per-module library filtering for a single-module consumer that does not +reference its dependency library. + +When a consumer library has one module and it does not reference any +module from a dependency library, changing modules in that dependency +must not trigger recompilation of the consumer. + +See: https://github.com/ocaml/dune/issues/4572 +See: https://github.com/ocaml/dune/pull/14116#issuecomment-4286949811 + + $ cat > dune-project < (lang dune 3.22) + > EOF + + $ cat > dune < (library (name libA) (wrapped false) (modules modA)) + > (library (name libB) (wrapped false) (modules modB) (libraries libA)) + > EOF + + $ cat > modA.ml < let x = 42 + > EOF + $ cat > modB.ml < let x = 12 + > EOF + + $ dune build @check + +Modify modA only; modB does not reference modA, so modB must not be +recompiled: + + $ echo "let x = 43" > modA.ml + $ dune build @check + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("modB"))] | length' + 0 From fa0984eb4dbccc87893049fc10daa781dca99475 Mon Sep 17 00:00:00 2001 From: Robin Bate Boerop Date: Thu, 23 Apr 2026 15:45:21 -0700 Subject: [PATCH 2/2] test: rewrite as observational baseline with signature-change edit The previous form used an implementation-only edit to [modA.ml] with no [.mli], leaving the cmi hash-stable and producing a trivially-true rebuild-count assertion of 0 that did not exercise any aspect of per-module library filtering. Rewrite with [modA.mli] so the edit forces a cmi hash change. The consumer [modB] still rebuilds on current main (count = 1) because [libB] is a single-module stanza: dune skips ocamldep for it and the consumer falls back to a glob over [libA]'s object directory. This form tests a distinct corner from [single-module-lib.t] (which covers single-module consumers that reference some but not all modules of a multi-module dep): the zero-reference case could be tightened to zero rebuilds by a future fix that drops the lib dep when ocamldep yields no references, without needing to solve the broader single-module-consumer skip-ocamldep limitation. Signed-off-by: Robin Bate Boerop --- .../single-module-unreferenced-lib.t | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t b/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t index 43a51146bdc..2745df4719a 100644 --- a/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t +++ b/test/blackbox-tests/test-cases/per-module-lib-deps/single-module-unreferenced-lib.t @@ -1,35 +1,67 @@ -Per-module library filtering for a single-module consumer that does not -reference its dependency library. +Baseline: single-module consumer library whose only module references +no module of its declared dependency library. -When a consumer library has one module and it does not reference any -module from a dependency library, changing modules in that dependency -must not trigger recompilation of the consumer. +This is an observational test. It records the number of rebuild +targets for the consumer's [spurious_rebuild] module when the +dependency library [dep_lib]'s only module has its interface +edited. + +[consumer_lib] declares [(libraries dep_lib)] but +[spurious_rebuild.ml] does not reference any module of [dep_lib]. +On current main this scenario still rebuilds [spurious_rebuild]: +[consumer_lib] is a single-module stanza, so dune skips ocamldep +as an optimisation and cannot discover that [spurious_rebuild] +references no module of [dep_lib]; the consumer falls back to a +glob over [dep_lib]'s object directory, which is invalidated by +the cmi change. + +The zero-reference case is a distinct corner from the single-module +consumer that references some (but not all) modules of its dep, +which [single-module-lib.t] already documents. A future fix that +detects "ocamldep yields no references to dep_lib" could tighten +this corner to zero rebuilds without needing to solve the broader +single-module-consumer skip-ocamldep limitation. See: https://github.com/ocaml/dune/issues/4572 See: https://github.com/ocaml/dune/pull/14116#issuecomment-4286949811 $ cat > dune-project < (lang dune 3.22) + > (lang dune 3.23) > EOF $ cat > dune < (library (name libA) (wrapped false) (modules modA)) - > (library (name libB) (wrapped false) (modules modB) (libraries libA)) + > (library (name dep_lib) (wrapped false) (modules dep_module)) + > (library + > (name consumer_lib) + > (wrapped false) + > (modules spurious_rebuild) + > (libraries dep_lib)) > EOF - $ cat > modA.ml < dep_module.ml < let x = 42 > EOF - $ cat > modB.ml < dep_module.mli < val x : int + > EOF + $ cat > spurious_rebuild.ml < let x = 12 > EOF $ dune build @check -Modify modA only; modB does not reference modA, so modB must not be -recompiled: +Edit [dep_module]'s interface. [spurious_rebuild] does not +reference [dep_module]. Record the number of [spurious_rebuild] +rebuild targets observed in the trace: - $ echo "let x = 43" > modA.ml + $ cat > dep_module.mli < val x : int + > val y : string + > EOF + $ cat > dep_module.ml < let x = 42 + > let y = "hello" + > EOF $ dune build @check - $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("modB"))] | length' - 0 + $ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("spurious_rebuild"))] | length' + 1