Type-safe codecs for the dune-format file family

Files
README.md

nox-dune #

Typed codecs for dune's configuration files, built on nox-sexp.

Linters, release scripts and tools that manage many packages at once all need the same answer: which libraries and packages does each dune and dune-project file declare? Parsing the S-expressions by hand yields an untyped tree, and every such tool then re-derives the same fields from it. This library decodes each kind of file into typed values once, and prints them back.

Dune.Codec holds one codec per kind of file. file () reads dune build files, project reads dune-project, workspace reads dune-workspace, and dune_package reads the installed metadata under _opam/lib/<pkg>/. Any of them can be passed to the flat entry points Dune.of_string, of_string_exn, to_string, of_reader, of_reader_exn and to_writer. The decoded values are read through Dune.File, Dune.Project, Dune.Workspace and Dune.Package.

Installation #

Install with opam:

$ opam install nox-dune

If opam cannot find the package, it may not yet be released in the public opam-repository. Add the overlay repository, then install it:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-dune

Quick start #

Read a dune file and list what it defines:

let dune_text = {|
  (library (name foo) (libraries fmt))
  (library (name bar))
  (executable (name main))
  (test (name test_foo) (libraries alcotest))
|}

let file = Dune.of_string_exn (Dune.Codec.file ()) dune_text
# Dune.File.library_names file
- : string list = ["foo"; "bar"]

# Dune.File.executable_names file
- : string list = ["main"]

# Dune.File.test_names file
- : string list = ["test_foo"]

A dune-project file decodes the same way, into project and package fields:

let project_text = {|
  (lang dune 3.17)
  (name my-project)
  (version 1.0.0)
  (license ISC)
  (authors "Alice" "Bob")
  (source (github owner/repo))
  (package
   (name my-project)
   (synopsis "A demo")
   (depends ocaml dune (alcotest :with-test)))
|}

let proj = Dune.of_string_exn Dune.Codec.project project_text
# Dune.Project.name proj
- : string option = Some "my-project"

# Dune.Project.authors proj
- : string list = ["Alice"; "Bob"]

# Dune.Project.source proj
- : string option = Some "github:owner/repo"

# Dune.Project.packages proj
  |> List.map (fun p ->
       Dune.Project.Package.dependencies p
       |> List.map Dune.Project.Dep.to_string)
- : string list list = [["ocaml"; "dune"; "(alcotest :with-test)"]]

# List.map Dune.Project.Package.depends (Dune.Project.packages proj)
- : string list list = [["ocaml"; "dune"; "alcotest"]]

A Dep.t keeps a (depends ...) entry as it was written, with the package name and its constraint in dune's own grammar. Reading a dune-project and writing it back therefore leaves the constraints as they were. Package.depends returns just the names.

To build a project without parsing one, call Dune.Project.v with the fields, then print it with Dune.to_string Dune.Codec.project. By default the output puts each stanza on one line. With ~indent:n, stanzas that have several fields wrap, indenting n spaces per nesting level; ~indent:1 gives the layout dune itself writes.

Atoms can contain %{name} and %{name:default} references. Dune.Var parses one, and Dune.expand_atom substitutes them from an environment:

# Dune.Var.to_string (Dune.Var.v ~default:"foo" "name")
- : string = "%{name:foo}"

# let env = function "name" -> Some "Alice" | _ -> None
val env : string -> string option = <fun>

# Dune.expand_atom ~env "Hello %{name}!"
- : string = "Hello Alice!"

API #

dune.mli has the full signatures of the file codecs, the modules that read their values, Dune.Var, Dune.expand_atom and Dune.has_variables. An error is a Dune.Error.t, which carries the source location that nox-sexp reported.

Licence #

ISC. See LICENSE.md.