diff --git a/bin/dune b/bin/dune index e0dc1d581..384a60960 100644 --- a/bin/dune +++ b/bin/dune @@ -13,3 +13,9 @@ (package melange) (modules melppx) (libraries melange.ppx ppxlib)) + +(executable + (public_name melobjinfo) + (package melange) + (modules melobjinfo) + (libraries cmdliner melangelib)) diff --git a/bin/melobjinfo.ml b/bin/melobjinfo.ml new file mode 100644 index 000000000..6da6d9503 --- /dev/null +++ b/bin/melobjinfo.ml @@ -0,0 +1,60 @@ +open Cmdliner +open Melangelib + +type dependencies = { + ml : string list; + runtime : string list; + external_ : string list; +} + +let dependencies (cmj : Js_cmj_format.t) = + List.fold_right + (fun (module_id : J.module_id) dependencies -> + let name = Lam_module_ident.name module_id in + match module_id.kind with + | Ml -> { dependencies with ml = name :: dependencies.ml } + | Runtime -> { dependencies with runtime = name :: dependencies.runtime } + | External _ -> + { dependencies with external_ = name :: dependencies.external_ }) + cmj.delayed_program.modules + { ml = []; runtime = []; external_ = [] } + +let print_names heading names = + match names with + | [] -> () + | _ -> + Printf.printf "%s:\n" heading; + List.iter (Printf.printf " %s\n") names + +(* CMJs do not store dependency digests. Dashes keep this compatible with the + implementation-import rows printed by ocamlobjinfo. *) +let print_implementation name = + Printf.printf " -------------------------------- %s\n" name + +let print_file file = + let cmj = Js_cmj_format.from_file file in + let { ml; runtime; external_ } = dependencies cmj in + Printf.printf "File %s\n" file; + print_names "Runtime modules imported" runtime; + print_names "JavaScript modules imported" external_; + Printf.printf "Implementations imported:\n"; + List.iter print_implementation ml + +let run files = + match files with + | [] -> `Error (true, "at least one CMJ file is required") + | _ -> ( + try + List.iter print_file files; + `Ok () + with exn -> `Error (false, Printexc.to_string exn)) + +let files = + let doc = "CMJ files to inspect." in + Arg.(value & pos_all file [] & info [] ~doc ~docv:"CMJ") + +let cmd = + let doc = "print information about Melange CMJ files" in + Cmd.v (Cmd.info "melobjinfo" ~doc) Term.(ret (const run $ files)) + +let () = exit (Cmd.eval cmd) diff --git a/test/blackbox-tests/melobjinfo.t b/test/blackbox-tests/melobjinfo.t new file mode 100644 index 000000000..c1d0ff3d0 --- /dev/null +++ b/test/blackbox-tests/melobjinfo.t @@ -0,0 +1,133 @@ +`melobjinfo` reports the dependencies serialized in CMJ files. + + $ . ./setup.sh + +CMJs with no dependencies are valid. Binary annotations are deliberately +disabled throughout this test: `melobjinfo` reads only the CMJ. + + $ cat > standalone.ml <<'EOF' + > let value = 1 + > EOF + $ melc --bs-no-bin-annot --mel-stop-after-cmj standalone.ml \ + > -o standalone.cmj + $ test ! -e standalone.cmt + $ melobjinfo standalone.cmj + File standalone.cmj + Implementations imported: + +Melange implementation dependencies retain their compilation-unit names, +including the names generated for wrapped libraries. + + $ cat > leaf.ml <<'EOF' + > let value = 1 + > EOF + $ melc --bs-no-bin-annot --mel-stop-after-cmj leaf.ml -o leaf.cmj + + $ cat > dep.ml <<'EOF' + > let value = Leaf.value + > EOF + $ melc -I . --bs-no-bin-annot --mel-stop-after-cmj dep.ml -o dep.cmj + + $ cat > namespace__Dep.ml <<'EOF' + > let value = 2 + > EOF + $ melc --bs-no-bin-annot --mel-stop-after-cmj namespace__Dep.ml \ + > -o namespace__Dep.cmj + +A virtual module has an interface but no CMJ. It is still an implementation +dependency of a CMJ that refers to it. + + $ cat > virtual_dep.mli <<'EOF' + > val value : int + > EOF + $ melc --bs-no-bin-annot --mel-cmi-only virtual_dep.mli + $ test ! -e virtual_dep.cmj + + $ cat > implementation_imports.ml <<'EOF' + > let ordinary = Dep.value + > let namespaced = Namespace__Dep.value + > let virtual_ = Virtual_dep.value + > EOF + $ melc -I . --bs-no-bin-annot --mel-stop-after-cmj \ + > implementation_imports.ml -o implementation_imports.cmj + $ test ! -e implementation_imports.cmt + $ melobjinfo implementation_imports.cmj + File implementation_imports.cmj + Implementations imported: + -------------------------------- Dep + -------------------------------- Namespace__Dep + -------------------------------- Virtual_dep + +Dependency rows are immediate, rather than the transitive closure. Leaf is +reported by Dep, but not by Implementation_imports above. + + $ melobjinfo dep.cmj + File dep.cmj + Implementations imported: + -------------------------------- Leaf + +Multiple inputs are reported in argument order, including files with no +dependencies. + + $ melobjinfo standalone.cmj dep.cmj + File standalone.cmj + Implementations imported: + File dep.cmj + Implementations imported: + -------------------------------- Leaf + +Runtime dependencies are reported separately from implementation imports. + + $ cat > runtime_imports.ml <<'EOF' + > let to_option value = Js.Nullable.toOption value + > EOF + $ melc --bs-no-bin-annot --mel-stop-after-cmj runtime_imports.ml \ + > -o runtime_imports.cmj + $ melobjinfo runtime_imports.cmj + File runtime_imports.cmj + Runtime modules imported: + Caml_option + Implementations imported: + +JavaScript dependencies include package and relative module specifiers. Named +and default exports, whether statically or dynamically imported, are retained. + + $ cat > javascript_imports.ml <<'EOF' + > external named_export : int = "named" [@@mel.module "package-name"] + > external relative : int = "value" [@@mel.module "./relative.js"] + > external default_export : int = "default" + > [@@mel.module "default-package"] + > external dynamic_default : int = "default" + > [@@mel.module "dynamic-package"] + > external dynamic_named : int = "named" + > [@@mel.module "dynamic-named-package"] + > let named_export = named_export + > let relative = relative + > let default_export = default_export + > let dynamic_default : int Js.promise = Js.import dynamic_default + > let dynamic_named : int Js.promise = Js.import dynamic_named + > EOF + $ melc --ppx melppx --bs-no-bin-annot --mel-stop-after-cmj \ + > javascript_imports.ml -o javascript_imports.cmj + $ melobjinfo javascript_imports.cmj + File javascript_imports.cmj + JavaScript modules imported: + ./relative.js + default-package + dynamic-named-package + dynamic-package + package-name + Implementations imported: + +Dynamic imports of Melange modules remain implementation dependencies. + + $ cat > dynamic_import.ml <<'EOF' + > module type Dependency = module type of Namespace__Dep + > let dependency = Js.import (module Namespace__Dep : Dependency) + > EOF + $ melc -I . --bs-no-bin-annot --mel-stop-after-cmj dynamic_import.ml \ + > -o dynamic_import.cmj + $ melobjinfo dynamic_import.cmj + File dynamic_import.cmj + Implementations imported: + -------------------------------- Namespace__Dep