- Rename Gleam module from encoding_simple to encoding - Move Gleam files from src/aprs/ to src/aprsme/ to match project namespace - Create custom Mix task for Gleam compilation (lib/mix/tasks/gleam_compile.ex) - Update EncodingUtils to wrap Gleam implementation instead of pure Elixir - Add Gleam dependencies to mix.exs and configure build paths - Update Mix aliases to include Gleam compilation in test and compile tasks - Add Gleam support to GitHub Actions CI workflow with caching - Add GLEAM_INTEGRATION.md documentation - All 357 tests passing with Gleam integration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
182 lines
6.1 KiB
Erlang
182 lines
6.1 KiB
Erlang
#!/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.
|