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

View file

@ -0,0 +1,351 @@
module List = ListLabels
module String = StringLabels
module Json = struct
type t =
[ `Int of int
| `Float of float
| `String of string
| `List of t list
| `Bool of bool
| `Assoc of (string * t) list
| `Null
]
end
module Timestamp : sig
type t
val to_json : t -> Json.t
val of_float_seconds : float -> t
val to_float_seconds : t -> float
end = struct
type t = float
let of_float_seconds x = x
let to_float_seconds x = x
let to_json f =
let n = int_of_float @@ (f *. 1_000_000.) in
`Int n
;;
end
module Id = struct
type t =
[ `Int of int
| `String of string
]
let create x = x
let to_string = function
| `String s -> s
| `Int i -> string_of_int i
;;
let to_json (t : t) = (t :> Json.t)
let field id = "id", to_json id
end
module Stack_frame = struct
module Raw = struct
type t = string list
let create t = t
let to_json t = `List (List.map t ~f:(fun s -> `String s))
end
type t =
{ parent : Id.t option
; name : string
; category : string
}
let create ?parent ~name ~category () = { parent; name; category }
let to_json { parent; name; category } : Json.t =
let json = [ "name", `String name; "category", `String category ] in
let json =
match parent with
| None -> json
| Some id -> ("parent", Id.to_json id) :: json
in
`Assoc json
;;
end
module Event = struct
[@@@ocaml.warning "-37"]
module Timestamp = Timestamp
type common_fields =
{ name : string
; cat : string list
; ts : Timestamp.t
; tts : Timestamp.t option
; pid : int
; tid : int
; cname : string option
; stackframe : [ `Id of Id.t | `Raw of Stack_frame.Raw.t ] option
}
let common_fields ?tts ?cname ?(cat = []) ?(pid = 0) ?(tid = 0) ?stackframe ~ts ~name ()
=
{ tts; cname; cat; ts; pid; tid; name; stackframe }
;;
let set_ts t ts = { t with ts }
let ts t = t.ts
type scope =
| Global
| Process
| Thread
type async =
| Start
| Instant
| End
type args = (string * Json.t) list
type object_kind =
| New
| Snapshot of
{ cat : string list option
; args : args
}
| Destroy
type metadata =
| Process_name of
{ pid : int
; name : string
}
| Process_labels of
{ pid : int
; labels : string
}
| Thread_name of
{ tid : int
; pid : int
; name : string
}
| Process_sort_index of
{ pid : int
; sort_index : int
}
| Thread_sort_index of
{ pid : int
; tid : int
; sort_index : int
}
(* TODO support flow, samples, references, memory dumps *)
type t =
| Counter of common_fields * args * Id.t option
| Duration_start of common_fields * args * Id.t option
| Duration_end of
{ pid : int
; tid : int
; ts : float
; args : args option
}
| Complete of
{ common : common_fields
; args : args option
; dur : Timestamp.t
; tdur : Timestamp.t option
}
| Instant of common_fields * scope option * args option
| Async of
{ common : common_fields
; async : async
; scope : string option
; id : Id.t
; args : args option
}
| Object of
{ common : common_fields
; object_kind : object_kind
; id : Id.t
; scope : string option
}
| Metadata of metadata
let phase s = "ph", `String s
let add_field_opt to_field field fields =
match field with
| None -> fields
| Some f -> to_field f :: fields
;;
let json_fields_of_common_fields { name; cat; ts; tts; pid; tid; cname; stackframe } =
let fields =
[ "name", `String name
; "cat", `String (String.concat ~sep:"," cat)
; "ts", Timestamp.to_json ts
; "pid", `Int pid
; "tid", `Int tid
]
in
let fields = add_field_opt (fun cname -> "cname", `String cname) cname fields in
let fields = add_field_opt (fun tts -> "tts", Timestamp.to_json tts) tts fields in
add_field_opt
(fun stackframe ->
match stackframe with
| `Id id -> "sf", Id.to_json id
| `Raw r -> "stack", Stack_frame.Raw.to_json r)
stackframe
fields
;;
let json_of_scope = function
| Global -> `String "g"
| Process -> `String "p"
| Thread -> `String "t"
;;
let args_field fields = "args", `Assoc fields
let json_fields_of_metadata m =
let fields =
let common pid name = [ "name", `String name; "pid", `Int pid ] in
match m with
| Process_name { pid; name } ->
args_field [ "name", `String name ] :: common pid "thread_name"
| Process_labels { pid; labels } ->
args_field [ "labels", `String labels ] :: common pid "process_labels"
| Thread_name { tid; pid; name } ->
("tid", `Int tid)
:: args_field [ "name", `String name ]
:: common pid "process_name"
| Process_sort_index { pid; sort_index } ->
args_field [ "sort_index", `Int sort_index ] :: common pid "process_sort_index"
| Thread_sort_index { pid; sort_index; tid } ->
("tid", `Int tid)
:: args_field [ "sort_index", `Int sort_index ]
:: common pid "thread_sort_index"
in
phase "M" :: fields
;;
let to_json_fields : t -> (string * Json.t) list = function
| Counter (common, args, id) ->
let fields = json_fields_of_common_fields common in
let fields = phase "C" :: args_field args :: fields in
add_field_opt Id.field id fields
| Duration_start (common, args, id) ->
let fields = json_fields_of_common_fields common in
let fields = phase "B" :: args_field args :: fields in
add_field_opt Id.field id fields
| Duration_end { pid; tid; ts; args } ->
let fields = [ "tid", `Int tid; "pid", `Int pid; "ts", `Float ts; phase "E" ] in
add_field_opt args_field args fields
| Complete { common; dur; args; tdur } ->
let fields = json_fields_of_common_fields common in
let fields = phase "X" :: ("dur", Timestamp.to_json dur) :: fields in
let fields =
add_field_opt (fun tdur -> "tdur", Timestamp.to_json tdur) tdur fields
in
add_field_opt args_field args fields
| Instant (common, scope, args) ->
let fields = json_fields_of_common_fields common in
let fields = phase "i" :: fields in
let fields = add_field_opt (fun s -> "s", json_of_scope s) scope fields in
add_field_opt args_field args fields
| Async { common; async; scope; id; args } ->
let fields = json_fields_of_common_fields common in
let fields = Id.field id :: fields in
let fields =
let ph =
let s =
match async with
| Start -> "b"
| Instant -> "n"
| End -> "e"
in
phase s
in
ph :: fields
in
let fields = add_field_opt (fun s -> "scope", `String s) scope fields in
add_field_opt args_field args fields
| Object { common; object_kind; id; scope } ->
let fields = json_fields_of_common_fields common in
let fields = Id.field id :: fields in
let fields =
let ph, args =
match object_kind with
| New -> "N", None
| Destroy -> "D", None
| Snapshot { cat; args } ->
let snapshot =
add_field_opt
(fun cat -> "cat", `String (String.concat ~sep:"," cat))
cat
args
in
"O", Some [ "snapshot", `Assoc snapshot ]
in
let fields = phase ph :: fields in
add_field_opt args_field args fields
in
add_field_opt (fun s -> "scope", `String s) scope fields
| Metadata m -> json_fields_of_metadata m
;;
let to_json t = `Assoc (to_json_fields t)
let counter ?id common args = Counter (common, args, id)
let complete ?tdur ?args ~dur common = Complete { common; tdur; dur; args }
let async ?scope ?args id async common = Async { common; args; scope; id; async }
let instant ?args ?scope common = Instant (common, scope, args)
end
module Output_object = struct
type t =
{ displayTimeUnit : [ `Ms | `Ns ] option
; traceEvents : Event.t list
; stackFrames : (Id.t * Stack_frame.t) list option
; extra_fields : (string * Json.t) list option
}
let to_json { displayTimeUnit; traceEvents; extra_fields; stackFrames } =
let json = [ "traceEvents", `List (List.map traceEvents ~f:Event.to_json) ] in
let json =
match displayTimeUnit with
| None -> json
| Some u ->
( "displayTimeUnit"
, `String
(match u with
| `Ms -> "ms"
| `Ns -> "ns") )
:: json
in
let json : (string * Json.t) list =
match stackFrames with
| None -> json
| Some frames ->
let frames =
List.map frames ~f:(fun (id, frame) ->
let id = Id.to_string id in
id, Stack_frame.to_json frame)
in
("stackFrames", `Assoc frames) :: json
in
let json =
match extra_fields with
| None -> json
| Some extra_fields -> json @ extra_fields
in
`Assoc json
;;
let create ?displayTimeUnit ?extra_fields ?stackFrames ~traceEvents () =
{ displayTimeUnit; extra_fields; traceEvents; stackFrames }
;;
end

View file

@ -0,0 +1,110 @@
[@@@alert unstable "The API of this library is not stable and may change without notice."]
[@@@alert "-unstable"]
(** Output trace data to a file in Chrome's trace_event format. This format is
compatible with chrome trace viewer [chrome://tracing].
Trace viewer is a part of the catapult project
(https://github.com/catapult-project/catapult/blob/master/tracing/README.md).
The trace format is documented at:
https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview *)
module Json : sig
(** Simplifies JSON type *)
type t =
[ `Int of int
| `Float of float
| `String of string
| `List of t list
| `Bool of bool
| `Assoc of (string * t) list
| `Null
]
end
module Id : sig
type t
val create : [ `String of string | `Int of int ] -> t
end
module Stack_frame : sig
module Raw : sig
type t
val create : string list -> t
end
type t
val create : ?parent:Id.t -> name:string -> category:string -> unit -> t
end
module Event : sig
type t
module Timestamp : sig
type t
val of_float_seconds : float -> t
val to_float_seconds : t -> float
end
type common_fields
val common_fields
: ?tts:Timestamp.t
-> ?cname:string
-> ?cat:string list
-> ?pid:int
-> ?tid:int
-> ?stackframe:[ `Id of Id.t | `Raw of Stack_frame.Raw.t ]
-> ts:Timestamp.t
-> name:string
-> unit
-> common_fields
val ts : common_fields -> Timestamp.t
val set_ts : common_fields -> Timestamp.t -> common_fields
type args = (string * Json.t) list
(** Create a counter event *)
val counter : ?id:Id.t -> common_fields -> args -> t
type async =
| Start
| Instant
| End
val async : ?scope:string -> ?args:args -> Id.t -> async -> common_fields -> t
val complete : ?tdur:Timestamp.t -> ?args:args -> dur:Timestamp.t -> common_fields -> t
val to_json : t -> Json.t
(** The scope of an instant event. The scopes below come from the standard
reference for this format *)
type scope =
| Global
| Process
| Thread
(** Create an instant event. *)
val instant : ?args:args -> ?scope:scope -> common_fields -> t
end
module Output_object : sig
(** The object format provided in whole *)
type t
val create
: ?displayTimeUnit:[ `Ms | `Ns ]
-> ?extra_fields:(string * Json.t) list
-> ?stackFrames:(Id.t * Stack_frame.t) list
-> traceEvents:Event.t list
-> unit
-> t
val to_json : t -> Json.t
end

View file

@ -0,0 +1,4 @@
(library
(name chrome_trace)
(public_name chrome-trace)
(synopsis "Emit catapult trace files, compatible with chrome://tracing"))

View file

@ -0,0 +1,51 @@
open Stdune
open Dune_tests_common
let () = init ()
let buf = Buffer.create 0
let c =
let write s = Buffer.add_string buf s in
let close () = () in
let flush () = () in
Dune_stats.create (Custom { write; close; flush }) ~extended_build_job_info:false
;;
let () =
let module Event = Chrome_trace.Event in
let module Id = Chrome_trace.Id in
let module Timestamp = Event.Timestamp in
let events =
[ Event.complete
~dur:(Timestamp.of_float_seconds 1.)
~args:[ "foo", `String "bar" ]
(Event.common_fields ~ts:(Timestamp.of_float_seconds 0.5) ~name:"foo" ())
; Event.counter
(Event.common_fields ~ts:(Timestamp.of_float_seconds 0.5) ~name:"cnt" ())
[ "bar", `Int 250 ]
; Event.async
(Id.create (`String "foo"))
Event.Start
(Event.common_fields ~ts:(Timestamp.of_float_seconds 0.5) ~name:"async" ())
~args:[ "foo", `Int 100 ]
]
in
List.iter events ~f:(Dune_stats.emit c);
Dune_stats.close c
;;
let buffer_lines () = String.split_lines (Buffer.contents buf)
let%expect_test _ =
Format.printf
"%a@."
Pp.to_fmt
(Pp.vbox (Pp.concat_map (buffer_lines ()) ~sep:Pp.cut ~f:Pp.verbatim));
[%expect
{|
[{"args":{"foo":"bar"},"ph":"X","dur":1000000,"name":"foo","cat":"","ts":500000,"pid":0,"tid":0}
,{"ph":"C","args":{"bar":250},"name":"cnt","cat":"","ts":500000,"pid":0,"tid":0}
,{"args":{"foo":100},"ph":"b","id":"foo","name":"async","cat":"","ts":500000,"pid":0,"tid":0}
]
|}]
;;

View file

@ -0,0 +1,16 @@
(library
(name chrome_trace_tests)
(inline_tests)
(libraries
dune_tests_common
stdune
dune_stats
chrome_trace
;; This is because of the (implicit_transitive_deps false)
;; in dune-project
ppx_expect.config
ppx_expect.config_types
base
ppx_inline_test.config)
(preprocess
(pps ppx_expect)))

View file

@ -0,0 +1,3 @@
(env
(_
(flags :standard \ -alert -unstable)))

View file

@ -0,0 +1 @@
module V1 = V1

View file

@ -0,0 +1,17 @@
(ocamllex extract_obj)
(library
(name configurator)
(public_name dune-configurator)
(private_modules import dune_lang ocaml_config)
(libraries unix csexp)
(flags
(:standard
-safe-string
(:include flags/flags.sexp)))
(special_builtin_support
(configurator
(api_version 1))))
(documentation
(package dune-configurator))

View file

@ -0,0 +1,87 @@
open Import
module Escape : sig
val quoted : string -> string
end = struct
let quote_length s =
let n = ref 0 in
let len = String.length s in
for i = 0 to len - 1 do
n
:= !n
+
match String.unsafe_get s i with
| '\"' | '\\' | '\n' | '\t' | '\r' | '\b' -> 2
| '%' -> if i + 1 < len && s.[i + 1] = '{' then 2 else 1
| ' ' .. '~' -> 1
| _ -> 4
done;
!n
;;
let escape_to s ~dst:s' ~ofs =
let n = ref ofs in
let len = String.length s in
for i = 0 to len - 1 do
(match String.unsafe_get s i with
| ('\"' | '\\') as c ->
Bytes.unsafe_set s' !n '\\';
incr n;
Bytes.unsafe_set s' !n c
| '\n' ->
Bytes.unsafe_set s' !n '\\';
incr n;
Bytes.unsafe_set s' !n 'n'
| '\t' ->
Bytes.unsafe_set s' !n '\\';
incr n;
Bytes.unsafe_set s' !n 't'
| '\r' ->
Bytes.unsafe_set s' !n '\\';
incr n;
Bytes.unsafe_set s' !n 'r'
| '\b' ->
Bytes.unsafe_set s' !n '\\';
incr n;
Bytes.unsafe_set s' !n 'b'
| '%' when i + 1 < len && s.[i + 1] = '{' ->
Bytes.unsafe_set s' !n '\\';
incr n;
Bytes.unsafe_set s' !n '%'
| ' ' .. '~' as c -> Bytes.unsafe_set s' !n c
| c ->
let a = Char.code c in
Bytes.unsafe_set s' !n '\\';
incr n;
Bytes.unsafe_set s' !n (Char.unsafe_chr (48 + (a / 100)));
incr n;
Bytes.unsafe_set s' !n (Char.unsafe_chr (48 + (a / 10 mod 10)));
incr n;
Bytes.unsafe_set s' !n (Char.unsafe_chr (48 + (a mod 10))));
incr n
done
;;
(* Surround [s] with quotes, escaping it if necessary. *)
let quoted s =
let len = String.length s in
let n = quote_length s in
let s' = Bytes.create (n + 2) in
Bytes.unsafe_set s' 0 '"';
if len = 0 || n > len
then escape_to s ~dst:s' ~ofs:1
else Bytes.blit_string ~src:s ~src_pos:0 ~dst:s' ~dst_pos:1 ~len;
Bytes.unsafe_set s' (n + 1) '"';
Bytes.unsafe_to_string s'
;;
end
type t =
| Quoted_string of string
| List of t list
let rec to_string t =
match t with
| Quoted_string s -> Escape.quoted s
| List l -> Printf.sprintf "(%s)" (List.map l ~f:to_string |> String.concat ~sep:" ")
;;

View file

@ -0,0 +1,6 @@
(** Subset of dune_lang to print flag lists *)
type t =
| Quoted_string of string
| List of t list
val to_string : t -> string

View file

@ -0,0 +1,3 @@
(** Read and extract the strings between a pair of BEGIN-\d+- and -END
delimiters. This is used to extract the compile time values from .obj files *)
val extract : (int * string) list -> Lexing.lexbuf -> (int * string) list

View file

@ -0,0 +1,11 @@
{}
rule extract acc = parse
| "BEGIN-" (['0' - '9']+ as i) "-"
{ read acc (int_of_string i) (Buffer.create 8) lexbuf }
| _ { extract acc lexbuf }
| eof { List.rev acc }
and read acc i b = parse
| "-END" { extract ((i, Buffer.contents b) :: acc) lexbuf }
| _ as c { Buffer.add_char b c; read acc i b lexbuf }
| eof { failwith "Unterminated BEGIN-" }
{}

View file

@ -0,0 +1,7 @@
(executable
(name mk))
(rule
(with-stdout-to
flags.sexp
(run ./mk.exe -ocamlv %{ocaml_version})))

View file

@ -0,0 +1,18 @@
open Printf
let parse_version s = Scanf.sscanf s "%d.%d.%d" (fun a b c -> a, b, c)
let () =
let usage = sprintf "%s -ocamlv version" (Filename.basename Sys.executable_name) in
let ocaml_version = ref "" in
let anon _ = raise (Arg.Bad "anonymous arguments aren't accepted") in
Arg.parse
[ "-ocamlv", Arg.String (fun s -> ocaml_version := s), "Version of ocaml being used" ]
anon
usage;
if !ocaml_version = ""
then raise (Arg.Bad "Provide version with -ocamlv")
else (
let x, y, _ = parse_version !ocaml_version in
if x >= 4 && y > 2 then printf "()\n" else printf "(-w -50)\n")
;;

View file

@ -0,0 +1,347 @@
let sprintf = Printf.sprintf
let eprintf = Printf.eprintf
let ( ^/ ) = Filename.concat
exception Fatal_error of string
let die fmt = Printf.ksprintf (fun s -> raise (Fatal_error s)) fmt
let warn fmt = Printf.ksprintf (fun msg -> prerr_endline ("Warning: " ^ msg)) fmt
module Result = struct
type ('a, 'b) t = ('a, 'b) result =
| Ok of 'a
| Error of 'b
let to_option = function
| Ok x -> Some x
| Error _ -> None
;;
end
module Exn = struct
external reraise : exn -> _ = "%reraise"
let protectx x ~f ~finally =
match f x with
| y ->
finally x;
y
| exception e ->
finally x;
raise e
;;
let protect ~f ~finally = protectx () ~f ~finally
include struct
[@@@ocaml.warning "-32"]
let raise_with_backtrace exn _bt = reraise exn
end
include Printexc
end
module Option = struct
let map t ~f =
match t with
| None -> None
| Some x -> Some (f x)
;;
let some_if cond x = if cond then Some x else None
let some x = Some x
let iter t ~f =
match t with
| None -> ()
| Some x -> f x
;;
module O = struct
let ( >>= ) x f =
match x with
| None -> None
| Some x -> f x
;;
let ( >>| ) x f = map x ~f
end
end
module List = struct
include ListLabels
let rec find_map l ~f =
match l with
| [] -> None
| x :: l ->
(match f x with
| None -> find_map l ~f
| Some _ as res -> res)
;;
end
module Array = ArrayLabels
module Bool = struct
let of_string s =
match bool_of_string s with
| s -> Some s
| exception Invalid_argument _ -> None
;;
end
module Map (S : Map.OrderedType) = struct
module M = MoreLabels.Map.Make (S)
include M
let update (type a) (t : a t) (key : M.key) ~(f : a option -> a option) : a t =
let v =
match find key t with
| exception Not_found -> None
| v -> Some v
in
match f v, v with
| None, None -> t
| None, Some _ -> remove key t
| Some data, _ -> add ~key ~data t
;;
let find m k =
match find k m with
| exception Not_found -> None
| s -> Some s
;;
let set t k v = add ~key:k ~data:v t
let of_list =
let rec loop acc = function
| [] -> Result.Ok acc
| (k, v) :: l ->
(match find acc k with
| None -> loop (set acc k v) l
| Some v_old -> Error (k, v_old, v))
in
fun l -> loop empty l
;;
let of_list_exn l =
match of_list l with
| Ok s -> s
| Error (_, _, _) -> failwith "Map.of_list_exn: duplicate key"
;;
end
module Int = struct
let of_string s =
match int_of_string s with
| s -> Some s
| exception Failure _ -> None
;;
module Map = struct
include Map (struct
type t = int
let compare = compare
end)
end
end
module Bytes = struct
include struct
[@@@ocaml.warning "-32"]
let blit_string ~(src : string) ~src_pos ~(dst : Bytes.t) ~dst_pos ~len =
for i = 0 to len - 1 do
Bytes.set dst (i + dst_pos) src.[i + src_pos]
done
;;
end
include BytesLabels
end
module String = struct
include StringLabels
module Map = Map (String)
let take s i = sub s ~pos:0 ~len:(min i (String.length s))
let drop s n =
let len = length s in
sub s ~pos:(min n len) ~len:(max (len - n) 0)
;;
let index s i =
match String.index s i with
| exception Not_found -> None
| s -> Some s
;;
let split_lines s =
let rec loop ~last_is_cr ~acc i j =
if j = length s
then (
let acc =
if j = i || (j = i + 1 && last_is_cr)
then acc
else sub s ~pos:i ~len:(j - i) :: acc
in
List.rev acc)
else (
match s.[j] with
| '\r' -> loop ~last_is_cr:true ~acc i (j + 1)
| '\n' ->
let line =
let len = if last_is_cr then j - i - 1 else j - i in
sub s ~pos:i ~len
in
loop ~acc:(line :: acc) (j + 1) (j + 1) ~last_is_cr:false
| _ -> loop ~acc i (j + 1) ~last_is_cr:false)
in
loop ~acc:[] 0 0 ~last_is_cr:false
;;
let exists =
let rec loop s i len f =
if i = len then false else f (unsafe_get s i) || loop s (i + 1) len f
in
fun s ~f -> loop s 0 (length s) f
;;
let is_empty = function
| "" -> true
| _ -> false
;;
let extract_words s ~is_word_char =
let rec skip_blanks i =
if i = length s
then []
else if is_word_char s.[i]
then parse_word i (i + 1)
else skip_blanks (i + 1)
and parse_word i j =
if j = length s
then [ sub s ~pos:i ~len:(j - i) ]
else if is_word_char s.[j]
then parse_word i (j + 1)
else sub s ~pos:i ~len:(j - i) :: skip_blanks (j + 1)
in
skip_blanks 0
;;
let extract_comma_space_separated_words s =
extract_words s ~is_word_char:(function
| ',' | ' ' | '\t' | '\n' -> false
| _ -> true)
;;
let extract_blank_separated_words s =
extract_words s ~is_word_char:(function
| ' ' | '\t' -> false
| _ -> true)
;;
let split s ~on =
let rec loop i j =
if j = length s
then [ sub s ~pos:i ~len:(j - i) ]
else if s.[j] = on
then sub s ~pos:i ~len:(j - i) :: loop (j + 1) (j + 1)
else loop i (j + 1)
in
loop 0 0
;;
end
module Io = struct
let open_in ?(binary = true) fn = if binary then open_in_bin fn else open_in fn
let open_out ?(binary = true) fn = if binary then open_out_bin fn else open_out fn
let input_lines =
let rec loop ic acc =
match input_line ic with
| exception End_of_file -> List.rev acc
| line -> loop ic (line :: acc)
in
fun ic -> loop ic []
;;
let with_file_in ?binary fn ~f = Exn.protectx (open_in ?binary fn) ~finally:close_in ~f
let with_file_out ?binary p ~f = Exn.protectx (open_out ?binary p) ~finally:close_out ~f
let write_file ?binary fn data =
with_file_out ?binary fn ~f:(fun oc -> output_string oc data)
;;
let write_lines ?binary fn lines =
with_file_out ?binary fn ~f:(fun oc ->
List.iter
~f:(fun line ->
output_string oc line;
output_string oc "\n")
lines)
;;
let read_all =
(* We use 65536 because that is the size of OCaml's IO buffers. *)
let chunk_size = 65536 in
(* Generic function for channels such that seeking is unsupported or
broken *)
let read_all_generic t buffer =
let rec loop () =
Buffer.add_channel buffer t chunk_size;
loop ()
in
try loop () with
| End_of_file -> Buffer.contents buffer
in
fun t ->
(* Optimisation for regular files: if the channel supports seeking, we
compute the length of the file so that we read exactly what we need and
avoid an extra memory copy. We expect that most files Dune reads are
regular files so this optimizations seems worth it. *)
match in_channel_length t with
| exception Sys_error _ -> read_all_generic t (Buffer.create chunk_size)
| n ->
let s = really_input_string t n in
(* For some files [in_channel_length] returns an invalid value. For
instance for files in /proc it returns [0]. So we try to read one
more character to make sure we did indeed reach the end of the
file *)
(match input_char t with
| exception End_of_file -> s
| c ->
(* The [+ chunk_size] is to make sure there is at least [chunk_size]
free space so that the first [Buffer.add_channel buffer t
chunk_size] in [read_all_generic] does not grow the buffer. *)
let buffer = Buffer.create (String.length s + 1 + chunk_size) in
Buffer.add_string buffer s;
Buffer.add_char buffer c;
read_all_generic t buffer)
;;
let read_file ?binary fn = with_file_in fn ~f:read_all ?binary
let with_lexbuf_from_file fn ~f =
with_file_in fn ~f:(fun ic ->
let lb = Lexing.from_channel ic in
lb.lex_curr_p <- { pos_fname = fn; pos_lnum = 1; pos_bol = 0; pos_cnum = 0 };
f lb)
;;
end
module Sexp = struct
module T = struct
type t =
| Atom of string
| List of t list
end
include T
include Csexp.Make (T)
end

View file

@ -0,0 +1,62 @@
{1 [dune-configurator] - Helper library for gathering system configuration }
[dune-configurator] is a small library that helps writing OCaml scripts that
test features available on the system, in order to generate [config.h]
files for instance.
Among other things, dune-configurator allows one to:
- test if a C program compiles
- query [pkg-config]
- import [#define] from OCaml header files
- generate a [config.h] file
{2 API Documentation }
The entry point for this library is {!Configurator.V1}.
{2 Example }
The following happens in a [dune] project that contains some C code that needs
to link against [libpng].
The following program ([discover/discover.ml]) uses [dune-configurator] to
query [pkg-config] and create [cflags.sexp] and [libs.sexp]:
{[
let () =
Configurator.V1.main ~name:"libpng"
(fun c ->
let pkg_config =
match Configurator.V1.Pkg_config.get c with
| Some p -> p
| None -> failwith "Cannot find pkg-config"
in
let conf = Configurator.V1.Pkg_config.query ~package:"libpng" in
Configurator.V1.Flags.write_sexp "cflags.sexp" conf.cflags;
Configurator.V1.Flags.write_sexp "libs.sexp" conf.libs)
]}
It can be built using the following [discover/dune] file:
{v
(executable
(name discover)
(libraries dune-configurator))
(rule
(targets cflags.sexp libs.sexp)
(action
(run ./discover.exe)))
v}
And used when building the C code in the following [dune] file:
{v
(library
(name png)
(foreign_stubs
(language c)
(names bindings)
(flags :standard (:include discover/cflags.sexp)))
(c_library_flags :standard (:include discover/libs.sexp)))
v}

View file

@ -0,0 +1,29 @@
open Import
module Vars = struct
type t = string String.Map.t
let of_lines lines =
let rec loop acc = function
| [] -> Ok acc
| line :: lines ->
(match String.index line ':' with
| Some i ->
let x =
(* skipping 2 chars because we also need to skip the space *)
String.take line i, String.drop line (i + 2)
in
loop (x :: acc) lines
| None -> Error (Printf.sprintf "Unrecognized line: %S" line))
in
match loop [] lines with
| Error _ as e -> e
| Ok s ->
(match String.Map.of_list s with
| Ok _ as s -> s
| Error (var, _, _) -> Error (sprintf "Variable %S present twice." var))
;;
let of_list_exn = String.Map.of_list_exn
let find t x = String.Map.find t x
end

View file

@ -0,0 +1,11 @@
(** Represent the parsed but uninterpreted output of [ocamlc -config] or
contents of [Makefile.config]. *)
module Vars : sig
type t
val find : t -> string -> string option
val of_list_exn : (string * string) list -> t
(** Parse the output of [ocamlc -config] given as a list of lines. *)
val of_lines : string list -> (t, string) result
end

View file

@ -0,0 +1,812 @@
open Import
let die = die
type t =
{ name : string
; dest_dir : string
; log : string -> unit
; mutable counter : int
; ext_obj : string
; c_compiler : string
; stdlib_dir : string
; ccomp_type : string
; c_libraries : string list
; ocamlc_config : Ocaml_config.Vars.t
; ocamlc_config_cmd : string
}
let rec rm_rf dir =
Array.iter (Sys.readdir dir) ~f:(fun fn ->
let fn = dir ^/ fn in
if Sys.is_directory fn then rm_rf fn else Unix.unlink fn);
Unix.rmdir dir
;;
module Temp = struct
(* Copied from filename.ml and adapted for directories *)
let prng = lazy (Random.State.make_self_init ())
let gen_name ~temp_dir ~prefix ~suffix =
let rnd = Random.State.bits (Lazy.force prng) land 0xFFFFFF in
temp_dir ^/ Printf.sprintf "%s%06x%s" prefix rnd suffix
;;
let create ~prefix ~suffix ~mk =
let temp_dir = Filename.get_temp_dir_name () in
let rec try_name counter =
let name = gen_name ~temp_dir ~prefix ~suffix in
match mk name with
| () -> name
| exception Unix.Unix_error _ when counter < 1000 -> try_name (counter + 1)
in
try_name 0
;;
let create_temp_dir ~prefix ~suffix =
let dir = create ~prefix ~suffix ~mk:(fun name -> Unix.mkdir name 0o700) in
at_exit (fun () -> rm_rf dir);
dir
;;
end
module Flags = struct
let extract_words = String.extract_words
let extract_comma_space_separated_words = String.extract_comma_space_separated_words
let extract_blank_separated_words = String.extract_blank_separated_words
let write_lines path s = Io.write_lines path s
let write_sexp path s =
let sexp = Dune_lang.List (List.map s ~f:(fun s -> Dune_lang.Quoted_string s)) in
Io.write_file path (Dune_lang.to_string sexp)
;;
end
module Find_in_path = struct
let path_sep = if Sys.win32 then ';' else ':'
let get_path () =
match Sys.getenv "PATH" with
| exception Not_found -> []
| s -> String.split s ~on:path_sep
;;
let exe = if Sys.win32 then ".exe" else ""
let prog_not_found prog = die "Program %s not found in PATH" prog
let best_prog dir prog =
let fn = dir ^/ prog ^ ".opt" ^ exe in
if Sys.file_exists fn
then Some fn
else (
let fn = dir ^/ prog ^ exe in
if Sys.file_exists fn then Some fn else None)
;;
let find_ocaml_prog prog =
match List.find_map (get_path ()) ~f:(fun dir -> best_prog dir prog) with
| None -> prog_not_found prog
| Some fn -> fn
;;
let which prog =
if Filename.is_implicit prog
then
List.find_map (get_path ()) ~f:(fun dir ->
let fn = dir ^/ prog ^ exe in
Option.some_if (Sys.file_exists fn) fn)
else (
let fn = if Filename.check_suffix prog exe then prog else prog ^ exe in
Option.some_if (Sys.file_exists fn) fn)
;;
end
let logf t fmt = Printf.ksprintf t.log fmt
let gen_id t =
let n = t.counter in
t.counter <- n + 1;
n
;;
let quote_if_needed =
let need_quote = function
| ' ' | '\"' -> true
| _ -> false
in
fun s ->
if String.is_empty s || String.exists ~f:need_quote s then Filename.quote s else s
;;
module Process = struct
type result =
{ exit_code : int
; stdout : string
; stderr : string
}
let command_line prog args =
String.concat ~sep:" " (List.map (prog :: args) ~f:quote_if_needed)
;;
let run_process t ?dir ?env prog args =
let prog_command_line = command_line prog args in
logf t "run: %s" prog_command_line;
let n = gen_id t in
let create_process =
let args = Array.of_list (prog :: args) in
match env with
| None -> Unix.create_process prog args
| Some env ->
let env = Array.of_list env in
Unix.create_process_env prog args env
in
let stdout_fn = t.dest_dir ^/ sprintf "stdout-%d" n in
let stderr_fn = t.dest_dir ^/ sprintf "stderr-%d" n in
let status =
let run () =
let openfile f =
Unix.openfile f [ O_WRONLY; O_CREAT; O_TRUNC; O_SHARE_DELETE ] 0o666
in
let stdout = openfile stdout_fn in
let stderr = openfile stderr_fn in
let stdin, stdin_w = Unix.pipe () in
Unix.close stdin_w;
let p = create_process stdin stdout stderr in
Unix.close stdin;
Unix.close stdout;
Unix.close stderr;
let _pid, status = Unix.waitpid [] p in
status
in
match dir with
| None -> run ()
| Some d ->
let old_dir = Sys.getcwd () in
Exn.protect
~f:(fun () ->
Sys.chdir d;
run ())
~finally:(fun () -> Sys.chdir old_dir)
in
match status with
| Unix.WSIGNALED signal -> die "signal %d killed process: %s" signal prog_command_line
| WSTOPPED signal -> die "signal %d stopped process: %s" signal prog_command_line
| WEXITED exit_code ->
logf t "-> process exited with code %d" exit_code;
let stdout = Io.read_file stdout_fn in
let stderr = Io.read_file stderr_fn in
logf t "-> stdout:";
List.iter (String.split_lines stdout) ~f:(logf t " | %s");
logf t "-> stderr:";
List.iter (String.split_lines stderr) ~f:(logf t " | %s");
{ exit_code; stdout; stderr }
;;
(* [cmd] which cannot be quoted (such as [t.c_compiler] which contains some
flags) followed by additional arguments. *)
let command_args cmd args =
String.concat ~sep:" " (cmd :: List.map args ~f:quote_if_needed)
;;
let run_command t ?dir ?(env = []) cmd =
logf t "run: %s" cmd;
let n = gen_id t in
let stdout_fn = t.dest_dir ^/ sprintf "stdout-%d" n in
let stderr_fn = t.dest_dir ^/ sprintf "stderr-%d" n in
let in_dir =
match dir with
| None -> ""
| Some dir -> sprintf "cd %s && " (Filename.quote dir)
in
let with_env =
match env with
| [] -> ""
| _ -> "env " ^ String.concat ~sep:" " env
in
let exit_code =
Printf.ksprintf
Sys.command
"%s%s %s > %s 2> %s"
in_dir
with_env
cmd
(Filename.quote stdout_fn)
(Filename.quote stderr_fn)
in
let stdout = Io.read_file stdout_fn in
let stderr = Io.read_file stderr_fn in
logf t "-> process exited with code %d" exit_code;
logf t "-> stdout:";
List.iter (String.split_lines stdout) ~f:(logf t " | %s");
logf t "-> stderr:";
List.iter (String.split_lines stderr) ~f:(logf t " | %s");
{ exit_code; stdout; stderr }
;;
let run_command_capture_exn t ?dir ?env cmd =
let { exit_code; stdout; stderr } = run_command t ?dir ?env cmd in
if exit_code <> 0
then die "command exited with code %d: %s" exit_code cmd
else if not (String.is_empty stderr)
then die "command has non-empty stderr: %s" cmd
else stdout
;;
let run_command_ok t ?dir ?env cmd = (run_command t ?dir ?env cmd).exit_code = 0
let run t ?dir ?env prog args = run_command t ?dir ?env (command_line prog args)
let run_capture_exn t ?dir ?env prog args =
run_command_capture_exn t ?dir ?env (command_line prog args)
;;
let run_ok t ?dir ?env prog args = run_command_ok t ?dir ?env (command_line prog args)
end
let ocaml_config_var t var = Ocaml_config.Vars.find t.ocamlc_config var
let ocaml_config_var_exn t var =
match Ocaml_config.Vars.find t.ocamlc_config var with
| None -> die "variable %S not found in the output of `%s`" var t.ocamlc_config_cmd
| Some s -> s
;;
type config =
{ ocamlc : string
; vars : Ocaml_config.Vars.t
}
let dune_is_too_old ~min:v =
die
"You seem to be running dune < %s. This version of dune-configurator requires at \
least dune %s."
v
v
;;
let read_dot_dune_configurator_file ~build_dir =
let file = Filename.concat build_dir ".dune/configurator.v2" in
if not (Sys.file_exists file) then dune_is_too_old ~min:"2.6";
let open Sexp in
let unable_to_parse err = die "Unable to parse %S.@.%s@." file err in
let sexp =
match Io.with_file_in file ~f:Sexp.input with
| Ok s -> s
| Error e -> unable_to_parse e
in
match sexp with
| Atom _ -> unable_to_parse "unexpected atom"
| List xs ->
let field name =
match
List.find_map xs ~f:(function
| List [ Atom name'; f ] when name = name' -> Some f
| _ -> None)
with
| None -> die "unable to find field %S" name
| Some f -> f
in
let ocamlc =
match field "ocamlc" with
| Atom o -> o
| _ -> die "invalid ocamlc field"
in
let vars =
let bindings =
match field "ocaml_config_vars" with
| List bindings ->
List.map bindings ~f:(function
| List [ Atom k; Atom v ] -> k, v
| _ -> die "invalid output")
| _ -> die "invalid output"
in
Ocaml_config.Vars.of_list_exn bindings
in
{ ocamlc; vars }
;;
let fill_in_fields_that_depends_on_ocamlc_config t =
let get = ocaml_config_var_exn t in
let get_flags var = get var |> String.trim |> Flags.extract_blank_separated_words in
let c_compiler, c_libraries =
match Ocaml_config.Vars.find t.ocamlc_config "c_compiler" with
| Some c_comp -> c_comp ^ " " ^ get "ocamlc_cflags", get_flags "native_c_libraries"
| None -> get "bytecomp_c_compiler", get_flags "bytecomp_c_libraries"
in
{ t with
ext_obj = get "ext_obj"
; c_compiler
; stdlib_dir = get "standard_library"
; ccomp_type = get "ccomp_type"
; c_libraries
}
;;
let create_from_inside_dune ~dest_dir ~log ~build_dir ~name =
let dest_dir =
match dest_dir with
| Some dir -> dir
| None -> Temp.create_temp_dir ~prefix:"ocaml-configurator" ~suffix:""
in
let { ocamlc; vars = ocamlc_config } = read_dot_dune_configurator_file ~build_dir in
let ocamlc_config_cmd = Process.command_line ocamlc [ "-config" ] in
fill_in_fields_that_depends_on_ocamlc_config
{ name
; log
; dest_dir
; counter = 0
; ocamlc_config
; ocamlc_config_cmd
; ext_obj = ""
; c_compiler = ""
; stdlib_dir = ""
; ccomp_type = ""
; c_libraries = []
}
;;
let create ?dest_dir ?ocamlc ?(log = ignore) name =
let inside_dune =
match Sys.getenv "INSIDE_DUNE" with
| exception Not_found -> None
| n -> Some n
in
match ocamlc, inside_dune with
| None, Some build_dir when build_dir <> "1" ->
create_from_inside_dune ~dest_dir ~log ~build_dir ~name
| _ ->
let dest_dir =
match dest_dir with
| Some dir -> dir
| None -> Temp.create_temp_dir ~prefix:"ocaml-configurator" ~suffix:""
in
let ocamlc =
match ocamlc with
| Some fn -> fn
| None -> Find_in_path.find_ocaml_prog "ocamlc"
in
let ocamlc_config_cmd = Process.command_line ocamlc [ "-config" ] in
let t =
{ name
; log
; dest_dir
; counter = 0
; ext_obj = ""
; c_compiler = ""
; stdlib_dir = ""
; ccomp_type = ""
; c_libraries = []
; ocamlc_config = Ocaml_config.Vars.of_list_exn []
; ocamlc_config_cmd
}
in
let ocamlc_config =
let ocamlc_config_output =
Process.run_command_capture_exn t ~dir:dest_dir ocamlc_config_cmd
|> String.split_lines
in
match Ocaml_config.Vars.of_lines ocamlc_config_output with
| Ok x -> x
| Error msg -> die "Failed to parse the output of '%s':@\n%s" ocamlc_config_cmd msg
in
fill_in_fields_that_depends_on_ocamlc_config { t with ocamlc_config }
;;
let is_msvc t =
match t.ccomp_type with
| "msvc" -> true
| _ -> false
;;
let compile_and_link_c_prog t ?(c_flags = []) ?(link_flags = []) code =
let dir = t.dest_dir ^/ sprintf "c-test-%d" (gen_id t) in
Unix.mkdir dir 0o777;
let base = dir ^/ "test" in
let c_fname = base ^ ".c" in
let exe_fname = base ^ ".exe" in
Io.write_file c_fname code;
logf t "compiling c program:";
List.iter (String.split_lines code) ~f:(logf t " | %s");
let run_ok args =
Process.run_command_ok t ~dir (Process.command_args t.c_compiler args)
in
let output_flag = if is_msvc t then [ "-Fe" ^ exe_fname ] else [ "-o"; exe_fname ] in
let ok =
run_ok
(List.concat
[ c_flags
; [ "-I"; t.stdlib_dir ]
; output_flag
; [ c_fname ]
; t.c_libraries
; link_flags
])
in
if ok then Ok () else Error ()
;;
let compile_c_prog t ?(c_flags = []) code =
let dir = t.dest_dir ^/ sprintf "c-test-%d" (gen_id t) in
Unix.mkdir dir 0o777;
let base = dir ^/ "test" in
let c_fname = base ^ ".c" in
let obj_fname = base ^ t.ext_obj in
Io.write_file c_fname code;
logf t "compiling c program:";
List.iter (String.split_lines code) ~f:(logf t " | %s");
let ok =
let output_flag = if is_msvc t then [ "-Fo" ^ obj_fname ] else [ "-o"; obj_fname ] in
Process.run_command_ok
t
~dir
(Process.command_args
t.c_compiler
(List.concat
[ c_flags
; [ "-I"; t.stdlib_dir ]
; output_flag
; [ "-c"; c_fname ]
; t.c_libraries
]))
in
if ok then Ok obj_fname else Error ()
;;
let c_test t ?c_flags ?link_flags code =
match compile_and_link_c_prog t ?c_flags ?link_flags code with
| Ok _ -> true
| Error _ -> false
;;
module C_define = struct
module Type = struct
type t =
| Switch
| Int
| String
let name = function
| Switch -> "bool"
| Int -> "int"
| String -> "string"
;;
end
module Value = struct
type t =
| Switch of bool
| Int of int
| String of string
end
let extract_program ?prelude includes vars =
let has_type t = List.exists vars ~f:(fun (_, t') -> t = t') in
let buf = Buffer.create 1024 in
let pr fmt = Printf.bprintf buf (fmt ^^ "\n") in
List.iter includes ~f:(pr "#include <%s>");
pr "";
Option.iter prelude ~f:(pr "%s");
if has_type Type.Int
then
pr
{|
#define DUNE_ABS(x) ((x >= 0)? x: -(x))
#define DUNE_D0(x) ('0'+(DUNE_ABS(x)/1 )%%10)
#define DUNE_D1(x) ('0'+(DUNE_ABS(x)/10 )%%10), DUNE_D0(x)
#define DUNE_D2(x) ('0'+(DUNE_ABS(x)/100 )%%10), DUNE_D1(x)
#define DUNE_D3(x) ('0'+(DUNE_ABS(x)/1000 )%%10), DUNE_D2(x)
#define DUNE_D4(x) ('0'+(DUNE_ABS(x)/10000 )%%10), DUNE_D3(x)
#define DUNE_D5(x) ('0'+(DUNE_ABS(x)/100000 )%%10), DUNE_D4(x)
#define DUNE_D6(x) ('0'+(DUNE_ABS(x)/1000000 )%%10), DUNE_D5(x)
#define DUNE_D7(x) ('0'+(DUNE_ABS(x)/10000000 )%%10), DUNE_D6(x)
#define DUNE_D8(x) ('0'+(DUNE_ABS(x)/100000000 )%%10), DUNE_D7(x)
#define DUNE_D9(x) ('0'+(DUNE_ABS(x)/1000000000)%%10), DUNE_D8(x)
#define DUNE_SIGN(x) ((x >= 0)? '0': '-')
|};
List.iteri vars ~f:(fun i (name, t) ->
match t with
| Type.Int ->
let c_arr_i =
let b = Buffer.create 8 in
let is = string_of_int i in
for i = 0 to String.length is - 1 do
Printf.bprintf b "'%c', " is.[i]
done;
Buffer.contents b
in
pr
{|
const char s%i[] = {
'B', 'E', 'G', 'I', 'N', '-', %s'-',
DUNE_SIGN((%s)),
DUNE_D9((%s)),
'-', 'E', 'N', 'D'
};
|}
i
c_arr_i
name
name
| String -> pr {|const char *s%i = "BEGIN-%i-" %s "-END";|} i i name
| Switch ->
pr
{|
#ifdef %s
const char *s%i = "BEGIN-%i-true-END";
#else
const char *s%i = "BEGIN-%i-false-END";
#endif
|}
name
i
i
i
i);
Buffer.contents buf
;;
let extract_values obj_file vars =
let values =
Io.with_lexbuf_from_file obj_file ~f:(Extract_obj.extract [])
|> List.fold_left ~init:Int.Map.empty ~f:(fun acc (key, v) ->
Int.Map.update acc key ~f:(function
| None -> Some [ v ]
| Some vs -> Some (v :: vs)))
in
List.mapi vars ~f:(fun i (name, t) ->
let raw_vals =
match Int.Map.find values i with
| Some v -> v
| None -> die "Unable to get value for %s" name
in
let parse_val_or_exn f =
let f x =
match f x with
| Some s -> s
| None ->
die
"Unable to read variable %S of type %s. Invalid value %S in %s found"
name
(Type.name t)
x
obj_file
in
let vs =
List.map ~f:(fun x -> x, f x) raw_vals
|> List.sort_uniq ~cmp:(fun (_, x) (_, y) -> compare x y)
in
match vs with
| [] -> assert false
| [ (_, v) ] -> v
| vs ->
let vs = List.map ~f:fst vs in
die
"Duplicate values for %s:\n%s"
name
(vs |> List.map ~f:(sprintf "- %s") |> String.concat ~sep:"\n")
in
let value =
match t with
| Type.Switch -> Value.Switch (parse_val_or_exn Bool.of_string)
| Int -> Value.Int (parse_val_or_exn Int.of_string)
| String -> String (parse_val_or_exn Option.some)
in
name, value)
;;
let import t ?prelude ?c_flags ~includes vars =
let program = extract_program ?prelude ("stdio.h" :: includes) vars in
match compile_c_prog t ?c_flags program with
| Error _ -> die "failed to compile program"
| Ok obj -> extract_values obj vars
;;
let gen_header_file t ~fname ?protection_var vars =
let protection_var =
match protection_var with
| Some v -> v
| None ->
String.map
(t.name ^ "_" ^ Filename.basename fname)
~f:(function
| 'a' .. 'z' as c -> Char.uppercase_ascii c
| ('A' .. 'Z' | '0' .. '9') as c -> c
| _ -> '_')
in
let vars = List.sort vars ~cmp:(fun (a, _) (b, _) -> compare a b) in
let lines =
List.map vars ~f:(fun (name, value) ->
match (value : Value.t) with
| Switch false -> sprintf "#undef %s" name
| Switch true -> sprintf "#define %s" name
| Int n -> sprintf "#define %s (%d)" name n
| String s -> sprintf "#define %s %S" name s)
in
let lines =
List.concat
[ [ sprintf "#ifndef %s" protection_var; sprintf "#define %s" protection_var ]
; lines
; [ "#endif" ]
]
in
logf t "writing header file %s" fname;
List.iter lines ~f:(logf t " | %s");
let tmp_fname = fname ^ ".tmp" in
Io.write_lines tmp_fname lines;
Sys.rename tmp_fname fname
;;
end
let which t prog =
logf t "which: %s" prog;
let x = Find_in_path.which prog in
logf
t
"-> %s"
(match x with
| None -> "not found"
| Some fn -> "found: " ^ quote_if_needed fn);
x
;;
module Pkg_config = struct
type nonrec t =
{ pkg_config : string
; pkg_config_args : string list
; configurator : t
}
let get c =
let get_pkg_config_args default =
match Sys.getenv "PKG_CONFIG_ARGN" with
| s -> String.split ~on:' ' s
| exception Not_found -> default
in
match Sys.getenv "PKG_CONFIG" with
| s ->
Option.map (which c s) ~f:(fun pkg_config ->
let pkg_config_args = get_pkg_config_args [] in
{ pkg_config; pkg_config_args; configurator = c })
| exception Not_found ->
(match which c "pkgconf" with
| None ->
Option.map (which c "pkg-config") ~f:(fun pkg_config ->
let pkg_config_args = get_pkg_config_args [] in
{ pkg_config; pkg_config_args; configurator = c })
| Some pkg_config ->
let pkg_config_args =
get_pkg_config_args
(match ocaml_config_var c "target" with
| None -> []
| Some target -> [ "--personality"; target ])
in
Some { pkg_config; pkg_config_args; configurator = c })
;;
type package_conf =
{ libs : string list
; cflags : string list
}
let gen_query t ~package ~expr =
let c = t.configurator in
let dir = c.dest_dir in
let expr =
match expr with
| Some e -> e
| None ->
if
String.exists package ~f:(function
| '=' | '>' | '<' -> true
| _ -> false)
then
warn
"Package name %S contains invalid characters. Use Pkg_config.query_expr to \
construct proper queries"
package;
package
in
let env =
match ocaml_config_var c "system" with
| Some "macosx" ->
let open Option.O in
which c "brew"
>>= fun brew ->
let new_pkg_config_path =
let prefix = String.trim (Process.run_capture_exn c ~dir brew [ "--prefix" ]) in
let p = sprintf "%s/opt/%s/lib/pkgconfig" (quote_if_needed prefix) package in
Option.some_if
(match Sys.is_directory p with
| s -> s
| exception Sys_error _ -> false)
p
in
new_pkg_config_path
>>| fun new_pkg_config_path ->
let _PKG_CONFIG_PATH = "PKG_CONFIG_PATH" in
let pkg_config_path =
match Sys.getenv _PKG_CONFIG_PATH with
| s -> s ^ ":"
| exception Not_found -> ""
in
[ sprintf "%s=%s%s" _PKG_CONFIG_PATH pkg_config_path new_pkg_config_path ]
| _ -> None
in
let pc_flags = "--print-errors" in
let { Process.exit_code; stderr; _ } =
Process.run_process c ~dir ?env t.pkg_config (t.pkg_config_args @ [ pc_flags; expr ])
in
if exit_code = 0
then (
let run what =
match
String.trim
(Process.run_capture_exn
c
~dir
?env
t.pkg_config
(t.pkg_config_args @ [ what; package ]))
with
| "" -> []
| s -> String.extract_blank_separated_words s
in
Ok { libs = run "--libs"; cflags = run "--cflags" })
else Error stderr
;;
let query t ~package = Result.to_option @@ gen_query t ~package ~expr:None
let query_expr t ~package ~expr =
Result.to_option @@ gen_query t ~package ~expr:(Some expr)
;;
let query_expr_err t ~package ~expr = gen_query t ~package ~expr:(Some expr)
end
let main ?(args = []) ~name f =
let build_dir =
match Sys.getenv "INSIDE_DUNE" with
| exception Not_found ->
die
"Configurator scripts must be run with Dune. To manually run a script, use $ \
dune exec."
| "1" -> dune_is_too_old ~min:"2.3"
| s -> s
in
let verbose = ref false in
let dest_dir = ref None in
let args =
Arg.align
([ "-verbose", Arg.Set verbose, " be verbose"
; ( "-dest-dir"
, Arg.String (fun s -> dest_dir := Some s)
, "DIR save temporary files to this directory" )
]
@ args)
in
let anon s = raise (Arg.Bad (sprintf "don't know what to do with %s" s)) in
let usage = sprintf "%s [OPTIONS]" (Filename.basename Sys.executable_name) in
Arg.parse args anon usage;
let log_db = ref [] in
let log s = log_db := s :: !log_db in
try
let t =
create_from_inside_dune
~dest_dir:!dest_dir
~log:(if !verbose then prerr_endline else log)
~build_dir
~name
in
f t
with
| exn ->
let bt = Printexc.get_raw_backtrace () in
List.iter (List.rev !log_db) ~f:(eprintf "%s\n");
(match exn with
| Fatal_error msg ->
eprintf "Error: %s\n%!" msg;
exit 1
| _ -> Exn.raise_with_backtrace exn bt)
;;

View file

@ -0,0 +1,185 @@
type t
val create
: ?dest_dir:string
-> ?ocamlc:string
-> ?log:(string -> unit)
-> string (** name, such as library name *)
-> t
(** Return the value associated to a variable in the output of [ocamlc -config] *)
val ocaml_config_var : t -> string -> string option
val ocaml_config_var_exn : t -> string -> string
(** [c_test t ?c_flags ?link_flags c_code] try to compile and link the C code
given in [c_code]. Return whether compilation was successful. *)
val c_test
: t
-> ?c_flags:string list (** default: [] *)
-> ?link_flags:string list (** default: [] *)
-> string
-> bool
module C_define : sig
module Type : sig
type t =
| Switch (** defined/undefined *)
| Int
| String
end
module Value : sig
type t =
| Switch of bool
| Int of int
| String of string
end
(** Import some #define from the given header files. For instance:
{v
# C.C_define.import c ~includes:"caml/config.h" ["ARCH_SIXTYFOUR", Switch];;
- (string * Configurator.C_define.Value.t) list = ["ARCH_SIXTYFOUR", Switch true]
v} *)
val import
: t
-> ?prelude:string
(** Define extra code be used with extracting values below. Note that
the compiled code is never executed. *)
-> ?c_flags:string list
-> includes:string list
-> (string * Type.t) list
-> (string * Value.t) list
(** Generate a C header file containing the following #define.
[protection_var] is used to enclose the file with:
{[
#ifndef BLAH #define BLAH ... #endif
]}
If not specified, it is inferred from the name given to [create] and the
filename. *)
val gen_header_file
: t
-> fname:string
-> ?protection_var:string
-> (string * Value.t) list
-> unit
end
module Pkg_config : sig
type configurator = t
type t
(** Search a pkg-config implementation in PATH. Use the one
defined in [PKG_CONFIG] environment variable if set else try
[pkgconf] then [pkg-config]. Append the [PKG_CONFIG_PATH]
environment variable to the searched pathes. Returns [None] if
nothing is not found. *)
val get : configurator -> t option
type package_conf =
{ libs : string list
; cflags : string list
}
(** [query t ~package] query pkg-config for the [package]. The package must
not contain a version constraint. Multiple, unversioned packages are
separated with spaces, for example "gtk+-3.0 gtksourceview-3.0". By
default, the OCaml compiler [target] is passed to pkgconf as
[--personality] argument. An alternative list of arguments can be
specified by setting the [PKG_CONFIG_ARGN] environment variable.
Returns [None] if [package] is not available *)
val query : t -> package:string -> package_conf option
val query_expr : t -> package:string -> expr:string -> package_conf option
[@@ocaml.deprecated "please use [query_expr_err]"]
(** [query_expr_err t ~package ~expr] query pkg-config for the [package].
[expr] may contain a version constraint, for example "gtk+-3.0 >= 3.18".
[package] must be just the name of the package. If [expr] is specified,
[package] must be specified as well. By default, the OCaml compiler
"target" is passed to pkgconf as [--personality] argument. An
alternative list of arguments can be specified by setting the
[PKG_CONFIG_ARGN] environment variable.
Returns [Error error_msg] if [package] is not available *)
val query_expr_err
: t
-> package:string
-> expr:string
-> (package_conf, string) result
end
with type configurator := t
module Flags : sig
(** [write_sexp fname s] writes the list of strings [s] to the file [fname] in
an appropriate format so that it can used in [dune] files with
[(:include [fname])]. *)
val write_sexp : string -> string list -> unit
(** [write_lines fname s] writes the list of string [s] to the file [fname]
with one line per string so that it can be used in Dune action rules with
[%{read-lines:<path>}]. *)
val write_lines : string -> string list -> unit
(** [extract_comma_space_separated_words s] returns a list of words in [s]
that are separated by a newline, tab, space or comma character. *)
val extract_comma_space_separated_words : string -> string list
(** [extract_blank_separated_words s] returns a list of words in [s] that are
separated by a tab or space character. *)
val extract_blank_separated_words : string -> string list
(** [extract_words s ~is_word_char] will split the string [s] into a list of
words. A valid word character is defined by the [is_word_char] predicate
returning true and anything else is considered a separator. Any blank
words are filtered out of the results. *)
val extract_words : string -> is_word_char:(char -> bool) -> string list
end
(** [which t prog] seek [prog] in the PATH and return the name of the program
prefixed with the first path where it is found. Return [None] the the
program is not found. *)
val which : t -> string -> string option
(** Execute external programs. *)
module Process : sig
type result =
{ exit_code : int
; stdout : string
; stderr : string
}
(** [run t prog args] runs [prog] with arguments [args] and returns its exit
status together with the content of stdout and stderr. The action is
logged.
@param dir change to [dir] before running the command.
@param env specify additional environment variables as a list of the form
NAME=VALUE. *)
val run : t -> ?dir:string -> ?env:string list -> string -> string list -> result
(** [run_capture_exn t prog args] same as [run t prog args] but returns
[stdout] and {!die} if the error code is nonzero or there is some output
on [stderr]. *)
val run_capture_exn
: t
-> ?dir:string
-> ?env:string list
-> string
-> string list
-> string
(** [run_ok t prog args] same as [run t prog args] but only cares whether the
execution terminated successfully (i.e., returned an error code of [0]). *)
val run_ok : t -> ?dir:string -> ?env:string list -> string -> string list -> bool
end
(** Typical entry point for configurator programs *)
val main : ?args:(Arg.key * Arg.spec * Arg.doc) list -> name:string -> (t -> unit) -> unit
(** Abort execution. If raised from within [main], the argument of [die] is
printed as [Error: <message>]. *)
val die : ('a, unit, string, 'b) format4 -> 'a

View file

@ -0,0 +1,26 @@
module C = Configurator.V1
let unix = {|
#include <math.h>
void *addr = &sin;
int main(void) {
return 0;
}
|}
let windows = {|
#include <winsock2.h>
void *addr = &gethostname;
int main(void) {
return 0;
}
|}
let main c =
let code = if Sys.os_type = "Win32" then windows else unix in
let b = C.c_test c code in
let f = open_out_bin "out" in
output_char f (if b then '1' else '0');
close_out f
let () = C.main ~name:"configurator-c-libraries" main

View file

@ -0,0 +1,3 @@
(executable
(name discover)
(libraries dune.configurator))

View file

@ -0,0 +1,13 @@
Test that configurator always picks the value of the `c_libraries`
flag from `ocamlc -config`. If not, there's a failure to link a
configuration test program that uses functions from these libraries.
For that, we need functions outside of libc. On Unix, that would be
`sin(3)` that requires the `-lm` flag for the math library, and on
Windows `gethostname` that requires WinSock2 (`ws2_32.dll`).
link successfully
==================================
$ dune exec -- ./discover.exe
$ cat out
1

View file

@ -0,0 +1,4 @@
let () =
let module C = Configurator.V1 in
C.main ~name:"foo" (fun _c ->
C.Flags.write_lines "foo" ["asdf"])

View file

@ -0,0 +1,4 @@
(executable
(name discover)
(modules discover)
(libraries dune.configurator))

View file

@ -0,0 +1 @@
$ dune exec ./discover.exe

View file

@ -0,0 +1,3 @@
(executable
(name run)
(libraries dune.configurator))

View file

@ -0,0 +1,16 @@
module Configurator = Configurator.V1
let () =
Configurator.main ~name:"c_test" (fun t ->
let c_result =
Configurator.c_test t {c|
#include <stdio.h>
int main(void)
{
printf("Hello, World!");
return 0;
}
|c} in
assert c_result;
print_endline "Successfully compiled c program"
)

View file

@ -0,0 +1,3 @@
(executable
(name run)
(libraries dune.configurator))

View file

@ -0,0 +1,19 @@
module Configurator = Configurator.V1
let () =
begin match Sys.getenv "INSIDE_DUNE" with
| exception Not_found -> failwith "INSIDE_DUNE is not passed"
| "1" -> print_endline "INSIDE_DUNE is from an old dune"
| dir -> print_endline "INSIDE_DUNE is present";
let config_path = ".dune/configurator.v2" in
Printf.printf "%s file is %s\n" config_path
(if Sys.file_exists (Filename.concat dir config_path) then
"present"
else
"not present")
end;
Configurator.main ~name:"config" (fun t ->
match Configurator.ocaml_config_var t "version" with
| None -> failwith "version is absent"
| Some _ -> print_endline "version is present"
)

View file

@ -0,0 +1,3 @@
(executable
(name run)
(libraries dune.configurator))

View file

@ -0,0 +1,24 @@
module Configurator = Configurator.V1
let () =
let module C_define = Configurator.C_define in
Configurator.main ~name:"c_test" (fun t ->
C_define.import t
~prelude:"#define CONFIGURATOR_TESTING \"foobar\"\n\
#define CONFIGURATOR_NEG_INT -127\n"
~includes:["caml/config.h"]
[ "CAML_CONFIG_H", C_define.Type.Switch
; "Page_log", C_define.Type.Int
; "CONFIGURATOR_TESTING", C_define.Type.String
; "CONFIGURATOR_NEG_INT", C_define.Type.Int
; "sizeof(char)", C_define.Type.Int
]
|> List.iter (fun (n, v) ->
Printf.printf "%s=%s\n"
n (match v with
| C_define.Value.String s -> s
| Int i -> string_of_int i
| Switch b -> string_of_bool b
)
)
)

View file

@ -0,0 +1,17 @@
Show that config values are present
$ dune exec config/run.exe
INSIDE_DUNE is present
.dune/configurator.v2 file is present
version is present
We're able to compile C program successfully
$ dune exec c_test/run.exe
Successfully compiled c program
Importing #define's from code is successful
$ dune exec import-define/run.exe
CAML_CONFIG_H=true
Page_log=12
CONFIGURATOR_TESTING=foobar
CONFIGURATOR_NEG_INT=-127
sizeof(char)=1

View file

@ -0,0 +1,13 @@
(cram
(deps
(package dune)
(package dune-configurator)))
(cram
(applies_to pkg-config-quoting)
(deps %{bin:pkg-config}))
(cram
(enabled_if
(<> %{ocaml-config:system} win))
(applies_to configurator.t))

View file

@ -0,0 +1,39 @@
(*
* OWL - OCaml Scientific and Engineering Computing
* Copyright (c) 2016-2022 Liang Wang <liang@ocaml.xyz>
*)
module Configurator = Configurator.V1
let header = {|
#define TEST "test"
|}
let default_cflags c =
let test =
let headerfile =
let file, fd =
Filename.open_temp_file ~mode:[ Open_wronly ] "discover" "test.h"
in
output_string fd header;
close_out fd;
file
in
let platform =
assert (Sys.file_exists headerfile);
Configurator.C_define.import c ~includes:[ headerfile ] [ ("TEST", String) ]
in
match List.map snd platform with
| [ String "test" ] -> `test
| _ -> `unknown
in
match test with `test -> [] | _ -> assert false
let () =
let flags_file = ref "" in
let args = ["-target", Arg.Set_string flags_file , "flags file"] in
Configurator.main ~args ~name:"test" (fun c ->
let libs = [] in
let cflags = default_cflags c in
let conf : Configurator.Pkg_config.package_conf = { cflags; libs } in
Configurator.Flags.write_sexp !flags_file conf.cflags)

View file

@ -0,0 +1,3 @@
(executable
(name configure)
(libraries dune.configurator))

View file

@ -0,0 +1,3 @@
(rule
(targets c_flags.sexp)
(action (run configure/configure.exe -target %{targets})))

View file

@ -0,0 +1,6 @@
Test that dune-configurator's `C_define.import` is able to include
custom header files correctly.
C_define.import functions properly
====================================================================
$ dune build ./c_flags.sexp

View file

@ -0,0 +1,13 @@
module C = Configurator.V1
let () =
C.main ~name:"config_test" (fun t ->
let pkg_config =
match C.Pkg_config.get t with
| None -> assert false
| Some p -> p
in
let query package = ignore (C.Pkg_config.query pkg_config ~package) in
query "dummy-pkg";
)

View file

@ -0,0 +1,19 @@
(executable
(name pkgconf)
(modules pkgconf))
(env
(_
(binaries
(./pkgconf.exe as pkgconf))))
(executable
(name config_test)
(libraries dune.configurator)
(modules config_test))
(rule
(alias default)
(deps %{bin:pkgconf})
(action
(run ./config_test.exe -verbose)))

View file

@ -0,0 +1,8 @@
(* We'd like to use String.equal but that's OCaml >= 4.03 *)
let not_flag x = not ("--print-errors" = x)
let () =
let args = List.tl (Array.to_list Sys.argv) in
let args = List.filter not_flag args in
Format.printf "@[<v>%a@]@."
(Format.pp_print_list Format.pp_print_string) args

View file

@ -0,0 +1,46 @@
$ unset PKG_CONFIG_ARGN
$ unset PKG_CONFIG
These tests show that setting `PKG_CONFIG_ARGN` passes extra args to `pkg-config`
$ dune build 2>&1 | awk '/run:.*bin\/pkgconf/{a=1}/stderr/{a=0}a' | sed s/$(ocamlc -config | sed -n "/^target:/ {s/target: //; p; }")/\$TARGET/g
run: $TESTCASE_ROOT/_build/default/.bin/pkgconf --personality $TARGET --print-errors dummy-pkg
-> process exited with code 0
-> stdout:
| --personality
| $TARGET
| dummy-pkg
run: $TESTCASE_ROOT/_build/default/.bin/pkgconf --personality $TARGET --cflags dummy-pkg
-> process exited with code 0
-> stdout:
| --personality
| $TARGET
| --cflags
| dummy-pkg
run: $TESTCASE_ROOT/_build/default/.bin/pkgconf --personality $TARGET --libs dummy-pkg
-> process exited with code 0
-> stdout:
| --personality
| $TARGET
| --libs
| dummy-pkg
$ dune clean
$ PKG_CONFIG_ARGN="--static" dune build 2>&1 | awk '/run:.*bin\/pkgconf/{a=1}/stderr/{a=0}a'
run: $TESTCASE_ROOT/_build/default/.bin/pkgconf --static --print-errors dummy-pkg
-> process exited with code 0
-> stdout:
| --static
| dummy-pkg
run: $TESTCASE_ROOT/_build/default/.bin/pkgconf --static --cflags dummy-pkg
-> process exited with code 0
-> stdout:
| --static
| --cflags
| dummy-pkg
run: $TESTCASE_ROOT/_build/default/.bin/pkgconf --static --libs dummy-pkg
-> process exited with code 0
-> stdout:
| --static
| --libs
| dummy-pkg

View file

@ -0,0 +1,15 @@
module C = Configurator.V1
let () =
C.main ~name:"config_test" (fun t ->
let pkg_config =
match C.Pkg_config.get t with
| None -> assert false
| Some p -> p
in
let query package = ignore (C.Pkg_config.query pkg_config ~package) in
query "gtk+-quartz-3.0";
query "gtk+-quartz-3.0 >= 3.18";
query "gtksourceview-3.0 >= 3.18"
)

View file

@ -0,0 +1,14 @@
(executable
(name pkg_config)
(public_name pkg-config)
(modules pkg_config))
(executable
(name config_test)
(libraries dune.configurator)
(modules config_test))
(alias
(name default)
(deps (package pkg-config))
(action (run ./config_test.exe -verbose)))

View file

@ -0,0 +1,8 @@
(* We'd like to use String.equal but that's OCaml >= 4.03 *)
let not_flag x = not ("--print-errors" = x)
let () =
let args = List.tl (Array.to_list Sys.argv) in
let args = List.filter not_flag args in
Format.printf "@[<v>%a@]@."
(Format.pp_print_list Format.pp_print_string) args

View file

@ -0,0 +1,44 @@
These tests show how various pkg-config invocations get quotes (and test specifying a custom PKG_CONFIG):
$ PKG_CONFIG=$PWD/_build/install/default/bin/pkg-config dune build 2>&1 | awk '/run:.*bin\/pkg-config/{a=1}/stderr/{a=0}a'
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --print-errors gtk+-quartz-3.0
-> process exited with code 0
-> stdout:
| gtk+-quartz-3.0
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --cflags gtk+-quartz-3.0
-> process exited with code 0
-> stdout:
| --cflags
| gtk+-quartz-3.0
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --libs gtk+-quartz-3.0
-> process exited with code 0
-> stdout:
| --libs
| gtk+-quartz-3.0
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --print-errors 'gtk+-quartz-3.0 >= 3.18'
-> process exited with code 0
-> stdout:
| gtk+-quartz-3.0 >= 3.18
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --cflags 'gtk+-quartz-3.0 >= 3.18'
-> process exited with code 0
-> stdout:
| --cflags
| gtk+-quartz-3.0 >= 3.18
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --libs 'gtk+-quartz-3.0 >= 3.18'
-> process exited with code 0
-> stdout:
| --libs
| gtk+-quartz-3.0 >= 3.18
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --print-errors 'gtksourceview-3.0 >= 3.18'
-> process exited with code 0
-> stdout:
| gtksourceview-3.0 >= 3.18
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --cflags 'gtksourceview-3.0 >= 3.18'
-> process exited with code 0
-> stdout:
| --cflags
| gtksourceview-3.0 >= 3.18
run: $TESTCASE_ROOT/_build/install/default/bin/pkg-config --libs 'gtksourceview-3.0 >= 3.18'
-> process exited with code 0
-> stdout:
| --libs
| gtksourceview-3.0 >= 3.18

View file

@ -0,0 +1,4 @@
(test
(name test_configurator)
(package dune-configurator)
(libraries configurator))

View file

@ -0,0 +1,3 @@
module Configurator = Configurator.V1
let () = Configurator.main ~name:"test_configurator" (fun _ -> ())

View file

@ -0,0 +1,12 @@
open Dune_action_plugin.V1
let action =
let open O in
let path_to_dependency = read_file ~path:(Path.of_string "foo_or_bar") in
path_to_dependency
|> stage ~f:(fun path_to_dependency ->
let+ data = read_file ~path:(Path.of_string path_to_dependency) in
print_endline data)
;;
let () = run action

View file

@ -0,0 +1,7 @@
(executables
(names simple_concatenation choosing_dependency)
(libraries dune_action_plugin))
(alias
(name examples)
(deps simple_concatenation.exe))

View file

@ -0,0 +1,12 @@
open Dune_action_plugin.V1
let action =
let source1 = read_file ~path:(Path.of_string "source1")
and source2 = read_file ~path:(Path.of_string "source2") in
both source1 source2
|> stage ~f:(fun (source1, source2) ->
let data = source1 ^ source2 in
write_file ~path:(Path.of_string "target") ~data)
;;
let () = run action

View file

@ -0,0 +1,6 @@
(library
(name dune_action_plugin)
(public_name dune-action-plugin)
(libraries stdune csexp dune-glob unix dune-rpc.private)
(synopsis
"[Internal] Monadic interface for defining scripts with dynamic or complex sets of dependencies."))

View file

@ -0,0 +1,211 @@
open Import
module V1 = struct
module Path = Path
module Glob = Dune_glob.V1
open Protocol
module Execution_error = struct
exception E of string
let raise string = raise (E string)
let raise_on_fs_error = function
| Error message -> raise message
| Ok result -> result
;;
end
module Fs : sig
val read_directory : string -> (string list, string) result
val read_file : string -> (string, string) result
val write_file : string -> string -> (unit, string) result
end = struct
let catch_system_exceptions f ~name =
try Ok (f ()) with
| Unix.Unix_error (error, syscall, arg) ->
let error = Unix_error.Detailed.create error ~syscall ~arg in
Error (name ^ ": " ^ Unix_error.Detailed.to_string_hum error)
| Sys_error error -> Error (name ^ ": " ^ error)
;;
let read_directory =
let rec loop dh acc =
match Unix.readdir dh with
| "." | ".." -> loop dh acc
| s -> loop dh (s :: acc)
| exception End_of_file -> acc
in
fun path ->
catch_system_exceptions ~name:"read_directory" (fun () ->
let dh = Unix.opendir path in
Exn.protect
~f:(fun () -> loop dh [] |> List.sort ~compare:String.compare)
~finally:(fun () -> Unix.closedir dh))
;;
let read_file path =
catch_system_exceptions ~name:"read_file" (fun () -> Io.String_path.read_file path)
;;
let write_file path data =
catch_system_exceptions ~name:"write_file" (fun () ->
Io.String_path.write_file path data)
;;
end
module Stage = struct
type 'a t =
{ action : unit -> 'a
; dependencies : Dependency.Set.t
; targets : String.Set.t
}
let map (t : 'a t) ~f = { t with action = (fun () -> f (t.action ())) }
let both (t1 : 'a t) (t2 : 'b t) =
{ action = (fun () -> t1.action (), t2.action ())
; dependencies = Dependency.Set.union t1.dependencies t2.dependencies
; targets = String.Set.union t1.targets t2.targets
}
;;
end
(* Construction inspired by free monad. *)
type 'a t =
| Pure of 'a
| Stage of 'a t Stage.t
let lift_stage stage = Stage (Stage.map stage ~f:(fun a -> Pure a))
let rec map (t : 'a t) ~f =
match t with
| Pure a -> Pure (f a)
| Stage at -> Stage (Stage.map ~f:(map ~f) at)
;;
let rec stage (t : 'a t) ~f =
match t with
| Pure a -> f a
| Stage at -> Stage (Stage.map ~f:(stage ~f) at)
;;
let return a = Pure a
let rec both (t1 : 'a t) (t2 : 'b t) =
match t1, t2 with
| Pure a1, _ -> map ~f:(fun a2 -> a1, a2) t2
| _, Pure a2 -> map ~f:(fun a1 -> a1, a2) t1
| Stage at1, Stage at2 ->
Stage (Stage.both at1 at2 |> Stage.map ~f:(fun (am1, am2) -> both am1 am2))
;;
let read_file ~path =
let path = Path.to_string path in
let action () = Fs.read_file path |> Execution_error.raise_on_fs_error in
lift_stage
{ action
; dependencies = Dependency.Set.singleton (File path)
; targets = String.Set.empty
}
;;
let write_file ~path ~data =
let path = Path.to_string path in
let action () = Fs.write_file path data |> Execution_error.raise_on_fs_error in
lift_stage
{ action; dependencies = Dependency.Set.empty; targets = String.Set.singleton path }
;;
(* TODO jstaron: If program tries to read empty directory, dune does not copy
it to `_build` so we get a "No such file or directory" error. *)
let read_directory_with_glob ~path ~glob =
let path = Path.to_string path in
let action () =
Fs.read_directory path
|> Execution_error.raise_on_fs_error
|> List.filter ~f:(Glob.test glob)
in
lift_stage
{ action
; dependencies =
Dependency.Set.singleton (Glob { path; glob = Glob.to_string glob })
; targets = String.Set.empty
}
;;
let rec run_by_dune t context =
match t with
| Pure () -> Context.respond context Done
| Stage at ->
let allowed_targets = Context.targets context in
let disallowed_targets = String.Set.diff at.targets allowed_targets in
(match String.Set.to_list disallowed_targets with
| [] -> ()
| [ t ] ->
Execution_error.raise
(Printf.sprintf
"%s is written despite not being declared as a target in dune file. To \
fix, add it to target list in dune file."
t)
| ts ->
Execution_error.raise
(Printf.sprintf
"Following files were written despite not being declared as targets in \
dune file:\n\
%sTo fix, add them to target list in dune file."
(ts |> String.concat ~sep:"\n")));
let prepared_dependencies = Context.prepared_dependencies context in
let required_dependencies =
Dependency.Set.diff at.dependencies prepared_dependencies
in
if Dependency.Set.is_empty required_dependencies
then run_by_dune (at.action ()) context
else Context.respond context (Need_more_deps required_dependencies)
;;
(* If executable is not run by dune, assume that all dependencies are already
prepared and no target checking is done. *)
let rec run_outside_of_dune t =
match t with
| Pure () -> ()
| Stage at -> run_outside_of_dune (at.action ())
;;
let do_run t =
match Protocol.Context.create () with
| Run_outside_of_dune -> run_outside_of_dune t
| Error message ->
Execution_error.raise
(Printf.sprintf
"Error during communication with dune. %s Did you use different dune version \
to compile the executable?"
message)
| Ok context -> run_by_dune t context
;;
let run t =
try
do_run t;
exit 0
with
| Execution_error.E message ->
prerr_endline message;
exit 1
;;
module O = struct
let ( let+ ) at f = map at ~f
let ( and+ ) = both
end
module Private = struct
module Protocol = Protocol
let do_run = do_run
module Execution_error = Execution_error
end
end
module Private = V1.Private

View file

@ -0,0 +1,122 @@
[@@@alert unstable "The API of this library is not stable and may change without notice."]
[@@@alert "-unstable"]
module V1 : sig
(** Applicative and monadic interface for declaring dependencies.
This module is intended to be used as an interface for declaring
dependencies of a computation. Dependencies can be declared dynamically -
the list of dependencies can depend on previous dependencies.
Note: Monadic "bind" is provided, but it can be very costly. It's called
[stage] to discourage people from overusing it. When dune decides that the
action needs to be re-run, it runs (nontrivial) stages one by one, and
starts a process from scratch for every stage. So a linear chain of binds
leads to a linear number of program re-runs, and therefore overall
quadratic time complexity. This also means that using non-deterministic
mutable state can lead to surprising results. (note that with the current
implementation, nontrivial stages are those that have some dependencies,
so a stage that merely writes out some targets is "free") *)
module Path = Path
type 'a t
(** {1:monadic_interface Applicative/monadic interface} *)
(** [return a] creates a pure computation resulting in [a]. *)
val return : 'a -> 'a t
(** If [at] is a computation resulting in [a] then [map at ~f] is a
computation resulting in [f a]. *)
val map : 'a t -> f:('a -> 'b) -> 'b t
(** If [at] is a computation resulting in [a] and [bt] is computation
resulting in [b] then [both at bt] is a computation resulting in [(a, b)]. *)
val both : 'a t -> 'b t -> ('a * 'b) t
(** If [at] is a computation resulting in value of type ['a] and [f] is a
function taking value of type ['a] and returning a computation [bt] then
[stage a ~f] is a computation that is equivalent to staging computation
[bt] after computation [at].
Note: This is a monadic "bind" function. This function is costly so
different name was chosen to discourage excessive use. *)
val stage : 'a t -> f:('a -> 'b t) -> 'b t
(** {1 Syntax sugar for applicative subset} *)
(** Syntax sugar for applicative subset of the interface. Syntax sugar for
[stage] is not provided to prevent accidental use.*)
module O : sig
(** {[
let+ a = g in
h
]}
is equivalent to:
{[
map g ~f:(fun a -> h)
]} *)
val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t
(** {[
let+ a1 = g1
and+ a2 = g2 in
h
]}
is equivalent to:
{[
both g1 g2 |> map ~f:(fun (a1, a2) -> h)
]} *)
val ( and+ ) : 'a t -> 'b t -> ('a * 'b) t
end
(** {1:interaction Declaring dependencies and interacting with filesystem} *)
(** [read_file ~path:file] returns a computation depending on a [file] to be
run and resulting in a file content. *)
val read_file : path:Path.t -> string t
(** [write_file ~path:file ~data] returns a computation that writes [data] to
a [file].
Note: [file] must be declared as a target in dune build file. *)
val write_file : path:Path.t -> data:string -> unit t
(** [read_directory_with_glob ~path:directory ~glob] returns a computation
depending on a listing of a [directory] (including source and target
files) filtered by glob and resulting in that listing.
It's better to specify as narrow filtering by [glob] as possible (as
opposed to filtering afterwards) because this makes dune aware of the
filtering, so dune won't re-run the action when the directory changes in
an unimportant way.
BUG: [read_directory_with_glob] doesn't work correctly for empty
directories.
BUG: the returned listing includes directories even though that dependency
is not tracked. *)
val read_directory_with_glob : path:Path.t -> glob:Dune_glob.V1.t -> string list t
(** {1:running Running the computation} *)
(** Runs the computation. This function never returns. *)
val run : unit t -> 'a
end
(* [Private] module should only be used by dune itself. Its stability will not
be maintained by the future library releases. *)
module Private : sig
module Protocol = Protocol
val do_run : unit V1.t -> unit
module Execution_error : sig
exception E of string
end
end

View file

@ -0,0 +1,16 @@
include struct
open Stdune
module Unix_error = Unix_error
module List = List
module Set = Set
module Exn = Exn
module String = String
module Io = Io
module Sexp = Sexp
module Option = Option
module Comparable = Comparable
module Result = Result
module Map = Map
end
module Conv = Dune_rpc_private.Conv

View file

@ -0,0 +1,19 @@
type t = string
let concat = Filename.concat
let to_string t = t
let of_string path =
match Filename.is_relative path with
| false ->
invalid_arg
(Printf.sprintf
"Path \"%s\" is absolute. All paths used with dune-action-plugin must be \
relative."
path)
| true -> path
;;
module O = struct
let ( ^/ ) = concat
end

View file

@ -0,0 +1,20 @@
(** Representation of paths for "dune_action_plugin" library. *)
(** We shouldn't use absolute paths when communicating with Dune, so this module
allows user to represent only relative paths. *)
type t
module O : sig
(** Concatenate two paths. *)
val ( ^/ ) : t -> t -> t
end
(** Concatenate two paths. *)
val concat : t -> t -> t
val to_string : t -> string
(** Convert path to string. Throws an Invalid_argument exception if passed
string is not a relative path. *)
val of_string : string -> t

View file

@ -0,0 +1,194 @@
open Import
let run_by_dune_env_variable = "DUNE_DYNAMIC_RUN_CLIENT"
module Error = Sexpable_intf.Error
module Dependency = struct
module T = struct
type t =
| File of string
| Directory of string
| Glob of
{ path : string
; glob : string
}
let conv =
let open Conv in
let file = constr "File" string (fun s -> File s) in
let directory = constr "Directory" string (fun s -> Directory s) in
let glob_cstr =
constr "Glob" (pair string string) (fun (path, glob) -> Glob { path; glob })
in
sum
[ econstr file; econstr directory; econstr glob_cstr ]
(function
| File s -> case s file
| Directory s -> case s directory
| Glob { path; glob } -> case (path, glob) glob_cstr)
;;
let compare x y =
match x, y with
| File x, File y -> String.compare x y
| File _, _ -> Lt
| _, File _ -> Gt
| Directory x, Directory y -> String.compare x y
| Directory _, _ -> Lt
| _, Directory _ -> Gt
| Glob { path; glob }, Glob t ->
let open Ordering.O in
let= () = String.compare path t.path in
String.compare glob t.glob
;;
let to_dyn = Dyn.opaque
end
include T
module O = Comparable.Make (T)
module Map = O.Map
module Set = struct
include O.Set
let conv : t Conv.value = Conv.iso (Conv.list conv) of_list to_list
end
end
module Greeting = struct
module T = struct
type t =
{ run_arguments_fn : string
; response_fn : string
}
let conv =
let open Conv in
let to_ (run_arguments_fn, response_fn) = { run_arguments_fn; response_fn } in
let from { run_arguments_fn; response_fn } = run_arguments_fn, response_fn in
iso (pair string string) to_ from
;;
let version = 0
end
include T
include Sexpable_intf.Make (T)
end
module Run_arguments = struct
module T = struct
type t =
{ prepared_dependencies : Dependency.Set.t
; targets : String.Set.t
}
let conv =
let from { prepared_dependencies; targets } = prepared_dependencies, targets in
let to_ (prepared_dependencies, targets) = { prepared_dependencies; targets } in
let string_set =
Conv.iso Conv.(list string) String.Set.of_list String.Set.to_list
in
let conv = Conv.pair Dependency.Set.conv string_set in
Conv.iso conv to_ from
;;
let version = 0
end
include T
include Sexpable_intf.Make (T)
end
module Response = struct
module T = struct
type t =
| Done
| Need_more_deps of Dependency.Set.t
let conv =
let open Conv in
let done_ = constr "Done" unit (fun () -> Done) in
let need_more_deps =
constr "Need_more_deps" Dependency.Set.conv (fun deps -> Need_more_deps deps)
in
sum
[ econstr done_; econstr need_more_deps ]
(function
| Done -> case () done_
| Need_more_deps deps -> case deps need_more_deps)
;;
let version = 0
end
include T
include Sexpable_intf.Make (T)
end
module Context = struct
type t =
{ response_fn : string
; prepared_dependencies : Dependency.Set.t
; targets : String.Set.t
}
type create_result =
| Ok of t
| Run_outside_of_dune
| Error of string
let cannot_parse_error = Error "Can not parse dune message."
let version_mismatch_error =
Error
"Dune version is incompatible with dune-action-plugin library version that was \
used to build this executable."
;;
let cannot_read_file = Error "Cannot read file containing dune message."
let file_not_found_error = Error "Cannot find file containing dune message."
let create () =
match Sys.getenv_opt run_by_dune_env_variable with
| None -> Run_outside_of_dune
| Some value ->
(match Csexp.parse_string value with
| Error _ -> cannot_parse_error
| Ok sexp ->
(match Greeting.of_sexp sexp with
| Error (Version_mismatch _) -> version_mismatch_error
| Error Parse_error -> cannot_parse_error
| Ok greeting ->
(match
( Result.try_with (fun () ->
Io.String_path.read_file greeting.run_arguments_fn)
, Sys.file_exists greeting.response_fn )
with
| _, false -> file_not_found_error
| Error _, _ -> cannot_read_file
| Ok data, true ->
(match Csexp.parse_string data with
| Error _ -> cannot_parse_error
| Ok sexp ->
(match Run_arguments.of_sexp sexp with
| Error (Version_mismatch _) -> version_mismatch_error
| Error Parse_error -> cannot_parse_error
| Ok { prepared_dependencies; targets } ->
Ok
{ response_fn = greeting.response_fn
; prepared_dependencies
; targets
})))))
;;
let prepared_dependencies (t : t) = t.prepared_dependencies
let targets (t : t) = t.targets
let respond (t : t) response =
let data = Response.to_sexp response |> Csexp.to_string in
Io.String_path.write_file t.response_fn data
;;
end

View file

@ -0,0 +1,62 @@
open Import
open Sexpable_intf
module Error : module type of Error
module Dependency : sig
type t =
| File of string
| Directory of string
| Glob of
{ path : string
; glob : string
}
module Map : Map.S with type key = t
module Set : sig
include Set.S with type elt = t and type 'a map = 'a Map.t
end
end
module Greeting : sig
type t =
{ run_arguments_fn : string
; response_fn : string
}
include Sexpable with type t := t
end
module Run_arguments : sig
type t =
{ prepared_dependencies : Dependency.Set.t
; targets : String.Set.t
}
include Sexpable with type t := t
end
module Response : sig
type t =
| Done
| Need_more_deps of Dependency.Set.t
include Sexpable with type t := t
end
(** Dune sets this environment variable to pass [Greeting.t] to client. *)
val run_by_dune_env_variable : string
module Context : sig
type t
type create_result =
| Ok of t
| Run_outside_of_dune
| Error of string
val create : unit -> create_result
val prepared_dependencies : t -> Dependency.Set.t
val targets : t -> String.Set.t
val respond : t -> Response.t -> unit
end

View file

@ -0,0 +1,42 @@
open Import
module Error = struct
type t =
| Version_mismatch of int
| Parse_error
end
module type Sexpable = sig
type t
val to_sexp : t -> Sexp.t
val of_sexp : Sexp.t -> (t, Error.t) result
end
module type S = sig
type t
val conv : t Conv.value
val version : int
end
module Make (Type : S) = struct
let conv =
let open Conv in
pair int Type.conv
;;
let of_sexp sexp : (_, Error.t) result =
match Conv.of_sexp Conv.(pair int sexp) ~version:(0, 0) sexp with
| Error _ -> Error Parse_error
| Ok (version, sexp) ->
(match Int.equal version Type.version with
| false -> Error (Version_mismatch version)
| true ->
(match Conv.of_sexp Type.conv ~version:(0, 0) sexp with
| Error _ -> Error Parse_error
| Ok v -> Ok v))
;;
let to_sexp t = Conv.to_sexp conv (Type.version, t)
end

View file

@ -0,0 +1,3 @@
(executables
(names foo)
(libraries dune-action-plugin dune-glob))

View file

@ -0,0 +1,12 @@
open Dune_action_plugin.V1
module Glob = Dune_glob.V1
let action =
let open Dune_action_plugin.V1.O in
let+ listing =
read_directory_with_glob ~glob:Glob.universal ~path:(Path.of_string "some_dir")
in
String.concat "; " listing |> Printf.printf "Directory listing: [%s]"
;;
let () = run action

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

View file

@ -0,0 +1,21 @@
$ cat > dune-project << EOF
> (lang dune 2.0)
> (using action-plugin 0.1)
> EOF
$ cat > dune << EOF
> (data_only_dirs some_dir)
> \
> (rule
> (alias runtest)
> (action (dynamic-run ./foo.exe)))
> EOF
$ mkdir some_dir
$ touch some_dir/some_file1
$ touch some_dir/some_file2
$ cp ./bin/foo.exe ./
$ dune runtest
Directory listing: [some_file1; some_file2]

View file

@ -0,0 +1,3 @@
(executables
(names foo)
(libraries dune-action-plugin))

View file

@ -0,0 +1,9 @@
open Dune_action_plugin.V1
let action =
let open Dune_action_plugin.V1.O in
let+ data = read_file ~path:(Path.of_string "some_file") in
print_endline data
;;
let () = run action

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

View file

@ -0,0 +1,31 @@
This test checks that 'dynamic-run' will not be reexecuted if
dependencies do not change (have the same digest) even if
they were forced to rebuild.
$ cat > dune-project << EOF
> (lang dune 2.0)
> (using action-plugin 0.1)
> EOF
$ cat > dune << EOF
> (rule
> (target some_file)
> (deps (universe))
> (action
> (progn
> (echo "Building some_file!\n")
> (with-stdout-to %{target} (echo "Hello from some_file!")))))
> \
> (rule
> (alias runtest)
> (action (dynamic-run ./foo.exe)))
> EOF
$ cp ./bin/foo.exe ./
$ dune runtest
Building some_file!
Hello from some_file!
$ dune runtest
Building some_file!

View file

@ -0,0 +1,3 @@
(executables
(names foo)
(libraries dune_action_plugin dune_glob))

View file

@ -0,0 +1,11 @@
open Dune_action_plugin.V1
module Glob = Dune_glob.V1
let action =
let open Dune_action_plugin.V1.O in
let glob = Glob.of_string "some_file*" in
let+ listing = read_directory_with_glob ~path:(Path.of_string "some_dir") ~glob in
String.concat "\n" listing |> print_endline
;;
let () = run action

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

View file

@ -0,0 +1,43 @@
$ cat > dune-project << EOF
> (lang dune 2.0)
> (using action-plugin 0.1)
> EOF
$ cat > dune << EOF
> (rule
> (alias runtest)
> (action (dynamic-run ./foo.exe)))
> EOF
$ mkdir some_dir
$ cat > some_dir/dune << EOF
> (rule
> (target some_file)
> (action
> (progn
> (echo "Building some_file!\n")
> (with-stdout-to %{target} (echo "")))))
> \
> (rule
> (target another_file)
> (action
> (progn
> (echo "SHOULD NOT BE PRINTED!")
> (with-stdout-to %{target} (echo "")))))
> \
> (rule
> (target some_file_but_different)
> (action
> (progn
> (echo "Building some_file_but_different!\n")
> (with-stdout-to %{target} (echo "")))))
> EOF
$ cp ./bin/foo.exe ./
$ dune runtest
Building some_file!
Building some_file_but_different!
some_file
some_file_but_different

View file

@ -0,0 +1,3 @@
(executables
(names foo)
(libraries dune-action-plugin dune-glob))

View file

@ -0,0 +1,11 @@
open Dune_action_plugin.V1
module Glob = Dune_glob.V1
let action =
let open Dune_action_plugin.V1.O in
let+ _ = read_directory_with_glob ~glob:Glob.universal ~path:(Path.of_string ".")
and+ _ = write_file ~path:(Path.of_string "some_file") ~data:"Hello from some_file!" in
()
;;
let () = run action

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

View file

@ -0,0 +1,22 @@
$ cat > dune-project << EOF
> (lang dune 2.0)
> (using action-plugin 0.1)
> EOF
$ cat > dune << EOF
> (rule
> (target some_file)
> (action
> (dynamic-run ./foo.exe)))
> EOF
$ cp ./bin/foo.exe ./
$ dune build some_file 2>&1 | awk '/Internal error/,/unable to serialize/'
Internal error, please report upstream including the contents of _build/log.
Description:
("unable to serialize exception",
^ This is not great. There is no actual dependency cycle, dune is just
interpreting glob dependency too coarsely (it builds all files instead
of just bringing the directory listing up to date).

View file

@ -0,0 +1,3 @@
(executables
(names foo1 foo2)
(libraries dune-action-plugin))

View file

@ -0,0 +1,5 @@
open Dune_action_plugin.V1
let path = Path.of_string "some_file1"
let action = read_file ~path |> stage ~f:(fun data -> write_file ~path ~data)
let () = run action

View file

@ -0,0 +1,13 @@
open Dune_action_plugin.V1
let path = Path.of_string "some_file2"
let action =
let open Dune_action_plugin.V1.O in
write_file ~path ~data:"Hello from some_file2!"
|> stage ~f:(fun () ->
let+ data = read_file ~path in
print_endline data)
;;
let () = run action

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

View file

@ -0,0 +1,29 @@
$ cat > dune-project << EOF
> (lang dune 2.0)
> (using action-plugin 0.1)
> EOF
$ cat > dune << EOF
> (rule
> (target some_file1)
> (action
> (dynamic-run ./foo1.exe)))
> \
> (rule
> (target some_file2)
> (action
> (dynamic-run ./foo2.exe)))
> EOF
$ cp ./bin/foo1.exe ./
$ cp ./bin/foo2.exe ./
$ dune build some_file1
Error: Dependency cycle between:
_build/default/some_file1
[1]
$ dune build some_file2
Error: Dependency cycle between:
_build/default/some_file2
[1]

View file

@ -0,0 +1,11 @@
open Dune_action_plugin.V1
let action =
let open Dune_action_plugin.V1.O in
let switch = read_file ~path:(Path.of_string "foo_or_bar") in
stage switch ~f:(fun file ->
let+ data = read_file ~path:(Path.of_string file) in
print_endline data)
;;
let () = run action

View file

@ -0,0 +1,3 @@
(executables
(names client)
(libraries dune-action-plugin))

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

View file

@ -0,0 +1,57 @@
This test checks that in case the dependency of multi staged computation changes,
only the dependencies up to this stage are rebuilt.
$ cat > dune-project << EOF
> (lang dune 2.0)
> (using action-plugin 0.1)
> EOF
$ cat > dune << EOF
> (rule
> (deps bar_source)
> (target bar)
> (action
> (progn
> (echo "Building bar!\n")
> (copy %{deps} %{target}))))
> \
> (rule
> (deps foo_source)
> (target foo)
> (action
> (progn
> (echo "Building foo!\n")
> (copy %{deps} %{target}))))
> \
> (rule
> (deps foo_or_bar_source)
> (target foo_or_bar)
> (action
> (progn
> (echo "Building foo_or_bar!\n")
> (copy %{deps} %{target}))))
> \
> (rule
> (alias runtest)
> (action (dynamic-run ./client.exe)))
> EOF
$ cp ./bin/client.exe ./
$ printf "foo" > foo_or_bar_source
$ printf "Hello from foo!" > foo_source
$ printf "SHOULD NOT BE PRINTED!" > bar_source
$ dune runtest
Building foo_or_bar!
Building foo!
Hello from foo!
$ printf "bar" > foo_or_bar_source
$ printf "SHOULD NOT BE PRINTED!" > foo_source
$ printf "Hello from bar!" > bar_source
$ dune runtest
Building foo_or_bar!
Building bar!
Hello from bar!

View file

@ -0,0 +1,5 @@
(cram
(applies_to :whole_subtree)
(alias run_dynamic)
(deps
(package dune)))

View file

@ -0,0 +1,3 @@
(executables
(names foo)
(libraries dune-action-plugin))

View file

@ -0,0 +1,3 @@
open Dune_action_plugin.V1
let () = run (return ())

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

View file

@ -0,0 +1,32 @@
Check that multiple 'dynamic-run' commands within single action are
detected and error is printed even if the rule is not executed.
$ cat > dune-project << EOF
> (lang dune 2.0)
> (using action-plugin 0.1)
> EOF
$ cat > dune << EOF
> (rule
> (target some_file)
> (action
> (progn
> (dynamic-run ./foo.exe some_arg)
> (dynamic-run ./foo.exe another_arg))))
> \
> (alias
> (name runtest)
> (action
> (echo "SHOULD NOT BE PRINTED")))
> EOF
$ cp ../bin/foo.exe ./
$ dune runtest
File "dune", lines 4-6, characters 2-84:
4 | (progn
5 | (dynamic-run ./foo.exe some_arg)
6 | (dynamic-run ./foo.exe another_arg))))
Error: Multiple 'dynamic-run' commands within single action are not
supported.
[1]

View file

@ -0,0 +1,3 @@
(executables
(names foo)
(libraries dune_action_plugin))

View file

@ -0,0 +1,3 @@
open Dune_action_plugin.V1
let () = run (return (print_endline "Hello from foo!"))

View file

@ -0,0 +1,3 @@
(cram
(deps
(glob_files bin/*.exe)))

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