From bba2abe7ee38b8903822a07578c46466923d13ed Mon Sep 17 00:00:00 2001 From: Christopher Vogt Date: Mon, 20 Mar 2017 22:09:38 -0400 Subject: start modularizing cbt into libraries this extracts certain parts of cbt into stand-alone libraries, which can be published to maven and used outside of cbt. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This also adds scalariform for these parts of the code. This slows down cbt’s own build a lot because of the number of projects involved! So we’ll follow this by a bunch of performance tweak commits. --- libraries/capture_args/build/build.scala | 12 +- libraries/capture_args/build/build/build.scala | 5 + libraries/capture_args/package.scala | 28 ++-- libraries/common-0/ProxySecurityManager.java | 130 ++++++++++++++++++ libraries/common-0/TrapSecurityManager.java | 88 ++++++++++++ libraries/common-0/TrapSystemExit.java | 35 +++++ libraries/common-0/build/build.scala | 7 + libraries/common-0/build/build/build.scala | 5 + libraries/common-1/ExitCode.java | 24 ++++ libraries/common-1/ExitCode.scala | 11 ++ libraries/common-1/build/build.scala | 8 ++ libraries/common-1/build/build/build.scala | 5 + libraries/common-1/common_1.scala | 34 +++++ libraries/file/build/build.scala | 8 ++ libraries/file/build/build/build.scala | 5 + libraries/file/file.scala | 112 ++++++++++++++++ libraries/interfaces/ExitCode.java | 4 + libraries/proguard/Proguard.scala | 10 +- libraries/proguard/build/build.scala | 8 +- libraries/proguard/build/build/build.scala | 8 +- libraries/reflect/StaticMethod.scala | 4 + libraries/reflect/build/build.scala | 8 ++ libraries/reflect/build/build/build.scala | 5 + libraries/reflect/reflect.scala | 177 +++++++++++++++++++++++++ 24 files changed, 710 insertions(+), 31 deletions(-) create mode 100644 libraries/capture_args/build/build/build.scala create mode 100644 libraries/common-0/ProxySecurityManager.java create mode 100644 libraries/common-0/TrapSecurityManager.java create mode 100644 libraries/common-0/TrapSystemExit.java create mode 100644 libraries/common-0/build/build.scala create mode 100644 libraries/common-0/build/build/build.scala create mode 100644 libraries/common-1/ExitCode.java create mode 100644 libraries/common-1/ExitCode.scala create mode 100644 libraries/common-1/build/build.scala create mode 100644 libraries/common-1/build/build/build.scala create mode 100644 libraries/common-1/common_1.scala create mode 100644 libraries/file/build/build.scala create mode 100644 libraries/file/build/build/build.scala create mode 100644 libraries/file/file.scala create mode 100644 libraries/interfaces/ExitCode.java create mode 100644 libraries/reflect/StaticMethod.scala create mode 100644 libraries/reflect/build/build.scala create mode 100644 libraries/reflect/build/build/build.scala create mode 100644 libraries/reflect/reflect.scala (limited to 'libraries') diff --git a/libraries/capture_args/build/build.scala b/libraries/capture_args/build/build.scala index 24c1faa..fab36bf 100644 --- a/libraries/capture_args/build/build.scala +++ b/libraries/capture_args/build/build.scala @@ -1,11 +1,21 @@ package cbt_build.cbt.capture_args import cbt._ -class Build(val context: Context) extends BaseBuild{ +import cbt_internal._ +class Build(val context: Context) extends Library{ + def description = ( + "macro that allows you to extract a functions arguments" + ++" as strings in order to programmatically pass them to a stringly typed" + ++" api such as a process call, http or a .main method" + ) + + def inceptionYear = 2017 + override def dependencies = ( super.dependencies ++ // don't forget super.dependencies here for scala-library, etc. Resolver( mavenCentral ).bind( MavenDependency( "org.scala-lang", "scala-reflect", scalaVersion ) ) ) + override def scalacOptions = super.scalacOptions :+ "-language:experimental.macros" } diff --git a/libraries/capture_args/build/build/build.scala b/libraries/capture_args/build/build/build.scala new file mode 100644 index 0000000..6fabf47 --- /dev/null +++ b/libraries/capture_args/build/build/build.scala @@ -0,0 +1,5 @@ +package cbt_build.cbt.capture_args.build +import cbt._ +class Build(val context: Context) extends BuildBuild with CbtInternal{ + override def dependencies = super.dependencies :+ cbtInternal.library +} diff --git a/libraries/capture_args/package.scala b/libraries/capture_args/package.scala index f5cd219..5c96a8d 100644 --- a/libraries/capture_args/package.scala +++ b/libraries/capture_args/package.scala @@ -2,29 +2,29 @@ package cbt.capture_args import scala.reflect._ import scala.reflect.macros.blackbox.Context -case class Argument( annotations: Seq[annotation.Annotation], name: String, values: Option[Seq[String]] ){ +case class Argument( annotations: Seq[annotation.Annotation], name: String, values: Option[Seq[String]] ) { def toSeqOption = values.map( name +: _ ) } case class Signature( name: String, args: Seq[Argument] ) -object `package`{ - def captureArgsImplementation(c: Context): c.Tree = { +object `package` { + def captureArgsImplementation( c: Context ): c.Tree = { import c.universe._ - def literal( a: Any ) = Literal(Constant(a)) - def ident( name: String ) = Ident(TermName(name)) + def literal( a: Any ) = Literal( Constant( a ) ) + def ident( name: String ) = Ident( TermName( name ) ) - def findOwnerRecursive(symbol: Symbol, predicate: Symbol => Boolean): Option[Symbol] = { - Option(symbol).flatMap{ + def findOwnerRecursive( symbol: Symbol, predicate: Symbol => Boolean ): Option[Symbol] = { + Option( symbol ).flatMap { s => - if(s == NoSymbol) None else if(predicate(s)) Some(s) else findOwnerRecursive(s.owner, predicate) + if ( s == NoSymbol ) None else if ( predicate( s ) ) Some( s ) else findOwnerRecursive( s.owner, predicate ) } } val method: MethodSymbol = ( - findOwnerRecursive(c.internal.enclosingOwner, _.isMethod).map(_.asMethod) + findOwnerRecursive( c.internal.enclosingOwner, _.isMethod ).map( _.asMethod ) orElse - findOwnerRecursive(c.internal.enclosingOwner, _.isClass).map(_.asClass.primaryConstructor.asMethod) + findOwnerRecursive( c.internal.enclosingOwner, _.isClass ).map( _.asClass.primaryConstructor.asMethod ) getOrElse { c.error( c.enclosingPosition, @@ -33,14 +33,14 @@ object `package`{ ??? } ) - val name = literal(method.name.decodedName.toString) + val name = literal( method.name.decodedName.toString ) // Note: method.paramLists requires explicitly annotated result type - val params = method.paramLists.flatten.map(_.asTerm) + val params = method.paramLists.flatten.map( _.asTerm ) - val args = params.map{ s => + val args = params.map { s => val name = literal( s.name.decodedName.toString ) val i = ident( s.name.toString ) - q"_root_.cbt.capture_args.Argument( _root_.scala.Seq( ..${s.annotations.map(_.tree)} ), $name, valueToStrings($i) )" + q"_root_.cbt.capture_args.Argument( _root_.scala.Seq( ..${s.annotations.map( _.tree )} ), $name, valueToStrings($i) )" } val tree = q""" _root_.cbt.capture_args.Signature( name = $name, args = Seq( ..$args ) ) diff --git a/libraries/common-0/ProxySecurityManager.java b/libraries/common-0/ProxySecurityManager.java new file mode 100644 index 0000000..4669add --- /dev/null +++ b/libraries/common-0/ProxySecurityManager.java @@ -0,0 +1,130 @@ +package cbt.reflect; + +import java.security.*; +import java.io.FileDescriptor; +import java.net.InetAddress; + +/* +SecurityManager proxy that forwards all calls to the provided target if != null. +Useful to replace a previously installed SecurityManager, overriding some methods +but forwarding the rest. +*/ +class ProxySecurityManager extends SecurityManager { + private SecurityManager target; + + protected ProxySecurityManager(SecurityManager target) { + this.target = target; + } + + public Object getSecurityContext() { + if (target != null) return target.getSecurityContext(); + else return super.getSecurityContext(); + } + + public void checkPermission(Permission perm) { + if (target != null) target.checkPermission(perm); + } + + public void checkPermission(Permission perm, Object context) { + if (target != null) target.checkPermission(perm, context); + } + + public void checkCreateClassLoader() { + if (target != null) target.checkCreateClassLoader(); + } + + public void checkAccess(Thread t) { + if (target != null) target.checkAccess(t); + } + + public void checkAccess(ThreadGroup g) { + if (target != null) target.checkAccess(g); + } + + public void checkExit(int status) { + if (target != null) target.checkExit(status); + } + + public void checkExec(String cmd) { + if (target != null) target.checkExec(cmd); + } + + public void checkLink(String lib) { + if (target != null) target.checkLink(lib); + } + + /* + public void checkRead(FileDescriptor fd) { + if (target != null) target.checkRead(fd); + } + + public void checkRead(String file) { + if (target != null) target.checkRead(file); + } + + public void checkRead(String file, Object context) { + if (target != null) target.checkRead(file, context); + } + */ + + public void checkWrite(FileDescriptor fd) { + if (target != null) target.checkWrite(fd); + } + + public void checkWrite(String file) { + if (target != null) target.checkWrite(file); + } + + public void checkDelete(String file) { + if (target != null) target.checkDelete(file); + } + + public void checkConnect(String host, int port) { + if (target != null) target.checkConnect(host, port); + } + + public void checkConnect(String host, int port, Object context) { + if (target != null) target.checkConnect(host, port, context); + } + + public void checkListen(int port) { + if (target != null) target.checkListen(port); + } + + public void checkAccept(String host, int port) { + if (target != null) target.checkAccept(host, port); + } + + public void checkMulticast(InetAddress maddr) { + if (target != null) target.checkMulticast(maddr); + } + + public void checkPropertiesAccess() { + if (target != null) target.checkPropertiesAccess(); + } + + public void checkPropertyAccess(String key) { + if (target != null) target.checkPropertyAccess(key); + } + + public void checkPrintJobAccess() { + if (target != null) target.checkPrintJobAccess(); + } + + public void checkPackageAccess(String pkg) { + if (target != null) target.checkPackageAccess(pkg); + } + + public void checkPackageDefinition(String pkg) { + if (target != null) target.checkPackageDefinition(pkg); + } + + public void checkSetFactory() { + if (target != null) target.checkSetFactory(); + } + + public ThreadGroup getThreadGroup() { + if (target != null) return target.getThreadGroup(); + else return super.getThreadGroup(); + } +} diff --git a/libraries/common-0/TrapSecurityManager.java b/libraries/common-0/TrapSecurityManager.java new file mode 100644 index 0000000..161b74f --- /dev/null +++ b/libraries/common-0/TrapSecurityManager.java @@ -0,0 +1,88 @@ +package cbt.reflect; + +import java.security.*; +/* +When enabled, this SecurityManager turns System.exit(...) calls into exceptions that can be caught and handled. +Installing a SecurityManager is a global side-effect and thus needs extra care in a persistent +background process like CBT's. The current approach is install it once during JVM-startup. +When disabled this delegates to the SecurityManager installed before if any, which +would be Nailgun's if running on Nailgun. If we do not delegate to Nailgun, it seems we +could in some cases kill the server process +*/ +public class TrapSecurityManager extends ProxySecurityManager { + public static ThreadLocal trapExitCode() { + // storing the flag in the installed security manager + // instead of e.g. a static member is necessary because + // we run multiple versions of CBT with multiple TrapSecurityManager classes + // but we need to affect the installed one + SecurityManager sm = System.getSecurityManager(); + if (sm instanceof TrapSecurityManager) { + return ((TrapSecurityManager) sm)._trapExitCode; + } else { + try { + @SuppressWarnings("unchecked") + ThreadLocal res = + (ThreadLocal) sm.getClass().getMethod("trapExitCode").invoke(null); + return res; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + private final ThreadLocal _trapExitCode = + new ThreadLocal() { + @Override + protected Boolean initialValue() { + return false; + } + }; + + protected TrapSecurityManager(SecurityManager parent) { + super(parent); + } + + public void checkPermission(Permission permission) { + /* + NOTE: is it actually ok, to just make these empty? + Calling .super leads to ClassNotFound exteption for a lambda. + Calling to the previous SecurityManager leads to a stack overflow + */ + if (!TrapSecurityManager.trapExitCode().get()) { + super.checkPermission(permission); + } + } + + public void checkPermission(Permission permission, Object context) { + /* Does this methods need to be overidden? */ + if (!TrapSecurityManager.trapExitCode().get()) { + super.checkPermission(permission, context); + } + } + + // FIXME: we should probably choose a more unique name for this + private static final String prefix = "[TrappedExit] "; + + @Override + public void checkExit(int status) { + if (TrapSecurityManager.trapExitCode().get()) { + // using a RuntimeException and a prefix here instead of a custom + // exception type because this is thrown by the installed TrapSecurityManager + // but other versions of cbt need to be able to catch it, that do not have access + // to that version of the TrapSecurityManager class + throw new RuntimeException(prefix + status); + } + super.checkExit(status); + } + + public static boolean isTrappedExit(Throwable t) { + return t instanceof RuntimeException + && t.getMessage() != null + && t.getMessage().startsWith(prefix); + } + + public static int exitCode(Throwable t) { + assert (isTrappedExit(t)); + return Integer.parseInt(t.getMessage().substring(prefix.length())); + } +} diff --git a/libraries/common-0/TrapSystemExit.java b/libraries/common-0/TrapSystemExit.java new file mode 100644 index 0000000..86bc880 --- /dev/null +++ b/libraries/common-0/TrapSystemExit.java @@ -0,0 +1,35 @@ +package cbt.reflect; + +import java.security.*; +import java.lang.reflect.InvocationTargetException; + +public abstract class TrapSystemExit { + public static SecurityManager createSecurityManager(SecurityManager delegateTo) { + return new TrapSecurityManager(delegateTo); + } + + public static T run(TrapSystemExit runnable) throws Throwable { + boolean trapExitCodeBefore = TrapSecurityManager.trapExitCode().get(); + try { + TrapSecurityManager.trapExitCode().set(true); + return runnable.run(); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + if (TrapSecurityManager.isTrappedExit(cause)) { + return runnable.wrap(TrapSecurityManager.exitCode(cause)); + } + throw exception; + } catch (Exception exception) { + if (TrapSecurityManager.isTrappedExit(exception)) { + return runnable.wrap(TrapSecurityManager.exitCode(exception)); + } + throw exception; + } finally { + TrapSecurityManager.trapExitCode().set(trapExitCodeBefore); + } + } + + public abstract T run() throws Throwable; + + public abstract T wrap(int exitCode); +} diff --git a/libraries/common-0/build/build.scala b/libraries/common-0/build/build.scala new file mode 100644 index 0000000..0a4e5cb --- /dev/null +++ b/libraries/common-0/build/build.scala @@ -0,0 +1,7 @@ +package cbt_build.common_0 +import cbt._ +import cbt_internal._ +class Build(val context: Context) extends Library{ + override def inceptionYear = 2017 + override def description = "classes shared by multiple cbt libraries and needed in stage 0" +} diff --git a/libraries/common-0/build/build/build.scala b/libraries/common-0/build/build/build.scala new file mode 100644 index 0000000..d3f98ce --- /dev/null +++ b/libraries/common-0/build/build/build.scala @@ -0,0 +1,5 @@ +package cbt_build.reflect.build +import cbt._ +class Build(val context: Context) extends BuildBuild with CbtInternal{ + override def dependencies = super.dependencies :+ cbtInternal.library +} diff --git a/libraries/common-1/ExitCode.java b/libraries/common-1/ExitCode.java new file mode 100644 index 0000000..1c16f67 --- /dev/null +++ b/libraries/common-1/ExitCode.java @@ -0,0 +1,24 @@ +package cbt; +/* +public class ExitCode{ + public int integer; + public ExitCode(int integer){ + this.integer = integer; + } + public static ExitCode apply(int integer){ + return new ExitCode( integer ); + } + public static ExitCode Success = new ExitCode(0); + public static ExitCode Failure = new ExitCode(1); + + @Override + public boolean equals(Object other){ + return (other instanceof ExitCode) && ((ExitCode) other).integer == integer; + } + @Override + public int hashCode(){ + return integer; + } +} + +*/ diff --git a/libraries/common-1/ExitCode.scala b/libraries/common-1/ExitCode.scala new file mode 100644 index 0000000..41d9f3f --- /dev/null +++ b/libraries/common-1/ExitCode.scala @@ -0,0 +1,11 @@ +package cbt +// CLI interop +case class ExitCode( integer: Int ) extends interfaces.ExitCode { + def ||( other: => ExitCode ) = if ( this == ExitCode.Success ) this else other + def &&( other: => ExitCode ) = if ( this != ExitCode.Success ) this else other +} +object ExitCode { + val Success = ExitCode( 0 ) + val Failure = ExitCode( 1 ) +} + diff --git a/libraries/common-1/build/build.scala b/libraries/common-1/build/build.scala new file mode 100644 index 0000000..247fd01 --- /dev/null +++ b/libraries/common-1/build/build.scala @@ -0,0 +1,8 @@ +package cbt_build.common_1 +import cbt._ +import cbt_internal._ +class Build(val context: Context) extends Library{ + override def inceptionYear = 2017 + override def description = "classes shared by multiple cbt libraries and needed in stage 1" + override def dependencies = super.dependencies :+ libraries.common_0 :+ libraries.interfaces +} diff --git a/libraries/common-1/build/build/build.scala b/libraries/common-1/build/build/build.scala new file mode 100644 index 0000000..d3f98ce --- /dev/null +++ b/libraries/common-1/build/build/build.scala @@ -0,0 +1,5 @@ +package cbt_build.reflect.build +import cbt._ +class Build(val context: Context) extends BuildBuild with CbtInternal{ + override def dependencies = super.dependencies :+ cbtInternal.library +} diff --git a/libraries/common-1/common_1.scala b/libraries/common-1/common_1.scala new file mode 100644 index 0000000..66da224 --- /dev/null +++ b/libraries/common-1/common_1.scala @@ -0,0 +1,34 @@ +package cbt.common_1 +import cbt.ExitCode +object `package` extends Module { + implicit class CbtExitCodeOps( val exitCode: ExitCode ) extends AnyVal with ops.CbtExitCodeOps + implicit class TypeInferenceSafeEquals[T]( val value: T ) extends AnyVal with ops.TypeInferenceSafeEquals[T] + implicit class CbtBooleanOps( val condition: Boolean ) extends AnyVal with ops.CbtBooleanOps + implicit class CbtStringOps( val string: String ) extends AnyVal with ops.CbtStringOps +} + +package ops { + trait CbtExitCodeOps extends Any { + def exitCode: ExitCode + def ||( other: => ExitCode ) = if ( exitCode == ExitCode.Success ) exitCode else other + def &&( other: => ExitCode ) = if ( exitCode != ExitCode.Success ) exitCode else other + } + trait TypeInferenceSafeEquals[T] extends Any { + def value: T + /** if you don't manually upcast, this will catch comparing different types */ + def ===( other: T ) = value == other + def =!=( other: T ) = value != other // =!= instead of !==, because it has better precedence + } + trait CbtBooleanOps extends Any { + def condition: Boolean + def option[T]( value: => T ): Option[T] = if ( condition ) Some( value ) else None + } + trait CbtStringOps extends Any { + def string: String + def escape = string.replace( "\\", "\\\\" ).replace( "\"", "\\\"" ) + def quote = s""""$escape"""" + def ~( right: String ): String = string + right + } +} + +trait Module diff --git a/libraries/file/build/build.scala b/libraries/file/build/build.scala new file mode 100644 index 0000000..d9017a1 --- /dev/null +++ b/libraries/file/build/build.scala @@ -0,0 +1,8 @@ +package cbt_build.reflect +import cbt._ +import cbt_internal._ +class Build(val context: Context) extends Library{ + override def inceptionYear = 2017 + override def description = "helpers to work with java io and nio" + override def dependencies = super.dependencies :+ libraries.common_1 +} diff --git a/libraries/file/build/build/build.scala b/libraries/file/build/build/build.scala new file mode 100644 index 0000000..d3f98ce --- /dev/null +++ b/libraries/file/build/build/build.scala @@ -0,0 +1,5 @@ +package cbt_build.reflect.build +import cbt._ +class Build(val context: Context) extends BuildBuild with CbtInternal{ + override def dependencies = super.dependencies :+ cbtInternal.library +} diff --git a/libraries/file/file.scala b/libraries/file/file.scala new file mode 100644 index 0000000..f20c9a8 --- /dev/null +++ b/libraries/file/file.scala @@ -0,0 +1,112 @@ +package cbt.file +import java.io.File +import java.nio.file.Files._ +import java.nio.file.StandardCopyOption._ +import cbt.common_1._ +object `package` extends Module { + implicit class CbtFileOps( val file: File ) extends ops.CbtFileOps +} + +package ops { + trait CbtFileOps extends Any { + def file: File + def ++( s: String ): File = { + if ( s endsWith "/" ) throw new Exception( + """Trying to append a String that ends in "/" to a File would loose the trailing "/". Use .stripSuffix("/") if you need to.""" + ) + new File( string + s ) // PERFORMANCE HOTSPOT + } + def /( s: String ): File = { + new File( file, s ) + } + def parent = realpath( file / ".." ) + def string = file.toString + + def listOrFail: Seq[File] = Option( file.listFiles ).getOrElse( throw new Exception( "no such file: " + file ) ).toVector + def listRecursive: Seq[File] = + file +: ( + if ( file.isDirectory ) file.listOrFail.flatMap( _.listRecursive ).toVector else Vector[File]() + ) + + def lastModifiedRecursive = listRecursive.map( _.lastModified ).max + + def readAsString = new String( readAllBytes( file.toPath ) ) + + def quote = s"""new _root_.java.io.File(${string.quote})""" + } +} + +trait Module { + def realpath( name: File ) = new File( java.nio.file.Paths.get( name.getAbsolutePath ).normalize.toString ) + def transformFiles( files: Seq[File], transform: String => String ): Seq[File] = { + transformFilesOrError( files, s => Right( transform( s ) ) )._1 + } + + def transformFilesOrError[T]( files: Seq[File], transform: String => Either[T, String] ): ( Seq[File], Seq[( File, T )] ) = { + val results = files.map { file => + val string = file.readAsString + transform( string ).left.map( + file -> _ + ).right.map( + replaced => + if ( string != replaced ) { + val tmpFile = file ++ ".cbt-tmp" + assert( !tmpFile.exists ) + write( tmpFile.toPath, replaced.getBytes ) + move( tmpFile.toPath, file.toPath, REPLACE_EXISTING ) + Some( file ) + } else None + ) + } + + ( results.map( _.right.toOption ).flatten.flatten, results.map( _.left.toOption ).flatten ) + } + + def autoRelative( + files: Seq[File], collector: PartialFunction[( File, String ), String] = { case ( _, r ) => r }, + allowDuplicates: Boolean = false + ): Seq[( File, String )] = { + val map = files.sorted.flatMap { base => + val b = base.getCanonicalFile.string + if ( base.isDirectory ) { + base.listRecursive.map { f => + f -> f.getCanonicalFile.string.stripPrefix( b ).stripPrefix( File.separator ) + } + } else { + Seq( base -> base.getName ) + } + }.collect { + case v @ ( file, _ ) if collector.isDefinedAt( v ) => file -> collector( v ) + } + if ( !allowDuplicates ) { + val relatives = map.unzip._2 + val duplicateFiles = ( relatives diff relatives.distinct ).distinct + assert( + duplicateFiles.isEmpty, { + val rs = relatives.toSet + "Conflicting:\n\n" + + map.filter( rs contains _._2 ).groupBy( _._2 ).mapValues( _.map( _._1 ).sorted ).toSeq.sortBy( _._1 ).map { + case ( name, files ) => s"$name:\n" ++ files.mkString( "\n" ) + }.mkString( "\n\n" ) + } + ) + } + map + } + + /* recursively deletes folders*/ + def deleteRecursive( file: File ): Unit = { + val s = file.string + // some desperate attempts to keep people from accidentally deleting their hard drive + assert( file === file.getCanonicalFile, "deleteRecursive requires previous .getCanonicalFile" ) + assert( file.isAbsolute, "deleteRecursive requires absolute path" ) + assert( file.string != "", "deleteRecursive requires non-empty file path" ) + assert( s.split( File.separator.replace( "\\", "\\\\" ) ).size > 4, "deleteRecursive requires absolute path of at least depth 4" ) + assert( !file.listRecursive.exists( _.isHidden ), "deleteRecursive requires no files to be hidden" ) + assert( file.listRecursive.forall( _.canWrite ), "deleteRecursive requires all files to be writable" ) + if ( file.isDirectory ) { + file.listOrFail.map( deleteRecursive ) + } + file.delete + } +} diff --git a/libraries/interfaces/ExitCode.java b/libraries/interfaces/ExitCode.java new file mode 100644 index 0000000..f29d8c0 --- /dev/null +++ b/libraries/interfaces/ExitCode.java @@ -0,0 +1,4 @@ +package cbt.interfaces; +public interface ExitCode{ + public int integer(); +} diff --git a/libraries/proguard/Proguard.scala b/libraries/proguard/Proguard.scala index 7d57a77..ec5c2e9 100644 --- a/libraries/proguard/Proguard.scala +++ b/libraries/proguard/Proguard.scala @@ -29,8 +29,8 @@ object ProGuard { } case class ProGuard[T]( main: Seq[String] => Int, - T: Seq[File] => T, - log: String => Unit = _ => () + T: Seq[File] => T, + log: String => Unit = _ => () ) { /** @@ -160,9 +160,9 @@ case class ProGuard[T]( 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 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 diff --git a/libraries/proguard/build/build.scala b/libraries/proguard/build/build.scala index 3ca38b5..754de20 100644 --- a/libraries/proguard/build/build.scala +++ b/libraries/proguard/build/build.scala @@ -1,11 +1,12 @@ package cbt_build.proguard import cbt._ +import cbt_internal._ import java.nio.file.Files._ import java.net._ import java.io._ import scala.xml._ -class Build(val context: Context) extends Scalafmt{ +class Build(val context: Context) extends Library{ def description: String = "Type-safe scala wrapper to interfaces with ProGuard.main runner" def inceptionYear = 2017 @@ -14,11 +15,6 @@ class Build(val context: Context) extends Scalafmt{ compile } - override def scalafmt = super.scalafmt.copy( - config = Scalafmt.cbtRecommendedConfig, - whiteSpaceInParenthesis = true - ) - override def compile = { // currently suffers from non-deterministic formatting. Try a few times to reproduce commit state. val formatted = scalafmt.apply.map(_.string).mkString("\n") diff --git a/libraries/proguard/build/build/build.scala b/libraries/proguard/build/build/build.scala index 7928cfa..5057404 100644 --- a/libraries/proguard/build/build/build.scala +++ b/libraries/proguard/build/build/build.scala @@ -1,13 +1,11 @@ -package cbt_build.proguard.build +package proguard_build.build import cbt._ -class Build(val context: Context) extends BuildBuild{ +class Build(val context: Context) extends BuildBuild with CbtInternal{ override def dependencies = ( super.dependencies ++ // don't forget super.dependencies here for scala-library, etc. Resolver( mavenCentral, sonatypeReleases ).bind( ScalaDependency("org.scala-lang.modules","scala-xml","1.0.5"), "org.ccil.cowan.tagsoup" % "tagsoup" % "1.2.1" - ) ++ Seq( - plugins.scalafmt - ) + ) ++ Seq( cbtInternal.library ) ) } diff --git a/libraries/reflect/StaticMethod.scala b/libraries/reflect/StaticMethod.scala new file mode 100644 index 0000000..e2a0d07 --- /dev/null +++ b/libraries/reflect/StaticMethod.scala @@ -0,0 +1,4 @@ +package cbt.reflect +case class StaticMethod[Arg, Result]( function: Arg => Result, name: String ) extends ( Arg => Result ) { + def apply( arg: Arg ): Result = function( arg ) +} diff --git a/libraries/reflect/build/build.scala b/libraries/reflect/build/build.scala new file mode 100644 index 0000000..5c27090 --- /dev/null +++ b/libraries/reflect/build/build.scala @@ -0,0 +1,8 @@ +package cbt_build.reflect +import cbt._ +import cbt_internal._ +class Build(val context: Context) extends Library{ + override def inceptionYear = 2017 + override def description = "discover classes on your classpath and invoke methods reflectively, preventing System.exit" + override def dependencies = super.dependencies :+ libraries.file +} diff --git a/libraries/reflect/build/build/build.scala b/libraries/reflect/build/build/build.scala new file mode 100644 index 0000000..d3f98ce --- /dev/null +++ b/libraries/reflect/build/build/build.scala @@ -0,0 +1,5 @@ +package cbt_build.reflect.build +import cbt._ +class Build(val context: Context) extends BuildBuild with CbtInternal{ + override def dependencies = super.dependencies :+ cbtInternal.library +} diff --git a/libraries/reflect/reflect.scala b/libraries/reflect/reflect.scala new file mode 100644 index 0000000..c18d926 --- /dev/null +++ b/libraries/reflect/reflect.scala @@ -0,0 +1,177 @@ +package cbt.reflect + +import java.io.File +import java.lang.reflect.{ Constructor, Method, InvocationTargetException, Modifier } + +import scala.reflect.ClassTag + +import cbt.ExitCode +import cbt.file._ + +object `package` extends Module { + implicit class CbtClassOps( val c: Class[_] ) extends AnyVal with ops.CbtClassOps + implicit class CbtConstructorOps( val c: Constructor[_] ) extends AnyVal with ops.CbtConstructorOps + implicit class CbtMethodOps( val m: Method ) extends AnyVal with ops.CbtMethodOps +} + +package ops { + trait CbtClassOps extends Any { + def c: Class[_] + def name = c.getName + def method( name: String, parameterTypes: Class[_]* ) = c.getMethod( name, parameterTypes: _* ) + def methods = c.getMethods + def modifiers = c.getModifiers + def constructors = c.getConstructors + def declaredMethods = c.getDeclaredMethods + def isInterface = Modifier.isInterface( c.getModifiers ) + def isAbstract = Modifier.isAbstract( c.getModifiers ) + def isPrivate = Modifier.isPrivate( c.getModifiers ) + def isProtected = Modifier.isProtected( c.getModifiers ) + def isPublic = Modifier.isPublic( c.getModifiers ) + def isFinal = Modifier.isFinal( c.getModifiers ) + } + trait CbtConstructorOps extends Any { + def c: Constructor[_] + def parameterTypes = c.getParameterTypes + } + trait CbtMethodOps extends Any { + def m: Method + def name = m.getName + def declaringClass = m.getDeclaringClass + def modifiers = m.getModifiers + def parameters = m.getParameters + def parameterTypes = m.getParameterTypes + def returnType = m.getReturnType + + def isAbstract = Modifier.isAbstract( m.getModifiers ) + def isFinal = Modifier.isFinal( m.getModifiers ) + def isNative = Modifier.isNative( m.getModifiers ) + def isPrivate = Modifier.isPrivate( m.getModifiers ) + def isProtected = Modifier.isProtected( m.getModifiers ) + def isPublic = Modifier.isPublic( m.getModifiers ) + def isStatic = Modifier.isStatic( m.getModifiers ) + def isStrict = Modifier.isStrict( m.getModifiers ) + def isSynchronized = Modifier.isSynchronized( m.getModifiers ) + def isTransient = Modifier.isTransient( m.getModifiers ) + def isVolatile = Modifier.isVolatile( m.getModifiers ) + } +} +trait Module { + def runMain( cls: Class[_], args: Seq[String] ): ExitCode = + discoverStaticExitMethodForced[Array[String]]( cls, "main" ).apply( args.to ) + + def discoverMain( cls: Class[_] ): Option[StaticMethod[Seq[String], ExitCode]] = { + discoverStaticExitMethod[Array[String]]( cls, "main" ) + .map( f => + f.copy( + function = ( arg: Seq[String] ) => f.function( arg.to ) + ) ) + } + + /** ignoreMissingClasses allows ignoring other classes root directories which are subdirectories of this one */ + def iterateClasses( + classesRootDirectory: File, + classLoader: ClassLoader, + ignoreMissingClasses: Boolean + ): Seq[Class[_]] = + iterateClassNames( classesRootDirectory ) + .map { name => + try { + classLoader.loadClass( name ) + } catch { + case e: ClassNotFoundException if ignoreMissingClasses => null + case e: NoClassDefFoundError if ignoreMissingClasses => null + } + } + .filterNot( ignoreMissingClasses && _ == null ) + + /** Given a directory corresponding to the root package, iterate + * the names of all classes derived from the class files found + */ + def iterateClassNames( classesRootDirectory: File ): Seq[String] = + classesRootDirectory.listRecursive + .filter( _.isFile ) + .map( _.getPath ) + .collect { + // no $ to avoid inner classes + case path if !path.contains( "$" ) && path.endsWith( ".class" ) => + path + .stripSuffix( ".class" ) + .stripPrefix( classesRootDirectory.getPath ) + .stripPrefix( File.separator ) // 1 for the slash + .replace( File.separator, "." ) + } + + def discoverStaticExitMethodForced[Arg: ClassTag]( + cls: Class[_], name: String + ): StaticMethod[Arg, ExitCode] = { + val f = discoverStaticMethodForced[Arg, Unit]( cls, name ) + f.copy( + function = arg => trapExitCode { f.function( arg ); ExitCode.Success } + ) + } + + def discoverStaticMethodForced[Arg, Result]( + cls: Class[_], name: String + )( + implicit + Result: ClassTag[Result], Arg: ClassTag[Arg] + ): StaticMethod[Arg, Result] = { + val m = cls.method( name, Arg.runtimeClass ) + assert( Result.runtimeClass.isAssignableFrom( m.returnType ) ) + typeStaticMethod( m ) + } + + def discoverStaticExitMethod[Arg: ClassTag]( + cls: Class[_], name: String + ): Option[StaticMethod[Arg, ExitCode]] = + discoverStaticMethod[Arg, Unit]( cls, name ).map( f => + f.copy( + function = arg => trapExitCode { f.function( arg ); ExitCode.Success } + ) ) + + def discoverStaticMethod[Arg, Result]( + cls: Class[_], name: String + )( + implicit + Result: ClassTag[Result], Arg: ClassTag[Arg] + ): Option[StaticMethod[Arg, Result]] = { + Some( cls ) + .filterNot( _.isAbstract ) + .filterNot( _.isInterface ) + .flatMap( _ + .getMethods + .find( m => + !m.isAbstract + && m.isPublic + && m.name == name + && m.parameterTypes.toList == List( Arg.runtimeClass ) + && Result.runtimeClass.isAssignableFrom( m.returnType ) ) ) + .map( typeStaticMethod ) + } + + def typeStaticMethod[Arg, Result]( method: Method ): StaticMethod[Arg, Result] = { + val m = method + val instance = + if ( m.isStatic ) null + else m.declaringClass.newInstance // Dottydoc needs this. It's main method is not static. + StaticMethod( + arg => m.invoke( instance, arg.asInstanceOf[AnyRef] ).asInstanceOf[Result], + m.getClass.name.stripSuffix( "$" ) ++ "." ++ m.name ++ "( " + ++ m.parameters.map( _.getType.name ).mkString( ", " ) + ++ " )" + ) + } + + def trapExitCodeOrValue[T]( result: => T, i: Int = 5 ): Either[ExitCode, T] = { + TrapSystemExit.run( + new TrapSystemExit[Either[ExitCode, T]] { + def run = Right( result ) + def wrap( exitCode: Int ) = Left( new ExitCode( exitCode ) ) + } + ) + } + + def trapExitCode( code: => ExitCode ): ExitCode = + trapExitCodeOrValue( code ).merge +} -- cgit v1.2.3