This commit is contained in:
swrup 2025-11-11 02:07:51 +01:00
parent aa2ff7b2f0
commit 2f3113f55d
11742 changed files with 1223940 additions and 0 deletions

View file

@ -0,0 +1,145 @@
open Import
module Alias = Dune_engine.Alias
module Alias0 = Dune_rules.Alias
module Alias_builder = Dune_rules.Alias_builder
type t =
{ name : Alias.Name.t
; recursive : bool
; dir : Path.Source.t
; contexts : Dune_rules.Context.t list
}
let pp { name; recursive; dir; contexts = _ } =
let open Pp.O in
let s =
(if recursive then "@" else "@@")
^ Path.Source.to_string (Path.Source.relative dir (Alias.Name.to_string name))
in
let pp = Pp.verbatim "alias" ++ Pp.space ++ Pp.verbatim s in
if recursive then Pp.verbatim "recursive" ++ Pp.space ++ pp else pp
;;
let in_dir ~name ~recursive ~contexts dir =
let checked = Util.check_path contexts dir in
match checked with
| External _ ->
User_error.raise
[ Pp.textf "@@ on the command line must be followed by a relative path" ]
| In_source_dir dir -> { dir; recursive; name; contexts }
| In_private_context _ ->
User_error.raise [ Pp.textf "no aliases in the testing context" ]
| In_install_dir _ ->
User_error.raise
[ Pp.textf
"Invalid alias: %s."
(Path.to_string_maybe_quoted
(Path.build Install.Context.install_context.build_dir))
; Pp.textf "There are no aliases in %s." (Path.to_string_maybe_quoted dir)
]
| In_build_dir (ctx, dir) ->
{ dir
; recursive
; name
; contexts =
[ List.find_exn contexts ~f:(fun c ->
Context_name.equal (Context.name c) (Context.name ctx))
]
}
;;
let of_string (root : Workspace_root.t) ~recursive s ~contexts =
let path = Path.relative Path.root (root.reach_from_root_prefix ^ s) in
if Path.is_root path
then
User_error.raise
[ Pp.textf "@ on the command line must be followed by a valid alias name" ]
else (
let dir = Path.parent_exn path in
let name = Alias.Name.of_string (Path.basename path) in
in_dir ~name ~recursive ~contexts dir)
;;
let find_dir_specified_on_command_line ~dir =
let open Memo.O in
Source_tree.find_dir dir
>>| function
| Some dir -> dir
| None ->
User_error.raise
[ Pp.textf
"Don't know about directory %s specified on the command line!"
(Path.Source.to_string_maybe_quoted dir)
]
;;
let dep_on_alias_multi_contexts ~dir ~name ~contexts =
ignore (find_dir_specified_on_command_line ~dir : _ Memo.t);
let context_to_alias_expansion ctx =
let ctx_dir = Context_name.build_dir ctx in
let dir = Path.Build.(append_source ctx_dir dir) in
Alias_builder.alias (Alias.make ~dir name)
in
Action_builder.all_unit (List.map contexts ~f:context_to_alias_expansion)
;;
let dep_on_alias_rec_multi_contexts ~dir:src_dir ~name ~contexts =
let open Action_builder.O in
let* dir = Action_builder.of_memo (find_dir_specified_on_command_line ~dir:src_dir) in
let* alias_statuses =
Action_builder.all
(List.map contexts ~f:(fun ctx ->
let dir =
Path.Build.append_source
(Context_name.build_dir ctx)
(Source_tree.Dir.path dir)
in
Dune_rules.Alias_rec.dep_on_alias_rec name dir))
in
match
Alias0.is_standard name
|| List.exists alias_statuses ~f:(fun (x : Alias_builder.Alias_status.t) ->
match x with
| Defined -> true
| Not_defined -> false)
with
| true -> Action_builder.return ()
| false ->
let* load_dir =
Action_builder.all
@@ List.map contexts ~f:(fun ctx ->
let dir =
Source_tree.Dir.path dir
|> Path.Build.append_source (Context_name.build_dir ctx)
|> Path.build
in
Action_builder.of_memo @@ Load_rules.load_dir ~dir)
in
let hints =
let candidates =
Alias.Name.Set.union_map load_dir ~f:(function
| Load_rules.Loaded.Build build -> Alias.Name.Set.of_keys build.aliases
| _ -> Alias.Name.Set.empty)
in
User_message.did_you_mean
(Alias.Name.to_string name)
~candidates:(Alias.Name.Set.to_list_map ~f:Alias.Name.to_string candidates)
in
User_error.raise
~hints
[ Pp.textf
"Alias %S specified on the command line is empty."
(Alias.Name.to_string name)
; Pp.textf
"It is not defined in %s or any of its descendants."
(Path.Source.to_string_maybe_quoted src_dir)
]
;;
let request { name; recursive; dir; contexts } =
let contexts = List.map ~f:Context.name contexts in
(if recursive then dep_on_alias_rec_multi_contexts else dep_on_alias_multi_contexts)
~dir
~name
~contexts
;;

View file

@ -0,0 +1,25 @@
open Import
type t = private
{ name : Dune_engine.Alias.Name.t
; recursive : bool
; dir : Path.Source.t
; contexts : Context.t list
}
val in_dir
: name:Dune_engine.Alias.Name.t
-> recursive:bool
-> contexts:Context.t list
-> Path.t
-> t
val of_string
: Workspace_root.t
-> recursive:bool
-> string
-> contexts:Context.t list
-> t
val pp : t -> _ Pp.t
val request : t -> unit Action_builder.t

View file

@ -0,0 +1,155 @@
open Stdune
include Cmdliner.Arg
include struct
open Dune_lang
module Stanza = Stanza
module String_with_vars = String_with_vars
module Profile = Profile
module Pform = Pform
module Lib_name = Lib_name
module Dep_conf = Dep_conf
end
module Package = Dune_lang.Package
module Context_name = Dune_engine.Context_name
let package_name = conv Package.Name.conv
module Path = struct
module External = struct
type t = string
let path p = Path.External.of_filename_relative_to_initial_cwd p
let arg s = s
let conv = conv ((fun p -> Ok p), Format.pp_print_string)
end
type t = string
let path p = Path.of_filename_relative_to_initial_cwd p
let arg s = s
let conv = conv ((fun p -> Ok p), Format.pp_print_string)
end
let path = Path.conv
let external_path = Path.External.conv
let profile = conv Profile.conv
module Dep = struct
module Dep_conf = Dep_conf
type t = Dep_conf.t
let equal = Dep_conf.equal
let file s = Dep_conf.File (String_with_vars.make_text Loc.none s)
let make_alias_sw ~dir s =
let path =
Dune_engine.Alias.Name.to_string s
|> Stdune.Path.Local.relative dir
|> Stdune.Path.Local.to_string
in
String_with_vars.make_text Loc.none path
;;
let alias ~dir s = Dep_conf.Alias (make_alias_sw ~dir s)
let alias_rec ~dir s = Dep_conf.Alias_rec (make_alias_sw ~dir s)
let parse_alias s =
if not (String.is_prefix s ~prefix:"@")
then None
else (
let pos, recursive =
if String.length s >= 2 && s.[1] = '@' then 2, false else 1, true
in
let s = String_with_vars.make_text Loc.none (String.drop s pos) in
Some (if recursive then Dep_conf.Alias_rec s else Dep_conf.Alias s))
;;
let dep_parser =
Dune_lang.Syntax.set
Stanza.syntax
(Active Stanza.latest_version)
(String_with_vars.set_decoding_env
(Pform.Env.initial ~stanza:Stanza.latest_version ~extensions:[])
Dep_conf.decode)
;;
let parser s =
match parse_alias s with
| Some dep -> Ok dep
| None ->
(match
Dune_lang.Decoder.parse
dep_parser
Univ_map.empty
(Dune_lang.Parser.parse_string
~fname:"command line"
~mode:Dune_lang.Parser.Mode.Single
s)
with
| x -> Ok x
| exception User_error.E msg -> Error (User_message.to_string msg))
;;
let string_of_alias ~recursive sv =
let prefix = if recursive then "@" else "@@" in
String_with_vars.text_only sv |> Option.map ~f:(fun s -> prefix ^ s)
;;
let printer ppf t =
let s =
match t with
| Dep_conf.Alias sv -> string_of_alias ~recursive:false sv
| Alias_rec sv -> string_of_alias ~recursive:true sv
| File sv -> Some (Dune_lang.to_string (String_with_vars.encode sv))
| _ -> None
in
let s =
match s with
| Some s -> s
| None -> Dune_lang.to_string (Dep_conf.encode t)
in
Format.pp_print_string ppf s
;;
let conv = conv' (parser, printer)
let to_string_maybe_quoted t = String.maybe_quoted (Format.asprintf "%a" printer t)
let alias_arg =
let parse x = Ok (Dep_conf.Alias (String_with_vars.make_text Loc.none x)) in
conv' (parse, printer)
;;
let alias_rec_arg =
let parse x = Ok (Dep_conf.Alias_rec (String_with_vars.make_text Loc.none x)) in
conv' (parse, printer)
;;
end
let dep = Dep.conv
let bytes =
let decode repr =
let ast =
Dune_lang.Parser.parse_string
~fname:"command line"
~mode:Dune_lang.Parser.Mode.Single
repr
in
match Dune_lang.Decoder.parse Dune_lang.Decoder.bytes_unit Univ_map.empty ast with
| x -> Result.Ok x
| exception User_error.E msg -> Result.Error (`Msg (User_message.to_string msg))
in
let pp_print_int64 state i = Format.pp_print_string state (Int64.to_string i) in
conv (decode, pp_print_int64)
;;
let graph_format : Dune_graph.Graph.File_format.t conv =
conv Dune_graph.Graph.File_format.conv
;;
let context_name : Context_name.t conv = conv Context_name.conv
let lib_name = conv Lib_name.conv
let version = pair ~sep:'.' int int

View file

@ -0,0 +1,42 @@
open Stdune
include module type of struct
include Cmdliner.Arg
end
module Path : sig
module External : sig
type t
val path : t -> Path.External.t
val arg : t -> string
end
type t
val path : t -> Path.t
val arg : t -> string
end
module Dep : sig
type t = Dune_lang.Dep_conf.t
val equal : t -> t -> bool
val file : string -> t
val alias : dir:Stdune.Path.Local.t -> Dune_engine.Alias.Name.t -> t
val alias_rec : dir:Stdune.Path.Local.t -> Dune_engine.Alias.Name.t -> t
val to_string_maybe_quoted : t -> string
val alias_arg : t conv
val alias_rec_arg : t conv
end
val bytes : int64 conv
val context_name : Dune_engine.Context_name.t conv
val dep : Dep.t conv
val graph_format : Dune_graph.Graph.File_format.t conv
val path : Path.t conv
val external_path : Path.External.t conv
val package_name : Dune_lang.Package.Name.t conv
val profile : Dune_lang.Profile.t conv
val lib_name : Dune_lang.Lib_name.t conv
val version : Dune_lang.Syntax.Version.t conv

View file

@ -0,0 +1,215 @@
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
;;

View file

@ -0,0 +1,24 @@
open Import
(** Connect to an RPC server (waiting for the server to start if necessary) and
then send a request to the server to build the specified targets. If the
build fails then a diagnostic error message is printed. If
[print_on_success] is true then this function will also print a message
after the build succeeds. *)
val build_via_rpc_server
: print_on_success:bool
-> targets:Dune_lang.Dep_conf.t list
-> unit Fiber.t
val run_build_system
: common:Common.t
-> request:(Dune_rules.Main.build_system -> unit Action_builder.t)
-> (unit, [ `Already_reported ]) result Fiber.t
val build : unit Cmd.t
val run_build_command
: common:Common.t
-> config:Dune_config.t
-> request:(Dune_rules.Main.build_system -> unit Action_builder.t)
-> unit

View file

@ -0,0 +1,117 @@
open Import
(* CR-someday amokhov: Implement other commands supported by Jenga. *)
let trim =
let info =
let doc = "Trim the Dune cache." in
let man =
[ `P "Trim the Dune cache to a specified size or by a specified amount."
; `S "EXAMPLES"
; `Pre
{|Trimming the Dune cache to 1 GB.
\$ dune cache trim --size=1GB |}
; `Pre
{|Trimming 500 MB from the Dune cache.
\$ dune cache trim --trimmed-size=500MB |}
]
in
Cmd.info "trim" ~doc ~man
in
Cmd.v info
@@ let+ trimmed_size =
Arg.(
value
& opt (some bytes) None
& info
~docv:"BYTES"
[ "trimmed-size" ]
~doc:"Size to trim from the cache. $(docv) is the same as for --size.")
and+ size =
Arg.(
value
& opt (some bytes) None
& info
~docv:"BYTES"
[ "size" ]
~doc:
(sprintf
"Size to trim the cache to. $(docv) is the number of bytes followed by \
a unit. Byte units can be one of %s."
(String.enumerate_or
(List.map
~f:(fun (units, _) -> List.hd units)
Bytes_unit.conversion_table))))
in
Log.init_disabled ();
let open Result.O in
match
let+ goal =
match trimmed_size, size with
| Some trimmed_size, None -> Result.Ok trimmed_size
| None, Some size ->
Result.Ok (Int64.sub (Dune_cache.Trimmer.overhead_size ()) size)
| _ -> Result.Error "please specify either --size or --trimmed-size"
in
Dune_cache.Trimmer.trim ~goal
with
| Error s -> User_error.raise [ Pp.text s ]
| Ok { trimmed_bytes; number_of_files_removed } ->
User_message.print
(User_message.make
[ Pp.textf
"Freed %s (%d files removed)"
(Bytes_unit.pp trimmed_bytes)
number_of_files_removed
])
;;
let size =
let info =
let doc = "Query the size of the Dune cache." in
let man =
[ `P
"Compute the total size of files in the Dune cache which are not hardlinked \
from any build directory and output it in a human-readable form."
]
in
Cmd.info "size" ~doc ~man
in
Cmd.v info
@@ let+ machine_readable =
Arg.(
value
& flag
& info [ "machine-readable" ] ~doc:"Outputs size as a plain number of bytes.")
in
let size = Dune_cache.Trimmer.overhead_size () in
if machine_readable
then User_message.print (User_message.make [ Pp.textf "%Ld" size ])
else User_message.print (User_message.make [ Pp.textf "%s" (Bytes_unit.pp size) ])
;;
let clear =
let info =
let doc = "Clear the Dune cache." in
let man = [ `P "Remove any traces of the Dune cache." ] in
Cmd.info "clear" ~doc ~man
in
Cmd.v info @@ Term.(const Dune_cache_storage.clear $ const ())
;;
let command =
let info =
let doc = "Manage Dune's shared cache of build artifacts." in
let man =
[ `S "DESCRIPTION"
; `P
"Dune can share build artifacts between workspaces. We currently only support \
a few subcommands; however, we plan to provide more functionality soon."
]
in
Cmd.info "cache" ~doc ~man
in
Cmd.group info [ trim; size; clear ]
;;

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,25 @@
open Import
let command =
let doc = "Clean the project." in
let man =
[ `S "DESCRIPTION"
; `P {|Removes files added by dune such as _build, <package>.install, and .merlin|}
; `Blocks Common.help_secs
]
in
let term =
let+ builder = Common.Builder.term in
(* Disable log file creation. Indeed, we are going to delete the whole build directory
right after and that includes deleting the log file. Not only would creating the
log file be useless but with some FS this also causes [dune clean] to fail (cf
https://github.com/ocaml/dune/issues/2964). *)
let builder = Common.Builder.disable_log_file builder in
let _common, _config = Common.init builder in
Dune_util.Global_lock.lock_exn ~timeout:None;
Dune_engine.Target_promotion.files_in_source_tree_to_delete ()
|> Path.Source.Set.iter ~f:(fun p -> Path.unlink_no_err (Path.source p));
Path.rm_rf Path.build_dir
in
Cmd.v (Cmd.info "clean" ~doc ~man) term
;;

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
open Dune_config_file
open Stdune
type t
val x : t -> Dune_engine.Context_name.t option
val capture_outputs : t -> bool
val root : t -> Workspace_root.t
val rpc
: t
-> [ `Allow of Dune_lang.Dep_conf.t Dune_rpc_impl.Server.t
(** Will run rpc if in watch mode and acquire the build lock *)
| `Forbid_builds (** Promise not to build anything. For now, this isn't checked *)
]
val watch_exclusions : t -> string list
val stats : t -> Dune_stats.t option
val print_metrics : t -> bool
val dump_memo_graph_file : t -> Path.External.t option
val dump_memo_graph_format : t -> Dune_graph.Graph.File_format.t
val dump_memo_graph_with_timing : t -> bool
val watch : t -> Dune_rpc_impl.Watch_mode_config.t
val file_watcher : t -> Dune_engine.Scheduler.Run.file_watcher
val prefix_target : t -> string -> string
(** [Builder] describes how to initialize Dune. *)
module Builder : sig
type t
val equal : t -> t -> bool
val root : t -> string option
val set_root : t -> string -> t
val forbid_builds : t -> t
val default_root_is_cwd : t -> bool
val set_default_root_is_cwd : t -> bool -> t
val set_log_file : t -> Dune_util.Log.File.t -> t
val disable_log_file : t -> t
val set_promote : t -> Dune_engine.Clflags.Promote.t -> t
val default_target : t -> Arg.Dep.t
val term : t Cmdliner.Term.t
val default : t
end
(** [init] creates a [Common.t] by executing a sequence of side-effecting actions to
initialize Dune's working environment based on the options determined in the\
[Builder.t].
Return the [Common.t] and the final configuration, which is the same as the one
returned in the [config] field of [Dune_rules.Workspace.workspace ()]) *)
val init : Builder.t -> t * Dune_config_file.Dune_config.t
(** [examples [("description", "dune cmd foo"); ...]] is an [EXAMPLES] manpage
section of enumerated examples illustrating how to run the documented
commands. *)
val examples : (string * string) list -> Cmdliner.Manpage.block
(** [command_synopsis subcommands] is a custom [SYNOPSIS] manpage section
listing the given [subcommands]. Each subcommand is prefixed with the `dune`
top-level command. *)
val command_synopsis : string list -> Cmdliner.Manpage.block list
val help_secs : Cmdliner.Manpage.block list
val footer : Cmdliner.Manpage.block
val envs : Cmdliner.Cmd.Env.info list
val debug_backtraces : bool Cmdliner.Term.t
val config_from_config_file : Dune_config.Partial.t Cmdliner.Term.t
val display_term : Dune_config.Display.t option Cmdliner.Term.t
val context_arg : doc:string -> Dune_engine.Context_name.t Cmdliner.Term.t
(** A [--build-info] command line argument that print build information
(included in [term]) *)
val build_info : unit Cmdliner.Term.t
val default_build_dir : string
module Let_syntax : sig
val ( let+ ) : 'a Cmdliner.Term.t -> ('a -> 'b) -> 'b Cmdliner.Term.t
val ( and+ ) : 'a Cmdliner.Term.t -> 'b Cmdliner.Term.t -> ('a * 'b) Cmdliner.Term.t
end
(** [one_of term1 term2] allows options from [term1] or exclusively options from
[term2]. If the user passes options from both terms, an error is reported. *)
val one_of : 'a Cmdliner.Term.t -> 'a Cmdliner.Term.t -> 'a Cmdliner.Term.t

View file

@ -0,0 +1,7 @@
open Import
let doc = "Command group related to Coq."
let sub_commands_synopsis = Common.command_synopsis [ "coq top FILE -- ARGS" ]
let man = [ `Blocks sub_commands_synopsis ]
let info = Cmd.info ~doc ~man "coq"
let group = Cmd.group info [ Coqtop.command ]

View file

@ -0,0 +1,3 @@
open Import
val group : unit Cmd.t

View file

@ -0,0 +1,159 @@
open Import
let doc = "Execute a Coq toplevel with the local configuration."
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune coq top FILE -- ARGS) runs the Coq toplevel to process the
given $(b,FILE). The given arguments are completed according to the
local configuration. This is equivalent to running $(b,coqtop ARGS)
with a $(b,_CoqProject) file containing the local configurations
from the $(b,dune) files, but does not require maintaining a
$(b,_CoqProject) file.|}
; `Blocks Common.help_secs
]
;;
let info = Cmd.info "top" ~doc ~man
let term =
let+ default_builder = Common.Builder.term
and+ context =
let doc = "Run the Coq toplevel in this build context." in
Common.context_arg ~doc
and+ coqtop =
let doc = "Run the given toplevel command instead of the default." in
Arg.(value & opt string "coqtop" & info [ "toplevel" ] ~docv:"CMD" ~doc)
and+ coq_file_arg =
Arg.(required & pos 0 (some string) None (Arg.info [] ~docv:"COQFILE"))
and+ extra_args = Arg.(value & pos_right 0 string [] (Arg.info [] ~docv:"ARGS"))
and+ no_rebuild =
Arg.(
value
& flag
& info [ "no-build" ] ~doc:"Don't rebuild dependencies before executing.")
in
let common, config =
let builder =
if no_rebuild then Common.Builder.forbid_builds default_builder else default_builder
in
Common.init builder
in
let coq_file_arg = Common.prefix_target common coq_file_arg |> Path.Local.of_string in
let coqtop, args, env =
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
let* setup = Memo.run setup in
let sctx = Import.Main.find_scontext_exn setup ~name:context in
let context = Dune_rules.Super_context.context sctx in
let coq_file_build =
Path.Build.append_local (Context.build_dir context) coq_file_arg
in
let dir =
(match Path.Local.parent coq_file_arg with
| None -> Path.Local.root
| Some dir -> dir)
|> Path.Build.append_local (Context.build_dir context)
in
let* coqtop, args, env =
build_exn
@@ fun () ->
let open Memo.O in
let* (tr : Dune_rules.Dir_contents.triage) =
Dune_rules.Dir_contents.triage sctx ~dir
in
let dir =
match tr with
| Group_part dir -> dir
| Standalone_or_root _ -> dir
in
let* dc = Dune_rules.Dir_contents.get sctx ~dir in
let* coq_src = Dune_rules.Dir_contents.coq dc in
let coq_module =
let source = coq_file_build in
match Dune_rules.Coq.Coq_sources.find_module ~source coq_src with
| Some m -> snd m
| None ->
let hints =
[ Pp.textf "Is the file part of a stanza?"
; Pp.textf "Has the file been written to disk?"
]
in
User_error.raise
~hints
[ Pp.textf "Cannot find file: %s" (coq_file_arg |> Path.Local.to_string) ]
in
let stanza = Dune_rules.Coq.Coq_sources.lookup_module coq_src coq_module in
let args, use_stdlib, coq_lang_version, wrapper_name, mode =
match stanza with
| None ->
User_error.raise
[ Pp.textf
"File not part of any stanza: %s"
(coq_file_arg |> Path.Local.to_string)
]
| Some (`Theory theory) ->
( Dune_rules.Coq.Coq_rules.coqtop_args_theory
~sctx
~dir
~dir_contents:dc
theory
coq_module
, theory.buildable.use_stdlib
, theory.buildable.coq_lang_version
, Dune_rules.Coq.Coq_lib_name.wrapper (snd theory.name)
, theory.buildable.mode )
| Some (`Extraction extr) ->
( Dune_rules.Coq.Coq_rules.coqtop_args_extraction ~sctx ~dir extr coq_module
, extr.buildable.use_stdlib
, extr.buildable.coq_lang_version
, "DuneExtraction"
, extr.buildable.mode )
in
(* Run coqdep *)
let* (_ : unit * Dep.Fact.t Dep.Map.t) =
let deps_of =
if no_rebuild
then Action_builder.return ()
else (
let mode =
match mode with
| None -> Dune_rules.Coq.Coq_mode.VoOnly
| Some mode -> mode
in
Dune_rules.Coq.Coq_rules.deps_of
~dir
~use_stdlib
~wrapper_name
~mode
~coq_lang_version
coq_module)
in
Action_builder.evaluate_and_collect_facts deps_of
in
(* Get args *)
let* (args, _) : string list * Dep.Fact.t Dep.Map.t =
let* args = args in
let dir = Path.external_ Path.External.initial_cwd in
let args = Dune_rules.Command.expand ~dir (S args) in
Action_builder.evaluate_and_collect_facts args.build
in
let* prog = Super_context.resolve_program_memo sctx ~dir ~loc:None coqtop in
let prog = Action.Prog.ok_exn prog in
let* () = Build_system.build_file prog in
let+ env = Super_context.context_env sctx in
Path.to_string prog, args, env
in
let args =
let topfile = Path.to_absolute_filename (Path.build coq_file_build) in
("-topfile" :: topfile :: args) @ extra_args
in
Fiber.return (coqtop, args, env)
in
restore_cwd_and_execve (Common.root common) coqtop args env
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,141 @@
open Import
let ls_term (fetch_results : Path.Build.t -> string list Action_builder.t) =
let+ builder = Common.Builder.term
and+ paths = Arg.(value & pos_all string [ "." ] & info [] ~docv:"DIR")
and+ context =
Common.context_arg ~doc:"The context to look in. Defaults to the default context."
in
let common, config = Common.init builder in
let request (_ : Dune_rules.Main.build_system) =
let header = List.length paths > 1 in
let open Action_builder.O in
let+ paragraphs =
Action_builder.List.map paths ~f:(fun path ->
(* The user supplied directory *)
let dir = Path.of_string path in
(* The _build and source tree version of this directory *)
let build_dir, src_dir =
match (dir : Path.t) with
| In_source_tree d ->
Path.Build.append_source (Dune_engine.Context_name.build_dir context) d, d
| In_build_dir d ->
let src_dir =
(* We only drop the build context if it is correct. *)
match Path.Build.extract_build_context d with
| Some (dir_context_name, d) ->
if
Dune_engine.Context_name.equal
context
(Dune_engine.Context_name.of_string dir_context_name)
then d
else
User_error.raise
[ Pp.textf
"Directory %s is not in context %S."
(Path.to_string_maybe_quoted dir)
(Dune_engine.Context_name.to_string context)
]
| None -> Code_error.raise "aliases_targets: build dir without context" []
in
d, src_dir
| External _ ->
User_error.raise
[ Pp.textf
"Directories outside of the project are not supported: %s"
(Path.to_string_maybe_quoted dir)
]
in
(* Check if the directory exists. *)
let* () =
Action_builder.of_memo
@@
let open Memo.O in
Source_tree.find_dir src_dir
>>= function
| Some _ -> Memo.return ()
| None ->
(* The directory didn't exist. We therefore check if it was a
directory target and error for the user accordingly. *)
let+ is_dir_target =
Load_rules.is_under_directory_target (Path.build build_dir)
in
if is_dir_target
then
User_error.raise
[ Pp.textf
"Directory %s is a directory target. This command does not support \
the inspection of directory targets."
(Path.to_string dir)
]
else
User_error.raise
[ Pp.textf "Directory %s does not exist." (Path.to_string dir) ]
in
let+ targets = fetch_results build_dir in
(* If we are printing multiple directories, we print the directory
name as a header. *)
(if header then [ Pp.textf "%s:" (Path.to_string dir) ] else [])
@ [ Pp.concat_map targets ~f:Pp.text ~sep:Pp.space ]
|> Pp.concat ~sep:Pp.space)
in
Console.print
[ Pp.vbox @@ Pp.concat_map ~f:Pp.vbox paragraphs ~sep:(Pp.seq Pp.space Pp.space) ]
in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
Build.run_build_system ~common ~request
>>| fun (_ : (unit, [ `Already_reported ]) result) -> ()
;;
module Aliases_cmd = struct
let fetch_results (dir : Path.Build.t) =
let open Action_builder.O in
let+ alias_targets =
let+ load_dir =
Action_builder.of_memo (Load_rules.load_dir ~dir:(Path.build dir))
in
match load_dir with
| Load_rules.Loaded.Build build -> Dune_engine.Alias.Name.Map.keys build.aliases
| _ -> []
in
List.map ~f:Dune_engine.Alias.Name.to_string alias_targets
;;
let term = ls_term fetch_results
let command =
let doc = "Print aliases in a given directory. Works similarly to ls." in
Cmd.v (Cmd.info "aliases" ~doc ~envs:Common.envs) term
;;
end
module Targets_cmd = struct
let fetch_results (dir : Path.Build.t) =
let open Action_builder.O in
let+ targets =
let open Memo.O in
Target.all_direct_targets (Some (Path.Build.drop_build_context_exn dir))
>>| Path.Build.Map.to_list
|> Action_builder.of_memo
in
List.filter_map targets ~f:(fun (path, kind) ->
match Path.Build.equal (Path.Build.parent_exn path) dir with
| false -> None
| true ->
(* directory targets can be distinguied by the trailing path separator
*)
Some
(match kind with
| Target.File -> Path.Build.basename path
| Directory -> Path.Build.basename path ^ Filename.dir_sep))
;;
let term = ls_term fetch_results
let command =
let doc = "Print targets in a given directory. Works similarly to ls." in
Cmd.v (Cmd.info "targets" ~doc ~envs:Common.envs) term
;;
end

View file

@ -0,0 +1,15 @@
open Import
(** ls like commands for showing aliases and targets *)
module Aliases_cmd : sig
(** The aliases command lists all the aliases available in the given
directory, defaulting to the current working directory. *)
val command : unit Cmd.t
end
module Targets_cmd : sig
(** The targets command lists all the targets available in the given
directory, defaulting to the current working directory. *)
val command : unit Cmd.t
end

View file

@ -0,0 +1,56 @@
open Import
(* This command is not yet versioned, but some people are using it in
non-released tools. If you change the format of the output, please contact:
- rotor people for "describe workspace"
- duniverse people for "describe opam-files" *)
let subcommands =
[ Describe_workspace.command
; Describe_external_lib_deps.command
; Describe_opam_files.command
; Describe_pp.command
; Printenv.command
; Print_rules.command
; Installed_libraries.command
; Aliases_targets.Targets_cmd.command
; Aliases_targets.Aliases_cmd.command
; Package_entries.command
; Describe_pkg.command
; Describe_contexts.command
; Describe_depexts.command
; Describe_location.command
]
;;
let group =
let doc = "Describe the workspace." in
let man =
[ `S "DESCRIPTION"
; `P
{|Describe what is in the current workspace in either human or
machine readable form.
By default, this command output a human readable description of
the current workspace. This output is aimed at human and is not
suitable for machine processing. In particular, it is not versioned.
If you want to interpret the output of this command from a program,
you must use the $(b,--format) option to specify a machine readable
format as well as the $(b,--lang) option to get a stable output.|}
; `Blocks Common.help_secs
]
in
let info = Cmd.info "describe" ~doc ~man in
let default = Describe_workspace.term in
Cmd.group ~default info subcommands
;;
module Show = struct
let group =
let doc = "Command group for showing information about the workspace" in
Cmd.group (Cmd.info ~doc "show") subcommands
;;
end

View file

@ -0,0 +1,9 @@
open Import
(** Command group for dune describe *)
val group : unit Cmd.t
module Show : sig
(** Command group for dune show (alias of describe) *)
val group : unit Cmd.t
end

View file

@ -0,0 +1,23 @@
open Import
let term =
let+ builder = Common.Builder.term in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
let+ setup = Memo.run setup in
let ctxts =
List.map
~f:(fun (name, _) -> Context_name.to_string name)
(Context_name.Map.to_list setup.scontexts)
in
List.iter ctxts ~f:print_endline
;;
let command =
let doc = "List the build contexts available in the workspace." in
let info = Cmd.info ~doc "contexts" in
Cmd.v info term
;;

View file

@ -0,0 +1,4 @@
open Import
(** Dune command to print out the available build contexts.*)
val command : unit Cmd.t

View file

@ -0,0 +1,24 @@
open Import
let print_depexts context_name =
let open Fiber.O in
let+ depexts =
build_exn (fun () -> Dune_rules.Pkg_rules.all_filtered_depexts context_name)
in
Console.print [ Pp.concat_map ~sep:Pp.newline ~f:Pp.verbatim depexts ]
;;
let term =
let+ builder = Common.Builder.term
and+ context_name = Common.context_arg ~doc:"Build context to use." in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config (fun () -> print_depexts context_name)
;;
let info =
let doc = "Print the list of all the available depexts" in
Cmd.info "depexts" ~doc
;;
let command = Cmd.v info term

View file

@ -0,0 +1,4 @@
open Import
(** Command to print all depexts *)
val command : unit Cmd.t

View file

@ -0,0 +1,234 @@
open Import
module Lib_dep = Dune_lang.Lib_dep
module Kind = struct
type t =
| Required
| Optional
let to_dyn : t -> Dyn.t = function
| Required -> String "required"
| Optional -> String "optional"
;;
end
type lib_dep =
{ name : Lib_name.t
; kind : Kind.t
}
let lib_dep_to_dyn t =
let open Dyn in
List [ String (Lib_name.to_string t.name); Kind.to_dyn t.kind ]
;;
module Item = struct
module Kind = struct
type t =
| Executables
| Library
| Tests
let to_string = function
| Executables -> "executables"
| Library -> "library"
| Tests -> "tests"
;;
end
type t =
{ kind : Kind.t
; dir : Path.Source.t
; external_deps : lib_dep list
; internal_deps : lib_dep list
; names : string list
; package : Package.t option
; extensions : string list
}
let to_dyn { kind; dir; external_deps; internal_deps; names; package; extensions } =
let open Dyn in
let record =
record
[ "names", (list string) names
; "extensions", (list string) extensions
; "package", option Package.Name.to_dyn (Option.map ~f:Package.name package)
; "source_dir", String (Path.Source.to_string dir)
; "external_deps", list lib_dep_to_dyn external_deps
; "internal_deps", list lib_dep_to_dyn internal_deps
]
in
Variant (Kind.to_string kind, [ record ])
;;
end
type dep =
| Local of lib_dep
| External of lib_dep
let is_external db name =
let open Memo.O in
let+ lib = Dune_rules.Lib.DB.find_even_when_hidden db name in
match lib with
| None -> true
| Some t ->
(match Dune_rules.Lib_info.status (Dune_rules.Lib.info t) with
| Installed_private | Public _ | Private _ -> false
| Installed -> true)
;;
let resolve_lib db name kind =
let open Memo.O in
let+ is_external = is_external db name in
if is_external then External { name; kind } else Local { name; kind }
;;
let resolve_lib_pps db preprocess =
let open Memo.O in
Dune_rules.Instrumentation.with_instrumentation
preprocess
~instrumentation_backend:(Dune_rules.Lib.DB.instrumentation_backend db)
|> Resolve.Memo.read_memo
>>| Dune_lang.Preprocess.Per_module.pps
>>= Memo.parallel_map ~f:(fun (_, name) -> resolve_lib db name Kind.Required)
;;
let resolve_lib_deps db lib_deps =
let open Memo.O in
Memo.parallel_map lib_deps ~f:(fun (lib : Lib_dep.t) ->
match lib with
| Direct (_, name) | Re_export (_, name) ->
let+ v = resolve_lib db name Kind.Required in
[ v ]
| Select select ->
select.choices
|> Memo.parallel_map ~f:(fun (choice : Lib_dep.Select.Choice.t) ->
Lib_name.Set.to_string_list choice.required
@ Lib_name.Set.to_string_list choice.forbidden
|> Memo.parallel_map ~f:(fun name ->
let name = Lib_name.of_string name in
resolve_lib db name Kind.Optional))
>>| List.concat)
>>| List.concat
;;
let resolve_libs db dir libraries preprocess names package kind extensions =
let open Memo.O in
let open Item in
let* lib_deps = resolve_lib_deps db libraries in
let+ lib_pps = resolve_lib_pps db preprocess in
let deps = lib_deps @ lib_pps in
let internal_deps, external_deps =
deps
|> List.partition_map ~f:(function
| Local lib -> Either.Left lib
| External lib -> Either.Right lib)
in
{ external_deps; internal_deps; kind; names; package; dir; extensions }
;;
let exes_extensions (lib_config : Dune_rules.Lib_config.t) modes =
Dune_rules.Executables.Link_mode.Map.to_list modes
|> List.map ~f:(fun (m, loc) ->
Dune_rules.Executables.Link_mode.extension
m
~loc
~ext_obj:lib_config.ext_obj
~ext_dll:lib_config.ext_dll)
;;
let libs db (context : Context.t) =
let open Memo.O in
let* dune_files = Context.name context |> Dune_rules.Dune_load.dune_files in
Memo.parallel_map dune_files ~f:(fun (dune_file : Dune_rules.Dune_file.t) ->
Dune_file.stanzas dune_file
>>= Memo.parallel_map ~f:(fun stanza ->
let dir = Dune_file.dir dune_file in
match Stanza.repr stanza with
| Dune_rules.Executables.T exes ->
let* ocaml = Context.ocaml context in
resolve_libs
db
dir
exes.buildable.libraries
exes.buildable.preprocess
(List.map (Nonempty_list.to_list exes.names) ~f:snd)
exes.package
Item.Kind.Executables
(exes_extensions ocaml.lib_config exes.modes)
>>| List.singleton
| Dune_rules.Library.T lib ->
resolve_libs
db
dir
lib.buildable.libraries
lib.buildable.preprocess
[ Dune_rules.Library.best_name lib |> Lib_name.to_string ]
(Dune_rules.Library.package lib)
Item.Kind.Library
[]
>>| List.singleton
| Dune_rules.Tests.T tests ->
let* ocaml = Context.ocaml context in
resolve_libs
db
dir
tests.exes.buildable.libraries
tests.exes.buildable.preprocess
(List.map (Nonempty_list.to_list tests.exes.names) ~f:snd)
(if Option.is_none tests.package then tests.exes.package else tests.package)
Item.Kind.Tests
(exes_extensions ocaml.lib_config tests.exes.modes)
>>| List.singleton
| _ -> Memo.return [])
>>| List.concat)
>>| List.concat
;;
let external_resolved_libs (context : Context.t) =
let open Memo.O in
let* scope = Dune_rules.Scope.DB.find_by_dir (Context.build_dir context) in
let db = Dune_rules.Scope.libs scope in
libs db context
>>| List.filter ~f:(fun (x : Item.t) ->
not (List.is_empty x.external_deps && List.is_empty x.internal_deps))
;;
let to_dyn context_name external_resolved_libs =
let open Dyn in
Tuple [ String context_name; list Item.to_dyn external_resolved_libs ]
;;
let term =
let+ builder = Common.Builder.term
and+ context_name = Common.context_arg ~doc:"Build context to use."
and+ _ = Describe_lang_compat.arg
and+ format = Describe_format.arg in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
let* setup = Memo.run setup in
let super_context = Import.Main.find_scontext_exn setup ~name:context_name in
build_exn
@@ fun () ->
let open Memo.O in
let context_name =
Super_context.context super_context
|> Context.name
|> Dune_engine.Context_name.to_string
in
external_resolved_libs (Super_context.context super_context)
>>| to_dyn context_name
>>| Describe_format.print_dyn format
;;
let command =
let doc =
"Print out external libraries needed to build the project. It's an approximated set \
of libraries."
in
let info = Cmd.info ~doc "external-lib-deps" in
Cmd.v info term
;;

View file

@ -0,0 +1,4 @@
open Import
(** Dune command to describe the external library dependencies *)
val command : unit Cmd.t

View file

@ -0,0 +1,34 @@
open Import
type t =
| Sexp
| Csexp
let all = [ "sexp", Sexp; "csexp", Csexp ]
let arg =
let doc = Printf.sprintf "$(docv) must be %s" (Arg.doc_alts_enum all) in
Arg.(value & opt (enum all) Sexp & info [ "format" ] ~docv:"FORMAT" ~doc)
;;
let print_as_sexp dyn =
let rec dune_lang_of_sexp : Sexp.t -> Dune_lang.t = function
| Atom s -> Dune_lang.atom_or_quoted_string s
| List l -> List (List.map l ~f:dune_lang_of_sexp)
in
let cst =
dyn
|> Sexp.of_dyn
|> dune_lang_of_sexp
|> Dune_lang.Ast.add_loc ~loc:Loc.none
|> Dune_lang.Cst.concrete
in
let version = Dune_lang.Syntax.greatest_supported_version_exn Stanza.syntax in
Pp.to_fmt Stdlib.Format.std_formatter (Dune_lang.Format.pp_top_sexps ~version [ cst ])
;;
let print_dyn t dyn =
match t with
| Csexp -> Csexp.to_channel stdout (Sexp.of_dyn dyn)
| Sexp -> print_as_sexp dyn
;;

View file

@ -0,0 +1,13 @@
open Import
(** Formatting utilities for dune describe commands *)
type t =
| Sexp
| Csexp
(** Command line option for taking a serialisation format *)
val arg : t Term.t
(** [print_dyn t dyn] prints the dyn to stdout serialised as configured in [t] *)
val print_dyn : t -> Dyn.t -> unit

View file

@ -0,0 +1,11 @@
let arg =
Arg.(
value
& opt (some string) None
& info
[ "lang" ]
~docv:"VERSION"
~doc:
"This argument has no effect and is deprecated. It exists solely for backwards \
compatibility.")
;;

View file

@ -0,0 +1,5 @@
(** Dune describe commands used to take a --lang argument that did nothing
expect for dune describe workspace. To keep compatilbility with accepting
such an argument we provide a dummy argument here that can be used. It's
value will typically be ignored. *)
val arg : string option Cmdliner.Term.t

View file

@ -0,0 +1,43 @@
open! Import
let doc =
"Print the path to the executable using the same resolution logic as [dune exec]."
;;
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune describe location NAME) prints the path to the executable NAME using the same logic as:
|}
; `Pre "$ dune exec NAME"
; `P
"Dune will first try to resolve the executable within the public executables in \
the current project, then inside the \"bin\" directory of each package among the \
project's dependencies (when using dune package management), and finally within \
the directories listed in the $PATH environment variable."
]
;;
let info = Cmd.info "location" ~doc ~man
let term : unit Term.t =
let+ builder = Common.Builder.term
and+ context = Common.context_arg ~doc:{|Run the command in this build context.|}
and+ prog =
Arg.(required & pos 0 (some Exec.Cmd_arg.conv) None (Arg.info [] ~docv:"PROG"))
in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
build_exn
@@ fun () ->
let open Memo.O in
let* sctx = setup >>| Import.Main.find_scontext_exn ~name:context in
let* prog = Exec.Cmd_arg.expand ~root:(Common.root common) ~sctx prog in
let+ path = Exec.get_path common sctx ~prog >>| Path.to_string in
Dune_console.printf "%s" path
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open! Import
val command : unit Cmd.t

View file

@ -0,0 +1,38 @@
open Import
let term =
let+ builder = Common.Builder.term
and+ format = Describe_format.arg
and+ _ = Describe_lang_compat.arg in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
build_exn
@@ fun () ->
let open Memo.O in
let+ project = Source_tree.root () >>| Source_tree.Dir.project in
let packages = Dune_project.packages project |> Package.Name.Map.values in
let opam_file_to_dyn pkg =
let opam_file = Path.source (Package.opam_file pkg) in
let contents =
if Dune_project.generate_opam_files project
then (
let template_file = Dune_rules.Opam_create.template_file opam_file in
let template =
if Path.exists template_file
then Some (template_file, Io.read_file template_file)
else None
in
Dune_rules.Opam_create.generate project pkg ~template)
else Io.read_file opam_file
in
Dyn.Tuple [ String (Path.to_string opam_file); String contents ]
in
packages |> Dyn.list opam_file_to_dyn |> Describe_format.print_dyn format
;;
let command =
let doc = "Print information about the opam files that have been discovered." in
let info = Cmd.info ~doc "opam-files" in
Cmd.v info term
;;

View file

@ -0,0 +1,4 @@
open Import
(** Dune command to describe the opam files in a workspace *)
val command : unit Cmd.t

View file

@ -0,0 +1,200 @@
open Import
module Lock_dir = Dune_pkg.Lock_dir
module Local_package = Dune_pkg.Local_package
module Show_lock = struct
let print_lock lock_dir_arg () =
let open Fiber.O in
let* lock_dir_paths =
Memo.run (Workspace.workspace ())
>>| Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dir_arg
in
Fiber.parallel_map lock_dir_paths ~f:(fun lock_dir_path ->
let+ platform = Pkg_common.solver_env_from_system_and_context ~lock_dir_path in
let lock_dir = Lock_dir.read_disk_exn lock_dir_path in
let packages =
Lock_dir.Packages.pkgs_on_platform_by_name lock_dir.packages ~platform
|> Package_name.Map.values
in
Pp.concat
~sep:Pp.space
[ Pp.hovbox
@@ Pp.textf "Contents of %s:" (Path.Source.to_string_maybe_quoted lock_dir_path)
; Pkg_common.pp_packages packages
]
|> Pp.vbox)
>>| Console.print
;;
let term =
let+ builder = Common.Builder.term
and+ lock_dir_arg = Pkg_common.Lock_dirs_arg.term in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config @@ print_lock lock_dir_arg
;;
let command =
let doc = "Display packages in a lock file" in
let info = Cmd.info ~doc "lock" in
Cmd.v info term
;;
end
module Dependency_hash = struct
let print_local_packages_hash () =
let open Fiber.O in
let+ local_packages =
Pkg_common.find_local_packages
|> Memo.run
>>| Package_name.Map.values
>>| List.map ~f:Local_package.for_solver
in
let hash =
Local_package.For_solver.non_local_dependencies local_packages
|> Local_package.Dependency_hash.of_dependency_formula
in
match hash with
| None -> User_error.raise [ Pp.text "No non-local dependencies" ]
| Some dependency_hash ->
print_endline (Local_package.Dependency_hash.to_string dependency_hash)
;;
let term =
let+ builder = Common.Builder.term in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config print_local_packages_hash
;;
let info =
let doc =
"Print the hash of the project's non-local dependencies such as what would appear \
in the \"dependency_hash\" field of a a lock.dune file."
in
Cmd.info "dependency-hash" ~doc
;;
let command = Cmd.v info term
end
module List_locked_dependencies = struct
module Package_universe = Dune_pkg.Package_universe
module Lock_dir = Dune_pkg.Lock_dir
module Opam_repo = Dune_pkg.Opam_repo
module Package_version = Dune_pkg.Package_version
module Opam_solver = Dune_pkg.Opam_solver
let info =
let doc = "List the dependencies locked by a lockdir" in
let man = [ `S "DESCRIPTION"; `P "List the dependencies locked by a lockdir" ] in
Cmd.info "list-locked-dependencies" ~doc ~man
;;
let package_deps_in_lock_dir_pp package_universe package_name ~transitive =
let traverse, traverse_word =
if transitive then `Transitive, "Transitive" else `Immediate, "Immediate"
in
let opam_package =
Package_universe.opam_package_of_package package_universe package_name
in
let list_dependencies which =
Package_universe.opam_package_dependencies_of_package
package_universe
package_name
~which
~traverse
in
Pp.concat
~sep:Pp.cut
[ Pp.hbox
(Pp.textf
"%s dependencies of local package %s"
traverse_word
(OpamPackage.to_string opam_package))
; Pp.enumerate (list_dependencies `Non_test) ~f:(fun opam_package ->
Pp.text (OpamPackage.to_string opam_package))
; Pp.enumerate (list_dependencies `Test_only) ~f:(fun opam_package ->
Pp.textf "%s (test only)" (OpamPackage.to_string opam_package))
]
|> Pp.vbox
;;
let enumerate_lock_dirs_by_path workspace ~lock_dirs =
let lock_dirs = Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dirs workspace in
List.filter_map lock_dirs ~f:(fun lock_dir_path ->
if Path.exists (Path.source lock_dir_path)
then (
try Some (lock_dir_path, Lock_dir.read_disk_exn lock_dir_path) with
| User_error.E e ->
User_warning.emit
[ Pp.textf
"Failed to parse lockdir %s:"
(Path.Source.to_string_maybe_quoted lock_dir_path)
; User_message.pp e
];
None)
else None)
;;
let list_locked_dependencies ~transitive ~lock_dirs () =
let open Fiber.O in
let* lock_dirs_by_path, local_packages =
let open Memo.O in
Memo.both
(Workspace.workspace () >>| enumerate_lock_dirs_by_path ~lock_dirs)
Pkg_common.find_local_packages
|> Memo.run
in
let+ pp =
Fiber.parallel_map lock_dirs_by_path ~f:(fun (lock_dir_path, lock_dir) ->
let+ platform = Pkg_common.solver_env_from_system_and_context ~lock_dir_path in
let package_universe =
Package_universe.create ~platform local_packages lock_dir |> User_error.ok_exn
in
Pp.vbox
(Pp.concat
~sep:Pp.cut
[ Pp.hbox
(Pp.textf
"Dependencies of local packages locked in %s"
(Path.Source.to_string_maybe_quoted lock_dir_path))
; Pp.enumerate
(Package_name.Map.keys local_packages)
~f:(package_deps_in_lock_dir_pp package_universe ~transitive)
|> Pp.box
]))
>>| Pp.concat ~sep:Pp.cut
>>| Pp.vbox
in
Console.print [ pp ]
;;
let term =
let+ builder = Common.Builder.term
and+ transitive =
Arg.(
value
& flag
& info
[ "transitive" ]
~doc:
"Display transitive dependencies (by default only immediate dependencies \
are displayed)")
and+ lock_dirs = Pkg_common.Lock_dirs_arg.term in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ list_locked_dependencies ~transitive ~lock_dirs
;;
let command = Cmd.v info term
end
let command =
let doc = "Subcommands related to package management" in
let info = Cmd.info ~doc "pkg" in
Cmd.group
info
[ Show_lock.command; List_locked_dependencies.command; Dependency_hash.command ]
;;

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,183 @@
open Import
module Dialect = Dune_lang.Dialect
let dialect_and_ml_kind file =
let open Memo.O in
let _base, ext =
let file = Path.of_string file in
Path.split_extension file
in
let+ project = Source_tree.root () >>| Source_tree.Dir.project in
let dialects = Dune_project.dialects project in
match Dialect.DB.find_by_extension dialects ext with
| None -> User_error.raise [ Pp.textf "unsupported extension: %s" ext ]
| Some x -> x
;;
let execute_pp_action ~sctx file pp_file dump_file =
let open Memo.O in
let* expander =
let bindings =
Dune_lang.Pform.Map.singleton
(Var Input_file)
[ Dune_lang.Value.Path (Path.build (pp_file |> Path.as_in_build_dir_exn)) ]
in
let dir = pp_file |> Path.parent_exn |> Path.as_in_build_dir_exn in
Super_context.expander sctx ~dir >>| Dune_rules.Expander.add_bindings ~bindings
in
let context = Dune_rules.Expander.context expander in
let build_dir = Context_name.build_dir context in
let* input =
let* action, _observing_facts =
let* loc, action =
let+ dialect, ml_kind = dialect_and_ml_kind file in
match Dialect.print_ast dialect ml_kind with
| Some print_ast -> print_ast
| None ->
(* fall back to the OCaml print_ast function, known to exist, if one
doesn't exist for this dialect. *)
Dialect.print_ast Dialect.ocaml ml_kind |> Option.value_exn
in
let build =
let open Action_builder.O in
let+ build =
Dune_rules.For_tests.Action_unexpanded.expand_no_targets
action
~chdir:build_dir
~loc
~expander
~deps:[]
~what:"describe pp"
in
Action.with_outputs_to dump_file build.action
in
Action_builder.evaluate_and_collect_facts build
in
let+ env = Dune_rules.Super_context.context_env sctx
and+ execution_parameters = Dune_engine.Execution_parameters.default in
let targets =
let unvalidated = Targets.File.create dump_file in
match Targets.validate unvalidated with
| Valid targets -> targets
| No_targets
| Inconsistent_parent_dir
| File_and_directory_target_with_the_same_name _ -> assert false
in
{ Dune_engine.Action_exec.targets = Some targets
; root = Path.build build_dir
; context = Some (Dune_engine.Build_context.create ~name:context)
; env
; rule_loc = Loc.none
; execution_parameters
; action
}
in
let ok =
let open Fiber.O in
let build_deps deps = Build_system.build_deps deps |> Memo.run in
let* result = Dune_engine.Action_exec.exec input ~build_deps in
Dune_engine.Action_exec.Exec_result.ok_exn result >>| ignore
in
Memo.of_non_reproducible_fiber ok
;;
let print_pped_file =
let dump_file pp_file ~ml_kind =
Path.set_extension
pp_file
~ext:
(match (ml_kind : Ocaml.Ml_kind.t) with
| Intf -> ".cmi.dump"
| Impl -> ".cmo.dump")
|> Path.as_in_build_dir_exn
in
fun ~sctx file pp_file ~ml_kind ->
let open Memo.O in
let dump_file = dump_file pp_file ~ml_kind in
let+ () = execute_pp_action ~sctx file pp_file dump_file in
let dump_file = Path.build dump_file in
match Path.stat dump_file with
| Ok { st_kind = S_REG; _ } ->
Io.cat dump_file;
Path.unlink_no_err dump_file
| _ ->
User_error.raise
[ Pp.textf "cannot find a dump file: %s" (Path.to_string dump_file) ]
;;
let find_module ~sctx file =
let open Memo.O in
let src = Path.drop_optional_build_context_src_exn (Path.build file) in
Dune_rules.Top_module.find_module sctx src
>>| function
| None -> None
| Some (m, _, _, origin) ->
(match
Dune_rules.Ml_sources.Origin.preprocess origin
|> Dune_lang.Preprocess.Per_module.find (Dune_rules.Module.name m)
with
| Pps { staged = true; loc; _ } -> Some (`Staged_pps loc)
| _ -> Some (`Module m))
;;
let get_pped_file super_context file =
let open Memo.O in
let context = Super_context.context super_context in
let in_build_dir file =
file |> Path.to_string |> Path.Build.relative (Context.build_dir context)
in
let file_in_build_dir =
if String.is_empty file
then User_error.raise [ Pp.textf "No file given." ]
else Path.of_string file |> in_build_dir
in
let* ml_kind =
let+ _, ml_kind = dialect_and_ml_kind file in
ml_kind
in
let file_not_found () =
User_error.raise
[ Pp.textf "%s does not exist" (Path.Build.to_string_maybe_quoted file_in_build_dir)
]
in
find_module ~sctx:super_context file_in_build_dir
>>= function
| None -> file_not_found ()
| Some (`Module m) ->
(match
Dune_rules.Module.source m ~ml_kind |> Option.map ~f:Dune_rules.Module.File.path
with
| None -> file_not_found ()
| Some pp_file ->
let+ () = Build_system.build_file pp_file in
Ok (pp_file, ml_kind))
| Some (`Staged_pps loc) ->
User_error.raise ~loc [ Pp.text "staged_pps are not supported." ]
;;
let term =
let+ builder = Common.Builder.term
and+ context_name = Common.context_arg ~doc:"Build context to use."
and+ _ = Describe_lang_compat.arg
and+ file = Arg.(required & pos 0 (some string) None (Arg.info [] ~docv:"FILE")) in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
let* setup = Memo.run setup in
let sctx = Import.Main.find_scontext_exn setup ~name:context_name in
build_exn
@@ fun () ->
let open Memo.O in
let* result = get_pped_file sctx file in
match result with
| Error file -> Io.cat file |> Memo.return
| Ok (pp_file, ml_kind) -> print_pped_file ~sctx file pp_file ~ml_kind
;;
let command =
let doc = "Build a given FILE and print the preprocessed output." in
let info = Cmd.info ~doc "pp" in
Cmd.v info term
;;

View file

@ -0,0 +1,4 @@
open Import
(** Dune command to show the preprocessed version of a file. *)
val command : unit Cmd.t

View file

@ -0,0 +1,687 @@
open Import
module Options = struct
(* Option flags for what to do while crawling the workspace *)
type t =
{ with_deps : bool (* whether to compute direct dependencies between modules *)
; with_pps : bool
(* whether to include the dependencies to ppx-rewriters (that are
used at compile time) *)
}
(* whether to sanitize absolute paths of workspace items, and their UIDs, to
ensure reproducible tests *)
let sanitize_for_tests = ref false
let arg_with_deps =
let open Arg in
value
& flag
& info
[ "with-deps" ]
~doc:"Whether the dependencies between modules should be printed."
;;
let arg_with_pps =
let open Arg in
value
& flag
& info
[ "with-pps" ]
~doc:
"Whether the dependencies towards ppx-rewriters (that are called at compile \
time) should be taken into account."
;;
let arg_sanitize_for_tests =
let open Arg in
value
& flag
& info
[ "sanitize-for-tests" ]
~doc:
"Sanitize the absolute paths in workspace items, and the associated UIDs, so \
that the output is reproducible."
;;
let arg : t Term.t =
let+ with_deps = arg_with_deps
and+ with_pps = arg_with_pps
and+ sanitize_for_tests_value = arg_sanitize_for_tests in
sanitize_for_tests := sanitize_for_tests_value;
{ with_deps; with_pps }
;;
end
(* The module [Descr] is a typed representation of the description of a
workspace, that is provided by the ``dune describe workspace`` command.
Each sub-module contains a [to_dyn] function, that translates the
descriptors to a value of type [Dyn.t].
The typed representation aims at precisely describing the structure of the
information computed by ``dune describe``, and hopefully make users' life
easier in decoding the S-expressions into meaningful contents. *)
module Descr = struct
(* [dyn_path p] converts a path to a value of type [Dyn.t]. Remark: this is
different from Path.to_dyn, that produces extra tags from a variant
datatype. *)
let dyn_path (p : Path.t) : Dyn.t = String (Path.to_string p)
(* Description of the dependencies of a module *)
module Mod_deps = struct
type t =
{ for_intf : Dune_rules.Module_name.t list
(* direct module dependencies for the interface *)
; for_impl : Dune_rules.Module_name.t list
(* direct module dependencies for the implementation *)
}
(* Conversion to the [Dyn.t] type *)
let to_dyn { for_intf; for_impl } =
let open Dyn in
record
[ "for_intf", list Dune_rules.Module_name.to_dyn for_intf
; "for_impl", list Dune_rules.Module_name.to_dyn for_impl
]
;;
end
(* Description of modules *)
module Mod = struct
type t =
{ name : Dune_rules.Module_name.t (* name of the module *)
; impl : Path.t option (* path to the .ml file, if any *)
; intf : Path.t option (* path to the .mli file, if any *)
; cmt : Path.t option (* path to the .cmt file, if any *)
; cmti : Path.t option (* path to the .cmti file, if any *)
; module_deps : Mod_deps.t (* direct module dependencies *)
}
(* Conversion to the [Dyn.t] type *)
let to_dyn { Options.with_deps; _ } { name; impl; intf; cmt; cmti; module_deps }
: Dyn.t
=
let open Dyn in
let optional_fields =
let module_deps =
if with_deps then Some ("module_deps", Mod_deps.to_dyn module_deps) else None
in
(* we build a list of options, that is later filtered, so that adding
new optional fields in the future can be done easily *)
match module_deps with
| None -> []
| Some module_deps -> [ module_deps ]
in
record
@@ [ "name", Dune_rules.Module_name.to_dyn name
; "impl", option dyn_path impl
; "intf", option dyn_path intf
; "cmt", option dyn_path cmt
; "cmti", option dyn_path cmti
]
@ optional_fields
;;
end
(* Description of executables *)
module Exe = struct
type t =
{ names : string list (* names of the executable *)
; requires : Digest.t list
(* list of direct dependencies to libraries, identified by their
digests *)
; modules : Mod.t list (* list of the modules the executable is composed of *)
; include_dirs : Path.t list (* list of include directories *)
}
let map_path t ~f = { t with include_dirs = List.map ~f t.include_dirs }
(* Conversion to the [Dyn.t] type *)
let to_dyn options { names; requires; modules; include_dirs } : Dyn.t =
let open Dyn in
record
[ "names", List (List.map ~f:(fun name -> String name) names)
; "requires", Dyn.(list string) (List.map ~f:Digest.to_string requires)
; "modules", list (Mod.to_dyn options) modules
; "include_dirs", list dyn_path include_dirs
]
;;
end
(* Description of libraries *)
module Lib = struct
type t =
{ name : Lib_name.t (* name of the library *)
; uid : Digest.t (* digest of the library *)
; local : bool (* whether this library is local *)
; requires : Digest.t list
(* list of direct dependendies to libraries, identified by their
digests *)
; source_dir : Path.t
(* path to the directory that contains the sources of this library *)
; modules : Mod.t list (* list of the modules the executable is composed of *)
; include_dirs : Path.t list (* list of include directories *)
}
let map_path t ~f =
{ t with source_dir = f t.source_dir; include_dirs = List.map ~f t.include_dirs }
;;
(* Conversion to the [Dyn.t] type *)
let to_dyn options { name; uid; local; requires; source_dir; modules; include_dirs }
: Dyn.t
=
let open Dyn in
record
[ "name", Lib_name.to_dyn name
; "uid", String (Digest.to_string uid)
; "local", Bool local
; "requires", (list string) (List.map ~f:Digest.to_string requires)
; "source_dir", dyn_path source_dir
; "modules", list (Mod.to_dyn options) modules
; "include_dirs", (list dyn_path) include_dirs
]
;;
end
(* Description of items: executables, or libraries *)
module Item = struct
type t =
| Executables of Exe.t
| Library of Lib.t
| Root of Path.t
| Build_context of Path.t
let map_path t ~f =
match t with
| Executables exe -> Executables (Exe.map_path exe ~f)
| Library lib -> Library (Lib.map_path lib ~f)
| Root r -> Root (f r)
| Build_context c -> Build_context (f c)
;;
(* Conversion to the [Dyn.t] type *)
let to_dyn options : t -> Dyn.t = function
| Executables exe_descr -> Variant ("executables", [ Exe.to_dyn options exe_descr ])
| Library lib_descr -> Variant ("library", [ Lib.to_dyn options lib_descr ])
| Root root -> Variant ("root", [ String (Path.to_absolute_filename root) ])
| Build_context build_ctxt ->
Variant ("build_context", [ String (Path.to_string build_ctxt) ])
;;
end
(* Description of a workspace: a list of items *)
module Workspace = struct
type t = Item.t list
(* Conversion to the [Dyn.t] type *)
let to_dyn options (items : t) : Dyn.t = Dyn.list (Item.to_dyn options) items
end
end
module Lang = struct
type t = Dune_lang.Syntax.Version.t
let arg_conv =
let parser s =
match Scanf.sscanf s "%u.%u" (fun a b -> a, b) with
| Ok t -> Ok t
| Error () -> Error (`Msg "Expected version of the form NNN.NNN.")
in
let printer ppf t =
Stdlib.Format.fprintf ppf "%s" (Dune_lang.Syntax.Version.to_string t)
in
Arg.conv ~docv:"VERSION" (parser, printer)
;;
let arg : t Term.t =
Term.ret
@@ let+ v =
Arg.(
value
& opt arg_conv (0, 1)
& info
[ "lang" ]
~docv:"VERSION"
~doc:"Behave the same as this version of Dune.")
in
if v = (0, 1)
then `Ok v
else (
let msg =
let pp =
"Only --lang 0.1 is available at the moment as this command is not yet \
stabilised. If you would like to release a software that relies on the \
output of 'dune describe', please open a ticket on \
https://github.com/ocaml/dune."
|> Pp.text
in
Stdlib.Format.asprintf "%a" Pp.to_fmt pp
in
`Error (true, msg))
;;
end
(* The following module is responsible sanitizing the output of
[dune describe workspace], so that the absolute paths and the UIDs that
depend on them are stable for tests. These paths may differ, depending on
the machine they are run on. *)
module Sanitize_for_tests = struct
module Workspace = struct
let fake_findlib = lazy (Path.External.of_string "/FINDLIB")
let fake_workspace = lazy (Path.External.of_string "/WORKSPACE_ROOT")
let sanitize_with_findlib ~findlib_paths path =
let path = Path.external_ path in
List.find_map findlib_paths ~f:(fun candidate ->
let open Option.O in
let* candidate = Path.as_external candidate in
(* if the path to rename is an external path, try to find the
OCaml root inside, and replace it with a fixed string *)
let+ without_prefix = Path.drop_prefix ~prefix:(Path.external_ candidate) path in
(* we have found the OCaml root path: let's replace it with a
constant string *)
Path.External.append_local (Lazy.force fake_findlib) without_prefix)
;;
(* Sanitizes a workspace description, by renaming non-reproducible UIDs and
paths *)
let really_sanitize ~findlib_paths items =
let rename_path = function
(* we have found a path for OCaml's root: let's define the renaming
function *)
| Path.External path ->
sanitize_with_findlib ~findlib_paths path
|> Option.value ~default:path
|> Path.external_
| In_source_tree p ->
(* Replace the workspace root with a fixed string *)
Path.External.append_local (Lazy.force fake_workspace) (Path.Source.to_local p)
|> Path.external_
| path ->
(* Otherwise, it should not be changed *)
path
in
(* now, we rename the UIDs in the [requires] field , while reversing the
list of items, so that we get back the original ordering *)
List.map ~f:(Descr.Item.map_path ~f:rename_path) items
;;
(* Sanitizes a workspace description when options ask to do so, or performs
no change at all otherwise *)
let sanitize ~findlib_paths items =
if !Options.sanitize_for_tests then really_sanitize ~findlib_paths items else items
;;
end
end
(* Crawl the workspace to get all the data *)
module Crawl = struct
open Dune_rules
open Dune_engine
open Memo.O
(* Computes the digest of a library *)
let uid_of_library (lib : Lib.t) : Digest.t =
let name = Lib.name lib in
if Lib.is_local lib
then (
let source_dir = Lib_info.src_dir (Lib.info lib) in
Digest.generic (name, Path.to_string source_dir))
else Digest.generic name
;;
let immediate_deps_of_module ~options ~obj_dir ~modules unit =
match (options : Options.t) with
| { with_deps = false; _ } ->
Action_builder.return { Ocaml.Ml_kind.Dict.intf = []; impl = [] }
| { with_deps = true; _ } ->
let deps ml_kind =
Dune_rules.Dep_rules.immediate_deps_of unit modules ~obj_dir ~ml_kind
in
let open Action_builder.O in
let+ intf, impl = Action_builder.both (deps Intf) (deps Impl) in
{ Ocaml.Ml_kind.Dict.intf; impl }
;;
(* Builds the description of a module from a module and its object directory *)
let module_
~obj_dir
~(deps_for_intf : Module.t list)
~(deps_for_impl : Module.t list)
(m : Module.t)
: Descr.Mod.t
=
let source ml_kind = Option.map (Module.source m ~ml_kind) ~f:Module.File.path in
let cmt ml_kind =
Dune_rules.Obj_dir.Module.cmt_file obj_dir m ~ml_kind ~cm_kind:(Ocaml Cmi)
in
{ Descr.Mod.name = Module.name m
; impl = source Impl
; intf = source Intf
; cmt = cmt Impl
; cmti = cmt Intf
; module_deps =
{ for_intf = List.map ~f:Module.name deps_for_intf
; for_impl = List.map ~f:Module.name deps_for_impl
}
}
;;
(* Builds the list of modules *)
let modules ~obj_dir ~deps_of modules_ : Descr.Mod.t list Memo.t =
modules_
|> Modules.With_vlib.drop_vlib
|> Modules.fold ~init:(Memo.return []) ~f:(fun m macc ->
let* acc = macc in
let deps = deps_of m in
let+ { Ocaml.Ml_kind.Dict.intf = deps_for_intf; impl = deps_for_impl }, _ =
Dune_engine.Action_builder.evaluate_and_collect_facts deps
in
module_ ~obj_dir ~deps_for_intf ~deps_for_impl m :: acc)
;;
(* Builds a workspace item for the provided executables object *)
let executables sctx ~options ~project ~dir (exes : Executables.t)
: (Descr.Item.t * Lib.Set.t) option Memo.t
=
let* expander = Super_context.expander sctx ~dir in
Expander.eval_blang expander exes.enabled_if
>>= function
| false -> Memo.return None
| true ->
let first_exe = snd (Nonempty_list.hd exes.names) in
let* scope =
Scope.DB.find_by_project (Super_context.context sctx |> Context.name) project
in
let* modules_, obj_dir =
let+ modules_, obj_dir =
Dir_contents.get sctx ~dir
>>= Dir_contents.ocaml
>>= Ml_sources.modules_and_obj_dir
~libs:(Scope.libs scope)
~for_:(Exe { first_exe })
in
Modules.With_vlib.modules modules_, obj_dir
in
let* pp_map =
let+ version =
let+ ocaml = Super_context.context sctx |> Context.ocaml in
ocaml.version
in
Staged.unstage
@@ Pp_spec.pped_modules_map
(Dune_lang.Preprocess.Per_module.without_instrumentation
exes.buildable.preprocess)
version
in
let deps_of module_ =
let module_ = pp_map module_ in
immediate_deps_of_module ~options ~obj_dir ~modules:modules_ module_
in
let obj_dir = Obj_dir.of_local obj_dir in
let* modules_ = modules ~obj_dir ~deps_of modules_ in
let+ requires =
let* compile_info = Exe_rules.compile_info ~scope exes in
let open Resolve.Memo.O in
let* requires = Lib.Compile.direct_requires compile_info in
if options.with_pps
then
let+ pps = Lib.Compile.pps compile_info in
pps @ requires
else Resolve.Memo.return requires
in
(match Resolve.peek requires with
| Error () -> None
| Ok libs ->
let include_dirs = Obj_dir.all_cmis obj_dir in
let exe_descr =
{ Descr.Exe.names = List.map ~f:snd (Nonempty_list.to_list exes.names)
; requires = List.map ~f:uid_of_library libs
; modules = modules_
; include_dirs
}
in
Some (Descr.Item.Executables exe_descr, Lib.Set.of_list libs))
;;
(* Builds a workspace item for the provided library object *)
let library sctx ~options (lib : Lib.t) : Descr.Item.t option Memo.t =
let* requires = Lib.requires lib in
match Resolve.peek requires with
| Error () -> Memo.return None
| Ok requires ->
let name = Lib.name lib in
let info = Lib.info lib in
let src_dir = Lib_info.src_dir info in
let obj_dir = Lib_info.obj_dir info in
let+ modules_ =
match Lib.is_local lib with
| false -> Memo.return []
| true ->
(* XXX why do we have a second object directory? *)
let* modules_, obj_dir_ =
let* libs =
Scope.DB.find_by_dir (Path.as_in_build_dir_exn src_dir) >>| Scope.libs
in
let+ modules_, obj_dir_ =
Dir_contents.get sctx ~dir:(Path.as_in_build_dir_exn src_dir)
>>= Dir_contents.ocaml
>>= Ml_sources.modules_and_obj_dir
~libs
~for_:(Library (Lib_info.lib_id info |> Lib_id.to_local_exn))
in
Modules.With_vlib.modules modules_, obj_dir_
in
let* pp_map =
let+ version =
let+ ocaml = Super_context.context sctx |> Context.ocaml in
ocaml.version
in
Staged.unstage
@@ Pp_spec.pped_modules_map
(Dune_lang.Preprocess.Per_module.without_instrumentation
(Lib_info.preprocess info))
version
in
let deps_of module_ =
immediate_deps_of_module
~options
~obj_dir:obj_dir_
~modules:modules_
(pp_map module_)
in
modules ~obj_dir ~deps_of modules_
in
let include_dirs = Obj_dir.all_cmis obj_dir in
let lib_descr =
{ Descr.Lib.name
; uid = uid_of_library lib
; local = Lib.is_local lib
; requires = List.map requires ~f:uid_of_library
; source_dir = src_dir
; modules = modules_
; include_dirs
}
in
Some (Descr.Item.Library lib_descr)
;;
(* [source_path_is_in_dirs dirs p] tests whether the source path [p] is a
descendant of some of the provided directory [dirs]. If [dirs = None],
then it always succeeds. If [dirs = Some l], then a matching directory is
search in the list [l]. *)
let source_path_is_in_dirs dirs (p : Path.Source.t) =
match dirs with
| None -> true
| Some dirs -> List.exists ~f:(fun dir -> Path.Source.is_descendant p ~of_:dir) dirs
;;
(* Tests whether a dune file is located in a path that is a descendant of
some directory *)
let dune_file_is_in_dirs dirs dune_file =
Dune_file.dir dune_file |> source_path_is_in_dirs dirs
;;
(* Tests whether a library is located in a path that is a descendant of some
directory *)
let lib_is_in_dirs dirs (lib : Lib.t) =
source_path_is_in_dirs
dirs
(Path.drop_build_context_exn @@ Lib_info.best_src_dir @@ Lib.info lib)
;;
(* Builds a workspace item for the root path *)
let root () = Descr.Item.Root Path.root
(* Builds a workspace item for the build directory path *)
let build_ctxt (context : Context.t) : Descr.Item.t =
Descr.Item.Build_context (Path.build (Context.build_dir context))
;;
(* Builds a workspace description for the provided dune setup and context *)
let workspace
options
({ Dune_rules.Main.contexts = _; scontexts } : Dune_rules.Main.build_system)
(context : Context.t)
dirs
: Descr.Workspace.t Memo.t
=
let context_name = Context.name context in
let sctx = Context_name.Map.find_exn scontexts context_name in
let open Memo.O in
let* dune_files =
Dune_load.dune_files context_name >>| List.filter ~f:(dune_file_is_in_dirs dirs)
in
let* exes, exe_libs =
(* the list of workspace items that describe executables, and the list of
their direct library dependencies *)
Memo.parallel_map dune_files ~f:(fun (dune_file : Dune_file.t) ->
Dune_file.stanzas dune_file
>>= Memo.parallel_map ~f:(fun stanza ->
match Stanza.repr stanza with
| Executables.T exes ->
let dir =
Path.Build.append_source
(Context.build_dir context)
(Dune_file.dir dune_file)
in
let project = Dune_file.project dune_file in
executables sctx ~options ~project ~dir exes
| _ -> Memo.return None)
>>| List.filter_opt)
>>| List.concat
>>| List.split
in
let exe_libs =
(* conflate the dependencies of executables into a single set *)
Lib.Set.union_all exe_libs
in
let* project_libs =
(* the list of libraries declared in the project *)
Dune_load.projects ()
>>= Memo.parallel_map ~f:(fun project ->
Scope.DB.find_by_project (Context.name context) project
>>| Scope.libs
>>= Lib.DB.all)
>>| Lib.Set.union_all
>>| Lib.Set.filter ~f:(lib_is_in_dirs dirs)
in
let+ libs =
(* the executables' libraries, and the project's libraries *)
Lib.Set.union exe_libs project_libs
|> Lib.Set.to_list
|> Lib.descriptive_closure ~with_pps:options.with_pps
>>= Memo.parallel_map ~f:(library ~options sctx)
>>| List.filter_opt
in
let root = root () in
let build_ctxt = build_ctxt context in
root :: build_ctxt :: (exes @ libs)
;;
end
let find_dir common dir =
let p = Path.Source.(relative root) (Common.prefix_target common dir) in
let s = Path.source p in
if not @@ Path.exists s
then User_error.raise [ Pp.textf "No such file or directory: %s" (Path.to_string s) ];
if not @@ Path.is_directory s
then
User_error.raise
[ Pp.textf "File exists, but is not a directory: %s" (Path.to_string s) ];
Memo.return p
;;
let term : unit Term.t =
let+ builder = Common.Builder.term
and+ what =
Arg.(
value
& pos_all string []
& info
[]
~docv:"DIRS"
~doc:
"prints a description of the workspace's structure. If some directories DIRS \
are provided, then only those directories of the workspace are considered.")
and+ context_name = Common.context_arg ~doc:"Build context to use."
and+ format = Describe_format.arg
and+ lang = Lang.arg
and+ options = Options.arg in
let common, config = Common.init builder in
let dirs =
let args = "workspace" :: what in
let parse =
Dune_lang.Syntax.set Stanza.syntax (Active lang)
@@
let open Dune_lang.Decoder in
fields
@@ field "workspace"
@@ let+ dirs = repeat relative_file in
(* [None] means that all directories should be accepted,
whereas [Some l] means that only the directories in the
list [l] should be accepted. The checks on whether the
paths exist and whether they are directories are performed
later in the [describe] function. *)
let dirs = if List.is_empty dirs then None else Some dirs in
dirs
in
let ast =
Dune_lang.Ast.add_loc
~loc:Loc.none
(List (List.map args ~f:Dune_lang.atom_or_quoted_string))
in
Dune_lang.Decoder.parse parse Univ_map.empty ast
in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
build_exn
@@ fun () ->
let open Memo.O in
let* setup = setup in
let super_context = Import.Main.find_scontext_exn setup ~name:context_name in
let context = Super_context.context super_context in
let* findlib_paths = Context.findlib_paths context in
(* prefix directories with the workspace root, so that the
command also works correctly when it is run from a
subdirectory *)
Memo.Option.map dirs ~f:(Memo.List.map ~f:(find_dir common))
>>= Crawl.workspace options setup context
>>| Sanitize_for_tests.Workspace.sanitize ~findlib_paths
>>| Descr.Workspace.to_dyn options
>>| Describe_format.print_dyn format
;;
let command =
let doc =
"Print a description of the workspace's structure. If some directories DIRS are \
provided, then only those directories of the workspace are considered."
in
let info = Cmd.info ~doc "workspace" in
Cmd.v info term
;;

View file

@ -0,0 +1,6 @@
open Import
val term : unit Term.t
(** Dune command that describes the workspace *)
val command : unit Cmd.t

View file

@ -0,0 +1,26 @@
open Import
let term =
let+ builder = Common.Builder.term
and+ context_name = Common.context_arg ~doc:"Build context to use."
and+ format = Describe_format.arg in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
let* setup = Memo.run setup in
let super_context = Import.Main.find_scontext_exn setup ~name:context_name in
build_exn
@@ fun () ->
let open Memo.O in
Dune_rules.Install_rules.stanzas_to_entries super_context
>>| Package.Name.Map.to_dyn (Dyn.list Install.Entry.Sourced.to_dyn)
>>| Describe_format.print_dyn format
;;
let command =
let doc = "prints information about the entries per package." in
let info = Cmd.info ~doc "package-entries" in
Cmd.v info term
;;

View file

@ -0,0 +1,4 @@
open Import
(** Dune command to print out information about the entries per package.*)
val command : unit Cmd.t

View file

@ -0,0 +1,38 @@
open Import
let exec () =
let open Fiber.O in
let where = Rpc_common.active_server_exn () in
let module Client = Dune_rpc_client.Client in
let+ errors =
let* connect = Client.Connection.connect_exn where in
Dune_rpc_impl.Client.client
connect
(Dune_rpc_private.Initialize.Request.create
~id:(Dune_rpc_private.Id.make (Sexp.Atom "diagnostics_cmd")))
~f:(fun cli ->
let* decl =
Client.Versioned.prepare_request cli Dune_rpc_private.Public.Request.diagnostics
in
match decl with
| Error e -> raise (Dune_rpc_private.Version_error.E e)
| Ok decl -> Client.request cli decl ())
in
match errors with
| Ok errors ->
List.iter errors ~f:(fun err ->
Console.print_user_message (Dune_rpc.Diagnostic.to_user_message err))
| Error e -> Rpc_common.raise_rpc_error e
;;
let info =
let doc = "Fetch and return errors from the current build." in
Cmd.info "diagnostics" ~doc
;;
let term =
let+ (builder : Common.Builder.t) = Common.Builder.term in
Rpc_common.client_term builder exec
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,87 @@
(include_subdirs unqualified)
(executable
(name main)
(public_name dune)
(package dune)
(enabled_if
(<> %{profile} dune-bootstrap))
(libraries
memo
promote
ocaml
ocaml_config
dune_lang
predicate_lang
fiber
fiber_event_bus
stdune
dune_console
unix
install
dune_findlib
dune_metrics
dune_digest
dune_cache
dune_cache_storage
dune_graph
dune_rules
dune_vcs
dune_engine
dune_targets
dune_util
dune_upgrader
dune_pkg
cmdliner
threads
; Kept to keep implicit_transitive_deps false working in 4.x
threads.posix
build_info
dune_config
dune_config_file
chrome_trace
dune_stats
csexp
csexp_rpc
dune_rpc_impl
dune_rules_rpc
dune_rpc_private
dune_rpc_client
dune_spawn
opam_format
source
xdg)
(bootstrap_info bootstrap-info))
; Installing the dune binary depends on the kind of build:
; - for bootstrap builds, dune.exe is copied from ../dune.exe
; and installed using a manual install stanza
; - for non-bootstrap builds (building dune with another dune),
; the executable stanza does everything (and attached it to the
; right package, which is important for build-info to succeed)
; but we still need to setup a dummy dune.exe so that profiles
; agree on the targets.
(rule
(enabled_if
(<> %{profile} dune-bootstrap))
(action
(with-stdout-to dune.exe (progn))))
(rule
(action
(copy ../_boot/dune.exe dune.exe))
(enabled_if
(= %{profile} dune-bootstrap)))
(install
(section bin)
(enabled_if
(= %{profile} dune-bootstrap))
(package dune)
(files
(dune.exe as dune)))
(deprecated_library_name
(old_public_name dune.configurator)
(new_public_name dune-configurator))

View file

@ -0,0 +1,611 @@
open Import
(** Because the dune_init utility deals with the addition of stanzas and fields
to dune projects and files, we need to inspect and manipulate the concrete
syntax tree (CST) a good deal. *)
module Cst = Dune_lang.Cst
(** Abstractions around the kinds of files handled during initialization *)
module File = struct
type dune =
{ path : Path.t
; name : string
; content : Cst.t list
}
type text =
{ path : Path.t
; name : string
; content : string
}
type t =
| Dune of dune
| Text of text
let make_text path name content = Text { path; name; content }
let full_path = function
| Dune { path; name; _ } | Text { path; name; _ } -> Path.relative path name
;;
(** Inspection and manipulation of stanzas in a file *)
module Stanza = struct
let pp s =
match Cst.to_sexp s with
| None -> Pp.nop
| Some s -> Dune_lang.pp s
;;
let libraries_conflict (a : Library.t) (b : Library.t) = a.name = b.name
let executables_conflict (a : Dune_rules.Executables.t) (b : Dune_rules.Executables.t)
=
let a_names = String.Set.of_list_map ~f:snd (Nonempty_list.to_list a.names) in
let b_names = String.Set.of_list_map ~f:snd (Nonempty_list.to_list b.names) in
String.Set.inter a_names b_names |> String.Set.is_empty |> not
;;
let tests_conflict (a : Dune_rules.Tests.t) (b : Dune_rules.Tests.t) =
executables_conflict a.exes b.exes
;;
let stanzas_conflict (a : Stanza.t) (b : Stanza.t) =
match Stanza.repr a, Stanza.repr b with
| Dune_rules.Executables.T a, Dune_rules.Executables.T b -> executables_conflict a b
| Library.T a, Library.T b -> libraries_conflict a b
| Dune_rules.Tests.T a, Dune_rules.Tests.T b -> tests_conflict a b
(* NOTE No other stanza types currently supported *)
| _ -> false
;;
let csts_conflict project (a : Cst.t) (b : Cst.t) =
let of_ast = Dune_rules.Stanzas.of_ast project in
(let open Option.O in
let* a_ast = Cst.abstract a in
let+ b_ast = Cst.abstract b in
let a_asts = of_ast a_ast in
let b_asts = of_ast b_ast in
List.exists ~f:(fun x -> List.exists ~f:(stanzas_conflict x) a_asts) b_asts)
|> Option.value ~default:false
;;
(* TODO(shonfeder): replace with stanza merging *)
let find_conflicting project new_stanzas existing_stanzas =
let conflicting_stanza stanza =
match List.find ~f:(csts_conflict project stanza) existing_stanzas with
| Some conflict -> Some (stanza, conflict)
| None -> None
in
List.find_map ~f:conflicting_stanza new_stanzas
;;
let add (project : Dune_project.t) stanzas = function
| Text f -> Text f (* Adding a stanza to a text file isn't meaningful *)
| Dune f ->
(match find_conflicting project stanzas f.content with
| None -> Dune { f with content = f.content @ stanzas }
| Some (a, b) ->
User_error.raise
[ Pp.text "Updating existing stanzas is not yet supported."
; Pp.text "A preexisting dune stanza conflicts with a generated stanza:"
; Pp.nop
; Pp.text "Generated stanza:"
; pp a
; Pp.nop
; Pp.text "Pre-existing stanza:"
; pp b
])
;;
end
(* Stanza *)
let create_dir path =
try Path.mkdir_p path with
| Unix.Unix_error (EACCES, _, _) ->
User_error.raise
[ Pp.textf
"A project directory cannot be created or accessed: Lacking permissions \
needed to create directory %s"
(Path.to_string_maybe_quoted path)
]
;;
let load_dune_file ~path =
let name = "dune" in
let full_path = Path.relative path name in
let content =
if not (Path.exists full_path)
then []
else if Path.is_directory full_path
then
User_error.raise
[ Pp.textf
"\"%s\" already exists and is a directory"
(Path.to_absolute_filename full_path)
]
else (
match Io.with_lexbuf_from_file ~f:Dune_lang.Format.parse full_path with
| Dune_lang.Format.Sexps content -> content
| Dune_lang.Format.OCaml_syntax _ ->
User_error.raise
[ Pp.textf
"Cannot load dune file %s because it uses OCaml syntax"
(Path.to_string_maybe_quoted full_path)
])
in
Dune { path; name; content }
;;
let write_dune_file (dune_file : dune) =
let path = Path.relative dune_file.path dune_file.name in
let version =
Dune_lang.Syntax.greatest_supported_version_exn Dune_lang.Stanza.syntax
in
Io.with_file_out
~binary:true
(* Why do we pass [~binary:true] but not anywhere else when formatting? *)
path
~f:(fun oc ->
let fmt = Format.formatter_of_out_channel oc in
Format.fprintf
fmt
"%a%!"
Pp.to_fmt
(Dune_lang.Format.pp_top_sexps ~version dune_file.content))
;;
let write f =
let path = full_path f in
match f with
| Dune f -> Ok (write_dune_file f)
| Text f ->
if Path.exists path
then Error path
else Ok (Io.write_file ~binary:false path f.content)
;;
end
(** The context in which the initialization is executed *)
module Init_context = struct
open Dune_config_file
type t =
{ dir : Path.t
; project : Dune_project.t
; defaults : Dune_config.Project_defaults.t
}
let make path defaults =
let open Memo.O in
let+ project =
(* CR-someday rgrinberg: why not get the project from the source tree? *)
Dune_project.load
~dir:Path.Source.root
~files:Filename.Set.empty
~infer_from_opam_files:true
~load_opam_file_with_contents:Dune_pkg.Opam_file.load_opam_file_with_contents
>>| function
| Some p -> p
| None ->
Dune_project.anonymous
~dir:Path.Source.root
Package_info.empty
Package.Name.Map.empty
in
let dir =
match path with
| None -> Path.root
| Some p -> Path.of_string p
in
File.create_dir dir;
{ dir; project; defaults }
;;
end
let check_module_name name =
let s = Dune_lang.Atom.to_string name in
let (_ : Dune_rules.Module_name.t) =
Dune_rules.Module_name.of_string_user_error (Loc.none, s) |> User_error.ok_exn
in
()
;;
module Public_name = struct
include Lib_name
module Pkg = Dune_lang.Package_name.Opam_compatible
let is_opam_compatible l =
Lib_name.package_name l |> Dune_lang.Package_name.is_opam_compatible
;;
let of_string_user_error (loc, s) =
let open Result.O in
let* l = of_string_user_error (loc, s) in
if is_opam_compatible l
then Ok l
else
Error
(User_error.make
[ Pp.text
"Public names are composed of an opam package name and optional \
dot-separated string suffixes."
; Pkg.description_of_valid_string
])
;;
let of_name_exn name =
let s = Dune_lang.Atom.to_string name in
of_string_user_error (Loc.none, s) |> User_error.ok_exn
;;
end
module Component = struct
module Options = struct
module Common = struct
type t =
{ name : Dune_lang.Atom.t
; public : Public_name.t option
; libraries : Dune_lang.Atom.t list
; pps : Dune_lang.Atom.t list
}
let package_name common =
let name =
match common.public with
| None -> Dune_lang.Atom.to_string common.name
| Some public -> Public_name.to_string public
in
Package.Name.of_string name
;;
end
module Executable = struct
type t = unit
end
module Library = struct
type t = { inline_tests : bool }
end
module Project = struct
module Template = struct
type t =
| Exec
| Lib
let of_string = function
| "executable" -> Some Exec
| "library" -> Some Lib
| _ -> None
;;
let commands = [ "executable", Exec; "library", Lib ]
end
module Pkg = struct
type t =
| Opam
| Esy
let commands = [ "opam", Opam; "esy", Esy ]
end
type t =
{ template : Template.t
; inline_tests : bool
; pkg : Pkg.t
}
end
module Test = struct
type t = unit
end
type 'options t =
{ context : Init_context.t
; common : Common.t
; options : 'options
}
end
(* Options *)
type 'options t =
| Executable : Options.Executable.t Options.t -> Options.Executable.t t
| Library : Options.Library.t Options.t -> Options.Library.t t
| Project : Options.Project.t Options.t -> Options.Project.t t
| Test : Options.Test.t Options.t -> Options.Test.t t
(** Internal representation of the files comprising a component *)
type target =
{ dir : Path.t
; files : File.t list
}
(** Creates Dune language CST stanzas describing components *)
module Stanza_cst = struct
open Dune_lang
module Field = struct
let inline_tests = Encoder.field_b "inline_tests"
let pps_encoder pps = Encoder.list Encoder.string ("pps" :: pps)
let preprocess_field = function
| [] -> []
| pps -> [ Encoder.field "preprocess" pps_encoder pps ]
;;
let common (options : Options.Common.t) =
[ Encoder.field "name" Encoder.string (Atom.to_string options.name)
; Encoder.field_l
"libraries"
Encoder.string
(List.map ~f:Atom.to_string options.libraries)
]
@ preprocess_field (List.map ~f:Atom.to_string options.pps)
;;
end
(* Make CST representation of a stanza for the given `kind` *)
let make kind common_options fields =
Encoder.named_record_fields kind (fields @ Field.common common_options)
(* Convert to a CST *)
|> Dune_lang.Ast.add_loc ~loc:Loc.none
|> Cst.concrete
(* Package as a list CSTs *) |> List.singleton
;;
let add_to_list_set elem set =
if List.mem ~equal:Dune_lang.Atom.equal set elem then set else elem :: set
;;
let public_name_field = Encoder.field_o "public_name" Public_name.encode
let executable (common : Options.Common.t) (() : Options.Executable.t) =
make "executable" common [ public_name_field common.public ]
;;
let library (common : Options.Common.t) { Options.Library.inline_tests } =
check_module_name common.name;
let common =
if inline_tests
then (
let pps =
add_to_list_set (Dune_lang.Atom.of_string "ppx_inline_test") common.pps
in
{ common with pps })
else common
in
make
"library"
common
[ public_name_field common.public; Field.inline_tests inline_tests ]
;;
let test common (() : Options.Test.t) = make "test" common []
(* A list of CSTs for dune-project file content *)
let dune_project
~opam_file_gen
~(defaults : Dune_config_file.Dune_config.Project_defaults.t)
dir
(common : Options.Common.t)
=
let cst =
let package =
Package.create
~name:(Options.Common.package_name common)
~loc:Loc.none
~version:None
~conflicts:[]
~depopts:[]
~info:Package_info.empty
~sites:Site.Map.empty
~allow_empty:false
~deprecated_package_names:Package.Name.Map.empty
~has_opam_file:(Exists false)
~original_opam_file:None
~dir
~synopsis:(Some "A short synopsis")
~description:(Some "A longer description")
~tags:[ "add topics"; "to describe"; "your"; "project" ]
~depends:
[ { Package_dependency.name = Package.Name.of_string "ocaml"
; constraint_ = None
}
]
in
let packages = Package.Name.Map.singleton (Package.name package) package in
let info =
Package_info.example
~authors:defaults.authors
~maintainers:defaults.maintainers
~license:defaults.license
in
Dune_project.anonymous ~dir info packages
|> Dune_project.set_generate_opam_files opam_file_gen
|> Dune_project.encode
|> List.map ~f:(fun exp ->
exp |> Dune_lang.Ast.add_loc ~loc:Loc.none |> Cst.concrete)
in
List.append
cst
[ Cst.Comment
( Loc.none
, [ " See the complete stanza docs at \
https://dune.readthedocs.io/en/stable/reference/dune-project/index.html"
] )
]
;;
end
(* TODO Support for merging in changes to an existing stanza *)
let add_stanza_to_dune_file ~(project : Dune_project.t) ~dir stanza =
File.load_dune_file ~path:dir |> File.Stanza.add project stanza
;;
(* Functions to make the various components, represented as lists of files *)
module Make = struct
let bin ({ context; common; options } : Options.Executable.t Options.t) =
let dir = context.dir in
let bin_dune =
Stanza_cst.executable common options
|> add_stanza_to_dune_file ~project:context.project ~dir
in
let bin_ml =
let name = sprintf "%s.ml" (Dune_lang.Atom.to_string common.name) in
let content = sprintf "let () = print_endline \"Hello, World!\"\n" in
File.make_text dir name content
in
let files = [ bin_dune; bin_ml ] in
[ { dir; files } ]
;;
let src ({ context; common; options } : Options.Library.t Options.t) =
let dir = context.dir in
let lib_dune =
Stanza_cst.library common options
|> add_stanza_to_dune_file ~project:context.project ~dir
in
let files = [ lib_dune ] in
[ { dir; files } ]
;;
let test ({ context; common; options } : Options.Test.t Options.t) =
(* Marking the current absence of test-specific options *)
let dir = context.dir in
let test_dune =
Stanza_cst.test common options
|> add_stanza_to_dune_file ~project:context.project ~dir
in
let test_ml =
let name = sprintf "%s.ml" (Dune_lang.Atom.to_string common.name) in
let content = "" in
File.make_text dir name content
in
let files = [ test_dune; test_ml ] in
[ { dir; files } ]
;;
let dune_project_file dir ({ context; common; options } : Options.Project.t Options.t)
=
let opam_file_gen =
match options.pkg with
| Opam -> true
| Esy -> false
in
let content =
Stanza_cst.dune_project
~opam_file_gen
~defaults:context.defaults
Path.(as_in_source_tree_exn context.dir)
common
in
File.Dune { path = dir; content; name = "dune-project" }
;;
let proj_exec dir ({ context; common; options } : Options.Project.t Options.t) =
let lib_target =
src
{ context = { context with dir = Path.relative dir "lib" }
; options = { inline_tests = options.inline_tests }
; common = { common with public = None }
}
in
let test_target =
let test_name = "test_" ^ Dune_lang.Atom.to_string common.name in
test
{ context = { context with dir = Path.relative dir "test" }
; options = ()
; common = { common with name = Dune_lang.Atom.of_string test_name }
}
in
let bin_target =
(* Add the lib_target as a library to the executable*)
let libraries = Stanza_cst.add_to_list_set common.name common.libraries in
bin
{ context = { context with dir = Path.relative dir "bin" }
; options = ()
; common = { common with libraries; name = Dune_lang.Atom.of_string "main" }
}
in
bin_target @ lib_target @ test_target
;;
let proj_lib dir ({ context; common; options } : Options.Project.t Options.t) =
let lib_target =
src
{ context = { context with dir = Path.relative dir "lib" }
; options = { inline_tests = options.inline_tests }
; common
}
in
let test_target =
let test_name = "test_" ^ Dune_lang.Atom.to_string common.name in
test
{ context = { context with dir = Path.relative dir "test" }
; options = ()
; common = { common with name = Dune_lang.Atom.of_string test_name }
}
in
lib_target @ test_target
;;
let proj ({ common; options; _ } as opts : Options.Project.t Options.t) =
let ({ template; pkg; _ } : Options.Project.t) = options in
let dir = Path.Source.root in
let proj_target =
let package_files =
match (pkg : Options.Project.Pkg.t) with
| Opam ->
let name = Options.Common.package_name common in
let opam_file = Path.source @@ Package_name.file name ~dir in
[ File.make_text (Path.parent_exn opam_file) (Path.basename opam_file) "" ]
| Esy -> [ File.make_text (Path.source dir) "package.json" "" ]
in
let dir = Path.source dir in
{ dir; files = dune_project_file dir opts :: package_files }
in
let component_targets =
(match (template : Options.Project.Template.t) with
| Exec -> proj_exec
| Lib -> proj_lib)
(Path.source dir)
opts
in
proj_target :: component_targets
;;
end
let report_uncreated_file = function
| Ok _ -> ()
| Error path ->
let open Pp.O in
User_warning.emit
[ Pp.textf "File "
++ Pp.tag
User_message.Style.Kwd
(Pp.verbatim (Path.to_string_maybe_quoted path))
++ Pp.text " was not created because it already exists"
]
;;
(** Creates a component, writing the files to disk *)
let create target =
File.create_dir target.dir;
List.map ~f:File.write target.files
;;
let init (type options) (t : options t) =
let target =
match t with
| Executable params -> Make.bin params
| Library params -> Make.src params
| Project params -> Make.proj params
| Test params -> Make.test params
in
List.concat_map ~f:create target |> List.iter ~f:report_uncreated_file
;;
end

View file

@ -0,0 +1,103 @@
(** Initialize dune components *)
open Import
(** The context in which the initialization is executed *)
module Init_context : sig
open Dune_config_file
type t =
{ dir : Path.t
; project : Dune_project.t
; defaults : Dune_config.Project_defaults.t
}
val make : string option -> Dune_config.Project_defaults.t -> t Memo.t
end
module Public_name : sig
type t
val to_string : t -> string
val of_string_user_error : Loc.t * string -> (t, User_message.t) result
val of_name_exn : Dune_lang.Atom.t -> t
end
(** A [Component.t] is a set of files that can be built or included as part of a
build. *)
module Component : sig
(** Options determining the details of a generated component *)
module Options : sig
(** The common options shared by all components *)
module Common : sig
type t =
{ name : Dune_lang.Atom.t
; public : Public_name.t option
; libraries : Dune_lang.Atom.t list
; pps : Dune_lang.Atom.t list
}
end
(** Options for executable components *)
module Executable : sig
(** NOTE: no options supported yet *)
type t = unit
end
(** Options for library components *)
module Library : sig
type t = { inline_tests : bool }
end
(** Options for test components *)
module Test : sig
(** NOTE: no options supported yet *)
type t = unit
end
(** Options for project components (which consist of several sub-components) *)
module Project : sig
(** Determines whether this is a library project or an executable project *)
module Template : sig
type t =
| Exec
| Lib
val of_string : string -> t option
val commands : (string * t) list
end
(** The package manager used for a project *)
module Pkg : sig
type t =
| Opam
| Esy
val commands : (string * t) list
end
type t =
{ template : Template.t
; inline_tests : bool
; pkg : Pkg.t
}
end
type 'a t =
{ context : Init_context.t
; common : Common.t
; options : 'a
}
end
(** All the the supported types of components *)
type 'options t =
| Executable : Options.Executable.t Options.t -> Options.Executable.t t
| Library : Options.Library.t Options.t -> Options.Library.t t
| Project : Options.Project.t Options.t -> Options.Project.t t
| Test : Options.Test.t Options.t -> Options.Test.t t
(** Create or update the component specified by the ['options t], where
['options] is *)
val init : 'options t -> unit
end

View file

@ -0,0 +1,328 @@
open Import
let doc = "Execute a command in a similar environment as if installation was performed."
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune exec -- COMMAND) should behave in the same way as if you
do:|}
; `Pre " \\$ dune install\n \\$ COMMAND"
; `P
{|In particular if you run $(b,dune exec ocaml), you will have
access to the libraries defined in the workspace using your usual
directives ($(b,#require) for instance)|}
; `P
{|When a leading / is present in the command (absolute path), then the
path is interpreted as an absolute path|}
; `P
{|When a / is present at any other position (relative path), then the
path is interpreted as relative to the build context + current
working directory (or the value of $(b,--root) when ran outside of
the project root)|}
; `Blocks Common.help_secs
; Common.examples
[ "Run the executable named `my_exec'", "dune exec my_exec"
; ( "Run the executable defined in `foo.ml' with the argument `arg'"
, "dune exec -- ./foo.exe arg" )
]
]
;;
let info = Cmd.info "exec" ~doc ~man
module Cmd_arg = struct
type t =
| Expandable of Dune_lang.String_with_vars.t * string
| Terminal of string
let parse s =
match Arg.conv_parser Arg.dep s with
| Ok (File sw) when Dune_lang.String_with_vars.has_pforms sw -> Expandable (sw, s)
| _ -> Terminal s
;;
let pp pps = function
| Expandable (_, s) -> Format.fprintf pps "%s" s
| Terminal s -> Format.fprintf pps "%s" s
;;
let expand t ~root ~sctx =
let open Memo.O in
match t with
| Terminal s -> Memo.return s
| Expandable (sw, _) ->
let+ path, _ =
Target.expand_path_from_root root sctx sw
|> Action_builder.evaluate_and_collect_facts
in
let context = Dune_rules.Super_context.context sctx in
(* TODO Why are we stringifying this path? *)
Path.to_string (Path.build (Path.Build.relative (Context.build_dir context) path))
;;
let conv = Arg.conv ((fun s -> Ok (parse s)), pp)
end
let not_found ~hints ~prog =
User_error.raise
~hints
[ Pp.concat
~sep:Pp.space
[ Pp.text "Program"; User_message.command prog; Pp.text "not found!" ]
]
;;
let not_found_with_suggestions ~dir ~prog =
let open Memo.O in
let+ hints =
(* Good candidates for the "./x.exe" instead of "x.exe" error are
executables present in the current directory. Note: we do not
check directory targets here; even if they do indeed include a
matching executable, they would be located in a subdirectory of
[dir], so it's unclear if that's what the user wanted. *)
let+ candidates =
let+ filename_set = Build_system.files_of ~dir:(Path.build dir) in
Filename_set.filenames filename_set
|> Filename.Set.to_list
|> List.filter ~f:(fun filename -> Filename.extension filename = ".exe")
|> List.map ~f:(fun filename -> "./" ^ filename)
in
User_message.did_you_mean prog ~candidates
in
not_found ~hints ~prog
;;
let program_not_built_yet prog =
User_error.raise
[ Pp.concat
~sep:Pp.space
[ Pp.text "Program"
; User_message.command prog
; Pp.text "isn't built yet. You need to build it first or remove the"
; User_message.command "--no-build"
; Pp.text "option."
]
]
;;
let build_prog ~no_rebuild ~prog p =
if no_rebuild
then if Path.exists p then Memo.return p else program_not_built_yet prog
else
let open Memo.O in
let+ () = Build_system.build_file p in
p
;;
let dir_of_context common sctx =
let context = Dune_rules.Super_context.context sctx in
Path.Build.relative (Context.build_dir context) (Common.prefix_target common "")
;;
let get_path common sctx ~prog =
let open Memo.O in
let dir = dir_of_context common sctx in
match Filename.analyze_program_name prog with
| In_path ->
Super_context.resolve_program_memo sctx ~dir ~loc:None prog
>>= (function
| Error (_ : Action.Prog.Not_found.t) -> not_found_with_suggestions ~dir ~prog
| Ok p -> Memo.return p)
| Relative_to_current_dir ->
let path = Path.relative_to_source_in_build_or_external ~dir prog in
Build_system.file_exists path
>>= (function
| true -> Memo.return path
| false -> not_found_with_suggestions ~dir ~prog)
| Absolute ->
(match
let prog = Path.of_string prog in
if Path.exists prog
then Some prog
else if not Sys.win32
then None
else (
let prog = Path.extend_basename prog ~suffix:Bin.exe in
Option.some_if (Path.exists prog) prog)
with
| Some prog -> Memo.return prog
| None -> not_found_with_suggestions ~dir ~prog)
;;
let get_path_and_build_if_necessary common sctx ~no_rebuild ~prog =
let open Memo.O in
let* path = get_path common sctx ~prog in
match Filename.analyze_program_name prog with
| In_path | Relative_to_current_dir -> build_prog ~no_rebuild ~prog path
| Absolute -> Memo.return path
;;
let step ~prog ~args ~common ~no_rebuild ~context ~on_exit () =
let open Memo.O in
let* sctx = Super_context.find_exn context in
let* path =
let* prog = Cmd_arg.expand ~root:(Common.root common) ~sctx prog in
get_path_and_build_if_necessary common sctx ~no_rebuild ~prog
and* args =
Memo.parallel_map args ~f:(Cmd_arg.expand ~root:(Common.root common) ~sctx)
in
let* env = Super_context.context_env sctx in
Memo.of_non_reproducible_fiber
@@ Dune_engine.Process.run_inherit_std_in_out
~dir:(Path.of_string Fpath.initial_cwd)
~env
path
args
>>| function
| 0 -> ()
| exit_code -> on_exit exit_code
;;
(* Similar to [get_path_and_build_if_necessary] but doesn't require the build
system (ie. it sequences with [Fiber] rather than with [Memo]) and builds
targets via an RPC server. Some functionality is not available but it can be
run concurrently while a second Dune process holds the global build
directory lock.
Returns the absolute path to the executable. *)
let build_prog_via_rpc_if_necessary ~dir ~no_rebuild prog =
match Filename.analyze_program_name prog with
| In_path ->
(* This case is reached if [dune exec] is passed the name of an
executable (rather than a path to an executable). When dune is running
directly, dune will try to resolve the executbale name within the public
executables defined in the current project and its dependencies, and
only if no executable with the given name is found will dune then
resolve the name within the $PATH variable instead. Looking up an
executable's name within the current project requires running the
build system, but running the build system is not allowed while
another dune instance holds the global build directory lock. In this
case dune will only resolve the executable's name within $PATH.
Because this behaviour is different from the default, print a warning
so users are hopefully less surprised.
*)
User_warning.emit
[ Pp.textf
"As this is not the main instance of Dune it is unable to locate the \
executable %S within this project. Dune will attempt to resolve the \
executable's name within your PATH only."
prog
];
let path = Env_path.path Env.initial in
(match Bin.which ~path prog with
| None -> not_found ~hints:[] ~prog
| Some prog_path -> Fiber.return (Path.to_absolute_filename prog_path))
| Relative_to_current_dir ->
let open Fiber.O in
let path = Path.relative_to_source_in_build_or_external ~dir prog in
let+ () =
if no_rebuild
then if Path.exists path then Fiber.return () else program_not_built_yet prog
else (
let target =
Dune_lang.Dep_conf.File
(Dune_lang.String_with_vars.make_text Loc.none (Path.to_string path))
in
Build.build_via_rpc_server ~print_on_success:false ~targets:[ target ])
in
Path.to_absolute_filename path
| Absolute ->
if Path.exists (Path.of_string prog)
then Fiber.return prog
else not_found ~hints:[] ~prog
;;
let exec_building_via_rpc_server ~common ~prog ~args ~no_rebuild =
let open Fiber.O in
let ensure_terminal v =
match (v : Cmd_arg.t) with
| Terminal s -> s
| Expandable (_, raw) ->
(* Variables cannot be expanded without running the build system. *)
User_error.raise
[ Pp.textf
"The term %S contains a variable but Dune is unable to expand variables when \
building via RPC."
raw
]
in
let context = Common.x common |> Option.value ~default:Context_name.default in
let dir = Context_name.build_dir context in
let prog = ensure_terminal prog in
let args = List.map args ~f:ensure_terminal in
let+ prog = build_prog_via_rpc_if_necessary ~dir ~no_rebuild prog in
restore_cwd_and_execve (Common.root common) prog args Env.initial
;;
let exec_building_directly ~common ~config ~context ~prog ~args ~no_rebuild =
match Common.watch common with
| Yes Passive ->
User_error.raise [ Pp.textf "passive watch mode is unsupported by exec" ]
| Yes Eager ->
Scheduler.go_with_rpc_server_and_console_status_reporting ~common ~config
@@ fun () ->
let open Fiber.O in
let on_exit = Console.printf "Program exited with code [%d]" in
Scheduler.Run.poll
@@
let* () = Fiber.return @@ Scheduler.maybe_clear_screen ~details_hum:[] config in
build @@ step ~prog ~args ~common ~no_rebuild ~context ~on_exit
| No ->
Scheduler.go_with_rpc_server ~common ~config
@@ fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
build_exn (fun () ->
let open Memo.O in
let* sctx = setup >>| Import.Main.find_scontext_exn ~name:context in
let* env = Super_context.context_env sctx
and* prog =
let* prog = Cmd_arg.expand ~root:(Common.root common) ~sctx prog in
get_path_and_build_if_necessary common sctx ~no_rebuild ~prog >>| Path.to_string
and* args =
Memo.parallel_map ~f:(Cmd_arg.expand ~root:(Common.root common) ~sctx) args
in
restore_cwd_and_execve (Common.root common) prog args env)
;;
let term : unit Term.t =
let+ builder = Common.Builder.term
and+ context = Common.context_arg ~doc:{|Run the command in this build context.|}
and+ prog = Arg.(required & pos 0 (some Cmd_arg.conv) None (Arg.info [] ~docv:"PROG"))
and+ no_rebuild =
Arg.(value & flag & info [ "no-build" ] ~doc:"don't rebuild target before executing")
and+ args = Arg.(value & pos_right 0 Cmd_arg.conv [] (Arg.info [] ~docv:"ARGS")) in
(* TODO we should make sure to finalize the current backend before exiting dune.
For watch mode, we should finalize the backend and then restart it in between
runs. *)
let common, config = Common.init builder in
match Dune_util.Global_lock.lock ~timeout:None with
| Error lock_held_by ->
(match Common.watch common with
| Yes _ ->
User_error.raise
[ Pp.textf
"Another instance of dune%s has locked the _build directory. Refusing to \
start a new watch server until no other instances of dune are running."
(match lock_held_by with
| Unknown -> ""
| Pid_from_lockfile pid -> sprintf " (pid: %d)" pid)
]
| No ->
if not (Common.Builder.equal builder Common.Builder.default)
then
User_warning.emit
[ Pp.textf
"Your build request is being forwarded to a running Dune instance%s. Note \
that certain command line arguments may be ignored."
(match lock_held_by with
| Unknown -> ""
| Pid_from_lockfile pid -> sprintf " (pid: %d)" pid)
];
Scheduler.go_without_rpc_server ~common ~config
@@ fun () -> exec_building_via_rpc_server ~common ~prog ~args ~no_rebuild)
| Ok () -> exec_building_directly ~common ~config ~context ~prog ~args ~no_rebuild
;;
let command = Cmd.v info term

View file

@ -0,0 +1,24 @@
open Import
module Cmd_arg : sig
type t
val conv : t Arg.conv
val expand : t -> root:Workspace_root.t -> sctx:Super_context.t -> string Memo.t
end
(** Returns the path to the executable [prog] as it will be resolved by dune:
- if [prog] is the name of an executable defined by the project then the path
to that executable will be returned, and evaluating the returned memo will
build the executable if necessary.
- otherwise if [prog] is the name of an executable in the "bin" directory of
a package in this project's dependency cone then the path to that executable
file will be returned. Note that for this reason all dependencies of the
project will be built when the returned memo is evaluated (unless the first
case is hit).
- otherwise if [prog] is the name of an executable in one of the directories
listed in the PATH environment variable, the path to that executable will be
returned. *)
val get_path : Common.t -> Super_context.t -> prog:string -> Path.t Memo.t
val command : unit Cmd.t

View file

@ -0,0 +1,20 @@
type t =
| Success
| Error
| Signal
let all = [ Success; Error; Signal ]
let code = function
| Success -> 0
| Error -> 1
| Signal -> 130
;;
let doc = function
| Success -> "on success."
| Error -> "if an error happened."
| Signal -> "if it was interrupted by a signal."
;;
let info e = Cmdliner.Cmd.Exit.info (code e) ~doc:(doc e)

View file

@ -0,0 +1,8 @@
type t =
| Success
| Error
| Signal
val all : t list
val info : t -> Cmdliner.Cmd.Exit.info
val code : t -> int

View file

@ -0,0 +1,28 @@
open Import
let doc = "Moved to dune describe external-lib-deps."
let man =
[ `S "DESCRIPTION"
; `P
"This subcommand used to print out an approximate set of external libraries that \
were required for building a given set of targets, without running the build. \
While this feature was useful, over time the quality of approximation had \
degraded and the cost of maintenance had increased, so we decided to remove it.\n"
; `Blocks Common.help_secs
]
;;
let info = Cmd.info "external-lib-deps" ~doc ~man
let term =
Term.ret
@@ let+ _ = Common.Builder.term
and+ _ = Arg.(value & flag & info [ "missing" ] ~doc:{|unused|})
and+ _ = Arg.(value & pos_all dep [] & Arg.info [] ~docv:"TARGET")
and+ _ = Arg.(value & flag & info [ "unstable-by-dir" ] ~doc:{|unused|})
and+ _ = Arg.(value & flag & info [ "sexp" ] ~doc:{|unused|}) in
`Error (false, "This subcommand has been moved to dune describe external-lib-deps.")
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,67 @@
open Import
let doc = "Format source code."
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune fmt) runs the formatter on the source code. The formatter is
automatically selected. ocamlformat is used to format OCaml source code
( *.ml and *.mli files) and refmt is used to format Reason source code
( *.re and *.rei files).|}
; `Blocks Common.help_secs
]
;;
let lock_ocamlformat () =
if Lazy.force Lock_dev_tool.is_enabled
then
(* Note that generating the ocamlformat lockdir here means
that it will be created when a user runs `dune fmt` but not
when a user runs `dune build @fmt`. It's important that
this logic remain outside of `dune build`, as `dune
build` is intended to only build targets, and generating
a lockdir is not building a target. *)
Lock_dev_tool.lock_dev_tool Ocamlformat |> Memo.run
else Fiber.return ()
;;
let run_fmt_command ~(common : Common.t) ~config =
let open Fiber.O in
let once () =
let* () = lock_ocamlformat () in
let request (setup : Import.Main.build_system) =
let dir = Path.(relative root) (Common.prefix_target common ".") in
Alias.in_dir ~name:Dune_rules.Alias.fmt ~recursive:true ~contexts:setup.contexts dir
|> Alias.request
in
Build.run_build_system ~common ~request
>>| function
| Ok () -> ()
| Error `Already_reported -> raise Dune_util.Report_error.Already_reported
in
Scheduler.go_with_rpc_server ~common ~config once
;;
let command =
let term =
let+ builder = Common.Builder.term
and+ no_promote =
Arg.(
value
& flag
& info
[ "preview" ]
~doc:
"Just print the changes that would be made without actually applying them. \
This takes precedence over auto-promote as that flag is assumed for this \
command.")
in
let builder =
Common.Builder.set_promote builder (if no_promote then Never else Automatically)
in
let common, config = Common.init builder in
run_fmt_command ~common ~config
in
Cmd.v (Cmd.info "fmt" ~doc ~man ~envs:Common.envs) term
;;

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,57 @@
open Import
let doc = "Format dune files."
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune format-dune-file) reads a dune file and outputs a formatted
version. This is a low-level command, meant to implement editor
support for example. To reformat a dune project, see the "Automatic
formatting" section in the manual.|}
]
;;
let info = Cmd.info "format-dune-file" ~doc ~man
let format_file ~version ~input =
let with_input =
match input with
| Some path -> fun f -> Io.with_lexbuf_from_file path ~f
| None ->
fun f ->
Exn.protect
~f:(fun () -> f (Lexing.from_channel stdin))
~finally:(fun () -> close_in_noerr stdin)
in
match with_input Dune_lang.Format.parse with
| Sexps sexps ->
Format.fprintf
Format.std_formatter
"%a%!"
Pp.to_fmt
(Dune_lang.Format.pp_top_sexps ~version sexps)
| OCaml_syntax loc ->
(match input with
| None -> User_error.raise ~loc [ Pp.text "OCaml syntax is not supported." ]
| Some path -> Io.with_file_in path ~f:(fun ic -> Io.copy_channels ic stdout))
;;
let term =
let+ path_opt =
let docv = "FILE" in
let doc = "Path to the dune file to parse." in
Arg.(value & pos 0 (some path) None & info [] ~docv ~doc)
and+ version =
let docv = "VERSION" in
let doc = "Which version of Dune language to use." in
let default =
Dune_lang.Syntax.greatest_supported_version_exn Dune_lang.Stanza.syntax
in
Arg.(value & opt version default & info [ "dune-version" ] ~docv ~doc)
in
let input = Option.map ~f:Arg.Path.path path_opt in
format_file ~version ~input
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,128 @@
open Import
let config =
( ("dune-config", 5, "", "Dune", "Dune manual")
, [ `S Manpage.s_name
; `P {|dune-config - configuring the dune build system|}
; `S Manpage.s_synopsis
; `Pre "~/.config/dune/config"
; `S Manpage.s_description
; `P
{|Unless $(b,--no-config) or $(b,-p) is passed, Dune will read a
configuration file from the user home directory. This file is used
to control various aspects of the behavior of Dune.|}
; `P
{|The configuration file is normally $(b,~/.config/dune/config) on
Unix systems and $(b,Local Settings/dune/config) in the User home
directory on Windows. However, it is possible to specify an
alternative configuration file with the $(b,--config-file) option.|}
; `P
{|The first line of the file must be of the form (lang dune X.Y)
where X.Y is the version of the dune language used in the file.|}
; `P
{|The rest of the file must be written in S-expression syntax and be
composed of a list of stanzas. The following sections describe
the stanzas available.|}
; `S "CACHING"
; `P {|Syntax: $(b,\(cache ENABLED\))|}
; `P
{| This stanza determines whether dune's build caching is enabled.
See https://dune.readthedocs.io/en/stable/caching.html for details.
Valid values for $(b, ENABLED) are $(b, enabled) or $(b, disabled).|}
; `S "DISPLAY MODES"
; `P {|Syntax: $(b,\(display MODE\))|}
; `P
{|This stanza controls how Dune reports what it is doing to the user.
This parameter can also be set from the command line via $(b,--display MODE).
The following display modes are available:|}
; `Blocks
(List.map
~f:(fun (x, desc) -> `I (sprintf "$(b,%s)" x, desc))
[ ( "progress"
, {|This is the default, Dune shows and update a
status line as build goals are being completed.|}
)
; "quiet", {|Only display errors.|}
; ( "short"
, {|Print one line per command being executed, with the
binary name on the left and the reason it is being executed for
on the right.|}
)
; ( "verbose"
, {|Print the full command lines of programs being
executed by Dune, with some colors to help differentiate
programs.|}
)
])
; `P
{|Note that when the selected display mode is $(b,progress) and the
output is not a terminal then the $(b,quiet) mode is selected
instead. This rule doesn't apply when running Dune inside Emacs.
Dune detects whether it is executed from inside Emacs or not by
looking at the environment variable $(b,INSIDE_EMACS) that is set by
Emacs. If you want the same behavior with another editor, you can set
this variable. If your editor already sets another variable,
please open a ticket on the ocaml/dune GitHub project so that we can
add support for it.|}
; `S "JOBS"
; `P {|Syntax: $(b,\(jobs NUMBER\))|}
; `P
{|Set the maximum number of jobs Dune might run in parallel.
This can also be set from the command line via $(b,-j NUMBER).|}
; `P {|The default for this value is the number of processors.|}
; `S "SANDBOXING"
; `P {|Syntax: $(b,\(sandboxing_preference MODE ...\))|}
; `P
{|Controls the sandboxing mode preference order used by dune. Dune will
use the earliest item from this list that's allowed by the action dependency
specification, or fall back on the hard-coded default. See $(b,man dune-build)
for the description of individual modes.|}
; Common.footer
] )
;;
type what =
| Man of Manpage.t
| List_topics
let commands = [ "config", Man config; "topics", List_topics ]
let doc = "Additional Dune help."
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune help TOPIC) provides additional help on the given topic.
The following topics are available:|}
; `Blocks
(List.concat_map commands ~f:(fun (s, what) ->
match what with
| List_topics -> []
| Man ((title, _, _, _, _), _) -> [ `I (sprintf "$(b,%s)" s, title) ]))
; Common.footer
]
;;
let info = Cmd.info "help" ~doc ~man ~envs:Common.envs
let term =
Term.ret
@@ let+ man_format = Arg.man_format
and+ what = Arg.(value & pos 0 (some (enum commands)) None & info [] ~docv:"TOPIC")
and+ () = Common.build_info in
match what with
| None -> `Help (man_format, Some "help")
| Some (Man man_page) ->
Format.printf "%a@?" (Manpage.print man_format) man_page;
`Ok ()
| Some List_topics ->
List.filter_map commands ~f:(fun (s, what) ->
match what with
| List_topics -> None
| _ -> Some s)
|> List.sort ~compare:String.compare
|> String.concat ~sep:"\n"
|> print_endline;
`Ok ()
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,288 @@
include Stdune
include Dune_config_file
include Dune_vcs
include struct
open Dune_engine
module Build_config = Build_config
module Build_system = Build_system
module Build_system_error = Build_system_error
module Load_rules = Load_rules
module Hooks = Hooks
module Action_builder = Dune_rules.Action_builder
module Action = Action
module Dep = Dep
module Action_to_sh = Action_to_sh
module Dpath = Dpath
module Findlib = Dune_rules.Findlib
module Diff_promotion = Diff_promotion
module Targets = Targets
module Context_name = Context_name
end
module Cached_digest = Dune_digest.Cached_digest
include struct
open Source
module Source_tree = Source_tree
module Source_dir_status = Source_dir_status
module Workspace = Workspace
end
include struct
open Dune_rules
module Super_context = Super_context
module Context = Context
module Dune_package = Dune_package
module Resolve = Resolve
module Dune_file = Dune_file
module Library = Library
module Melange = Melange
module Melange_stanzas = Melange_stanzas
module Executables = Executables
end
include struct
open Cmdliner
module Term = Term
module Manpage = Manpage
module Cmd = struct
include Cmd
let default_exits = List.map ~f:Exit_code.info Exit_code.all
let info ?docs ?doc ?man ?envs ?version name =
info ?docs ?doc ?man ?envs ?version ~exits:default_exits name
;;
end
end
module Digest = Dune_digest
module Metrics = Dune_metrics
module Console = Dune_console
include struct
open Dune_lang
module Stanza = Stanza
module Profile = Profile
module Lib_name = Lib_name
module Package_name = Package_name
module Package = Package
module Package_version = Package_version
module Source_kind = Source_kind
module Package_info = Package_info
module Section = Section
module Dune_project_name = Dune_project_name
module Dune_project = Dune_project
end
module Log = Dune_util.Log
module Dune_rpc = Dune_rpc_private
module Graph = Dune_graph.Graph
include Common.Let_syntax
module Main : sig
include module type of struct
include Dune_rules.Main
end
val setup : unit -> build_system Memo.t Fiber.t
end = struct
include Dune_rules.Main
let setup () =
let open Fiber.O in
let* scheduler = Dune_engine.Scheduler.t () in
Console.Status_line.set
(Live
(fun () ->
match Fiber.Svar.read Build_system.state with
| Initializing
| Restarting_current_build
| Build_succeeded__now_waiting_for_changes
| Build_failed__now_waiting_for_changes -> Pp.nop
| Building
{ Build_system.Progress.number_of_rules_executed = done_
; number_of_rules_discovered = total
; number_of_rules_failed = failed
} ->
Pp.verbatim
(sprintf
"Done: %u%% (%u/%u, %u left%s) (jobs: %u)"
(if total = 0 then 0 else done_ * 100 / total)
done_
total
(total - done_)
(if failed = 0 then "" else sprintf ", %u failed" failed)
(Dune_engine.Scheduler.running_jobs_count scheduler))));
Fiber.return (Memo.of_thunk get)
;;
end
module Scheduler = struct
include Dune_engine.Scheduler
let maybe_clear_screen ~details_hum (dune_config : Dune_config.t) =
match Execution_env.inside_dune with
| true -> (* Don't print anything here to make tests less verbose *) ()
| false ->
(match dune_config.terminal_persistence with
| Clear_on_rebuild -> Console.reset ()
| Clear_on_rebuild_and_flush_history -> Console.reset_flush_history ()
| Preserve ->
let message =
sprintf
"********** NEW BUILD (%s) **********"
(String.concat ~sep:", " details_hum)
in
Console.print_user_message
(User_message.make
[ Pp.nop; Pp.tag User_message.Style.Success (Pp.verbatim message); Pp.nop ]))
;;
let on_event dune_config _config = function
| Run.Event.Tick -> Console.Status_line.refresh ()
| Source_files_changed { details_hum } -> maybe_clear_screen ~details_hum dune_config
| Build_interrupted ->
Console.Status_line.set
(Live
(fun () ->
let progression =
match Fiber.Svar.read Build_system.state with
| Initializing
| Restarting_current_build
| Build_succeeded__now_waiting_for_changes
| Build_failed__now_waiting_for_changes -> Build_system.Progress.init
| Building progress -> progress
in
Pp.seq
(Pp.tag User_message.Style.Error (Pp.verbatim "Source files changed"))
(Pp.verbatim
(sprintf
", restarting current build... (%u/%u)"
progression.number_of_rules_executed
progression.number_of_rules_discovered))))
| Build_finish build_result ->
let message =
match build_result with
| Success -> Pp.tag User_message.Style.Success (Pp.verbatim "Success")
| Failure ->
let failure_message =
match
Build_system_error.(
Id.Map.cardinal (Set.current (Fiber.Svar.read Build_system.errors)))
with
| 1 -> Pp.textf "Had 1 error"
| n -> Pp.textf "Had %d errors" n
in
Pp.tag User_message.Style.Error failure_message
in
Console.Status_line.set
(Constant (Pp.seq message (Pp.verbatim ", waiting for filesystem changes...")))
;;
let rpc server =
{ Dune_engine.Rpc.run = Dune_rpc_impl.Server.run server
; stop = Dune_rpc_impl.Server.stop server
; ready = Dune_rpc_impl.Server.ready server
}
;;
let go_without_rpc_server ~(common : Common.t) ~config:dune_config f =
let stats = Common.stats common in
let config =
let watch_exclusions = Common.watch_exclusions common in
Dune_config.for_scheduler
dune_config
stats
~print_ctrl_c_warning:true
~watch_exclusions
in
Dune_rules.Clflags.concurrency := config.concurrency;
Run.go config ~on_event:(on_event dune_config) f
;;
let go_with_rpc_server ~common ~config f =
let f =
match Common.rpc common with
| `Allow server -> fun () -> Dune_engine.Rpc.with_background_rpc (rpc server) f
| `Forbid_builds -> f
in
go_without_rpc_server ~common ~config f
;;
let go_with_rpc_server_and_console_status_reporting
~(common : Common.t)
~config:dune_config
run
=
let server =
match Common.rpc common with
| `Allow server -> rpc server
| `Forbid_builds -> Code_error.raise "rpc must be enabled in polling mode" []
in
let stats = Common.stats common in
let config =
let watch_exclusions = Common.watch_exclusions common in
Dune_config.for_scheduler
dune_config
stats
~print_ctrl_c_warning:true
~watch_exclusions
in
Dune_rules.Clflags.concurrency := config.concurrency;
let file_watcher = Common.file_watcher common in
let run () =
let open Fiber.O in
Dune_engine.Rpc.with_background_rpc server
@@ fun () ->
let* () = Dune_engine.Rpc.ensure_ready () in
run ()
in
Run.go config ~file_watcher ~on_event:(on_event dune_config) run
;;
end
let string_path_relative_to_specified_root (root : Workspace_root.t) path =
if Filename.is_relative path then Filename.concat root.dir path else path
;;
let restore_cwd_and_execve root prog args env =
let prog = string_path_relative_to_specified_root root prog in
Proc.restore_cwd_and_execve prog args ~env
;;
(* Adapted from
https://github.com/ocaml/opam/blob/fbbe93c3f67034da62d28c8666ec6b05e0a9b17c/src/client/opamArg.ml#L759 *)
let command_alias ?orig_name cmd term name =
let orig =
match orig_name with
| Some s -> s
| None -> Cmd.name cmd
in
let doc = Printf.sprintf "An alias for $(b,%s)." orig in
let man =
[ `S "DESCRIPTION"
; `P (Printf.sprintf "$(mname)$(b, %s) is an alias for $(mname)$(b, %s)." name orig)
; `P (Printf.sprintf "See $(mname)$(b, %s --help) for details." orig)
; `Blocks Common.help_secs
]
in
Cmd.v (Cmd.info name ~docs:"COMMAND ALIASES" ~doc ~man) term
;;
(* The build system has some global state which makes it unsafe for
multiple instances of it to be executed concurrently, so we ensure
serialization by holding this mutex while running the build system. *)
let build_system_mutex = Fiber.Mutex.create ()
let build f =
Hooks.End_of_build.once Promote.Diff_promotion.finalize;
Fiber.Mutex.with_lock build_system_mutex ~f:(fun () -> Build_system.run f)
;;
let build_exn f =
Hooks.End_of_build.once Promote.Diff_promotion.finalize;
Fiber.Mutex.with_lock build_system_mutex ~f:(fun () -> Build_system.run_exn f)
;;

View file

@ -0,0 +1,341 @@
open Import
open Dune_init
(** {1 Helper functions} *)
(** {2 Cmdliner Argument Converters} *)
let atom_parser s =
match Dune_lang.Atom.parse s with
| Some s -> Ok s
| None -> Error (`Msg "expected a valid dune atom")
;;
let atom_printer ppf a = Format.pp_print_string ppf (Dune_lang.Atom.to_string a)
let component_name_parser s =
(* TODO refactor to use Lib_name.Local.conv *)
let err_msg () =
User_error.make
[ Pp.textf "invalid component name `%s'" s; Lib_name.Local.valid_format_doc ]
|> User_message.to_string
|> fun m -> `Msg m
in
let open Result.O in
let* atom = atom_parser s in
let* _ =
match Lib_name.Local.of_string_opt s with
| None -> Error (err_msg ())
| Some s -> Ok s
in
Ok atom
;;
let project_name_parser s =
(* TODO refactor Dune_project_name to be Stringlike *)
match Dune_project_name.named Loc.none s with
| v -> Ok v
| exception User_error.E _ ->
User_error.make
[ Pp.textf "invalid project name `%s'" s
; Pp.text
"Project names must start with a letter and be composed only of letters, \
numbers, '-' or '_'"
]
|> User_message.to_string
|> fun m -> Error (`Msg m)
;;
let project_name_printer ppf p =
Format.pp_print_string ppf (Dune_project_name.to_string_hum p)
;;
let atom_conv = Arg.conv (atom_parser, atom_printer)
let component_name_conv = Arg.conv (component_name_parser, atom_printer)
let project_name_conv = Arg.conv (project_name_parser, project_name_printer)
(** {2 Status reporting} *)
let print_completion kind name =
let open Pp.O in
Console.print_user_message
(User_message.make
[ Pp.tag User_message.Style.Ok (Pp.verbatim "Success")
++ Pp.textf ": initialized %s component named " kind
++ Pp.tag User_message.Style.Kwd (Pp.verbatim (Dune_lang.Atom.to_string name))
])
;;
(** {1 CLI} *)
let path =
let docv = "PATH" in
Arg.(value & pos 1 (some string) None & info [] ~docv)
;;
let context_cwd : Init_context.t Term.t =
let+ builder = Common.Builder.term
and+ path = path in
let builder = Common.Builder.set_default_root_is_cwd builder true in
let common, config = Common.init builder in
let project_defaults = config.project_defaults in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
Memo.run (Init_context.make path project_defaults))
;;
module Public_name = struct
type t =
| Use_name
| Public_name of Public_name.t
let public_name_to_string = function
| Use_name -> "<default>"
| Public_name p -> Public_name.to_string p
;;
let public_name default_name = function
| None -> None
| Some Use_name -> Some (Public_name.of_name_exn default_name)
| Some (Public_name n) -> Some n
;;
let conv =
let parser s =
if String.is_empty s
then Ok Use_name
else (
match Public_name.of_string_user_error (Loc.none, s) with
| Ok n -> Ok (Public_name n)
| Error e -> Error (`Msg (User_message.to_string e)))
in
let printer ppf public_name =
Format.pp_print_string ppf (public_name_to_string public_name)
in
Arg.conv (parser, printer)
;;
end
let libraries =
let docv = "LIBRARIES" in
let doc = "A comma separated list of libraries on which the component depends" in
Arg.(value & opt (list atom_conv) [] & info [ "libs" ] ~docv ~doc)
;;
let pps =
let docv = "PREPROCESSORS" in
let doc = "A comma separated list of ppx preprocessors used by the component" in
Arg.(value & opt (list atom_conv) [] & info [ "ppx" ] ~docv ~doc)
;;
let public : Public_name.t option Term.t =
let docv = "PUBLIC_NAME" in
let doc =
"If called with an argument, make the component public under the given PUBLIC_NAME. \
If supplied without an argument, use NAME."
in
Arg.(
value
& opt ~vopt:(Some Public_name.Use_name) (some Public_name.conv) None
& info [ "public" ] ~docv ~doc)
;;
let common : Component.Options.Common.t Term.t =
let+ name =
let docv = "NAME" in
Arg.(required & pos 0 (some component_name_conv) None & info [] ~docv)
and+ public = public
and+ libraries = libraries
and+ pps = pps in
let public = Public_name.public_name name public in
{ Component.Options.Common.name; public; libraries; pps }
;;
let project_common : Component.Options.Common.t Term.t =
let+ project_name =
let docv = "NAME" in
Arg.(required & pos 0 (some project_name_conv) None & info [] ~docv)
and+ libraries = libraries
and+ pps = pps in
let public = Dune_project_name.to_string_hum project_name in
let name =
String.map
~f:(function
| '-' -> '_'
| c -> c)
public
|> Dune_lang.Atom.of_string
in
let public =
Some (Dune_lang.Atom.of_string public |> Dune_init.Public_name.of_name_exn)
in
{ Component.Options.Common.name; public; libraries; pps }
;;
let inline_tests : bool Term.t =
let docv = "USE_INLINE_TESTS" in
let doc =
"Whether to use inline tests. Only applicable for $(b,library) and $(b,project) \
components."
in
Arg.(value & flag & info [ "inline-tests" ] ~docv ~doc)
;;
let opt_default ~default term = Term.(const (Option.value ~default) $ term)
let executable =
let doc = "A binary executable." in
let man = [] in
let kind = "executable" in
Cmd.v (Cmd.info kind ~doc ~man)
@@ let+ context = context_cwd
and+ common = common in
Component.init (Executable { context; common; options = () });
print_completion kind common.name
;;
let library =
let doc = "An OCaml library." in
let man = [] in
let kind = "library" in
Cmd.v (Cmd.info kind ~doc ~man)
@@ let+ context = context_cwd
and+ common = common
and+ inline_tests = inline_tests in
Component.init (Library { context; common; options = { inline_tests } });
print_completion kind common.name
;;
let test =
let doc =
"A test harness. (For inline tests, use the $(b,--inline-tests) flag along with the \
other component kinds.)"
in
let man = [] in
let kind = "test" in
Cmd.v (Cmd.info kind ~doc ~man)
@@ let+ context = context_cwd
and+ common = common in
Component.init (Test { context; common; options = () });
print_completion kind common.name
;;
let project =
let module Builder = Common.Builder in
let doc =
"A project is a predefined composition of components arranged in a standard \
directory structure. The kind of project initialized is determined by the value of \
the $(b,--kind) flag and defaults to an executable project, composed of a library, \
an executable, and a test component."
in
let man = [] in
Cmd.v (Cmd.info "project" ~doc ~man)
@@ let+ common_builder = Builder.term
and+ path = path
and+ common = project_common
and+ inline_tests = inline_tests
and+ template =
let docv = "PROJECT_KIND" in
let doc =
"The kind of project to initialize. Valid options are $(b,e[xecutable]) or \
$(b,l[ibrary]). Defaults to $(b,executable). Only applicable for $(b,project) \
components."
in
opt_default
~default:Component.Options.Project.Template.Exec
Arg.(
value
& opt (some (enum Component.Options.Project.Template.commands)) None
& info [ "kind" ] ~docv ~doc)
and+ pkg =
let docv = "PACKAGE_MANAGER" in
let doc =
"Which package manager to use. Valid options are $(b,o[pam]) or $(b,e[sy]). \
Defaults to $(b,opam). Only applicable for $(b,project) components."
in
opt_default
~default:Component.Options.Project.Pkg.Opam
Arg.(
value
& opt (some (enum Component.Options.Project.Pkg.commands)) None
& info [ "pkg" ] ~docv ~doc)
in
let name =
match common.public with
| None -> Dune_lang.Atom.to_string common.name
| Some public -> Dune_init.Public_name.to_string public
in
let context =
let init_context = Init_context.make path in
let root =
match path with
(* If a path is given, we use that for the root during project
initialization, creating the path to it if needed. *)
| Some path -> path
(* Otherwise we will use the project's given name, and create a
directory accordingly. *)
| None -> name
in
let builder = Builder.set_root common_builder root in
let (_ : Fpath.mkdir_p_result) = Fpath.mkdir_p root in
let common, config = Common.init builder in
let project_defaults = config.project_defaults in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
Memo.run @@ init_context project_defaults)
in
Component.init
(Project { context; common; options = { template; inline_tests; pkg } });
print_completion "project" (Dune_lang.Atom.of_string name)
;;
let group =
let doc = "Command group for initializing Dune components." in
let synopsis =
Common.command_synopsis
[ "init project NAME [PATH] [OPTION]... "
; "init executable NAME [PATH] [OPTION]... "
; "init library NAME [PATH] [OPTION]... "
; "init test NAME [PATH] [OPTION]... "
]
in
let man =
[ `Blocks synopsis
; `S "DESCRIPTION"
; `P
{|$(b,dune init COMPONENT NAME [PATH] [OPTION]...) initializes a new dune
configuration for a component of the kind specified by the subcommand
$(b,COMPONENT), named $(b,NAME), with fields determined by the supplied
$(b,OPTION)s.|}
; `P
{|Run a subcommand with $(b, --help) for for details on it's supported arguments|}
; `P
{|If the optional $(b,PATH) is provided, it must be a path to a directory, and
the component will be created there. Otherwise, it is created in a child of the
current working directory, called $(b, NAME). To initialize a component in the
current working directory, use `.` as the $(b,PATH).|}
; `P
{|Any prefix of a $(b,COMMAND)'s name can be supplied in place of
full name (as illustrated in the synopsis).|}
; `P
{|For more details, see https://dune.readthedocs.io/en/stable/usage.html#initializing-components|}
; Common.examples
[ ( {|Generate a project skeleton for an executable named `myproj' in a
new directory named `myproj', depending on the bos library and
using inline tests along with ppx_inline_test |}
, {|dune init project myproj --libs bos --ppx ppx_inline_test --inline-tests|} )
; ( {|Configure an executable component named `myexe' in a dune file in the
current directory|}
, {|dune init executable myexe|} )
; ( {|Configure a library component named `mylib' in a dune file in the ./src
directory depending on the core and cmdliner libraries, the ppx_let
and ppx_inline_test preprocessors, and declared as using inline
tests|}
, {|dune init library mylib src --libs core,cmdliner --ppx ppx_let,ppx_inline_test --inline-tests|}
)
; ( {|Configure a test component named `mytest' in a dune file in the
./test directory that depends on `mylib'|}
, {|dune init test mytest test --libs mylib|} )
]
]
in
Cmd.group (Cmd.info "init" ~doc ~man) [ executable; project; library; test ]
;;

View file

@ -0,0 +1,3 @@
open Import
val group : unit Cmd.t

View file

@ -0,0 +1,856 @@
open Import
module Artifact_substitution = Dune_rules.Artifact_substitution
let synopsis =
[ `P "The installation directories used are defined by priority:"
; `Noblank
; `P
"- directories set on the command line of $(i,dune install), or corresponding \
environment variables"
; `Noblank
; `P
"- directories set in dune binary. They are setup before the compilation of dune \
with $(i,./configure)"
; `Noblank
; `P "- inferred from the environment variable $(i,OPAM_SWITCH_PREFIX) if present"
]
;;
let print_line ~(verbosity : Dune_engine.Display.t) fmt =
Printf.ksprintf
(fun s ->
match verbosity with
| Quiet -> ()
| _ -> Console.print [ Pp.verbatim s ])
fmt
;;
let interpret_destdir ~destdir path =
match destdir with
| None -> path
| Some destdir -> Path.append_local destdir (Path.local_part path)
;;
let get_dirs context ~prefix_from_command_line ~from_command_line =
let open Fiber.O in
let module Roots = Install.Roots in
let prefix_from_command_line = Option.map ~f:Path.of_string prefix_from_command_line in
let+ roots =
match prefix_from_command_line with
| None -> Memo.run (Context.roots context)
| Some prefix ->
Roots.opam_from_prefix prefix ~relative:Path.relative
|> Roots.map ~f:(fun s -> Some s)
|> Fiber.return
in
let roots = Roots.first_has_priority from_command_line roots in
let must_be_defined name v =
match v with
| Some v -> v
| None ->
(* We suggest that the user sets --prefix first rather than the specific
missing option, since this is the most common case. *)
User_error.raise
[ Pp.textf "The %s installation directory is unknown." name ]
~hints:
[ Pp.concat
~sep:Pp.space
[ Pp.text "It can be specified with"
; User_message.command "--prefix"
; Pp.textf "or by setting"
; User_message.command (sprintf "--%s" name)
]
|> Pp.hovbox
]
in
{ Roots.lib_root = must_be_defined "libdir" roots.lib_root
; libexec_root = must_be_defined "libexecdir" roots.libexec_root
; bin = must_be_defined "bindir" roots.bin
; sbin = must_be_defined "sbindir" roots.sbin
; etc_root = must_be_defined "etcdir" roots.etc_root
; doc_root = must_be_defined "docdir" roots.doc_root
; share_root = must_be_defined "datadir" roots.share_root
; man = must_be_defined "mandir" roots.man
}
;;
module Workspace = struct
type t =
{ packages : Package.t Package.Name.Map.t
; contexts : Context.t list
}
let get () =
let open Memo.O in
Memo.run
(let+ packages = Dune_rules.Dune_load.packages ()
and+ contexts = Context.DB.all () in
{ packages; contexts })
;;
let package_install_file t ~findlib_toolchain pkg =
match Package.Name.Map.find t.packages pkg with
| None -> Error ()
| Some p ->
let name = Package.name p in
let dir = Package.dir p in
Ok
(Path.Source.relative
dir
(Dune_rules.Install_rules.install_file ~package:name ~findlib_toolchain))
;;
end
let resolve_package_install workspace ~findlib_toolchain pkg =
match Workspace.package_install_file workspace ~findlib_toolchain pkg with
| Ok path -> path
| Error () ->
let pkg = Package.Name.to_string pkg in
User_error.raise
[ Pp.textf "Unknown package %s!" pkg ]
~hints:
(User_message.did_you_mean
pkg
~candidates:
(Package.Name.Map.keys workspace.packages
|> List.map ~f:Package.Name.to_string))
;;
let print_unix_error f =
try f () with
| Unix.Unix_error (error, syscall, arg) ->
let error = Unix_error.Detailed.create error ~syscall ~arg in
User_message.prerr (User_error.make [ Unix_error.Detailed.pp error ])
;;
module Special_file = struct
type t =
| META
| Dune_package
let of_entry (e : _ Install.Entry.t) =
match e.section with
| Lib ->
let dst = Install.Entry.Dst.to_string e.dst in
if dst = Dune_findlib.Findlib.Package.meta_fn
then Some META
else if dst = Dune_package.fn
then Some Dune_package
else None
| _ -> None
;;
end
type copy_kind =
| Substitute (** Use [Artifact_substitution.copy_file]. Will scan all bytes. *)
| Special of Special_file.t (** Hooks to add version numbers, replace sections, etc *)
type rmdir_mode =
| Fail
| Warn
(** Operations that act on real files or just pretend to (for --dry-run) *)
module type File_operations = sig
val copy_file
: src:Path.t
-> dst:Path.t
-> executable:bool
-> kind:copy_kind
-> package:Package.Name.t
-> conf:Artifact_substitution.Conf.t
-> unit Fiber.t
val mkdir_p : Path.t -> unit
val remove_file_if_exists : Path.t -> unit
val remove_dir_if_exists : if_non_empty:rmdir_mode -> Path.t -> unit
end
module File_ops_dry_run (Verbosity : sig
val verbosity : Dune_engine.Display.t
end) : File_operations = struct
open Verbosity
let print_line fmt = print_line ~verbosity fmt
let copy_file ~src ~dst ~executable ~kind:_ ~package:_ ~conf:_ =
print_line
"Copying %s to %s (executable: %b)"
(Path.to_string_maybe_quoted src)
(Path.to_string_maybe_quoted dst)
executable;
Fiber.return ()
;;
let mkdir_p path = print_line "Creating directory %s" (Path.to_string_maybe_quoted path)
let remove_file_if_exists path =
print_line "Removing (if it exists) %s" (Path.to_string_maybe_quoted path)
;;
let remove_dir_if_exists ~if_non_empty path =
print_line
"Removing directory (%s if not empty) %s"
(match if_non_empty with
| Fail -> "fail"
| Warn -> "warn")
(Path.to_string_maybe_quoted path)
;;
end
module File_ops_real (W : sig
val verbosity : Dune_engine.Display.t
val workspace : Workspace.t
end) : File_operations = struct
open W
let print_line = print_line ~verbosity
let get_vcs p = Source_tree.nearest_vcs p
type copy_special_file_status =
| Done
| Use_plain_copy
let with_ppf oc ~f =
let ppf = Format.formatter_of_out_channel oc in
f ppf;
Format.pp_print_flush ppf ()
;;
let copy_special_file ~src ~package ~ic ~oc ~f =
let open Fiber.O in
let get_version () =
let* packages =
match Package.Name.Map.find workspace.packages package with
| None -> Fiber.return None
| Some package -> Memo.run (get_vcs (Package.dir package))
in
match packages with
| None -> Fiber.return None
| Some vcs -> Memo.run (Vcs.describe vcs)
in
try f ~get_version ic ~src oc with
| _ (* XXX should we really be catching everything here? *) ->
User_warning.emit
~loc:(Loc.in_file src)
[ Pp.text "Failed to parse file, not adding version and locations information." ];
Fiber.return Use_plain_copy
;;
let process_meta ~get_version ic ~src:_ oc =
let module Meta = Dune_findlib.Findlib.Meta in
let lb = Lexing.from_channel ic in
let meta : Meta.t = { name = None; entries = Meta.parse_entries lb } in
let need_more_versions =
try
let (_ : Meta.t) =
Meta.add_versions meta ~get_version:(fun _ -> raise_notrace Exit)
in
false
with
| Exit -> true
in
if not need_more_versions
then Fiber.return Use_plain_copy
else
let open Fiber.O in
let+ version = get_version () in
with_ppf oc ~f:(fun ppf ->
let meta = Meta.add_versions meta ~get_version:(fun _ -> version) in
Pp.to_fmt ppf (Meta.pp meta.entries));
Done
;;
let process_dune_package ~get_version ~get_location ic ~src oc =
let lb = Lexing.from_channel ic in
let dune_version = Dune_lang.Syntax.greatest_supported_version_exn Stanza.syntax in
match Dune_package.Or_meta.parse src lb |> User_error.ok_exn with
| Use_meta ->
with_ppf oc ~f:(Dune_package.Or_meta.pp_use_meta ~dune_version);
Fiber.return Done
| Dune_package dp ->
let open Fiber.O in
(* replace sites with external path in the file *)
let dp, replace_info = Dune_package.replace_site_sections ~get_location dp in
(* replace version if needed in the file *)
let need_version = Option.is_none dp.version in
let+ dp =
if need_version
then
let+ version_opt = get_version () in
match version_opt with
| Some version -> { dp with version = Some (Package_version.of_string version) }
| None -> dp
else Fiber.return dp
in
with_ppf oc ~f:(fun ppf ->
(* CR-emillon: we should write absolute paths only if necessary *)
Dune_package.Or_meta.pp
~dune_version
ppf
(Dune_package dp)
~encoding:(Absolute replace_info));
Done
;;
let copy_file
~src
~dst
~executable
~kind
~package
~(conf : Artifact_substitution.Conf.t)
=
let chmod = if executable then fun _ -> 0o755 else fun _ -> 0o644 in
let plain_copy () = Io.copy_file ~chmod ~src ~dst () in
match kind with
| Substitute -> Artifact_substitution.copy_file ~conf ~src ~dst ~chmod ()
| Special sf ->
let open Fiber.O in
let ic, oc = Io.setup_copy ~chmod ~src ~dst () in
let+ status =
Fiber.finalize
~finally:(fun () ->
Io.close_both (ic, oc);
Fiber.return ())
(fun () ->
let f =
match sf with
| META -> process_meta
| Dune_package ->
process_dune_package
~get_location:(Artifact_substitution.Conf.get_location conf)
in
copy_special_file ~src ~package ~ic ~oc ~f)
in
(match status with
| Done -> ()
| Use_plain_copy -> plain_copy ())
;;
let remove_file_if_exists dst =
if Path.exists dst
then (
print_line "Deleting %s" (Path.to_string_maybe_quoted dst);
print_unix_error (fun () -> Path.unlink_exn dst))
;;
let remove_dir_if_exists ~if_non_empty dir =
match Path.readdir_unsorted dir with
| Error (Unix.ENOENT, _, _) -> ()
| Ok [] ->
print_line "Deleting empty directory %s" (Path.to_string_maybe_quoted dir);
print_unix_error (fun () -> Path.rmdir dir)
| Error (e, _, _) ->
User_message.prerr (User_error.make [ Pp.text (Unix.error_message e) ])
| _ ->
let dir = Path.to_string_maybe_quoted dir in
(match if_non_empty with
| Warn ->
User_message.prerr
(User_error.make
[ Pp.textf "Directory %s is not empty, cannot delete (ignoring)." dir ])
| Fail ->
User_error.raise
[ Pp.textf "Please delete non-empty directory %s manually." dir ])
;;
let mkdir_p p =
(* CR-someday amokhov: We should really change [Path.mkdir_p dir] to fail if
it turns out that [dir] exists and is not a directory. Even better, make
[Path.mkdir_p] return an explicit variant to deal with. *)
match Fpath.mkdir_p (Path.to_string p) with
| Created -> ()
| Already_exists ->
(match Path.is_directory p with
| true -> ()
| false ->
User_error.raise
[ Pp.textf "Please delete file %s manually." (Path.to_string_maybe_quoted p) ])
;;
end
module Sections = struct
type t =
| All
| Only of Section.Set.t
let sections_conv =
let all =
Section.all
|> Section.Set.to_list
|> List.map ~f:(fun section -> Section.to_string section, section)
in
Arg.list ~sep:',' (Arg.enum all)
;;
let term =
let doc = "sections that should be installed" in
let open Cmdliner.Arg in
let+ sections = value & opt (some sections_conv) None & info [ "sections" ] ~doc in
match sections with
| None -> All
| Some sections -> Only (Section.Set.of_list sections)
;;
let should_install t section =
match t with
| All -> true
| Only set -> Section.Set.mem set section
;;
end
let file_operations ~verbosity ~dry_run ~workspace : (module File_operations) =
if dry_run
then
(module File_ops_dry_run (struct
let verbosity = verbosity
end))
else
(module File_ops_real (struct
let workspace = workspace
let verbosity = verbosity
end))
;;
let package_is_vendored (pkg : Package.t) =
let dir = Package.dir pkg in
Memo.run (Source_tree.is_vendored dir)
;;
type what =
| Install
| Uninstall
let pp_what fmt = function
| Install -> Format.pp_print_string fmt "Install"
| Uninstall -> Format.pp_print_string fmt "Uninstall"
;;
let cmd_what = function
| Install -> "install"
| Uninstall -> "uninstall"
;;
let install_entry
~ops
~conf
~package
~dir
~create_install_files
(entry : Path.t Install.Entry.t)
~dst
~verbosity
=
let module Ops = (val ops : File_operations) in
let open Fiber.O in
let special_file = Special_file.of_entry entry in
(match special_file with
| _ when not create_install_files -> Fiber.return true
| Some Special_file.META | Some Special_file.Dune_package -> Fiber.return true
| None ->
Artifact_substitution.test_file ~src:entry.src ()
>>| (function
| Some_substitution -> true
| No_substitution -> false))
>>= function
| false -> Fiber.return entry
| true ->
let+ () =
(match Path.is_directory dst with
| true -> Ops.remove_dir_if_exists ~if_non_empty:Fail dst
| false -> Ops.remove_file_if_exists dst);
print_line
~verbosity
"%s %s"
(if create_install_files then "Copying to" else "Installing")
(Path.to_string_maybe_quoted dst);
Ops.mkdir_p dir;
let executable = Section.should_set_executable_bit entry.section in
let kind =
match special_file with
| Some special -> Special special
| None ->
(* CR-emillon: for most cases we could use a fast copy here, but some
kinds of files do need artifact substitution(at least
executable files and artifacts built from generated sites
modules), but it's too late to know without reading the file. *)
Substitute
in
Ops.copy_file ~src:entry.src ~dst ~executable ~kind ~package ~conf
in
Install.Entry.set_src entry dst
;;
let run
what
context
common
pkgs
sections
(config : Dune_config.t)
~dry_run
~destdir
~relocatable
~create_install_files
~prefix_from_command_line
~(from_command_line : _ Install.Roots.t)
=
let open Fiber.O in
let* workspace = Workspace.get () in
let contexts =
match context with
| None ->
(match Common.x common with
| Some findlib_toolchain ->
let contexts =
List.filter workspace.contexts ~f:(fun (ctx : Context.t) ->
match Context.findlib_toolchain ctx with
| None -> false
| Some ctx_findlib_toolchain ->
Dune_engine.Context_name.equal ctx_findlib_toolchain findlib_toolchain)
in
contexts
| None -> workspace.contexts)
| Some name ->
(match
List.find workspace.contexts ~f:(fun c ->
Dune_engine.Context_name.equal (Context.name c) name)
with
| Some ctx -> [ ctx ]
| None ->
User_error.raise
[ Pp.textf "Context %S not found!" (Dune_engine.Context_name.to_string name) ])
in
let* pkgs =
match pkgs with
| _ :: _ -> Fiber.return pkgs
| [] ->
Package.Name.Map.values workspace.packages
|> Fiber.parallel_map ~f:(fun pkg ->
package_is_vendored pkg
>>| function
| true -> None
| false -> Some (Package.name pkg))
>>| List.filter_opt
in
let install_files, missing_install_files =
List.concat_map pkgs ~f:(fun pkg ->
List.map contexts ~f:(fun (ctx : Context.t) ->
let fn =
let fn =
resolve_package_install
workspace
~findlib_toolchain:(Context.findlib_toolchain ctx)
pkg
in
Path.append_source (Path.build (Context.build_dir ctx)) fn
in
if Path.exists fn then Left (ctx, (pkg, fn)) else Right fn))
|> List.partition_map ~f:Fun.id
in
if missing_install_files <> []
then
User_error.raise
[ Pp.textf "The following <package>.install are missing:"
; Pp.enumerate missing_install_files ~f:(fun p -> Pp.text (Path.to_string p))
]
~hints:
[ Pp.concat
~sep:Pp.space
[ Pp.text "try running"
; User_message.command "dune build [-p <pkg>] @install"
]
|> Pp.hovbox
];
(match contexts, prefix_from_command_line, from_command_line.lib_root with
| _ :: _ :: _, Some _, _ | _ :: _ :: _, _, Some _ ->
User_error.raise
[ Pp.concat
~sep:Pp.space
[ Pp.text "Cannot specify"
; User_message.command "--prefix"
; Pp.text "or"
; User_message.command "--libdir"
; Pp.text "when installing into multiple contexts!"
]
]
| _ -> ());
let install_files_by_context =
let module CMap = Map.Make (Context) in
CMap.of_list_multi install_files
|> CMap.to_list_map ~f:(fun context install_files ->
let entries_per_package =
List.map install_files ~f:(fun (package, install_file) ->
let entries =
Install.Entry.load_install_file install_file Path.of_local
|> List.filter ~f:(fun (entry : Path.t Install.Entry.t) ->
Sections.should_install sections entry.section)
in
match
List.filter_map entries ~f:(fun entry ->
(* CR rgrinberg: this is ignoring optional entries *)
Option.some_if (not (Path.exists entry.src)) entry.src)
with
| [] -> package, entries
| missing_files ->
User_error.raise
[ Pp.textf
"The following files which are listed in %s cannot be installed \
because they do not exist:"
(Path.to_string_maybe_quoted install_file)
; Pp.enumerate missing_files ~f:(fun p ->
Pp.verbatim (Path.to_string_maybe_quoted p))
])
in
context, entries_per_package)
in
let destdir =
Option.map
~f:Path.of_string
(if create_install_files
then
(* CR-rgrinberg: why are we silently ignoring an argument instead
of erroring given that they mutually exclusive? *)
Some (Option.value ~default:"_destdir" destdir)
else destdir)
in
let relocatable =
if relocatable
then (
match prefix_from_command_line with
| Some dir -> Some (Path.of_string dir)
| None ->
User_error.raise
[ Pp.concat
~sep:Pp.space
[ Pp.text "Option"
; User_message.command "--prefix"
; Pp.text "is needed with"
; User_message.command "--relocation"
]
|> Pp.hovbox
])
else None
in
let verbosity =
match config.display with
| Simple display -> display.verbosity
| Tui -> Quiet
in
let open Fiber.O in
let (module Ops) = file_operations ~verbosity ~dry_run ~workspace in
let files_deleted_in = ref Path.Set.empty in
let+ () =
Fiber.sequential_iter
install_files_by_context
~f:(fun (context, entries_per_package) ->
let* roots = get_dirs context ~prefix_from_command_line ~from_command_line in
let conf = Artifact_substitution.Conf.of_install ~relocatable ~roots ~context in
Fiber.sequential_iter entries_per_package ~f:(fun (package, entries) ->
let+ entries =
(* CR rgrinberg: why don't we install things concurrently? *)
Fiber.sequential_map entries ~f:(fun entry ->
let dst =
let paths = Install.Paths.make ~relative:Path.relative ~package ~roots in
Install.Entry.relative_installed_path entry ~paths
|> interpret_destdir ~destdir
in
let dir = Path.parent_exn dst in
match what with
| Uninstall ->
Ops.remove_file_if_exists dst;
files_deleted_in := Path.Set.add !files_deleted_in dir;
Fiber.return entry
| Install ->
install_entry
~ops:(module Ops)
~conf
~package
~dir
~create_install_files
~dst
~verbosity
entry)
in
if create_install_files
then (
let fn =
resolve_package_install
workspace
~findlib_toolchain:(Context.findlib_toolchain context)
package
in
Install.Entry.gen_install_file entries |> Io.write_file (Path.source fn))))
in
Path.Set.to_list !files_deleted_in
(* This [List.rev] is to ensure we process children directories before
their parents *)
|> List.rev
|> List.iter ~f:(Ops.remove_dir_if_exists ~if_non_empty:Warn)
;;
let make ~what =
let doc = Format.asprintf "%a packages defined in the workspace." pp_what what in
let name_ = Arg.info [] ~docv:"PACKAGE" in
let absolute_path =
Arg.conv'
( (fun path ->
if Filename.is_relative path
then Error "the path must be absolute to avoid ambiguity"
else Ok path)
, Arg.conv_printer Arg.string )
in
let term =
let+ builder = Common.Builder.term
and+ prefix_from_command_line =
Arg.(
value
& opt (some string) None
& info
[ "prefix" ]
~env:(Cmd.Env.info "DUNE_INSTALL_PREFIX")
~docv:"PREFIX"
~doc:
"Directory where files are copied. For instance binaries are copied into \
$(i,\\$prefix/bin), library files into $(i,\\$prefix/lib), etc...")
and+ destdir =
Arg.(
value
& opt (some string) None
& info
[ "destdir" ]
~env:(Cmd.Env.info "DESTDIR")
~docv:"PATH"
~doc:"This directory is prepended to all installed paths.")
and+ libdir_from_command_line =
Arg.(
value
& opt (some absolute_path) None
& info
[ "libdir" ]
~docv:"PATH"
~doc:
"Directory where library files are copied, relative to $(b,prefix) or \
absolute. If $(b,--prefix) is specified the default is \
$(i,\\$prefix/lib). Only absolute path accepted.")
and+ mandir_from_command_line =
let doc =
"Manually override the directory to install man pages. Only absolute path \
accepted."
in
Arg.(value & opt (some absolute_path) None & info [ "mandir" ] ~docv:"PATH" ~doc)
and+ docdir_from_command_line =
let doc =
"Manually override the directory to install documentation files. Only absolute \
path accepted."
in
Arg.(value & opt (some absolute_path) None & info [ "docdir" ] ~docv:"PATH" ~doc)
and+ etcdir_from_command_line =
let doc =
"Manually override the directory to install configuration files. Only absolute \
path accepted."
in
Arg.(value & opt (some absolute_path) None & info [ "etcdir" ] ~docv:"PATH" ~doc)
and+ bindir_from_command_line =
let doc =
"Manually override the directory to install public binaries. Only absolute path \
accepted."
in
Arg.(value & opt (some absolute_path) None & info [ "bindir" ] ~docv:"PATH" ~doc)
and+ sbindir_from_command_line =
let doc =
"Manually override the directory to install files from sbin section. Only \
absolute path accepted."
in
Arg.(value & opt (some absolute_path) None & info [ "sbindir" ] ~docv:"PATH" ~doc)
and+ datadir_from_command_line =
let doc =
"Manually override the directory to install files from share section. Only \
absolute path accepted."
in
Arg.(value & opt (some absolute_path) None & info [ "datadir" ] ~docv:"PATH" ~doc)
and+ libexecdir_from_command_line =
let doc =
"Manually override the directory to install executable library files. Only \
absolute path accepted."
in
Arg.(
value & opt (some absolute_path) None & info [ "libexecdir" ] ~docv:"PATH" ~doc)
and+ dry_run =
Arg.(
value
& flag
& info
[ "dry-run" ]
~doc:"Only display the file operations that would be performed.")
and+ relocatable =
Arg.(
value
& flag
& info
[ "relocatable" ]
~doc:
"Make the binaries relocatable (the installation directory can be moved). \
The installation directory must be specified with --prefix")
and+ create_install_files =
Arg.(
value
& flag
& info
[ "create-install-files" ]
~doc:
"Do not directly install, but create install files in the root directory \
and create substituted files if needed in destdir (_destdir by default).")
and+ pkgs = Arg.(value & pos_all package_name [] name_)
and+ context =
Arg.(
value
& opt (some Arg.context_name) None
& info
[ "context" ]
~docv:"CONTEXT"
~doc:
"Select context to install from. By default, install files from all \
defined contexts.")
and+ sections = Sections.term in
let builder = Common.Builder.forbid_builds builder in
let builder = Common.Builder.disable_log_file builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
let from_command_line =
{ Install.Roots.lib_root = libdir_from_command_line
; etc_root = etcdir_from_command_line
; doc_root = docdir_from_command_line
; man = mandir_from_command_line
; bin = bindir_from_command_line
; sbin = sbindir_from_command_line
; libexec_root = libexecdir_from_command_line
; share_root = datadir_from_command_line
}
|> Install.Roots.map ~f:(Option.map ~f:Path.of_string)
|> Install.Roots.complete
in
run
what
context
common
pkgs
sections
config
~dry_run
~destdir
~relocatable
~create_install_files
~prefix_from_command_line
~from_command_line)
in
Cmd.v
(Cmd.info
(cmd_what what)
~doc
~man:Manpage.(`S s_synopsis :: (synopsis @ Common.help_secs)))
term
;;
let install = make ~what:Install
let uninstall = make ~what:Uninstall

View file

@ -0,0 +1,4 @@
open Import
val install : unit Cmd.t
val uninstall : unit Cmd.t

View file

@ -0,0 +1,76 @@
open Import
let doc = "Print out libraries installed on the system."
let info = Cmd.info "installed-libraries" ~doc
let term =
let+ builder = Common.Builder.term
and+ na =
Arg.(
value
& flag
& info
[ "na"; "not-available" ]
~doc:"List libraries that are not available and explain why")
in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server
~common
~config
(let run () =
let open Memo.O in
let* ctxs = Context.DB.all () in
let ctx = List.hd ctxs in
let* findlib = Findlib.create (Context.name ctx) in
let* all_packages = Findlib.all_packages findlib in
if na
then (
let+ broken =
Findlib.all_broken_packages findlib
>>| List.map ~f:(fun (name, _) ->
Lib_name.of_package_name name, "invalid dune file")
in
let hidden =
List.filter_map all_packages ~f:(function
| Hidden_library lib ->
Some
( Dune_package.Lib.info lib |> Dune_rules.Lib_info.name
, "unsatisfied 'exists_if'" )
| _ -> None)
in
let all =
List.sort (broken @ hidden) ~compare:(fun (a, _) (b, _) ->
Lib_name.compare a b)
in
let longest = String.longest_map all ~f:(fun (n, _) -> Lib_name.to_string n) in
let ppf = Format.std_formatter in
List.iter all ~f:(fun (n, r) ->
Format.fprintf ppf "%-*s -> %s@\n" longest (Lib_name.to_string n) r);
Format.pp_print_flush ppf ())
else (
let pkgs =
List.filter all_packages ~f:(function
| Dune_package.Entry.Hidden_library _ -> false
| _ -> true)
in
let max_len =
String.longest_map pkgs ~f:(fun e ->
Lib_name.to_string (Dune_package.Entry.name e))
in
List.iter pkgs ~f:(fun e ->
let ver_string =
match Dune_package.Entry.version e with
| Some v -> Package_version.to_string v
| _ -> "n/a"
in
Printf.printf
"%-*s (version: %s)\n"
max_len
(Lib_name.to_string (Dune_package.Entry.name e))
ver_string);
Memo.return ())
in
fun () -> Memo.run (run ()))
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,12 @@
open Import
let latest_lang_version =
Cmd.v
(Cmd.info "latest-lang-version")
(let+ () = Term.const () in
print_endline
(Dune_lang.Syntax.greatest_supported_version_exn Stanza.syntax
|> Dune_lang.Syntax.Version.to_string))
;;
let group = Cmd.group (Cmd.info "internal") [ Internal_dump.command; latest_lang_version ]

View file

@ -0,0 +1,3 @@
open Import
val group : unit Cmd.t

View file

@ -0,0 +1,24 @@
open Import
module Persistent = Dune_util.Persistent
let doc = "Dump the contents of a file stored in Dune's persistent database."
let man =
[ `S "DESCRIPTION"
; `P
{|Dump the contents of a file stored in Dune's persistent database in a human readable format.|}
; `Blocks Common.help_secs
]
;;
let info = Cmd.info "dump" ~doc ~man
let term =
let+ builder = Common.Builder.term
and+ file = Arg.(required & pos 0 (some Arg.path) None & Arg.info [] ~docv:"FILE") in
let _common, _config = Common.init builder in
let (Persistent.T ((module D), data)) = Persistent.load_exn (Arg.Path.path file) in
Console.print [ Dyn.pp (D.to_dyn data) ]
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,254 @@
open Dune_config
open Import
module Lock_dir = Dune_pkg.Lock_dir
module Pin = Dune_pkg.Pin
let is_enabled =
lazy
(match Config.get Dune_rules.Compile_time.lock_dev_tools with
| `Enabled -> true
| `Disabled -> false)
;;
(* Returns a version constraint accepting (almost) all versions whose prefix is
the given version. This allows alternative distributions of packages to be
chosen, such as choosing "ocamlformat.0.26.2+binary" when .ocamlformat
contains "version=0.26.2". *)
let relaxed_version_constraint_of_version version =
let open Dune_lang in
let min_version = Package_version.to_string version in
(* The goal here is to add a suffix to [min_version] to construct a version
number higher than than any version number likely to appear with
[min_version] as a prefix. "_" is the highest ascii symbol that can appear
in version numbers, excluding "~" which has a special meaning. It's
conceivable that one or two consecutive "_" characters may be used in a
version, so this appends "___" to [min_version].
Read more at: https://opam.ocaml.org/doc/Manual.html#Version-ordering
*)
let max_version = min_version ^ "___MAX_VERSION" in
Package_constraint.And
[ Package_constraint.Uop
(Relop.Gte, Package_constraint.Value.String_literal min_version)
; Package_constraint.Uop
(Relop.Lte, Package_constraint.Value.String_literal max_version)
]
;;
(* The solver satisfies dependencies for local packages, but dev tools
are not local packages. As a workaround, create an empty local package
which depends on the dev tool package. *)
let make_local_package_wrapping_dev_tool ~dev_tool ~dev_tool_version ~extra_dependencies
: Dune_pkg.Local_package.t
=
let dev_tool_pkg_name = Dune_pkg.Dev_tool.package_name dev_tool in
let dependency =
let open Dune_lang in
let open Package_dependency in
let constraint_ =
Option.map dev_tool_version ~f:relaxed_version_constraint_of_version
in
{ name = dev_tool_pkg_name; constraint_ }
in
let local_package_name =
Package_name.of_string (Package_name.to_string dev_tool_pkg_name ^ "_dev_tool_wrapper")
in
{ Dune_pkg.Local_package.name = local_package_name
; version = Dune_pkg.Lock_dir.Pkg_info.default_version
; dependencies =
Dune_pkg.Dependency_formula.of_dependencies (dependency :: extra_dependencies)
; conflicts = []
; depopts = []
; pins = Package_name.Map.empty
; conflict_class = []
; loc = Loc.none
; command_source = Opam_file { build = []; install = [] }
}
;;
let solve ~dev_tool ~local_packages =
let open Memo.O in
let* solver_env_from_current_system =
Pkg_common.poll_solver_env_from_current_system ()
|> Memo.of_reproducible_fiber
>>| Option.some
and* workspace =
let+ workspace = Workspace.workspace () in
match Config.get Dune_rules.Compile_time.bin_dev_tools with
| `Enabled ->
Workspace.add_repo workspace Dune_pkg.Pkg_workspace.Repository.binary_packages
| `Disabled -> workspace
in
let lock_dir = Lock_dir.dev_tool_lock_dir_path dev_tool in
Memo.of_reproducible_fiber
@@ Lock.solve
workspace
~local_packages
~project_pins:Pin.DB.empty
~solver_env_from_current_system
~version_preference:None
~lock_dirs:[ lock_dir ]
~print_perf_stats:false
~portable_lock_dir:false
;;
let compiler_package_name = Package_name.of_string "ocaml"
(* Some dev tools must be built with the same version of the ocaml
compiler as the project. This function returns the version of the
"ocaml" package used to compile the project in the default build
context.
TODO: This only makes sure that the version of compiler used to
build the dev tool matches the version of the compiler used to
build this project. This will fail if the project is built with a
custom compiler (e.g. ocaml-variants) since the version of the
compiler will be the same between the project and dev tool while
they still use different compilers. A more robust solution would be
to ensure that the exact compiler package used to build the dev
tool matches the package used to build the compiler. *)
let locked_ocaml_compiler_version () =
let open Memo.O in
let context =
(* Dev tools are only ever built with the default context. *)
Context_name.default
in
let* result = Dune_rules.Lock_dir.get context
and* platform =
Pkg_common.poll_solver_env_from_current_system () |> Memo.of_reproducible_fiber
in
match result with
| Error _ ->
User_error.raise
[ Pp.text "Unable to load the lockdir for the default build context." ]
~hints:
[ Pp.concat
~sep:Pp.space
[ Pp.text "Try running"; User_message.command "dune pkg lock" ]
]
| Ok { packages; _ } ->
let packages = Lock_dir.Packages.pkgs_on_platform_by_name packages ~platform in
(match Package_name.Map.find packages compiler_package_name with
| None ->
User_error.raise
[ Pp.textf
"The lockdir doesn't contain a lockfile for the package %S."
(Package_name.to_string compiler_package_name)
]
~hints:
[ Pp.concat
~sep:Pp.space
[ Pp.textf
"Add a dependency on %S to one of the packages in dune-project and \
then run"
(Package_name.to_string compiler_package_name)
; User_message.command "dune pkg lock"
]
]
| Some pkg -> Memo.return pkg.info.version)
;;
(* Returns a dependency constraint on the version of the ocaml
compiler in the lockdir associated with the default context. *)
let locked_ocaml_compiler_constraint () =
let open Dune_lang in
let open Memo.O in
let+ ocaml_compiler_version = locked_ocaml_compiler_version () in
let constraint_ =
Some
(Package_constraint.Uop
(Eq, String_literal (Package_version.to_string ocaml_compiler_version)))
in
{ Package_dependency.name = compiler_package_name; constraint_ }
;;
let extra_dependencies dev_tool =
let open Memo.O in
match Dune_pkg.Dev_tool.needs_to_build_with_same_compiler_as_project dev_tool with
| false -> Memo.return []
| true ->
let+ constraint_ = locked_ocaml_compiler_constraint () in
[ constraint_ ]
;;
let lockdir_status dev_tool =
let open Memo.O in
let dev_tool_lock_dir = Lock_dir.dev_tool_lock_dir_path dev_tool in
match Lock_dir.read_disk dev_tool_lock_dir with
| Error _ -> Memo.return `No_lockdir
| Ok { packages; _ } ->
(match Dune_pkg.Dev_tool.needs_to_build_with_same_compiler_as_project dev_tool with
| false -> Memo.return `Lockdir_ok
| true ->
let* platform =
Pkg_common.poll_solver_env_from_current_system () |> Memo.of_reproducible_fiber
in
let packages = Lock_dir.Packages.pkgs_on_platform_by_name packages ~platform in
(match Package_name.Map.find packages compiler_package_name with
| None -> Memo.return `No_compiler_lockfile_in_lockdir
| Some { info; _ } ->
let+ ocaml_compiler_version = locked_ocaml_compiler_version () in
(match Package_version.equal info.version ocaml_compiler_version with
| true -> `Lockdir_ok
| false ->
`Dev_tool_needs_to_be_relocked_because_project_compiler_version_changed
(User_message.make
[ Pp.textf
"The version of the compiler package (%S) in this project's \
lockdir has changed to %s (formerly the compiler version was %s). \
The dev-tool %S will be re-locked and rebuilt with this version \
of the compiler."
(Package_name.to_string compiler_package_name)
(Package_version.to_string ocaml_compiler_version)
(Package_version.to_string info.version)
(Dune_pkg.Dev_tool.package_name dev_tool |> Package_name.to_string)
]))))
;;
(* [lock_dev_tool_at_version dev_tool version] generates the lockdir for the
dev tool [dev_tool]. If [version] is [Some v] then version [v] of the tool
will be chosen by the solver. Otherwise the solver is free to choose the
appropriate version of the tool to install. *)
let lock_dev_tool_at_version dev_tool version =
let open Memo.O in
let* need_to_solve =
lockdir_status dev_tool
>>| function
| `Lockdir_ok -> false
| `No_lockdir -> true
| `No_compiler_lockfile_in_lockdir ->
Console.print
[ Pp.textf
"The lockdir for %s lacks a lockfile for %s. Regenerating..."
(Dune_pkg.Dev_tool.package_name dev_tool |> Package_name.to_string)
(Package_name.to_string compiler_package_name)
];
true
| `Dev_tool_needs_to_be_relocked_because_project_compiler_version_changed message ->
Console.print_user_message message;
true
in
if need_to_solve
then
let* extra_dependencies = extra_dependencies dev_tool in
let local_pkg =
make_local_package_wrapping_dev_tool
~dev_tool
~dev_tool_version:version
~extra_dependencies
in
let local_packages = Package_name.Map.singleton local_pkg.name local_pkg in
solve ~dev_tool ~local_packages
else Memo.return ()
;;
let lock_ocamlformat () =
let version = Dune_pkg.Ocamlformat.version_of_current_project's_ocamlformat_config () in
lock_dev_tool_at_version Ocamlformat version
;;
let lock_dev_tool dev_tool =
match (dev_tool : Dune_pkg.Dev_tool.t) with
| Ocamlformat -> lock_ocamlformat ()
| other -> lock_dev_tool_at_version other None
;;

View file

@ -0,0 +1,4 @@
open! Import
val is_enabled : bool Lazy.t
val lock_dev_tool : Dune_pkg.Dev_tool.t -> unit Memo.t

View file

@ -0,0 +1,117 @@
open Import
let all : _ Cmdliner.Cmd.t list =
let terms =
Runtest.commands
@ [ Installed_libraries.command
; External_lib_deps.command
; Build.build
; Fmt.command
; Clean.command
; Install_uninstall.install
; Install_uninstall.uninstall
; Exec.command
; Subst.command
; Print_rules.command
; Utop.command
; Promotion.promote
; command_alias Printenv.command Printenv.term "printenv"
; Help.command
; Format_dune_file.command
; Upgrade.command
; Cache.command
; Top.command
; Ocaml_merlin.command
; Shutdown.command
; Diagnostics.command
; Monitor.command
]
in
let groups =
[ Ocaml_cmd.group
; Coq.group
; Describe.group
; Describe.Show.group
; Rpc.group
; Internal.group
; Init.group
; Promotion.group
; Pkg.group
; Pkg.Alias.group
; Tools.group
]
in
terms @ groups
;;
(* Short reminders for the most used and useful commands *)
let common_commands_synopsis =
Common.command_synopsis
[ "build [--watch]"
; "runtest [--watch]"
; "exec NAME"
; "utop [DIR]"
; "install"
; "init project NAME [PATH] [--libs=l1,l2 --ppx=p1,p2 --inline-tests]"
]
;;
let info =
let doc = "composable build system for OCaml" in
Cmd.info
"dune"
~doc
~envs:Common.envs
~version:
(match Build_info.V1.version () with
| None -> "n/a"
| Some v -> Build_info.V1.Version.to_string v)
~man:
[ `Blocks common_commands_synopsis
; `S "DESCRIPTION"
; `P
{|Dune is a build system designed for OCaml projects only. It
focuses on providing the user with a consistent experience and takes
care of most of the low-level details of OCaml compilation. All you
have to do is provide a description of your project and Dune will
do the rest.
|}
; `P
{|The scheme it implements is inspired from the one used inside Jane
Street and adapted to the open source world. It has matured over a
long time and is used daily by hundreds of developers, which means
that it is highly tested and productive.
|}
; `Blocks Common.help_secs
; Common.examples
[ "Initialise a new project named `foo'", "dune init project foo"
; "Build all targets in the current source tree", "dune build"
; "Run the executable named `bar'", "dune exec bar"
; "Run all tests in the current source tree", "dune runtest"
; "Install all components defined in the project", "dune install"
; "Remove all build artefacts", "dune clean"
]
]
;;
let cmd = Cmd.group info all
let exit_and_flush code =
Console.finish ();
exit (Exit_code.code code)
;;
let () =
Dune_rules.Colors.setup_err_formatter_colors ();
try
match Cmd.eval_value cmd ~catch:false with
| Ok _ -> exit_and_flush Success
| Error _ -> exit_and_flush Error
with
| Scheduler.Run.Shutdown.E Requested -> exit_and_flush Success
| Scheduler.Run.Shutdown.E (Signal _) -> exit_and_flush Signal
| exn ->
let exn = Exn_with_backtrace.capture exn in
Dune_util.Report_error.report exn;
exit_and_flush Error
;;

View file

@ -0,0 +1 @@
(** Main module *)

View file

@ -0,0 +1,295 @@
open Import
open Fiber.O
module Client = Dune_rpc_client.Client
module Version_error = Dune_rpc_private.Version_error
include struct
open Dune_rpc
module Diagnostic = Diagnostic
module Progress = Progress
module Job = Job
module Sub = Sub
module Conv = Conv
end
(** Utility module for generating [Map] modules for [Diagnostic]s and [Job]s which use
their [Id] as keys. *)
module Id_map (Id : sig
type t
val compare : t -> t -> Ordering.t
val sexp : (t, Conv.values) Conv.t
end) =
struct
include Map.Make (struct
include Id
let to_dyn t = Sexp.to_dyn (Conv.to_sexp Id.sexp t)
end)
end
module Diagnostic_id_map = Id_map (Diagnostic.Id)
module Job_id_map = Id_map (Job.Id)
module Event = struct
(** Events that the render loop will process. *)
type t =
| Diagnostics of Diagnostic.Event.t list
| Jobs of Job.Event.t list
| Progress of Progress.t
end
module State : sig
(** Internal state of the render loop. *)
type t
(** Initial empty state. *)
val init : unit -> t
module Update : sig
(** Incremental updates to the state. Computes increments of the state that
will be used for efficient rendering. *)
type t
end
val update : t -> Event.t -> Update.t
(** Given a state update, render the update. *)
val render : t -> Update.t -> unit
end = struct
type t =
{ mutable diagnostics : Diagnostic.t Diagnostic_id_map.t
; mutable jobs : Job.t Job_id_map.t
; mutable progress : Progress.t
}
let init () =
{ diagnostics = Diagnostic_id_map.empty; jobs = Job_id_map.empty; progress = Waiting }
;;
let done_status ~complete ~remaining ~failed state =
Pp.textf
"Done: %d%% (%d/%d, %d left%s) (jobs: %d)"
(if complete + remaining = 0 then 0 else complete * 100 / (complete + remaining))
complete
(complete + remaining)
remaining
(match failed with
| 0 -> ""
| failed -> sprintf ", %d failed" failed)
(Job_id_map.cardinal state.jobs)
;;
let waiting_for_file_system_changes message =
Pp.seq message (Pp.verbatim ", waiting for filesystem changes...")
;;
let restarting_current_build message =
Pp.seq message (Pp.verbatim ", restarting current build...")
;;
let had_errors state =
match Diagnostic_id_map.cardinal state.diagnostics with
| 1 -> Pp.verbatim "Had 1 error"
| n -> Pp.textf "Had %d errors" n
;;
let status (state : t) =
Console.Status_line.set
(Live
(fun () ->
match (state.progress : Progress.t) with
| Waiting -> Pp.verbatim "Initializing..."
| In_progress { complete; remaining; failed } ->
done_status ~complete ~remaining ~failed state
| Interrupted ->
Pp.tag User_message.Style.Error (Pp.verbatim "Source files changed")
|> restarting_current_build
| Success ->
Pp.tag User_message.Style.Success (Pp.verbatim "Success")
|> waiting_for_file_system_changes
| Failed ->
Pp.tag User_message.Style.Error (had_errors state)
|> waiting_for_file_system_changes))
;;
module Update = struct
type t =
| Update_status
| Add_diagnostics of Diagnostic.t list
| Refresh
let jobs state jobs =
let jobs =
List.fold_left jobs ~init:state.jobs ~f:(fun acc job_event ->
match (job_event : Job.Event.t) with
| Start job -> Job_id_map.add_exn acc job.id job
| Stop id -> Job_id_map.remove acc id)
in
state.jobs <- jobs;
Update_status
;;
let progress state progress =
state.progress <- progress;
Update_status
;;
let diagnostics state diagnostics =
let mode, diagnostics =
List.fold_left
diagnostics
~init:(`Add_only [], state.diagnostics)
~f:(fun (mode, acc) diag_event ->
match (diag_event : Diagnostic.Event.t) with
| Remove diag -> `Remove, Diagnostic_id_map.remove acc diag.id
| Add diag ->
( (match mode with
| `Add_only diags -> `Add_only (diag :: diags)
| `Remove -> `Remove)
, Diagnostic_id_map.add_exn acc diag.id diag ))
in
state.diagnostics <- diagnostics;
match mode with
| `Add_only update -> Add_diagnostics (List.rev update)
| `Remove -> Refresh
;;
end
let update state (event : Event.t) =
match event with
| Jobs jobs -> Update.jobs state jobs
| Progress progress -> Update.progress state progress
| Diagnostics diagnostics -> Update.diagnostics state diagnostics
;;
let render =
let f d = Console.print_user_message (Diagnostic.to_user_message d) in
fun (state : t) (update : Update.t) ->
(match (update : Update.t) with
| Add_diagnostics diags -> List.iter diags ~f
| Update_status -> ()
| Refresh ->
Console.reset ();
Diagnostic_id_map.iter state.diagnostics ~f);
status state
;;
end
(* A generic loop that continuously fetches events from a [sub] that it opens a
poll to and writes them to the [event] bus. *)
let fetch_loop ~(event : Event.t Fiber_event_bus.t) ~client ~f sub =
Client.poll client sub
>>= function
| Error version_error ->
let* () = Fiber_event_bus.close event in
User_error.raise [ Pp.verbatim (Version_error.message version_error) ]
| Ok poller ->
let rec loop () =
Fiber.collect_errors (fun () -> Client.Stream.next poller)
>>= (function
| Ok (Some payload) -> Fiber_event_bus.push event (f payload)
| Error _ | Ok None -> Fiber_event_bus.close event >>> Fiber.return `Closed)
>>= function
| `Closed -> Fiber.return ()
| `Ok -> loop ()
in
loop ()
;;
(* Main render loop *)
let render_loop ~(event : Event.t Fiber_event_bus.t) =
Console.reset ();
let state = State.init () in
let rec loop () =
Fiber_event_bus.pop event
>>= function
| `Closed ->
Console.print_user_message
(User_error.make [ Pp.textf "Lost connection to server." ]);
Fiber.return ()
| `Next event ->
let update = State.update state event in
(* CR-someday alizter: If performance of rendering here on every loop is bad we can
instead batch updates. It should be very simple to write a [State.Update.union]
function that can combine incremental updates to be done at once. *)
State.render state update;
loop ()
in
loop ()
;;
let monitor ~quit_on_disconnect () =
Fiber.repeat_while ~init:1 ~f:(fun i ->
match Dune_rpc_impl.Where.get () with
| Some where ->
let* connect = Client.Connection.connect_exn where in
let+ () =
Dune_rpc_impl.Client.client
connect
(Dune_rpc.Initialize.Request.create
~id:(Dune_rpc.Id.make (Sexp.Atom "monitor_cmd")))
~f:(fun client ->
let event = Fiber_event_bus.create () in
let module Sub = Dune_rpc_private.Public.Sub in
Fiber.all_concurrently_unit
[ render_loop ~event
; fetch_loop ~event ~client ~f:(fun x -> Event.Jobs x) Sub.running_jobs
; fetch_loop ~event ~client ~f:(fun x -> Event.Progress x) Sub.progress
; fetch_loop ~event ~client ~f:(fun x -> Event.Diagnostics x) Sub.diagnostic
])
in
Some i
| None when quit_on_disconnect ->
User_error.raise [ Pp.text "RPC server not running." ]
| None ->
Console.Status_line.set
(Console.Status_line.Live
(fun () -> Pp.verbatim ("Waiting for RPC server" ^ String.make (i mod 4) '.')));
let+ () = Scheduler.sleep ~seconds:0.3 in
Some (i + 1))
;;
let man =
[ `S "DESCRIPTION"
; `P
"$(b,dune monitor) connects to an RPC server running in the current workspace and \
displays the build progress and diagnostics. If no server is running or it was \
disconnected, it will continuously try to reconnect."
]
;;
let command =
let info =
let doc = "Connect to a Dune RPC server and monitor it." in
Cmd.info "monitor" ~doc ~man
and term =
let open Import in
let+ builder = Common.Builder.term
and+ quit_on_disconnect =
Arg.(
value
& flag
& info
[ "quit-on-disconnect" ]
~doc:"Quit if the connection to the server is lost.")
in
let builder = Common.Builder.forbid_builds builder in
let builder = Common.Builder.disable_log_file builder in
let common, config = Common.init builder in
let stats = Common.stats common in
let config =
Dune_config.for_scheduler
config
stats
~print_ctrl_c_warning:true
~watch_exclusions:[]
in
Scheduler.Run.go
config
~on_event:(fun _ _ -> ())
~file_watcher:No_watcher
(monitor ~quit_on_disconnect)
in
Cmd.v info term
;;

View file

@ -0,0 +1 @@
val command : unit Cmdliner.Cmd.t

View file

@ -0,0 +1,68 @@
open Import
module Main = Import.Main
let doc = "Build and view the documentation of an OCaml project"
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune ocaml doc) builds and then opens the documentation of an OCaml project in the users default browser.|}
; `Blocks Common.help_secs
]
;;
let info = Cmd.info "doc" ~doc ~man
let lock_odoc_if_dev_tool_enabled () =
match Lazy.force Lock_dev_tool.is_enabled with
| false -> Action_builder.return ()
| true -> Action_builder.of_memo (Lock_dev_tool.lock_dev_tool Odoc)
;;
let term =
let+ builder = Common.Builder.term in
let common, config = Common.init builder in
let request (setup : Main.build_system) =
let dir = Path.(relative root) (Common.prefix_target common ".") in
let open Action_builder.O in
let* () = lock_odoc_if_dev_tool_enabled () in
let+ () =
Alias.in_dir ~name:Dune_rules.Alias.doc ~recursive:true ~contexts:setup.contexts dir
|> Alias.request
in
let relative_toplevel_index_path =
let toplevel_index_path =
let is_default ctx = ctx |> Context.name |> Dune_engine.Context_name.is_default in
let doc_ctx = List.find_exn setup.contexts ~f:is_default in
Dune_rules.Odoc.Paths.toplevel_index doc_ctx
in
Path.(toplevel_index_path |> build |> to_string_maybe_quoted)
in
Console.print
[ Pp.textf "Docs built. Index can be found here: %s" relative_toplevel_index_path ];
match
let open Option.O in
let* cmd_name, args =
match Platform.OS.value with
| Darwin -> Some ("open", [])
| Other | FreeBSD | NetBSD | OpenBSD | Haiku | Linux -> Some ("xdg-open", [])
| Windows -> None
in
let+ open_command =
let path = Env_path.path Env.initial in
Bin.which ~path cmd_name
in
open_command, args @ [ relative_toplevel_index_path ]
with
| Some (cmd, args) ->
Proc.restore_cwd_and_execve (Path.to_absolute_filename cmd) args ~env:Env.initial
| None ->
User_warning.emit
[ Pp.text
"No browser could be found, you will have to open the documentation yourself."
]
in
Build.run_build_command ~common ~config ~request
;;
let cmd = Cmd.v info term

View file

@ -0,0 +1,3 @@
open! Import
val cmd : unit Cmd.t

View file

@ -0,0 +1,16 @@
open Import
let info = Cmd.info "ocaml" ~doc:"Command group related to OCaml."
let group =
Cmdliner.Cmd.group
info
[ Utop.command
; Ocaml_merlin.command
; Ocaml_merlin.Dump_dot_merlin.command
; Top.command
; Top.module_command
; Ocaml_merlin.group
; Doc.cmd
]
;;

View file

@ -0,0 +1,3 @@
open Import
val group : unit Cmd.t

View file

@ -0,0 +1,317 @@
open Import
module Selected_context = struct
let arg =
let ctx_name_conv =
let parse ctx_name =
match Context_name.of_string_opt ctx_name with
| None -> Error (`Msg (Printf.sprintf "Invalid context name %S" ctx_name))
| Some ctx_name -> Ok ctx_name
in
let print ppf t = Stdlib.Format.fprintf ppf "%s" (Context_name.to_string t) in
Arg.conv ~docv:"context" (parse, print)
in
Arg.(
value
& opt ctx_name_conv Context_name.default
& info
[ "context" ]
~docv:"CONTEXT"
~doc:"Select the Dune build context that will be used to return information")
;;
end
module Server : sig
val dump : selected_context:Context_name.t -> string -> unit Fiber.t
val dump_dot_merlin : selected_context:Context_name.t -> string -> unit Fiber.t
(** Once started the server will wait for commands on stdin, read the
requested merlin dot file and return its content on stdout. The server
will halt when receiving EOF of a bad csexp. *)
val start : selected_context:Context_name.t -> unit -> unit Fiber.t
end = struct
open Fiber.O
module Merlin_conf = struct
type t = Sexp.t
let make_error msg = Sexp.(List [ List [ Atom "ERROR"; Atom msg ] ])
let to_stdout (t : t) =
Csexp.to_channel stdout t;
flush stdout
;;
end
module Commands = struct
type t =
| File of string
| Halt
| Unknown of string
let read_input in_channel =
match Csexp.input_opt in_channel with
| Ok None -> Halt
| Ok (Some sexp) ->
let open Sexp in
(match sexp with
| Atom "Halt" -> Halt
| List [ Atom "File"; Atom path ] -> File path
| sexp ->
let msg = Printf.sprintf "Bad input: %s" (Sexp.to_string sexp) in
Unknown msg)
| Error err ->
Format.eprintf "Bad input: %s@." err;
Halt
;;
end
(* [make_relative_to_root p] will check that [Path.root] is a prefix of the
absolute path [p] and remove it if that is the case. Under Windows and
Cygwin environment both paths are lowarcased before the comparison *)
let make_relative_to_root p =
let p = Path.to_absolute_filename p in
let prefix = Path.(to_absolute_filename root) in
(if Sys.win32 || Sys.cygwin then String.Caseless.drop_prefix else String.drop_prefix)
~prefix
p
(* After dropping the prefix we need to remove the leading path separator *)
|> Option.map ~f:(fun s -> String.drop s 1)
;;
(* Given a path [p] relative to the workspace root, [get_merlin_files_paths p]
navigates to the [_build] directory and reaches this path from the correct
context. Then it returns the list of available Merlin configurations for
this directory. *)
let get_merlin_files_paths dir =
let merlin_path =
Path.Build.relative dir Dune_rules.Merlin_ident.merlin_folder_name
in
Path.build merlin_path
|> Path.readdir_unsorted
|> Result.value ~default:[]
|> List.sort ~compare:String.compare
|> List.map ~f:(fun f -> Path.Build.relative merlin_path f |> Path.build)
;;
module Merlin = Dune_rules.Merlin
let load_merlin_file file =
(* We search for an appropriate merlin configuration in the current
directory and its parents *)
let rec find_closest path =
match
get_merlin_files_paths path
|> List.find_map ~f:(fun file_path ->
(* FIXME we are racing against the build system writing these
files here *)
match Merlin.Processed.load_file file_path with
| Error msg -> Some (Merlin_conf.make_error msg)
| Ok config -> Merlin.Processed.get config ~file)
with
| Some p -> Some p
| None ->
(match Path.Build.parent path with
| None -> None
| Some dir -> find_closest dir)
in
match find_closest (Path.Build.parent_exn file) with
| Some x -> x
| None ->
Path.Build.drop_build_context_exn file
|> Path.Source.to_string_maybe_quoted
|> Printf.sprintf "No config found for file %s. Try calling 'dune build'."
|> Merlin_conf.make_error
;;
(* [to_local p] makes path [p] relative to the project's root. [p] can be: -
An absolute path - A path relative to [Path.initial_cwd] *)
let to_local file_path =
let error msg = Error msg in
(* This ensure the path is absolute. If not it is prefixed with
[Path.initial_cwd] *)
let abs_file_path = Path.of_filename_relative_to_initial_cwd file_path in
(* Then we make the path relative to [Path.root] (and not
[Path.initial_cwd]) *)
match make_relative_to_root abs_file_path with
| Some path ->
(try
let path = Path.of_string path in
(* If dune ocaml-merlin is called from within the build dir we must
remove the build context *)
Ok (Path.drop_optional_build_context path |> Path.local_part)
with
| User_error.E mess -> User_message.to_string mess |> error)
| None ->
Printf.sprintf
"Path %s is not in dune workspace (%s)."
(String.maybe_quoted file_path)
(String.maybe_quoted @@ Path.(to_absolute_filename Path.root))
|> error
;;
let to_local ~selected_context file =
match to_local file with
| Error s -> Fiber.return (Error s)
| Ok file ->
(match Dune_engine.Context_name.is_default selected_context with
| false ->
Fiber.return
(Ok (Path.Build.append_local (Context_name.build_dir selected_context) file))
| true ->
let+ workspace = Memo.run (Workspace.workspace ()) in
(match workspace.merlin_context with
| None -> Error "no merlin context configured"
| Some context ->
Ok (Path.Build.append_local (Context_name.build_dir context) file)))
;;
let print_merlin_conf ~selected_context file =
to_local ~selected_context file
>>| (function
| Error s -> Merlin_conf.make_error s
| Ok file -> load_merlin_file file)
>>| Merlin_conf.to_stdout
;;
let dump ~selected_context s =
to_local ~selected_context s
>>| function
| Error mess -> Printf.eprintf "%s\n%!" mess
| Ok path -> get_merlin_files_paths path |> List.iter ~f:Merlin.Processed.print_file
;;
let dump_dot_merlin ~selected_context s =
to_local ~selected_context s
>>| function
| Error mess -> Printf.eprintf "%s\n%!" mess
| Ok path ->
let files = get_merlin_files_paths path in
Merlin.Processed.print_generic_dot_merlin files
;;
let start ~selected_context () =
let open Fiber.O in
let rec main () =
match Commands.read_input stdin with
| Halt -> Fiber.return ()
| File path ->
let* () = print_merlin_conf ~selected_context path in
main ()
| Unknown msg ->
Merlin_conf.to_stdout (Merlin_conf.make_error msg);
main ()
in
main ()
;;
end
module Dump_config = struct
let info =
Cmd.info
~doc:
"Print the entire content of the merlin configuration for the given folder in a \
user friendly form. This is for testing and debugging purposes only and should \
not be considered as a stable output."
"dump-config"
;;
let term =
let+ builder = Common.Builder.term
and+ dir = Arg.(value & pos 0 dir "" & info [] ~docv:"PATH")
and+ selected_context = Selected_context.arg in
let common, config =
let builder =
let builder = Common.Builder.forbid_builds builder in
Common.Builder.disable_log_file builder
in
Common.init builder
in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
Server.dump ~selected_context dir)
;;
let command = Cmd.v info term
end
let doc = "Start a merlin configuration server."
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune ocaml-merlin) starts a server that can be queried to get
.merlin information. It is meant to be used by Merlin itself and does not
provide a user-friendly output.|}
; `Blocks Common.help_secs
; Common.footer
]
;;
let start_session_info name = Cmd.info name ~doc ~man
let start_session_term =
let+ builder = Common.Builder.term
and+ selected_context = Selected_context.arg in
let common, config =
let builder =
let builder = Common.Builder.forbid_builds builder in
Common.Builder.disable_log_file builder
in
Common.init builder
in
Scheduler.go_with_rpc_server ~common ~config (Server.start ~selected_context)
;;
let command = Cmd.v (start_session_info "ocaml-merlin") start_session_term
module Dump_dot_merlin = struct
let doc = "Print Merlin configuration"
let man =
[ `S "DESCRIPTION"
; `P
{|$(b,dune ocaml dump-dot-merlin) will attempt to read previously
generated configuration in a source folder, merge them and print
it to the standard output in Merlin configuration syntax. The
output of this command should always be checked and adapted to
the project needs afterward.|}
; Common.footer
]
;;
let info = Cmd.info "dump-dot-merlin" ~doc ~man
let term =
let+ builder = Common.Builder.term
and+ path =
Arg.(
value
& pos 0 (some string) None
& info
[]
~docv:"PATH"
~doc:
"The path to the folder of which the configuration should be printed. \
Defaults to the current directory.")
and+ selected_context = Selected_context.arg in
let common, config =
let builder =
let builder = Common.Builder.forbid_builds builder in
Common.Builder.disable_log_file builder
in
Common.init builder
in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
match path with
| Some s -> Server.dump_dot_merlin ~selected_context s
| None -> Server.dump_dot_merlin ~selected_context ".")
;;
let command = Cmd.v info term
end
let group =
Cmdliner.Cmd.group
(Cmd.info "merlin" ~doc:"Command group related to merlin")
[ Dump_config.command; Cmd.v (start_session_info "start-session") start_session_term ]
;;

View file

@ -0,0 +1,9 @@
open Import
val command : unit Cmd.t
module Dump_dot_merlin : sig
val command : unit Cmd.t
end
val group : unit Cmd.t

View file

@ -0,0 +1,248 @@
open Import
let doc =
"Print a list of toplevel directives for including directories and loading cma files."
;;
let man =
[ `S "DESCRIPTION"
; `P
{|Print a list of toplevel directives for including directories and loading cma files.|}
; `P
{|The output of $(b,dune top) should be evaluated in a toplevel
to make a library available there.|}
; `Blocks Common.help_secs
]
;;
let info = Cmd.info "top" ~doc ~man
let link_deps sctx link =
let open Memo.O in
let* lib_config =
let+ ocaml = Super_context.context sctx |> Context.ocaml in
ocaml.lib_config
in
Memo.parallel_map link ~f:(fun t ->
Dune_rules.Lib_flags.link_deps sctx t Dune_rules.Link_mode.Byte lib_config)
>>| List.concat
;;
let files_to_load_of_requires sctx requires =
let open Memo.O in
let* files = link_deps sctx requires in
let+ () = Memo.parallel_iter files ~f:Build_system.build_file in
List.filter files ~f:(fun p ->
let ext = Path.extension p in
ext = Ocaml.Mode.compiled_lib_ext Byte || ext = Ocaml.Cm_kind.ext Cmo)
;;
let term =
let+ builder = Common.Builder.term
and+ dir = Arg.(value & pos 0 string "" & Arg.info [] ~docv:"DIR")
and+ ctx_name = Common.context_arg ~doc:{|Select context where to build/run utop.|} in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
build_exn (fun () ->
let open Memo.O in
let* setup = setup in
let sctx =
Dune_engine.Context_name.Map.find setup.scontexts ctx_name |> Option.value_exn
in
let context = Super_context.context sctx in
let* libs =
let dir =
let build_dir = Context.build_dir context in
Path.Build.relative build_dir (Common.prefix_target common dir)
in
let* db =
let+ scope = Dune_rules.Scope.DB.find_by_dir dir in
Dune_rules.Scope.libs scope
in
(* TODO why don't we read ppx as well?*)
Dune_rules.Utop.libs_under_dir sctx ~db ~dir:(Path.build dir)
in
let* requires =
Dune_rules.Resolve.Memo.read_memo (Dune_rules.Lib.closure ~linking:true libs)
in
let* lib_config =
let+ ocaml = Context.ocaml context in
ocaml.lib_config
in
let include_paths =
Dune_rules.Lib_flags.L.toplevel_include_paths requires lib_config
in
let+ files_to_load = files_to_load_of_requires sctx requires in
Dune_rules.Toplevel.print_toplevel_init_file
{ include_paths; files_to_load; uses = []; pp = None; ppx = None; code = [] }))
;;
let command = Cmd.v info term
module Module = struct
let doc = "Print a list of toplevel directives for loading a module into the topevel."
let man =
[ `S "DESCRIPTION"
; `P doc
; `P
"The module's source is evaluated in the toplevel without being sealed by the \
mli."
; `P
{|The output of $(b,dune top) should be evaluated in a toplevel
to make the module available there.|}
; `Blocks Common.help_secs
]
;;
let info = Cmd.info "top-module" ~doc ~man
let module_directives sctx mod_ =
let ctx = Super_context.context sctx in
let src = Path.Build.append_source (Context.build_dir ctx) mod_ in
let dir = Path.Build.parent_exn src in
let filename = Path.Build.basename src in
if Filename.extension filename = ""
then User_error.raise [ Pp.text "file is missing an extension" ];
let open Memo.O in
let module_name =
let name = Filename.remove_extension filename in
Dune_rules.Module_name.of_string_user_error (Loc.none, name) |> User_error.ok_exn
in
let* expander = Super_context.expander sctx ~dir in
let* top_module_info = Dune_rules.Top_module.find_module sctx mod_ in
match top_module_info with
| None -> User_error.raise [ Pp.text "no module found" ]
| Some (_, _, _, Melange _) ->
User_error.raise [ Pp.text "Modules belonging to `melange.emit' are not supported" ]
| Some (module_, cctx, merlin, _) ->
let module Compilation_context = Dune_rules.Compilation_context in
let module Obj_dir = Dune_rules.Obj_dir in
let module Top_module = Dune_rules.Top_module in
let* requires =
let* requires = Compilation_context.requires_link cctx in
Dune_rules.Resolve.read_memo requires
in
let private_obj_dir = Top_module.private_obj_dir ctx mod_ in
let include_paths =
let libs =
let lib_config = (Compilation_context.ocaml cctx).lib_config in
Dune_rules.Lib_flags.L.toplevel_include_paths requires lib_config
in
Path.Set.add libs (Path.build (Obj_dir.byte_dir private_obj_dir))
in
let files_to_load () =
let+ libs, modules =
Memo.fork_and_join
(fun () -> files_to_load_of_requires sctx requires)
(fun () ->
let cmis () =
let glob =
Dune_engine.File_selector.of_glob
~dir:(Path.build (Obj_dir.byte_dir private_obj_dir))
(Dune_lang.Glob.of_string_exn Loc.none "*.cmi")
in
let* files = Build_system.eval_pred glob in
Memo.parallel_iter
(Filename_set.to_list files)
~f:Build_system.build_file
in
let cmos () =
let obj_dir = Compilation_context.obj_dir cctx in
let dep_graph = (Compilation_context.dep_graphs cctx).impl in
let* modules =
let graph =
Dune_rules.Dep_graph.top_closed_implementations dep_graph [ module_ ]
in
let+ modules, _ = Action_builder.evaluate_and_collect_facts graph in
modules
in
let cmos =
let module Module = Dune_rules.Module in
let module Module_name = Dune_rules.Module_name in
let module_obj_name = Module.obj_name module_ in
List.filter_map modules ~f:(fun m ->
let obj_dir =
if Module_name.Unique.equal module_obj_name (Module.obj_name m)
then private_obj_dir
else obj_dir
in
Obj_dir.Module.cm_file obj_dir m ~kind:(Ocaml Cmo)
|> Option.map ~f:Path.build)
in
let+ (_ : Dep.Facts.t) =
Build_system.build_deps (Dep.Set.of_files cmos)
in
cmos
in
Memo.fork_and_join_unit cmis cmos)
in
libs @ modules
in
let pps () =
let module Merlin = Dune_rules.Merlin in
let pps = Merlin.pp_config merlin ctx ~expander in
let+ pps, _ = Action_builder.evaluate_and_collect_facts pps in
let pp = Dune_rules.Module_name.Per_item.get pps module_name in
match pp with
| None -> None, None
| Some pp_flags ->
let args = Merlin.Processed.pp_args pp_flags in
(match Merlin.Processed.pp_kind pp_flags with
| Pp -> Some args, None
| Ppx -> None, Some args)
in
let+ (pp, ppx), files_to_load = Memo.fork_and_join pps files_to_load in
let code =
let modules = Dune_rules.Compilation_context.modules cctx in
let opens_ = Dune_rules.Modules.With_vlib.local_open modules module_ in
List.map opens_ ~f:(fun name ->
sprintf "open %s" (Dune_rules.Module_name.to_string name))
in
{ Dune_rules.Toplevel.files_to_load; pp; ppx; include_paths; uses = []; code }
;;
let term =
let+ builder = Common.Builder.term
and+ module_path =
Arg.(
required
& pos 0 (some string) None
& Arg.info [] ~docv:"MODULE" ~doc:"Path to an OCaml module.")
and+ ctx_name = Common.context_arg ~doc:{|Select context where to build/run utop.|} in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
build_exn (fun () ->
let open Memo.O in
let* setup = setup in
let sctx =
Dune_engine.Context_name.Map.find setup.scontexts ctx_name |> Option.value_exn
in
let+ directives =
let module_path =
if Filename.is_relative module_path
then Path.Local.of_string module_path
else (
let root =
(Common.root common).dir
|> Path.of_string
|> Path.to_absolute_filename
|> Path.of_string
in
match Path.drop_prefix ~prefix:root (Path.of_string module_path) with
| Some module_path -> module_path
| None ->
User_error.raise
[ Pp.text "Module path not a descendent of workspace root." ])
in
module_directives sctx (Path.Source.of_local module_path)
in
Dune_rules.Toplevel.print_toplevel_init_file directives))
;;
end
let module_command = Cmd.v Module.info Module.term

View file

@ -0,0 +1,4 @@
open Import
val command : unit Cmd.t
val module_command : unit Cmd.t

View file

@ -0,0 +1,107 @@
open Import
module Utop = Dune_rules.Utop
let doc = "Load library in utop."
let man =
[ `S "DESCRIPTION"
; `P {|$(b,dune utop DIR) build and run utop toplevel with libraries defined in DIR|}
; `Blocks Common.help_secs
]
;;
let info = Cmd.info "utop" ~doc ~man
let lock_utop_if_dev_tool_enabled () =
match Lazy.force Lock_dev_tool.is_enabled with
| false -> Memo.return ()
| true -> Lock_dev_tool.lock_dev_tool Utop
;;
let term =
let+ builder = Common.Builder.term
and+ dir = Arg.(value & pos 0 string "" & Arg.info [] ~docv:"DIR")
and+ ctx_name = Common.context_arg ~doc:{|Select context where to build/run utop.|}
and+ args = Arg.(value & pos_right 0 string [] (Arg.info [] ~docv:"ARGS")) in
let common, config = Common.init builder in
let dir = Common.prefix_target common dir in
if not (Path.is_directory (Path.of_string dir))
then User_error.raise [ Pp.textf "cannot find directory: %s" (String.maybe_quoted dir) ];
let env, utop_path =
Scheduler.go_with_rpc_server ~common ~config (fun () ->
let open Fiber.O in
let* setup = Import.Main.setup () in
build_exn (fun () ->
let open Memo.O in
let* setup = setup in
let context = Import.Main.find_context_exn setup ~name:ctx_name in
let utop_target_path filename =
Path.build
(Path.Build.relative
(Context.build_dir context)
(Filename.concat dir filename))
in
let utop_exe = utop_target_path Utop.utop_exe in
let utop_findlib_conf = utop_target_path Utop.utop_findlib_conf in
let* () =
(* Calling [Build_system.file_exists] has the side effect of checking
and memoizing whether or not the utop dev tool lockdir exists.
thus if we generate the lockdir any later than this point, dune
will not observe the fact that it now exists. *)
lock_utop_if_dev_tool_enabled ()
in
Build_system.file_exists utop_exe
>>= function
| false ->
User_error.raise
[ Pp.textf "no library is defined in %s" (String.maybe_quoted dir) ]
| true ->
let* () = Build_system.build_file utop_exe in
let* utop_dev_tool_lock_dir_exists =
Memo.Lazy.force Utop.utop_dev_tool_lock_dir_exists
in
let* () =
if utop_dev_tool_lock_dir_exists
then
(* Generate the custom findlib.conf file needed when utop is run
as a dev tool. *)
Build_system.build_file utop_findlib_conf
else Memo.return ()
in
let sctx = Import.Main.find_scontext_exn setup ~name:ctx_name in
let* requires =
let dir = Path.Build.relative (Context.build_dir context) dir in
Utop.requires_under_dir sctx ~dir
in
let+ requires = Resolve.read_memo requires
and+ lib_config =
let+ ocaml = Context.ocaml context in
ocaml.lib_config
and+ env = Super_context.context_env sctx in
let env =
Dune_rules.Lib_flags.L.toplevel_ld_paths requires lib_config
|> Path.Set.fold
~f:(fun dir env ->
Env_path.cons ~var:Ocaml.Env.caml_ld_library_path env ~dir)
~init:env
in
let env =
if utop_dev_tool_lock_dir_exists
then
(* If there's a utop lockdir then dune will have built utop as a
dev tool. In order for it to run correctly dune needed to
generate a custom findlib.conf that contains the locations of
all of utop's dependencies within the project's _build
directory. Setting this environment variable causes the custom
findlib.conf file to be used instead of the default
findlib.conf. *)
Env.add env ~var:"OCAMLFIND_CONF" ~value:(Path.to_string utop_findlib_conf)
else env
in
env, Path.to_string utop_exe))
in
Hooks.End_of_build.run ();
restore_cwd_and_execve (Common.root common) utop_path args env
;;
let command = Cmd.v info term

View file

@ -0,0 +1 @@
val command : unit Cmdliner.Cmd.t

View file

@ -0,0 +1,380 @@
open Dune_config
open Import
open Pkg_common
module Package_version = Dune_pkg.Package_version
module Opam_repo = Dune_pkg.Opam_repo
module Lock_dir = Dune_pkg.Lock_dir
module Pin_stanza = Dune_lang.Pin_stanza
module Pin = Dune_pkg.Pin
module Progress_indicator = struct
module Per_lockdir = struct
module State = struct
module Repository = Dune_pkg.Pkg_workspace.Repository
type t =
| Updating_repos of Repository.Name.t list
| Solving
let pp = function
| Updating_repos repo_names ->
Pp.textf
"Updating package repos %s..."
(List.map repo_names ~f:(fun repo_name ->
Repository.Name.to_string repo_name |> String.quoted)
|> String.enumerate_and)
| Solving -> Pp.text "Solving..."
;;
end
type t =
{ lockdir_path : Path.Source.t
; state : State.t option ref
}
let create lockdir_path = { lockdir_path; state = ref None }
end
(* The progress indicator for the entire lock operation, which may
involve generating multiple lockdirs *)
type t = Per_lockdir.t list
let pp (t : t) =
(* Only display the first non-done lockdir state, since the status
line can only consist of a single line. *)
List.find_map t ~f:(fun { Per_lockdir.lockdir_path; state } ->
Option.map !state ~f:(fun state ->
Pp.concat
[ Pp.textf "Locking %s: " (Path.Source.to_string_maybe_quoted lockdir_path)
; Per_lockdir.State.pp state
]))
|> Option.value ~default:Pp.nop
;;
let add_overlay (t : t) = Console.Status_line.add_overlay (Live (fun () -> pp t))
end
let project_and_package_pins project =
let dir = Dune_project.root project in
let pins = Dune_project.pins project in
let packages = Dune_project.packages project in
Pin.DB.add_opam_pins (Pin.DB.of_stanza ~dir pins) packages
;;
(* For recursive pins, we must traverse the pinned sources. The [project_pins]
are the initial pins that we have in our project. *)
let resolve_project_pins project_pins =
let scan_project ~read ~files =
let read file = Memo.of_reproducible_fiber (read file) in
let open Memo.O in
(* Opam files may never contain recursive pins, so don't both reading them *)
Dune_project.gen_load
~read
~files
~dir:Path.Source.root
~infer_from_opam_files:false
~load_opam_file_with_contents:Dune_pkg.Opam_file.load_opam_file_with_contents
>>| Option.map ~f:(fun project ->
let packages = Dune_project.packages project in
let pins = project_and_package_pins project in
pins, packages)
|> Memo.run
in
Pin.resolve project_pins ~scan_project
;;
let solve_multiple_platforms
base_solver_env
version_preference
repos
~pins
~local_packages
~constraints
~selected_depopts
~solve_for_platforms
~portable_lock_dir
=
let open Fiber.O in
let solve_for_env env =
Dune_pkg.Opam_solver.solve_lock_dir
env
version_preference
repos
~pins
~local_packages
~constraints
~selected_depopts
~portable_lock_dir
in
let portable_solver_env =
Dune_pkg.Solver_env.unset_multi
base_solver_env
Dune_lang.Package_variable_name.platform_specific
in
let+ results =
Fiber.parallel_map solve_for_platforms ~f:(fun platform_env ->
let solver_env = Dune_pkg.Solver_env.extend portable_solver_env platform_env in
solve_for_env solver_env)
in
let solver_results, errors =
List.partition_map results ~f:(function
| Ok result -> Left result
| Error (`Diagnostic_message message) -> Right message)
in
match solver_results, errors with
| [], [] -> Code_error.raise "Solver did not run for any platforms." []
| [], errors -> `All_error errors
| x :: xs, errors ->
let merged_solver_result =
List.fold_left xs ~init:x ~f:Dune_pkg.Opam_solver.Solver_result.merge
in
if List.is_empty errors
then `All_ok merged_solver_result
else `Partial (merged_solver_result, errors)
;;
let solve_lock_dir
workspace
~local_packages
~project_pins
~print_perf_stats
~portable_lock_dir
version_preference
solver_env_from_current_system
lock_dir_path
progress_state
=
let open Fiber.O in
let lock_dir = Workspace.find_lock_dir workspace lock_dir_path in
let project_pins, solve_for_platforms =
match lock_dir with
| None -> project_pins, Dune_pkg.Solver_env.popular_platform_envs
| Some lock_dir ->
let workspace =
Pin.DB.Workspace.of_stanza workspace.pins
|> Pin.DB.Workspace.extract ~names:lock_dir.pins
in
Pin.DB.combine_exn workspace project_pins, lock_dir.solve_for_platforms
in
let solver_env_from_context =
Option.bind lock_dir ~f:(fun lock_dir -> lock_dir.solver_env)
in
let solver_env =
solver_env
~solver_env_from_context
~solver_env_from_current_system
~unset_solver_vars_from_context:
(unset_solver_vars_of_workspace workspace ~lock_dir_path)
in
let solve_for_platforms =
match portable_lock_dir with
| true ->
(match solver_env_from_context with
| Some solver_env_from_context ->
List.map solve_for_platforms ~f:(fun platform_env ->
Dune_pkg.Solver_env.extend solver_env_from_context platform_env)
| None -> solve_for_platforms)
| false -> [ solver_env ]
in
let time_start = Unix.gettimeofday () in
let* repos =
let repo_map = repositories_of_workspace workspace in
let repo_names =
Dune_pkg.Pkg_workspace.Repository.Name.Map.keys repo_map
|> List.sort ~compare:Dune_pkg.Pkg_workspace.Repository.Name.compare
in
progress_state
:= Some (Progress_indicator.Per_lockdir.State.Updating_repos repo_names);
get_repos repo_map ~repositories:(repositories_of_lock_dir workspace ~lock_dir_path)
in
let* pins = resolve_project_pins project_pins in
let time_solve_start = Unix.gettimeofday () in
progress_state := Some Progress_indicator.Per_lockdir.State.Solving;
let* result =
solve_multiple_platforms
solver_env
(Pkg_common.Version_preference.choose
~from_arg:version_preference
~from_context:
(Option.bind lock_dir ~f:(fun lock_dir -> lock_dir.version_preference)))
repos
~pins
~local_packages:
(Package_name.Map.map local_packages ~f:Dune_pkg.Local_package.for_solver)
~constraints:(constraints_of_workspace workspace ~lock_dir_path)
~selected_depopts:(depopts_of_workspace workspace ~lock_dir_path)
~solve_for_platforms
~portable_lock_dir
in
let solver_result =
match result with
| `All_error messages -> Error messages
| `All_ok solver_result -> Ok (solver_result, [])
| `Partial (solver_result, errors) ->
Log.info errors;
Ok
( solver_result
, [ Pp.nop
; Pp.text
"No solution was found for some platforms. See the log or run with \
--verbose for more details."
|> Pp.tag User_message.Style.Warning
] )
in
match solver_result with
| Error messages -> Fiber.return (Error (lock_dir_path, messages))
| Ok (solver_result, maybe_unsolved_platforms_message) ->
let { Dune_pkg.Opam_solver.Solver_result.lock_dir
; files
; pinned_packages
; num_expanded_packages
}
=
solver_result
in
let time_end = Unix.gettimeofday () in
let maybe_perf_stats =
if print_perf_stats
then
[ Pp.nop
; Pp.textf "Expanded packages: %d" num_expanded_packages
; Pp.textf "Updated repos in: %.2fs" (time_solve_start -. time_start)
; Pp.textf "Solved dependencies in: %.2fs" (time_end -. time_solve_start)
]
else []
in
let summary_message =
User_message.make
((Pp.tag
User_message.Style.Success
(Pp.textf
"Solution for %s:"
(Path.Source.to_string_maybe_quoted lock_dir_path))
:: (match Lock_dir.Packages.to_pkg_list lock_dir.packages with
| [] ->
Pp.tag User_message.Style.Warning @@ Pp.text "(no dependencies to lock)"
| packages -> pp_packages packages)
:: maybe_perf_stats)
@ maybe_unsolved_platforms_message)
in
progress_state := None;
let+ lock_dir = Lock_dir.compute_missing_checksums ~pinned_packages lock_dir in
Ok
( Lock_dir.Write_disk.prepare ~portable_lock_dir ~lock_dir_path ~files lock_dir
, summary_message )
;;
let solve
workspace
~local_packages
~project_pins
~solver_env_from_current_system
~version_preference
~lock_dirs
~print_perf_stats
~portable_lock_dir
=
let open Fiber.O in
(* a list of thunks that will perform all the file IO side
effects after performing validation so that if materializing any
lockdir would fail then no side effect takes place. *)
(let+ errors, solutions =
let progress_indicator =
List.map lock_dirs ~f:Progress_indicator.Per_lockdir.create
in
let overlay = Progress_indicator.add_overlay progress_indicator in
let+ result =
Fiber.finalize
~finally:(fun () ->
Console.Status_line.remove_overlay overlay;
Fiber.return ())
(fun () ->
Fiber.parallel_map progress_indicator ~f:(fun { lockdir_path; state } ->
solve_lock_dir
workspace
~local_packages
~project_pins
~print_perf_stats
~portable_lock_dir
version_preference
solver_env_from_current_system
lockdir_path
state))
in
List.partition_map result ~f:Result.to_either
in
match errors with
| [] -> Ok solutions
| _ -> Error errors)
>>| function
| Error errors ->
User_error.raise
([ Pp.text "Unable to solve dependencies for the following lock directories:" ]
@ List.concat_map errors ~f:(fun (path, messages) ->
[ Pp.textf "Lock directory %s:" (Path.Source.to_string_maybe_quoted path)
; Pp.hovbox (Pp.concat ~sep:Pp.newline messages)
]))
| Ok write_disks_with_summaries ->
let write_disk_list, summary_messages = List.split write_disks_with_summaries in
List.iter summary_messages ~f:Console.print_user_message;
(* All the file IO side effects happen here: *)
List.iter write_disk_list ~f:Lock_dir.Write_disk.commit
;;
let project_pins =
let open Memo.O in
Dune_rules.Dune_load.projects ()
>>| List.fold_left ~init:Pin.DB.empty ~f:(fun acc project ->
let pins = project_and_package_pins project in
Pin.DB.combine_exn acc pins)
;;
let lock ~version_preference ~lock_dirs_arg ~print_perf_stats ~portable_lock_dir =
let open Fiber.O in
let* solver_env_from_current_system =
poll_solver_env_from_current_system () >>| Option.some
and* workspace, local_packages, project_pins =
Memo.run
@@
let open Memo.O in
let+ workspace = Workspace.workspace ()
and+ local_packages = find_local_packages
and+ project_pins = project_pins in
workspace, local_packages, project_pins
in
let lock_dirs =
Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dirs_arg workspace
in
solve
workspace
~local_packages
~project_pins
~solver_env_from_current_system
~version_preference
~lock_dirs
~print_perf_stats
~portable_lock_dir
;;
let term =
let+ builder = Common.Builder.term
and+ version_preference = Version_preference.term
and+ lock_dirs_arg = Pkg_common.Lock_dirs_arg.term
and+ print_perf_stats = Arg.(value & flag & info [ "print-perf-stats" ]) in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
let portable_lock_dir =
match Config.get Dune_rules.Compile_time.portable_lock_dir with
| `Enabled -> true
| `Disabled -> false
in
lock ~version_preference ~lock_dirs_arg ~print_perf_stats ~portable_lock_dir)
;;
let info =
let doc = "Create a lockfile" in
Cmd.info "lock" ~doc
;;
let command = Cmd.v info term

View file

@ -0,0 +1,15 @@
open Import
val solve
: Workspace.t
-> local_packages:Dune_pkg.Local_package.t Package_name.Map.t
-> project_pins:Dune_pkg.Pin.DB.t
-> solver_env_from_current_system:Dune_pkg.Solver_env.t option
-> version_preference:Dune_pkg.Version_preference.t option
-> lock_dirs:Path.Source.t list
-> print_perf_stats:bool
-> portable_lock_dir:bool
-> unit Fiber.t
(** Command to create lock directory *)
val command : unit Cmd.t

View file

@ -0,0 +1,103 @@
open Import
open Pkg_common
let find_outdated_packages ~transitive ~lock_dirs_arg () =
let open Fiber.O in
let+ pps, not_founds =
let* workspace = Memo.run (Workspace.workspace ()) in
Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dirs_arg workspace
|> Fiber.parallel_map ~f:(fun lock_dir_path ->
(* updating makes sense when checking for outdated packages *)
let* repos =
get_repos
(repositories_of_workspace workspace)
~repositories:(repositories_of_lock_dir workspace ~lock_dir_path)
and+ local_packages = Memo.run find_local_packages
and+ platform = solver_env_from_system_and_context ~lock_dir_path in
let lock_dir = Dune_pkg.Lock_dir.read_disk_exn lock_dir_path in
let packages =
Dune_pkg.Lock_dir.Packages.pkgs_on_platform_by_name lock_dir.packages ~platform
in
let+ results = Dune_pkg.Outdated.find ~repos ~local_packages packages in
( Dune_pkg.Outdated.pp ~transitive ~lock_dir_path results
, ( Dune_pkg.Outdated.packages_that_were_not_found results
|> Package_name.Set.of_list
|> Package_name.Set.to_list
, lock_dir_path
, repos ) ))
>>| List.split
in
(match pps with
| [ _ ] -> Console.print pps
| _ -> Console.print [ Pp.enumerate ~f:Fun.id pps ]);
let error_messages =
List.filter_map not_founds ~f:(function
| [], _, _ -> None
| packages, lock_dir_path, repos ->
Pp.concat
~sep:Pp.space
[ Pp.textf
"When checking %s, the following packages:"
(Path.Source.to_string_maybe_quoted lock_dir_path)
|> Pp.hovbox
; Pp.concat
~sep:Pp.space
[ Pp.enumerate packages ~f:(fun name ->
Dune_lang.Package_name.to_string name |> Pp.verbatim)
; Pp.text "were not found in the following opam repositories:" |> Pp.hovbox
; Pp.enumerate repos ~f:(fun repo ->
(* CR-rgrinberg: why are we outputting [Dyn.t] in error
messages? *)
Dune_pkg.Opam_repo.serializable repo
|> Dyn.option Dune_pkg.Opam_repo.Serializable.to_dyn
|> Dyn.pp)
]
|> Pp.vbox
]
|> Pp.hovbox
|> Option.some)
in
match error_messages with
| [] -> ()
| error_messages ->
User_error.raise (Pp.text "Some packages could not be found." :: error_messages)
;;
let term =
let+ builder = Common.Builder.term
and+ transitive =
Arg.(
value
& flag
& info
[ "transitive" ]
~doc:"Check for outdated packages in transitive dependencies")
and+ lock_dirs_arg = Pkg_common.Lock_dirs_arg.term in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config
@@ find_outdated_packages ~transitive ~lock_dirs_arg
;;
let info =
let doc = "Check for outdated packages" in
let man =
[ `S "DESCRIPTION"
; `P
"List packages in from lock directory that have newer versions available. By \
default, only direct dependencies are checked. The $(b,--transitive) flag can \
be used to check transitive dependencies as well."
; `P "For example:"
; `Pre " \\$ dune pkg outdated"
; `Noblank
; `Pre " 1/2 packages in dune.lock are outdated."
; `Noblank
; `Pre " - ocaml 4.14.1 < 5.1.0"
; `Noblank
; `Pre " - dune 3.7.1 < 3.11.0"
]
in
Cmd.info "outdated" ~doc ~man
;;
let command = Cmd.v info term

View file

@ -0,0 +1,4 @@
open Import
(** Command to print outdated packages *)
val command : unit Cmd.t

View file

@ -0,0 +1,28 @@
open Import
let man =
[ `S "DESCRIPTION"
; `P {|Commands for OCaml package management|}
; `Blocks Common.help_secs
]
;;
let subcommands =
[ Lock.command
; Print_solver_env.command
; Outdated.command
; Validate_lock_dir.command
; Pkg_enabled.command
]
;;
let info name =
let doc = "Experimental package management" in
Cmd.info name ~doc ~man
;;
let group = Cmd.group (info "pkg") subcommands
module Alias = struct
let group = Cmd.group (info "package") subcommands
end

View file

@ -0,0 +1,7 @@
open Import
val group : unit Cmd.t
module Alias : sig
val group : unit Cmd.t
end

View file

@ -0,0 +1,228 @@
open Import
module Lock_dir = Dune_pkg.Lock_dir
module Solver_env = Dune_pkg.Solver_env
module Package_variable_name = Dune_lang.Package_variable_name
module Variable_value = Dune_pkg.Variable_value
let solver_env
~solver_env_from_current_system
~solver_env_from_context
~unset_solver_vars_from_context
=
let solver_env =
[ solver_env_from_current_system; solver_env_from_context ]
|> List.filter_opt
|> List.fold_left ~init:Solver_env.with_defaults ~f:Solver_env.extend
in
match unset_solver_vars_from_context with
| None -> solver_env
| Some unset_solver_vars -> Solver_env.unset_multi solver_env unset_solver_vars
;;
let poll_solver_env_from_current_system () =
Dune_pkg.Sys_poll.make ~path:(Env_path.path Stdune.Env.initial)
|> Dune_pkg.Sys_poll.solver_env_from_current_system
;;
let get_lock_dir_from_context ~lock_dir_path =
Memo.run
@@
let open Memo.O in
let+ workspace = Workspace.workspace () in
Workspace.find_lock_dir workspace lock_dir_path
;;
let get_solver_env_from_context ~lock_dir_path =
let open Fiber.O in
let+ lock_dir = get_lock_dir_from_context ~lock_dir_path in
Option.bind lock_dir ~f:(fun lock_dir -> lock_dir.solver_env)
;;
let get_unset_solver_vars_from_context ~lock_dir_path =
let open Fiber.O in
let+ lock_dir = get_lock_dir_from_context ~lock_dir_path in
Option.bind lock_dir ~f:(fun lock_dir -> lock_dir.unset_solver_vars)
;;
let solver_env_from_system_and_context ~lock_dir_path =
let open Fiber.O in
let+ solver_env_from_current_system =
poll_solver_env_from_current_system () >>| Option.some
and+ solver_env_from_context = get_solver_env_from_context ~lock_dir_path
and+ unset_solver_vars_from_context =
get_unset_solver_vars_from_context ~lock_dir_path
in
solver_env
~solver_env_from_current_system
~solver_env_from_context
~unset_solver_vars_from_context
;;
module Version_preference = struct
include Dune_pkg.Version_preference
let term =
let all_strings = List.map all_by_string ~f:fst in
let doc =
sprintf
"Whether to prefer the newest compatible version of a package or the oldest \
compatible version of packages while solving dependencies. This overrides any \
setting in the current workspace. The default is %s."
(to_string default)
in
let docv = String.concat ~sep:"|" all_strings |> sprintf "(%s)" in
Arg.(
value
& opt (some (enum all_by_string)) None
& info [ "version-preference" ] ~doc ~docv)
;;
let choose ~from_arg ~from_context =
match from_arg, from_context with
| Some from_arg, _ -> from_arg
| None, Some from_context -> from_context
| None, None -> default
;;
end
let repositories_of_workspace (workspace : Workspace.t) =
List.map workspace.repos ~f:(fun repo ->
Dune_pkg.Pkg_workspace.Repository.name repo, repo)
|> Dune_pkg.Pkg_workspace.Repository.Name.Map.of_list_exn
;;
let constraints_of_workspace (workspace : Workspace.t) ~lock_dir_path =
match Workspace.find_lock_dir workspace lock_dir_path with
| None -> []
| Some lock_dir -> lock_dir.constraints
;;
let depopts_of_workspace (workspace : Workspace.t) ~lock_dir_path =
match Workspace.find_lock_dir workspace lock_dir_path with
| None -> []
| Some lock_dir -> lock_dir.depopts |> List.map ~f:snd
;;
let repositories_of_lock_dir workspace ~lock_dir_path =
match Workspace.find_lock_dir workspace lock_dir_path with
| Some lock_dir -> lock_dir.repositories
| None ->
List.map workspace.repos ~f:(fun repo ->
let name = Dune_pkg.Pkg_workspace.Repository.name repo in
let loc = Loc.none in
loc, name)
;;
let unset_solver_vars_of_workspace workspace ~lock_dir_path =
let open Option.O in
let* lock_dir = Workspace.find_lock_dir workspace lock_dir_path in
lock_dir.unset_solver_vars
;;
let get_repos repos ~repositories =
let module Repository = Dune_pkg.Pkg_workspace.Repository in
repositories
|> Fiber.parallel_map ~f:(fun (loc, name) ->
match Repository.Name.Map.find repos name with
| None ->
User_error.raise
~loc
[ Pp.textf "Repository '%s' is not a known repository"
@@ Repository.Name.to_string name
]
| Some repo ->
let loc, opam_url = Repository.opam_url repo in
let module Opam_repo = Dune_pkg.Opam_repo in
(match Dune_pkg.OpamUrl.classify opam_url loc with
| `Git -> Opam_repo.of_git_repo loc opam_url
| `Path path -> Fiber.return @@ Opam_repo.of_opam_repo_dir_path loc path
| `Archive ->
User_error.raise
~loc
[ Pp.textf "Repositories stored in archives (%s) are currently unsupported"
@@ OpamUrl.to_string opam_url
]))
;;
let find_local_packages =
let open Memo.O in
Dune_rules.Dune_load.packages ()
>>| Package.Name.Map.map ~f:Dune_pkg.Local_package.of_package
;;
let pp_package { Lock_dir.Pkg.info = { Lock_dir.Pkg_info.name; version; avoid; _ }; _ } =
let warn =
if avoid
then Pp.tag User_message.Style.Warning (Pp.text " (this version should be avoided)")
else Pp.nop
in
let open Pp.O in
Pp.verbatim
(Package_name.to_string name ^ "." ^ Dune_pkg.Package_version.to_string version)
++ warn
;;
let pp_packages packages = Pp.enumerate packages ~f:pp_package
module Lock_dirs_arg = struct
type t =
| All
| Selected of Path.Source.t list
let all = All
let term =
Common.one_of
(let+ arg =
Arg.(
value
& pos_all string []
& info
[]
~docv:"LOCKDIRS"
~doc:
"Lock directories to check for outdated packages. Defaults to dune.lock.")
in
Selected (List.map arg ~f:Path.Source.of_string))
(let+ _all =
Arg.(
value
& flag
& info
[ "all" ]
~doc:"Check all lock directories in the workspace for outdated packages.")
in
All)
;;
let lock_dirs_of_workspace t (workspace : Workspace.t) =
let workspace_lock_dirs =
Lock_dir.default_path
:: List.map workspace.lock_dirs ~f:(fun (lock_dir : Workspace.Lock_dir.t) ->
lock_dir.path)
|> Path.Source.Set.of_list
|> Path.Source.Set.to_list
in
match t with
| All -> workspace_lock_dirs
| Selected [] -> [ Lock_dir.default_path ]
| Selected chosen_lock_dirs ->
let workspace_lock_dirs_set = Path.Source.Set.of_list workspace_lock_dirs in
let chosen_lock_dirs_set = Path.Source.Set.of_list chosen_lock_dirs in
if Path.Source.Set.is_subset chosen_lock_dirs_set ~of_:workspace_lock_dirs_set
then chosen_lock_dirs
else (
let unknown_lock_dirs =
Path.Source.Set.diff chosen_lock_dirs_set workspace_lock_dirs_set
|> Path.Source.Set.to_list
in
let f x = Path.pp (Path.source x) in
User_error.raise
[ Pp.text
"The following directories are not lock directories in this workspace:"
; Pp.enumerate unknown_lock_dirs ~f
; Pp.text "This workspace contains the following lock directories:"
; Pp.enumerate workspace_lock_dirs ~f
])
;;
end

View file

@ -0,0 +1,92 @@
open Import
(** Create a [Dune_pkg.Solver_env.t] by combining variables taken from the
current system and variables taken from the current context, with priority
being given to the latter. Some variables are initialized to default values
(which can be overridden by the arguments to this function):
- "with-doc" is set to "false"
- "opam-version" is set to the version of opam vendored in dune *)
val solver_env
: solver_env_from_current_system:Dune_pkg.Solver_env.t option
-> solver_env_from_context:Dune_pkg.Solver_env.t option
-> unset_solver_vars_from_context:Dune_lang.Package_variable_name.Set.t option
-> Dune_pkg.Solver_env.t
val poll_solver_env_from_current_system : unit -> Dune_pkg.Solver_env.t Fiber.t
val solver_env_from_system_and_context
: lock_dir_path:Path.Source.t
-> Dune_pkg.Solver_env.t Fiber.t
module Version_preference : sig
type t := Dune_pkg.Version_preference.t
val term : Dune_pkg.Version_preference.t option Term.t
val choose : from_arg:t option -> from_context:t option -> t
end
val unset_solver_vars_of_workspace
: Workspace.t
-> lock_dir_path:Path.Source.t
-> Dune_lang.Package_variable_name.Set.t option
val repositories_of_workspace
: Workspace.t
-> Dune_pkg.Pkg_workspace.Repository.t Dune_pkg.Pkg_workspace.Repository.Name.Map.t
val repositories_of_lock_dir
: Workspace.t
-> lock_dir_path:Path.Source.t
-> (Loc.t * Dune_pkg.Pkg_workspace.Repository.Name.t) list
val constraints_of_workspace
: Workspace.t
-> lock_dir_path:Path.Source.t
-> Dune_lang.Package_dependency.t list
val depopts_of_workspace
: Workspace.t
-> lock_dir_path:Path.Source.t
-> Package_name.t list
val get_repos
: Dune_pkg.Pkg_workspace.Repository.t Dune_pkg.Pkg_workspace.Repository.Name.Map.t
-> repositories:(Loc.t * Dune_pkg.Pkg_workspace.Repository.Name.t) list
-> Dune_pkg.Opam_repo.t list Fiber.t
val find_local_packages : Dune_pkg.Local_package.t Package_name.Map.t Memo.t
module Lock_dirs_arg : sig
(** [Lock_dirs_arg.t] is the type of lock directory arguments. This can be
created with [Lock_dirs_arg.term] and used with
[Lock_dirs_arg.lock_dirs_of_workspace]. *)
type t
(** Select all lockdirs *)
val all : t
(** [Lock_dirs_arg.term] is a command-line argument that can be used to
specify the lock directories to consider. This can then be passed to
[Lock_dirs_arg.lock_dirs_of_workspace].
There are two mutually exclusive cases:
- The user passed a list of lick directories as positional
arguments.contents
- The user passed the ["--all"] flag, in which case all lock directories
of the workspace are considered. *)
val term : t Term.t
(** [Lock_dirs_arg.lock_dirs_of_workspace t workspace] returns the list of
lock directories that should be considered for various operations.
The [workspace] argument is used to determine the list of all lock lock
directories.
A user error is raised if the list of positional arguments used when
creating [t] is not a subset of the lock directories of the workspace. *)
val lock_dirs_of_workspace : t -> Workspace.t -> Path.Source.t list
end
(** [pp_packages lock_dir] returns a list of pretty-printed packages occurring in
[lock_dir]. *)
val pp_packages : Dune_pkg.Lock_dir.Pkg.t list -> User_message.Style.t Pp.t

View file

@ -0,0 +1,36 @@
open Import
let term =
let+ builder = Common.Builder.term in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config (fun () ->
Memo.run
@@
let open Memo.O in
let+ workspace = Workspace.workspace () in
let lock_dir_paths =
Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace
Pkg_common.Lock_dirs_arg.all
workspace
in
let any_lockdir_exists =
List.exists lock_dir_paths ~f:(fun lock_dir_path ->
Path.exists (Path.source lock_dir_path))
in
(* CR-Leonidas-from-XIV: change this logic when we stop detecting lock
directories in the source tree *)
let enabled = any_lockdir_exists || workspace.config.pkg_enabled in
match enabled with
| true -> ()
| false -> exit 1)
;;
let info =
let doc =
"Check if the project indicates that dune's package management features should be \
enabled. Exits with 0 if package management is enabled and 1 otherwise."
in
Cmd.info "enabled" ~doc
;;
let command = Cmd.v info term

View file

@ -0,0 +1,3 @@
open Import
val command : unit Cmd.t

View file

@ -0,0 +1,56 @@
open Import
open Pkg_common
let print_solver_env_for_lock_dir workspace ~solver_env_from_current_system lock_dir_path =
let solver_env_from_context =
Option.bind (Workspace.find_lock_dir workspace lock_dir_path) ~f:(fun lock_dir ->
lock_dir.solver_env)
in
let solver_env =
solver_env
~solver_env_from_current_system
~solver_env_from_context
~unset_solver_vars_from_context:
(Pkg_common.unset_solver_vars_of_workspace workspace ~lock_dir_path)
in
Console.print
[ Pp.textf
"Solver environment for lock directory %s:"
(Path.Source.to_string_maybe_quoted lock_dir_path)
; Dune_pkg.Solver_env.pp solver_env
]
;;
let print_solver_env ~lock_dirs_arg =
let open Fiber.O in
let+ workspace = Memo.run (Workspace.workspace ())
and+ solver_env_from_current_system =
Dune_pkg.Sys_poll.make ~path:(Env_path.path Stdune.Env.initial)
|> Dune_pkg.Sys_poll.solver_env_from_current_system
>>| Option.some
in
let lock_dirs = Lock_dirs_arg.lock_dirs_of_workspace lock_dirs_arg workspace in
List.iter
lock_dirs
~f:(print_solver_env_for_lock_dir workspace ~solver_env_from_current_system)
;;
let term =
let+ builder = Common.Builder.term
and+ lock_dirs_arg = Lock_dirs_arg.term in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config (fun () -> print_solver_env ~lock_dirs_arg)
;;
let info =
let doc =
"Print a description of the environment that would be used to solve dependencies and \
then exit without attempting to solve the dependencies or generate the lockfile. \
Intended to be used to debug situations where no solution can be found to a \
project's dependencies."
in
Cmd.info "print-solver-env" ~doc
;;
let command = Cmd.v info term

View file

@ -0,0 +1,4 @@
open Import
(** Command to print solver environment *)
val command : unit Cmd.t

View file

@ -0,0 +1,88 @@
open! Import
open Pkg_common
module Package_universe = Dune_pkg.Package_universe
module Lock_dir = Dune_pkg.Lock_dir
module Opam_repo = Dune_pkg.Opam_repo
module Package_version = Dune_pkg.Package_version
module Opam_solver = Dune_pkg.Opam_solver
let info =
let doc = "Validate that a lockdir contains a solution for local packages" in
let man = [ `S "DESCRIPTION"; `P doc ] in
Cmd.info "validate-lockdir" ~doc ~man
;;
(* CR-someday alizter: The logic here is a little more complicated than it needs
to be and can be simplified. *)
let enumerate_lock_dirs_by_path ~lock_dirs () =
let open Memo.O in
let+ per_contexts =
Workspace.workspace () >>| Pkg_common.Lock_dirs_arg.lock_dirs_of_workspace lock_dirs
in
List.filter_map per_contexts ~f:(fun lock_dir_path ->
if Path.exists (Path.source lock_dir_path)
then (
try Some (Ok (lock_dir_path, Lock_dir.read_disk_exn lock_dir_path)) with
| User_error.E e -> Some (Error (lock_dir_path, `Parse_error e)))
else None)
;;
let validate_lock_dirs ~lock_dirs () =
let open Fiber.O in
let* lock_dirs_by_path, local_packages =
Memo.both (enumerate_lock_dirs_by_path ~lock_dirs ()) Pkg_common.find_local_packages
|> Memo.run
in
if List.is_empty lock_dirs_by_path
then
let+ () = Fiber.return () in
Console.print [ Pp.text "No lockdirs to validate." ]
else
let+ universes =
Fiber.parallel_map lock_dirs_by_path ~f:(function
| Error e -> Fiber.return (Some e)
| Ok (lock_dir_path, lock_dir) ->
let+ platform = solver_env_from_system_and_context ~lock_dir_path in
(match Package_universe.create ~platform local_packages lock_dir with
| Ok _ -> None
| Error e -> Some (lock_dir_path, `Lock_dir_out_of_sync e)))
>>| List.filter_opt
in
match universes with
| [] -> ()
| errors_by_path ->
List.iter errors_by_path ~f:(fun (path, error) ->
match error with
| `Parse_error error ->
User_message.prerr
(User_message.make
[ Pp.textf
"Failed to parse lockdir %s:"
(Path.Source.to_string_maybe_quoted path)
; User_message.pp error
])
| `Lock_dir_out_of_sync error ->
User_message.prerr
(User_message.make
[ Pp.textf
"Lockdir %s does not contain a solution for local packages:"
(Path.Source.to_string path)
]);
User_message.prerr error);
User_error.raise
[ Pp.text "Some lockdirs do not contain solutions for local packages:"
; Pp.enumerate errors_by_path ~f:(fun (path, _) ->
Pp.text (Path.Source.to_string path))
]
;;
let term =
let+ builder = Common.Builder.term
and+ lock_dirs = Pkg_common.Lock_dirs_arg.term in
let builder = Common.Builder.forbid_builds builder in
let common, config = Common.init builder in
Scheduler.go_with_rpc_server ~common ~config @@ validate_lock_dirs ~lock_dirs
;;
let command = Cmd.v info term

View file

@ -0,0 +1,4 @@
open Import
(** Command to check if local packages and lockdir agree *)
val command : unit Cmd.t

Some files were not shown because too many files have changed in this diff Show more