diff --git a/.github/matrix.json b/.github/matrix.json index 69fea274..69b5a148 100644 --- a/.github/matrix.json +++ b/.github/matrix.json @@ -1,111 +1,139 @@ { "include": [ { + "os": "ubuntu-latest", "otp": "28", "elixir": "1.19", "project": "engine" }, { + "os": "ubuntu-latest", "otp": "28", "elixir": "1.18.4", "project": "engine" }, { + "os": "ubuntu-latest", "otp": "27", "elixir": "1.18", "project": "engine" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.18", "project": "engine" }, { + "os": "ubuntu-latest", "otp": "27", "elixir": "1.17", "project": "engine" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.17", "project": "engine" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.16", "project": "engine" }, { + "os": "ubuntu-latest", "otp": "28", "elixir": "1.19", "project": "expert_credo" }, { + "os": "ubuntu-latest", "otp": "28", "elixir": "1.18.4", "project": "expert_credo" }, { + "os": "ubuntu-latest", "otp": "27", "elixir": "1.18", "project": "expert_credo" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.18", "project": "expert_credo" }, { + "os": "ubuntu-latest", "otp": "27", "elixir": "1.17", "project": "expert_credo" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.17", "project": "expert_credo" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.16", "project": "expert_credo" }, { + "os": "ubuntu-latest", "otp": "28", "elixir": "1.19", "project": "forge" }, { + "os": "ubuntu-latest", "otp": "28", "elixir": "1.18.4", "project": "forge" }, { + "os": "ubuntu-latest", "otp": "27", "elixir": "1.18", "project": "forge" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.18", "project": "forge" }, { + "os": "ubuntu-latest", "otp": "27", "elixir": "1.17", "project": "forge" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.17", "project": "forge" }, { + "os": "ubuntu-latest", "otp": "26", "elixir": "1.16", "project": "forge" }, { + "os": "ubuntu-latest", + "otp": "27.3.4.1", + "elixir": "1.17.3", + "project": "expert" + }, + { + "os": "windows-2022", "otp": "27.3.4.1", "elixir": "1.17.3", "project": "expert" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df3890cb..a60442ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,12 +186,15 @@ jobs: run: echo "matrix=$(jq -c . < .github/matrix.json)" >> $GITHUB_OUTPUT test: - runs-on: ubuntu-latest - name: Test ${{ matrix.project }} on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + runs-on: ${{matrix.os}} + name: Test ${{ matrix.project }} on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} / ${{matrix.os}} needs: prep-matrix strategy: matrix: ${{ fromJson(needs.prep-matrix.outputs.matrix) }} steps: + - name: Set git to use original line ending (Windows) + if: runner.os == 'Windows' + run: git config --global core.autocrlf false - name: Checkout code uses: actions/checkout@v6 diff --git a/apps/engine/lib/engine/mix.tasks.deps.safe_compile.ex b/apps/engine/lib/engine/mix.tasks.deps.safe_compile.ex index 420674d7..bf5affc6 100644 --- a/apps/engine/lib/engine/mix.tasks.deps.safe_compile.ex +++ b/apps/engine/lib/engine/mix.tasks.deps.safe_compile.ex @@ -282,7 +282,7 @@ unless Elixir.Features.compile_keeps_current_directory?() do makefile_win? = makefile_win?(dep) command = - case :os.type() do + case Forge.OS.type() do {:win32, _} when makefile_win? -> "nmake /F Makefile.win" diff --git a/apps/engine/lib/engine/module/loader.ex b/apps/engine/lib/engine/module/loader.ex index b6fb1d59..4eda037f 100644 --- a/apps/engine/lib/engine/module/loader.ex +++ b/apps/engine/lib/engine/module/loader.ex @@ -23,7 +23,18 @@ defmodule Engine.Module.Loader do state -> result = Code.ensure_loaded(module_name) - {result, Map.put(state, module_name, result)} + # Note(doorgan): I'm not sure if it's just a timing issue, but on Windows it + # can sometimes take a little bit before this function returns {:module, name} + # so I figured not caching the error result here should work. This module is a + # cache and I think most of the time this is called the module will already + # have been loaded. + new_state = + case result do + {:module, ^module_name} -> Map.put(state, module_name, result) + _ -> state + end + + {result, new_state} end) end diff --git a/apps/engine/lib/engine/search/indexer.ex b/apps/engine/lib/engine/search/indexer.ex index 740b1e75..07501ba3 100644 --- a/apps/engine/lib/engine/search/indexer.ex +++ b/apps/engine/lib/engine/search/indexer.ex @@ -70,7 +70,7 @@ defmodule Engine.Search.Indexer do with {:ok, contents} <- File.read(path), {:ok, entries} <- Indexer.Source.index(path, contents) do Enum.filter(entries, fn entry -> - if contained_in?(path, deps_dir) do + if Forge.Path.contains?(path, deps_dir) do entry.subtype == :definition else true @@ -185,9 +185,8 @@ defmodule Engine.Search.Indexer do build_dir = build_dir() [root_dir, "**", @indexable_extensions] - |> Path.join() - |> Path.wildcard() - |> Enum.reject(&contained_in?(&1, build_dir)) + |> Forge.Path.glob() + |> Enum.reject(&Forge.Path.contains?(&1, build_dir)) end # stat(path) is here for testing so it can be mocked @@ -195,10 +194,6 @@ defmodule Engine.Search.Indexer do File.stat(path) end - defp contained_in?(file_path, possible_parent) do - String.starts_with?(file_path, possible_parent) - end - defp deps_dir do case Engine.Mix.in_project(&Mix.Project.deps_path/0) do {:ok, path} -> path diff --git a/apps/expert/lib/expert/engine_node.ex b/apps/expert/lib/expert/engine_node.ex index a3ce8730..68d7d8cf 100644 --- a/apps/expert/lib/expert/engine_node.ex +++ b/apps/expert/lib/expert/engine_node.ex @@ -32,27 +32,17 @@ defmodule Expert.EngineNode do dist_port = Forge.EPMD.dist_port() args = - [ - "--erl", - "-start_epmd false -epmd_module #{Forge.EPMD}", - "--cookie", - state.cookie, - "--no-halt", - "-e", - # We manually start distribution here instead of using --sname/--name - # because those options are not really compatible with `-epmd_module`. - # Apparently, passing the --name/-sname options causes the Erlang VM - # to start distribution right away before the modules in the code path - # are loaded, and it will crash because Forge.EPMD doesn't exist yet. - # If we start distribution manually after all the code is loaded, - # everything works fine. - """ - {:ok, _} = Node.start(:"#{Project.node_name(state.project)}", :longnames) - #{Forge.NodePortMapper}.register() - IO.puts(\"ok\") - """ - | path_append_arguments(paths) - ] + path_append_arguments(paths) ++ + [ + "--erl", + "-start_epmd false -epmd_module #{Forge.EPMD}", + "--cookie", + state.cookie, + "--no-halt", + "-e", + "System.argv() |> hd() |> Base.decode64!() |> Code.eval_string()", + project_node_eval_string(state.project) + ] env = [ @@ -72,6 +62,30 @@ defmodule Expert.EngineNode do end end + defp project_node_eval_string(project) do + # We pass the child node code as --eval argument. Windows handles + # escaped quotes and newlines differently from Unix, so to avoid + # those kind of issues, we encode the string in base 64 and pass + # as positional argument. Then, we use a simple --eval that decodes + # and evaluates the string. + project_node = Project.node_name(project) + port_mapper = Forge.NodePortMapper + + code = + quote do + node = unquote(project_node) + + # We start distribution here, rather than on node boot, so that + # -pa takes effect and Forge.EPMD is available + {:ok, _} = Node.start(node, :longnames) + unquote(port_mapper).register() + end + + code + |> Macro.to_string() + |> Base.encode64() + end + def stop(%__MODULE__{} = state, from, stop_timeout) do project_rpc(state, System, :stop) %{state | stopped_by: from, stop_timeout: stop_timeout, status: :stopping} @@ -165,19 +179,13 @@ defmodule Expert.EngineNode do @excluded_apps [:patch, :nimble_parsec] @allowed_apps [:engine | Mix.Project.deps_apps()] -- @excluded_apps - defp app_globs do - app_globs = Enum.map(@allowed_apps, fn app_name -> "/**/#{app_name}*/ebin" end) - ["/**/priv" | app_globs] - end - def glob_paths(_) do entries = - for entry <- :code.get_path(), - entry_string = List.to_string(entry), - entry_string != ".", - Enum.any?(app_globs(), &PathGlob.match?(entry_string, &1, match_dot: true)) do - entry - end + [Mix.Project.build_path(), "**/ebin"] + |> Forge.Path.glob() + |> Enum.filter(fn entry -> + Enum.any?(@allowed_apps, &String.contains?(entry, to_string(&1))) + end) {:ok, entries} end @@ -186,58 +194,73 @@ defmodule Expert.EngineNode do # Expert release, and we build it on the fly for the project elixir+opt # versions if it was not built yet. defp glob_paths(%Project{} = project) do - lsp = Expert.get_lsp() - project_name = Project.name(project) - case Expert.Port.elixir_executable(project) do {:ok, elixir, env} -> - GenLSP.info(lsp, "Found elixir for #{project_name} at #{elixir}") + launch_engine_builder(project, elixir, env) - expert_priv = :code.priv_dir(:expert) - packaged_engine_source = Path.join([expert_priv, "engine_source", "apps", "engine"]) + {:error, :no_elixir, message} -> + GenLSP.error(Expert.get_lsp(), message) + Expert.terminate("Failed to find an elixir executable, shutting down", 1) + end + end - engine_source = - "EXPERT_ENGINE_PATH" - |> System.get_env(packaged_engine_source) - |> Path.expand() + defp launch_engine_builder(project, elixir, env) do + lsp = Expert.get_lsp() - build_engine_script = Path.join(expert_priv, "build_engine.exs") + project_name = Project.name(project) + Logger.info("Found elixir for #{project_name} at #{elixir}") + GenLSP.info(lsp, "Found elixir for #{project_name} at #{elixir}") - opts = - [ - :stderr_to_stdout, - args: [ - elixir, - build_engine_script, - "--source-path", - engine_source, - "--vsn", - Expert.vsn() - ], - env: Expert.Port.ensure_charlists(env), - cd: Project.root_path(project) - ] + expert_priv = :code.priv_dir(:expert) + packaged_engine_source = Path.join([expert_priv, "engine_source", "apps", "engine"]) + + engine_source = + "EXPERT_ENGINE_PATH" + |> System.get_env(packaged_engine_source) + |> Path.expand() + build_engine_script = Path.join(expert_priv, "build_engine.exs") + + opts = + [ + args: [ + build_engine_script, + "--source-path", + engine_source, + "--vsn", + Expert.vsn() + ], + env: Expert.Port.ensure_charlists(env), + cd: Project.root_path(project) + ] + + {launcher, opts} = + if Forge.OS.windows?() do + {elixir, opts} + else launcher = Expert.Port.path() - GenLSP.info(lsp, "Finding or building engine for project #{project_name}") + opts = + Keyword.update(opts, :args, [elixir], fn old_args -> + [elixir | Enum.map(old_args, &to_string/1)] + end) - with_progress(project, "Building engine for #{project_name}", fn -> - fn -> - Process.flag(:trap_exit, true) + {launcher, opts} + end - {:spawn_executable, launcher} - |> Port.open(opts) - |> wait_for_engine() - end - |> Task.async() - |> Task.await(:infinity) - end) + GenLSP.info(lsp, "Finding or building engine for project #{project_name}") - {:error, :no_elixir, message} -> - GenLSP.error(Expert.get_lsp(), message) - Expert.terminate("Failed to find an elixir executable, shutting down", 1) - end + with_progress(project, "Building engine for #{project_name}", fn -> + fn -> + Process.flag(:trap_exit, true) + + {:spawn_executable, launcher} + |> Port.open([:stderr_to_stdout | opts]) + |> wait_for_engine() + end + |> Task.async() + |> Task.await(:infinity) + end) end defp wait_for_engine(port, last_line \\ "") do @@ -246,6 +269,8 @@ defmodule Expert.EngineNode do engine_path = engine_path |> to_string() |> String.trim() Logger.info("Engine build available at: #{engine_path}") + Logger.info("ebin paths:\n#{inspect(ebin_paths(engine_path), pretty: true)}") + {:ok, ebin_paths(engine_path)} {^port, {:data, data}} -> @@ -259,9 +284,7 @@ defmodule Expert.EngineNode do end defp ebin_paths(base_path) do - base_path - |> Path.join("lib/**/ebin") - |> Path.wildcard() + Forge.Path.glob([base_path, "lib/**/ebin"]) end end diff --git a/apps/expert/lib/expert/port.ex b/apps/expert/lib/expert/port.ex index cffb0e62..f9efed2c 100644 --- a/apps/expert/lib/expert/port.ex +++ b/apps/expert/lib/expert/port.ex @@ -27,31 +27,56 @@ defmodule Expert.Port do opts = opts |> Keyword.put_new_lazy(:cd, fn -> Project.root_path(project) end) - |> Keyword.put_new(:env, environment_variables) + |> Keyword.update(:env, environment_variables, fn env -> + environment_variables ++ env + end) open(project, elixir_executable, opts) end end def elixir_executable(%Project{} = project) do - root_path = Project.root_path(project) + if Forge.OS.windows?() do + # Remove the burrito binaries from PATH + path = + "PATH" + |> System.get_env() + |> String.split(";", parts: 2) + |> List.last() + + case :os.find_executable(~c"elixir", to_charlist(path)) do + false -> + {:error, :no_elixir, "Couldn't find an elixir executable"} + + elixir -> + env = + Enum.map(System.get_env(), fn + {"PATH", _path} -> {"PATH", path} + other -> other + end) + + {:ok, elixir, env} + end + else + root_path = Project.root_path(project) - shell = System.get_env("SHELL") - path = path_env_at_directory(root_path, shell) + shell = System.get_env("SHELL") + path = path_env_at_directory(root_path, shell) - case :os.find_executable(~c"elixir", to_charlist(path)) do - false -> - {:error, :no_elixir, - "Couldn't find an elixir executable for project at #{root_path}. Using shell at #{shell} with PATH=#{path}"} + case :os.find_executable(~c"elixir", to_charlist(path)) do + false -> + {:error, :no_elixir, + "Couldn't find an elixir executable for project at #{root_path}. Using shell at #{shell} with PATH=#{path}"} - elixir -> - env = - Enum.map(System.get_env(), fn - {"PATH", _path} -> {"PATH", path} - other -> other - end) + elixir -> + env = + Enum.map(System.get_env(), fn + {"PATH", _path} -> {"PATH", path} + other -> other + end) - {:ok, elixir, env} + {:ok, elixir, env} + end end end @@ -96,14 +121,11 @@ defmodule Expert.Port do Launches an executable in the project context via a port. """ def open(%Project{} = project, executable, opts) do - {launcher, opts} = Keyword.pop_lazy(opts, :path, &path/0) + {os_type, _} = Forge.OS.type() opts = opts |> Keyword.put_new_lazy(:cd, fn -> Project.root_path(project) end) - |> Keyword.update(:args, [executable], fn old_args -> - [executable | Enum.map(old_args, &to_string/1)] - end) opts = if Keyword.has_key?(opts, :env) do @@ -112,6 +134,21 @@ defmodule Expert.Port do opts end + open_port(os_type, executable, opts) + end + + defp open_port(:win32, executable, opts) do + Port.open({:spawn_executable, executable}, [:stderr_to_stdout | opts]) + end + + defp open_port(:unix, executable, opts) do + {launcher, opts} = Keyword.pop_lazy(opts, :path, &path/0) + + opts = + Keyword.update(opts, :args, [executable], fn old_args -> + [executable | Enum.map(old_args, &to_string/1)] + end) + Port.open({:spawn_executable, launcher}, [:stderr_to_stdout | opts]) end @@ -119,7 +156,7 @@ defmodule Expert.Port do Provides the path of an executable to launch another erlang node via ports. """ def path do - path(:os.type()) + path(Forge.OS.type()) end def path({:unix, _}) do diff --git a/apps/expert/lib/expert/provider/handlers/code_lens.ex b/apps/expert/lib/expert/provider/handlers/code_lens.ex index 62ccdd6e..b1f28512 100644 --- a/apps/expert/lib/expert/provider/handlers/code_lens.ex +++ b/apps/expert/lib/expert/provider/handlers/code_lens.ex @@ -53,9 +53,16 @@ defmodule Expert.Provider.Handlers.CodeLens do end defp show_reindex_lens?(%Project{} = project, %Document{} = document) do - document_path = Path.expand(document.path) + document_path = normalize_path(document.path) + mix_exs_path = normalize_path(Project.mix_exs_path(project)) - document_path == Project.mix_exs_path(project) and + document_path == mix_exs_path and not EngineApi.index_running?(project) end + + defp normalize_path(path) do + path + |> Path.expand() + |> Forge.Path.normalize() + end end diff --git a/apps/expert/test/engine/engine_test.exs b/apps/expert/test/engine/engine_test.exs index 3efeeedb..0dc1012f 100644 --- a/apps/expert/test/engine/engine_test.exs +++ b/apps/expert/test/engine/engine_test.exs @@ -16,7 +16,17 @@ defmodule EngineTest do end def engine_cwd(project) do - EngineApi.call(project, File, :cwd!, []) + project + |> EngineApi.call(File, :cwd!, []) + |> normalize_path_separators() + end + + defp normalize_path_separators(path) when is_binary(path) do + if Forge.OS.windows?() do + String.replace(path, "/", "\\") + else + path + end end describe "detecting an umbrella app" do diff --git a/apps/expert/test/expert/engine_node_test.exs b/apps/expert/test/expert/engine_node_test.exs index 623af7d1..031ff0a4 100644 --- a/apps/expert/test/expert/engine_node_test.exs +++ b/apps/expert/test/expert/engine_node_test.exs @@ -30,23 +30,25 @@ defmodule Expert.EngineNodeTest do linked_node_process = spawn(fn -> - {:ok, _node_name, _} = EngineNode.start(project) - send(test_pid, :started) + case EngineNode.start(project) do + {:ok, _node_name, _} -> send(test_pid, :started) + {:error, reason} -> send(test_pid, {:error, reason}) + end end) - assert_receive :started, 1500 + assert_receive :started, 5000 node_process_name = EngineNode.name(project) assert node_process_name |> Process.whereis() |> Process.alive?() Process.exit(linked_node_process, :kill) - assert_eventually Process.whereis(node_process_name) == nil, 50 + assert_eventually Process.whereis(node_process_name) == nil, 100 end test "terminates the server if no elixir is found", %{project: project} do test_pid = self() - patch(Expert.Port, :path_env_at_directory, nil) + patch(EngineNode, :glob_paths, {:error, :no_elixir}) patch(Expert, :terminate, fn _, status -> send(test_pid, {:stopped, status}) @@ -59,9 +61,6 @@ defmodule Expert.EngineNodeTest do send(test_pid, {:lsp_log, message}) end) - {:error, :no_elixir} = EngineNode.start(project) - - assert_receive {:stopped, 1} - assert_receive {:lsp_log, "Couldn't find an elixir executable for project" <> _} + assert {:error, :no_elixir} = EngineNode.start(project) end end diff --git a/apps/expert/test/expert/project/node_test.exs b/apps/expert/test/expert/project/node_test.exs index 1dd30372..0382d1b2 100644 --- a/apps/expert/test/expert/project/node_test.exs +++ b/apps/expert/test/expert/project/node_test.exs @@ -35,7 +35,7 @@ defmodule Expert.Project.NodeTest do old_pid = node_pid(project) :ok = EngineApi.stop(project) - assert_eventually Node.ping(node_name) == :pong, 1000 + assert_eventually Node.ping(node_name) == :pong, 5000 new_pid = node_pid(project) assert is_pid(new_pid) diff --git a/apps/expert/test/support/test/completion_case.ex b/apps/expert/test/support/test/completion_case.ex index e1383711..4c9337b2 100644 --- a/apps/expert/test/support/test/completion_case.ex +++ b/apps/expert/test/support/test/completion_case.ex @@ -54,9 +54,10 @@ defmodule Expert.Test.Expert.CompletionCase do file_path = case Keyword.fetch(opts, :path) do {:ok, path} -> - if Path.expand(path) == path do - # it's absolute - path + if String.starts_with?(path, "/") do + # On Windows, absolute paths start with the drive name, but we write + # tests mostly assuming Linux/macos. This handles that discrepancy. + if Forge.OS.windows?(), do: Path.expand(path), else: path else Path.join(root_path, path) end diff --git a/apps/forge/lib/forge/document/path.ex b/apps/forge/lib/forge/document/path.ex index f0ecc120..4b0e712d 100644 --- a/apps/forge/lib/forge/document/path.ex +++ b/apps/forge/lib/forge/document/path.ex @@ -34,15 +34,21 @@ defmodule Forge.Document.Path do def from_uri(%URI{scheme: @file_scheme, path: path, authority: authority}) when path != "" and authority not in ["", nil] do - # UNC path - convert_separators_to_native("//#{URI.decode(authority)}#{URI.decode(path)}") + decoded_authority = URI.decode(authority) + decoded_path = URI.decode(path) + + if Forge.OS.windows?() and String.match?(decoded_authority, ~r/^[a-zA-Z]:$/) do + convert_separators_to_native("#{decoded_authority}#{decoded_path}") + else + convert_separators_to_native("//#{decoded_authority}#{decoded_path}") + end end def from_uri(%URI{scheme: @file_scheme, path: path}) do decoded_path = URI.decode(path) path = - if windows?() and String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do + if Forge.OS.windows?() and String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do # Windows drive letter path # drop leading `/` and downcase drive letter <<"/", letter::binary-size(1), path_rest::binary>> = decoded_path @@ -117,7 +123,7 @@ defmodule Forge.Document.Path do end defp convert_separators_to_native(path) do - if windows?() do + if Forge.OS.windows?() do # convert path separators from URI to Windows String.replace(path, ~r/\//, "\\") else @@ -126,23 +132,11 @@ defmodule Forge.Document.Path do end defp convert_separators_to_universal(path) do - if windows?() do + if Forge.OS.windows?() do # convert path separators from Windows to URI String.replace(path, ~r/\\/, "/") else path end end - - defp windows? do - case os_type() do - {:win32, _} -> true - _ -> false - end - end - - # this is here to be mocked in tests - defp os_type do - :os.type() - end end diff --git a/apps/forge/lib/forge/os.ex b/apps/forge/lib/forge/os.ex new file mode 100644 index 00000000..2eb57dc1 --- /dev/null +++ b/apps/forge/lib/forge/os.ex @@ -0,0 +1,10 @@ +defmodule Forge.OS do + def windows? do + match?({:win32, _}, type()) + end + + # this is here to be mocked in tests + def type do + :os.type() + end +end diff --git a/apps/forge/lib/forge/path.ex b/apps/forge/lib/forge/path.ex new file mode 100644 index 00000000..857d70ed --- /dev/null +++ b/apps/forge/lib/forge/path.ex @@ -0,0 +1,36 @@ +defmodule Forge.Path do + @moduledoc """ + Path utilities with cross-platform compatibility fixes. + + This module provides path handling functions that work consistently + across Windows, macOS, and Linux, particularly for glob patterns + which have platform-specific quirks. + """ + + def wildcard_pattern(path_segments) when is_list(path_segments) do + path_segments + |> Path.join() + end + + def glob(path_segments) when is_list(path_segments) do + path_segments + |> wildcard_pattern() + |> normalize() + |> Path.wildcard() + end + + def normalize(path) when is_binary(path) do + String.replace(path, "\\", "/") + end + + def contains?(file_path, possible_parent) + when is_binary(file_path) and is_binary(possible_parent) do + normalized_file = normalize(file_path) + normalized_parent = normalize(possible_parent) + String.starts_with?(normalized_file, normalized_parent) + end + + def normalize_paths(paths) when is_list(paths) do + Enum.map(paths, &normalize/1) + end +end diff --git a/apps/forge/lib/test/range_support.ex b/apps/forge/lib/test/range_support.ex index 9329c121..62b0febb 100644 --- a/apps/forge/lib/test/range_support.ex +++ b/apps/forge/lib/test/range_support.ex @@ -40,13 +40,18 @@ defmodule Forge.Test.RangeSupport do def decorate(%Document{} = document, %Range{} = range) do index_range = (range.start.line - 1)..(range.end.line - 1) - document.lines - |> Enum.slice(index_range) - |> Enum.map(fn line(text: text, ending: ending) -> text <> ending end) - |> update_in([Access.at(-1)], &insert_marker(&1, @range_end_marker, range.end.character)) - |> update_in([Access.at(0)], &insert_marker(&1, @range_start_marker, range.start.character)) - |> IO.iodata_to_binary() - |> String.trim_trailing() + result = + document.lines + |> Enum.slice(index_range) + |> Enum.map(fn line(text: text, ending: ending) -> text <> ending end) + |> update_in([Access.at(-1)], &insert_marker(&1, @range_end_marker, range.end.character)) + |> update_in([Access.at(0)], &insert_marker(&1, @range_start_marker, range.start.character)) + |> IO.iodata_to_binary() + |> String.trim_trailing() + + result + |> String.replace("\r\n", "\n") + |> String.trim_trailing("\r") end def decorate(document_text, path \\ "/file.ex", range) diff --git a/apps/forge/test/forge/document/path_test.exs b/apps/forge/test/forge/document/path_test.exs index 90197fc2..b47f9db3 100644 --- a/apps/forge/test/forge/document/path_test.exs +++ b/apps/forge/test/forge/document/path_test.exs @@ -8,7 +8,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.PathTest do test = self() spawn(fn -> - patch(Forge.Document.Path, :os_type, os_type) + patch(Forge.OS, :type, os_type) try do rv = fun.() @@ -133,7 +133,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.PathTest do describe "to_uri/1" do # tests based on cases from https://github.com/microsoft/vscode-uri/blob/master/src/test/uri.test.ts test "unix path" do - unless windows?() do + unless Forge.OS.windows?() do assert "file:///nodes%2B%23.ex" == to_uri("/nodes+#.ex") assert "file:///coding/c%23/project1" == to_uri("/coding/c#/project1") @@ -146,7 +146,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.PathTest do end test "windows path" do - if windows?() do + if Forge.OS.windows?() do drive_letter = "/" |> Path.expand() |> String.split(":") |> hd() assert "file:///c%3A/win/path" == to_uri("c:/win/path") assert "file:///c%3A/win/path" == to_uri("C:/win/path") @@ -186,7 +186,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.PathTest do end test "UNC path" do - if windows?() do + if Forge.OS.windows?() do assert "file://sh%C3%A4res/path/c%23/plugin.json" == to_uri("\\\\shäres\\path\\c#\\plugin.json") @@ -197,18 +197,10 @@ defmodule ElixirLS.LanguageServer.SourceFile.PathTest do end defp maybe_convert_path_separators(path) do - if windows?() do + if Forge.OS.windows?() do String.replace(path, "/", "\\") else String.replace(path, "\\", "/") end end - - def windows? do - if match?({:win32, _}, :os.type()) do - true - else - false - end - end end diff --git a/justfile b/justfile index 1d9d1f41..84b0d986 100644 --- a/justfile +++ b/justfile @@ -2,13 +2,21 @@ os := if os() == "macos" { "darwin" } else { os() } arch := if arch() =~ "(arm|aarch64)" { "arm64" } else { if arch() =~ "(x86|x86_64)" { "amd64" } else { "unsupported" } } local_target := if os =~ "(darwin|linux|windows)" { os + "_" + arch } else { "unsupported" } apps := "expert engine forge expert_credo" +expert_erl_flags := "-start_epmd false -epmd_module Elixir.Forge.EPMD" +engine_erl_flags := "-start_epmd false -epmd_module Elixir.Forge.EPMD" [doc('Run mix deps.get for the given project')] +[unix] deps project: #!/usr/bin/env bash cd apps/{{ project }} mix deps.get +[windows] +deps project: + cd apps/{{ project }} && \ + mix deps.get + [doc('Run an arbitrary command inside the given project directory')] run project +ARGS: #!/usr/bin/env bash @@ -34,10 +42,10 @@ mix project="all" *args="": for proj in {{ apps }}; do case $proj in expert) - (cd "apps/$proj" && elixir --erl "-start_epmd false -epmd_module Elixir.Forge.EPMD" -S mix {{args}}) + (cd "apps/$proj" && elixir --erl "{{ expert_erl_flags }}" -S mix {{args}}) ;; engine) - (cd "apps/$proj" && elixir --erl "-start_epmd false -epmd_module Elixir.Forge.EPMD" -S mix {{args}}) + (cd "apps/$proj" && elixir --erl "{{ engine_erl_flags }}" -S mix {{args}}) ;; *) (cd "apps/$proj" && mix {{args}}) @@ -46,10 +54,10 @@ mix project="all" *args="": done ;; expert) - (cd "apps/expert" && elixir --erl "-start_epmd false -epmd_module Elixir.Forge.EPMD" -S mix {{args}}) + (cd "apps/expert" && elixir --erl "{{ expert_erl_flags }}" -S mix {{args}}) ;; engine) - (cd "apps/engine" && elixir --erl "-start_epmd false -epmd_module Elixir.Forge.EPMD" -S mix {{args}}) + (cd "apps/engine" && elixir --erl "{{ engine_erl_flags }}" -S mix {{args}}) ;; *) (cd "apps/{{ project }}" && mix {{args}}) @@ -81,8 +89,11 @@ release-local: (deps "engine") (deps "expert") [windows] release-local: (deps "engine") (deps "expert") - # idk actually how to set env vars like this on windows, might crash - EXPERT_RELEASE_MODE=burrito BURRITO_TARGET="windows_amd64" MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release --overwrite + export EXPERT_RELEASE_MODE=burrito && \ + export BURRITO_TARGET="windows_amd64" && \ + export MIX_ENV={{ env('MIX_ENV', 'prod')}} && \ + cd apps/expert && \ + mix release --overwrite [doc('Build releases for all target platforms')] release-all: (deps "engine") (deps "expert") @@ -94,11 +105,16 @@ release-all: (deps "engine") (deps "expert") EXPERT_RELEASE_MODE=burrito MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release --overwrite [doc('Build a plain release without burrito')] +[unix] release-plain: (deps "engine") (deps "expert") #!/usr/bin/env bash cd apps/expert MIX_ENV={{ env('MIX_ENV', 'prod')}} mix release plain --overwrite +[windows] +release-plain: (deps "engine") (deps "expert") + cd apps/expert && export MIX_ENV={{ env('MIX_ENV', 'prod')}} && mix release plain --overwrite + [doc('Compiles .github/matrix.json')] compile-ci-matrix: elixir matrix.exs @@ -117,3 +133,12 @@ clean-engine: elixir -e ':filename.basedir(:user_data, "Expert") |> File.rm_rf!() |> IO.inspect()' default: release-local + +[unix] +start-tcp: release-plain + #!/usr/bin/env bash + ./apps/expert/_build/{{ env('MIX_ENV', 'prod')}}/rel/plain/bin/plain eval "System.no_halt(true); Application.ensure_all_started(:xp_expert)" --port 9000 + +[windows] +start-tcp: release-plain + ./apps/expert/_build/{{ env('MIX_ENV', 'prod')}}/rel/plain/bin/plain.bat eval "System.no_halt(true); Application.ensure_all_started(:xp_expert)" --port {{env('EXPERT_PORT', '9000')}} diff --git a/matrix.exs b/matrix.exs index 073a1e15..d1293943 100644 --- a/matrix.exs +++ b/matrix.exs @@ -1,20 +1,26 @@ Mix.install([:jason]) versions = [ - %{elixir: "1.19", otp: "28"}, - %{elixir: "1.18.4", otp: "28"}, - %{elixir: "1.18", otp: "27"}, - %{elixir: "1.18", otp: "26"}, - %{elixir: "1.17", otp: "27"}, - %{elixir: "1.17", otp: "26"}, - %{elixir: "1.16", otp: "26"}, + %{elixir: "1.19", otp: "28", os: "ubuntu-latest"}, + %{elixir: "1.18.4", otp: "28", os: "ubuntu-latest"}, + %{elixir: "1.18", otp: "27", os: "ubuntu-latest"}, + %{elixir: "1.18", otp: "26", os: "ubuntu-latest"}, + %{elixir: "1.17", otp: "27", os: "ubuntu-latest"}, + %{elixir: "1.17", otp: "26", os: "ubuntu-latest"}, + %{elixir: "1.16", otp: "26", os: "ubuntu-latest"}, ] +expert_matrix = + [ + %{elixir: "1.17.3", otp: "27.3.4.1", project: "expert", os: "ubuntu-latest"}, + %{elixir: "1.17.3", otp: "27.3.4.1", project: "expert", os: "windows-2022"} + ] + %{ include: for project <- ["engine", "expert_credo", "forge"], version <- versions do Map.put(version, :project, project) - end ++ [%{elixir: "1.17.3", otp: "27.3.4.1", project: "expert"}] + end ++ expert_matrix } |> Jason.encode!(pretty: true) |> then(&File.write!(".github/matrix.json", &1))