1-module(lustre_dev_tools_ffi).
2-export([
3 check_live_reloading/0,
4 fs_start_link/2,
5 get_cwd/0,
6 get_cpu/0,
7 get_esbuild/1,
8 get_tailwind/1,
9 get_os/0,
10 otp_version/0,
11 unzip_esbuild/1,
12 exec/3
13]).
14
15otp_version() ->
16 Version = erlang:system_info(otp_release),
17 list_to_binary(Version).
18
19get_cwd() ->
20 case file:get_cwd() of
21 {ok, Cwd} -> {ok, list_to_binary(Cwd)};
22 {error, Reason} -> {error, Reason}
23 end.
24
25get_os() ->
26 case os:type() of
27 {win32, _} -> <<"win32">>;
28 {unix, darwin} -> <<"darwin">>;
29 {unix, linux} -> <<"linux">>;
30 {_, Unknown} -> atom_to_binary(Unknown, utf8)
31 end.
32
33get_cpu() ->
34 case erlang:system_info(os_type) of
35 {unix, _} ->
36 [Arch, _] = string:split(erlang:system_info(system_architecture), "-"),
37 list_to_binary(Arch);
38 {win32, _} ->
39 case erlang:system_info(wordsize) of
40 4 -> <<"ia32">>;
41 8 -> <<"x64">>
42 end
43 end.
44
45get_esbuild(Url) ->
46 inets:start(),
47 ssl:start(),
48
49 case httpc:request(get, {Url, []}, [], [{body_format, binary}]) of
50 {ok, {{_, 200, _}, _, Zip}} -> {ok, Zip};
51 {ok, Res} -> {error, Res};
52 {error, Err} -> {error, Err}
53 end.
54
55get_tailwind(Url) ->
56 inets:start(),
57 ssl:start(),
58
59 case httpc:request(get, {Url, []}, [], [{body_format, binary}]) of
60 {ok, {{_, 200, _}, _, Bin}} -> {ok, Bin};
61 {ok, Res} -> {error, Res};
62 {error, Err} -> {error, Err}
63 end.
64
65unzip_esbuild(Zip) ->
66 Filepath =
67 case os:type() of
68 {win32, _} -> "package/esbuild.exe";
69 _ -> "package/bin/esbuild"
70 end,
71
72 Result =
73 erl_tar:extract({binary, Zip}, [
74 memory, compressed, {files, [Filepath]}
75 ]),
76
77 case Result of
78 {ok, [{_, Esbuild}]} -> {ok, Esbuild};
79 {ok, Res} -> {error, Res};
80 {error, Err} -> {error, Err}
81 end.
82
83exec(Command, Args, Cwd) ->
84 Command_ = binary_to_list(Command),
85 Args_ = lists:map(fun(Arg) -> binary_to_list(Arg) end, Args),
86 Cwd_ = binary_to_list(Cwd),
87
88 Name = case Command_ of
89 "./" ++ _ -> {spawn_executable, Command_};
90 "/" ++ _ -> {spawn_executable, Command_};
91 _ -> {spawn_executable, os:find_executable(Command_)}
92 end,
93
94 Port = open_port(Name, [
95 exit_status,
96 binary,
97 hide,
98 stream,
99 eof,
100 stderr_to_stdout, % We need this to hide the process' stdout
101 {args, Args_},
102 {cd, Cwd_}
103 ]),
104
105 do_exec(Port, []).
106
107do_exec(Port, Acc) ->
108 receive
109 {Port, {data, Data}} -> do_exec(Port, [Data | Acc]);
110 {Port, {exit_status, 0}} ->
111 port_close(Port),
112 {ok, list_to_binary(lists:reverse(Acc))};
113 {Port, {exit_status, Code}} ->
114 port_close(Port),
115 {error, {Code, list_to_binary(lists:reverse(Acc))}}
116 end.
117
118fs_start_link(Id, Path) ->
119 % We temporarily disable all logs when we call `start_link` because we want
120 % to be displaying a nicer custom error message and not the logged error
121 % coming from `fs`.
122 logger:add_primary_filter(discard_all, {fun(_, _) -> stop end, no_extra}),
123 Result = fs:start_link(Id, Path),
124 logger:remove_primary_filter(discard_all),
125 Result.
126
127% This is what the underlying `fs` library does to check if it has support for
128% a given os:
129%
130% https://github.com/5HT/fs/blob/23a5b46b033437a3d69504811ae6c72f7704a78a/src/fs_sup.erl#L18-L46
131%
132% Sadly the library doesn't expose such a function and just logs any error
133% instead of surfacing it as a value, so we have to implement a slightly
134% modified version of it to have proper error messages.
135check_live_reloading() ->
136 Watcher =
137 case os:type() of
138 {unix, darwin} -> fsevents;
139 {unix, linux} -> inotifywait;
140 {unix, sunos} -> undefined;
141 {unix, _} -> kqueue;
142 {win32, nt} -> inotifywait_win32;
143 _ -> undefined
144 end,
145
146 case Watcher of
147 undefined -> {error, no_file_watcher_supported_for_os};
148 _ ->
149 case Watcher:find_executable() of
150 false -> {error, {no_file_watcher_installed, Watcher}};
151 _ -> {ok, nil}
152 end
153 end.