Source file pkgbuild_backend.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
(**************************************************************************)
(*                                                                        *)
(*    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 OpamFilename.Op

let vars : Installer_config.vars = { install_path = "$INSTALL_PATH" }

let create_work_dir () =
  let tmp_dir = Filename.get_temp_dir_name () in
  let work_dir_name = Printf.sprintf "oui-macos-%d" (Random.int 1000000) in
  let work_dir_path = Filename.concat tmp_dir work_dir_name in
  let work_dir = OpamFilename.Dir.of_string work_dir_path in
  OpamFilename.mkdir work_dir;
  work_dir

let copy_manpages bundle ~bundle_dir ~manpages =
  match manpages with
  | None -> ()
  | Some man_sections ->
    List.iter (fun (section, files) ->
        let relative_path = Filename.concat "man" section in
        let section_dir = Macos_app_bundle.add_subdir bundle ~relative_path in
        List.iter (fun file_path ->
            let src = bundle_dir // file_path in
            if OpamFilename.exists src then
              let basename = OpamFilename.basename src in
              let dst = section_dir // OpamFilename.Base.to_string basename in
              OpamFilename.copy ~src ~dst
            else
              OpamConsole.warning "Manpage not found: %s" file_path
          ) files
      ) man_sections

let create_info_plist bundle ~installer_config =
  let plist = Info_plist.make_info_plist
      ~bundle_id:bundle.Macos_app_bundle.bundle_id
      ~executable:bundle.binary_name
      ~name:bundle.app_name
      ~display_name:installer_config.Installer_config.fullname
      ~version:installer_config.version
  in
  let plist_path = bundle.contents // "Info.plist" in
  Info_plist.save plist plist_path;
  OpamConsole.msg "Created Info.plist: %s\n"
    (OpamFilename.to_string plist_path)

let handle_dylibs bundle ~binary_dst =
  OpamConsole.msg "Processing dylib dependencies...\n";
  let dylibs = Otool.get_dylibs binary_dst in
  if List.length dylibs = 0 then
    OpamConsole.msg "  No external dylibs found\n"
  else
    List.iter
      (fun dylib ->
         ignore (Macos_app_bundle.copy_dylib bundle ~dylib))
      dylibs;
    Install_name_tool.relocate_to_executable_path binary_dst

(** Generate install.conf content *)
let generate_install_conf ~resources_path ~(installer_config : Installer_config.internal) =
  let lines =
    [ Printf.sprintf "version=%s" installer_config.version ]
    @ (match installer_config.plugin_dirs with
        | None -> []
        | Some pd ->
          [ Printf.sprintf "plugins=%s/%s" resources_path pd.Installer_config.plugins_dir
          ; Printf.sprintf "lib=%s/%s" resources_path pd.lib_dir
          ])
  in
  String.concat "\n" lines ^ "\n"

let write_install_conf bundle ~installer_config =
  let resources_path =
    Printf.sprintf "/Applications/%s.app/Contents/Resources"
      (String.capitalize_ascii installer_config.Installer_config.name)
  in
  let content = generate_install_conf ~resources_path ~installer_config in
  let install_conf_path = bundle.Macos_app_bundle.resources // "install.conf" in
  OpamFilename.write install_conf_path content;
  OpamConsole.msg "Created install.conf: %s\n"
    (OpamFilename.to_string install_conf_path)

(** Create the .pkg installer from the bundle *)
let create_installer
    ~(installer_config : Installer_config.internal) ~bundle_dir installer =
  Random.self_init ();

  let work_dir = create_work_dir () in
  OpamConsole.msg "Working directory: %s\n"
    (OpamFilename.Dir.to_string work_dir);

  (* Create .app bundle structure *)
  let bundle = Macos_app_bundle.create ~installer_config ~work_dir in

  (* Copy all bundle contents to Resources *)
  Macos_app_bundle.copy_bundle_contents bundle ~bundle_dir;

  (* Install main binary to MacOS directory (if exec_files provided) *)
  let binary_name = match installer_config.exec_files with
    | [] ->
      (* Plugin-only bundle - no main binary *)
      OpamConsole.msg "No exec_files specified, creating plugin-only package\n";
      None
    | binary :: _ ->
      let binary_src = bundle_dir // binary.path in
      let binary_dst =
        Macos_app_bundle.install_binary bundle ~binary_path:binary_src
      in
      handle_dylibs bundle ~binary_dst;
      (* Sign the binary with ad-hoc signature *)
      OpamConsole.msg "Signing binary...\n";
      Codesign.sign_binary_adhoc binary_dst;
      Some bundle.binary_name
  in

  create_info_plist bundle ~installer_config;

  copy_manpages bundle ~bundle_dir ~manpages:installer_config.manpages;

  (* Create symlinks for dune-site relocatable support *)
  List.iter (fun dir_name ->
      let src_dir = bundle.resources / dir_name in
      let link_path =
        OpamFilename.Dir.to_string bundle.contents ^ "/" ^ dir_name
      in
      if OpamFilename.exists_dir src_dir then
        (OpamConsole.msg "Creating symlink: Contents/%s -> Resources/%s\n"
           dir_name dir_name;
         Unix.symlink ("Resources/" ^ dir_name) link_path)
      else
        OpamConsole.warning
          "Directory %s not found in Resources, skipping symlink"
          dir_name
    ) installer_config.macos_symlink_dirs;

  (* Write install.conf for plugin support *)
  write_install_conf bundle ~installer_config;

  (* Create postinstall script *)
  let scripts_dir = work_dir / "scripts" in
  let has_binary = Option.is_some binary_name in
  let binary_name_for_scripts = match binary_name with
    | Some n -> n
    | None -> installer_config.name
  in
  let postinstall_content = Macos_postinstall.generate_postinstall_script
      ~env:installer_config.environment
      ~app_name:bundle.app_name
      ~binary_name:binary_name_for_scripts
      ~has_binary
      ~plugins:installer_config.plugins
      ()
  in
  let _postinstall_path = Macos_postinstall.save_postinstall_script
      ~content:postinstall_content
      ~scripts_dir
  in

  (* Create uninstall script in bundle *)
  let uninstall_content = Macos_postinstall.generate_uninstall_script
      ~app_name:bundle.app_name
      ~binary_name:binary_name_for_scripts
      ~has_binary
      ~plugins:installer_config.plugins
  in
  let _uninstall_path = Macos_postinstall.save_uninstall_script
      ~content:uninstall_content
      ~resources_dir:bundle.resources
  in

  let component_pkg_path =
    let base = OpamFilename.chop_extension installer in
    OpamFilename.add_extension base "-component.pkg" in

  let install_location =
    Printf.sprintf "/Applications/%s.app" bundle.app_name in

  OpamConsole.msg "Creating component package...\n";
  let pkgbuild_args : System.pkgbuild_args = {
    root = bundle.app_bundle_dir;
    identifier = bundle.bundle_id;
    version = installer_config.version;
    install_location;
    scripts = Some scripts_dir;
    output = component_pkg_path;
  } in
  System.call_unit System.Pkgbuild pkgbuild_args;

  OpamConsole.msg "Creating final installer package...\n";
  let productbuild_args : System.productbuild_args = {
    package = component_pkg_path;
    output = installer;
  } in
  System.call_unit System.Productbuild productbuild_args;

  OpamFilename.remove component_pkg_path;

  OpamFilename.rmdir work_dir;

  OpamConsole.formatted_msg "Created: %s\n"
    (OpamConsole.colorise `green (OpamFilename.to_string installer))