Library
Module
Module type
Parameter
Class
Class type
A mini-language (DSL) for Dune files embedded in OCaml that produces readable, valid Dune include files. The mini-language closely matches the structure of a regular Dune file, and because it is embedded in OCaml you get OCaml type-safety and the power of your favorite OCaml IDE as you write your Dune files.
The Dune DSL uses a tagless final design so your Dune DSL can be interpreted in a variety of ways that are independent of Dune itself. The standard dkml-dune-dsl-show interpreter allows all of the Dune string values (executable names, rule targets, etc.) to be parameterized from external JSON files. That lets you produce many similar executables, libraries or assets using the same Dune logic without repeating yourself (DRY).
This is an early release. The author jonahbeckford@ put in enough Dune syntax for what he needed; if you want more, this project accepts PRs!
There is at least one sharp edge:
"dune.inc"
and then rerun Dune twice. What is happening? If the version of Dune DSL you are using has a bug where it generates a syntactically incorrect Dune file, Dune will stop the build (which is expected). However even if you upgrade to a bugfix version of the Dune DSL, Dune still will not be able to generated a corrected Dune file. You will need to truncate the "dune.inc"
file so that the file is completely empty; then Dune will be able to generate the correct "dune.inc"
, and if you run Dune once more then the new "dune.inc"
logic will run. The situation is a classic chicken-and-egg because we let Dune generate its own include files.If you already know how to use Dune DSL, you can skip to the API documentation at DkmlDuneDsl.Dune.SYM
.
Install the DSL and a DSL interpreter with Opam using:
opam install dkml-dune-dsl dkml-dune-dsl-show
If you use dune-project files to manage your project, add the DSL to it with the build
flag:
(package ... (depends ... (dkml-dune-dsl (and (>= 0.1.0) :build)) (dkml-dune-dsl-show (and (>= 0.1.0) :build)) ) )
An interpreter takes your Dune DSL expression and any command line arguments you supply to it, and performs some activity on it. In particular:
The format of the parameters file for dkml-dune-dsl-show is:
{ "param-sets": [ ... we'll describe this soon ... ] }
You can add your own JSON fields as long as they start with an underscore. So the following is a valid parameters file:
{ "_documentation": "Some documentation of this file", "param-sets": [ ... we'll describe this soon ... ] }
In what follows we will use the dkml-dune-dsl-show interpreter and give it a parameters file called "dune-parameters.json"
:
{ "param-sets": [ {"name": "batman", "age": 39}, {"name": "robin", "age": 24} ] }
The example above has two parameter sets, which the dkml-dune-dsl-show interpreter will use to print your Dune DSL expression twice into the same file:
"{{{ name }}}"
and "{{{ age }}}"
strings with "batman"
and "39"
, respectively"{{{ name }}}"
and "{{{ age }}}"
strings with "robin"
and "24"
, respectivelyLet's complete the parameter set story:
"{{{ ... }}}"
expressions are known as Mustache expressions. Mustache is great at operating on JSON documents, which is exactly what the parameters file is. Most but not all of the Mustache expression language can be used; see OCaml Mustache for the exact specification."{{{ ... }}}"
rather than two braces "{{ ... }}"
because the three braces is not HTML escaped.Any "{{{ xyz }}}"
parameter where xyz
is not in your parameter set will raise an error:
Fatal error: exception Mustache_types.Missing_variable("xyz")
{"name": "batman", "age": 39}
), you can use the entire parameters file if you use the pragma once
mode. We'll describe that pragma in the next section, but for now remember that pragma once
gives you different parameters.Write your Dune configuration file in the OCaml DSL language. For what follows, we'll assume it is called "my_dune.ml"
:
open DkmlDuneDsl
module Build (I: Dune.SYM) = struct
open I
let res = [
pragma "once"
(rule
[
deps [ glob_files "*_data.ml" ];
target "common.ml";
action
(run
[ `S "something.exe"; `S "-o"; `S "%{target}"; `S "%{deps}" ]);
]);
pragma "once" (library [ name "common"; modules (set_of [ "common" ]) ]);
(*
Being part of a DSL gives us whatever power the interpreter "I" lets us have.
The dkml-dune-dsl-show interpreter "I" supports Mustache template expressions
like {{name}} that are consumed from a parameters file.
.
We didn't want to repeat the previous stanzas multiple times, so we wrapped
them in a (pragma "once" ...).
*)
library
[
name "{{name}}";
modules (set_of [ "{{name}}" ]);
libraries [ `S "common" ];
preprocess (pps [ `S "ppx_deriving.ord" ]);
];
]
end
Let's peek ahead for a second. Once we complete all the steps, the Dune DSL tooling should give us a valid Dune include file that looks roughly like:
(rule [ (deps [glob_files "*_asset.png"]); (target "common.ml"); (action (run ["something.exe"; "--output"; "%{target}"; "%{deps}"]))]); (library [ (name "common"); (modules [ "common" ]); ]); (library [ (name "batman"); (modules [ "batman" ]); (libraries ["common"]); (preprocess (pps ["ppx_deriving.ord"]))]) (library [ (name "robin"); (modules [ "robin" ]); (libraries ["common"]); (preprocess (pps ["ppx_deriving.ord"]))])
The full set of expressions is available in DkmlDuneDsl.Dune.SYM
. The res
result variable you write must be a list of type [`Stanza] repr
.
If you have a stanza that does not use a parameter "{{{ ... }}}"
that is a strong signal you should wrap it in a DkmlDuneDsl.Dune.SYM.pragma
"once"
.
Write some small Dune glue in your "dune" file to execute your Dune configuration file with an interpreter. The only standard interpreter is the dkml-dune-dsl-show interpreter. The following snippet goes into the normal "dune" file, not the DSL:
(rule (target dune_inc.ml) (action (with-stdout-to %{target} (echo ; YOU: Change "My_dune" to the name of your Dune DSL expression module "module M = My_dune.Build (DkmlDuneDslShow.I) let () = print_string @@ DkmlDuneDslShow.plain_hum M.res")))) (executable (name dune_inc) ; YOU: Change "my_dune" to the name of your Dune DSL file (modules dune_inc my_dune) (libraries dkml-dune-dsl-show)) (rule (alias gendune) (action (progn (with-stdout-to dune.gen.inc (run ./dune_inc.exe %{dep:dune-parameters.json})) (diff? dune.inc dune.gen.inc)))) (include dune.inc)
You will need to change "my_dune"
and the corresponding name of the module ("My_dune"
) in the above snippet.
Then write an empty "dune.inc"
file in the same directory as your "dune"
file.
Run "dune build '@gendune'"
. Dune will create a "dune.inc"
in your "_build/default"
directory (not your source directory) containing the information from your DSL expression. If you run it again, Dune will run all the build instructions in the "dune.inc"
.
It will also tell you when your source directory is out of sync with your DSL expression. If you are comfortable with the changes it shows you, run dune promote
and commit the changes to your source code repository.
You are done!
If you are a user, you don't need to read this section. It is for those interested in creating their own configuration format using tagless final DSL languages.
The author jonahbeckford@ uses DSLs for configuration files because:
The sore spot today is the setup required to author a configuration file. You need:
The setup should ideally be zero. It would be great that anybody could open up a DSL configuration file in their favorite editor and start configuring.
To support this, there is a convention that has been followed:
open TheCamelCasedNamedOfTheDslOpamPackage
module NamedFromTheDslSpec (I: NamedFromTheDslSpec.SYM) = struct
open I
let res = failwith "TODO: Replace this with your DSL expression here"
end
There should be nothing else in a configuration file except OCaml odoc comments. Just the open
statement at the top followed by a module
definition!
For the Dune DSL:
TheCamelCasedNamedOfTheDslOpamPackage
is DkmlDuneDsl
NamedFromTheDslSpec
is the pair (Build, Dune)
(more on this below)AnyNameTheLanguageCreatorWants
is Dune
The convention for using I
as the interpreter, using SYM
as the module type, and using res
as the interpreted result comes directly from Oleg Kiselyov's Tagless-final primer. The tagless final primer is a good read if you want to implement your own DSL!
You can lightly parse a configuration and know that the Opam package for DkmlDuneDsl
is "dkml-dune-dsl"
with a library with the same "dkml-dune-dsl"
name.
Introspecting that library with a OCaml toplevel REPL would show:
utop # #require "dkml-dune-dsl";; utop # DkmlDuneDsl.spec_version;; - : int = 1 utop # DkmlDuneDsl.spec_modules;; - : (string * string) list = [("Build", "Dune"); ("Project", "DuneProject"); ("Workspace", "DuneWorkspace")]
1
is the only supported spec_version
spec_modules
are the (fst,snd)
pairs that fill the placeholders in the module expression "module ?fst? : (I: ?snd?.SYM)"