First commit
This commit is contained in:
commit
bee9f14226
19 changed files with 1570 additions and 0 deletions
13
.ocamlformat
Normal file
13
.ocamlformat
Normal file
|
@ -0,0 +1,13 @@
|
|||
version=0.27.0
|
||||
exp-grouping=preserve
|
||||
break-infix=wrap-or-vertical
|
||||
break-collection-expressions=wrap
|
||||
break-sequences=false
|
||||
break-infix-before-func=false
|
||||
dock-collection-brackets=true
|
||||
break-separators=before
|
||||
field-space=tight
|
||||
if-then-else=compact
|
||||
break-sequences=false
|
||||
sequence-blank-line=compact
|
||||
exp-grouping=preserve
|
6
TODO.md
Normal file
6
TODO.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
- [ ] be able to load *.cmx{,a} when we use "#require"
|
||||
`findlib`/`topfind` only loads directories, `ocamlnat` wants to load files.
|
||||
Let's use our work about `uniq` to solve dependencies and load artifacts
|
||||
- [ ] do some tests with Miou and see if we can execute small applications
|
||||
- [ ] start to eval a file (and show the result?)
|
||||
- [ ] start to make a nice intf for a HTTP server via `httpcats`
|
6
bin/dune
Normal file
6
bin/dune
Normal file
|
@ -0,0 +1,6 @@
|
|||
(executable
|
||||
(name vif)
|
||||
(public_name vif)
|
||||
(flags
|
||||
(:standard -w -18))
|
||||
(libraries logs.fmt fmt.tty vif.top))
|
51
bin/vif.ml
Normal file
51
bin/vif.ml
Normal file
|
@ -0,0 +1,51 @@
|
|||
let reporter ppf =
|
||||
let report src level ~over k msgf =
|
||||
let k _ = over (); k () in
|
||||
let with_metadata header _tags k ppf fmt =
|
||||
Format.kfprintf k ppf
|
||||
("%a[%a]: " ^^ fmt ^^ "\n%!")
|
||||
Logs_fmt.pp_header (level, header)
|
||||
Fmt.(styled `Magenta string)
|
||||
(Logs.Src.name src)
|
||||
in
|
||||
msgf @@ fun ?header ?tags fmt -> with_metadata header tags k ppf fmt
|
||||
in
|
||||
{ Logs.report }
|
||||
|
||||
let () = Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ~utf_8:true ()
|
||||
let () = Logs.set_reporter (reporter Fmt.stdout)
|
||||
let () = Logs.set_level ~all:true (Some Logs.Debug)
|
||||
|
||||
let main =
|
||||
[
|
||||
{ocaml|let v = 42;;|ocaml}
|
||||
; {ocaml|let rec infinity () = infinity ();;|ocaml}
|
||||
; {ocaml|print_endline "Hello World!";;|ocaml}
|
||||
; {ocaml|let a = Bool.to_int true;;|ocaml}
|
||||
; {ocaml|#show_dirs;;|ocaml}
|
||||
; {ocaml|#require "miou";;|ocaml}
|
||||
; {ocaml|let fn () = print_endline "Hello from Miou!";;|ocaml}
|
||||
; {ocaml|Miou.run fn;;|ocaml}
|
||||
]
|
||||
|
||||
let stdlib = Fpath.v "/home/dinosaure/.opam/5.2.0+ocamlnat/lib/ocaml/"
|
||||
|
||||
let run roots =
|
||||
let cfg = Vif_top.config ~stdlib roots in
|
||||
match Vif_top.eval cfg main with
|
||||
| Ok sstr -> List.iter print_endline sstr
|
||||
| Error sstr -> List.iter prerr_endline sstr
|
||||
|
||||
open Cmdliner
|
||||
|
||||
let term =
|
||||
let open Term in
|
||||
const run $ Vif_meta.setup
|
||||
|
||||
let cmd =
|
||||
let doc = "vif" in
|
||||
let man = [] in
|
||||
let info = Cmd.info "vif" ~doc ~man in
|
||||
Cmd.v info term
|
||||
|
||||
let () = Cmd.(exit @@ eval cmd)
|
2
dune-project
Normal file
2
dune-project
Normal file
|
@ -0,0 +1,2 @@
|
|||
(lang dune 3.0)
|
||||
(name vif)
|
17
lib/digest.ml
Normal file
17
lib/digest.ml
Normal file
|
@ -0,0 +1,17 @@
|
|||
include Stdlib.Digest
|
||||
|
||||
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
|
||||
let pp ppf t = Fmt.string ppf (Stdlib.Digest.to_hex t)
|
||||
|
||||
let of_string str =
|
||||
match of_hex str with
|
||||
| v -> Ok v
|
||||
| exception Invalid_argument _ -> error_msgf "Invalid digest value: %S" str
|
||||
|
||||
let length = String.length (string "")
|
||||
|
||||
module Map = Map.Make (struct
|
||||
type nonrec t = t
|
||||
|
||||
let compare = compare
|
||||
end)
|
7
lib/digest.mli
Normal file
7
lib/digest.mli
Normal file
|
@ -0,0 +1,7 @@
|
|||
include module type of Stdlib.Digest
|
||||
|
||||
val pp : t Fmt.t
|
||||
val of_string : string -> (t, [> `Msg of string ]) result
|
||||
val length : int
|
||||
|
||||
module Map : Map.S with type key = t
|
27
lib/dune
Normal file
27
lib/dune
Normal file
|
@ -0,0 +1,27 @@
|
|||
(library
|
||||
(name vif_top)
|
||||
(modules vif_top)
|
||||
(public_name vif.top)
|
||||
(modes native)
|
||||
(libraries
|
||||
vif.meta
|
||||
compiler-libs
|
||||
compiler-libs.common
|
||||
compiler-libs.native-toplevel))
|
||||
|
||||
(ocamllex vif_meta_lexer)
|
||||
|
||||
(library
|
||||
(name vif_meta)
|
||||
(modules vif_meta vif_meta_lexer objinfo digest modname unitname)
|
||||
(public_name vif.meta)
|
||||
(modes native)
|
||||
(libraries
|
||||
compiler-libs
|
||||
compiler-libs.common
|
||||
fmt
|
||||
bos
|
||||
logs
|
||||
unix
|
||||
fpath
|
||||
cmdliner))
|
37
lib/modname.ml
Normal file
37
lib/modname.ml
Normal file
|
@ -0,0 +1,37 @@
|
|||
type t = string
|
||||
|
||||
let msgf fmt = Format.kasprintf (fun msg -> `Msg msg) fmt
|
||||
|
||||
let for_all f str =
|
||||
let rec go acc idx =
|
||||
if idx < String.length str then go (f str.[idx] && acc) (succ idx) else acc
|
||||
in
|
||||
go true 0
|
||||
|
||||
let is_upper = function 'A' .. 'Z' -> true | _ -> false
|
||||
let is_lower = function 'a' .. 'z' -> true | _ -> false
|
||||
|
||||
let is_valid_module_char = function
|
||||
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '\'' -> true
|
||||
| '-' ->
|
||||
true
|
||||
(* XXX(dinosaure): an example exists: [First-class-modules].
|
||||
[ocamlopt] can compile it but it emits an warning. *)
|
||||
| _ -> false
|
||||
|
||||
let of_string str =
|
||||
if String.length str < 1 then Error (msgf "Invalid empty module name")
|
||||
else if
|
||||
(is_upper str.[0] || is_lower str.[0]) && for_all is_valid_module_char str
|
||||
then Ok str
|
||||
else Error (msgf "Invalid module name: %S" str)
|
||||
|
||||
let v str =
|
||||
match of_string str with Ok v -> v | Error (`Msg err) -> failwith err
|
||||
|
||||
let pp ppf t = Format.pp_print_string ppf t
|
||||
let reflect ppf t = Fmt.pf ppf "(Modname.v %S)" t
|
||||
let to_string v = v
|
||||
let compare = String.compare
|
||||
|
||||
module Map = Map.Make (String)
|
42
lib/modname.mli
Normal file
42
lib/modname.mli
Normal file
|
@ -0,0 +1,42 @@
|
|||
(** Module name.
|
||||
|
||||
the module name is a [string] that respects certain predicates. It is
|
||||
impossible to have any other characters than:
|
||||
- ['a' .. 'z']
|
||||
- ['A' .. 'Z']
|
||||
- ['0' .. '9']
|
||||
- ['_'], ['-'] or ['\'']
|
||||
|
||||
{b NOTE}: The dash is accepted even if it is normally prohibited. You can
|
||||
compile a module [my-module.ml]. However, the compiler issues a warning. In
|
||||
order to be the least restrictive, we accept this character.
|
||||
|
||||
The module name must start with a letter.
|
||||
|
||||
{b NOTE}: The name of a {i module type} can start with a non-capital letter:
|
||||
|
||||
{[
|
||||
module type foo = sig end
|
||||
]} *)
|
||||
|
||||
type t
|
||||
(** The type of module names. *)
|
||||
|
||||
val of_string : string -> (t, [> `Msg of string ]) result
|
||||
(** [of_string str] validates the given [str] as a module name or return an
|
||||
error with an explanation. *)
|
||||
|
||||
val v : string -> t
|
||||
(** [v str] calls {!val:of_string}. It raises an [Invalid_argument] instead of
|
||||
returning an error. *)
|
||||
|
||||
val pp : t Fmt.t
|
||||
(** Pretty printer of {!type:t}. *)
|
||||
|
||||
val to_string : t -> string
|
||||
(** [to_string t] casts the given [t] as a [string]. *)
|
||||
|
||||
val compare : t -> t -> int
|
||||
val reflect : t Fmt.t
|
||||
|
||||
module Map : Map.S with type key = t
|
368
lib/objinfo.ml
Normal file
368
lib/objinfo.ml
Normal file
|
@ -0,0 +1,368 @@
|
|||
let src = Logs.Src.create "uniq.info"
|
||||
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
|
||||
|
||||
[@@@warning "-32"]
|
||||
|
||||
let ( % ) f g x = f (g x)
|
||||
|
||||
type t = {
|
||||
name: Unitname.t
|
||||
; version: int option
|
||||
; exports: (Modname.t * Digest.t option) list
|
||||
; intfs: elt list
|
||||
; impls: elt list
|
||||
; format: format
|
||||
}
|
||||
|
||||
and elt =
|
||||
| Qualified of Modname.t * Digest.t
|
||||
| Fully_qualified of Modname.t * Digest.t * Fpath.t
|
||||
| Located of Modname.t * Fpath.t
|
||||
| Named of Modname.t
|
||||
|
||||
and 'a kind =
|
||||
| Cmo : Cmo_format.compilation_unit kind
|
||||
| Cma : Cmo_format.library kind
|
||||
| Cmi : Cmi_format.cmi_infos kind
|
||||
| Cmx : Cmx_format.unit_infos kind
|
||||
| Cmxa : Cmx_format.library_infos kind
|
||||
|
||||
and format = Format : 'a kind * 'a -> format
|
||||
|
||||
let equal a b =
|
||||
String.equal (Unitname.filepath a.name) (Unitname.filepath b.name)
|
||||
|
||||
exception Inconsistency of Unitname.t * Modname.t * Digest.t * Digest.t
|
||||
|
||||
let inconsistency location name crc crc' =
|
||||
let unit = Unitname.modulize (Fpath.to_string location) in
|
||||
raise (Inconsistency (unit, name, crc, crc'))
|
||||
|
||||
let is_fully_resolved t =
|
||||
List.for_all (function Fully_qualified _ -> true | _ -> false) t.intfs
|
||||
&& List.for_all (function Fully_qualified _ -> true | _ -> false) t.impls
|
||||
|
||||
let is_a_library t =
|
||||
match t.format with
|
||||
| Format (Cma, _) -> true
|
||||
| Format (Cmxa, _) -> true
|
||||
| _ -> false
|
||||
|
||||
let is_native t =
|
||||
match t.format with
|
||||
| Format (Cmx, _) -> true
|
||||
| Format (Cmxa, _) -> true
|
||||
| Format (Cmi, _) -> true
|
||||
| _ -> false
|
||||
|
||||
let is_an_interface t =
|
||||
match t.format with Format (Cmi, _) -> true | _ -> false
|
||||
|
||||
let is_a_cmi t = match t.format with Format (Cmi, _) -> true | _ -> false
|
||||
|
||||
let of_elt = function
|
||||
| Qualified (m, crc) -> (m, Some crc)
|
||||
| Fully_qualified (m, crc, _) -> (m, Some crc)
|
||||
| Located (m, _) -> (m, None)
|
||||
| Named m -> (m, None)
|
||||
|
||||
let exports t = t.exports
|
||||
let location t = Fpath.v (Unitname.filepath t.name)
|
||||
let intfs_imported t = List.map of_elt t.intfs
|
||||
let impls_imported t = List.map of_elt t.impls
|
||||
let modname t = Unitname.modname t.name
|
||||
|
||||
let missing t =
|
||||
let intfs, _ =
|
||||
List.partition_map
|
||||
(function
|
||||
| Named m | Qualified (m, _) -> Either.Left m | _ -> Either.Right ())
|
||||
t.intfs
|
||||
in
|
||||
let impls, _ =
|
||||
List.partition_map
|
||||
(function
|
||||
| Named m | Qualified (m, _) -> Either.Left m | _ -> Either.Right ())
|
||||
t.impls
|
||||
in
|
||||
(intfs, impls)
|
||||
|
||||
let crc_of t m =
|
||||
let ( >>| ) x f = Stdlib.Option.map f x in
|
||||
List.find_opt (fun (m', _) -> Modname.compare m m' = 0) t.exports
|
||||
>>| snd
|
||||
|> Option.join
|
||||
|
||||
let kind t =
|
||||
match t.format with
|
||||
| Format (Cmo, _) | Format (Cma, _) | Format (Cmx, _) | Format (Cmxa, _) ->
|
||||
`Impl
|
||||
| Format (Cmi, _) -> `Intf
|
||||
|
||||
let elt_name = function
|
||||
| Qualified (x, _) | Fully_qualified (x, _, _) | Named x | Located (x, _) -> x
|
||||
|
||||
let elt_replace elt elts =
|
||||
List.fold_left
|
||||
(fun acc elt' ->
|
||||
let a = elt_name elt in
|
||||
let b = elt_name elt' in
|
||||
if Modname.compare a b = 0 then elt :: acc else elt' :: acc)
|
||||
[] elts
|
||||
|> List.rev
|
||||
|
||||
let qualify t ?location ?crc kind modname =
|
||||
let elt =
|
||||
match (location, crc) with
|
||||
| None, Some crc -> Qualified (modname, crc)
|
||||
| Some location, Some crc -> Fully_qualified (modname, crc, location)
|
||||
| None, None -> Named modname
|
||||
| Some location, None -> Located (modname, location)
|
||||
in
|
||||
match kind with
|
||||
| `Intf -> { t with intfs= elt_replace elt t.intfs }
|
||||
| `Impl -> { t with impls= elt_replace elt t.impls }
|
||||
|
||||
let elt_compare a b =
|
||||
let a = elt_name a in
|
||||
let b = elt_name b in
|
||||
Modname.compare a b
|
||||
|
||||
let elt_find modname lst =
|
||||
try
|
||||
List.find
|
||||
(fun elt ->
|
||||
let modname' = elt_name elt in
|
||||
Modname.compare modname modname' = 0)
|
||||
lst
|
||||
|> function
|
||||
| Qualified (_, crc) -> [ (modname, Some crc) ]
|
||||
| Fully_qualified (_, crc, _) -> [ (modname, Some crc) ]
|
||||
| Named _ -> [ (modname, None) ]
|
||||
| Located _ -> [ (modname, None) ]
|
||||
with Not_found -> []
|
||||
|
||||
open Bos
|
||||
open Rresult
|
||||
|
||||
let to_elt (str, crc) =
|
||||
match crc with
|
||||
| Some crc -> Qualified (Modname.v str, crc)
|
||||
| None -> Named (Modname.v str)
|
||||
|
||||
let info_of_cmi ~location ~version _ic =
|
||||
match Cmt_format.read (Fpath.to_string location) with
|
||||
| None, _ -> error_msgf "Invalid cmi object: %a" Fpath.pp location
|
||||
| Some cmi, _ ->
|
||||
let intfs = cmi.Cmi_format.cmi_crcs in
|
||||
let intfs = List.map to_elt intfs in
|
||||
let intfs = List.sort elt_compare intfs in
|
||||
let impls = [] in
|
||||
let format = Format (Cmi, cmi) in
|
||||
let name = Unitname.modulize (Fpath.to_string location) in
|
||||
let exports = elt_find (Unitname.modname name) intfs in
|
||||
Ok { name; version; exports; intfs; impls; format }
|
||||
| exception _ -> error_msgf "Invalid cmi object: %a" Fpath.pp location
|
||||
|
||||
let info_of_cmo ~location ~version ic =
|
||||
let cu_pos = input_binary_int ic in
|
||||
seek_in ic cu_pos;
|
||||
let cu = (input_value ic : Cmo_format.compilation_unit) in
|
||||
let intfs = List.map to_elt cu.cu_imports in
|
||||
let intfs = List.sort elt_compare intfs in
|
||||
let impls = [] in
|
||||
let format = Format (Cmo, cu) in
|
||||
let name = Unitname.modulize (Fpath.to_string location) in
|
||||
let exports = [ (Unitname.modname name, None) ] in
|
||||
Ok { name; version; exports; intfs; impls; format }
|
||||
|
||||
let info_of_cmx ~location ~version ic =
|
||||
let ui = (input_value ic : Cmx_format.unit_infos) in
|
||||
let name = Unitname.modulize (Fpath.to_string location) in
|
||||
let exports = [ (Unitname.modname name, Some (Digest.input ic)) ] in
|
||||
let intfs = List.map to_elt ui.ui_imports_cmi in
|
||||
let intfs = List.sort elt_compare intfs in
|
||||
let impls = List.map to_elt ui.ui_imports_cmx in
|
||||
let impls = List.sort elt_compare impls in
|
||||
let format = Format (Cmx, ui) in
|
||||
Ok { name; version; exports; intfs; impls; format }
|
||||
|
||||
let to_elt (modname, crc) =
|
||||
match crc with Some crc -> Qualified (modname, crc) | None -> Named modname
|
||||
|
||||
let info_of_cma ~location ~version ic =
|
||||
let toc_pos = input_binary_int ic in
|
||||
seek_in ic toc_pos;
|
||||
let toc = (input_value ic : Cmo_format.library) in
|
||||
let importss =
|
||||
List.map (fun { Cmo_format.cu_imports; _ } -> cu_imports) toc.lib_units
|
||||
in
|
||||
let fold m (str, crc) =
|
||||
let name = Modname.v str in
|
||||
match (Modname.Map.find_opt name m, crc) with
|
||||
| None, _ -> Modname.Map.add name crc m
|
||||
| Some None, _ | Some (Some _), None ->
|
||||
Modname.Map.add name crc (Modname.Map.remove name m)
|
||||
| Some (Some crc'), Some crc ->
|
||||
if crc <> crc' then inconsistency location name crc crc';
|
||||
m
|
||||
in
|
||||
let imports = List.concat importss in
|
||||
let m = List.fold_left fold Modname.Map.empty imports in
|
||||
let exports =
|
||||
List.map
|
||||
(fun { Cmo_format.cu_name= Compunit cu_name; _ } ->
|
||||
(Modname.v cu_name, None))
|
||||
toc.lib_units
|
||||
in
|
||||
let intfs = Modname.Map.bindings m in
|
||||
let intfs = List.map to_elt intfs in
|
||||
let impls = [] in
|
||||
let format = Format (Cma, toc) in
|
||||
let name = Unitname.modulize (Fpath.to_string location) in
|
||||
Ok { name; version; exports; intfs; impls; format }
|
||||
|
||||
let info_of_cmxa ~location ~version ic =
|
||||
let li = (input_value ic : Cmx_format.library_infos) in
|
||||
let importss_cmi =
|
||||
List.map
|
||||
(fun ({ Cmx_format.ui_imports_cmi; _ }, _crc) -> ui_imports_cmi)
|
||||
li.lib_units
|
||||
in
|
||||
let importss_cmx =
|
||||
List.map
|
||||
(fun ({ Cmx_format.ui_imports_cmx; _ }, _crc) -> ui_imports_cmx)
|
||||
li.lib_units
|
||||
in
|
||||
let fold m (str, crc) =
|
||||
let name = Modname.v str in
|
||||
match (Modname.Map.find_opt name m, crc) with
|
||||
| None, _ -> Modname.Map.add name crc m
|
||||
| Some None, _ | Some (Some _), None ->
|
||||
Modname.Map.add name crc (Modname.Map.remove name m)
|
||||
| Some (Some crc'), Some crc ->
|
||||
if crc <> crc' then inconsistency location name crc crc';
|
||||
m
|
||||
in
|
||||
let exports =
|
||||
List.map
|
||||
(fun ({ Cmx_format.ui_name; _ }, crc) -> (Modname.v ui_name, Some crc))
|
||||
li.lib_units
|
||||
in
|
||||
let m = List.fold_left fold Modname.Map.empty (List.concat importss_cmi) in
|
||||
let intfs = Modname.Map.bindings m in
|
||||
let intfs = List.map to_elt intfs in
|
||||
let m = List.fold_left fold Modname.Map.empty (List.concat importss_cmx) in
|
||||
let impls = Modname.Map.bindings m in
|
||||
let impls = List.map to_elt impls in
|
||||
let format = Format (Cmxa, li) in
|
||||
let name = Unitname.modulize (Fpath.to_string location) in
|
||||
Ok { name; version; exports; intfs; impls; format }
|
||||
|
||||
let is_intf location = Fpath.mem_ext [ ".mli" ] location
|
||||
|
||||
let from_object location { Misc.Magic_number.kind; version } ic =
|
||||
let version = Some version in
|
||||
match kind with
|
||||
| Misc.Magic_number.Cmi -> info_of_cmi ~location ~version ic
|
||||
| Cmo -> info_of_cmo ~location ~version ic
|
||||
| Cma -> info_of_cma ~location ~version ic
|
||||
| Cmx _ -> info_of_cmx ~location ~version ic
|
||||
| Cmxa _ -> info_of_cmxa ~location ~version ic
|
||||
| _ -> error_msgf "Unexpected OCaml object: %a" Fpath.pp location
|
||||
|
||||
let v location =
|
||||
OS.File.with_ic location @@ fun ic () ->
|
||||
match Misc.Magic_number.read_info ic with
|
||||
| Ok info -> from_object location info ic
|
||||
| Error _ -> error_msgf "Invalid object: %a" Fpath.pp location
|
||||
|
||||
let pp ppf t = Fmt.string ppf (Unitname.filepath t.name)
|
||||
|
||||
let v location =
|
||||
match v location () |> R.join with
|
||||
| value -> value
|
||||
| exception Inconsistency (unit, name, crc, crc') ->
|
||||
error_msgf
|
||||
"Inconsistency between interfaces:\n\
|
||||
The given library %s requires two times the interface %a with\n\
|
||||
1) the digest %a\n\
|
||||
2) and the digest %a\n"
|
||||
(Unitname.filepath unit) Modname.pp name Digest.pp crc Digest.pp crc'
|
||||
|
||||
let vs lst =
|
||||
let ( let* ) = Result.bind in
|
||||
let fn acc path =
|
||||
match acc with
|
||||
| Error _ as err -> err
|
||||
| Ok acc ->
|
||||
let* a = v path in
|
||||
Ok (a :: acc)
|
||||
in
|
||||
List.fold_left fn (Ok []) lst
|
||||
|
||||
let dummy = String.make Digest.length '-'
|
||||
|
||||
let show_elt ppf = function
|
||||
| Qualified (name, crc) ->
|
||||
Fmt.pf ppf "\t%a\t%a\n%!"
|
||||
Fmt.(styled `Bold Digest.pp)
|
||||
crc
|
||||
Fmt.(styled `Yellow Modname.pp)
|
||||
name
|
||||
| Fully_qualified (name, crc, location) ->
|
||||
Fmt.pf ppf "\t%a\t%a (%a)\n%!"
|
||||
Fmt.(styled `Bold Digest.pp)
|
||||
crc
|
||||
Fmt.(styled `Yellow Modname.pp)
|
||||
name
|
||||
Fmt.(styled `Green Fpath.pp)
|
||||
location
|
||||
| Named name ->
|
||||
Fmt.pf ppf "\t%a\t%a\n%!"
|
||||
Fmt.(styled `Bold string)
|
||||
dummy
|
||||
Fmt.(styled `Yellow Modname.pp)
|
||||
name
|
||||
| Located (name, location) ->
|
||||
Fmt.pf ppf "\t%a\t%a (%a)\n%!"
|
||||
Fmt.(styled `Bold string)
|
||||
dummy
|
||||
Fmt.(styled `Yellow Modname.pp)
|
||||
name
|
||||
Fmt.(styled `Green Fpath.pp)
|
||||
location
|
||||
|
||||
let show_export ppf (name, crc) =
|
||||
match crc with
|
||||
| None ->
|
||||
Fmt.pf ppf "\t%a\t%a\n%!"
|
||||
Fmt.(styled `Bold string)
|
||||
dummy
|
||||
Fmt.(styled `Yellow Modname.pp)
|
||||
name
|
||||
| Some crc ->
|
||||
Fmt.pf ppf "\t%a\t%a\n%!"
|
||||
Fmt.(styled `Bold Digest.pp)
|
||||
crc
|
||||
Fmt.(styled `Yellow Modname.pp)
|
||||
name
|
||||
|
||||
let show ppf t =
|
||||
Fmt.pf ppf "File: %a\n%!"
|
||||
Fmt.(styled `Green string)
|
||||
(Unitname.filepath t.name);
|
||||
Fmt.pf ppf "Name: %a\n%!"
|
||||
Fmt.(styled `Yellow Modname.pp)
|
||||
(Unitname.modname t.name);
|
||||
if Stdlib.Option.is_some t.version then
|
||||
Fmt.pf ppf "Version: %d\n%!" (Stdlib.Option.get t.version);
|
||||
if t.intfs <> [] then Fmt.pf ppf "Interfaces imported:\n%!";
|
||||
List.iter (Fmt.pf ppf "%a" show_elt) t.intfs;
|
||||
if t.impls <> [] then Fmt.pf ppf "Implementations imported:\n%!";
|
||||
List.iter (Fmt.pf ppf "%a" show_elt) t.impls;
|
||||
Fmt.pf ppf "Export:\n%!";
|
||||
List.iter (Fmt.pf ppf "%a" show_export) t.exports
|
48
lib/unitname.ml
Normal file
48
lib/unitname.ml
Normal file
|
@ -0,0 +1,48 @@
|
|||
type t = { modname: Modname.t; filepath: string }
|
||||
(* TODO(dinosaure): we probably should validate [filename] too as regular
|
||||
file path. *)
|
||||
|
||||
let invalid_arg fmt = Format.kasprintf invalid_arg fmt
|
||||
let is_upper = function 'A' .. 'Z' -> true | _ -> false
|
||||
let is_lower = function 'a' .. 'z' -> true | _ -> false
|
||||
|
||||
let modulize filepath =
|
||||
let filename = Filename.basename filepath in
|
||||
let name = Filename.remove_extension filename in
|
||||
if String.length name < 1 then
|
||||
invalid_arg "Impossible to modulize an empty string";
|
||||
if not (is_upper name.[0] || is_lower name.[0]) then
|
||||
invalid_arg "Impossible to modulize %S (from %S)" name filename;
|
||||
let modname = String.capitalize_ascii name in
|
||||
let modname = Modname.v modname in
|
||||
{ modname; filepath }
|
||||
|
||||
let pp ppf { modname; filepath } =
|
||||
Fmt.pf ppf "%a(%s)" Modname.pp modname filepath
|
||||
|
||||
let pp_as_modname ppf { modname; _ } = Modname.pp ppf modname
|
||||
let pp_as_filepath ppf { filepath; _ } = Format.pp_print_string ppf filepath
|
||||
let reflect ppf t = Fmt.pf ppf "(Unitname.modulize %S)" t.filepath
|
||||
let modname { modname; _ } = modname
|
||||
let filename { filepath; _ } = Filename.basename filepath
|
||||
let filepath { filepath; _ } = filepath
|
||||
let compare_as_modnames a b = Modname.compare a.modname b.modname
|
||||
|
||||
let change_file_extension f t =
|
||||
match Filename.extension t.filepath with
|
||||
| "" -> t
|
||||
| ext ->
|
||||
let filepath = Filename.remove_extension t.filepath ^ "." ^ f ext in
|
||||
{ t with filepath }
|
||||
|
||||
module Map = Map.Make (struct
|
||||
type nonrec t = t
|
||||
|
||||
let compare a b = Modname.compare a.modname b.modname
|
||||
end)
|
||||
|
||||
module Set = Set.Make (struct
|
||||
type nonrec t = t
|
||||
|
||||
let compare a b = Modname.compare a.modname b.modname
|
||||
end)
|
74
lib/unitname.mli
Normal file
74
lib/unitname.mli
Normal file
|
@ -0,0 +1,74 @@
|
|||
(** The Unit name is the name of a module which can be represented by a file.
|
||||
The unit name {b transforms} the given file path into a {!type:Modname.t}
|
||||
and keep the file path internally.
|
||||
|
||||
By this way, an {!t} as two views:
|
||||
- as a file path (where the module is located into the file-system)
|
||||
- as a module name *)
|
||||
|
||||
type t
|
||||
(** Type of an unit name. *)
|
||||
|
||||
val modulize : string -> t
|
||||
(** [modulize filename] makes a new {!type:t} which contains the given
|
||||
[filename] and {i modulize} it: it removes the extension (if it exists), it
|
||||
capitalises the first letter of the [filename]'s {!val:Filename.basename}
|
||||
and replace any wrong characters by ['_'].
|
||||
|
||||
For instance:
|
||||
- ["foo.ml"] => ["Foo"]
|
||||
- ["Foo"] => ["Foo"]
|
||||
- ["foo'.ml"] => ["Foo_"]
|
||||
- ["lib/foo.ml"] => ["Foo"]
|
||||
- ["foo-bar.ml"] => ["Foo-bar"]
|
||||
|
||||
We assert that:
|
||||
{[
|
||||
# let v = Unitname.modulize "foo.ml" ;;
|
||||
# assert (Stdlib.compare v Unitname.(modulize (filepath v)) = 0) ;;
|
||||
]} *)
|
||||
|
||||
val modname : t -> Modname.t
|
||||
|
||||
val filename : t -> string
|
||||
(** [filename v] returns the {b filename} of the given unit name. The filename
|
||||
is the {!val:Filename.basename} of the [filepath] given to construct [v]
|
||||
with {!val:modulize}. *)
|
||||
|
||||
val filepath : t -> string
|
||||
(** [filepath v] returns the {b filepath} of the given unit name. The file path
|
||||
is the one used to construct [v] with {!val:modulize}. *)
|
||||
|
||||
val pp : t Fmt.t
|
||||
val pp_as_modname : t Fmt.t
|
||||
val pp_as_filepath : t Fmt.t
|
||||
val reflect : t Fmt.t
|
||||
|
||||
val compare_as_modnames : t -> t -> int
|
||||
(** [compare_as_modnames a b] compares [a] and [b] from their modname's views.
|
||||
For instance,
|
||||
|
||||
{[
|
||||
# let a = Unitname.modulize "foo/a.ml" ;;
|
||||
# let b = Unitname.modulize "bar/a.ml" ;;
|
||||
# Unitname.compare_as_modnames a b ;;
|
||||
- : int = 0
|
||||
]} *)
|
||||
|
||||
val change_file_extension : (string -> string) -> t -> t
|
||||
(** [change_file_extension f t] tries to replace the current extension of the
|
||||
given [t] (the {i filename} view) by something else returned by [f]. If [t]
|
||||
has no extension, we return it unchanged. Otherwise, we call [f] and give to
|
||||
it the current extension.
|
||||
|
||||
{b NOTE}: The {i modname} view is {b unchanged} in any cases:
|
||||
{[
|
||||
# let v0 = Unitname.modulize "foo.ml" ;;
|
||||
# let v1 = Unitname.change_file_extension (fun _ -> "mli") v0 ;;
|
||||
# assert (Modname.compare (Unitname.modname v0) (Unitname.modname v1) = 0) ;;
|
||||
]}
|
||||
|
||||
However, [v0] and [v1] are not equal anymore. *)
|
||||
|
||||
module Map : Map.S with type key = t
|
||||
module Set : Set.S with type elt = t
|
399
lib/vif_meta.ml
Normal file
399
lib/vif_meta.ml
Normal file
|
@ -0,0 +1,399 @@
|
|||
let src = Logs.Src.create "uniq.meta"
|
||||
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
|
||||
|
||||
type t =
|
||||
| Node of { name: string; value: string; contents: t list }
|
||||
(* [name] "[value]" ( [contents] ),
|
||||
like [package "lib" ( ... )] *)
|
||||
| Set of { name: string; predicates: predicate list; value: string }
|
||||
(* [name] [(...) as predicates] = [value],
|
||||
like [archive(native) = "lib.cmxa"]*)
|
||||
| Add of { name: string; predicates: predicate list; value: string }
|
||||
(* [name] [(...) as predicates] = [value],
|
||||
like [archive(native) += "lib.cmxa"]*)
|
||||
|
||||
and predicate = Include of string | Exclude of string
|
||||
|
||||
let pp_predicate ppf = function
|
||||
| Include p -> Fmt.string ppf p
|
||||
| Exclude p -> Fmt.pf ppf "-%s" p
|
||||
|
||||
let rec pp ppf = function
|
||||
| Node { name; value; contents } ->
|
||||
Fmt.pf ppf "%s %S (@\n@[<2>%a@]@\n)" name value
|
||||
Fmt.(list ~sep:(any "@\n") pp)
|
||||
contents
|
||||
| Set { name; predicates= []; value } -> Fmt.pf ppf "%s = %S" name value
|
||||
| Set { name; predicates; value } ->
|
||||
Fmt.pf ppf "%s(%a) = %S" name
|
||||
Fmt.(list ~sep:(any ",") pp_predicate)
|
||||
predicates value
|
||||
| Add { name; predicates= []; value } -> Fmt.pf ppf "%s += %S" name value
|
||||
| Add { name; predicates; value } ->
|
||||
Fmt.pf ppf "%s(%a) += %S" name
|
||||
Fmt.(list ~sep:(any ",") pp_predicate)
|
||||
predicates value
|
||||
|
||||
module Assoc = struct
|
||||
type t = (string * string list) list
|
||||
|
||||
let add k v t =
|
||||
match List.assoc_opt k t with
|
||||
| Some vs ->
|
||||
let vs = List.sort_uniq String.compare (v :: vs) in
|
||||
(k, vs) :: List.remove_assoc k t
|
||||
| None -> (k, [ v ]) :: t
|
||||
|
||||
let set k v t =
|
||||
match List.assoc_opt k t with
|
||||
| Some _ -> (k, [ v ]) :: List.remove_assoc k t
|
||||
| None -> (k, [ v ]) :: t
|
||||
end
|
||||
|
||||
module Path = struct
|
||||
type t = string list
|
||||
|
||||
let of_string str =
|
||||
let pkg = String.split_on_char '.' str in
|
||||
let rec go = function
|
||||
| [] -> Ok pkg
|
||||
| "" :: _ -> error_msgf "Invalid package name: %S" str
|
||||
| _ :: rest -> go rest
|
||||
in
|
||||
go pkg
|
||||
|
||||
let of_string_exn str =
|
||||
match of_string str with
|
||||
| Ok pkg -> pkg
|
||||
| Error (`Msg msg) -> invalid_arg msg
|
||||
|
||||
let pp ppf pkg = Fmt.string ppf (String.concat "." pkg)
|
||||
let equal a b = try List.for_all2 String.equal a b with _ -> false
|
||||
end
|
||||
|
||||
let compile ~predicates t ks =
|
||||
let incl ps =
|
||||
let one = function
|
||||
| Include p -> List.exists (String.equal p) predicates
|
||||
| Exclude p -> not (List.exists (String.equal p) predicates)
|
||||
in
|
||||
List.exists one ps
|
||||
in
|
||||
let rec go acc t = function
|
||||
| [] ->
|
||||
let rec go acc = function
|
||||
| [] -> acc
|
||||
| Node _ :: rest -> go acc rest
|
||||
| Add { name; predicates= []; value } :: rest ->
|
||||
go (Assoc.add name value acc) rest
|
||||
| Set { name; predicates= []; value } :: rest ->
|
||||
go (Assoc.set name value acc) rest
|
||||
| Add { name; predicates; value } :: rest ->
|
||||
if incl predicates then go (Assoc.add name value acc) rest
|
||||
else go acc rest
|
||||
| Set { name; predicates; value } :: rest ->
|
||||
if incl predicates then go (Assoc.set name value acc) rest
|
||||
else go acc rest
|
||||
in
|
||||
go acc t
|
||||
| k :: ks -> (
|
||||
match t with
|
||||
| [] -> acc
|
||||
| Node { name= "package"; value; contents } :: rest ->
|
||||
if k = value then go acc contents ks else go acc rest (k :: ks)
|
||||
| _ :: rest -> go acc rest (k :: ks))
|
||||
in
|
||||
go [] t ks
|
||||
|
||||
exception Parser_error of string
|
||||
|
||||
let raise_parser_error lexbuf fmt =
|
||||
let p = Lexing.lexeme_start_p lexbuf in
|
||||
let c = p.Lexing.pos_cnum - p.Lexing.pos_bol + 1 in
|
||||
Fmt.kstr
|
||||
(fun msg -> raise (Parser_error msg))
|
||||
("%s (l.%d c.%d): " ^^ fmt)
|
||||
p.Lexing.pos_fname p.Lexing.pos_lnum c
|
||||
|
||||
let pp_token ppf = function
|
||||
| Vif_meta_lexer.Name name -> Fmt.string ppf name
|
||||
| String str -> Fmt.pf ppf "%S" str
|
||||
| Minus -> Fmt.string ppf "-"
|
||||
| Lparen -> Fmt.string ppf "("
|
||||
| Rparen -> Fmt.string ppf ")"
|
||||
| Comma -> Fmt.string ppf ","
|
||||
| Equal -> Fmt.string ppf "="
|
||||
| Plus_equal -> Fmt.string ppf "+="
|
||||
| Eof -> Fmt.string ppf "#eof"
|
||||
|
||||
let invalid_token lexbuf token =
|
||||
raise_parser_error lexbuf "Invalid token %a" pp_token token
|
||||
|
||||
let lparen lexbuf =
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| Lparen -> ()
|
||||
| token -> invalid_token lexbuf token
|
||||
|
||||
let name lexbuf =
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| Name name -> name
|
||||
| token -> invalid_token lexbuf token
|
||||
|
||||
let string lexbuf =
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| String str -> str
|
||||
| token -> invalid_token lexbuf token
|
||||
|
||||
let rec predicates lexbuf acc =
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| Rparen -> List.rev acc
|
||||
| Name predicate -> begin
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| Comma -> predicates lexbuf (Include predicate :: acc)
|
||||
| Rparen -> List.rev (Include predicate :: acc)
|
||||
| token -> invalid_token lexbuf token
|
||||
end
|
||||
| Minus ->
|
||||
let predicate = name lexbuf in
|
||||
begin
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| Comma -> predicates lexbuf (Exclude predicate :: acc)
|
||||
| Rparen -> List.rev (Exclude predicate :: acc)
|
||||
| token -> invalid_token lexbuf token
|
||||
end
|
||||
| token -> invalid_token lexbuf token
|
||||
|
||||
let rec parser lexbuf depth acc =
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| Rparen when depth > 0 -> List.rev acc
|
||||
| Rparen ->
|
||||
raise_parser_error lexbuf
|
||||
"Closing parenthesis without matching opening one"
|
||||
| Eof when depth = 0 -> List.rev acc
|
||||
| Eof -> raise_parser_error lexbuf "%d closing parenthesis missing" depth
|
||||
| Name name -> begin
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| String value ->
|
||||
lparen lexbuf;
|
||||
let contents = parser lexbuf (succ depth) [] in
|
||||
parser lexbuf depth (Node { name; value; contents } :: acc)
|
||||
| Equal ->
|
||||
let value = string lexbuf in
|
||||
parser lexbuf depth (Set { name; predicates= []; value } :: acc)
|
||||
| Plus_equal ->
|
||||
let value = string lexbuf in
|
||||
parser lexbuf depth (Add { name; predicates= []; value } :: acc)
|
||||
| Lparen ->
|
||||
let predicates = predicates lexbuf [] in
|
||||
begin
|
||||
match Vif_meta_lexer.token lexbuf with
|
||||
| Equal ->
|
||||
let value = string lexbuf in
|
||||
parser lexbuf depth (Set { name; predicates; value } :: acc)
|
||||
| Plus_equal ->
|
||||
let value = string lexbuf in
|
||||
parser lexbuf depth (Add { name; predicates; value } :: acc)
|
||||
| token -> invalid_token lexbuf token
|
||||
end
|
||||
| token -> invalid_token lexbuf token
|
||||
end
|
||||
| token -> invalid_token lexbuf token
|
||||
|
||||
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
|
||||
|
||||
let parser lexbuf =
|
||||
try Ok (parser lexbuf 0 []) with
|
||||
| Parser_error err -> Error (`Msg err)
|
||||
| Vif_meta_lexer.Lexical_error (msg, f, l, c) ->
|
||||
error_msgf "%s at l.%d, c.%d: %s" f l c msg
|
||||
|
||||
let parser path =
|
||||
Log.debug (fun m -> m "Parse %a" Fpath.pp path);
|
||||
let ( let@ ) finally fn = Fun.protect ~finally fn in
|
||||
let ic = open_in (Fpath.to_string path) in
|
||||
let@ _ = fun () -> close_in ic in
|
||||
let lexbuf = Lexing.from_channel ic in
|
||||
Lexing.set_filename lexbuf (Fpath.to_string path);
|
||||
parser lexbuf
|
||||
|
||||
let rec incl us vs =
|
||||
match (us, vs) with
|
||||
| u :: us, v :: vs -> if u = v then incl us vs else false
|
||||
| [], _ | _, [] -> true
|
||||
|
||||
let rec diff us vs =
|
||||
match (us, vs) with
|
||||
| u :: us, v :: vs ->
|
||||
if u = v then diff us vs else error_msgf "Different paths (%S <> %S)" u v
|
||||
| [], x | x, [] -> Ok x
|
||||
|
||||
let relativize ~roots path =
|
||||
let rec go = function
|
||||
| [] -> assert false
|
||||
| root :: roots ->
|
||||
if Fpath.is_prefix root path then
|
||||
match Fpath.relativize ~root path with
|
||||
| Some rel -> (root, rel)
|
||||
| None -> go roots
|
||||
else go roots
|
||||
in
|
||||
go roots
|
||||
|
||||
let search ~roots ?(predicates = [ "native"; "byte" ]) meta_path =
|
||||
let ( >>= ) = Result.bind in
|
||||
let ( >>| ) x f = Result.map f x in
|
||||
let elements path =
|
||||
if Sys.is_directory (Fpath.to_string path) then Ok false
|
||||
else if Fpath.basename path = "META" then Ok true
|
||||
else Ok false
|
||||
in
|
||||
let traverse path =
|
||||
if List.exists (Fpath.equal path) roots then Ok true
|
||||
else begin
|
||||
let _, rel = relativize ~roots path in
|
||||
let meta_path' = Fpath.segs rel in
|
||||
Ok (incl meta_path meta_path')
|
||||
end
|
||||
in
|
||||
let fold path acc =
|
||||
let root, rel = relativize ~roots path in
|
||||
let package = Fpath.(rem_empty_seg (parent rel)) in
|
||||
let meta_path' = Fpath.(segs package) in
|
||||
match
|
||||
diff meta_path meta_path' >>= fun ks ->
|
||||
parser path >>| fun meta -> compile ~predicates meta ks
|
||||
with
|
||||
| Ok descr -> Fpath.Map.add Fpath.(root // parent rel) descr acc
|
||||
| Error (`Msg msg) ->
|
||||
Log.warn (fun m ->
|
||||
m "Impossible to extract the META file of %a: %s" Fpath.pp path msg);
|
||||
acc
|
||||
in
|
||||
let err _path _ = Ok () in
|
||||
Bos.OS.Path.fold ~err ~dotfiles:false ~elements:(`Sat elements)
|
||||
~traverse:(`Sat traverse) fold Fpath.Map.empty roots
|
||||
>>| Fpath.Map.bindings
|
||||
|
||||
let dependencies_of (_path, descr) =
|
||||
Stdlib.Option.value ~default:[] (List.assoc_opt "requires" descr)
|
||||
|> List.map (Astring.String.cuts ~empty:false ~sep:" ")
|
||||
|> List.concat
|
||||
|> List.map Path.of_string_exn
|
||||
|
||||
exception Cycle
|
||||
|
||||
let equal_dep meta_path (meta_path', _, _) = Path.equal meta_path meta_path'
|
||||
|
||||
let sort libs =
|
||||
let rec go acc later todo progress =
|
||||
match (todo, later) with
|
||||
| [], [] -> List.rev acc
|
||||
| [], _ -> if progress then go acc [] later false else raise Cycle
|
||||
| ((_, path, descr) as x) :: r, _ ->
|
||||
let deps = dependencies_of (path, descr) in
|
||||
let deps_already_added =
|
||||
let fn dep = List.exists (equal_dep dep) acc in
|
||||
List.for_all fn deps
|
||||
in
|
||||
if deps_already_added then go (x :: acc) later r true
|
||||
else go acc (x :: later) r progress
|
||||
in
|
||||
let starts, todo =
|
||||
List.partition
|
||||
(fun (_, path, descr) -> dependencies_of (path, descr) = [])
|
||||
libs
|
||||
in
|
||||
go starts [] todo false
|
||||
|
||||
let ancestors ~roots ?(predicates = [ "native"; "byte" ]) meta_path =
|
||||
let rec go acc visited = function
|
||||
| [] -> Ok acc
|
||||
| meta_path :: todo when List.mem meta_path visited -> go acc visited todo
|
||||
| meta_path :: todo -> (
|
||||
Log.debug (fun m -> m "search %a" Path.pp meta_path);
|
||||
match search ~roots ~predicates meta_path with
|
||||
| Ok pkgs ->
|
||||
let requires = List.concat (List.map dependencies_of pkgs) in
|
||||
Log.debug (fun m ->
|
||||
m "search @[<hov>%a@]" Fmt.(Dump.list Path.pp) requires);
|
||||
let pkgs =
|
||||
List.map (fun (path, descr) -> (meta_path, path, descr)) pkgs
|
||||
in
|
||||
go (List.rev_append pkgs acc) (meta_path :: visited)
|
||||
(List.rev_append requires todo)
|
||||
| Error _ as err -> err)
|
||||
in
|
||||
let open Rresult in
|
||||
go [] [] [ meta_path ] >>| sort
|
||||
|
||||
let to_artifacts pkgs =
|
||||
let ( let* ) = Result.bind in
|
||||
let fn acc (path, pkg) =
|
||||
match acc with
|
||||
| Error _ as err -> err
|
||||
| Ok acc ->
|
||||
let directory = List.assoc_opt "directory" pkg in
|
||||
let* directory =
|
||||
match directory with
|
||||
| Some [ dir ] -> Ok Fpath.(path / dir)
|
||||
| Some _ ->
|
||||
error_msgf "Multiple directories referenced by %a" Fpath.pp
|
||||
Fpath.(path / "META")
|
||||
| None -> Ok path
|
||||
in
|
||||
let directory = Fpath.to_dir_path directory in
|
||||
let archive = List.assoc_opt "archive" pkg in
|
||||
let archive = Stdlib.Option.value ~default:[] archive in
|
||||
let plugin = List.assoc_opt "plugin" pkg in
|
||||
let plugin = Stdlib.Option.value ~default:[] plugin in
|
||||
let archive = List.map (Fpath.add_seg directory) archive in
|
||||
let plugin = List.map (Fpath.add_seg directory) plugin in
|
||||
Ok List.(rev_append archive (rev_append plugin acc))
|
||||
in
|
||||
let* paths = List.fold_left fn (Ok []) pkgs in
|
||||
Objinfo.vs paths
|
||||
|
||||
open Cmdliner
|
||||
|
||||
let directories =
|
||||
let doc = "The source directory containing the META files." in
|
||||
let parser str =
|
||||
match Fpath.of_string str with
|
||||
| Ok _ as v when Sys.file_exists str && Sys.is_directory str -> v
|
||||
| Ok v -> error_msgf "%a is not a directory or does not exist" Fpath.pp v
|
||||
| Error _ as err -> err
|
||||
in
|
||||
let open Arg in
|
||||
value
|
||||
& opt_all (conv (parser, Fpath.pp)) []
|
||||
& info [ "I" ] ~doc ~docv:"DIRECTORY"
|
||||
|
||||
let setup user's_directories =
|
||||
let cmd = Bos.Cmd.(v "ocamlfind" % "printconf" % "path") in
|
||||
let ( let* ) = Result.bind in
|
||||
let directories =
|
||||
let* exists = Bos.OS.Cmd.exists cmd in
|
||||
if exists then
|
||||
let r = Bos.OS.Cmd.run_out cmd in
|
||||
let* directories, _ = Bos.OS.Cmd.out_lines ~trim:true r in
|
||||
let directories =
|
||||
List.fold_left
|
||||
(fun acc path ->
|
||||
match Fpath.of_string path with
|
||||
| Ok path -> path :: acc
|
||||
| Error (`Msg _) ->
|
||||
Logs.warn (fun m ->
|
||||
m "ocamlfind returned an invalid path: %S" path);
|
||||
acc)
|
||||
[] directories
|
||||
in
|
||||
Ok directories
|
||||
else Ok []
|
||||
in
|
||||
let directories = Result.value ~default:[] directories in
|
||||
List.rev_append directories user's_directories
|
||||
|
||||
let setup = Term.(const setup $ directories)
|
33
lib/vif_meta.mli
Normal file
33
lib/vif_meta.mli
Normal file
|
@ -0,0 +1,33 @@
|
|||
module Assoc : sig
|
||||
type t = (string * string list) list
|
||||
end
|
||||
|
||||
module Path : sig
|
||||
type t = private string list
|
||||
|
||||
val of_string : string -> (t, [> `Msg of string ]) result
|
||||
val of_string_exn : string -> t
|
||||
val pp : t Fmt.t
|
||||
end
|
||||
|
||||
type t
|
||||
|
||||
val pp : t Fmt.t
|
||||
val parser : Fpath.t -> (t list, [> `Msg of string ]) result
|
||||
|
||||
val search :
|
||||
roots:Fpath.t list
|
||||
-> ?predicates:string list
|
||||
-> Path.t
|
||||
-> ((Fpath.t * Assoc.t) list, [> `Msg of string ]) result
|
||||
|
||||
val ancestors :
|
||||
roots:Fpath.t list
|
||||
-> ?predicates:string list
|
||||
-> Path.t
|
||||
-> ((Path.t * Fpath.t * Assoc.t) list, [> `Msg of string ]) result
|
||||
|
||||
val to_artifacts :
|
||||
(Fpath.t * Assoc.t) list -> (Objinfo.t list, [> `Msg of string ]) result
|
||||
|
||||
val setup : Fpath.t list Cmdliner.Term.t
|
80
lib/vif_meta_lexer.mll
Normal file
80
lib/vif_meta_lexer.mll
Normal file
|
@ -0,0 +1,80 @@
|
|||
(* The MIT License
|
||||
|
||||
Copyright (c) 2016 Jane Street Group, LLC <opensource@janestreet.com>
|
||||
Copyright (c) 2024 Romain Calascibetta <romain.calascibetta@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE. *)
|
||||
|
||||
{
|
||||
exception Lexical_error of string * string * int * int
|
||||
|
||||
type token =
|
||||
| Name of string
|
||||
| String of string
|
||||
| Minus
|
||||
| Lparen
|
||||
| Rparen
|
||||
| Comma
|
||||
| Equal
|
||||
| Plus_equal
|
||||
| Eof
|
||||
|
||||
let escaped_buf = Buffer.create 256
|
||||
let raise_lexical_error lexbuf fmt =
|
||||
let p = Lexing.lexeme_start_p lexbuf in
|
||||
Fmt.kstr (fun msg ->
|
||||
raise (Lexical_error (msg, p.Lexing.pos_fname,
|
||||
p.Lexing.pos_lnum,
|
||||
p.Lexing.pos_cnum - p.Lexing.pos_bol + 1)))
|
||||
fmt
|
||||
}
|
||||
|
||||
rule token = parse
|
||||
| [' ' '\t' '\r']* { token lexbuf }
|
||||
| '#' [^ '\n']* { token lexbuf }
|
||||
| '\n' { Lexing.new_line lexbuf; token lexbuf }
|
||||
|
||||
| ['A'-'Z' 'a'-'z' '0'-'9' '_' '.']+ as s { Name s }
|
||||
| '"'
|
||||
{ Buffer.clear escaped_buf;
|
||||
string escaped_buf lexbuf }
|
||||
| '-' { Minus }
|
||||
| '(' { Lparen }
|
||||
| ')' { Rparen }
|
||||
| ',' { Comma }
|
||||
| '=' { Equal }
|
||||
| "+=" { Plus_equal }
|
||||
| eof { Eof }
|
||||
| _
|
||||
{ raise_lexical_error lexbuf "illegal character %S"
|
||||
(String.escaped (Lexing.lexeme lexbuf)) }
|
||||
|
||||
and string buf = parse
|
||||
| '"'
|
||||
{ String (Buffer.contents buf) }
|
||||
| "\\\n"
|
||||
| '\n'
|
||||
{ Lexing.new_line lexbuf;
|
||||
Buffer.add_char buf '\n';
|
||||
string buf lexbuf }
|
||||
| '\\' (_ as c)
|
||||
| (_ as c)
|
||||
{ Buffer.add_char buf c;
|
||||
string buf lexbuf }
|
||||
| eof { raise_lexical_error lexbuf "unterminated string" }
|
327
lib/vif_top.ml
Normal file
327
lib/vif_top.ml
Normal file
|
@ -0,0 +1,327 @@
|
|||
let src = Logs.Src.create "vif.top"
|
||||
|
||||
module Log = (val Logs.src_log src : Logs.LOG)
|
||||
|
||||
type cfg = {
|
||||
stdlib: Fpath.t
|
||||
; roots: Fpath.t list
|
||||
}
|
||||
|
||||
let errors = ref false
|
||||
|
||||
module Lexbuf = struct
|
||||
open Lexing
|
||||
|
||||
let toplevel_fname = "//vif//"
|
||||
|
||||
let shift_toplevel_position ~start pos =
|
||||
{
|
||||
pos_fname= toplevel_fname
|
||||
; pos_lnum= pos.pos_lnum - start.pos_lnum + 1
|
||||
; pos_bol= pos.pos_bol - start.pos_cnum - 1
|
||||
; pos_cnum= pos.pos_cnum - start.pos_cnum
|
||||
}
|
||||
|
||||
let shift_toplevel_location ~start loc =
|
||||
let open Location in
|
||||
{
|
||||
loc with
|
||||
loc_start= shift_toplevel_position ~start loc.loc_start
|
||||
; loc_end= shift_toplevel_position ~start loc.loc_end
|
||||
}
|
||||
|
||||
let semisemi_action =
|
||||
let lexbuf = Lexing.from_string ";;" in
|
||||
match Lexer.token lexbuf with
|
||||
| Parser.SEMISEMI -> lexbuf.Lexing.lex_last_action
|
||||
| _ -> assert false
|
||||
|
||||
let map_error_loc ~fn (error : Location.error) =
|
||||
let fn_msg (msg : Location.msg) = { msg with loc= fn msg.loc } in
|
||||
{ error with main= fn_msg error.main; sub= List.map fn_msg error.sub }
|
||||
|
||||
let shift_location_error start =
|
||||
map_error_loc ~fn:(shift_toplevel_location ~start)
|
||||
|
||||
let position_mapper start =
|
||||
let open Ast_mapper in
|
||||
let start = { start with pos_fname= toplevel_fname } in
|
||||
let location mapper loc =
|
||||
shift_toplevel_location ~start (default_mapper.location mapper loc)
|
||||
in
|
||||
{ default_mapper with location }
|
||||
end
|
||||
|
||||
module Phrase = struct
|
||||
open Lexing
|
||||
open Parsetree
|
||||
|
||||
type t = {
|
||||
startpos: position
|
||||
; parsed: (Parsetree.toplevel_phrase, exn) result
|
||||
}
|
||||
|
||||
let result t = t.parsed
|
||||
let start t = t.startpos
|
||||
|
||||
let error_of_exn exn =
|
||||
match Location.error_of_exn exn with
|
||||
| None -> None
|
||||
| Some `Already_displayed -> None
|
||||
| Some (`Ok error) -> Some error
|
||||
|
||||
let parse lines =
|
||||
let contents = String.concat "\n" lines in
|
||||
let lexbuf = Lexing.from_string contents in
|
||||
let startpos = lexbuf.Lexing.lex_start_p in
|
||||
let parsed =
|
||||
match !Toploop.parse_toplevel_phrase lexbuf with
|
||||
| phrase -> Ok phrase
|
||||
| exception exn ->
|
||||
let exn =
|
||||
match error_of_exn exn with
|
||||
| None -> raise exn
|
||||
| Some error ->
|
||||
Location.Error (Lexbuf.shift_location_error startpos error)
|
||||
in
|
||||
begin
|
||||
if lexbuf.Lexing.lex_last_action <> Lexbuf.semisemi_action then
|
||||
let rec go () =
|
||||
match Lexer.token lexbuf with
|
||||
| Parser.SEMISEMI | Parser.EOF -> ()
|
||||
| exception Lexer.Error (_, _) -> ()
|
||||
| _ -> go ()
|
||||
in
|
||||
go ()
|
||||
end;
|
||||
Error exn
|
||||
in
|
||||
{ startpos; parsed }
|
||||
|
||||
let parse lines =
|
||||
match parse lines with exception End_of_file -> None | t -> Some t
|
||||
|
||||
let top_directive_name (toplevel_phrase : Parsetree.toplevel_phrase) =
|
||||
match toplevel_phrase with
|
||||
| Ptop_def _ -> None
|
||||
| Ptop_dir { pdir_name= { txt; _ }; _ } -> Some txt
|
||||
|
||||
let _is_findlib_directive =
|
||||
let findlib_directive = function
|
||||
| "require" | "camlp4o" | "camlp4r" | "thread" -> true
|
||||
| _ -> false
|
||||
in
|
||||
function
|
||||
| { parsed= Ok toplevel_phrase; _ } -> begin
|
||||
match top_directive_name toplevel_phrase with
|
||||
| Some dir -> findlib_directive dir
|
||||
| None -> false
|
||||
end
|
||||
| _ -> false
|
||||
end
|
||||
|
||||
let load cfg str =
|
||||
let ( let* ) = Result.bind in
|
||||
Log.debug (fun m -> m "load: %s" str);
|
||||
let* path = Vif_meta.Path.of_string str in
|
||||
let* deps = Vif_meta.ancestors ~roots:cfg.roots ~predicates:[ "native" ] path in
|
||||
let fn acc (_, path, descr) =
|
||||
let path = match List.assoc_opt "directory" descr with
|
||||
| Some (dir :: _) -> Fpath.(to_dir_path (path / dir))
|
||||
| Some [] | None -> path in
|
||||
match List.assoc_opt "plugin" descr with
|
||||
| Some (plugin :: _) -> Fpath.(path / plugin) :: acc
|
||||
| Some [] | None -> acc in
|
||||
let artifacts = List.fold_left fn [] deps in
|
||||
let artifacts = List.rev artifacts in
|
||||
Log.debug (fun m -> m "load: @[<hov>%a@]"
|
||||
Fmt.(Dump.list Fpath.pp) artifacts);
|
||||
Ok artifacts
|
||||
|
||||
let load cfg str =
|
||||
match load cfg str with
|
||||
| Ok artifacts ->
|
||||
let fn artifact =
|
||||
let dir = Fpath.parent artifact in
|
||||
Topdirs.dir_directory (Fpath.(to_string dir));
|
||||
Topdirs.dir_load Fmt.stderr (Fpath.to_string artifact)
|
||||
in
|
||||
List.iter fn artifacts
|
||||
| Error (`Msg msg) ->
|
||||
Log.err (fun m -> m "Impossible to load %S: %s" str msg)
|
||||
|
||||
let init cfg =
|
||||
Clflags.native_code := true;
|
||||
Topcommon.update_search_path_from_env ();
|
||||
Topcommon.set_paths ();
|
||||
Toploop.toplevel_env := Compmisc.initial_env ();
|
||||
let objs = !Compenv.first_objfiles in
|
||||
List.iter (Topdirs.dir_load Fmt.stderr) objs;
|
||||
Topcommon.run_hooks Topcommon.Startup;
|
||||
Compmisc.init_path ();
|
||||
Sys.interactive := false;
|
||||
(* Topdirs.dir_directory (Fpath.(to_string cfg.stdlib)); *)
|
||||
Toploop.add_directive "require"
|
||||
(Toploop.Directive_string (load cfg))
|
||||
{ Toploop.section= "Vif loader"
|
||||
; doc= "Load a package" }
|
||||
|
||||
let config ~stdlib roots =
|
||||
let cfg = { stdlib; roots } in
|
||||
init cfg; cfg
|
||||
|
||||
let eval _cfg ppf ph =
|
||||
match Phrase.result ph with
|
||||
| Error exn -> raise exn
|
||||
| Ok phrase -> begin
|
||||
Warnings.reset_fatal ();
|
||||
let mapper = Lexbuf.position_mapper (Phrase.start ph) in
|
||||
let phrase =
|
||||
match phrase with
|
||||
| Parsetree.Ptop_def str ->
|
||||
Parsetree.Ptop_def (mapper.Ast_mapper.structure mapper str)
|
||||
| Ptop_dir _ as v -> v
|
||||
in
|
||||
let phrase =
|
||||
match phrase with
|
||||
| Ptop_dir _ as v -> v
|
||||
| Ptop_def str ->
|
||||
Ptop_def (Pparse.apply_rewriters_str ~tool_name:"vif" str)
|
||||
in
|
||||
if !Clflags.dump_parsetree then Printast.top_phrase ppf phrase;
|
||||
if !Clflags.dump_source then Pprintast.top_phrase ppf phrase;
|
||||
Env.reset_cache_toplevel ();
|
||||
try Toploop.execute_phrase true (* verbose *) ppf phrase
|
||||
with Compenv.Exit_with_status code ->
|
||||
Format.fprintf ppf "[%d]@." code;
|
||||
false
|
||||
end
|
||||
|
||||
let redirect : fn:(capture:(Buffer.t -> unit) -> 'a) -> 'a =
|
||||
fun ~fn ->
|
||||
let filename = Filename.temp_file "vif-" ".stdout" in
|
||||
Log.debug (fun m -> m "redirect stdout/stderr into %s" filename);
|
||||
let stdout' = Unix.dup ~cloexec:true Unix.stdout in
|
||||
let stderr' = Unix.dup ~cloexec:true Unix.stderr in
|
||||
let fd =
|
||||
Unix.openfile filename Unix.[ O_WRONLY; O_CREAT; O_TRUNC; O_CLOEXEC ] 0o600
|
||||
in
|
||||
Unix.dup2 ~cloexec:false fd Unix.stdout;
|
||||
Unix.dup2 ~cloexec:false fd Unix.stderr;
|
||||
let ic = open_in filename in
|
||||
let read_up_to = ref 0 in
|
||||
let capture buf =
|
||||
flush stdout;
|
||||
flush stderr;
|
||||
let pos = Unix.lseek fd 0 Unix.SEEK_CUR in
|
||||
let len = pos - !read_up_to in
|
||||
read_up_to := pos;
|
||||
Buffer.add_channel buf ic len
|
||||
in
|
||||
let finally () =
|
||||
close_in_noerr ic;
|
||||
Unix.close fd;
|
||||
Unix.dup2 ~cloexec:false stdout' Unix.stdout;
|
||||
Unix.dup2 ~cloexec:false stderr' Unix.stderr;
|
||||
Unix.close stdout';
|
||||
Unix.close stderr'
|
||||
in
|
||||
Fun.protect ~finally @@ fun () -> fn ~capture
|
||||
|
||||
type vv = V : 'a ref * 'a -> vv
|
||||
|
||||
let protect_vars =
|
||||
let set_vars lst = List.iter (fun (V (r, v)) -> r := v) lst in
|
||||
fun vars ~fn ->
|
||||
let backup = List.map (fun (V (r, _)) -> V (r, !r)) vars in
|
||||
set_vars vars;
|
||||
let finally () = set_vars backup in
|
||||
Fun.protect ~finally fn
|
||||
|
||||
let capture_compiler_stuff ppf fn =
|
||||
protect_vars [ V (Location.formatter_for_warnings, ppf) ] ~fn
|
||||
|
||||
let trim str =
|
||||
let len = String.length str in
|
||||
if len = 0 then str
|
||||
else
|
||||
let trim_from = if str.[0] = '\n' then 1 else 0 in
|
||||
let trim_to = if str.[len - 1] = '\n' then len - 1 else len in
|
||||
if trim_to - trim_from <= 0 then ""
|
||||
else String.sub str trim_from (trim_to - trim_from)
|
||||
|
||||
let rec ltrim = function "" :: r -> ltrim r | lst -> lst
|
||||
let rtrim lst = List.rev (ltrim (List.rev lst))
|
||||
let trim lst = ltrim (rtrim (List.map trim lst))
|
||||
|
||||
let rec ends_by_semi_semi = function
|
||||
| [] -> false
|
||||
| [ x ] ->
|
||||
String.length x >= 2
|
||||
&& x.[String.length x - 1] = ';'
|
||||
&& x.[String.length x - 2] = ';'
|
||||
| _ :: r -> ends_by_semi_semi r
|
||||
|
||||
let cut_into_phrases lst =
|
||||
let rec go acc phrase = function
|
||||
| [] -> List.rev (List.rev phrase :: acc)
|
||||
| x :: r when ends_by_semi_semi [ x ] ->
|
||||
go (List.rev (x :: phrase) :: acc) [] r
|
||||
| x :: r -> go acc (x :: phrase) r
|
||||
in
|
||||
go [] [] lst
|
||||
|
||||
let eval cfg cmd =
|
||||
let buf = Buffer.create 0x7ff in
|
||||
let ppf = Format.formatter_of_out_channel stderr in
|
||||
errors := false;
|
||||
let eval ~capture phrase =
|
||||
let lines = ref [] in
|
||||
let capture () =
|
||||
capture buf;
|
||||
match Buffer.contents buf with
|
||||
| "" -> ()
|
||||
| str ->
|
||||
Buffer.clear buf;
|
||||
lines := str :: !lines
|
||||
in
|
||||
let out_phrase = !Oprint.out_phrase in
|
||||
let fn_out_phrase ppf = function
|
||||
| Outcometree.Ophr_exception _ as phr -> out_phrase ppf phr
|
||||
| phr -> capture (); out_phrase ppf phr; capture ()
|
||||
in
|
||||
Oprint.out_phrase := fn_out_phrase;
|
||||
let restore () = Oprint.out_phrase := out_phrase in
|
||||
begin
|
||||
match eval cfg ppf phrase with
|
||||
| ok ->
|
||||
errors := (not ok) || !errors;
|
||||
restore ()
|
||||
| exception exn ->
|
||||
errors := true;
|
||||
restore ();
|
||||
Location.report_exception ppf exn
|
||||
end;
|
||||
Format.pp_print_flush ppf ();
|
||||
capture ();
|
||||
trim (List.rev !lines)
|
||||
in
|
||||
Log.debug (fun m -> m "Start to eval: %a" Fmt.(Dump.list (fmt "%S")) cmd);
|
||||
let fn ~capture =
|
||||
capture_compiler_stuff ppf @@ fun () ->
|
||||
let cmd =
|
||||
match cmd with [] | [ _ ] -> cmd | x :: r -> x :: List.map (( ^ ) " ") r
|
||||
in
|
||||
let phrases = cut_into_phrases cmd in
|
||||
let phrases =
|
||||
List.map
|
||||
(fun phrase ->
|
||||
match Phrase.parse phrase with
|
||||
| Some t -> eval ~capture t
|
||||
| None -> [])
|
||||
phrases
|
||||
in
|
||||
let phrases = List.concat phrases in
|
||||
if !errors then Error phrases else Ok phrases
|
||||
in
|
||||
redirect ~fn
|
8
lib/vif_top.mli
Normal file
8
lib/vif_top.mli
Normal file
|
@ -0,0 +1,8 @@
|
|||
type cfg
|
||||
|
||||
val config :
|
||||
stdlib:Fpath.t
|
||||
-> Fpath.t list
|
||||
-> cfg
|
||||
|
||||
val eval : cfg -> string list -> (string list, string list) result
|
25
vif.opam
Normal file
25
vif.opam
Normal file
|
@ -0,0 +1,25 @@
|
|||
opam-version: "2.0"
|
||||
maintainer: "Robur <team@robur.coop>"
|
||||
authors: ["Robur <team@robur.coop>"]
|
||||
homepage: "https://github.com/robur-coop/vif"
|
||||
dev-repo: "git+https://github.com/robur-coop/vif.git"
|
||||
bug-reports: "https://github.com/robur-coop/vif/issues"
|
||||
license: "BSD-3-clause"
|
||||
|
||||
depends: [
|
||||
"ocaml" {>= "5.0.0"}
|
||||
"dune" {>= "2.0.0"}
|
||||
"fmt"
|
||||
"bos"
|
||||
"logs"
|
||||
"unix"
|
||||
"fpath"
|
||||
"cmdliner"
|
||||
]
|
||||
conflicts: [ "result" {< "1.5"} ]
|
||||
build: [
|
||||
["dune" "subst"] {dev}
|
||||
["dune" "build" "-p" name "-j" jobs]
|
||||
]
|
||||
|
||||
synopsis: ""
|
Loading…
Reference in a new issue