From 1a2e4f1197f70215e95ca34759ff493f12a8044e Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Mon, 25 Sep 2023 09:07:38 -0400 Subject: [PATCH] Improve incompatible version errors on boot This commit makes a few improvements to the errors shown when Lexical boots with incompatible Elixir/Erlang versions: * Consistent formatting for all errors * Prevent an exception that would occur if using an incompatible major version (e.g. Elixir 1.12) * If both Elixir and Erlang are incompatible, show both errors * Show full range of compatible versions instead of just the range for the current major version --- apps/common/lib/lexical/vm/versions.ex | 8 +- apps/server/lib/lexical/server/boot.ex | 93 +++++++++++-------- apps/server/test/lexical/server/boot_test.exs | 68 ++++++++++++++ 3 files changed, 124 insertions(+), 45 deletions(-) create mode 100644 apps/server/test/lexical/server/boot_test.exs diff --git a/apps/common/lib/lexical/vm/versions.ex b/apps/common/lib/lexical/vm/versions.ex index 32e305280..bc31f24ae 100644 --- a/apps/common/lib/lexical/vm/versions.ex +++ b/apps/common/lib/lexical/vm/versions.ex @@ -196,20 +196,16 @@ defmodule Lexical.VM.Versions do Enum.join(normalized, ".") end - require Logger - defp code_find_file(file_name) when is_binary(file_name) do file_name |> String.to_charlist() |> code_find_file() end - defp code_find_file(file_name) do - Logger.info("file name is #{file_name}") - + defp code_find_file(file_name) when is_list(file_name) do case :code.where_is_file(file_name) do :non_existing -> - :error + {:error, {:file_missing, file_name}} path -> {:ok, List.to_string(path)} diff --git a/apps/server/lib/lexical/server/boot.ex b/apps/server/lib/lexical/server/boot.ex index 07741ca93..e45828016 100644 --- a/apps/server/lib/lexical/server/boot.ex +++ b/apps/server/lib/lexical/server/boot.ex @@ -16,16 +16,31 @@ defmodule Lexical.Server.Boot do def start do {:ok, _} = Application.ensure_all_started(:mix) + Application.stop(:logger) load_config() Application.ensure_all_started(:logger) Enum.each(@dep_apps, &load_app_modules/1) - verify_packaging() - verify_versioning() + + case detect_errors() do + [] -> + :ok + + errors -> + errors + |> Enum.join("\n\n") + |> halt() + end + Application.ensure_all_started(:server) end + @doc false + def detect_errors do + List.wrap(packaging_errors()) ++ List.wrap(versioning_errors()) + end + defp load_config do config = read_config("config.exs") runtime = read_config("runtime.exs") @@ -68,7 +83,7 @@ defmodule Lexical.Server.Boot do end end - defp verify_packaging do + defp packaging_errors do unless Versions.compatible?() do {:ok, compiled_versions} = Versions.compiled() @@ -78,21 +93,17 @@ defmodule Lexical.Server.Boot do compiled_erlang = compiled_versions.erlang current_erlang = current_versions.erlang - message = """ - Lexical failed its version check. This is a FATAL Error! - Lexical is running on Erlang #{current_erlang} and the compiled files were built on - Erlang #{compiled_erlang}. - - If you wish to run Lexical under Erlang version #{current_erlang}, you must rebuild lexical - under an Erlang version that is <= #{current_erlang.major}. - - Detected Lexical running on erlang #{current_erlang.major} and needs >= #{compiled_erlang.major} """ + FATAL: Lexical version check failed - halt(message) + The Lexical release being used must be compiled with a major version + of Erlang/OTP that is older or equal to the runtime. - Process.sleep(500) - System.halt() + Compiled with: #{compiled_erlang} + Started with: #{current_erlang} + + To run Lexical with #{current_erlang}, please recompile using Erlang/OTP <= #{current_erlang.major}. + """ end end @@ -107,49 +118,53 @@ defmodule Lexical.Server.Boot do "26" => ">= 26.0.2" } - defp verify_versioning do + defp versioning_errors do versions = Versions.to_versions(Versions.current()) elixir_base = to_string(%Version{versions.elixir | patch: 0}) erlang_base = to_string(versions.erlang.major) - detected_elixir_range = Map.get(@allowed_elixir, elixir_base) - detected_erlang_range = Map.get(@allowed_erlang, erlang_base) - - elixir_ok? = Version.match?(versions.elixir, detected_elixir_range) - erlang_ok? = Version.match?(versions.erlang, detected_erlang_range) + detected_elixir_range = Map.get(@allowed_elixir, elixir_base, false) + detected_erlang_range = Map.get(@allowed_erlang, erlang_base, false) - cond do - not elixir_ok? -> - message = """ - The version of elixir lexical found (#{versions.elixir}) is not compatible with lexical, - and lexical can't start. + elixir_ok? = detected_elixir_range && Version.match?(versions.elixir, detected_elixir_range) + erlang_ok? = detected_erlang_range && Version.match?(versions.erlang, detected_erlang_range) - Please change your version of elixir to #{detected_elixir_range} + errors = [ + unless elixir_ok? do """ + FATAL: Lexical is not compatible with Elixir #{versions.elixir} - halt(message) + Lexical is compatible with the following versions of Elixir: - not erlang_ok? -> - message = """ - The version of erlang lexical found (#{versions.erlang}) is not compatible with lexical, - and lexical can't start. + #{format_allowed_versions(@allowed_elixir)} + """ + end, + unless erlang_ok? do + """ + FATAL: Lexical is not compatible with Erlang/OTP #{versions.erlang} - Please change your version of erlang to one of the following: #{detected_erlang_range} + Lexical is compatible with the following versions of Erlang/OTP: + + #{format_allowed_versions(@allowed_erlang)} """ + end + ] - halt(message) + Enum.filter(errors, &Function.identity/1) + end - true -> - :ok - end + defp format_allowed_versions(%{} = versions) do + versions + |> Map.values() + |> Enum.sort() + |> Enum.map_join("\n", fn range -> " #{range}" end) end defp halt(message) do Mix.Shell.IO.error(message) Logger.emergency(message) - # Wait for the logs to flush - Process.sleep(500) + Logger.flush() System.halt() end end diff --git a/apps/server/test/lexical/server/boot_test.exs b/apps/server/test/lexical/server/boot_test.exs new file mode 100644 index 000000000..796367151 --- /dev/null +++ b/apps/server/test/lexical/server/boot_test.exs @@ -0,0 +1,68 @@ +defmodule Lexical.Server.BootTest do + alias Lexical.Server.Boot + alias Lexical.VM.Versions + + use ExUnit.Case + use Patch + + describe "detect_errors/0" do + test "returns empty list when all checks succeed" do + patch_runtime_versions("1.14.5", "25.0") + patch_compiled_versions("1.14.5", "25.0") + + assert [] = Boot.detect_errors() + end + + test "includes error when compiled erlang is too new" do + patch_runtime_versions("1.14.5", "25.0") + patch_compiled_versions("1.14.5", "26.1") + + assert [error] = Boot.detect_errors() + assert error =~ "FATAL: Lexical version check failed" + assert error =~ "Compiled with: 26.1" + assert error =~ "Started with: 25.0" + end + + test "includes error when runtime elixir is incompatible" do + patch_runtime_versions("1.12.0", "24.3.4") + patch_compiled_versions("1.13.4", "24.3.4") + + assert [error] = Boot.detect_errors() + assert error =~ "FATAL: Lexical is not compatible with Elixir 1.12.0" + end + + test "includes error when runtime erlang is incompatible" do + patch_runtime_versions("1.13.4", "23.0") + patch_compiled_versions("1.13.4", "23.0") + + assert [error] = Boot.detect_errors() + assert error =~ "FATAL: Lexical is not compatible with Erlang/OTP 23.0.0" + end + + test "includes multiple errors when runtime elixir and erlang are incompatible" do + patch_runtime_versions("1.15.2", "26.0.0") + patch_compiled_versions("1.15.6", "26.1") + + assert [elixir_error, erlang_error] = Boot.detect_errors() + assert elixir_error =~ "FATAL: Lexical is not compatible with Elixir 1.15.2" + assert erlang_error =~ "FATAL: Lexical is not compatible with Erlang/OTP 26.0.0" + end + end + + defp patch_runtime_versions(elixir, erlang) do + patch(Versions, :elixir_version, elixir) + patch(Versions, :erlang_version, erlang) + end + + defp patch_compiled_versions(elixir, erlang) do + patch(Versions, :code_find_file, fn file -> {:ok, file} end) + + patch(Versions, :read_file, fn file -> + if String.ends_with?(file, ".elixir") do + {:ok, elixir} + else + {:ok, erlang} + end + end) + end +end