Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions apps/expert/lib/expert/port.ex
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ defmodule Expert.Port do

_ ->
cmd = "cd #{directory} && printf \"#{@path_marker}:%s:#{@path_marker}\" \"$PATH\""
["-i", "-l", "-c", cmd]
path_lookup_args(cmd)
end

{output, exit_code} = System.cmd(shell, args, env: env)
Expand All @@ -288,7 +288,21 @@ defmodule Expert.Port do
clean_path

_ ->
output |> String.trim() |> String.split("\n") |> List.last()
output
|> String.trim()
|> String.split("\n")
|> List.last()
end
end

defp path_lookup_args(cmd) do
case System.get_env("EXPERT_PATH_SHELL_MODE", "login") do
# Some users configure version managers or PATH mutations only in
# interactive shell startup files. Keep an escape hatch for those
# environments, but default to non-interactive login shells so LSP
# startup avoids shell UI side effects like zle/fzf or job-control noise.
"interactive" -> ["-i", "-l", "-c", cmd]
_ -> ["-l", "-c", cmd]
end
end

Expand Down
85 changes: 85 additions & 0 deletions apps/expert/test/expert/port_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
defmodule Expert.PortTest do
use ExUnit.Case, async: false

alias Expert.Port
alias Forge.Test.Fixtures

setup do
original_env = %{
"SHELL" => System.get_env("SHELL"),
"EXPERT_PATH_SHELL_MODE" => System.get_env("EXPERT_PATH_SHELL_MODE"),
"EXPERT_ARGS_FILE" => System.get_env("EXPERT_ARGS_FILE")
}

on_exit(fn ->
Enum.each(original_env, fn {key, value} ->
if value do
System.put_env(key, value)
else
System.delete_env(key)
end
end)
end)

:ok
end

if match?({:unix, _}, :os.type()) do
@tag :tmp_dir
test "uses a non-interactive login shell by default", %{tmp_dir: tmp_dir} do
args = capture_lookup_args(tmp_dir)

assert ["-l", "-c", _cmd] = args
end

@tag :tmp_dir
test "supports interactive shell lookup as an opt-in", %{tmp_dir: tmp_dir} do
System.put_env("EXPERT_PATH_SHELL_MODE", "interactive")

args = capture_lookup_args(tmp_dir)

assert ["-i", "-l", "-c", _cmd] = args
end

@tag :tmp_dir
test "falls back to non-interactive login shell for unknown shell mode", %{tmp_dir: tmp_dir} do
System.put_env("EXPERT_PATH_SHELL_MODE", "bogus")

args = capture_lookup_args(tmp_dir)

assert ["-l", "-c", _cmd] = args
end
end

defp capture_lookup_args(tmp_dir) do
args_file = Path.join(tmp_dir, "shell-args.txt")
shell_path = write_shell_probe(tmp_dir)

System.put_env("EXPERT_ARGS_FILE", args_file)
System.put_env("SHELL", shell_path)

project = Fixtures.project()

assert {:ok, _elixir, _env} = Port.find_project_executable(project, "elixir")

args_file
|> File.read!()
|> String.split("\n", trim: true)
end

defp write_shell_probe(tmp_dir) do
shell_path = Path.join(tmp_dir, "zsh")

File.write!(
shell_path,
"""
#!/bin/sh
printf '%s\\n' "$@" > "$EXPERT_ARGS_FILE"
printf '__EXPERT_PATH__:%s:__EXPERT_PATH__' "$PATH"
"""
)

File.chmod!(shell_path, 0o755)
shell_path
end
end