From fea0b2172291ce1fe63d46119c3c0664aac0f12a Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Fri, 19 Jul 2024 13:02:56 -0400 Subject: [PATCH 1/2] Ensure the remote control node and server use the same Elixir executable Closes #776. --- .../lib/lexical/remote_control.ex | 86 ------------------- .../lib/lexical/remote_control/port.ex | 10 +-- apps/server/lib/lexical/server/boot.ex | 20 ++++- .../lib/lexical/server/configuration.ex | 3 +- bin/boot.exs | 2 +- bin/start_lexical.sh | 3 +- .../lexical_shared/lib/lexical/project.ex | 21 +++-- 7 files changed, 40 insertions(+), 105 deletions(-) diff --git a/apps/remote_control/lib/lexical/remote_control.ex b/apps/remote_control/lib/lexical/remote_control.ex index e8b776b25..5cf125849 100644 --- a/apps/remote_control/lib/lexical/remote_control.ex +++ b/apps/remote_control/lib/lexical/remote_control.ex @@ -125,25 +125,6 @@ defmodule Lexical.RemoteControl do end end - def elixir_executable(%Project{} = project) do - root_path = Project.root_path(project) - - {path_result, env} = - with nil <- version_manager_path_and_env("asdf", root_path), - nil <- version_manager_path_and_env("mise", root_path), - nil <- version_manager_path_and_env("rtx", root_path) do - {File.cd!(root_path, fn -> System.find_executable("elixir") end), System.get_env()} - end - - case path_result do - nil -> - {:error, :no_elixir} - - executable when is_binary(executable) -> - {:ok, executable, env} - end - end - defp app_globs do app_globs = Enum.map(@allowed_apps, fn app_name -> "/**/#{app_name}*/ebin" end) ["/**/priv" | app_globs] @@ -158,71 +139,4 @@ defmodule Lexical.RemoteControl do {:error, :epmd_failed} end end - - defp version_manager_path_and_env(manager, root_path) do - with true <- is_binary(System.find_executable(manager)), - env = reset_env(manager, root_path), - {path, 0} <- System.cmd(manager, ~w(which elixir), cd: root_path, env: env) do - {String.trim(path), env} - else - _ -> - nil - end - end - - # We launch lexical by asking the version managers to provide an environment, - # which contains path munging. This initial environment is present in the running - # VM, and needs to be undone so we can find the correct elixir executable in the project. - defp reset_env("asdf", _root_path) do - orig_path = System.get_env("PATH_SAVE", System.get_env("PATH")) - - Enum.map(System.get_env(), fn - {"ASDF_ELIXIR_VERSION", _} -> {"ASDF_ELIXIR_VERSION", nil} - {"ASDF_ERLANG_VERSION", _} -> {"ASDF_ERLANG_VERSION", nil} - {"PATH", _} -> {"PATH", orig_path} - other -> other - end) - end - - defp reset_env("rtx", root_path) do - {env, _} = System.cmd("rtx", ~w(env -s bash), cd: root_path) - - env - |> String.trim() - |> String.split("\n") - |> Enum.map(fn - "export " <> key_and_value -> - [key, value] = - key_and_value - |> String.split("=", parts: 2) - |> Enum.map(&String.trim/1) - - {key, value} - - _ -> - nil - end) - |> Enum.reject(&is_nil/1) - end - - defp reset_env("mise", root_path) do - {env, _} = System.cmd("mise", ~w(env -s bash), cd: root_path) - - env - |> String.trim() - |> String.split("\n") - |> Enum.map(fn - "export " <> key_and_value -> - [key, value] = - key_and_value - |> String.split("=", parts: 2) - |> Enum.map(&String.trim/1) - - {key, value} - - _ -> - nil - end) - |> Enum.reject(&is_nil/1) - end end diff --git a/apps/remote_control/lib/lexical/remote_control/port.ex b/apps/remote_control/lib/lexical/remote_control/port.ex index 05a210dea..c0cce068d 100644 --- a/apps/remote_control/lib/lexical/remote_control/port.ex +++ b/apps/remote_control/lib/lexical/remote_control/port.ex @@ -4,7 +4,6 @@ defmodule Lexical.RemoteControl.Port do """ alias Lexical.Project - alias Lexical.RemoteControl @type open_opt :: {:env, list()} @@ -22,14 +21,9 @@ defmodule Lexical.RemoteControl.Port do """ @spec open_elixir(Project.t(), open_opts()) :: port() def open_elixir(%Project{} = project, opts) do - {:ok, elixir_executable, environment_variables} = RemoteControl.elixir_executable(project) + opts = Keyword.put_new_lazy(opts, :cd, fn -> Project.root_path(project) end) - opts = - opts - |> Keyword.put_new_lazy(:cd, fn -> Project.root_path(project) end) - |> Keyword.put_new(:env, environment_variables) - - open(project, elixir_executable, opts) + open(project, project.elixir_executable, opts) end @doc """ diff --git a/apps/server/lib/lexical/server/boot.ex b/apps/server/lib/lexical/server/boot.ex index 2a5df77cc..3a970457c 100644 --- a/apps/server/lib/lexical/server/boot.ex +++ b/apps/server/lib/lexical/server/boot.ex @@ -15,13 +15,21 @@ defmodule Lexical.Server.Boot do @target Mix.target() @dep_apps Enum.map(Mix.Dep.cached(), & &1.app) - def start do + def start(argv) do {:ok, _} = Application.ensure_all_started(:mix) Application.stop(:logger) load_config() Application.ensure_all_started(:logger) + case fetch_elixir_executable(argv) do + {:ok, exe} -> + Application.put_env(:server, :elixir_executable, exe) + + :error -> + halt("FATAL: Lexical must be passed an Elixir executable path on boot") + end + Enum.each(@dep_apps, &load_app_modules/1) case detect_errors() do @@ -42,6 +50,16 @@ defmodule Lexical.Server.Boot do versioning_errors() end + defp fetch_elixir_executable(argv) do + case get_elixir_executable(argv) do + nil -> :error + exe -> {:ok, exe} + end + end + + defp get_elixir_executable([maybe_exe | _]), do: System.find_executable(maybe_exe) + defp get_elixir_executable([]), do: nil + defp load_config do config = read_config("config.exs") runtime = read_config("runtime.exs") diff --git a/apps/server/lib/lexical/server/configuration.ex b/apps/server/lib/lexical/server/configuration.ex index cb0dc1d60..4cedbbf6e 100644 --- a/apps/server/lib/lexical/server/configuration.ex +++ b/apps/server/lib/lexical/server/configuration.ex @@ -34,7 +34,8 @@ defmodule Lexical.Server.Configuration do @spec new(Lexical.uri(), map(), String.t() | nil) :: t def new(root_uri, %ClientCapabilities{} = client_capabilities, client_name) do support = Support.new(client_capabilities) - project = Project.new(root_uri) + elixir_executable = Application.get_env(:server, :elixir_executable) + project = Project.new(root_uri, elixir_executable: elixir_executable) %__MODULE__{support: support, project: project, client_name: client_name} |> tap(&set/1) diff --git a/bin/boot.exs b/bin/boot.exs index 031a6e985..aaad712ea 100644 --- a/bin/boot.exs +++ b/bin/boot.exs @@ -20,7 +20,7 @@ end) |> Code.append_path() end) -LXical.Server.Boot.start() +LXical.Server.Boot.start(System.argv()) if System.get_env("LX_HALT_AFTER_BOOT") do require Logger diff --git a/bin/start_lexical.sh b/bin/start_lexical.sh index 4b29a39ba..07f831e3f 100755 --- a/bin/start_lexical.sh +++ b/bin/start_lexical.sh @@ -20,4 +20,5 @@ esac $elixir_command \ --cookie "lexical" \ --no-halt \ - "$script_dir/boot.exs" + "$script_dir/boot.exs" \ + "$(which $elixir_command)" diff --git a/projects/lexical_shared/lib/lexical/project.ex b/projects/lexical_shared/lib/lexical/project.ex index 054d172f5..ee9804707 100644 --- a/projects/lexical_shared/lib/lexical/project.ex +++ b/projects/lexical_shared/lib/lexical/project.ex @@ -14,7 +14,8 @@ defmodule Lexical.Project do mix_target: nil, env_variables: %{}, project_module: nil, - entropy: 1 + entropy: 1, + elixir_executable: nil @type message :: String.t() @type restart_notification :: {:restart, Logger.level(), String.t()} @@ -30,14 +31,15 @@ defmodule Lexical.Project do @workspace_directory_name ".lexical" - # Public - @spec new(Lexical.uri()) :: t - def new(root_uri) do + @spec new(Lexical.uri(), attrs :: keyword()) :: t + def new(root_uri, attrs \\ []) do entropy = :rand.uniform(65_536) - %__MODULE__{entropy: entropy} + __MODULE__ + |> struct!([entropy: entropy] ++ attrs) |> maybe_set_root_uri(root_uri) |> maybe_set_mix_exs_uri() + |> maybe_set_elixir_executable() end @spec set_project_module(t(), module() | nil) :: t() @@ -237,8 +239,7 @@ defmodule Lexical.Project do end end - defp maybe_set_root_uri(%__MODULE__{} = project, nil), - do: %__MODULE__{project | root_uri: nil} + defp maybe_set_root_uri(%__MODULE__{} = project, nil), do: project defp maybe_set_root_uri(%__MODULE__{} = project, "file://" <> _ = root_uri) do root_path = @@ -271,6 +272,12 @@ defmodule Lexical.Project do end end + defp maybe_set_elixir_executable(%__MODULE__{elixir_executable: nil} = project) do + %__MODULE__{project | elixir_executable: System.find_executable("elixir")} + end + + defp maybe_set_elixir_executable(%__MODULE__{} = project), do: project + # Project Path # Environment variables From 70e5d091950673ca3d659cd58bc633bbc29d3f10 Mon Sep 17 00:00:00 2001 From: Zach Allaun Date: Fri, 19 Jul 2024 12:34:07 -0400 Subject: [PATCH 2/2] Fix mise setup in integration test Erlang is now a core plugin, so it doesn't need to (and can't) be installed separately. --- integration/boot/set_up_mise.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/integration/boot/set_up_mise.sh b/integration/boot/set_up_mise.sh index 87783bb65..17d23e0e3 100755 --- a/integration/boot/set_up_mise.sh +++ b/integration/boot/set_up_mise.sh @@ -27,7 +27,6 @@ chmod +x ./mise eval "$(./mise activate bash)" export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac --without-termcap --without-wx" -./mise plugin install -y erlang ./mise use --global "erlang@$ERLANG_VERSION" ./mise plugins install -y elixir