Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MiMa CLI #823

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
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(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

}