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]
gleam_stdlib = "0.62.0"
gleeunit = "1.6.0"
gleam_stdlib = "0.62.0"

View file

@ -78,12 +78,13 @@ defmodule Mix.Tasks.GleamCompile do
else
Mix.shell().info("Gleam output directory #{gleam_output} does not exist")
end
# Also copy Gleam stdlib files
gleam_stdlib_dir = "build/dev/erlang/gleam_stdlib/ebin"
if File.exists?(gleam_stdlib_dir) do
Mix.shell().info("Copying Gleam stdlib files...")
gleam_stdlib_dir
|> File.ls!()
|> Enum.filter(&String.ends_with?(&1, ".beam"))
@ -92,25 +93,25 @@ defmodule Mix.Tasks.GleamCompile do
dest = Path.join(ebin_dir, beam_file)
File.copy!(src, dest)
end)
Mix.shell().info("Copied Gleam stdlib files")
end
else
# Last resort: Check for pre-compiled BEAM files in priv/gleam
Mix.shell().info("No Gleam compiler available, checking for pre-compiled BEAM files...")
priv_gleam = "priv/gleam"
ebin_dir = "_build/#{env}/lib/aprsme/ebin"
if File.exists?(priv_gleam) do
File.mkdir_p!(ebin_dir)
# Copy all Gleam-related BEAM files
priv_gleam
|> File.ls!()
|> Enum.filter(fn file ->
String.ends_with?(file, ".beam") and
(String.starts_with?(file, "gleam") or String.starts_with?(file, "aprsme@"))
|> Enum.filter(fn file ->
String.ends_with?(file, ".beam") and
(String.starts_with?(file, "gleam") or String.starts_with?(file, "aprsme@"))
end)
|> Enum.each(fn beam_file ->
src = Path.join(priv_gleam, beam_file)
@ -118,7 +119,7 @@ defmodule Mix.Tasks.GleamCompile do
File.copy!(src, dest)
Mix.shell().info("Copied pre-compiled #{beam_file} from priv/gleam")
end)
# Check if we got the main module
if not File.exists?(Path.join(ebin_dir, "aprsme@encoding.beam")) do
Mix.shell().error("Pre-compiled aprsme@encoding.beam not found in priv/gleam")
@ -126,10 +127,10 @@ defmodule Mix.Tasks.GleamCompile do
else
# Try to copy from dev build if available
dev_ebin = "_build/dev/lib/aprsme/ebin"
if File.exists?(dev_ebin) and env != :dev do
File.mkdir_p!(ebin_dir)
dev_ebin
|> File.ls!()
|> Enum.filter(&String.starts_with?(&1, "aprsme@"))
@ -140,7 +141,9 @@ defmodule Mix.Tasks.GleamCompile do
Mix.shell().info("Copied #{beam_file} from dev build")
end)
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

View file

@ -147,14 +147,15 @@ defmodule Aprsme.MixProject do
# {:aprs, github: "aprsme/aprs", branch: "main"}
# end
end
# Copy Gleam BEAM files to the release
defp copy_gleam_files(release) do
app_dir = Path.join([release.path, "lib", "aprsme-#{release.version}", "ebin"])
File.mkdir_p!(app_dir)
# Copy from priv/gleam if available
priv_gleam = "priv/gleam"
if File.exists?(priv_gleam) do
priv_gleam
|> File.ls!()
@ -165,7 +166,7 @@ defmodule Aprsme.MixProject do
File.copy!(src, dest)
end)
end
release
end
end