unison

Fork of Unison, a bi-directional file synchronization tool
git clone git://git.laack.co/unison.git
Log | Files | Refs | README | LICENSE

make_tools.ml (22565B)


      1 #directory "+unix";;
      2 #load "unix.cma";;
      3 
      4 module type TOOLS = sig
      5   module Env : Map.S with type key = string
      6   val env : string Env.t       (* Environment variables *)
      7   val args : string Env.t      (* Command line argument variables *)
      8   val inputs : string Env.t    (* [env] + [args], with [args] taking priority *)
      9   val vars : string Env.t ref  (* Internally defined variables *)
     10   val ( .$() ) : string Env.t -> string -> string (* Get value from any map *)
     11   val ( $ ) : string -> string (* Value from [vars] with fallback to [inputs] *)
     12   val ( <-- ) : string -> string -> unit  (* Set value in [vars] *)
     13   val ( <-+= ) : string -> string -> unit (* Append to value in [vars] (defaults to value in [inputs]) *)
     14   val ( <--? ) : string -> string -> string (* Copy from [inputs] to [vars], or set a default *)
     15   val shell : ?err_null:bool -> ?exit_status:Unix.process_status ref -> string -> string
     16   val shell_input : ?exit_status:Unix.process_status ref -> string -> string -> string
     17   val get_command : string -> string option
     18   val is_empty : string -> bool
     19   val not_empty : string -> bool
     20   val has_substring : string -> string -> bool
     21   val exists : string -> string -> bool
     22   val info : string -> unit
     23   val error : string -> 'a
     24 end
     25 module Make = functor (Tools : TOOLS) -> struct
     26 open Tools
     27 
     28 let output = ref []
     29 let outp s = output := s :: !output
     30 
     31 (* [2023-05] This check is here only temporarily for a smooth transition *)
     32 let () =
     33   if inputs.$("STATIC") = "true" then
     34     error "Variable STATIC is no longer in use. Please set appropriate \
     35       LDFLAGS, like -static, instead. See build instructions."
     36 
     37 (*********************************************************************
     38 *** Compilers ***)
     39 
     40 let tool_prefix = "TOOL_PREFIX" <--?
     41   if inputs.$("OSCOMP") = "cross" then "x86_64-w64-mingw32-" else ""
     42   (* FIXME: OSCOMP is a legacy argument; probably not used by anyone *)
     43 
     44 let ocamlc   = "OCAMLC"   <--? tool_prefix ^ "ocamlc"
     45 let ocamlopt = "OCAMLOPT" <--? tool_prefix ^ "ocamlopt"
     46 
     47 let ocaml_conf_var varname = shell (ocamlc ^ " -config-var " ^ varname)
     48 
     49 (*********************************************************************
     50 *** Try to automatically guess OS ***)
     51 
     52 (* Cygwin is for doing POSIX builds in Windows.
     53    MinGW is for native Windows builds in a minimal POSIX environment.
     54    MSVC is for native Windows builds without POSIX environment. *)
     55 
     56 let osarch = "OSARCH" <--?
     57   match ocaml_conf_var "os_type" with
     58   | "Win32" -> "Win32"   (* Native Windows build *)
     59   | "Cygwin" -> "Cygwin" (* POSIX build for Windows *)
     60   | _ -> shell "uname"
     61 (* Darwin is reported for macOS
     62    SunOS is reported for Solaris, OpenSolaris and derivatives (illumos) *)
     63 
     64 let osarch_win32 = osarch = "Win32"
     65 let osarch_cygwin = osarch = "Cygwin"
     66 let osarch_macos = osarch = "Darwin"
     67 
     68 (*********************************************************************
     69 *** Try to automatically guess UI style ***)
     70 
     71 let () =
     72   if inputs.$("UISTYLE") <> "" then
     73     error "UISTYLE is no longer used. See build instructions."
     74 
     75 let ocaml_libdir = "OCAMLLIBDIR" <--? ocaml_conf_var "standard_library"
     76 
     77 let ocamlfind = get_command "ocamlfind"
     78 
     79 let build_GUI =
     80   let has_lablgtk3 =
     81     match ocamlfind with
     82     | Some cmd -> shell ~err_null:true (cmd ^ " query lablgtk3") |> not_empty
     83     | None -> exists ocaml_libdir "lablgtk3"
     84   in
     85   if not has_lablgtk3 then begin
     86     info "GUI library lablgtk not found. GTK GUI will not be built."
     87   end;
     88   has_lablgtk3
     89 let () = if build_GUI then outp "guimaybe: gui"
     90 
     91 let build_macGUI =
     92   if osarch_macos then begin
     93     (* If XCode is not installed, xcodebuild is just a placeholder telling
     94        that XCode is not installed and any invocation of xcodebuild results
     95        in a non-0 exit code. *)
     96     if Sys.command "xcodebuild -version > /dev/null" = 0 then
     97       true
     98     else begin
     99       info "Not building macOS native GUI because XCode is not installed.";
    100       false
    101     end
    102   end else begin
    103     info "Not on macOS. macOS native GUI will not be built.";
    104     false
    105   end
    106 let () = if build_macGUI then outp "macuimaybe: macui"
    107 
    108 (*********************************************************************
    109 *** Default parameters ***)
    110 
    111 (* Generate backtrace information for exceptions *)
    112 let () = "CAMLFLAGS" <-+= "-g $(INCLFLAGS)"
    113 
    114 (* Use 64-bit file offset if possible (for copy_stubs.c). It is
    115    included here unconditionally since OCaml itself has had this
    116    defined unconditionally since 2002. *)
    117 let () = "CAMLCFLAGS" <-+= "-ccopt -D_FILE_OFFSET_BITS=64"
    118 
    119 let () =
    120   [
    121     "CAMLCFLAGS", "CFLAGS", "-ccopt";
    122     "CAMLCFLAGS", "CPPFLAGS", "-ccopt";
    123     "CAMLLDFLAGS", "LDFLAGS", "-cclib";
    124     "CLIBS", "LDLIBS", "-cclib";
    125   ]
    126   |> List.iter (fun (varname, envname, arg) ->
    127     if not_empty inputs.$(envname) then
    128       varname <-+= arg ^ " \"" ^ inputs.$(envname) ^ "\"")
    129 
    130 (* The messy situation requiring the use of OUTPUT_SEL was fixed in OCaml 4.13.
    131    All usages of OUTPUT_SEL should be removed when 4.13 becomes a requirement. *)
    132 let () =
    133   if osarch_win32 then begin
    134     (* Native Win32 build *)
    135     "EXEC_EXT" <-- ".exe";
    136     "OBJ_EXT" <-- ocaml_conf_var "ext_obj";
    137     if ocaml_conf_var "ccomp_type" = "msvc" then begin
    138       "OUTPUT_SEL" <-- "-Fo";
    139       "CLIBS" <-+= "-cclib user32.lib -cclib \"-link win32rc/unison.res\"";
    140       outp "buildexecutable: win32rc/unison.res";
    141     end else begin
    142       "OUTPUT_SEL" <-- "-o";
    143       "CLIBS" <-+= "-cclib \"-link win32rc/unison.res.lib\"";
    144       outp "buildexecutable: win32rc/unison.res.lib";
    145     end;
    146     (* Make Windows GUI come up with no console window *)
    147     if ($)"UI_WINOS" <> "hybrid" then begin
    148       "CAMLLDFLAGS_GUI" <-- "-cclib \"-subsystem windows\"";
    149       (* -subsystem is a flexlink arg;
    150          respective gcc args: -mwindows or -Wl,--subsystem,windows
    151          respective MSVC linker arg: /SUBSYSTEM:WINDOWS *)
    152     end;
    153     "CWD" <-- ".";
    154     "WINCOBJS" <-+= "system/system_win_stubs" ^ ($)"OBJ_EXT" ^ " lwt/lwt_unix_stubs" ^ ($)"OBJ_EXT";
    155     "WINOBJS" <-- "system/system_win.cmo";
    156     "SYSTEM" <-- "win";
    157     "building_for" <-- "Building for Windows";
    158   end else begin
    159     (* Unix build, or Cygwin POSIX (GNU C) build *)
    160     "OBJ_EXT" <-- ".o";
    161     "OUTPUT_SEL" <-- "'-o '";
    162     "WINOBJS" <-- "";
    163     "SYSTEM" <-- "generic";
    164     (* This is not strictly necessary as Cygwin can do a generic Unix build
    165        (and is actually meant to). *)
    166     if osarch_cygwin then begin
    167       "CWD" <-- ".";
    168       "EXEC_EXT" <-- ".exe";
    169       "CLIBS" <-+= "-cclib win32rc/unison.res.lib";
    170       outp "buildexecutable: win32rc/unison.res.lib";
    171       "building_for" <-- "Building for Cygwin";
    172     end else begin
    173       "CWD" <-- shell "pwd";
    174       "EXEC_EXT" <-- "";
    175       (* openpty is in the libutil library *)
    176       if not osarch_macos && osarch <> "SunOS" then begin
    177         "CLIBS" <-+= "-cclib -lutil"
    178       end;
    179       if osarch = "SunOS" then begin
    180         (* ACL functions *)
    181         "CLIBS" <-+= "-cclib -lsec";
    182         "CLIBS" <-+= "-cclib -lsendfile";
    183       end;
    184       "building_for" <-- "Building for Unix";
    185     end;
    186     outp "manpage: manpagefile";
    187     outp "docs: docfiles";
    188     outp "clean::\n\tcd ../doc && $(MAKE) clean";
    189   end
    190 
    191 (*********************************************************************
    192 *** Compilation boilerplate ***)
    193 
    194 let native =
    195   let native =
    196     let nat = ($)"NATIVE" |> String.lowercase_ascii in
    197     if nat <> "true" && nat <> "false" then
    198       get_command ocamlopt <> None
    199     else bool_of_string nat
    200   in
    201   "NATIVE" <-- string_of_bool native;
    202   native
    203 
    204 let () =
    205   if native then begin
    206     (* Set up for native code compilation *)
    207     "CAMLC" <-- ocamlopt;
    208     "CAMLOBJS" <-- "$(OCAMLOBJS:.cmo=.cmx)";
    209     "CAMLOBJS_TUI" <-- "$(OCAMLOBJS_TUI:.cmo=.cmx)";
    210     "CAMLOBJS_GUI" <-- "$(OCAMLOBJS_GUI:.cmo=.cmx)";
    211     "CAMLOBJS_FSM" <-- "$(FSMOCAMLOBJS:.cmo=.cmx)";
    212     "CAMLOBJS_MAC" <-- "$(OCAMLOBJS_MAC:.cmo=.cmx)";
    213     "CAMLLIBS" <-- "$(OCAMLLIBS:.cma=.cmxa)";
    214     "CAMLLIBS_TUI" <-- "$(OCAMLLIBS_TUI:.cma=.cmxa)";
    215     "CAMLLIBS_GUI" <-- "$(OCAMLLIBS_GUI:.cma=.cmxa)";
    216     "CAMLLIBS_MAC" <-- "$(OCAMLLIBS_MAC:.cma=.cmxa)";
    217     "CAMLLIBS_FSM" <-- "$(FSMOCAMLLIBS:.cma=.cmxa)";
    218   end else begin
    219     (* Set up for bytecode compilation *)
    220     "CAMLC" <-- ocamlc;
    221     (* -output-complete-exe is available since OCaml 4.10
    222        OCaml > 5.2.0 no longer supports detection of compiler options,
    223        hence the hack of comparing the output to -version. *)
    224     if String.trim (shell (ocamlc ^ " -output-complete-exe -version 2>&1")) =
    225        String.trim (shell (ocamlc ^ " -version")) then begin
    226       "CAMLLDFLAGS" <-+= "-output-complete-exe";  (* can safely strip the binary *)
    227     end else begin
    228       "CAMLLDFLAGS" <-+= "-custom";
    229     end;
    230     "CAMLOBJS" <-- "$(OCAMLOBJS)";
    231     "CAMLOBJS_TUI" <-- "$(OCAMLOBJS_TUI)";
    232     "CAMLOBJS_GUI" <-- "$(OCAMLOBJS_GUI)";
    233     "CAMLOBJS_MAC" <-- "$(OCAMLOBJS_MAC)";
    234     "CAMLOBJS_FSM" <-- "$(FSMOCAMLOBJS)";
    235     "CAMLLIBS" <-- "$(OCAMLLIBS)";
    236     "CAMLLIBS_TUI" <-- "$(OCAMLLIBS_TUI)";
    237     "CAMLLIBS_GUI" <-- "$(OCAMLLIBS_GUI)";
    238     "CAMLLIBS_MAC" <-- "$(OCAMLLIBS_MAC)";
    239     "CAMLLIBS_FSM" <-- "$(FSMOCAMLLIBS)";
    240   end
    241 
    242 let () = "WINDRES" <--
    243   (if not_empty tool_prefix then tool_prefix else begin
    244     let cc = Filename.basename (ocaml_conf_var "c_compiler") in
    245     let rec extract_prefix pre l =
    246       match l with
    247       | [] -> "" (* could not detect the prefix *)
    248       | x :: xs ->
    249           let found_base =
    250             String.length x > 1 && String.(sub x 0 2 |> lowercase_ascii) = "cl" ||
    251             String.length x > 2 && String.(sub x 0 3 |> lowercase_ascii) = "gcc"
    252           in
    253           if not found_base then extract_prefix (x :: pre) xs
    254           else if pre = [] then "" else (String.concat "-" (List.rev pre)) ^ "-"
    255     in
    256     extract_prefix [] (String.split_on_char '-' cc)
    257   end) ^ "windres"
    258 
    259 let () =
    260   if is_empty inputs.$("_NMAKE_VER") then begin
    261     if is_empty inputs.$("MAKE") || has_substring "FRC true" (
    262         shell_input "_cf_test: FRC ; @echo $^ true\nFRC: ;"
    263           (inputs.$("MAKE") ^ " -f -")) then
    264       "ALL__SRC" <-- "$^"  (* GNU and POSIX make, new versions of BSD make *)
    265     else
    266       "ALL__SRC" <-- "$>"  (* Older versions of BSD make *)
    267   end else
    268     "ALL__SRC" <-- "$(**)" (* NMAKE; enclose in brackets for safety if not run by NMAKE *)
    269 
    270 let () = "rule_sep" <-- if not_empty inputs.$("ASSUME_DMAKE") then ":=" else ":"
    271 
    272 (*********************************************************************
    273 *** User Interface setup ***)
    274 
    275 let () =
    276   if not build_GUI && (not_empty inputs.$("_NMAKE_VER") || osarch = "OpenBSD") then
    277     "CAMLFLAGS_GUI" <--? "" |> ignore
    278   else
    279   "CAMLFLAGS_GUI" <-+=
    280   match ocamlfind with
    281   | Some cmd ->
    282       (* The weird quoting is required for Windows, but harmless in sh *)
    283       shell (cmd ^ " query -format \"-I \"\"%d\"\"\" lablgtk3") ^ " " ^
    284       shell (cmd ^ " query -format \"-I \"\"%d\"\"\" cairo2")
    285   | None -> "-I +lablgtk3 -I +cairo2"
    286 
    287 let () =
    288   if osarch_macos && is_empty inputs.$("XCODEFLAGS") then
    289     (* Prevent Xcode from trying to build universal binaries by default *)
    290     "XCODEFLAGS" <-- "-arch " ^ (shell "uname -m")
    291 
    292 (*********************************************************************
    293 *** Filesystem monitoring ***)
    294 
    295 let fsmon_outp = ref []
    296 
    297 let fsmonitor_dir =
    298   match osarch with
    299   | "Linux" -> "FSMDIR" <--? "fsmonitor/inotify"
    300   | "SunOS" -> "FSMDIR" <--? "fsmonitor/solaris"
    301   | "Win32" -> "FSMDIR" <--? "fsmonitor/windows"
    302   | "FreeBSD" | "OpenBSD" | "NetBSD" | "DragonFly" ->
    303       begin
    304         let libnotify_exists =
    305           shell "pkg-config --exists libinotify && echo true" = "true" in
    306         if libnotify_exists then begin
    307           let pkg_flags = shell "pkg-config --cflags libinotify 2> /dev/null" in
    308           if not_empty pkg_flags then begin
    309             let libinotify_inc = "-ccopt '" ^ pkg_flags ^ "'" in
    310             fsmon_outp := ("CAMLCFLAGS_FSM = " ^ libinotify_inc) :: !fsmon_outp
    311           end;
    312           let pkg_lib = shell "pkg-config --libs libinotify 2> /dev/null" in
    313           if not_empty pkg_lib then begin
    314             let libinotify_lib = "-cclib '" ^ pkg_lib ^ "'" in
    315             fsmon_outp := ("CLIBS_FSM = " ^ libinotify_lib) :: !fsmon_outp
    316           end;
    317           "FSMDIR" <--? "fsmonitor/inotify"
    318         end else
    319           inputs.$("FSMDIR")
    320       end
    321   | _ -> inputs.$("FSMDIR")
    322 
    323 let build_fsmonitor =
    324   if not_empty fsmonitor_dir then begin
    325     outp ("include " ^ fsmonitor_dir ^ "/Makefile");
    326     List.iter outp !fsmon_outp;
    327     true
    328   end else begin
    329     info "fsmonitor implementation is not available or not configured for this system. fsmonitor will not be built.";
    330     false
    331   end
    332 let () = if build_fsmonitor then outp "fsmonitor: fsmonitorexecutable"
    333 
    334 
    335 let () = "RM" <-- if is_empty inputs.$("_NMAKE_VER") then "rm -f" else "del /f"
    336 
    337 
    338 (*** Output configuration ***)
    339 
    340 let () =
    341   Env.iter (fun k v -> print_string k; print_string " = "; print_endline v) !vars
    342 
    343 let () =
    344   List.iter print_endline (List.rev !output)
    345 
    346 let () =
    347   info "";
    348   info (($)"building_for");
    349   info ("NATIVE = " ^ ($)"NATIVE");
    350   info ""
    351 
    352 end
    353 
    354 
    355 module Tools = struct
    356 
    357 module Env = Map.Make(String)
    358 
    359 let ( .$() ) m n =
    360   try Env.find n m with Not_found ->
    361     try
    362       Env.filter
    363         (fun k _ -> String.(equal (uppercase_ascii n) (uppercase_ascii k))) m
    364       |> Env.choose |> snd
    365     with Not_found -> ""
    366 
    367 let ( .%() ) m n =
    368   try Some (Env.find n m) with Not_found ->
    369     try
    370       Some (Env.filter
    371           (fun k _ -> String.(equal (uppercase_ascii n) (uppercase_ascii k))) m
    372         |> Env.choose |> snd)
    373     with Not_found -> None
    374 
    375 let make_env arr =
    376   let add_var map s =
    377     let k, v =
    378       try
    379         let open String in
    380         let v = index s '=' in
    381         sub s 0 v,
    382         try sub s (v + 1) (length s - v - 1) with Invalid_argument _ -> ""
    383       with Not_found -> s, ""
    384     in
    385     Env.add k v map
    386   in
    387   Array.fold_left add_var Env.empty arr
    388 
    389 let env =
    390   Unix.handle_unix_error Unix.environment () |> make_env
    391 
    392 let args =
    393   (if Array.length Sys.argv < 2 then [||] else
    394     Array.sub Sys.argv 2 (Array.length Sys.argv - 2))
    395   |> make_env
    396 
    397 let inputs =
    398   let override _ a b =
    399     match a, b with
    400     | Some _, None -> a
    401     | _, Some _ -> b
    402     | None, None -> None
    403   in
    404   Env.merge override env args
    405 
    406 let vars = ref Env.empty
    407 
    408 let ( $ ) k =
    409   match !vars.%(k) with Some s -> s | None -> inputs.$(k)
    410 
    411 let ( <-- ) k v =
    412   vars := Env.add k v !vars
    413 
    414 let ( <-+= ) k v =
    415   vars := Env.add k (($)k ^ " " ^ v) !vars
    416 
    417 let ( <--? ) k v =
    418   match !vars.%(k) with
    419   | Some s -> s
    420   | None -> begin
    421       match inputs.%(k) with
    422       | Some s -> vars := Env.add k s !vars; s
    423       | None -> vars := Env.add k v !vars; v
    424       end
    425 
    426 let info msg =
    427   prerr_endline msg
    428 
    429 let error msg =
    430   prerr_string "Error: ";
    431   prerr_endline msg;
    432   exit 2
    433 
    434 let shell_internal ?(err_null = false) ?exit_status ?(input_str = "") cmd =
    435   let shell' open_proc close_proc =
    436     let outp = open_proc () in
    437     let buf = Buffer.create 512 in
    438     let () = try Buffer.add_channel buf outp 16384 with | End_of_file -> () in
    439     (* Trim the final newline *)
    440     let trim_end =
    441       if Buffer.length buf >= 2 then begin
    442         if Buffer.(sub buf (length buf - 2) 2) = "\r\n" then 2
    443         else if Buffer.(sub buf (length buf - 1) 1) = "\n" then 1
    444         else 0
    445       end else if Buffer.length buf >= 1 && Buffer.(sub buf (length buf - 1) 1) = "\n" then 1
    446       else 0
    447     in
    448     let proc_status = close_proc () in
    449     (proc_status, Buffer.(sub buf 0 (length buf - trim_end)))
    450   in
    451   let status, output =
    452     Unix.handle_unix_error (fun () ->
    453       if err_null || input_str <> "" then
    454         let (sh_out, sh_in, _) as sh_full =
    455           Unix.open_process_full cmd [|"PATH=" ^ env.$("PATH")|] in
    456         if input_str <> "" then begin
    457           output_string sh_in input_str;
    458           flush sh_in
    459         end;
    460         let () = close_out sh_in in
    461         shell' (fun () -> sh_out) (fun () -> Unix.close_process_full sh_full)
    462       else
    463         let sh_out = Unix.open_process_in cmd in
    464         shell' (fun () -> sh_out) (fun () -> Unix.close_process_in sh_out)
    465     ) ()
    466   in
    467   begin match exit_status with
    468   | None -> ()
    469   | Some ref -> ref := status
    470   end;
    471   output
    472 
    473 let shell ?err_null ?exit_status cmd =
    474   shell_internal ?err_null ?exit_status cmd
    475 
    476 let shell_input ?exit_status input_str cmd =
    477   shell_internal ?exit_status ~input_str cmd
    478 
    479 let exec cmd =
    480   let cmd = String.concat " " cmd in
    481   print_endline cmd;
    482   match Sys.command cmd with
    483   | 0 -> ()
    484   | e -> error (string_of_int e)
    485 
    486 let path =
    487   env.$("PATH")
    488   |> String.split_on_char (if Sys.win32 then ';' else ':')
    489 
    490 let pathext =
    491   if Sys.win32 || Sys.cygwin then
    492     env.$("PATHEXT") |> String.split_on_char ';'
    493   else
    494     [""]
    495 
    496 let search_in_path ?(path = path) name =
    497   Filename.concat
    498     (List.find
    499       (fun dir -> List.exists
    500         (fun ext -> Sys.file_exists (Filename.concat dir (name ^ ext)))
    501         pathext)
    502       path)
    503     name
    504 
    505 let search_in_path_opt ?path name =
    506   try Some (search_in_path ?path name) with
    507   | Not_found -> None
    508 
    509 let get_command name = search_in_path_opt name
    510 
    511 let exists dir name =
    512   Sys.file_exists (Filename.concat dir name)
    513 
    514 let rm =
    515   let rm' n =
    516     if String.length n > 0 && n.[0] <> '-' && n.[0] <> '/' && n.[0] <> '\\'
    517         && not (String.contains n '*') then
    518       try Sys.remove n with Sys_error _ -> ()
    519   in
    520   Array.iter rm'
    521 
    522 let is_empty s =
    523   String.length (String.trim s) = 0
    524 
    525 let not_empty s = not (is_empty s)
    526 
    527 let has_substring needle haystack =
    528   let l1 = String.length needle in
    529   let l2 = String.length haystack in
    530   let max_i = l2 - l1 in
    531   let rec loop i =
    532     if i > max_i then false
    533     else if needle = String.sub haystack i l1 then true
    534     else loop (i + 1)
    535   in loop 0
    536 
    537 end
    538 
    539 
    540 let target_local_vars () =
    541   let open Tools in
    542   (* NMAKE and OpenBSD don't support target-specific local variables *)
    543   (* The same applies for make in NetBSD < 10 and FreeBSD < 13 *)
    544   let make_supports_local_vars =
    545     is_empty inputs.$("_NMAKE_VER") && (
    546       is_empty inputs.$("MAKE") ||
    547       (let exit_status = ref (Unix.WEXITED (-1)) in
    548       has_substring "test ok" (
    549         shell_input ~exit_status
    550           "TEST_VAL = test\n\
    551            _local_var_test: LOCAL_VAR_TEST = $(TEST_VAL)\n\
    552            _local_var_test: ; @echo $(LOCAL_VAR_TEST) ok"
    553           (inputs.$("MAKE") ^ " -f -")) &&
    554       !exit_status = Unix.WEXITED 0)
    555     )
    556   in
    557   if make_supports_local_vars then begin
    558     let is_old_gmake =
    559       not_empty inputs.$("MAKE") &&
    560       let s = shell ~err_null:true (inputs.$("MAKE") ^ " --version") in
    561       if String.length s >= 10 then String.sub s 0 10 = "GNU Make 3" else false
    562     in
    563     let rule_sep = if not is_old_gmake then " $(rule_sep) " else ": " in
    564     [
    565       ["$(NAME_GUI)$(EXEC_EXT) $(CAMLOBJS_GUI)"; rule_sep; "CAMLFLAGS_GUI_X = $(CAMLFLAGS_GUI)"];
    566       ["$(NAME_FSM)$(EXEC_EXT) $(CAMLOBJS_FSM) $(FSMOCAMLOBJS:.cmo=.cmi)"; rule_sep; "CAMLFLAGS_FSM_X = $(CAMLFLAGS_FSM)"];
    567       ["$(FSMCOBJS)"; rule_sep; "CAMLCFLAGS_FSM_X = $(CAMLCFLAGS_FSM)"];
    568       ["$(NAME)-blob.o $(CAMLOBJS_MAC)"; rule_sep; "CAMLFLAGS_MAC_X = $(CAMLFLAGS_MAC)"];
    569     ]
    570     |> List.iter (fun l -> String.concat "" l |> print_endline)
    571   end else
    572     print_string
    573       "CAMLFLAGS_GUI_X = $(CAMLFLAGS_GUI)\n\
    574        CAMLFLAGS_FSM_X = $(CAMLFLAGS_FSM)\n\
    575        CAMLCFLAGS_FSM_X = $(CAMLCFLAGS_FSM)"
    576 
    577 
    578 let project_info () =
    579   let open Tools in
    580   {|let myName = "|} ^ ($)"NAME" ^ {|"|} |> print_endline;
    581   {|let myVersion = "|} ^ ($)"VERSION" ^ {|"|} |> print_endline;
    582   {|let myMajorVersion = "|} ^ ($)"MAJORVERSION" ^ {|"|} |> print_endline
    583 
    584 
    585 let install () =
    586   let open Tools in
    587   if not_empty inputs.$("_NMAKE_VER") || Sys.file_exists "src/unison.exe" then begin
    588     let cwd = Sys.getcwd () in
    589     let map_sep =
    590       if String.contains cwd '\\' then function '/' -> '\\' | c -> c
    591       else fun c -> c
    592     in
    593     let files =
    594       let check_file name l =
    595         if Sys.file_exists name then
    596           (String.map map_sep (Filename.concat cwd name)) :: l
    597         else l
    598       in
    599       List.fold_right check_file
    600         ["src/unison.exe"; "src/unison-gui.exe"; "src/unison-fsmonitor.exe"]
    601         []
    602     in
    603     if files = [] then
    604       error "The application has not been built yet."
    605     else
    606       info ("\n\nYou can find the built application as the following files \
    607         that you can copy to your desired destination.\n    " ^
    608         (String.concat "\n    " files) ^ "\n");
    609       exit 0
    610   end;
    611 
    612   let install = "INSTALL" <--? "install" in
    613   let install_program = "INSTALL_PROGRAM" <--? install in
    614   let install_data = "INSTALL_DATA" <--? install ^ " -m 644" in
    615 
    616   let destdir = ($)"DESTDIR" in
    617   let prefix = "PREFIX" <--? "/usr/local" in
    618   let exec_prefix = "EXEC_PREFIX" <--? prefix in
    619   let bindir = "BINDIR" <--? exec_prefix ^ "/bin" in
    620   let datarootdir = "DATAROOTDIR" <--? prefix ^ "/share" in
    621   let mandir = "MANDIR" <--? datarootdir ^ "/man" in
    622   let man1dir = "MAN1DIR" <--? mandir ^ "/man1" in
    623   let manext = "MANEXT" <--? ".1" in
    624 
    625   let install_if_exists dir name dest =
    626     if exists dir name then exec
    627       [install_program; Filename.concat dir name; Filename.concat dest name]
    628   in
    629 
    630   print_endline ("DESTDIR = " ^ destdir);
    631   print_endline ("PREFIX = " ^ prefix);
    632   exec [install; "-d"; destdir ^ bindir];
    633   install_if_exists "src" "unison" (destdir ^ bindir);
    634   install_if_exists "src" "unison-gui" (destdir ^ bindir);
    635   install_if_exists "src" "unison-fsmonitor" (destdir ^ bindir);
    636   if exists "man" "unison.1" then begin
    637     exec [install; "-d"; destdir ^ man1dir];
    638     exec [install_data; "man/unison.1"; destdir ^ man1dir ^ "/unison" ^ manext]
    639   end;
    640   if exists "src/uimac/build/Default" "Unison.app" then begin
    641     print_endline ("!!! The GUI for macOS has been built but will NOT be \
    642       installed automatically. You can find the built GUI package at " ^
    643       (Filename.concat (Sys.getcwd ()) "src/uimac/build/Default/Unison.app"))
    644   end
    645 
    646 
    647 let run cmd_and_args =
    648   let map_sep =
    649     if Sys.win32 then function '/' -> '\\' | c -> c else fun c -> c
    650   in
    651   match Array.to_list cmd_and_args with
    652   | [] -> ()
    653   | cmd :: args -> Tools.exec (String.map map_sep cmd :: args)
    654 
    655 
    656 let () =
    657   if Array.length Sys.argv < 2 then
    658     Tools.error "Missing sub-command"
    659   else
    660     match Sys.argv.(1) with
    661     | "conf" -> let module Conf = Make(Tools) in ()
    662     | "conf2" -> target_local_vars ()
    663     | "projectinfo" -> project_info ()
    664     | "install" -> install ()
    665     | "run" -> run (Array.sub Sys.argv 2 (Array.length Sys.argv - 2))
    666     | "rm" -> Tools.rm (Array.sub Sys.argv 2 (Array.length Sys.argv - 2))
    667     | s -> Tools.error ("Invalid sub-commmand '" ^ s ^ "'")