Skip to content

Commit a01b21f

Browse files
jeremiediminochristinerose
authored andcommitted
Allow %{read:...} in more places such as enabled_if (ocaml#4994)
Signed-off-by: Jeremie Dimino <jeremie@dimino.org> Co-authored-by: Christine Rose <christinerose@users.noreply.github.com>
1 parent e68e97d commit a01b21f

File tree

7 files changed

+116
-29
lines changed

7 files changed

+116
-29
lines changed

CHANGES.md

+3
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ Unreleased
227227
cram test cannot escape the sandbox and pick up some random git or
228228
mercurial repository on the file system (#4996, @jeremiedimino)
229229

230+
- Allow `%{read:...}` in more places such as `(enabled_if ...)`
231+
(#4994, @jeremiedimino)
232+
230233
- Run each action in its own process group so that we don't leave
231234
stray processes behind when killing actions (#4998, @jeremiedimino)
232235

doc/concepts.rst

+45
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,51 @@ generated by an OCaml program via:
249249
250250
List.iter (fun s -> print_string (String.escaped s)) l
251251
252+
#. Dealing with circular dependencies introduced by variables
253+
254+
If you ever see Dune reporting a dependency cycle that involves a
255+
variable such as `%{read:<path>}`, try to move `<path>` to a different
256+
directory.
257+
258+
The reason you might see such dependency cycle is because Dune is
259+
trying to evaluate the `%{read:<path>}` too early. For instance, let's
260+
consider the following example:
261+
262+
.. code:: lisp
263+
264+
(rule
265+
(targets x)
266+
(enabled_if %{read:y})
267+
(action ...)
268+
269+
(rule
270+
(with-stdout-to y (...)))
271+
272+
When Dune loads and interprets this file, it decides whether the
273+
first rule is enabled by evaluating ``%{read:y}``. To
274+
evaluate ``%{read:y}``, it must build ``y``. To build ``y``, it must
275+
figure out the build rule that produces ``y``, and in order to do that, it must
276+
first load and evaluate the above ``dune`` file. You can see how this
277+
creates a cycle.
278+
279+
Some cycles might be more complex. In any case, when you see such an
280+
error, the easiest thing to do is move the file that's being read
281+
to a different directory, preferably a standalone one. You can use the
282+
:ref:`subdir` stanza to keep the logic self-contained in the same
283+
``dune`` file:
284+
285+
.. code:: lisp
286+
287+
(rule
288+
(targets x)
289+
(enabled_if %{read:dir-for-y/y})
290+
(action ...)
291+
292+
(subdir
293+
dir-for-y
294+
(rule
295+
(with-stdout-to y (...))))
296+
252297
#. Expansion of lists
253298

254299
Forms that expands to list of items, such as ``%{cc}``, ``%{deps}``,

doc/dune-files.rst

+2
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,8 @@ run this toplevel with:
17071707
of `library`_. Currently, ``action`` and ``future_syntax`` are not supported
17081708
in the toplevel.
17091709

1710+
.. _subdir:
1711+
17101712
subdir
17111713
------
17121714

src/dune_engine/action_builder.ml

-15
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,6 @@ let lines_of p =
9595
(x, Dep.Map.empty))
9696
}
9797

98-
let strings p =
99-
let f x =
100-
match Scanf.unescaped x with
101-
| Error () ->
102-
User_error.raise
103-
[ Pp.textf "Unable to parse %s" (Path.to_string_maybe_quoted p)
104-
; Pp.textf
105-
"This file must be a list of lines escaped using OCaml's \
106-
conventions"
107-
]
108-
| Ok s -> s
109-
in
110-
let+ l = lines_of p in
111-
List.map l ~f
112-
11398
let read_sexp p =
11499
let+ s = contents p in
115100
Dune_lang.Parser.parse_string s ~fname:(Path.to_string p) ~mode:Single

src/dune_engine/action_builder.mli

-4
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,6 @@ val contents : Path.t -> string t
130130
of the file at [path] as a list of lines. *)
131131
val lines_of : Path.t -> string list t
132132

133-
(** [strings path] is like [lines_of path] except each line is unescaped using
134-
the OCaml conventions. *)
135-
val strings : Path.t -> string list t
136-
137133
(** Load an S-expression from a file *)
138134
val read_sexp : Path.t -> Dune_lang.Ast.t t
139135

src/dune_rules/expander.ml

+32-10
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,23 @@ let[@inline never] invalid_use_of_target_variable t
266266
~field:multiplicity ~variable:var_multiplicity;
267267
assert false)
268268

269+
let expand_read_macro ~dir ~source s ~read ~pack =
270+
let path = relative ~source dir s in
271+
let read =
272+
let open Memo.Build.O in
273+
let+ x = Build_system.read_file path ~f:read in
274+
pack x
275+
in
276+
Need_full_expander
277+
(fun t ->
278+
if Dune_project.dune_version (Scope.project t.scope) >= (3, 0) then
279+
Without read
280+
else
281+
(* To prevent it from working in certain position before Dune 3.0. It'd
282+
be nice if we could invite the user to upgrade to (lang dune 3.0),
283+
but this is a bigger refactoring. *)
284+
With (Action_builder.memo_build read))
285+
269286
let expand_pform_gen ~(context : Context.t) ~bindings ~dir ~source
270287
(pform : Pform.t) : expansion_result =
271288
match Pform.Map.find bindings pform with
@@ -571,18 +588,23 @@ let expand_pform_gen ~(context : Context.t) ~bindings ~dir ~source
571588
(let open Memo.Build.O in
572589
let+ b = Artifacts.Bin.binary_available t.bin_artifacts_host s in
573590
b |> string_of_bool |> string))
574-
| Read ->
575-
let path = relative ~source dir s in
576-
Direct
577-
(With (Action_builder.map (Action_builder.contents path) ~f:string))
591+
| Read -> expand_read_macro ~dir ~source s ~read:Io.read_file ~pack:string
578592
| Read_lines ->
579-
let path = relative ~source dir s in
580-
Direct
581-
(With (Action_builder.map (Action_builder.lines_of path) ~f:strings))
593+
expand_read_macro ~dir ~source s ~read:Io.lines_of_file ~pack:strings
582594
| Read_strings ->
583-
let path = relative ~source dir s in
584-
Direct
585-
(With (Action_builder.map (Action_builder.strings path) ~f:strings))))
595+
expand_read_macro ~dir ~source s ~read:Io.lines_of_file
596+
~pack:(fun lines ->
597+
strings
598+
(List.map lines ~f:(fun line ->
599+
match Scanf.unescaped line with
600+
| Error () ->
601+
User_error.raise
602+
~loc:(Loc.in_file (relative ~source dir s))
603+
[ Pp.textf
604+
"This file must be a list of lines escaped using \
605+
OCaml's conventions"
606+
]
607+
| Ok s -> s)))))
586608

587609
(* Make sure to delay exceptions *)
588610
let expand_pform_gen ~context ~bindings ~dir ~source pform =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Check that %{read:...} is allowed in enabled_if from Dune 3.0
2+
3+
Doesn't work with < 3.0:
4+
5+
$ echo '(lang dune 2.9)' > dune-project
6+
$ cat >dune <<"EOF"
7+
> (rule
8+
> (alias default)
9+
> (enabled_if %{read:x/x})
10+
> (action (echo "Hello, world!\n")))
11+
> EOF
12+
13+
We have to use a sub-directory to avoid a circular dependency. The
14+
circular dependency exist because "enabled_if" fields are evaluated at
15+
"rule production time" rather than "rule execution time".
16+
17+
$ mkdir x
18+
$ cat >x/dune <<"EOF"
19+
> (rule
20+
> (with-stdout-to x (system "printf true")))
21+
> EOF
22+
23+
$ dune build
24+
File "dune", line 3, characters 13-24:
25+
3 | (enabled_if %{read:x/x})
26+
^^^^^^^^^^^
27+
Error: %{read:..} isn't allowed in this position.
28+
[1]
29+
30+
But works with 3.0:
31+
32+
$ echo '(lang dune 3.0)' > dune-project
33+
$ dune build
34+
Hello, world!

0 commit comments

Comments
 (0)