Conflicting dependencies and use of the ~> operator

It’s an interesting proposal and I’m still thinking about it. I definitely understand the pain points with override and have noticed the same issues you are raising.

We cannot give an error message like “this override only overrides the dependency that foo at exactly version x.x.x has on bar”. Overrides are not implemented in the solver at a level where we can use to them to derive if they affected what caused the resolution failure.

We could warn as a post check: “you are overriding bar on behalf of foo 1.2.3 but foo 1.2.4 was picked”, but you could just as well pin foo 1.2.3 and add a comment why it’s pinned and it would achieve basically the same thing?

This one is also tricky to implement in the solver. The only way to know if you no longer need the override is to run the solver again without the override and compare the results. This becomes a problem when you have multiple overrides because they affect each other.

2 Likes

Hmm…

This is something that could potentially be overcome right? Like its a limitation of the current implementation, not a fundamentally impossible aspect of dependency resolution?

I have also definitely felt the pain of override: true overriding the version specifications of other libraries more than I’d like. What if we tackled this another way. Given the similar setup of foo 0.1.1 depending on bar ~> 0.1 what if instead of {:bar, "~> x.x", override: [foo: "x.x.x"]} what if we could write:

{:bar, "~> 1.0"},
{:foo, "= 0.1.1", override_deps: [bar: "~> 1.0"]},

This would cause :foo to treated as if it depended on :bar ~> 1.0 instead of :bar ~> 0.1. So rather than putting the override: true on :bar and ignoring all other libraries version specifications on :bar, we specifically override how :foo depends on :bar, substituting the specification with our own. I think this is helpful for a few reasons:

  • If we later introduce :baz which depends on :bar ~> 1.0.26 then we would get an error if we were using :bar 1.0.0
  • The specific override we choose provides additional information for the next developer to understand the intent of the override
  • I think it is pretty clear what’s happening and pretty intuitive (and possibly easier to understand the semantics than override: true)
    • The only thing that may not be clear is that I am envisioning that if foo also depended on qux, then the dependency on qux will not be overridden
      • If you did want to override it then you’d have to write something like {:foo, "= 0.1.1", override_deps: [bar: "~> 1.0", qux: "~> 0.1.5"]}

There are scenarios where you’d still want to use override: true instead of override_deps, the main one being if you wanted to override a commonly used library like :jason it would be a bit annoying to need to pass override_deps for all of the other libraries that used it. Another case would be if you want to override the deps because you’re using a :path dependency. But I think that override_deps would have many cases where it would be useful (and hopefully the maintenance burden of writing it and maintaining it will not be too high).

2 Likes

I wonder if this could be taken one step forward and allow somehow usage of 2 library versions when specifically defined. For example:

{:bar, "~> 2.0"},
# Specifically instruct to use version 1.0 for this dependency
{:foo, "= 0.1.1", specific_local_dep: [bar: "~> 1.0"]},

This would keep mainly the things as they are now in terms of speed of compiling, as the global version of bar will be 2.0, while also giving full flexibility in cases where usage of a global version for a library is simply impossible without upgrading the library.

It can probably be done, anyone should of course feel free to take a stab at it. But it would probably be better to do it as post check outside of the solver. It would keep the solution much simpler.

In some ways I like this more because it reads in the correct order compared {:bar, "~> x.x", override: [foo: "x.x.x"]} which sounds like bar is overriding foo but it’s actually the other way around.

The problem with this is that it would be hard to make any error message from the solver indicate that something failed because of the override, like I mentioned above. You could only say that foo 0.1.1 depends on bar ~> 1.0 but not because of the override. So figuring out why something failed may become harder. I am not saying it’s impossible to make it work, only that it would require big changes to the solver.

You cannot have multiple versions of the same library loaded so this is not possible.

1 Like

I assume this is based on some identifier like name - couldn’t the name be modified to something-0.1.1 and something-0.2.2, and both could be loaded?

1 Like

Perhaps propose this alternative on the elixir GitHub issue here: Allow specifying why a dep is using override · Issue #14082 · elixir-lang/elixir · GitHub

There is more to it than just changing the library name. There are lots of global names that would have to be modified to ensure uniqueness, application names, module names, process names, ets table names etc.

7 Likes

What about you have three dependencies and two of them need to be overridden? Now you have to repeat the overrides and the specification in 2 places. It is best to specify the override once and then say which deps the overrides apply to. :slight_smile: otherwise it gets even harder to understand what you are supposed to get, if it is now spread over multiple multiple entries.

3 Likes

I feel like mix warning or raising if an override: true was not actually needed would be a great start, and I guess simple to add.

But I second the proposal, explicitly specifying which intermediary deps are overriden would be a very good feature.

1 Like