Functoria
Functoria is a DSL to describe a set of modules and functors, their types and how to apply them in order to produce a complete application.
The main use case is mirage. See the Mirage
documentation for details.
Functoria is a DSL to write configuration files for functor-heavy applications. Such configuration files (imaginatively called config.ml
) usually contains three parts: one for defining toplevel modules, one for defining configuration kyes and one for defining applications using these modules and keys.
To define toplevel modules, use the main
function. Among its various arguments, it takes the module name and its signature. The type is assembled with the Type
combinators, like the @->
operator, which represents a functor arrow.
let main = main "Unikernel.Main" (m @-> job)
This declares that the functor Unikernel.Main
takes a module of type m
and returns a module of type DSL.job
. job
has a specific meaning for functoria: it is a module which defines at least a function start
, which should have one argument per functor argument and should return unit
.
It is up to the user to ensure that the declaration matches the implementation, or be rewarded by a compiler error later on. If the declaration is correct, everything that follows will be.
A configuration key is composed of:
Consider a multilingual application: we want to pass the default language as a parameter. We will use a simple string, so we can use the predefined description Key.Arg.string
. We want to be able to define it both at configure and run time, so we use the stage Both
. This gives us the following code:
let lang_key =
let doc =
Key.Arg.info ~doc:"The default language for the application."
[ "l"; "lang" ]
in
Key.create "language" @@ Key.Arg.(opt ~stage:`Both string "en" doc)
Here, we defined both a long option "--lang"
and a short one "-l"
(the format is similar to the one used by Cmdliner. In the application code, the value is retrieved with Key_gen.language ()
.
The option is also documented in the "--help"
option for both the configure
subcommand (at configure time) and ./app.exe
(at startup time).
-l VAL, --lang=VAL (absent=en) The default language for the application.
To register a new application, use register
:
let () = register "app" [ main $ impl ]
This function (which should only be called once) takes as argument the name of the application and a list of jobs. The jobs are defined using the Impl
DSL; for instance the operator $
is used to apply the functor main
(aka Unikernel.Main
) to the default console.
Once an application is registered, it can be configured and built using command-line arguments.
Configuration keys we can use be used to switch implementation at configure time. This is done by using the Key
DSL, for instance to check whether lang_key
is instanciated with a given string:
let lang_is "s" = Key.(pure (( = ) s) $ value lang_key)
Then by using the if_impl
combinator to choose between two implementations depending on the value of the key:
let impl = if_impl (is "fi") finnish_impl not_finnish_implementation
The Functoria DSL allows users to describe how to create portable and flexible applications. It allows to pass application parameters easily using command-line arguments either at configure-time or at runtime.
include DSL
type 'a typ = 'a Type.t
The type for values representing module types.
val typ : 'a -> 'a typ
type t
is a value representing the module type t
.
Construct a functor type from a type and an existing functor type. This corresponds to prepending a parameter to the list of functor parameters. For example:
kv_ro @-> ip @-> kv_ro
This describes a functor type that accepts two arguments -- a kv_ro
and an ip
device -- and returns a kv_ro
.
type 'a impl = 'a Impl.t
The type for values representing module implementations.
type abstract_impl = Impl.abstract
Same as impl
but with hidden type.
val dep : 'a impl -> abstract_impl
dep t
is the (build-time) dependency towards t
.
type 'a key = 'a Key.key
The type for configure-time command-line arguments.
type 'a runtime_arg = 'a Runtime_arg.arg
The type for runtime command-line arguments.
val runtime_arg :
pos:(string * int * int * int) ->
?packages:Package.t list ->
string ->
Runtime_arg.t
runtime_arg ~pos ?packages v
is the runtime argument pointing to the value v
. pos
is expected to be __POS__
. packages
specifies in which opam package the value v
is defined.
type abstract_key = Key.t
The type for abstract keys.
type context = Context.t
The type for keys' parsing context. See Key.context
.
if_impl v impl1 impl2
is impl1
if v
is resolved to true and impl2
otherwise.
match_impl v cases ~default
chooses the implementation amongst cases
by matching the v
's value. default
is chosen if no value matches.
For specifying opam package dependencies, the type package
is used. It consists of the opam package name, the ocamlfind names, and optional lower and upper bounds. The version constraints are merged with other modules.
type package = Package.t
The type for opam packages.
type scope = Package.scope
Installation scope of a package.
val package :
?scope:scope ->
?build:bool ->
?sublibs:string list ->
?libs:string list ->
?min:string ->
?max:string ->
?pin:string ->
?pin_version:string ->
string ->
package
package ~scope ~build ~sublibs ~libs ~min ~max ~pin opam
is a package
. Build
indicates a build-time dependency only, defaults to false
. The library name is by default the same as opam
, you can specify ~sublibs
to add additional sublibraries (e.g. ~sublibs:["mirage"] "foo"
will result in the library names ["foo"; "foo.mirage"]
. In case the library name is disjoint (or empty), use ~libs
. Specifying both ~libs
and ~sublibs
leads to an invalid argument. Version constraints are given as min
(inclusive) and max
(exclusive). If pin
is provided, a pin-depends is generated, pin_version
is "dev"
by default. ~scope
specifies the installation location of the package.
Values of type impl
are tied to concrete module implementation with the device
and main
construct. Module implementations of type job
can then be registered into an application builder. The builder is in charge if parsing the command-line arguments and of generating code for the final application. See Functoria.Lib
for details.
type info = Info.t
The type for build information.
val main :
?pos:(string * int * int * int) ->
?packages:package list ->
?packages_v:package list value ->
?runtime_args:Runtime_arg.t list ->
?deps:abstract_impl list ->
string ->
'a typ ->
'a impl
main name typ
is the functor name
, having the module type typ
. The connect code will call <name>.start
.
packages
or packages_v
is set, then the given packages are installed before compiling the current application.type 'a code = 'a Device.code
val code :
pos:(string * int * int * int) ->
('a, Stdlib.Format.formatter, unit, 'b code) Stdlib.format4 ->
'a
type 'a device = ('a, abstract_impl) Device.t
val impl :
?packages:package list ->
?packages_v:package list Key.value ->
?install:(Info.t -> Install.t) ->
?install_v:(Info.t -> Install.t Key.value) ->
?keys:Key.t list ->
?runtime_args:Runtime_arg.t list ->
?extra_deps:abstract_impl list ->
?connect:(info -> string -> string list -> 'a code) ->
?dune:(info -> Dune.stanza list) ->
?configure:(info -> unit Action.t) ->
?files:(info -> Fpath.t list) ->
string ->
'a typ ->
'a impl
impl ~packages ~packages_v ~install ~install_v ~keys ~runtime_args ~extra_deps ~connect ~dune ~configure ~files module_name module_type
is an implementation of the device constructed by the arguments. packages
and packages_v
are the dependencies (where packages_v
is inside Key.value
). install
and install_v
are the install instructions (used in the generated opam file), keys
are the configuration-time keys, runtime_args
the arguments at runtime, extra_deps
are a list of extra dependencies (other implementations), connect
is the code emitted for initializing the device, dune
are dune stanzas added to the build rule, configure
are commands executed at the configuration phase, files
are files to be added to the list of generated files, module_name
is the name of the device module, and module_type
is the type of the module.
module Package : sig ... end
Representation of opam packages.
module Info : sig ... end
Information about the final application.
module Install : sig ... end
module Device : sig ... end
Signature for functoria devices. A device
is a module implementation which contains a runtime state which can be set either at configuration time (by the application builder) or at runtime, using command-line arguments.
noop
is an implementation of job
that holds no state, does nothing and has no dependency.
runtime_args a
is an implementation of job
that holds the parsed command-line arguments. By default runtime_package
is "mirage-runtime.functoria"
and runtime_modname
is "Functoria_runtime"
.
module Type : sig ... end
Representation of module signatures.
module Impl : sig ... end
module Context : sig ... end
Universal map of keys
module Key : sig ... end
Configuration command-line arguments.
module Runtime_arg : sig ... end
Define runtime command-line arguments.
module Opam : sig ... end
module Lib : sig ... end
Application builder. API for building libraries to link with config.ml
module Tool : sig ... end
Creation of CLI tools to assemble functors.
module Engine : sig ... end
Functoria engine.
module DSL : sig ... end
The Functoria DSL allows users to describe how to create portable and flexible applications. It allows to pass application parameters easily using command-line arguments either at configure-time or at runtime.
module Cli : sig ... end
Command-line handling.
module Action : sig ... end
Wrapper around Bos
which provides a "dry run" feature.
module Dune : sig ... end
Dune files.