Current section

Files

Jump to
parrot src parrot_ffi.erl
Raw

src/parrot_ffi.erl

-module(parrot_ffi).
-export([get_cpu/0, get_os/0, download_file/1, download_zip/1, extract_sqlc_binary/1, extract_sqlc_from_targz/1, os_command/5, os_exit/1, os_which/1, start_arguments/0]).
get_os() ->
case os:type() of
{win32, _} ->
<<"win32">>;
{unix, darwin} ->
<<"darwin">>;
{unix, linux} ->
<<"linux">>;
{_, Unknown} ->
atom_to_binary(Unknown, utf8)
end.
get_cpu() ->
case erlang:system_info(os_type) of
{unix, _} ->
[Arch, _] =
string:split(
erlang:system_info(system_architecture), "-"),
list_to_binary(Arch);
{win32, _} ->
case erlang:system_info(wordsize) of
4 ->
<<"ia32">>;
8 ->
<<"x64">>
end
end.
%% Generic file download function
download_file(Url) ->
case httpc:request(get, {Url, []}, [{timeout, 30000}], [{body_format, binary}]) of
{ok, {{_, 200, _}, _Headers, Body}} ->
{ok, Body};
{ok, {{_, StatusCode, _}, _Headers, _Body}} ->
{error, {http_error, StatusCode}};
{error, Reason} ->
{error, Reason}
end.
%% Backward compatibility - alias for download_file
download_zip(Url) ->
download_file(Url).
%% Extract sqlc binary from either zip or tar.gz archive
extract_sqlc_binary(ArchiveData) ->
case detect_archive_type(ArchiveData) of
zip ->
extract_from_zip(ArchiveData);
targz ->
extract_from_targz(ArchiveData);
{error, Reason} ->
{error, Reason}
end.
%% Extract sqlc binary from tar.gz archive
extract_sqlc_from_targz(TarGzData) ->
extract_from_targz(TarGzData).
%% Helper function to detect archive type by magic bytes
detect_archive_type(<<16#1F, 16#8B, _/binary>>) ->
% Gzip magic bytes (tar.gz files start with gzip header)
targz;
detect_archive_type(<<16#50, 16#4B, 16#03, 16#04, _/binary>>) ->
% ZIP magic bytes (local file header signature)
zip;
detect_archive_type(<<16#50, 16#4B, 16#05, 16#06, _/binary>>) ->
% ZIP magic bytes (end of central directory signature)
zip;
detect_archive_type(<<16#50, 16#4B, 16#07, 16#08, _/binary>>) ->
% ZIP magic bytes (data descriptor signature)
zip;
detect_archive_type(_) ->
{error, unknown_archive_type}.
%% Extract from ZIP archive
extract_from_zip(ZipData) ->
case zip:unzip(ZipData, [memory]) of
{ok, FileList} ->
find_sqlc_binary(FileList);
{error, Reason} ->
{error, {zip_error, Reason}}
end.
%% Extract from tar.gz archive
extract_from_targz(TarGzData) ->
case extract_tar_gz(TarGzData) of
{ok, TarData} ->
case erl_tar:extract({binary, TarData}, [memory]) of
{ok, FileList} ->
find_sqlc_binary(FileList);
{error, Reason} ->
{error, {tar_extract_error, Reason}}
end;
{error, Reason} ->
{error, Reason}
end.
%% Helper function to decompress gzip data
extract_tar_gz(TarGzData) ->
try
TarData = zlib:gunzip(TarGzData),
{ok, TarData}
catch
error:Reason ->
{error, {gzip_error, Reason}}
end.
%% Helper function to find sqlc binary in file list
find_sqlc_binary(FileList) ->
case find_sqlc_in_files(FileList) of
{ok, Data} ->
{ok, Data};
not_found ->
{error, file_not_found}
end.
%% Recursively search for sqlc binary in file list
find_sqlc_in_files([]) ->
not_found;
find_sqlc_in_files([{Filename, Data} | Rest]) ->
case is_sqlc_binary(Filename) of
true ->
{ok, Data};
false ->
find_sqlc_in_files(Rest)
end.
%% Check if filename indicates it's the sqlc binary
is_sqlc_binary(Filename) ->
% Convert to string for easier manipulation
FilenameStr = binary_to_list(iolist_to_binary(Filename)),
% Check for exact match or sqlc with extension
case filename:basename(FilenameStr) of
"sqlc" -> true;
"sqlc.exe" -> true;
_ -> false
end.
%% === === === === === === === === === %%
os_command(Command, Args, Dir, Opts, EnvBin) ->
HasPathSep = is_absolute_or_has_path_separator(Command),
Which =
case HasPathSep of
true ->
% Command has path separators (like "./sqlc" or "/usr/bin/cmd")
% Resolve it relative to Dir if it's relative
ResolvedCommand = resolve_command_path(Command, Dir),
Result = os_which_absolute(ResolvedCommand),
Result;
false ->
% Command is just a name (like "sqlc"), try PATH first, then Dir
case os_which(Command) of
{error, _} ->
JoinedPath = filename:join(Dir, Command),
Result = os_which_absolute(JoinedPath),
Result;
WhichResult ->
WhichResult
end
end,
{ExitCode, Output} =
case Which of
{error, WhichError} ->
{1, WhichError};
{ok, Executable} ->
ExecutableChars = binary_to_list(Executable),
LetBeStdout = maps:get(let_be_stdout, Opts, false),
FromBin = fun({Name, Val}) ->
{
binary_to_list(Name),
unicode:characters_to_list(Val, file:native_name_encoding())
}
end,
Env = lists:map(FromBin, EnvBin),
PortSettings = lists:merge([
[
{args, Args},
{cd, Dir},
{env, Env},
eof,
exit_status,
hide,
in
],
case maps:get(overlapped_stdio, Opts, false) of
true -> [overlapped_io];
_ -> []
end,
case LetBeStdout or maps:get(let_be_stderr, Opts, false) of
true -> [];
_ -> [stderr_to_stdout]
end,
case LetBeStdout of
true -> [{line, 99999999}];
_ -> [stream]
end
]),
try
Port = open_port({spawn_executable, ExecutableChars}, PortSettings),
{Status, OutputChars} = get_data(Port, []),
case LetBeStdout of
true -> {Status, <<>>};
_ -> {Status, list_to_binary(OutputChars)}
end
catch
Error:Reason ->
{1, list_to_binary(io_lib:format("Port execution failed: ~p:~p", [Error, Reason]))}
end
end,
case ExitCode of
0 ->
{ok, Output};
2 when Output == <<>> ->
DirError = list_to_binary(
"The directory \"" ++
binary_to_list(Dir) ++
"\" does not exist\n"
),
{error, {ExitCode, DirError}};
_ ->
{error, {ExitCode, Output}}
end.
% Helper to check if command contains path separators
is_absolute_or_has_path_separator(Command) ->
CommandStr = binary_to_list(Command),
PathType = filename:pathtype(CommandStr),
HasSlash = lists:member($/, CommandStr),
HasBackslash = lists:member($\\, CommandStr),
Result = PathType =/= relative orelse HasSlash orelse HasBackslash,
Result.
% Resolve command path relative to Dir if it's a relative path
resolve_command_path(Command, Dir) ->
CommandStr = binary_to_list(Command),
DirStr = binary_to_list(Dir),
PathType = filename:pathtype(CommandStr),
Result = case PathType of
absolute ->
% Already absolute, use as-is
list_to_binary(CommandStr);
relative ->
% For relative paths, we need to resolve the command relative to Dir
% But we need to preserve the original command's relative nature
% First, make Dir absolute if it isn't already
AbsDir = case filename:pathtype(DirStr) of
absolute -> DirStr;
relative -> filename:absname(DirStr)
end,
% Now resolve the command relative to the absolute Dir
% Use filename:absname/2 to resolve CommandStr relative to AbsDir
NormalizedPath = filename:absname(CommandStr, AbsDir),
list_to_binary(NormalizedPath)
end,
Result.
% Version of os_which that works with absolute paths and doesn't use os:find_executable
os_which_absolute(Command) ->
CommandChars = binary_to_list(Command),
FileExists = filelib:is_file(CommandChars),
case FileExists of
false ->
ExecutableError = "command `" ++ CommandChars ++ "` not found\n",
{error, list_to_binary(ExecutableError)};
true ->
IsRegular = filelib:is_regular(CommandChars),
case IsRegular of
false ->
ExecutableError = "command `" ++ CommandChars ++ "` is not a regular file\n",
{error, list_to_binary(ExecutableError)};
true ->
% Check if file is executable (Unix-like systems)
{ok, Command}
end
end.
get_data(Port, SoFar) ->
receive
{Port, {data, {_Flag, Bytes}}} ->
get_data(Port, [SoFar | Bytes]);
{Port, {data, Bytes}} ->
get_data(Port, [SoFar | Bytes]);
{Port, eof} ->
Port ! {self(), close},
receive
{Port, closed} ->
true
end,
receive
{'EXIT', Port, _} ->
ok
% force context switch
after 1 ->
ok
end,
ExitCode =
receive
{Port, {exit_status, Code}} ->
Code
end,
{ExitCode, lists:flatten(SoFar)}
end.
os_exit(Status) ->
halt(Status).
os_which(Command) ->
CommandChars = binary_to_list(Command),
{Result, OutputChars} =
case os:find_executable(CommandChars) of
false ->
case filelib:is_file(CommandChars) of
false ->
ExecutableError =
"command `" ++ CommandChars ++ "` not found\n",
{error, ExecutableError};
true ->
{ok, CommandChars}
end;
Executable ->
{ok, Executable}
end,
{Result, list_to_binary(OutputChars)}.
start_arguments() ->
lists:map(fun unicode:characters_to_binary/1, init:get_plain_arguments()).