-
Notifications
You must be signed in to change notification settings - Fork 30
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
SIP-51 - Drop Forwards Binary Compatibility of the Scala 2.13 Standard Library #54
Conversation
Such a change would have a significant impact on the Scala.js and Scala Native releases and version policy. I will talk about Scala.js, but the same applies to Scala Native, IIRC. Scala.js builds its own version of the scala-library. Its build contains a When Scala 2.13.11 gets released, we cannot change With the current forward binary compat guarantee, this is not a problem. Users can immediately upgrade their If we remove the forward binary compatibility guarantee, this model ceases to work. If someone updates their In practice, that will force us to do one of two things:
/cc @gzm0 |
I see, thank you for the explanation. Why is |
Thank you for bringing this to my attention. My thoughts: First thing that came to mind when I saw this: It feels like requiring the Scala std lib to maintain forward binary compatibility is too restrictive. I think we should try to work towards removing this requirement.
Because if it isn't it is impossible for us to publish a Scala.js only bug-fix to a library (we'd have to wait for a new Scala version).
This seems like the problem to me. I assume this is because of how build tools handle compilation? Or does the Scala compiler require the exact version (if yes, then maybe the cross versioning of scalalib was wrong all along?)
IIUC this would be the same scenario than during 2.13 milestone releases right? If so, this is not acceptable to me. It had way too much impact to my private life (I believe there are write-ups of this, I can dig them out if you need me to). |
Thanks @gzm0, happy to hear you're supportive in principle!
Does that mean scalajs-library contains more than the Or is it about fixes in the scala-js compiler that would produce fixed I see that for other libraries (I looked at scala-xml_sjs1_2.13), the jar contains both
I'm not sure what you mean exactly; libraries on the compilation classpath (including the Scala library) need to contain classfiles, which contain "pickles" / serialized Scala signatures. Until now, the Scala library on the compiliation classpath always matches the Scala version of the running compiler, but this SIP would change that, and this will work fine (i.e., a 2.13.10 compiler can run with a 2.13.11 library on the compilation classpath). |
Currently,
It would be possible to split the scala-library in a separate artifact, for example called
For the
Because of the latter two, I think we should keep versioning Therefore, the Scala version part of it must be in the artifact name. Something like If we do that, we will have to tell the tools that, whenever
Exactly. It is basically impossible to convince tools not to put |
Thank you for all the details. So the options are:
1.a. split up the library, version only the part corresponding to the Scala library to the Scala version
I think options 2 and 3 are not good because of the pressure on the scala.js maintainers. 1a seems the most reasonable solution to me. Maybe the circular dependency can be avoided by including a subset of the scala.js specific additions into the artifact corresponding to the Scala library? I don't see any other / better alternatives either. |
However, I believe that allowing to (carefully) evolve the standard library is greatly beneficial for the Scala community. | ||
|
||
|
||
## Alternatives and Related Work |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than requiring the compiler to work with newer standard libraries, have you considered to require downstream projects to be compiled with an equal or newer Scala version than dependencies? We are using this approach in Scala.js for quite a while.
Pros:
- Not require forward compilation classpath compatibility in the compiler
- Not lead to a different scala stdlib than
scalaVersion
Cons:
- Library maintainers might not want to upgrade the Scala version since it would force it onto downstream projects (OTOH, this proposal would do this as well for the stdlib, just silently)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I have considered this option. In case there are effectively difficulties making older compilers work with a more recent Scala library, that would certainly be a solution. But I don't think this is going to cause problems.
FWIW, I have attempted to formalize all of this a bit, but it ended up being very verbose. So I'm not sure it is really useful. In any case, here's the gist: https://gist.github.com/gzm0/d6ec17f8a8fe0d8e4989b1aa75818bd4 |
Could we do something like this:
This has the following properties:
|
Thank you @sjrd, that sounds good to me. For the other reviewers of this porposal (@gabro, @Kordyjan), I think you don't need to understand the details how Scala.js and Scala Native would need to be adapted. Let's assume will be solution for that (having a solution is of course a precondition for this proposal). |
WiP for Scala.js: scala-js/scala-js#4787 |
For a long time, I have supported treating the stdlib as any other dependency. However, I wasn't aware of all the complications for the non-JVM targets. This is why I have waited to express my opinion. The proposed solution for scala.js seems reasonable. |
In the case of Scala Native the dependency tree looks somehow different. In Scala Native it looks a bit different, starting from the bottom of the dependency list:
In Scala Native, we currently don't try to remove references from dependencies in the upper layers. Eg. we try to limit the usage of the Scala standard library in the
Because Dotty's |
|
||
Because the build tool can update the Scala library version, a project might accidentally use / link to new API that does not yet exist in the `scalaVersion` that is defined in the build definition. | ||
This is safe, as the project's POM file will have a dependency on the newer version of the Scala library. | ||
The same situation can appear with any other dependency of a project. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that build tools could provide warnings to say e.g. “scalaVersion
has been promoted to 2.13.11 (instead of 2.13.10 as per the scalaVersion
key) because it depends on the library foo
, which itself depends on Scala 2.13.11.”
We need to experiment, see what technical solutions are really needed. I'll try and make some progress on scala-js/scala-js#4787 soon-ish. Someone (tm) should investigate the required changes in sbt. |
Isn’t that part of the “experimental” phase? |
Yes? No? Perhaps. 🤷♂️ |
@dwijnand agreed to take a look |
Bazel is becoming increasingly popular among companies using scala, so I think it would be great to know if there is no hidden risk there. @tanishiking: Will you be able to leave a short comments about how bazel is handling stdlib now and do you see any potential problems if we change our compatibility guarantees? |
Basically, bazel (rules_scala) treats the Scala library in the same way as other dependencies. How Bazel downloads external depsRepositories / toolchainSeveral libraries like Those hard-coded dependencies are automatically added to the classpath. For example, when we define a test target with http_jar / maven_jarhttps://bazel.build/rules/lib/repo/http#http_jar rules_jvm_external / Bazel depsBoth tools resolve transitive dependencies, rules_jvm_external should have a same dependency resolution behavior as sbt since it uses coursier under the hood. dependency_moderules_scala has a feature called dependency_mode. This mode controls how far Bazel will put transitive dependencies into the classpath, if we set
Therefore, even if the dependent library has a scala-library in its dependency, Bazel won't add its transitive deps (scala-library) to the classpath. That being said, it's possible to run into a linkage error in Bazel in the following cases
If stdlibs drop the forward compatibilities,
Otherwise, scalalib from resolved transitive dependency will be included into the classpath (same as Gradle). (FYI @eed3si9n since Eugene would be the best person to talk about dependency resolution in Bazel) Also, FYI, I have a small setup for Bazel + Scala + rules_jvm_external: https://github.com/tanishiking/bazel-tutorial-scala/tree/main/02_scala_maven you can play around with. |
The Scala standard library is treated specially by sbt and other build tools, its version is always pinned to the `scalaVersion` of the build definition and never updated automatically. | ||
|
||
For example, the `"com.softwaremill.sttp.client3" %% "core" % "3.8.3"` library has a dependency on `"org.scala-lang" % "scala-library" % "2.13.10"` in its POM file. | ||
When a project uses this version of the sttp client in a project with `scalaVersion` 2.13.8, sbt will put the Scala library version 2.13.8 on the classpath. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There were several premises that motivated this behavior of sbt, which we can list and reevaluate.
- In general, sbt assumes that users knows what they are doing enough to specify the compiling Scala version. This is encoded as
scalaVersion
key. - sbt assumes that the scala-compiler version (
scalaVersion
) must match thescala-library
version for Scala 2.x.- Before sbt introduced the enforcement, I feel like there were some complaints from the Scala team that they can't reliably predict the scala-library.jar that would be on the classpath during compilation.
- As the ultimate variant of the alignment use case, sbt allows users to override
scalaOrganization
to forkscala-compiler
andscala-library
together to introduce additional feature to Scala 2.x. See Typelevel Scala and Override scala organization and version transitively at the Ivy level sbt/sbt#2634.
For example, the `"com.softwaremill.sttp.client3" %% "core" % "3.8.3"` library has a dependency on `"org.scala-lang" % "scala-library" % "2.13.10"` in its POM file. | ||
When a project uses this version of the sttp client in a project with `scalaVersion` 2.13.8, sbt will put the Scala library version 2.13.8 on the classpath. | ||
|
||
This means that the standard library is required to remain both backwards and forwards binary compatible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the relationship between the sbt's alignment behavior and Scala library's compatibility requirement is a bit overstated here.
- The policy of compatibility predates the introduction of alignment in 2016.
- Scala 3 dropped forward compatibility.
Pre-2016, I think Scala 2.x kept forward compatibility, I think because not keeping the forward compatibility means coloring every library with an effective minimum Scala version beyond 0th patch i.e. 2.11.0, 2.12.0, 2.13.0 etc. So if someone wanted to use Maven or just download a bunch of JARs manually and use Ant or something they could use any patch version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note prior to sbt 1.3.0 (2019) sbt used Apache Ivy, which emulates Maven's nearest-wins dependency resolution semantics, so it may not have resolved to the latest scala-version found in the transitive graph.
This means that the standard library is required to remain both backwards and forwards binary compatible. | ||
The implementation of sttp client 3.8.3 can use any feature available in Scala 2.13.10, and that compiled code needs to work correctly with the Scala 2.13.8 standard library. | ||
|
||
The suggested change of this SIP is to drop this special handling of the Scala standard library and therefore lift the forwards binary compatibility requirement. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to Scala 3 minor versions 3.1.x, 3.2.x etc, I feel like Scala 2.13.x patch series could drop forward compatibility without modifying sbt's scala-library alignment behavior. To aid the user, we can fail the build with eviction error when greater Scala 2.13 versions is found, similar to what happens with cats-effect 2.x vs 3.x.
Not that I am an expert on rules_scala, I can share a bit of perspective as I've worked with large-scale codebases using Bazel. For companies using Bazel, Scala 2.13.x dropping forward compatibility likely would be net-positive or neutral. Yes, it might require bumping Scala 2.13 patch versions every now and then, but on Bazel figuring out the dependency version puzzle is already required for literally every other library dependencies anyway, and typically you have a team overseeing the health of monorepo, so if there's a breakage that could be caught at compile-time, Bazel will catch it. Re dependency_mode, what I am about to reveal might be shocking for non-Bazel users, so beware. In an unhealthy monorepo, it's not uncommon to have 1000 JAR files, if not multiple thousands of dependencies, in part because every directory is a target in Bazel, kitchen sink targets, hand-written copy-pasta etc. It's so many JAR files that compilers or JVM simply going through them is a performance hit. There are multiple ways Bazel, plugins (rules), and users try to mitigate this issue. One is called ijar, which makes up fake JAR file without impl body. Another is this idea sometimes called "strict_deps" to present the compiler with only the direct dependencies. "dependency_mode" in rules_scala is a variant of that. This is really nice because instead of 1000 JARs, we can present compilers and JVM with 20 JARs just to get through the compilation. (As you can imagine Scala presents all sorts of challenges trying to load type info etc) This is a long-winded way of saying requiring
For the actual dependency resolution, it's difficult to make general statement about Bazel because each company has its own solution for how 3rdparty/jvm graph is resolved. Some people use johnynek/bazel-deps, Twitter had twitter/bazel-multiversion, but one thing that's common is that a single Scala 2.13.x version is selected for the entire monorepo both in terms of scala-compiler and scala-library, so we don't get into a situation where target |
I don't have much to add on the technical side; the people already involved are more knowledgeable than I am on the details. From a SIP process standpoint: I like the proposal, and I haven't seen any strong objection against it in principle. In my opinion, the proposal would need to answer the open points raised recently and probably include a more specific idea of how Bazel support would look like. Once we have that, I think it's fine to vote to promote it to the experimental phase, even though there will be gaps to fill via further experimentation. |
I also recommend moving this proposal to the experimental stage, where we can gather more information about potential implementation problems. |
I am merging this PR since the proposal has been voted on and accepted on February 17th. During the experimentation phase, it will still be possible to amend the content of the SIP by creating a new PR. |
As mentioned by @sjrd during the latest SIP meeting, implementing this is on the Scala Center's roadmap for Q4 of 2023. |
FTR, support in Scala.js for this SIP is under review at scala-js/scala-js#4913. |
Update I forgot to mention here: the Scala.js support was shipped in Scala.js 1.15.0. |
No description provided.