Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
Reproduction: today, building only the consumer's `.cmo` of an
executable that depends on `mylib` (where `mylib` has two modules,
`a` default-pp and `b` `(staged_pps ...)`) fails on a compile error
in `mylib/b.ml` — even though the consumer references only `A`.
The cctx-wide `.cmi` glob over `mylib`'s objdir pulls `b.cmi` into
the consumer's compile rule, which forces dune to compile `b.ml`,
and `b.ml` contains an unresolvable identifier.

Companion to `mixed-per-module-preprocess.t` (the soundness sibling).

$ make_dune_project 3.24

A no-op staged ppx (identical to the soundness reproducer).

$ mkdir ppx
$ cat > ppx/dune <<EOF
> (library
> (name ppx_noop)
> (kind ppx_rewriter)
> (ppx.driver (main Ppx_noop.main)))
> EOF
$ cat > ppx/ppx_noop.ml <<EOF
> let main () =
> let n = Array.length Sys.argv in
> if n < 4 || Sys.argv.(1) <> "--as-ppx" then assert false;
> let input = Sys.argv.(n - 2) in
> let output = Sys.argv.(n - 1) in
> Filename.quote_command "cp" [input; output]
> |> Sys.command
> |> exit
> EOF

`mylib`: `a` uses default preprocessing (Some-entry); `b` uses
`(staged_pps ...)` (None-entry). `a`'s source is independent of `b`;
`b.ml` contains an unresolvable identifier so any attempt to compile
it will fail.

$ mkdir mylib
$ cat > mylib/dune <<EOF
> (library
> (name mylib)
> (wrapped false)
> (preprocess (per_module ((staged_pps ppx_noop) b))))
> EOF
$ cat > mylib/a.ml <<EOF
> let answer = 42
> EOF
$ cat > mylib/b.ml <<EOF
> let bar = no_such_thing
> EOF

`consumer` references only `A`:

$ mkdir consumer
$ cat > consumer/dune <<EOF
> (executable (name consumer) (libraries mylib))
> EOF
$ cat > consumer/consumer.ml <<EOF
> let () = print_int A.answer
> EOF

The consumer's compile rule tracks the wide glob over `mylib`'s
byte objdir — which materialises `b.cmi` and so forces dune to
compile `b.ml`.

$ dune rules --root . --format=json --deps '%{cmo:consumer/consumer}' > deps.json
$ jq -r 'include "dune"; .[] | depsGlobs
> | select(.dir | endswith("mylib/.mylib.objs/byte"))
> | .dir + " " + .predicate' < deps.json
_build/default/mylib/.mylib.objs/byte *.cmi
$ jq -r 'include "dune"; .[] | depsFilePaths
> | select(endswith("mylib/.mylib.objs/byte/a.cmi"))' < deps.json
$ jq -r 'include "dune"; .[] | depsFilePaths
> | select(endswith("mylib/.mylib.objs/byte/b.cmi"))' < deps.json

Build only the consumer's `.cmo` (compile rule, not link). Today,
dune attempts to compile `b.ml` to produce `b.cmi` and fails on
the unresolvable identifier.

$ dune build '%{cmo:consumer/consumer}'
Comment thread
robinbb marked this conversation as resolved.
File "mylib/b.ml", line 1, characters 10-23:
1 | let bar = no_such_thing
^^^^^^^^^^^^^
Error: Unbound value no_such_thing
[1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
An unwrapped library has two modules with mixed preprocessing: `a`
uses default preprocessing, `b` uses `(staged_pps ...)`. The
consumer references `A.identity` whose type is `B.t -> B.t`. Pins
that the consumer's compile rule correctly tracks `b.cmi` as a sandbox-
required dep — even though the consumer never names `B` in source.

$ make_dune_project 3.24

A no-op staged ppx, modelled on
`test/blackbox-tests/test-cases/staged-pps-relative-directory-gh8158.t`.
The driver copies its input verbatim.

$ mkdir ppx
$ cat > ppx/dune <<EOF
> (library
> (name ppx_noop)
> (kind ppx_rewriter)
> (ppx.driver (main Ppx_noop.main)))
> EOF
$ cat > ppx/ppx_noop.ml <<EOF
> let main () =
> let n = Array.length Sys.argv in
> if n < 4 || Sys.argv.(1) <> "--as-ppx" then assert false;
> let input = Sys.argv.(n - 2) in
> let output = Sys.argv.(n - 1) in
> Filename.quote_command "cp" [input; output]
> |> Sys.command
> |> exit
> EOF

`mylib` is `(wrapped false)` with `a` default-pp and `b` staged-pps.
`a`'s interface mentions `B.t`, so the consumer's call to
`A.identity` forces the compiler to load `b.cmi` to resolve the
type.

$ mkdir mylib
$ cat > mylib/dune <<EOF
> (library
> (name mylib)
> (wrapped false)
> (preprocess (per_module ((staged_pps ppx_noop) b))))
> EOF
$ cat > mylib/a.mli <<EOF
> val identity : B.t -> B.t
> EOF
$ cat > mylib/a.ml <<EOF
> let identity (x : B.t) = x
> EOF
$ cat > mylib/b.ml <<EOF
> type t = int
> let zero : t = 0
> EOF

`consumer` references `A.identity` but never names `B`. The
`--sandbox=copy` build below is the discriminator: if a regression
dropped `b.cmi` from the consumer's compile-rule deps, the sandbox
would not stage it and the build would fail with "no such file"
deterministically rather than passing silently from a stale
`_build/`.

$ mkdir consumer
$ cat > consumer/dune <<EOF
> (executable (name consumer) (libraries mylib))
> EOF
$ cat > consumer/consumer.ml <<EOF
> let _ = A.identity 0
> EOF

$ dune build --sandbox=copy consumer/consumer.exe

The consumer's compile rule for `consumer` tracks `mylib`'s byte
objdir — both `a.cmi` (referenced) and `b.cmi` (needed for the
type of `A.identity`).

$ dune rules --root . --format=json --deps '%{cmo:consumer/consumer}' > deps.json
$ jq -r 'include "dune"; .[] | depsGlobs
> | select(.dir | endswith("mylib/.mylib.objs/byte"))
> | .dir + " " + .predicate' < deps.json
_build/default/mylib/.mylib.objs/byte *.cmi
Loading