MiMa (for "Migration Manager") is a tool for identifying binary incompatibilities in Scala libraries.
It's pronounced MEE-ma.
MiMa compares the classfiles of two versions of a library. It reports changes that make
code compiled against the old version fail with the new one, with a LinkageError such as
NoSuchMethodError or AbstractMethodError.
MiMa does not check behaviour: a method that keeps its signature but does something else is not reported. It does not check source compatibility either: a binary compatible change can still stop client code from compiling.
MiMa checks your binary API as Scala code sees it. Some changes break only Java clients, and MiMa does not report them:
private[foo]definitions and nestedprivateclasses are public in bytecode. Java code can use them, MiMa ignores them. See Qualified private definitions.- Java code can implement a sealed type. MiMa does not report new abstract methods in a sealed hierarchy. See Sealing.
- Java calls an object's methods through static forwarders in the companion class. MiMa checks the object's methods, not the forwarders. A forwarder can disappear while the method stays.
- A class gets a public copy of each
private[foo]method of its traits. MiMa ignores these. - MiMa ignores methods with a
$in their name, except extension methods, default argument getters and$init$. Scala code cannot see them. - Generic signatures, which javac uses, are only compared on request. See IncompatibleSignatureProblem.
Scala 3 compiles against TASTy, which holds more than the bytecode: exact Scala types,
and the bodies of inline methods.
TASTy-MiMa checks TASTy compatibility the way
MiMa checks bytecode. Use both for a Scala 3 library.
MiMa checks Scala 2.12, 2.13 and 3 artifacts. Its sbt plugin supports sbt 1.x and sbt 2.0.7 or newer.
To use it add the following to your project/plugins.sbt file:
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "<version>")
Add the following to your build.sbt file:
mimaPreviousArtifacts := Set("com.example" %% "my-library" % "<version>")
and run mimaReportBinaryIssues to see something like the following:
[error] my-library: Failed binary compatibility check against com.example:my-library_3:1.0.0! Found 2 potential problems
[error] * method close()Unit in class com.example.Resource does not have a correspondent in current version
[error] * abstract method reset()Unit in trait com.example.Pool is present only in current version
[error] To accept the incompatible changes above, add the lines below to mimaBinaryIssueFilters, or to src/main/mima-filters/<version>.backwards.excludes.
[error] ProblemFilters.exclude[DirectMissingMethodProblem]("com.example.Resource.close()Unit"),
[error] ProblemFilters.exclude[ReversedMissingMethodProblem]("com.example.Pool.reset()Unit"),
Each problem comes with a filter to accept it, see Filtering binary incompatibilities.
MiMa checks backward compatibility, that code compiled against the previous version keeps
working. mimaCheckDirection := "forward" checks the other direction, "both" checks both.
A MiMa plugin for Mill is maintained at lolgab/mill-mima.
To use it add the following to your build.sc:
import $ivy.`com.github.lolgab::mill-mima::x.y.z`
import com.github.lolgab.mill.mima._Please check this page for further information.
The command-line interface compares two jars directly. Launch it with Coursier:
cs launch com.typesafe:mima-cli_3:latest.release -- old.jar new.jarOr create a reusable script:
cs bootstrap com.typesafe:mima-cli_3:latest.release --output mima
./mima old.jar new.jarHere are the usage instructions:
Usage:
mima [OPTIONS] oldfile newfile
oldfile: Old (or, previous) files - a JAR or a directory containing classfiles
newfile: New (or, current) files - a JAR or a directory containing classfiles
Options:
-cp CLASSPATH:
Specify Java classpath, separated by ':'
-v, --verbose:
Show a human-readable description of each problem
-f, --forward-only:
Show only forward-binary-compatibility problems
-b, --backward-only:
Show only backward-binary-compatibility problems
-g, --include-generics:
Include generic signature problems, which may not directly cause bincompat
problems and are hidden by default. Has no effect if using --forward-only.
-j, --bytecode-names:
Show bytecode names of fields and methods, rather than human-readable names
-s, --suggestions:
Print the lines to add to a build to accept the problems
Exit code: 0 if no problems were found, 1 if there were, 2 for a usage error.
A problem you accept, such as a change in an internal package, goes in
mimaBinaryIssueFilters:
import com.typesafe.tools.mima.core._
mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[MissingClassProblem]("com.example.mylibrary.internal.Foo"),
)* stands for any part of a name, and Problem for any kind of problem:
mimaBinaryIssueFilters += ProblemFilters.exclude[Problem]("com.example.mylibrary.internal.*")Names are the ones the report prints, signature included, so a filter covers one method:
ProblemFilters.exclude[DirectMissingMethodProblem]("com.example.mylibrary.Foo.bar(Int)Int")Leaving the signature out covers every overload of bar.
Filters can live in src/main/mima-filters instead of the build, one exclude per line,
# or // for comments. A file is named after the release its filters are for:
1.1.0.backwards.excludes applies while mimaPreviousArtifacts names 1.1.0 or anything
before it, and is ignored once it moves past. forwards and both name the other
directions, and 1.1.x stands for every patch release of 1.1.
A line pasted from the report, trailing comma and all, is accepted.
Most MiMa checks compare erased types. def names: List[String] and
def names: List[Int] look the same to them, yet a client compiled against the first
casts each element to String and fails with a ClassCastException.
The compiler also writes a Java generic signature, the classfile's Signature attribute,
which maps the Scala type as far as Java generics can express it. MiMa compares it with
ThisBuild / mimaReportSignatureProblems := trueor -g on the command line. It reports differences as IncompatibleSignatureProblem,
and changed type arguments of a parent class as IncompatibleClassSignatureProblem:
class Base[T](val value: T)
class Public extends Base[String]("hi") // changed to Base[Integer]The check is off by default because the mapping is imperfect:
- It misses changes. A primitive type argument is written as
Object, soList[Int]toList[Long]looks unchanged, though a client that unboxes the element fails. - It reports changes that break nothing. Only a client that casts the value fails, and
some
Signaturechanges are not type changes: Scala 2.12.9 and 2.13.1 changed how value classes are written, see #423.
For Scala 3, TASTy-MiMa compares the Scala types themselves.
private[foo] is a Scala rule, not a JVM one. In bytecode these definitions are
public, so a client can end up depending on one even though it cannot name it.
MiMa ignores a qualified-private member, such as private[foo] def: no Scala code
outside foo can call it. Narrowing a public member to private[foo] is reported,
though, as MethodNoLongerCheckedProblem: an already-compiled caller keeps linking, but
the member has left the API, and MiMa stops watching it from here on, so a later
removal would go unreported.
protected[foo] is not qualified private: a subclass anywhere can still reach it, so
MiMa treats it like plain protected and keeps checking it.
A private member marked @publicInBinary
(Scala 3.4+) is the exception: an inline method outside the scope calls it directly
once inlined, so MiMa checks it like a public member. Narrowing a public member to
@publicInBinary private[foo] keeps it in the binary API and is not reported; dropping
the annotation is.
A nested private class is emitted ACC_PUBLIC too, and MiMa reads the same rules from
the pickle: no Scala code outside the enclosing class can name it, so it is ignored, and
narrowing a public nested class to private is reported the same way as narrowing it
to private[foo].
A qualified-private class is checked only when it escapes, e.g., a public method returns it, a public field holds it, or a public class extends it:
package foo
private[foo] class C { def bar(x: Int) = x }
object Lib { def go: C = new C }C escapes through Lib.go, so a client can write Lib.go.bar(1), and changing
bar breaks it. MiMa reports changes to C and to its public members, and every report
says how the class escapes, since the class alone does not show it:
method bar(Int)Int in private[..] class foo.C does not have a correspondent in new version (foo.C escapes through foo.Lib.go)
A qualified-private class that never reaches a public signature is ignored.
Losing that last escape route is reported, as ClassNoLongerCheckedProblem:
dropping Lib.go, or narrowing a public class to private[foo] in the first place,
takes the class out of MiMa's sight, so nothing that happens to it afterwards can be
reported. Make the class public, or filter the problem if it really is internal from
here on.
A class that becomes private[foo] while it still escapes is not reported, because
MiMa carries on checking it just the same. An escape is a leak to close, not a
substitute for making the class public.
Escape detection reads the bytecode and the pickle, so it sees a class that leaks only through a type alias or the bound of an abstract type member, neither of which the classfile mentions:
object t {
private[t] class C
type K = C // no mention of K in the classfile
type L <: C // nor of L
}To make MiMa treat qualified private declarations as private, including the
public members of a qualified private class, filter on
Problem.isExternallyAccessible:
import com.typesafe.tools.mima.core._
mimaBinaryIssueFilters += { (p: Problem) => p.isExternallyAccessible }This drops the escaping classes too, so it can hide a real break: in the example
above it would silence the report about C.bar.
MiMa reports a change that would break a client implementing one of your types (a new
abstract method, a newly inherited one) only while a client can still write that
implementation. Once a type is sealed and every one of its subtypes is closed
(final, sealed, or no longer nameable from outside), no new Scala code can, so
those checks stop. Java code still can, since javac ignores sealed.
Clients that implemented it while it was open still exist, though, so the version
that closes the hierarchy is reported, as HierarchyNoLongerCheckedProblem: it is the
last one at which MiMa can tell you that an abstract method you add would break them.
Sealing a trait closes a hierarchy; so does making its last open subclass final or
private[foo].
Narrowing a public definition to private[foo], or closing a hierarchy, is binary compatible: the
bytecode stays public. But MiMa reports anyway, as MethodNoLongerCheckedProblem,
ClassNoLongerCheckedProblem or HierarchyNoLongerCheckedProblem, because from that version on
it stops watching the definition, while code compiled against an earlier release still calls it.
A later removal would go unreported.
Filtering such a report would be counter-productive. It silences the notice, the definition
stays out of sight, and once mimaPreviousArtifacts moves to a new version, the filter is dead.
mimaBinaryApi solves this problem:
mimaBinaryApi += BinaryApi.keep[MethodNoLongerCheckedProblem]("foo.C.bar(Int)Int")MiMa now checks bar as it checks a public method, and the message about narrowing is silenced.
The keep entry names the kind of problem MiMa continues checking:
| entry | keeps |
|---|---|
keep[ClassNoLongerCheckedProblem]("foo.C") |
the type checked, with its members and its accessible nested classes |
keep[MethodNoLongerCheckedProblem]("foo.C.bar(Int)Int") |
the method checked |
keep[HierarchyNoLongerCheckedProblem]("foo.T") |
an abstract method added to the type later reported |
Names work as in filters, and * stands for any part of a name. An object has two classes,
O and O$, so keeping it takes two entries; keeping a method of an object takes one. Entries
can also go in src/main/mima-filters/binary-api, one per line, # or // for comments.
A method entry includes the signature, so it keeps one method. Without it, the entry keeps every overload of that name, including any that was never part of the API, and removing one of those is then reported.
A keep entry that is unused is reported as having no effect. It can either be a typo, or that the
named definition is public again, or gone.
On Scala 3.4+,
@publicInBinary says the same about a method
at the definition site, and MiMa honours it.
mimaExcludeAnnotations makes MiMa ignore classes, objects, methods and vals that carry
one of the given annotations, such as an "experimental" or "internal" marker:
mimaExcludeAnnotations += "scala.annotation.experimental"The annotation is read from the version being checked against, so the release that adds it is still checked in full; every release after it skips the annotated definition entirely, with no report.
On Scala 2, this works for classes and objects only, not for methods and vals.
It can depend on other settings. For example, a cross-built project with no Scala 3 release yet has nothing to compare its Scala 3 build against:
mimaPreviousArtifacts := {
if (scalaBinaryVersion.value == "3") Set.empty
else Set("com.example" %% "my-library" % "1.2.3")
}To name the project itself, use moduleName, not name: it escapes characters like .,
and is the name publish uses, so it is also what your users depend on.
mimaPreviousArtifacts := Set(organization.value %% moduleName.value % "0.1.0")mimaReportBinaryIssues fails on a problem, and also when mimaPreviousArtifacts is
empty, so that a project does not go unchecked by accident. To report problems without
failing, set mimaFailOnProblem := false. For a project with nothing to compare against,
set mimaFailOnNoPrevious := false, or disablePlugins(MimaPlugin). Setting it on
ThisBuild does that for the whole build.