diff options
Diffstat (limited to 'stage2')
-rw-r--r-- | stage2/AdminStage2.scala | 11 | ||||
-rw-r--r-- | stage2/AdminTasks.scala | 12 | ||||
-rw-r--r-- | stage2/BuildBuild.scala | 17 | ||||
-rw-r--r-- | stage2/DefaultBuild.scala | 233 | ||||
-rw-r--r-- | stage2/Lib.scala | 436 | ||||
-rw-r--r-- | stage2/Scaffold.scala | 148 | ||||
-rw-r--r-- | stage2/Stage2.scala | 47 | ||||
-rw-r--r-- | stage2/dependencies.scala | 36 | ||||
-rw-r--r-- | stage2/mixins.scala | 35 |
9 files changed, 975 insertions, 0 deletions
diff --git a/stage2/AdminStage2.scala b/stage2/AdminStage2.scala new file mode 100644 index 0000000..e7e2284 --- /dev/null +++ b/stage2/AdminStage2.scala @@ -0,0 +1,11 @@ +package cbt +object AdminStage2{ + def main(args: Array[String]) = { + val init = new Stage1.Init(args.drop(3)) + val lib = new Lib(init.logger) + val adminTasks = new AdminTasks(lib, args.drop(3)) + new lib.ReflectObject(adminTasks){ + def usage = "Available methods: " + lib.taskNames(subclassType) + }.callNullary(args.lift(2)) + } +} diff --git a/stage2/AdminTasks.scala b/stage2/AdminTasks.scala new file mode 100644 index 0000000..2f7efe1 --- /dev/null +++ b/stage2/AdminTasks.scala @@ -0,0 +1,12 @@ +package cbt +class AdminTasks(lib: Lib, args: Array[String]){ + def resolve = { + ClassPath.flatten( + args(0).split(",").toVector.map{ + d => + val v = d.split(":") + new MavenDependency(v(0),v(1),v(2))(lib.logger).classpath + } + ) + } +} diff --git a/stage2/BuildBuild.scala b/stage2/BuildBuild.scala new file mode 100644 index 0000000..41589db --- /dev/null +++ b/stage2/BuildBuild.scala @@ -0,0 +1,17 @@ +package cbt +import scala.collection.immutable.Seq + +class BuildBuild(context: Context) extends Build(context){ + override def dependencies = Seq( CbtDependency(context.logger) ) ++ super.dependencies + def managedBuildDirectory = lib.realpath(projectDirectory + "/../") + val managedBuild = { + val managedContext = context.copy( cwd = managedBuildDirectory ) + val cl = new cbt.URLClassLoader( + classpath, + classOf[BuildBuild].getClassLoader // FIXME: this looks wrong. Should be ClassLoader.getSystemClassLoader but that crashes + ) + lib.create( lib.buildClassName )( managedContext )( cl ).asInstanceOf[Build] + } + override def triggerLoopFiles = super.triggerLoopFiles ++ managedBuild.triggerLoopFiles + override def finalBuild = managedBuild.finalBuild +} diff --git a/stage2/DefaultBuild.scala b/stage2/DefaultBuild.scala new file mode 100644 index 0000000..c0072ff --- /dev/null +++ b/stage2/DefaultBuild.scala @@ -0,0 +1,233 @@ +package cbt +import cbt.paths._ + +import java.io._ +import java.lang.reflect.InvocationTargetException +import java.net._ +import java.nio.file.{Path =>_,_} +import java.nio.file.Files.readAllBytes +import java.security.MessageDigest +import java.util.jar._ + +import scala.collection.immutable.Seq +import scala.reflect.runtime.{universe => ru} +import scala.util._ + +import ammonite.ops.{cwd => _,_} + + + + +abstract class PackageBuild(context: Context) extends Build(context) with ArtifactInfo{ + def `package`: Seq[File] = lib.concurrently( enableConcurrency )( + Seq(() => jar, () => docJar, () => srcJar) + )( _() ) + + private object cacheJarBasicBuild extends Cache[File] + def jar: File = cacheJarBasicBuild{ + lib.jar( artifactId, version, compile, jarTarget ) + } + + private object cacheSrcJarBasicBuild extends Cache[File] + def srcJar: File = cacheSrcJarBasicBuild{ + lib.srcJar(sources, artifactId, version, scalaTarget) + } + + private object cacheDocBasicBuild extends Cache[File] + def docJar: File = cacheDocBasicBuild{ + lib.docJar( sources, dependencyClasspath, apiTarget, jarTarget, artifactId, version, scalacOptions ) + } + + override def jars = jar +: dependencyJars + override def exportedJars: Seq[File] = Seq(jar) +} +abstract class PublishBuild(context: Context) extends PackageBuild(context){ + def name = artifactId + def description: String + def url: URL + def developers: Seq[Developer] + def licenses: Seq[License] + def scmUrl: String + def scmConnection: String + def pomExtra: Seq[scala.xml.Node] = Seq() + + // ========== package ========== + + /** put additional xml that should go into the POM file in here */ + def pom: File = lib.pom( + groupId = groupId, + artifactId = artifactId, + version = version, + name = name, + description = description, + url = url, + developers = developers, + licenses = licenses, + scmUrl = scmUrl, + scmConnection = scmConnection, + dependencies = dependencies, + pomExtra = pomExtra, + jarTarget = jarTarget + ) + + // ========== publish ========== + final protected def releaseFolder = s"/${groupId.replace(".","/")}/$artifactId/$version/" + def snapshotUrl = new URL("https://oss.sonatype.org/content/repositories/snapshots") + def releaseUrl = new URL("https://oss.sonatype.org/service/local/staging/deploy/maven2") + def publishSnapshot: Unit = lib.publishSnapshot(sourceFiles, pom +: `package`, new URL(snapshotUrl + releaseFolder) ) + def publishSigned: Unit = lib.publishSigned(sourceFiles, pom +: `package`, new URL(releaseUrl + releaseFolder) ) +} + + +class BasicBuild(context: Context) extends Build(context) +class Build(val context: Context) extends Dependency with TriggerLoop{ + // library available to builds + final val logger = context.logger + override final protected val lib: Lib = new Lib(logger) + // ========== general stuff ========== + + def enableConcurrency = false + final def projectDirectory: File = new File(context.cwd) + assert( projectDirectory.exists, "projectDirectory does not exist: "+projectDirectory ) + final def usage: Unit = new lib.ReflectBuild(this).usage +/* + def scaffold: Unit = lib.generateBasicBuildFile( + projectDirectory, scalaVersion, groupId, artifactId, version + ) +*/ + // ========== meta data ========== + + def scalaVersion: String = constants.scalaVersion + final def scalaMajorVersion: String = scalaVersion.split("\\.").take(2).mkString(".") + def zincVersion = "0.3.9" + + def dependencies: Seq[Dependency] = Seq( + "org.scala-lang" % "scala-library" % scalaVersion + ) + + // ========== paths ========== + final private val defaultSourceDirectory = new File(projectDirectory+"/src/") + + /** base directory where stuff should be generated */ + def target = new File(projectDirectory+"/target") + /** base directory where stuff should be generated for this scala version*/ + def scalaTarget = new File(target + s"/scala-$scalaMajorVersion") + /** directory where jars (and the pom file) should be put */ + def jarTarget = scalaTarget + /** directory where the scaladoc should be put */ + def apiTarget = new File(scalaTarget + "/api") + /** directory where the class files should be put (in package directories) */ + def compileTarget = new File(scalaTarget + "/classes") + + /** 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) + + /** Which file endings to consider being source files. */ + def sourceFileFilter(file: File): Boolean = file.toString.endsWith(".scala") || file.toString.endsWith(".java") + + /** Absolute path names for all individual files found in sources directly or contained in directories. */ + final def sourceFiles: Seq[File] = for { + base <- sources.filter(_.exists).map(lib.realpath) + file <- lib.listFilesRecursive(base) if file.isFile && sourceFileFilter(file) + } yield file + + protected def assertSourceDirectories(): Unit = { + val nonExisting = + sources + .filterNot( _.exists ) + .diff( Seq(defaultSourceDirectory) ) + assert( + nonExisting.isEmpty, + "Some sources do not exist: \n"+nonExisting.mkString("\n") + ) + } + assertSourceDirectories() + + + + + /** SBT-like dependency builder DSL */ + class GroupIdAndArtifactId( groupId: String, artifactId: String ){ + def %(version: String) = new MavenDependency(groupId, artifactId, version)(lib.logger) + } + implicit class DependencyBuilder(groupId: String){ + def %%(artifactId: String) = new GroupIdAndArtifactId( groupId, artifactId+"_"+scalaMajorVersion ) + def %(artifactId: String) = new GroupIdAndArtifactId( groupId, artifactId ) + } + + final def BuildDependency(path: String) = cbt.BuildDependency( + context.copy( + cwd = path, + args = Seq() + ) + ) + + def triggerLoopFiles: Seq[File] = sources ++ transitiveDependencies.collect{ case b: TriggerLoop => b.triggerLoopFiles }.flatten + + + def localJars : Seq[File] = + Seq(projectDirectory + "/lib/") + .map(new File(_)) + .filter(_.exists) + .flatMap(_.listFiles) + .filter(_.toString.endsWith(".jar")) + + //def cacheJar = false + override def dependencyClasspath : ClassPath = ClassPath(localJars) ++ super.dependencyClasspath + override def dependencyJars : Seq[File] = localJars ++ super.dependencyJars + + def exportedClasspath : ClassPath = ClassPath(Seq(compile)) + def exportedJars: Seq[File] = Seq() + // ========== compile, run, test ========== + + /** scalac options used for zinc and scaladoc */ + def scalacOptions: Seq[String] = Seq( "-feature", "-deprecation", "-unchecked" ) + + val updated: Boolean = { + val existingClassFiles = lib.listFilesRecursive(compileTarget) + val sourcesChanged = existingClassFiles.nonEmpty && { + val oldestClassFile = existingClassFiles.sortBy(_.lastModified).head + val oldestClassFileAge = oldestClassFile.lastModified + val changedSourceFiles = sourceFiles.filter(_.lastModified > oldestClassFileAge) + if(changedSourceFiles.nonEmpty){ + /* + println(changedSourceFiles) + println(changedSourceFiles.map(_.lastModified)) + println(changedSourceFiles.map(_.lastModified > oldestClassFileAge)) + println(oldestClassFile) + println(oldestClassFileAge) + println("-"*80) + */ + } + changedSourceFiles.nonEmpty + } + sourcesChanged || transitiveDependencies.map(_.updated).fold(false)(_ || _) + } + + private object cacheCompileBasicBuild extends Cache[File] + def compile: File = cacheCompileBasicBuild{ + //println(transitiveDependencies.filter(_.updated).mkString("\n")) + lib.compile( + updated, + sourceFiles, compileTarget, dependencyClasspath, scalacOptions, + zincVersion = zincVersion, scalaVersion = scalaVersion + ) + } + + def runClass: String = "Main" + def run: Unit = lib.runMainIfFound( runClass, Seq(), classLoader ) + + def test: Unit = lib.test(context) + + context.logger.composition(">"*80) + context.logger.composition("class "+this.getClass) + context.logger.composition("dir "+context.cwd) + context.logger.composition("sources "+sources.toList.mkString(" ")) + context.logger.composition("target "+target) + context.logger.composition("dependencyTree\n"+dependencyTree) + context.logger.composition("<"*80) + + // ========== cbt internals ========== + private[cbt] def finalBuild = this + override def show = this.getClass.getSimpleName + "("+context.cwd+")" +} diff --git a/stage2/Lib.scala b/stage2/Lib.scala new file mode 100644 index 0000000..b92e0b3 --- /dev/null +++ b/stage2/Lib.scala @@ -0,0 +1,436 @@ +package cbt +import cbt.paths._ + +import java.io._ +import java.net._ +import java.lang.reflect.InvocationTargetException +import java.nio.file.{Path =>_,_} +import java.nio.file.Files.readAllBytes +import java.security.MessageDigest +import java.util.jar._ + +import scala.collection.immutable.Seq +import scala.reflect.runtime.{universe => ru} +import scala.util._ + +import ammonite.ops.{cwd => _,_} + +case class Developer(id: String, name: String, timezone: String, url: URL) +case class License(name: String, url: URL) + +/** Don't extend. Create your own libs :). */ +final class Lib(logger: Logger) extends Stage1Lib(logger) with Scaffold{ + lib => + + val buildClassName = "Build" + val buildBuildClassName = "BuildBuild" + + /** Loads Build for given Context */ + def loadDynamic(context: Context, default: Context => Build = new Build(_)): Build = { + context.logger.composition( context.logger.showInvocation("Build.loadDynamic",context) ) + loadRoot(context, default).finalBuild + } + /** + Loads whatever Build needs to be executed first in order to eventually build the build for the given context. + This can either the Build itself, of if exists a BuildBuild or a BuildBuild for a BuildBuild and so on. + */ + def loadRoot(context: Context, default: Context => Build = new Build(_)): Build = { + context.logger.composition( context.logger.showInvocation("Build.loadRoot",context) ) + def findStartDir(cwd: String): String = { + val buildDir = realpath(cwd+"/build") + if(new File(buildDir).exists) findStartDir(buildDir) else cwd + } + + val start = findStartDir(context.cwd) + + val useBasicBuildBuild = context.cwd == start + + val rootBuildClassName = if( useBasicBuildBuild ) buildBuildClassName else buildClassName + try{ + if(useBasicBuildBuild) default( context ) else new cbt.BuildBuild( context.copy( cwd = start ) ) + } catch { + case e:ClassNotFoundException if e.getMessage == rootBuildClassName => + throw new Exception(s"no class $rootBuildClassName found in "+start) + } + } + + def compile( + updated: Boolean, + sourceFiles: Seq[File], compileTarget: File, dependenyClasspath: ClassPath, + compileArgs: Seq[String], zincVersion: String, scalaVersion: String + ): File = { + if(sourceFiles.nonEmpty) + lib.zinc( + updated, sourceFiles, compileTarget, dependenyClasspath, compileArgs + )( zincVersion = zincVersion, scalaVersion = scalaVersion ) + compileTarget + } + + def srcJar(sources: Seq[File], artifactId: String, version: String, jarTarget: File): File = { + val file = new File(jarTarget+"/"+artifactId+"-"+version+"-sources.jar") + lib.jarFile(file, sources) + file + } + + def jar(artifactId: String, version: String, compileTarget: File, jarTarget: File): File = { + val file = new File(jarTarget+"/"+artifactId+"-"+version+".jar") + lib.jarFile(file, Seq(compileTarget)) + file + } + + def docJar( + sourceFiles: Seq[File], + dependenyClasspath: ClassPath, + apiTarget: File, + jarTarget: File, + artifactId: String, + version: String, + compileArgs: Seq[String] + ): File = { + class DisableSystemExit extends Exception + object DisableSystemExit{ + def apply(e: Throwable): Boolean = { + e match { + case i: InvocationTargetException => apply(i.getTargetException) + case _: DisableSystemExit => true + case _ => false + } + } + } + + // FIXME: get this dynamically somehow, or is this even needed? + val javacp = ClassPath( + "/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/System/Library/Java/Extensions/MRJToolkit.jar".split(":").toVector.map(new File(_)) + ) + + mkdir(Path(apiTarget)) + if(sourceFiles.nonEmpty){ + System.err.println("creating docs") + try{ + System.setSecurityManager( + new SecurityManager{ + override def checkPermission( permission: java.security.Permission ) = { + if( permission.getName.startsWith("exitVM") ) throw new DisableSystemExit + } + } + ) + redirectOutToErr{ + runMain( + "scala.tools.nsc.ScalaDoc", + Seq( + // FIXME: can we use compiler dependency here? + "-cp", /*javacp+":"+*/ScalaDependencies(logger).classpath.string + ":" + dependenyClasspath.string, + "-d", apiTarget.toString + ) ++ compileArgs ++ sourceFiles.map(_.toString), + new URLClassLoader( + ScalaDependencies(logger).classpath ++ javacp, + ClassLoader.getSystemClassLoader + ) + ) + } + } catch { + case e:InvocationTargetException if DisableSystemExit(e) => + } finally { + System.setSecurityManager(null) + } + } + val docJar = new File(jarTarget+"/"+artifactId+"-"+version+"-javadoc.jar") + lib.jarFile(docJar, Vector(apiTarget)) + docJar + } + + def test( context: Context ) = { + logger.lib(s"invoke testDefault( $context )") + loadDynamic( + context.copy( cwd = context.cwd+"/test/" ), + new Build(_) with mixins.Test + ).run + logger.lib(s"return testDefault( $context )") + + } + + // task reflection helpers + import ru._ + private lazy val anyRefMembers = ru.typeOf[AnyRef].members.toVector.map(taskName) + def taskNames(tpe: Type) = tpe.members.toVector.flatMap(lib.toTask).map(taskName).sorted + private def taskName(method: Symbol) = method.name.decodedName.toString + def toTask(symbol: Symbol): Option[MethodSymbol] = { + Option(symbol) + .filter(_.isPublic) + .filter(_.isMethod) + .map(_.asMethod) + .filter(_.paramLists.flatten.size == 0) + .filterNot(taskName(_) contains "$") + .filterNot(t => anyRefMembers contains taskName(t)) + } + + class ReflectBuild(build: Build) extends ReflectObject(build){ + def usage = { + val baseTasks = lib.taskNames(ru.typeOf[Build]) + val thisTasks = lib.taskNames(subclassType) diff baseTasks + ( + ( + if( thisTasks.nonEmpty ){ + s"""Methods provided by Build ${build.context.cwd} + + ${thisTasks.mkString(" ")} + +""" + } else "" + ) + s"""Methods provided by CBT (but possibly overwritten) + + ${baseTasks.mkString(" ")}""" + ) + "\n" + } + } + + abstract class ReflectObject[T:scala.reflect.ClassTag](obj: T){ + lazy val mirror = ru.runtimeMirror(obj.getClass.getClassLoader) + lazy val subclassType = mirror.classSymbol(obj.getClass).toType + def usage: String + def callNullary( taskName: Option[String] ): Unit = { + taskName + .map{ n => subclassType.member(ru.TermName(n).encodedName) } + .filter(_ != ru.NoSymbol) + .flatMap(toTask _) + .map{ methodSymbol => + val result = mirror.reflect(obj).reflectMethod(methodSymbol)() + // Try to render console representation. Probably not the best way to do this. + val method = scala.util.Try( result.getClass.getDeclaredMethod("toConsole") ) + + method.foreach(m => println(m.invoke(result))) + method.recover{ + case e:NoSuchMethodException if e.getMessage contains "toConsole" => + result match { + case () => "" + case other => println( other.toString ) // no method .toConsole, using to String + } + } + }.getOrElse{ + taskName.foreach{ n => + System.err.println(s"Method not found: $n") + System.err.println("") + } + System.err.println(usage) + System.exit(1) + } + } + } + + + // file system helpers + def basename(path: String) = path.stripSuffix("/").split("/").last + def basename(path: File) = path.toString.stripSuffix("/").split("/").last + def dirname(path: String) = realpath(path).stripSuffix("/").split("/").dropRight(1).mkString("/") + def realpath(name: String) = Paths.get(new File(name).getAbsolutePath).normalize.toString + def realpath(name: File) = new File(Paths.get(name.getAbsolutePath).normalize.toString) + def nameAndContents(file: File) = basename(file.toString) -> readAllBytes(Paths.get(file.toString)) + + def jarFile( jarFile: File, files: Seq[File] ): Unit = { + logger.lib("Start packaging "+jarFile) + val manifest = new Manifest + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0") + val jar = new JarOutputStream(new FileOutputStream(jarFile.toString), manifest) + + val names = for { + base <- files.filter(_.exists).map(realpath) + file <- listFilesRecursive(base) if file.isFile + } yield { + val name = if(base.isDirectory){ + file.toString stripPrefix base.toString + } else file.toString + val entry = new JarEntry( name ) + entry.setTime(file.lastModified) + jar.putNextEntry(entry) + jar.write( Files.readAllBytes( Paths.get(file.toString) ) ) + jar.closeEntry + name + } + + val duplicateFiles = (names diff names.distinct).distinct + assert( + duplicateFiles.isEmpty, + s"Conflicting file names when trying to create $jarFile: "+duplicateFiles.mkString(", ") + ) + + jar.close + logger.lib("Done packaging "+jarFile) + } + + lazy val passphrase = + Option(System.console).getOrElse( + throw new Exception("Can't access Console. This probably shouldn't be run through Nailgun.") + ).readPassword( + "GPG Passphrase please:" + ).mkString + + def sign(file: File): File = { + //http://stackoverflow.com/questions/16662408/correct-way-to-sign-and-verify-signature-using-bouncycastle + val statusCode = + new ProcessBuilder( "gpg", "--batch", "--yes", "-a", "-b", "-s", "--passphrase", passphrase, file.toString ) + .inheritIO.start.waitFor + + if( 0 != statusCode ) throw new Exception("gpg exited with status code "+statusCode) + + new File(file+".asc") + } + + //def requiredForPom[T](name: String): T = throw new Exception(s"You need to override `def $name` in order to generate a valid pom.") + + def pom( + groupId: String, + artifactId: String, + version: String, + name: String, + description: String, + url: URL, + developers: Seq[Developer], + licenses: Seq[License], + scmUrl: String, // seems like invalid URLs are used here in pom files + scmConnection: String, + dependencies: Seq[Dependency], + pomExtra: Seq[scala.xml.Node], + jarTarget: File + ): File = { + val xml = + <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"> + <modelVersion>4.0.0</modelVersion> + <groupId>{groupId}</groupId> + <artifactId>{artifactId}</artifactId> + <version>{version}</version> + <packaging>jar</packaging> + <name>{name}</name> + <description>{description}</description> + <url>{url}</url> + <licenses> + {licenses.map{ license => + <license> + <name>{license.name}</name> + <url>{license.url}</url> + <distribution>repo</distribution> + </license> + }} + </licenses> + <developers> + {developers.map{ developer => + <developer> + <id>{developer.id}</id> + <name>{developer.name}</name> + <timezone>{developer.timezone}</timezone> + <url>{developer.url}</url> + </developer> + }} + </developers> + <scm> + <url>{scmUrl}</url> + <connection>{scmConnection}</connection> + </scm> + {pomExtra} + <dependencies> + { + dependencies.map{ + case d:ArtifactInfo => + <dependency> + <groupId>{d.groupId}</groupId> + <artifactId>{d.artifactId}</artifactId> + <version>{d.version}</version> + </dependency> + } + } + </dependencies> + </project> + val path = new File(jarTarget+"/"+artifactId+"-"+version+".pom") + write.over(Path(path), "<?xml version='1.0' encoding='UTF-8'?>\n" + xml.toString) + path + } + + def concurrently[T,R]( concurrencyEnabled: Boolean )( items: Seq[T] )( projection: T => R ): Seq[R] = { + if(concurrencyEnabled) items.par.map(projection).seq + else items.map(projection) + } + + def publishSnapshot( sourceFiles: Seq[File], artifacts: Seq[File], url: URL ): Unit = { + if(sourceFiles.nonEmpty){ + val files = artifacts.map(nameAndContents) + uploadAll(url, files) + } + } + + def publishSigned( sourceFiles: Seq[File], artifacts: Seq[File], url: URL ): Unit = { + // TODO: make concurrency configurable here + if(sourceFiles.nonEmpty){ + val files = (artifacts ++ artifacts.map(sign)).map(nameAndContents) + lazy val checksums = files.flatMap{ + case (name, content) => Seq( + name+".md5" -> md5(content).toArray.map(_.toByte), + name+".sha1" -> sha1(content).toArray.map(_.toByte) + ) + } + val all = (files ++ checksums) + uploadAll(url, all) + } + } + + + def uploadAll(url: URL, nameAndContents: Seq[(String, Array[Byte])]): Unit = + nameAndContents.map{ case(name, content) => upload(name, content, url) } + + def upload(fileName: String, fileContents: Array[Byte], baseUrl: URL): Unit = { + import java.net._ + import java.io._ + logger.task("uploading "+fileName) + val url = new URL( + baseUrl + fileName + ) + val httpCon = url.openConnection.asInstanceOf[HttpURLConnection] + httpCon.setDoOutput(true) + httpCon.setRequestMethod("PUT") + val userPassword = read(Path(sonatypeLogin)).trim + val encoding = new sun.misc.BASE64Encoder().encode(userPassword.getBytes) + httpCon.setRequestProperty("Authorization", "Basic " + encoding) + httpCon.setRequestProperty("Content-Type", "application/binary") + httpCon.getOutputStream.write( + fileContents + ) + httpCon.getInputStream + } + + + // code for continuous compile + def watch(files: Seq[File])(action: PartialFunction[File, Unit]): Unit = { + import com.barbarysoftware.watchservice._ + import scala.collection.JavaConversions._ + val watcher = WatchService.newWatchService + + files.map{ + file => + if(file.isFile) new File( dirname(file.toString) ) + else file + }.distinct.map{ file => + val watchableFile = new WatchableFile(file) + val key = watchableFile.register( + watcher, + StandardWatchEventKind.ENTRY_CREATE, + StandardWatchEventKind.ENTRY_DELETE, + StandardWatchEventKind.ENTRY_MODIFY + ) + } + + scala.util.control.Breaks.breakable{ + while(true){ + logger.loop("Waiting for file changes...") + Option(watcher.take).map{ + key => + val changedFiles = key + .pollEvents + .filterNot(_.kind == StandardWatchEventKind.OVERFLOW) + .map(_.context.toString) + .map(new File(_)) + changedFiles.foreach( f => logger.loop("Changed: "+f) ) + changedFiles.collect(action) + key.reset + } + } + } + } +} diff --git a/stage2/Scaffold.scala b/stage2/Scaffold.scala new file mode 100644 index 0000000..00c8706 --- /dev/null +++ b/stage2/Scaffold.scala @@ -0,0 +1,148 @@ +package cbt +import java.io._ +import java.net._ +import ammonite.ops.{cwd => _,_} + +trait Scaffold{ + def logger: Logger + + def generateBasicBuildFile( + projectDirectory: File, + scalaVersion: String, + groupId: String, + artifactId: String, + version: String + ): Unit = { + /** + TODO: + - make behavior more user friendly: + - not generate half and then throw exception for one thing already existing + - maybe not generate all of this, e.g. offer different variants + */ + + val generatedFiles = Seq( + "build/build.scala" -> s"""import cbt._ +import java.net.URL +import java.io.File +import scala.collection.immutable.Seq + +class Build(context: Context) extends BasicBuild(context) with BuildShared{ + override def artifactId: String = "$artifactId" + override def groupId = "$groupId" + + override def dependencies = super.dependencies ++ Seq( // don't forget super.dependencies here + // "org.cvogt" %% "scala-extensions" % "0.4.1" + ) + + // required for .pom file + override def name = artifactId + override def description : String = lib.requiredForPom("description") +} +""", + + "build/build/build.scala" -> s"""import cbt._ +import java.net.URL +import java.io.File +import scala.collection.immutable.Seq + +class Build(context: Context) extends BuildBuild(context){ + override def scalaVersion: String = "2.11.7" + + override def dependencies = super.dependencies ++ Seq( + BuildDependency( projectDirectory + "/../build-shared/") + // , "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + ) +} +""", + + "test/Main.scala" -> s"""object Main{ + def main( args: Array[String] ) = { + assert( false, "Go. Write some tests :)!" ) + } +} +""", + + "test/build/build.scala" -> s"""import cbt._ +import java.net.URL +import java.io.File +import scala.collection.immutable.Seq + +class Build(context: cbt.Context) extends BasicBuild(context) with BuildShared/* with cbt.mixins.ScalaTest*/{ + // def scalaTestVersion = "2.2.6" + + override def dependencies = super.dependencies ++ Seq( + // , "org.scalacheck" %% "scalacheck" % "1.13.0" + ) +} +""", + + "test/build/build/build.scala" -> s"""import cbt._ +import java.net.URL +import java.io.File +import scala.collection.immutable.Seq + +class Build(context: Context) extends BuildBuild(context){ + override def scalaVersion: String = "2.11.7" + + override def dependencies = super.dependencies ++ Seq( + BuildDependency( projectDirectory + "/../../build-shared/") + // , "com.lihaoyi" %% "ammonite-ops" % "0.5.5" + ) +} +""", + + "build-shared/build/build.scala" -> s"""import cbt._ +import java.net.URL +import java.io.File +import scala.collection.immutable.Seq + +class Build(context: Context) extends BasicBuild(context){ + override def scalaVersion: String = "$scalaVersion" + + override def dependencies = super.dependencies ++ Seq( // don't forget super.dependencies here + CbtDependency + // , "org.cvogt" %% "scala-extensions" % "0.4.1" + ) +} +""", + + "build-shared/BuildShared.scala" -> s"""import cbt._ +import java.net.URL +import java.io.File +import scala.collection.immutable.Seq + +trait BuildShared extends BasicBuild{ + override def scalaVersion: String = "$scalaVersion" + override def enableConcurrency = false // enable for speed, disable for debugging + + override def groupId = "$groupId" + override def version = "$version" + + // required for .pom file + override def url : URL = lib.requiredForPom("url") + override def developers: Seq[Developer] = lib.requiredForPom("developers") + override def licenses : Seq[License] = lib.requiredForPom("licenses") + override def scmUrl : String = lib.requiredForPom("scmUrl") + override def scmConnection: String = lib.requiredForPom("scmConnection") + override def pomExtra: Seq[scala.xml.Node] = Seq() +} +""" + ) + + generatedFiles.map{ + case ( fileName, code ) => + scala.util.Try{ + write( Path(projectDirectory+"/"+fileName), code ) + import scala.Console._ + println( GREEN + "Created " + fileName + RESET ) + } + }.foreach( + _.recover{ + case e: java.nio.file.FileAlreadyExistsException => + e.printStackTrace + }.get + ) + return () + } + +}
\ No newline at end of file diff --git a/stage2/Stage2.scala b/stage2/Stage2.scala new file mode 100644 index 0000000..05b7c58 --- /dev/null +++ b/stage2/Stage2.scala @@ -0,0 +1,47 @@ +package cbt +import cbt.paths._ +import java.io._ +import scala.collection.immutable.Seq + +object Stage2{ + def main(args: Array[String]) = { + import java.time.LocalTime.now + val init = new Stage1.Init(args) + import java.time._ + val start = LocalTime.now() + def timeTaken = Duration.between(start, LocalTime.now()).toMillis + init.logger.stage2(s"[$now] Stage2 start") + + import init._ + val loop = argsV.lift(1) == Some("loop") + val direct = argsV.lift(1) == Some("direct") + val taskIndex = if(loop || direct) 2 else 1 + val task = argsV.lift( taskIndex ) + + val lib = new Lib(new Stage1.Init(args).logger) + + val context = Context( cwd, argsV.drop( taskIndex + 1 ), logger ) + val first = lib.loadRoot( context ) + val build = first.finalBuild + + val res = if( loop ){ + // TODO: this should allow looping over task specific files, like test files as well + val triggerFiles = first.triggerLoopFiles.map(lib.realpath) + val triggerCbtFiles = Seq( nailgun, stage1, stage2 ).map(lib.realpath _) + val allTriggerFiles = triggerFiles ++ triggerCbtFiles + + logger.loop("Looping change detection over:\n - "+allTriggerFiles.mkString("\n - ")) + + lib.watch(allTriggerFiles){ + case file if triggerCbtFiles.exists(file.toString startsWith _.toString) => + logger.loop("Change is in CBT' own source code.") + logger.loop("Restarting CBT.") + scala.util.control.Breaks.break + case file if triggerFiles.exists(file.toString startsWith _.toString) => + new lib.ReflectBuild( lib.loadDynamic(context) ).callNullary(task) + } + } else new lib.ReflectBuild(build).callNullary(task) + init.logger.stage2(s"[$now] Stage2 end") + res + } +} diff --git a/stage2/dependencies.scala b/stage2/dependencies.scala new file mode 100644 index 0000000..8ed36eb --- /dev/null +++ b/stage2/dependencies.scala @@ -0,0 +1,36 @@ +package cbt +import java.io.File +import scala.collection.immutable.Seq +/* +sealed abstract class ProjectProxy extends Ha{ + protected def delegate: ProjectMetaData + def artifactId: String = delegate.artifactId + def groupId: String = delegate.groupId + def version: String = delegate.version + def exportedClasspath = delegate.exportedClasspath + def dependencies = Seq(delegate) +} +*/ +trait TriggerLoop extends Dependency{ + def triggerLoopFiles: Seq[File] +} +/** You likely want to use the factory method in the BasicBuild class instead of this. */ +case class BuildDependency(context: Context) extends TriggerLoop{ + override def show = this.getClass.getSimpleName + "("+context.cwd+")" + final override lazy val logger = context.logger + final override lazy val lib: Lib = new Lib(logger) + private val root = lib.loadRoot( context.copy(args=Seq()) ) + lazy val build = root.finalBuild + def exportedClasspath = ClassPath(Seq()) + def exportedJars = Seq() + def dependencies = Seq(build) + def triggerLoopFiles = root.triggerLoopFiles + final val updated = build.updated +} +/* +case class DependencyOr(first: BuildDependency, second: MavenDependency) extends ProjectProxy with BuildDependencyBase{ + val isFirst = new File(first.context.cwd).exists + def triggerLoopFiles = if(isFirst) first.triggerLoopFiles else Seq() + protected val delegate = if(isFirst) first else second +} +*/
\ No newline at end of file diff --git a/stage2/mixins.scala b/stage2/mixins.scala new file mode 100644 index 0000000..5ed26d8 --- /dev/null +++ b/stage2/mixins.scala @@ -0,0 +1,35 @@ +package cbt +package mixins +import scala.collection.immutable.Seq +import java.io._ +trait Test extends Build{ + lazy val testedBuild = BuildDependency(projectDirectory+"/../") + override def dependencies = Seq( testedBuild ) ++ super.dependencies + override def scalaVersion = testedBuild.build.scalaVersion +} +trait Sbt extends Build{ + override def sources = Seq(new File(projectDirectory+"/src/main/scala/")) +} +trait SbtTest extends Test{ + override def sources = Vector(new File(projectDirectory+"/../src/test/scala")) +} +trait ScalaTest extends Build with Test{ + def scalaTestVersion: String + + override def dependencies = Seq( + "org.scalatest" %% "scalatest" % scalaTestVersion + ) ++ super.dependencies + + // workaround probable ScalaTest bug throwing away the outer classloader. Not caching doesn't nest them. + override def cacheDependencyClassLoader = false + + override def run = { + val discoveryPath = compile.toString+"/" + context.logger.lib("discoveryPath: "+discoveryPath) + lib.runMain( + "org.scalatest.tools.Runner", + Seq("-R", discoveryPath, "-oF") ++ context.args.drop(1), + classLoader + ) + } +} |