Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mix.Project.config/0 sometimes doesn't return the project configuration #717

Closed
sasa1977 opened this issue Aug 12, 2022 · 10 comments
Closed

Comments

@sasa1977
Copy link

Environment

  • Elixir & Erlang versions (elixir --version): 1.13, OTP 24
  • Elixir Language Server version: 0.10.0
  • Operating system: Ubuntu
  • Editor or IDE name (e.g. Emacs/VSCode): VSCode
  • Editor Plugin/LSP Client name and version: ElixirLS

Current behavior

I'm investigating crashes in the boundary library. These crashes take place only when the project is built inside ElixirLS.

For the purpose of this report, it's enough to know that boundary is a custom compiler which invokes Mix.Project.config/0 to get the project configuration.

It appears that in some situations, Mix.Project.config/0 isn't returning the project config, but rather some generic default template. As the docs state: If there is no project defined, it still returns a keyword list with default values. This allows many Mix tasks to work without the need for an underlying project.. This is only happening in ElixirLS, and only after the previous compilation failed due to an error.

Here are steps to reproduce:

  1. mix new my_app

  2. In mix.exs add {:boundary, "~> 0.9"} as a dep, and fetch deps.

  3. In my_app.ex add use Boundary to the MyApp module.

  4. In mix.exs add compilers: [:boundary | Mix.compilers()] in project/0. Only do this after step 3, or otherwise ElixirLS will crash due to another bug (which I need to fix in the boundary lib).

    Make sure that the project is compiling from ElixirLS (I used vscode for testing, and observed the output view).

  5. Introduce a compilation error in my_app.ex, e.g.:

    def hello do
      foobar()
      :world1
    end
  6. Wait until the compilation fails, then fix the error. At this point, the boundary compiler will crash with:

    ** (KeyError) key :app not found in: [aliases: [], build_embedded: false, build_per_environment: true, build_scm: Mix.SCM.Path, config_path: "config/config.exs", consolidate_protocols: true, default_task: "run", deps: [], deps_path: "deps", elixirc_paths: ["lib"], erlc_paths: ["src"], erlc_include_path: "include", erlc_options: [], lockfile: "mix.lock", preferred_cli_env: [], start_permanent: false]
      (elixir 1.13.1) lib/keyword.ex:559: Keyword.fetch!/2
      (boundary 0.9.3) lib/boundary/mix/xref.ex:123: Boundary.Mix.Xref.seen_table/0
      (boundary 0.9.3) lib/boundary/mix/xref.ex:34: Boundary.Mix.Xref.initialize_module/1
      (boundary 0.9.3) lib/boundary/mix/tasks/compile/boundary.ex:128: Mix.Tasks.Compile.Boundary.record/5
    

Notice the keyword list which is inspected in this output. This list is obtained via Mix.Project.config(), but it doesn't correspond to the actual project config. For example the :app key is missing, as well as the :compilers key.

Restarting ElixirLS will fix the issue. I wasn't able to reproduce the problem outside of ElixirLS. Due to this, it seems that the problem is somehow related to the way ElixirLS builds the project after a failed compilation.

Expected behavior

Mix.Project.config/0 should always return the correct project configuration.

@lukaszsamson
Copy link
Collaborator

@sasa1977 do you have this error in a phoenix project with reloader? It uses a similar approach to recompilation.

@sasa1977
Copy link
Author

@sasa1977 do you have this error in a phoenix project with reloader? It uses a similar approach to recompilation.

Just tried the following:

  1. mix phx.new (latest archive)
  2. Add boundary, setup web and main boundaries (both with check: [in: false, out: false] options, to avoid boundary errors)
  3. Start iex -S mix phx.server, visit localhost:4000
  4. in PageController introduce a compilation error, refresh the browser page
  5. Fix the compilation error, refresh the page again

Result:

  1. Phoenix recompiles the single file successfully
  2. ElixirLS recompilation crashes, as describe above

@seungjinRonin
Copy link

seungjinRonin commented Sep 8, 2022

Adding a data point - I have this issue as well

elixir 1.12.3-otp-24
erlang 24.1
on Mac Pro M1
VSCode, ElxiirLS v0.11.0

@gusbicalho
Copy link

gusbicalho commented Sep 27, 2022

While we wait for a fix, here is a workaround (no sure if this is the best env var to check for, but it seems to work)

  # Workaround to avoid Boundary breaking ElixirLS:
  # ElixirLS runs with the ELS_MODE env var set, so we disable the
  # Boundary compiler in that case.
  # See https://github.com/elixir-lsp/elixir-ls/issues/717
  @use_boundary (if System.get_env("ELS_MODE") do
                   []
                 else
                   [:boundary]
                 end)

  def project do
    [
      # ...
      compilers: @use_boundary ++ [:other :compilers] ++ Mix.compilers(),
      # ...
    ]
  end

@lukaszsamson
Copy link
Collaborator

@sasa1977 after some debugging this looks like a problem in boundary exposed by elixir-ls. We reload and recompile MixProject on every build to pick up changes in mix config. During normal successful compilation everything works as expected. See

Mix.ProjectStack.peek() on MixProject reload: %{
  config: [
    app: :boundary_crash,
  ],
}
Mix.ProjectStack.peek() after pop: nil
Mix.ProjectStack.peek() after post config: nil
Code.get_compiler_options: %{
  tracers: [ElixirLS.LanguageServer.Tracer],
}
Mix.ProjectStack.peek() after load config: %{
  config: [
    app: :boundary_crash,
  ],
}
------boundary compiler executes
Compiling 1 file (.ex)

Here's what happens when the build fails

Code.get_compiler_options: %{
  tracers: [ElixirLS.LanguageServer.Tracer],
}
Mix.ProjectStack.peek() after load config: %{
  config: [
    app: :boundary_crash,
  ],
}
------boundary compiler executes
Compiling 1 file (.ex)

== Compilation error in file lib/boundary_crash.ex ==
** (CompileError) lib/boundary_crash.ex:17: undefined function foobar/0 (expected BoundaryCrash to define such a function or for it to be imported, but none are available)

And then after fixing build

Mix.ProjectStack.peek() after post config: nil
Code.get_compiler_options: %{
  tracers: [Mix.Tasks.Compile.Boundary, ElixirLS.LanguageServer.Tracer],
}

== Compilation error in file mix.exs ==
** (KeyError) key :app not found in: [aliases: [], build_embedded: false, build_per_environment: true, build_scm: Mix.SCM.Path, config_path: "config/config.exs", consolidate_protocols: true, default_task: "run", deps: [], deps_path: "deps", elixirc_paths: ["lib"], erlc_paths: ["src"], erlc_include_path: "include", erlc_options: [], lockfile: "mix.lock", preferred_cli_env: [], start_permanent: false]

boundary is not properly clearing its tracer from compiler_options on failed builds and when elixir-ls tries to reload and recompile MixProject this tracer crashes. While elixir-ls could clean up tracers and reset it to known state I think this responsibility should be on boundary.

@lukaszsamson
Copy link
Collaborator

lukaszsamson commented Sep 29, 2022

Hmm it may be difficult to properly implement cleanup as it seems that when build fails Mix.Task.Compiler.after_compiler callback is not invoked (tested on a simple vanilla mix app and a simple noop compiler task).

Edit:
reported upstream elixir-lang/elixir#12159

@sasa1977
Copy link
Author

Thanks for debugging! Looking at the boundary code, it doesn't seem correct. The tracer is cleared on success, but not on error.

I'll try the fix tomorrow and report back, although your follow-up report suggests that an Elixir bug also has to be fixed.

Either way, I'm a bit confused, because this used to work just fine up until recently. I suppose something has changed in the way ElixirLS recompiles the project, right?

@sasa1977
Copy link
Author

Yeah, I've fixed the behaviour in boundary, but the error still appears, because the callback is not invoked, so I guess we'll have to wait for the Elixir fix.

@lukaszsamson
Copy link
Collaborator

I'll try the workaround with clearing tracers

@sasa1977
Copy link
Author

FYI, I took the approach advised in elixir-lang/elixir#12159 (comment) (cleaning up the tracers after the Elixir compiler) and this resolved the issue. The fix is published on hex in boundary 0.9.4. Thanks for helping troubleshoot this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants