Clerk improvements

- Add a `-I` option that allows defined modules to be available from other
  directories

- Add reporting of the number of successful / failed tests

- Locate the project root, and always run the commands from there
This commit is contained in:
Louis Gesbert 2023-11-14 16:05:54 +01:00
parent c019d1568f
commit 86b7f80e90
9 changed files with 372 additions and 160 deletions

View File

@ -44,15 +44,27 @@ module Cli = struct
let build_dir =
Arg.(
value
& opt string "_build"
& opt (some string) None
& info ["build-dir"] ~docv:"DIR"
~doc:"Directory where compilation artifacts should be written")
~doc:
"Directory where compilation artifacts should be written. Defaults \
to '_build'.")
let include_dirs =
Arg.(
value
& opt_all dir []
& info ["I"; "include"] ~docv:"DIR"
~doc:
"Make modules from the given directory available from everywhere.")
module Global : sig
val term :
(chdir:File.t option ->
catala_exe:File.t option ->
catala_opts:string list ->
build_dir:File.t option ->
include_dirs:string list ->
color:Cli.when_enum ->
debug:bool ->
ninja_output:File.t option ->
@ -93,11 +105,24 @@ module Cli = struct
let term f =
Term.(
const (fun chdir catala_exe catala_opts color debug ninja_output ->
f ~chdir ~catala_exe ~catala_opts ~color ~debug ~ninja_output)
const
(fun
chdir
catala_exe
catala_opts
build_dir
include_dirs
color
debug
ninja_output
->
f ~chdir ~catala_exe ~catala_opts ~build_dir ~include_dirs ~color
~debug ~ninja_output)
$ chdir
$ catala_exe
$ catala_opts
$ build_dir
$ include_dirs
$ color
$ debug
$ ninja_output)
@ -106,7 +131,7 @@ module Cli = struct
let files_or_folders =
Arg.(
value
& pos_all file []
& pos_all string []
& info [] ~docv:"FILE(S)" ~doc:"File(s) or folder(s) to process")
let single_file =
@ -206,15 +231,33 @@ end
(** Some functions that poll the surrounding systems (think [./configure]) *)
module Poll = struct
let project_root_absrel : (File.t option * File.t) Lazy.t =
lazy
(let open File in
let home = try Sys.getenv "HOME" with Not_found -> "" in
let rec lookup dir rel =
if
Sys.file_exists (dir / "catala.opam")
|| Sys.file_exists (dir / ".git")
|| Sys.file_exists (dir / "clerk.toml")
then Some dir, rel
else if dir = home then None, Filename.current_dir_name
else
let parent = Filename.dirname dir in
if parent = dir then None, Filename.current_dir_name
else lookup parent (rel / Filename.parent_dir_name)
in
lookup (Sys.getcwd ()) Filename.current_dir_name)
let project_root = lazy (fst (Lazy.force project_root_absrel))
let project_root_relative = lazy (snd (Lazy.force project_root_absrel))
(** Scans for a parent directory being the root of the Catala source repo *)
let catala_project_root : File.t option Lazy.t =
let rec aux dir =
if Sys.file_exists File.(dir / "catala.opam") then Some dir
else
let dir' = File.dirname dir in
if dir' = dir then None else aux dir'
in
lazy (aux (Sys.getcwd ()))
lazy
(match Lazy.force project_root with
| Some root when Sys.file_exists File.(root / "catala.opam") -> Some root
| _ -> None)
let exec_dir : File.t =
(* Do not use Sys.executable_name, which may resolve symlinks: we want the
@ -228,28 +271,26 @@ module Poll = struct
(let f = File.(exec_dir / "catala") in
if Sys.file_exists f then Unix.realpath f
else
match Lazy.force catala_project_root with
| Some root ->
match Lazy.force project_root with
| Some root when Sys.file_exists File.(root / "catala.opam") ->
Unix.realpath
File.(root / "_build" / "default" / "compiler" / "catala.exe")
| None -> File.check_exec "catala")
| _ -> File.check_exec "catala")
let build_dir : File.t Lazy.t =
lazy
(let d = "_build" in
match Sys.is_directory d with
| exception Sys_error _ ->
Sys.mkdir d 0o770;
d
| true -> d
| false ->
Message.raise_error "Build directory %a exists but is not a directory"
File.format d)
(* Note: it could be safer here to use File.(Sys.getcwd () / "_build"), but
Ninja treats relative and absolute paths separately so that you wouldn't
then be able to build target _build/foo.ml but would have to write the full
path every time *)
(* TODO: probably detect the project root and put this there instead *)
let build_dir : ?dir:File.t -> unit -> File.t =
fun ?(dir = "_build") () ->
match Sys.is_directory dir with
| exception Sys_error _ ->
Sys.mkdir dir 0o770;
dir
| true -> dir
| false ->
Message.raise_error "Build directory %a exists but is not a directory"
File.format dir
(* Note: it could be safer here to use File.(Sys.getcwd () / "_build") by
default, but Ninja treats relative and absolute paths separately so that
you wouldn't then be able to build target _build/foo.ml but would have to
write the full path every time *)
(** Locates the main [lib] directory containing the OCaml libs *)
let ocaml_libdir : File.t Lazy.t =
@ -348,6 +389,16 @@ module Poll = struct
])
end
(* Adjusts paths specified from the command-line relative to the user cwd to be
instead relative to the project root *)
let fix_path =
let from_dir = Sys.getcwd () in
fun d ->
let to_dir = Lazy.force Poll.project_root_relative in
match Catala_utils.Cli.reverse_path ~from_dir ~to_dir d with
| "" -> "."
| f -> f
(**{1 Building rules}*)
(** Ninja variable names *)
@ -380,12 +431,21 @@ module Var = struct
let ( ! ) = Var.v
end
let base_bindings catala_exe catala_flags =
let base_bindings catala_exe catala_flags build_dir include_dirs =
let includes =
List.fold_right
(fun dir flags ->
if Filename.is_relative dir then
"-I" :: File.(Var.(!builddir) / dir) :: flags
else "-I" :: dir :: flags)
include_dirs []
in
let catala_flags = ("--directory=" ^ Var.(!builddir)) :: catala_flags in
let ocamlopt_flags = ["-I"; Lazy.force Poll.ocaml_runtime_dir] in
[
Nj.binding Var.ninja_required_version ["1.7"];
(* use of implicit outputs *)
Nj.binding Var.builddir [Lazy.force Poll.build_dir];
Nj.binding Var.builddir [build_dir];
Nj.binding Var.clerk_exe [Lazy.force Poll.clerk_exe];
Nj.binding Var.catala_exe
[
@ -393,77 +453,115 @@ let base_bindings catala_exe catala_flags =
| Some e -> File.check_exec e
| None -> Lazy.force Poll.catala_exe);
];
Nj.binding Var.catala_flags catala_flags;
Nj.binding Var.catala_flags (catala_flags @ includes);
Nj.binding Var.clerk_flags
("-e"
:: Var.(!catala_exe)
:: List.map (fun f -> "--catala-opts=" ^ f) catala_flags);
:: Var.(!catala_exe)
:: ("--build-dir=" ^ Var.(!builddir))
:: includes
@ List.map (fun f -> "--catala-opts=" ^ f) catala_flags);
Nj.binding Var.ocamlopt_exe ["ocamlopt"];
Nj.binding Var.ocamlopt_flags ["-I"; Lazy.force Poll.ocaml_runtime_dir];
Nj.binding Var.ocamlopt_flags (ocamlopt_flags @ includes);
Nj.binding Var.runtime_ocaml_libs (Lazy.force Poll.ocaml_link_flags);
Nj.binding Var.diff (Lazy.force Poll.diff_command);
Nj.binding Var.post_test [Var.(!diff)];
]
let static_base_rules =
let[@ocamlformat "disable"] static_base_rules =
let open Var in
let color = Message.has_color stdout in
[
Nj.rule "copy"
~command:["cp"; "-f"; !input; !output]
~description:["<copy>"; !input];
Nj.rule "catala-ocaml"
~command:[!catala_exe; "ocaml"; !catala_flags; !input; "-o"; !output]
~description:["<catala>"; "ocaml"; ""; !output];
Nj.rule "ocaml-module"
~command:
[!ocamlopt_exe; "-shared"; !ocamlopt_flags; !input; "-o"; !output]
~description:["<ocaml>"; ""; !output];
Nj.rule "ocaml-exec"
~command:
[
!ocamlopt_exe;
!ocamlopt_flags;
!runtime_ocaml_libs;
!input;
"-o";
!output;
]
~command: [
!ocamlopt_exe; !runtime_ocaml_libs; !ocamlopt_flags;
!input;
"-o"; !output;
]
~description:["<ocaml>"; ""; !output];
Nj.rule "out-test"
~command:
[
!catala_exe;
!test_command;
!catala_flags;
!input;
">";
!output;
"2>&1";
"||";
"true";
]
~command: [
!catala_exe; !test_command; !catala_flags; !input;
">"; !output; "2>&1";
"||"; "true";
]
~description:
["<catala>"; "test"; !test_id; ""; !input; "(" ^ !test_command ^ ")"];
Nj.rule "inline-tests"
~command:
[!clerk_exe; "runtest"; !clerk_flags; !input; ">"; !output; "2>&1"]
[!clerk_exe; "runtest"; !clerk_flags; !input; ">"; !output; "2>&1"; "||"; "true"]
~description:["<catala>"; "inline-tests"; ""; !input];
Nj.rule "post-test" ~command:[!post_test; !input]
~description:["<test-validation>"];
Nj.rule "post-test"
~command:[
!post_test; !input; ";";
"echo"; "-n"; "$$?"; ">"; !output;
]
~description:["<test>"; !output];
Nj.rule "interpret"
~command:
[!catala_exe; "interpret"; !catala_flags; !input; "--scope=" ^ !scope]
~description:["<catala>"; "interpret"; !scope; ""; !input]
~vars:[pool, ["console"]];
Nj.rule "dir-tests"
~command:["cat"; !input; ">"; !output; ";"]
~description:["<test>"; !output];
Nj.rule "test-results"
~command:[
"out=" ^ !output; ";";
"success=$$("; "tr"; "-cd"; "0"; "<"; !input; "|"; "wc"; "-c"; ")"; ";";
"total=$$("; "wc"; "-c"; "<"; !input; ")"; ";";
"pass=$$("; ")"; ";";
"if"; "test"; "\"$$success\""; "-eq"; "\"$$total\""; ";"; "then";
"printf";
(if color then "\"\\n[\\033[32mPASS\\033[m] \\033[1m%s\\033[m: \
\\033[32m%3d\\033[m/\\033[32m%d\\033[m\\n\""
else "\"\\n[PASS] %s: %3d/%d\\n\"");
"$${out%@test}"; "$$success"; "$$total"; ";";
"else";
"printf";
(if color then "\"\\n[\\033[31mFAIL\\033[m] \\033[1m%s\\033[m: \
\\033[31m%3d\\033[m/\\033[32m%d\\033[m\\n\""
else "\"\\n[FAIL] %s: %3d/%d\\n\"");
"$${out%@test}"; "$$success"; "$$total"; ";";
"return"; "1"; ";";
"fi";
]
~description:["<test>"; !output];
]
let gen_build_statements (item : Scan.item) : Nj.ninja =
let gen_build_statements
(include_dirs : string list)
(same_dir_modules : string list)
(item : Scan.item) : Nj.ninja =
let open File in
let ( ! ) = Var.( ! ) in
let src = item.file_name in
let modules = List.rev item.used_modules in
let inc x = File.(!Var.builddir / x) in
let modd x = File.(src /../ x) ^ "@module" in
let modfile ext modname =
if List.mem modname same_dir_modules then
(!Var.builddir / src /../ modname) ^ ext
else modname ^ ext
in
let inc x = !Var.builddir / x in
let modd x = modfile "@module" x in
let def_src = Nj.binding Var.src [Filename.remove_extension src] in
let srcv = !Var.src ^ Filename.extension src in
let include_deps =
@ -492,21 +590,34 @@ let gen_build_statements (item : Scan.item) : Nj.ninja =
| Some m ->
let target ext = (!Var.builddir / src /../ m) ^ "." ^ ext in
Nj.build "ocaml-module" ~inputs:[ml_file]
~implicit_in:
(List.map (fun m -> (!Var.builddir / src /../ m) ^ ".cmi") modules)
~implicit_in:(List.map modd modules)
~outputs:[target "cmxs"]
~implicit_out:(List.map target implicit_out_exts)
~vars:
[
( Var.ocamlopt_flags,
[!Var.ocamlopt_flags; "-I"; File.(!Var.builddir / src /../ "")] );
!Var.ocamlopt_flags
:: "-I"
:: (!Var.builddir / src /../ "")
:: List.concat_map
(fun d ->
[
"-I";
(if Filename.is_relative d then !Var.builddir / d else d);
])
include_dirs );
]
| None ->
let target ext = (!Var.builddir / !Var.src) ^ "." ^ ext in
let inputs =
List.map (fun m -> (!Var.builddir / src /../ m) ^ ".cmx") modules
@ [ml_file]
let inputs, modules =
List.partition_map
(fun m ->
if List.mem m same_dir_modules then
Left ((!Var.builddir / src /../ m) ^ ".cmx")
else Right m)
modules
in
let inputs = inputs @ [ml_file] in
(* Note: this rule is incomplete in that it only provide the direct module
dependencies, and ocamlopt needs the transitive closure of dependencies
for linking, which we can't provide here ; catala does that work for
@ -514,12 +625,46 @@ let gen_build_statements (item : Scan.item) : Nj.ninja =
[clerk link]) command that gathers these dependencies and wraps
[ocamlopt]. *)
Nj.build "ocaml-exec" ~inputs
~implicit_in:(List.map (fun m -> m ^ "@module") modules)
~outputs:[target "exe"]
~implicit_out:(List.map target implicit_out_exts)
~vars:
[
( Var.ocamlopt_flags,
!Var.ocamlopt_flags
:: "-I"
:: (!Var.builddir / src /../ "")
:: List.concat_map
(fun d ->
[
"-I";
(if Filename.is_relative d then !Var.builddir / d else d);
])
include_dirs
@ (List.map (fun m -> m ^".cmx") modules) );
]
in
let expose_module =
match item.module_def with
| Some m when List.mem (dirname src) include_dirs ->
Some
(Nj.build "phony"
~outputs:[m ^ "@module"]
~inputs:
[
(!Var.builddir / src /../ m) ^ ".cmi";
(!Var.builddir / src /../ m) ^ ".cmxs";
])
| _ -> None
in
let interp_deps =
!Var.catala_exe
:: List.map (fun m -> (!Var.builddir / src /../ m) ^ ".cmxs") modules
:: List.map
(fun m ->
if List.mem m same_dir_modules then
(!Var.builddir / src /../ m) ^ ".cmxs"
else m ^ "@module")
modules
in
let interpret =
Nj.build "interpret"
@ -564,24 +709,32 @@ let gen_build_statements (item : Scan.item) : Nj.ninja =
]
in
let tests =
let results =
Nj.build "test-results"
~outputs:[srcv ^ "@test"]
~inputs:[inc (srcv ^ "@test")]
in
if item.has_inline_tests then
[
Nj.build "post-test"
~outputs:[srcv ^ "@test"]
~outputs:[inc (srcv ^ "@test")]
~inputs:[srcv; inc (srcv ^ "@out")]
~implicit_in:
(List.map
(fun test -> legacy_test_reference test ^ "@post")
item.legacy_tests);
results;
]
else if item.legacy_tests <> [] then
[
Nj.build "phony"
~outputs:[srcv ^ "@test"]
~outputs:[inc (srcv ^ "@test")]
~implicit_out:[srcv ^ "@test"]
~inputs:
(List.map
(fun test -> legacy_test_reference test ^ "@post")
item.legacy_tests);
results;
]
else []
in
@ -594,91 +747,85 @@ let gen_build_statements (item : Scan.item) : Nj.ninja =
Seq.return def_src;
Seq.return include_deps;
Option.to_seq module_deps;
Option.to_seq expose_module;
Seq.return ocaml;
Seq.return ocamlopt;
List.to_seq tests;
Seq.return interpret;
]
let test_targets_by_dir items =
let stmt target_sfx dir sub =
Nj.build "phony"
~outputs:[dir ^ target_sfx]
~inputs:(List.map (fun s -> s ^ target_sfx) sub)
let gen_build_statements_dir
(include_dirs : string list)
(items : Scan.item list) : Nj.ninja =
let same_dir_modules =
List.filter_map (fun item -> item.Scan.module_def) items
in
let alias dir sub = List.to_seq [stmt "@test" dir sub; Nj.comment ""] in
(* This relies on the fact that the sequence is returned ordered by
directory *)
let rec aux curdir seq =
let prefix = if curdir = "" then "" else curdir ^ "/" in
match seq () with
| Seq.Cons (item, seq) as node
when String.starts_with ~prefix item.Scan.file_name -> (
if item.Scan.legacy_tests = [] && not item.Scan.has_inline_tests then
aux curdir seq
else
match
String.split_on_char '/'
(String.remove_prefix ~prefix item.Scan.file_name)
with
| [] -> assert false
| [_] ->
let rules, cur, seq = aux curdir seq in
rules, item.Scan.file_name :: cur, seq
| subdir :: _ ->
let subdir = File.(curdir / subdir) in
let rules, sub, seq = aux subdir (fun () -> node) in
let rules =
if sub = [] then rules else Seq.append rules (alias subdir sub)
in
let rules1, cur, seq = aux curdir seq in
Seq.append rules rules1, subdir :: cur, seq)
| node -> Seq.empty, [], fun () -> node
in
let rules, top, seq = aux "" items in
assert (Seq.is_empty seq);
Seq.append rules
@@ List.to_seq
[
Nj.build "phony" ~outputs:["test"]
~inputs:(List.map (fun s -> s ^ "@test") top);
]
Seq.flat_map
(gen_build_statements include_dirs same_dir_modules)
(List.to_seq items)
let build_statements dir =
(* Todo: generate the targets_by_dir alongside the targets, avoiding the need
for [memoize] and two traversals here *)
let dir_test_rules dir subdirs items =
let open File in
let inputs =
List.rev_append
(List.rev_map (fun s -> (Var.(!builddir) / s) ^ "@test") subdirs)
(List.filter_map
(fun item ->
if item.Scan.legacy_tests = [] && not item.Scan.has_inline_tests then
None
else Some ((Var.(!builddir) / item.Scan.file_name) ^ "@test"))
items)
in
List.to_seq
[
Nj.Comment "";
Nj.build "dir-tests" ~outputs:[(Var.(!builddir) / dir) ^ "@test"] ~inputs;
Nj.build "test-results"
~outputs:[dir ^ "@test"]
~inputs:[(Var.(!builddir) / dir) ^ "@test"];
]
let build_statements include_dirs dir =
Scan.tree dir
|> Seq.memoize
|> fun items ->
Seq.concat
@@ List.to_seq
[
Seq.flat_map gen_build_statements items;
Seq.return (Nj.comment "\n- Global targets - #\n");
test_targets_by_dir items;
]
|> Seq.flat_map
@@ fun (dir, subdirs, items) ->
Seq.append
(gen_build_statements_dir include_dirs items)
(dir_test_rules dir subdirs items)
let gen_ninja_file catala_exe catala_flags dir =
let gen_ninja_file catala_exe catala_flags build_dir include_dirs dir =
let ( @+ ) = Seq.append in
Seq.return
(Nj.Comment (Printf.sprintf "File generated by Clerk v.%s\n" version))
@+ Seq.return (Nj.Comment "- Global variables - #\n")
@+ List.to_seq (base_bindings catala_exe catala_flags)
@+ List.to_seq (base_bindings catala_exe catala_flags build_dir include_dirs)
@+ Seq.return (Nj.Comment "\n- Base rules - #\n")
@+ List.to_seq static_base_rules
@+ Seq.return (Nj.Comment "- Project-specific build statements - #")
@+ build_statements dir
@+ build_statements include_dirs dir
@+ Seq.return (Nj.build "phony" ~outputs:["test"] ~inputs:[".@test"])
(** {1 Driver} *)
let ninja_init ~chdir ~catala_exe ~catala_opts ~color ~debug ~ninja_output :
extra:def Seq.t -> (File.t -> 'a) -> 'a =
Option.iter Sys.chdir chdir;
let ninja_init
~chdir
~catala_exe
~catala_opts
~build_dir
~include_dirs
~color
~debug
~ninja_output : extra:def Seq.t -> (File.t -> 'a) -> 'a =
let _options = Catala_utils.Cli.enforce_globals ~debug ~color () in
let chdir =
match chdir with None -> Lazy.force Poll.project_root | some -> some
in
Option.iter Sys.chdir chdir;
let build_dir = Poll.build_dir ?dir:build_dir () in
let with_ninja_output k =
match ninja_output with
| Some f -> k f
| None when debug -> k File.(Lazy.force Poll.build_dir / "clerk.ninja")
| None when debug -> k File.(build_dir / "clerk.ninja")
| None -> File.with_temp_file "clerk_build_" ".ninja" k
in
fun ~extra k ->
@ -690,7 +837,7 @@ let ninja_init ~chdir ~catala_exe ~catala_opts ~color ~debug ~ninja_output :
Seq.concat
@@ List.to_seq
[
gen_ninja_file catala_exe catala_opts ".";
gen_ninja_file catala_exe catala_opts build_dir include_dirs ".";
Seq.return (Nj.comment "\n - Command-specific targets - #");
extra;
]
@ -715,6 +862,14 @@ let build_cmd =
let run ninja_init (targets : string list) (ninja_flags : string) =
ninja_init ~extra:Seq.empty
@@ fun nin_file ->
let targets =
List.map
(fun f ->
if String.exists (function '/' | '.' -> true | _ -> false) f then
fix_path f
else f)
targets
in
let ninja_cmd = ninja_cmdline ninja_flags nin_file targets in
Message.emit_debug "executing '%s'..." ninja_cmd;
Sys.command ninja_cmd
@ -734,9 +889,8 @@ let test_cmd =
(reset_test_outputs : bool)
(ninja_flags : string) =
let targets =
match files_or_folders with
| [] -> ["test"]
| files -> List.map (fun f -> f ^ "@test") files
let fs = if files_or_folders = [] then ["."] else files_or_folders in
List.map (fun f -> fix_path f ^ "@test") fs
in
let extra =
List.to_seq
@ -755,7 +909,7 @@ let test_cmd =
in
ninja_init ~extra
@@ fun nin_file ->
let ninja_cmd = ninja_cmdline ninja_flags nin_file [] in
let ninja_cmd = ninja_cmdline ninja_flags nin_file targets in
Message.emit_debug "executing '%s'..." ninja_cmd;
Sys.command ninja_cmd
in
@ -806,7 +960,13 @@ let run_cmd =
$ Cli.ninja_flags)
let runtest_cmd =
let run catala_exe catala_opts build_dir file =
let run catala_exe catala_opts build_dir include_dirs file =
let catala_opts =
List.fold_left
(fun opts dir -> "-I" :: dir :: opts)
catala_opts include_dirs
in
let build_dir = Poll.build_dir ?dir:build_dir () in
Clerk_runtest.run_inline_tests
(Option.value ~default:"catala" catala_exe)
catala_opts build_dir file;
@ -822,6 +982,7 @@ let runtest_cmd =
$ Cli.catala_exe
$ Cli.catala_opts
$ Cli.build_dir
$ Cli.include_dirs
$ Cli.single_file)
let main_cmd = Cmd.group Cli.info [build_cmd; test_cmd; run_cmd; runtest_cmd]

View File

@ -120,7 +120,7 @@ let get_lang file =
Option.bind (Re.exec_opt catala_suffix_regex file)
@@ fun g -> List.assoc_opt (Re.Group.get g 1) Catala_utils.Cli.languages
let tree (dir : File.t) : item Seq.t =
let tree (dir : File.t) : (File.t * File.t list * item list) Seq.t =
File.scan_tree
(fun f ->
match get_lang f with

View File

@ -51,9 +51,9 @@ val get_lang : File.t -> Cli.backend_lang option
val catala_file : File.t -> Catala_utils.Cli.backend_lang -> item
(** Scans a single Catala file into an item *)
val tree : File.t -> item Seq.t
(** Recursively scans a directory, and returns the corresponding items in
sequence. *)
val tree : File.t -> (File.t * File.t list * item list) Seq.t
(** Recursively scans a directory, and returns the corresponding subdirectories
and items in sequence, by directory. *)
val test_command_args : string -> string option
(** Parses a test command-line (in the form "$ catala <args>") and returns the

View File

@ -113,10 +113,6 @@ let file_lang filename =
@{<yellow>%s@}, and @{<bold>--language@} was not specified"
filename)
(** If [to_dir] is a path to a given directory and [f] a path to a file as seen
from absolute path [from_dir], [reverse_path ~from_dir ~to_dir f] is a path
leading to [f] from [to_dir]. The results attempts to be relative to
[to_dir]. *)
let reverse_path ?(from_dir = Sys.getcwd ()) ~to_dir f =
if Filename.is_relative from_dir then invalid_arg "File.with_reverse_path"
else if not (Filename.is_relative f) then f

View File

@ -54,6 +54,12 @@ val file_lang : file -> backend_lang
val input_src_file : input_src -> file
val reverse_path : ?from_dir:file -> to_dir:file -> file -> file
(** If [to_dir] is a path to a given directory and [f] a path to a file as seen
from absolute path [from_dir], [reverse_path ~from_dir ~to_dir f] is a path
leading to [f] from [to_dir]. The results attempts to be relative to
[to_dir]. *)
(** {2 Configuration globals} *)
type options = private {

View File

@ -135,9 +135,7 @@ let check_exec t =
"Could not find the @{<yellow>%s@} program, please fix your installation"
(Filename.quote t)
let ( / ) a b =
if a = "" || a = Filename.current_dir_name then b else Filename.concat a b
let ( / ) a b = if a = Filename.current_dir_name then b else Filename.concat a b
let dirname = Filename.dirname
let ( /../ ) a b = dirname a / b
let ( -.- ) file ext = Filename.chop_extension file ^ "." ^ ext
@ -180,16 +178,30 @@ let scan_tree f t =
|> Array.to_list
|> List.filter not_hidden
|> List.map (fun t -> d / t)
|> do_files
and do_files flist =
|> do_files d
and do_files d flist =
let dirs, files =
flist |> List.sort (fun a b -> -compare a b) |> List.partition is_dir
in
Seq.append
(Seq.concat (Seq.map do_dir (List.to_seq dirs)))
(Seq.filter_map f (List.to_seq files))
let rec gather_subdirs subdirs_list_acc subdirs_seq () =
match subdirs_seq () with
| Seq.Nil -> (
match List.rev subdirs_list_acc, List.filter_map f files with
| [], [] -> Seq.Nil
| sdirs, items -> Seq.return (d, sdirs, items) ())
| Seq.Cons (subdir_name, subdir_next) -> (
match do_dir subdir_name () with
| Seq.Nil -> gather_subdirs subdirs_list_acc subdir_next ()
| Seq.Cons (sd0, sds) ->
Seq.Cons
( sd0,
Seq.append sds
(gather_subdirs (subdir_name :: subdirs_list_acc) subdir_next)
))
in
gather_subdirs [] (List.to_seq dirs)
in
do_files [t]
if is_dir t then do_dir t else Seq.return (dirname t, [], Option.to_list (f t))
module Tree = struct
type path = t

View File

@ -110,6 +110,9 @@ val ( -.- ) : t -> string -> t
(** Extension replacement: chops the given filename extension, and replaces it
with the given one (which shouldn't contain a dot) *)
val path_to_list : t -> string list
(** Empty elements or current-directory (".") are skipped in the resulting list *)
val equal : t -> t -> bool
(** Case-insensitive string comparison (no file resolution whatsoever) *)
@ -123,10 +126,14 @@ val format : Format.formatter -> t -> unit
module Set : Set.S with type elt = t
module Map : Map.S with type key = t
val scan_tree : (t -> 'a option) -> t -> 'a Seq.t
val scan_tree : (t -> 'a option) -> t -> (t * t list * 'a list) Seq.t
(** Recursively scans a directory for files. Directories or files matching ".*"
or "_*" are ignored. Unreadable files or subdirectories are ignored with a
debug message. If [t] is a plain file, scan just that non-recursively. *)
debug message. If [t] is a plain file, scan just that non-recursively.
The matching results are returned grouped by directory, case-insensitively
ordered by filename, as a list of non-empty subdirs and a list of extracted
items. *)
module Tree : sig
(** A lazy tree structure mirroring the filesystem ; uses the comparison from

View File

@ -123,6 +123,8 @@ val unformat : (Format.formatter -> unit) -> string
(** Converts [f] to a string, discarding formatting and skipping newlines and
indents *)
val has_color : out_channel -> bool
(* {1 More general color-enabled formatting helpers}*)
val formatter_of_out_channel : out_channel -> Format.formatter

View File

@ -0,0 +1,28 @@
> Using Mod_def
> Using Mod_middle
Somehow there is a bug when multiple modules are declared in this order, so this
test completes mod_use2
```catala
declaration scope T:
t1 scope Mod_middle.S
# input i content Enum1
output o1 content Mod_def.S
output o2 content money
output o3 content money
scope T:
definition t1.x equals 3
definition o1 equals t1.o1
definition o2 equals t1.o2
definition o3 equals t1.o3
```
```catala-test-inline
$ catala interpret -s T
[RESULT] Computation successful! Results:
[RESULT] o1 = Mod_def.S { -- sr: $1,000.00 -- e1: Maybe () }
[RESULT] o2 = $2,500.00
[RESULT] o3 = $132.00
```