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

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

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