Cross-binary artifact conflict resolution

This is about to be a significant problem for the Scala ecosystem as a whole. Trying to get ahead of it just a bit…

Scenario:

  1. Project with scalaVersion := "0.27.0-RC1"
  2. Artifact A and artifact B, each of which depend on artifact C. All are cross-published scala libs
  3. Artifact C has been published for both 0.27.0-RC1 and 2.13
  4. Artifact A depends on the 2.13 cross-build, while artifact B depends on the Dotty cross-build

I haven’t actually tested this, but I’m relatively certain this turns into an undetected (and thus, unwarned!) transitive dependency conflict. It will ultimately only get resolved at runtime by classpath linearization. This in and of itself sounds benign, and it would be except for the fact that numerous libraries have significant version-specific changes under the hood in order to achieve source-compatibility between Scala 2 and Scala 3.

I honestly think this is something that only sbt can really address. The only option users have is to, first, figure out that they’re in this situation (which requires either staring at dependency graphs, or hitting weird bugs in production and happening to see suspicious things in classpaths), and then they need to add a lot of excludeAll in a lot of non-obvious places. It gets even more fun if the transitive dependencies on C are to slightly different versions.

Ultimately, I think that sbt needs to detect this kind of situation (where a pair of artifacts would be mutually evictable if they had been published against they same binary version) and, for the special case of 2.13 and any dotty binary version, they should be evicted forward to the Dotty version.

How are traits encoded?

Related to this, I think, is do A (using 0.27.0-RC1) compiled with C_2.13 and B (using 0.27.0-RC1) compiled with C_0.27 generate the same bytecode in the end? I’m guessing that the answer depends on how trait, object, lazy etc is JVM-encoded in C 2.13 vs 0.27. Assuming “o hell no,” we would basically have two flavors of various invocations. In the original example, A uses 2.13-oriented invocations to call C types, while B uses 0.27-oriented invocations to call C types.

Coursier?

Maybe it’s better to address this using Coursier so other build tools could benefit from the checking?