Skip to content

Commit

Permalink
Merge pull request #67 from pdalpra/renderer
Browse files Browse the repository at this point in the history
Introduce possibility to use different renderers
  • Loading branch information
eed3si9n committed Jul 9, 2015
2 parents 6f70fd5 + 67b1dfd commit c180e21
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 120 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ lazy val root = (project in file(".")).
name := "sbt-buildinfo",
// sbtVersion in Global := "0.13.0"
// scalaVersion in Global := "2.10.2"
scalacOptions := Seq("-unchecked", "-deprecation"),
scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-language:implicitConversions"),
description := "sbt plugin to generate build info",
licenses := Seq("MIT License" -> url("https://github.com/sbt/sbt-buildinfo/blob/master/LICENSE"))
).
Expand Down
145 changes: 30 additions & 115 deletions src/main/scala/sbtbuildinfo/BuildInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,34 @@ package sbtbuildinfo

import sbt._, Keys._

object BuildInfo {
def apply(dir: File, obj: String, pkg: String, keys: Seq[BuildInfoKey],
options: Seq[BuildInfoOption],
proj: ProjectRef, state: State, cacheDir: File): File =
BuildInfoTask(dir, obj, pkg, keys, options, proj, state, cacheDir).file
case class BuildInfoResult(identifier: String, value: Any, typeExpr: TypeExpression)

private case class BuildInfoTask(dir: File, obj: String, pkg: String,
keys: Seq[BuildInfoKey], options: Seq[BuildInfoOption],
proj: ProjectRef, state: State, cacheDir: File) {
object BuildInfo {
def apply(dir: File, renderer: BuildInfoRenderer, obj: String,
keys: Seq[BuildInfoKey], options: Seq[BuildInfoOption],
proj: ProjectRef, state: State, cacheDir: File): File =
BuildInfoTask(dir, renderer, obj, keys, options, proj, state, cacheDir).file

private case class BuildInfoTask(dir: File,
renderer: BuildInfoRenderer,
obj: String,
keys: Seq[BuildInfoKey],
options: Seq[BuildInfoOption],
proj: ProjectRef,
state: State,
cacheDir: File) {
import FileInfo.hash
import Tracked.inputChanged

def extracted = Project.extract(state)
val tempFile = cacheDir / "sbtbuildinfo" / s"""$obj.scala"""
val outFile = dir / (s"""$obj.scala""")
val tempFile = cacheDir / "sbtbuildinfo" / s"$obj.${renderer.extension}"
val outFile = dir / s"$obj.${renderer.extension}"

// 1. make the file under cache/sbtbuildinfo.
// 2. compare its SHA1 against cache/sbtbuildinfo-inputs
def file: File = {
makeFile(tempFile)
cachedCopyFile { hash(tempFile) }
cachedCopyFile(hash(tempFile))
outFile
}

Expand All @@ -35,62 +42,27 @@ object BuildInfo {

def makeFile(file: File): File = {
val distinctKeys = keys.toList.distinct
val lines =
List(s"""package $pkg""",
"",
"import java.io.File",
"import java.net.URL",
"",
s"""/** This object was generated by sbt-buildinfo. */""",
s"""case object $obj {""") :::
(distinctKeys flatMap { line(_) }) :::
List(toStringLine(distinctKeys)) :::
toMapLine(distinctKeys) :::
toJsonLine :::
List("}")
val values = distinctKeys.flatMap(entry(_))
val lines = renderer.header ++ renderer.renderKeys(values) ++ renderer.footer
IO.write(file, lines.mkString("\n"))
file
}

def line(info: BuildInfoKey): List[String] =
entry(info) match {
case Some((ident, value)) =>
val typeDecl: String = getType(info) map { ": " + _ } getOrElse ""
List(s""" /** The value is ${quote(value)}. */""",
s""" val $ident$typeDecl = ${quote(value)}""")
case _ => Nil
def entry[A](info: BuildInfoKey.Entry[A]): Option[BuildInfoResult] = {
val typeExpr = TypeExpression.parse(info.manifest.toString())._1
val result = info match {
case BuildInfoKey.Setting(key) => extracted getOpt (key in scope(key)) map { ident(key) -> _ }
case BuildInfoKey.Task(key) => Some(ident(key) -> extracted.runTask(key in scope(key), state)._2)
case BuildInfoKey.Constant(tuple) => Some(tuple)
case BuildInfoKey.Action(name, fun) => Some(name -> fun.apply)
case BuildInfoKey.Mapped(from, fun) => entry(from).map { r => fun(r.identifier -> r.value.asInstanceOf[A]) }
}

def toStringLine(keys: Seq[BuildInfoKey]): String = {
val idents = keys.map(entry(_)).flatten.map(_._1)
val fmt = idents.map("%s: %%s" format _).mkString(", ")
val vars = idents.mkString(", ")
s""" override val toString: String = "$fmt" format ($vars)"""
}

def toJsonLine: List[String] =
if (options contains BuildInfoOption.ToJson)
List(""" val toJson: String = toMap.map(i => "\"" + i._1 + "\":\"" + i._2 + "\"").mkString("{", ", ", "}")""")
else Nil

def toMapLine(distinctKeys: List[BuildInfoKey]): List[String] =
if ((options contains BuildInfoOption.ToMap) || (options contains BuildInfoOption.ToJson))
(distinctKeys.map { key => entry(key) }.flatten.map {
case (ident, _) => " \"%s\" -> %s".format(ident, ident)
}.mkString(" val toMap: Map[String, Any] = Map[String, Any](\n", ",\n", ")").split("\n").toList) ::: List("")
else Nil

def entry[A](info: BuildInfoKey.Entry[A]): Option[(String, A)] = info match {
case BuildInfoKey.Setting(key) => extracted getOpt (key in scope(key)) map { ident(key) -> _ }
case BuildInfoKey.Task(key) => Some(ident(key) -> extracted.runTask(key in scope(key), state)._2)
case BuildInfoKey.Constant(tuple) => Some(tuple)
case BuildInfoKey.Action(name, fun) => Some(name -> fun.apply)
case BuildInfoKey.Mapped(from, fun) => entry(from) map fun
result.map(r => BuildInfoResult(r._1, r._2, typeExpr))
}

def scope(scoped: Scoped) = {
val scope0 = scoped.scope
if (scope0.project == This) scope0 in (proj)
if (scope0.project == This) scope0 in proj
else scope0
}

Expand All @@ -110,62 +82,5 @@ object BuildInfo {
case x :: xs => x + (xs map {_.capitalize}).mkString("")
})
}

def getType(info: BuildInfoKey): Option[String] = {
val mf = info.manifest
val (tpe0, rest) = TypeExpression.parse(mf.toString)
def tpeToReturnType(tpe: TypeExpression): Option[String] =
tpe match {
case TypeExpression("Any", Nil) => None
case TypeExpression("Int", Nil) => Some("Int")
case TypeExpression("Long", Nil) => Some("Long")
case TypeExpression("Double", Nil) => Some("Double")
case TypeExpression("Boolean", Nil) => Some("Boolean")
case TypeExpression("scala.Symbol", Nil) => Some("scala.Symbol")
case TypeExpression("java.lang.String", Nil) => Some("String")
case TypeExpression("java.net.URL", Nil) => Some("URL")
case TypeExpression("sbt.URL", Nil) => Some("URL")
case TypeExpression("java.io.File", Nil) => Some("File")
case TypeExpression("sbt.File", Nil) => Some("File")
case TypeExpression("scala.xml.NodeSeq", Nil) => Some("scala.xml.NodeSeq")

case TypeExpression("sbt.ModuleID", Nil) => Some("String")
case TypeExpression("sbt.Resolver", Nil) => Some("String")

case TypeExpression("scala.Option", Seq(arg)) =>
tpeToReturnType(arg) map { x => s"Option[$x]" }
case TypeExpression("scala.collection.Seq", Seq(arg)) =>
tpeToReturnType(arg) map { x => s"Seq[$x]" }
case TypeExpression("scala.collection.immutable.Map", Seq(arg0, arg1)) =>
for {
x0 <- tpeToReturnType(arg0)
x1 <- tpeToReturnType(arg1)
} yield s"Map[$x0, $x1]"
case TypeExpression("scala.Tuple2", Seq(arg0, arg1)) =>
for {
x0 <- tpeToReturnType(arg0)
x1 <- tpeToReturnType(arg1)
} yield s"($x0, $x1)"
case _ => None
}
tpeToReturnType(tpe0)
}

def quote(v: Any): String = v match {
case x @ ( _: Int | _: Double | _: Boolean | _: Symbol) => x.toString
case x: Long => x.toString + "L"
case node: scala.xml.NodeSeq if node.toString.trim.nonEmpty => node.toString
case (k, _v) => "(%s -> %s)" format(quote(k), quote(_v))
case mp: Map[_, _] => mp.toList.map(quote(_)).mkString("Map(", ", ", ")")
case seq: Seq[_] => seq.map(quote(_)).mkString("Seq(", ", ", ")")
case op: Option[_] => op map { x => "Some(" + quote(x) + ")" } getOrElse {"None"}
case url: java.net.URL => "new URL(%s)" format quote(url.toString)
case file: java.io.File => "new File(%s)" format quote(file.toString)
case s => "\"%s\"" format encodeStringLiteral(s.toString)
}

def encodeStringLiteral(str: String): String =
str.replace("\\","\\\\").replace("\n","\\n").replace("\b","\\b").replace("\r","\\r").
replace("\t","\\t").replace("\'","\\'").replace("\f","\\f").replace("\"","\\\"")
}
}
3 changes: 2 additions & 1 deletion src/main/scala/sbtbuildinfo/BuildInfoKeys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import Keys._

trait BuildInfoKeys {
lazy val buildInfo = taskKey[Seq[File]]("Generates build info.")
lazy val buildInfoRenderer = settingKey[BuildInfoRenderer]("The renderer used to create the build info file")
lazy val buildInfoObject = settingKey[String]("The name for the generated object.")
lazy val buildInfoPackage = settingKey[String]("The name for teh generated package.")
lazy val buildInfoKeys = settingKey[Seq[BuildInfoKey.Entry[_]]]("Entries for build info.")
lazy val buildInfoBuildNumber = taskKey[Int]("The build number.")
lazy val buildInfoOptions = settingKey[Seq[BuildInfoOption]]("Options to generate build info.")
}
object BuildInfoKeys extends BuildInfoKeys {}
object BuildInfoKeys extends BuildInfoKeys
17 changes: 14 additions & 3 deletions src/main/scala/sbtbuildinfo/BuildInfoPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ object BuildInfoPlugin extends sbt.AutoPlugin {
type BuildInfoKey = sbtbuildinfo.BuildInfoKey
val BuildInfoOption = sbtbuildinfo.BuildInfoOption
type BuildInfoOption = sbtbuildinfo.BuildInfoOption
val BuildInfoType = sbtbuildinfo.BuildInfoType
type BuildInfoType = sbtbuildinfo.BuildInfoType
}
import autoImport._

Expand All @@ -39,19 +41,28 @@ object BuildInfoPlugin extends sbt.AutoPlugin {
buildInfo := {
val dir = (sourceManaged in Compile).value
Seq(BuildInfo(dir / "sbt-buildinfo",
buildInfoRenderer.value,
buildInfoObject.value,
buildInfoPackage.value,
buildInfoKeys.value,
buildInfoOptions.value,
thisProjectRef.value,
state.value,
streams.value.cacheDirectory))
},
sourceGenerators in Compile <+= buildInfo,
sourceGenerators in Compile ++= {
if (buildInfoRenderer.value.isSource) Seq(buildInfo.taskValue) else Nil
},
resourceGenerators in Compile ++= {
if(buildInfoRenderer.value.isResource) Seq(buildInfo.taskValue) else Nil
},
buildInfoRenderer := ScalaClassRenderer(
buildInfoOptions.value,
buildInfoPackage.value,
buildInfoObject.value),
buildInfoObject := "BuildInfo",
buildInfoPackage := "buildinfo",
buildInfoKeys := Seq(name, version, scalaVersion, sbtVersion),
buildInfoBuildNumber <<= (baseDirectory) map { (dir) => buildNumberTask(dir, 1) },
buildInfoBuildNumber := buildNumberTask(baseDirectory.value, 1),
buildInfoOptions := Seq()
)
}
13 changes: 13 additions & 0 deletions src/main/scala/sbtbuildinfo/BuildInfoRenderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package sbtbuildinfo

trait BuildInfoRenderer {

def fileType: BuildInfoType
def extension: String
def header: Seq[String]
def renderKeys(infoKeysNameAndValues: Seq[BuildInfoResult]): Seq[String]
def footer: Seq[String]

def isSource = fileType == BuildInfoType.Source
def isResource = fileType == BuildInfoType.Resource
}
7 changes: 7 additions & 0 deletions src/main/scala/sbtbuildinfo/BuildInfoType.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sbtbuildinfo

sealed trait BuildInfoType
object BuildInfoType {
case object Source extends BuildInfoType
case object Resource extends BuildInfoType
}
108 changes: 108 additions & 0 deletions src/main/scala/sbtbuildinfo/ScalaClassRenderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package sbtbuildinfo

private[sbtbuildinfo] case class ScalaClassRenderer(options: Seq[BuildInfoOption], pkg: String, obj: String) extends BuildInfoRenderer {

override def fileType = BuildInfoType.Source
override def extension = "scala"

override def header = List(
s"package $pkg",
"",
"import java.io.File",
"import java.net.URL",
"",
s"/** This object was generated by sbt-buildinfo. */",
s"case object $obj {")

override def footer = List("}")

override def renderKeys(buildInfoResults: Seq[BuildInfoResult]) =
buildInfoResults.flatMap(line) ++ Seq(toStringLine(buildInfoResults)) ++
toMapLine(buildInfoResults) ++ toJsonLine

private def line(result: BuildInfoResult): Seq[String] = {
import result._
val typeDecl = getType(result.typeExpr) map { ": " + _ } getOrElse ""

List(
s" /** The value is ${quote(value)}. */",
s" val $identifier$typeDecl = ${quote(value)}"
)
}

def toStringLine(results: Seq[BuildInfoResult]): String = {
val idents = results.map(_.identifier)
val fmt = idents.map("%s: %%s" format _).mkString(", ")
val vars = idents.mkString(", ")
s""" override val toString: String = "$fmt" format ($vars)"""
}

def toMapLine(results: Seq[BuildInfoResult]): Seq[String] =
if (options.contains(BuildInfoOption.ToMap) || options.contains(BuildInfoOption.ToJson))
results
.map(result => " \"%s\" -> %s".format(result.identifier, result.identifier))
.mkString(" val toMap: Map[String, Any] = Map[String, Any](\n", ",\n", ")")
.split("\n")
.toList ::: List("")
else Nil

def toJsonLine: Seq[String] =
if (options contains BuildInfoOption.ToJson)
List(""" val toJson: String = toMap.map(i => "\"" + i._1 + "\":\"" + i._2 + "\"").mkString("{", ", ", "}")""")
else Nil

private def getType(typeExpr: TypeExpression): Option[String] = {
def tpeToReturnType(tpe: TypeExpression): Option[String] =
tpe match {
case TypeExpression("Any", Nil) => None
case TypeExpression("Int", Nil) => Some("Int")
case TypeExpression("Long", Nil) => Some("Long")
case TypeExpression("Double", Nil) => Some("Double")
case TypeExpression("Boolean", Nil) => Some("Boolean")
case TypeExpression("scala.Symbol", Nil) => Some("scala.Symbol")
case TypeExpression("java.lang.String", Nil) => Some("String")
case TypeExpression("java.net.URL", Nil) => Some("URL")
case TypeExpression("sbt.URL", Nil) => Some("URL")
case TypeExpression("java.io.File", Nil) => Some("File")
case TypeExpression("sbt.File", Nil) => Some("File")
case TypeExpression("scala.xml.NodeSeq", Nil) => Some("scala.xml.NodeSeq")

case TypeExpression("sbt.ModuleID", Nil) => Some("String")
case TypeExpression("sbt.Resolver", Nil) => Some("String")

case TypeExpression("scala.Option", Seq(arg)) =>
tpeToReturnType(arg) map { x => s"Option[$x]" }
case TypeExpression("scala.collection.Seq", Seq(arg)) =>
tpeToReturnType(arg) map { x => s"Seq[$x]" }
case TypeExpression("scala.collection.immutable.Map", Seq(arg0, arg1)) =>
for {
x0 <- tpeToReturnType(arg0)
x1 <- tpeToReturnType(arg1)
} yield s"Map[$x0, $x1]"
case TypeExpression("scala.Tuple2", Seq(arg0, arg1)) =>
for {
x0 <- tpeToReturnType(arg0)
x1 <- tpeToReturnType(arg1)
} yield s"($x0, $x1)"
case _ => None
}
tpeToReturnType(typeExpr)
}

private def quote(v: Any): String = v match {
case x @ ( _: Int | _: Double | _: Boolean | _: Symbol) => x.toString
case x: Long => x.toString + "L"
case node: scala.xml.NodeSeq if node.toString().trim.nonEmpty => node.toString()
case (k, _v) => "(%s -> %s)" format(quote(k), quote(_v))
case mp: Map[_, _] => mp.toList.map(quote(_)).mkString("Map(", ", ", ")")
case seq: Seq[_] => seq.map(quote).mkString("Seq(", ", ", ")")
case op: Option[_] => op map { x => "Some(" + quote(x) + ")" } getOrElse {"None"}
case url: java.net.URL => "new URL(%s)" format quote(url.toString)
case file: java.io.File => "new File(%s)" format quote(file.toString)
case s => "\"%s\"" format encodeStringLiteral(s.toString)
}

def encodeStringLiteral(str: String): String =
str.replace("\\","\\\\").replace("\n","\\n").replace("\b","\\b").replace("\r","\\r").
replace("\t","\\t").replace("\'","\\'").replace("\f","\\f").replace("\"","\\\"")
}

0 comments on commit c180e21

Please sign in to comment.