@@ -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,21 @@ 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+ buildOptions = builds.map(_.options).reduce(_ orElse _),
161+ allArtifacts = builds.map(_.artifacts),
162+ mainJarsOrClassDirs =
163+ if (asJar) builds.map(Library .libraryJar(_)) else builds.map(_.output),
164+ allowExit = allowExit,
165+ runMode = runMode,
166+ successfulBuilds = builds
171167 )
168+ }
172169
173170 val cross = options.sharedRepl.compileCross.cross.getOrElse(false )
174171 val configDb = ConfigDbUtils .configDb.orExit(logger)
@@ -178,18 +175,22 @@ object Repl extends ScalaCommand[ReplOptions] {
178175 )
179176
180177 if (inputs.isEmpty) {
181- val artifacts = initialBuildOptions.artifacts(logger, Scope .Main ).orExit(logger)
178+ val allArtifacts =
179+ Seq (initialBuildOptions.artifacts(logger, Scope .Main ).orExit(logger)) ++
180+ (if options.sharedRepl.scope.test
181+ then Seq (initialBuildOptions.artifacts(logger, Scope .Test ).orExit(logger))
182+ else Nil )
182183 // synchronizing, so that multiple presses to enter (handled by WatchUtil.waitForCtrlC)
183184 // don't try to run repls in parallel
184185 val lock = new Object
185186 def runThing () = lock.synchronized {
186187 doRunRepl(
187- initialBuildOptions,
188- artifacts ,
189- None ,
188+ buildOptions = initialBuildOptions,
189+ allArtifacts = allArtifacts ,
190+ mainJarsOrClassDirs = Seq .empty ,
190191 allowExit = ! options.sharedRepl.watch.watchMode,
191192 runMode = runMode(options),
192- buildOpt = None
193+ successfulBuilds = Seq .empty
193194 )
194195 }
195196 runThing()
@@ -207,22 +208,20 @@ object Repl extends ScalaCommand[ReplOptions] {
207208 None ,
208209 logger,
209210 crossBuilds = cross,
210- buildTests = false ,
211+ buildTests = options.sharedRepl.scope.test ,
211212 partial = None ,
212213 actionableDiagnostics = actionableDiagnostics,
213214 postAction = () => WatchUtil .printWatchMessage()
214215 ) { res =>
215216 for (builds <- res.orReport(logger))
216- builds.main match {
217- case s : Build . Successful =>
217+ postBuild( builds, allowExit = false ) {
218+ successfulBuilds =>
218219 doRunReplFromBuild(
219- s ,
220+ successfulBuilds ,
220221 allowExit = false ,
221222 runMode = runMode(options),
222223 asJar = options.shared.asJar
223224 )
224- case _ : Build .Failed => buildFailed(allowExit = false )
225- case _ : Build .Cancelled => buildCancelled(allowExit = false )
226225 }
227226 }
228227 try WatchUtil .waitForCtrlC(() => watcher.schedule())
@@ -237,25 +236,35 @@ object Repl extends ScalaCommand[ReplOptions] {
237236 None ,
238237 logger,
239238 crossBuilds = cross,
240- buildTests = false ,
239+ buildTests = options.sharedRepl.scope.test ,
241240 partial = None ,
242241 actionableDiagnostics = actionableDiagnostics
243242 )
244243 .orExit(logger)
245- builds.main match {
246- case s : Build . Successful =>
244+ postBuild( builds, allowExit = false ) {
245+ successfulBuilds =>
247246 doRunReplFromBuild(
248- s ,
247+ successfulBuilds ,
249248 allowExit = true ,
250249 runMode = runMode(options),
251250 asJar = options.shared.asJar
252251 )
253- case _ : Build .Failed => buildFailed(allowExit = true )
254- case _ : Build .Cancelled => buildCancelled(allowExit = true )
255252 }
256253 }
257254 }
258255
256+ def postBuild (builds : Builds , allowExit : Boolean )(f : Seq [Build .Successful ] => Unit ): Unit = {
257+ if builds.anyBuildFailed then {
258+ System .err.println(" Compilation failed" )
259+ if allowExit then sys.exit(1 )
260+ }
261+ else if builds.anyBuildCancelled then {
262+ System .err.println(" Build cancelled" )
263+ if allowExit then sys.exit(1 )
264+ }
265+ else f((Seq (builds.main) ++ builds.get(Scope .Test ).toSeq).map(_.asInstanceOf [Build .Successful ]))
266+ }
267+
259268 private def maybeAdaptForWindows (args : Seq [String ]): Seq [String ] =
260269 if (Properties .isWin)
261270 args.map { a =>
@@ -268,24 +277,28 @@ object Repl extends ScalaCommand[ReplOptions] {
268277 private def runRepl (
269278 options : BuildOptions ,
270279 programArgs : Seq [String ],
271- artifacts : Artifacts ,
272- mainJarOrClassDir : Option [os.Path ],
280+ allArtifacts : Seq [ Artifacts ] ,
281+ mainJarsOrClassDirs : Seq [os.Path ],
273282 directories : scala.build.Directories ,
274283 logger : Logger ,
275284 allowExit : Boolean ,
276285 dryRun : Boolean ,
277286 runMode : RunMode .HasRepl ,
278- buildOpt : Option [Build .Successful ]
287+ successfulBuilds : Seq [Build .Successful ]
279288 ): Either [BuildException , Unit ] = either {
280289
281290 val setupPython = options.notForBloopOptions.python.getOrElse(false )
282291
283292 val cache = options.internal.cache.getOrElse(FileCache ())
284293 val shouldUseAmmonite = options.notForBloopOptions.replOptions.useAmmonite
285294
286- val scalaParams = artifacts.scalaOpt match {
287- case Some (artifacts) => artifacts.params
288- case None => ScalaParameters (Constants .defaultScalaVersion)
295+ val scalaParams : ScalaParameters = value {
296+ val distinctScalaArtifacts = allArtifacts.flatMap(_.scalaOpt).distinctBy(_.params)
297+ if distinctScalaArtifacts.isEmpty then
298+ Right (ScalaParameters (Constants .defaultScalaVersion))
299+ else if distinctScalaArtifacts.length == 1 then
300+ Right (distinctScalaArtifacts.head.params)
301+ else Left (MultipleScalaVersionsError (distinctScalaArtifacts.map(_.params.scalaVersion)))
289302 }
290303
291304 val (scalapyJavaOpts, scalapyExtraEnv) =
@@ -302,7 +315,7 @@ object Repl extends ScalaCommand[ReplOptions] {
302315 // Putting current dir in PYTHONPATH, see
303316 // https://github.com/VirtusLab/scala-cli/pull/1616#issuecomment-1333283174
304317 // for context.
305- val dirs = buildOpt .map(_.inputs.workspace).toSeq ++ Seq (os.pwd)
318+ val dirs = successfulBuilds .map(_.inputs.workspace) ++ Seq (os.pwd)
306319 (props0, pythonPathEnv(dirs : _* ))
307320 }
308321 else
@@ -338,15 +351,14 @@ object Repl extends ScalaCommand[ReplOptions] {
338351
339352 // TODO Allow to disable printing the welcome banner and the "Loading..." message in Ammonite.
340353
341- val rootClasses = mainJarOrClassDir match {
342- case None => Nil
343- case Some (dir) if os.isDir(dir) =>
354+ val rootClasses = mainJarsOrClassDirs.flatMap {
355+ case dir if os.isDir(dir) =>
344356 os.list(dir)
345357 .filter(_.last.endsWith(" .class" ))
346358 .filter(os.isFile(_)) // just in case
347359 .map(_.last.stripSuffix(" .class" ))
348360 .sorted
349- case Some ( jar) =>
361+ case jar =>
350362 var zf : ZipFile = null
351363 try {
352364 zf = new ZipFile (jar.toIO)
@@ -396,7 +408,7 @@ object Repl extends ScalaCommand[ReplOptions] {
396408 replArtifacts.replJavaOpts ++
397409 options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
398410 extraProps.toVector.sorted.map { case (k, v) => s " -D $k= $v" },
399- classPath = mainJarOrClassDir.toSeq ++ replArtifacts.replClassPath,
411+ classPath = mainJarsOrClassDirs ++ replArtifacts.replClassPath,
400412 mainClass = replArtifacts.replMainClass,
401413 args = maybeAdaptForWindows(depClassPathArgs ++ replArgs),
402414 logger = logger,
@@ -411,8 +423,8 @@ object Repl extends ScalaCommand[ReplOptions] {
411423 value {
412424 ReplArtifacts .default(
413425 scalaParams,
414- artifacts. userDependencies,
415- artifacts. extraClassPath,
426+ allArtifacts.flatMap(_. userDependencies).distinct ,
427+ allArtifacts.flatMap(_. extraClassPath).distinct ,
416428 logger,
417429 cache,
418430 value(options.finalRepositories),
@@ -427,9 +439,9 @@ object Repl extends ScalaCommand[ReplOptions] {
427439 ReplArtifacts .ammonite(
428440 scalaParams,
429441 options.notForBloopOptions.replOptions.ammoniteVersion(scalaParams.scalaVersion, logger),
430- artifacts. userDependencies,
431- artifacts. extraClassPath,
432- artifacts. extraSourceJars,
442+ allArtifacts.flatMap(_. userDependencies) ,
443+ allArtifacts.flatMap(_. extraClassPath) ,
444+ allArtifacts.flatMap(_. extraSourceJars) ,
433445 value(options.finalRepositories),
434446 logger,
435447 cache,
0 commit comments