package dkml-package-console

  1. Overview
  2. Docs

Source file windows_registry.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
(*
     * https://nsis.sourceforge.io/Add_uninstall_information_to_Add/Remove_Programs
     * https://docs.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key
*)

open Dkml_install_runner.Error_handling.Monad_syntax
open Author_types

(** Find reg.exe. For safety we'll look in the same directory as cmd.exe
    first. *)
let find_reg_exe () =
  let* std_search_path, _fl =
    Dkml_install_runner.Error_handling.map_msg_error_to_progress
      (Bos.OS.Cmd.search_path_dirs (Bos.OS.Env.opt_var ~absent:"" "PATH"))
  in
  let* search, _fl =
    (* Ex. C:\WINDOWS\system32\reg.exe *)
    match Bos.OS.Env.var "COMSPEC" with
    | None -> return std_search_path
    | Some comspec ->
        (* Ex. C:\WINDOWS\system32 *)
        let comspec_dir = Fpath.(v comspec |> parent) in
        let* exists, _fl =
          Dkml_install_runner.Error_handling.map_msg_error_to_progress
            (Bos.OS.Dir.exists comspec_dir)
        in
        if exists then return (comspec_dir :: std_search_path)
        else return std_search_path
  in
  Dkml_install_runner.Error_handling.map_msg_error_to_progress
    (Bos.OS.Cmd.get_tool ~search Bos.Cmd.(v "reg"))

module Add_remove_programs = struct
  let registry_template_pf =
    Printf.sprintf
      {|

Windows Registry Editor Version 5.00

[%s]
"DisplayName"="%s"
"DisplayVersion"="%s"
%s
"Publisher"="%s"
"InstallDate"="%s"
"InstallLocation"="%s"
"QuietUninstallString"="\"%s\\uninstall.exe\" --ci --prefix \"%s\" --quiet --color=never"
"UninstallString"="\"%s\\uninstall.exe\" --prefix \"%s\""
"URLInfoAbout"="%s"
"URLUpdateInfo"="%s"
"HelpLink"="%s"
%s
"NoModify"=dword:00000001
"NoRepair"=dword:00000001
"Language"=%s
|}

  (** [registry_key ~program_name] is
    ["HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\<program_name.name_camel_case_nospaces>"] *)
  let registry_key ~(program_name : program_name) =
    "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"
    ^ program_name.name_camel_case_nospaces

  let registry_template ~installation_prefix ~(organization : organization)
      ~(program_name : program_name) ~(program_info : program_info)
      ~program_version ~app_ico_path_opt =
    let escaped_installation_prefix =
      String.escaped (Fpath.to_string installation_prefix)
    in
    let localnow = Unix.localtime (Unix.time ()) in
    let localyyyymmdd =
      Printf.sprintf "%04d%02d%02d" (1900 + localnow.tm_year)
        (localnow.tm_mon + 1) localnow.tm_mday
    in

    registry_template_pf
      (* [HKEY\...\_] *) (registry_key ~program_name)
      (* DisplayName=%s *)
      program_name.name_full (* DisplayVersion=%s *) program_version
      (* "DisplayIcon"="C:\\Users\\beckf\\AppData\\Local\\Programs\\DiskuvOCaml\\32x32.ico" *)
      (Option.fold ~none:""
         ~some:(fun app_ico_path ->
           let escaped_app_ico_path =
             String.escaped (Fpath.to_string app_ico_path)
           in
           Printf.sprintf {|"DisplayIcon"="%s"|} escaped_app_ico_path)
         app_ico_path_opt)
      (* Publisher=%s *)
      organization.legal_name (* InstallDate=%s *) localyyyymmdd
      (* InstallLocation=%s *)
      escaped_installation_prefix
      (* QuietUninstallString=%s --prefix %s *)
      escaped_installation_prefix escaped_installation_prefix
      (* UninstallString=%s --prefix %s *)
      escaped_installation_prefix escaped_installation_prefix
      (* URLInfoAbout=%s *)
      (Option.fold ~none:""
         ~some:(Printf.sprintf {|"URLInfoAbout"="%s"|})
         program_info.url_info_about_opt)
      (* URLUpdateInfo=%s *)
      (Option.fold ~none:""
         ~some:(Printf.sprintf {|"URLUpdateInfo"="%s"|})
         program_info.url_update_info_opt)
      (* HelpLink=%s *)
      (Option.fold ~none:""
         ~some:(Printf.sprintf {|"HelpLink"="%s"|})
         program_info.help_link_opt)
      (* "EstimatedSize"=dword:0015cff7 | units in KB *)
      (Option.fold ~none:""
         ~some:(fun estimated_byte_size ->
           Printf.sprintf {|"EstimatedSize"=dword:%08Lx|}
             (Int64.div estimated_byte_size 1024L))
         program_info.estimated_byte_size_opt)
      (* "Language"=%s | default is en-US (0x409) *)
      (Printf.sprintf "dword:%08x"
         (Option.value ~default:0x409 program_info.windows_language_code_id_opt))

  let delete_program_entry ~program_name =
    (* Delete from registry.

       https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-delete *)
    let* reg_exe, _fl = find_reg_exe () in
    let cmd =
      Bos.Cmd.(
        v (Fpath.to_string reg_exe)
        % "delete" % registry_key ~program_name % "/f")
    in
    Logs.debug (fun l -> l "Running:@ %a" Bos.Cmd.pp cmd);
    Spawn.spawn ~err_ok:true cmd

  let write_program_entry ~installation_prefix ~organization ~program_name
      ~program_assets ~program_version ~program_info =
    (* Make absolute path for installation prefix.

       No guarantee that prefix is not relative like in --prefix=_build/p.
       Since goes into Registry, it needs to be an absolute path. *)
    let* pwd, _fl =
      Dkml_install_runner.Error_handling.map_msg_error_to_progress
        (Bos.OS.Dir.current ())
    in
    let installation_prefix =
      match Fpath.is_rel installation_prefix with
      | true -> Fpath.(pwd // installation_prefix)
      | false -> installation_prefix
    in

    (* Make PREFIX/app.ico if available *)
    let* app_ico_path_opt, _fl =
      match program_assets.logo_icon_32x32_opt with
      | None -> return None
      | Some logo_icon_32x32 ->
          let app_ico_path = Fpath.(installation_prefix / "app.ico") in
          let* (), _fl =
            Dkml_install_runner.Error_handling.map_msg_error_to_progress
              (Bos.OS.File.write app_ico_path logo_icon_32x32)
          in
          return (Some app_ico_path)
    in

    (* Make a TMP/uninstall.reg file *)
    let registry_contents =
      String.trim
        (registry_template ~installation_prefix ~organization ~program_name
           ~program_version ~program_info ~app_ico_path_opt)
    in
    let* registry_file, _fl =
      Dkml_install_runner.Error_handling.map_msg_error_to_progress
        (Bos.OS.File.tmp "uninstall%s.reg")
    in
    let* (), _fl =
      Dkml_install_runner.Error_handling.map_msg_error_to_progress
        (Bos.OS.File.write registry_file registry_contents)
    in

    (* Write into registry.

       https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-import *)
    let* reg_exe, _fl = find_reg_exe () in
    Logs.info (fun l -> l "Writing to registry for Add/Remove Programs");
    let cmd =
      Bos.Cmd.(
        v (Fpath.to_string reg_exe) % "import" % Fpath.to_string registry_file)
    in
    Logs.debug (fun l ->
        l "Running:@ %a@ with the contents:@ @[<v>  %a@]" Bos.Cmd.pp cmd
          Fmt.lines registry_contents);
    Spawn.spawn cmd
end
OCaml

Innovation. Community. Security.