diff --git a/build.sc b/build.sc index 36936669af..14ef874ad3 100644 --- a/build.sc +++ b/build.sc @@ -13,6 +13,7 @@ import $file.project.settings, settings.{ ScalaCliSbtModule, ScalaCliScalafixModule, localRepoResourcePath, + moduleConfigFileName, platformExecutableJarExtension, workspaceDirName, projectFileName, @@ -474,6 +475,7 @@ trait Core extends ScalaCliCrossSbtModule | def workspaceDirName = "$workspaceDirName" | def projectFileName = "$projectFileName" | def jvmPropertiesFileName = "$jvmPropertiesFileName" + | def moduleConfigFileName = "$moduleConfigFileName" | def scalacArgumentsFileName = "scalac.args.txt" | def maxScalacArgumentsCount = 5000 | @@ -701,7 +703,8 @@ trait Build extends ScalaCliCrossSbtModule Deps.scalaJsEnvNodeJs, Deps.scalaJsTestAdapter, Deps.swoval, - Deps.zipInputStream + Deps.zipInputStream, + Deps.tomlScala ) def repositoriesTask = @@ -738,6 +741,8 @@ trait Build extends ScalaCliCrossSbtModule | def defaultScalaVersion = "${Scala.defaultUser}" | def defaultScala212Version = "${Scala.scala212}" | def defaultScala213Version = "${Scala.scala213}" + | + | def moduleConfigFileName = "$moduleConfigFileName" |} |""".stripMargin if (!os.isFile(dest) || os.read(dest) != code) @@ -1032,6 +1037,7 @@ trait CliIntegration extends SbtModule with ScalaCliPublishModule with HasTests )}" | def cs = "${settings.cs().replace("\\", "\\\\")}" | def workspaceDirName = "$workspaceDirName" + | def moduleConfigFileName = "$moduleConfigFileName" | def libsodiumVersion = "${deps.libsodiumVersion}" | def dockerArchLinuxImage = "${TestDeps.archLinuxImage}" | diff --git a/modules/build-macros/src/main/scala/scala/build/EitherCps.scala b/modules/build-macros/src/main/scala/scala/build/EitherCps.scala index e991f20e2a..989d01c9c0 100644 --- a/modules/build-macros/src/main/scala/scala/build/EitherCps.scala +++ b/modules/build-macros/src/main/scala/scala/build/EitherCps.scala @@ -13,6 +13,11 @@ object EitherCps: case Left(e) => throw EitherFailure(e, cps) case Right(v) => v + def failure[E](using + cps: EitherCps[_ >: E] + )(e: E) = // Adding a context bounds breaks incremental compilation + throw EitherFailure(e, cps) + final class Helper[E](): def apply[V](op: EitherCps[E] ?=> V): Either[E, V] = val cps = new EitherCps[E] diff --git a/modules/build/src/main/scala/scala/build/Bloop.scala b/modules/build/src/main/scala/scala/build/Bloop.scala index 50f109ebd5..fd6445f1b7 100644 --- a/modules/build/src/main/scala/scala/build/Bloop.scala +++ b/modules/build/src/main/scala/scala/build/Bloop.scala @@ -11,10 +11,11 @@ import java.io.{File, IOException} import scala.annotation.tailrec import scala.build.EitherCps.{either, value} +import scala.build.bsp.buildtargets.ProjectName import scala.build.errors.{BuildException, ModuleFormatError} -import scala.build.internal.CsLoggerUtil._ +import scala.build.internal.CsLoggerUtil.* import scala.concurrent.duration.FiniteDuration -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* object Bloop { @@ -30,7 +31,7 @@ object Bloop { } def compile( - projectName: String, + projectName: ProjectName, buildServer: BuildServer, logger: Logger, buildTargetsTimeout: FiniteDuration @@ -39,16 +40,16 @@ object Bloop { logger.debug("Listing BSP build targets") val results = buildServer.workspaceBuildTargets() .get(buildTargetsTimeout.length, buildTargetsTimeout.unit) - val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName) + val buildTargetOpt = results.getTargets.asScala.find(_.getDisplayName == projectName.name) val buildTarget = buildTargetOpt.getOrElse { throw new Exception( - s"Expected to find project '$projectName' in build targets (only got ${results.getTargets + s"Expected to find project '${projectName.name}' in build targets (only got ${results.getTargets .asScala.map("'" + _.getDisplayName + "'").mkString(", ")})" ) } - logger.debug(s"Compiling $projectName with Bloop") + logger.debug(s"Compiling ${projectName.name} with Bloop") val compileRes = buildServer.buildTargetCompile( new bsp4j.CompileParams(List(buildTarget.getId).asJava) ).get() diff --git a/modules/build/src/main/scala/scala/build/BloopBuildClient.scala b/modules/build/src/main/scala/scala/build/BloopBuildClient.scala index f38bc568d1..1344a8d109 100644 --- a/modules/build/src/main/scala/scala/build/BloopBuildClient.scala +++ b/modules/build/src/main/scala/scala/build/BloopBuildClient.scala @@ -2,11 +2,12 @@ package scala.build import ch.epfl.scala.bsp4j +import scala.build.bsp.buildtargets.ProjectName import scala.build.options.Scope trait BloopBuildClient extends bsp4j.BuildClient { def setProjectParams(newParams: Seq[String]): Unit - def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]): Unit + def setGeneratedSources(projectName: ProjectName, newGeneratedSources: Seq[GeneratedSource]): Unit def diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]] def clear(): Unit } diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index 975db8f226..25bb6f1ace 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -12,6 +12,7 @@ import java.util.concurrent.{ScheduledExecutorService, ScheduledFuture} import scala.annotation.tailrec import scala.build.EitherCps.{either, value} import scala.build.Ops.* +import scala.build.bsp.buildtargets.ProjectName import scala.build.compiler.{ScalaCompiler, ScalaCompilerMaker} import scala.build.errors.* import scala.build.input.VirtualScript.VirtualScriptNameRegex @@ -29,7 +30,7 @@ import scala.util.control.NonFatal import scala.util.{Properties, Try} trait Build { - def inputs: Inputs + def inputs: ModuleInputs def options: BuildOptions def scope: Scope def outputOpt: Option[os.Path] @@ -42,7 +43,7 @@ trait Build { object Build { final case class Successful( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions, scalaParams: Option[ScalaParameters], scope: Scope, @@ -170,7 +171,7 @@ object Build { } final case class Failed( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions, scope: Scope, sources: Sources, @@ -184,7 +185,7 @@ object Build { } final case class Cancelled( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions, scope: Scope, reason: String @@ -199,9 +200,9 @@ object Build { * Using only the command-line options not the ones from the sources. */ def updateInputs( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions - ): Inputs = { + ): ModuleInputs = { // If some options are manually overridden, append a hash of the options to the project name // Using options, not options0 - only the command-line options are taken into account. No hash is @@ -212,11 +213,11 @@ object Build { } private def allInputs( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions, logger: Logger )(using ScalaCliInvokeData) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, Sources.defaultPreprocessors( options.archiveCache, @@ -229,7 +230,7 @@ object Build { ) private def build( - inputs: Inputs, + inputs: ModuleInputs, crossSources: CrossSources, options: BuildOptions, logger: Logger, @@ -244,7 +245,7 @@ object Build { val sharedOptions = crossSources.sharedOptions(options) val crossOptions = sharedOptions.crossOptions - def doPostProcess(build: Build, inputs: Inputs, scope: Scope): Unit = build match { + def doPostProcess(build: Build, inputs: ModuleInputs, scope: Scope): Unit = build match { case build: Build.Successful => for (sv <- build.project.scalaCompiler.map(_.scalaVersion)) postProcess( @@ -422,7 +423,7 @@ object Build { } private def build( - inputs: Inputs, + inputs: ModuleInputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, @@ -474,23 +475,28 @@ object Build { } } - def projectRootDir(root: os.Path, projectName: String): os.Path = - root / Constants.workspaceDirName / projectName - def classesRootDir(root: os.Path, projectName: String): os.Path = + def projectRootDir(root: os.Path, projectName: ProjectName): os.Path = + root / Constants.workspaceDirName / projectName.name + def classesRootDir(root: os.Path, projectName: ProjectName): os.Path = projectRootDir(root, projectName) / "classes" - def classesDir(root: os.Path, projectName: String, scope: Scope, suffix: String = ""): os.Path = + def classesDir( + root: os.Path, + projectName: ProjectName, + scope: Scope, + suffix: String = "" + ): os.Path = classesRootDir(root, projectName) / s"${scope.name}$suffix" def resourcesRegistry( root: os.Path, - projectName: String, + projectName: ProjectName, scope: Scope ): os.Path = - root / Constants.workspaceDirName / projectName / s"resources-${scope.name}" + root / Constants.workspaceDirName / projectName.name / s"resources-${scope.name}" def scalaNativeSupported( options: BuildOptions, - inputs: Inputs, + inputs: ModuleInputs, logger: Logger ): Either[BuildException, Option[ScalaNativeCompatibilityError]] = either { @@ -548,7 +554,7 @@ object Build { } def build( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions, compilerMaker: ScalaCompilerMaker, docCompilerMakerOpt: Option[ScalaCompilerMaker], @@ -628,7 +634,7 @@ object Build { } def watch( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions, compilerMaker: ScalaCompilerMaker, docCompilerMakerOpt: Option[ScalaCompilerMaker], @@ -832,7 +838,7 @@ object Build { * a bloop [[Project]] */ def buildProject( - inputs: Inputs, + inputs: ModuleInputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, @@ -997,7 +1003,7 @@ object Build { } def prepareBuild( - inputs: Inputs, + inputs: ModuleInputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, @@ -1078,7 +1084,7 @@ object Build { } def buildOnce( - inputs: Inputs, + inputs: ModuleInputs, sources: Sources, generatedSources: Seq[GeneratedSource], options: BuildOptions, @@ -1110,7 +1116,7 @@ object Build { } buildClient.clear() - buildClient.setGeneratedSources(scope, generatedSources) + buildClient.setGeneratedSources(inputs.scopeProjectName(scope), generatedSources) val partial = partialOpt.getOrElse { options.notForBloopOptions.packageOptions.packageTypeOpt.exists(_.sourceBased) @@ -1248,7 +1254,7 @@ object Build { else path.toString private def jmhBuild( - inputs: Inputs, + inputs: ModuleInputs, build: Build.Successful, logger: Logger, javaCommand: String, @@ -1257,7 +1263,7 @@ object Build { buildTests: Boolean, actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData): Either[BuildException, Option[Build]] = either { - val jmhProjectName = inputs.projectName + "_jmh" + val jmhProjectName = inputs.projectName.name + "_jmh" val jmhOutputDir = inputs.workspace / Constants.workspaceDirName / jmhProjectName os.remove.all(jmhOutputDir) val jmhSourceDir = jmhOutputDir / "sources" diff --git a/modules/build/src/main/scala/scala/build/ConsoleBloopBuildClient.scala b/modules/build/src/main/scala/scala/build/ConsoleBloopBuildClient.scala index 7055e29e80..3cda92a4a8 100644 --- a/modules/build/src/main/scala/scala/build/ConsoleBloopBuildClient.scala +++ b/modules/build/src/main/scala/scala/build/ConsoleBloopBuildClient.scala @@ -6,6 +6,7 @@ import java.io.File import java.net.URI import java.nio.file.Paths +import scala.build.bsp.buildtargets.ProjectName import scala.build.errors.Severity import scala.build.internal.WrapperParams import scala.build.internals.ConsoleUtils.ScalaCliConsole @@ -17,7 +18,7 @@ import scala.jdk.CollectionConverters.* class ConsoleBloopBuildClient( logger: Logger, keepDiagnostics: Boolean = false, - generatedSources: mutable.Map[Scope, Seq[GeneratedSource]] = mutable.Map() + generatedSources: mutable.Map[ProjectName, Seq[GeneratedSource]] = mutable.Map() ) extends BloopBuildClient { import ConsoleBloopBuildClient._ private var projectParams = Seq.empty[String] @@ -32,8 +33,8 @@ class ConsoleBloopBuildClient( private val diagnostics0 = new mutable.ListBuffer[(Either[String, os.Path], bsp4j.Diagnostic)] - def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]) = - generatedSources(scope) = newGeneratedSources + def setGeneratedSources(projectName: ProjectName, newGeneratedSources: Seq[GeneratedSource]) = + generatedSources(projectName) = newGeneratedSources def setProjectParams(newParams: Seq[String]): Unit = { projectParams = newParams } diff --git a/modules/build/src/main/scala/scala/build/CrossSources.scala b/modules/build/src/main/scala/scala/build/CrossSources.scala index f9c917d49b..99f3d17348 100644 --- a/modules/build/src/main/scala/scala/build/CrossSources.scala +++ b/modules/build/src/main/scala/scala/build/CrossSources.scala @@ -140,7 +140,7 @@ final case class CrossSources( object CrossSources { - private def withinTestSubDirectory(p: ScopePath, inputs: Inputs): Boolean = + private def withinTestSubDirectory(p: ScopePath, inputs: ModuleInputs): Boolean = p.root.exists { path => val fullPath = path / p.subPath inputs.elements.exists { @@ -155,14 +155,14 @@ object CrossSources { /** @return * a CrossSources and Inputs which contains element processed from using directives */ - def forInputs( - inputs: Inputs, + def forModuleInputs( + inputs: ModuleInputs, preprocessors: Seq[Preprocessor], logger: Logger, suppressWarningOptions: SuppressWarningOptions, exclude: Seq[Positioned[String]] = Nil, maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e) - )(using ScalaCliInvokeData): Either[BuildException, (CrossSources, Inputs)] = either { + )(using ScalaCliInvokeData): Either[BuildException, (CrossSources, ModuleInputs)] = either { def preprocessSources(elems: Seq[SingleElement]) : Either[BuildException, Seq[PreprocessedSource]] = @@ -379,7 +379,7 @@ object CrossSources { * the resource directories that should be added to the classpath */ private def resolveResourceDirs( - allInputs: Inputs, + allInputs: ModuleInputs, preprocessedSources: Seq[PreprocessedSource] ): Seq[WithBuildRequirements[os.Path]] = { val fromInputs = allInputs.elements diff --git a/modules/build/src/main/scala/scala/build/Project.scala b/modules/build/src/main/scala/scala/build/Project.scala index f6d793be82..f8f4fb964e 100644 --- a/modules/build/src/main/scala/scala/build/Project.scala +++ b/modules/build/src/main/scala/scala/build/Project.scala @@ -1,8 +1,8 @@ package scala.build -import _root_.bloop.config.{Config => BloopConfig, ConfigCodecs => BloopCodecs} -import _root_.coursier.{Dependency => CsDependency, core => csCore, util => csUtil} -import com.github.plokhotnyuk.jsoniter_scala.core.{writeToArray => writeAsJsonToArray} +import _root_.bloop.config.{Config as BloopConfig, ConfigCodecs as BloopCodecs} +import _root_.coursier.{Dependency as CsDependency, core as csCore, util as csUtil} +import com.github.plokhotnyuk.jsoniter_scala.core.writeToArray as writeAsJsonToArray import coursier.core.Classifier import java.io.ByteArrayOutputStream @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Path import java.util.Arrays +import scala.build.bsp.buildtargets.ProjectName import scala.build.options.{ScalacOpt, Scope, ShadowingSeq} final case class Project( @@ -21,7 +22,7 @@ final case class Project( scalaCompiler: Option[ScalaCompilerParams], scalaJsOptions: Option[BloopConfig.JsConfig], scalaNativeOptions: Option[BloopConfig.NativeConfig], - projectName: String, + projectName: ProjectName, classPath: Seq[os.Path], sources: Seq[os.Path], resolution: Option[BloopConfig.Resolution], @@ -53,7 +54,7 @@ final case class Project( baseBloopProject( projectName, directory.toNIO, - (directory / ".bloop" / projectName).toNIO, + (directory / ".bloop" / projectName.name).toNIO, classesDir.toNIO, scope ) @@ -117,7 +118,7 @@ final case class Project( def writeBloopFile(strictCheck: Boolean, logger: Logger): Boolean = { lazy val bloopFileContent = writeAsJsonToArray(bloopFile)(BloopCodecs.codecFile) - val dest = directory / ".bloop" / s"$projectName.json" + val dest = directory / ".bloop" / s"${projectName.name}.json" val doWrite = if (strictCheck) !os.isFile(dest) || { @@ -176,14 +177,14 @@ object Project { ) private def baseBloopProject( - name: String, + projectName: ProjectName, directory: Path, out: Path, classesDir: Path, scope: Scope ): BloopConfig.Project = { val project = BloopConfig.Project( - name = name, + name = projectName.name, directory = directory, workspaceDir = None, sources = Nil, diff --git a/modules/build/src/main/scala/scala/build/Sources.scala b/modules/build/src/main/scala/scala/build/Sources.scala index a46ad8954e..e6d813e11e 100644 --- a/modules/build/src/main/scala/scala/build/Sources.scala +++ b/modules/build/src/main/scala/scala/build/Sources.scala @@ -6,7 +6,7 @@ import coursier.util.Task import java.nio.charset.StandardCharsets import scala.build.info.BuildInfo -import scala.build.input.Inputs +import scala.build.input.ModuleInputs import scala.build.internal.{CodeWrapper, WrapperParams} import scala.build.options.{BuildOptions, Scope} import scala.build.preprocessing.* @@ -19,7 +19,7 @@ final case class Sources( buildOptions: BuildOptions ) { - def withVirtualDir(inputs: Inputs, scope: Scope, options: BuildOptions): Sources = { + def withVirtualDir(inputs: ModuleInputs, scope: Scope, options: BuildOptions): Sources = { val srcRootPath = inputs.generatedSrcRoot(scope) val resourceDirs0 = options.classPathOptions.resourcesVirtualDir.map { path => diff --git a/modules/build/src/main/scala/scala/build/bsp/BloopSession.scala b/modules/build/src/main/scala/scala/build/bsp/BloopSession.scala index c3a4648b4b..8dbf4e5fc3 100644 --- a/modules/build/src/main/scala/scala/build/bsp/BloopSession.scala +++ b/modules/build/src/main/scala/scala/build/bsp/BloopSession.scala @@ -6,29 +6,36 @@ import java.util.concurrent.atomic.AtomicReference import scala.build.Build import scala.build.compiler.BloopCompiler -import scala.build.input.{Inputs, OnDisk, SingleFile, Virtual} +import scala.build.input.{ModuleInputs, OnDisk, SingleFile, Virtual, compose} final class BloopSession( - val inputs: Inputs, - val inputsHash: String, + val inputs: compose.Inputs, val remoteServer: BloopCompiler, val bspServer: BspServer, val watcher: Build.Watcher ) { - def resetDiagnostics(localClient: BspClient): Unit = - for (targetId <- bspServer.targetIds) - inputs.flattened().foreach { - case f: SingleFile => - localClient.resetDiagnostics(f.path, targetId) - case _: Virtual => - } + val inputsHash: String = inputs.sourceHash + + def resetDiagnostics(localClient: BspClient): Unit = for { + module <- inputs.modules + targetId <- bspServer.targetProjectIdOpt(module.projectName) + } do + module.flattened().foreach { + case f: SingleFile => + localClient.resetDiagnostics(f.path, targetId) + case _: Virtual => + } + def dispose(): Unit = { watcher.dispose() remoteServer.shutdown() } - def registerWatchInputs(): Unit = - inputs.elements.foreach { + def registerWatchInputs(): Unit = for { + module <- inputs.modules + element <- module.elements + } do + element match { case elem: OnDisk => val eventFilter: PathWatchers.Event => Boolean = { event => val newOrDeletedFile = @@ -37,8 +44,11 @@ final class BloopSession( lazy val p = os.Path(event.getTypedPath.getPath.toAbsolutePath) lazy val relPath = p.relativeTo(elem.path) lazy val isHidden = relPath.segments.exists(_.startsWith(".")) - def isScalaFile = relPath.last.endsWith(".sc") || relPath.last.endsWith(".scala") - def isJavaFile = relPath.last.endsWith(".java") + + def isScalaFile = relPath.last.endsWith(".sc") || relPath.last.endsWith(".scala") + + def isJavaFile = relPath.last.endsWith(".java") + newOrDeletedFile && !isHidden && (isScalaFile || isJavaFile) } val watcher0 = watcher.newWatcher() @@ -56,11 +66,11 @@ final class BloopSession( object BloopSession { def apply( - inputs: Inputs, + inputs: compose.Inputs, remoteServer: BloopCompiler, bspServer: BspServer, watcher: Build.Watcher - ): BloopSession = new BloopSession(inputs, inputs.sourceHash(), remoteServer, bspServer, watcher) + ): BloopSession = new BloopSession(inputs, remoteServer, bspServer, watcher) final class Reference { private val ref = new AtomicReference[BloopSession](null) diff --git a/modules/build/src/main/scala/scala/build/bsp/Bsp.scala b/modules/build/src/main/scala/scala/build/bsp/Bsp.scala index a253098d8d..01dee1e4ef 100644 --- a/modules/build/src/main/scala/scala/build/bsp/Bsp.scala +++ b/modules/build/src/main/scala/scala/build/bsp/Bsp.scala @@ -3,17 +3,17 @@ package scala.build.bsp import java.io.{InputStream, OutputStream} import scala.build.errors.BuildException -import scala.build.input.{Inputs, ScalaCliInvokeData} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, compose} import scala.concurrent.Future trait Bsp { - def run(initialInputs: Inputs, initialBspOptions: BspReloadableOptions): Future[Unit] + def run(initialInputs: compose.Inputs, initialBspOptions: BspReloadableOptions): Future[Unit] def shutdown(): Unit } object Bsp { def create( - argsToInputs: Seq[String] => Either[BuildException, Inputs], + argsToInputs: Seq[String] => Either[BuildException, compose.Inputs], bspReloadableOptionsReference: BspReloadableOptions.Reference, threads: BspThreads, in: InputStream, diff --git a/modules/build/src/main/scala/scala/build/bsp/BspClient.scala b/modules/build/src/main/scala/scala/build/bsp/BspClient.scala index fa6499053f..99cd6cab1f 100644 --- a/modules/build/src/main/scala/scala/build/bsp/BspClient.scala +++ b/modules/build/src/main/scala/scala/build/bsp/BspClient.scala @@ -10,6 +10,7 @@ import java.nio.file.Paths import java.util.concurrent.{ConcurrentHashMap, ExecutorService} import scala.build.Position.File +import scala.build.bsp.buildtargets.{ManagesBuildTargets, ManagesBuildTargetsImpl} import scala.build.bsp.protocol.TextEdit import scala.build.errors.{BuildException, CompositeBuildException, Diagnostic, Severity} import scala.build.internal.util.WarningMessages @@ -21,7 +22,7 @@ class BspClient( @volatile var logger: Logger, var forwardToOpt: Option[b.BuildClient] = None ) extends b.BuildClient with BuildClientForwardStubs with BloopBuildClient - with HasGeneratedSourcesImpl { + with ManagesBuildTargetsImpl { private def updatedPublishDiagnosticsParams( params: b.PublishDiagnosticsParams, @@ -98,9 +99,10 @@ class BspClient( } private def actualBuildPublishDiagnostics(params: b.PublishDiagnosticsParams): Unit = { - val updatedParamsOpt = targetScopeOpt(params.getBuildTarget).flatMap { scope => - generatedSources.getOrElse(scope, HasGeneratedSources.GeneratedSources(Nil)) - .uriMap + val updatedParamsOpt = targetProjectNameOpt(params.getBuildTarget).flatMap { projectName => + val uriMap = managedTargets(projectName).uriMap + + uriMap .get(params.getTextDocument.getUri) .map { genSource => updatedPublishDiagnosticsParams(params, genSource) diff --git a/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala b/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala index 9313ea8990..eb74f8acf4 100644 --- a/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala +++ b/modules/build/src/main/scala/scala/build/bsp/BspImpl.scala @@ -13,6 +13,7 @@ import java.util.concurrent.{CompletableFuture, Executor} import scala.build.EitherCps.{either, value} import scala.build.* +import scala.build.bsp.buildtargets.{ManagesBuildTargets, ProjectName} import scala.build.compiler.BloopCompiler import scala.build.errors.{ BuildException, @@ -20,7 +21,7 @@ import scala.build.errors.{ Diagnostic, ParsingInputsException } -import scala.build.input.{Inputs, ScalaCliInvokeData} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, compose} import scala.build.internal.Constants import scala.build.options.{BuildOptions, Scope} import scala.collection.mutable.ListBuffer @@ -32,7 +33,7 @@ import scala.util.{Failure, Success} /** The implementation for [[Bsp]] command. * * @param argsToInputs - * a function transforming terminal args to [[Inputs]] + * a function transforming terminal args to [[ModuleInputs]] * @param bspReloadableOptionsReference * reference to the current instance of [[BspReloadableOptions]] * @param threads @@ -43,7 +44,7 @@ import scala.util.{Failure, Success} * the output stream of bytes */ final class BspImpl( - argsToInputs: Seq[String] => Either[BuildException, Inputs], + argsToInputs: Seq[String] => Either[BuildException, compose.Inputs], bspReloadableOptionsReference: BspReloadableOptions.Reference, threads: BspThreads, in: InputStream, @@ -51,7 +52,13 @@ final class BspImpl( actionableDiagnostics: Option[Boolean] )(using ScalaCliInvokeData) extends Bsp { - import BspImpl.{PreBuildData, PreBuildProject, buildTargetIdToEvent, responseError} + import BspImpl.{ + PreBuildData, + PreBuildModule, + PreBuildProject, + buildTargetIdToEvent, + responseError + } private val shownGlobalMessages = new java.util.concurrent.ConcurrentHashMap[String, Unit]() @@ -67,12 +74,11 @@ final class BspImpl( */ private def notifyBuildChange(currentBloopSession: BloopSession): Unit = { val events = - for (targetId <- currentBloopSession.bspServer.targetIds) - yield { - val event = new b.BuildTargetEvent(targetId) - event.setKind(b.BuildTargetEventKind.CHANGED) - event - } + for (targetId <- currentBloopSession.bspServer.targetIds) yield { + val event = new b.BuildTargetEvent(targetId) + event.setKind(b.BuildTargetEventKind.CHANGED) + event + } val params = new b.DidChangeBuildTarget(events.asJava) actualLocalClient.onBuildTargetDidChange(params) } @@ -90,158 +96,178 @@ final class BspImpl( private def prepareBuild( currentBloopSession: BloopSession, reloadableOptions: BspReloadableOptions, - maybeRecoverOnError: Scope => BuildException => Option[BuildException] = _ => e => Some(e) - ): Either[(BuildException, Scope), PreBuildProject] = either[(BuildException, Scope)] { - val logger = reloadableOptions.logger - val buildOptions = reloadableOptions.buildOptions - val verbosity = reloadableOptions.verbosity - logger.log("Preparing build") - - val persistentLogger = new PersistentDiagnosticLogger(logger) - val bspServer = currentBloopSession.bspServer - val inputs = currentBloopSession.inputs - - // allInputs contains elements from using directives - val (crossSources, allInputs) = value { - CrossSources.forInputs( - inputs = inputs, - preprocessors = Sources.defaultPreprocessors( - buildOptions.archiveCache, - buildOptions.internal.javaClassNameVersionOpt, - () => buildOptions.javaHome().value.javaCommand - ), - logger = persistentLogger, - suppressWarningOptions = buildOptions.suppressWarningOptions, - exclude = buildOptions.internal.exclude, - maybeRecoverOnError = maybeRecoverOnError(Scope.Main) - ).left.map((_, Scope.Main)) - } + maybeRecoverOnError: ProjectName => BuildException => Option[BuildException] = _ => e => Some(e) + ): Either[(BuildException, ProjectName), PreBuildProject] = + either[(BuildException, ProjectName)] { + val logger = reloadableOptions.logger + val buildOptions = reloadableOptions.buildOptions + val verbosity = reloadableOptions.verbosity + logger.log("Preparing build") + + val persistentLogger = new PersistentDiagnosticLogger(logger) + val bspServer = currentBloopSession.bspServer + + val prebuildModules = for (module <- currentBloopSession.inputs.modules) yield { + val mainProjectName = module.projectName + val testProjectName = module.scopeProjectName(Scope.Test) + + // allInputs contains elements from using directives + val (crossSources, allInputs) = value { + CrossSources.forModuleInputs( + inputs = module, + preprocessors = Sources.defaultPreprocessors( + buildOptions.archiveCache, + buildOptions.internal.javaClassNameVersionOpt, + () => buildOptions.javaHome().value.javaCommand + ), + logger = persistentLogger, + suppressWarningOptions = buildOptions.suppressWarningOptions, + exclude = buildOptions.internal.exclude, + maybeRecoverOnError = maybeRecoverOnError(mainProjectName) + ).left.map(_ -> mainProjectName) + } - val sharedOptions = crossSources.sharedOptions(buildOptions) + val sharedOptions = crossSources.sharedOptions(buildOptions) - if (verbosity >= 3) - pprint.err.log(crossSources) + if (verbosity >= 3) + pprint.err.log(crossSources) - val scopedSources = - value(crossSources.scopedSources(buildOptions).left.map((_, Scope.Main))) + val scopedSources = + value(crossSources.scopedSources(buildOptions).left.map(_ -> mainProjectName)) - if (verbosity >= 3) - pprint.err.log(scopedSources) + if (verbosity >= 3) + pprint.err.log(scopedSources) - val sourcesMain = value { - scopedSources.sources(Scope.Main, sharedOptions, allInputs.workspace, persistentLogger) - .left.map((_, Scope.Main)) - } + val sourcesMain = value { + scopedSources.sources(Scope.Main, sharedOptions, allInputs.workspace, persistentLogger) + .left.map(_ -> mainProjectName) + } - val sourcesTest = value { - scopedSources.sources(Scope.Test, sharedOptions, allInputs.workspace, persistentLogger) - .left.map((_, Scope.Test)) - } + val sourcesTest = value { + scopedSources.sources(Scope.Test, sharedOptions, allInputs.workspace, persistentLogger) + .left.map(_ -> testProjectName) + } - if (verbosity >= 3) - pprint.err.log(sourcesMain) - - val options0Main = sourcesMain.buildOptions - val options0Test = sourcesTest.buildOptions.orElse(options0Main) - - val generatedSourcesMain = sourcesMain.generateSources(allInputs.generatedSrcRoot(Scope.Main)) - val generatedSourcesTest = sourcesTest.generateSources(allInputs.generatedSrcRoot(Scope.Test)) - - bspServer.setExtraDependencySources(options0Main.classPathOptions.extraSourceJars) - bspServer.setExtraTestDependencySources(options0Test.classPathOptions.extraSourceJars) - bspServer.setGeneratedSources(Scope.Main, generatedSourcesMain) - bspServer.setGeneratedSources(Scope.Test, generatedSourcesTest) - - val (classesDir0Main, scalaParamsMain, artifactsMain, projectMain, buildChangedMain) = value { - val res = Build.prepareBuild( - allInputs, - sourcesMain, - generatedSourcesMain, - options0Main, - None, - Scope.Main, - currentBloopSession.remoteServer, - persistentLogger, - localClient, - maybeRecoverOnError(Scope.Main) - ) - res.left.map((_, Scope.Main)) - } + if (verbosity >= 3) + pprint.err.log(sourcesMain) + + val options0Main = sourcesMain.buildOptions + val options0Test = sourcesTest.buildOptions.orElse(options0Main) + + val generatedSourcesMain = + sourcesMain.generateSources(allInputs.generatedSrcRoot(Scope.Main)) + val generatedSourcesTest = + sourcesTest.generateSources(allInputs.generatedSrcRoot(Scope.Test)) + + bspServer.setExtraDependencySources(options0Main.classPathOptions.extraSourceJars) + bspServer.setExtraTestDependencySources(options0Test.classPathOptions.extraSourceJars) + bspServer.setGeneratedSources(mainProjectName, generatedSourcesMain) + bspServer.setGeneratedSources(testProjectName, generatedSourcesTest) + + val (classesDir0Main, scalaParamsMain, artifactsMain, projectMain, buildChangedMain) = + value { + val res = Build.prepareBuild( + allInputs, + sourcesMain, + generatedSourcesMain, + options0Main, + None, + Scope.Main, + currentBloopSession.remoteServer, + persistentLogger, + localClient, + maybeRecoverOnError(mainProjectName) + ) + res.left.map(_ -> mainProjectName) + } - val (classesDir0Test, scalaParamsTest, artifactsTest, projectTest, buildChangedTest) = value { - val res = Build.prepareBuild( - allInputs, - sourcesTest, - generatedSourcesTest, - options0Test, - None, - Scope.Test, - currentBloopSession.remoteServer, - persistentLogger, - localClient, - maybeRecoverOnError(Scope.Test) - ) - res.left.map((_, Scope.Test)) - } + val (classesDir0Test, scalaParamsTest, artifactsTest, projectTest, buildChangedTest) = + value { + val res = Build.prepareBuild( + allInputs, + sourcesTest, + generatedSourcesTest, + options0Test, + None, + Scope.Test, + currentBloopSession.remoteServer, + persistentLogger, + localClient, + maybeRecoverOnError(testProjectName) + ) + res.left.map(_ -> testProjectName) + } - localClient.setGeneratedSources(Scope.Main, generatedSourcesMain) - localClient.setGeneratedSources(Scope.Test, generatedSourcesTest) - - val mainScope = PreBuildData( - sourcesMain, - options0Main, - classesDir0Main, - scalaParamsMain, - artifactsMain, - projectMain, - generatedSourcesMain, - buildChangedMain - ) + localClient.setGeneratedSources(mainProjectName, generatedSourcesMain) + localClient.setGeneratedSources(testProjectName, generatedSourcesTest) + + val mainScope = PreBuildData( + sourcesMain, + options0Main, + classesDir0Main, + scalaParamsMain, + artifactsMain, + projectMain, + generatedSourcesMain, + buildChangedMain + ) - val testScope = PreBuildData( - sourcesTest, - options0Test, - classesDir0Test, - scalaParamsTest, - artifactsTest, - projectTest, - generatedSourcesTest, - buildChangedTest - ) + val testScope = PreBuildData( + sourcesTest, + options0Test, + classesDir0Test, + scalaParamsTest, + artifactsTest, + projectTest, + generatedSourcesTest, + buildChangedTest + ) - if (actionableDiagnostics.getOrElse(true)) { - val projectOptions = options0Test.orElse(options0Main) - projectOptions.logActionableDiagnostics(persistentLogger) - } + if (actionableDiagnostics.getOrElse(true)) { + val projectOptions = options0Test.orElse(options0Main) + projectOptions.logActionableDiagnostics(persistentLogger) + } - PreBuildProject(mainScope, testScope, persistentLogger.diagnostics) - } + PreBuildModule(module, mainScope, testScope, persistentLogger.diagnostics) + } + + PreBuildProject(prebuildModules) + } private def buildE( currentBloopSession: BloopSession, notifyChanges: Boolean, reloadableOptions: BspReloadableOptions - ): Either[(BuildException, Scope), Unit] = { - def doBuildOnce(data: PreBuildData, scope: Scope): Either[(BuildException, Scope), Build] = + ): Either[(BuildException, ProjectName), Unit] = { + def doBuildOnce( + moduleInputs: ModuleInputs, + data: PreBuildData, + scope: Scope + ): Either[(BuildException, ProjectName), Build] = Build.buildOnce( - currentBloopSession.inputs, - data.sources, - data.generatedSources, - data.buildOptions, - scope, - reloadableOptions.logger, - actualLocalClient, - currentBloopSession.remoteServer, + inputs = moduleInputs, + sources = data.sources, + generatedSources = data.generatedSources, + options = data.buildOptions, + scope = scope, + logger = reloadableOptions.logger, + buildClient = actualLocalClient, + compiler = currentBloopSession.remoteServer, partialOpt = None - ).left.map(_ -> scope) + ).left.map(_ -> moduleInputs.scopeProjectName(scope)) - either[(BuildException, Scope)] { + either[(BuildException, ProjectName)] { val preBuild = value(prepareBuild(currentBloopSession, reloadableOptions)) - if (notifyChanges && (preBuild.mainScope.buildChanged || preBuild.testScope.buildChanged)) - notifyBuildChange(currentBloopSession) - value(doBuildOnce(preBuild.mainScope, Scope.Main)) - value(doBuildOnce(preBuild.testScope, Scope.Test)) - () + for (preBuildModule <- preBuild.prebuildModules) do { + val moduleInputs = preBuildModule.inputs + // TODO notify only specific build target + if ( + notifyChanges && (preBuildModule.mainScope.buildChanged || preBuildModule.testScope.buildChanged) + ) + notifyBuildChange(currentBloopSession) + value(doBuildOnce(moduleInputs, preBuildModule.mainScope, Scope.Main)) + value(doBuildOnce(moduleInputs, preBuildModule.testScope, Scope.Test)) + } } } @@ -252,9 +278,9 @@ final class BspImpl( reloadableOptions: BspReloadableOptions ): Unit = buildE(currentBloopSession, notifyChanges, reloadableOptions) match { - case Left((ex, scope)) => + case Left((ex, projectName)) => client.reportBuildException( - currentBloopSession.bspServer.targetScopeIdOpt(scope), + currentBloopSession.bspServer.targetProjectIdOpt(projectName), ex ) reloadableOptions.logger.debug(s"Caught $ex during BSP build, ignoring it") @@ -295,28 +321,25 @@ final class BspImpl( () => prepareBuild(currentBloopSession, reloadableOptions) match { case Right(preBuild) => - if (preBuild.mainScope.buildChanged || preBuild.testScope.buildChanged) - notifyBuildChange(currentBloopSession) + for (preBuildModule <- preBuild.prebuildModules) do + if (preBuildModule.mainScope.buildChanged || preBuildModule.testScope.buildChanged) + notifyBuildChange(currentBloopSession) + Right(preBuild) - case Left((ex, scope)) => - Left((ex, scope)) + case Left((ex, projectName)) => + Left((ex, projectName)) }, executor ) preBuild.thenCompose { - case Left((ex, scope)) => + case Left((ex, projectName)) => val taskId = new b.TaskId(UUID.randomUUID().toString) - for targetId <- currentBloopSession.bspServer.targetScopeIdOpt(scope) do { - val target = targetId.getUri match { - case s"$_?id=$targetId" => targetId - case targetIdUri => targetIdUri - } - + for targetId <- currentBloopSession.bspServer.targetProjectIdOpt(projectName) do { val taskStartParams = new b.TaskStartParams(taskId) taskStartParams.setEventTime(System.currentTimeMillis()) - taskStartParams.setMessage(s"Preprocessing '$target'") + taskStartParams.setMessage(s"Preprocessing '$projectName'") taskStartParams.setDataKind(b.TaskStartDataKind.COMPILE_TASK) taskStartParams.setData(new b.CompileTask(targetId)) @@ -329,7 +352,7 @@ final class BspImpl( val taskFinishParams = new b.TaskFinishParams(taskId, b.StatusCode.ERROR) taskFinishParams.setEventTime(System.currentTimeMillis()) - taskFinishParams.setMessage(s"Preprocessed '$target'") + taskFinishParams.setMessage(s"Preprocessed '$projectName'") taskFinishParams.setDataKind(b.TaskFinishDataKind.COMPILE_REPORT) val errorSize = ex match { @@ -349,15 +372,24 @@ final class BspImpl( for (targetId <- currentBloopSession.bspServer.targetIds) actualLocalClient.resetBuildExceptionDiagnostics(targetId) - val targetId = currentBloopSession.bspServer.targetIds.head - actualLocalClient.reportDiagnosticsForFiles(targetId, params.diagnostics, reset = false) + for { + preBuildModule <- params.prebuildModules + targetId <- currentBloopSession.bspServer + .targetProjectIdOpt(preBuildModule.inputs.projectName) + .toSeq + } do + actualLocalClient.reportDiagnosticsForFiles( + targetId, + preBuildModule.diagnostics, + reset = false + ) doCompile().thenCompose { res => - def doPostProcess(data: PreBuildData, scope: Scope): Unit = + def doPostProcess(inputs: ModuleInputs, data: PreBuildData, scope: Scope): Unit = for (sv <- data.project.scalaCompiler.map(_.scalaVersion)) Build.postProcess( data.generatedSources, - currentBloopSession.inputs.generatedSrcRoot(scope), + inputs.generatedSrcRoot(scope), data.classesDir, reloadableOptions.logger, currentBloopSession.inputs.workspace, @@ -369,8 +401,11 @@ final class BspImpl( if (res.getStatusCode == b.StatusCode.OK) CompletableFuture.supplyAsync( () => { - doPostProcess(params.mainScope, Scope.Main) - doPostProcess(params.testScope, Scope.Test) + for (preBuildModule <- params.prebuildModules) do { + val moduleInputs = preBuildModule.inputs + doPostProcess(moduleInputs, preBuildModule.mainScope, Scope.Main) + doPostProcess(moduleInputs, preBuildModule.testScope, Scope.Test) + } res }, executor @@ -388,7 +423,7 @@ final class BspImpl( * BSP client */ private def getLocalClient(verbosity: Int): b.BuildClient with BloopBuildClient = - if (verbosity >= 3) + if (verbosity >= 2) new BspImpl.LoggingBspClient(actualLocalClient) else actualLocalClient @@ -405,20 +440,21 @@ final class BspImpl( * a new [[BloopSession]] */ private def newBloopSession( - inputs: Inputs, + inputs: compose.Inputs, reloadableOptions: BspReloadableOptions, presetIntelliJ: Boolean = false ): BloopSession = { val logger = reloadableOptions.logger val buildOptions = reloadableOptions.buildOptions + val workspace = inputs.workspace val createBloopServer = () => BloopServer.buildServer( reloadableOptions.bloopRifleConfig, "scala-cli", Constants.version, - (inputs.workspace / Constants.workspaceDirName).toNIO, - Build.classesRootDir(inputs.workspace, inputs.projectName).toNIO, + (workspace / Constants.workspaceDirName).toNIO, + Build.classesRootDir(workspace, inputs.modules.head.projectName).toNIO, localClient, threads.buildThreads.bloop, logger.bloopRifleLogger @@ -430,6 +466,7 @@ final class BspImpl( ) lazy val bspServer = new BspServer( remoteServer.bloopServer.server, + localClient, doCompile => compile(bloopSession0, threads.prepareBuildExecutor, reloadableOptions, doCompile), logger, @@ -455,7 +492,10 @@ final class BspImpl( * the initial input sources passed upon initializing the BSP connection (which are subject to * change on subsequent workspace/reload requests) */ - override def run(initialInputs: Inputs, initialBspOptions: BspReloadableOptions): Future[Unit] = { + override def run( + initialInputs: compose.Inputs, + initialBspOptions: BspReloadableOptions + ): Future[Unit] = { val logger = initialBspOptions.logger val verbosity = initialBspOptions.verbosity @@ -470,14 +510,14 @@ final class BspImpl( with b.JavaBuildServer with b.JvmBuildServer with ScalaScriptBuildServer - with HasGeneratedSources = new BuildServerProxy( + with ManagesBuildTargets = new BuildServerProxy( () => bloopSession.get().bspServer, () => onReload() ) val localServer: b.BuildServer with b.ScalaBuildServer with b.JavaBuildServer with b.JvmBuildServer with ScalaScriptBuildServer = - if (verbosity >= 3) + if (verbosity >= 2) new LoggingBuildServerAll(actualLocalServer) else actualLocalServer @@ -495,9 +535,9 @@ final class BspImpl( actualLocalClient.newInputs(initialInputs) currentBloopSession.resetDiagnostics(actualLocalClient) - val recoverOnError: Scope => BuildException => Option[BuildException] = scope => + val recoverOnError: ProjectName => BuildException => Option[BuildException] = projectName => e => { - actualLocalClient.reportBuildException(actualLocalServer.targetScopeIdOpt(scope), e) + actualLocalClient.reportBuildException(actualLocalServer.targetProjectIdOpt(projectName), e) logger.log(e) None } @@ -507,8 +547,8 @@ final class BspImpl( initialBspOptions, maybeRecoverOnError = recoverOnError ) match { - case Left((ex, scope)) => recoverOnError(scope)(ex) - case Right(_) => + case Left((ex, projectName)) => recoverOnError(projectName)(ex) + case Right(_) => } logger.log { @@ -557,8 +597,8 @@ final class BspImpl( */ private def reloadBsp( currentBloopSession: BloopSession, - previousInputs: Inputs, - newInputs: Inputs, + previousInputs: compose.Inputs, + newInputs: compose.Inputs, reloadableOptions: BspReloadableOptions ): CompletableFuture[AnyRef] = { val previousTargetIds = currentBloopSession.bspServer.targetIds @@ -578,9 +618,12 @@ final class BspImpl( ) ) case Right(preBuildProject) => - lazy val projectJavaHome = preBuildProject.mainScope.buildOptions - .javaHome() - .value + lazy val projectJavaHome = { + val projectBuildOptions = preBuildProject.prebuildModules + .flatMap(m => Seq(m.mainScope.buildOptions, m.testScope.buildOptions)) + + BuildOptions.pickJavaHomeWithHighestVersion(projectBuildOptions) + } val finalBloopSession = if ( @@ -590,10 +633,8 @@ final class BspImpl( s"Bloop JVM version too low, current ${bloopSession.get().remoteServer.jvmVersion.get .value} expected ${projectJavaHome.version}, restarting server" ) - // RelodableOptions don't take into account buildOptions from sources + // ReloadableOptions don't take into account buildOptions from sources, so we need to update the bloopRifleConfig val updatedReloadableOptions = reloadableOptions.copy( - buildOptions = - reloadableOptions.buildOptions orElse preBuildProject.mainScope.buildOptions, bloopRifleConfig = reloadableOptions.bloopRifleConfig.copy( javaPath = projectJavaHome.javaCommand, minimumBloopJvm = projectJavaHome.version @@ -612,15 +653,22 @@ final class BspImpl( } else newBloopSession0 - if (previousInputs.projectName != preBuildProject.mainScope.project.projectName) - for (client <- finalBloopSession.bspServer.clientOpt) { - val newTargetIds = finalBloopSession.bspServer.targetIds - val events = - newTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.CREATED)) ++ - previousTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.DELETED)) - val didChangeBuildTargetParams = new b.DidChangeBuildTarget(events.asJava) - client.onBuildTargetDidChange(didChangeBuildTargetParams) - } + val previousProjectNames = previousInputs.modules.flatMap(m => + Seq(m.scopeProjectName(Scope.Main), m.scopeProjectName(Scope.Test)) + ).toSet + val newProjectNames = newInputs.modules.flatMap(m => + Seq(m.scopeProjectName(Scope.Main), m.scopeProjectName(Scope.Test)) + ).toSet + + if (previousProjectNames != newProjectNames) { + val client = finalBloopSession.bspServer.bspCLient + val newTargetIds = finalBloopSession.bspServer.targetIds + val events = + newTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.CREATED)) ++ + previousTargetIds.map(buildTargetIdToEvent(_, b.BuildTargetEventKind.DELETED)) + val didChangeBuildTargetParams = new b.DidChangeBuildTarget(events.asJava) + client.onBuildTargetDidChange(didChangeBuildTargetParams) + } CompletableFuture.completedFuture(new Object()) } } @@ -652,12 +700,14 @@ final class BspImpl( } } val newInputs = value(argsToInputs(ideInputs.args)) - val newHash = newInputs.sourceHash() val previousInputs = currentBloopSession.inputs - val previousHash = currentBloopSession.inputsHash + + val newHash = newInputs.sourceHash + val previousHash = currentBloopSession.inputsHash if newInputs == previousInputs && newHash == previousHash then CompletableFuture.completedFuture(new Object) - else reloadBsp(currentBloopSession, previousInputs, newInputs, reloadableOptions) + else + reloadBsp(currentBloopSession, previousInputs, newInputs, reloadableOptions) } maybeResponse match { case Left(errorMessage) => @@ -719,8 +769,8 @@ object BspImpl { def diagnostics = underlying.diagnostics def setProjectParams(newParams: Seq[String]) = underlying.setProjectParams(newParams) - def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]) = - underlying.setGeneratedSources(scope, newGeneratedSources) + def setGeneratedSources(projectName: ProjectName, newGeneratedSources: Seq[GeneratedSource]) = + underlying.setGeneratedSources(projectName, newGeneratedSources) } private final case class PreBuildData( @@ -734,7 +784,10 @@ object BspImpl { buildChanged: Boolean ) - private final case class PreBuildProject( + private final case class PreBuildProject(prebuildModules: Seq[PreBuildModule]) + + private final case class PreBuildModule( + inputs: ModuleInputs, mainScope: PreBuildData, testScope: PreBuildData, diagnostics: Seq[Diagnostic] diff --git a/modules/build/src/main/scala/scala/build/bsp/BspReloadableOptions.scala b/modules/build/src/main/scala/scala/build/bsp/BspReloadableOptions.scala index 1ed30feee0..72ab51d788 100644 --- a/modules/build/src/main/scala/scala/build/bsp/BspReloadableOptions.scala +++ b/modules/build/src/main/scala/scala/build/bsp/BspReloadableOptions.scala @@ -5,7 +5,10 @@ import bloop.rifle.BloopRifleConfig import scala.build.Logger import scala.build.options.BuildOptions -/** The options and configurations that may be picked up on a bsp workspace/reload request. +/** The options and configurations that may be picked up on a bsp workspace/reload request. They + * don't take into account options from sources. The only two exceptions are the initial options in + * BspImpl.run and in options used to launch new bloop in BspImpl.reloadBsp, which have the + * [[bloopRifleConfig]] updated. * * @param buildOptions * passed options for building sources diff --git a/modules/build/src/main/scala/scala/build/bsp/BspServer.scala b/modules/build/src/main/scala/scala/build/bsp/BspServer.scala index 375dd555f2..98ca4d8174 100644 --- a/modules/build/src/main/scala/scala/build/bsp/BspServer.scala +++ b/modules/build/src/main/scala/scala/build/bsp/BspServer.scala @@ -9,6 +9,7 @@ import java.util.concurrent.{CompletableFuture, TimeUnit} import java.util as ju import scala.build.Logger +import scala.build.bsp.buildtargets.{ManagesBuildTargets, ManagesBuildTargetsImpl, ProjectName} import scala.build.internal.Constants import scala.build.options.Scope import scala.concurrent.{Future, Promise} @@ -17,6 +18,7 @@ import scala.util.Random class BspServer( bloopServer: b.BuildServer & b.ScalaBuildServer & b.JavaBuildServer & b.JvmBuildServer, + client: BuildClient, compile: (() => CompletableFuture[b.CompileResult]) => CompletableFuture[b.CompileResult], logger: Logger, presetIntelliJ: Boolean = false @@ -25,15 +27,13 @@ class BspServer( with ScalaBuildServerForwardStubs with JavaBuildServerForwardStubs with JvmBuildServerForwardStubs - with HasGeneratedSourcesImpl { + with ManagesBuildTargetsImpl { - private var client: Option[BuildClient] = None + val bspCLient = client @volatile private var intelliJ: Boolean = presetIntelliJ def isIntelliJ: Boolean = intelliJ - def clientOpt: Option[BuildClient] = client - @volatile private var extraDependencySources: Seq[os.Path] = Nil def setExtraDependencySources(sourceJars: Seq[os.Path]): Unit = { extraDependencySources = sourceJars @@ -51,7 +51,7 @@ class BspServer( val message = s"Fatal error has occured within $context. Shutting down the server:\n ${sw.toString}" System.err.println(message) - client.foreach(_.onBuildLogMessage(new LogMessageParams(MessageType.ERROR, message))) + client.onBuildLogMessage(new LogMessageParams(MessageType.ERROR, message)) // wait random bit before shutting down server to reduce risk of multiple scala-cli instances starting bloop at the same time val timeout = Random.nextInt(400) @@ -59,13 +59,6 @@ class BspServer( sys.exit(1) } - private def maybeUpdateProjectTargetUri(res: b.WorkspaceBuildTargetsResult): Unit = - for { - (_, n) <- projectNames.iterator - if n.targetUriOpt.isEmpty - target <- res.getTargets.asScala.iterator.find(_.getDisplayName == n.name) - } n.targetUriOpt = Some(target.getId.getUri) - private def stripInvalidTargets(params: b.WorkspaceBuildTargetsResult): Unit = { val updatedTargets = params .getTargets @@ -124,12 +117,12 @@ class BspServer( params } private def mapGeneratedSources(res: b.SourcesResult): Unit = { - val gen = generatedSources.values.toVector + val gen = managedTargets.values.map(_.uriMap).toVector for { item <- res.getItems.asScala if validTarget(item.getTarget) sourceItem <- item.getSources.asScala - genSource <- gen.iterator.flatMap(_.uriMap.get(sourceItem.getUri).iterator).take(1) + genSource <- gen.iterator.flatMap(_.get(sourceItem.getUri).iterator).take(1) updatedUri <- genSource.reportingPath.toOption.map(_.toNIO.toUri.toASCIIString) } { sourceItem.setUri(updatedUri) @@ -137,7 +130,7 @@ class BspServer( } // GeneratedSources not corresponding to files that exist on disk (unlike script wrappers) - val sourcesWithReportingPathString = generatedSources.values.flatMap(_.sources) + val sourcesWithReportingPathString = managedTargets.values.flatMap(_.generatedSources) .filter(_.reportingPath.isLeft) for { @@ -232,7 +225,10 @@ class BspServer( val target = params.getTarget if (!validTarget(target)) logger.debug( - s"Got invalid target in Run request: ${target.getUri} (expected ${targetScopeIdOpt(Scope.Main).orNull})" + s"""Got invalid target in Run request: ${target.getUri}. + |Available build targets: + |${targetIds.mkString(" - ", System.lineSeparator() + " - ", "")} + |""".stripMargin ) super.buildTargetRun(params) } @@ -272,7 +268,6 @@ class BspServer( override def workspaceBuildTargets(): CompletableFuture[b.WorkspaceBuildTargetsResult] = super.workspaceBuildTargets().thenApply { res => - maybeUpdateProjectTargetUri(res) val res0 = res.duplicate() stripInvalidTargets(res0) for (target <- res0.getTargets.asScala) { @@ -292,10 +287,10 @@ class BspServer( def buildTargetWrappedSources(params: WrappedSourcesParams) : CompletableFuture[WrappedSourcesResult] = { - def sourcesItemOpt(scope: Scope) = targetScopeIdOpt(scope).map { id => - val items = generatedSources - .getOrElse(scope, HasGeneratedSources.GeneratedSources(Nil)) - .sources + def wrappedSourceItems(buildTargetId: b.BuildTargetIdentifier): WrappedSourcesItem = { + val items = managedTargets.values.find(_.targetId == buildTargetId) + .map(_.generatedSources) + .getOrElse(Nil) .flatMap { s => s.reportingPath.toSeq.map(_.toNIO.toUri.toASCIIString).map { uri => val item = new WrappedSourceItem(uri, s.generated.toNIO.toUri.toASCIIString) @@ -310,10 +305,12 @@ class BspServer( item } } - new WrappedSourcesItem(id, items.asJava) + new WrappedSourcesItem(buildTargetId, items.asJava) } - val sourceItems = Seq(Scope.Main, Scope.Test).flatMap(sourcesItemOpt(_).toSeq) - val res = new WrappedSourcesResult(sourceItems.asJava) + + val targetsAsked = managedTargets.values.toList.map(_.targetId) + val sourceItems = targetsAsked.map(wrappedSourceItems) + val res = new WrappedSourcesResult(sourceItems.asJava) CompletableFuture.completedFuture(res) } diff --git a/modules/build/src/main/scala/scala/build/bsp/BuildServerProxy.scala b/modules/build/src/main/scala/scala/build/bsp/BuildServerProxy.scala index 2cd444aec2..0e1d631e38 100644 --- a/modules/build/src/main/scala/scala/build/bsp/BuildServerProxy.scala +++ b/modules/build/src/main/scala/scala/build/bsp/BuildServerProxy.scala @@ -1,11 +1,12 @@ package scala.build.bsp -import ch.epfl.scala.{bsp4j => b} +import ch.epfl.scala.bsp4j as b import java.util.concurrent.CompletableFuture import scala.build.GeneratedSource -import scala.build.input.Inputs +import scala.build.bsp.buildtargets.{ManagesBuildTargets, ProjectName} +import scala.build.input.{ModuleInputs, compose} import scala.build.options.Scope /** A wrapper for [[BspServer]], allowing to reload the workspace on the fly. @@ -18,7 +19,7 @@ class BuildServerProxy( bspServer: () => BspServer, onReload: () => CompletableFuture[Object] ) extends b.BuildServer with b.ScalaBuildServer with b.JavaBuildServer with b.JvmBuildServer - with ScalaScriptBuildServer with HasGeneratedSources { + with ScalaScriptBuildServer with ManagesBuildTargets { override def buildInitialize(params: b.InitializeBuildParams) : CompletableFuture[b.InitializeBuildResult] = bspServer().buildInitialize(params) @@ -99,14 +100,19 @@ class BuildServerProxy( bspServer().buildTargetJvmTestEnvironment(params) def targetIds: List[b.BuildTargetIdentifier] = bspServer().targetIds - def targetScopeIdOpt(scope: Scope): Option[b.BuildTargetIdentifier] = - bspServer().targetScopeIdOpt(scope) - def setGeneratedSources(scope: Scope, sources: Seq[GeneratedSource]): Unit = - bspServer().setGeneratedSources(scope, sources) - def setProjectName(workspace: os.Path, name: String, scope: Scope): Unit = - bspServer().setProjectName(workspace, name, scope) - def resetProjectNames(): Unit = - bspServer().resetProjectNames() - def newInputs(inputs: Inputs): Unit = + def targetProjectIdOpt(projectName: ProjectName): Option[b.BuildTargetIdentifier] = + bspServer().targetProjectIdOpt(projectName) + def setGeneratedSources(projectName: ProjectName, sources: Seq[GeneratedSource]): Unit = + bspServer().setGeneratedSources(projectName, sources) + def addTarget( + projectName: ProjectName, + workspace: os.Path, + scope: Scope, + generatedSources: Seq[GeneratedSource] = Nil + ): Unit = + bspServer().addTarget(projectName, workspace, scope, generatedSources) + def resetTargets(): Unit = + bspServer().resetTargets() + def newInputs(inputs: compose.Inputs): Unit = bspServer().newInputs(inputs) } diff --git a/modules/build/src/main/scala/scala/build/bsp/HasGeneratedSources.scala b/modules/build/src/main/scala/scala/build/bsp/HasGeneratedSources.scala deleted file mode 100644 index 74cf3ce0f3..0000000000 --- a/modules/build/src/main/scala/scala/build/bsp/HasGeneratedSources.scala +++ /dev/null @@ -1,50 +0,0 @@ -package scala.build.bsp - -import ch.epfl.scala.{bsp4j => b} - -import scala.build.GeneratedSource -import scala.build.input.Inputs -import scala.build.internal.Constants -import scala.build.options.Scope - -trait HasGeneratedSources { - def targetIds: List[b.BuildTargetIdentifier] - def targetScopeIdOpt(scope: Scope): Option[b.BuildTargetIdentifier] - def setProjectName(workspace: os.Path, name: String, scope: Scope): Unit - def resetProjectNames(): Unit - def newInputs(inputs: Inputs): Unit - def setGeneratedSources(scope: Scope, sources: Seq[GeneratedSource]): Unit -} - -object HasGeneratedSources { - final case class GeneratedSources( - sources: Seq[GeneratedSource] - ) { - - lazy val uriMap: Map[String, GeneratedSource] = - sources - .flatMap { g => - g.reportingPath match { - case Left(_) => Nil - case Right(_) => Seq(g.generated.toNIO.toUri.toASCIIString -> g) - } - } - .toMap - } - - final case class ProjectName( - bloopWorkspace: os.Path, - name: String, - var targetUriOpt: Option[String] = None - ) { - targetUriOpt = - Some( - (bloopWorkspace / Constants.workspaceDirName) - .toIO - .toURI - .toASCIIString - .stripSuffix("/") + - "/?id=" + name - ) - } -} diff --git a/modules/build/src/main/scala/scala/build/bsp/HasGeneratedSourcesImpl.scala b/modules/build/src/main/scala/scala/build/bsp/HasGeneratedSourcesImpl.scala deleted file mode 100644 index 4855cbf824..0000000000 --- a/modules/build/src/main/scala/scala/build/bsp/HasGeneratedSourcesImpl.scala +++ /dev/null @@ -1,61 +0,0 @@ -package scala.build.bsp - -import ch.epfl.scala.{bsp4j => b} - -import scala.build.GeneratedSource -import scala.build.input.Inputs -import scala.build.internal.Constants -import scala.build.options.Scope -import scala.collection.mutable - -trait HasGeneratedSourcesImpl extends HasGeneratedSources { - - import HasGeneratedSources._ - - protected val projectNames = mutable.Map[Scope, ProjectName]() - protected val generatedSources = mutable.Map[Scope, GeneratedSources]() - - def targetIds: List[b.BuildTargetIdentifier] = - projectNames - .toList - .sortBy(_._1) - .map(_._2) - .flatMap(_.targetUriOpt) - .map(uri => new b.BuildTargetIdentifier(uri)) - - def targetScopeIdOpt(scope: Scope): Option[b.BuildTargetIdentifier] = - projectNames - .get(scope) - .flatMap(_.targetUriOpt) - .map(uri => new b.BuildTargetIdentifier(uri)) - - def resetProjectNames(): Unit = - projectNames.clear() - def setProjectName(workspace: os.Path, name: String, scope: Scope): Unit = - if (!projectNames.contains(scope)) - projectNames(scope) = ProjectName(workspace, name) - - def newInputs(inputs: Inputs): Unit = { - resetProjectNames() - setProjectName(inputs.workspace, inputs.projectName, Scope.Main) - setProjectName(inputs.workspace, inputs.scopeProjectName(Scope.Test), Scope.Test) - } - - def setGeneratedSources(scope: Scope, sources: Seq[GeneratedSource]): Unit = { - generatedSources(scope) = GeneratedSources(sources) - } - - protected def targetWorkspaceDirOpt(id: b.BuildTargetIdentifier): Option[String] = - projectNames.collectFirst { - case (_, projName) if projName.targetUriOpt.contains(id.getUri) => - (projName.bloopWorkspace / Constants.workspaceDirName).toIO.toURI.toASCIIString - } - protected def targetScopeOpt(id: b.BuildTargetIdentifier): Option[Scope] = - projectNames.collectFirst { - case (scope, projName) if projName.targetUriOpt.contains(id.getUri) => - scope - } - protected def validTarget(id: b.BuildTargetIdentifier): Boolean = - targetScopeOpt(id).nonEmpty - -} diff --git a/modules/build/src/main/scala/scala/build/bsp/buildtargets/ManagesBuildTargets.scala b/modules/build/src/main/scala/scala/build/bsp/buildtargets/ManagesBuildTargets.scala new file mode 100644 index 0000000000..c42a2c77ba --- /dev/null +++ b/modules/build/src/main/scala/scala/build/bsp/buildtargets/ManagesBuildTargets.scala @@ -0,0 +1,86 @@ +package scala.build.bsp.buildtargets + +import ch.epfl.scala.bsp4j.BuildTargetIdentifier +import ch.epfl.scala.bsp4j as b + +import scala.build.GeneratedSource +import scala.build.input.{ModuleInputs, compose} +import scala.build.internal.Constants +import scala.build.options.Scope + +trait ManagesBuildTargets { + def targetIds: List[b.BuildTargetIdentifier] + def targetProjectIdOpt(projectName: ProjectName): Option[b.BuildTargetIdentifier] + def addTarget( + projectName: ProjectName, + workspace: os.Path, + scope: Scope, + generatedSources: Seq[GeneratedSource] = Nil + ): Unit + def resetTargets(): Unit + def newInputs(inputs: compose.Inputs): Unit + def setGeneratedSources(projectName: ProjectName, sources: Seq[GeneratedSource]): Unit +} + +object ManagesBuildTargets { + + /** Represents a BuildTarget managed by the BSP + * @originalSources + * \- paths of sources seen by the user + */ + final case class BuildTarget( + projectName: ProjectName, + bloopWorkspace: os.Path, + scope: Scope, +// originalSources: Seq[os.Path], + generatedSources: Seq[GeneratedSource] + ) { + val targetId: BuildTargetIdentifier = { + val identifier = (bloopWorkspace / Constants.workspaceDirName) + .toIO + .toURI + .toASCIIString + .stripSuffix("/") + + "/?id=" + projectName.name + new b.BuildTargetIdentifier(identifier) + } + + lazy val uriMap: Map[String, GeneratedSource] = + generatedSources + .flatMap { g => + g.reportingPath match { + case Left(_) => Nil + case Right(_) => Seq(g.generated.toNIO.toUri.toASCIIString -> g) + } + } + .toMap + } + + final case class GeneratedSources(sources: Seq[GeneratedSource]) { + lazy val uriMap: Map[String, GeneratedSource] = + sources + .flatMap { g => + g.reportingPath match { + case Left(_) => Nil + case Right(_) => Seq(g.generated.toNIO.toUri.toASCIIString -> g) + } + } + .toMap + } + + final case class OldProjectName( + bloopWorkspace: os.Path, + name: String, + var targetUriOpt: Option[String] = None + ) { + targetUriOpt = + Some( + (bloopWorkspace / Constants.workspaceDirName) + .toIO + .toURI + .toASCIIString + .stripSuffix("/") + + "/?id=" + name + ) + } +} diff --git a/modules/build/src/main/scala/scala/build/bsp/buildtargets/ManagesBuildTargetsImpl.scala b/modules/build/src/main/scala/scala/build/bsp/buildtargets/ManagesBuildTargetsImpl.scala new file mode 100644 index 0000000000..520f94ce17 --- /dev/null +++ b/modules/build/src/main/scala/scala/build/bsp/buildtargets/ManagesBuildTargetsImpl.scala @@ -0,0 +1,68 @@ +package scala.build.bsp.buildtargets + +import ch.epfl.scala.bsp4j as b + +import scala.build.GeneratedSource +import scala.build.bsp.buildtargets.ManagesBuildTargets +import scala.build.errors.{BuildException, WorkspaceError} +import scala.build.input.{ModuleInputs, compose} +import scala.build.internal.Constants +import scala.build.options.Scope +import scala.collection.mutable +import scala.util.Try + +trait ManagesBuildTargetsImpl extends ManagesBuildTargets { + + import ManagesBuildTargets.* + +// protected val projectNames = mutable.Map[Scope, OldProjectName]() +// protected val generatedSources = mutable.Map[Scope, GeneratedSources]() + protected var managedTargets = mutable.Map[ProjectName, BuildTarget]() + + override def targetIds: List[b.BuildTargetIdentifier] = + managedTargets.values.toList.map(_.targetId) + + override def targetProjectIdOpt(projectName: ProjectName): Option[b.BuildTargetIdentifier] = + managedTargets.get(projectName).map(_.targetId) + + override def resetTargets(): Unit = managedTargets.clear() + override def addTarget( + projectName: ProjectName, + workspace: os.Path, + scope: Scope, + generatedSources: Seq[GeneratedSource] = Nil + ): Unit = + managedTargets.put(projectName, BuildTarget(projectName, workspace, scope, generatedSources)) + + override def newInputs(inputs: compose.Inputs): Unit = { + resetTargets() + inputs.modules.foreach { module => + addTarget(module.projectName, module.workspace, Scope.Main) + addTarget(module.scopeProjectName(Scope.Test), module.workspace, Scope.Test) + } + } + override def setGeneratedSources( + projectName: ProjectName, + sources: Seq[GeneratedSource] + ): Unit = { + val buildTarget = Try(managedTargets(projectName)) + // TODO MG + .getOrElse(throw WorkspaceError("No BuildTarget to put generated sources")) + + managedTargets.put(projectName, buildTarget.copy(generatedSources = sources)) + } + + protected def targetWorkspaceDirOpt(id: b.BuildTargetIdentifier): Option[String] = + managedTargets.values.collectFirst { + case b: BuildTarget if b.targetId == id => + (b.bloopWorkspace / Constants.workspaceDirName).toIO.toURI.toASCIIString + } + protected def targetProjectNameOpt(id: b.BuildTargetIdentifier): Option[ProjectName] = + managedTargets.collectFirst { + case projectName -> buildTarget if buildTarget.targetId == id => projectName + } + + protected def validTarget(id: b.BuildTargetIdentifier): Boolean = + targetProjectNameOpt(id).nonEmpty + +} diff --git a/modules/build/src/main/scala/scala/build/bsp/buildtargets/ProjectName.scala b/modules/build/src/main/scala/scala/build/bsp/buildtargets/ProjectName.scala new file mode 100644 index 0000000000..373c4ed09e --- /dev/null +++ b/modules/build/src/main/scala/scala/build/bsp/buildtargets/ProjectName.scala @@ -0,0 +1,8 @@ +package scala.build.bsp.buildtargets + +import scala.build.options.Scope + +final case class ProjectName(name: String) { + def withScopeAppended(scope: Scope): ProjectName = + if scope == Scope.Main then this else copy(name = s"$name-${scope.name.toLowerCase}") +} diff --git a/modules/build/src/main/scala/scala/build/input/Inputs.scala b/modules/build/src/main/scala/scala/build/input/ModuleInputs.scala similarity index 90% rename from modules/build/src/main/scala/scala/build/input/Inputs.scala rename to modules/build/src/main/scala/scala/build/input/ModuleInputs.scala index 572386a179..6185550eaa 100644 --- a/modules/build/src/main/scala/scala/build/input/Inputs.scala +++ b/modules/build/src/main/scala/scala/build/input/ModuleInputs.scala @@ -7,6 +7,7 @@ import java.security.MessageDigest import scala.annotation.tailrec import scala.build.Directories +import scala.build.bsp.buildtargets.ProjectName import scala.build.errors.{BuildException, InputsException, WorkspaceError} import scala.build.input.ElementsUtils.* import scala.build.internal.Constants @@ -16,7 +17,7 @@ import scala.build.preprocessing.SheBang.isShebangScript import scala.util.matching.Regex import scala.util.{Properties, Try} -final case class Inputs( +final case class ModuleInputs( elements: Seq[Element], defaultMainClassElement: Option[Script], workspace: os.Path, @@ -27,6 +28,9 @@ final case class Inputs( allowRestrictedFeatures: Boolean ) { + def withForcedWorkspace(workspacePath: os.Path) = + copy(workspace = workspacePath, workspaceOrigin = Some(WorkspaceOrigin.Forced)) + def isEmpty: Boolean = elements.isEmpty def singleFiles(): Seq[SingleFile] = @@ -51,34 +55,35 @@ final case class Inputs( } private lazy val inputsHash: String = elements.inputsHash - lazy val projectName: String = { + + lazy val projectName: ProjectName = { val needsSuffix = mayAppendHash && (elements match { case Seq(d: Directory) => d.path != workspace case _ => true }) - if needsSuffix then s"$baseProjectName-$inputsHash" else baseProjectName + if needsSuffix then ProjectName(s"$baseProjectName-$inputsHash") + else ProjectName(baseProjectName) } - def scopeProjectName(scope: Scope): String = - if scope == Scope.Main then projectName else s"$projectName-${scope.name}" + def scopeProjectName(scope: Scope): ProjectName = projectName.withScopeAppended(scope) - def add(extraElements: Seq[Element]): Inputs = + def add(extraElements: Seq[Element]): ModuleInputs = if elements.isEmpty then this else copy(elements = (elements ++ extraElements).distinct) - def withElements(elements: Seq[Element]): Inputs = + def withElements(elements: Seq[Element]): ModuleInputs = copy(elements = elements) def generatedSrcRoot(scope: Scope): os.Path = - workspace / Constants.workspaceDirName / projectName / "src_generated" / scope.name + workspace / Constants.workspaceDirName / projectName.name / "src_generated" / scope.name - private def inHomeDir(directories: Directories): Inputs = + private def inHomeDir(directories: Directories): ModuleInputs = copy( workspace = elements.homeWorkspace(directories), mayAppendHash = false, workspaceOrigin = Some(WorkspaceOrigin.HomeDir) ) - def avoid(forbidden: Seq[os.Path], directories: Directories): Inputs = + def avoid(forbidden: Seq[os.Path], directories: Directories): ModuleInputs = if forbidden.exists(workspace.startsWith) then inHomeDir(directories) else this - def checkAttributes(directories: Directories): Inputs = { + def checkAttributes(directories: Directories): ModuleInputs = { @tailrec def existingParent(p: os.Path): Option[os.Path] = if (os.exists(p)) Some(p) @@ -117,17 +122,16 @@ final case class Inputs( } def nativeWorkDir: os.Path = - workspace / Constants.workspaceDirName / projectName / "native" + workspace / Constants.workspaceDirName / projectName.name / "native" def nativeImageWorkDir: os.Path = - workspace / Constants.workspaceDirName / projectName / "native-image" + workspace / Constants.workspaceDirName / projectName.name / "native-image" def libraryJarWorkDir: os.Path = - workspace / Constants.workspaceDirName / projectName / "jar" + workspace / Constants.workspaceDirName / projectName.name / "jar" def docJarWorkDir: os.Path = - workspace / Constants.workspaceDirName / projectName / "doc" - + workspace / Constants.workspaceDirName / projectName.name / "doc" } -object Inputs { +object ModuleInputs { private def forValidatedElems( validElems: Seq[Element], workspace: os.Path, @@ -135,8 +139,9 @@ object Inputs { workspaceOrigin: WorkspaceOrigin, enableMarkdown: Boolean, allowRestrictedFeatures: Boolean, - extraClasspathWasPassed: Boolean - ): Inputs = { + extraClasspathWasPassed: Boolean, + forcedProjectName: Option[ProjectName] + ): ModuleInputs = { assert(extraClasspathWasPassed || validElems.nonEmpty) val allDirs = validElems.collect { case d: Directory => d.path } val updatedElems = validElems.filter { @@ -149,11 +154,11 @@ object Inputs { } // only on-disk scripts need a main class override val defaultMainClassElemOpt = validElems.collectFirst { case script: Script => script } - Inputs( + ModuleInputs( updatedElems, defaultMainClassElemOpt, workspace, - baseName(workspace), + forcedProjectName.fold(baseName(workspace))(_.name), mayAppendHash = needsHash, workspaceOrigin = Some(workspaceOrigin), enableMarkdown = enableMarkdown, @@ -329,8 +334,9 @@ object Inputs { forcedWorkspace: Option[os.Path], enableMarkdown: Boolean, allowRestrictedFeatures: Boolean, - extraClasspathWasPassed: Boolean - )(using invokeData: ScalaCliInvokeData): Either[BuildException, Inputs] = { + extraClasspathWasPassed: Boolean, + forcedProjectName: Option[ProjectName] + )(using invokeData: ScalaCliInvokeData): Either[BuildException, ModuleInputs] = { val validatedArgs: Seq[Either[String, Seq[Element]]] = validateArgs( args, @@ -412,7 +418,8 @@ object Inputs { workspaceOrigin0, enableMarkdown, allowRestrictedFeatures, - extraClasspathWasPassed + extraClasspathWasPassed, + forcedProjectName )) } else @@ -422,7 +429,7 @@ object Inputs { def apply( args: Seq[String], cwd: os.Path, - defaultInputs: () => Option[Inputs] = () => None, + defaultInputs: () => Option[ModuleInputs] = () => None, download: String => Either[String, Array[Byte]] = _ => Left("URL not supported"), stdinOpt: => Option[Array[Byte]] = None, scriptSnippetList: List[String] = List.empty, @@ -433,8 +440,9 @@ object Inputs { forcedWorkspace: Option[os.Path] = None, enableMarkdown: Boolean = false, allowRestrictedFeatures: Boolean, - extraClasspathWasPassed: Boolean - )(using ScalaCliInvokeData): Either[BuildException, Inputs] = + extraClasspathWasPassed: Boolean, + forcedProjectName: Option[ProjectName] = None + )(using ScalaCliInvokeData): Either[BuildException, ModuleInputs] = if ( args.isEmpty && scriptSnippetList.isEmpty && scalaSnippetList.isEmpty && javaSnippetList.isEmpty && markdownSnippetList.isEmpty && !extraClasspathWasPassed @@ -456,13 +464,14 @@ object Inputs { forcedWorkspace, enableMarkdown, allowRestrictedFeatures, - extraClasspathWasPassed + extraClasspathWasPassed, + forcedProjectName ) - def default(): Option[Inputs] = None + def default(): Option[ModuleInputs] = None - def empty(workspace: os.Path, enableMarkdown: Boolean): Inputs = - Inputs( + def empty(workspace: os.Path, enableMarkdown: Boolean): ModuleInputs = + ModuleInputs( elements = Nil, defaultMainClassElement = None, workspace = workspace, @@ -473,9 +482,8 @@ object Inputs { allowRestrictedFeatures = false ) - def empty(projectName: String): Inputs = - Inputs(Nil, None, os.pwd, projectName, false, None, true, false) + def empty(projectName: String): ModuleInputs = + ModuleInputs(Nil, None, os.pwd, projectName, false, None, true, false) def baseName(p: os.Path) = if (p == os.root) "" else p.baseName - } diff --git a/modules/build/src/main/scala/scala/build/input/compose/Inputs.scala b/modules/build/src/main/scala/scala/build/input/compose/Inputs.scala new file mode 100644 index 0000000000..117e1eab65 --- /dev/null +++ b/modules/build/src/main/scala/scala/build/input/compose/Inputs.scala @@ -0,0 +1,55 @@ +package scala.build.input.compose + +import scala.build.bsp.buildtargets.ProjectName +import scala.build.input.{ModuleInputs, WorkspaceOrigin} +import scala.build.options.BuildOptions +import scala.collection.mutable + +sealed trait Inputs { + + def modules: Seq[ModuleInputs] + def workspaceOrigin: Option[WorkspaceOrigin] + def workspace: os.Path + + def preprocessInputs(preprocess: ModuleInputs => (ModuleInputs, BuildOptions)) + : (Inputs, Seq[BuildOptions]) + + def sourceHash: String = + modules.map(_.sourceHash()) + .mkString +} + +/** Result of using [[InputsComposer]] with module config file present */ +case class ComposedInputs( + modules: Seq[ModuleInputs], + workspace: os.Path +) extends Inputs { + + // Forced to be the directory where module config file (modules.yaml) resides + override val workspaceOrigin: Option[WorkspaceOrigin] = Some(WorkspaceOrigin.Forced) + + def preprocessInputs(preprocess: ModuleInputs => (ModuleInputs, BuildOptions)) + : (ComposedInputs, Seq[BuildOptions]) = { + val (preprocessedModules, buildOptions) = + modules.map(preprocess) + .unzip + + copy(modules = preprocessedModules) -> buildOptions + } +} + +/** Essentially a wrapper over a single module, no config file for modules involved */ +case class SimpleInputs( + singleModule: ModuleInputs +) extends Inputs { + override val modules: Seq[ModuleInputs] = Seq(singleModule) + + override val workspace: os.Path = singleModule.workspace + + override val workspaceOrigin: Option[WorkspaceOrigin] = singleModule.workspaceOrigin + + override def preprocessInputs(preprocess: ModuleInputs => (ModuleInputs, BuildOptions)) + : (SimpleInputs, Seq[BuildOptions]) = + val (preprocessedModule, buildOptions) = preprocess(singleModule) + copy(singleModule = preprocessedModule) -> Seq(buildOptions) +} diff --git a/modules/build/src/main/scala/scala/build/input/compose/InputsComposer.scala b/modules/build/src/main/scala/scala/build/input/compose/InputsComposer.scala new file mode 100644 index 0000000000..1f93f5fb42 --- /dev/null +++ b/modules/build/src/main/scala/scala/build/input/compose/InputsComposer.scala @@ -0,0 +1,163 @@ +package scala.build.input.compose + +import toml.Value +import toml.Value.* + +import scala.build.EitherCps.* +import scala.build.EitherSequence +import scala.build.bsp.buildtargets.ProjectName +import scala.build.errors.{BuildException, CompositeBuildException, ModuleConfigurationError} +import scala.build.input.ModuleInputs +import scala.build.input.compose.InputsComposer +import scala.build.internal.Constants +import scala.build.options.BuildOptions +import scala.collection.mutable + +object InputsComposer { + + // TODO errors on corner cases + def findModuleConfig( + args: Seq[String], + cwd: os.Path + ): Either[ModuleConfigurationError, Option[os.Path]] = { + def moduleConfigDirectlyFromArgs = { + val moduleConfigPathOpt = args + .map(arg => os.Path(arg, cwd)) + .find(_.endsWith(os.RelPath(Constants.moduleConfigFileName))) + + moduleConfigPathOpt match { + case Some(path) if os.exists(path) => Right(Some(path)) + case Some(path) => Left(ModuleConfigurationError( + s"""File does not exist: + | - $path + |""".stripMargin + )) + case None => Right(None) + } + } + + def moduleConfigFromCwd = + Right(os.walk(cwd).find(p => p.endsWith(os.RelPath(Constants.moduleConfigFileName)))) + + for { + fromArgs <- moduleConfigDirectlyFromArgs + fromCwd <- moduleConfigFromCwd + } yield fromArgs.orElse(fromCwd) + } + + private[input] object Keys { + val modules = "modules" + val roots = "roots" + } + + private[input] case class ModuleDefinition( + name: String, + roots: Seq[String] + ) + + // TODO Check for module dependencies that do not exist + private[input] def readAllModules(modules: Option[Value]) + : Either[BuildException, Seq[ModuleDefinition]] = modules match { + case Some(Tbl(values)) => EitherSequence.sequence { + values.toSeq.map(readModule) + }.left.map(CompositeBuildException.apply) + case _ => Left(ModuleConfigurationError(s"$modules must exist and must be a table")) + } + + private def readModule( + key: String, + value: Value + ): Either[ModuleConfigurationError, ModuleDefinition] = + value match + case Tbl(values) => + val maybeRoots = values.get(Keys.roots).map { + case Str(value) => Right(Seq(value)) + case Arr(values) => EitherSequence.sequence { + values.map { + case Str(value) => Right(value) + case _ => Left(()) + } + }.left.map(_ => ()) + case _ => Left(()) + }.getOrElse(Right(Seq(key))) + .left.map(_ => + ModuleConfigurationError( + s"${Keys.modules}.$key.${Keys.roots} must be a string or a list of strings" + ) + ) + + for { + roots <- maybeRoots + } yield ModuleDefinition(key, roots) + + case _ => Left(ModuleConfigurationError(s"${Keys.modules}.$key must be a table")) +} + +/** Creates [[ModuleInputs]] given the initial arguments passed to the command, Looks for module + * config .toml file and if found composes module inputs according to the defined config, otherwise + * if module config is not found or if [[allowForbiddenFeatures]] is not set, returns only one + * basic module created from initial args (see [[simpleInputs]]) + * + * @param args + * initial args passed to command + * @param cwd + * working directory + * @param inputsFromArgs + * function that proceeds with the whole [[ModuleInputs]] creation flow (validating elements, + * etc.) this takes into account options passed from CLI like in SharedOptions + * @param allowForbiddenFeatures + */ +final case class InputsComposer( + args: Seq[String], + cwd: os.Path, + inputsFromArgs: (Seq[String], Option[ProjectName]) => Either[BuildException, ModuleInputs], + allowForbiddenFeatures: Boolean +) { + import InputsComposer.* + + /** Inputs with no dependencies coming only from args */ + private def simpleInputs = for (inputs <- inputsFromArgs(args, None)) yield SimpleInputs(inputs) + + def getInputs: Either[BuildException, Inputs] = + if allowForbiddenFeatures then + findModuleConfig(args, cwd) match { + case Right(Some(moduleConfigPath)) => + val configText = os.read(moduleConfigPath) + for { + table <- + toml.Toml.parse(configText).left.map(e => + ModuleConfigurationError(e._2) + ) // TODO use the Address value returned to show better errors + modules <- readAllModules(table.values.get(Keys.modules)) + moduleInputs <- fromModuleDefinitions(modules, moduleConfigPath) + } yield moduleInputs + case Right(None) => simpleInputs + case Left(err) => Left(err) + } + else simpleInputs + + /** Create module inputs using a supplied function [[inputsFromArgs]], link them with their module + * dependencies' names + * + * @return + * a list of module inputs for the extracted modules + */ + private def fromModuleDefinitions( + modules: Seq[ModuleDefinition], + moduleConfigPath: os.Path + ): Either[BuildException, ComposedInputs] = either { + val workspacePath = moduleConfigPath / os.up + val moduleInputs: Seq[ModuleInputs] = modules.map { m => + val moduleName = ProjectName(m.name) + val argsWithWorkspace = m.roots.map(r => os.Path(r, workspacePath).toString) + val moduleInputs = inputsFromArgs(argsWithWorkspace, Some(moduleName)) + value(moduleInputs) + .copy(mayAppendHash = + false + ) // Important only for modules with dependencies in bloop config, but let's keep it + .withForcedWorkspace(workspacePath) + } + + ComposedInputs(modules = moduleInputs, workspace = workspacePath) + } +} diff --git a/modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala b/modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala index 31037dd3af..0b101569bd 100644 --- a/modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala +++ b/modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala @@ -1,7 +1,7 @@ package scala.build.internal.resource import scala.build.Build -import scala.build.input.{CFile, Inputs} +import scala.build.input.{CFile, ModuleInputs} object NativeResourceMapper { diff --git a/modules/build/src/main/scala/scala/build/preprocessing/DataPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/DataPreprocessor.scala index e767875ca1..0268d3c4ff 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/DataPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/DataPreprocessor.scala @@ -5,7 +5,7 @@ import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException -import scala.build.input.{Inputs, ScalaCliInvokeData, SingleElement, VirtualData} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, SingleElement, VirtualData} import scala.build.options.{ BuildOptions, BuildRequirements, diff --git a/modules/build/src/main/scala/scala/build/preprocessing/JarPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/JarPreprocessor.scala index 8e7b5e994b..38acd5e129 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/JarPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/JarPreprocessor.scala @@ -5,7 +5,7 @@ import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException -import scala.build.input.{Inputs, JarFile, ScalaCliInvokeData, SingleElement} +import scala.build.input.{JarFile, ModuleInputs, ScalaCliInvokeData, SingleElement} import scala.build.options.{ BuildOptions, BuildRequirements, diff --git a/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala index 8dbdcf7d8d..d8610b434f 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala @@ -8,7 +8,13 @@ import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException -import scala.build.input.{Inputs, JavaFile, ScalaCliInvokeData, SingleElement, VirtualJavaFile} +import scala.build.input.{ + JavaFile, + ModuleInputs, + ScalaCliInvokeData, + SingleElement, + VirtualJavaFile +} import scala.build.internal.JavaParserProxyMaker import scala.build.options.{ BuildOptions, diff --git a/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala index 74d23e09da..641c93b119 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala @@ -6,8 +6,8 @@ import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException import scala.build.input.{ - Inputs, MarkdownFile, + ModuleInputs, ScalaCliInvokeData, SingleElement, VirtualMarkdownFile diff --git a/modules/build/src/main/scala/scala/build/preprocessing/Preprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/Preprocessor.scala index d3050f9edd..1ad4eb919e 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/Preprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/Preprocessor.scala @@ -2,7 +2,7 @@ package scala.build.preprocessing import scala.build.Logger import scala.build.errors.BuildException -import scala.build.input.{Inputs, ScalaCliInvokeData, SingleElement} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, SingleElement} import scala.build.options.SuppressWarningOptions trait Preprocessor { diff --git a/modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala index edadd18cf8..bd2140eb73 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala @@ -13,7 +13,13 @@ import scala.build.directives.{ HasBuildRequirements } import scala.build.errors.* -import scala.build.input.{Inputs, ScalaCliInvokeData, ScalaFile, SingleElement, VirtualScalaFile} +import scala.build.input.{ + ModuleInputs, + ScalaCliInvokeData, + ScalaFile, + SingleElement, + VirtualScalaFile +} import scala.build.internal.Util import scala.build.options.* import scala.build.preprocessing.directives.{ diff --git a/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala index 3d3633f303..7c2d9b8108 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala @@ -5,7 +5,7 @@ import java.nio.charset.StandardCharsets import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.errors.BuildException -import scala.build.input.{Inputs, ScalaCliInvokeData, Script, SingleElement, VirtualScript} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, Script, SingleElement, VirtualScript} import scala.build.internal.* import scala.build.internal.util.WarningMessages import scala.build.options.{BuildOptions, BuildRequirements, Platform, SuppressWarningOptions} diff --git a/modules/build/src/test/scala/scala/build/input/compose/InputsComposerTest.scala b/modules/build/src/test/scala/scala/build/input/compose/InputsComposerTest.scala new file mode 100644 index 0000000000..cd47f4ea47 --- /dev/null +++ b/modules/build/src/test/scala/scala/build/input/compose/InputsComposerTest.scala @@ -0,0 +1,39 @@ +package scala.build.input.compose + +import scala.build.Build +import scala.build.bsp.buildtargets.ProjectName +import scala.build.errors.BuildException +import scala.build.input.ModuleInputs +import scala.build.input.compose.InputsComposer +import scala.build.internal.Constants +import scala.build.options.BuildOptions +import scala.build.tests.{TestInputs, TestUtil} + +class InputsComposerTest extends TestUtil.ScalaCliBuildSuite { + + test("read simple module config") { + val configText = + """[modules.webpage] + | + |[modules.core] + |roots = ["Core.scala", "Utils.scala"] + |""".stripMargin + + val parsedModules = { + for { + table <- toml.Toml.parse(configText) + modules <- InputsComposer.readAllModules(table.values.get(InputsComposer.Keys.modules)) + } yield modules + }.toSeq.flatten + + assert(parsedModules.nonEmpty) + + assert(parsedModules.head.name == "webpage") + val webpageModule = parsedModules.head + assert(webpageModule.roots.toSet == Set("webpage")) + + val coreModule = parsedModules.last + assert(coreModule.name == "core") + assert(coreModule.roots.toSet == Set("Core.scala", "Utils.scala")) + } +} diff --git a/modules/build/src/test/scala/scala/build/input/compose/InputsComposerUtils.scala b/modules/build/src/test/scala/scala/build/input/compose/InputsComposerUtils.scala new file mode 100644 index 0000000000..8a61f621da --- /dev/null +++ b/modules/build/src/test/scala/scala/build/input/compose/InputsComposerUtils.scala @@ -0,0 +1,18 @@ +package scala.build.input.compose + +import scala.build.Build +import scala.build.options.BuildOptions +import scala.build.bsp.buildtargets.ProjectName +import scala.build.errors.BuildException +import scala.build.input.ModuleInputs + +object InputsComposerUtils { + def argsToEmptyModules( + args: Seq[String], + projectNameOpt: Option[ProjectName] + ): Either[BuildException, ModuleInputs] = { + assert(projectNameOpt.isDefined) + val emptyInputs = ModuleInputs.empty(projectNameOpt.get.name) + Right(Build.updateInputs(emptyInputs, BuildOptions())) + } +} diff --git a/modules/build/src/test/scala/scala/build/tests/BspServerTests.scala b/modules/build/src/test/scala/scala/build/tests/BspServerTests.scala index fa8f0f86b3..a9614517ed 100644 --- a/modules/build/src/test/scala/scala/build/tests/BspServerTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/BspServerTests.scala @@ -6,6 +6,7 @@ import org.scalajs.logging.{NullLogger, Logger as ScalaJsLogger} import java.util.concurrent.TimeUnit import scala.build.Ops.* +import scala.build.bsp.buildtargets.ProjectName import scala.build.{Build, BuildThreads, Directories, GeneratedSource, LocalRepo} import scala.build.options.{BuildOptions, InternalOptions, Scope} import scala.build.bsp.{ @@ -32,13 +33,13 @@ class BspServerTests extends TestUtil.ScalaCliBuildSuite { val buildThreads = BuildThreads.create() def getScriptBuildServer( + projectName: ProjectName, generatedSources: Seq[GeneratedSource], workspace: os.Path, scope: Scope = Scope.Main ): ScalaScriptBuildServer = { - val bspServer = new BspServer(null, null, null) - bspServer.setGeneratedSources(Scope.Main, generatedSources) - bspServer.setProjectName(workspace, "test", scope) + val bspServer = new BspServer(null, null, null, null) + bspServer.addTarget(projectName, workspace, scope, generatedSources) bspServer } @@ -53,7 +54,7 @@ class BspServerTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withBuild(baseOptions, buildThreads, None) { - (root, _, maybeBuild) => + (root, inputs, maybeBuild) => val build: Build = maybeBuild.orThrow build match { @@ -67,7 +68,7 @@ class BspServerTests extends TestUtil.ScalaCliBuildSuite { .take(wrappedScript.wrapperParamsOpt.map(_.topWrapperLineCount).getOrElse(0)) .mkString("", System.lineSeparator(), System.lineSeparator()) - val bspServer = getScriptBuildServer(generatedSources, root) + val bspServer = getScriptBuildServer(inputs.projectName, generatedSources, root) val wrappedSourcesResult: WrappedSourcesResult = bspServer .buildTargetWrappedSources(new WrappedSourcesParams(ArrayBuffer.empty.asJava)) diff --git a/modules/build/src/test/scala/scala/build/tests/BuildProjectTests.scala b/modules/build/src/test/scala/scala/build/tests/BuildProjectTests.scala index f1dd1dbf9c..09bc38b65b 100644 --- a/modules/build/src/test/scala/scala/build/tests/BuildProjectTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/BuildProjectTests.scala @@ -8,7 +8,7 @@ import org.scalajs.logging.{NullLogger, Logger as ScalaJsLogger} import java.io.PrintStream import scala.build.Ops.* import scala.build.errors.{BuildException, Diagnostic, Severity} -import scala.build.input.Inputs +import scala.build.input.ModuleInputs import scala.build.internals.FeatureType import scala.build.options.{ BuildOptions, @@ -71,7 +71,7 @@ class BuildProjectTests extends TestUtil.ScalaCliBuildSuite { LocalRepo.localRepo(scala.build.Directories.default().localRepoDir, TestLogger()) ) ) - val inputs = Inputs.empty("project") + val inputs = ModuleInputs.empty("project") val sources = Sources(Nil, Nil, None, Nil, options) val logger = new LoggerMock() val artifacts = options.artifacts(logger, Scope.Test).orThrow diff --git a/modules/build/src/test/scala/scala/build/tests/ExcludeTests.scala b/modules/build/src/test/scala/scala/build/tests/ExcludeTests.scala index bdea746535..0021058f44 100644 --- a/modules/build/src/test/scala/scala/build/tests/ExcludeTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/ExcludeTests.scala @@ -41,7 +41,7 @@ class ExcludeTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (_, inputs) => val crossSources = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -64,7 +64,7 @@ class ExcludeTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (_, inputs) => val crossSources = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -88,7 +88,7 @@ class ExcludeTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -122,7 +122,7 @@ class ExcludeTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -156,7 +156,7 @@ class ExcludeTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -190,7 +190,7 @@ class ExcludeTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), diff --git a/modules/build/src/test/scala/scala/build/tests/InputsTests.scala b/modules/build/src/test/scala/scala/build/tests/InputsTests.scala index 6c6812ccd8..a0142973a0 100644 --- a/modules/build/src/test/scala/scala/build/tests/InputsTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/InputsTests.scala @@ -5,7 +5,7 @@ import com.eed3si9n.expecty.Expecty.expect import scala.build.Build import scala.build.input.{ - Inputs, + ModuleInputs, ScalaCliInvokeData, VirtualJavaFile, VirtualScalaFile, @@ -142,7 +142,7 @@ class InputsTests extends TestUtil.ScalaCliBuildSuite { ) TestInputs().fromRoot { root => - val elements = Inputs.validateArgs( + val elements = ModuleInputs.validateArgs( urls, root, download = url => Right(Array.emptyByteArray), diff --git a/modules/build/src/test/scala/scala/build/tests/PreprocessingTests.scala b/modules/build/src/test/scala/scala/build/tests/PreprocessingTests.scala index f3089c964f..fb9791b597 100644 --- a/modules/build/src/test/scala/scala/build/tests/PreprocessingTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/PreprocessingTests.scala @@ -3,7 +3,7 @@ package scala.build.tests import scala.build.preprocessing.{MarkdownPreprocessor, ScalaPreprocessor, ScriptPreprocessor} import com.eed3si9n.expecty.Expecty.expect -import scala.build.input.{Inputs, MarkdownFile, ScalaCliInvokeData, Script, SourceScalaFile} +import scala.build.input.{ModuleInputs, MarkdownFile, ScalaCliInvokeData, Script, SourceScalaFile} import scala.build.options.SuppressWarningOptions class PreprocessingTests extends TestUtil.ScalaCliBuildSuite { diff --git a/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala b/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala index c50f3afc45..e049242c67 100644 --- a/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala @@ -56,7 +56,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -99,7 +99,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { val expectedDeps = Nil testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -139,7 +139,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { val expectedDeps = Nil testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -179,7 +179,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { val testInputs = TestInputs(files, Seq(".")) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -222,7 +222,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -267,7 +267,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -354,7 +354,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -396,7 +396,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -440,7 +440,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -475,7 +475,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -522,7 +522,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (root, inputs) => val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -572,7 +572,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (_, inputs) => val crossSources = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -593,7 +593,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (_, inputs) => val crossSources = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), @@ -630,7 +630,7 @@ class SourcesTests extends TestUtil.ScalaCliBuildSuite { ) testInputs.withInputs { (_, inputs) => val crossSourcesResult = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, preprocessors, TestLogger(), diff --git a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala index 3923ac58ec..fb89dc1386 100644 --- a/modules/build/src/test/scala/scala/build/tests/TestInputs.scala +++ b/modules/build/src/test/scala/scala/build/tests/TestInputs.scala @@ -6,7 +6,7 @@ import java.nio.charset.StandardCharsets import scala.build.{Build, BuildThreads, Builds, Directories} import scala.build.compiler.{BloopCompilerMaker, SimpleScalaCompilerMaker} import scala.build.errors.BuildException -import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, SubCommand} import scala.build.internal.Util import scala.build.options.{BuildOptions, Scope} import scala.util.control.NonFatal @@ -17,7 +17,7 @@ final case class TestInputs( inputArgs: Seq[String] = Seq.empty, forceCwd: Option[os.Path] = None ) { - def withInputs[T](f: (os.Path, Inputs) => T): T = + def withInputs[T](f: (os.Path, ModuleInputs) => T): T = withCustomInputs(false, None)(f) def fromRoot[T](f: os.Path => T, skipCreatingSources: Boolean = false): T = @@ -38,7 +38,7 @@ final case class TestInputs( forcedWorkspaceOpt: Option[os.FilePath], skipCreatingSources: Boolean = false )( - f: (os.Path, Inputs) => T + f: (os.Path, ModuleInputs) => T ): T = fromRoot( { tmpDir => @@ -46,7 +46,7 @@ final case class TestInputs( if (viaDirectory) Seq(tmpDir.toString) else if (inputArgs.isEmpty) files.map(_._1.toString) else inputArgs - val res = Inputs( + val res = ModuleInputs( inputArgs0, tmpDir, forcedWorkspace = forcedWorkspaceOpt.map(_.resolveFrom(tmpDir)), @@ -66,7 +66,7 @@ final case class TestInputs( buildThreads: BuildThreads, // actually only used when bloopConfigOpt is non-empty bloopConfigOpt: Option[BloopRifleConfig], fromDirectory: Boolean = false - )(f: (os.Path, Inputs, Build) => T) = + )(f: (os.Path, ModuleInputs, Build) => T) = withBuild(options, buildThreads, bloopConfigOpt, fromDirectory)((p, i, maybeBuild) => maybeBuild match { case Left(e) => throw e @@ -82,7 +82,7 @@ final case class TestInputs( buildTests: Boolean = true, actionableDiagnostics: Boolean = false, skipCreatingSources: Boolean = false - )(f: (os.Path, Inputs, Either[BuildException, Builds]) => T): T = + )(f: (os.Path, ModuleInputs, Either[BuildException, Builds]) => T): T = withCustomInputs(fromDirectory, None, skipCreatingSources) { (root, inputs) => val compilerMaker = bloopConfigOpt match { case Some(bloopConfig) => @@ -119,7 +119,7 @@ final case class TestInputs( actionableDiagnostics: Boolean = false, scope: Scope = Scope.Main, skipCreatingSources: Boolean = false - )(f: (os.Path, Inputs, Either[BuildException, Build]) => T): T = + )(f: (os.Path, ModuleInputs, Either[BuildException, Build]) => T): T = withBuilds( options, buildThreads, diff --git a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala index 7c2595ac94..c4ed750d34 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/bsp/Bsp.scala @@ -8,7 +8,7 @@ import scala.build.EitherCps.{either, value} import scala.build.* import scala.build.bsp.{BspReloadableOptions, BspThreads} import scala.build.errors.BuildException -import scala.build.input.Inputs +import scala.build.input.{ModuleInputs, compose} import scala.build.internals.EnvVar import scala.build.options.{BuildOptions, Scope} import scala.cli.commands.ScalaCommand @@ -83,93 +83,96 @@ object Bsp extends ScalaCommand[BspOptions] { refreshPowerMode(getLauncherOptions(), getSharedOptions(), getEnvsFromFile()) - val preprocessInputs: Seq[String] => Either[BuildException, (Inputs, BuildOptions)] = + val preprocessInputs + : Seq[String] => Either[BuildException, (compose.Inputs, Seq[BuildOptions])] = argsSeq => either { val sharedOptions = getSharedOptions() val launcherOptions = getLauncherOptions() val envs = getEnvsFromFile() - val initialInputs = value(sharedOptions.inputs(argsSeq, () => Inputs.default())) refreshPowerMode(launcherOptions, sharedOptions, envs) - if (sharedOptions.logging.verbosity >= 3) - pprint.err.log(initialInputs) - val baseOptions = buildOptions(sharedOptions, launcherOptions, envs) val latestLogger = sharedOptions.logging.logger val persistentLogger = new PersistentDiagnosticLogger(latestLogger) - val crossResult = CrossSources.forInputs( - initialInputs, - Sources.defaultPreprocessors( - baseOptions.archiveCache, - baseOptions.internal.javaClassNameVersionOpt, - () => baseOptions.javaHome().value.javaCommand - ), - persistentLogger, - baseOptions.suppressWarningOptions, - baseOptions.internal.exclude - ) + val initialInputs: compose.Inputs = value(sharedOptions.composeInputs(argsSeq)) - val (allInputs, finalBuildOptions) = { - for - crossSourcesAndInputs <- crossResult - // compiler bug, can't do : - // (crossSources, crossInputs) <- crossResult - (crossSources, crossInputs) = crossSourcesAndInputs - sharedBuildOptions = crossSources.sharedOptions(baseOptions) - scopedSources <- crossSources.scopedSources(sharedBuildOptions) - resolvedBuildOptions = - scopedSources.buildOptionsFor(Scope.Main).foldRight(sharedBuildOptions)(_ orElse _) - yield (crossInputs, resolvedBuildOptions) - }.getOrElse(initialInputs -> baseOptions) - - Build.updateInputs(allInputs, baseOptions) -> finalBuildOptions + if (sharedOptions.logging.verbosity >= 3) + pprint.err.log(initialInputs) + + initialInputs.preprocessInputs { moduleInputs => + val crossResult = CrossSources.forModuleInputs( + moduleInputs, + Sources.defaultPreprocessors( + baseOptions.archiveCache, + baseOptions.internal.javaClassNameVersionOpt, + () => baseOptions.javaHome().value.javaCommand + ), + persistentLogger, + baseOptions.suppressWarningOptions, + baseOptions.internal.exclude + ) + + val (allInputs, finalBuildOptions) = { + for + crossSourcesAndInputs <- crossResult + // compiler bug, can't do : + // (crossSources, crossInputs) <- crossResult + (crossSources, crossInputs) = crossSourcesAndInputs + sharedBuildOptions = crossSources.sharedOptions(baseOptions) + scopedSources <- crossSources.scopedSources(sharedBuildOptions) + resolvedBuildOptions = + scopedSources.buildOptionsFor(Scope.Main).foldRight(sharedBuildOptions)( + _ orElse _ + ) + yield (crossInputs, resolvedBuildOptions) + }.getOrElse(moduleInputs -> baseOptions) + + allInputs -> finalBuildOptions + } } - val (inputs, finalBuildOptions) = preprocessInputs(args.all).orExit(logger) + val inputsAndBuildOptions = preprocessInputs(args.all).orExit(logger) - /** values used for launching the bsp, especially for launching the bloop server, they do not - * include options extracted from sources, except in bloopRifleConfig - it's needed for - * correctly launching the bloop server - */ - val initialBspOptions = { + // We use this sequence of options to pick a suitable version of the JVM for Bloop + val allModulesBuildOptions = inputsAndBuildOptions._2 + val inputs = inputsAndBuildOptions._1 + + if (options.shared.logging.verbosity >= 3) + pprint.err.log(allModulesBuildOptions) + + val bspReloadableOptionsReference = BspReloadableOptions.Reference { () => val sharedOptions = getSharedOptions() val launcherOptions = getLauncherOptions() val envs = getEnvsFromFile() - val bspBuildOptions = buildOptions(sharedOptions, launcherOptions, envs) refreshPowerMode(launcherOptions, sharedOptions, envs) BspReloadableOptions( - buildOptions = bspBuildOptions, - bloopRifleConfig = sharedOptions.bloopRifleConfig(Some(finalBuildOptions)) - .orExit(sharedOptions.logger), + buildOptions = buildOptions(sharedOptions, launcherOptions, envs), + bloopRifleConfig = sharedOptions.bloopRifleConfig().orExit(sharedOptions.logger), logger = sharedOptions.logging.logger, verbosity = sharedOptions.logging.verbosity ) } - val bspReloadableOptionsReference = BspReloadableOptions.Reference { () => - val sharedOptions = getSharedOptions() - val launcherOptions = getLauncherOptions() - val envs = getEnvsFromFile() - val bloopRifleConfig = sharedOptions.bloopRifleConfig() - - refreshPowerMode(launcherOptions, sharedOptions, envs) + /** values used for launching the bsp, especially for launching the bloop server, they do not + * include options extracted from sources, except in bloopRifleConfig - it's needed for + * correctly launching the bloop server + */ + val initialBspOptions = { + val sharedOptions = getSharedOptions() - BspReloadableOptions( - buildOptions = buildOptions(sharedOptions, launcherOptions, envs), - bloopRifleConfig = sharedOptions.bloopRifleConfig().orExit(sharedOptions.logger), - logger = sharedOptions.logging.logger, - verbosity = sharedOptions.logging.verbosity + bspReloadableOptionsReference.get.copy( + bloopRifleConfig = + sharedOptions.bloopRifleConfig(allModulesBuildOptions).orExit(sharedOptions.logger) ) } CurrentParams.workspaceOpt = Some(inputs.workspace) - val actionableDiagnostics = - options.shared.logging.verbosityOptions.actions + val actionableDiagnostics = options.shared.logging.verbosityOptions.actions BspThreads.withThreads { threads => val bsp = scala.build.bsp.Bsp.create( diff --git a/modules/cli/src/main/scala/scala/cli/commands/clean/Clean.scala b/modules/cli/src/main/scala/scala/cli/commands/clean/Clean.scala index 1d2c2e5287..ba08300f9f 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/clean/Clean.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/clean/Clean.scala @@ -2,7 +2,7 @@ package scala.cli.commands.clean import caseapp.* -import scala.build.input.Inputs +import scala.build.input.ModuleInputs import scala.build.internal.Constants import scala.build.{Logger, Os} import scala.cli.CurrentParams @@ -16,10 +16,10 @@ object Clean extends ScalaCommand[CleanOptions] { override def scalaSpecificationLevel = SpecificationLevel.IMPLEMENTATION override def runCommand(options: CleanOptions, args: RemainingArgs, logger: Logger): Unit = { - val inputs = Inputs( + val inputs = ModuleInputs( args.all, Os.pwd, - defaultInputs = () => Inputs.default(), + defaultInputs = () => ModuleInputs.default(), forcedWorkspace = options.workspace.forcedWorkspaceOpt, allowRestrictedFeatures = allowRestrictedFeatures, extraClasspathWasPassed = false diff --git a/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala b/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala index 26b5dfc9c9..e82e007536 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/default/Default.scala @@ -4,7 +4,7 @@ import caseapp.core.help.RuntimeCommandsHelp import caseapp.core.{Error, RemainingArgs} import scala.build.Logger -import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, SubCommand} import scala.cli.commands.ScalaCommandWithCustomHelp import scala.cli.commands.repl.{Repl, ReplOptions} import scala.cli.commands.run.{Run, RunOptions} @@ -56,7 +56,7 @@ class Default(actualHelp: => RuntimeCommandsHelp) runOptions, args.remaining, args.unparsed, - () => Inputs.default(), + () => ModuleInputs.default(), logger, invokeData ) diff --git a/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala b/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala index bb05607fa8..61a9982222 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/dependencyupdate/DependencyUpdate.scala @@ -30,7 +30,7 @@ object DependencyUpdate extends ScalaCommand[DependencyUpdateOptions] { val inputs = options.shared.inputs(args.all).orExit(logger) val (crossSources, _) = - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, Sources.defaultPreprocessors( buildOptions.archiveCache, diff --git a/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala b/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala index ddd62921c0..784bc743cc 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala @@ -14,7 +14,7 @@ import java.nio.charset.{Charset, StandardCharsets} import scala.build.EitherCps.{either, value} import scala.build.* import scala.build.errors.BuildException -import scala.build.input.Inputs +import scala.build.input.ModuleInputs import scala.build.internal.Constants import scala.build.options.{BuildOptions, Platform, Scope} import scala.cli.CurrentParams @@ -31,7 +31,7 @@ object Export extends ScalaCommand[ExportOptions] { super.helpFormat.withPrimaryGroup(HelpGroup.BuildToolExport) private def prepareBuild( - inputs: Inputs, + inputs: ModuleInputs, buildOptions: BuildOptions, logger: Logger, verbosity: Int, @@ -40,8 +40,8 @@ object Export extends ScalaCommand[ExportOptions] { logger.log("Preparing build") - val (crossSources: CrossSources, allInputs: Inputs) = value { - CrossSources.forInputs( + val (crossSources: CrossSources, allInputs: ModuleInputs) = value { + CrossSources.forModuleInputs( inputs, Sources.defaultPreprocessors( buildOptions.archiveCache, diff --git a/modules/cli/src/main/scala/scala/cli/commands/fix/Fix.scala b/modules/cli/src/main/scala/scala/cli/commands/fix/Fix.scala index 5da3696fd2..dc4a689dfd 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/fix/Fix.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/fix/Fix.scala @@ -31,7 +31,7 @@ object Fix extends ScalaCommand[FixOptions] { val newLine = System.lineSeparator() override def runCommand(options: FixOptions, args: RemainingArgs, logger: Logger): Unit = { - val inputs = options.shared.inputs(args.remaining, () => Inputs.default()).orExit(logger) + val inputs = options.shared.inputs(args.remaining, () => ModuleInputs.default()).orExit(logger) val (mainSources, testSources) = getProjectSources(inputs) .left.map(CompositeBuildException(_)) @@ -118,10 +118,10 @@ object Fix extends ScalaCommand[FixOptions] { .foreach(ttd => removeDirectivesFrom(ttd.positions, toKeep = ttd.noTestPrefixAvailable)) } - def getProjectSources(inputs: Inputs): Either[::[BuildException], (Sources, Sources)] = { + def getProjectSources(inputs: ModuleInputs): Either[::[BuildException], (Sources, Sources)] = { val buildOptions = BuildOptions() - val (crossSources, _) = CrossSources.forInputs( + val (crossSources, _) = CrossSources.forModuleInputs( inputs, preprocessors = Sources.defaultPreprocessors( buildOptions.archiveCache, diff --git a/modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala b/modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala index 5dd03e6302..28916b4ee7 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/fmt/Fmt.scala @@ -4,7 +4,7 @@ import caseapp.* import caseapp.core.help.HelpFormat import dependency.* -import scala.build.input.{Inputs, Script, SourceScalaFile} +import scala.build.input.{ModuleInputs, Script, SourceScalaFile} import scala.build.internal.{Constants, ExternalBinaryParams, FetchExternalBinary, Runner} import scala.build.options.BuildOptions import scala.build.{Logger, Sources} diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala index c5f38e7385..e6ebe8b343 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala @@ -28,7 +28,7 @@ import scala.build.Ops.* import scala.build.* import scala.build.compiler.ScalaCompilerMaker import scala.build.errors.{BuildException, CompositeBuildException, NoMainClassFoundError, Severity} -import scala.build.input.Inputs +import scala.build.input.ModuleInputs import scala.build.internal.Util import scala.build.internal.Util.ScalaDependencyOps import scala.build.options.publish.{Developer, License, Signer => PSigner, Vcs} @@ -274,7 +274,7 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers { /** Build artifacts */ def doRun( - inputs: Inputs, + inputs: ModuleInputs, logger: Logger, initialBuildOptions: BuildOptions, compilerMaker: ScalaCompilerMaker, diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala index a01a47196b..7081cd6285 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/PublishSetup.scala @@ -80,7 +80,7 @@ object PublishSetup extends ScalaCommand[PublishSetupOptions] { ) ) - val (crossSources, _) = CrossSources.forInputs( + val (crossSources, _) = CrossSources.forModuleInputs( inputs, Sources.defaultPreprocessors( cliBuildOptions.archiveCache, diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index ea0dc10186..5d36d89a53 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -18,7 +18,7 @@ import scala.build.errors.{ FetchingDependenciesError, MultipleScalaVersionsError } -import scala.build.input.Inputs +import scala.build.input.ModuleInputs import scala.build.internal.{Constants, Runner} import scala.build.options.{BuildOptions, JavaOpt, MaybeScalaVersion, Scope} import scala.cli.commands.publish.ConfigUtil.* @@ -109,8 +109,8 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { override def runCommand(options: ReplOptions, args: RemainingArgs, logger: Logger): Unit = { val initialBuildOptions = buildOptionsOrExit(options) - def default = Inputs.default().getOrElse { - Inputs.empty(Os.pwd, options.shared.markdown.enableMarkdown) + def default = ModuleInputs.default().getOrElse { + ModuleInputs.empty(Os.pwd, options.shared.markdown.enableMarkdown) } val inputs = options.shared.inputs(args.remaining, defaultInputs = () => Some(default)).orExit(logger) diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index cdb997af3d..6f67010318 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.build.EitherCps.{either, value} import scala.build.* import scala.build.errors.BuildException -import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand} +import scala.build.input.{ModuleInputs, ScalaCliInvokeData, SubCommand} import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig} import scala.build.internals.ConsoleUtils.ScalaCliConsole import scala.build.internals.EnvVar @@ -64,7 +64,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { options, args.remaining, args.unparsed, - () => Inputs.default(), + () => ModuleInputs.default(), logger, invokeData ) @@ -113,7 +113,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { options0: RunOptions, inputArgs: Seq[String], programArgs: Seq[String], - defaultInputs: () => Option[Inputs], + defaultInputs: () => Option[ModuleInputs], logger: Logger, invokeData: ScalaCliInvokeData ): Unit = { diff --git a/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala b/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala index d8d4fe8ec5..d998161e6b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/setupide/SetupIde.scala @@ -12,7 +12,8 @@ import scala.build.EitherCps.{either, value} import scala.build.* import scala.build.bsp.IdeInputs import scala.build.errors.{BuildException, WorkspaceError} -import scala.build.input.{Inputs, OnDisk, Virtual, WorkspaceOrigin} +import scala.build.input.compose.{ComposedInputs, InputsComposer, SimpleInputs} +import scala.build.input.{ModuleInputs, OnDisk, Virtual, WorkspaceOrigin, compose} import scala.build.internal.Constants import scala.build.internals.EnvVar import scala.build.options.{BuildOptions, Scope} @@ -26,7 +27,7 @@ import scala.jdk.CollectionConverters.* object SetupIde extends ScalaCommand[SetupIdeOptions] { def downloadDeps( - inputs: Inputs, + inputs: ModuleInputs, options: BuildOptions, logger: Logger ): Either[BuildException, Artifacts] = { @@ -34,7 +35,7 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { // ignoring errors related to sources themselves val maybeSourceBuildOptions = either { val (crossSources, allInputs) = value { - CrossSources.forInputs( + CrossSources.forModuleInputs( inputs, Sources.defaultPreprocessors( options.archiveCache, @@ -68,7 +69,7 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { override def runCommand(options: SetupIdeOptions, args: RemainingArgs, logger: Logger): Unit = { val buildOptions = buildOptionsOrExit(options) - val inputs = options.shared.inputs(args.all).orExit(logger) + val inputs = options.shared.composeInputs(args.all).orExit(logger) CurrentParams.workspaceOpt = Some(inputs.workspace) val bspPath = writeBspConfiguration( @@ -84,7 +85,7 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { def runSafe( options: SharedOptions, - inputs: Inputs, + inputs: ModuleInputs, logger: Logger, buildOptions: BuildOptions, previousCommandName: Option[String], @@ -92,7 +93,7 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { ): Unit = writeBspConfiguration( SetupIdeOptions(shared = options), - inputs, + SimpleInputs(inputs), buildOptions, previousCommandName, args @@ -106,13 +107,13 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { private def writeBspConfiguration( options: SetupIdeOptions, - inputs: Inputs, + inputs: compose.Inputs, buildOptions: BuildOptions, previousCommandName: Option[String], args: Seq[String] ): Either[BuildException, Option[os.Path]] = either { - val virtualInputs = inputs.elements.collect { + val virtualInputs = inputs.modules.flatMap(_.elements).collect { case v: Virtual => v } if (virtualInputs.nonEmpty) @@ -125,23 +126,26 @@ object SetupIde extends ScalaCommand[SetupIdeOptions] { val logger = options.shared.logger if (buildOptions.classPathOptions.allExtraDependencies.toSeq.nonEmpty) - value(downloadDeps( - inputs, - buildOptions, - logger - )) + for (module <- inputs.modules) do value(downloadDeps(module, buildOptions, logger)) - val (bspName, bspJsonDestination) = bspDetails(inputs.workspace, options.bspFile) - val scalaCliBspJsonDestination = - inputs.workspace / Constants.workspaceDirName / "ide-options-v2.json" + val workspace = inputs.workspace + + val (bspName, bspJsonDestination) = bspDetails(workspace, options.bspFile) + val scalaCliBspJsonDestination = workspace / Constants.workspaceDirName / "ide-options-v2.json" val scalaCliBspLauncherOptsJsonDestination = - inputs.workspace / Constants.workspaceDirName / "ide-launcher-options.json" + workspace / Constants.workspaceDirName / "ide-launcher-options.json" val scalaCliBspInputsJsonDestination = - inputs.workspace / Constants.workspaceDirName / "ide-inputs.json" - val scalaCliBspEnvsJsonDestination = - inputs.workspace / Constants.workspaceDirName / "ide-envs.json" - - val inputArgs = inputs.elements.collect { case d: OnDisk => d.path.toString } + workspace / Constants.workspaceDirName / "ide-inputs.json" + val scalaCliBspEnvsJsonDestination = workspace / Constants.workspaceDirName / "ide-envs.json" + + // FIXME single modules can also be defined with module config toml file + val inputArgs = inputs match + case ComposedInputs(modules, workspace) => + InputsComposer.findModuleConfig(args, Os.pwd) + .orExit(logger) + .fold(args)(p => Seq(p.toString)) + case SimpleInputs(singleModule) => singleModule.elements + .collect { case d: OnDisk => d.path.toString } val ideInputs = IdeInputs( options.shared.validateInputArgs(args) diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index 4d1d039099..c8118bc7c7 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -20,10 +20,12 @@ import java.util.concurrent.atomic.AtomicBoolean import scala.build.EitherCps.{either, value} import scala.build.Ops.EitherOptOps import scala.build.* +import scala.build.bsp.buildtargets.ProjectName import scala.build.compiler.{BloopCompilerMaker, ScalaCompilerMaker, SimpleScalaCompilerMaker} import scala.build.directives.DirectiveDescription import scala.build.errors.{AmbiguousPlatformError, BuildException, ConfigDbException, Severity} -import scala.build.input.{Element, Inputs, ResourceDirectory, ScalaCliInvokeData} +import scala.build.input.compose.InputsComposer +import scala.build.input.{Element, ModuleInputs, ResourceDirectory, ScalaCliInvokeData, compose} import scala.build.interactive.Interactive import scala.build.interactive.Interactive.{InteractiveAsk, InteractiveNop} import scala.build.internal.util.WarningMessages @@ -564,23 +566,31 @@ final case class SharedOptions( .getOrElse(None) ) - def bloopRifleConfig(extraBuildOptions: Option[BuildOptions] = None) + def bloopRifleConfig(extraBuildOptions: Seq[BuildOptions] = Nil) : Either[BuildException, BloopRifleConfig] = either { - val options = extraBuildOptions.foldLeft(value(buildOptions()))(_ orElse _) + val sharedBuildOptions = value(buildOptions()) lazy val defaultJvmHome = value { - JvmUtils.downloadJvm(OsLibc.defaultJvm(OsLibc.jvmIndexOs), options) + JvmUtils.downloadJvm(OsLibc.defaultJvm(OsLibc.jvmIndexOs), sharedBuildOptions) } + def maybeHighestJavaFromExtraOptions = + if (extraBuildOptions.nonEmpty) + Some(BuildOptions.pickJavaHomeWithHighestVersion(extraBuildOptions)) + else None + val javaHomeInfo = compilationServer.bloopJvm - .map(jvmId => value(JvmUtils.downloadJvm(jvmId, options))) - .orElse { - for (javaHome <- options.javaHomeLocationOpt()) yield { + .map(jvmId => value(JvmUtils.downloadJvm(jvmId, sharedBuildOptions))) + .orElse { // JavaHome from SharedOptions + for (javaHome <- sharedBuildOptions.javaHomeLocationOpt()) yield { val (javaHomeVersion, javaHomeCmd) = OsLibc.javaHomeVersion(javaHome.value) - if (javaHomeVersion >= Constants.minimumBloopJavaVersion) - BuildOptions.JavaHomeInfo(javaHome.value, javaHomeCmd, javaHomeVersion) - else defaultJvmHome + + BuildOptions.JavaHomeInfo(javaHome.value, javaHomeCmd, javaHomeVersion) } - }.getOrElse(defaultJvmHome) + }.filter(_.version >= Constants.minimumBloopJavaVersion) + // JavaHome from additional options, the one with the highest version + .orElse(maybeHighestJavaFromExtraOptions) + .filter(_.version >= Constants.minimumBloopJavaVersion) + .getOrElse(defaultJvmHome) compilationServer.bloopRifleConfig( logging.logger, @@ -600,7 +610,7 @@ final case class SharedOptions( SimpleScalaCompilerMaker("java", Nil, scaladoc = true) else if (compilationServer.server.getOrElse(true)) new BloopCompilerMaker( - options => bloopRifleConfig(Some(options)), + options => bloopRifleConfig(Seq(options)), threads.bloop, strictBloopJsonCheckOrDefault, coursier.getOffline().getOrElse(false) @@ -609,27 +619,53 @@ final case class SharedOptions( lazy val coursierCache = coursier.coursierCache(logging.logger.coursierLogger("")) - def inputs( + private def moduleInputsFromArgs( args: Seq[String], - defaultInputs: () => Option[Inputs] = () => Inputs.default() - )(using ScalaCliInvokeData): Either[BuildException, Inputs] = - SharedOptions.inputs( + forcedProjectName: Option[ProjectName], + defaultInputs: () => Option[ModuleInputs] = () => ModuleInputs.default() + )(using ScalaCliInvokeData) = SharedOptions.inputs( + args, + defaultInputs, + resourceDirs, + Directories.directories, + logger = logger, + coursierCache, + workspace.forcedWorkspaceOpt, + input.defaultForbiddenDirectories, + input.forbid, + scriptSnippetList = allScriptSnippets, + scalaSnippetList = allScalaSnippets, + javaSnippetList = allJavaSnippets, + markdownSnippetList = allMarkdownSnippets, + enableMarkdown = markdown.enableMarkdown, + extraClasspathWasPassed = extraClasspathWasPassed, + forcedProjectName = forcedProjectName + ) + + def composeInputs( + args: Seq[String], + defaultInputs: () => Option[ModuleInputs] = () => ModuleInputs.default() + )(using ScalaCliInvokeData): Either[BuildException, compose.Inputs] = { + val updatedModuleInputsFromArgs + : (Seq[String], Option[ProjectName]) => Either[BuildException, ModuleInputs] = + (args, projectNameOpt) => + for { + moduleInputs <- moduleInputsFromArgs(args, projectNameOpt, defaultInputs) + options <- buildOptions() + } yield Build.updateInputs(moduleInputs, options) + + InputsComposer( args, - defaultInputs, - resourceDirs, - Directories.directories, - logger = logger, - coursierCache, - workspace.forcedWorkspaceOpt, - input.defaultForbiddenDirectories, - input.forbid, - scriptSnippetList = allScriptSnippets, - scalaSnippetList = allScalaSnippets, - javaSnippetList = allJavaSnippets, - markdownSnippetList = allMarkdownSnippets, - enableMarkdown = markdown.enableMarkdown, - extraClasspathWasPassed = extraClasspathWasPassed - ) + Os.pwd, + updatedModuleInputsFromArgs, + ScalaCli.allowRestrictedFeatures + ).getInputs + } + + def inputs( + args: Seq[String], + defaultInputs: () => Option[ModuleInputs] = () => ModuleInputs.default() + )(using ScalaCliInvokeData) = moduleInputsFromArgs(args, forcedProjectName = None, defaultInputs) def allScriptSnippets: List[String] = snippet.scriptSnippet ++ snippet.executeScript def allScalaSnippets: List[String] = snippet.scalaSnippet ++ snippet.executeScala @@ -643,7 +679,7 @@ final case class SharedOptions( def validateInputArgs( args: Seq[String] )(using ScalaCliInvokeData): Seq[Either[String, Seq[Element]]] = - Inputs.validateArgs( + ModuleInputs.validateArgs( args, Os.pwd, SharedOptions.downloadInputs(coursierCache), @@ -672,10 +708,10 @@ object SharedOptions { .map(f => os.read.bytes(os.Path(f, Os.pwd))) } - /** [[Inputs]] builder, handy when you don't have a [[SharedOptions]] instance at hand */ + /** [[ModuleInputs]] builder, handy when you don't have a [[SharedOptions]] instance at hand */ def inputs( args: Seq[String], - defaultInputs: () => Option[Inputs], + defaultInputs: () => Option[ModuleInputs], resourceDirs: Seq[String], directories: scala.build.Directories, logger: scala.build.Logger, @@ -688,8 +724,9 @@ object SharedOptions { javaSnippetList: List[String], markdownSnippetList: List[String], enableMarkdown: Boolean = false, - extraClasspathWasPassed: Boolean = false - )(using ScalaCliInvokeData): Either[BuildException, Inputs] = { + extraClasspathWasPassed: Boolean = false, + forcedProjectName: Option[ProjectName] = None + )(using ScalaCliInvokeData): Either[BuildException, ModuleInputs] = { val resourceInputs = resourceDirs .map(os.Path(_, Os.pwd)) .map { path => @@ -699,7 +736,7 @@ object SharedOptions { } .map(ResourceDirectory.apply) - val maybeInputs = Inputs( + val maybeInputs = ModuleInputs( args, Os.pwd, defaultInputs = defaultInputs, @@ -713,7 +750,8 @@ object SharedOptions { forcedWorkspace = forcedWorkspaceOpt, enableMarkdown = enableMarkdown, allowRestrictedFeatures = ScalaCli.allowRestrictedFeatures, - extraClasspathWasPassed = extraClasspathWasPassed + extraClasspathWasPassed = extraClasspathWasPassed, + forcedProjectName = forcedProjectName ) maybeInputs.map { inputs => diff --git a/modules/cli/src/main/scala/scala/cli/errors/FoundVirtualInputsError.scala b/modules/cli/src/main/scala/scala/cli/errors/FoundVirtualInputsError.scala index e143fe84b2..64d90756d1 100644 --- a/modules/cli/src/main/scala/scala/cli/errors/FoundVirtualInputsError.scala +++ b/modules/cli/src/main/scala/scala/cli/errors/FoundVirtualInputsError.scala @@ -1,7 +1,7 @@ package scala.cli.errors import scala.build.errors.BuildException -import scala.build.input.{Inputs, Virtual} +import scala.build.input.{ModuleInputs, Virtual} final class FoundVirtualInputsError( val virtualInputs: Seq[Virtual] diff --git a/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala b/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala index 3fcdc157e1..d58d123ea9 100644 --- a/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala +++ b/modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala @@ -5,7 +5,7 @@ import java.nio.charset.StandardCharsets import java.security.MessageDigest import scala.build.Build -import scala.build.input.{Inputs, OnDisk, ResourceDirectory} +import scala.build.input.{ModuleInputs, OnDisk, ResourceDirectory} import scala.build.internal.Constants object CachedBinary { diff --git a/modules/core/src/main/scala/scala/build/errors/ModuleConfigurationError.scala b/modules/core/src/main/scala/scala/build/errors/ModuleConfigurationError.scala new file mode 100644 index 0000000000..79ba69a9eb --- /dev/null +++ b/modules/core/src/main/scala/scala/build/errors/ModuleConfigurationError.scala @@ -0,0 +1,4 @@ +package scala.build.errors + +final class ModuleConfigurationError(message: String) + extends BuildException(message) diff --git a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala index 593cbb237b..ba5ac85bbb 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala @@ -1,6 +1,6 @@ package scala.cli.integration -import ch.epfl.scala.bsp4j.JvmTestEnvironmentParams +import ch.epfl.scala.bsp4j.{BuildTargetEvent, JvmTestEnvironmentParams} import ch.epfl.scala.bsp4j as b import com.eed3si9n.expecty.Expecty.expect import com.google.gson.{Gson, JsonElement} @@ -9,6 +9,7 @@ import java.net.URI import java.nio.file.Paths import scala.async.Async.{async, await} +import scala.cli.integration.compose.ComposeBspTestDefinitions import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration.* @@ -16,7 +17,8 @@ import scala.jdk.CollectionConverters.* import scala.util.Properties abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs - with BspSuite with ScriptWrapperTestDefinitions { + with BspSuite with ScriptWrapperTestDefinitions + with ComposeBspTestDefinitions { _: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions @@ -348,8 +350,9 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg expect(compileResp.getStatusCode == b.StatusCode.ERROR) val diagnosticsParams = { - val diagnostics = localClient.diagnostics() - val params = diagnostics(2) + val diagnostics = localClient.latestDiagnostics() + expect(diagnostics.isDefined) + val params = diagnostics.get expect(params.getBuildTarget.getUri == targetUri) expect( TestUtil.normalizeUri(params.getTextDocument.getUri) == @@ -603,7 +606,12 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg val changes = didChangeParams.getChanges.asScala.toSeq expect(changes.length == 2) - val change = changes.head + val change: BuildTargetEvent = { + val targets = changes.map(_.getTarget) + expect(targets.length == 2) + val mainTarget = extractMainTargets(targets) + changes.find(_.getTarget == mainTarget).get + } expect(change.getTarget.getUri == targetUri) expect(change.getKind == b.BuildTargetEventKind.CHANGED) @@ -719,7 +727,12 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg val changes = didChangeParams.getChanges.asScala.toSeq expect(changes.length == 2) - val change = changes.head + val change: BuildTargetEvent = { + val targets = changes.map(_.getTarget) + expect(targets.length == 2) + val mainTarget = extractMainTargets(targets) + changes.find(_.getTarget == mainTarget).get + } expect(change.getTarget.getUri == targetUri) expect(change.getKind == b.BuildTargetEventKind.CHANGED) } diff --git a/modules/integration/src/test/scala/scala/cli/integration/compose/ComposeBspTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/compose/ComposeBspTestDefinitions.scala new file mode 100644 index 0000000000..924099531f --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/compose/ComposeBspTestDefinitions.scala @@ -0,0 +1,161 @@ +package scala.cli.integration.compose + +import ch.epfl.scala.bsp4j.BuildTargetIdentifier +import ch.epfl.scala.bsp4j as b +import com.eed3si9n.expecty.Expecty.expect + +import scala.async.Async.{async, await} +import scala.cli.integration.* +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.jdk.CollectionConverters.* + +trait ComposeBspTestDefinitions extends ScalaCliSuite { _: BspTestDefinitions => + test( + "composed setup-ide should write the .bsp file in the directory where module config file was found" + ) { + val testInputs = TestInputs( + os.rel / Constants.moduleConfigFileName -> + """[modules.webpage] + |dependsOn = ["core"] + | + |[modules.core] + |roots = ["Core.scala", "Utils.scala"] + |""".stripMargin, + os.rel / "webpage" / "Website.scala" -> "", + os.rel / "Core.scala" -> "", + os.rel / "Utils.scala" -> "" + ) + + testInputs.fromRoot { root => + os.proc(TestUtil.cli, "--power", "setup-ide", ".", extraOptions).call( + cwd = root, + stdout = os.Inherit + ) + val details = readBspConfig(root) + val expectedIdeOptionsFile = root / Constants.workspaceDirName / "ide-options-v2.json" + val expectedIdeLaunchFile = root / Constants.workspaceDirName / "ide-launcher-options.json" + val expectedIdeInputsFile = root / Constants.workspaceDirName / "ide-inputs.json" + val expectedIdeEnvsFile = root / Constants.workspaceDirName / "ide-envs.json" + val expectedArgv = Seq( + TestUtil.cliPath, + "--power", + "bsp", + "--json-options", + expectedIdeOptionsFile.toString, + "--json-launcher-options", + expectedIdeLaunchFile.toString, + "--envs-file", + expectedIdeEnvsFile.toString, + (root / Constants.moduleConfigFileName).toString + ) + expect(details.argv == expectedArgv) + expect(os.isFile(expectedIdeOptionsFile)) + expect(os.isFile(expectedIdeInputsFile)) + } + } + + test("composed bsp should have build targets for all modules and compile OK") { + val testInputs = TestInputs( + os.rel / Constants.moduleConfigFileName -> + """[modules.core] + | + |[modules.utils] + |roots = ["Utils.scala", "Utils2.scala"] + |""".stripMargin, + os.rel / "core" / "Core.scala" -> + """object Core extends App { + | println("Core") + |} + |""".stripMargin, + os.rel / "Utils.scala" -> "object Utils { def util: String = \"util\"}", + os.rel / "Utils2.scala" -> "object Utils2 { def util: String = \"util2\"}" + ) + + withBsp(testInputs, Seq("--power", ".", "-v", "-v", "-v")) { (root, _, remoteServer) => + async { + val buildTargetsResp = await(remoteServer.workspaceBuildTargets().asScala) + val (coreTarget, utilsTarget) = { + val targets = buildTargetsResp.getTargets.asScala.map(_.getId).toSeq + expect(targets.length == 4) + expect(extractMainTargetsOfModules(targets).size == 2) + expect(extractTestTargetsOfModules(targets).size == 2) + extractMainTargets(targets.filter(_.getUri.contains("core"))) -> + extractMainTargets(targets.filter(_.getUri.contains("utils"))) + } + + def checkTarget( + target: BuildTargetIdentifier, + expectedSources: Seq[os.Path] + ): Future[Unit] = async { + val targetUri = TestUtil.normalizeUri(target.getUri) + checkTargetUri(root, targetUri) + + val targets = List(target).asJava + + { + val resp = await { + remoteServer + .buildTargetDependencySources(new b.DependencySourcesParams(targets)) + .asScala + } + val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq + expect(foundTargets == Seq(targetUri)) + val foundDepSources = resp.getItems.asScala + .flatMap(_.getSources.asScala) + .toSeq + .map { uri => + val idx = uri.lastIndexOf('/') + uri.drop(idx + 1) + } + if (actualScalaVersion.startsWith("2.")) { + expect(foundDepSources.length == 1) + expect(foundDepSources.forall(_.startsWith("scala-library-"))) + } + else { + expect(foundDepSources.length == 2) + expect(foundDepSources.exists(_.startsWith("scala-library-"))) + expect(foundDepSources.exists(_.startsWith("scala3-library_3-3"))) + } + expect(foundDepSources.forall(_.endsWith("-sources.jar"))) + } + + { + val resp = await(remoteServer.buildTargetSources(new b.SourcesParams(targets)).asScala) + val foundTargets = resp.getItems.asScala.map(_.getTarget.getUri).toSeq + expect(foundTargets == Seq(targetUri)) + val foundSources = resp.getItems.asScala + .map(_.getSources.asScala.map(_.getUri).toSeq) + .toSeq + .flatMap(_.map(TestUtil.normalizeUri)) + + val expextedSources0 = + expectedSources.map(s => TestUtil.normalizeUri(s.toNIO.toUri.toASCIIString)) + + expect(foundSources == expextedSources0) + } + + { + val resp = await(remoteServer.buildTargetCompile(new b.CompileParams(targets)).asScala) + expect(resp.getStatusCode == b.StatusCode.OK) + } + } + + await(checkTarget(coreTarget, Seq(root / "core" / "Core.scala"))) + await(checkTarget(utilsTarget, Seq(root / "Utils.scala", root / "Utils2.scala"))) + } + } + } + + private def extractMainTargetsOfModules(targets: Seq[BuildTargetIdentifier]) + : Seq[BuildTargetIdentifier] = + targets.collect { + case t if !t.getUri.contains("-test") => t + } + + private def extractTestTargetsOfModules(targets: Seq[BuildTargetIdentifier]) + : Seq[BuildTargetIdentifier] = + targets.collect { + case t if t.getUri.contains("-test") => t + } +} diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 193c01331e..1457b2608b 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -619,6 +619,10 @@ object BuildOptions { } } + def pickJavaHomeWithHighestVersion(buildOptionsSeq: Seq[BuildOptions]): JavaHomeInfo = + buildOptionsSeq.map(_.javaHome().value) + .maxBy(_.version) + implicit val hasHashData: HasHashData[BuildOptions] = HasHashData.derive implicit val monoid: ConfigMonoid[BuildOptions] = ConfigMonoid.derive } diff --git a/project/deps.sc b/project/deps.sc index 681076a4af..812e368cb9 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -232,6 +232,7 @@ object Deps { def svm = ivy"org.graalvm.nativeimage:svm:$graalVmVersion" def swoval = ivy"com.swoval:file-tree-views:2.1.12" def testInterface = ivy"org.scala-sbt:test-interface:1.0" + def tomlScala = ivy"tech.sparse:toml-scala_2.13:0.2.2" val toolkitVersion = "0.5.0" val toolkitVersionForNative04 = "0.3.0" val toolkitVersionForNative05 = toolkitVersion diff --git a/project/settings.sc b/project/settings.sc index aeb0435bd9..800a1a1324 100644 --- a/project/settings.sc +++ b/project/settings.sc @@ -837,6 +837,7 @@ trait ScalaCliModule extends ScalaModule { def workspaceDirName = ".scala-build" def projectFileName = "project.scala" def jvmPropertiesFileName = ".scala-jvmopts" +def moduleConfigFileName = "modules.toml" case class License(licenseId: String, name: String, reference: String) object License {