Skip to content

Commit 64482b2

Browse files
authored
Add a ppx pform to simplify writing tests (#12711)
The pform %{ppx:lib1+lib2+..+libn} is substituted to the path of the ppx binary that has all the ppx's lib1,lib2,...,libn linked in. This pform automatically adds a dependnecy on this executable. Signed-off-by: Rudi Grinberg <[email protected]>
1 parent 7e9639b commit 64482b2

File tree

9 files changed

+211
-1
lines changed

9 files changed

+211
-1
lines changed

doc/changes/added/12711.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- Introduce a `%{ppx:lib1+..+libn}` stanza to make it possible to refer to ppx
2+
executables built by dune. This is useful for writing tests (#12711,
3+
@rgrinberg)

doc/concepts/variables.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ In addition, ``(action ...)`` fields support the following special variables:
131131
file.
132132
- ``read-strings:<path>`` expands to the list of lines in the given
133133
file, unescaped using OCaml lexical convention.
134+
- ``ppx:lib1+..+libn`` expands to the ppx executable with ppx libraries
135+
``lib1`` to ``libn`` linked in. This form also introduces a dependency on
136+
this executable.
134137

135138
The ``%{<kind>:...}`` forms are what allows you to write custom rules that work
136139
transparently, whether things are installed or not.

src/dune_lang/pform.ml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ module Macro = struct
300300
| Artifact of Artifact.t
301301
| Pkg
302302
| Pkg_self
303+
| Ppx
303304

304305
let compare x y =
305306
match x, y with
@@ -357,6 +358,9 @@ module Macro = struct
357358
| Pkg_self, Pkg_self -> Eq
358359
| Pkg_self, _ -> Lt
359360
| _, Pkg_self -> Gt
361+
| Ppx, Ppx -> Eq
362+
| Ppx, _ -> Lt
363+
| _, Ppx -> Gt
360364
| Artifact x, Artifact y -> Artifact.compare x y
361365
;;
362366

@@ -384,6 +388,7 @@ module Macro = struct
384388
| Artifact ext -> variant "Artifact" [ Artifact.to_dyn ext ]
385389
| Pkg -> variant "Pkg" []
386390
| Pkg_self -> variant "Pkg_self" []
391+
| Ppx -> string "Ppx"
387392
;;
388393

389394
let encode = function
@@ -407,6 +412,7 @@ module Macro = struct
407412
| Env -> Ok "env"
408413
| Pkg -> Ok "pkg"
409414
| Pkg_self -> Ok "pkg-self"
415+
| Ppx -> Ok "ppx"
410416
| Artifact a -> Ok (String.drop (Artifact.ext a) 1)
411417
;;
412418
end
@@ -672,6 +678,7 @@ module Env = struct
672678
; "path-no-dep", deleted_in ~version:(1, 0) Macro.Path_no_dep
673679
; "ocaml-config", macro Ocaml_config
674680
; "env", since ~version:(1, 4) Macro.Env
681+
; "ppx", since ~version:(3, 21) Macro.Ppx
675682
; "coq", macro Coq_config
676683
]
677684
@ List.map ~f:artifact Artifact.all)

src/dune_lang/pform.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ module Macro : sig
141141
| Artifact of Artifact.t
142142
| Pkg
143143
| Pkg_self
144+
| Ppx
144145

145146
val compare : t -> t -> Ordering.t
146147
val to_dyn : t -> Dyn.t

src/dune_rules/expander.ml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,23 @@ let expand_pform_macro
707707
(let open Memo.O in
708708
let* artifacts_host = t.artifacts_host in
709709
Coq_config.expand source macro_invocation artifacts_host))
710+
| Ppx ->
711+
Need_full_expander
712+
(fun t ->
713+
With
714+
(let open Action_builder.O in
715+
let+ exe =
716+
let* scope = Action_builder.of_memo t.scope in
717+
Pform.Macro_invocation.Args.whole macro_invocation
718+
|> String.split ~on:'+'
719+
|> List.map ~f:(fun name ->
720+
let loc = Dune_lang.Template.Pform.loc source in
721+
let name = Lib_name.parse_string_exn (loc, name) in
722+
loc, name)
723+
|> Ppx_exe.get_ppx_exe context ~scope
724+
|> Resolve.Memo.read
725+
in
726+
[ Value.Path (Path.build exe) ]))
710727
;;
711728

712729
let expand_pform_gen ~(context : Context.t) ~bindings ~dir ~source (pform : Pform.t)

src/dune_rules/ppx_exe.ml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
open Import
2+
open Memo.O
3+
4+
(* Encoded representation of a set of library names + scope *)
5+
module Key : sig
6+
type encoded = Digest.t
7+
8+
module Decoded : sig
9+
type t = private
10+
{ pps : Lib_name.t list
11+
; project_root : Path.Source.t option
12+
}
13+
14+
val of_libs : Lib.t list -> t
15+
end
16+
17+
val encode : Decoded.t -> encoded
18+
end = struct
19+
type encoded = Digest.t
20+
21+
module Decoded = struct
22+
type t =
23+
{ pps : Lib_name.t list
24+
; project_root : Path.Source.t option
25+
}
26+
27+
let equal x y =
28+
List.equal Lib_name.equal x.pps y.pps
29+
&& Option.equal Path.Source.equal x.project_root y.project_root
30+
;;
31+
32+
let to_string { pps; project_root } =
33+
let s = String.enumerate_and (List.map pps ~f:Lib_name.to_string) in
34+
match project_root with
35+
| None -> s
36+
| Some dir ->
37+
sprintf "%s (in project: %s)" s (Path.Source.to_string_maybe_quoted dir)
38+
;;
39+
40+
let of_libs libs =
41+
let pps =
42+
(let compare a b = Lib_name.compare (Lib.name a) (Lib.name b) in
43+
List.sort libs ~compare)
44+
|> List.map ~f:Lib.name
45+
in
46+
let project =
47+
List.fold_left libs ~init:None ~f:(fun acc lib ->
48+
let scope_for_key =
49+
let info = Lib.info lib in
50+
let status = Lib_info.status info in
51+
match status with
52+
| Private (scope_name, _) -> Some scope_name
53+
| Installed_private | Public _ | Installed -> None
54+
in
55+
Option.merge acc scope_for_key ~f:(fun a b ->
56+
assert (Dune_project.equal a b);
57+
a))
58+
in
59+
{ pps; project_root = Option.map project ~f:Dune_project.root }
60+
;;
61+
end
62+
63+
let reverse_table : (Digest.t, Decoded.t) Table.t = Table.create (module Digest) 128
64+
65+
let encode ({ Decoded.pps; project_root } as x) =
66+
let y = Digest.generic (pps, project_root) in
67+
match Table.find reverse_table y with
68+
| None ->
69+
Table.set reverse_table y x;
70+
y
71+
| Some x' ->
72+
if Decoded.equal x x'
73+
then y
74+
else
75+
User_error.raise
76+
[ Pp.textf "Hash collision between set of ppx drivers:"
77+
; Pp.textf "- cache : %s" (Decoded.to_string x')
78+
; Pp.textf "- fetch : %s" (Decoded.to_string x)
79+
]
80+
;;
81+
end
82+
83+
let ppx_exe_path (ctx : Build_context.t) ~key =
84+
Path.Build.relative ctx.build_dir (".ppx/" ^ key ^ "/ppx.exe")
85+
;;
86+
87+
let ppx_driver_exe (ctx : Context.t) libs =
88+
let key = Digest.to_string (Key.Decoded.of_libs libs |> Key.encode) in
89+
Context.host ctx >>| Context.build_context >>| ppx_exe_path ~key
90+
;;
91+
92+
let get_ppx_exe ctx ~scope pps =
93+
let open Resolve.Memo.O in
94+
let* libs = Lib.DB.resolve_pps (Scope.libs scope) pps in
95+
ppx_driver_exe ctx libs |> Resolve.Memo.lift_memo
96+
;;

src/dune_rules/ppx_exe.mli

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
open Import
2+
3+
(** Get the path to a ppx driver executable for a list of ppx rewriter libraries.
4+
This module provides the core ppx executable resolution logic without depending
5+
on the Expander module. *)
6+
7+
(** Get the path to the ppx driver executable for a list of ppx libraries.
8+
The libraries must be provided with their locations for error reporting. *)
9+
val get_ppx_exe
10+
: Context.t
11+
-> scope:Scope.t
12+
-> (Loc.t * Lib_name.t) list
13+
-> Path.Build.t Resolve.Memo.t

test/blackbox-tests/test-cases/ppx/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
(cram
2-
(applies_to ppx-rewriter 9650-bytecode-rewriter)
2+
(applies_to ppx-rewriter 9650-bytecode-rewriter ppx-pform)
33
(deps
44
(package ppxlib)
55
%{bin:ocamlfind}))
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
Test the %{ppx:...} pform that creates a combined ppx executable
2+
3+
$ cat >dune-project <<EOF
4+
> (lang dune 3.21)
5+
> EOF
6+
7+
Create two simple ppx rewriters
8+
9+
$ mkdir ppx1 ppx2
10+
11+
$ cat >ppx1/dune <<EOF
12+
> (library
13+
> (name ppx1)
14+
> (kind ppx_rewriter))
15+
> EOF
16+
17+
$ cat >ppx1/ppx1.ml <<EOF
18+
> let () = Ppxlib.Driver.register_transformation "ppx1"
19+
> ~impl:(fun structure -> structure)
20+
> EOF
21+
22+
$ cat >ppx2/dune <<EOF
23+
> (library
24+
> (name ppx2)
25+
> (kind ppx_rewriter))
26+
> EOF
27+
28+
$ cat >ppx2/ppx2.ml <<EOF
29+
> let () = Ppxlib.Driver.register_transformation "ppx2"
30+
> ~impl:(fun structure -> structure)
31+
> EOF
32+
33+
Create a rule that uses the ppx pform
34+
35+
$ cat >dune <<EOF
36+
> (rule
37+
> (alias test-ppx)
38+
> (action (system "echo %{ppx:ppx1+ppx2}")))
39+
> EOF
40+
41+
Run the test
42+
43+
$ dune build @test-ppx
44+
.ppx/1b1fa3a921019504476f74bb87685798/ppx.exe
45+
46+
Test that the order of libraries doesn't matter
47+
48+
$ cat >dune <<EOF
49+
> (rule
50+
> (alias test-ppx)
51+
> (action (system "echo %{ppx:ppx2+ppx1}")))
52+
> EOF
53+
54+
$ dune build @test-ppx
55+
56+
57+
Invalid ppx form
58+
59+
$ cat >dune <<EOF
60+
> (rule
61+
> (alias test-ppx)
62+
> (action (system "echo %{ppx:.faz+bar}")))
63+
> EOF
64+
65+
$ dune build @test-ppx
66+
File "dune", line 3, characters 23-38:
67+
3 | (action (system "echo %{ppx:.faz+bar}")))
68+
^^^^^^^^^^^^^^^
69+
Error: ".faz" is an invalid library name.
70+
[1]

0 commit comments

Comments
 (0)