This commit is contained in:
Graham McIntire 2025-07-18 14:16:17 -05:00
parent 05d35150c3
commit 05267f20ad
No known key found for this signature in database
4 changed files with 21 additions and 199 deletions

View file

@ -1,182 +0,0 @@
#!/usr/bin/env escript
-mode(compile).
% TODO: Don't concurrently print warnings and errors
% TODO: Some tests
main(_) ->
ok = io:setopts([binary, {encoding, utf8}]),
ok = configure_logging(),
compile_package_loop().
compile_package_loop() ->
case io:get_line("") of
eof -> ok;
Line ->
Chars = unicode:characters_to_list(Line),
{ok, Tokens, _} = erl_scan:string(Chars),
{ok, {Lib, Out, Modules}} = erl_parse:parse_term(Tokens),
case compile_package(Lib, Out, Modules) of
{ok, ModuleNames} ->
PrintModuleName = fun(ModuleName) ->
io:put_chars("gleam-compile-module:" ++ atom_to_list(ModuleName) ++ "\n")
end,
lists:map(PrintModuleName, ModuleNames),
io:put_chars("gleam-compile-result-ok\n");
err ->
io:put_chars("gleam-compile-result-error\n")
end,
compile_package_loop()
end.
compile_package(Lib, Out, Modules) ->
IsElixirModule = fun(Module) ->
filename:extension(Module) =:= ".ex"
end,
{ElixirModules, ErlangModules} = lists:partition(IsElixirModule, Modules),
ok = filelib:ensure_dir([Out, $/]),
ok = add_lib_to_erlang_path(Lib),
{ErlangOk, ErlangBeams} = compile_erlang(ErlangModules, Out),
{ElixirOk, ElixirBeams} = case ErlangOk of
true -> compile_elixir(ElixirModules, Out);
false -> {false, []}
end,
ok = del_lib_from_erlang_path(Lib),
case ErlangOk andalso ElixirOk of
true ->
ModuleNames = proplists:get_keys(ErlangBeams ++ ElixirBeams),
{ok, ModuleNames};
false ->
err
end.
compile_erlang(Modules, Out) ->
Workers = start_compiler_workers(Out),
ok = producer_loop(Modules, Workers),
collect_results({true, []}).
collect_results(Acc = {Result, Beams}) ->
receive
{compiled, ModuleName, Beam} -> collect_results({Result, [{ModuleName, Beam} | Beams]});
failed -> collect_results({false, Beams})
after 0 -> Acc
end.
producer_loop([], 0) ->
ok;
producer_loop([], Workers) ->
receive
{work_please, _} -> producer_loop([], Workers - 1)
end;
producer_loop([Module | Modules], Workers) ->
receive
{work_please, Worker} ->
erlang:send(Worker, {module, Module}),
producer_loop(Modules, Workers)
end.
start_compiler_workers(Out) ->
Parent = self(),
NumSchedulers = erlang:system_info(schedulers),
SpawnWorker = fun(_) ->
erlang:spawn_link(fun() -> worker_loop(Parent, Out) end)
end,
lists:foreach(SpawnWorker, lists:seq(1, NumSchedulers)),
NumSchedulers.
worker_loop(Parent, Out) ->
Options = [report_errors, report_warnings, debug_info, {outdir, Out}],
erlang:send(Parent, {work_please, self()}),
receive
{module, Module} ->
log({compiling, Module}),
case compile:file(Module, Options) of
{ok, ModuleName} ->
Beam = filename:join(Out, ModuleName) ++ ".beam",
Message = {compiled, ModuleName, Beam},
log(Message),
erlang:send(Parent, Message);
error ->
log({failed, Module}),
erlang:send(Parent, failed)
end,
worker_loop(Parent, Out)
end.
compile_elixir(Modules, Out) ->
Error = [
"The program elixir was not found. Is it installed?",
$\n,
"Documentation for installing Elixir can be viewed here:",
$\n,
"https://elixir-lang.org/install.html"
],
case Modules of
[] -> {true, []};
_ ->
log({starting, "compiler.app"}),
ok = application:start(compiler),
log({starting, "elixir.app"}),
case application:start(elixir) of
ok -> do_compile_elixir(Modules, Out);
_ ->
io:put_chars(standard_error, [Error, $\n]),
{false, []}
end
end.
do_compile_elixir(Modules, Out) ->
ModuleBins = lists:map(fun(Module) ->
log({compiling, Module}),
list_to_binary(Module)
end, Modules),
OutBin = list_to_binary(Out),
Options = [{dest, OutBin}],
% Silence "redefining module" warnings.
% Compiled modules in the build directory are added to the code path.
% These warnings result from recompiling loaded modules.
% TODO: This line can likely be removed if/when the build directory is cleaned before every compilation.
'Elixir.Code':compiler_options([{ignore_module_conflict, true}]),
case 'Elixir.Kernel.ParallelCompiler':compile_to_path(ModuleBins, OutBin, Options) of
{ok, ModuleAtoms, _} ->
ToBeam = fun(ModuleAtom) ->
Beam = filename:join(Out, atom_to_list(ModuleAtom)) ++ ".beam",
log({compiled, Beam}),
{ModuleAtom, Beam}
end,
{true, lists:map(ToBeam, ModuleAtoms)};
{error, Errors, _} ->
% Log all filenames associated with modules that failed to compile.
% Note: The compiler prints compilation errors upon encountering them.
ErrorFiles = lists:usort([File || {File, _, _} <- Errors]),
Log = fun(File) ->
log({failed, binary_to_list(File)})
end,
lists:foreach(Log, ErrorFiles),
{false, []};
_ -> {false, []}
end.
add_lib_to_erlang_path(Lib) ->
code:add_paths(expand_lib_paths(Lib)).
-if(?OTP_RELEASE >= 26).
del_lib_from_erlang_path(Lib) ->
code:del_paths(expand_lib_paths(Lib)).
-else.
del_lib_from_erlang_path(Lib) ->
lists:foreach(fun code:del_path/1, expand_lib_paths(Lib)).
-endif.
expand_lib_paths(Lib) ->
filelib:wildcard([Lib, "/*/ebin"]).
configure_logging() ->
Enabled = os:getenv("GLEAM_LOG") /= false,
persistent_term:put(gleam_logging_enabled, Enabled).
log(Term) ->
case persistent_term:get(gleam_logging_enabled) of
true -> io:fwrite("~p~n", [Term]), ok;
false -> ok
end.

View file

@ -1,3 +1,3 @@
[packages] [packages]
gleam_stdlib = "0.62.0"
gleeunit = "1.6.0" gleeunit = "1.6.0"
gleam_stdlib = "0.62.0"

View file

@ -81,6 +81,7 @@ defmodule Mix.Tasks.GleamCompile do
# Also copy Gleam stdlib files # Also copy Gleam stdlib files
gleam_stdlib_dir = "build/dev/erlang/gleam_stdlib/ebin" gleam_stdlib_dir = "build/dev/erlang/gleam_stdlib/ebin"
if File.exists?(gleam_stdlib_dir) do if File.exists?(gleam_stdlib_dir) do
Mix.shell().info("Copying Gleam stdlib files...") Mix.shell().info("Copying Gleam stdlib files...")
@ -110,7 +111,7 @@ defmodule Mix.Tasks.GleamCompile do
|> File.ls!() |> File.ls!()
|> Enum.filter(fn file -> |> Enum.filter(fn file ->
String.ends_with?(file, ".beam") and String.ends_with?(file, ".beam") and
(String.starts_with?(file, "gleam") or String.starts_with?(file, "aprsme@")) (String.starts_with?(file, "gleam") or String.starts_with?(file, "aprsme@"))
end) end)
|> Enum.each(fn beam_file -> |> Enum.each(fn beam_file ->
src = Path.join(priv_gleam, beam_file) src = Path.join(priv_gleam, beam_file)
@ -140,7 +141,9 @@ defmodule Mix.Tasks.GleamCompile do
Mix.shell().info("Copied #{beam_file} from dev build") Mix.shell().info("Copied #{beam_file} from dev build")
end) end)
else else
Mix.shell().error("Unable to find Gleam compiled files. Please ensure Gleam code is compiled before deployment.") Mix.shell().error(
"Unable to find Gleam compiled files. Please ensure Gleam code is compiled before deployment."
)
end end
end end
end end

View file

@ -155,6 +155,7 @@ defmodule Aprsme.MixProject do
# Copy from priv/gleam if available # Copy from priv/gleam if available
priv_gleam = "priv/gleam" priv_gleam = "priv/gleam"
if File.exists?(priv_gleam) do if File.exists?(priv_gleam) do
priv_gleam priv_gleam
|> File.ls!() |> File.ls!()