Skip to content

Commit 96dcaea

Browse files
authored
Merge pull request #823 from kubukoz/add-mima-cli
Add MiMa CLI
2 parents 66efdf1 + 3b218c0 commit 96dcaea

File tree

4 files changed

+299
-4
lines changed

4 files changed

+299
-4
lines changed

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,55 @@ import com.github.lolgab.mill.mima._
8989

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

92+
### CLI
93+
94+
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.
95+
96+
You can launch it with Coursier:
97+
98+
```bash
99+
cs launch com.typesafe:mima-cli_3:latest.release -- old.jar new.jar
100+
```
101+
102+
Or create a reusable script:
103+
104+
```bash
105+
cs bootstrap com.typesafe:mima-cli_3:latest.release --output mima
106+
./mima old.jar new.jar
107+
```
108+
109+
Here are the usage instructions:
110+
111+
```
112+
Usage:
113+
114+
mima [OPTIONS] oldfile newfile
115+
116+
oldfile: Old (or, previous) files - a JAR or a directory containing classfiles
117+
newfile: New (or, current) files - a JAR or a directory containing classfiles
118+
119+
Options:
120+
-cp CLASSPATH:
121+
Specify Java classpath, separated by ':'
122+
123+
-v, --verbose:
124+
Show a human-readable description of each problem
125+
126+
-f, --forward-only:
127+
Show only forward-binary-compatibility problems
128+
129+
-b, --backward-only:
130+
Show only backward-binary-compatibility problems
131+
132+
-g, --include-generics:
133+
Include generic signature problems, which may not directly cause bincompat
134+
problems and are hidden by default. Has no effect if using --forward-only.
135+
136+
-j, --bytecode-names:
137+
Show bytecode names of fields and methods, rather than human-readable names
138+
```
139+
140+
92141
## Filtering binary incompatibilities
93142

94143
When MiMa reports a binary incompatibility that you consider acceptable, such as a change in an internal package,

build.sbt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ val root = project.in(file(".")).settings(
5656
mimaFailOnNoPrevious := false,
5757
publish / skip := true,
5858
)
59-
aggregateProjects(core.jvm, core.native, sbtplugin, functionalTests)
59+
aggregateProjects(core.jvm, core.native, cli.jvm, sbtplugin, functionalTests)
6060

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

@@ -65,7 +65,6 @@ val core = crossProject(JVMPlatform, NativePlatform).crossType(CrossType.Pure).s
6565
crossScalaVersions ++= Seq(scala213, scala3),
6666
scalacOptions ++= compilerOptions(scalaVersion.value),
6767
libraryDependencies += munit.value % Test,
68-
testFrameworks += new TestFramework("munit.Framework"),
6968
MimaSettings.mimaSettings,
7069
apiMappings ++= {
7170
// WORKAROUND https://github.com/scala/bug/issues/9311
@@ -77,9 +76,22 @@ val core = crossProject(JVMPlatform, NativePlatform).crossType(CrossType.Pure).s
7776
}
7877
.toMap
7978
},
80-
8179
).nativeSettings(mimaPreviousArtifacts := Set.empty)
8280

81+
val cli = crossProject(JVMPlatform)
82+
.crossType(CrossType.Pure)
83+
.settings(
84+
name := "mima-cli",
85+
crossScalaVersions ++= Seq(scala3),
86+
scalacOptions ++= compilerOptions(scalaVersion.value),
87+
libraryDependencies += munit.value % Test,
88+
MimaSettings.mimaSettings,
89+
// cli has no previous release,
90+
// but also we don't care about its binary compatibility as it's meant to be used standalone
91+
mimaPreviousArtifacts := Set.empty
92+
)
93+
.dependsOn(core)
94+
8395
val sbtplugin = project.enablePlugins(SbtPlugin).dependsOn(core.jvm).settings(
8496
name := "sbt-mima-plugin",
8597
scalacOptions ++= compilerOptions(scalaVersion.value),
@@ -99,7 +111,6 @@ val functionalTests = Project("functional-tests", file("functional-tests"))
99111
libraryDependencies += "io.get-coursier" %% "coursier" % "2.1.24",
100112
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
101113
libraryDependencies += munit.value,
102-
testFrameworks += new TestFramework("munit.Framework"),
103114
scalacOptions ++= compilerOptions(scalaVersion.value),
104115
//Test / run / fork := true,
105116
//Test / run / forkOptions := (Test / run / forkOptions).value.withWorkingDirectory((ThisBuild / baseDirectory).value),
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.typesafe.tools.mima.cli
2+
3+
import com.typesafe.tools.mima.lib.MiMaLib
4+
5+
import java.io.File
6+
import scala.annotation.tailrec
7+
8+
case class Main(
9+
classpath: Seq[File] = Nil,
10+
oldBinOpt: Option[File] = None,
11+
newBinOpt: Option[File] = None,
12+
formatter: ProblemFormatter = ProblemFormatter()
13+
) {
14+
15+
def run(): Int = {
16+
val oldBin = oldBinOpt.getOrElse(
17+
throw new IllegalArgumentException("Old binary was not specified")
18+
)
19+
val newBin = newBinOpt.getOrElse(
20+
throw new IllegalArgumentException("New binary was not specified")
21+
)
22+
val problems = new MiMaLib(classpath)
23+
.collectProblems(oldBin, newBin, Nil)
24+
.flatMap(formatter.formatProblem)
25+
problems.foreach(println)
26+
problems.size
27+
}
28+
29+
}
30+
31+
object Main {
32+
33+
def main(args: Array[String]): Unit =
34+
try System.exit(parseArgs(args.toList, Main()).run())
35+
catch {
36+
case err: IllegalArgumentException =>
37+
println(err.getMessage())
38+
printUsage()
39+
}
40+
41+
def printUsage(): Unit = println(
42+
s"""Usage:
43+
|
44+
|mima [OPTIONS] oldfile newfile
45+
|
46+
| oldfile: Old (or, previous) files - a JAR or a directory containing classfiles
47+
| newfile: New (or, current) files - a JAR or a directory containing classfiles
48+
|
49+
|Options:
50+
| -cp CLASSPATH:
51+
| Specify Java classpath, separated by '${File.pathSeparatorChar}'
52+
|
53+
| -v, --verbose:
54+
| Show a human-readable description of each problem
55+
|
56+
| -f, --forward-only:
57+
| Show only forward-binary-compatibility problems
58+
|
59+
| -b, --backward-only:
60+
| Show only backward-binary-compatibility problems
61+
|
62+
| -g, --include-generics:
63+
| Include generic signature problems, which may not directly cause bincompat
64+
| problems and are hidden by default. Has no effect if using --forward-only.
65+
|
66+
| -j, --bytecode-names:
67+
| Show bytecode names of fields and methods, rather than human-readable names
68+
|
69+
|""".stripMargin
70+
)
71+
72+
@tailrec
73+
private def parseArgs(remaining: List[String], current: Main): Main =
74+
remaining match {
75+
case Nil => current
76+
case ("-cp" | "--classpath") :: cpStr :: rest =>
77+
parseArgs(
78+
rest,
79+
current.copy(classpath =
80+
cpStr.split(File.pathSeparatorChar).toSeq.map(new File(_))
81+
)
82+
)
83+
84+
case ("-f" | "--forward-only") :: rest =>
85+
parseArgs(
86+
rest,
87+
current.copy(formatter =
88+
current.formatter.copy(showForward = true, showBackward = false)
89+
)
90+
)
91+
92+
case ("-b" | "--backward-only") :: rest =>
93+
parseArgs(
94+
rest,
95+
current.copy(formatter =
96+
current.formatter.copy(showForward = false, showBackward = true)
97+
)
98+
)
99+
100+
case ("-j" | "--bytecode-names") :: rest =>
101+
parseArgs(
102+
rest,
103+
current.copy(formatter =
104+
current.formatter.copy(useBytecodeNames = true)
105+
)
106+
)
107+
108+
case ("-v" | "--verbose") :: rest =>
109+
parseArgs(
110+
rest,
111+
current.copy(formatter =
112+
current.formatter.copy(showDescriptions = true)
113+
)
114+
)
115+
116+
case ("-g" | "--include-generics") :: rest =>
117+
parseArgs(
118+
rest,
119+
current.copy(formatter =
120+
current.formatter.copy(showIncompatibleSignature = true)
121+
)
122+
)
123+
124+
case filename :: rest if current.oldBinOpt.isEmpty =>
125+
parseArgs(rest, current.copy(oldBinOpt = Some(new File(filename))))
126+
case filename :: rest if current.newBinOpt.isEmpty =>
127+
parseArgs(rest, current.copy(newBinOpt = Some(new File(filename))))
128+
case wut :: _ =>
129+
throw new IllegalArgumentException(s"Unknown argument $wut")
130+
}
131+
132+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.typesafe.tools.mima.cli
2+
3+
import com.typesafe.tools.mima.core.AbstractMethodProblem
4+
import com.typesafe.tools.mima.core.DirectMissingMethodProblem
5+
import com.typesafe.tools.mima.core.FinalMethodProblem
6+
import com.typesafe.tools.mima.core.InaccessibleFieldProblem
7+
import com.typesafe.tools.mima.core.InaccessibleMethodProblem
8+
import com.typesafe.tools.mima.core.IncompatibleFieldTypeProblem
9+
import com.typesafe.tools.mima.core.IncompatibleMethTypeProblem
10+
import com.typesafe.tools.mima.core.IncompatibleResultTypeProblem
11+
import com.typesafe.tools.mima.core.IncompatibleSignatureProblem
12+
import com.typesafe.tools.mima.core.MemberInfo
13+
import com.typesafe.tools.mima.core.MemberProblem
14+
import com.typesafe.tools.mima.core.MissingFieldProblem
15+
import com.typesafe.tools.mima.core.MissingMethodProblem
16+
import com.typesafe.tools.mima.core.NewMixinForwarderProblem
17+
import com.typesafe.tools.mima.core.Problem
18+
import com.typesafe.tools.mima.core.ReversedAbstractMethodProblem
19+
import com.typesafe.tools.mima.core.ReversedMissingMethodProblem
20+
import com.typesafe.tools.mima.core.TemplateProblem
21+
import com.typesafe.tools.mima.core.UpdateForwarderBodyProblem
22+
23+
case class ProblemFormatter(
24+
showForward: Boolean = true,
25+
showBackward: Boolean = true,
26+
showIncompatibleSignature: Boolean = false,
27+
useBytecodeNames: Boolean = false,
28+
showDescriptions: Boolean = false
29+
) {
30+
31+
private def str(problem: TemplateProblem): String =
32+
s"${if (useBytecodeNames) problem.ref.bytecodeName
33+
else problem.ref.fullName}: ${problem.getClass.getSimpleName.stripSuffix("Problem")}${description(problem)}"
34+
35+
private def str(problem: MemberProblem): String =
36+
s"${memberName(problem.ref)}: ${problem.getClass.getSimpleName.stripSuffix("Problem")}${description(problem)}"
37+
38+
private def description(problem: Problem): String =
39+
if (showDescriptions) ": " + problem.description("new") else ""
40+
41+
private def memberName(info: MemberInfo): String =
42+
if (useBytecodeNames)
43+
bytecodeFullName(info)
44+
else
45+
info.fullName
46+
47+
private def bytecodeFullName(info: MemberInfo): String = {
48+
val pkg = info.owner.owner.fullName.replace('.', '/')
49+
val clsName = info.owner.bytecodeName
50+
val memberName = info.bytecodeName match {
51+
case "<init>" => "\"<init>\""
52+
case name => name
53+
}
54+
val sig = info.descriptor
55+
56+
s"$pkg/$clsName.$memberName$sig"
57+
}
58+
59+
// format: off
60+
def formatProblem(problem: Problem): Option[String] = problem match {
61+
case prob: TemplateProblem if showBackward => Some(str(prob))
62+
case _: TemplateProblem => None
63+
64+
case problem: MemberProblem => problem match {
65+
case prob: AbstractMethodProblem if showBackward => Some(str(prob))
66+
case _: AbstractMethodProblem => None
67+
68+
case problem: MissingMethodProblem => problem match {
69+
case prob: DirectMissingMethodProblem if showBackward => Some(str(prob))
70+
case _: DirectMissingMethodProblem => None
71+
case prob: ReversedMissingMethodProblem if showForward => Some(str(prob))
72+
case _: ReversedMissingMethodProblem => None
73+
}
74+
75+
case prob: ReversedAbstractMethodProblem if showForward => Some(str(prob))
76+
case _: ReversedAbstractMethodProblem => None
77+
case prob: MissingFieldProblem if showBackward => Some(str(prob))
78+
case _: MissingFieldProblem => None
79+
case prob: InaccessibleFieldProblem if showBackward => Some(str(prob))
80+
case _: InaccessibleFieldProblem => None
81+
case prob: IncompatibleFieldTypeProblem if showBackward => Some(str(prob))
82+
case _: IncompatibleFieldTypeProblem => None
83+
case prob: InaccessibleMethodProblem if showBackward => Some(str(prob))
84+
case _: InaccessibleMethodProblem => None
85+
case prob: IncompatibleMethTypeProblem if showBackward => Some(str(prob))
86+
case _: IncompatibleMethTypeProblem => None
87+
case prob: IncompatibleResultTypeProblem if showBackward => Some(str(prob))
88+
case _: IncompatibleResultTypeProblem => None
89+
case prob: FinalMethodProblem if showBackward => Some(str(prob))
90+
case _: FinalMethodProblem => None
91+
case prob: UpdateForwarderBodyProblem if showBackward => Some(str(prob))
92+
case _: UpdateForwarderBodyProblem => None
93+
case prob: NewMixinForwarderProblem if showBackward => Some(str(prob))
94+
case _: NewMixinForwarderProblem => None
95+
96+
case prob: IncompatibleSignatureProblem
97+
if showBackward && showIncompatibleSignature => Some(str(prob))
98+
case _: IncompatibleSignatureProblem => None
99+
}
100+
}
101+
// format: on
102+
103+
}

0 commit comments

Comments
 (0)