Skip to main content

src/gleamdoc_ffi.erl

-module(gleamdoc_ffi).
-export([
    export_package_interface/2,
    interface_fingerprint/1,
    stdout_supports_color/0,
    stderr_supports_color/0,
    exit_status/1
]).

stdout_supports_color() ->
    supports_color(standard_io).

stderr_supports_color() ->
    supports_color(standard_error).

supports_color(Device) ->
    os:getenv("NO_COLOR") =:= false andalso
    os:getenv("TERM") =/= "dumb" andalso
    case io:columns(Device) of
        {ok, _} -> true;
        _ -> false
    end.

export_package_interface(Directory, Output) ->
    case os:find_executable("gleam") of
        false -> {error, <<"Could not find the Gleam executable on PATH.">>};
        Gleam ->
            run(
                Gleam,
                [
                    "export",
                    "package-interface",
                    "--out",
                    binary_to_list(Output)
                ],
                binary_to_list(Directory)
            )
    end.

run(Executable, Arguments, Directory) ->
    Port = open_port(
        {spawn_executable, Executable},
        [
            binary,
            exit_status,
            stderr_to_stdout,
            use_stdio,
            {args, Arguments},
            {cd, Directory}
        ]
    ),
    collect(Port, []).

collect(Port, Output) ->
    receive
        {Port, {data, Data}} -> collect(Port, [Data | Output]);
        {Port, {exit_status, 0}} -> {ok, nil};
        {Port, {exit_status, _}} ->
            {error, iolist_to_binary(lists:reverse(Output))}
    end.

interface_fingerprint(Paths) ->
    try
        Hash = lists:foldl(
            fun(Path, Context) ->
                Content = case file:read_file(Path) of
                    {ok, Data} -> Data;
                    {error, enoent} -> <<"<missing>">>;
                    {error, Reason} -> error({read_failed, Path, Reason})
                end,
                crypto:hash_update(Context, [Path, 0, Content, 0])
            end,
            crypto:hash_init(sha256),
            lists:sort(Paths)
        ),
        {ok, binary:encode_hex(crypto:hash_final(Hash), lowercase)}
    catch
        error:{read_failed, Path, Reason} ->
            {error, iolist_to_binary(io_lib:format(
                "Could not fingerprint ~ts: ~tp", [Path, Reason]
            ))}
    end.

exit_status(Status) ->
    erlang:halt(Status).