Merge pull request #932 from diml/version-other-files

Version `dune-workspace` and `~/.config/dune/config` files
This commit is contained in:
Rudi Grinberg 2018-07-01 15:25:09 +07:00 committed by GitHub
commit a877fc00df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 463 additions and 262 deletions

View File

@ -93,6 +93,8 @@ next
- Make `dev` the default build profile (#920, @diml) - Make `dev` the default build profile (#920, @diml)
- Version `dune-workspace` and `~/.config/dune/config` files (#..., @diml)
1.0+beta20 (10/04/2018) 1.0+beta20 (10/04/2018)
----------------------- -----------------------

View File

@ -579,8 +579,10 @@ let installed_libraries =
let env = Main.setup_env ~capture_outputs:common.capture_outputs in let env = Main.setup_env ~capture_outputs:common.capture_outputs in
Scheduler.go ~log:(Log.create common) ~common Scheduler.go ~log:(Log.create common) ~common
(Context.create (Context.create
(Default { targets = [Native] (Default { loc = Loc.of_pos __POS__
; profile = Config.default_build_profile }) ; targets = [Native]
; profile = Config.default_build_profile
})
~env ~env
>>= fun ctxs -> >>= fun ctxs ->
let ctx = List.hd ctxs in let ctx = List.hd ctxs in
@ -1424,8 +1426,11 @@ module Help = struct
Unix systems and $(b,Local Settings/dune/config) in the User home Unix systems and $(b,Local Settings/dune/config) in the User home
directory on Windows. However, it is possible to specify an directory on Windows. However, it is possible to specify an
alternative configuration file with the $(b,--config-file) option.|} alternative configuration file with the $(b,--config-file) option.|}
; `P {|This file must be written in S-expression syntax and be composed of ; `P {|The first line of the file must be of the form (lang dune X.Y) \
a list of stanzas. The following sections describe the stanzas available.|} 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 "DISPLAY MODES" ; `S "DISPLAY MODES"
; `P {|Syntax: $(b,\(display MODE\))|} ; `P {|Syntax: $(b,\(display MODE\))|}
; `P {|This stanza controls how Dune reports what it is doing to the user. ; `P {|This stanza controls how Dune reports what it is doing to the user.

View File

@ -112,16 +112,16 @@ building executables needed by the other contexts.
With such a setup, calling ``jbuilder build @install`` will build all With such a setup, calling ``jbuilder build @install`` will build all
the packages three times. the packages three times.
Note that instead of writing a ``jbuild-workspace`` file, you can also Note that instead of writing a ``dune-workspace`` file, you can also
use the ``-x`` command line option. Passing ``-x foo`` to ``jbuilder`` use the ``-x`` command line option. Passing ``-x foo`` to ``dune``
without having a ``jbuild-workspace`` file is the same as writing the without having a ``dune-workspace`` file is the same as writing the
following ``jbuild-workspace`` file: following ``dune-workspace`` file:
.. code:: scheme .. code:: scheme
(context (default (targets (foo)))) (context (default (targets (foo))))
If you have a ``jbuild-workspace`` and pass a ``-x foo`` option, If you have a ``dune-workspace`` and pass a ``-x foo`` option,
``foo`` will be added as target of all context stanzas. ``foo`` will be added as target of all context stanzas.
How does it work? How does it work?

View File

@ -38,7 +38,7 @@ Terminology
specific configuration from the user, there is always a ``default`` specific configuration from the user, there is always a ``default``
build context, which corresponds to the environment in which Jbuilder build context, which corresponds to the environment in which Jbuilder
is executed. Build contexts can be specified by writing a is executed. Build contexts can be specified by writing a
:ref:`jbuild-workspace` file :ref:`dune-workspace` file
- **build context root**: the root of a build context named ``foo`` is - **build context root**: the root of a build context named ``foo`` is
``<root>/_build/<foo>`` ``<root>/_build/<foo>``

View File

@ -9,71 +9,51 @@ This section describe usage of Jbuilder from the shell.
Finding the root Finding the root
================ ================
.. _jbuild-workspace: .. _dune-workspace:
jbuild-workspace dune-workspace
---------------- --------------
The root of the current workspace is determined by looking up a The root of the current workspace is determined by looking up a
``jbuild-workspace`` or ``dune-project`` file in the current directory ``dune-workspace`` or ``dune-project`` file in the current directory
and parent directories. and parent directories.
``jbuilder`` prints out the root when starting if it is not the ``dune`` prints out the root when starting if it is not the current
current directory: directory:
.. code:: bash .. code:: bash
$ jbuilder runtest $ dune runtest
Entering directory '/home/jdimino/code/jbuilder' Entering directory '/home/jdimino/code/dune'
... ...
More precisely, it will choose the outermost ancestor directory containing a More precisely, it will choose the outermost ancestor directory containing a
``jbuild-workspace`` file as root. For instance if you are in ``dune-workspace`` file as root. For instance if you are in
``/home/me/code/myproject/src``, then jbuilder will look for all these files in ``/home/me/code/myproject/src``, then jbuilder will look for all these files in
order: order:
- ``/jbuild-workspace`` - ``/dune-workspace``
- ``/home/jbuild-workspace`` - ``/home/dune-workspace``
- ``/home/me/jbuild-workspace`` - ``/home/me/dune-workspace``
- ``/home/me/code/jbuild-workspace`` - ``/home/me/code/dune-workspace``
- ``/home/me/code/myproject/jbuild-workspace`` - ``/home/me/code/myproject/dune-workspace``
- ``/home/me/code/myproject/src/jbuild-workspace`` - ``/home/me/code/myproject/src/dune-workspace``
The first entry to match in this list will determine the root. In The first entry to match in this list will determine the root. In
practice this means that if you nest your workspaces, Jbuilder will practice this means that if you nest your workspaces, Jbuilder will
always use the outermost one. always use the outermost one.
In addition to determining the root, ``jbuilder`` will read this file as to In addition to determining the root, ``dune`` will read this file as
setup the configuration of the workspace unless the ``--workspace`` command line to setup the configuration of the workspace unless the ``--workspace``
option is used. See the section `Workspace configuration`_ for the syntax of command line option is used. See the section `Workspace
this file. configuration`_ for the syntax of this file.
jbuild-workspace\*
------------------
The following is deprecated and no longer works with ``dune``.
In addition to the previous rule, if no ``jbuild-workspace`` file is found,
``jbuilder`` will look for any file whose name starts with ``jbuild-workspace``
in ancestor directories. For instance ``jbuild-workspace.dev``. If such a file
is found, it will mark the root of the workspace. ``jbuilder`` will however not
read its contents.
The rationale for this rule is that it is good practice to have a
``jbuild-workspace.dev`` file at the root of your project.
For quick experiments, simply do this to mark the root:
.. code:: bash
$ touch jbuild-workspace.here
Current directory Current directory
----------------- -----------------
If none of the two previous rules appies, i.e. no ancestor directories If the previous rule doesn't apply, i.e. no ancestor directory has a
have a file whose name starts with ``jbuild-workspace``, then the file named ``dune-workspace``, then the current directory will be used
current directory will be used as root. as root.
Forcing the root (for scripts) Forcing the root (for scripts)
------------------------------ ------------------------------
@ -319,47 +299,49 @@ Workspace configuration
======================= =======================
By default, a workspace has only one build context named ``default`` By default, a workspace has only one build context named ``default``
which correspond to the environment in which ``jbuilder`` is run. You which correspond to the environment in which ``dune`` is run. You can
can define more contexts by writing a ``jbuild-workspace`` file. define more contexts by writing a ``dune-workspace`` file.
You can point ``jbuilder`` to an explicit ``jbuild-workspace`` file with You can point ``dune`` to an explicit ``dune-workspace`` file with
the ``--workspace`` option. For instance it is good practice to write a the ``--workspace`` option. For instance it is good practice to write a
``jbuild-workspace.dev`` in your project with all the version of OCaml ``dune-workspace.dev`` in your project with all the version of OCaml
your projects support. This way developers can tests that the code your projects support. This way developers can tests that the code
builds with all version of OCaml by simply running: builds with all version of OCaml by simply running:
.. code:: bash .. code:: bash
$ jbuilder build --workspace jbuild-workspace.dev @install @runtest $ dune build --workspace dune-workspace.dev @install @runtest
jbuild-workspace dune-workspace
---------------- --------------
The ``jbuild-workspace`` file uses the S-expression syntax. This is what The ``dune-workspace`` file uses the S-expression syntax. This is what
a typical ``jbuild-workspace`` file looks like: a typical ``dune-workspace`` file looks like:
.. code:: scheme .. code:: scheme
(lang dune 1.0)
(context (opam (switch 4.02.3))) (context (opam (switch 4.02.3)))
(context (opam (switch 4.03.0))) (context (opam (switch 4.03.0)))
(context (opam (switch 4.04.0))) (context (opam (switch 4.04.0)))
The rest of this section describe the stanzas available. The rest of this section describe the stanzas available.
Note that an empty ``jbuild-workspace`` file is interpreted the same Note that an empty ``dune-workspace`` file is interpreted the same
as one containing exactly: as one containing exactly:
.. code:: scheme .. code:: scheme
(lang dune 1.0)
(context default) (context default)
This allows you to use an empty ``jbuild-workspace`` file to mark This allows you to use an empty ``dune-workspace`` file to mark
the root of your project. the root of your project.
profile profile
~~~~~~~ ~~~~~~~
The build profile can be selected in the ``jbuild-workspace`` file by The build profile can be selected in the ``dune-workspace`` file by
write a ``(profile ...)`` stanza. For instance: write a ``(profile ...)`` stanza. For instance:
.. code:: scheme .. code:: scheme
@ -404,22 +386,13 @@ for more information.
Merlin reads compilation artifacts and it can only read the Merlin reads compilation artifacts and it can only read the
compilation artifacts of a single context. Usually, you should use compilation artifacts of a single context. Usually, you should use
the artifacts from the ``default`` context, and if you have the the artifacts from the ``default`` context, and if you have the
``(context default)`` stanza in your ``jbuild-workspace`` file, that ``(context default)`` stanza in your ``dune-workspace`` file, that
is the one Jbuilder will use. is the one Jbuilder will use.
For rare cases where this is not what you want, you can force Jbuilder For rare cases where this is not what you want, you can force Jbuilder
to use a different build contexts for merlin by adding the field to use a different build contexts for merlin by adding the field
``(merlin)`` to this context. ``(merlin)`` to this context.
Note that the following syntax is still accepted but is deprecated:
.. code:: scheme
(context ((switch <opam-switch-name>)
<optional-fields>))
it is interpreted the same as ``(context (opam (switch ...) ...))``.
Building JavaScript with js_of_ocaml Building JavaScript with js_of_ocaml
==================================== ====================================

View File

@ -1,3 +1,5 @@
(lang dune 1.0)
;; This file is used by `make all-supported-ocaml-versions` ;; This file is used by `make all-supported-ocaml-versions`
(context (opam (switch 4.02.3))) (context (opam (switch 4.02.3)))
(context (opam (switch 4.03.0))) (context (opam (switch 4.03.0)))

View File

@ -29,7 +29,11 @@ let default_build_profile =
| Dune -> "dev" | Dune -> "dev"
| Jbuilder -> "release" | Jbuilder -> "release"
open Sexp.Of_sexp open Stanza.Of_sexp
(* the configuration file use the same version numbers as dune-project
files for simplicity *)
let syntax = Stanza.syntax
module Display = struct module Display = struct
type t = type t =
@ -106,21 +110,35 @@ let default =
} }
let t = let t =
record field "display" Display.t ~default:default.display
(field "display" Display.t ~default:default.display >>= fun display ->
>>= fun display -> field "jobs" Concurrency.t ~default:default.concurrency
field "jobs" Concurrency.t ~default:default.concurrency >>= fun concurrency ->
>>= fun concurrency -> return { display
return { display ; concurrency
; concurrency }
})
let t = fields t
let user_config_file = let user_config_file =
Path.relative (Path.of_filename_relative_to_initial_cwd Xdg.config_dir) Path.relative (Path.of_filename_relative_to_initial_cwd Xdg.config_dir)
"dune/config" "dune/config"
include Versioned_file.Make(struct type t = unit end)
let () = Lang.register syntax ()
let load_config_file p = let load_config_file p =
(Sexp.Of_sexp.parse t Univ_map.empty) (Io.Sexp.load p ~mode:Many_as_one) match Which_program.t with
| Dune -> load p ~f:(fun _lang -> t)
| Jbuilder ->
Io.with_lexbuf_from_file p ~f:(fun lb ->
match Dune_lexer.maybe_first_line lb with
| None ->
parse (enter t)
(Univ_map.singleton (Syntax.key syntax) (0, 0))
(Io.Sexp.load p ~mode:Many_as_one ~lexer:Sexp.Lexer.jbuild_token)
| Some first_line ->
parse_contents lb first_line ~f:(fun _lang -> t))
let load_user_config_file () = let load_user_config_file () =
if Path.exists user_config_file then if Path.exists user_config_file then

View File

@ -453,7 +453,7 @@ let create_for_opam ?root ~env ~targets ~profile ~switch ~name
let create ?merlin ~env def = let create ?merlin ~env def =
match (def : Workspace.Context.t) with match (def : Workspace.Context.t) with
| Default { targets; profile } -> default ~env ~profile ~targets ?merlin () | Default { targets; profile; _ } -> default ~env ~profile ~targets ?merlin ()
| Opam { name; switch; root; targets; profile; _ } -> | Opam { name; switch; root; targets; profile; _ } ->
create_for_opam ?root ~env ~profile ~switch ~name ?merlin ~targets () create_for_opam ?root ~env ~profile ~switch ~name ?merlin ~targets ()

View File

@ -12,3 +12,5 @@ val first_line : Lexing.lexbuf -> first_line
(** Parse the first line of a versioned file but do not fail if it (** Parse the first line of a versioned file but do not fail if it
doesn't start with [(lang ...)]. *) doesn't start with [(lang ...)]. *)
val maybe_first_line : Lexing.lexbuf -> first_line option val maybe_first_line : Lexing.lexbuf -> first_line option
val eof_reached : Lexing.lexbuf -> bool

View File

@ -61,6 +61,10 @@ and to_eol = parse
{ () { ()
} }
and eof_reached = parse
| eof { true }
| "" { false }
{ {
let first_line lb = let first_line lb =
match maybe_first_line lb with match maybe_first_line lb with

View File

@ -141,51 +141,9 @@ type t =
; project_file : Project_file.t ; project_file : Project_file.t
} }
module Lang = struct include Versioned_file.Make(struct
type t = type t = Stanza.Parser.t list
{ syntax : Syntax.t end)
; stanzas : Stanza.Parser.t list
}
type instance =
{ lang : t
; version : Syntax.Version.t
}
let langs = Hashtbl.create 32
let register syntax stanzas =
let name = Syntax.name syntax in
if Hashtbl.mem langs name then
Exn.code_error "Dune_project.Lang.register: already registered"
[ "name", Sexp.To_sexp.string name ];
Hashtbl.add langs name { syntax; stanzas }
let parse first_line =
let { Dune_lexer.
lang = (name_loc, name)
; version = (ver_loc, ver)
} = first_line
in
let ver =
Sexp.Of_sexp.parse Syntax.Version.t Univ_map.empty
(Atom (ver_loc, Sexp.Atom.of_string ver)) in
match Hashtbl.find langs name with
| None ->
Loc.fail name_loc "Unknown language %S.%s" name
(hint name (Hashtbl.keys langs))
| Some t ->
Syntax.check_supported t.syntax (ver_loc, ver);
{ lang = t
; version = ver
}
let get_exn name =
let lang = Option.value_exn (Hashtbl.find langs name) in
{ lang
; version = Syntax.greatest_supported_version lang.syntax
}
end
module Project_file_edit = struct module Project_file_edit = struct
open Project_file open Project_file
@ -296,8 +254,8 @@ module Extension = struct
acc) acc)
end end
let make_parsing_context ~(lang : Lang.instance) ~extensions = let make_parsing_context ~(lang : Lang.Instance.t) ~extensions =
let acc = Univ_map.singleton (Syntax.key lang.lang.syntax) lang.version in let acc = Univ_map.singleton (Syntax.key lang.syntax) lang.version in
List.fold_left extensions ~init:acc List.fold_left extensions ~init:acc
~f:(fun acc (ext : Extension.instance) -> ~f:(fun acc (ext : Extension.instance) ->
Univ_map.add acc (Syntax.key ext.extension.syntax) ext.version) Univ_map.add acc (Syntax.key ext.extension.syntax) ext.version)
@ -338,7 +296,7 @@ let anonymous = lazy (
; root = get_local_path Path.root ; root = get_local_path Path.root
; version = None ; version = None
; stanza_parser = ; stanza_parser =
Sexp.Of_sexp.(set_many parsing_context (sum lang.lang.stanzas)) Sexp.Of_sexp.(set_many parsing_context (sum lang.data))
; project_file = { file = Path.relative Path.root filename; exists = false } ; project_file = { file = Path.relative Path.root filename; exists = false }
}) })
@ -367,7 +325,7 @@ let name ~dir ~packages =
| None -> return (default_name ~dir ~packages) | None -> return (default_name ~dir ~packages)
let parse ~dir ~lang ~packages ~file = let parse ~dir ~lang ~packages ~file =
record fields
(name ~dir ~packages >>= fun name -> (name ~dir ~packages >>= fun name ->
field_o "version" string >>= fun version -> field_o "version" string >>= fun version ->
multi_field "using" multi_field "using"
@ -396,7 +354,7 @@ let parse ~dir ~lang ~packages ~file =
let parsing_context = make_parsing_context ~lang ~extensions in let parsing_context = make_parsing_context ~lang ~extensions in
let stanzas = let stanzas =
List.concat List.concat
(lang.lang.stanzas :: (lang.data ::
List.map extensions ~f:(fun (ext : Extension.instance) -> List.map extensions ~f:(fun (ext : Extension.instance) ->
ext.parse_args ext.parse_args
(Sexp.Of_sexp.set_many parsing_context ext.extension.stanzas))) (Sexp.Of_sexp.set_many parsing_context ext.extension.stanzas)))
@ -412,12 +370,8 @@ let parse ~dir ~lang ~packages ~file =
}) })
let load_dune_project ~dir packages = let load_dune_project ~dir packages =
let fname = Path.relative dir filename in let file = Path.relative dir filename in
Io.with_lexbuf_from_file fname ~f:(fun lb -> load file ~f:(fun lang -> parse ~dir ~lang ~packages ~file)
let lang = Lang.parse (Dune_lexer.first_line lb) in
let sexp = Sexp.Parser.parse lb ~mode:Many_as_one in
Sexp.Of_sexp.parse (parse ~dir ~lang ~packages ~file:fname)
Univ_map.empty sexp)
let make_jbuilder_project ~dir packages = let make_jbuilder_project ~dir packages =
let lang = Lang.get_exn "dune" in let lang = Lang.get_exn "dune" in
@ -428,7 +382,7 @@ let make_jbuilder_project ~dir packages =
; version = None ; version = None
; packages ; packages
; stanza_parser = ; stanza_parser =
Sexp.Of_sexp.(set_many parsing_context (sum lang.lang.stanzas)) Sexp.Of_sexp.(set_many parsing_context (sum lang.data))
; project_file = { file = Path.relative dir filename; exists = false } ; project_file = { file = Path.relative dir filename; exists = false }
} }

View File

@ -58,29 +58,17 @@ let setup ?(log=Log.no_log)
| None -> | None ->
match workspace_file with match workspace_file with
| Some p -> | Some p ->
if not (Path.exists p) then
die "@{<error>Error@}: workspace file %s does not exist"
(Path.to_string_maybe_quoted p);
Workspace.load ?x ?profile p Workspace.load ?x ?profile p
| _ -> | None ->
match match
let p = Path.of_string Workspace.filename in let p = Path.of_string Workspace.filename in
if Path.exists p then Option.some_if (Path.exists p) p
Some p
else
None
with with
| Some p -> Workspace.load ?x ?profile p | Some p -> Workspace.load ?x ?profile p
| None -> | None -> Workspace.default ?x ?profile ()
{ merlin_context = Some "default"
; contexts = [Default
{ targets = [
match x with
| None -> Native
| Some x -> Named x
]
; profile =
Option.value profile
~default:Config.default_build_profile
}]
}
in in
Fiber.parallel_map workspace.contexts ~f:(fun ctx_def -> Fiber.parallel_map workspace.contexts ~f:(fun ctx_def ->
@ -262,14 +250,7 @@ let bootstrap () =
Scheduler.go ~log ~config Scheduler.go ~log ~config
(set_concurrency config (set_concurrency config
>>= fun () -> >>= fun () ->
setup ~log ~workspace:{ merlin_context = Some "default" setup ~log ~workspace:(Workspace.default ?profile:!profile ())
; contexts = [Default { targets = [Native]
; profile =
Option.value !profile
~default:"dev"
}
]
}
?profile:!profile ?profile:!profile
~extra_ignored_subtrees:ignored_during_bootstrap ~extra_ignored_subtrees:ignored_during_bootstrap
() ()

View File

@ -309,6 +309,7 @@ let create
; "ext_lib" , string context.ext_lib ; "ext_lib" , string context.ext_lib
; "ext_dll" , string context.ext_dll ; "ext_dll" , string context.ext_dll
; "ext_exe" , string context.ext_exe ; "ext_exe" , string context.ext_exe
; "build_profile" , string context.profile
] ]
in in
let vars = let vars =

View File

@ -74,9 +74,12 @@ let check_supported t (loc, ver) =
(String.concat ~sep:"\n" (String.concat ~sep:"\n"
(List.map (Supported_versions.supported_ranges t.supported_versions) (List.map (Supported_versions.supported_ranges t.supported_versions)
~f:(fun (a, b) -> ~f:(fun (a, b) ->
sprintf "- %s to %s" if a = b then
(Version.to_string a) sprintf "- %s" (Version.to_string a)
(Version.to_string b)))) else
sprintf "- %s to %s"
(Version.to_string a)
(Version.to_string b))))
let greatest_supported_version t = let greatest_supported_version t =
Supported_versions.greatest_supported_version t.supported_versions Supported_versions.greatest_supported_version t.supported_versions

88
src/versioned_file.ml Normal file
View File

@ -0,0 +1,88 @@
open Import
module type S = sig
type data
module Lang : sig
val register : Syntax.t -> data -> unit
module Instance : sig
type t =
{ syntax : Syntax.t
; data : data
; version : Syntax.Version.t
}
end
val get_exn : string -> Instance.t
end
val load : Path.t -> f:(Lang.Instance.t -> 'a Sexp.Of_sexp.t) -> 'a
val parse_contents
: Lexing.lexbuf
-> Dune_lexer.first_line
-> f:(Lang.Instance.t -> 'a Sexp.Of_sexp.t)
-> 'a
end
module Make(Data : sig type t end) = struct
module Lang = struct
type t =
{ syntax : Syntax.t
; data : Data.t
}
module Instance = struct
type t =
{ syntax : Syntax.t
; data : Data.t
; version : Syntax.Version.t
}
end
let langs = Hashtbl.create 32
let register syntax data =
let name = Syntax.name syntax in
if Hashtbl.mem langs name then
Exn.code_error "Versioned_file.Lang.register: already registered"
[ "name", Sexp.To_sexp.string name ];
Hashtbl.add langs name { syntax; data }
let parse first_line : Instance.t =
let { Dune_lexer.
lang = (name_loc, name)
; version = (ver_loc, ver)
} = first_line
in
let ver =
Sexp.Of_sexp.parse Syntax.Version.t Univ_map.empty
(Atom (ver_loc, Sexp.Atom.of_string ver)) in
match Hashtbl.find langs name with
| None ->
Loc.fail name_loc "Unknown language %S.%s" name
(hint name (Hashtbl.keys langs))
| Some t ->
Syntax.check_supported t.syntax (ver_loc, ver);
{ syntax = t.syntax
; data = t.data
; version = ver
}
let get_exn name : Instance.t =
let t = Option.value_exn (Hashtbl.find langs name) in
{ syntax = t.syntax
; data = t.data
; version = Syntax.greatest_supported_version t.syntax
}
end
let parse_contents lb first_line ~f =
let lang = Lang.parse first_line in
let sexp = Sexp.Parser.parse lb ~mode:Many_as_one in
let parsing_context =
Univ_map.singleton (Syntax.key lang.syntax) lang.version
in
Sexp.Of_sexp.parse (Sexp.Of_sexp.enter (f lang)) parsing_context sexp
let load fn ~f =
Io.with_lexbuf_from_file fn ~f:(fun lb ->
parse_contents lb (Dune_lexer.first_line lb) ~f)
end

44
src/versioned_file.mli Normal file
View File

@ -0,0 +1,44 @@
(** Implementation of versioned files *)
open Stdune
module type S = sig
type data
module Lang : sig
(** [register id data] registers a new language. Users will select
this language by writing:
{[ (lang <name> <version>) ]}
as the first line of the versioned file. *)
val register : Syntax.t -> data -> unit
module Instance : sig
type t =
{ syntax : Syntax.t
; data : data
; version : Syntax.Version.t
}
end
(** Return the latest version of a language. *)
val get_exn : string -> Instance.t
end
(** [load fn ~f] loads a versioned file. It parses the first line,
looks up the language, checks that the version is supported and
parses the rest of the file with [f]. *)
val load : Path.t -> f:(Lang.Instance.t -> 'a Sexp.Of_sexp.t) -> 'a
(** Parse the contents of a versioned file after the first line has
been read. *)
val parse_contents
: Lexing.lexbuf
-> Dune_lexer.first_line
-> f:(Lang.Instance.t -> 'a Sexp.Of_sexp.t)
-> 'a
end
module Make(Data : sig type t end) : S with type data := Data.t

View File

@ -1,5 +1,9 @@
open Import open Import
open Sexp.Of_sexp open Stanza.Of_sexp
(* workspace files use the same version numbers as dune-project files
for simplicity *)
let syntax = Stanza.syntax
module Context = struct module Context = struct
module Target = struct module Target = struct
@ -11,11 +15,35 @@ module Context = struct
map string ~f:(function map string ~f:(function
| "native" -> Native | "native" -> Native
| s -> Named s) | s -> Named s)
let add ts x =
match x with
| None -> ts
| Some t ->
if List.mem t ~set:ts then
ts
else
ts @ [t]
end
module Name = struct
let t =
plain_string (fun ~loc name ->
if name = "" ||
String.is_prefix name ~prefix:"." ||
name = "log" ||
name = "install" ||
String.contains name '/' ||
String.contains name '\\' then
of_sexp_errorf loc
"%S is not allowed as a build context name" name;
name)
end end
module Opam = struct module Opam = struct
type t = type t =
{ name : string { loc : Loc.t
; name : string
; profile : string ; profile : string
; switch : string ; switch : string
; root : string option ; root : string option
@ -23,58 +51,69 @@ module Context = struct
; targets : Target.t list ; targets : Target.t list
} }
let t ~profile = let t ~profile ~x =
field "switch" string >>= fun switch -> field "switch" string >>= fun switch ->
field "name" string ~default:switch >>= fun name -> field "name" Name.t ~default:switch >>= fun name ->
field "targets" (list Target.t) ~default:[Target.Native] >>= fun targets -> field "targets" (list Target.t) ~default:[Target.Native] >>= fun targets ->
field_o "root" string >>= fun root -> field_o "root" string >>= fun root ->
field_b "merlin" >>= fun merlin -> field_b "merlin" >>= fun merlin ->
field "profile" string ~default:profile >>= fun profile -> field "profile" string ~default:profile >>= fun profile ->
return { switch loc >>= fun loc ->
return { loc
; switch
; name ; name
; root ; root
; merlin ; merlin
; targets ; targets = Target.add targets x
; profile ; profile
} }
end end
module Default = struct module Default = struct
type t = type t =
{ profile : string { loc : Loc.t
; profile : string
; targets : Target.t list ; targets : Target.t list
} }
let t ~profile = let t ~profile ~x =
field "targets" (list Target.t) ~default:[Target.Native] field "targets" (list Target.t) ~default:[Target.Native]
>>= fun targets -> >>= fun targets ->
field "profile" string ~default:profile field "profile" string ~default:profile
>>= fun profile -> >>= fun profile ->
return { targets; profile } loc
>>= fun loc ->
return { loc
; targets = Target.add targets x
; profile
}
end end
type t = Default of Default.t | Opam of Opam.t type t = Default of Default.t | Opam of Opam.t
let t ~profile = let loc = function
Sexp.Of_sexp.( | Default x -> x.loc
peek_exn >>= function | Opam x -> x.loc
| Atom _ | Quoted_string _ ->
enum [ "default", let t ~profile ~x =
Default { targets = [Native] sum
; profile [ "default",
} (fields (Default.t ~profile ~x) >>| fun x ->
] Default x)
| List (_, List _ :: _) -> ; "opam",
record (Opam.t ~profile) >>| fun x -> Opam x (fields (Opam.t ~profile ~x) >>| fun x ->
| _ -> Opam x)
sum ]
[ "default",
(fields (Default.t ~profile) >>| fun x -> let t ~profile ~x =
Default x) Syntax.get_exn syntax >>= function
; "opam", | (0, _) ->
(fields (Opam.t ~profile) >>| fun x -> (* jbuild-workspace files *)
Opam x) (peek_exn >>= function
]) | List (_, List _ :: _) ->
Sexp.Of_sexp.record (Opam.t ~profile ~x) >>| fun x -> Opam x
| _ -> t ~profile ~x)
| _ -> t ~profile ~x
let name = function let name = function
| Default _ -> "default" | Default _ -> "default"
@ -89,6 +128,14 @@ module Context = struct
n :: List.filter_map (targets t) ~f:(function n :: List.filter_map (targets t) ~f:(function
| Native -> None | Native -> None
| Named s -> Some (n ^ "." ^ s)) | Named s -> Some (n ^ "." ^ s))
let default ?x ?profile () =
Default
{ loc = Loc.of_pos __POS__
; targets = [Option.value x ~default:Target.Native]
; profile = Option.value profile
~default:Config.default_build_profile
}
end end
type t = type t =
@ -96,75 +143,32 @@ type t =
; contexts : Context.t list ; contexts : Context.t list
} }
type item = Context of Sexp.Ast.t | Profile of Loc.t * string include Versioned_file.Make(struct type t = unit end)
let () = Lang.register syntax ()
let item_of_sexp = let t ?x ?profile:cmdline_profile () =
sum field "profile" string ~default:Config.default_build_profile
[ "context", (raw >>|fun x -> Context x) >>= fun profile ->
; "profile", let profile = Option.value cmdline_profile ~default:profile in
(loc >>= fun loc -> multi_field "context" (Context.t ~profile ~x)
string >>= fun x -> >>= fun contexts ->
return (Profile (loc, x)))
]
let t ?x ?profile:cmdline_profile sexps =
let defined_names = ref String.Set.empty in let defined_names = ref String.Set.empty in
let profiles, contexts =
List.partition_map sexps ~f:(fun sexp ->
match Sexp.Of_sexp.parse item_of_sexp Univ_map.empty sexp with
| Profile (loc, p) -> Left (loc, p)
| Context c -> Right c)
in
let profile =
match profiles, cmdline_profile with
| _ :: (loc, _) :: _, _ ->
Loc.fail loc "profile defined too many times"
| _, Some p -> p
| [], None -> Config.default_build_profile
| [(_, p)], None -> p
in
let { merlin_context; contexts } = let { merlin_context; contexts } =
let init = let init =
{ merlin_context = None { merlin_context = None
; contexts = [] ; contexts = []
} }
in in
List.fold_left contexts ~init ~f:(fun t sexp -> List.fold_left contexts ~init ~f:(fun t ctx ->
let ctx = Sexp.Of_sexp.parse (Context.t ~profile) Univ_map.empty sexp in
let ctx =
match x with
| None -> ctx
| Some s ->
let target = Context.Target.Named s in
let add_target target targets =
if List.mem target ~set:targets then
targets
else
targets @ [target]
in
match ctx with
| Default d ->
Default { d with targets = add_target target d.targets }
| Opam o ->
Opam { o with targets = add_target target o.targets }
in
let name = Context.name ctx in let name = Context.name ctx in
if name = "" ||
String.is_prefix name ~prefix:"." ||
name = "log" ||
name = "install" ||
String.contains name '/' ||
String.contains name '\\' then
of_sexp_errorf (Sexp.Ast.loc sexp)
"%S is not allowed as a build context name" name;
if String.Set.mem !defined_names name then if String.Set.mem !defined_names name then
of_sexp_errorf (Sexp.Ast.loc sexp) Loc.fail (Context.loc ctx)
"second definition of build context %S" name; "second definition of build context %S" name;
defined_names := String.Set.union !defined_names defined_names := String.Set.union !defined_names
(String.Set.of_list (Context.all_names ctx)); (String.Set.of_list (Context.all_names ctx));
match ctx, t.merlin_context with match ctx, t.merlin_context with
| Opam { merlin = true; _ }, Some _ -> | Opam { merlin = true; _ }, Some _ ->
of_sexp_errorf (Sexp.Ast.loc sexp) Loc.fail (Context.loc ctx)
"you can only have one context for merlin" "you can only have one context for merlin"
| Opam { merlin = true; _ }, None -> | Opam { merlin = true; _ }, None ->
{ merlin_context = Some name; contexts = ctx :: t.contexts } { merlin_context = Some name; contexts = ctx :: t.contexts }
@ -173,7 +177,7 @@ let t ?x ?profile:cmdline_profile sexps =
in in
let contexts = let contexts =
match contexts with match contexts with
| [] -> [Context.Default { targets = [Native]; profile }] | [] -> [Context.default ?x ~profile ()]
| _ -> contexts | _ -> contexts
in in
let merlin_context = let merlin_context =
@ -186,11 +190,40 @@ let t ?x ?profile:cmdline_profile sexps =
else else
None None
in in
{ merlin_context return
; contexts = List.rev contexts { merlin_context
; contexts = List.rev contexts
}
let t ?x ?profile () = fields (t ?x ?profile ())
let default ?x ?profile () =
{ merlin_context = Some "default"
; contexts = [Context.default ?x ?profile ()]
} }
let load ?x ?profile p = t ?x ?profile (Io.Sexp.load p ~mode:Many) let load ?x ?profile p =
let x = Option.map x ~f:(fun s -> Context.Target.Named s) in
match Which_program.t with
| Dune ->
Io.with_lexbuf_from_file p ~f:(fun lb ->
if Dune_lexer.eof_reached lb then
default ?x ?profile ()
else
let first_line = Dune_lexer.first_line lb in
parse_contents lb first_line ~f:(fun _lang -> t ?x ?profile ()))
| Jbuilder ->
let sexp =
Io.Sexp.load p ~mode:Many_as_one ~lexer:Sexp.Lexer.jbuild_token
in
parse
(enter (t ?x ?profile ()))
(Univ_map.singleton (Syntax.key syntax) (0, 0))
sexp
let default ?x ?profile () =
let x = Option.map x ~f:(fun s -> Context.Target.Named s) in
default ?x ?profile ()
let filename = let filename =
match Which_program.t with match Which_program.t with

View File

@ -10,7 +10,8 @@ module Context : sig
end end
module Opam : sig module Opam : sig
type t = type t =
{ name : string { loc : Loc.t
; name : string
; profile : string ; profile : string
; switch : string ; switch : string
; root : string option ; root : string option
@ -21,7 +22,8 @@ module Context : sig
module Default : sig module Default : sig
type t = type t =
{ profile : string { loc : Loc.t
; profile : string
; targets : Target.t list ; targets : Target.t list
} }
end end
@ -40,3 +42,6 @@ val load : ?x:string -> ?profile:string -> Path.t -> t
(** Default name of workspace files *) (** Default name of workspace files *)
val filename : string val filename : string
(** Default configuration *)
val default : ?x:string -> ?profile:string -> unit -> t

View File

@ -579,6 +579,14 @@
test-cases/windows-diff test-cases/windows-diff
(progn (run %{exe:cram.exe} -test run.t) (diff? run.t run.t.corrected))))) (progn (run %{exe:cram.exe} -test run.t) (diff? run.t run.t.corrected)))))
(alias
(name workspaces)
(deps (package dune) (source_tree test-cases/workspaces))
(action
(chdir
test-cases/workspaces
(progn (run %{exe:cram.exe} -test run.t) (diff? run.t run.t.corrected)))))
(alias (alias
(name runtest) (name runtest)
(deps (deps
@ -648,7 +656,8 @@
(alias syntax-versioning) (alias syntax-versioning)
(alias use-meta) (alias use-meta)
(alias utop) (alias utop)
(alias windows-diff))) (alias windows-diff)
(alias workspaces)))
(alias (alias
(name runtest-no-deps) (name runtest-no-deps)
@ -710,7 +719,8 @@
(alias select) (alias select)
(alias syntax-versioning) (alias syntax-versioning)
(alias use-meta) (alias use-meta)
(alias windows-diff))) (alias windows-diff)
(alias workspaces)))
(alias (name runtest-disabled) (deps (alias reason))) (alias (name runtest-disabled) (deps (alias reason)))

View File

@ -0,0 +1,3 @@
(alias
(name runtest)
(action (echo "build profile: %{build_profile}")))

View File

@ -0,0 +1 @@
(lang dune 1.0)

View File

@ -0,0 +1,3 @@
(lang dune 1.0)
(context (default (profile foobar)))

View File

@ -0,0 +1,3 @@
(lang dune 1.0)
(context (does-not-exist))

View File

@ -0,0 +1 @@
(context (default))

View File

@ -0,0 +1 @@
(context (does-not-exist))

View File

@ -0,0 +1,8 @@
(lang dune 1.0)
(context
(opam
(switch foo-switch)
(name foo-name)
(profile foo-profile)
(merlin false)))

View File

@ -0,0 +1,50 @@
jbuild still discovers workspaces as usual
$ jbuilder build --root jbuilder-default-name
File "jbuild-workspace", line 1, characters 10-24:
Error: Unknown constructor does-not-exist
[1]
and dune ignores this file:
$ dune build --root jbuilder-default-name
Entering directory 'jbuilder-default-name'
dune uses a versioned file. If the version is missing, then we get an error.
$ dune build --root dune-no-version
File "dune-workspace", line 1, characters 0-19:
Error: Invalid first line, expected: (lang <lang> <version>)
[1]
analogously, jbuilder will ignore it
$ jbuilder build --root dune-no-version
Entering directory 'dune-no-version'
specifying the workspace file is possible:
$ dune build --root custom-workspace --workspace custom-workspace/dune-workspace.dev
Error: workspace file custom-workspace/dune-workspace.dev does not exist
[1]
Workspaces let you set custom profiles
$ dune runtest --root custom-profile
Entering directory 'custom-profile'
build profile: foobar
A workspace context can ve defined using an opam switch. This test is disabled
because we don't really have a way to mock an opam switch.
# $ dune build --root opam --display quiet 2>&1
Workspaces also allow you to set "target" for cross compilation. This feature is
a bit hard to test since it requires mocking more than one context. But we can
see how we can set a "native" target. Which is the default.
$ dune exec ./foo.exe --root targets-native
Info: creating file dune-project with this contents: (lang dune 1.0)
Entering directory 'targets-native'
Entering directory 'targets-native'
message from targets-native test

View File

@ -0,0 +1,2 @@
(executable (name foo))

View File

@ -0,0 +1,3 @@
(lang dune 1.0)
(context (default (targets native)))

View File

@ -0,0 +1 @@
print_endline "message from targets-native test";;