diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6bd83498f..b97965066a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,11 @@ jobs: run: | .github/setup-test-projects.sh &&\ ./mill -i "bridges.scalajs-1[_].publishLocal" &&\ - ./mill -i "bridges.scalajs-1[_].test" + ./mill -i "bridges.scala-native-04[_].publishLocal" &&\ + ./mill -i "bridges.scala-native-05[_].publishLocal" &&\ + ./mill -i "bridges.scalajs-1[_].test" &&\ + ./mill -i "bridges.scala-native-04[_].test" &&\ + ./mill -i "bridges.scala-native-05[_].test" shell: bash test: @@ -67,8 +71,8 @@ jobs: .github/setup-test-projects.sh &&\ ./mill -i 'backend[_].test.compile' &&\ ./mill -i 'frontend[_].test.compile' &&\ - ./mill -i 'backend[2.12.18].test' &&\ - ./mill -i 'frontend[2.12.18].test' + ./mill -i 'backend[2.12.19].test' &&\ + ./mill -i 'frontend[2.12.19].test' shell: bash jvm-tests: diff --git a/.scalafmt.conf b/.scalafmt.conf index 09ae845903..a0c6724130 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.7.15" +version = "3.8.0" align.preset = more maxColumn = 100 diff --git a/backend/src/main/scala/bloop/ClientClassesObserver.scala b/backend/src/main/scala/bloop/ClientClassesObserver.scala new file mode 100644 index 0000000000..e84810e5aa --- /dev/null +++ b/backend/src/main/scala/bloop/ClientClassesObserver.scala @@ -0,0 +1,65 @@ +package bloop + +import java.io.File +import java.util.concurrent.atomic.AtomicReference + +import scala.jdk.CollectionConverters._ + +import bloop.io.AbsolutePath +import bloop.task.Task + +import monix.reactive.Observable +import monix.reactive.subjects.PublishSubject +import sbt.internal.inc.PlainVirtualFileConverter +import xsbti.VirtualFileRef +import xsbti.compile.CompileAnalysis +import xsbti.compile.analysis.Stamp + +/** + * Each time a new compile analysis is produced for a given client, it is given to + * the [[ClientClassObserver]] which computes the list of classes that changed or got created. + * + * A client can subscribe to the observer to get notified of classes to update. + * It is used by DAP to hot reload classes in the debuggee process. + * + * @param clientClassesDir the class directory for the client + */ +private[bloop] class ClientClassesObserver(val classesDir: AbsolutePath) { + private val converter = PlainVirtualFileConverter.converter + private val previousAnalysis: AtomicReference[CompileAnalysis] = new AtomicReference() + private val classesSubject: PublishSubject[Seq[String]] = PublishSubject() + + def observable: Observable[Seq[String]] = classesSubject + + def nextAnalysis(analysis: CompileAnalysis): Task[Unit] = { + val prev = previousAnalysis.getAndSet(analysis) + if (prev != null && classesSubject.size > 0) { + Task { + val previousStamps = prev.readStamps.getAllProductStamps + analysis.readStamps.getAllProductStamps.asScala.iterator.collect { + case (vf, stamp) if isClassFile(vf) && isNewer(stamp, previousStamps.get(vf)) => + getFullyQualifiedClassName(vf) + }.toSeq + } + .flatMap { classesToUpdate => + Task.fromFuture(classesSubject.onNext(classesToUpdate)).map(_ => ()) + } + } else Task.unit + } + + private def isClassFile(vf: VirtualFileRef): Boolean = vf.id.endsWith(".class") + + private def isNewer(current: Stamp, previous: Stamp): Boolean = + previous == null || { + val currentHash = current.getHash + val previousHash = previous.getHash + currentHash.isPresent && + (!previousHash.isPresent || currentHash.get != previousHash.get) + } + + private def getFullyQualifiedClassName(vf: VirtualFileRef): String = { + val path = converter.toPath(vf) + val relativePath = classesDir.underlying.relativize(path) + relativePath.toString.replace(File.separator, ".").stripSuffix(".class") + } +} diff --git a/backend/src/main/scala/bloop/CompileBackgroundTasks.scala b/backend/src/main/scala/bloop/CompileBackgroundTasks.scala index 41306919ae..5b4b57c5d2 100644 --- a/backend/src/main/scala/bloop/CompileBackgroundTasks.scala +++ b/backend/src/main/scala/bloop/CompileBackgroundTasks.scala @@ -8,7 +8,7 @@ import bloop.tracing.BraveTracer abstract class CompileBackgroundTasks { def trigger( - clientClassesDir: AbsolutePath, + clientClassesObserver: ClientClassesObserver, clientReporter: Reporter, clientTracer: BraveTracer, clientLogger: Logger @@ -20,7 +20,7 @@ object CompileBackgroundTasks { val empty: CompileBackgroundTasks = { new CompileBackgroundTasks { def trigger( - clientClassesDir: AbsolutePath, + clientClassesObserver: ClientClassesObserver, clientReporter: Reporter, clientTracer: BraveTracer, clientLogger: Logger diff --git a/backend/src/main/scala/bloop/Compiler.scala b/backend/src/main/scala/bloop/Compiler.scala index 70f0d76f16..ab299e9fdb 100644 --- a/backend/src/main/scala/bloop/Compiler.scala +++ b/backend/src/main/scala/bloop/Compiler.scala @@ -40,6 +40,7 @@ import sbt.util.InterfaceUtil import xsbti.T2 import xsbti.VirtualFileRef import xsbti.compile.{CompilerCache => _, ScalaInstance => _, _} +import scala.util.Try case class CompileInputs( scalaInstance: ScalaInstance, @@ -319,6 +320,7 @@ object Compiler { val classpathOptions = compileInputs.classpathOptions val compilers = compileInputs.compilerCache.get( scalaInstance, + classpathOptions, compileInputs.javacBin, compileInputs.javacOptions.toList ) @@ -451,11 +453,12 @@ object Compiler { val backgroundTasks = new CompileBackgroundTasks { def trigger( - clientClassesDir: AbsolutePath, + clientClassesObserver: ClientClassesObserver, clientReporter: Reporter, clientTracer: BraveTracer, clientLogger: Logger ): Task[Unit] = Task.defer { + val clientClassesDir = clientClassesObserver.classesDir clientLogger.debug(s"Triggering background tasks for $clientClassesDir") val updateClientState = updateExternalClassesDirWithReadOnly(clientClassesDir, clientTracer, clientLogger) @@ -471,10 +474,20 @@ object Compiler { } val deleteNewClassesDir = Task(BloopPaths.delete(AbsolutePath(newClassesDir))) - val allTasks = List(deleteNewClassesDir, updateClientState, writeAnalysisIfMissing) + val publishClientAnalysis = Task { + rebaseAnalysisClassFiles( + analysis, + readOnlyClassesDir, + clientClassesDir.underlying, + sourcesWithFatal + ) + } + .flatMap(clientClassesObserver.nextAnalysis) Task - .gatherUnordered(allTasks) - .map(_ => ()) + .gatherUnordered( + List(deleteNewClassesDir, updateClientState, writeAnalysisIfMissing) + ) + .flatMap(_ => publishClientAnalysis) .onErrorHandleWith(err => { clientLogger.debug("Caught error in background tasks"); clientLogger.trace(err); Task.raiseError(err) @@ -494,14 +507,12 @@ object Compiler { ) } else { val allGeneratedProducts = allGeneratedRelativeClassFilePaths.toMap - val analysisForFutureCompilationRuns = { - rebaseAnalysisClassFiles( - analysis, - readOnlyClassesDir, - newClassesDir, - sourcesWithFatal - ) - } + val analysisForFutureCompilationRuns = rebaseAnalysisClassFiles( + analysis, + readOnlyClassesDir, + newClassesDir, + sourcesWithFatal + ) val resultForFutureCompilationRuns = { resultForDependentCompilationsInSameRun.withAnalysis( @@ -516,12 +527,12 @@ object Compiler { // Schedule the tasks to run concurrently after the compilation end val backgroundTasksExecution = new CompileBackgroundTasks { def trigger( - clientClassesDir: AbsolutePath, + clientClassesObserver: ClientClassesObserver, clientReporter: Reporter, clientTracer: BraveTracer, clientLogger: Logger ): Task[Unit] = { - val clientClassesDirPath = clientClassesDir.toString + val clientClassesDir = clientClassesObserver.classesDir val successBackgroundTasks = backgroundTasksWhenNewSuccessfulAnalysis .map(f => f(clientClassesDir, clientReporter, clientTracer)) @@ -542,7 +553,7 @@ object Compiler { val syntax = path.syntax if (syntax.startsWith(readOnlyClassesDirPath)) { val rebasedFile = AbsolutePath( - syntax.replace(readOnlyClassesDirPath, clientClassesDirPath) + syntax.replace(readOnlyClassesDirPath, clientClassesDir.toString) ) if (rebasedFile.exists) { Files.delete(rebasedFile.underlying) @@ -550,7 +561,18 @@ object Compiler { } } } - Task.gatherUnordered(List(firstTask, secondTask)).map(_ => ()) + + val publishClientAnalysis = Task { + rebaseAnalysisClassFiles( + analysis, + newClassesDir, + clientClassesDir.underlying, + sourcesWithFatal + ) + }.flatMap(clientClassesObserver.nextAnalysis) + Task + .gatherUnordered(List(firstTask, secondTask)) + .flatMap(_ => publishClientAnalysis) } allClientSyncTasks.doOnFinish(_ => Task(clientReporter.reportEndCompilation())) @@ -632,25 +654,33 @@ object Compiler { case None => scalacOptions case Some(_) if existsReleaseSetting || sameHome => scalacOptions case Some(version) => - try { - val numVer = if (version.startsWith("1.8")) 8 else version.takeWhile(_.isDigit).toInt - val bloopNumVer = JavaRuntime.version.takeWhile(_.isDigit).toInt - if (bloopNumVer > numVer) { - scalacOptions ++ List("-release", numVer.toString()) - } else { - logger.warn( - s"Bloop is runing with ${JavaRuntime.version} but your code requires $version to compile, " + - "this might cause some compilation issues when using JDK API unsupported by the Bloop's current JVM version" - ) - scalacOptions + val options: Option[Array[String]] = + for { + numVer <- parseJavaVersion(version) + bloopNumVer <- parseJavaVersion(JavaRuntime.version) + if (bloopNumVer >= 9 && numVer != bloopNumVer) + } yield { + if (bloopNumVer > numVer) { + scalacOptions ++ List("-release", numVer.toString()) + } else { + logger.warn( + s"Bloop is running with ${JavaRuntime.version} but your code requires $version to compile, " + + "this might cause some compilation issues when using JDK API unsupported by the Bloop's current JVM version" + ) + scalacOptions + } } - } catch { - case NonFatal(_) => - scalacOptions - } + options.getOrElse(scalacOptions) } } + private def parseJavaVersion(version: String): Option[Int] = + version.split('-').head.split('.').toList match { + case "1" :: minor :: _ => Try(minor.toInt).toOption + case single :: _ => Try(single.toInt).toOption + case _ => None + } + private def getCompilationOptions( inputs: CompileInputs, logger: Logger, @@ -688,11 +718,12 @@ object Compiler { ): CompileBackgroundTasks = { new CompileBackgroundTasks { def trigger( - clientClassesDir: AbsolutePath, + clientClassesObserver: ClientClassesObserver, clientReporter: Reporter, tracer: BraveTracer, clientLogger: Logger ): Task[Unit] = { + val clientClassesDir = clientClassesObserver.classesDir val backgroundTasks = tasks.map(f => f(clientClassesDir, clientReporter, tracer)) Task.gatherUnordered(backgroundTasks).memoize.map(_ => ()) } @@ -780,8 +811,8 @@ object Compiler { */ def rebaseAnalysisClassFiles( analysis0: CompileAnalysis, - readOnlyClassesDir: Path, - newClassesDir: Path, + origin: Path, + target: Path, sourceFilesWithFatalWarnings: scala.collection.Set[File] ): Analysis = { // Cast to the only internal analysis that we support @@ -789,10 +820,10 @@ object Compiler { def rebase(file: VirtualFileRef): VirtualFileRef = { val filePath = converter.toPath(file).toAbsolutePath() - if (!filePath.startsWith(readOnlyClassesDir)) file + if (!filePath.startsWith(origin)) file else { // Hash for class file is the same because the copy duplicates metadata - val path = newClassesDir.resolve(readOnlyClassesDir.relativize(filePath)) + val path = target.resolve(origin.relativize(filePath)) converter.toVirtualFile(path) } } diff --git a/backend/src/main/scala/bloop/CompilerCache.scala b/backend/src/main/scala/bloop/CompilerCache.scala index 0bac7cb89b..9315c28cc3 100644 --- a/backend/src/main/scala/bloop/CompilerCache.scala +++ b/backend/src/main/scala/bloop/CompilerCache.scala @@ -35,10 +35,10 @@ import sbt.internal.util.LoggerWriter import xsbti.ComponentProvider import xsbti.VirtualFile import xsbti.compile.ClassFileManager +import xsbti.compile.ClasspathOptions import xsbti.compile.Compilers import xsbti.compile.JavaCompiler import xsbti.compile.Output -import xsbti.compile.ScalaCompiler import xsbti.{Logger => XLogger} import xsbti.{Reporter => XReporter} @@ -50,19 +50,21 @@ final class CompilerCache( logger: Logger ) { - private val scalaCompilerCache = new ConcurrentHashMap[ScalaInstance, ScalaCompiler]() - + private val scalaInstanceCache = new ConcurrentHashMap[ScalaInstance, ScalaInstance]() private val javaCompilerCache = new ConcurrentHashMap[JavacKey, JavaCompiler]() def get( scalaInstance: ScalaInstance, + classpathOptions: ClasspathOptions, javacBin: Option[AbsolutePath], javacOptions: List[String] ): Compilers = { - val scalaCompiler = scalaCompilerCache.computeIfAbsent( + val scalaInstanceFromCache = scalaInstanceCache.computeIfAbsent( scalaInstance, - getScalaCompiler(_, componentProvider) + _ => scalaInstance ) + val scalaCompiler = + getScalaCompiler(scalaInstanceFromCache, classpathOptions, componentProvider) val allowLocal = !hasRuntimeJavacOptions(javacOptions) val javaCompiler = @@ -108,12 +110,13 @@ final class CompilerCache( def getScalaCompiler( scalaInstance: ScalaInstance, + classpathOptions: ClasspathOptions, componentProvider: ComponentProvider ): AnalyzingCompiler = { val bridgeSources = BloopComponentCompiler.getModuleForBridgeSources(scalaInstance) val bridgeId = BloopComponentCompiler.getBridgeComponentId(bridgeSources, scalaInstance) componentProvider.component(bridgeId) match { - case Array(jar) => ZincUtil.scalaCompiler(scalaInstance, jar) + case Array(jar) => ZincUtil.scalaCompiler(scalaInstance, jar, classpathOptions) case _ => BloopZincLibraryManagement.scalaCompiler( scalaInstance, @@ -121,6 +124,7 @@ final class CompilerCache( componentProvider, Some(Paths.getCacheDirectory("bridge-cache").toFile), bridgeSources, + classpathOptions, logger ) } diff --git a/backend/src/main/scala/bloop/io/ParallelOps.scala b/backend/src/main/scala/bloop/io/ParallelOps.scala index 4f2e79f87e..49b7709788 100644 --- a/backend/src/main/scala/bloop/io/ParallelOps.scala +++ b/backend/src/main/scala/bloop/io/ParallelOps.scala @@ -173,7 +173,7 @@ object ParallelOps { () } catch { case NonFatal(t) => - logger.report( + logger.error( s"Unexpected error when copying $originFile to $targetFile, you might need to restart the build server.", t ) diff --git a/backend/src/main/scala/bloop/logging/BloopLogger.scala b/backend/src/main/scala/bloop/logging/BloopLogger.scala index e36aa182b3..e20ccc67d7 100644 --- a/backend/src/main/scala/bloop/logging/BloopLogger.scala +++ b/backend/src/main/scala/bloop/logging/BloopLogger.scala @@ -146,13 +146,6 @@ object BloopLogger { def default(name: String): BloopLogger = at(name, System.out, System.err, false, DebugFilter.All) - def prettyPrintException(t: Throwable): String = { - val sw = new java.io.StringWriter() - val pw = new java.io.PrintWriter(sw) - t.printStackTrace(pw) - sw.toString() - } - private lazy val colorsRegex = "\u001b\\[[0-9;]*m".r /** diff --git a/backend/src/main/scala/bloop/util/JavaRuntime.scala b/backend/src/main/scala/bloop/util/JavaRuntime.scala index 403016ad8f..f354e3a97a 100644 --- a/backend/src/main/scala/bloop/util/JavaRuntime.scala +++ b/backend/src/main/scala/bloop/util/JavaRuntime.scala @@ -4,7 +4,9 @@ import javax.tools.JavaCompiler import javax.tools.ToolProvider import scala.annotation.nowarn +import scala.collection.concurrent.TrieMap import scala.util.Failure +import scala.util.Properties import scala.util.Try import bloop.io.AbsolutePath @@ -13,8 +15,6 @@ import com.typesafe.config.ConfigException import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigSyntax -import scala.collection.concurrent.TrieMap -import scala.util.Properties sealed trait JavaRuntime object JavaRuntime { diff --git a/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala b/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala index 973b442204..c01fdfd619 100644 --- a/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala +++ b/backend/src/main/scala/sbt/internal/inc/BloopZincLibraryManagement.scala @@ -29,6 +29,7 @@ object BloopZincLibraryManagement { componentProvider: ComponentProvider, secondaryCacheDir: Option[File], compilerBridgeSource: ModuleID, + classpathOptions: ClasspathOptions, logger: _root_.bloop.logging.Logger ): AnalyzingCompiler = { val compilerBridgeProvider = BloopComponentCompiler.interfaceProvider( @@ -40,7 +41,7 @@ object BloopZincLibraryManagement { new AnalyzingCompiler( scalaInstance, compilerBridgeProvider, - ClasspathOptionsUtil.boot(), + classpathOptions, _ => (), loader ) diff --git a/backend/src/test/scala/bloop/CompilerCacheSpec.scala b/backend/src/test/scala/bloop/CompilerCacheSpec.scala index 29dcac41eb..806e026825 100644 --- a/backend/src/test/scala/bloop/CompilerCacheSpec.scala +++ b/backend/src/test/scala/bloop/CompilerCacheSpec.scala @@ -19,6 +19,7 @@ import sbt.internal.inc.BloopComponentCompiler import sbt.internal.inc.javac.WriteReportingJavaFileObject import sbt.io.syntax.File import xsbti.compile.ClassFileManager +import xsbti.compile.ClasspathOptionsUtil @Category(Array(classOf[FastTests])) class CompilerCacheSpec { @@ -104,12 +105,31 @@ class CompilerCacheSpec { bloop.internal.build.BloopScalaInfo.scalaVersion, new RecordingLogger() ) + val cpOptions = ClasspathOptionsUtil.boot() - val javac0 = compilerCache.get(scalaInstance, None, Nil).javaTools().javac() - val javac1 = compilerCache.get(scalaInstance, None, Nil).javaTools().javac() + val javac0 = compilerCache.get(scalaInstance, cpOptions, None, Nil).javaTools().javac() + val javac1 = compilerCache.get(scalaInstance, cpOptions, None, Nil).javaTools().javac() assertTrue(javac0 + " was not eq to " + javac1, javac0 eq javac1) } + @Test + def differentClasspathOptions(): Unit = withCompilerCache { compilerCache => + val scalaInstance = ScalaInstance.resolve( + "org.scala-lang", + "scala-compiler", + bloop.internal.build.BloopScalaInfo.scalaVersion, + new RecordingLogger() + ) + val classpathOptions1 = ClasspathOptionsUtil.boot() + val classpathOptions2 = ClasspathOptionsUtil.manual() + + val scalac1 = compilerCache.get(scalaInstance, classpathOptions1, None, Nil).scalac() + val scalac2 = compilerCache.get(scalaInstance, classpathOptions2, None, Nil).scalac() + assertEquals(scalac1.classpathOptions, classpathOptions1) + assertEquals(scalac2.classpathOptions, classpathOptions2) + assertEquals(scalac1.scalaInstance, scalac2.scalaInstance) + } + @Test def runtimeOptionsNeverLocal(): Unit = withCompilerCache { compilerCache => val scalaInstance = ScalaInstance.resolve( @@ -118,13 +138,15 @@ class CompilerCacheSpec { bloop.internal.build.BloopScalaInfo.scalaVersion, new RecordingLogger() ) + val cpOptions = ClasspathOptionsUtil.boot() // We first populate the compiler cache with a compiler that may be local. - val javac0 = compilerCache.get(scalaInstance, None, Nil).javaTools().javac() - val javac1 = compilerCache.get(scalaInstance, None, List("-J-Dfoo=bar")).javaTools().javac() + val _ = compilerCache.get(scalaInstance, cpOptions, None, Nil).javaTools().javac() + val javac1 = + compilerCache.get(scalaInstance, cpOptions, None, List("-J-Dfoo=bar")).javaTools().javac() assertTrue( - s"`javac1` was not a forked compiler, despite the runtime flag: ${javac0.getClass}", + s"`javac1` was not a forked compiler, despite the runtime flag: ${javac1.getClass}", javac1.isInstanceOf[compilerCache.BloopForkedJavaCompiler] ) } diff --git a/bloop-rifle/src/bloop/rifle/internal/Operations.scala b/bloop-rifle/src/bloop/rifle/internal/Operations.scala index 81755d15bf..314e8c16fe 100644 --- a/bloop-rifle/src/bloop/rifle/internal/Operations.scala +++ b/bloop-rifle/src/bloop/rifle/internal/Operations.scala @@ -145,6 +145,7 @@ object Operations { val writeOutputToOpt0 = if (bloopServerSupportsFileTruncating) Some(s.outputPath) else None + (Seq(s"daemon:${s.path}"), "bloop.Bloop", writeOutputToOpt0) } diff --git a/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala b/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala index 773d689a7d..2498e12517 100644 --- a/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala +++ b/bloop-rifle/src/bloop/rifle/internal/SnailgunClient.scala @@ -40,11 +40,9 @@ class SnailgunClient(openSocket: () => Socket) extends snailgun.Client { case t: SocketException => logger.debug("Tracing an ignored socket exception...") logger.trace(t) - () case t: SocketExceptionLike => logger.debug("Tracing an ignored socket exception-like...") logger.trace(t) - () } } } diff --git a/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala b/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala index 2e3b4bb9be..3f29b296fe 100644 --- a/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala +++ b/bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala @@ -37,6 +37,7 @@ object NativeBridge { } val nativeLTO = config.mode match { case LinkerMode.Debug => build.LTO.none + case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.full case LinkerMode.Release => build.LTO.thin } diff --git a/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala b/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala index 5d61cc8038..3733523393 100644 --- a/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala +++ b/bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala @@ -53,7 +53,8 @@ class ScalaNativeToolchainSpec { val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3) assertTrue(s"Linking failed: ${logger.getMessages.mkString("\n")}", resultingState.status.isOk) - logger.getMessages.assertContain("Optimizing (release-full mode)", atLevel = "info") + // nothing is printed for fast release + logger.getMessages.assertContain("Generated native binary", atLevel = "info") } @Test def canRunScalaNativeProjectDefaultMainClass(): Unit = { diff --git a/bridges/scala-native-0.5/src/main/scala/bloop/scalanative/NativeBridge.scala b/bridges/scala-native-0.5/src/main/scala/bloop/scalanative/NativeBridge.scala new file mode 100644 index 0000000000..04627c5ecc --- /dev/null +++ b/bridges/scala-native-0.5/src/main/scala/bloop/scalanative/NativeBridge.scala @@ -0,0 +1,102 @@ +package bloop.scalanative +import java.nio.file.Files +import java.nio.file.Path + +import scala.scalanative.build +import scala.scalanative.util.Scope +import scala.concurrent.Future +import scala.concurrent.ExecutionContext + +import bloop.config.Config.LinkerMode +import bloop.config.Config.NativeConfig +import bloop.data.Project +import bloop.io.Paths +import bloop.logging.DebugFilter +import bloop.logging.Logger + +object NativeBridge { + private implicit val ctx: DebugFilter = DebugFilter.Link + private val sharedScope = Scope.unsafe() + + def nativeLink( + config0: NativeConfig, + project: Project, + classpath: Array[Path], + entry: String, + target: Path, + logger: Logger, + ec: ExecutionContext + ): Future[Path] = { + val workdir = project.out.resolve("native") + if (workdir.isDirectory) Paths.delete(workdir) + Files.createDirectories(workdir.underlying) + + val nativeLogger = + build.Logger(logger.trace _, logger.debug _, logger.info _, logger.warn _, logger.error _) + val config = setUpNativeConfig(project, classpath, config0) + val nativeMode = config.mode match { + case LinkerMode.Debug => build.Mode.debug + case LinkerMode.Release => build.Mode.releaseFast + } + val nativeLTO = config.mode match { + case LinkerMode.Debug => build.LTO.none + case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.none + case LinkerMode.Release => build.LTO.thin + } + + val nativeConfig = + build.Config.empty + .withMainClass(Option(entry)) + .withClassPath(classpath) + .withBaseDir(target.getParent()) + .withLogger(nativeLogger) + .withCompilerConfig( + build.NativeConfig.empty + .withClang(config.clang) + .withClangPP(config.clangpp) + .withBaseName(target.getFileName().toString()) + .withCompileOptions(config.options.compiler) + .withLinkingOptions(config.options.linker) + .withGC(build.GC(config.gc)) + .withMode(nativeMode) + .withLTO(nativeLTO) + .withLinkStubs(config.linkStubs) + .withCheck(config.check) + .withDump(config.dump) + .withTargetTriple(config.targetTriple) + ) + + build.Build.build(nativeConfig)(sharedScope, ec) + } + + private[scalanative] def setUpNativeConfig( + project: Project, + classpath: Array[Path], + config: NativeConfig + ): NativeConfig = { + val mode = config.mode + val options = config.options + val gc = if (config.gc.isEmpty) build.GC.default.name else config.gc + val clang = if (config.clang.toString.isEmpty) build.Discover.clang() else config.clang + val clangpp = if (config.clangpp.toString.isEmpty) build.Discover.clangpp() else config.clangpp + val lopts = if (options.linker.isEmpty) build.Discover.linkingOptions() else options.linker + val copts = if (options.compiler.isEmpty) build.Discover.compileOptions() else options.compiler + + val targetTriple = config.targetTriple + + NativeConfig.apply( + version = config.version, + mode = mode, + toolchain = Nil, // No worries, toolchain is on this project's classpath + gc = gc, + targetTriple = targetTriple, + clang = clang, + clangpp = clangpp, + options = options, + linkStubs = config.linkStubs, + check = config.check, + dump = config.dump, + output = config.output + ) + } +} diff --git a/bridges/scala-native-0.5/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala b/bridges/scala-native-0.5/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala new file mode 100644 index 0000000000..11bd91719b --- /dev/null +++ b/bridges/scala-native-0.5/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala @@ -0,0 +1,105 @@ +package bloop.scalanative + +import java.util.concurrent.TimeUnit + +import scala.concurrent.duration.Duration + +import bloop.cli.Commands +import bloop.cli.OptimizerConfig +import bloop.data.Platform +import bloop.data.Project +import bloop.engine.Run +import bloop.engine.tasks.toolchains.ScalaNativeToolchain +import bloop.logging.RecordingLogger +import bloop.util.TestUtil + +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.experimental.categories.Category +import bloop.engine.State +import bloop.config.Config + +@Category(Array(classOf[bloop.FastTests])) +class ScalaNativeToolchainSpec { + private val state0 = { + def setUpNative(p: Project): Project = { + val platform = p.platform match { + case nativePlatform: Platform.Native => + nativePlatform.copy( + toolchain = Some(ScalaNativeToolchain.apply(this.getClass.getClassLoader)) + ) + + case _ => p.platform + } + p.copy(platform = platform) + } + + val configDir = TestUtil.getBloopConfigDir("cross-test-build-scala-native-0.5") + val logger = bloop.logging.BloopLogger.default(configDir.toString()) + TestUtil.loadTestProject(configDir, logger, true, _.map(setUpNative)) + } + @Test def canLinkScalaNativeProject(): Unit = { + val logger = new RecordingLogger + val state = state0.copy(logger = logger) + val action = Run(Commands.Link(List("test-projectNative"))) + val resultingState = TestUtil.blockingExecute(action, state, maxDuration) + + assertTrue(s"Linking failed: ${logger.getMessages.mkString("\n")}", resultingState.status.isOk) + logger.getMessages.assertContain("Generated native binary '", atLevel = "info") + } + + @Test def canLinkScalaNativeProjectInReleaseMode(): Unit = { + val logger = new RecordingLogger + val mode = OptimizerConfig.Release + val state = state0.copy(logger = logger) + val action = Run(Commands.Link(List("test-projectNative"), optimize = Some(mode))) + val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3) + + assertTrue( + s"Linking failed: ${logger.getMessages.mkString("\n")}", + resultingState.status.isOk + ) + logger.getMessages.assertContain("Optimizing (release-fast mode)", atLevel = "info") + } + + @Test def canRunScalaNativeProjectDefaultMainClass(): Unit = { + val logger = new RecordingLogger + val state = state0.copy(logger = logger) + val action = Run(Commands.Run(List("test-projectNative"))) + val resultingState = TestUtil.blockingExecute(action, state, maxDuration) + + assertTrue( + s"Linking failed: ${logger.getMessages.mkString("\n")}", + resultingState.status.isOk + ) + logger.getMessages.assertContain("Hello, world from DefaultApp!", atLevel = "info") + } + + @Test def canRunScalaJvmProjectDefaultMainClass(): Unit = { + val logger = new RecordingLogger + val state = state0.copy(logger = logger) + val action = Run(Commands.Run(List("test-project"), main = None)) + val resultingState = TestUtil.blockingExecute(action, state, maxDuration) + + assertTrue( + s"Linking failed: ${logger.getMessages.mkString("\n")}", + resultingState.status.isOk + ) + logger.getMessages.assertContain("Hello, world!", atLevel = "info") + } + + private val maxDuration = Duration.apply(90, TimeUnit.SECONDS) + private implicit class RichLogs(logs: List[(String, String)]) { + def assertContain(needle: String, atLevel: String): Unit = { + def failMessage = s"""Logs did not contain `$needle` at level `$atLevel`. Logs were: + |${logs.mkString("\n")}""".stripMargin + assertTrue( + failMessage, + logs.exists { + case (`atLevel`, msg) => msg.contains(needle) + case _ => false + } + ) + } + } +} diff --git a/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala b/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala index 86877f94d5..b189f22055 100644 --- a/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala +++ b/bridges/scalajs-1/src/main/scala/bloop/scalajs/JsBridge.scala @@ -14,12 +14,12 @@ import bloop.config.Config.ModuleKindJS import bloop.data.Project import bloop.logging.DebugFilter import bloop.logging.{Logger => BloopLogger} -import bloop.scalajs.jsenv.JsDomNodeJsEnv -import bloop.scalajs.jsenv.NodeJSConfig -import bloop.scalajs.jsenv.NodeJSEnv import org.scalajs.jsenv.Input +import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv +import org.scalajs.jsenv.nodejs.NodeJSEnv import org.scalajs.linker.PathIRContainer +import org.scalajs.linker.PathOutputDirectory import org.scalajs.linker.PathOutputFile import org.scalajs.linker.StandardImpl import org.scalajs.linker.interface.{ModuleKind => ScalaJSModuleKind, _} @@ -27,20 +27,10 @@ import org.scalajs.logging.Level import org.scalajs.logging.{Logger => JsLogger} import org.scalajs.testing.adapter.TestAdapter import org.scalajs.testing.adapter.TestAdapterInitializer +import java.nio.file.Files /** * Defines operations provided by the Scala.JS 1.x toolchain. - * - * The 1.x js bridge needs to inline the implementation of `NodeJSEnv`, - * `JSDOMNodeJSEnv` and `ComRunner` because there is a bug in the latest - * Scala.js release that does not run `close` on the underlying process, - * skipping the destruction of the process running Scala.js tests. Aside - * from leaking, this is fatal in Windows because the underlying process - * is alive and keeps open references to the output JS file. - * - * We can remove all of the js environments and runners as soon as this - * issue is fixed upstream. Note that our 0.6.x version handles cancellation - * correctly. */ object JsBridge { private class Logger(logger: BloopLogger)(implicit filter: DebugFilter) extends JsLogger { @@ -94,13 +84,13 @@ object JsBridge { classpath: Array[Path], runMain: java.lang.Boolean, mainClass: Option[String], - target: Path, + targetDirectory: Path, logger: BloopLogger, executionContext: ExecutionContext ): Unit = { implicit val ec = executionContext implicit val logFilter: DebugFilter = DebugFilter.Link - val linker = ScalaJSLinker.reuseOrCreate(config, target) + val linker = ScalaJSLinker.reuseOrCreate(config, targetDirectory) val cache = StandardImpl.irFileCache().newCache val irContainersPairs = PathIRContainer.fromClasspath(classpath) @@ -126,11 +116,14 @@ object JsBridge { } } - val output = LinkerOutput(PathOutputFile(target)) - val resultFuture = for { libraryIRs <- libraryIrsFuture - _ <- linker.link(libraryIRs, moduleInitializers, output, new Logger(logger)) + _ <- linker.link( + libraryIRs, + moduleInitializers, + PathOutputDirectory(targetDirectory), + new Logger(logger) + ) } yield () Await.result(resultFuture, Duration.Inf) @@ -151,11 +144,12 @@ object JsBridge { if (nodeModules.toFile().exists()) { logger.debug("Node.js module path: " + nodeModules.toString()) val fullEnv = Map("NODE_PATH" -> nodeModules.toString()) ++ env - val config = - NodeJSConfig().withExecutable(nodePath).withCwd(Some(baseDirectory)).withEnv(fullEnv) val nodeEnv = - if (!jsConfig.jsdom.contains(true)) new NodeJSEnv(logger, config) - else new JsDomNodeJsEnv(logger, config) + if (!jsConfig.jsdom.contains(true)) + new NodeJSEnv( + NodeJSEnv.Config().withExecutable(nodePath).withEnv(fullEnv) + ) + else new JSDOMNodeJSEnv(JSDOMNodeJSEnv.Config().withExecutable(nodePath).withEnv(fullEnv)) // The order of the scripts mandates the load order in the JavaScript runtime val input = jsConfig.kind match { @@ -169,7 +163,7 @@ object JsBridge { val result = adapter.loadFrameworks(frameworkNames).flatMap(_.toList) (result, () => adapter.close()) } else { - logger.error( + logger.warn( s"Cannot discover test frameworks, missing node_modules in test project, expected them at $nodeModules" ) (Nil, () => ()) diff --git a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/JsDomNodeJsEnv.scala b/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/JsDomNodeJsEnv.scala deleted file mode 100644 index 1fd0ef1cfe..0000000000 --- a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/JsDomNodeJsEnv.scala +++ /dev/null @@ -1,198 +0,0 @@ -package bloop.scalajs.jsenv - -import java.io.File -import java.io.InputStream -import java.net.URI -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption - -import scala.util.control.NonFatal - -import bloop.logging.Logger - -import com.google.common.jimfs.Jimfs -import org.scalajs.jsenv.ExternalJSRun -import org.scalajs.jsenv.Input -import org.scalajs.jsenv.JSComRun -import org.scalajs.jsenv.JSEnv -import org.scalajs.jsenv.JSRun -import org.scalajs.jsenv.JSUtils.escapeJS -import org.scalajs.jsenv.RunConfig -import org.scalajs.jsenv.UnsupportedInputException -import org.scalajs.jsenv.nodejs.BloopComRun - -/** - * See comments in [[bloop.scalajs.JsBridge]]. - * - * Adapted from `jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala`. - */ -class JsDomNodeJsEnv(logger: Logger, config: NodeJSConfig) extends JSEnv { - private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) - - val name: String = "Node.js with jsdom" - - def start(input: Seq[Input], runConfig: RunConfig): JSRun = { - JsDomNodeJsEnv.validator.validate(runConfig) - val scripts = validateInput(input) - try { - internalStart(codeWithJSDOMContext(scripts), runConfig) - } catch { - case NonFatal(t) => - JSRun.failed(t) - } - } - - def startWithCom(input: Seq[Input], runConfig: RunConfig, onMessage: String => Unit): JSComRun = { - validator.validate(runConfig) - val scripts = validateInput(input) - BloopComRun.start(runConfig, onMessage) { comLoader => - internalStart(comLoader :: codeWithJSDOMContext(scripts), runConfig) - } - } - - private def validateInput(input: Seq[Input]): List[Path] = { - input.map { - case Input.Script(script) => - script - - case _ => - throw new UnsupportedInputException(input) - }.toList - } - - private def internalStart(files: List[Path], runConfig: RunConfig): JSRun = - NodeJSEnv.internalStart(logger, config, env)( - NodeJSEnv.write(files.map(Input.Script)), - runConfig - ) - - private def env: Map[String, String] = - Map("NODE_MODULE_CONTEXTS" -> "0") ++ config.env - - private def codeWithJSDOMContext(scripts: List[Path]): List[Path] = { - val scriptsURIs = scripts.map(JsDomNodeJsEnv.materialize) - val scriptsURIsAsJSStrings = - scriptsURIs.map(uri => "\"" + escapeJS(uri.toASCIIString) + "\"") - val scriptsURIsJSArray = scriptsURIsAsJSStrings.mkString("[", ", ", "]") - val jsDOMCode = { - s""" - | - |(function () { - | var jsdom = require("jsdom"); - | - | if (typeof jsdom.JSDOM === "function") { - | // jsdom >= 10.0.0 - | var virtualConsole = new jsdom.VirtualConsole() - | .sendTo(console, { omitJSDOMErrors: true }); - | virtualConsole.on("jsdomError", function (error) { - | try { - | // Display as much info about the error as possible - | if (error.detail && error.detail.stack) { - | console.error("" + error.detail); - | console.error(error.detail.stack); - | } else { - | console.error(error); - | } - | } finally { - | // Whatever happens, kill the process so that the run fails - | process.exit(1); - | } - | }); - | - | var dom = new jsdom.JSDOM("", { - | virtualConsole: virtualConsole, - | url: "http://localhost/", - | - | /* Allow unrestricted