open Import

let with_metrics ~common f =
  let start_time = Unix.gettimeofday () in
  Fiber.finalize f ~finally:(fun () ->
    let duration = Unix.gettimeofday () -. start_time in
    if Common.print_metrics common
    then (
      let gc_stat = Gc.quick_stat () in
      (* We reset Memo counters below, unconditionally. *)
      let memo_counters_report = Memo.Metrics.report ~reset_after_reporting:false in
      Console.print_user_message
        (User_message.make
           ([ Pp.textf "%s" memo_counters_report
            ; Pp.textf
                "(%.2fs total, %.1fM heap words)"
                duration
                (float_of_int gc_stat.heap_words /. 1_000_000.)
            ; Pp.text "Timers:"
            ]
            @ List.map
                ~f:(fun (timer, { Metrics.Timer.Measure.cumulative_time; count }) ->
                  Pp.textf
                    "%s - time spent = %.2fs, count = %d"
                    timer
                    cumulative_time
                    count)
                (String.Map.to_list (Metrics.Timer.aggregated_timers ())))));
    Memo.Metrics.reset ();
    Fiber.return ())
;;

let run_build_system ~common ~request =
  let run ~(toplevel : unit Memo.Lazy.t) =
    with_metrics ~common (fun () -> build (fun () -> Memo.Lazy.force toplevel))
  in
  let open Fiber.O in
  Fiber.finalize
    (fun () ->
       (* CR-someday amokhov: Currently we invalidate cached timestamps on every
         incremental rebuild. This conservative approach helps us to work around
         some [mtime] resolution problems (e.g. on Mac OS). It would be nice to
         find a way to avoid doing this. In fact, this may be unnecessary even
         for the initial build if we assume that the user does not modify files
         in the [_build] directory. For now, it's unclear if optimising this is
         worth the effort. *)
       Cached_digest.invalidate_cached_timestamps ();
       let* setup = Import.Main.setup () in
       let request =
         Action_builder.bind (Action_builder.of_memo setup) ~f:(fun setup ->
           request setup)
       in
       (* CR-someday cmoseley: Can we avoid creating a new lazy memo node every
         time the build system is rerun? *)
       (* This top-level node is used for traversing the whole Memo graph. *)
       let toplevel_cell, toplevel =
         Memo.Lazy.Expert.create ~name:"toplevel" (fun () ->
           let open Memo.O in
           let+ (), (_ : Dep.Fact.t Dep.Map.t) =
             Action_builder.evaluate_and_collect_facts request
           in
           ())
       in
       let* res = run ~toplevel in
       let+ () =
         match Common.dump_memo_graph_file common with
         | None -> Fiber.return ()
         | Some file ->
           let path = Path.external_ file in
           let+ graph =
             Memo.dump_cached_graph
               ~time_nodes:(Common.dump_memo_graph_with_timing common)
               toplevel_cell
           in
           Graph.serialize graph ~path ~format:(Common.dump_memo_graph_format common)
         (* CR-someday cmoseley: It would be nice to use Persistent to dump a
           copy of the graph's internal representation here, so it could be used
           without needing to re-run the build*)
       in
       res)
    ~finally:(fun () ->
      Hooks.End_of_build.run ();
      Fiber.return ())
;;

let poll_handling_rpc_build_requests ~(common : Common.t) ~config =
  let open Fiber.O in
  let rpc =
    match Common.rpc common with
    | `Allow server -> server
    | `Forbid_builds -> Code_error.raise "rpc server must be allowed in passive mode" []
  in
  Scheduler.Run.poll_passive
    ~get_build_request:
      (let+ (Build (targets, ivar)) = Dune_rpc_impl.Server.pending_build_action rpc in
       let request setup =
         Target.interpret_targets (Common.root common) config setup targets
       in
       run_build_system ~common ~request, ivar)
;;

let run_build_command_poll_eager ~(common : Common.t) ~config ~request : unit =
  Scheduler.go_with_rpc_server_and_console_status_reporting ~common ~config (fun () ->
    let open Fiber.O in
    (* Run two fibers concurrently. One is responible for rebuilding targets
       named on the command line in reaction to file system changes. The other
       is responsible for building targets named in RPC build requests. *)
    let+ () = Scheduler.Run.poll (run_build_system ~common ~request)
    and+ () = poll_handling_rpc_build_requests ~common ~config in
    ())
;;

let run_build_command_poll_passive ~common ~config ~request:_ : unit =
  (* CR-someday aalekseyev: It would've been better to complain if [request] is
     non-empty, but we can't check that here because [request] is a function.*)
  Scheduler.go_with_rpc_server_and_console_status_reporting ~common ~config (fun () ->
    poll_handling_rpc_build_requests ~common ~config)
;;

let run_build_command_once ~(common : Common.t) ~config ~request =
  let open Fiber.O in
  let once () =
    let+ res = run_build_system ~common ~request in
    match res with
    | Error `Already_reported -> raise Dune_util.Report_error.Already_reported
    | Ok () -> ()
  in
  Scheduler.go_with_rpc_server ~common ~config once
;;

let run_build_command ~(common : Common.t) ~config ~request =
  (match Common.watch common with
   | Yes Eager -> run_build_command_poll_eager
   | Yes Passive -> run_build_command_poll_passive
   | No -> run_build_command_once)
    ~common
    ~config
    ~request
;;

let build_via_rpc_server ~print_on_success ~targets =
  Rpc_common.wrap_build_outcome_exn
    ~print_on_success
    (Rpc.Build.build ~wait:true)
    targets
    ()
;;

let build =
  let doc = "Build the given targets, or the default ones if none are given." in
  let man =
    [ `S "DESCRIPTION"
    ; `P {|Targets starting with a $(b,@) are interpreted as aliases.|}
    ; `Blocks Common.help_secs
    ; Common.examples
        [ "Build all targets in the current source tree", "dune build"
        ; "Build targets in the `./foo/bar' directory", "dune build ./foo/bar"
        ; ( "Build the minimal set of targets required for tooling such as Merlin \
             (useful for quickly detecting errors)"
          , "dune build @check" )
        ; "Run all code formatting tools in-place", "dune build --auto-promote @fmt"
        ]
    ]
  in
  let name_ = Arg.info [] ~docv:"TARGET" in
  let term =
    let+ builder = Common.Builder.term
    and+ targets = Arg.(value & pos_all dep [] name_)
    and+ aliases_rec = Arg.(value & opt_all Dep.alias_rec_arg [] & info [ "alias-rec" ])
    and+ aliases = Arg.(value & opt_all Dep.alias_arg [] & info [ "alias" ]) in
    let targets = List.concat [ targets; aliases; aliases_rec ] in
    let targets =
      match targets with
      | [] -> [ Common.Builder.default_target builder ]
      | _ :: _ -> targets
    in
    let common, config = Common.init builder in
    (* Here we need to find out whether another instance of dune already holds
       the global build lock, as this will determine whether the current
       instance of dune will perform the build itself or send a build request
       to the RPC server in an already-running dune process. The method of
       checking whether another dune instance holds the lock is to simply try
       to take the lock. If taking the lock succeeds then the current process
       will perform the build itself, and future attempts by this process to
       take the lock are guaranteed to succeed. If taking the lock fails then
       we know that another instance of dune must have it, and the current
       process will send a build RPC request to that dune instance. Checking
       the status of the lock by taking prevents a race condition where the
       state of the lock could otherwise change between checking it and taking
       it. *)
    match Dune_util.Global_lock.lock ~timeout:None with
    | Error lock_held_by ->
      (* This case is reached if dune detects that another instance of dune
         is already running. Rather than performing the build itself, the
         current instance of dune will instruct the already-running instance to
         perform the build by sending an RPC message. As only one RPC server
         can run at a time we need to use a fiber scheduler which does not run
         an RPC server in the background to schedule the fiber which will
         perform the RPC call.
      *)
      Rpc_common.run_via_rpc
        ~builder
        ~common
        ~config
        lock_held_by
        (Rpc.Build.build ~wait:true)
        targets
    | Ok () ->
      let request setup =
        Target.interpret_targets (Common.root common) config setup targets
      in
      run_build_command ~common ~config ~request
  in
  Cmd.v (Cmd.info "build" ~doc ~man ~envs:Common.envs) term
;;
