From 8e960c1ed5663839e683151379ac1f6104cbee7a Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Mon, 28 Sep 2026 14:30:27 -0400 Subject: [PATCH] fix: replacement of existing NIF files Commit 051306b74c91 changed metadata.file_name to include .tar.gz, so File.rm/1 targeted the wrong path and its error was ignored. Strip .tar.gz from the removal path and remove the existing library before extracting cached or downloaded artifacts. This prevents crashes when replacing a NIF that is already loaded. --- lib/rustler_precompiled.ex | 22 ++++++++--- test/rustler_precompiled_test.exs | 63 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 6 deletions(-) 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,