diff --git a/.gitignore b/.gitignore index 401e9f2..40e6adb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build/ target/ project/target/ +test/ diff --git a/Dockerfile b/Dockerfile index 3ed5213..d34b8b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,11 @@ FROM alpine:3.8 as build ARG BINTRAY_USERNAME ARG BINTRAY_API -RUN apk add --no-cache openjdk8 python curl +RUN apk add --no-cache openjdk8 python curl nodejs ENV LANG C.UTF-8 ENV JAVA_HOME /usr/lib/jvm/java-1.8-openjdk -ENV PATH $PATH:$JAVA_HOME/jre/bin:$JAVA_HOME/bin +ENV PATH $PATH:$JAVA_HOME/jre/bin:$JAVA_HOME/bin:/seed/bloop COPY build.sbt BLOOP SEED COURSIER /seed/ COPY project/ /seed/project/ @@ -27,12 +27,13 @@ RUN curl -L https://github.com/scalacenter/bloop/releases/download/v$(cat BLOOP) # Pre-fetch bridges and their dependencies to speed up dependency resolution # later -RUN bloop/blp-coursier fetch \ +RUN blp-coursier fetch \ ch.epfl.scala:bloop-js-bridge-0-6_2.12:$(cat BLOOP) \ ch.epfl.scala:bloop-js-bridge-1-0_2.12:$(cat BLOOP) \ ch.epfl.scala:bloop-native-bridge_2.12:$(cat BLOOP) RUN set -x && \ + (blp-server &) && \ curl -o csbt https://raw.githubusercontent.com/coursier/sbt-launcher/master/csbt && \ chmod +x csbt && \ (./csbt compile || \ @@ -62,7 +63,7 @@ RUN set -x && \ ./csbt test && \ BINTRAY_USER=$BINTRAY_USERNAME BINTRAY_PASS=$BINTRAY_API ./csbt --add-coursier=true "; publishLocal; publish" -RUN bloop/blp-coursier bootstrap tindzk:seed_2.12:$(cat SEED) -f -o seed +RUN blp-coursier bootstrap tindzk:seed_2.12:$(cat SEED) -f -o seed # # Run stage diff --git a/build.sbt b/build.sbt index a945c77..714dcab 100644 --- a/build.sbt +++ b/build.sbt @@ -43,7 +43,8 @@ libraryDependencies ++= Seq( "com.joefkelley" %% "argyle" % "1.0.0", "org.scalaj" %% "scalaj-http" % "2.4.1", "commons-io" % "commons-io" % "2.6", - "io.monix" %% "minitest" % "2.2.2" % "test", + "com.zaxxer" % "nuprocess" % "1.2.4" % "test", + "io.monix" %% "minitest" % "2.3.2" % "test", scalaOrganization.value % "scala-reflect" % scalaVersion.value ) diff --git a/src/main/scala/seed/generation/Bloop.scala b/src/main/scala/seed/generation/Bloop.scala index 18b8f1a..e0c96d2 100644 --- a/src/main/scala/seed/generation/Bloop.scala +++ b/src/main/scala/seed/generation/Bloop.scala @@ -438,14 +438,11 @@ object Bloop { name = name + "-test", bloopPath = bloopPath, buildPath = buildPath, - dependencies = targets.map(name + "-" + _ + "-test"), + dependencies = targets.map(t => name + "-" + t.id + "-test"), classesDir = buildPath, classPath = List(), sources = List(), - // TODO Cannot be set to None - scalaCompiler = Some(Resolution.ScalaCompiler( - build.project.scalaOrganisation, build.project.scalaVersion, List(), - List())), + scalaCompiler = None, scalaOptions = List(), testFrameworks = List(), platform = None) @@ -457,14 +454,11 @@ object Bloop { name = name, bloopPath = bloopPath, buildPath = buildPath, - dependencies = module.targets.map(name + "-" + _), + dependencies = module.targets.map(t => name + "-" + t.id), classesDir = buildPath, classPath = List(), sources = List(), - // TODO Cannot be set to None - scalaCompiler = Some(Resolution.ScalaCompiler( - build.project.scalaOrganisation, build.project.scalaVersion, List(), - List())), + scalaCompiler = None, scalaOptions = List(), testFrameworks = List(), platform = None) diff --git a/src/test/scala/seed/generation/BloopIntegrationSpec.scala b/src/test/scala/seed/generation/BloopIntegrationSpec.scala new file mode 100644 index 0000000..00524f2 --- /dev/null +++ b/src/test/scala/seed/generation/BloopIntegrationSpec.scala @@ -0,0 +1,75 @@ +package seed.generation + +import java.nio.file.{Files, Paths} + +import minitest.SimpleTestSuite +import org.apache.commons.io.FileUtils +import seed.artefact.{ArtefactResolution, Coursier} +import seed.generation.util.ProcessHelper +import seed.model.{Build, Platform} + +import scala.concurrent.ExecutionContext.Implicits._ + +object BloopIntegrationSpec extends SimpleTestSuite { + testAsync("Generate and compile meta modules") { + val projectPath = Paths.get("test/meta-module") + if (Files.exists(projectPath)) FileUtils.deleteDirectory(projectPath.toFile) + + val bloopPath = projectPath.resolve(".bloop") + val buildPath = projectPath.resolve("build") + val sourcePath = projectPath.resolve("src") + + Set(bloopPath, buildPath, sourcePath) + .foreach(Files.createDirectories(_)) + + val build = Build( + project = Build.Project( + "2.12.8", scalaJsVersion = Some("0.6.26")), + module = Map("example" -> Build.Module( + sources = List(sourcePath), + targets = List(Platform.JVM, Platform.JavaScript)))) + + val resolvedIvyPath = Coursier.DefaultIvyPath + val resolvedCachePath = Coursier.DefaultCachePath + + val compilerDeps = ArtefactResolution.allCompilerDeps(build) + val platformDeps = ArtefactResolution.allPlatformDeps(build) + val libraryDeps = ArtefactResolution.allLibraryDeps(build) + + val resolution = + Coursier.resolveAndDownload(platformDeps ++ libraryDeps, build.resolvers, + resolvedIvyPath, resolvedCachePath, false) + val compilerResolution = + compilerDeps.map(d => + Coursier.resolveAndDownload(d, build.resolvers, resolvedIvyPath, + resolvedCachePath, false)) + + Bloop.buildModule( + projectPath, + bloopPath, + buildPath, + build, + resolution, + compilerResolution, + build.module.keys.head, + build.module.values.head) + + FileUtils.write(sourcePath.resolve("Main.scala").toFile, + """object Main extends App { println("hello") }""", + "UTF-8") + + def compile = + ProcessHelper.runBloop(projectPath)("compile", "example").map { x => + assertEquals(x.contains("Compiled example-jvm"), true) + assertEquals(x.contains("Compiled example-js"), true) + } + + def run = + ProcessHelper.runBloop(projectPath)("run", "example-js", "example-jvm") + .map { x => + assertEquals(x.split("\n").count(_ == "hello"), 2) + } + + for { _ <- compile; _ <- run } yield () + } +} diff --git a/src/test/scala/seed/generation/util/ProcessHelper.scala b/src/test/scala/seed/generation/util/ProcessHelper.scala new file mode 100644 index 0000000..a22830a --- /dev/null +++ b/src/test/scala/seed/generation/util/ProcessHelper.scala @@ -0,0 +1,95 @@ +package seed.generation.util + +import java.nio.ByteBuffer +import java.nio.file.Path +import java.util.concurrent.{Executors, TimeUnit} + +import com.zaxxer.nuprocess.{NuAbstractProcessHandler, NuProcess, NuProcessBuilder} +import seed.Log + +import scala.collection.JavaConverters._ +import scala.concurrent.{Future, Promise} + +sealed trait ProcessOutput +object ProcessOutput { + case class StdErr(output: String) extends ProcessOutput + case class StdOut(output: String) extends ProcessOutput +} + +/** + * @param onProcStart Takes PID + * @param onProcExit Takes exit code + */ +class ProcessHandler(onLog: ProcessOutput => Unit, + onProcStart: Int => Unit, + onProcExit: Int => Unit + ) extends NuAbstractProcessHandler { + override def onStart(nuProcess: NuProcess): Unit = + onProcStart(nuProcess.getPID) + + override def onExit(statusCode: Int): Unit = onProcExit(statusCode) + + override def onStderr(buffer: ByteBuffer, closed: Boolean): Unit = + if (!closed) { + val bytes = new Array[Byte](buffer.remaining) + buffer.get(bytes) + new String(bytes).split("\n").foreach(line => + onLog(ProcessOutput.StdErr(line))) + } + + override def onStdout(buffer: ByteBuffer, closed: Boolean): Unit = + if (!closed) { + val bytes = new Array[Byte](buffer.remaining) + buffer.get(bytes) + new String(bytes).split("\n").foreach(line => + onLog(ProcessOutput.StdOut(line))) + } +} + +object ProcessHelper { + val scheduler = Executors.newScheduledThreadPool(1) + def schedule(seconds: Int)(f: => Unit): Unit = + scheduler.schedule({ () => f }: Runnable, seconds, TimeUnit.SECONDS) + + def runBloop(cwd: Path, silent: Boolean = false) + (args: String*): Future[String] = { + val promise = Promise[String]() + val sb = new StringBuilder + val pb = new NuProcessBuilder((List("bloop") ++ args).asJava) + + var terminated = false + + pb.setProcessListener(new ProcessHandler( + { case ProcessOutput.StdOut(output) => + if (!silent) Log.info("stdout: " + output) + sb.append(output + "\n") + case ProcessOutput.StdErr(output) => + if (!silent) Log.error("stderr: " + output) + sb.append(output + "\n") + }, + pid => if (!silent) Log.info("PID: " + pid) else (), + { statusCode => + if (!silent) Log.info("Status code: " + statusCode) + if (terminated || statusCode == 0) promise.success(sb.toString) + else promise.failure(new Exception("Status code: " + statusCode)) + } + )) + pb.setCwd(cwd) + val proc = pb.start() + + // Work around a CI problem where onExit() does not get called on + // ProcessHandler + schedule(60) { + if (!promise.isCompleted) { + Log.error(s"Process did not terminate after 60s (isRunning = ${proc.isRunning})") + if (proc.isRunning) { + Log.error("Forcing termination...") + terminated = true + proc.destroy(true) + } + } + } + + promise.future + } +}