diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index 6eae09903a..84e3fdd57c 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -59,13 +59,14 @@ object Build { def outputOpt: Some[os.Path] = Some(output) def dependencyClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.classPath def fullClassPath: Seq[os.Path] = Seq(output) ++ dependencyClassPath + private lazy val mainClassesFoundInProject: Seq[String] = MainClass.find(output).sorted + private lazy val mainClassesFoundOnExtraClasspath: Seq[String] = + options.classPathOptions.extraClassPath.flatMap(MainClass.find).sorted + private lazy val mainClassesFoundInUserExtraDependencies: Seq[String] = + artifacts.jarsForUserExtraDependencies.flatMap(MainClass.findInDependency).sorted def foundMainClasses(): Seq[String] = { - val found = - MainClass.find(output).sorted ++ - options.classPathOptions.extraClassPath.flatMap(MainClass.find).sorted - if (inputs.isEmpty && found.isEmpty) - artifacts.jarsForUserExtraDependencies.flatMap(MainClass.findInDependency).sorted - else found + val found = mainClassesFoundInProject ++ mainClassesFoundOnExtraClasspath + if inputs.isEmpty && found.isEmpty then mainClassesFoundInUserExtraDependencies else found } def retainedMainClass( mainClasses: Seq[String], @@ -119,28 +120,65 @@ object Build { case Sources.InMemory(_, _, _, Some(wrapperParams)) => wrapperParams.mainClass } - - val filteredMainClasses = - mainClasses.filter(!scriptInferredMainClasses.contains(_)) - if (filteredMainClasses.length == 1) { - val pickedMainClass = filteredMainClasses.head - if (scriptInferredMainClasses.nonEmpty) { - val firstScript = scriptInferredMainClasses.head - val scriptsString = scriptInferredMainClasses.mkString(", ") + .filter(mainClasses.contains(_)) + val rawInputInferredMainClasses = + mainClasses + .filterNot(scriptInferredMainClasses.contains(_)) + .filterNot(mainClassesFoundOnExtraClasspath.contains(_)) + .filterNot(mainClassesFoundInUserExtraDependencies.contains(_)) + val extraClasspathInferredMainClasses = + mainClassesFoundOnExtraClasspath.filter(mainClasses.contains(_)) + val userExtraDependenciesInferredMainClasses = + mainClassesFoundInUserExtraDependencies.filter(mainClasses.contains(_)) + + def logMessageOnLesserPriorityMainClasses( + pickedMainClass: String, + mainClassDescriptor: String, + lesserPriorityMainClasses: Seq[String] + ): Unit = + if lesserPriorityMainClasses.nonEmpty then { + val first = lesserPriorityMainClasses.head + val completeString = lesserPriorityMainClasses.mkString(", ") logger.message( - s"Running $pickedMainClass. Also detected script main classes: $scriptsString" + s"""Running $pickedMainClass. Also detected $mainClassDescriptor: $completeString + |You can run any one of them by passing option --main-class, i.e. --main-class $first + |All available main classes can always be listed by passing option --list-main-classes""".stripMargin ) - logger.message( - s"You can run any one of them by passing option --main-class, i.e. --main-class $firstScript" + } + + ( + rawInputInferredMainClasses, + scriptInferredMainClasses, + extraClasspathInferredMainClasses, + userExtraDependenciesInferredMainClasses + ) match { + case (Seq(pickedMainClass), scriptInferredMainClasses, _, _) => + logMessageOnLesserPriorityMainClasses( + pickedMainClass = pickedMainClass, + mainClassDescriptor = "script main classes", + lesserPriorityMainClasses = scriptInferredMainClasses ) - logger.message( - "All available main classes can always be listed by passing option --list-main-classes" + Right(pickedMainClass) + case (rawMcs, scriptMcs, extraCpMcs, userExtraDepsMcs) if rawMcs.length > 1 => + Left(rawMcs ++ scriptMcs ++ extraCpMcs ++ userExtraDepsMcs) + case (Nil, Seq(pickedMainClass), _, _) => Right(pickedMainClass) + case (Nil, scriptMcs, extraCpMcs, userExtraDepsMcs) if scriptMcs.length > 1 => + Left(scriptMcs ++ extraCpMcs ++ userExtraDepsMcs) + case (Nil, Nil, Seq(pickedMainClass), userExtraDepsMcs) => + logMessageOnLesserPriorityMainClasses( + pickedMainClass = pickedMainClass, + mainClassDescriptor = "other main classes in dependencies", + lesserPriorityMainClasses = userExtraDepsMcs ) - } - Right(pickedMainClass) + Right(pickedMainClass) + case (Nil, Nil, extraCpMcs, userExtraDepsMcs) if extraCpMcs.length > 1 => + Left(extraCpMcs ++ userExtraDepsMcs) + case (Nil, Nil, Nil, Seq(pickedMainClass)) => Right(pickedMainClass) + case (Nil, Nil, Nil, userExtraDepsMcs) if userExtraDepsMcs.length > 1 => + Left(userExtraDepsMcs) + case (rawMcs, scriptMcs, extraCpMcs, userExtraDepsMcs) => + Left(rawMcs ++ scriptMcs ++ extraCpMcs ++ userExtraDepsMcs) } - else - Left(mainClasses) } def retainedMainClassOpt( mainClasses: Seq[String], diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index 42b95b41b7..d15c5cdf24 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -2303,4 +2303,55 @@ abstract class RunTestDefinitions expect(res.out.trim() == expectedOutput) } } + + { + val expectedMessage = "Hello" + for { + (actualInputPath, inputPathToCall, inputs) <- { + val scalaInputPath = os.rel / "Main.scala" + val scriptInputPath = os.rel / "script.sc" + val scalaInputs = TestInputs( + scalaInputPath -> s"""object Main extends App { println("$expectedMessage") }""" + ) + val scriptInputs = TestInputs(scriptInputPath -> s"""println("$expectedMessage")""") + Seq( + (scalaInputPath, ".", scalaInputs), + (scalaInputPath, scalaInputPath.toString, scalaInputs), + (scriptInputPath, ".", scriptInputs), + (scriptInputPath, scriptInputPath.toString, scriptInputs) + ) + } + inputExtension = "." + actualInputPath.last.split('.').last + } + test( + s"prioritise main class in a $inputExtension file passed as $inputPathToCall over main classes in dependencies on the classpath" + ) { + inputs.fromRoot { root => + val localCache = root / "local-cache" + val dependencyVersion = "42.7.4" + val csRes = os.proc( + TestUtil.cs, + "fetch", + "--cache", + localCache, + s"org.postgresql:postgresql:$dependencyVersion" + ) + .call(cwd = root) + val dependencyJar = csRes.out.trim().linesIterator.toSeq.head + + // pass classpath via -cp + val res = + os.proc(TestUtil.cli, "run", inputPathToCall, extraOptions, "-cp", dependencyJar) + .call(cwd = root) + expect(res.out.trim() == expectedMessage) + + // pass classpath via args file + val argsFileName = "args.txt" + os.write(root / argsFileName, s"-cp $dependencyJar") + val res2 = os.proc(TestUtil.cli, "run", inputPathToCall, extraOptions, s"@$argsFileName") + .call(cwd = root) + expect(res2.out.trim() == expectedMessage) + } + } + } }