From 5daa5558167c0cbb2fdbab2a989e30e1466edc01 Mon Sep 17 00:00:00 2001 From: Shon Feder Date: Thu, 11 Oct 2018 08:00:09 -0400 Subject: [PATCH] Add `dune init` command Signed-off-by: Shon Feder Signed-off-by: Rudi Grinberg --- CHANGES.md | 3 + bin/common.mli | 9 + bin/init.ml | 115 ++++++ bin/init.mli | 1 + bin/main.ml | 1 + doc/dune.inc | 9 + doc/update-jbuild.sh | 2 +- doc/usage.rst | 54 ++- src/dune_file.mli | 9 + src/dune_init.ml | 377 ++++++++++++++++++ src/dune_init.mli | 68 ++++ src/dune_lang/atom.ml | 2 + src/dune_lang/atom.mli | 2 + src/dune_lang/dune_lang.ml | 3 + src/dune_lang/dune_lang.mli | 6 +- src/dune_project.ml | 6 + src/dune_project.mli | 3 + src/format_dune_lang.ml | 7 + src/format_dune_lang.mli | 10 + test/blackbox-tests/dune.inc | 10 + .../dune-init/existing_project/bin/main.ml | 1 + .../dune-init/existing_project/src/dune | 5 + .../blackbox-tests/test-cases/dune-init/run.t | 281 +++++++++++++ 23 files changed, 981 insertions(+), 3 deletions(-) create mode 100644 bin/init.ml create mode 100644 bin/init.mli create mode 100644 src/dune_init.ml create mode 100644 src/dune_init.mli create mode 100644 test/blackbox-tests/test-cases/dune-init/existing_project/bin/main.ml create mode 100644 test/blackbox-tests/test-cases/dune-init/existing_project/src/dune create mode 100644 test/blackbox-tests/test-cases/dune-init/run.t diff --git a/CHANGES.md b/CHANGES.md index e79a170839b..98a9cd0ca84 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,9 @@ unreleased - Add support for library variants and default implementations. (#1900, @TheLortex) +- Add experimental `$ dune init` command. This command is used to create or + update project boilerplate. (#1448, fixes #159, @shonfeder) + 1.8.2 (10/03/2019) ------------------ diff --git a/bin/common.mli b/bin/common.mli index 7d96f6c17fd..742b913224c 100644 --- a/bin/common.mli +++ b/bin/common.mli @@ -27,10 +27,19 @@ type t = val prefix_target : t -> string -> string +(** [set_common common ~targets] is [set_dirs common] followed by + [set_common_other common ~targets]. In general, [set_common] executes + sequence of side-effecting actions to initialize Dune's working + environment based on the options determined in a [Common.t] record *) val set_common : t -> targets:string list -> unit +(** [set_common_other common ~targets] sets all stateful values dictated by + [common], except those accounted for by [set_dirs]. [targets] are + used to obtain external library dependency hints, if needed. *) val set_common_other : t -> targets:string list -> unit +(** [set_dirs common] sets the workspace root and build directories, and makes + the root the current working directory *) val set_dirs : t -> unit val help_secs diff --git a/bin/init.ml b/bin/init.ml new file mode 100644 index 00000000000..397743e02d6 --- /dev/null +++ b/bin/init.ml @@ -0,0 +1,115 @@ +open Stdune +open Import + +open Dune.Dune_init + +(* TODO(shonfeder): Remove when nested subcommands are available *) +let validate_component_options kind ~unsupported_options = + let report_invalid_option = function + | _, false -> () (* The option wasn't supplied *) + | option_name, true -> + die "The %s component does not support the %s option" + (Kind.to_string kind) option_name + in + List.iter ~f:report_invalid_option unsupported_options + +let doc = "Initialize dune components" +let man = + [ `S "DESCRIPTION" + ; `P {|$(b,dune init {lib,exe,test} NAME [PATH]) initialize a new dune + component of the specified kind, named $(b,NAME), with fields + determined by the supplied options.|} + ; `P {|If the optional $(b,PATH) is provided, the project will be created + there. Otherwise, it is created in the current working directory.|} + ; `P {|The command can be used to add stanzas to existing dune files as + well as for creating new dune files and basic component templates.|} + ; `S "EXAMPLES" + ; `Pre {| +Define an executable component named 'myexe' in a dune file in the +current directory: + + dune init exe myexe + +Define 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 lib mylib src --libs core,cmdliner --ppx ppx_let,ppx_inline_test --inline-tests + +Define a library component named mytest in a dune file in the ./test +directory that depends on mylib: + + dune init test myexe test --libs mylib|} + ] + +let info = Term.info "init" ~doc ~man + +let term = + let+ common_term = Common.term + and+ kind = + (* TODO(shonfeder): Replace with nested subcommand once we have support for that *) + Arg.(required & pos 0 (some (enum Kind.commands)) None & info [] ~docv:"INIT_KIND") + and+ name = + Arg.(required & pos 1 (some string) None & info [] ~docv:"NAME") + and+ path = + Arg.(value & pos 2 (some string) None & info [] ~docv:"PATH" ) + and+ libraries = + Arg.(value + & opt (list string) [] + & info ["libs"] + ~docv:"LIBRARIES" + ~doc:"Libraries on which the component depends") + and+ pps = + Arg.(value + & opt (list string) [] + & info ["ppx"] + ~docv:"PREPROCESSORS" + ~doc:"ppx preprocessors used by the component") + and+ public = + (* TODO(shonfeder): Move to subcommands {lib, exe} once implemented *) + Arg.(value + & opt ~vopt:(Some "") (some string) None + & info ["public"] + ~docv:"PUBLIC_NAME" + ~doc:"If called with an argument, make the component public \ + under the given PUBLIC_NAME. If supplied without an \ + argument, use NAME.") + and+ inline_tests = + (* TODO Move to subcommand lib once implemented *) + Arg.(value + & flag + & info ["inline-tests"] + ~docv:"USE_INLINE_TESTS" + ~doc:"Whether to use inline tests. \ + Only applicable for lib components.") + in + + validate_component_name name; + + Common.set_common common_term ~targets:[]; + let open Component in + let context = Init_context.make path in + let common : Options.common = { name; libraries; pps } in + let given_public = Option.is_some public in + begin match kind with + | Kind.Library -> + init @@ Library { context; common; options = {public; inline_tests} } + | Kind.Executable -> + let unsupported_options = + ["inline-tests", inline_tests] + in + validate_component_options kind ~unsupported_options; + init @@ Executable { context; common; options = {public} } + | Kind.Test -> + let unsupported_options = + [ "public", given_public + ; "inline-tests", inline_tests] + in + validate_component_options kind ~unsupported_options; + init @@ Test { context; common; options = () } + end; + + print_completion kind name + +let command = term, info + diff --git a/bin/init.mli b/bin/init.mli new file mode 100644 index 00000000000..6d988967f3a --- /dev/null +++ b/bin/init.mli @@ -0,0 +1 @@ +val command : unit Cmdliner.Term.t * Cmdliner.Term.info diff --git a/bin/main.ml b/bin/main.ml index 8abea47d3b2..646e1b667cb 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -136,6 +136,7 @@ let all = ; Subst.command ; Print_rules.command ; Utop.command + ; Init.command ; promote ; Printenv.command ; Help.command diff --git a/doc/dune.inc b/doc/dune.inc index f461112c630..f336d997577 100644 --- a/doc/dune.inc +++ b/doc/dune.inc @@ -62,6 +62,15 @@ (package dune) (files dune-help.1)) +(rule + (with-stdout-to dune-init.1 + (run dune init --help=groff))) + +(install + (section man) + (package dune) + (files dune-init.1)) + (rule (with-stdout-to dune-install.1 (run dune install --help=groff))) diff --git a/doc/update-jbuild.sh b/doc/update-jbuild.sh index a1b2cee34d3..2db491c3a5c 100755 --- a/doc/update-jbuild.sh +++ b/doc/update-jbuild.sh @@ -5,7 +5,7 @@ set -e -o pipefail CMDS=$(dune --help=plain | \ - sed -n '/COMMANDS/,/OPTIONS/p' | sed -En 's/^ ([a-z-]+)/\1/p') + sed -n '/COMMANDS/,/OPTIONS/p' | sed -En 's/^ ([a-z-]+) ?.*/\1/p') for cmd in $CMDS; do cat < kind:Dune_lang.Syntax.t diff --git a/src/dune_init.ml b/src/dune_init.ml new file mode 100644 index 00000000000..8b803abb2cc --- /dev/null +++ b/src/dune_init.ml @@ -0,0 +1,377 @@ +open! Stdune +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 + +module Kind = struct + type t = + | Executable + | Library + | Test + + let to_string = function + | Executable -> "executable" + | Library -> "library" + | Test -> "test" + + let pp ppf t = Format.pp_print_string ppf (to_string t) + + let commands = + [ "exe", Executable + ; "lib", Library + ; "test", Test + ] +end + +(** 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 + + (** Defines uniqueness criteria for stanzas *) + module Signature = struct + + (** The uniquely identifying fields of a stanza *) + type t = + { kind: string + ; name: string option + ; public_name: string option + } + + (* TODO(shonfeder): replace with stanza merging *) + (* TODO(shonfeder): replace with a function Cst.t -> Dune_file.Stanza.t *) + let of_cst stanza : t option = + let open Dune_lang in + let open Option.O in + let to_atom = function | Atom a -> Some a | _ -> None in + let is_field name = function + | List (field_name :: _) -> + Option.value ~default:false + (to_atom field_name >>| Atom.equal (Atom.of_string name)) + | _ -> false + in + Cst.to_sexp stanza >>= function + | List (component_kind :: fields) -> + let find_field_value field_name fields = + List.find ~f:(is_field field_name) fields >>= function + | List [_; value] -> Some (to_string ~syntax:Dune value) + | _ -> None + in + let kind = to_string ~syntax:Dune component_kind in + let name = find_field_value "name" fields in + let public_name = find_field_value "public_name" fields in + Some {kind; name; public_name} + | _ -> None + + let equal a b = + (* Like Option.equal but doesn't treat None's as equal *) + let strict_equal x y = + match x, y with + | Some x, Some y -> String.equal x y + | _, _ -> false + in + String.equal a.kind b.kind + && strict_equal a.name b.name + || strict_equal a.public_name b.public_name + end + + let pp ppf s = + Option.iter (Cst.to_sexp s) ~f:(Dune_lang.pp Dune ppf) + + (* TODO(shonfeder): replace with stanza merging *) + let find_conflicting new_stanzas existing_stanzas = + let stanzas_conflict a b = + (let open Option.O in + let* a = Signature.of_cst a in + let+ b = Signature.of_cst b in + Signature.equal a b) + |> Option.value ~default:false + in + let conflicting_stanza stanza = + match List.find ~f:(stanzas_conflict stanza) existing_stanzas with + | Some conflict -> Some (stanza, conflict) + | None -> None + in + List.find_map ~f:conflicting_stanza new_stanzas + + let add stanzas = function + | Text f -> Text f (* Adding a stanza to a text file isn't meaningful *) + | Dune f -> + match find_conflicting stanzas f.content with + | None -> Dune {f with content = f.content @ stanzas} + | Some (a, b) -> + die "Updating existing stanzas is not yet supported.@\n\ + A preexisting dune stanza conflicts with a generated stanza:\ + @\n@\nGenerated stanza:@.%a@.@.Pre-existing stanza:@.%a" + pp a pp b + end (* Stanza *) + + let create_dir path = + try Path.mkdir_p path with + | Unix.Unix_error (EACCES, _, _) -> + die "A project directory cannot be created or accessed: \ + Lacking permissions needed to create directory %a" + Path.pp 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 + match Format_dune_lang.parse_file (Some full_path) with + | Format_dune_lang.Sexps content -> content + | Format_dune_lang.OCaml_syntax _ -> + die "Cannot load dune file %a because it uses OCaml syntax" + Path.pp 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 + Format_dune_lang.write_file ~path 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 + type t = + { dir : Path.t + ; project : Dune_project.t + } + + let make path = + let project = + match Dune_project.load ~dir:Path.root ~files:String.Set.empty with + | Some p -> p + | None -> Lazy.force Dune_project.anonymous + in + let dir = + match path with + | None -> Path.root + | Some p -> Path.of_string p + in + File.create_dir dir; + { dir; project } +end + +module Component = struct + + module Options = struct + type common = + { name : string + ; libraries : string list + ; pps : string list + } + + type executable = + { public: string option + } + + type library = + { public: string option + ; inline_tests: bool + } + + (* NOTE: no options supported yet *) + type test = () + + type 'options t = + { context : Init_context.t + ; common : common + ; options : 'options + } + end + + type 'options t = + | Executable : Options.executable Options.t -> Options.executable t + | Library : Options.library Options.t -> Options.library t + | Test : Options.test Options.t -> Options.test t + + 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 atoms = List.map ~f:atom + let public_name name = List [atom "public_name"; atom name] + let name name = List [atom "name"; atom name] + let inline_tests = List [atom "inline_tests"] + let libraries libs = List (atom "libraries" :: atoms libs) + let pps pps = List [atom "preprocess"; List (atom "pps" :: atoms pps)] + + let optional_field ~f = function + | [] -> [] + | args -> [f args] + + let common (options : Options.common) = + let optional_fields = + optional_field ~f:libraries options.libraries + @ optional_field ~f:pps options.pps + in + name options.name :: optional_fields + end + + let make kind common_options fields = + (* Form the AST *) + List (atom kind + :: fields + @ Field.common common_options) + (* Convert to a CST *) + |> Dune_lang.add_loc ~loc:Loc.none + |> Cst.concrete + (* Package as a list CSTs *) + |> List.singleton + + let add_to_list_set elem set = + if List.mem elem ~set then set else elem :: set + + let public_name_field ~default = function + | None -> [] + | Some "" -> [Field.public_name default] + | Some n -> [Field.public_name n] + + let executable (common : Options.common) (options : Options.executable) = + let public_name = + public_name_field ~default:common.name options.public + in + make "executable" {common with name = "main"} public_name + + let library (common : Options.common) (options: Options.library) = + let (common, inline_tests) = + if not options.inline_tests then + (common, []) + else + let pps = + add_to_list_set "ppx_inline_tests" common.pps + in + ({common with pps}, [Field.inline_tests]) + in + let public_name = + public_name_field ~default:common.name options.public + in + make "library" common (public_name @ inline_tests) + + let test common ((): Options.test) = + make "test" common [] + end + + (* TODO Support for merging in changes to an existing stanza *) + let add_stanza_to_dune_file ~dir stanza = + File.load_dune_file ~path:dir + |> File.Stanza.add stanza + + let bin ({context; common; options} : Options.executable Options.t) = + let dir = context.dir in + let bin_dune = + Stanza_cst.executable common options + |> add_stanza_to_dune_file ~dir + in + let bin_ml = + let name = "main.ml" 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 Options.t) = + let dir = context.dir in + let lib_dune = + Stanza_cst.library common options + |> add_stanza_to_dune_file ~dir + in + let files = [lib_dune] in + {dir; files} + + let test ({context; common; options}: Options.test 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 ~dir + in + let test_ml = + let name = sprintf "%s.ml" common.name in + let content = "" in + File.make_text dir name content + in + let files = [test_dune; test_ml] in + {dir; files} + + let report_uncreated_file = function + | Ok _ -> () + | Error path -> + Errors.kerrf ~f:print_to_console + "@{Warning@}: file @{%a@} was not created \ + because it already exists\n" + Path.pp path + + 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 -> bin params + | Library params -> src params + | Test params -> test params + in + create target + |> List.iter ~f:report_uncreated_file +end + +let validate_component_name name = + match Lib_name.Local.of_string name with + | Ok _ -> () + | _ -> + die "A component named '%s' cannot be created because it is an %s" + name Lib_name.Local.invalid_message + +let print_completion kind name = + Errors.kerrf ~f:print_to_console + "@{Success@}: initialized %a component named @{%s@}\n" + Kind.pp kind name diff --git a/src/dune_init.mli b/src/dune_init.mli new file mode 100644 index 00000000000..33c2c63d285 --- /dev/null +++ b/src/dune_init.mli @@ -0,0 +1,68 @@ +(** Initialize dune components *) +open! Stdune + +(** Supported kinds of components for initialization *) +module Kind : sig + type t = + | Executable + | Library + | Test + + val to_string : t -> string + (* val kind_strings : string list *) + val commands : (string * t) list +end + +(** The context in which the initialization is executed *) +module Init_context : sig + type t = + { dir : Path.t + ; project : Dune_project.t + } + + val make : string option -> 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 + type common = + { name: string + ; libraries: string list + ; pps: string list + } + + type executable = + { public: string option + } + + type library = + { public: string option + ; inline_tests: bool + } + + (** NOTE: no options supported yet *) + type test = () + + type 'a t = + { context : Init_context.t + ; common : common + ; options : 'a + } + end + + type 'options t = + | Executable : Options.executable Options.t -> Options.executable t + | Library : Options.library Options.t -> Options.library t + | Test : Options.test Options.t -> Options.test t + + (** Create or update the component specified by the ['options t], + where ['options] is *) + val init : 'options t -> unit +end + +val validate_component_name : string -> unit +val print_completion : Kind.t -> string -> unit diff --git a/src/dune_lang/atom.ml b/src/dune_lang/atom.ml index d214e11f102..d0c5b4bf373 100644 --- a/src/dune_lang/atom.ml +++ b/src/dune_lang/atom.ml @@ -2,6 +2,8 @@ open Stdune type t = A of string [@@unboxed] +let equal (A a) (A b) = String.equal a b + let is_valid_dune = let rec loop s i len = i = len || diff --git a/src/dune_lang/atom.mli b/src/dune_lang/atom.mli index 7228737a71f..68fd2d922b7 100644 --- a/src/dune_lang/atom.mli +++ b/src/dune_lang/atom.mli @@ -2,6 +2,8 @@ open Stdune type t = private A of string [@@unboxed] +val equal : t -> t -> bool + val is_valid_dune : string -> bool val is_valid : t -> Syntax.t -> bool diff --git a/src/dune_lang/dune_lang.ml b/src/dune_lang/dune_lang.ml index b6b6388850b..4851b7b0c18 100644 --- a/src/dune_lang/dune_lang.ml +++ b/src/dune_lang/dune_lang.ml @@ -215,6 +215,9 @@ module Cst = struct | Template t -> Template t | List (loc, l) -> List (loc, List.map ~f:concrete l) + let to_sexp c = + abstract c |> Option.map ~f:Ast.remove_locs + let extract_comments = let rec loop acc = function | Atom _ | Quoted_string _ | Template _ -> acc diff --git a/src/dune_lang/dune_lang.mli b/src/dune_lang/dune_lang.mli index f5e3b06a98b..5b80b1deb9c 100644 --- a/src/dune_lang/dune_lang.mli +++ b/src/dune_lang/dune_lang.mli @@ -7,6 +7,7 @@ module Atom : sig type t = private A of string [@@unboxed] val is_valid : t -> Syntax.t -> bool + val equal : t -> t -> bool val of_string : string -> t val to_string : t -> string @@ -111,6 +112,7 @@ val add_loc : t -> loc:Loc.t -> Ast.t (** Concrete syntax tree *) module Cst : sig + type sexp = t module Comment : sig type t = | Lines of string list @@ -150,9 +152,11 @@ module Cst : sig val concrete : Ast.t -> t + val to_sexp : t -> sexp option + (** Return all the comments contained in a concrete syntax tree *) val extract_comments : t list -> (Loc.t * Comment.t) list -end +end with type sexp := t (** Insert comments in a concrete syntax tree. Comments are inserted based on their location. *) diff --git a/src/dune_project.ml b/src/dune_project.ml index e4df98bb0c9..25db6804b1e 100644 --- a/src/dune_project.ml +++ b/src/dune_project.ml @@ -222,6 +222,10 @@ module Project_file_edit = struct let notify_user s = kerrf ~f:print_to_console "@{Info@}: %s\n" s + let lang_stanza () = + let ver = (Lang.get_exn "dune").version in + sprintf "(lang dune %s)" (Syntax.Version.to_string ver) + let ensure_exists t = if t.exists then Already_exist @@ -262,6 +266,8 @@ module Project_file_edit = struct what end +let lang_stanza = Project_file_edit.lang_stanza + let ensure_project_file_exists t = Project_file_edit.ensure_exists t.project_file diff --git a/src/dune_project.mli b/src/dune_project.mli index 3472e4c8872..f211d2fbeda 100644 --- a/src/dune_project.mli +++ b/src/dune_project.mli @@ -106,6 +106,9 @@ val anonymous : t Lazy.t type created_or_already_exist = Created | Already_exist +(** Generate an appropriate project [lang] stanza *) +val lang_stanza : unit -> string + (** Check that the dune-project file exists and create it otherwise. *) val ensure_project_file_exists : t -> created_or_already_exist diff --git a/src/format_dune_lang.ml b/src/format_dune_lang.ml index f461aaf9267..ef0071e3335 100644 --- a/src/format_dune_lang.ml +++ b/src/format_dune_lang.ml @@ -119,6 +119,13 @@ let pp_top_sexp fmt sexp = let pp_top_sexps = Fmt.list ~pp_sep:Fmt.nl pp_top_sexp +let write_file ~path sexps = + let f oc = + let fmt = (Format.formatter_of_out_channel oc) in + Format.fprintf fmt "%a%!" pp_top_sexps sexps + in + Io.with_file_out ~binary:true path ~f + let format_file ~input = match parse_file input with | exception Dune_lang.Parse_error e -> diff --git a/src/format_dune_lang.mli b/src/format_dune_lang.mli index 63fbb0958d3..2a9185a4324 100644 --- a/src/format_dune_lang.mli +++ b/src/format_dune_lang.mli @@ -1,5 +1,15 @@ open Import +type dune_file = + | OCaml_syntax of Loc.t + | Sexps of Dune_lang.Cst.t list + +(** Read a file into its concrete syntax *) +val parse_file : Path.t option -> dune_file + +(** Write the formatted concrete syntax to the file at [path] *) +val write_file : path:Path.t -> Dune_lang.Cst.t list -> unit + (** Reformat a dune file. [None] corresponds to stdin. *) val format_file : input:Path.t option -> unit diff --git a/test/blackbox-tests/dune.inc b/test/blackbox-tests/dune.inc index a63c000efe1..e5d00998c2f 100644 --- a/test/blackbox-tests/dune.inc +++ b/test/blackbox-tests/dune.inc @@ -191,6 +191,14 @@ test-cases/dune-build-dir-exec-1101 (progn (run %{exe:cram.exe} -test run.t) (diff? run.t run.t.corrected))))) +(alias + (name dune-init) + (deps (package dune) (source_tree test-cases/dune-init)) + (action + (chdir + test-cases/dune-init + (progn (run %{exe:cram.exe} -test run.t) (diff? run.t run.t.corrected))))) + (alias (name dune-jbuild-var-case) (deps (package dune) (source_tree test-cases/dune-jbuild-var-case)) @@ -1371,6 +1379,7 @@ (alias dir-target-dep) (alias double-echo) (alias dune-build-dir-exec-1101) + (alias dune-init) (alias dune-jbuild-var-case) (alias dune-package) (alias dune-ppx-driver-system) @@ -1536,6 +1545,7 @@ (alias dir-target-dep) (alias double-echo) (alias dune-build-dir-exec-1101) + (alias dune-init) (alias dune-jbuild-var-case) (alias dune-package) (alias dune-project-edition) diff --git a/test/blackbox-tests/test-cases/dune-init/existing_project/bin/main.ml b/test/blackbox-tests/test-cases/dune-init/existing_project/bin/main.ml new file mode 100644 index 00000000000..05c2a25a77e --- /dev/null +++ b/test/blackbox-tests/test-cases/dune-init/existing_project/bin/main.ml @@ -0,0 +1 @@ +() = print_endline "Goodbye" diff --git a/test/blackbox-tests/test-cases/dune-init/existing_project/src/dune b/test/blackbox-tests/test-cases/dune-init/existing_project/src/dune new file mode 100644 index 00000000000..c6ac264cd57 --- /dev/null +++ b/test/blackbox-tests/test-cases/dune-init/existing_project/src/dune @@ -0,0 +1,5 @@ +; A comment + +(library + ; Another comment + (name test_lib)) diff --git a/test/blackbox-tests/test-cases/dune-init/run.t b/test/blackbox-tests/test-cases/dune-init/run.t new file mode 100644 index 00000000000..1bae590d357 --- /dev/null +++ b/test/blackbox-tests/test-cases/dune-init/run.t @@ -0,0 +1,281 @@ +Adding a library +---------------- + +Can init a public library + + $ dune init lib test_lib ./_test_lib_dir --public + Success: initialized library component named test_lib + +Can build the public library + + $ cd _test_lib_dir && touch test_lib.opam && dune build + Info: creating file dune-project with this contents: + | (lang dune 1.9) + | (name test_lib) + + $ cat ./_test_lib_dir/dune + (library + (public_name test_lib) + (name test_lib)) + +Clean up the library tests + + $ rm -rf ./_test_lib_dir + +Can init library with a specified public name + + $ dune init lib test_lib ./_test_lib_dir --public test_lib_public_name + Success: initialized library component named test_lib + $ cat ./_test_lib_dir/dune + (library + (public_name test_lib_public_name) + (name test_lib)) + +Clean up library with specified public name + + $ rm -rf ./_test_lib_dir + +Can add a library with inline tests + + $ dune init lib test_lib ./_inline_tests_lib --inline-tests --ppx ppx_inline_tests + Success: initialized library component named test_lib + $ cat ./_inline_tests_lib/dune + (library + (inline_tests) + (name test_lib) + (preprocess + (pps ppx_inline_tests))) + +Clean up library with inlines tests + + $ rm -rf ./_inline_tests_lib + +Adding an executable +-------------------- + +Can init a public executable + + $ dune init exe test_bin ./_test_bin_dir --public + Success: initialized executable component named test_bin + +Can build an executable + + $ cd _test_bin_dir && touch test_bin.opam && dune build + Info: creating file dune-project with this contents: + | (lang dune 1.9) + | (name test_bin) + + +Can run the created executable + + $ cd _test_bin_dir && dune exec test_bin + Hello, World! + +Clean up the executable tests + + $ rm -rf ./_test_bin_dir + +Adding tests +------------ + +Can init tests + + $ dune init test test_tests ./_test_tests_dir --libs foo,bar + Success: initialized test component named test_tests + $ ls ./_test_tests_dir + dune + test_tests.ml + $ cat ./_test_tests_dir/dune + (test + (name test_tests) + (libraries foo bar)) + +Clean up the test tests + + $ rm -rf ./_test_tests_dir + +Adding components to default and non-standard places +--------------------------------------------------- + +Add a library in the current working directory + + $ dune init lib test_lib + Success: initialized library component named test_lib + $ cat dune + (library + (name test_lib)) + +Clean the library creation + + $ rm ./dune + +Add a library to a dune file in a specified directory + + $ dune init lib test_lib ./_test_dir + Success: initialized library component named test_lib + $ test -f ./_test_dir/dune + +Clean up from the dune file created in ./_test_dir + + $ rm -rf ./_test_dir + +Add a library to a dune file in a directory specified with an absolute path + + $ dune init lib test_lib $PWD/_test_dir + Success: initialized library component named test_lib + $ test -f $PWD/_test_dir/dune + +Clean up from the dune file created at an absolute path + + $ rm -rf $PWD/_test_dir + +Adding a library and an executable dependent on that library +------------------------------------------------------------ + +Can init a library and dependent executable in a combo project + + $ dune init lib test_lib ./_test_lib_exe_dir/src + Success: initialized library component named test_lib + $ dune init exe test_bin ./_test_lib_exe_dir/bin --libs test_lib --public + Success: initialized executable component named test_bin + +Can build the combo project + + $ cd _test_lib_exe_dir && touch test_bin.opam && dune build + Info: creating file dune-project with this contents: + | (lang dune 1.9) + | (name test_bin) + + +Can run the combo project + + $ cd _test_lib_exe_dir && dune exec test_bin + Hello, World! + +Clean up the combo project + + $ rm -rf ./_test_lib_exe_dir + +Adding libraries in a single directory +-------------------------------------- + +Can add multiple libraries in the same directory + + $ dune init lib test_lib1 ./_test_lib --public + Success: initialized library component named test_lib1 + $ dune init lib test_lib2 ./_test_lib --libs test_lib1 + Success: initialized library component named test_lib2 + $ cat ./_test_lib/dune + (library + (public_name test_lib1) + (name test_lib1)) + + (library + (name test_lib2) + (libraries test_lib1)) + +Can build the multiple library project + + $ cd _test_lib && touch test_lib1.opam && dune build + Info: creating file dune-project with this contents: + | (lang dune 1.9) + | (name test_lib1) + + +Clan up the multiple library project + + $ rm -rf ./_test_lib + +Multiple ppxs and library dependencies +-------------------------------------- + +Can add multiple library dependencies in one command + + $ dune init lib test_lib ./_test_lib --libs foo,bar --ppx ppx_foo,ppx_bar + Success: initialized library component named test_lib + $ cat _test_lib/dune + (library + (name test_lib) + (libraries foo bar) + (preprocess + (pps ppx_foo ppx_bar))) + +Clean up the multiple dependencies project + + $ rm -rf ./_test_lib + +Safety and Validation +--------------------- + +Will not overwrite existing files + + $ dune init exe test_bin ./existing_project/bin + Warning: file existing_project/bin/main.ml was not created because it already exists + Success: initialized executable component named test_bin + $ cat ./existing_project/bin/main.ml + () = print_endline "Goodbye" + +Comments in dune files are preserved + + $ dune init lib test_lib2 ./existing_project/src + Success: initialized library component named test_lib2 + $ cat ./existing_project/src/dune + ; A comment + + (library + ; Another comment + (name test_lib)) + + (library + (name test_lib2)) + +Will not create components with invalid names + + $ dune init lib invalid-component-name ./_test_lib + A component named 'invalid-component-name' cannot be created because it is an invalid library name. + Hint: library names must be non-empty and composed only of the following characters: 'A'..'Z', 'a'..'z', '_' or '0'..'9' + [1] + $ test -f ./_test_lib + [1] + +Will fail and inform user when invalid component command is given + + $ dune init foo blah + dune: INIT_KIND argument: invalid value `foo', expected one of `exe', `lib' + or `test' + Usage: dune init [OPTION]... INIT_KIND NAME [PATH] + Try `dune init --help' or `dune --help' for more information. + [1] + +Will fail and inform user when an invalid option is given to a component + + $ dune init test test_foo --public + The test component does not support the public option + [1] + $ dune init exe test_exe --inline-tests + The executable component does not support the inline-tests option + [1] + +Adding fields to existing stanzas +--------------------------------- + +# TODO(shonfeder) +Adding fields to existing stanzas is currently not supported + + $ dune init exe test_bin ./_test_bin --libs test_lib1 --public + Success: initialized executable component named test_bin + $ dune init exe test_bin ./_test_bin --libs test_lib2 + Updating existing stanzas is not yet supported. + A preexisting dune stanza conflicts with a generated stanza: + + Generated stanza: + (executable (name main) (libraries test_lib2)) + + Pre-existing stanza: + (executable (public_name test_bin) (name main) (libraries test_lib1)) + [1] + $ cat ./_test_bin/dune + (executable + (public_name test_bin) + (name main) + (libraries test_lib1))