You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A diverse and comprehensive set of libraries is important to any productive software ecosystem. While it is easy to develop and distribute Scala libraries, good library authorship goes far
12
-
beyond just writing code and publishing them.
11
+
A diverse and comprehensive set of libraries is important to any productive software ecosystem. While it is easy to develop and distribute Scala libraries, good library authorship goes
12
+
beyond just writing code and publishing it.
13
13
14
14
In this guide, we will cover the important topic of **Binary Compatibility**:
15
15
16
16
* How binary incompatibility can cause production failures in your applications
17
-
* How library authors can avoid breaking binary compatibility, and/or convey breakages clearly to library users when they happen
17
+
* How to avoid breaking binary compatibility
18
+
* How to reason about and communicate the impact of their code changes
18
19
19
-
Before we start, first we need to understand how code is compiled and executed on the Java Virtual Machine (JVM).
20
+
Before we start, let's understand how code is compiled and executed on the Java Virtual Machine (JVM).
20
21
21
22
## The JVM execution model
22
23
23
-
Code compiled to run on the JVM is compiled to a platform-independent format called **JVM bytecode** and stored in **Class File** format (with `.class` extension) and these class files are stored
24
-
in JAR files. The bytecode is what we refer to as the **Binary** format.
24
+
Scala is compiled to a platform-independent format called **JVM bytecode** and stored in `.class` files. These class files are collated in JAR files for distribution.
25
25
26
-
When application or library code is compiled, their bytecode invokes named references of classes/methods from their dependencies instead of including the dependencies' actual bytecode
27
-
(unless inlining is explicitly requested). During runtime, the JVM classloader will search through the provided class files for classes/methods referenced by and invoke them.
26
+
When some code depends on a library, its compiled bytecode references the library's bytecode. The library's bytecode is referenced by its class/method signatures and loaded lazily
27
+
by the the JVM classloader during runtime. If a class or method matching the signature is not found, an exception is thrown.
28
28
29
-
Let's illustrate with an example:
29
+
As a result of this execution model:
30
30
31
-
We got an application `App` that depends on `A` which itself depends on library `C`. When starting the application we need to provide the class files
32
-
for all of `App`, `A` and `C` (something like `java -cp App.jar:A.jar:C.jar:. MainClass`). If we did not provide `C.jar`, or if we provided a `C.jar` that does not contain certain classes/methods
33
-
which `A` expected to exist in library `C`, we will get an exception when our code attempt to invoke the missing classes/methods.
31
+
* We need to provide the JARs of every library used in our dependency tree when starting an application, since the library's bytecode is only referenced -- not merged into its user's bytecode
32
+
* A missing class/method problem may only surface after the application has been running for a while, due to lazy loading.
34
33
35
-
This is what we call **Binary Incompatibility Errors** - The bytecode interface used for compilation differs and is incompatible with the bytecode provided during runtime.
34
+
Common exceptions from classloading failures includes
35
+
`InvocationTargetException`, `ClassNotFoundException`, `MethodNotFoundException`, and `AbstractMethodError`.
36
+
37
+
Let's illustrate this with an example:
38
+
39
+
Consider an application `App` that depends on `A` which itself depends on library `C`. When starting the application we need to provide the class files
40
+
for all of `App`, `A` and `C` (something like `java -cp App.jar:A.jar:C.jar:. MainClass`). If we did not provide `C.jar` or if we provided a `C.jar` that does not contain some classes/methods
41
+
which `A` calls, we will get classloading exceptions when our code attempts to invoke the missing classes/methods.
42
+
43
+
These are what we call **Binary Incompatibility Errors**. An error caused by binary incompatibility happens when the compiled bytecode references a name that cannot be resolved during runtime
36
44
37
45
## What are Evictions, Source Compatibility and Binary Compatibility?
38
46
39
-
Since the classloader only loads the first match of a class, having multiple versions of the same library in the classpath is redundant.
40
-
Therefore when deciding which JARs to use for compilation, SBT only selects one version from each library (by default the highest),
41
-
and all other versions of the same library are **evicted**. When packaging applications, the same versions of libraries that was used for compiling the
42
-
application is packaged and used during runtime.
47
+
### Evictions
48
+
When a class is needed during execution, the JVM classloader loads the first matching class file from the classpath (any other matching class files are ignored).
49
+
Because of this, having multiple versions of the same library in the classpath is generally undesireable:
50
+
51
+
* Unnecessary application size increase
52
+
* Unexpected runtime behaviour if the order of class files changes
53
+
54
+
Therefore, when resolving JARs to use for compilation and packaging, most build tools will pick only one version of each library and **evict** the rest.
55
+
56
+
### Source Compatibility
57
+
Two library versions are **Source Compatible** if switching one for the other does not incur any compile errors.
58
+
For example, If we can upgrade `v1.0.0` of a dependency to `v1.1.0` and recompile our code without any compilation errors, `v1.1.0` is source compatible with `v1.0.0`.
59
+
60
+
### Binary Compatibility
61
+
Two library versions are **Binary Compatible** if the compiled bytecode of these versions can be interchanged without causing binary compatibility errors.
62
+
For example, if we can replace the class files of a library's `v1.0.0` with the class files of `v1.1.0` without any binary compatibility errors during runtime,
63
+
`v1.1.0` is binary compatible with `v1.0.0`.
64
+
65
+
**NOTE:** While breaking source compatibility normally results in binary compatibility breakages as well, they are actually orthogonal -- breaking one does not imply breaking the other.
66
+
67
+
### Forwards and Backwards Compatibility
68
+
69
+
There are two "directions" when we describe compatibility of a library release:
43
70
44
-
Two library versions are said to be **Source Compatible**if switching one for the other does not incur any compile errors. For example, If we can switch from `v1.0.0` of a dependency to `v1.1.0`and
45
-
recompile our code without causing any compilation errors, `v1.1.0`is said to be source compatible with `v1.0.0`.
71
+
**Backwards Compatible**means that a newer library version can be used in an environment where an older version is expected. When talking about binary and source compatibility,
72
+
this is the common and implied direction.
46
73
47
-
Two library versions are said to be **Binary Compatible** if the compiled bytecode of these versions are compatible. Using the example above, removing a class will render two version
48
-
binary incompatible too, as the compiled bytecode for v2.0.0 will no longer contain the removed class.
74
+
**Forwards Compatible** means that an older library can be used in an environment where a newer version is expected.
75
+
Forward compatibility is generally not upheld for userland libraries. It is only important in situations where an older version of a library is commonly
76
+
used at runtime against code that is compiled with newer version. (e.g. Scala's standard library)
49
77
50
-
When talking about two versions being compatible, the direction matters too. If we can use `v2.0.0` in place of `v1.0.0`, `v2.0.0` is said to be **backwards compatible** with `v1.0.0`. Conversely,
51
-
if we say that any library release of `v1.x.x` will be forwards compatible, we can use `v1.0.0` anywhere where `v1.1.0` was originally used.
78
+
Let's look at an example where library `A v1.0.0` is compiled with library `C v1.1.0`.
52
79
53
-
(For the rest of this guide, when you see the word "compatible" assume backwards compatibility, as it is the more common case of compatibility guarantee)
An important note to make is that while source compatibility breakages normally results in binary compatibility breakages as well, they are actually orthogonal
56
-
(breaking one does not imply breaking the other).
82
+
`C v1.1.0 ` is **Forwards Binary Compatible** with `v1.0.0` if we can use `v1.0.0`'s JAR at runtime instead of `v1.1.0`'s JAR without any binary compatibility errors.
83
+
84
+
`C v1.2.0 ` is **Backwards Binary Compatible** with `v1.1.0` if we can use `v1.2.0`'s JAR at runtime instaed of `v1.1.0`'s JAR without any binary compatibility errors..
57
85
58
86
## Why binary compatibility matters
59
87
60
-
Let's look at an example where binary incompatibility between versions of a library can have catastrophic consequences:
88
+
Binary Compatibility matters because failing to maintain it makes life hard for everyone.
89
+
90
+
* End users has to update all library versions in their whole transitive dependency tree such that they are binary compatible, otherwise binary compatibility errors will happen at runtime
91
+
* Library authors are forced to update the dependencies of their library so users can continue using them, greatly increases the effort required to maintain libraries
92
+
93
+
Constant binary compatibility breakages in libraries, especially ones that are used by other libraries, is detrimental to our ecosystem as they require a lot of effort
94
+
from users and library authors to resolve.
95
+
96
+
Let's look at an example where binary incompatibility can cause grief and frustration:
97
+
98
+
### An example of "Dependency Hell"
61
99
62
-
Our application depends on library `A` and `B`. Both `A` and `B` depends on library `C`. Initially both `A` and `B` depends on `C v1.0.0`.
100
+
Our application `App`depends on library `A` and `B`. Both `A` and `B` depends on library `C`. Initially both `A` and `B` depends on `C v1.0.0`.
Some time later, we see `B v1.1.0` is available and upgrade its version in our build.sbt. Our code compiles and seems to work so we push it to production and go home for dinner.
104
+
Some time later, we see `B v1.1.0` is available and upgrade its version in our build. Our code compiles and seems to work so we push it to production and go home for dinner.
67
105
68
-
Unfortunately at 2am, we got frantic calls from customers saying that our App is broken! Looking at the logs, you find lots of `NoSuchMethodError` is being thrown by some code in `A`!
106
+
Unfortunately at 2am, we got frantic calls from customers saying that our application is broken! Looking at the logs, you find lots of `NoSuchMethodError` is being thrown by some code in `A`!
Why did we get a `NoSuchMethodError`? Remember that `A v1.0.0` is compiled with `C v1.0.0` and thus calls methods available in `Cv1.0.0`. While `B` and
73
-
our App has been recompiled with available classes/methods in `C v2.0.0`, `A v1.0.0`'s bytecode hasn't changed - it still calls the same method that is now missing in `C v2.0.0`!
110
+
Why did we get a `NoSuchMethodError`? Remember that `A v1.0.0` is compiled with `C v1.0.0` and thus calls methods available in `C v1.0.0`.
111
+
While `B v1.1.0` and `App` has been recompiled with `C v2.0.0`, `A v1.0.0`'s bytecode hasn't changed - it still calls the method that is now missing in `C v2.0.0`!
74
112
75
-
This situation can only be resolved by ensuring that the chosen version of `C` is binary compatible with all other evicted versions of `C`. In this case, we need a new version of `A` that depends
113
+
This situation can only be resolved by ensuring that the chosen version of `C` is binary compatible with all other evicted versions of `C` in your dependency tree. In this case, we need a new version of `A` that depends
76
114
on `C v2.0.0` (or any other future `C` version that is binary compatible with `C v2.0.0`).
77
115
78
-
Now imagine if our App is more complex with lots of dependencies themselves depending on `C` (either directly or transitively) - it becomes extremely difficult to upgrade any dependencies because it now
79
-
pulls in a version of `C` that is incompatible with the rest of the versions of `C` in our dependency tree! In the example below, we cannot upgrade `D` because it will transitively pull in `C v2.0.0`, causing breakages
116
+
Now imagine if `App` is more complex with lots of dependencies themselves depending on `C` (either directly or transitively) - it becomes extremely difficult to upgrade any dependencies because it now
117
+
pulls in a version of `C` that is incompatible with the rest of `C` versions in our dependency tree!
118
+
119
+
In the example below, we cannot upgrade to `D v1.1.1` because it will transitively pull in `C v2.0.0`, causing breakages
80
120
due to binary incompatibility. This inability to upgrade any packages without breaking anything is commonly known as **Dependency Hell**.
@@ -87,7 +127,7 @@ How can we, as library authors, spare our users of runtime errors and dependency
87
127
***Avoid breaking binary compatibility** through careful design and evolution of your library interfaces
88
128
* Communicate binary compatibility breakages clearly through **versioning**
89
129
90
-
## MiMa - Check binary compatibility with previous library versions
130
+
## MiMa - Checking binary compatibility against previous library versions
91
131
92
132
The [Migration Manager for Scala](https://github.com/typesafehub/migration-manager) (MiMa) is a tool for diagnosing binary incompatibilities between different library versions.
93
133
It works by comparing the class files of two provided JARs and report any binary incompatibilities found.
@@ -117,15 +157,21 @@ For brevity of this guide, detailed explanation and runnable code examples can b
117
157
118
158
Again, we recommend using MiMa to double check that you have not broken binary compatibility after making changes.
Library authors use verioning schemes to communicate compatibility guarantees between library releases to their users. Versioning schemes like [Semantic Versioning](http://semver.org/)(SemVer) allow
163
+
users to easily reason about the impact of a updating a library, without needing to read the detailed release note.
164
+
165
+
In the following section we will outline a versioning scheme based on Semantic Versioning that we **strongly encourage** you to adopt for your libraries. The rules listed below are **in addition** to
166
+
Semantic Versioning v2.0.0.
121
167
122
168
### Recommmended Versioning Scheme
123
-
We recommend using the following schemes to communicate binary and source compatibility to your users:
124
169
125
-
* Any release with the same major version are **Binary Backwards Compatible** with each other
126
-
* A minor version bump signals new features and **may contain minor source incompatibilities** that can be easily fixed by the end user
127
-
* Patch version for bug fixes and minor behavioral changes
128
-
* For **experimental library versions** (where the major version is `0`, such as `v0.1.0`), a minor version bump **may contain both source and binary breakages**
170
+
* If backwards **binary compatibility** is broken, **major version number** must be increased
171
+
* If backwards **source compatibility** is broken, **minor version number** must be increased
172
+
* A change in **patch version number** signals **no binary nor source incompatibility**. According to SemVer, patch versions should contain only bug fixes that fixes incorrect behavior so major behavioral
173
+
change in method/classes should result in a minor version bump.
174
+
* When major version is `0`, a minor version bump **may contain both source and binary breakages**
129
175
* Some libraries may take a harder stance on maintaining source compatibility, bumping the major version number for ANY source incompatibility even if they are binary compatible
130
176
131
177
Some examples:
@@ -139,9 +185,14 @@ Some examples:
139
185
Many libraries in the Scala ecosystem has adopted this versioning scheme. A few examples are [Akka](http://doc.akka.io/docs/akka/2.5/scala/common/binary-compatibility-rules.html),
140
186
[Cats](https://github.com/typelevel/cats#binary-compatibility-and-versioning) and [Scala.js](https://www.scala-js.org/).
141
187
188
+
If this version scheme is followed, reasoning about binary compatibility is now very simple:
189
+
190
+
* Ensure major versions of the all versions of a library in the dependency tree are the same
191
+
* Pick latest version and evict the rest (This is the default behavior of SBT).
192
+
142
193
### Explanation
143
194
144
-
Why do we use the major version number to signal binary compatibility releases?
195
+
Why do we use the major version number to signal binary incompatible releases?
145
196
146
197
From our [example](#why-binary-compatibility-matters) above, we have learned two important lessons:
147
198
@@ -156,5 +207,5 @@ is also important, if they are minor breakages that does not require effort to f
156
207
In this guide we covered the importance of binary compatibility and showed you a few tricks to avoid breaking binary compatibility. Finally, we laid out a versioning scheme to communicate
157
208
binary compatibility breakages clearly to your users.
158
209
159
-
If we follow these guidelines, we as a community can spend less time untangling dependency hell and more time making cool things!
210
+
If we follow these guidelines, we as a community can spend less time untangling dependency hell and more time building cool things!
0 commit comments