ligo/src/compiler/tezos_compiler.ml
2017-06-12 11:04:43 +02:00

437 lines
15 KiB
OCaml

(**************************************************************************)
(* *)
(* Copyright (c) 2014 - 2016. *)
(* Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* All rights reserved. No warranty, explicit or implicit, provided. *)
(* *)
(**************************************************************************)
(** The OCaml compiler not being implemented with Lwt, the compilation
take place in a separated process (by using [Lwt_process.exec]).
The [main] function is the entry point for the forked process.
While [Updater.compile] is the 'forking' function to be called by
the [tezos-node] process.
*)
open Tezos_data
(* GRGR TODO: fail in the presence of "external" *)
module Backend = struct
(* See backend_intf.mli. *)
let symbol_for_global' = Compilenv.symbol_for_global'
let closure_symbol = Compilenv.closure_symbol
let really_import_approx = Import_approx.really_import_approx
let import_symbol = Import_approx.import_symbol
let size_int = Arch.size_int
let big_endian = Arch.big_endian
let max_sensible_number_of_arguments =
(* The "-1" is to allow for a potential closure environment parameter. *)
Proc.max_arguments_for_tailcalls - 1
end
let backend = (module Backend : Backend_intf.S)
let warnings = "+a-4-6-7-9-29-40..42-44-45-48"
let warn_error = "-a+8"
let () =
Clflags.unsafe_string := false ;
Clflags.native_code := true
(** Compilation environment.
[tezos_protocol_env] defines the list of [cmi] available while compiling
the protocol version. The [cmi] are packed into the [tezos-node]
binary by using [ocp-ocamlres], see the Makefile.
[register_env] defines a complementary list of [cmi] available
while compiling the generated [register.ml] file (that register
the protocol first-class module into the [Updater.versions]
hashtable).
*)
let tezos_protocol_env =
[ "camlinternalFormatBasics", Embedded_cmis.camlinternalFormatBasics_cmi ;
"proto_environment", Embedded_cmis.proto_environment_cmi ;
]
let register_env =
[ "register", Embedded_cmis.register_cmi ]
(** Helpers *)
let (//) = Filename.concat
let create_file ?(perm = 0o644) name content =
let open Unix in
let fd = openfile name [O_TRUNC; O_CREAT; O_WRONLY] perm in
ignore(write_substring fd content 0 (String.length content));
close fd
let read_md5 file =
let ic = open_in file in
let md5 = input_line ic in
close_in ic ;
md5
let rec create_dir ?(perm = 0o755) dir =
if not (Sys.file_exists dir) then begin
create_dir (Filename.dirname dir);
Unix.mkdir dir perm
end
let dump_cmi dir (file, content) =
create_file (dir // file ^ ".cmi") content
let safe_unlink file =
try Unix.unlink file
with Unix.Unix_error(Unix.ENOENT, _, _) -> ()
let unlink_cmi dir (file, _) =
safe_unlink (dir // file ^ ".cmi")
let unlink_object obj =
safe_unlink obj;
safe_unlink (Filename.chop_suffix obj ".cmx" ^ ".cmi");
safe_unlink (Filename.chop_suffix obj ".cmx" ^ ".o")
(** TEZOS_PROTOCOL files *)
module Meta = struct
let name = "TEZOS_PROTOCOL"
let config_file_encoding =
let open Data_encoding in
obj2
(opt "hash" ~description:"Used to force the hash of the protocol" Protocol_hash.encoding)
(req "modules" ~description:"Modules comprising the protocol" (list string))
let to_file dirname ?hash modules =
let config_file =
Data_encoding.Json.construct config_file_encoding (hash, modules) in
Utils.write_file ~bin:false (dirname // name) @@
Data_encoding_ezjsonm.to_string config_file
let of_file dirname =
Utils.read_file ~bin:false (dirname // name) |>
Data_encoding_ezjsonm.from_string |> function
| Error err -> Pervasives.failwith err
| Ok json -> Data_encoding.Json.destruct config_file_encoding json
end
let find_component dirname module_name =
let open Protocol in
let name_lowercase = String.uncapitalize_ascii module_name in
let implementation = dirname // name_lowercase ^ ".ml" in
let interface = implementation ^ "i" in
match Sys.file_exists implementation, Sys.file_exists interface with
| false, _ -> Pervasives.failwith @@ "Not such file: " ^ implementation
| true, false ->
let implementation = Utils.read_file ~bin:false implementation in
{ name = module_name; interface = None; implementation }
| _ ->
let interface = Utils.read_file ~bin:false interface in
let implementation = Utils.read_file ~bin:false implementation in
{ name = module_name; interface = Some interface; implementation }
let read_dir dirname =
let _hash, modules = Meta.of_file dirname in
List.map (find_component dirname) modules
(** Semi-generic compilation functions *)
let compile_mli ?(ctxt = "") ?(keep_object = false) target mli =
Printf.printf "OCAMLOPT%s %s\n%!" ctxt (Filename.basename target ^ ".cmi");
Compenv.(readenv Format.err_formatter (Before_compile mli)) ;
Optcompile.interface Format.err_formatter mli target ;
if not keep_object then
at_exit (fun () -> safe_unlink (target ^ ".cmi"))
let compile_ml ?(ctxt = "") ?(keep_object = false) ?for_pack target ml =
Printf.printf "OCAMLOPT%s %s\n%!" ctxt (Filename.basename target ^ ".cmx") ;
Compenv.(readenv Format.err_formatter (Before_compile ml));
Clflags.for_package := for_pack;
Optcompile.implementation
~backend Format.err_formatter ml target;
Clflags.for_package := None;
if not keep_object then
at_exit (fun () -> unlink_object (target ^ ".cmx")) ;
target ^ ".cmx"
let modification_date file = Unix.((stat file).st_mtime)
let compile_units
?ctxt
?(update_needed = true)
?keep_object ?for_pack ~source_dir ~build_dir units =
let compile_unit update_needed unit =
let basename = String.uncapitalize_ascii unit in
let mli = source_dir // basename ^ ".mli" in
let cmi = build_dir // basename ^ ".cmi" in
let ml = source_dir // basename ^ ".ml" in
let cmx = build_dir // basename ^ ".cmx" in
let target = build_dir // basename in
let update_needed =
update_needed
|| not (Sys.file_exists cmi)
|| ( Sys.file_exists mli
&& modification_date cmi < modification_date mli )
|| not (Sys.file_exists cmx)
|| modification_date cmx < modification_date ml in
if update_needed then begin
unlink_object cmx ;
if Sys.file_exists mli then compile_mli ?ctxt ?keep_object target mli ;
ignore (compile_ml ?ctxt ?keep_object ?for_pack target ml)
end ;
update_needed, cmx in
List.fold_left
(fun (update_needed, acc) unit->
let update_needed, output = compile_unit update_needed unit in
update_needed, output :: acc)
(update_needed, []) units
|> snd |> List.rev
let pack_objects ?(ctxt = "") ?(keep_object = false) output objects =
Printf.printf "PACK%s %s\n%!" ctxt (Filename.basename output);
Compmisc.init_path true;
Asmpackager.package_files
~backend Format.err_formatter Env.initial_safe_string objects output;
if not keep_object then at_exit (fun () -> unlink_object output) ;
Warnings.check_fatal ()
let link_shared ?(static=false) output objects =
Printf.printf "LINK %s\n%!" (Filename.basename output);
Compenv.(readenv Format.err_formatter Before_link);
Compmisc.init_path true;
if static then
Asmlibrarian.create_archive objects output
else
Asmlink.link_shared Format.err_formatter objects output;
Warnings.check_fatal ()
(** Main for the 'forked' compiler.
It expect the following arguments:
output.cmxs source_dir
where, [source_dir] should contains a TEZOS_PROTOCOL file such as:
hash = "69872d2940b7d11c9eabbc685115bd7867a94424"
modules = [Data; Main]
The [source_dir] should also contains the corresponding source
file. For instance: [data.ml], [main.ml] and optionnaly [data.mli]
and [main.mli].
*)
let create_register_file client file hash packname modules =
let unit = List.hd (List.rev modules) in
let environment_module = packname ^ ".Local_environment.Environment" in
let error_monad_module = environment_module ^ ".Error_monad" in
let context_module = environment_module ^ ".Context" in
let hash_module = environment_module ^ ".Hash" in
create_file file
(Printf.sprintf
"module Packed_protocol = struct\n\
\ let hash = (%s.Protocol_hash.of_b58check_exn %S)\n\
\ type error = %s.error = ..\n\
\ type 'a tzresult = 'a %s.tzresult\n\
\ include %s.%s\n\
\ let error_encoding = %s.error_encoding ()\n\
\ let classify_errors = %s.classify_errors\n\
\ let pp = %s.pp\n\
\ let complete_b58prefix = %s.complete
\ end\n\
\ %s\n\
"
hash_module
(Protocol_hash.to_b58check hash)
error_monad_module
error_monad_module
packname (String.capitalize_ascii unit)
error_monad_module
error_monad_module
error_monad_module
context_module
(if client then
"include Register.Make(Packed_protocol)"
else
Printf.sprintf
"let () = Register.register (%s.__cast (module Packed_protocol : %s.PACKED_PROTOCOL))" environment_module environment_module))
let mktemp_dir () =
Filename.get_temp_dir_name () //
Printf.sprintf "tezos-protocol-build-%06X" (Random.int 0xFFFFFF)
let main () =
Random.self_init () ;
Sodium.Random.stir () ;
let anonymous = ref []
and client = ref false
and build_dir = ref None
and include_dirs = ref [] in
let static = ref false in
let args_spec = [
"-static", Arg.Set static, " Build a library (.cmxa)";
"-client", Arg.Set client, " Preserve type equality with concrete node environment (used to embed protocol into the client)" ;
"-I", Arg.String (fun s -> include_dirs := s :: !include_dirs), "path Path for concrete node signatures (used to embed protocol into the client)" ;
"-bin-annot", Arg.Set Clflags.binary_annotations, " (see ocamlopt)" ;
"-g", Arg.Set Clflags.debug, " (see ocamlopt)" ;
"-build-dir", Arg.String (fun s -> build_dir := Some s), "path Reuse build dir (incremental compilation)"] in
let usage_msg = Printf.sprintf "Usage: %s <out> <src>\nOptions are:" Sys.argv.(0) in
Arg.parse args_spec (fun s -> anonymous := s :: !anonymous) usage_msg ;
let client = !client and include_dirs = !include_dirs in
let output, source_dir =
match List.rev !anonymous with
| [ output ; source_dir ] -> output, source_dir
| _ -> Arg.usage args_spec usage_msg ; Pervasives.exit 1 in
if include_dirs <> [] && not client then begin
Arg.usage args_spec usage_msg ; Pervasives.exit 1
end ;
let keep_object, build_dir, sigs_dir =
match !build_dir with
| None ->
let build_dir = mktemp_dir () in
false, build_dir, build_dir // "sigs"
| Some build_dir ->
true, build_dir, mktemp_dir () in
create_dir build_dir ;
create_dir sigs_dir ;
at_exit (fun () ->
Unix.rmdir sigs_dir ;
if not keep_object then Unix.rmdir build_dir ) ;
let hash, units = Meta.of_file source_dir in
let hash = match hash with
| Some hash -> hash
| None -> Protocol.hash @@ List.map (find_component source_dir) units
in
let packname =
if keep_object then
String.capitalize_ascii (Filename.(basename @@ chop_extension output))
else
Format.asprintf "Protocol_%a" Protocol_hash.pp hash in
let packed_objects =
if keep_object then
Filename.dirname output // String.uncapitalize_ascii packname ^ ".cmx"
else
build_dir // packname ^ ".cmx" in
let ctxt = Printf.sprintf " (%s)" (Filename.basename output) in
let logname =
if keep_object then
try
Scanf.sscanf
Filename.(basename @@ chop_extension output)
"embedded_proto_%s"
(fun s -> "proto." ^ s)
with _ ->
Filename.(basename @@ chop_extension output)
else
Format.asprintf "proto.%a" Protocol_hash.pp hash in
(* TODO proper error *)
assert (List.length units >= 1);
(* Initialize the compilers *)
Compenv.(readenv Format.err_formatter Before_args);
if not client then Clflags.no_std_include := true;
Clflags.include_dirs := build_dir :: sigs_dir :: include_dirs;
Clflags.nopervasives := true;
Warnings.parse_options false warnings;
Warnings.parse_options true warn_error;
let md5 =
if not client then
Digest.(to_hex (file Sys.executable_name))
else
try
let environment_cmi =
Misc.find_in_path_uncap !Clflags.include_dirs "environment.cmi" in
let environment_cmx =
Misc.find_in_path_uncap !Clflags.include_dirs "environment.cmx" in
Digest.(to_hex (file Sys.executable_name) ^
(to_hex (file environment_cmi)) ^
(to_hex (file environment_cmx)))
with Not_found ->
Printf.eprintf "%s: Cannot find 'environment.cmi'.\n%!" Sys.argv.(0);
Pervasives.exit 1
in
let update_needed =
not (Sys.file_exists (build_dir // ".tezos_compiler"))
|| read_md5 (build_dir // ".tezos_compiler") <> md5 in
if keep_object then
create_file (build_dir // ".tezos_compiler") (md5 ^ "\n");
(* Compile the /ad-hoc/ Error_monad. *)
List.iter (dump_cmi sigs_dir) tezos_protocol_env ;
at_exit (fun () -> List.iter (unlink_cmi sigs_dir) tezos_protocol_env ) ;
let local_environment_unit = "local_environment" in
let local_environment_ml = build_dir // local_environment_unit ^ ".ml" in
create_file local_environment_ml @@ Printf.sprintf {|
module Environment = %s.Make(struct let name = %S end)()
|}
(if client then "Environment" else "Proto_environment")
logname ;
if not keep_object then
at_exit (fun () ->
safe_unlink local_environment_ml) ;
let local_environment_object =
compile_units
~ctxt
~for_pack:packname
~keep_object
~build_dir ~source_dir:build_dir [local_environment_unit]
in
Compenv.implicit_modules :=
[ "Local_environment"; "Environment" ;
"Error_monad" ; "Hash" ; "Logging" ; "Tezos_data" ];
(* Compile the protocol *)
let objects =
compile_units
~ctxt
~update_needed
~keep_object ~for_pack:packname ~build_dir ~source_dir units in
pack_objects ~ctxt ~keep_object
packed_objects (local_environment_object @ objects) ;
(* Compiler the 'registering module' *)
List.iter (dump_cmi sigs_dir) register_env;
at_exit (fun () -> List.iter (unlink_cmi sigs_dir) register_env ) ;
let register_unit =
if client then
Filename.dirname output //
"register_" ^
Filename.(basename @@ chop_extension output)
else
build_dir // Format.asprintf "register_%s" packname in
let register_file = register_unit ^ ".ml" in
create_register_file client register_file hash packname units ;
if not keep_object then at_exit (fun () -> safe_unlink register_file) ;
if keep_object then
Clflags.include_dirs := !Clflags.include_dirs @ [Filename.dirname output] ;
let register_object =
compile_ml ~keep_object:client (register_unit) register_file in
(* Create the final [cmxs] *)
Clflags.link_everything := true ;
link_shared ~static:!static output [packed_objects; register_object]