Fork of daniellemaywood.uk/gleam — Wasm codegen work
6.3 kB
182 lines
1#!/usr/bin/env escript
2-mode(compile).
3
4% TODO: Don't concurrently print warnings and errors
5% TODO: Some tests
6
7main(_) ->
8 ok = io:setopts([binary, {encoding, utf8}]),
9 ok = configure_logging(),
10 compile_package_loop().
11
12compile_package_loop() ->
13 case io:get_line("") of
14 eof -> ok;
15 Line ->
16 Chars = unicode:characters_to_list(Line),
17 {ok, Tokens, _} = erl_scan:string(Chars),
18 {ok, {Lib, Out, Modules}} = erl_parse:parse_term(Tokens),
19 case compile_package(Lib, Out, Modules) of
20 {ok, ModuleNames} ->
21 PrintModuleName = fun(ModuleName) ->
22 io:put_chars("gleam-compile-module:" ++ atom_to_list(ModuleName) ++ "\n")
23 end,
24 lists:map(PrintModuleName, ModuleNames),
25 io:put_chars("gleam-compile-result-ok\n");
26 err ->
27 io:put_chars("gleam-compile-result-error\n")
28 end,
29 compile_package_loop()
30 end.
31
32compile_package(Lib, Out, Modules) ->
33 IsElixirModule = fun(Module) ->
34 filename:extension(Module) =:= ".ex"
35 end,
36 {ElixirModules, ErlangModules} = lists:partition(IsElixirModule, Modules),
37 ok = filelib:ensure_dir([Out, $/]),
38 ok = add_lib_to_erlang_path(Lib),
39 {ErlangOk, ErlangBeams} = compile_erlang(ErlangModules, Out),
40 {ElixirOk, ElixirBeams} = case ErlangOk of
41 true -> compile_elixir(ElixirModules, Out);
42 false -> {false, []}
43 end,
44 ok = del_lib_from_erlang_path(Lib),
45 case ErlangOk andalso ElixirOk of
46 true ->
47 ModuleNames = proplists:get_keys(ErlangBeams ++ ElixirBeams),
48 {ok, ModuleNames};
49 false ->
50 err
51 end.
52
53compile_erlang(Modules, Out) ->
54 Workers = start_compiler_workers(Out),
55 ok = producer_loop(Modules, Workers),
56 collect_results({true, []}).
57
58collect_results(Acc = {Result, Beams}) ->
59 receive
60 {compiled, ModuleName, Beam} -> collect_results({Result, [{ModuleName, Beam} | Beams]});
61 failed -> collect_results({false, Beams})
62 after 0 -> Acc
63 end.
64
65producer_loop([], 0) ->
66 ok;
67producer_loop([], Workers) ->
68 receive
69 {work_please, _} -> producer_loop([], Workers - 1)
70 end;
71producer_loop([Module | Modules], Workers) ->
72 receive
73 {work_please, Worker} ->
74 erlang:send(Worker, {module, Module}),
75 producer_loop(Modules, Workers)
76 end.
77
78start_compiler_workers(Out) ->
79 Parent = self(),
80 NumSchedulers = erlang:system_info(schedulers),
81 SpawnWorker = fun(_) ->
82 erlang:spawn_link(fun() -> worker_loop(Parent, Out) end)
83 end,
84 lists:foreach(SpawnWorker, lists:seq(1, NumSchedulers)),
85 NumSchedulers.
86
87worker_loop(Parent, Out) ->
88 Options = [report_errors, report_warnings, debug_info, {outdir, Out}],
89 erlang:send(Parent, {work_please, self()}),
90 receive
91 {module, Module} ->
92 log({compiling, Module}),
93 case compile:file(Module, Options) of
94 {ok, ModuleName} ->
95 Beam = filename:join(Out, ModuleName) ++ ".beam",
96 Message = {compiled, ModuleName, Beam},
97 log(Message),
98 erlang:send(Parent, Message);
99 error ->
100 log({failed, Module}),
101 erlang:send(Parent, failed)
102 end,
103 worker_loop(Parent, Out)
104 end.
105
106compile_elixir(Modules, Out) ->
107 Error = [
108 "The program elixir was not found. Is it installed?",
109 $\n,
110 "Documentation for installing Elixir can be viewed here:",
111 $\n,
112 "https://elixir-lang.org/install.html"
113 ],
114 case Modules of
115 [] -> {true, []};
116 _ ->
117 log({starting, "compiler.app"}),
118 ok = application:start(compiler),
119 log({starting, "elixir.app"}),
120 case application:start(elixir) of
121 ok -> do_compile_elixir(Modules, Out);
122 _ ->
123 io:put_chars(standard_error, [Error, $\n]),
124 {false, []}
125 end
126 end.
127
128do_compile_elixir(Modules, Out) ->
129 ModuleBins = lists:map(fun(Module) ->
130 log({compiling, Module}),
131 list_to_binary(Module)
132 end, Modules),
133 OutBin = list_to_binary(Out),
134 Options = [{dest, OutBin}],
135 % Silence "redefining module" warnings.
136 % Compiled modules in the build directory are added to the code path.
137 % These warnings result from recompiling loaded modules.
138 % TODO: This line can likely be removed if/when the build directory is cleaned before every compilation.
139 'Elixir.Code':compiler_options([{ignore_module_conflict, true}]),
140 case 'Elixir.Kernel.ParallelCompiler':compile_to_path(ModuleBins, OutBin, Options) of
141 {ok, ModuleAtoms, _} ->
142 ToBeam = fun(ModuleAtom) ->
143 Beam = filename:join(Out, atom_to_list(ModuleAtom)) ++ ".beam",
144 log({compiled, Beam}),
145 {ModuleAtom, Beam}
146 end,
147 {true, lists:map(ToBeam, ModuleAtoms)};
148 {error, Errors, _} ->
149 % Log all filenames associated with modules that failed to compile.
150 % Note: The compiler prints compilation errors upon encountering them.
151 ErrorFiles = lists:usort([File || {File, _, _} <- Errors]),
152 Log = fun(File) ->
153 log({failed, binary_to_list(File)})
154 end,
155 lists:foreach(Log, ErrorFiles),
156 {false, []};
157 _ -> {false, []}
158 end.
159
160add_lib_to_erlang_path(Lib) ->
161 code:add_paths(expand_lib_paths(Lib)).
162
163-if(?OTP_RELEASE >= 26).
164del_lib_from_erlang_path(Lib) ->
165 code:del_paths(expand_lib_paths(Lib)).
166-else.
167del_lib_from_erlang_path(Lib) ->
168 lists:foreach(fun code:del_path/1, expand_lib_paths(Lib)).
169-endif.
170
171expand_lib_paths(Lib) ->
172 filelib:wildcard([Lib, "/*/ebin"]).
173
174configure_logging() ->
175 Enabled = os:getenv("GLEAM_LOG") /= false,
176 persistent_term:put(gleam_logging_enabled, Enabled).
177
178log(Term) ->
179 case persistent_term:get(gleam_logging_enabled) of
180 true -> io:fwrite("~p~n", [Term]), ok;
181 false -> ok
182 end.