(* * Copyright (c) 2013-2020 Thomas Gazagnaire * Copyright (c) 2013-2020 Anil Madhavapeddy * Copyright (c) 2015-2020 Gabriel Radanne * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *) (** {1 The Functoria DSL} *) (** 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. {2 Defining toplevel modules} 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 {!module-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. {2 Defining configuration keys} A configuration key is composed of: - {i name} : The name of the value in the program. - {i description} : How it should be displayed/serialized. - {i stage} : Is the key available only at runtime, at configure time or both? - {i documentation} : It is not optional so you should really write it. 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 {{:http://erratique.ch/software/cmdliner} 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). {v -l VAL, --lang=VAL (absent=en) The default language for the application. v} {2 Defining applications} 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 ]} *) module type DSL = module type of DSL include DSL module Package = Package module Info = Info module Install = Install module Device = Device (** {1 Useful module implementations} *) val job : job typ (** [job] is the signature for user's application main module. *) val noop : job impl (** [noop] is an implementation of {!type-job} that holds no state, does nothing and has no dependency. *) type argv = Argv.t (** The type for command-line arguments, similar to the usual [Sys.argv]. *) val argv : argv typ (** [argv] is a value representing {!type-argv} module types. *) val sys_argv : argv impl (** [sys_argv] is a device providing command-line arguments by using [Sys.argv]. *) val runtime_args : ?runtime_package:package -> ?runtime_modname:string -> argv impl -> job impl (** [runtime_args a] is an implementation of {!type-job} that holds the parsed command-line arguments. By default [runtime_package] is ["mirage-runtime.functoria"] and [runtime_modname] is ["Functoria_runtime"]. *) module Type = Type module Impl = Impl module Context = Context module Key = Key module Runtime_arg = Runtime_arg module Opam = Opam module Lib = Lib module Tool = Tool module Engine = Engine module DSL = DSL module Cli = Cli module Action = Action module Dune = Dune