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 <script> tags. This is exactly as - | * "dangerous" as the arbitrary execution of script files we - | * do in the non-jsdom Node.js env. - | */ - | resources: "usable", - | runScripts: "dangerously" - | }); - | - | var window = dom.window; - | window["scalajsCom"] = global.scalajsCom; - | - | var scriptsSrcs = $scriptsURIsJSArray; - | for (var i = 0; i < scriptsSrcs.length; i++) { - | var script = window.document.createElement("script"); - | script.src = scriptsSrcs[i]; - | window.document.body.appendChild(script); - | } - | } else { - | // jsdom v9.x - | var virtualConsole = jsdom.createVirtualConsole() - | .sendTo(console, { omitJsdomErrors: true }); - | virtualConsole.on("jsdomError", function (error) { - | /* This inelegant if + console.error is the only way I found - | * to make sure the stack trace of the original error is - | * printed out. - | */ - | if (error.detail && error.detail.stack) - | console.error(error.detail.stack); - | - | // Throw the error anew to make sure the whole execution fails - | throw error; - | }); - | - | jsdom.env({ - | html: "", - | virtualConsole: virtualConsole, - | url: "http://localhost/", - | created: function (error, window) { - | if (error == null) { - | window["scalajsCom"] = global.scalajsCom; - | } else { - | throw error; - | } - | }, - | scripts: $scriptsURIsJSArray - | }); - | } - |})(); - |""".stripMargin - } - List( - Files.write( - Jimfs.newFileSystem().getPath("codeWithJSDOMContext.js"), - jsDOMCode.getBytes(StandardCharsets.UTF_8) - ) - ) - } -} - -object JsDomNodeJsEnv { - private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) - - // tmpSuffixRE and tmpFile copied from HTMLRunnerBuilder.scala in Scala.js - - private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r - private def tmpFile(path: String, in: InputStream): URI = { - try { - /* - createTempFile requires a prefix of at least 3 chars - * - we use a safe part of the path as suffix so the extension stays (some - * browsers need that) and there is a clue which file it came from. - */ - val suffix = tmpSuffixRE.findFirstIn(path).orNull - - val f = File.createTempFile("tmp-", suffix) - f.deleteOnExit() - Files.copy(in, f.toPath, StandardCopyOption.REPLACE_EXISTING) - f.toURI - } finally { - in.close() - } - } - - private def materialize(path: Path): URI = { - try { - path.toFile.toURI - } catch { - case _: UnsupportedOperationException => - tmpFile(path.toString, Files.newInputStream(path)) - } - } -} diff --git a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/NodeJsEnv.scala b/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/NodeJsEnv.scala deleted file mode 100644 index 82e9a7a983..0000000000 --- a/bridges/scalajs-1/src/main/scala/bloop/scalajs/jsenv/NodeJsEnv.scala +++ /dev/null @@ -1,308 +0,0 @@ -package bloop.scalajs.jsenv - -import java.io.File -import java.io.OutputStream -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption - -import scala.concurrent.Future - -import bloop.engine.ExecutionContext -import bloop.exec.Forker -import bloop.logging.DebugFilter -import bloop.logging.Logger - -import com.google.common.jimfs.Jimfs -import monix.execution.Cancelable -import monix.execution.atomic.AtomicBoolean -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 -import org.scalajs.jsenv.nodejs.NodeJSEnv.SourceMap - -// Copy pasted verbatim from Scala.js source code, added withCwd() -// Original file: scala-js/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala -final class NodeJSConfig private ( - val executable: String, - val args: List[String], - val env: Map[String, String], - val cwd: Option[Path], - val sourceMap: SourceMap -) { - private def this() = { - this( - executable = "node", - args = Nil, - env = Map.empty, - cwd = None, - sourceMap = SourceMap.EnableIfAvailable - ) - } - - def withExecutable(executable: String): NodeJSConfig = - copy(executable = executable) - - def withArgs(args: List[String]): NodeJSConfig = - copy(args = args) - - def withEnv(env: Map[String, String]): NodeJSConfig = - copy(env = env) - - def withCwd(cwd: Option[Path]): NodeJSConfig = - copy(cwd = cwd) - - def withSourceMap(sourceMap: SourceMap): NodeJSConfig = - copy(sourceMap = sourceMap) - - /** Forces enabling (true) or disabling (false) source maps. */ - def withSourceMap(sourceMap: Boolean): NodeJSConfig = - withSourceMap(if (sourceMap) SourceMap.EnableIfAvailable else SourceMap.Disable) - - private def copy( - executable: String = executable, - args: List[String] = args, - env: Map[String, String] = env, - cwd: Option[Path] = cwd, - sourceMap: SourceMap = sourceMap - ): NodeJSConfig = { - new NodeJSConfig(executable, args, env, cwd, sourceMap) - } -} - -object NodeJSConfig { - - /** - * Returns a default configuration for a [[jsenv.NodeJSEnv]]. - * - * The defaults are: - * - * - `executable`: `"node"` - * - `args`: `Nil` - * - `env`: `Map.empty` - * - `cwd`: `None` - * - `sourceMap`: [[org.scalajs.jsenv.nodejs.NodeJSEnv.SourceMap.EnableIfAvailable]] - */ - def apply(): NodeJSConfig = new NodeJSConfig() -} - -/** - * See comments in [[bloop.scalajs.JsBridge]]. - * - * Adapted from `scala-js/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/NodeJSEnv.scala`. - */ -final class NodeJSEnv(logger: Logger, config: NodeJSConfig) extends JSEnv { - override val name: String = "Node.js" - private def env: Map[String, String] = Map("NODE_MODULE_CONTEXTS" -> "0") ++ config.env - - def start(input: Seq[Input], runConfig: RunConfig): JSRun = { - NodeJSEnv.validator.validate(runConfig) - validateInput(input) - internalStart(initFiles ++ input, runConfig) - } - - def startWithCom(input: Seq[Input], runConfig: RunConfig, onMessage: String => Unit): JSComRun = { - NodeJSEnv.validator.validate(runConfig) - validateInput(input) - BloopComRun.start(runConfig, onMessage) { comLoader => - internalStart(initFiles ++ (Input.Script(comLoader) +: input), runConfig) - } - } - - private def validateInput(input: Seq[Input]): Unit = input.foreach { - case _: Input.Script | _: Input.ESModule | _: Input.CommonJSModule => - // ok - case _ => - throw new UnsupportedInputException(input) - } - - private def internalStart(input: Seq[Input], runConfig: RunConfig): JSRun = { - runConfig.logger.debug("Using input file: " + (input.mkString(","))) - NodeJSEnv.internalStart(logger, config, env)(NodeJSEnv.write(input), runConfig) - } - - import NodeJSEnv.installSourceMap - import NodeJSEnv.installSourceMapIfAvailable - - private def initFiles: Seq[Input] = config.sourceMap match { - case SourceMap.Disable => Nil - case SourceMap.EnableIfAvailable => Input.Script(installSourceMapIfAvailable) :: Nil - case SourceMap.Enable => Input.Script(installSourceMap) :: Nil - } -} - -object NodeJSEnv { - private lazy val fs = Jimfs.newFileSystem() - implicit val debugFilter: DebugFilter = DebugFilter.Test - private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) - - private lazy val installSourceMapIfAvailable = { - Files.write( - fs.getPath("optionalSourceMapSupport.js"), - """ - |try { - | require('source-map-support').install(); - |} catch (e) { - |}; - """.stripMargin.getBytes(StandardCharsets.UTF_8) - ) - } - - private lazy val installSourceMap = { - Files.write( - fs.getPath("sourceMapSupport.js"), - "require('source-map-support').install();".getBytes(StandardCharsets.UTF_8) - ) - } - - private[jsenv] def write(input: Seq[Input])(out: OutputStream): Unit = { - def runScript(path: Path): String = { - try { - val f = path.toFile - val pathJS = "\"" + escapeJS(f.getAbsolutePath) + "\"" - s""" - require('vm').runInThisContext( - require('fs').readFileSync($pathJS, { encoding: "utf-8" }), - { filename: $pathJS, displayErrors: true } - ) - """ - } catch { - case _: UnsupportedOperationException => - val code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8) - val codeJS = "\"" + escapeJS(code) + "\"" - val pathJS = "\"" + escapeJS(path.toString) + "\"" - s""" - require('vm').runInThisContext( - $codeJS, - { filename: $pathJS, displayErrors: true } - ) - """ - } - } - - def requireCommonJSModule(module: Path): String = - s"""require("${escapeJS(toFile(module).getAbsolutePath)}")""" - - def importESModule(module: Path): String = - s"""import("${escapeJS(toFile(module).toURI.toASCIIString)}")""" - - def execInputExpr(input: Input): String = input match { - case Input.Script(script) => runScript(script) - case Input.CommonJSModule(module) => requireCommonJSModule(module) - case Input.ESModule(module) => importESModule(module) - } - - def println(str: String): Unit = { - out.write((str + "\n").getBytes("UTF-8")) - out.flush() - } - - if (!input.exists(_.isInstanceOf[Input.ESModule])) { - /* If there is no ES module in the input, we can do everything - * synchronously, and directly on the standard input. - */ - for (item <- input) - println(execInputExpr(item) + ";") - } else { - /* If there is at least one ES module, we must asynchronous chain things, - * and we must use an actual file to feed code to Node.js (because - * `import()` cannot be used from the standard input). - */ - val importChain = input.foldLeft("Promise.resolve()") { (prev, item) => - s"$prev.\n then(${execInputExpr(item)})" - } - val importerFileContent = { - s""" - |$importChain.catch(e => { - | console.error(e); - | process.exit(1); - |}); - """.stripMargin - } - val f = createTmpFile("importer.js") - Files.write(f.toPath, importerFileContent.getBytes(StandardCharsets.UTF_8)) - println(s"""require("${escapeJS(f.getAbsolutePath)}");""") - } - } - - private def toFile(path: Path): File = { - try { - path.toFile - } catch { - case _: UnsupportedOperationException => - val f = createTmpFile(path.toString) - Files.copy(path, f.toPath, StandardCopyOption.REPLACE_EXISTING) - f - } - } - - // tmpSuffixRE and createTmpFile copied from HTMLRunnerBuilder.scala - - private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r - - private def createTmpFile(path: String): File = { - /* - createTempFile requires a prefix of at least 3 chars - * - we use a safe part of the path as suffix so the extension stays (some - * browsers need that) and there is a clue which file it came from. - */ - val suffix = tmpSuffixRE.findFirstIn(path).orNull - - val f = File.createTempFile("tmp-", suffix) - f.deleteOnExit() - f - } - - def internalStart( - logger: Logger, - config: NodeJSConfig, - env: Map[String, String] - )( - write: OutputStream => Unit, - runConfig: RunConfig - ): JSRun = { - val command = config.executable :: config.args - logger.debug(s"Starting process ${command.mkString(" ")}...") - logger.debug(s"Current working directory: ${config.cwd}") - logger.debug(s"Current environment: ${config.env}") - - val cancellable = - Forker - .run( - config.cwd.map(_.toFile), - command, - logger, - config.env, - writeToStdIn = outputStream => { - write(outputStream) - Cancelable.empty - }, - debugLog = msg => logger.debug(msg) - ) - .runAsync(ExecutionContext.ioScheduler) - - new JSRun { - import scala.concurrent.ExecutionContext.Implicits.global - - private val isClosed = AtomicBoolean(false) - override def future: Future[Unit] = cancellable.map { results => - logger.debug(s"Finished with results: ${results}") - () - } - override def close(): Unit = { - // Make sure we only destroy the process once, the test adapter can call this several times! - if (!isClosed.getAndSet(true)) { - logger.debug(s"Destroying process...") - cancellable.cancel() - } - () - } - } - } -} diff --git a/bridges/scalajs-1/src/main/scala/org/scalajs/jsenv/nodejs/BloopComRun.scala b/bridges/scalajs-1/src/main/scala/org/scalajs/jsenv/nodejs/BloopComRun.scala deleted file mode 100644 index 25381ca79a..0000000000 --- a/bridges/scalajs-1/src/main/scala/org/scalajs/jsenv/nodejs/BloopComRun.scala +++ /dev/null @@ -1,330 +0,0 @@ -/* __ *\ -** ________ ___ / / ___ __ ____ Scala.js Node.js env ** -** / __/ __// _ | / / / _ | __ / // __/ (c) 2017, LAMP/EPFL ** -** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** -** /____/\___/_/ |_/____/_/ | |__/ /____/ ** -** |/____/ ** -\* */ - -package org.scalajs.jsenv.nodejs - -import java.io._ -import java.net._ -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent._ -import scala.util.Failure -import scala.util.Success -import scala.util.control.NonFatal - -import com.google.common.jimfs.Jimfs -import org.scalajs.jsenv._ - -/** - * A copy of the `ComRun` in the scala-js codebase to handles - * cancellation gracefully, as the Scala.js 0.6.x runner does. - * - * The runner has the following changes: - * - * 1. Invoke `run.close()` in `def close()` - * 2. Only invoke the logic in `def close()` once as the call-site - * calls it more than once. - */ -private final class BloopComRun( - run: JSRun, - handleMessage: String => Unit, - serverSocket: ServerSocket -) extends JSComRun { - import BloopComRun._ - - /** Promise that completes once the receiver thread is completed. */ - private[this] val promise = Promise[Unit]() - - @volatile - private[this] var state: State = AwaitingConnection(Nil) - - // If the run completes, make sure we also complete. - run.future.onComplete { - case Failure(t) => forceClose(t) - case Success(_) => onJSTerminated() - } - - // TODO replace this with scheduled tasks on the execution context. - private[this] val receiver = new Thread { - setName("ComRun receiver") - - override def run(): Unit = { - try { - try { - /* We need to await the connection unconditionally. Otherwise the JS end - * might try to connect indefinitely. - */ - awaitConnection() - - while (state != Closing) { - state match { - case s: AwaitingConnection => - throw new IllegalStateException(s"Unexpected state: $s") - - case Closing => - /* We can end up here if there is a race between the two read to - * state. Do nothing, loop will terminate. - */ - - case Connected(_, _, js2jvm) => - try { - val len = js2jvm.readInt() - val carr = Array.fill(len)(js2jvm.readChar()) - handleMessage(String.valueOf(carr)) - } catch { - case _: EOFException => - // JS end terminated gracefully. Close. - close() - } - } - } - } catch { - case _: IOException if state == Closing => - // We got interrupted by a graceful close. - // This is OK. - } - - /* Everything got closed. We wait for the run to terminate. - * We need to wait in order to make sure that closing the - * underlying run does not fail it. - */ - BloopComRun.this.run.future.foreach { _ => - BloopComRun.this.run.close() - promise.trySuccess(()) - } - } catch { - case t: Throwable => handleThrowable(t) - } - } - } - - receiver.start() - - def future: Future[Unit] = promise.future - - def send(msg: String): Unit = synchronized { - state match { - case AwaitingConnection(msgs) => - state = AwaitingConnection(msg :: msgs) - - case Connected(_, jvm2js, _) => - try { - writeMsg(jvm2js, msg) - jvm2js.flush() - } catch { - case t: Throwable => handleThrowable(t) - } - - case Closing => // ignore msg. - } - } - - // ++ bloop - def close(): Unit = synchronized { - val oldState = state - if (oldState == Closing) () - else { - // Close the underlying run - run.close() - - // Signal receiver thread that it is OK if socket read fails. - state = Closing - - oldState match { - case c: Connected => - // Interrupts the receiver thread and signals the VM to terminate. - closeAll(c) - - case Closing | _: AwaitingConnection => - } - } - } - // -- bloop - - private def onJSTerminated() = { - close() - - /* Interrupt receiver if we are still waiting for connection. - * Should only be relevant if we are still awaiting the connection. - * Note: We cannot do this in close(), otherwise if the JVM side closes - * before the JS side connected, the JS VM will fail instead of terminate - * normally. - */ - serverSocket.close() - } - - private def forceClose(cause: Throwable) = { - promise.tryFailure(cause) - close() - run.close() - serverSocket.close() - } - - private def handleThrowable(cause: Throwable) = { - forceClose(cause) - if (!NonFatal(cause)) - throw cause - } - - private def awaitConnection(): Unit = { - var comSocket: Socket = null - var jvm2js: DataOutputStream = null - var js2jvm: DataInputStream = null - - try { - comSocket = serverSocket.accept() - serverSocket.close() // we don't need it anymore. - jvm2js = new DataOutputStream(new BufferedOutputStream(comSocket.getOutputStream)) - js2jvm = new DataInputStream(new BufferedInputStream(comSocket.getInputStream)) - - onConnected(Connected(comSocket, jvm2js, js2jvm)) - } catch { - case t: Throwable => - closeAll(comSocket, jvm2js, js2jvm) - throw t - } - } - - private def onConnected(c: Connected): Unit = synchronized { - state match { - case AwaitingConnection(msgs) => - msgs.reverse.foreach(writeMsg(c.jvm2js, _)) - c.jvm2js.flush() - state = c - - case _: Connected => - throw new IllegalStateException(s"Unexpected state: $state") - - case Closing => - closeAll(c) - } - } -} - -object BloopComRun { - - /** - * Starts a [[JSComRun]] using the provided [[JSRun]] launcher. - * - * @param config Configuration for the run. - * @param onMessage callback upon message reception. - * @param startRun [[JSRun]] launcher. Gets passed a - * [[java.nio.file.Path Path]] that initializes `scalaJSCom` on - * `global`. Requires Node.js libraries. - */ - def start(config: RunConfig, onMessage: String => Unit)( - startRun: Path => JSRun - ): JSComRun = { - try { - val serverSocket = - new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address - - val run = startRun(setupFile(serverSocket.getLocalPort)) - - new BloopComRun(run, onMessage, serverSocket) - } catch { - case NonFatal(t) => - println(s"GOT exception when running BloopComRun.start ${t}") - JSComRun.failed(t) - } - } - - private def closeAll(c: Closeable*): Unit = - c.withFilter(_ != null).foreach(_.close()) - - private def closeAll(c: Connected): Unit = - closeAll(c.comSocket, c.jvm2js, c.js2jvm) - - private sealed trait State - - private final case class AwaitingConnection(sendQueue: List[String]) extends State - - private final case class Connected( - comSocket: Socket, - jvm2js: DataOutputStream, - js2jvm: DataInputStream - ) extends State - - private final case object Closing extends State - - private def writeMsg(s: DataOutputStream, msg: String): Unit = { - s.writeInt(msg.length) - s.writeChars(msg) - } - - private def setupFile(port: Int): Path = { - Files.write( - Jimfs.newFileSystem().getPath("comSetup.js"), - s""" - |(function() { - | // The socket for communication - | var socket = require('net').connect($port); - | - | // Buffers received data - | var inBuffer = Buffer.alloc(0); - | - | // Buffers received messages - | var inMessages = []; - | - | // The callback where received messages go - | var onMessage = null; - | - | socket.on('data', function(data) { - | inBuffer = Buffer.concat([inBuffer, data]); - | - | while (inBuffer.length >= 4) { - | var msgLen = inBuffer.readInt32BE(0); - | var byteLen = 4 + msgLen * 2; - | - | if (inBuffer.length < byteLen) return; - | var res = ""; - | - | for (var i = 0; i < msgLen; ++i) - | res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2)); - | - | inBuffer = inBuffer.slice(byteLen); - | - | if (inMessages !== null) inMessages.push(res); - | else onMessage(res); - | } - | }); - | - | socket.on('error', function(err) { - | console.error("Scala.js Com failed: " + err); - | process.exit(-1); - | }); - | - | socket.on('close', function() { process.exit(0); }); - | - | global.scalajsCom = { - | init: function(onMsg) { - | if (onMessage !== null) throw new Error("Com already initialized"); - | onMessage = onMsg; - | process.nextTick(function() { - | for (var i = 0; i < inMessages.length; ++i) - | onMessage(inMessages[i]); - | inMessages = null; - | }); - | }, - | send: function(msg) { - | var len = msg.length; - | var buf = Buffer.allocUnsafe(4 + len * 2); - | buf.writeInt32BE(len, 0); - | for (var i = 0; i < len; ++i) - | buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2); - | socket.write(buf); - | } - | } - |}).call(this); - """.stripMargin.getBytes(StandardCharsets.UTF_8) - ) - } -} diff --git a/build.sc b/build.sc index 39d3f97d42..3d64c217ba 100644 --- a/build.sc +++ b/build.sc @@ -16,26 +16,26 @@ import java.io.File import scala.concurrent.duration.{Duration, DurationInt} object Dependencies { - def scala212 = "2.12.18" - def scala213 = "2.13.12" + def scala212 = "2.12.19" + def scala213 = "2.13.13" def scalaVersions = Seq(scala212, scala213) def serverScalaVersion = scala212 - def asmVersion = "9.6" + def asmVersion = "9.7" def coursierVersion = "2.1.0-M6-53-gb4f448130" def graalvmVersion = "22.2.0" def jsoniterVersion = "2.13.3.2" - def scalaJs1Version = "1.14.0" + def scalaJs1Version = "1.16.0" def scalaJsEnvsVersion = "1.1.1" def asm = ivy"org.ow2.asm:asm:$asmVersion" def asmUtil = ivy"org.ow2.asm:asm-util:$asmVersion" def bloopConfig = ivy"ch.epfl.scala::bloop-config:1.5.5" - def brave = ivy"io.zipkin.brave:brave:5.16.0" - def bsp4j = ivy"ch.epfl.scala:bsp4j:2.1.0-M7" - def bsp4s = ivy"ch.epfl.scala::bsp4s:2.1.0-M7" + def brave = ivy"io.zipkin.brave:brave:5.18.1" + def bsp4j = ivy"ch.epfl.scala:bsp4j:2.1.1" + def bsp4s = ivy"ch.epfl.scala::bsp4s:2.1.1" def caseApp = ivy"com.github.alexarchambault::case-app:2.0.6" def caseApp21 = ivy"com.github.alexarchambault::case-app:2.1.0-M15" def collectionCompat = ivy"org.scala-lang.modules::scala-collection-compat:2.9.0" @@ -54,33 +54,34 @@ object Dependencies { def junit = ivy"com.github.sbt:junit-interface:0.13.3" def libdaemonjvm = ivy"io.github.alexarchambault.libdaemon::libdaemon:0.0.11" def libraryManagement = ivy"org.scala-sbt::librarymanagement-ivy:1.9.3" - def log4j = ivy"org.apache.logging.log4j:log4j-core:2.21.1" - def logback = ivy"ch.qos.logback:logback-classic:1.4.6" + def log4j = ivy"org.apache.logging.log4j:log4j-core:2.23.0" + def logback = ivy"ch.qos.logback:logback-classic:1.4.14" def macroParadise = ivy"org.scalamacros:::paradise:2.1.1" def monix = ivy"io.monix::monix:3.2.0" def munit = ivy"org.scalameta::munit:0.7.29" def nailgun = ivy"io.github.alexarchambault.bleep:nailgun-server:1.0.7" def osLib = ivy"com.lihaoyi::os-lib:0.9.0" def pprint = ivy"com.lihaoyi::pprint:0.8.1" - def sbtTestAgent = ivy"org.scala-sbt:test-agent:1.9.7" + def sbtTestAgent = ivy"org.scala-sbt:test-agent:1.9.9" def sbtTestInterface = ivy"org.scala-sbt:test-interface:1.0" - def scalaDebugAdapter = ivy"ch.epfl.scala::scala-debug-adapter:3.1.4" + def scalaDebugAdapter = ivy"ch.epfl.scala::scala-debug-adapter:4.0.3" def scalaJsLinker1 = ivy"org.scala-js::scalajs-linker:$scalaJs1Version" def scalaJsEnvs1 = ivy"org.scala-js::scalajs-js-envs:$scalaJsEnvsVersion" def scalaJsEnvNode1 = ivy"org.scala-js::scalajs-env-nodejs:$scalaJsEnvsVersion" def scalaJsEnvJsdomNode1 = ivy"org.scala-js::scalajs-env-jsdom-nodejs:1.1.0" def scalaJsSbtTestAdapter1 = ivy"org.scala-js::scalajs-sbt-test-adapter:$scalaJs1Version" def scalaJsLogging1 = ivy"org.scala-js::scalajs-logging:1.1.1" - def scalaNativeTools04 = ivy"org.scala-native::tools:0.4.16" - def scalazCore = ivy"org.scalaz::scalaz-core:7.3.7" + def scalaNativeTools04 = ivy"org.scala-native::tools:0.4.17" + def scalaNativeTools05 = ivy"org.scala-native::tools:0.5.0-RC2" + def scalazCore = ivy"org.scalaz::scalaz-core:7.3.8" def snailgun = ivy"io.github.alexarchambault.scala-cli.snailgun::snailgun-core:0.4.1-sc2" def sourcecode = ivy"com.lihaoyi::sourcecode:0.3.1" def svm = ivy"org.graalvm.nativeimage:svm:$graalvmVersion" def utest = ivy"com.lihaoyi::utest:0.8.2" def xxHashLibrary = ivy"net.jpountz.lz4:lz4:1.3.0" def zinc = ivy"org.scala-sbt::zinc:1.9.5" - def zipkinSender = ivy"io.zipkin.reporter2:zipkin-sender-urlconnection:2.16.4" - def zt = ivy"org.zeroturnaround:zt-zip:1.16" + def zipkinSender = ivy"io.zipkin.reporter2:zipkin-sender-urlconnection:2.17.2" + def zt = ivy"org.zeroturnaround:zt-zip:1.17" def graalVmId = s"graalvm-java17:$graalvmVersion" } @@ -401,6 +402,7 @@ class Frontend(val crossScalaVersion: String) extends BloopCrossSbtModule with B | def zincVersion = "${Dependencies.zinc.dep.version}" | def snailgunVersion = "0.4.1-sc2" | def nativeBridge04 = "${bridges.`scala-native-04`().artifactId()}" + | def nativeBridge05 = "${bridges.`scala-native-05`().artifactId()}" | def jsBridge1 = "${bridges.`scalajs-1`().artifactId()}" |} |""".stripMargin @@ -539,14 +541,19 @@ object bridges extends Module { } object `scala-native-04` extends Cross[ScalaNative04](Dependencies.scalaVersions: _*) - class ScalaNative04(val crossScalaVersion: String) + object `scala-native-05` extends Cross[ScalaNative05](Dependencies.scalaVersions: _*) + + class ScalaNative04(val crossScalaVersion: String) extends ScalaNative(crossScalaVersion, 4) + class ScalaNative05(val crossScalaVersion: String) extends ScalaNative(crossScalaVersion, 5) + + class ScalaNative(val crossScalaVersion: String, nativeMajorVersion: Int) extends BloopCrossSbtModule with BloopPublish { - def artifactName = "bloop-native-bridge-0-4" + def artifactName = s"bloop-native-bridge-0-$nativeMajorVersion" private def updateSources(originalSources: Seq[PathRef]): Seq[PathRef] = - if (millSourcePath.endsWith(os.rel / "scala-native-04")) { - val updatedSourcePath = millSourcePath / os.up / "scala-native-0.4" + if (millSourcePath.endsWith(os.rel / s"scala-native-0$nativeMajorVersion")) { + val updatedSourcePath = millSourcePath / os.up / s"scala-native-0.$nativeMajorVersion" originalSources.map { case pathRef if pathRef.path.startsWith(millSourcePath) => PathRef(updatedSourcePath / pathRef.path.relativeTo(millSourcePath)) @@ -563,9 +570,10 @@ object bridges extends Module { shared(), backend() ) - def compileIvyDeps = super.compileIvyDeps() ++ Agg( - Dependencies.scalaNativeTools04 - ) + def compileIvyDeps = super.compileIvyDeps() ++ Agg { + if (nativeMajorVersion == 4) Dependencies.scalaNativeTools04 + else Dependencies.scalaNativeTools05 + } object test extends Tests { def sources = T.sources(updateSources(super.sources())) diff --git a/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala b/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala index 634d8b0e9b..7c0550b121 100644 --- a/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala +++ b/frontend/src/main/scala/bloop/bsp/BloopBspServices.scala @@ -60,9 +60,9 @@ import bloop.internal.build.BuildInfo import bloop.io.AbsolutePath import bloop.io.Environment.lineSeparator import bloop.io.RelativePath -import bloop.logging.BloopLogger import bloop.logging.BspServerLogger import bloop.logging.DebugFilter +import bloop.logging.Logger import bloop.reporter.BspProjectReporter import bloop.reporter.ProblemPerPhase import bloop.reporter.ReporterConfig @@ -139,6 +139,7 @@ final class BloopBspServices( .requestAsync(endpoints.BuildTarget.scalaMainClasses)(p => schedule(scalaMainClasses(p))) .requestAsync(endpoints.BuildTarget.scalaTestClasses)(p => schedule(scalaTestClasses(p))) .requestAsync(endpoints.BuildTarget.dependencySources)(p => schedule(dependencySources(p))) + .requestAsync(endpoints.BuildTarget.dependencyModules)(p => schedule(dependencyModules(p))) .requestAsync(endpoints.DebugSession.start)(p => schedule(startDebugSession(p))) .requestAsync(endpoints.BuildTarget.jvmTestEnvironment)(p => schedule(jvmTestEnvironment(p))) .requestAsync(endpoints.BuildTarget.jvmRunEnvironment)(p => schedule(jvmRunEnvironment(p))) @@ -308,7 +309,7 @@ final class BloopBspServices( debugProvider = Some(BloopBspServices.DefaultDebugProvider), inverseSourcesProvider = Some(true), dependencySourcesProvider = Some(true), - dependencyModulesProvider = None, + dependencyModulesProvider = Some(true), resourcesProvider = Some(true), outputPathsProvider = None, buildTargetChangedProvider = Some(false), @@ -558,7 +559,7 @@ final class BloopBspServices( Tasks.clean(state, projectsToClean, includeDeps = false).materialize.map { case Success(state) => (state, Right(bsp.CleanCacheResult(None, cleaned = true))) case Failure(exception) => - val t = BloopLogger.prettyPrintException(exception) + val t = Logger.prettyPrintException(exception) val msg = s"Unexpected error when cleaning build targets!${lineSeparator}$t" state -> Right(bsp.CleanCacheResult(Some(msg), cleaned = false)) } @@ -602,10 +603,7 @@ final class BloopBspServices( params: bsp.DebugSessionParams ): BspEndpointResponse[bsp.DebugSessionAddress] = { - def inferDebuggee( - projects: Seq[Project], - state: State - ): BspResponse[Debuggee] = { + def inferDebuggee(projects: Seq[Project], state: State): BspResponse[Debuggee] = { def convert[A: JsonValueCodec]( f: A => Either[String, Debuggee] ): Either[Response.Error, Debuggee] = { @@ -646,7 +644,7 @@ final class BloopBspServices( BloopDebuggeeRunner.forTestSuite(projects, testClasses, state, ioScheduler) }) case Some(bsp.DebugSessionParamsDataKind.ScalaAttachRemote) => - Right(BloopDebuggeeRunner.forAttachRemote(state, ioScheduler, projects)) + Right(BloopDebuggeeRunner.forAttachRemote(projects, state, ioScheduler)) case dataKind => Left(Response.invalidRequest(s"Unsupported data kind: $dataKind")) } } @@ -901,11 +899,12 @@ final class BloopBspServices( } case platform @ Platform.Js(config, _, _) => val cmd = Commands.Run(List(project.name)) - val target = ScalaJsToolchain.linkTargetFrom(project, config) - linkMainWithJs(cmd, project, state, mainClass.className, target, platform) + val targetDir = ScalaJsToolchain.linkTargetFrom(project, config) + linkMainWithJs(cmd, project, state, mainClass.className, targetDir, platform) .flatMap { state => + val files = targetDir.list.map(_.toString()) // We use node to run the program (is this a special case?) - val args = ("node" +: target.syntax +: cmd.args).toArray + val args = ("node" +: files ::: cmd.args).toArray if (!state.status.isOk) Task.now(state) else Tasks.runNativeOrJs(state, cwd, args) } @@ -1205,6 +1204,58 @@ final class BloopBspServices( } } + def dependencyModules( + request: bsp.DependencyModulesParams + ): BspEndpointResponse[bsp.DependencyModulesResult] = { + def modules( + projects: Seq[ProjectMapping], + state: State + ): BspResult[bsp.DependencyModulesResult] = { + val response = bsp.DependencyModulesResult( + projects.iterator.map { + case (target, project) => + val modules = project.resolution.toList.flatMap { res => + res.modules.map { module => + val mavenDependencyModule = bsp.MavenDependencyModule( + module.organization, + module.name, + module.version, + module.artifacts.map(artifact => + bsp.MavenDependencyModuleArtifact( + bsp.Uri(AbsolutePath(artifact.path).toBspUri), + artifact.classifier + ) + ), + None + ) + + val encoded = writeToArray(mavenDependencyModule) + bsp.DependencyModule( + module.name, + module.version, + Some(bsp.DependencyModuleDataKind.Maven), + Some(RawJson(encoded)) + ) + } + }.distinct + bsp.DependencyModulesItem(target, modules) + }.toList + ) + + Task.now((state, Right(response))) + } + + ifInitialized(None) { (state: State, logger: BspServerLogger) => + mapToProjects(request.targets, state) match { + case Left(error) => + // Log the mapping error to the user via a log event + an error status code + logger.error(error) + Task.now((state, Right(bsp.DependencyModulesResult(Nil)))) + case Right(mappings) => modules(mappings, state) + } + } + } + def dependencySources( request: bsp.DependencySourcesParams ): BspEndpointResponse[bsp.DependencySourcesResult] = { diff --git a/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala b/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala index 8d56ce2fc9..75fa303592 100644 --- a/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala +++ b/frontend/src/main/scala/bloop/dap/BloopDebugToolsResolver.scala @@ -34,13 +34,12 @@ class BloopDebugToolsResolver(logger: Logger) extends DebugToolsResolver { } } - override def resolveStepFilter(scalaVersion: ScalaVersion): Try[ClassLoader] = { - getOrTryUpdate(stepFilterCache, scalaVersion) { - val stepFilterModule = s"${BuildInfo.scala3StepFilterName}_${scalaVersion.binaryVersion}" - val stepFilter = Artifact(BuildInfo.organization, stepFilterModule, BuildInfo.version) - val tastyCore = Artifact("org.scala-lang", "tasty-core_3", scalaVersion.value) + override def resolveDecoder(scalaVersion: ScalaVersion): Try[ClassLoader] = { + getOrTryUpdate(decoderCache, scalaVersion) { + val decoderModule = s"${BuildInfo.decoderName}_${scalaVersion.binaryVersion}" + val artifact = Artifact(BuildInfo.organization, decoderModule, BuildInfo.version) DependencyResolution - .resolveWithErrors(List(stepFilter, tastyCore), logger) + .resolveWithErrors(List(artifact), logger) .map(jars => toClassLoader(jars, true)) .toTry } @@ -66,5 +65,5 @@ class BloopDebugToolsResolver(logger: Logger) extends DebugToolsResolver { object BloopDebugToolsResolver { private val expressionCompilerCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty - private val stepFilterCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty + private val decoderCache: mutable.Map[ScalaVersion, ClassLoader] = mutable.Map.empty } diff --git a/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala b/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala index b7614c72de..0f4dbf92b0 100644 --- a/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala +++ b/frontend/src/main/scala/bloop/dap/BloopDebuggee.scala @@ -1,6 +1,9 @@ package bloop.dap +import java.io.Closeable + import scala.collection.mutable +import scala.concurrent.Future import ch.epfl.scala.bsp import ch.epfl.scala.bsp.ScalaMainClass @@ -15,29 +18,38 @@ import bloop.engine.Dag import bloop.engine.State import bloop.engine.tasks.RunMode import bloop.engine.tasks.Tasks +import bloop.io.AbsolutePath import bloop.task.Task import bloop.testing.DebugLoggingEventHandler import bloop.testing.TestInternals +import monix.execution.Ack import monix.execution.Scheduler +import monix.reactive.Observable abstract class BloopDebuggee( initialState: State, - ioScheduler: Scheduler, - debugeeScalaVersion: Option[String] -) extends Debuggee { - + classUpdates: Observable[Seq[String]] +)(implicit ioScheduler: Scheduler) + extends Debuggee { + protected def scalaVersionOpt: Option[String] // The version doesn't matter for project without Scala version (Java only) - val scalaVersion = ScalaVersion(debugeeScalaVersion.getOrElse("2.13.8")) + override val scalaVersion = ScalaVersion(scalaVersionOpt.getOrElse("2.13.8")) override def run(listener: DebuggeeListener): CancelableFuture[Unit] = { val debugSessionLogger = new DebuggeeLogger(listener, initialState.logger) val task = start(initialState.copy(logger = debugSessionLogger), listener) .map { status => - if (!status.isOk) throw new Exception(s"debugee failed with ${status.name}") + if (!status.isOk) throw new Exception(s"debuggee failed with ${status.name}") } - DapCancellableFuture.runAsync(task, ioScheduler) + DapCancellableFuture.runAsync(task) + } + + override def observeClassUpdates(onClassUpdate: Seq[String] => Unit): Closeable = { + val subscription = + classUpdates.subscribe(onClassUpdate.andThen(_ => Future.successful(Ack.Continue))) + () => subscription.cancel() } protected def start(state: State, listener: DebuggeeListener): Task[ExitStatus] @@ -49,11 +61,14 @@ private final class MainClassDebugAdapter( val modules: Seq[Module], val libraries: Seq[Library], val unmanagedEntries: Seq[UnmanagedEntry], + val classUpdates: Observable[Seq[String]], env: JdkConfig, initialState: State, - ioScheduler: Scheduler, - scalaVersion: Option[String] -) extends BloopDebuggee(initialState, ioScheduler, scalaVersion) { + ioScheduler: Scheduler +) extends BloopDebuggee(initialState, classUpdates)(ioScheduler) { + + protected def scalaVersionOpt: Option[String] = project.scalaInstance.map(_.version) + val javaRuntime: Option[JavaRuntime] = JavaRuntime(env.javaHome.underlying) def name: String = s"${getClass.getSimpleName}(${project.name}, ${mainClass.className})" def start(state: State, listener: DebuggeeListener): Task[ExitStatus] = { @@ -83,10 +98,14 @@ private final class TestSuiteDebugAdapter( val libraries: Seq[Library], val unmanagedEntries: Seq[UnmanagedEntry], val javaRuntime: Option[JavaRuntime], + val classUpdates: Observable[Seq[String]], initialState: State, - ioScheduler: Scheduler, - val debugeeScalaVersion: Option[String] -) extends BloopDebuggee(initialState, ioScheduler, debugeeScalaVersion) { + ioScheduler: Scheduler +) extends BloopDebuggee(initialState, classUpdates)(ioScheduler) { + + protected def scalaVersionOpt: Option[String] = + projects.headOption.flatMap(_.scalaInstance.map(_.version)) + override def name: String = { val projectsStr = projects.map(_.bspUri).mkString("[", ", ", "]") val selectedTests = testClasses.suites @@ -116,14 +135,19 @@ private final class TestSuiteDebugAdapter( } private final class AttachRemoteDebugAdapter( + projects: Seq[Project], val modules: Seq[Module], val libraries: Seq[Library], val unmanagedEntries: Seq[UnmanagedEntry], val javaRuntime: Option[JavaRuntime], + val classUpdates: Observable[Seq[String]], initialState: State, - ioScheduler: Scheduler, - val debugeeScalaVersion: Option[String] -) extends BloopDebuggee(initialState, ioScheduler, debugeeScalaVersion) { + ioScheduler: Scheduler +) extends BloopDebuggee(initialState, classUpdates)(ioScheduler) { + + protected def scalaVersionOpt: Option[String] = + projects.headOption.flatMap(_.scalaInstance.map(_.version)) + override def name: String = s"${getClass.getSimpleName}(${initialState.build.origin})" override def start(state: State, listener: DebuggeeListener): Task[ExitStatus] = Task( ExitStatus.Ok @@ -131,7 +155,6 @@ private final class AttachRemoteDebugAdapter( } object BloopDebuggeeRunner { - def forMainClass( projects: Seq[Project], mainClass: ScalaMainClass, @@ -143,11 +166,8 @@ object BloopDebuggeeRunner { case Seq(project) => project.platform match { case jvm: Platform.Jvm => - val dag = state.build.getDagFor(project) - val modules = getModules(dag, state.client) - val libraries = getLibraries(dag) - val unmanagedEntries = - getUnmanagedEntries(project, dag, state.client, modules ++ libraries) + val (modules, libraries, unmanagedEntries, classUpdates) = + getEntriesAndClassUpdates(project, state) Right( new MainClassDebugAdapter( project, @@ -155,10 +175,10 @@ object BloopDebuggeeRunner { modules, libraries, unmanagedEntries, - jvm.config, + classUpdates, + jvm.runtimeConfig.getOrElse(jvm.config), state, - ioScheduler, - project.scalaInstance.map(_.version) + ioScheduler ) ) case platform => @@ -180,12 +200,10 @@ object BloopDebuggeeRunner { s"No projects specified for the test suites: [${testClasses.suites.map(_.className).sorted}]" ) case Seq(project) if project.platform.isInstanceOf[Platform.Jvm] => - val dag = state.build.getDagFor(project) - val modules = getModules(dag, state.client) - val libraries = getLibraries(dag) - val unmanagedEntries = getUnmanagedEntries(project, dag, state.client, modules ++ libraries) - val Platform.Jvm(config, _, _, _, _, _) = project.platform - val javaRuntime = JavaRuntime(config.javaHome.underlying) + val (modules, libraries, unmanagedEntries, classUpdates) = + getEntriesAndClassUpdates(project, state) + val Platform.Jvm(config, _, _, runtimeConfig, _, _) = project.platform + val javaRuntime = JavaRuntime(runtimeConfig.getOrElse(config).javaHome.underlying) Right( new TestSuiteDebugAdapter( projects, @@ -194,13 +212,13 @@ object BloopDebuggeeRunner { libraries, unmanagedEntries, javaRuntime, + classUpdates, state, - ioScheduler, - project.scalaInstance.map(_.version) + ioScheduler ) ) - case project :: _ => + case _ => Right( new TestSuiteDebugAdapter( projects, @@ -209,9 +227,9 @@ object BloopDebuggeeRunner { Seq.empty, Seq.empty, None, + Observable.empty, state, - ioScheduler, - project.scalaInstance.map(_.version) + ioScheduler ) ) @@ -219,43 +237,57 @@ object BloopDebuggeeRunner { } def forAttachRemote( + projects: Seq[Project], state: State, - ioScheduler: Scheduler, - projects: Seq[Project] + ioScheduler: Scheduler ): Debuggee = { projects match { case Seq(project) if project.platform.isInstanceOf[Platform.Jvm] => - val dag = state.build.getDagFor(project) - val libraries = getLibraries(dag) - val modules = getModules(dag, state.client) - val unmanagedEntries = getUnmanagedEntries(project, dag, state.client, modules ++ libraries) - val Platform.Jvm(config, _, _, _, _, _) = project.platform - val javaRuntime = JavaRuntime(config.javaHome.underlying) + val (modules, libraries, unmanagedEntries, classUpdates) = + getEntriesAndClassUpdates(project, state) + val Platform.Jvm(config, _, _, runtimeConfig, _, _) = project.platform + val javaRuntime = JavaRuntime(runtimeConfig.getOrElse(config).javaHome.underlying) new AttachRemoteDebugAdapter( + Seq(project), modules, libraries, unmanagedEntries, javaRuntime, + classUpdates, state, - ioScheduler, - project.scalaInstance.map(_.version) + ioScheduler ) case projects => new AttachRemoteDebugAdapter( + projects, Seq.empty, Seq.empty, Seq.empty, None, + Observable.empty, state, - ioScheduler, - projects.headOption.flatMap(_.scalaInstance).map(_.version) + ioScheduler ) } } - private def getLibraries(dag: Dag[Project]): Seq[Library] = { - Dag - .dfs(dag, mode = Dag.PreOrder) + private def getEntriesAndClassUpdates( + project: Project, + state: State + ): (Seq[Module], Seq[Library], Seq[UnmanagedEntry], Observable[Seq[String]]) = { + val dag = state.build.getDagFor(project) + val projects = Dag.dfs(dag, mode = Dag.PreOrder) + val modules = getModules(projects, state.client) + val libraries = getLibraries(projects) + val fullClasspath = project.fullClasspath(dag, state.client) + val unmanagedEntries = getUnmanagedEntries(fullClasspath, modules ++ libraries) + val allClassUpdates = projects.map(state.client.getClassesObserverFor(_).observable) + val mergedClassUpdates = Observable.fromIterable(allClassUpdates).merge + (modules, libraries, unmanagedEntries, mergedClassUpdates) + } + + private def getLibraries(projects: Seq[Project]): Seq[Library] = { + projects .flatMap(_.resolution) .flatMap(_.modules) .distinct @@ -272,20 +304,16 @@ object BloopDebuggeeRunner { } private def getUnmanagedEntries( - project: Project, - dag: Dag[Project], - client: ClientInfo, + fullClasspath: Seq[AbsolutePath], managedEntries: Seq[ManagedEntry] ): Seq[UnmanagedEntry] = { val managedPaths = managedEntries.map(_.absolutePath).toSet - val fullClasspath = project.fullClasspath(dag, client).map(_.underlying).toSeq fullClasspath - .filter(p => !managedPaths.contains(p)) - .map(UnmanagedEntry.apply) + .collect { case p if !managedPaths.contains(p.underlying) => UnmanagedEntry(p.underlying) } } - private def getModules(dag: Dag[Project], client: ClientInfo): Seq[Module] = { - Dag.dfs(dag, mode = Dag.PreOrder).map { project => + private def getModules(projects: Seq[Project], client: ClientInfo): Seq[Module] = { + projects.map { project => val sourceBuffer = mutable.Buffer.empty[SourceEntry] for (sourcePath <- project.sources) { if (sourcePath.isDirectory) { diff --git a/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala b/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala index 70e0acc643..70ed8e7e1d 100644 --- a/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala +++ b/frontend/src/main/scala/bloop/dap/DapCancellableFuture.scala @@ -4,6 +4,8 @@ import scala.concurrent.Future import scala.concurrent.Promise import scala.util.Success +import ch.epfl.scala.debugadapter.CancelableFuture + import bloop.task.Task import monix.execution.Cancelable @@ -16,10 +18,7 @@ private class DapCancellableFuture(future: Future[Unit], cancelable: Cancelable) } object DapCancellableFuture { - def runAsync( - task: Task[Unit], - ioScheduler: Scheduler - ): ch.epfl.scala.debugadapter.CancelableFuture[Unit] = { + def runAsync(task: Task[Unit])(implicit ioScheduler: Scheduler): CancelableFuture[Unit] = { val promise = Promise[Unit]() val cancelable = task .doOnFinish { diff --git a/frontend/src/main/scala/bloop/data/ClientInfo.scala b/frontend/src/main/scala/bloop/data/ClientInfo.scala index 53de37989e..478985dc6f 100644 --- a/frontend/src/main/scala/bloop/data/ClientInfo.scala +++ b/frontend/src/main/scala/bloop/data/ClientInfo.scala @@ -15,6 +15,7 @@ import bloop.io.AbsolutePath import bloop.io.Filenames import bloop.io.Paths import bloop.util.UUIDUtil +import bloop.ClientClassesObserver sealed trait ClientInfo { @@ -40,6 +41,18 @@ sealed trait ClientInfo { */ def getUniqueClassesDirFor(project: Project, forceGeneration: Boolean): AbsolutePath + /** + * Provides the classes observer for a given project. One can subscribe to it + * to get notified when some classes change or get created. + * It is used by DAP to hot reload classes in the debuggee process. + */ + private val classesObserver = new ConcurrentHashMap[Project, ClientClassesObserver]() + def getClassesObserverFor(project: Project): ClientClassesObserver = + classesObserver.computeIfAbsent( + project, + project => new ClientClassesObserver(getUniqueClassesDirFor(project, true)) + ) + /** * Tells the caller whether this client manages its own client classes * directories or whether bloop should take care of any created resources. diff --git a/frontend/src/main/scala/bloop/data/Project.scala b/frontend/src/main/scala/bloop/data/Project.scala index fb91f04654..cabb75d403 100644 --- a/frontend/src/main/scala/bloop/data/Project.scala +++ b/frontend/src/main/scala/bloop/data/Project.scala @@ -85,7 +85,7 @@ final case class Project( case _ => None } - customWorkingDirectory.getOrElse(baseDirectory) + customWorkingDirectory.orElse(workspaceDirectory).getOrElse(baseDirectory) } def allGeneratorInputs: Task[List[AbsolutePath]] = diff --git a/frontend/src/main/scala/bloop/engine/Interpreter.scala b/frontend/src/main/scala/bloop/engine/Interpreter.scala index d48ea75239..c4a32e8e18 100644 --- a/frontend/src/main/scala/bloop/engine/Interpreter.scala +++ b/frontend/src/main/scala/bloop/engine/Interpreter.scala @@ -139,7 +139,7 @@ object Interpreter { } if (!bloop.util.CrossPlatform.isWindows) - state.logger.info("\u001b[H\u001b[2J") + state.logger.info(bloop.util.Console.clearCommand) // Force the first execution before relying on the file watching task fg(state).flatMap(newState => watcher.watch(newState, fg)) @@ -521,8 +521,8 @@ object Interpreter { LinkTask.linkMainWithNative(cmd, project, state, mainClass, target, platform) case platform @ Platform.Js(config, _, _) => - val target = ScalaJsToolchain.linkTargetFrom(project, config) - LinkTask.linkMainWithJs(cmd, project, state, mainClass, target, platform) + val targetDirectory = ScalaJsToolchain.linkTargetFrom(project, config) + LinkTask.linkMainWithJs(cmd, project, state, mainClass, targetDirectory, platform) case _: Platform.Jvm => val msg = Feedback.noLinkFor(project) @@ -565,14 +565,16 @@ object Interpreter { else Tasks.runNativeOrJs(state, cwd, args) } case platform @ Platform.Js(config, _, _) => - val target = ScalaJsToolchain.linkTargetFrom(project, config) - LinkTask.linkMainWithJs(cmd, project, state, mainClass, target, platform).flatMap { - state => + val targetDirectory = ScalaJsToolchain.linkTargetFrom(project, config) + LinkTask + .linkMainWithJs(cmd, project, state, mainClass, targetDirectory, platform) + .flatMap { state => // We use node to run the program (is this a special case?) - val args = ("node" +: target.syntax +: cmd.args).toArray + val files = targetDirectory.list.map(_.toString()) + val args = ("node" +: files ::: cmd.args).toArray if (!state.status.isOk) Task.now(state) else Tasks.runNativeOrJs(state, cwd, args) - } + } case jvm: Platform.Jvm => val javaEnv = project.runtimeJdkConfig.getOrElse(jvm.config) Tasks.runJVM( diff --git a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala index 4a198343d1..19e20f4096 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala @@ -22,7 +22,6 @@ import bloop.engine.tasks.compilation._ import bloop.io.ParallelOps import bloop.io.ParallelOps.CopyMode import bloop.io.{Paths => BloopPaths} -import bloop.logging.BloopLogger import bloop.logging.DebugFilter import bloop.logging.Logger import bloop.logging.LoggerAction @@ -37,6 +36,7 @@ import bloop.tracing.BraveTracer import monix.execution.CancelableFuture import monix.reactive.MulticastStrategy import monix.reactive.Observable + object CompileTask { private implicit val logContext: DebugFilter = DebugFilter.Compilation def compile[UseSiteLogger <: Logger]( @@ -98,7 +98,6 @@ object CompileTask { compileProjectTracer.terminate() Task.now(earlyResultBundle) case Right(CompileSourcesAndInstance(sources, instance, _)) => - val externalUserClassesDir = bundle.clientClassesDir val readOnlyClassesDir = lastSuccessful.classesDir val newClassesDir = compileOut.internalNewClassesDir val classpath = bundle.dependenciesData.buildFullCompileClasspathFor( @@ -173,7 +172,7 @@ object CompileTask { val postCompilationTasks = backgroundTasks .trigger( - externalUserClassesDir, + bundle.clientClassesObserver, reporter.underlying, compileProjectTracer, logger @@ -247,14 +246,14 @@ object CompileTask { val o = state.commonOptions val cancel = cancelCompilation val logger = ObservedLogger(rawLogger, observer) - val dir = state.client.getUniqueClassesDirFor(inputs.project, forceGeneration = true) + val clientClassesObserver = state.client.getClassesObserverFor(inputs.project) val underlying = createReporter(ReporterInputs(inputs.project, cwd, rawLogger)) val reporter = new ObservedReporter(logger, underlying) val sourceGeneratorCache = state.sourceGeneratorCache CompileBundle.computeFrom( inputs, sourceGeneratorCache, - dir, + clientClassesObserver, reporter, last, prev, @@ -292,7 +291,7 @@ object CompileTask { } else { results.foreach { case FinalNormalCompileResult.HasException(project, err) => - val errMsg = err.fold(identity, BloopLogger.prettyPrintException) + val errMsg = err.fold(identity, Logger.prettyPrintException) rawLogger.error(s"Unexpected error when compiling ${project.name}: $errMsg") case _ => () // Do nothing when the final compilation result is not an actual error } diff --git a/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala b/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala index c6b379dcf6..4fe6c4ba3b 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/LinkTask.scala @@ -19,7 +19,7 @@ object LinkTask { project: Project, state: State, mainClass: String, - target: AbsolutePath, + targetDirectory: AbsolutePath, platform: Platform.Js ): Task[State] = { import state.logger @@ -36,10 +36,19 @@ object LinkTask { // Pass in the default scheduler used by this task to the linker Task.deferAction { s => toolchain - .link(config, project, fullClasspath, true, Some(mainClass), target, s, logger) + .link( + config, + project, + fullClasspath, + true, + Some(mainClass), + targetDirectory, + s, + logger + ) .map { case scala.util.Success(_) => - state.withInfo(s"Generated JavaScript file '${target.syntax}'") + state.withInfo(s"Generated JavaScript file '${targetDirectory.syntax}'") case scala.util.Failure(t) => val msg = Feedback.failedToLink(project, ScalaJsToolchain.name, t) state.withError(msg, ExitStatus.LinkingError).withTrace(t) diff --git a/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala b/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala index b1df2daf6a..72a181fc77 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/Tasks.scala @@ -73,7 +73,7 @@ object Tasks { val loader = ClasspathUtil.makeLoader(pathEntries, instance) val compiler = state.compilerCache - .get(instance, javacBin, project.javacOptions) + .get(instance, project.classpathOptions, javacBin, project.javacOptions) .scalac .asInstanceOf[AnalyzingCompiler] val options = project.scalacOptions :+ "-Xnojline" diff --git a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala index f741483000..89e695c540 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileBundle.scala @@ -23,6 +23,7 @@ import bloop.tracing.BraveTracer import monix.reactive.Observable import sbt.internal.inc.PlainVirtualFileConverter +import bloop.ClientClassesObserver sealed trait CompileBundle @@ -75,7 +76,7 @@ case object CancelledCompileBundle extends CompileBundle */ final case class SuccessfulCompileBundle( project: Project, - clientClassesDir: AbsolutePath, + clientClassesObserver: ClientClassesObserver, dependenciesData: CompileDependenciesData, javaSources: List[AbsolutePath], scalaSources: List[AbsolutePath], @@ -95,7 +96,7 @@ final case class SuccessfulCompileBundle( project.out, project.analysisOut, project.genericClassesDir, - clientClassesDir, + clientClassesObserver.classesDir, readOnlyClassesDir ) } @@ -152,7 +153,7 @@ object CompileBundle { def computeFrom( inputs: CompileDefinitions.BundleInputs, sourceGeneratorCache: SourceGeneratorCache, - clientExternalClassesDir: AbsolutePath, + clientClassesObserver: ClientClassesObserver, reporter: ObservedReporter, lastSuccessful: LastSuccessfulResult, lastResult: Compiler.Result, @@ -228,7 +229,7 @@ object CompileBundle { new SuccessfulCompileBundle( project, - clientExternalClassesDir, + clientClassesObserver, compileDependenciesData, javaSources, scalaSources, diff --git a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala index 1ad3840fda..f596ce335f 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/compilation/CompileGraph.scala @@ -166,8 +166,7 @@ object CompileGraph { val previousProblems = Compiler.previousProblemsFromResult(bundle.latestResult, previousSuccessfulProblems) - val externalClassesDir = - client.getUniqueClassesDirFor(bundle.project, forceGeneration = true) + val clientClassesObserver = client.getClassesObserverFor(bundle.project) // Replay events asynchronously to waiting for the compilation result import scala.concurrent.duration.FiniteDuration @@ -210,7 +209,7 @@ object CompileGraph { reporter.processEndCompilation( previousSuccessfulProblems, a.code, - Some(externalClassesDir), + Some(clientClassesObserver.classesDir), Some(bundle.out.analysisOut) ) } @@ -263,7 +262,7 @@ object CompileGraph { case s: Compiler.Result.Success => // Wait on new classes to be populated for correctness val runningBackgroundTasks = s.backgroundTasks - .trigger(externalClassesDir, reporter, bundle.tracer, logger) + .trigger(clientClassesObserver, reporter, bundle.tracer, logger) .runAsync(ExecutionContext.ioScheduler) Task.now(results.copy(runningBackgroundTasks = runningBackgroundTasks)) case _: Compiler.Result.Cancelled => diff --git a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala index 3e95405bd6..d6191d7036 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaJsToolchain.scala @@ -119,8 +119,8 @@ object ScalaJsToolchain extends ToolchainCompanion[ScalaJsToolchain] { def linkTargetFrom(project: Project, config: JsConfig): AbsolutePath = { config.output match { - case Some(p) => AbsolutePath(p) - case None => project.out.resolve(s"${project.name}.js") + case Some(p) if p.toFile().isDirectory() => AbsolutePath(p) + case None => project.out.resolve(s"${project.name}-js").createDirectories } } diff --git a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala index aa5faeb6e2..1b51af487c 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala @@ -9,10 +9,12 @@ import bloop.DependencyResolution import bloop.config.Config import bloop.config.Config.NativeConfig import bloop.data.Project +import bloop.engine.ExecutionContext import bloop.internal.build.BuildInfo import bloop.io.AbsolutePath import bloop.logging.Logger import bloop.task.Task +import scala.concurrent.Future final class ScalaNativeToolchain private (classLoader: ClassLoader) { @@ -35,7 +37,10 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) { logger: Logger ): Task[Try[Unit]] = { val bridgeClazz = classLoader.loadClass("bloop.scalanative.NativeBridge") - val nativeLinkMeth = bridgeClazz.getMethod("nativeLink", paramTypes: _*) + val isNative05 = config.version.startsWith("0.5") + val nativeLinkMeth = + if (isNative05) bridgeClazz.getMethod("nativeLink", paramTypes05: _*) + else bridgeClazz.getMethod("nativeLink", paramTypes04: _*) // Scala Native 0.4.{0,1,2} expect to receive the companion object class' name val fullEntry = config.version match { @@ -44,15 +49,32 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) { case _ => mainClass.stripSuffix("$") } - val linkage = Task { - nativeLinkMeth - .invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger) - .asInstanceOf[Unit] - }.materialize - + val linkage = if (isNative05) { + Task.fromFuture { + nativeLinkMeth + .invoke( + null, + config, + project, + fullClasspath, + fullEntry, + target.underlying, + logger, + ExecutionContext.ioScheduler + ) + .asInstanceOf[Future[Unit]] + }.materialize + } else { + Task { + nativeLinkMeth + .invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger) + .asInstanceOf[Unit] + }.materialize + } linkage.map { case s @ scala.util.Success(_) => s case f @ scala.util.Failure(t) => + t.printStackTrace() t match { case it: InvocationTargetException => scala.util.Failure(it.getCause) case _ => f @@ -60,9 +82,12 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) { } } - // format: OFF - private val paramTypes = classOf[NativeConfig] :: classOf[Project] :: classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil - // format: ON + private val paramTypes04 = classOf[NativeConfig] :: classOf[Project] :: + classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil + + private val paramTypes05 = classOf[NativeConfig] :: classOf[Project] :: + classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: + classOf[scala.concurrent.ExecutionContext] :: Nil } object ScalaNativeToolchain extends ToolchainCompanion[ScalaNativeToolchain] { @@ -73,7 +98,8 @@ object ScalaNativeToolchain extends ToolchainCompanion[ScalaNativeToolchain] { override def artifactNameFrom(version: String): String = { if (version.length == 3) sys.error("The full Scala Native version must be provided") else if (version.startsWith("0.4")) BuildInfo.nativeBridge04 - else sys.error(s"Expected compatible Scala Native version [0.3, 0.4], $version given") + else if (version.startsWith("0.5")) BuildInfo.nativeBridge05 + else sys.error(s"Expected compatible Scala Native version [0.4, 0.5], $version given") } override def getPlatformData(platform: Platform): Option[PlatformData] = { diff --git a/frontend/src/main/scala/bloop/io/SourceWatcher.scala b/frontend/src/main/scala/bloop/io/SourceWatcher.scala index 3838376d42..eeb46094d8 100644 --- a/frontend/src/main/scala/bloop/io/SourceWatcher.scala +++ b/frontend/src/main/scala/bloop/io/SourceWatcher.scala @@ -42,7 +42,7 @@ final class SourceWatcher private ( def runAction(state: State, events: Seq[DirectoryChangeEvent]): Task[State] = { // Windows is not supported for now if (!bloop.util.CrossPlatform.isWindows) - logger.info("\u001b[H\u001b[2J") // Clean terminal before acting on the event action + logger.info(bloop.util.Console.clearCommand) events.foreach(e => logger.debug(s"A ${e.eventType()} in ${e.path()} has triggered an event")) action(state) } diff --git a/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala b/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala index 60fd679297..52544ae32d 100644 --- a/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala +++ b/frontend/src/main/scala/bloop/reporter/BspProjectReporter.scala @@ -295,13 +295,7 @@ final class BspProjectReporter( ): Unit = { val problemsInPreviousAnalysisPerFile = Reporter.groupProblemsByFile(previousSuccessfulProblems) - def mockNoOpCompileEventsAndEnd: CompilationEvent.EndCompilation = { - // When no-op, we keep reporting the start and the end of compilation for consistency - val startMsg = s"Start no-op compilation for ${project.name}" - logger.publishCompilationStart( - CompilationEvent.StartCompilation(project.name, project.bspUri, startMsg, taskId) - ) - + def recheckProblems = { recentlyReportProblemsPerFile.foreach { case (sourceFile, problemsPerFile) if reportAllPreviousProblems => reportAllProblems(sourceFile, problemsPerFile) @@ -326,30 +320,17 @@ final class BspProjectReporter( logger.noDiagnostic(CompilationEvent.NoDiagnostic(project.bspUri, sourceFile)) } } - - val liftedProblems = allProblems.toIterator.map(super.liftFatalWarning(_)).toList - CompilationEvent.EndCompilation( - project.name, - project.bspUri, - taskId, - liftedProblems, - code, - isNoOp = true, - isLastCycle = true, - clientClassesDir, - clientAnalysisOut - ) } - endEvent = Some( - if (cycleCount.get == 0) mockNoOpCompileEventsAndEnd - else { - // Great, let's report the pending end incremental cycle as the last one - val inputs = - CycleInputs(true, problemsInPreviousAnalysisPerFile, clientClassesDir, clientAnalysisOut) - processEndPreviousCycle(inputs, Some(code)) - } - ) + endEvent = if (cycleCount.get == 0) { + recheckProblems + None + } else { + // Great, let's report the pending end incremental cycle as the last one + val inputs = + CycleInputs(true, problemsInPreviousAnalysisPerFile, clientClassesDir, clientAnalysisOut) + Some(processEndPreviousCycle(inputs, Some(code))) + } // Clear the state of files with problems at the end of compilation clearedFilesForClient.clear() diff --git a/frontend/src/main/scala/bloop/testing/TestInternals.scala b/frontend/src/main/scala/bloop/testing/TestInternals.scala index 9637a519eb..8d6b26a20b 100644 --- a/frontend/src/main/scala/bloop/testing/TestInternals.scala +++ b/frontend/src/main/scala/bloop/testing/TestInternals.scala @@ -340,7 +340,7 @@ object TestInternals { } catch { case _: ClassNotFoundException => None case NonFatal(t) => - logger.report(s"Initialisation of test framework $fqn failed", t) + logger.error(s"Initialisation of test framework $fqn failed", t) None } } diff --git a/frontend/src/main/scala/bloop/util/Console.scala b/frontend/src/main/scala/bloop/util/Console.scala new file mode 100644 index 0000000000..55438fd1ea --- /dev/null +++ b/frontend/src/main/scala/bloop/util/Console.scala @@ -0,0 +1,10 @@ +package bloop.util + +object Console { + private val escape = "\u001b[" + private val cursorHome = "H" + private val eraseScreen = "2J" + private val eraseScrollbar = "3J" + + val clearCommand: String = escape + cursorHome + escape + eraseScreen + escape + eraseScrollbar +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt index d6e44edf90..491b1868f7 100644 --- a/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt @@ -6,7 +6,7 @@ lazy val `test-project` = .withoutSuffixFor(JVMPlatform) .settings( name := "test-project", - scalaVersion := "2.13.4", + scalaVersion := "2.13.13", mainClass in (Compile, run) := Some("hello.App") ) diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt index c3d5191808..f674586ddd 100644 --- a/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.3") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") val pluginVersion = sys.props.getOrElse( "bloopVersion", diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/build.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.5/build.sbt new file mode 100644 index 0000000000..491b1868f7 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/build.sbt @@ -0,0 +1,20 @@ +bloopExportJarClassifiers in Global := Some(Set("sources")) +bloopConfigDir in Global := baseDirectory.value / "bloop-config" + +lazy val `test-project` = + crossProject(NativePlatform, JVMPlatform) + .withoutSuffixFor(JVMPlatform) + .settings( + name := "test-project", + scalaVersion := "2.13.13", + mainClass in (Compile, run) := Some("hello.App") + ) + +lazy val `test-project-native` = `test-project`.native.settings( + // Should override default set above. Tested as part of ScalaNativeToolchainSpec. + (Compile / run / bloopMainClass) := Some("hello.DefaultApp") +) + +lazy val `test-project-jvm` = `test-project`.jvm.settings( + (Compile / run / bloopMainClass) := Some("hello.App") +) diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/build.properties b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/build.properties new file mode 100644 index 0000000000..b089b60c7a --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/build.properties @@ -0,0 +1,2 @@ +sbt.version=1.9.9 + diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/plugins.sbt b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/plugins.sbt new file mode 100644 index 0000000000..cf1c2ba4a5 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/project/plugins.sbt @@ -0,0 +1,11 @@ +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.0-RC2") + +val pluginVersion = sys.props.getOrElse( + "bloopVersion", + throw new RuntimeException("Unable to find -DbloopVersion") +) + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % pluginVersion) + +updateOptions := updateOptions.value.withLatestSnapshots(false) diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/native/src/main/scala/hello/DefaultApp.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/native/src/main/scala/hello/DefaultApp.scala new file mode 100644 index 0000000000..f29441fdf8 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/native/src/main/scala/hello/DefaultApp.scala @@ -0,0 +1,10 @@ +package hello + +import scalanative.unsafe._ +import scalanative.libc.stdio + +object DefaultApp { + def main(args: Array[String]): Unit = Zone { implicit z => + stdio.vprintf(c"Hello, world from DefaultApp!\n", toCVarArgList()) + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/App.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/App.scala new file mode 100644 index 0000000000..344dc7e549 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/App.scala @@ -0,0 +1,7 @@ +package hello + +object App { + def main(args: Array[String]): Unit = { + println(Hello.greet("world")) + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Environment.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Environment.scala new file mode 100644 index 0000000000..9d92c70ec3 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Environment.scala @@ -0,0 +1,7 @@ +package hello + +object Environment { + def requireEnvironmentVariable(): Unit = { + sys.env.get("BLOOP_OWNER").getOrElse(sys.error("Missing BLOOP_OWNER env variable!")) + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Hello.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Hello.scala new file mode 100644 index 0000000000..45671be900 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/main/scala/hello/Hello.scala @@ -0,0 +1,9 @@ +package hello + +object Hello { + def greet(name: String): String = { + // We do this to check that environment variables work + Environment.requireEnvironmentVariable() + s"Hello, $name!" + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/EternalUTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/EternalUTest.scala new file mode 100644 index 0000000000..e60441b7fc --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/EternalUTest.scala @@ -0,0 +1,11 @@ +package hello + +import utest._ + +object EternalUTest extends TestSuite { + val tests = Tests { + "This test never ends" - { + while (true) () + } + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/JUnitTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/JUnitTest.scala new file mode 100644 index 0000000000..6c3325dae9 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/JUnitTest.scala @@ -0,0 +1,11 @@ +package hello + +import org.junit.Test +import org.junit.Assert.assertEquals + +class JUnitTest { + @Test + def myTest: Unit = { + assertEquals(4, 2 + 2) + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaCheckTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaCheckTest.scala new file mode 100644 index 0000000000..0a52e66501 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaCheckTest.scala @@ -0,0 +1,11 @@ +package hello + +import org.scalacheck.Properties +import org.scalacheck.Prop.forAll + +object ScalaCheckTest extends Properties("Greeting") { + + property("is personal") = forAll { (name: String) => + Hello.greet(name) == s"Hello, $name!" + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaTestTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaTestTest.scala new file mode 100644 index 0000000000..c9b6a92ea1 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/ScalaTestTest.scala @@ -0,0 +1,9 @@ +package hello + +import org.scalatest._ + +class ScalaTestTest extends FlatSpec with Matchers { + "A greeting" should "be very personal" in { + Hello.greet("Martin") shouldBe "Hello, Martin!" + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/Specs2Test.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/Specs2Test.scala new file mode 100644 index 0000000000..373220d148 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/Specs2Test.scala @@ -0,0 +1,17 @@ +package hello + +import org.specs2._ + +class Specs2Test extends Specification { + def is = s2""" + + This is a specification to check the `Hello` object. + + A greeting + is very personal $isPersonal + """ + + def isPersonal = { + Hello.greet("Martin") must beEqualTo("Hello, Martin!") + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/UTestTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/UTestTest.scala new file mode 100644 index 0000000000..6022e8847b --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/UTestTest.scala @@ -0,0 +1,15 @@ +package hello + +import utest._ + +object UTestTest extends TestSuite { + import utest.framework.Formatter + override def utestFormatter: Formatter = new Formatter { + override def formatColor = false + } + val tests = Tests { + "Greetings are very personal" - { + assert(Hello.greet("Martin") == "Hello, Martin!") + } + } +} diff --git a/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/WritingTest.scala b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/WritingTest.scala new file mode 100644 index 0000000000..4868ea6968 --- /dev/null +++ b/frontend/src/test/resources/cross-test-build-scala-native-0.5/test-project/shared/src/test/scala/hello/WritingTest.scala @@ -0,0 +1,9 @@ +package hello + +import org.scalatest._ + +class WritingTest extends FlatSpec with Matchers { + "A test" should "be able to print stuff" in { + (1 to 10).foreach(_ => println("message")) + } +} diff --git a/frontend/src/test/resources/scala-java-processorpath/build.sbt b/frontend/src/test/resources/scala-java-processorpath/build.sbt index 6e19092d72..6461a454a1 100644 --- a/frontend/src/test/resources/scala-java-processorpath/build.sbt +++ b/frontend/src/test/resources/scala-java-processorpath/build.sbt @@ -1,6 +1,6 @@ bloopConfigDir in Global := baseDirectory.value / "bloop-config" -ThisBuild / scalaVersion := "2.12.18" +ThisBuild / scalaVersion := "2.12.19" ThisBuild / version := "0.1.0-SNAPSHOT" ThisBuild / organization := "com.example" ThisBuild / organizationName := "example" diff --git a/frontend/src/test/scala/bloop/BuildLoaderSpec.scala b/frontend/src/test/scala/bloop/BuildLoaderSpec.scala index 14528e906b..b8b0f68ddb 100644 --- a/frontend/src/test/scala/bloop/BuildLoaderSpec.scala +++ b/frontend/src/test/scala/bloop/BuildLoaderSpec.scala @@ -9,6 +9,7 @@ import bloop.data.TraceSettings import bloop.data.WorkspaceSettings import bloop.engine.Build import bloop.internal.build.BuildInfo +import bloop.internal.build.BuildTestInfo import bloop.io.AbsolutePath import bloop.logging.RecordingLogger import bloop.task.Task @@ -17,7 +18,7 @@ import bloop.tracing.TraceProperties import bloop.util.TestUtil object BuildLoaderSpec extends BaseSuite { - val semanticdbVersion = "4.8.4" + val semanticdbVersion = "4.8.15" val oldSemanticdbVersion = "4.5.13" testLoad("don't reload if nothing changes") { (testBuild, logger) => diff --git a/frontend/src/test/scala/bloop/DeduplicationSpec.scala b/frontend/src/test/scala/bloop/DeduplicationSpec.scala index bed80eca33..ff76849202 100644 --- a/frontend/src/test/scala/bloop/DeduplicationSpec.scala +++ b/frontend/src/test/scala/bloop/DeduplicationSpec.scala @@ -109,10 +109,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { assertNoDiff( firstCompiledState.lastDiagnostics(build.userProject), - """#1: task start 2 + """#1: task start 1 | -> Msg: Compiling user (2 Scala sources) | -> Data kind: compile-task - |#1: task finish 2 + |#1: task finish 1 | -> errors 0, warnings 0 | -> Msg: Compiled 'user' | -> Data kind: compile-report @@ -151,14 +151,7 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { assertNoDiff( firstCompiledState.lastDiagnostics(build.userProject), - """#2: task start 4 - | -> Msg: Start no-op compilation for user - | -> Data kind: compile-task - |#2: task finish 4 - | -> errors 0, warnings 0 - | -> Msg: Compiled 'user' - | -> Data kind: compile-report - """.stripMargin + "" // expect None here since it's a no-op which turns into "" ) // Same check as before because no-op should not show any more input @@ -242,10 +235,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { assertNoDiff( firstCompiledState.lastDiagnostics(build.userProject), - """#1: task start 2 + """#1: task start 1 | -> Msg: Compiling user (2 Scala sources) | -> Data kind: compile-task - |#1: task finish 2 + |#1: task finish 1 | -> errors 0, warnings 0 | -> Msg: Compiled 'user' | -> Data kind: compile-report @@ -335,10 +328,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { assertNoDiff( secondCompiledState.lastDiagnostics(build.userProject), - """#2: task start 4 + """#2: task start 2 | -> Msg: Compiling user (1 Scala source) | -> Data kind: compile-task - |#2: task finish 4 + |#2: task finish 2 | -> errors 0, warnings 0 | -> Msg: Compiled 'user' | -> Data kind: compile-report @@ -426,10 +419,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { // First compilation should not report warning assertNoDiff( firstCompiledState.lastDiagnostics(build.userProject), - """#1: task start 2 + """#1: task start 1 | -> Msg: Compiling user (2 Scala sources) | -> Data kind: compile-task - |#1: task finish 2 + |#1: task finish 1 | -> errors 0, warnings 0 | -> Msg: Compiled 'user' | -> Data kind: compile-report @@ -585,13 +578,13 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { // We reproduce the same streaming side effects during compilation assertNoDiff( firstCompiledState.lastDiagnostics(`B`), - """#1: task start 2 + """#1: task start 1 | -> Msg: Compiling b (1 Scala source) | -> Data kind: compile-task |#1: b/src/B.scala | -> List(Diagnostic(Range(Position(2,28),Position(2,28)),Some(Error),Some(_),Some(_),type mismatch; found : Int required: String,None,None,Some({"actions":[]}))) | -> reset = true - |#1: task finish 2 + |#1: task finish 1 | -> errors 1, warnings 0 | -> Msg: Compiled 'b' | -> Data kind: compile-report @@ -666,13 +659,13 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { assertNoDiff( secondBspState.lastDiagnostics(`B`), - """#2: task start 4 + """#2: task start 2 | -> Msg: Compiling b (1 Scala source) | -> Data kind: compile-task |#2: b/src/B.scala | -> List(Diagnostic(Range(Position(2,28),Position(2,28)),Some(Error),Some(_),Some(_),type mismatch; found : Int required: String,None,None,Some({"actions":[]}))) | -> reset = true - |#2: task finish 4 + |#2: task finish 2 | -> errors 1, warnings 0 | -> Msg: Compiled 'b' | -> Data kind: compile-report @@ -766,13 +759,13 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { assertNoDiff( thirdBspState.lastDiagnostics(`B`), - """#3: task start 6 + """#3: task start 3 | -> Msg: Compiling b (1 Scala source) | -> Data kind: compile-task |#3: b/src/B.scala | -> List() | -> reset = true - |#3: task finish 6 + |#3: task finish 3 | -> errors 0, warnings 0 | -> Msg: Compiled 'b' | -> Data kind: compile-report @@ -923,10 +916,10 @@ object DeduplicationSpec extends bloop.bsp.BspBaseSuite { assertNoDiff( firstCompiledState.lastDiagnostics(`B`), s""" - |#1: task start 2 + |#1: task start 1 | -> Msg: Compiling b (6 Scala sources) | -> Data kind: compile-task - |#1: task finish 2 + |#1: task finish 1 | -> errors 0, warnings 0 | -> Msg: Compiled 'b' | -> Data kind: compile-report diff --git a/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala b/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala index 0afdc2b209..99023195a7 100644 --- a/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala +++ b/frontend/src/test/scala/bloop/ScalaVersionsSpec.scala @@ -77,7 +77,7 @@ object ScalaVersionsSpec extends bloop.testing.BaseSuite { "2.13.10", "3.1.3", "3.2.1", - "2.13.12-bin-86f40c2" + "2.13.13" ) val allVersions = scalaVersions diff --git a/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala b/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala index 6cc6ce0f57..544f662e50 100644 --- a/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala +++ b/frontend/src/test/scala/bloop/bsp/BspBaseSuite.scala @@ -349,6 +349,22 @@ abstract class BspBaseSuite extends BaseSuite with BspClientTest { TestUtil.await(FiniteDuration(5, "s"))(resourcesTask) } + def requestDependencyModules(project: TestProject): bsp.DependencyModulesResult = { + val dependencyModulesTask = { + client0 + .request( + endpoints.BuildTarget.dependencyModules, + bsp.DependencyModulesParams(List(project.bspId)) + ) + .map { + case RpcFailure(_, error) => fail(s"Received error ${error}") + case RpcSuccess(modules, _) => modules + } + } + + TestUtil.await(FiniteDuration(5, "s"))(dependencyModulesTask) + } + def requestDependencySources(project: TestProject): bsp.DependencySourcesResult = { val dependencySourcesTask = { client0 diff --git a/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala b/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala index 4766a785f2..31036278fc 100644 --- a/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala +++ b/frontend/src/test/scala/bloop/bsp/BspCompileSpec.scala @@ -86,13 +86,7 @@ class BspCompileSpec( assertSameExternalClassesDirs(compiledState, secondCompiledState, projects) assertNoDiff( secondCompiledState.lastDiagnostics(`A`), - """#2: task start 2 - | -> Msg: Start no-op compilation for a - | -> Data kind: compile-task - |#2: task finish 2 - | -> errors 0, warnings 0 - | -> Msg: Compiled 'a' - | -> Data kind: compile-report""".stripMargin + "" // no-op ) } } @@ -586,17 +580,10 @@ class BspCompileSpec( assertSameExternalClassesDirs(thirdCompiledState, compiledState, projects) assertNoDiff( thirdCompiledState.lastDiagnostics(`A`), - """#3: task start 3 - | -> Msg: Start no-op compilation for a - | -> Data kind: compile-task - |#3: a/src/A.scala + """#3: a/src/A.scala | -> List() | -> reset = true - |#3: task finish 3 - | -> errors 0, warnings 0 - | -> Msg: Compiled 'a' - | -> Data kind: compile-report - """.stripMargin + """.stripMargin // no-op so it only gives the compile-report ) writeFile(`A`.srcFor("/A.scala"), Sources.`A3.scala`) @@ -613,7 +600,7 @@ class BspCompileSpec( assertSameExternalClassesDirs(fourthCompiledState, compiledState, projects) assertNoDiff( fourthCompiledState.lastDiagnostics(`A`), - """#4: task start 4 + """#4: task start 3 | -> Msg: Compiling a (1 Scala source) | -> Data kind: compile-task |#4: a/src/A.scala @@ -622,7 +609,7 @@ class BspCompileSpec( |#4: a/src/A.scala | -> List(Diagnostic(Range(Position(0,0),Position(0,26)),Some(Warning),Some(_),Some(_),Unused import,None,None,Some({"actions":[]}))) | -> reset = false - |#4: task finish 4 + |#4: task finish 3 | -> errors 1, warnings 1 | -> Msg: Compiled 'a' | -> Data kind: compile-report @@ -636,13 +623,13 @@ class BspCompileSpec( assertDifferentExternalClassesDirs(fifthCompiledState, compiledState, projects) assertNoDiff( fifthCompiledState.lastDiagnostics(`A`), - """#5: task start 5 + """#5: task start 4 | -> Msg: Compiling a (1 Scala source) | -> Data kind: compile-task |#5: a/src/A.scala | -> List(Diagnostic(Range(Position(0,0),Position(0,26)),Some(Warning),Some(_),Some(_),Unused import,None,None,Some({"actions":[]}))) | -> reset = true - |#5: task finish 5 + |#5: task finish 4 | -> errors 0, warnings 1 | -> Msg: Compiled 'a' | -> Data kind: compile-report @@ -662,17 +649,17 @@ class BspCompileSpec( assertSameExternalClassesDirs(sixthCompiledState, fifthCompiledState, projects) assertNoDiff( sixthCompiledState.lastDiagnostics(`A`), - """#6: task start 6 + """#6: task start 5 | -> Msg: Compiling a (1 Scala source) | -> Data kind: compile-task |#6: a/src/A.scala | -> List() | -> reset = true - |#6: task finish 6 + |#6: task finish 5 | -> errors 0, warnings 0 | -> Msg: Compiled 'a' | -> Data kind: compile-report - |#6: task start 6 + |#6: task start 5 | -> Msg: Compiling a (1 Scala source) | -> Data kind: compile-task |#6: a/src/A.scala @@ -681,7 +668,7 @@ class BspCompileSpec( |#6: a/src/A.scala | -> List(Diagnostic(Range(Position(1,0),Position(3,1)),Some(Error),Some(_),Some(_),object creation impossible, since value y in trait Base of type Int is not defined,None,None,Some({"actions":[]}))) | -> reset = false - |#6: task finish 6 + |#6: task finish 5 | -> errors 1, warnings 1 | -> Msg: Compiled 'a' | -> Data kind: compile-report @@ -696,13 +683,13 @@ class BspCompileSpec( assertNoDiff( seventhCompiledState.lastDiagnostics(`A`), - """#7: task start 7 + """#7: task start 6 | -> Msg: Compiling a (1 Scala source) | -> Data kind: compile-task |#7: a/src/A.scala | -> List(Diagnostic(Range(Position(0,0),Position(0,26)),Some(Warning),Some(_),Some(_),Unused import,None,None,Some({"actions":[]}))) | -> reset = true - |#7: task finish 7 + |#7: task finish 6 | -> errors 0, warnings 0 | -> Msg: Compiled 'a' | -> Data kind: compile-report @@ -753,18 +740,12 @@ class BspCompileSpec( assertSameExternalClassesDirs(compiledState.toTestState, secondCliCompiledState, `A`) // BSP publishes warnings even if it's a no-op + // however it skips the task start/end assertNoDiff( compiledState.lastDiagnostics(`A`), - """#1: task start 1 - | -> Msg: Start no-op compilation for a - | -> Data kind: compile-task - |#1: a/src/main/scala/App.scala + """#1: a/src/main/scala/App.scala | -> List(Diagnostic(Range(Position(2,4),Position(2,4)),Some(Warning),Some(_),Some(_),a pure expression does nothing in statement position,None,None,Some({"actions":[]}))) - | -> reset = true - |#1: task finish 1 - | -> errors 0, warnings 0 - | -> Msg: Compiled 'a' - | -> Data kind: compile-report""".stripMargin + | -> reset = true""".stripMargin ) } } @@ -987,16 +968,9 @@ class BspCompileSpec( assertNoDiff( thirdCompiledState.lastDiagnostics(`A`), - """#3: task start 3 - | -> Msg: Start no-op compilation for a - | -> Data kind: compile-task - |#3: a/src/main/scala/Bar.scala + """#3: a/src/main/scala/Bar.scala | -> List() - | -> reset = true - |#3: task finish 3 - | -> errors 0, warnings 0 - | -> Msg: Compiled 'a' - | -> Data kind: compile-report""".stripMargin + | -> reset = true""".stripMargin ) } } @@ -1098,14 +1072,7 @@ class BspCompileSpec( assertNoDiff( compiledState.lastDiagnostics(`B`), - """|#2: task start 4 - | -> Msg: Start no-op compilation for b - | -> Data kind: compile-task - |#2: task finish 4 - | -> errors 0, warnings 0 - | -> Msg: Compiled 'b' - | -> Data kind: compile-report - |""".stripMargin + "" // no-op ) writeFile(`A`.srcFor("/Foo.scala"), Sources.`Foo3.scala`) @@ -1115,7 +1082,7 @@ class BspCompileSpec( assertValidCompilationState(thirdCompiledState, projects) assertNoDiff( thirdCompiledState.lastDiagnostics(`A`), - """|#3: task start 5 + """|#3: task start 4 | -> Msg: Compiling a (1 Scala source) | -> Data kind: compile-task |#3: a/src/Foo.scala @@ -1124,7 +1091,7 @@ class BspCompileSpec( |#3: a/src/Foo.scala | -> List(Diagnostic(Range(Position(1,0),Position(1,7)),Some(Error),Some(_),Some(_),Unused import,None,None,Some({"actions":[]}))) | -> reset = false - |#3: task finish 5 + |#3: task finish 4 | -> errors 2, warnings 0 | -> Msg: Compiled 'a' | -> Data kind: compile-report @@ -1133,13 +1100,13 @@ class BspCompileSpec( assertNoDiff( compiledState.lastDiagnostics(`B`), - """|#3: task start 6 + """|#3: task start 5 | -> Msg: Compiling b (1 Scala source) | -> Data kind: compile-task |#3: b/src/Buzz.scala | -> List(Diagnostic(Range(Position(0,0),Position(0,7)),Some(Error),Some(_),Some(_),Unused import,None,None,Some({"actions":[]}))) | -> reset = true - |#3: task finish 6 + |#3: task finish 5 | -> errors 1, warnings 0 | -> Msg: Compiled 'b' | -> Data kind: compile-report diff --git a/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala b/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala index d1814c121e..97d57eed5c 100644 --- a/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala +++ b/frontend/src/test/scala/bloop/bsp/BspMetalsClientSpec.scala @@ -11,6 +11,7 @@ import bloop.cli.ExitStatus import bloop.data.WorkspaceSettings import bloop.engine.ExecutionContext import bloop.internal.build.BuildInfo +import bloop.internal.build.BuildTestInfo import bloop.io.AbsolutePath import bloop.io.Environment.LineSplitter import bloop.io.Environment.lineSeparator @@ -27,7 +28,7 @@ class BspMetalsClientSpec( override val protocol: BspProtocol ) extends BspBaseSuite { private val testedScalaVersion = BuildInfo.scalaVersion - private val semanticdbVersion = "4.8.4" + private val semanticdbVersion = "4.9.2" private val javaSemanticdbVersion = "0.5.7" private val semanticdbJar = s"semanticdb-scalac_$testedScalaVersion-$semanticdbVersion.jar" @@ -170,7 +171,7 @@ class BspMetalsClientSpec( val scalacOptions = state.scalaOptions(`A`)._2.items.head.options val expectedScalacOptions = correctSourceRootOption :: List( - s"-Xplugin:path-to-plugin/semanticdb-scalac_2.12.18-$semanticdbVersion.jar", + s"-Xplugin:path-to-plugin/semanticdb-scalac_2.12.19-$semanticdbVersion.jar", "-Yrangepos", "-P:semanticdb:failures:warning", "-P:semanticdb:synthetics:on", @@ -509,9 +510,9 @@ class BspMetalsClientSpec( // TestUtil.withinWorkspace { workspace => // val logger = new RecordingLogger(ansiCodesSupported = false) - // val projectName = "scala-java-processorpath" - // val localScalaVersion = "2.12.18" - // val localSemanticdbJar = s"semanticdb-scalac_$localScalaVersion-$semanticdbVersion.jar" + // val projectName = "scala-java-processorpath" + // val localScalaVersion = BuildInfo.scalaVersion + // val localSemanticdbJar = s"semanticdb-scalac_$localScalaVersion-$semanticdbVersion.jar" // val extraParams = BloopExtraBuildParams( // ownsBuildFiles = None, diff --git a/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala b/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala index be51b1de20..51ec49ae91 100644 --- a/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala +++ b/frontend/src/test/scala/bloop/bsp/BspProtocolSpec.scala @@ -516,6 +516,66 @@ class BspProtocolSpec( } } + test("dependency modules request works") { + TestUtil.withinWorkspace { workspace => + val logger = new RecordingLogger(ansiCodesSupported = false) + loadBspBuildFromResources("cross-test-build-scalajs-0.6", workspace, logger) { build => + val mainProject = build.projectFor("test-project") + val testProject = build.projectFor("test-project-test") + val mainJsProject = build.projectFor("test-projectJS") + val testJsProject = build.projectFor("test-projectJS-test") + val rootMain = build.projectFor("cross-test-build-scalajs-0-6") + val rootTest = build.projectFor("cross-test-build-scalajs-0-6-test") + + def checkDependencyModules(project: TestProject): Unit = { + val dependencyModulesResult = build.state.requestDependencyModules(project) + assert(dependencyModulesResult.items.size == 1) + val dependencyModules = dependencyModulesResult.items.flatMap(item => + item.modules + .map(dependencyModule => { + val json = dependencyModule.data.get + val mavenModule = readFromArray[bsp.MavenDependencyModule](json.value) + val artifacts: List[Config.Artifact] = mavenModule.artifacts + .map(artifact => + Config.Artifact( + dependencyModule.name, + artifact.classifier, + None, + artifact.uri.toPath + ) + ) + .distinct; + new Config.Module( + mavenModule.organization, + mavenModule.name, + mavenModule.version, + None, + artifacts + ) + }) + .distinct + ) + + val expectedModules = project.config.resolution.toList.flatMap { res => + res.modules.map { m => + val artifacts = m.artifacts.map(artifact => artifact.copy(checksum = None)) + m.copy(configurations = None, artifacts = artifacts) + } + }.distinct + + assertEquals(dependencyModules, expectedModules) + } + + checkDependencyModules(mainProject) + checkDependencyModules(testProject) + checkDependencyModules(mainJsProject) + checkDependencyModules(testJsProject) + checkDependencyModules(rootMain) + checkDependencyModules(rootTest) + } + } + } + test("dependency sources request works") { TestUtil.withinWorkspace { workspace => val logger = new RecordingLogger(ansiCodesSupported = false) diff --git a/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala b/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala index d1cad2896a..14e94b35a5 100644 --- a/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala +++ b/frontend/src/test/scala/bloop/bsp/BspSbtClientSpec.scala @@ -160,6 +160,7 @@ class BspSbtClientSpec( assertValidCompilationState(newCompiledState, projects) assertNoDiff( + diagnosticsWithoutTaskIds(newCompiledState, `A`), s"""# task start 6 | -> Msg: Compiling a (1 Scala source) | -> Data kind: compile-task @@ -167,56 +168,34 @@ class BspSbtClientSpec( | -> errors 0, warnings 0 | -> origin = $secondOriginId | -> Msg: Compiled 'a' - | -> Data kind: compile-report""".stripMargin, - diagnosticsWithoutTaskIds(newCompiledState, `A`) + | -> Data kind: compile-report""".stripMargin ) assertNoDiff( - s"""# task start 7 - | -> Msg: Start no-op compilation for b - | -> Data kind: compile-task - |# task finish 7 - | -> errors 0, warnings 0 - | -> origin = $secondOriginId - | -> Msg: Compiled 'b' - | -> Data kind: compile-report""".stripMargin, - diagnosticsWithoutTaskIds(newCompiledState, `B`) + diagnosticsWithoutTaskIds(newCompiledState, `B`), + "" // no-op ) assertNoDiff( - s"""# task start 8 + diagnosticsWithoutTaskIds(newCompiledState, `C`), + s"""# task start 7 | -> Msg: Compiling c (1 Scala source) | -> Data kind: compile-task - |# task finish 8 + |# task finish 7 | -> errors 0, warnings 0 | -> origin = $secondOriginId | -> Msg: Compiled 'c' - | -> Data kind: compile-report""".stripMargin, - diagnosticsWithoutTaskIds(newCompiledState, `C`) + | -> Data kind: compile-report""".stripMargin ) assertNoDiff( - s"""# task start 9 - | -> Msg: Start no-op compilation for d - | -> Data kind: compile-task - |# task finish 9 - | -> errors 0, warnings 0 - | -> origin = $secondOriginId - | -> Msg: Compiled 'd' - | -> Data kind: compile-report""".stripMargin, - diagnosticsWithoutTaskIds(newCompiledState, `D`) + diagnosticsWithoutTaskIds(newCompiledState, `D`), + "" // no-op ) assertNoDiff( - s"""# task start 10 - | -> Msg: Start no-op compilation for e - | -> Data kind: compile-task - |# task finish 10 - | -> errors 0, warnings 0 - | -> origin = $secondOriginId - | -> Msg: Compiled 'e' - | -> Data kind: compile-report""".stripMargin, - diagnosticsWithoutTaskIds(newCompiledState, `E`) + diagnosticsWithoutTaskIds(newCompiledState, `E`), + "" // no-op ) assertSameExternalClassesDirs(newCompiledState, initialStateBackup, List(`B`, `D`, `E`)) @@ -307,17 +286,9 @@ class BspSbtClientSpec( assertNoDiff( thirdCompiledState.lastDiagnostics(`A`), - """ - |#3: task start 3 - | -> Msg: Start no-op compilation for a - | -> Data kind: compile-task - |#3: a/src/main/scala/Foo.scala + """#3: a/src/main/scala/Foo.scala | -> List() - | -> reset = true - |#3: task finish 3 - | -> errors 0, warnings 0 - | -> Msg: Compiled 'a' - | -> Data kind: compile-report """.stripMargin + | -> reset = true""".stripMargin ) } } diff --git a/frontend/src/test/scala/bloop/bsp/TestConstants.scala b/frontend/src/test/scala/bloop/bsp/TestConstants.scala index db3b4359b2..f3699b62e6 100644 --- a/frontend/src/test/scala/bloop/bsp/TestConstants.scala +++ b/frontend/src/test/scala/bloop/bsp/TestConstants.scala @@ -16,6 +16,7 @@ object TestConstants { "debugProvider": { "languageIds": ["scala", "java"] }, "inverseSourcesProvider": true, "dependencySourcesProvider": true, + "dependencyModulesProvider": true, "resourcesProvider": true, "buildTargetChangedProvider": false, "jvmRunEnvironmentProvider": true, diff --git a/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala b/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala index c51ec9edba..0aa7154506 100644 --- a/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala +++ b/frontend/src/test/scala/bloop/dap/DebugAdapterConnection.scala @@ -14,12 +14,7 @@ import bloop.task.Task import com.microsoft.java.debug.core.protocol.Events import com.microsoft.java.debug.core.protocol.Requests._ -import com.microsoft.java.debug.core.protocol.Responses.ContinueResponseBody -import com.microsoft.java.debug.core.protocol.Responses.EvaluateResponseBody -import com.microsoft.java.debug.core.protocol.Responses.ScopesResponseBody -import com.microsoft.java.debug.core.protocol.Responses.SetBreakpointsResponseBody -import com.microsoft.java.debug.core.protocol.Responses.StackTraceResponseBody -import com.microsoft.java.debug.core.protocol.Responses.VariablesResponseBody +import com.microsoft.java.debug.core.protocol.Responses._ import com.microsoft.java.debug.core.protocol.Types.Capabilities import monix.execution.Cancelable import monix.execution.Scheduler @@ -119,6 +114,11 @@ private[dap] final class DebugAdapterConnection( adapter.request(Attach, arguments) } + def redefineClasses(): Task[RedefineClassesResponse] = { + val args = new RedefineClassesArguments() + adapter.request(RedefineClasses, args) + } + def close(): Unit = { try socket.close() finally { diff --git a/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala b/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala index e27b1527e6..06f18d71e2 100644 --- a/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala +++ b/frontend/src/test/scala/bloop/dap/DebugServerSpec.scala @@ -1,33 +1,40 @@ package bloop.dap +import java.io.Closeable import java.net.ConnectException import java.net.SocketException import java.net.SocketTimeoutException import java.util.NoSuchElementException import java.util.concurrent.TimeUnit.MILLISECONDS -import java.util.concurrent.TimeUnit.SECONDS +import scala.collection.JavaConverters._ import scala.collection.mutable import scala.concurrent.Future import scala.concurrent.Promise import scala.concurrent.TimeoutException import scala.concurrent.duration.Duration import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration._ +import ch.epfl.scala.bsp import ch.epfl.scala.bsp.ScalaMainClass import ch.epfl.scala.debugadapter._ +import bloop.Cli import bloop.ScalaInstance -import ch.epfl.scala.bsp +import bloop.cli.CommonOptions import bloop.cli.ExitStatus import bloop.data.Platform import bloop.data.Project +import bloop.engine.NoPool import bloop.engine.State import bloop.engine.tasks.RunMode import bloop.engine.tasks.Tasks +import bloop.internal.build import bloop.internal.build.BuildTestInfo import bloop.io.AbsolutePath import bloop.io.Environment.lineSeparator +import bloop.logging.BspClientLogger import bloop.logging.Logger import bloop.logging.LoggerAction import bloop.logging.LoggerAction.LogInfoMessage @@ -42,6 +49,8 @@ import bloop.util.TestUtil import com.microsoft.java.debug.core.protocol.Requests.SetBreakpointArguments import com.microsoft.java.debug.core.protocol.Types import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint +import coursierapi.Dependency +import coursierapi.Fetch import monix.execution.Ack import monix.reactive.Observer @@ -50,11 +59,6 @@ object DebugServerSpec extends DebugBspBaseSuite { private val Success: ExitStatus = ExitStatus.Ok private val resolver = new BloopDebugToolsResolver(NoopLogger) - override def test(name: String)(fun: => Any): Unit = - super.test(name) { - TestUtil.retry()(fun) - } - testTask("cancelling server closes server connection", FiniteDuration(10, SECONDS)) { startDebugServer(Task.now(Success)) { server => for { @@ -207,6 +211,81 @@ object DebugServerSpec extends DebugBspBaseSuite { } } + testTask( + "runs-correct-runtime", + FiniteDuration(60, SECONDS) + ) { + TestUtil.withinWorkspace { workspace => + val runtimeClasspath = Fetch + .create() + .addDependencies( + Dependency.of( + "ch.qos.logback", + "logback-classic", + "1.2.7" + ), + Dependency.of( + "org.scala-lang", + "scala-library", + build.BuildInfo.scalaVersion + ) + ) + .fetch() + .asScala + .map(_.toPath()) + .toList + + val main = + """|/main/scala/Main.scala + |object Main { + | def main(args: Array[String]): Unit = { + | println(s">>> hello world! <<<") + | val cn = Class.forName("ch.qos.logback.classic.Logger") + | println(s"$cn") + | } + |} + | + |""".stripMargin + + val logger = new RecordingLogger(ansiCodesSupported = false) + val project = + TestProject(workspace, "r", List(main), runtimeClasspath = Some(runtimeClasspath)) + + loadBspStateWithTask(workspace, List(project), logger) { state => + val runner = mainRunner( + project, + state, + arguments = Nil, + jvmOptions = Nil, + environmentVariables = Nil + ) + + startDebugServer(runner) { server => + for { + client <- server.startConnection + _ <- client.initialize() + _ <- client.launch(noDebug = true) + _ <- client.configurationDone() + _ <- client.exited + _ <- client.terminated + _ <- Task.fromFuture(client.closedPromise.future) + output <- client.takeCurrentOutput + } yield { + assert(client.socket.isClosed) + assertNoDiff( + output.linesIterator + .filterNot(_.contains("ERROR: JDWP Unable to get JNI 1.2 environment")) + .filterNot(_.contains("JDWP exit error AGENT_ERROR_NO_JNI_ENV")) + .mkString(lineSeparator), + """|>>> hello world! <<< + |class ch.qos.logback.classic.Logger""".stripMargin + ) + } + } + } + } + } + testTask("supports scala and java breakpoints", FiniteDuration(60, SECONDS)) { TestUtil.withinWorkspace { workspace => object Sources { @@ -541,9 +620,9 @@ object DebugServerSpec extends DebugBspBaseSuite { val attachRemoteProcessRunner = BloopDebuggeeRunner.forAttachRemote( + Seq(buildProject), state.compile(project).toTestState.state, - defaultScheduler, - Seq(buildProject) + defaultScheduler ) startDebugServer(attachRemoteProcessRunner) { server => @@ -784,9 +863,9 @@ object DebugServerSpec extends DebugBspBaseSuite { val attachRemoteProcessRunner = BloopDebuggeeRunner.forAttachRemote( + Seq(buildProject), testState.state, - defaultScheduler, - Seq(buildProject) + defaultScheduler ) startDebugServer(attachRemoteProcessRunner) { server => @@ -890,6 +969,87 @@ object DebugServerSpec extends DebugBspBaseSuite { } } + testTask("hot code replace", 30.seconds) { + val mainSource = + """|/Main.scala + |object Main { + | def main(args: Array[String]): Unit = { + | val a = new example.A + | a.m() + | } + |} + |""".stripMargin + val originalSource = + """|/example/A.scala + |package example + |class A { + | def m() = { + | println("A") + | } + |} + |""".stripMargin + val modifiedSource = + """|/example/A.scala + |package example + |class A { + | def m() = { + | println("B") + | } + |} + |""".stripMargin + val logger = new RecordingLogger(ansiCodesSupported = false) + TestUtil.withinWorkspace { workspace => + val dependency = TestProject(workspace, "a", List(originalSource)) + val mainProject = TestProject(workspace, "main", List(mainSource), List(dependency)) + val configDir = TestProject.populateWorkspace(workspace, List(mainProject, dependency)) + + def cliCompile(project: TestProject) = { + val compileArgs = Array("compile", project.config.name, "--config-dir", configDir.syntax) + val compileAction = Cli.parse(compileArgs, CommonOptions.default) + Task.eval(Cli.run(compileAction, NoPool)).executeAsync + } + + def bspCommand() = createBspCommand(configDir) + val state = TestUtil.loadTestProject(configDir.underlying, logger) + openBspConnection(state, bspCommand, configDir, new BspClientLogger(logger)) + .withinSession { state => + val testState = state.compile(mainProject).toTestState + val `A.scala` = testState + .getProjectFor(dependency) + .sources + .map(_.resolve("example/A.scala")) + .find(_.exists) + .get + + val runner = mainRunner(mainProject, state) + startDebugServer(runner) { server => + for { + client <- server.startConnection + _ <- client.initialize() + _ <- client.launch(noDebug = false) + _ <- client.initialized + response <- client.setBreakpoints(breakpointsArgs(`A.scala`, 4)) + _ = assert(response.breakpoints.forall(_.verified)) + _ <- client.configurationDone() + stopped <- client.stopped + _ = writeFile(`A.scala`, modifiedSource) + _ <- cliCompile(mainProject) // another client trigger a compilation + _ = state.compile(mainProject) // noop + _ <- client.redefineClasses() + _ <- client.continue(stopped.threadId) + _ <- client.exited + _ <- client.terminated + _ <- Task.fromFuture(client.closedPromise.future) + output <- client.takeCurrentOutput + } yield { + assert(client.socket.isClosed) + assertNoDiff(output, "B") + } + } + } + } + } + private def startRemoteProcess(buildProject: Project, testState: TestState): Task[Int] = { val attachPort = Promise[Int]() @@ -1025,9 +1185,11 @@ object DebugServerSpec extends DebugBspBaseSuite { override def javaRuntime: Option[JavaRuntime] = None def name: String = "MockRunner" def run(listener: DebuggeeListener): CancelableFuture[Unit] = { - DapCancellableFuture.runAsync(task.map(_ => ()), defaultScheduler) + DapCancellableFuture.runAsync(task.map(_ => ()))(defaultScheduler) } def scalaVersion: ScalaVersion = ScalaVersion("2.12.17") + + override def observeClassUpdates(onClassUpdate: Seq[String] => Unit): Closeable = () => () } startDebugServer( @@ -1040,7 +1202,7 @@ object DebugServerSpec extends DebugBspBaseSuite { def startDebugServer( debuggee: Debuggee, - gracePeriod: Duration = Duration(5, SECONDS) + gracePeriod: Duration = 5.seconds )(f: TestServer => Task[Unit]): Task[Unit] = { val logger = new RecordingLogger(ansiCodesSupported = false) val dapLogger = new DebugServerLogger(logger) @@ -1067,7 +1229,7 @@ object DebugServerSpec extends DebugBspBaseSuite { override def close(): Unit = { cancel() val allClientsClosed = clients.map(c => Task.fromFuture(c.closedPromise.future)) - TestUtil.await(10, SECONDS)(Task.sequence(allClientsClosed)); () + TestUtil.await(10.seconds)(Task.sequence(allClientsClosed)); () } def startConnection: Task[DebugAdapterConnection] = Task { diff --git a/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala b/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala index 158ac70f81..275fc620ad 100644 --- a/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala +++ b/frontend/src/test/scala/bloop/dap/DebugTestEndpoints.scala @@ -4,12 +4,7 @@ import bloop.dap.DebugTestProtocol._ import com.microsoft.java.debug.core.protocol.Events import com.microsoft.java.debug.core.protocol.Requests._ -import com.microsoft.java.debug.core.protocol.Responses.ContinueResponseBody -import com.microsoft.java.debug.core.protocol.Responses.EvaluateResponseBody -import com.microsoft.java.debug.core.protocol.Responses.ScopesResponseBody -import com.microsoft.java.debug.core.protocol.Responses.SetBreakpointsResponseBody -import com.microsoft.java.debug.core.protocol.Responses.StackTraceResponseBody -import com.microsoft.java.debug.core.protocol.Responses.VariablesResponseBody +import com.microsoft.java.debug.core.protocol.Responses._ import com.microsoft.java.debug.core.protocol.Types private[dap] object DebugTestEndpoints { @@ -24,6 +19,8 @@ private[dap] object DebugTestEndpoints { val Variables = new Request[VariablesArguments, VariablesResponseBody]("variables") val Evaluate = new Request[EvaluateArguments, EvaluateResponseBody]("evaluate") val Continue = new Request[ContinueArguments, ContinueResponseBody]("continue") + val RedefineClasses = + new Request[RedefineClassesArguments, RedefineClassesResponse]("redefineClasses") val ConfigurationDone = new Request[Unit, Unit]("configurationDone") val Exited = new Event[Events.ExitedEvent]("exited") diff --git a/frontend/src/test/scala/bloop/testing/BloopHelpers.scala b/frontend/src/test/scala/bloop/testing/BloopHelpers.scala index e668381ae3..61a5899bc2 100644 --- a/frontend/src/test/scala/bloop/testing/BloopHelpers.scala +++ b/frontend/src/test/scala/bloop/testing/BloopHelpers.scala @@ -21,6 +21,7 @@ import bloop.io.ParallelOps import bloop.io.ParallelOps.CopyMode import bloop.io.RelativePath import bloop.logging.Logger +import bloop.logging.NoopLogger import bloop.logging.RecordingLogger import bloop.task.Task import bloop.util.TestProject @@ -28,7 +29,6 @@ import bloop.util.TestUtil import monix.execution.CancelableFuture import monix.execution.Scheduler -import bloop.logging.NoopLogger trait BloopHelpers { def loadState( diff --git a/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala b/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala index 149aa7324d..cb185f9f35 100644 --- a/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala +++ b/frontend/src/test/scala/bloop/testing/ProjectBaseSuite.scala @@ -2,11 +2,12 @@ package bloop.testing import java.nio.file.Files +import scala.concurrent.duration.Duration + import bloop.io.AbsolutePath import bloop.io.Paths import bloop.logging.RecordingLogger import bloop.task.Task -import scala.concurrent.duration.Duration class ProjectBaseSuite(buildName: String) extends BaseSuite { val workspace: AbsolutePath = AbsolutePath(Files.createTempDirectory(s"workspace-${buildName}")) diff --git a/frontend/src/test/scala/bloop/util/TestProject.scala b/frontend/src/test/scala/bloop/util/TestProject.scala index 81811d907c..b55ba3a387 100644 --- a/frontend/src/test/scala/bloop/util/TestProject.scala +++ b/frontend/src/test/scala/bloop/util/TestProject.scala @@ -107,6 +107,7 @@ abstract class BaseTestProject { scalaVersion: Option[String] = None, resources: List[String] = Nil, runtimeResources: Option[List[String]] = None, + runtimeClasspath: Option[List[Path]] = None, jvmConfig: Option[Config.JvmConfig] = None, runtimeJvmConfig: Option[Config.JvmConfig] = None, order: Config.CompileOrder = Config.Mixed, @@ -136,7 +137,7 @@ abstract class BaseTestProject { mkScalaInstance(finalScalaOrg, finalScalaCompiler, scalaVersion, jars.toList, NoopLogger) val allJars = instance.allJars.map(AbsolutePath.apply) - val (compileClasspath, runtimeClasspath) = { + val (compileClasspath, runtimeClasspathForStrict) = { val transitiveClasspath = (directDependencies.flatMap(classpathDeps) ++ allJars ++ jars).map(_.underlying) val directClasspath = @@ -170,7 +171,7 @@ abstract class BaseTestProject { javaConfig, None, runtimeJvmConfig, - Some(runtimeClasspath), + runtimeClasspath.orElse(Some(runtimeClasspathForStrict)), runtimeResourcesList ) diff --git a/shared/src/main/scala/bloop/io/AbsolutePath.scala b/shared/src/main/scala/bloop/io/AbsolutePath.scala index 26eab39a18..7faff8220c 100644 --- a/shared/src/main/scala/bloop/io/AbsolutePath.scala +++ b/shared/src/main/scala/bloop/io/AbsolutePath.scala @@ -6,6 +6,9 @@ import java.net.URI import java.nio.file.Files import java.nio.file.Path import java.nio.file.{Paths => NioPaths} +import java.util.stream.Collectors + +import scala.collection.JavaConverters._ final class AbsolutePath private (val underlying: Path) extends AnyVal { def syntax: String = toString @@ -21,6 +24,7 @@ final class AbsolutePath private (val underlying: Path) extends AnyVal { def getParent: AbsolutePath = AbsolutePath(underlying.getParent) def createDirectories: AbsolutePath = AbsolutePath(Files.createDirectories(underlying)) def exists: Boolean = Files.exists(underlying) + def list: List[Path] = Files.list(underlying).collect(Collectors.toList()).asScala.toList def isFile: Boolean = Files.isRegularFile(underlying) def isDirectory: Boolean = Files.isDirectory(underlying) def isSameFile(other: AbsolutePath): Boolean = Files.isSameFile(underlying, other.underlying) diff --git a/shared/src/main/scala/bloop/logging/Logger.scala b/shared/src/main/scala/bloop/logging/Logger.scala index dd2df7ae3c..19497d7603 100644 --- a/shared/src/main/scala/bloop/logging/Logger.scala +++ b/shared/src/main/scala/bloop/logging/Logger.scala @@ -2,6 +2,8 @@ package bloop.logging import java.util.function.Supplier +import bloop.io.Environment + abstract class Logger extends xsbti.Logger with BaseSbtLogger { /** The name of the logger */ @@ -31,7 +33,12 @@ abstract class Logger extends xsbti.Logger with BaseSbtLogger { override def info(msg: Supplier[String]): Unit = info(msg.get) override def trace(exception: Supplier[Throwable]): Unit = trace(exception.get()) - def report(msg: String, t: Throwable): Unit = { error(msg); trace(t) } + def error(msg: String, t: Throwable): Unit = { + error( + msg + Environment.lineSeparator + Logger.prettyPrintException(t) + ) + } + def handleCompilationEvent(): Unit = () /** Display a message as a warning to user using `showMessage` in BSP-based loggers and `warn` otherwise. */ @@ -48,3 +55,12 @@ private[logging] trait BaseSbtLogger extends sbt.testing.Logger { private[logging] def printDebug(line: String): Unit override def debug(msg: String): Unit = printDebug(msg) } + +object Logger { + def prettyPrintException(t: Throwable): String = { + val sw = new java.io.StringWriter() + val pw = new java.io.PrintWriter(sw) + t.printStackTrace(pw) + sw.toString() + } +}