package dkml-dune-dsl
Install
Dune Dependency
Authors
Maintainers
Sources
md5=2c09714a7cb75c351d8b8fed6b186e2b
sha512=ce8a21efbc9a7f238de703206293f0a7c3eb938084afe0e582fefe58840d78939b0f8ab717ca745db732a1e50039bb0ab79618252c6e705adaeb5257e381de83
doc/index.html
Dune DSL
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).
Status
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:
- TLDR: If your build is not updating, erase everything in your
"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.
Usage
If you already know how to use Dune DSL, you can skip to the API documentation at DkmlDuneDsl.Dune.SYM
.
Step One - Install
Install the DSL and a DSL interpreter with Opam using:
opam install dkml-dune-dsl dkml-dune-dsl-show
Step Two - dune-project
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)) ) )
Step Three - Parameters
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 dkml-dune-dsl-show interpreter takes your Dune DSL expression and prints your DSL expression as a valid Dune file. The dkml-dune-dsl-show interpreter takes an optional parameters file which lets you parameterize the printed Dune file (to create multiple but similar executables, for example).
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:
- The first print will replace all
"{{{ name }}}"
and"{{{ age }}}"
strings with"batman"
and"39"
, respectively - The second print will replace all
"{{{ name }}}"
and"{{{ age }}}"
strings with"robin"
and"24"
, respectively
Let's complete the parameter set story:
- All of those
"{{{ ... }}}"
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. - Our examples use three braces
"{{{ ... }}}"
rather than two braces"{{ ... }}"
because the three braces is not HTML escaped. Any
"{{{ xyz }}}"
parameter wherexyz
is not in your parameter set will raise an error:Fatal error: exception Mustache_types.Missing_variable("xyz")
- If you don't supply a parameters file to the dkml-dune-dsl-show interpreter it will print your Dune DSL expression once.
- Rather than use a single parameter set to print (ex.
{"name": "batman", "age": 39}
), you can use the entire parameters file if you use thepragma once
mode. We'll describe that pragma in the next section, but for now remember thatpragma once
gives you different parameters.
Step Four - Writing
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"
.
Step Five - Glue
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 (target dune.inc) (mode promote) (action (with-stdout-to %{target} (run ./dune_inc.exe)))) (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.
Finished!
Run dune build
. 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!
Tagless Final Configuration
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:
- as a creator of configuration file formats, I don't need to write a lexer or a parser. I instead re-use the OCaml language and just lean on OCaml's type-safety to create a rich DSL
- as an author of configuration files, I get syntax checking, documentation and perhaps auto-completion with a modern editor with some setup
- as a user of configuration files, I just need to compile it to see if the syntax is correct
- at some point in the future, as a user of configuration files I can inspect the compiled bytecode to see if it is "safe" and avoids any system calls (all system calls, like printing to the standard output, should be done by the interpreter). For some configuration files calling out to the system is okay, but it is easy to conceive of a tool that validates safety for the majority of configuration files that don't need it.
The sore spot today is the setup required to author a configuration file. You need:
- an Opam switch (or a configured findlib)
- the DSL package installed (ex. dkml-dune-dsl)
- the OCaml language server protocol and/or Merlin installed depending on your editor
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:
- the
TheCamelCasedNamedOfTheDslOpamPackage
isDkmlDuneDsl
- the
NamedFromTheDslSpec
is the pair(Build, Dune)
(more on this below) - the
AnyNameTheLanguageCreatorWants
isDune
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")]
- As of the writing of this documentation, the number
1
is the only supportedspec_version
- The
spec_modules
are the(fst,snd)
pairs that fill the placeholders in the module expression"module ?fst? : (I: ?snd?.SYM)"
Low Priority Tasks
- Technically the dkml-dune-dsl-show interpreter should be a composite of two interpreters. The first would just apply the Mustache template to all the strings of the DSL expression. The second would print out the DSL expression as s-exps. The first could be re-used and composed by other interpreters. Even with a split no user code needs to change; just name the first and second interpreters as ".template" and ".sexp", and the composite ".show". So this can wait until someone writes their own interpreter. The tagless final primer has notes on how to do compositions of interpreters.