aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/proguard-example/build/build.scala4
-rw-r--r--libraries/proguard/Proguard.scala249
-rw-r--r--libraries/proguard/Readme.md4
-rw-r--r--libraries/proguard/build/build.scala151
-rw-r--r--libraries/proguard/build/build/build.scala (renamed from plugins/proguard/build/build/build.scala)0
-rw-r--r--libraries/proguard/spec/refcard.html (renamed from plugins/proguard/spec/refcard.html)0
-rw-r--r--plugins/proguard/Proguard.scala32
-rw-r--r--plugins/proguard/build/build.scala125
-rw-r--r--plugins/proguard/src/generated/Proguard.scala226
-rw-r--r--plugins/proguard/templates/Proguard.scala85
-rw-r--r--stage1/cbt.scala3
-rw-r--r--stage2/BasicBuild.scala13
-rw-r--r--stage2/plugins/GeneratedSections.scala40
13 files changed, 493 insertions, 439 deletions
diff --git a/examples/proguard-example/build/build.scala b/examples/proguard-example/build/build.scala
index 0b12981..2b3709e 100644
--- a/examples/proguard-example/build/build.scala
+++ b/examples/proguard-example/build/build.scala
@@ -1,7 +1,7 @@
import cbt._
-class Build(val context: Context) extends Proguard{
- def proguard = proguardKeep( (Nil, """
+class Build(val context: Context) extends ProGuard{
+ def proguard = ProGuard( (Nil, """
public class proguard_example.Main{
public void main(java.lang.String[]);
}
diff --git a/libraries/proguard/Proguard.scala b/libraries/proguard/Proguard.scala
new file mode 100644
index 0000000..c4a74bd
--- /dev/null
+++ b/libraries/proguard/Proguard.scala
@@ -0,0 +1,249 @@
+package cbt
+package proguard
+import java.io.File
+import java.nio.file.Files.deleteIfExists
+
+sealed class KeepOptionModifier(val string: String)
+object KeepOptionModifier {
+ /* AUTO GENERATED SECTION BEGIN: keepModifiers */
+ /** Also keep any classes in the descriptors of specified fields and methods. */
+ object includedescriptorclasses extends KeepOptionModifier("includedescriptorclasses")
+
+ /** Allow the specified entry points to be removed in the shrinking step. */
+ object allowshrinking extends KeepOptionModifier("allowshrinking")
+
+ /** Allow the specified entry points to be modified in the optimization step. */
+ object allowoptimization extends KeepOptionModifier("allowoptimization")
+
+ /** Allow the specified entry points to be renamed in the obfuscation step. */
+ object allowobfuscation extends KeepOptionModifier("allowobfuscation")
+ /* AUTO GENERATED SECTION END: keepModifiers */
+}
+
+object ProGuard {
+ val artifactId = "proguard-base"
+ val groupId = "net.sf.proguard"
+ val version = "5.3.2"
+ val mainClass = "proguard.ProGuard"
+ val `rt.jar` = Seq(new File(System.getProperty("java.home"), "lib/rt.jar"))
+}
+case class ProGuard[T](
+ main: Seq[String] => Int,
+ T: Seq[File] => T,
+ log: String => Unit = _ => ()
+) {
+
+ /**
+ Typed interface on top of the proguard command line tool.
+ Check the official ProGuard docs for usage.
+ Use `Some(None)` to call an option without arguments.
+ Use `true` to set a flag.
+
+ @see https://www.guardsquare.com/en/proguard/manual/refcard
+ @see https://www.guardsquare.com/en/proguard/manual/usage
+
+ /* AUTO GENERATED SECTION BEGIN: docs */
+ @param include Read configuration options from the given file.
+ @param basedirectory Specifies the base directory for subsequent relative file names.
+ @param injars Specifies the program jars (or wars, ears, zips, or directories).
+ @param outjars Specifies the names of the output jars (or wars, ears, zips, or directories).
+ @param libraryjars Specifies the library jars (or wars, ears, zips, or directories).
+ @param skipnonpubliclibraryclasses Ignore non-public library classes.
+ @param dontskipnonpubliclibraryclasses Don't ignore non-public library classes (the default).
+ @param dontskipnonpubliclibraryclassmembers Don't ignore package visible library class members.
+ @param keepdirectories Keep the specified directories in the output jars (or wars, ears, zips, or directories).
+ @param target Set the given version number in the processed classes.
+ @param forceprocessing Process the input, even if the output seems up to date.
+ @param keep Preserve the specified classes and class members.
+ @param keepclassmembers Preserve the specified class members, if their classes are preserved as well.
+ @param keepclasseswithmembers Preserve the specified classes and class members, if all of the specified class members are present.
+ @param keepnames Preserve the names of the specified classes and class members (if they aren't removed in the shrinking step).
+ @param keepclassmembernames Preserve the names of the specified class members (if they aren't removed in the shrinking step).
+ @param keepclasseswithmembernames Preserve the names of the specified classes and class members, if all of the specified class members are present (after the shrinking step).
+ @param printseeds List classes and class members matched by the various -keep options, to the standard output or to the given file.
+ @param dontshrink Don't shrink the input class files.
+ @param printusage List dead code of the input class files, to the standard output or to the given file.
+ @param whyareyoukeeping Print details on why the given classes and class members are being kept in the shrinking step.
+ @param dontoptimize Don't optimize the input class files.
+ @param optimizations The optimizations to be enabled and disabled.
+ @param optimizationpasses The number of optimization passes to be performed.
+ @param assumenosideeffects Assume that the specified methods don't have any side effects, while optimizing.
+ @param allowaccessmodification Allow the access modifiers of classes and class members to be modified, while optimizing.
+ @param mergeinterfacesaggressively Allow any interfaces to be merged, while optimizing.
+ @param dontobfuscate Don't obfuscate the input class files.
+ @param printmapping Print the mapping from old names to new names for classes and class members that have been renamed, to the standard output or to the given file.
+ @param applymapping Reuse the given mapping, for incremental obfuscation.
+ @param obfuscationdictionary Use the words in the given text file as obfuscated field names and method names.
+ @param classobfuscationdictionary Use the words in the given text file as obfuscated class names.
+ @param packageobfuscationdictionary Use the words in the given text file as obfuscated package names.
+ @param overloadaggressively Apply aggressive overloading while obfuscating.
+ @param useuniqueclassmembernames Ensure uniform obfuscated class member names for subsequent incremental obfuscation.
+ @param dontusemixedcaseclassnames Don't generate mixed-case class names while obfuscating.
+ @param keeppackagenames Keep the specified package names from being obfuscated.
+ @param flattenpackagehierarchy Repackage all packages that are renamed into the single given parent package.
+ @param repackageclasses Repackage all class files that are renamed into the single given package.
+ @param keepattributes Preserve the given optional attributes; typically Exceptions, InnerClasses, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Synthetic, EnclosingMethod, and *Annotation*.
+ @param keepparameternames Keep the parameter names and types of methods that are kept.
+ @param renamesourcefileattribute Put the given constant string in the SourceFile attributes.
+ @param adaptclassstrings Adapt string constants in the specified classes, based on the obfuscated names of any corresponding classes.
+ @param adaptresourcefilenames Rename the specified resource files, based on the obfuscated names of the corresponding class files.
+ @param adaptresourcefilecontents Update the contents of the specified resource files, based on the obfuscated names of the processed classes.
+ @param dontpreverify Don't preverify the processed class files.
+ @param microedition Target the processed class files at Java Micro Edition.
+ @param verbose Write out some more information during processing.
+ @param dontnote Don't print notes about potential mistakes or omissions in the configuration.
+ @param dontwarn Don't warn about unresolved references at all.
+ @param ignorewarnings Print warnings about unresolved references, but continue processing anyhow.
+ @param printconfiguration Write out the entire configuration in traditional ProGuard style, to the standard output or to the given file.
+ @param dump Write out the internal structure of the processed class files, to the standard output or to the given file.
+ /* AUTO GENERATED SECTION END: docs */
+ */
+ case class proguard(
+ /* AUTO GENERATED SECTION BEGIN: params */
+ include: Option[File] = None,
+ basedirectory: Option[File] = None,
+ injars: Option[Seq[File]] = None,
+ outjars: Option[Seq[File]] = None,
+ libraryjars: Option[Seq[File]] = None,
+ skipnonpubliclibraryclasses: Boolean = false,
+ dontskipnonpubliclibraryclasses: Boolean = false,
+ dontskipnonpubliclibraryclassmembers: Boolean = false,
+ keepdirectories: Option[Option[String]] = None,
+ target: Option[String] = None,
+ forceprocessing: Boolean = false,
+ keep: Option[(Seq[KeepOptionModifier], String)] = None,
+ keepclassmembers: Option[(Seq[KeepOptionModifier], String)] = None,
+ keepclasseswithmembers: Option[(Seq[KeepOptionModifier], String)] = None,
+ keepnames: Option[String] = None,
+ keepclassmembernames: Option[String] = None,
+ keepclasseswithmembernames: Option[String] = None,
+ printseeds: Option[Option[File]] = None,
+ dontshrink: Boolean = false,
+ printusage: Option[Option[File]] = None,
+ whyareyoukeeping: Option[String] = None,
+ dontoptimize: Boolean = false,
+ optimizations: Option[String] = None,
+ optimizationpasses: Option[Int] = None,
+ assumenosideeffects: Option[String] = None,
+ allowaccessmodification: Boolean = false,
+ mergeinterfacesaggressively: Boolean = false,
+ dontobfuscate: Boolean = false,
+ printmapping: Option[Option[File]] = None,
+ applymapping: Option[File] = None,
+ obfuscationdictionary: Option[File] = None,
+ classobfuscationdictionary: Option[File] = None,
+ packageobfuscationdictionary: Option[File] = None,
+ overloadaggressively: Boolean = false,
+ useuniqueclassmembernames: Boolean = false,
+ dontusemixedcaseclassnames: Boolean = false,
+ keeppackagenames: Option[Option[String]] = None,
+ flattenpackagehierarchy: Option[Option[String]] = None,
+ repackageclasses: Option[Option[String]] = None,
+ keepattributes: Option[Option[String]] = None,
+ keepparameternames: Boolean = false,
+ renamesourcefileattribute: Option[Option[String]] = None,
+ adaptclassstrings: Option[Option[String]] = None,
+ adaptresourcefilenames: Option[Option[String]] = None,
+ adaptresourcefilecontents: Option[Option[String]] = None,
+ dontpreverify: Boolean = false,
+ microedition: Boolean = false,
+ verbose: Boolean = false,
+ dontnote: Option[Option[String]] = None,
+ dontwarn: Option[Option[String]] = None,
+ ignorewarnings: Boolean = false,
+ printconfiguration: Option[Option[File]] = None,
+ dump: Option[Option[File]] = None
+ /* AUTO GENERATED SECTION END: params */
+ ) extends (() => T) {
+ // type class rendering scala values into string arguments
+ private class argsFor[T](val apply: T => Option[Seq[String]])
+ private object argsFor {
+ def apply[T: argsFor](value: T) = implicitly[argsFor[T]].apply(value)
+ implicit object SeqFile extends argsFor[Seq[File]](v => Some(Seq(v.map(_.getPath).mkString(":"))))
+ implicit object File extends argsFor[File](v => Some(Seq(v.getPath)))
+ implicit object String extends argsFor[String](v => Some(Seq(v)))
+ implicit object Int extends argsFor[Int](i => Some(Seq(i.toString)))
+ implicit object Boolean
+ extends argsFor[Boolean]({
+ case false => None
+ case true => Some(Nil)
+ })
+ implicit def Option2[T: argsFor]: argsFor[Option[T]] = new argsFor(
+ _.map(implicitly[argsFor[T]].apply(_).toSeq.flatten)
+ )
+ implicit def Option3[T: argsFor]: argsFor[Option[Option[String]]] =
+ new argsFor(_.map(_.toSeq))
+ implicit def SpecWithModifiers: argsFor[(Seq[KeepOptionModifier], String)] =
+ new argsFor({
+ case (modifiers, spec) =>
+ Some(Seq(modifiers.map(_.string).map("," ++ _).mkString).filterNot(_ == "") :+ spec)
+ })
+ }
+
+ // capture string argument values and names
+ val args = (
+ /* AUTO GENERATED SECTION BEGIN: args */
+ argsFor(include).map("-include" +: _)
+ ++ argsFor(basedirectory).map("-basedirectory" +: _)
+ ++ argsFor(injars).map("-injars" +: _)
+ ++ argsFor(outjars).map("-outjars" +: _)
+ ++ argsFor(libraryjars).map("-libraryjars" +: _)
+ ++ argsFor(skipnonpubliclibraryclasses).map("-skipnonpubliclibraryclasses" +: _)
+ ++ argsFor(dontskipnonpubliclibraryclasses).map("-dontskipnonpubliclibraryclasses" +: _)
+ ++ argsFor(dontskipnonpubliclibraryclassmembers).map("-dontskipnonpubliclibraryclassmembers" +: _)
+ ++ argsFor(keepdirectories).map("-keepdirectories" +: _)
+ ++ argsFor(target).map("-target" +: _)
+ ++ argsFor(forceprocessing).map("-forceprocessing" +: _)
+ ++ argsFor(keep).map("-keep" +: _)
+ ++ argsFor(keepclassmembers).map("-keepclassmembers" +: _)
+ ++ argsFor(keepclasseswithmembers).map("-keepclasseswithmembers" +: _)
+ ++ argsFor(keepnames).map("-keepnames" +: _)
+ ++ argsFor(keepclassmembernames).map("-keepclassmembernames" +: _)
+ ++ argsFor(keepclasseswithmembernames).map("-keepclasseswithmembernames" +: _)
+ ++ argsFor(printseeds).map("-printseeds" +: _)
+ ++ argsFor(dontshrink).map("-dontshrink" +: _)
+ ++ argsFor(printusage).map("-printusage" +: _)
+ ++ argsFor(whyareyoukeeping).map("-whyareyoukeeping" +: _)
+ ++ argsFor(dontoptimize).map("-dontoptimize" +: _)
+ ++ argsFor(optimizations).map("-optimizations" +: _)
+ ++ argsFor(optimizationpasses).map("-optimizationpasses" +: _)
+ ++ argsFor(assumenosideeffects).map("-assumenosideeffects" +: _)
+ ++ argsFor(allowaccessmodification).map("-allowaccessmodification" +: _)
+ ++ argsFor(mergeinterfacesaggressively).map("-mergeinterfacesaggressively" +: _)
+ ++ argsFor(dontobfuscate).map("-dontobfuscate" +: _)
+ ++ argsFor(printmapping).map("-printmapping" +: _)
+ ++ argsFor(applymapping).map("-applymapping" +: _)
+ ++ argsFor(obfuscationdictionary).map("-obfuscationdictionary" +: _)
+ ++ argsFor(classobfuscationdictionary).map("-classobfuscationdictionary" +: _)
+ ++ argsFor(packageobfuscationdictionary).map("-packageobfuscationdictionary" +: _)
+ ++ argsFor(overloadaggressively).map("-overloadaggressively" +: _)
+ ++ argsFor(useuniqueclassmembernames).map("-useuniqueclassmembernames" +: _)
+ ++ argsFor(dontusemixedcaseclassnames).map("-dontusemixedcaseclassnames" +: _)
+ ++ argsFor(keeppackagenames).map("-keeppackagenames" +: _)
+ ++ argsFor(flattenpackagehierarchy).map("-flattenpackagehierarchy" +: _)
+ ++ argsFor(repackageclasses).map("-repackageclasses" +: _)
+ ++ argsFor(keepattributes).map("-keepattributes" +: _)
+ ++ argsFor(keepparameternames).map("-keepparameternames" +: _)
+ ++ argsFor(renamesourcefileattribute).map("-renamesourcefileattribute" +: _)
+ ++ argsFor(adaptclassstrings).map("-adaptclassstrings" +: _)
+ ++ argsFor(adaptresourcefilenames).map("-adaptresourcefilenames" +: _)
+ ++ argsFor(adaptresourcefilecontents).map("-adaptresourcefilecontents" +: _)
+ ++ argsFor(dontpreverify).map("-dontpreverify" +: _)
+ ++ argsFor(microedition).map("-microedition" +: _)
+ ++ argsFor(verbose).map("-verbose" +: _)
+ ++ argsFor(dontnote).map("-dontnote" +: _)
+ ++ argsFor(dontwarn).map("-dontwarn" +: _)
+ ++ argsFor(ignorewarnings).map("-ignorewarnings" +: _)
+ ++ argsFor(printconfiguration).map("-printconfiguration" +: _)
+ ++ argsFor(dump).map("-dump" +: _)
+ /* AUTO GENERATED SECTION END: args */
+ ).flatten.toSeq
+
+ def apply: T = {
+ outjars.foreach(_.map(_.toPath).map(deleteIfExists))
+ val c = main(args)
+ if (c != 0) throw new Exception
+ T(outjars.toSeq.flatten)
+ }
+ }
+}
diff --git a/libraries/proguard/Readme.md b/libraries/proguard/Readme.md
new file mode 100644
index 0000000..36d61d4
--- /dev/null
+++ b/libraries/proguard/Readme.md
@@ -0,0 +1,4 @@
+Type-safe Scala api on top of proguards .main method.
+
+TODO:
+- capture stdout and make it available to the caller via stream
diff --git a/libraries/proguard/build/build.scala b/libraries/proguard/build/build.scala
new file mode 100644
index 0000000..399714e
--- /dev/null
+++ b/libraries/proguard/build/build.scala
@@ -0,0 +1,151 @@
+package cbt_build.proguard
+import cbt._
+import java.nio.file.Files._
+import java.net._
+import java.io._
+import scala.xml._
+
+class Build(val context: Context) extends Scalafmt with GeneratedSections{
+ def refcard = projectDirectory / "spec/refcard.html"
+
+ /** downloads html proguard parameter specification */
+ def updateSpec = {
+ System.err.println(lib.blue("downloading ")+refcard)
+ lib.download(
+ new URL("https://www.guardsquare.com/en/proguard/manual/refcard"),
+ refcard,
+ None,
+ replace = true
+ )
+ System.err.println("simplifying html")
+ val tables = (
+ loadSloppyHtml( refcard.readAsString ) \ "body" \\ "table"
+ )
+ val s = (
+ "<html><body>\n" ++ tables.map( table =>
+ " <table>\n" ++ (table \\ "tr").map( tr =>
+ " <tr>\n" ++ (tr \\ "td").map( td =>
+ " <td>" ++ td.text ++ "</td>\n"
+ ).mkString ++ " </tr>\n"
+ ).mkString ++ " </table>\n"
+ ).mkString ++ "</body></html>\n"
+ )
+ System.err.println("writing file")
+ write( refcard.toPath, s.getBytes)
+ }
+
+ private def loadSloppyHtml(html: String): scala.xml.Elem = {
+ object XmlNotDownloadingDTD extends scala.xml.factory.XMLLoader[scala.xml.Elem] {
+ override def parser: javax.xml.parsers.SAXParser = {
+ val f = javax.xml.parsers.SAXParserFactory.newInstance()
+ f.setNamespaceAware(false)
+ f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ f.newSAXParser()
+ }
+ }
+
+ val p = new org.ccil.cowan.tagsoup.Parser
+ val w = new StringWriter
+ p.setContentHandler(new org.ccil.cowan.tagsoup.XMLWriter(w))
+ p.parse(
+ new org.xml.sax.InputSource(
+ new ByteArrayInputStream( "<!DOCTYPE[^<]*>".r.replaceFirstIn( html, "" ).getBytes )
+ )
+ )
+ XmlNotDownloadingDTD.loadString( w.toString )
+ }
+
+ override def scalafmtConfig = {
+ import org.scalafmt.config._
+ ScalafmtConfig.defaultWithAlign.copy(
+ maxColumn = 120,
+ continuationIndent = super.scalafmtConfig.continuationIndent.copy(
+ defnSite = 2
+ ),
+ align = super.scalafmtConfig.align.copy(
+ tokens = AlignToken.default,
+ arrowEnumeratorGenerator = true,
+ mixedOwners = true
+ ),
+ binPack = super.scalafmtConfig.binPack.copy(
+ parentConstructors = true
+ ),
+ spaces = super.scalafmtConfig.spaces.copy(
+ inImportCurlyBraces = true
+ ),
+ lineEndings = LineEndings.unix,
+ newlines = super.scalafmtConfig.newlines.copy(
+ penalizeSingleSelectMultiArgList = false
+ ),
+ runner = super.scalafmtConfig.runner.copy(
+ optimizer = super.scalafmtConfig.runner.optimizer.copy(
+ forceConfigStyleOnOffset = -1
+ )
+ )
+ )
+ }
+
+ /** generates Scala code from parameter specification html */
+ def replacements = {
+ val tables = XML.loadFile(refcard) \\ "table"
+ def cellsToSeq( node: Node ) = (node \\ "tr").map(
+ tr => (tr \\ "td").map( td => td.text ) match {
+ case Seq( k, v ) => k -> v
+ }
+ )
+ val options = cellsToSeq( tables(0) ).collect{
+ case (k, v) if k.startsWith("-") => k.drop(1).split(" ").toList -> v
+ }.map{
+ case (k,description) =>
+ val name = k(0)
+ val tpe = k.drop(1).mkString(" ") match {
+ case "" => "Boolean"
+ case "n" => "Int"
+ case "class_specification" | "version" | "optimization_filter" => "String"
+ case "filename" | "directoryname" => "File"
+ case "class_path" if name === "outjars" => "Seq[File]"
+ case "class_path" => "Seq[File]"
+ case "[filename]" => "Option[File]"
+ case "[directory_filter]" | "[package_filter]" | "[package_name]"
+ | "[attribute_filter]" | "[string]" | "[class_filter]" | "[file_filter]"
+ => "Option[String]"
+ case "[,modifier,...] class_specification" => "(Seq[KeepOptionModifier], String)"
+ }
+ (name, tpe, description.split("\n").mkString(" "))
+ }//.sortBy(_._1)
+
+ val docs = options.map{
+ case (name, tpe, description) => s" @param $name $description"
+ }.mkString("\n")
+
+ val params = options.map{
+ case v@(_, "Boolean", _) => v -> Some("false")
+ case (n, t, d) => (n, s"Option[$t]", d) -> Some("None")
+ }.map{
+ case ((name, tpe, description), default) => s" $name: $tpe" ++ default.map(" = "++_).getOrElse("")
+ }.mkString(",\n")
+
+ val keepModifiers = cellsToSeq( tables(2) ).map{
+ case (k, v) => s""" /** $v */\n object $k extends KeepOptionModifier("$k")"""
+ }.mkString("\n")
+
+ val args = options.map{
+ case (name, _, description) => s"""argsFor($name).map("-$name" +: _)"""
+ }.mkString("\n++ ")
+
+ Seq(
+ "keepModifiers" -> keepModifiers,
+ "docs" -> docs,
+ "args" -> args,
+ "params" -> params
+ )
+ }
+ override def generate{
+ super.generate
+ compile
+ }
+ override def compile = {
+ scalafmt
+ super.compile
+ }
+}
diff --git a/plugins/proguard/build/build/build.scala b/libraries/proguard/build/build/build.scala
index 7928cfa..7928cfa 100644
--- a/plugins/proguard/build/build/build.scala
+++ b/libraries/proguard/build/build/build.scala
diff --git a/plugins/proguard/spec/refcard.html b/libraries/proguard/spec/refcard.html
index 9cb0c72..9cb0c72 100644
--- a/plugins/proguard/spec/refcard.html
+++ b/libraries/proguard/spec/refcard.html
diff --git a/plugins/proguard/Proguard.scala b/plugins/proguard/Proguard.scala
new file mode 100644
index 0000000..486d969
--- /dev/null
+++ b/plugins/proguard/Proguard.scala
@@ -0,0 +1,32 @@
+package cbt
+
+trait ProGuard extends BaseBuild {
+ def proguard: () => ClassPath
+ def ProGuard(keep: (Seq[cbt.proguard.KeepOptionModifier], String)) = {
+ cbt.ProGuard(context).proguard(
+ outjars = Some( Seq(scalaTarget / "proguarded.jar") ),
+ injars = Some( classpath.files ),
+ libraryjars = Some( ClassPath( cbt.proguard.ProGuard.`rt.jar` ).files ),
+ keep = Some( keep )
+ )
+ }
+}
+
+object ProGuard {
+ def apply( implicit context: Context ) = {
+ import context._
+ val lib = new Lib(context.logger)
+ import cbt.proguard.ProGuard._
+ cbt.proguard.ProGuard(
+ (args: Seq[String]) => MavenResolver(
+ cbtLastModified, context.paths.mavenCache, mavenCentral
+ )(
+ context.logger, transientCache, context.classLoaderCache
+ ).bindOne(
+ MavenDependency(groupId, artifactId, version)
+ ).runMain(cbt.proguard.ProGuard.mainClass, args: _*).integer,
+ ClassPath(_),
+ context.logger.log("proguard",_)
+ )
+ }
+}
diff --git a/plugins/proguard/build/build.scala b/plugins/proguard/build/build.scala
index 5edd7a8..4237e94 100644
--- a/plugins/proguard/build/build.scala
+++ b/plugins/proguard/build/build.scala
@@ -1,129 +1,8 @@
package cbt_build.proguard
import cbt._
-import java.nio.file.Files._
-import java.net._
-import java.io._
-import scala.xml._
-class Build(val context: Context) extends Plugin with Scalafmt{
+class Build(val context: Context) extends Plugin{
override def dependencies = (
super.dependencies ++ // don't forget super.dependencies here for scala-library, etc.
- Resolver( mavenCentral ).bind(
- MavenDependency("net.sf.proguard","proguard-base","5.3.2")
- ) :+ libraries.captureArgs
+ Seq( libraries.proguard )
)
-
- def refcard = projectDirectory / "spec/refcard.html"
-
- /** downloads html proguard parameter specification */
- def updateSpec = {
- System.err.println(lib.blue("downloading ")+refcard)
- lib.download(
- new URL("https://www.guardsquare.com/en/proguard/manual/refcard"),
- refcard,
- None,
- replace = true
- )
- System.err.println("simplifying html")
- val tables = (
- loadSloppyHtml( new String( readAllBytes( refcard.toPath ) ) ) \ "body" \\ "table"
- )
- val s = (
- "<html><body>\n" ++ tables.map( table =>
- " <table>\n" ++ (table \\ "tr").map( tr =>
- " <tr>\n" ++ (tr \\ "td").map( td =>
- " <td>" ++ td.text ++ "</td>\n"
- ).mkString ++ " </tr>\n"
- ).mkString ++ " </table>\n"
- ).mkString ++ "</body></html>\n"
- )
- System.err.println("writing file")
- write( refcard.toPath, s.getBytes)
- }
-
- private def loadSloppyHtml(html: String): scala.xml.Elem = {
- object XmlNotDownloadingDTD extends scala.xml.factory.XMLLoader[scala.xml.Elem] {
- override def parser: javax.xml.parsers.SAXParser = {
- val f = javax.xml.parsers.SAXParserFactory.newInstance()
- f.setNamespaceAware(false)
- f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
- f.newSAXParser()
- }
- }
-
- val p = new org.ccil.cowan.tagsoup.Parser
- val w = new StringWriter
- p.setContentHandler(new org.ccil.cowan.tagsoup.XMLWriter(w))
- p.parse(
- new org.xml.sax.InputSource(
- new ByteArrayInputStream( "<!DOCTYPE[^<]*>".r.replaceFirstIn( html, "" ).getBytes )
- )
- )
- XmlNotDownloadingDTD.loadString( w.toString )
- }
-
- /** generates Scala code from parameter specification html */
- def generate = {
- val tables = XML.loadFile(refcard) \\ "table"
- def cellsToSeq( node: Node ) = (node \\ "tr").map(
- tr => (tr \\ "td").map( td => td.text ) match {
- case Seq( k, v ) => k -> v
- }
- )
- val options = cellsToSeq( tables(0) ).collect{
- case (k, v) if k.startsWith("-") => k.drop(1).split(" ").toList -> v
- }.map{
- case (k,description) =>
- val name = k(0)
- val tpe = k.drop(1).mkString(" ") match {
- case "" => "Boolean"
- case "n" => "Int"
- case "class_specification" | "version" | "optimization_filter" => "String"
- case "filename" | "directoryname" => "File"
- case "class_path" if name === "outjars" => "Seq[File]"
- case "class_path" => "ClassPath"
- case "[filename]" => "Option[File]"
- case "[directory_filter]" | "[package_filter]" | "[package_name]"
- | "[attribute_filter]" | "[string]" | "[class_filter]" | "[file_filter]"
- => "Option[String]"
- case "[,modifier,...] class_specification" => "(Seq[KeepOptionModifier], String)"
- }
- (name, tpe, description.split("\n").mkString(" "))
- }.sortBy(_._1)
-
- val docs = options.map{
- case (name, tpe, description) => s" @param $name $description"
- }.mkString("\n")
-
- val args = options.map{
- case v@(_, "Boolean", _) => v -> Some("false")
- case v@("injars" | "libraryjars" | "keep" | "outjars", _, _) => v -> None
- case (n, t, d) => (n, s"Option[$t]", d) -> Some("None")
- }.map{
- case ((name, tpe, description), default) => s" $name: $tpe" ++ default.map(" = "++_).getOrElse("")
- }.mkString(",\n")
-
- val keepModifiers = cellsToSeq( tables(2) ).map{
- case (k, v) => s""" /** $v */\n object $k extends KeepOptionModifier("$k")"""
- }.mkString("\n")
-
- val template = new String(
- readAllBytes(
- (projectDirectory / "templates/Proguard.scala").toPath
- )
- )
- val code = (
- "/* automatically generated by build/build.scala from templates/Proguard.scala */\n" ++
- template
- .replace ("/* ${generated-top-level} */", keepModifiers )
- .replace( "${generated-docs}", docs )
- .replace( "/* ${generated-args} */", args )
- )
-
- val targetFile = projectDirectory / "src/generated/Proguard.scala"
- targetFile.getParentFile.mkdirs
- write( targetFile.toPath, code.getBytes )
-
- scalafmt
- compile
- }
}
diff --git a/plugins/proguard/src/generated/Proguard.scala b/plugins/proguard/src/generated/Proguard.scala
deleted file mode 100644
index 553df07..0000000
--- a/plugins/proguard/src/generated/Proguard.scala
+++ /dev/null
@@ -1,226 +0,0 @@
-/* automatically generated by build/build.scala from templates/Proguard.scala */
-package cbt
-import java.io.File
-import java.nio.file.Files.deleteIfExists
-
-sealed class KeepOptionModifier(val string: String)
-object KeepOptionModifier {
-
- /** Also keep any classes in the descriptors of specified fields and methods. */
- object includedescriptorclasses
- extends KeepOptionModifier("includedescriptorclasses")
-
- /** Allow the specified entry points to be removed in the shrinking step. */
- object allowshrinking extends KeepOptionModifier("allowshrinking")
-
- /** Allow the specified entry points to be modified in the optimization step. */
- object allowoptimization extends KeepOptionModifier("allowoptimization")
-
- /** Allow the specified entry points to be renamed in the obfuscation step. */
- object allowobfuscation extends KeepOptionModifier("allowobfuscation")
-}
-
-trait Proguard extends BaseBuild {
- def proguardKeep(keep: (Seq[KeepOptionModifier], String)) = {
- ProguardLib(context.cbtLastModified, context.paths.mavenCache).proguard(
- outjars = Seq(scalaTarget / "proguarded.jar"),
- injars = classpath,
- libraryjars = Proguard.`rt.jar`,
- keep = keep
- )
- }
-}
-
-object Proguard {
- val version = "5.3.2"
- val `rt.jar` = ClassPath(
- Seq(new File(System.getProperty("java.home"), "lib/rt.jar")))
-}
-
-case class ProguardLib(
- cbtLastModified: Long,
- mavenCache: File,
- dependency: Option[DependencyImplementation] = None
-)(
- implicit logger: Logger,
- transientCache: java.util.Map[AnyRef, AnyRef],
- classLoaderCache: ClassLoaderCache
-) {
-
- /**
- Typed interface on top of the proguard command line tool.
- Check the official ProGuard docs for usage.
- Use `Some(None)` to call an option without arguments.
- Use `true` to set a flag.
-
- @see https://www.guardsquare.com/en/proguard/manual/refcard
- @see https://www.guardsquare.com/en/proguard/manual/usage
-
- @param adaptclassstrings Adapt string constants in the specified classes, based on the obfuscated names of any corresponding classes.
- @param adaptresourcefilecontents Update the contents of the specified resource files, based on the obfuscated names of the processed classes.
- @param adaptresourcefilenames Rename the specified resource files, based on the obfuscated names of the corresponding class files.
- @param allowaccessmodification Allow the access modifiers of classes and class members to be modified, while optimizing.
- @param applymapping Reuse the given mapping, for incremental obfuscation.
- @param assumenosideeffects Assume that the specified methods don't have any side effects, while optimizing.
- @param basedirectory Specifies the base directory for subsequent relative file names.
- @param classobfuscationdictionary Use the words in the given text file as obfuscated class names.
- @param dontnote Don't print notes about potential mistakes or omissions in the configuration.
- @param dontobfuscate Don't obfuscate the input class files.
- @param dontoptimize Don't optimize the input class files.
- @param dontpreverify Don't preverify the processed class files.
- @param dontshrink Don't shrink the input class files.
- @param dontskipnonpubliclibraryclasses Don't ignore non-public library classes (the default).
- @param dontskipnonpubliclibraryclassmembers Don't ignore package visible library class members.
- @param dontusemixedcaseclassnames Don't generate mixed-case class names while obfuscating.
- @param dontwarn Don't warn about unresolved references at all.
- @param dump Write out the internal structure of the processed class files, to the standard output or to the given file.
- @param flattenpackagehierarchy Repackage all packages that are renamed into the single given parent package.
- @param forceprocessing Process the input, even if the output seems up to date.
- @param ignorewarnings Print warnings about unresolved references, but continue processing anyhow.
- @param include Read configuration options from the given file.
- @param injars Specifies the program jars (or wars, ears, zips, or directories).
- @param keep Preserve the specified classes and class members.
- @param keepattributes Preserve the given optional attributes; typically Exceptions, InnerClasses, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Synthetic, EnclosingMethod, and *Annotation*.
- @param keepclasseswithmembernames Preserve the names of the specified classes and class members, if all of the specified class members are present (after the shrinking step).
- @param keepclasseswithmembers Preserve the specified classes and class members, if all of the specified class members are present.
- @param keepclassmembernames Preserve the names of the specified class members (if they aren't removed in the shrinking step).
- @param keepclassmembers Preserve the specified class members, if their classes are preserved as well.
- @param keepdirectories Keep the specified directories in the output jars (or wars, ears, zips, or directories).
- @param keepnames Preserve the names of the specified classes and class members (if they aren't removed in the shrinking step).
- @param keeppackagenames Keep the specified package names from being obfuscated.
- @param keepparameternames Keep the parameter names and types of methods that are kept.
- @param libraryjars Specifies the library jars (or wars, ears, zips, or directories).
- @param mergeinterfacesaggressively Allow any interfaces to be merged, while optimizing.
- @param microedition Target the processed class files at Java Micro Edition.
- @param obfuscationdictionary Use the words in the given text file as obfuscated field names and method names.
- @param optimizationpasses The number of optimization passes to be performed.
- @param optimizations The optimizations to be enabled and disabled.
- @param outjars Specifies the names of the output jars (or wars, ears, zips, or directories).
- @param overloadaggressively Apply aggressive overloading while obfuscating.
- @param packageobfuscationdictionary Use the words in the given text file as obfuscated package names.
- @param printconfiguration Write out the entire configuration in traditional ProGuard style, to the standard output or to the given file.
- @param printmapping Print the mapping from old names to new names for classes and class members that have been renamed, to the standard output or to the given file.
- @param printseeds List classes and class members matched by the various -keep options, to the standard output or to the given file.
- @param printusage List dead code of the input class files, to the standard output or to the given file.
- @param renamesourcefileattribute Put the given constant string in the SourceFile attributes.
- @param repackageclasses Repackage all class files that are renamed into the single given package.
- @param skipnonpubliclibraryclasses Ignore non-public library classes.
- @param target Set the given version number in the processed classes.
- @param useuniqueclassmembernames Ensure uniform obfuscated class member names for subsequent incremental obfuscation.
- @param verbose Write out some more information during processing.
- @param whyareyoukeeping Print details on why the given classes and class members are being kept in the shrinking step.
- */
- case class proguard(
- adaptclassstrings: Option[Option[String]] = None,
- adaptresourcefilecontents: Option[Option[String]] = None,
- adaptresourcefilenames: Option[Option[String]] = None,
- allowaccessmodification: Boolean = false,
- applymapping: Option[File] = None,
- assumenosideeffects: Option[String] = None,
- basedirectory: Option[File] = None,
- classobfuscationdictionary: Option[File] = None,
- dontnote: Option[Option[String]] = None,
- dontobfuscate: Boolean = false,
- dontoptimize: Boolean = false,
- dontpreverify: Boolean = false,
- dontshrink: Boolean = false,
- dontskipnonpubliclibraryclasses: Boolean = false,
- dontskipnonpubliclibraryclassmembers: Boolean = false,
- dontusemixedcaseclassnames: Boolean = false,
- dontwarn: Option[Option[String]] = None,
- dump: Option[Option[File]] = None,
- flattenpackagehierarchy: Option[Option[String]] = None,
- forceprocessing: Boolean = false,
- ignorewarnings: Boolean = false,
- include: Option[File] = None,
- injars: ClassPath,
- keep: (Seq[KeepOptionModifier], String),
- keepattributes: Option[Option[String]] = None,
- keepclasseswithmembernames: Option[String] = None,
- keepclasseswithmembers: Option[(Seq[KeepOptionModifier], String)] = None,
- keepclassmembernames: Option[String] = None,
- keepclassmembers: Option[(Seq[KeepOptionModifier], String)] = None,
- keepdirectories: Option[Option[String]] = None,
- keepnames: Option[String] = None,
- keeppackagenames: Option[Option[String]] = None,
- keepparameternames: Boolean = false,
- libraryjars: ClassPath,
- mergeinterfacesaggressively: Boolean = false,
- microedition: Boolean = false,
- obfuscationdictionary: Option[File] = None,
- optimizationpasses: Option[Int] = None,
- optimizations: Option[String] = None,
- outjars: Seq[File],
- overloadaggressively: Boolean = false,
- packageobfuscationdictionary: Option[File] = None,
- printconfiguration: Option[Option[File]] = None,
- printmapping: Option[Option[File]] = None,
- printseeds: Option[Option[File]] = None,
- printusage: Option[Option[File]] = None,
- renamesourcefileattribute: Option[Option[String]] = None,
- repackageclasses: Option[Option[String]] = None,
- skipnonpubliclibraryclasses: Boolean = false,
- target: Option[String] = None,
- useuniqueclassmembernames: Boolean = false,
- verbose: Boolean = false,
- whyareyoukeeping: Option[String] = None
- ) extends (() => ClassPath) {
-
- // type class rendering scala values into string arguments
- private class valueToStrings[T](val apply: T => Option[Seq[String]])
- private object valueToStrings {
- def apply[T: valueToStrings](value: T) =
- implicitly[valueToStrings[T]].apply(value)
- implicit object SeqFile
- extends valueToStrings[Seq[File]](v => Some(v.map(_.string)))
- implicit object ClassPath
- extends valueToStrings[ClassPath](v => Some(Seq(v.string)))
- implicit object File
- extends valueToStrings[File](v => Some(Seq(v.string)))
- implicit object String extends valueToStrings[String](v => Some(Seq(v)))
- implicit object Int
- extends valueToStrings[Int](i => Some(Seq(i.toString)))
- implicit object Boolean
- extends valueToStrings[Boolean]({
- case false => None
- case true => Some(Nil)
- })
- implicit def Option2[T: valueToStrings]: valueToStrings[Option[T]] =
- new valueToStrings(
- _.map(implicitly[valueToStrings[T]].apply(_).toSeq.flatten)
- )
- implicit def Option3[T: valueToStrings]
- : valueToStrings[Option[Option[String]]] =
- new valueToStrings(_.map(_.toSeq))
- implicit def SpecWithModifiers: valueToStrings[(Seq[KeepOptionModifier],
- String)] =
- new valueToStrings({
- case (modifiers, spec) =>
- Some(
- Seq(modifiers.map(_.string).map("," ++ _).mkString)
- .filterNot(_ == "") :+ spec)
- })
- }
-
- // capture string argument values and names
- val capturedArgs = capture_args.captureArgs
-
- def apply: ClassPath = {
- val args = capturedArgs.args
- .map(arg => arg.copy(name = "-" ++ arg.name))
- .flatMap(_.toSeqOption)
- .flatten
- outjars.map(_.toPath).map(deleteIfExists)
- val c = dependency getOrElse MavenResolver(cbtLastModified,
- mavenCache,
- mavenCentral).bindOne(
- MavenDependency("net.sf.proguard", "proguard-base", Proguard.version)
- ) runMain (
- "proguard.ProGuard",
- args: _*
- )
- if (c != ExitCode.Success) throw new Exception
- ClassPath(outjars)
- }
- }
-}
diff --git a/plugins/proguard/templates/Proguard.scala b/plugins/proguard/templates/Proguard.scala
deleted file mode 100644
index 23a3117..0000000
--- a/plugins/proguard/templates/Proguard.scala
+++ /dev/null
@@ -1,85 +0,0 @@
-package cbt
-import java.io.File
-import java.nio.file.Files.deleteIfExists
-
-sealed class KeepOptionModifier(val string: String)
-object KeepOptionModifier{
-/* ${generated-top-level} */
-}
-
-trait Proguard extends BaseBuild {
- def proguard( keep: (Seq[KeepOptionModifier], String) ) = {
- ProguardLib(context.cbtLastModified, context.paths.mavenCache).proguard(
- outjars = Seq( scalaTarget / "proguarded.jar" ),
- injars = classpath,
- libraryjars = Proguard.`rt.jar`,
- keep = keep
- )
- }
-}
-
-object Proguard{
- val version = "5.3.2"
- val `rt.jar` = ClassPath( Seq( new File(System.getProperty("java.home"),"lib/rt.jar") ) )
-}
-
-case class ProguardLib(
- cbtLastModified: Long, mavenCache: File,
- dependency: Option[DependencyImplementation] = None
-)(
- implicit logger: Logger, transientCache: java.util.Map[AnyRef,AnyRef], classLoaderCache: ClassLoaderCache
-){
- /**
- Typed interface on top of the proguard command line tool.
- Check the official ProGuard docs for usage.
- Use `Some(None)` to call an option without arguments.
- Use `true` to set a flag.
-
- @see https://www.guardsquare.com/en/proguard/manual/refcard
- @see https://www.guardsquare.com/en/proguard/manual/usage
-
-${generated-docs}
- */
- case class proguard(
-/* ${generated-args} */
- ) extends ( () => ClassPath ){
-
- // type class rendering scala values into string arguments
- private class valueToStrings[T]( val apply: T => Option[Seq[String]] )
- private object valueToStrings{
- def apply[T:valueToStrings](value: T) = implicitly[valueToStrings[T]].apply(value)
- implicit object SeqFile extends valueToStrings[Seq[File]](v => Some(v.map(_.string)))
- implicit object ClassPath extends valueToStrings[ClassPath](v => Some(Seq(v.string)))
- implicit object File extends valueToStrings[File](v => Some(Seq(v.string)))
- implicit object String extends valueToStrings[String](v => Some(Seq(v)))
- implicit object Int extends valueToStrings[Int](i => Some(Seq(i.toString)))
- implicit object Boolean extends valueToStrings[Boolean]({
- case false => None
- case true => Some(Nil)
- })
- implicit def Option2[T:valueToStrings]: valueToStrings[Option[T]] = new valueToStrings(
- _.map(implicitly[valueToStrings[T]].apply(_).toSeq.flatten)
- )
- implicit def Option3[T:valueToStrings]: valueToStrings[Option[Option[String]]] = new valueToStrings(_.map(_.toSeq))
- implicit def SpecWithModifiers: valueToStrings[(Seq[KeepOptionModifier], String)] = new valueToStrings({
- case (modifiers, spec) => Some( Seq( modifiers.map(_.string).map(","++_).mkString ).filterNot(_ == "") :+ spec )
- })
- }
-
- // capture string argument values and names
- val capturedArgs = capture_args.captureArgs
-
- def apply: ClassPath = {
- val args = capturedArgs.args.map(arg => arg.copy(name="-"++arg.name)).flatMap(_.toSeqOption).flatten
- outjars.map(_.toPath).map(deleteIfExists)
- val c = dependency getOrElse MavenResolver( cbtLastModified, mavenCache, mavenCentral ).bindOne(
- MavenDependency("net.sf.proguard","proguard-base",Proguard.version)
- ) runMain (
- "proguard.ProGuard",
- args : _*
- )
- if(c != ExitCode.Success) throw new Exception
- ClassPath(outjars)
- }
- }
-}
diff --git a/stage1/cbt.scala b/stage1/cbt.scala
index ea3237d..d28789c 100644
--- a/stage1/cbt.scala
+++ b/stage1/cbt.scala
@@ -1,6 +1,7 @@
package cbt
import java.io._
import java.nio.file._
+import java.nio.file.Files._
import java.net._
object `package`{
@@ -59,6 +60,8 @@ object `package`{
if( file.isDirectory ) file.listFiles.flatMap(_.listRecursive).toVector else Seq[File]()
)
}
+
+ def readAsString = new String( readAllBytes( file.toPath ) )
}
implicit class URLExtensionMethods( url: URL ){
def ++( s: String ): URL = new URL( url.toString ++ s )
diff --git a/stage2/BasicBuild.scala b/stage2/BasicBuild.scala
index b6a2870..910cd5e 100644
--- a/stage2/BasicBuild.scala
+++ b/stage2/BasicBuild.scala
@@ -13,8 +13,10 @@ trait BaseBuild extends BuildInterface with DependencyImplementation with Trigge
implicit def transientCache: java.util.Map[AnyRef,AnyRef] = context.transientCache
object libraries{
- def eval = DirectoryDependency( context.cbtHome ++ "/libraries/eval" )
- def captureArgs = DirectoryDependency( context.cbtHome ++ "/libraries/capture_args" )
+ private def dep(name: String) = DirectoryDependency( context.cbtHome / "libraries" / name )
+ def captureArgs = dep( "capture_args" )
+ def eval = dep( "eval" )
+ def proguard = dep( "proguard" )
}
// library available to builds
@@ -86,8 +88,13 @@ trait BaseBuild extends BuildInterface with DependencyImplementation with Trigge
*/
def compileStatusFile: File = compileTarget ++ ".last-success"
+ def generatedSources: Seq[File] = Seq( projectDirectory / "src_generated" )
/** Source directories and files. Defaults to .scala and .java files in src/ and top-level. */
- def sources: Seq[File] = Seq(defaultSourceDirectory) ++ projectDirectory.listFiles.toVector.filter(sourceFileFilter)
+ def sources: Seq[File] = (
+ Seq(defaultSourceDirectory)
+ ++ generatedSources
+ ++ projectDirectory.listFiles.toVector.filter(sourceFileFilter)
+ )
/** Which file endings to consider being source files. */
def sourceFileFilter(file: File) = lib.sourceFileFilter(file)
diff --git a/stage2/plugins/GeneratedSections.scala b/stage2/plugins/GeneratedSections.scala
new file mode 100644
index 0000000..417278c
--- /dev/null
+++ b/stage2/plugins/GeneratedSections.scala
@@ -0,0 +1,40 @@
+package cbt
+import java.nio.file.Files._
+trait GeneratedSections extends BaseBuild{
+ def generatedSectionStartMarker( name: String ) = s"AUTO GENERATED SECTION BEGIN: $name "
+ def generatedSectionEndMarker( name: String ) = s"AUTO GENERATED SECTION END: $name "
+ assert(
+ generatedSectionStartMarker("foo").endsWith(" "),
+ "generatedSectionStartMarker needs to end with a space character"
+ )
+ assert(
+ generatedSectionEndMarker("foo").endsWith(" "),
+ "generatedSectionEndMarker needs to end with a space character"
+ )
+
+ def replacements: Seq[(String, String)]
+
+ def generate = {
+ def replaceSections(subject: String, sections: Seq[(String, String)]): String = {
+ sections.headOption.map{
+ case (name, replacement) =>
+ replaceSections(
+ s"(?s)(\n[^\n]*AUTO GENERATED SECTION BEGIN: $name [^\n]*\n).*(\n[^\n]*AUTO GENERATED SECTION END: $name [^\n]*\n)"
+ .r.replaceAllIn( subject, m => m.group(1) ++ replacement ++ m.group(2) ),
+ sections.tail
+ )
+ }.getOrElse(subject)
+ }
+
+ val updated = sourceFiles.flatMap{ file =>
+ val template = file.readAsString
+ val replaced = replaceSections( template, replacements )
+ if( template != replaced ) {
+ write( file.toPath, replaced.getBytes )
+ Some(file)
+ } else None
+ }
+
+ logger.log("generated-sections","Updated:" + updated.map(_ ++ "\n").mkString)
+ }
+}