├── .gitignore ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── src └── main │ ├── scala-sbt-0.13 │ └── sbtjavap │ │ └── Serialization.scala │ ├── scala-sbt-1.0 │ └── sbtjavap │ │ └── Serialization.scala │ └── scala │ └── sbtjavap │ └── JavapPlugin.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | project/boot 2 | target 3 | .ensime 4 | .ensime_lucene 5 | .ensime_cache 6 | TAGS 7 | \#*# 8 | *~ 9 | .#* 10 | .lib 11 | .history 12 | .*.swp 13 | .idea 14 | .idea/* 15 | .idea_modules 16 | .DS_Store 17 | .sbtrc 18 | *.sublime-project 19 | *.sublime-workspace 20 | tests.iml 21 | # Auto-copied by sbt-microsites 22 | docs/src/main/tut/contributing.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## sbt-javap 2 | 3 | > As above, so below. 4 | 5 | ### overview 6 | 7 | *sbt-javap* is an SBT plugin to call `javap` directly from SBT. 8 | 9 | Java provides the `javap` tool to disassemble and inspect Java 10 | bytecode. This allows authors to see how their code operates at a low 11 | level, to understand how high-level Scala concepts are encoded, and to 12 | potentially spot performance issues. 13 | 14 | One challenge with using `javap` is correctly specifying a classpath. 15 | Since SBT knows your project's classpath, *sbt-javap* can 16 | automatically invoke `javap` for you. 17 | 18 | ### quick start 19 | 20 | Currently, *sbt-javap* is published for SBT 1.x. 21 | 22 | To use *sbt-javap*, add the following to `project/plugins.sbt`: 23 | 24 | ```scala 25 | addSbtPlugin("org.spire-math" % "sbt-javap" % "0.0.1") 26 | ``` 27 | 28 | ### usage 29 | 30 | *sbt-javap* provides one new SBT task: `javap`. 31 | 32 | This command takes the fully-qualified name of a class, dissassembles 33 | that class into a file, and then displays that file interactively via 34 | a pager (by default `less`). 35 | 36 | For example, running `javap scala.Unit` produces the following: 37 | 38 | ``` 39 | sbt:myproject> javap scala.Unit 40 | decompiling scala.Unit to /Users/erik/t/sbt-javap-test/target/scala-2.12/javap/scala.Unit.bytecode 41 | ``` 42 | 43 | At that point, `less` will be used to view that file: 44 | 45 | ``` 46 | Compiled from "Unit.scala" 47 | public abstract class scala.Unit { 48 | public static java.lang.String toString(); 49 | Code: 50 | 0: getstatic #16 // Field scala/Unit$.MODULE$:Lscala/Unit$; 51 | 3: invokevirtual #18 // Method scala/Unit$.toString:()Ljava/lang/String; 52 | 6: areturn 53 | 54 | public static void unbox(java.lang.Object); 55 | Code: 56 | 0: getstatic #16 // Field scala/Unit$.MODULE$:Lscala/Unit$; 57 | 3: aload_0 58 | 4: invokevirtual #22 // Method scala/Unit$.unbox:(Ljava/lang/Object;)V 59 | 7: return 60 | 61 | public static scala.runtime.BoxedUnit box(scala.runtime.BoxedUnit); 62 | Code: 63 | 0: getstatic #16 // Field scala/Unit$.MODULE$:Lscala/Unit$; 64 | 3: aload_0 65 | 4: invokevirtual #26 // Method scala/Unit$.box:(Lscala/runtime/BoxedUnit;)Lscala/runtime/BoxedUnit; 66 | 7: areturn 67 | 68 | public scala.Unit(); 69 | Code: 70 | 0: aload_0 71 | 1: invokespecial #30 // Method java/lang/Object."":()V 72 | 4: return 73 | } 74 | (END) 75 | ``` 76 | 77 | Note that you can decompile any Scala or Java class that's on your 78 | classpath, not just classes that you defined. Decompiling classes from 79 | your dependencies (or from the standard library) can be very 80 | illuminating. 81 | 82 | ### name-mangling 83 | 84 | Scala uses name-mangling to encode various types of names into the 85 | single Java class namespace. Here are some examples: 86 | 87 | * Scala objects (`object Foo`) have a `$` suffix appended (`Foo$`). 88 | * Scala traits (`trait Bar`) have a `$class` suffix appended (`Bar$class`). 89 | * Scala classes (`class Qux`) use their names as normal (`Qux`). 90 | 91 | (In all of the above cases the other name-mangling rules may still apply.) 92 | 93 | Scala types are often found inside of `object` values as a form of 94 | namespacing. Scala uses a `$` delimiter to mangle these names. For 95 | example, given `object Kennel { class Dog }` the inner class name 96 | would become `Kennel$Dog`. 97 | 98 | Finally, Scala also allows a wider range of names than Java. For 99 | example, `+` is a valid name (e.g. `object + { ... }`), and would be 100 | encoded in Java as `$plus`. If your class/trait/object has a name 101 | containing these characters, you'll need to determine how the name was 102 | mangled. 103 | 104 | It's often useful to use the REPL to see how particular characters of 105 | a name are encoded: 106 | 107 | ``` 108 | scala> object +*% 109 | defined object $plus$times$percent 110 | ``` 111 | 112 | ### future work 113 | 114 | Error handling and reporting could be a lot better. 115 | 116 | It's likely that the SBT-related code could be improved. 117 | 118 | It should be possible to cross-publish this plugin for SBT 0.13.x as 119 | well as SBT 1.x. 120 | 121 | This README could easily be expanded to document SBT options, expand 122 | on name-mangling, and explain how to interpet bytecode (or link to 123 | other references). 124 | 125 | It might be nice to parse and/or highlight bytecode to make it easier 126 | to read. It would also be nice to support an option to decompile only 127 | a particular method, rather than an entire class. 128 | 129 | ### copyright & license 130 | 131 | All code is available to you under the Apache 2 license, available at 132 | https://opensource.org/licenses/Apache-2.0. 133 | 134 | Copyright Erik Osheim, 2017-2018. 135 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import ReleaseTransformations._ 2 | 3 | name := "sbt-javap" 4 | organization := "org.spire-math" 5 | sbtPlugin := true 6 | scalacOptions += "-feature" 7 | 8 | // release stuff 9 | releaseCrossBuild := true 10 | releasePublishArtifactsAction := PgpKeys.publishSigned.value 11 | publishMavenStyle := true 12 | publishArtifact in Test := false 13 | pomIncludeRepository := Function.const(false) 14 | releaseProcess := Seq[ReleaseStep]( 15 | checkSnapshotDependencies, 16 | inquireVersions, 17 | runClean, 18 | //runTest, 19 | setReleaseVersion, 20 | commitReleaseVersion, 21 | tagRelease, 22 | publishArtifacts, 23 | setNextVersion, 24 | commitNextVersion, 25 | releaseStepCommand("sonatypeReleaseAll"), 26 | pushChanges 27 | ) 28 | publishTo := { 29 | val nexus = "https://oss.sonatype.org/" 30 | if (isSnapshot.value) 31 | Some("Snapshots" at nexus + "content/repositories/snapshots") 32 | else 33 | Some("Releases" at nexus + "service/local/staging/deploy/maven2") 34 | } 35 | pomExtra := ( 36 | https://github.com/non/sbt-javap 37 | 38 | 39 | Apache 2 40 | http://www.apache.org/licenses/LICENSE-2.0.txt 41 | repo 42 | A business-friendly OSS license 43 | 44 | 45 | 46 | git@github.com:non/sbt-javap.git 47 | scm:git:git@github.com:non/sbt-javap.git 48 | 49 | 50 | 51 | non 52 | Erik Osheim 53 | http://github.com/non/ 54 | 55 | 56 | ) 57 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") 2 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") 4 | -------------------------------------------------------------------------------- /src/main/scala-sbt-0.13/sbtjavap/Serialization.scala: -------------------------------------------------------------------------------- 1 | package sbtjavap 2 | 3 | private[sbtjavap] object Serialization { 4 | object Implicits extends sbinary.DefaultProtocol { 5 | implicit def seqFormat[A: sbinary.Format] = sbt.Cache.seqFormat[A] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala-sbt-1.0/sbtjavap/Serialization.scala: -------------------------------------------------------------------------------- 1 | package sbtjavap 2 | 3 | private[sbtjavap] object Serialization { 4 | val Implicits = sjsonnew.BasicJsonProtocol 5 | } 6 | -------------------------------------------------------------------------------- /src/main/scala/sbtjavap/JavapPlugin.scala: -------------------------------------------------------------------------------- 1 | package sbtjavap 2 | 3 | import sbt._ 4 | import Keys._ 5 | import complete.DefaultParsers._ 6 | import Serialization.Implicits._ 7 | import java.io._ 8 | import java.util.stream.Collectors 9 | import sbt.complete.Parser 10 | import scala.collection.JavaConverters._ 11 | import scala.reflect.NameTransformer 12 | import xsbti.api.{ClassLike, DefinitionType} 13 | 14 | object JavapPlugin extends AutoPlugin { 15 | 16 | override def requires = sbt.plugins.JvmPlugin 17 | 18 | object autoImport { 19 | lazy val Javap = (config("javap") extend Compile).hide 20 | lazy val javap = inputKey[Unit]("Run javap on the given class") 21 | lazy val javapOpts = settingKey[List[String]]("Options to pass to javap") 22 | lazy val javapTargetDirectory = settingKey[File]("Where to put decompiled bytecode") 23 | lazy val javapPager = settingKey[String]("Pager to use for javap") 24 | lazy val javapClassNames = taskKey[Seq[String]]("") 25 | } 26 | 27 | import autoImport._ 28 | 29 | override def trigger = allRequirements 30 | 31 | private[this] val defaultParser = Space ~> token(StringBasic, "") 32 | 33 | private[this] def createParser(classNames: Seq[String]): Parser[String] = { 34 | classNames match { 35 | case Seq() => 36 | defaultParser 37 | case _ => 38 | val other = Space ~> token(StringBasic, _ => true) 39 | (Space ~> classNames.distinct.map(token(_)).reduce(_ | _)) | other 40 | } 41 | } 42 | 43 | override lazy val projectSettings = 44 | inConfig(Javap)(Defaults.configSettings) ++ 45 | Seq( 46 | javapClassNames := Tests.allDefs((compile in Compile).value).collect{ 47 | case c: ClassLike => 48 | val decoded = c.name.split('.').map(NameTransformer.decode).mkString(".") 49 | c.definitionType match { 50 | case DefinitionType.Module => 51 | decoded + "$" 52 | case _ => 53 | decoded 54 | } 55 | }, 56 | javapClassNames := (javapClassNames storeAs javapClassNames triggeredBy (compile in Compile)).value, 57 | javapOpts := List("-private", "-c"), 58 | javapPager := "less", 59 | javapTargetDirectory := crossTarget.value / "javap", 60 | javap := InputTask.createDyn( 61 | Defaults.loadForParser(javapClassNames)( 62 | (state, classes) => classes.fold(defaultParser)(createParser) 63 | ) 64 | ){ 65 | Def.task{ 66 | val loader = (testLoader in Test).value 67 | val r = (runner in (Javap, run)).value 68 | val cp = (fullClasspath or (fullClasspath in Runtime)).value 69 | val pager = (javapPager in Javap).value 70 | val opts = (javapOpts in Javap).value 71 | val s = streams.value 72 | (cls: String) => { 73 | val clazz = cls.split('.').map(NameTransformer.encode).mkString(".") 74 | val dir = javapTargetDirectory.value // output root 75 | Def.task(runJavap(s, r, clazz, dir, cp, pager, opts)) 76 | } 77 | } 78 | }.evaluated 79 | ) 80 | 81 | def readAll(is: InputStream): String = 82 | new BufferedReader(new InputStreamReader(is)).lines().collect(Collectors.joining("\n")) 83 | 84 | def runJavap(streams: TaskStreams, r: ScalaRun, cls: String, dir: File, cp: Classpath, pager: String, opts: List[String]): Unit = { 85 | val jars = cp.map(_.data.toString).mkString(":") 86 | val args = List("javap", "-classpath", jars) ::: opts ::: List(cls) 87 | dir.mkdirs() 88 | val dest = dir / s"$cls.bytecode" 89 | 90 | val pb = new ProcessBuilder(args.asJava) 91 | val proc = pb.start() 92 | val output = readAll(proc.getInputStream()) 93 | val errors = readAll(proc.getErrorStream()) 94 | val retval = proc.waitFor() 95 | 96 | if (retval == 0) { 97 | println(s"decompiling $cls to $dest") 98 | val pw = new PrintWriter(dest) 99 | pw.print(output) 100 | pw.close() 101 | loadInPager(pager, dest) 102 | } else { 103 | println(errors) 104 | } 105 | } 106 | 107 | def loadInPager(pager: String, file: File): Unit = { 108 | import java.lang.ProcessBuilder 109 | val pb = new ProcessBuilder(pager, file.toString) 110 | pb.inheritIO() 111 | val p = pb.start() 112 | p.waitFor() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.0.2-SNAPSHOT" 2 | --------------------------------------------------------------------------------