Skip to content

Commit

Permalink
Add MiMa CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz committed Sep 2, 2024
1 parent dd8804b commit 0e66c3f
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 4 deletions.
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.0.0")

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(scala213, scala3),
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.10",
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
133 changes: 133 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,133 @@
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")
)
// TODO: should have some machine-readable output here, as an option
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

}

0 comments on commit 0e66c3f

Please sign in to comment.