Rule Generation¶
Using these parsed stanzas, the next step is to generate rules. This work
starts in src/dune_rules/gen_rules.ml, which dispatches to various
modules in src/dune_rules/.
Rules are registered on the build engine using the following function from the
Super_context module:
val add_rule
: t
-> ?mode:Rule.Mode.t
-> ?loc:Loc.t
-> dir:Path.Build.t
-> Action.Full.t Action_builder.With_targets.t
-> unit Memo.t
A value of Super_context.t represents an OCaml toolchain (Context.t) as
well as various capabilities to expand variables and refer to (env)
stanzas. The last, unlabelled argument corresponds to
the fully annotated action. We’ll go through its type below.
The modules in src/dune_rules often expose a function gen_rules
taking a parsed stanza, a Super_context.t value, a directory name (and
other arguments), and returning unit Memo.t.
Note
The Memo module is central to how Dune operates. It is a monadic
memoization framework that allows two things:
Sharing and caching expensive internal computations, such as computing the list of libraries Dune knows about, or computing the list of flags that should be used to compile a given module.
Incremental recomputation of this cached data.
Memotracks dependencies between memoized values and will only recompute the necessary ones when an input changes. This is a mini in-memory build system that works like a spreadsheet. It is essential to the watch mode.
An example of rule is the mdx stanza, implemented in
src/dune_rules/mdx.ml. There are several steps in setting up rules for
a (mdx) stanza:
How to run
ocaml-mdx depson the input file to produce a.mdx.depsRun
ocaml-mdx dune-gento produce amdx_gen.ml-genOCaml source fileCompile this executable
Run this executable to produce a
.correctedfileRegister a diff action between the
.correctedfile and the original file
Let’s walk through these rules.
The first one is about producing a .mdx.deps file. It is a simple call to
Super_context.add_rule.
312let* () = Super_context.add_rule sctx ~loc ~dir (Deps.rule ~dir ~mdx_prog files)
Deps.rule is defined in a helper function:
77let rule ~dir ~mdx_prog (files : Files.t) =
78 Command.run_dyn_prog
79 ~dir:(Path.build dir)
80 mdx_prog
81 ~stdout_to:files.deps
82 [ Command.Args.A "deps"; Lazy.force color_always; Dep (Path.build files.Files.src) ]
This is a rule made by just running a command, here mdx_prog (a resolved
path to ocaml-mdx, meaning it can point to a binary in PATH or a built
version in the current workspace). Its arguments are a domain-specific language
defined in src/dune_rules/command.mli where A refers to a plain
string, and Dep refers to a string that should be interpreted as a dependency.
Between that, and the ~stdout_to parameter, it is enough for Dune to know
about the rule’s dependencies (what it will read) and its target (what it will
produce).
The second rule, which generates mdx_gen.ml-gen, is similar. It is also done
by calling Command.run_dyn_prog.
The third rule, to build the executable, calls Exe.build_and_link that is a
helper function.
Let’s observe how the fourth rule (that calls the generated executable) is set up.
let mdx_action ~loc:_ =
let open Action_builder.With_targets.O in
let mdx_input_dependencies = (* ... *) in
let executable, command_line = (* ... *) in
let deps, sandbox = (* ... *) in
let+ action =
Action_builder.with_no_targets deps
>>> Action_builder.with_no_targets
(Action_builder.env_var "MDX_RUN_NON_DETERMINISTIC")
>>> Action_builder.with_no_targets
(Action_builder.map mdx_input_dependencies ~f:(fun d -> (), d)
|> Action_builder.dyn_deps)
>>> Command.run_dyn_prog
~dir:(Path.build dir)
~stdout_to:files.corrected
executable
command_line
and+ locks =
Expander.expand_locks expander stanza.locks |> Action_builder.with_no_targets
in
Action.Full.add_locks locks action |> Action.Full.add_sandbox sandbox
in
Super_context.add_rule sctx ~loc ~dir (mdx_action ~loc)
Here, the mdx_action that is set up is not just a single
Command.run_dyn_prog call. It is assembled using combinators from
Action_builder.With_targets. This is another monad used in Dune. It
corresponds to what can happen at build time, like running commands or creating
files, or more complex actions such as reading a file that needs to be built by
another rule. It is also used to track dependencies and targets. The “thing”
that we register to the Dune engine using Super_context.add_rule has type
Action.Full.t Action_builder.With_targets.t.
Note
This is different from Memo, which corresponds to what happens within
Dune itself. But it is also possible to use Memo from an
Action_builder context. In that sense, Action_builder is more
powerful: at execution time, Action_builder will manage what happens in
the _build directory, while Memo is only concerned with what happens
in memory.
Finally, to register the correction, the technique is to attach the diff action to the @runtest alias (a collection of rules) using this call:
405(* Attach the diff action to the @runtest for the src and corrected files *)
406Files.diff_action files
407|> Super_context.add_alias_action sctx (Alias.make Alias0.runtest ~dir) ~loc ~dir
Where Files.diff_action is defined as:
33let diff_action { src; corrected; deps = _ } =
34 let src = Path.build src in
35 let open Action_builder.O in
36 let+ () = Action_builder.path src
37 and+ () = Action_builder.path (Path.build corrected) in
38 Action.Full.make (Action.diff ~optional:false src corrected)
39;;
As explained above, Action_builder keeps tracks of dependencies, so using
let+ () = Action_builder.path src is a way to declare src as a
dependency of the current action.