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 jardiff-sbt #52

Draft
wants to merge 1 commit 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ bin/
.history
.classpath
.project
.metals
.bloop
**/eclipse.sbt
**/.cache
/.idea/
42 changes: 33 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
val buildName = "jardiff"

inThisBuild(Seq[Setting[_]](
version := "1.0-SNAPSHOT",
version := "2.0.0-SNAPSHOT",
organization := "org.scala-lang",
scalaVersion := "2.13.0",
scalaVersion := "2.12.13",
startYear := Some(2017),
organizationName := "Lightbend Inc. <https://www.lightbend.com>",
licenses := List(("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0.txt"))),
Expand All @@ -17,18 +17,35 @@ inThisBuild(Seq[Setting[_]](

lazy val root = (
project.in(file("."))
aggregate(core)
aggregate(core, cli, sbtPlugin)
settings(
name := buildName,
skip in publish := true,
)
)

lazy val core = (
lazy val cli = (
project.
settings(
libraryDependencies ++= Seq(
"commons-cli" % "commons-cli" % "1.4",
"org.scalatest" %% "scalatest" % "3.1.1" % Test,
),
name := buildName + "-cli",
headerLicense := Some(HeaderLicense.Custom("Copyright (C) Lightbend Inc. <https://www.lightbend.com>")),
assemblyMergeStrategy in assembly := {
case "module-info.class" => MergeStrategy.discard
case "rootdoc.txt" => MergeStrategy.discard
case x => (assemblyMergeStrategy in assembly).value(x)
},
)
.dependsOn(core)
)

lazy val core = (
project.
settings(
libraryDependencies ++= Seq(
"org.ow2.asm" % "asm" % AsmVersion,
"org.ow2.asm" % "asm-util" % AsmVersion,
"org.scala-lang" % "scalap" % System.getProperty("scalap.version", scalaVersion.value),
Expand All @@ -40,12 +57,19 @@ lazy val core = (
),
name := buildName + "-core",
headerLicense := Some(HeaderLicense.Custom("Copyright (C) Lightbend Inc. <https://www.lightbend.com>")),
assemblyMergeStrategy in assembly := {
case "module-info.class" => MergeStrategy.discard
case "rootdoc.txt" => MergeStrategy.discard
case x => (assemblyMergeStrategy in assembly).value(x)
},
)
)

lazy val sbtPlugin =
project.
enablePlugins(SbtPlugin).
settings(
name := "sbt-jardiff",
headerLicense := Some(HeaderLicense.Custom("Copyright (C) Lightbend Inc. <https://www.lightbend.com>")),
scriptedLaunchOpts := { scriptedLaunchOpts.value ++
Seq("-Xmx1024M", "-Dplugin.version=" + version.value)
},
scriptedBufferLog := false
).dependsOn(core)

val AsmVersion = "7.2"
13 changes: 13 additions & 0 deletions cli/src/main/resources/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2017-2019 Lightbend, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
14 changes: 14 additions & 0 deletions cli/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="warn">
<appender-ref ref="STDOUT" />
</root>
</configuration>
13 changes: 8 additions & 5 deletions core/src/test/scala/scala/tools/jardiff/IOUtilSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import java.util.zip.ZipOutputStream
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.util.Using

final class IOUtilSpec extends AnyFlatSpec with Matchers {
behavior of "IOUtil.rootPath"

it should "handle jar path with spaces" in {
val jar = Files.createTempDirectory("app support").resolve("best project.jar")
Using.resource(new ZipOutputStream(Files.newOutputStream(jar)))(_.closeEntry()) // create jar
IOUtil.rootPath(jar).toString shouldBe "/"
IOUtil.rootPath(jar.resolve("foo/Bar.class")).toString shouldBe "/foo/Bar.class"
val stream = new ZipOutputStream(Files.newOutputStream(jar))
try {
IOUtil.rootPath(jar).toString shouldBe "/"
IOUtil.rootPath(jar.resolve("foo/Bar.class")).toString shouldBe "/foo/Bar.class"
}
finally {
stream.closeEntry()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package scala.tools.jardiff

package sbtjardiff

import sbt._
import Keys._
import sbt.librarymanagement.ModuleFilter

object SbtJardiff extends AutoPlugin {
override def trigger = allRequirements

object autoImport {
val jardiff =
taskKey[Boolean]("run jardiff, returning true if there is a difference")
val jardiffGetPreviousVersion =
taskKey[File]("Resolves and downloads the version set in jardiffPreviousVersionCoordinates")

val jardiffSettings = taskKey[JarDiff.Config]("jardiff settings")
val jardiffLeaveRepoAt = settingKey[Option[String]](
"Directory to output a git repository containing the diff"
)
val jardiffMethodBodies = settingKey[Boolean](
"Whether or not method bodies should be included in the output"
)
val jardiffRaw = settingKey[Boolean]("unsorted and unfiltered")
val jardiffIncludePrivates = settingKey[Boolean]("include private members")
val jardiffUnifiedDiffContext = settingKey[Option[Int]](
"Create a unified diff with this number of context lines"
)
val jardiffOutputStream =
taskKey[java.io.OutputStream]("Stream to write the diff to. Defaults to stdout")
val jardiffIgnore = settingKey[List[String]](
"list of file patterns to ignore, using .gitignore syntax"
)
val jardiffReferenceVersionCoordinates =
settingKey[ModuleID]("reference to jardiff against, in libraryDependencies format")

val jardiffReferenceVersion = settingKey[String](
"version number of version to compare against, resolved with current organization and name." +
" If those changed, set jardiffReferenceVersionCoordinates instead, and leave this unset"
)
}

import autoImport._
override lazy val globalSettings: Seq[Setting[_]] = Seq(
jardiffMethodBodies := true,
jardiffLeaveRepoAt := None,
jardiffRaw := false,
jardiffIncludePrivates := true,
jardiffOutputStream := System.out,
jardiffUnifiedDiffContext := None,
jardiffIgnore := Nil
)

override lazy val projectSettings: Seq[Setting[_]] = Seq(
jardiffSettings := {
val repo = jardiffLeaveRepoAt.value.map(java.nio.file.Paths.get(_))
val code = jardiffMethodBodies.value
val raw = jardiffRaw.value
val privates = jardiffIncludePrivates.value
val contextLines = jardiffUnifiedDiffContext.value
val output = jardiffOutputStream.value
val ignore = jardiffIgnore.value
JarDiff.Config(repo, code, raw, privates, contextLines, output, ignore)
},
jardiffGetPreviousVersion := {
//Adapted from a demonstration by Anton Sviridov
val logger = streams.value.log
val scalaV = scalaBinaryVersion.value
val moduleId = jardiffReferenceVersionCoordinates.?.value
.orElse {
jardiffReferenceVersion.?.value.map(v => organization.value %% name.value % v)
}
.getOrElse {
logger.error(
"sbt-jardiff: set jardiffReferenceVersion to the version to compare to"
)
throw new Exception("failed to resolve jardiff previous version")
}
val resolver = dependencyResolution.in(update).value
val updateConfiguration_ = updateConfiguration.in(update).value
val unresolvedWarningConfiguration_ = unresolvedWarningConfiguration.in(update).value
val descriptor = resolver.wrapDependencyInModule(moduleId)

val mfilter: ModuleFilter = module => {
val orgResult = module.organization == moduleId.organization
val nameResult = module.name == moduleId.name
val nameResultAlt = module.name == s"${moduleId.name}_$scalaV"
val revisionResult = module.revision == moduleId.revision
val result = orgResult && (nameResult || nameResultAlt) && revisionResult

if (result)
logger.debug(
s"module $module matched search for $moduleId, matched by matching name ${module.name} against ${moduleId.name}"
)
else {
logger.debug(s"module $module is not our dreamed module $moduleId")
if (!orgResult)
logger.debug(
s"the organization doesn't match: ${module.organization} vs ${moduleId.organization}"
)
else if (!nameResult)
logger.debug(
s"the name doesn't match: ${module.name} vs ${moduleId.name} or ${moduleId.name}_$scalaV"
)
else if (!revisionResult)
logger.debug(s"the revision doesn't match: ${module.revision} vs ${moduleId.revision}")
}
result
}

resolver
.update(descriptor, updateConfiguration_, unresolvedWarningConfiguration_, logger)
.fold(
uw => throw uw.resolveException,
x =>
x.select(mfilter) match {
case Vector(head) => head
case Vector(head, tail @ _*) => {
logger.warn(
s"sbt-jardiff expected single file for moduleId $moduleId, but got more than that. Arbitrarily selecting $head and discarding ${tail
.mkString(", ")}"
)
head
}
case v => {
logger.error(s"no file found for module $moduleId. This is a bug in sbt-jardiff")
v.head
}
}
)
},
jardiff := {
//register the call to value on compile to make it a dependency of this
//task so it gets executed and completes before this task gets executed
//TODO: figure out what to do when compilation fails and how to
//how to communicate that back upstream
val _ = (Compile / compile).value

//we assume the previous version can be compared to whatever
//is in the classDirectory directory after compilation.
//It's a somewhat daring assumption that underlines our swashbuckling
//and freebooting disposition.

//As a mild justification: this is the default location and we're just
//going to have to hope that the project using this plugin don't change
//that. It's unlikely that they'll have: in order to do that, they would
//have had to modified one of the upstreams of classDirectory:
//productDirectories, bloopGenerate, compileIncremental,
//manipulateBytecode or compile itself.
//Modifying these is to the best of my knowledge rare enough that assuming
//they're unchanged. If they are changed, this will break, but the user
//will probably know they are doing something unconventional.

//That there aren't further steps that happen before the artifact ends up
//is harder to justify, which we do only through furious handwaving and
//victim blaming if it doesn't work, to hide my own incompetence in
//figuring out where to get the exact compilation artifacts for some
//project.
val classDir = (Compile / classDirectory).value
val currentPath = JarDiff.expandClassPath(classDir.getAbsolutePath())

val previousJar = jardiffGetPreviousVersion.value
val previousPath = JarDiff.expandClassPath(previousJar.getAbsolutePath())

val config = jardiffSettings.value
val differ = JarDiff.apply(List(previousPath, currentPath), config)

differ.diff()
}
)
}
17 changes: 17 additions & 0 deletions sbtPlugin/src/sbt-test/jardifftest/jardiffs/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
inThisBuild(Seq[Setting[_]](
scalaVersion := "2.13.1",
organization := "org.example"
))

lazy val original = (project in file("empty"))
.settings(
name := "exampleproject",
version := "0.1.0"
)

lazy val changed = (project in file("changed"))
.settings(
name := "exampleproject",
version := "0.2.0",
jardiffPreviousVersion := "0.1.0",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example

object Example {
def member = "public has method body"
private[this] def privatethis = "privatethis has method body"
private[this] val privatefield: Int = 7
}

class OtherExample {
def othermember = "other public has method body"
}
Loading