Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,55 @@ import com.github.lolgab.mill.mima._

Please check [this page](https://github.com/lolgab/mill-mima) for further information.

### CLI

You can use MiMa using its command-line interface - it's the most straightforward way to compare two jars and see some human-readable descriptions of the issues.

You can launch it with Coursier:

```bash
cs launch com.typesafe:mima-cli_3:latest.release -- old.jar new.jar
```

Or create a reusable script:

```bash
cs bootstrap com.typesafe:mima-cli_3:latest.release --output mima
./mima old.jar new.jar
```

Here 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
```


## Filtering binary incompatibilities

When MiMa reports a binary incompatibility that you consider acceptable, such as a change in an internal package,
Expand Down
19 changes: 15 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ val root = project.in(file(".")).settings(
mimaFailOnNoPrevious := false,
publish / skip := true,
)
aggregateProjects(core.jvm, core.native, sbtplugin, functionalTests)
aggregateProjects(core.jvm, core.native, cli.jvm, sbtplugin, functionalTests)

val munit = Def.setting("org.scalameta" %%% "munit" % "1.1.1")

Expand All @@ -65,7 +65,6 @@ val core = crossProject(JVMPlatform, NativePlatform).crossType(CrossType.Pure).s
crossScalaVersions ++= Seq(scala213, scala3),
scalacOptions ++= compilerOptions(scalaVersion.value),
libraryDependencies += munit.value % Test,
testFrameworks += new TestFramework("munit.Framework"),
MimaSettings.mimaSettings,
apiMappings ++= {
// WORKAROUND https://github.com/scala/bug/issues/9311
Expand All @@ -77,9 +76,22 @@ val core = crossProject(JVMPlatform, NativePlatform).crossType(CrossType.Pure).s
}
.toMap
},

).nativeSettings(mimaPreviousArtifacts := Set.empty)

val cli = crossProject(JVMPlatform)
.crossType(CrossType.Pure)
.settings(
name := "mima-cli",
crossScalaVersions ++= Seq(scala3),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, was the intention here to set crossScalaVersions := Seq(scala3)? because with ++= we get

sbt:mima> show cliJVM/crossScalaVersions
[info] * 2.12.20
[info] * 3.3.6

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, oversight. I'll come back with a fix

scalacOptions ++= compilerOptions(scalaVersion.value),
libraryDependencies += munit.value % Test,
MimaSettings.mimaSettings,
// cli has no previous release,
// but also we don't care about its binary compatibility as it's meant to be used standalone
mimaPreviousArtifacts := Set.empty
)
.dependsOn(core)

val sbtplugin = project.enablePlugins(SbtPlugin).dependsOn(core.jvm).settings(
name := "sbt-mima-plugin",
scalacOptions ++= compilerOptions(scalaVersion.value),
Expand All @@ -99,7 +111,6 @@ val functionalTests = Project("functional-tests", file("functional-tests"))
libraryDependencies += "io.get-coursier" %% "coursier" % "2.1.24",
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
libraryDependencies += munit.value,
testFrameworks += new TestFramework("munit.Framework"),
scalacOptions ++= compilerOptions(scalaVersion.value),
//Test / run / fork := true,
//Test / run / forkOptions := (Test / run / forkOptions).value.withWorkingDirectory((ThisBuild / baseDirectory).value),
Expand Down
132 changes: 132 additions & 0 deletions cli/src/main/scala/com/typesafe/tools/mima/cli/MimaCli.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.typesafe.tools.mima.cli

import com.typesafe.tools.mima.lib.MiMaLib

import java.io.File
import scala.annotation.tailrec

case class Main(
classpath: Seq[File] = Nil,
oldBinOpt: Option[File] = None,
newBinOpt: Option[File] = None,
formatter: ProblemFormatter = ProblemFormatter()
) {

def run(): Int = {
val oldBin = oldBinOpt.getOrElse(
throw new IllegalArgumentException("Old binary was not specified")
)
val newBin = newBinOpt.getOrElse(
throw new IllegalArgumentException("New binary was not specified")
)
val problems = new MiMaLib(classpath)
.collectProblems(oldBin, newBin, Nil)
.flatMap(formatter.formatProblem)
problems.foreach(println)
problems.size
}

}

object Main {

def main(args: Array[String]): Unit =
try System.exit(parseArgs(args.toList, Main()).run())
catch {
case err: IllegalArgumentException =>
println(err.getMessage())
printUsage()
}

def printUsage(): Unit = println(
s"""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 '${File.pathSeparatorChar}'
|
| -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
|
|""".stripMargin
)

@tailrec
private def parseArgs(remaining: List[String], current: Main): Main =
remaining match {
case Nil => current
case ("-cp" | "--classpath") :: cpStr :: rest =>
parseArgs(
rest,
current.copy(classpath =
cpStr.split(File.pathSeparatorChar).toSeq.map(new File(_))
)
)

case ("-f" | "--forward-only") :: rest =>
parseArgs(
rest,
current.copy(formatter =
current.formatter.copy(showForward = true, showBackward = false)
)
)

case ("-b" | "--backward-only") :: rest =>
parseArgs(
rest,
current.copy(formatter =
current.formatter.copy(showForward = false, showBackward = true)
)
)

case ("-j" | "--bytecode-names") :: rest =>
parseArgs(
rest,
current.copy(formatter =
current.formatter.copy(useBytecodeNames = true)
)
)

case ("-v" | "--verbose") :: rest =>
parseArgs(
rest,
current.copy(formatter =
current.formatter.copy(showDescriptions = true)
)
)

case ("-g" | "--include-generics") :: rest =>
parseArgs(
rest,
current.copy(formatter =
current.formatter.copy(showIncompatibleSignature = true)
)
)

case filename :: rest if current.oldBinOpt.isEmpty =>
parseArgs(rest, current.copy(oldBinOpt = Some(new File(filename))))
case filename :: rest if current.newBinOpt.isEmpty =>
parseArgs(rest, current.copy(newBinOpt = Some(new File(filename))))
case wut :: _ =>
throw new IllegalArgumentException(s"Unknown argument $wut")
}

}
103 changes: 103 additions & 0 deletions cli/src/main/scala/com/typesafe/tools/mima/cli/ProblemFormatter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.typesafe.tools.mima.cli

import com.typesafe.tools.mima.core.AbstractMethodProblem
import com.typesafe.tools.mima.core.DirectMissingMethodProblem
import com.typesafe.tools.mima.core.FinalMethodProblem
import com.typesafe.tools.mima.core.InaccessibleFieldProblem
import com.typesafe.tools.mima.core.InaccessibleMethodProblem
import com.typesafe.tools.mima.core.IncompatibleFieldTypeProblem
import com.typesafe.tools.mima.core.IncompatibleMethTypeProblem
import com.typesafe.tools.mima.core.IncompatibleResultTypeProblem
import com.typesafe.tools.mima.core.IncompatibleSignatureProblem
import com.typesafe.tools.mima.core.MemberInfo
import com.typesafe.tools.mima.core.MemberProblem
import com.typesafe.tools.mima.core.MissingFieldProblem
import com.typesafe.tools.mima.core.MissingMethodProblem
import com.typesafe.tools.mima.core.NewMixinForwarderProblem
import com.typesafe.tools.mima.core.Problem
import com.typesafe.tools.mima.core.ReversedAbstractMethodProblem
import com.typesafe.tools.mima.core.ReversedMissingMethodProblem
import com.typesafe.tools.mima.core.TemplateProblem
import com.typesafe.tools.mima.core.UpdateForwarderBodyProblem

case class ProblemFormatter(
showForward: Boolean = true,
showBackward: Boolean = true,
showIncompatibleSignature: Boolean = false,
useBytecodeNames: Boolean = false,
showDescriptions: Boolean = false
) {

private def str(problem: TemplateProblem): String =
s"${if (useBytecodeNames) problem.ref.bytecodeName
else problem.ref.fullName}: ${problem.getClass.getSimpleName.stripSuffix("Problem")}${description(problem)}"

private def str(problem: MemberProblem): String =
s"${memberName(problem.ref)}: ${problem.getClass.getSimpleName.stripSuffix("Problem")}${description(problem)}"

private def description(problem: Problem): String =
if (showDescriptions) ": " + problem.description("new") else ""

private def memberName(info: MemberInfo): String =
if (useBytecodeNames)
bytecodeFullName(info)
else
info.fullName

private def bytecodeFullName(info: MemberInfo): String = {
val pkg = info.owner.owner.fullName.replace('.', '/')
val clsName = info.owner.bytecodeName
val memberName = info.bytecodeName match {
case "<init>" => "\"<init>\""
case name => name
}
val sig = info.descriptor

s"$pkg/$clsName.$memberName$sig"
}

// format: off
def formatProblem(problem: Problem): Option[String] = problem match {
case prob: TemplateProblem if showBackward => Some(str(prob))
case _: TemplateProblem => None

case problem: MemberProblem => problem match {
case prob: AbstractMethodProblem if showBackward => Some(str(prob))
case _: AbstractMethodProblem => None

case problem: MissingMethodProblem => problem match {
case prob: DirectMissingMethodProblem if showBackward => Some(str(prob))
case _: DirectMissingMethodProblem => None
case prob: ReversedMissingMethodProblem if showForward => Some(str(prob))
case _: ReversedMissingMethodProblem => None
}

case prob: ReversedAbstractMethodProblem if showForward => Some(str(prob))
case _: ReversedAbstractMethodProblem => None
case prob: MissingFieldProblem if showBackward => Some(str(prob))
case _: MissingFieldProblem => None
case prob: InaccessibleFieldProblem if showBackward => Some(str(prob))
case _: InaccessibleFieldProblem => None
case prob: IncompatibleFieldTypeProblem if showBackward => Some(str(prob))
case _: IncompatibleFieldTypeProblem => None
case prob: InaccessibleMethodProblem if showBackward => Some(str(prob))
case _: InaccessibleMethodProblem => None
case prob: IncompatibleMethTypeProblem if showBackward => Some(str(prob))
case _: IncompatibleMethTypeProblem => None
case prob: IncompatibleResultTypeProblem if showBackward => Some(str(prob))
case _: IncompatibleResultTypeProblem => None
case prob: FinalMethodProblem if showBackward => Some(str(prob))
case _: FinalMethodProblem => None
case prob: UpdateForwarderBodyProblem if showBackward => Some(str(prob))
case _: UpdateForwarderBodyProblem => None
case prob: NewMixinForwarderProblem if showBackward => Some(str(prob))
case _: NewMixinForwarderProblem => None

case prob: IncompatibleSignatureProblem
if showBackward && showIncompatibleSignature => Some(str(prob))
case _: IncompatibleSignatureProblem => None
}
}
// format: on

}