How to properly deal with Elixir version restrictions of dependencies

So, consider I want to target my library foo as Elixir ~> 1.12

This has a dependency bar, which in v1 has a dependency to Elixir~> 1.10, but in v2 it has a dependency to Elixir ~> 1.18. This is totally fine so far.

But when I use mix to lock the dependencies (mix deps.get), it will always generate a mix.lock file using the newest version (v2), and naturally when I run CI using my target minimum Elixir version of 1.12, CI fails because standard lib functions are used that do not exist in 1.12.

Now, most of this is still correct - I target Elixir ~> 1.12, so it is correct to retrieve the newest compatible dependency for the given version, because Elixir 1.18 does match ~> 1.12.

But not, if I’m running it using an Elixir 1.12 toolchain, which I am.

How do I deal with this properly? Maybe I’m missing something?

Currently I have to manually search through every revision of every direct dependency to find a useable version, then do the same for every single transitive dependency, and then manually set said version. That’s quite the effort for something the dependency solver already does for every dependency (except Elixir itself)

You just have to do mix deps.get in CI. When you have a dependency, you also usually specify the range like bar >= v1. If you call mix deps.get with elixir 1.12, you will have bar v1 in lock, but if with elixir 1.18, you will have bar v2 in lock.

1 Like

That would help a lot, but that’s not what it’s doing:

#> elixir --version
Erlang/OTP 24 [erts-12.3.2.17] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit]

Elixir 1.12.3 (compiled with Erlang/OTP 22)

#> mix hex.info
Hex:    2.3.1
Elixir: 1.12.3
OTP:    24.3.4.17

Built with: Elixir 1.12.3 and OTP 22.3

#> mix deps.unlock earmark_parser
Unlocked deps:
* earmark_parser

#> mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.071s
Unchanged:
  <skip>
New:
  earmark_parser 1.4.44
All dependencies are up to date

#> cat deps/earmark_parser/mix.exs | grep 'elixir:'
      elixir: "~> 1.13",

I mean I realize this isn’t the question you are asking but this is ANCIENT and a security issue as this doesn’t get patched and hasn’t in years. I would start by picking an Elixir version at least as new as 1.17 and going from there.

2 Likes

That just moves the issue to different numbers.

Could you please share the deps of your mix.exs? And dont unlock one dependency, try unlocking them all

I don’t think elixir versions are checked for dependency resolution. Afaik they’re just used to warn people once they try using something not supported by their current version of elixir. So to support an older elixir version you’d need to explicitly restrict the dependency to supported versions. You could use CI to detect this early if that’s what you’re wondering about.

3 Likes

I know they aren’t. They’re only checked at compile time, eg.

#> mix deps.compile
==> earmark_parser
warning: the dependency :earmark_parser requires Elixir "~> 1.13" but you are running on v1.12.3
==> ex_doc
warning: the dependency :ex_doc requires Elixir "~> 1.15" but you are running on v1.12.3

I guess I could use CI to check for that and fail the build on such a message, but just a “failure” means (as written above) still a potentially large effort, since I have to check dependencies manually for matching versions. What I’m looking for would be at least some way to find the most recent dependency versions that match the given toolchain.

If you have a way to circumvent this completely that doesn’t boil down to “just use the most recent elixir version”, I’m open to suggestions, that’s why I’m asking such an open question.

1 Like
defmodule Foo.MixProject do
  use Mix.Project

  def project do
    [
      app: :foo,
      version: "0.1.0",
      elixir: "1.12",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:ex_doc, "~> 0.16", only: :dev, runtime: false}
    ]
  end
end
Same test as above with this mix project
#> elixir --version
Erlang/OTP 24 [erts-12.3.2.17] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit]

Elixir 1.12.3 (compiled with Erlang/OTP 22)

#> mix clean

#> mix deps.clean --all
* Cleaning earmark_parser
* Cleaning ex_doc
* Cleaning makeup
* Cleaning makeup_elixir
* Cleaning makeup_erlang
* Cleaning nimble_parsec

#> mix deps.unlock --all

#> mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.117s
New:
  earmark_parser 1.4.44
  ex_doc 0.39.3
  makeup 1.2.1
  makeup_elixir 1.0.1
  makeup_erlang 1.0.2
  nimble_parsec 1.4.2
* Getting ex_doc (Hex package)
* Getting earmark_parser (Hex package)
* Getting makeup_elixir (Hex package)
* Getting makeup_erlang (Hex package)
* Getting makeup (Hex package)
* Getting nimble_parsec (Hex package)

#> grep -e 'elixir:' deps/*/mix.exs
deps/earmark_parser/mix.exs:      elixir: "~> 1.13",
deps/ex_doc/mix.exs:      elixir: "~> 1.15",
deps/ex_doc/mix.exs:            packages: [:ex_doc, elixir: "main"]
deps/makeup/mix.exs:      elixir: "~> 1.12",
deps/makeup_elixir/mix.exs:      elixir: "~> 1.12",
deps/makeup_erlang/mix.exs:      elixir: "~> 1.6",
deps/nimble_parsec/mix.exs:      elixir: "~> 1.12",

Yeah, I was able to reproduce the problem you are talking about. Sounds like a bug in version resolution, but fixing it would require a backport to all hex implementations (for every version of Elixir)

If I were you, I would take one of two approaches

  1. Drop support for older Elixir versions. Elixir is backward compatible and newer versions of the language can work with older code, so that users of your code can upgrade their Elixir version and the worst they will get is a bunch of warnings. But it can be a problem for those who compile with --warnings-as-errors, which is de facto a standard (and this is one of the few things I don’t like about Elixir)
  2. Implement a separate branch with support for the lowest version of Elixir you need. If you plan to use lower versions of your dependencies, it means that your code will have to call them differently (this may vary depending on the dependency, of course). If this is just about ex_doc, then branching is the best approach

This is not correct. Warnings as errors applies to user code, NOT dependencies.

I was not talking about dependencies, I was talking about the code of the project, not it’s dependencies

I may be missing something, how does a change in how your library code works impact the warnings present in an end users project code? Are you injecting a lot of code via macros?

Very simple. I have some code written for Elixir 1.12 and it uses some functions which were deprecated in Elixir 1.13 (or later). If I upgrade Elixir to the latest, I will get a deprecation warning and my code will fail the compilation with --warnings-as-errors