Module Dkml_install_api

Component Configuration

You are responsible for creating a module of type Component_config to describe your component. A do-nothing implementation for most of the module is available as Default_component_config.

Dkml_install_api will use the Component_config to create four (4) command line applications.

Each of the command line applications are "subcommands" in the language of the OCaml Cmdliner package. You will not need to understand Cmdliner to define your own component, although you may visit the Cmdliner documentation if you want more information.

The four (4) command line applications have limited access to the OCaml runtime. The expectation is that all installation logic is embedded in bytecode executables which have the complete set of package dependencies you need to run your logic. Through Component_config Dkml_install_api will have given you a ~ctx_t Cmdliner term that, when evaluated, leads to the context record Context.t. The context record has the information needed to run your bytecode executables.

On Windows it is recommended security practice to separate functionality that requires administrative privileges from functionality that does not require non-administrative privileges. Dkml_install_api follows the same recommendations:

  1. Administrator installation defined by Component_config.install_admin_subcommand
  2. Administrator uninstallation defined by Component_config.uninstall_admin_subcommand
  1. User installation defined by Component_config.install_user_subcommand
  2. User uninstallation defined by Component_config.uninstall_user_subcommand
module Context : sig ... end

Context is a module providing a record type for the context.

Configuration
module type Component_config_defaultable = sig ... end

Component configuration values that can be supplied with defaults.

module type Component_config = sig ... end

Each component must define a configuration module

You should include Default_component_config in any of your components so that your component can be future-proof against changes in the Component_config signature.

module Default_component_config : sig ... end

Default values for a subset of the module type Component_config.

Process execution
val log_spawn_onerror_exit : id:string -> ?conformant_subprocess_exitcodes:bool -> Bos.Cmd.t -> unit

log_spawn_onerror_exit ~id ?conformant_subprocess_exitcodes cmd logs the command cmd and runs it synchronously, and prints an error on the fatal logger fl ~id and then exits with a non-zero exit code if the command exits with a non-zero error code.

The environment variable "OCAMLRUNPARAM" will be set to "b" so that any OCaml bytecode launched by log_spawn_onerror_exit will have backtraces. Any exiting environment variable "OCAMLRUNPARAM" will be kept, however.

Exit Codes

The exit code used to leave this process depends on conformant_subprocess_exitcodes.

When conformant_subprocess_exitcodes = true or conformant_subprocess_exitcodes is not specified, the exit code will be the same as the spawned process exit code if and only if the exit code belongs to one of Forward_progress.Exit_code; if the spawned exit code does not belong then the exit code will be Forward_progress.Exit_code.t.Exit_transient_failure.

When conformant_subprocess_exitcodes = false the exit code will always be Forward_progress.Exit_code.t.Exit_transient_failure if the spawned process ends in error.

Uninstallation
val uninstall_directory_onerror_exit : id:string -> dir:Fpath.t -> wait_seconds_if_stuck:float -> unit

uninstall_directory ~id ~dir ~wait_seconds_if_stuck removes the directory dir and, if any process is using the files in dir, will give the wait_seconds_if_stuck seconds to stop using the program. If the directory cannot be removed then prints an error on the fatal logger fl ~id and exists with a transient error code.

For Windows machines a file cannot be removed if it is in use. For most *nix machines the file can be removed since the inode lives on. Consequently only on Windows machines will trigger the logic to check if a process is using a file or directory. This behavior may change in the future.

Logging

Logging follows the Cmdliner standards.

All dkml_install generated executables can be supplied with the following options:

      --color=WHEN (absent=auto)
          Colorize the output. WHEN must be one of `auto', `always' or
          `never'.

      -q, --quiet
          Be quiet. Takes over -v and --verbosity.

      -v, --verbose
          Increase verbosity. Repeatable, but more than twice does not bring
          more.

      --verbosity=LEVEL (absent=warning)
          Be more or less verbose. LEVEL must be one of `quiet', `error',
          `warning', `info' or `debug'. Takes over -v.

You can use Log_config to pass the color and verbosity options into your own bytecode executables.

Start by initializing the logger in your own executables with the following setup_log_t Cmdliner Term:

let setup_log style_renderer level =
  Fmt_tty.setup_std_outputs ?style_renderer ();
  Logs.set_level level;
  Logs.set_reporter (Logs_fmt.reporter ());
  Dkml_install_api.Log_config.create ?log_config_style_renderer:style_renderer
    ?log_config_level:level ()

let setup_log_t =
  Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ())

Finally, with a Log_config.t you can use Log_config.to_args to pass the correct command line options into your own executables. For components that are configured to spawn bytecode programs you can locate the Log_config.t in the Dkml_install_api.Context.t.log_config (ctx.Dkml_install_api.Context.log_config) context field. That could look like:

let execute ctx =
  let ocamlrun =
    ctx.Context.path_eval "%{staging-ocamlrun:share-abi}/bin/ocamlrun"
  in
  log_spawn_onerror_exit
    (* Always use your own unique id; create it with PowerShell on Windows:
          [guid]::NewGuid().Guid.Substring(0,8)
       or on macOS/Unix:
          uuidgen | tr A-Z a-z | cut -c1-8
     *)
    ~id:"9b7e32e0"
    Cmd.(
      v (Fpath.to_string
          (ctx.Context.path_eval "%{staging-ocamlrun:share-abi}/bin/ocamlrun"))
      % Fpath.to_string
          (ctx.Context.path_eval "%{_:share}%/generic/your_bytecode.bc")
      (* Pass --verbosity and --color to your bytecode *)
      %% of_list (Array.to_list (Log_config.to_args ctx.Context.log_config)))

let () =
  let reg = Component_registry.get () in
  Component_registry.add_component reg
    (module struct
      include Default_component_config

      let component_name = "enduser-yourcomponent"

      let install_depends_on = [ "staging-ocamlrun" ]

      let install_user_subcommand ~component_name:_ ~subcommand_name ~fl ~ctx_t =
        let doc = "Install your component" in
        Dkml_install_api.Forward_progress.Continue_progress (Cmdliner.Term.(const execute $ ctx_t, info subcommand_name ~doc), fl)
    end)

Others can use the Log_config.t return value from setup_log when calling Log_config.to_args.

module Log_config : module type of Log_config
module Forward_progress : sig ... end

Forward_progress provides common functions to handle graceful and informative exits from the nested chain of subprocesses typical in DKML Install API and many other applications.