diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index ce93c05da8..332a7da206 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -59,9 +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 - def foundMainClasses(): Seq[String] = - MainClass.find(output).sorted ++ - options.classPathOptions.extraClassPath.flatMap(MainClass.find).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 + } def retainedMainClass( mainClasses: Seq[String], commandString: String, diff --git a/modules/build/src/main/scala/scala/build/internal/MainClass.scala b/modules/build/src/main/scala/scala/build/internal/MainClass.scala index b5148077c3..1faba2ae2a 100644 --- a/modules/build/src/main/scala/scala/build/internal/MainClass.scala +++ b/modules/build/src/main/scala/scala/build/internal/MainClass.scala @@ -4,6 +4,7 @@ import org.objectweb.asm import org.objectweb.asm.ClassReader import java.io.{ByteArrayInputStream, InputStream} +import java.util.jar.{Attributes, JarFile, JarInputStream, Manifest} import java.util.zip.ZipEntry import scala.build.input.Element @@ -67,6 +68,16 @@ object MainClass { ) } + def findInDependency(jar: os.Path): Option[String] = + jar match { + case jar if os.isFile(jar) && jar.last.endsWith(".jar") => + val jarFile = new JarFile(jar.toIO) + val manifest = jarFile.getManifest() + val mainClass = manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS) + Option(mainClass).map(_.asInstanceOf[String]) + case _ => None + } + def find(output: os.Path): Seq[String] = output match { case o if os.isFile(o) && o.last.endsWith(".class") => 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 881526a43f..e19b87aa2b 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 @@ -113,13 +113,22 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { } def runCommand( - options: RunOptions, + options0: RunOptions, inputArgs: Seq[String], programArgs: Seq[String], defaultInputs: () => Option[Inputs], logger: Logger, invokeData: ScalaCliInvokeData ): Unit = { + val shouldDefaultServerFalse = + inputArgs.isEmpty && options0.shared.compilationServer.server.isEmpty && + !options0.shared.hasSnippets + val options = if (shouldDefaultServerFalse) options0.copy(shared = + options0.shared.copy(compilationServer = + options0.shared.compilationServer.copy(server = Some(false)) + ) + ) + else options0 val initialBuildOptions = { val buildOptions = buildOptionsOrExit(options) if (invokeData.subCommand == SubCommand.Shebang) { 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 830c8e382c..f3272d62ab 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 @@ -478,7 +478,8 @@ final case class SharedOptions( )) .extractedClassPath - def extraClasspathWasPassed: Boolean = extraJarsAndClassPath.exists(!_.hasSourceJarSuffix) + def extraClasspathWasPassed: Boolean = + extraJarsAndClassPath.exists(!_.hasSourceJarSuffix) || dependencies.dependency.nonEmpty def extraCompileOnlyClassPath: List[os.Path] = extraCompileOnlyJars.extractedClassPath @@ -627,6 +628,10 @@ final case class SharedOptions( def allJavaSnippets: List[String] = snippet.javaSnippet ++ snippet.executeJava def allMarkdownSnippets: List[String] = snippet.markdownSnippet ++ snippet.executeMarkdown + def hasSnippets = + allScriptSnippets.nonEmpty || allScalaSnippets.nonEmpty || allJavaSnippets + .nonEmpty || allMarkdownSnippets.nonEmpty + def validateInputArgs( args: Seq[String] )(using ScalaCliInvokeData): Seq[Either[String, Seq[Element]]] = diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala index 15cf4ce22a..1051cdf3bb 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala @@ -9,6 +9,11 @@ import scala.util.Properties trait RunScalacCompatTestDefinitions { _: RunTestDefinitions => + + final val smithyVersion = "1.50.0" + private def shutdownBloop() = + os.proc(TestUtil.cli, "bloop", "exit", "--power").call(mergeErrIntoOut = true) + def commandLineScalacXOption(): Unit = { val inputs = TestInputs( os.rel / "Test.scala" -> @@ -274,6 +279,42 @@ trait RunScalacCompatTestDefinitions { expect(runRes.out.trim() == expectedOutput) } } + + test("run main class from --dep even when no explicit inputs are passed") { + shutdownBloop() + val output = os.proc( + TestUtil.cli, + "--dep", + s"software.amazon.smithy:smithy-cli:$smithyVersion", + "--main-class", + "software.amazon.smithy.cli.SmithyCli", + "--", + "--version" + ).call() + assert(output.exitCode == 0) + assert(output.out.text().contains(smithyVersion)) + + // assert bloop wasn't started + assertNoDiff(shutdownBloop().out.text(), "No running Bloop server found.") + } + + test("find and run main class from --dep even when no explicit inputs are passed") { + shutdownBloop() + val output = os.proc( + TestUtil.cli, + "run", + "--dep", + s"software.amazon.smithy:smithy-cli:$smithyVersion", + "--", + "--version" + ).call() + assert(output.exitCode == 0) + assert(output.out.text().contains(smithyVersion)) + + // assert bloop wasn't started + assertNoDiff(shutdownBloop().out.text(), "No running Bloop server found.") + } + test("dont clear output dir") { val expectedOutput = "Hello" val `lib.scala` = os.rel / "lib.scala" diff --git a/modules/options/src/main/scala/scala/build/Artifacts.scala b/modules/options/src/main/scala/scala/build/Artifacts.scala index cf41c566ac..4234bfbfab 100644 --- a/modules/options/src/main/scala/scala/build/Artifacts.scala +++ b/modules/options/src/main/scala/scala/build/Artifacts.scala @@ -22,13 +22,14 @@ import scala.build.errors.{ import scala.build.internal.Constants import scala.build.internal.Constants.* import scala.build.internal.CsLoggerUtil.* -import scala.build.internal.Util.PositionedScalaDependencyOps +import scala.build.internal.Util.{PositionedScalaDependencyOps, ScalaModuleOps} import scala.collection.mutable final case class Artifacts( javacPluginDependencies: Seq[(AnyDependency, String, os.Path)], extraJavacPlugins: Seq[os.Path], - userDependencies: Seq[AnyDependency], + defaultDependencies: Seq[AnyDependency], + extraDependencies: Seq[AnyDependency], userCompileOnlyDependencies: Seq[AnyDependency], internalDependencies: Seq[AnyDependency], detailedArtifacts: Seq[(CsDependency, csCore.Publication, csUtil.Artifact, os.Path)], @@ -41,6 +42,22 @@ final case class Artifacts( hasJvmRunner: Boolean, resolution: Option[Resolution] ) { + + def userDependencies = defaultDependencies ++ extraDependencies + lazy val jarsForUserExtraDependencies = { + val extraDependenciesMap = + extraDependencies.map(dep => dep.module.name -> dep.version).toMap + detailedArtifacts + .iterator + .collect { + case (dep, pub, _, path) + if pub.classifier != Classifier.sources && + extraDependenciesMap.get(dep.module.name.value).contains(dep.version) => path + } + .toVector + .distinct + } + lazy val artifacts: Seq[(String, os.Path)] = detailedArtifacts .iterator @@ -93,7 +110,8 @@ object Artifacts { scalaArtifactsParamsOpt: Option[ScalaArtifactsParams], javacPluginDependencies: Seq[Positioned[AnyDependency]], extraJavacPlugins: Seq[os.Path], - dependencies: Seq[Positioned[AnyDependency]], + defaultDependencies: Seq[Positioned[AnyDependency]], + extraDependencies: Seq[Positioned[AnyDependency]], compileOnlyDependencies: Seq[Positioned[AnyDependency]], extraClassPath: Seq[os.Path], extraCompileOnlyJars: Seq[os.Path], @@ -109,6 +127,7 @@ object Artifacts { logger: Logger, maybeRecoverOnError: BuildException => Option[BuildException] ): Either[BuildException, Artifacts] = either { + val dependencies = defaultDependencies ++ extraDependencies val jvmTestRunnerDependencies = if (addJvmTestRunner) @@ -428,7 +447,8 @@ object Artifacts { Artifacts( javacPlugins0, extraJavacPlugins, - dependencies.map(_.value) ++ scalaOpt.toSeq.flatMap(_.extraDependencies), + defaultDependencies.map(_.value), + extraDependencies.map(_.value) ++ scalaOpt.toSeq.flatMap(_.extraDependencies), compileOnlyDependencies.map(_.value), internalDependencies.map(_.value), fetchRes.fullDetailedArtifacts.collect { case (d, p, a, Some(f)) => 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 de105bb4d1..695022ec04 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -130,12 +130,11 @@ final case class BuildOptions( } else Nil } - private def dependencies: Either[BuildException, Seq[Positioned[AnyDependency]]] = either { + private def defaultDependencies: Either[BuildException, Seq[Positioned[AnyDependency]]] = either { value(maybeJsDependencies).map(Positioned.none(_)) ++ value(maybeNativeDependencies).map(Positioned.none(_)) ++ value(scalaLibraryDependencies).map(Positioned.none(_)) ++ - value(scalaCompilerDependencies).map(Positioned.none(_)) ++ - classPathOptions.extraDependencies.toSeq + value(scalaCompilerDependencies).map(Positioned.none(_)) } private def semanticDbPlugins(logger: Logger): Either[BuildException, Seq[AnyDependency]] = @@ -451,7 +450,8 @@ final case class BuildOptions( scalaArtifactsParamsOpt, javacPluginDependencies = value(javacPluginDependencies), extraJavacPlugins = javaOptions.javacPlugins.map(_.value), - dependencies = value(dependencies), + defaultDependencies = value(defaultDependencies), + extraDependencies = classPathOptions.extraDependencies.toSeq, compileOnlyDependencies = classPathOptions.extraCompileOnlyDependencies.toSeq, extraClassPath = allExtraJars, extraCompileOnlyJars = allExtraCompileOnlyJars,