Skip to content
Draft
102 changes: 101 additions & 1 deletion doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ are:
- `fullname`, **string**, **required**: *TODO* (remove or make wix specific?)
- `version`, **string**, **required**: the version of the app. *Example:*
`"1.0.0"`.
- `exec_files`, **string array**, **required**: The list of executables to
- `exec_files`, **string array**, **optional**: The list of executables to
install from the bundle. Should be a list of paths, relative to the bundle
root, pointing to executable files that should be installed and made available
to the user. *Example:* `["bin/oui", "bin/opam-oui"]`.
Expand All @@ -53,6 +53,12 @@ are:
reverse DNS format. Must remain the same for all subsequent versions for updates
to work correctly on Windows.
*Example*: `"com.MyCompany.MyApp"`.
- `plugins`, **object array**, **optional**: A list of objects describing plugins
for external applications contained in the bundle. See the
[plugin object section](#plugin-object) for the object format.
- `plugin_dirs`, **object**, **optional**: A JSON object describind where
plugins for the described application should install themselves. See the
[plugin_dirs object section](#plugin_dirs-object) for the object format.
- `wix_manufacturer`, **string**, **required**: The application developer/editor
- `wix_description`, **string**, **optional**: A short description of the application,
shown in the installer properties
Expand Down Expand Up @@ -112,6 +118,58 @@ man section `man1` and that `bundle/config/spec/oui.json.1` and
`bundle/lib/save/oui.save.1` will be installed as manpages in the man section
`man5` on the target system.

### plugin object

This simply describes the configuration format, for more detailed information on
how plugins are installed, please read the [Installing plugins
section](#installing-plugins).

A plugin object describes one plugin contained within the bundle.

It's a JSON object with the following fields:
- `name`, **string**, **required**: The name of the plugin, this is mostly
informational.
- `app_name`, **string**, **required**: The name of the application this plugin
is meant for. It must match the exact name of the application as described in
the app's `oui.json` as it will be used to look it up on the target system.
- `plugin_dir`, **string**, **required**: The path, relative to the bundle's
root, to the directory that should be installed in the app's plugin directory.
- `lib_dir`, **string**, **required**: The path, relative to the bundle's root,
to the directory that should be installed in the app's lib directory.
- `dyn_deps`, **string array**, **optional**: A list of paths, relative to the
bundle's root, to dynamic dependencies directories that should be installed
along with the plugin itself in the app's lib directory.

*Example:*
```json
{
"name": "frama-c-metacsl",
"app_name": "frama-c",
"plugin_dir": "lib/frama-c/plugins/metacsl",
"lib_dir": "lib/frama-c-metacsl",
"dyn_deps": ["lib/findlib"]
}
```

### plugin_dirs object

A plugin_dirs object describes where plugins of the described application should
install themselves.

It's a JSON object with the following fields:
- `plugins_dir`, **string**, **required**: The path, relative to the bundle's
root, to the directory where plugins should be installed.
- `lib_dir`, **string**, **required**: The path, relative to the bundle's root,
to the directory where ocaml libraries should be installed.

*Example:*
```json
{
"plugins_dir": "lib/frama-c/plugins",
"lib_dir": "lib"
}
```

## Generating a binary installer for your dune project

If you're developping an application in OCaml you are most likely to use
Expand Down Expand Up @@ -288,6 +346,44 @@ Now you can generate the installer by running:
oui oui.json <installation-bundle-dir>
```

## Installing plugins

`oui` can handle installation of plugins for apps that have been instaled by a
`oui` generated installer separately.

Plugins are just treated as a part of a bundle and can be installed alongside a
regular application or all by themselves.

You will note that we currently support the plugin layout required by
`dune-sites` plugins. If this does not fit the way your application handles
plugins, please reach out!

`dune-sites` plugins are installed by adding a "main" directory in the `lib/`
folder that contain the actual plugin binaries and a "redirect" directory within
the app's `lib/` directory where it looks up all of its plugins. This "redirect"
directory contains a `META` file used by `dune-sites` to locate the actual
plugin.

You need to make sure both those are present in the installation bundle and
add the right [`plugin`](#plugin-object) description to your `oui.json` file.

In a typical dune project, using dune-sites to define plugins, these should be
present in the bundle generated by `dune install --relocatable --prefix bundle`.
For a plugin `a-b` meant for app `a`, the main directory will be found in
`lib/a-b` and the redirect one in `lib/a/plugins/b`.

Some plugins depend on libraries that are not linked in the main application
and that need to be dynamically linked before loading the plugin itself. These
libraries should be included in the bundle and listed in the plugin's `dyn_deps`
field so that they can be correctly installed and found by `dune-sites` at
loading time.

### Building compatible plugin binaries

Note that you need to be careful with how you build the plugin binaries (`.cmxs`
files) as if they haven't been compiled in the same environment as the main
application binary they likey won't be compatible.

## Installation layout

oui aims at producing the most consistent installs across platforms but each
Expand All @@ -311,6 +407,10 @@ of priority:
1. `/usr/local/share/man`
2. `/usr/local/man` if **1.** does not exist

If plugins need to be installed, the installer will locate the plugin's main
application and add symlinks to the plugins directory in the app's install path,
as described by the plugin and the app respective `oui.json` configuration.

An `uninstall.sh` script is also installed alongside the application
that can be run to cleanly remove it from the system. It will remove
the installation folder and all symlinks created during the installation.
Expand Down
16 changes: 16 additions & 0 deletions src/oui_lib/compat.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
(* Thin stdlib compat layer until stdcompat catches up with the latest compiler.
*)

module List = struct
include Stdlib.List

let find_mapi f =
let rec aux i = function
| [] -> None
| x :: l ->
begin match f i x with
| Some _ as result -> result
| None -> aux (i+1) l
end in
aux 0
end
90 changes: 80 additions & 10 deletions src/oui_lib/installer_config.ml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
(* *)
(**************************************************************************)

open Compat

type man_section =
| Man_dir of string
| Man_files of string list
Expand Down Expand Up @@ -43,14 +45,31 @@ type manpages =

type expanded_manpages = (string * string list) list

type plugin =
{ name : string
; app_name : string
; plugin_dir : string
; lib_dir : string
; dyn_deps : string list [@default []]
}
[@@deriving yojson {meta = true}]

type plugin_dirs =
{ plugins_dir : string
; lib_dir : string
}
[@@deriving yojson {meta = true}]

type 'manpages t = {
name : string;
fullname : string ;
version : string;
exec_files : string list;
exec_files : string list; [@default []]
manpages : 'manpages option; [@default None]
environment : (string * string) list; [@default []]
unique_id : string;
plugins : plugin list; [@default []]
plugin_dirs : plugin_dirs option; [@default None]
wix_manufacturer : string;
wix_description : string option; [@default None]
wix_tags : string list; [@default []]
Expand Down Expand Up @@ -116,14 +135,22 @@ let manpages_of_expanded l =
let errorf fmt =
Printf.ksprintf (fun s -> Error s) fmt

let dir_in ~bundle_dir path =
OpamFilename.Op.(bundle_dir / path)

let file_in ~bundle_dir path =
OpamFilename.Op.(bundle_dir // path)

let can_exec perm =
Int.equal (perm land 0o001) 0o001
&& Int.equal (perm land 0o010) 0o010
&& Int.equal (perm land 0o100) 0o100

let errors_list l =
List.filter_map (function Ok _ -> None | Error msg -> Some msg) l

let collect_errors ~f l =
List.map f l
|> List.filter_map (function Ok _ -> None | Error msg -> Some msg)
List.map f l |> errors_list

let collect_error_opt ~f x =
match x with
Expand All @@ -150,7 +177,7 @@ let check_file ~field file =
let check_exec ~bundle_dir rel_path =
let open Letop.Result in
let field = "exec_files" in
let path = OpamFilename.Op.(bundle_dir // rel_path) in
let path = file_in ~bundle_dir rel_path in
let path_str = OpamFilename.to_string path in
let* () = check_file ~field:"exec_files" path in
let stats = Unix.stat path_str in
Expand All @@ -163,20 +190,35 @@ let check_man_section ~bundle_dir (name, man_section) =
let field = "manpages." ^ name in
match man_section with
| Man_dir d ->
let dir = OpamFilename.Op.(bundle_dir / d) in
check_dir ~field dir
check_dir ~field (dir_in ~bundle_dir d)
|> Result.map_error (fun msg -> [msg])
| Man_files l ->
let errs =
collect_errors l
~f:(fun f ->
let page = OpamFilename.Op.(bundle_dir // f) in
check_file ~field page)
collect_errors l ~f:(fun f -> check_file ~field (file_in ~bundle_dir f))
in
match errs with
| [] -> Ok ()
| _ -> Error errs

let check_plugin_dirs ~bundle_dir plugin_dirs =
match plugin_dirs with
| None -> []
| Some {plugins_dir; lib_dir} ->
errors_list
[ check_dir ~field:"plugin_dirs.plugins_dir"
(dir_in ~bundle_dir plugins_dir)
; check_dir ~field:"plugin_dirs.lib_dir" (dir_in ~bundle_dir lib_dir)
]

let check_plugin ~bundle_dir
{app_name = _; name = _; plugin_dir; lib_dir; dyn_deps} =
errors_list
[ check_dir ~field:"plugins.plugin_dir" (dir_in ~bundle_dir plugin_dir)
; check_dir ~field:"plugins.lib_dir" (dir_in ~bundle_dir lib_dir)
]
@ collect_errors dyn_deps
~f:(fun d -> check_dir ~field:"plugin.dyn_deps" (dir_in ~bundle_dir d))

let expand_man_section ~bundle_dir man_section =
match man_section with
| Man_files l -> l
Expand Down Expand Up @@ -218,9 +260,12 @@ let check_and_expand ~bundle_dir user =
(List.map
(fun d -> OpamFilename.Op.(bundle_dir / d)) user.macos_symlink_dirs)
in
let plugin_errors = List.concat_map (check_plugin ~bundle_dir) user.plugins in
let plugin_dirs_errors = check_plugin_dirs ~bundle_dir user.plugin_dirs in
let all_errors =
exec_errors @ manpages_errors @ wix_icon_error @ wix_dlg_bmp_error
@ wix_banner_bmp_error @ wix_license_error @ macos_symlink_dirs_errors
@ plugin_dirs_errors @ plugin_errors
in
match all_errors with
| [] ->
Expand Down Expand Up @@ -248,6 +293,8 @@ module String_set = Set.Make(String)

let keys = String_set.of_list Yojson_meta.keys
let manpages_keys = String_set.of_list Yojson_meta_manpages.keys
let plugin_keys = String_set.of_list Yojson_meta_plugin.keys
let plugin_dirs_keys = String_set.of_list Yojson_meta_plugin_dirs.keys

let first_invalid_key ~keys assoc_list =
List.find_map
Expand All @@ -271,6 +318,22 @@ let pretty_object_error ~file ~keys ?field json =
in
invalid_config ~file "%sshould be a JSON object" prefix

let pretty_plugin_error ~file json =
match json with
| `List l ->
List.find_mapi
(fun i elm ->
match elm with
| `Assoc l ->
Option.map
(fun key -> invalid_config ~file "invalid key plugins.[%d].%s" i key)
(first_invalid_key ~keys:plugin_keys l)
| _ ->
Some (invalid_config ~file "plugins.[%d] should be a JSON object" i))
l
| _ ->
Some (invalid_config ~file "plugins should be a JSON array")

(* Turn a derived of_yojson error message into a user friendly one when
possible. *)
let pretty_error ~file ~msg json =
Expand All @@ -279,6 +342,13 @@ let pretty_error ~file ~msg json =
| "Installer_config.manpages", `Assoc l ->
pretty_object_error ~file ~keys:manpages_keys ~field:"manpages"
(List.assoc "manpages" l)
| "Installer_config.plugin_dirs", `Assoc l ->
pretty_object_error ~file ~keys:plugin_dirs_keys ~field:"plugin_dirs"
(List.assoc "plugin_dirs" l)
| "Installer_config.plugin", `Assoc l ->
(match pretty_plugin_error ~file (List.assoc "plugins" l) with
| None -> invalid_config ~file "please report upstream"
| Some err -> err)
| msg, _ ->
let field_name =
match String.split_on_char '.' msg with
Expand Down
18 changes: 18 additions & 0 deletions src/oui_lib/installer_config.mli
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ type manpages =
; man8 : man_section
}

type plugin =
{ name : string
; app_name : string
; plugin_dir : string
; lib_dir : string
; dyn_deps : string list [@default []]
}

type plugin_dirs =
{ plugins_dir : string
; lib_dir : string
}

(** Manpages as association list from man section name to list of manpages *)
type expanded_manpages = (string * string list) list

Expand All @@ -44,6 +57,11 @@ type 'manpages t = {
unique_id : string;
(** Unique ID in reverse DNS format. Used by macOS and Wix backends.
Deduced from fields {i maintainer} and {i name} in opam. *)
plugins: plugin list;
(** List of plugins for external applications within the bundle. *)
plugin_dirs: plugin_dirs option;
(** Paths to directories in the bundle where external plugin should be
installed. *)
wix_manufacturer : string;
(** Product manufacturer. Deduced from field {i maintainer} in opam file *)
wix_description : string option;
Expand Down
Loading
Loading