Skip to content

Commit b4d2988

Browse files
committed
Refactor build logic to remove TOML dependency
1 parent 390ce02 commit b4d2988

File tree

6 files changed

+154
-98
lines changed

6 files changed

+154
-98
lines changed

rustler_mix/lib/rustler.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ defmodule Rustler do
7979
8080
* `:target` - Specify a compile [target] triple.
8181
82-
* `:target_dir` - Override the compiler output directory.
83-
8482
Any of the above options can be passed directly into the `use` macro like so:
8583
8684
defmodule MyNIF do
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
defmodule Rustler.BuildResults do
2+
defstruct artifacts: []
3+
end
4+
5+
defmodule Rustler.BuildArtifact do
6+
defstruct [:kind, :name, :filename]
7+
8+
def from_line(line) do
9+
if line["reason"] == "compiler-artifact" do
10+
target = line["target"]
11+
12+
base = %__MODULE__{
13+
name: target["name"],
14+
filename: line["filenames"] |> :erlang.hd()
15+
}
16+
17+
kind = target["kind"]
18+
19+
maybe_lib =
20+
if kind |> Enum.member?("dylib") || kind |> Enum.member?("cdylib") do
21+
[%{base | kind: :lib}]
22+
end || []
23+
24+
maybe_exec =
25+
if kind |> Enum.member?("bin") do
26+
[%{base | kind: :bin, filename: line["executable"]}]
27+
end || []
28+
29+
maybe_lib ++ maybe_exec
30+
end || []
31+
end
32+
end
33+
34+
defimpl Collectable, for: Rustler.BuildResults do
35+
def into(results) do
36+
maybe_log_msg = fn elem ->
37+
if elem["reason"] == "compiler-message" do
38+
# TODO: Log message
39+
# Mix.shell().info(elem["message"]["message"])
40+
end
41+
end
42+
43+
collector_fun = fn
44+
acc, {:cont, elem} ->
45+
case Jason.decode(elem) do
46+
{:ok, elem} ->
47+
maybe_log_msg.(elem)
48+
update_in(acc.artifacts, &(&1 ++ Rustler.BuildArtifact.from_line(elem)))
49+
50+
{:error, _err} ->
51+
acc
52+
end
53+
54+
acc, :done ->
55+
acc
56+
57+
_acc, :halt ->
58+
:ok
59+
end
60+
61+
{results, collector_fun}
62+
end
63+
end

rustler_mix/lib/rustler/compiler.ex

Lines changed: 89 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,76 @@ defmodule Rustler.Compiler do
88
config = Config.from(otp_app, config, opts)
99

1010
if !config.skip_compilation? do
11-
crate_full_path = Path.expand(config.path, File.cwd!())
12-
1311
File.mkdir_p!(priv_dir())
1412

1513
Mix.shell().info("Compiling crate #{config.crate} in #{config.mode} mode (#{config.path})")
14+
do_compile(config)
15+
Mix.Project.build_structure()
16+
17+
# See #326: Ensure that the libraries are copied into the correct subdirectory
18+
# in `_build`.
19+
missing = Enum.reject(expected_files(config), &File.exists?/1)
20+
21+
if missing != [] do
22+
Mix.shell().info("Expected files do not exist: #{inspect(missing)}, rebuilding")
23+
24+
do_compile(config, true)
25+
Mix.Project.build_structure()
26+
end
27+
28+
missing = Enum.reject(expected_files(config), &File.exists?/1)
29+
30+
if missing != [] do
31+
raise "Expected files do still not exist: #{inspect(missing)}\nPlease verify your configuration"
32+
end
33+
end
1634

17-
[cmd | args] =
18-
make_base_command(config.cargo)
19-
|> make_no_default_features_flag(config.default_features)
20-
|> make_features_flag(config.features)
21-
|> make_target_flag(config.target)
22-
|> make_build_mode_flag(config.mode)
35+
config
36+
end
2337

24-
ensure_platform_requirements!(crate_full_path, config, :os.type())
38+
defp do_compile(config, recompile \\ false) do
39+
[cmd | args] =
40+
rustc(config.cargo)
41+
|> build_flags(config)
42+
|> make_build_mode_flag(config.mode)
43+
44+
args =
45+
if recompile do
46+
# HACK: Cargo does not allow forcing a recompilation, but we need the
47+
# compiler-artifact messages, so force a recompilation via a "config
48+
# change"
49+
args ++ ["--cfg", "build_#{System.system_time(:millisecond)}"]
50+
else
51+
args
52+
end
2553

26-
compile_result =
27-
System.cmd(cmd, args,
28-
cd: crate_full_path,
29-
stderr_to_stdout: true,
30-
env: [{"CARGO_TARGET_DIR", config.target_dir} | config.env],
31-
into: IO.stream(:stdio, :line)
32-
)
54+
compile_result =
55+
System.cmd(cmd, args, env: config.env, into: %Rustler.BuildResults{}, lines: 1024)
3356

57+
artifacts =
3458
case compile_result do
35-
{_, 0} -> :ok
59+
{build_results, 0} -> build_results.artifacts
3660
{_, code} -> raise "Rust NIF compile error (rustc exit code #{code})"
3761
end
3862

39-
handle_artifacts(crate_full_path, config)
40-
# See #326: Ensure that the libraries are copied into the correct subdirectory
41-
# in `_build`.
42-
Mix.Project.build_structure()
43-
end
63+
handle_artifacts(artifacts, config)
64+
end
4465

45-
config
66+
defp build_flags(args, config) do
67+
args
68+
|> make_package_flag(config.crate)
69+
|> make_no_default_features_flag(config.default_features)
70+
|> make_features_flag(config.features)
71+
|> make_target_flag(config.target)
4672
end
4773

48-
defp make_base_command(:system), do: ["cargo", "rustc"]
49-
defp make_base_command({:system, channel}), do: ["cargo", channel, "rustc"]
50-
defp make_base_command({:bin, path}), do: [path, "rustc"]
74+
defp rustc(cargo) do
75+
make_base_command(cargo) ++ ["rustc", "--message-format=json-render-diagnostics", "--quiet"]
76+
end
77+
78+
defp make_base_command(:system), do: ["cargo"]
79+
defp make_base_command({:system, channel}), do: ["cargo", channel]
80+
defp make_base_command({:bin, path}), do: [path]
5181

5282
defp make_base_command({:rustup, version}) do
5383
if Rustup.version() == :none do
@@ -58,10 +88,10 @@ defmodule Rustler.Compiler do
5888
throw_error({:rust_version_not_installed, version})
5989
end
6090

61-
["rustup", "run", version, "cargo", "rustc"]
91+
["rustup", "run", version, "cargo"]
6292
end
6393

64-
defp ensure_platform_requirements!(_, _, _), do: :ok
94+
defp make_package_flag(args, crate), do: args ++ ["-p", "#{crate}"]
6595

6696
defp make_no_default_features_flag(args, true), do: args
6797
defp make_no_default_features_flag(args, false), do: args ++ ["--no-default-features"]
@@ -99,71 +129,54 @@ defmodule Rustler.Compiler do
99129
end
100130
end
101131

102-
defp handle_artifacts(path, config) do
103-
toml = toml_data(path)
132+
defp handle_artifacts(artifacts, config) do
104133
target = config.target
105-
names = get_name(toml, :lib) ++ get_name(toml, :bin)
106134

107-
output_dir =
108-
if is_binary(target) do
109-
Path.join([target, Atom.to_string(config.mode)])
110-
else
111-
Atom.to_string(config.mode)
112-
end
135+
if artifacts == [] do
136+
throw_error(:no_artifacts)
137+
end
113138

114-
Enum.each(names, fn {name, type} ->
115-
{src_file, dst_file} = make_file_names(name, type, target)
116-
compiled_lib = Path.join([config.target_dir, output_dir, src_file])
117-
destination_lib = Path.join(priv_dir(), dst_file)
118-
119-
# If the file exists already, we delete it first. This is to ensure that another
120-
# process, which might have the library dynamically linked in, does not generate
121-
# a segfault. By deleting it first, we ensure that the copy operation below does
122-
# not write into the existing file.
123-
File.rm(destination_lib)
124-
File.cp!(compiled_lib, destination_lib)
139+
Enum.each(artifacts, fn artifact = %Rustler.BuildArtifact{} ->
140+
if artifact.kind == :lib or artifact.kind == :bin do
141+
dst_file = output_file(artifact.name, artifact.kind, target)
142+
destination_lib = Path.join(priv_dir(), dst_file)
143+
144+
# If the file exists already, we delete it first. This is to ensure that another
145+
# process, which might have the library dynamically linked in, does not generate
146+
# a segfault. By deleting it first, we ensure that the copy operation below does
147+
# not write into the existing file.
148+
Mix.shell().info("Copying #{artifact.filename} to #{destination_lib}")
149+
File.rm(destination_lib)
150+
File.cp!(artifact.filename, destination_lib)
151+
end
125152
end)
126153
end
127154

128-
defp get_name(toml, section) do
129-
case toml[to_string(section)] do
130-
nil -> []
131-
values when is_map(values) -> [{values["name"], section}]
132-
values when is_list(values) -> Enum.map(values, &{&1["name"], section})
133-
end
134-
end
155+
defp expected_files(config) do
156+
lib = if config.lib, do: [output_file(config.crate, :lib, config.target)], else: []
157+
bins = for bin <- config.binaries, do: output_file(bin, :bin, config.target)
135158

136-
defp get_suffix(target, :lib) do
137-
case get_target_os_type(target) do
138-
{:win32, _} -> "dll"
139-
{:unix, :darwin} -> "dylib"
140-
{:unix, _} -> "so"
159+
for filename <- lib ++ bins do
160+
Path.join(priv_dir(), filename)
141161
end
142162
end
143163

144-
defp get_suffix(target, :bin) do
145-
case get_target_os_type(target) do
146-
{:win32, _} -> "exe"
147-
{:unix, _} -> ""
148-
end
164+
defp output_file(base_name, kind, target) do
165+
"#{base_name}#{suffix(kind, target)}"
149166
end
150167

151-
defp make_file_names(base_name, :lib, target) do
152-
suffix = get_suffix(target, :lib)
153-
168+
defp suffix(:lib, target) do
154169
case get_target_os_type(target) do
155-
{:win32, _} -> {"#{base_name}.#{suffix}", "lib#{base_name}.dll"}
156-
{:unix, :darwin} -> {"lib#{base_name}.#{suffix}", "lib#{base_name}.so"}
157-
{:unix, _} -> {"lib#{base_name}.#{suffix}", "lib#{base_name}.so"}
170+
{:win32, _} -> ".dll"
171+
{:unix, :darwin} -> ".so"
172+
{:unix, _} -> ".so"
158173
end
159174
end
160175

161-
defp make_file_names(base_name, :bin, target) do
162-
suffix = get_suffix(target, :bin)
163-
176+
defp suffix(:bin, target) do
164177
case get_target_os_type(target) do
165-
{:win32, _} -> {"#{base_name}.#{suffix}", "#{base_name}.exe"}
166-
{:unix, _} -> {base_name, base_name}
178+
{:win32, _} -> ".exe"
179+
{:unix, _} -> ""
167180
end
168181
end
169182

@@ -172,19 +185,5 @@ defmodule Rustler.Compiler do
172185
raise "Compilation error"
173186
end
174187

175-
defp toml_data(path) do
176-
if !File.dir?(path) do
177-
throw_error({:nonexistent_crate_directory, path})
178-
end
179-
180-
case File.read("#{path}/Cargo.toml") do
181-
{:error, :enoent} ->
182-
throw_error({:cargo_toml_not_found, path})
183-
184-
{:ok, text} ->
185-
Toml.decode!(text)
186-
end
187-
end
188-
189188
defp priv_dir, do: "priv/native"
190189
end

rustler_mix/lib/rustler/compiler/config.ex

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,15 @@ defmodule Rustler.Compiler.Config do
1818
external_resources: [],
1919
features: [],
2020
lib: true,
21+
binaries: [],
2122
load_data: 0,
2223
load_data_fun: nil,
2324
load_from: nil,
2425
mode: :release,
2526
otp_app: nil,
2627
path: "",
27-
priv_dir: "",
2828
skip_compilation?: false,
2929
target: nil,
30-
target_dir: "",
3130
metadata: nil
3231

3332
alias Rustler.Compiler.Config
@@ -37,11 +36,10 @@ defmodule Rustler.Compiler.Config do
3736

3837
defaults = %Config{
3938
crate: crate,
40-
load_from: {otp_app, "priv/native/lib#{crate}"},
4139
mode: :release,
4240
otp_app: otp_app,
4341
path: "native/#{crate}",
44-
target_dir: Application.app_dir(otp_app, "native/#{crate}")
42+
load_from: {otp_app, "priv/native/#{crate}"}
4543
}
4644

4745
defaults

rustler_mix/mix.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ defmodule Rustler.Mixfile do
2424

2525
defp deps do
2626
[
27-
{:toml, "~> 0.7", runtime: false},
2827
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
2928
{:jason, "~> 1.0", runtime: false}
3029
]

rustler_mix/priv/templates/basic/Cargo.toml.eex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ authors = []
55
edition = "2021"
66

77
[lib]
8-
name = "<%= library_name %>"
98
crate-type = ["cdylib"]
109

1110
[dependencies]

0 commit comments

Comments
 (0)