package oui
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
A tool that builds standalone installers for your OCaml applications
Install
dune-project
Dependency
Authors
Maintainers
Sources
oui-0.1.0.tbz
sha256=b806a63c9acd581a00729a48f1faf86b294cc82936fc705fc628a20ede33f977
sha512=e61c35a6e08671634c1edabd7b0add926ac6e0c6c43c9e0a095e89efab7696b6ae5cc1ea0844283a80640c19adb36478bdfb2414bbcab5077ceb7fd16ae45e66
doc/src/oui.lib/installer_config.ml.html
Source file installer_config.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451(**************************************************************************) (* *) (* Copyright 2025 OCamlPro *) (* *) (* All rights reserved. This file is distributed under the terms of the *) (* GNU Lesser General Public License version 2.1, with the special *) (* exception on linking described in the file LICENSE. *) (* *) (**************************************************************************) open Compat type exec_file = { path : string; symlink : bool; [@default true] deps : bool; [@default true] } [@@deriving yojson {meta = true}] let exec_file_to_yojson exec_file = if exec_file.symlink && exec_file.deps then `String exec_file.path else [%to_yojson: exec_file] exec_file let exec_file_of_yojson : Yojson.Safe.t -> (exec_file, string) result = function | `String path -> Ok ({ path; symlink = true; deps = true}) | `Assoc _ as json -> let open Letop.Result in let* exec_file = [%of_yojson: exec_file] json in Ok (exec_file) | _ -> Error "Invalid exec_file, should be a JSON string or an object." type man_section = | Man_dir of string | Man_files of string list let man_section_to_yojson = function | Man_dir s -> `String s | Man_files l -> `List (List.map (fun s -> `String s) l) let man_section_of_yojson : Yojson.Safe.t -> (man_section, string) result = function | `String s -> Ok (Man_dir s) | `List (_::_) as json -> let open Letop.Result in let* files = [%of_yojson: string list] json in Ok (Man_files files) | _ -> Error "Invalid man_section, should be a JSON string or a non empty array of \ strings." type manpages = { man1 : man_section [@default Man_files []] ; man2 : man_section [@default Man_files []] ; man3 : man_section [@default Man_files []] ; man4 : man_section [@default Man_files []] ; man5 : man_section [@default Man_files []] ; man6 : man_section [@default Man_files []] ; man7 : man_section [@default Man_files []] ; man8 : man_section [@default Man_files []] } [@@deriving yojson {meta = true}] 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 vars = { install_path : string } type ('manpages, 'env_val) t = { name : string; fullname : string ; version : string; exec_files : exec_file list; [@default []] manpages : 'manpages option; [@default None] environment : (string * 'env_val) 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 []] wix_icon_file : string option; [@default None] wix_dlg_bmp_file : string option; [@default None] wix_banner_bmp_file : string option; [@default None] wix_license_file : string option; [@default None] macos_symlink_dirs : string list; [@default []] } [@@deriving yojson {meta = true}] type user = (manpages, String_with_vars.t) t [@@deriving yojson] type internal = (expanded_manpages, string) t let manpages_to_list mnpgs_opt = match mnpgs_opt with | None -> [] | Some mnpgs -> [ ("man1", mnpgs.man1) ; ("man2", mnpgs.man2) ; ("man3", mnpgs.man3) ; ("man4", mnpgs.man4) ; ("man5", mnpgs.man5) ; ("man6", mnpgs.man6) ; ("man7", mnpgs.man7) ; ("man8", mnpgs.man8) ] |> List.filter (function (_, Man_files []) -> false | _ -> true) let manpages_of_expanded l = let nil = Man_files [] in let init = { man1 = nil; man2 = nil; man3 = nil; man4 = nil; man5 = nil; man6 = nil ; man7 = nil; man8 = nil } in List.fold_left (fun acc (section, pages) -> match acc, section with | {man1 = Man_files []; _}, "man1" -> {acc with man1 = Man_files pages} | {man2 = Man_files []; _}, "man2" -> {acc with man2 = Man_files pages} | {man3 = Man_files []; _}, "man3" -> {acc with man3 = Man_files pages} | {man4 = Man_files []; _}, "man4" -> {acc with man4 = Man_files pages} | {man5 = Man_files []; _}, "man5" -> {acc with man5 = Man_files pages} | {man6 = Man_files []; _}, "man6" -> {acc with man6 = Man_files pages} | {man7 = Man_files []; _}, "man7" -> {acc with man7 = Man_files pages} | {man8 = Man_files []; _}, "man8" -> {acc with man8 = Man_files pages} | _, ("man1"|"man2"|"man3"|"man4"|"man5"|"man6"|"man7"|"man8") -> invalid_arg @@ Printf.sprintf "%s: multiple occurences of the same section." __FUNCTION__ | _, _ -> invalid_arg @@ Printf.sprintf "%s: Invalid manpage section %S." __FUNCTION__ section) init 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 ?(exec=false) ~bundle_dir path = let full_path p = OpamFilename.Op.(bundle_dir // p) in match OpamStd.Sys.os () with | OpamStd.Sys.Win32 | OpamStd.Sys.Cygwin -> if exec then full_path (System.maybe_exe ~dir:bundle_dir ~path) else full_path path | _ -> full_path path let can_exec perm = match OpamStd.Sys.os () with | OpamStd.Sys.Win32 | OpamStd.Sys.Cygwin -> true | _ -> 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 |> errors_list let collect_error_opt ~f x = match x with | None -> [] | Some x -> match f x with | Ok () -> [] | Error e -> [e] let guard cond fmt = if cond then Printf.ksprintf (fun _ -> Ok ()) fmt else errorf fmt let check_unique_id unique_id = let valid_first_char c = c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c = '_' in let valid_char c = valid_first_char c || c >= '0' && c <= '9' || c = '.' in guard (String.length unique_id > 0 && valid_first_char unique_id.[0] && String.for_all valid_char unique_id) "unique_id: '%s' has invalid characters (must contain only alphanumeric, \ underscore or dot, must start with alphabetic or underscore)" unique_id let check_dir ~field dir = guard (OpamFilename.exists_dir dir) "%s: directory %s does not exist" field (OpamFilename.Dir.to_string dir) let check_file ~field file = guard (OpamFilename.exists file) "%s: file %s does not exist" field (OpamFilename.to_string file) let check_exec ~bundle_dir (exec_file : exec_file) = let open Letop.Result in let rel_path = exec_file.path in let field = "exec_files" in let path = file_in ~exec:true ~bundle_dir rel_path in let path_str = OpamFilename.to_string path in let* () = check_file ~field path in let stats = Unix.stat path_str in let perm = stats.st_perm in guard (can_exec perm) "%s: file %s does not have exec permissions" field path_str let check_man_section ~bundle_dir (name, man_section) = let field = "manpages." ^ name in match man_section with | Man_dir d -> 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 -> 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 | Man_dir d -> let dir = OpamFilename.Op.(bundle_dir / d) in let files = OpamFilename.files dir in ListLabels.map files ~f:(fun file -> let base = OpamFilename.(Base.to_string (basename file)) in Filename.concat d base) let expand_environment ~vars env = let { install_path } = vars in let expanded, warnings = List.fold_left (fun (expanded, warnings) (var, value) -> let res = String_with_vars.subst ~install_path value in let e = (var, res.subst_string) in let w = List.map (Printf.sprintf "environment.%s: unknown var %s" var) res.unknown_vars in e::expanded, List.rev_append w warnings) ([], []) env in List.rev expanded, List.rev warnings let check_and_expand ~bundle_dir ~vars user = let id_errors = collect_errors ~f:(check_unique_id) [user.unique_id] in let exec_errors = collect_errors ~f:(check_exec ~bundle_dir) user.exec_files in let manpages = manpages_to_list user.manpages in let manpages_errors = collect_errors ~f:(check_man_section ~bundle_dir) manpages |> List.concat in let wix_icon_error = collect_error_opt ~f:(check_file ~field:"wix_icon_file") (Option.map OpamFilename.of_string user.wix_icon_file) in let wix_dlg_bmp_error = collect_error_opt ~f:(check_file ~field:"wix_dlg_bmp_file") (Option.map OpamFilename.of_string user.wix_dlg_bmp_file) in let = collect_error_opt ~f:(check_file ~field:"wix_banner_bmp_file") (Option.map OpamFilename.of_string user.wix_banner_bmp_file) in let wix_license_error = collect_error_opt ~f:(check_file ~field:"wix_license_file") (Option.map OpamFilename.of_string user.wix_license_file) in let macos_symlink_dirs_errors = collect_errors ~f:(check_dir ~field:"macos_symlink_dirs") (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 = id_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 let environment, warnings = expand_environment ~vars user.environment in let res = match all_errors with | [] -> let manpages = ListLabels.filter_map manpages ~f:(fun (section_name, man_section) -> let expanded = expand_man_section ~bundle_dir man_section in match expanded with | [] -> None | _ -> Some (section_name, expanded)) |> function | [] -> None | l -> Some l in Ok {user with manpages; environment} | _ -> Error (`Inconsistent_config all_errors) in res, warnings let invalid_config ~file fmt = Printf.ksprintf (fun s -> `Invalid_config s) ("Could not parse installer config %s: " ^^ fmt) file 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 (fun (key, _val) -> if String_set.mem key keys then None else Some key) assoc_list let pretty_object_error ~file ~keys ?field json = match json with | `Assoc l -> (match first_invalid_key ~keys l with | None -> invalid_config ~file "please report upstream" | Some key -> let key = match field with None -> key | Some f -> f ^ "." ^ key in invalid_config ~file "invalid key %S" key) | _ -> let prefix = match field with | None -> "" | Some f -> f ^ " " 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 = match msg, json with | "Installer_config.t", _ -> pretty_object_error ~file ~keys 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 | ["Installer_config"; "t"; field_name] -> field_name | ["Installer_config"; subtype; field_name] -> subtype ^ "." ^ field_name | _ -> msg in invalid_config ~file "missing or invalid field %S" field_name let load filename = let file = (OpamFilename.to_string filename) in let json = Yojson.Safe.from_file file in match user_of_yojson json with | Ok user_config -> Ok user_config | Error msg -> Error (pretty_error ~file ~msg json) let save t filename = Yojson.Safe.to_file (OpamFilename.to_string filename) (user_to_yojson t)
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>