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.
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.
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.
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.
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
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
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)
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
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