diff --git a/lib/rustler_precompiled.ex b/lib/rustler_precompiled.ex index 5e72093..982a094 100644 --- a/lib/rustler_precompiled.ex +++ b/lib/rustler_precompiled.ex @@ -732,8 +732,11 @@ defmodule RustlerPrecompiled do cached_tar_gz = Map.fetch!(metadata, :cached_tar_gz) cache_dir = Path.dirname(cached_tar_gz) - file_name = Map.fetch!(metadata, :file_name) - lib_file = Path.join(native_dir, file_name) + lib_file = + metadata + |> Map.fetch!(:file_name) + |> String.replace_suffix(".tar.gz", "") + |> then(&Path.join(native_dir, &1)) base_url = config.base_url nif_module = config.module @@ -745,11 +748,8 @@ defmodule RustlerPrecompiled do } if File.exists?(cached_tar_gz) do - # Remove existing NIF file so we don't have processes using it. - # See: https://github.com/rusterlium/rustler/blob/46494d261cbedd3c798f584459e42ab7ee6ea1f4/rustler_mix/lib/rustler/compiler.ex#L134 - File.rm(lib_file) - with :ok <- check_file_integrity(cached_tar_gz, nif_module), + :ok <- remove_existing_nif(lib_file), :ok <- :erl_tar.extract(cached_tar_gz, [:compressed, cwd: Path.dirname(lib_file)]) do Logger.debug("Copying NIF from cache and extracting to #{lib_file}") {:ok, result} @@ -764,6 +764,7 @@ defmodule RustlerPrecompiled do with_retry(fn -> download_nif_artifact(tar_gz_url) end, config.max_retries), :ok <- File.write(cached_tar_gz, tar_gz), :ok <- check_file_integrity(cached_tar_gz, nif_module), + :ok <- remove_existing_nif(lib_file), :ok <- :erl_tar.extract({:binary, tar_gz}, [:compressed, cwd: Path.dirname(lib_file)]) do Logger.debug("NIF cached at #{cached_tar_gz} and extracted to #{lib_file}") @@ -773,6 +774,15 @@ defmodule RustlerPrecompiled do end end + # Remove existing NIF file so we don't have processes using it. + # See: https://github.com/rusterlium/rustler/blob/46494d261cbedd3c798f584459e42ab7ee6ea1f4/rustler_mix/lib/rustler/compiler.ex#L134 + defp remove_existing_nif(path) do + case File.rm(path) do + {:error, :enoent} -> :ok + result -> result + end + end + defp checksum_map(nif_module) when is_atom(nif_module) do nif_module |> checksum_file() diff --git a/test/rustler_precompiled_test.exs b/test/rustler_precompiled_test.exs index 02c7e8e..5d67166 100644 --- a/test/rustler_precompiled_test.exs +++ b/test/rustler_precompiled_test.exs @@ -517,6 +517,69 @@ defmodule RustlerPrecompiledTest do end) end + @tag :tmp_dir + test "safely replaces an open NIF file", %{ + tmp_dir: tmp_dir, + checksum_sample: checksum_sample, + nif_fixtures_dir: nif_fixtures_dir + } do + bypass = Bypass.open() + + in_tmp(tmp_dir, fn -> + File.write!("checksum-Elixir.RustlerPrecompilationExample.Native.exs", checksum_sample) + + Bypass.expect_once(bypass, fn conn -> + file_name = List.last(conn.path_info) + file = File.read!(Path.join([nif_fixtures_dir, "precompiled_nifs", file_name])) + + Plug.Conn.resp(conn, 200, file) + end) + + capture_log(fn -> + config = + RustlerPrecompiled.Config.new( + otp_app: :rustler_precompiled, + module: RustlerPrecompilationExample.Native, + base_cache_dir: tmp_dir, + base_url: "http://localhost:#{bypass.port}/download", + version: "0.2.0", + crate: "example", + targets: @available_targets, + nif_versions: @default_nif_versions, + force_build: false + ) + + {:ok, metadata} = RustlerPrecompiled.build_metadata(config) + archive = Path.join([nif_fixtures_dir, "precompiled_nifs", metadata.file_name]) + File.mkdir_p!(Path.dirname(metadata.cached_tar_gz)) + File.cp!(archive, metadata.cached_tar_gz) + + lib_file = + :rustler_precompiled + |> Application.app_dir("priv/native") + |> Path.join(Path.rootname(metadata.file_name, ".tar.gz")) + + File.mkdir_p!(Path.dirname(lib_file)) + File.write!(lib_file, "old NIF") + + File.open!(lib_file, [:read, :binary], fn file -> + assert {:ok, _} = RustlerPrecompiled.download_or_reuse_nif_file(config, metadata) + assert IO.binread(file, :eof) == "old NIF" + refute File.read!(lib_file) == "old NIF" + end) + + File.rm!(metadata.cached_tar_gz) + File.write!(lib_file, "old NIF") + + File.open!(lib_file, [:read, :binary], fn file -> + assert {:ok, _} = RustlerPrecompiled.download_or_reuse_nif_file(config, metadata) + assert IO.binread(file, :eof) == "old NIF" + refute File.read!(lib_file) == "old NIF" + end) + end) + end) + end + @tag :tmp_dir test "a project downloading precompiled NIFs with custom header", %{ tmp_dir: tmp_dir,