@@ -12,7 +12,12 @@ import java.util.zip.ZipFile
1212
1313import scala .build .EitherCps .{either , value }
1414import scala .build .*
15- import scala .build .errors .{BuildException , CantDownloadAmmoniteError , FetchingDependenciesError }
15+ import scala .build .errors .{
16+ BuildException ,
17+ CantDownloadAmmoniteError ,
18+ FetchingDependenciesError ,
19+ MultipleScalaVersionsError
20+ }
1621import scala .build .input .Inputs
1722import scala .build .internal .{Constants , Runner }
1823import scala .build .options .{BuildOptions , JavaOpt , MaybeScalaVersion , Scope }
@@ -24,6 +29,7 @@ import scala.cli.commands.run.Run.{
2429}
2530import scala .cli .commands .run .RunMode
2631import scala .cli .commands .shared .{HelpCommandGroup , HelpGroup , SharedOptions }
32+ import scala .cli .commands .util .BuildCommandHelpers
2733import scala .cli .commands .{ScalaCommand , WatchUtil }
2834import scala .cli .config .{ConfigDb , Keys }
2935import scala .cli .packaging .Library
@@ -33,7 +39,7 @@ import scala.cli.{CurrentParams, ScalaCli}
3339import scala .jdk .CollectionConverters .*
3440import scala .util .Properties
3541
36- object Repl extends ScalaCommand [ReplOptions ] {
42+ object Repl extends ScalaCommand [ReplOptions ] with BuildCommandHelpers {
3743 override def group : String = HelpCommandGroup .Main .toString
3844 override def scalaSpecificationLevel = SpecificationLevel .MUST
3945 override def helpFormat : HelpFormat = super .helpFormat
@@ -117,36 +123,25 @@ object Repl extends ScalaCommand[ReplOptions] {
117123
118124 val directories = Directories .directories
119125
120- def buildFailed (allowExit : Boolean ): Unit = {
121- System .err.println(" Compilation failed" )
122- if (allowExit)
123- sys.exit(1 )
124- }
125- def buildCancelled (allowExit : Boolean ): Unit = {
126- System .err.println(" Build cancelled" )
127- if (allowExit)
128- sys.exit(1 )
129- }
130-
131126 def doRunRepl (
132127 buildOptions : BuildOptions ,
133- artifacts : Artifacts ,
134- mainJarOrClassDir : Option [os.Path ],
128+ allArtifacts : Seq [ Artifacts ] ,
129+ mainJarsOrClassDirs : Seq [os.Path ],
135130 allowExit : Boolean ,
136131 runMode : RunMode .HasRepl ,
137- buildOpt : Option [Build .Successful ]
132+ successfulBuilds : Seq [Build .Successful ]
138133 ): Unit = {
139134 val res = runRepl(
140- buildOptions,
141- programArgs,
142- artifacts ,
143- mainJarOrClassDir ,
144- directories,
145- logger,
135+ options = buildOptions,
136+ programArgs = programArgs ,
137+ allArtifacts = allArtifacts ,
138+ mainJarsOrClassDirs = mainJarsOrClassDirs ,
139+ directories = directories ,
140+ logger = logger ,
146141 allowExit = allowExit,
147- options.sharedRepl.replDryRun,
148- runMode,
149- buildOpt
142+ dryRun = options.sharedRepl.replDryRun,
143+ runMode = runMode ,
144+ successfulBuilds = successfulBuilds
150145 )
151146 res match {
152147 case Left (ex) =>
@@ -156,19 +151,23 @@ object Repl extends ScalaCommand[ReplOptions] {
156151 }
157152 }
158153 def doRunReplFromBuild (
159- build : Build .Successful ,
154+ builds : Seq [ Build .Successful ] ,
160155 allowExit : Boolean ,
161156 runMode : RunMode .HasRepl ,
162157 asJar : Boolean
163- ): Unit =
158+ ): Unit = {
164159 doRunRepl(
165- build.options,
166- build.artifacts,
167- Some (if (asJar) Library .libraryJar(build) else build.output),
168- allowExit,
169- runMode,
170- Some (build)
160+ // build options should be the same for both scopes
161+ // combining them may cause for ammonite args to be duplicated, so we're using the main scope's opts
162+ buildOptions = builds.head.options,
163+ allArtifacts = builds.map(_.artifacts),
164+ mainJarsOrClassDirs =
165+ if (asJar) builds.map(Library .libraryJar(_)) else builds.map(_.output),
166+ allowExit = allowExit,
167+ runMode = runMode,
168+ successfulBuilds = builds
171169 )
170+ }
172171
173172 val cross = options.sharedRepl.compileCross.cross.getOrElse(false )
174173 val configDb = ConfigDbUtils .configDb.orExit(logger)
@@ -178,18 +177,22 @@ object Repl extends ScalaCommand[ReplOptions] {
178177 )
179178
180179 if (inputs.isEmpty) {
181- val artifacts = initialBuildOptions.artifacts(logger, Scope .Main ).orExit(logger)
180+ val allArtifacts =
181+ Seq (initialBuildOptions.artifacts(logger, Scope .Main ).orExit(logger)) ++
182+ (if options.sharedRepl.scope.test
183+ then Seq (initialBuildOptions.artifacts(logger, Scope .Test ).orExit(logger))
184+ else Nil )
182185 // synchronizing, so that multiple presses to enter (handled by WatchUtil.waitForCtrlC)
183186 // don't try to run repls in parallel
184187 val lock = new Object
185188 def runThing () = lock.synchronized {
186189 doRunRepl(
187- initialBuildOptions,
188- artifacts ,
189- None ,
190+ buildOptions = initialBuildOptions,
191+ allArtifacts = allArtifacts ,
192+ mainJarsOrClassDirs = Seq .empty ,
190193 allowExit = ! options.sharedRepl.watch.watchMode,
191194 runMode = runMode(options),
192- buildOpt = None
195+ successfulBuilds = Seq .empty
193196 )
194197 }
195198 runThing()
@@ -207,22 +210,20 @@ object Repl extends ScalaCommand[ReplOptions] {
207210 None ,
208211 logger,
209212 crossBuilds = cross,
210- buildTests = false ,
213+ buildTests = options.sharedRepl.scope.test ,
211214 partial = None ,
212215 actionableDiagnostics = actionableDiagnostics,
213216 postAction = () => WatchUtil .printWatchMessage()
214217 ) { res =>
215218 for (builds <- res.orReport(logger))
216- builds.main match {
217- case s : Build . Successful =>
219+ postBuild( builds, allowExit = false ) {
220+ successfulBuilds =>
218221 doRunReplFromBuild(
219- s ,
222+ successfulBuilds ,
220223 allowExit = false ,
221224 runMode = runMode(options),
222225 asJar = options.shared.asJar
223226 )
224- case _ : Build .Failed => buildFailed(allowExit = false )
225- case _ : Build .Cancelled => buildCancelled(allowExit = false )
226227 }
227228 }
228229 try WatchUtil .waitForCtrlC(() => watcher.schedule())
@@ -237,25 +238,35 @@ object Repl extends ScalaCommand[ReplOptions] {
237238 None ,
238239 logger,
239240 crossBuilds = cross,
240- buildTests = false ,
241+ buildTests = options.sharedRepl.scope.test ,
241242 partial = None ,
242243 actionableDiagnostics = actionableDiagnostics
243244 )
244245 .orExit(logger)
245- builds.main match {
246- case s : Build . Successful =>
246+ postBuild( builds, allowExit = false ) {
247+ successfulBuilds =>
247248 doRunReplFromBuild(
248- s ,
249+ successfulBuilds ,
249250 allowExit = true ,
250251 runMode = runMode(options),
251252 asJar = options.shared.asJar
252253 )
253- case _ : Build .Failed => buildFailed(allowExit = true )
254- case _ : Build .Cancelled => buildCancelled(allowExit = true )
255254 }
256255 }
257256 }
258257
258+ def postBuild (builds : Builds , allowExit : Boolean )(f : Seq [Build .Successful ] => Unit ): Unit = {
259+ if builds.anyBuildFailed then {
260+ System .err.println(" Compilation failed" )
261+ if allowExit then sys.exit(1 )
262+ }
263+ else if builds.anyBuildCancelled then {
264+ System .err.println(" Build cancelled" )
265+ if allowExit then sys.exit(1 )
266+ }
267+ else f(builds.builds.sortBy(_.scope).map(_.asInstanceOf [Build .Successful ]))
268+ }
269+
259270 private def maybeAdaptForWindows (args : Seq [String ]): Seq [String ] =
260271 if (Properties .isWin)
261272 args.map { a =>
@@ -268,24 +279,28 @@ object Repl extends ScalaCommand[ReplOptions] {
268279 private def runRepl (
269280 options : BuildOptions ,
270281 programArgs : Seq [String ],
271- artifacts : Artifacts ,
272- mainJarOrClassDir : Option [os.Path ],
282+ allArtifacts : Seq [ Artifacts ] ,
283+ mainJarsOrClassDirs : Seq [os.Path ],
273284 directories : scala.build.Directories ,
274285 logger : Logger ,
275286 allowExit : Boolean ,
276287 dryRun : Boolean ,
277288 runMode : RunMode .HasRepl ,
278- buildOpt : Option [Build .Successful ]
289+ successfulBuilds : Seq [Build .Successful ]
279290 ): Either [BuildException , Unit ] = either {
280291
281292 val setupPython = options.notForBloopOptions.python.getOrElse(false )
282293
283294 val cache = options.internal.cache.getOrElse(FileCache ())
284295 val shouldUseAmmonite = options.notForBloopOptions.replOptions.useAmmonite
285296
286- val scalaParams = artifacts.scalaOpt match {
287- case Some (artifacts) => artifacts.params
288- case None => ScalaParameters (Constants .defaultScalaVersion)
297+ val scalaParams : ScalaParameters = value {
298+ val distinctScalaParams = allArtifacts.flatMap(_.scalaOpt).map(_.params).distinct
299+ if distinctScalaParams.isEmpty then
300+ Right (ScalaParameters (Constants .defaultScalaVersion))
301+ else if distinctScalaParams.length == 1 then
302+ Right (distinctScalaParams.head)
303+ else Left (MultipleScalaVersionsError (distinctScalaParams.map(_.scalaVersion)))
289304 }
290305
291306 val (scalapyJavaOpts, scalapyExtraEnv) =
@@ -302,7 +317,7 @@ object Repl extends ScalaCommand[ReplOptions] {
302317 // Putting current dir in PYTHONPATH, see
303318 // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
304319 // for context.
305- val dirs = buildOpt .map(_.inputs.workspace).toSeq ++ Seq (os.pwd)
320+ val dirs = successfulBuilds .map(_.inputs.workspace) ++ Seq (os.pwd)
306321 (props0, pythonPathEnv(dirs : _* ))
307322 }
308323 else
@@ -338,15 +353,14 @@ object Repl extends ScalaCommand[ReplOptions] {
338353
339354 // TODO Allow to disable printing the welcome banner and the "Loading..." message in Ammonite.
340355
341- val rootClasses = mainJarOrClassDir match {
342- case None => Nil
343- case Some (dir) if os.isDir(dir) =>
356+ val rootClasses = mainJarsOrClassDirs.flatMap {
357+ case dir if os.isDir(dir) =>
344358 os.list(dir)
345359 .filter(_.last.endsWith(" .class" ))
346360 .filter(os.isFile(_)) // just in case
347361 .map(_.last.stripSuffix(" .class" ))
348362 .sorted
349- case Some ( jar) =>
363+ case jar =>
350364 var zf : ZipFile = null
351365 try {
352366 zf = new ZipFile (jar.toIO)
@@ -396,7 +410,7 @@ object Repl extends ScalaCommand[ReplOptions] {
396410 replArtifacts.replJavaOpts ++
397411 options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
398412 extraProps.toVector.sorted.map { case (k, v) => s " -D $k= $v" },
399- classPath = mainJarOrClassDir.toSeq ++ replArtifacts.replClassPath,
413+ classPath = mainJarsOrClassDirs ++ replArtifacts.replClassPath,
400414 mainClass = replArtifacts.replMainClass,
401415 args = maybeAdaptForWindows(depClassPathArgs ++ replArgs),
402416 logger = logger,
@@ -411,8 +425,8 @@ object Repl extends ScalaCommand[ReplOptions] {
411425 value {
412426 ReplArtifacts .default(
413427 scalaParams,
414- artifacts. userDependencies,
415- artifacts. extraClassPath,
428+ allArtifacts.flatMap(_. userDependencies).distinct ,
429+ allArtifacts.flatMap(_. extraClassPath).distinct ,
416430 logger,
417431 cache,
418432 value(options.finalRepositories),
@@ -427,9 +441,9 @@ object Repl extends ScalaCommand[ReplOptions] {
427441 ReplArtifacts .ammonite(
428442 scalaParams,
429443 options.notForBloopOptions.replOptions.ammoniteVersion(scalaParams.scalaVersion, logger),
430- artifacts. userDependencies,
431- artifacts. extraClassPath,
432- artifacts. extraSourceJars,
444+ allArtifacts.flatMap(_. userDependencies) ,
445+ allArtifacts.flatMap(_. extraClassPath) ,
446+ allArtifacts.flatMap(_. extraSourceJars) ,
433447 value(options.finalRepositories),
434448 logger,
435449 cache,
0 commit comments