Skip to content

Commit

Permalink
Improve incompatible version errors on boot
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zachallaun committed Sep 25, 2023
1 parent ef0a427 commit 1a2e4f1
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 45 deletions.
8 changes: 2 additions & 6 deletions apps/common/lib/lexical/vm/versions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
93 changes: 54 additions & 39 deletions apps/server/lib/lexical/server/boot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()

Expand All @@ -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

Expand All @@ -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
68 changes: 68 additions & 0 deletions apps/server/test/lexical/server/boot_test.exs
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 1a2e4f1

Please sign in to comment.