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 e274348a22..890ba8492a 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala @@ -8,7 +8,14 @@ import java.util.Locale import scala.build.{Inputs, Os, Sources} import scala.build.internal.AmmUtil -import scala.build.options.{BuildOptions, BuildRequirements, ClassPathOptions, ScalaOptions} +import scala.build.options.{ + BuildOptions, + BuildRequirements, + ClassPathOptions, + ScalaJsOptions, + ScalaNativeOptions, + ScalaOptions +} import scala.collection.JavaConverters._ case object ScalaPreprocessor extends Preprocessor { @@ -105,8 +112,33 @@ case object ScalaPreprocessor extends Preprocessor { scalaVersion = Some(scalaVer) ) ) - case _ => - sys.error(s"Unrecognized using directive: ${dir.values.mkString(" ")}") + case Seq("repository", repo) if repo.nonEmpty => + BuildOptions( + classPathOptions = ClassPathOptions( + extraRepositories = Seq(repo) + ) + ) + case other => + val maybeOptions = + // TODO Accept several platforms for cross-compilation + if (other.lengthCompare(1) == 0) + isPlatform(normalizePlatform(other.head)).map { + case BuildRequirements.Platform.JVM => + BuildOptions() + case BuildRequirements.Platform.JS => + BuildOptions( + scalaJsOptions = ScalaJsOptions(enable = true) + ) + case BuildRequirements.Platform.Native => + BuildOptions( + scalaNativeOptions = ScalaNativeOptions(enable = true) + ) + } + else + None + maybeOptions.getOrElse { + sys.error(s"Unrecognized using directive: ${other.mkString(" ")}") + } } } .foldLeft(BuildOptions())(_ orElse _) diff --git a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala index a77e504b5d..2e56d1c43e 100644 --- a/modules/cli/src/main/scala/scala/cli/ScalaCli.scala +++ b/modules/cli/src/main/scala/scala/cli/ScalaCli.scala @@ -20,6 +20,7 @@ object ScalaCli extends CommandsEntryPoint { Clean, Compile, Directories, + Export, InstallCompletions, Metabrowse, Repl, diff --git a/modules/cli/src/main/scala/scala/cli/commands/Export.scala b/modules/cli/src/main/scala/scala/cli/commands/Export.scala new file mode 100644 index 0000000000..8b75aa1290 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/Export.scala @@ -0,0 +1,69 @@ +package scala.cli.commands + +import caseapp._ + +import scala.build.{BloopBuildClient, Build, CrossSources, Inputs, Logger, Sources} +import scala.build.internal.CustomCodeWrapper +import scala.build.options.BuildOptions +import scala.build.GeneratedSource + +import scala.cli.export._ + +object Export extends ScalaCommand[ExportOptions] { + + private def prepareBuild( + inputs: Inputs, + buildOptions: BuildOptions, + logger: Logger, + verbosity: Int + ): (Sources, BuildOptions) = { + + logger.log("Preparing build") + + val crossSources = CrossSources.forInputs( + inputs, + Sources.defaultPreprocessors( + buildOptions.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper) + ) + ) + val sources = crossSources.sources(buildOptions) + + if (verbosity >= 3) + pprint.better.log(sources) + + val options0 = buildOptions.orElse(sources.buildOptions) + + (sources, options0) + } + + def sbtBuildTool = Sbt("1.5.5") + def defaultBuildTool = sbtBuildTool + + def run(options: ExportOptions, args: RemainingArgs): Unit = { + + val logger = options.shared.logger + val inputs = options.shared.inputsOrExit(args) + val baseOptions = options.buildOptions + + val (sources, options0) = + prepareBuild(inputs, baseOptions, logger, options.shared.logging.verbosity) + + val buildTool = + if (options.sbt.getOrElse(true)) + sbtBuildTool + else + defaultBuildTool + + val project = buildTool.export(options0, sources) + + val output = options.output.getOrElse("dest") + val dest = os.Path(output, os.pwd) + if (os.exists(dest)) { + System.err.println(s"Error: $output already exists.") + sys.exit(1) + } + + os.makeDir.all(dest) + project.writeTo(dest) + } +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/ExportOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/ExportOptions.scala new file mode 100644 index 0000000000..43d90fb579 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/ExportOptions.scala @@ -0,0 +1,28 @@ +package scala.cli.commands + +import caseapp._ + +import scala.build.options.BuildOptions + +// format: off +final case class ExportOptions( + // FIXME There might be too many options for 'scala-cli export' there + @Recurse + shared: SharedOptions = SharedOptions(), + @Recurse + mainClass: MainClassOptions = MainClassOptions(), + + sbt: Option[Boolean] = None, + + @Name("o") + output: Option[String] = None +) { + // format: on + + def buildOptions: BuildOptions = { + val baseOptions = shared.buildOptions(enableJmh = false, None, ignoreErrors = false) + baseOptions.copy( + mainClass = mainClass.mainClass.filter(_.nonEmpty) + ) + } +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/MainClassOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/MainClassOptions.scala new file mode 100644 index 0000000000..633e98a271 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/commands/MainClassOptions.scala @@ -0,0 +1,18 @@ +package scala.cli.commands + +import caseapp._ + +// format: off +final case class MainClassOptions( + @Group("Entrypoint") + @HelpMessage("Specify which main class to run") + @ValueDescription("main-class") + @Name("M") + mainClass: Option[String] = None +) +// format: on + +object MainClassOptions { + implicit val parser = Parser[MainClassOptions] + implicit val help = Help[MainClassOptions] +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/PackageOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/PackageOptions.scala index b30b4afb05..704f7d86b2 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/PackageOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/PackageOptions.scala @@ -15,6 +15,8 @@ final case class PackageOptions( watch: SharedWatchOptions = SharedWatchOptions(), @Recurse compileCross: CompileCrossOptions = CompileCrossOptions(), + @Recurse + mainClass: MainClassOptions = MainClassOptions(), @Group("Package") @HelpMessage("Set destination path") @@ -32,11 +34,6 @@ final case class PackageOptions( @HelpMessage("Generate an assembly JAR") assembly: Boolean = false, - @Group("Package") - @HelpMessage("Specify which main class to run") - @ValueDescription("main-class") - @Name("M") - mainClass: Option[String] = None, @Recurse packager: PackagerOptions = PackagerOptions(), @Group("Package") @@ -72,7 +69,7 @@ final case class PackageOptions( def buildOptions: BuildOptions = { val baseOptions = shared.buildOptions(enableJmh = false, jmhVersion = None) baseOptions.copy( - mainClass = mainClass.filter(_.nonEmpty), + mainClass = mainClass.mainClass.filter(_.nonEmpty), packageOptions = baseOptions.packageOptions.copy( version = Some(packager.version), launcherApp = packager.launcherApp, diff --git a/modules/cli/src/main/scala/scala/cli/commands/RunOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/RunOptions.scala index 8c9d8701be..ee5b24461b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/RunOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/RunOptions.scala @@ -22,12 +22,8 @@ final case class RunOptions( watch: SharedWatchOptions = SharedWatchOptions(), @Recurse compileCross: CompileCrossOptions = CompileCrossOptions(), - - @Group("Runner") - @HelpMessage("Specify which main class to run") - @ValueDescription("main-class") - @Name("M") - mainClass: Option[String] = None + @Recurse + mainClass: MainClassOptions = MainClassOptions() ) { // format: on @@ -37,7 +33,7 @@ final case class RunOptions( jmhVersion = benchmarking.jmhVersion ) baseOptions.copy( - mainClass = mainClass, + mainClass = mainClass.mainClass, javaOptions = baseOptions.javaOptions.copy( javaOpts = baseOptions.javaOptions.javaOpts ++ sharedJava.allJavaOpts ) diff --git a/modules/cli/src/main/scala/scala/cli/export/BuildTool.scala b/modules/cli/src/main/scala/scala/cli/export/BuildTool.scala new file mode 100644 index 0000000000..50a087ac20 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/export/BuildTool.scala @@ -0,0 +1,8 @@ +package scala.cli.export + +import scala.build.options.BuildOptions +import scala.build.Sources + +sealed abstract class BuildTool extends Product with Serializable { + def export(options: BuildOptions, sources: Sources): Project +} diff --git a/modules/cli/src/main/scala/scala/cli/export/Project.scala b/modules/cli/src/main/scala/scala/cli/export/Project.scala new file mode 100644 index 0000000000..2cc1c87186 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/export/Project.scala @@ -0,0 +1,5 @@ +package scala.cli.export + +abstract class Project extends Product with Serializable { + def writeTo(dir: os.Path): Unit +} diff --git a/modules/cli/src/main/scala/scala/cli/export/Sbt.scala b/modules/cli/src/main/scala/scala/cli/export/Sbt.scala new file mode 100644 index 0000000000..e7c4baec40 --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/export/Sbt.scala @@ -0,0 +1,169 @@ +package scala.cli.export + +import scala.build.internal.Constants +import scala.build.options.BuildOptions +import scala.build.Sources + +import java.nio.charset.StandardCharsets + +import coursier.ivy.IvyRepository +import coursier.maven.MavenRepository +import coursier.parse.RepositoryParser +import dependency.{NoAttributes, ScalaNameAttributes} + +final case class Sbt(sbtVersion: String) { + def export(options: BuildOptions, sources: Sources): SbtProject = { + val q = "\"" + val nl = System.lineSeparator() + val charset = StandardCharsets.UTF_8 + + val mainSources = sources.paths.map { + case (path, relPath) => + val language = + if (path.last.endsWith(".java")) "java" + else "scala" // FIXME Others + // FIXME asSubPath might throw… Make it a SubPath earlier in the API? + (relPath.asSubPath, language, os.read.bytes(path)) + } + + val extraMainSources = sources.inMemory.map { + case (_, relPath, content, _) => + val language = + if (relPath.last.endsWith(".java")) "java" + else "scala" + (relPath.asSubPath, language, content.getBytes(charset)) + } + + // TODO Handle Scala CLI cross-builds + + // TODO Detect pure Java projects? + + val (plugins, pluginSettings) = + if (options.scalaJsOptions.enable) + Seq( + """"org.scala-js" % "sbt-scalajs" % "1.7.0"""" + ) -> Seq( + "enablePlugins(ScalaJSPlugin)", + "scalaJSUseMainModuleInitializer := true" + ) + else if (options.scalaNativeOptions.enable) + Seq( + """"org.scala-native" % "sbt-scala-native" % "0.4.0"""" + ) -> Seq( + "enablePlugins(ScalaNativePlugin)" + ) + else + Nil -> Nil + + val scalaVerSetting = { + val sv = options.scalaOptions.scalaVersion.getOrElse(Constants.defaultScalaVersion) + s"""scalaVersion := "$sv"""" + } + + val repoSettings = + if (options.classPathOptions.extraRepositories.isEmpty) Nil + else { + val repos = options.classPathOptions + .extraRepositories + .map(repo => (repo, RepositoryParser.repository(repo))) + .zipWithIndex + .map { + case ((repoStr, Right(repo: IvyRepository)), idx) => + // TODO repo.authentication? + // TODO repo.metadataPatternOpt + s"""Resolver.url("repo-$idx") artifacts "${repo.pattern.string}"""" + case ((repoStr, Right(repo: MavenRepository)), idx) => + // TODO repo.authentication? + s""""repo-$idx" at "${repo.root}"""" + case _ => + ??? + } + Seq(s"""resolvers ++= Seq(${repos.mkString(", ")})""") + } + + val customJarsSettings = + if (options.classPathOptions.extraCompileOnlyJars.isEmpty) Nil + else { + val jars = options.classPathOptions.extraCompileOnlyJars.map(p => s"""file("$p")""") + Seq(s"""Compile / unmanagedClasspath ++= Seq(${jars.mkString(", ")})""") + } + + // TODO options.classPathOptions.extraJars + + // TODO options.javaOptions.javaOpts + // TODO options.scalaJsOptions.* + // TODO options.scalaNativeOptions.* + + // TODO options.scalaOptions.addScalaLibrary + + val mainClassOptions = options.mainClass match { + case None => Nil + case Some(mainClass) => + Seq(s"""Compile / mainClass := Some("$mainClass")""") + } + + val scalacOptionsSettings = + if (options.scalaOptions.scalacOptions.isEmpty) Nil + else { + val options0 = options + .scalaOptions + .scalacOptions + .map(o => "\"" + o.replace("\"", "\\\"") + "\"") + Seq(s"""scalacOptions ++= Seq(${options0.mkString(", ")})""") + } + + // TODO options.testOptions.frameworkOpt + + val depSettings = { + val depStrings = options.classPathOptions + .extraDependencies + .map { dep => + val org = dep.organization + val name = dep.name + val ver = dep.version + // TODO dep.userParams + // TODO dep.exclude + // TODO dep.attributes + val (sep, suffixOpt) = dep.nameAttributes match { + case NoAttributes => ("%", None) + case s: ScalaNameAttributes => + val suffixOpt0 = + if (s.fullCrossVersion.getOrElse(false)) Some(".cross(CrossVersion.full)") + else None + val sep = + if (s.platform.getOrElse(false)) "%%%" + else "%%" + (sep, suffixOpt0) + } + + val baseDep = s"""$q$org$q $sep $q$name$q % $q$ver$q""" + suffixOpt.fold(baseDep)(suffix => s"($baseDep)$suffix") + } + + if (depStrings.isEmpty) Nil + else if (depStrings.lengthCompare(1) == 0) + Seq(s"""libraryDependencies += ${depStrings.head}""") + else + Seq(s"""libraryDependencies ++= Seq($nl${depStrings.map(" " + _ + nl).mkString})""") + } + + val settings = + Seq( + pluginSettings, + Seq(scalaVerSetting), + mainClassOptions, + scalacOptionsSettings, + repoSettings, + depSettings, + customJarsSettings + ) + + SbtProject( + plugins, + settings, + "1.5.5", + mainSources ++ extraMainSources, + Nil + ) + } +} diff --git a/modules/cli/src/main/scala/scala/cli/export/SbtProject.scala b/modules/cli/src/main/scala/scala/cli/export/SbtProject.scala new file mode 100644 index 0000000000..57426c81ed --- /dev/null +++ b/modules/cli/src/main/scala/scala/cli/export/SbtProject.scala @@ -0,0 +1,48 @@ +package scala.cli.export + +import java.nio.charset.StandardCharsets + +final case class SbtProject( + plugins: Seq[String], + settings: Seq[Seq[String]], + sbtVersion: String, + mainSources: Seq[(os.SubPath, String, Array[Byte])], + testSources: Seq[(os.SubPath, String, Array[Byte])] +) extends Project { + def writeTo(dir: os.Path): Unit = { + val nl = System.lineSeparator() + val charset = StandardCharsets.UTF_8 + + val buildPropsContent = s"sbt.version=$sbtVersion" + nl + os.write( + dir / "project" / "build.properties", + buildPropsContent.getBytes(charset), + createFolders = true + ) + + if (plugins.nonEmpty) { + val pluginsSbtContent = plugins + .map { p => + s"addSbtPlugin($p)" + nl + } + .mkString + os.write(dir / "project" / "plugins.sbt", pluginsSbtContent.getBytes(charset)) + } + + val buildSbtContent = settings + .map { settings0 => + settings0.map(s => s + nl).mkString + nl + } + .mkString + os.write(dir / "build.sbt", buildSbtContent.getBytes(charset)) + + for ((path, language, content) <- mainSources) { + val path0 = dir / "src" / "main" / language / path + os.write(path0, content, createFolders = true) + } + for ((path, language, content) <- testSources) { + val path0 = dir / "src" / "test" / language / path + os.write(path0, content) + } + } +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestDefinitions.scala new file mode 100644 index 0000000000..afb0796d0f --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestDefinitions.scala @@ -0,0 +1,157 @@ +package scala.cli.integration + +import com.eed3si9n.expecty.Expecty.expect +import java.nio.charset.Charset + +abstract class ExportSbtTestDefinitions(val scalaVersionOpt: Option[String]) + extends munit.FunSuite with TestScalaVersionArgs { + + protected lazy val extraOptions = scalaVersionArgs ++ TestUtil.extraOptions + + private lazy val sbtLaunchJar = { + val res = + os.proc(TestUtil.cs, "fetch", "--intransitive", "org.scala-sbt:sbt-launch:1.5.5").call() + val rawPath = res.out.text.trim + val path = os.Path(rawPath, os.pwd) + if (os.isFile(path)) path + else sys.error(s"Something went wrong (invalid sbt launch JAR path '$rawPath')") + } + + protected lazy val sbt: os.Shellable = + Seq[os.Shellable]( + "java", + "-Djline.terminal=jline.UnsupportedTerminal", + "-Dsbt.log.noformat=true", + "-jar", + sbtLaunchJar + ) + + test("JVM") { + val testFile = + if (actualScalaVersion.startsWith("3.")) + s"""using scala $actualScalaVersion + |using org.scala-lang::scala3-compiler:$actualScalaVersion + | + |object Test { + | def main(args: Array[String]): Unit = { + | val message = "Hello from " + dotty.tools.dotc.config.Properties.simpleVersionString + | println(message) + | } + |} + |""".stripMargin + else + s"""using scala $actualScalaVersion + | + |object Test { + | def main(args: Array[String]): Unit = { + | val message = "Hello from " + scala.util.Properties.versionNumberString + | println(message) + | } + |} + |""".stripMargin + val inputs = TestInputs( + Seq( + os.rel / "Test.scala" -> testFile + ) + ) + inputs.fromRoot { root => + os.proc(TestUtil.cli, "export", extraOptions, "--sbt", "-o", "sbt-proj", ".") + .call(cwd = root, stdout = os.Inherit) + val res = os.proc(sbt, "run").call(cwd = root / "sbt-proj") + val output = res.out.text(Charset.defaultCharset()) + expect(output.contains("Hello from " + actualScalaVersion)) + } + } + + test("Scala.JS") { + val testFile = + if (actualScalaVersion.startsWith("3.")) + s"""using scala $actualScalaVersion + |using scala-js + | + |import scala.scalajs.js + | + |object Test: + | def main(args: Array[String]): Unit = + | val console = js.Dynamic.global.console + | console.log("Hello from " + "sbt") + |""".stripMargin + else + s"""using scala $actualScalaVersion + |using scala-js + | + |import scala.scalajs.js + | + |object Test { + | def main(args: Array[String]): Unit = { + | val console = js.Dynamic.global.console + | console.log("Hello from " + "sbt") + | } + |} + |""".stripMargin + val inputs = TestInputs( + Seq( + os.rel / "Test.scala" -> testFile + ) + ) + inputs.fromRoot { root => + os.proc(TestUtil.cli, "export", extraOptions, "--sbt", "-o", "sbt-proj", ".") + .call(cwd = root, stdout = os.Inherit) + val res = os.proc(sbt, "run").call(cwd = root / "sbt-proj") + val output = res.out.text(Charset.defaultCharset()) + expect(output.contains("Hello from sbt")) + } + } + + def scalaNativeTest(): Unit = { + val nl = "\\n" + val testFile = + if (actualScalaVersion.startsWith("3.")) + s"""using scala $actualScalaVersion + |using scala-native + | + |import scala.scalanative.libc._ + |import scala.scalanative.unsafe._ + | + |object Test: + | def main(args: Array[String]): Unit = + | val message = "Hello from " + "sbt" + "$nl" + | Zone { implicit z => + | stdio.printf(toCString(message)) + | } + |""".stripMargin + else + s"""using scala $actualScalaVersion + |using scala-native + | + |import scala.scalanative.libc._ + |import scala.scalanative.unsafe._ + | + |object Test { + | def main(args: Array[String]): Unit = { + | val message = "Hello from " + "sbt" + "$nl" + | Zone { implicit z => + | stdio.printf(toCString(message)) + | } + | } + |} + |""".stripMargin + val inputs = TestInputs( + Seq( + os.rel / "Test.scala" -> testFile + ) + ) + inputs.fromRoot { root => + os.proc(TestUtil.cli, "export", extraOptions, "--sbt", "-o", "sbt-proj", ".") + .call(cwd = root, stdout = os.Inherit) + val res = os.proc(sbt, "run").call(cwd = root / "sbt-proj") + val output = res.out.text(Charset.defaultCharset()) + expect(output.contains("Hello from sbt")) + } + } + if (TestUtil.canRunNative && !actualScalaVersion.startsWith("3.")) + test("Scala Native") { + scalaNativeTest() + } + +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests212.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests212.scala new file mode 100644 index 0000000000..cbd8b110a4 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests212.scala @@ -0,0 +1,7 @@ +package scala.cli.integration + +// format: off +class ExportSbtTests212 extends ExportSbtTestDefinitions( + scalaVersionOpt = Some(Constants.scala212) +) +// format: on diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests213.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests213.scala new file mode 100644 index 0000000000..4c9c5b18dc --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests213.scala @@ -0,0 +1,7 @@ +package scala.cli.integration + +// format: off +class ExportSbtTests213 extends ExportSbtTestDefinitions( + scalaVersionOpt = Some(Constants.scala213) +) +// format: on diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests3.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests3.scala new file mode 100644 index 0000000000..cc1601ee18 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTests3.scala @@ -0,0 +1,78 @@ +package scala.cli.integration + +import com.eed3si9n.expecty.Expecty.expect + +import java.nio.charset.Charset + +// format: off +class ExportSbtTests3 extends ExportSbtTestDefinitions( + scalaVersionOpt = Some(Constants.scala3) +) { + // format: on + + test("repository") { + val testFile = + s"""using scala $actualScalaVersion + |using com.github.jupyter:jvm-repr:0.4.0 + |using repository jitpack + |import jupyter._ + |object Test: + | def main(args: Array[String]): Unit = + | val message = "Hello from " + "sbt" + | println(message) + |""".stripMargin + val inputs = TestInputs( + Seq( + os.rel / "Test.scala" -> testFile + ) + ) + inputs.fromRoot { root => + os.proc(TestUtil.cli, "export", extraOptions, "--sbt", "-o", "sbt-proj", ".") + .call(cwd = root, stdout = os.Inherit) + val res = os.proc(sbt, "run").call(cwd = root / "sbt-proj") + val output = res.out.text(Charset.defaultCharset()) + expect(output.contains("Hello from sbt")) + } + } + + test("main class") { + val testFile = + s"""using scala $actualScalaVersion + | + |object Test: + | def main(args: Array[String]): Unit = + | val message = "Hello from " + "sbt" + | println(message) + |""".stripMargin + val otherTestFile = + s"""object Other: + | def main(args: Array[String]): Unit = + | val message = "Hello from " + "other file" + | println(message) + |""".stripMargin + val inputs = TestInputs( + Seq( + os.rel / "Test.scala" -> testFile, + os.rel / "Other.scala" -> otherTestFile + ) + ) + inputs.fromRoot { root => + os.proc( + TestUtil.cli, + "export", + extraOptions, + "--sbt", + "-o", + "sbt-proj", + ".", + "--main-class", + "Test" + ) + .call(cwd = root, stdout = os.Inherit) + val res = os.proc(sbt, "run").call(cwd = root / "sbt-proj") + val output = res.out.text(Charset.defaultCharset()) + expect(output.contains("Hello from sbt")) + } + } + +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestsDefault.scala b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestsDefault.scala new file mode 100644 index 0000000000..309a31579e --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/ExportSbtTestsDefault.scala @@ -0,0 +1,3 @@ +package scala.cli.integration + +class ExportSbtTestsDefault extends CompileTestDefinitions(scalaVersionOpt = None) diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index dfd76a79a8..267b300d6a 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -52,6 +52,7 @@ Available in commands: - [`bloop start`](./commands#bloop-start) - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -124,6 +125,7 @@ Available in commands: - [`bloop start`](./commands#bloop-start) - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -153,6 +155,7 @@ Available in commands: Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -175,6 +178,20 @@ Aliases: `--repo`, `-r` Add repositories +## Export options + +Available in commands: +- [`export`](./commands#export) + + + + +#### `--sbt` + +#### `--output` + +Aliases: `-o` + ## Help options Available in commands: @@ -186,6 +203,7 @@ Available in commands: - [`clean`](./commands#clean) - [`compile`](./commands#compile) - [`directories`](./commands#directories) +- [`export`](./commands#export) - [`install completions`](./commands#install-completions) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) @@ -261,6 +279,7 @@ Available in commands: - [`bloop start`](./commands#bloop-start) - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -301,6 +320,7 @@ Available in commands: - [`bsp`](./commands#bsp) - [`clean`](./commands#clean) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`install completions`](./commands#install-completions) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) @@ -328,6 +348,22 @@ Decrease verbosity Use progress bars +## Main class options + +Available in commands: +- [`export`](./commands#export) +- [`package`](./commands#package) +- [`run`](./commands#run) + + + + +#### `--main-class` + +Aliases: `-M` + +Specify which main class to run + ## Metabrowse options Available in commands: @@ -392,12 +428,6 @@ Generate a library JAR rather than an executable JAR Generate an assembly JAR -#### `--main-class` - -Aliases: `-M` - -Specify which main class to run - #### `--deb` Build debian package, available only on linux @@ -552,25 +582,12 @@ Aliases: `-a` Don't actually run the REPL, only fetch it -## Run options - -Available in commands: -- [`run`](./commands#run) - - - - -#### `--main-class` - -Aliases: `-M` - -Specify which main class to run - ## Scala.JS options Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -602,6 +619,7 @@ Enable Scala.JS Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -639,6 +657,7 @@ Enable Scala Native Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) @@ -678,6 +697,7 @@ Aliases: `--name` Available in commands: - [`bsp`](./commands#bsp) - [`compile`](./commands#compile) +- [`export`](./commands#export) - [`browse` / `metabrowse`](./commands#browse) - [`package`](./commands#package) - [`console` / `repl`](./commands#console) diff --git a/website/docs/reference/commands.md b/website/docs/reference/commands.md index 4419c8014d..fe763e5cea 100644 --- a/website/docs/reference/commands.md +++ b/website/docs/reference/commands.md @@ -47,6 +47,21 @@ Accepts options: ## `directories` +## `export` + +Accepts options: +- [compilation server](./cli-options.md#compilation-server-options) +- [coursier](./cli-options.md#coursier-options) +- [dependency](./cli-options.md#dependency-options) +- [export](./cli-options.md#export-options) +- [jvm](./cli-options.md#jvm-options) +- [logging](./cli-options.md#logging-options) +- [main class](./cli-options.md#main-class-options) +- [Scala.JS](./cli-options.md#scalajs-options) +- [Scala Native](./cli-options.md#scala-native-options) +- [scalac](./cli-options.md#scalac-options) +- [shared](./cli-options.md#shared-options) + ## `install completions` Accepts options: @@ -105,6 +120,7 @@ Accepts options: - [dependency](./cli-options.md#dependency-options) - [jvm](./cli-options.md#jvm-options) - [logging](./cli-options.md#logging-options) +- [main class](./cli-options.md#main-class-options) - [package](./cli-options.md#package-options) - [packager](./cli-options.md#packager-options) - [Scala.JS](./cli-options.md#scalajs-options) @@ -129,7 +145,7 @@ Accepts options: - [java](./cli-options.md#java-options) - [jvm](./cli-options.md#jvm-options) - [logging](./cli-options.md#logging-options) -- [run](./cli-options.md#run-options) +- [main class](./cli-options.md#main-class-options) - [Scala.JS](./cli-options.md#scalajs-options) - [Scala Native](./cli-options.md#scala-native-options) - [scalac](./cli-options.md#scalac-options)