Skip to content

Commit

Permalink
Add --as-jar option
Browse files Browse the repository at this point in the history
Putting the project byte code in the class path as a JAR, in the
compile / run / test / repl commands
  • Loading branch information
alexarchambault committed Apr 13, 2023
1 parent 9d8ce60 commit affa002
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import scala.cli.commands.update.Update
import scala.cli.commands.util.BuildCommandHelpers
import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil}
import scala.cli.config.{ConfigDb, Keys}
import scala.cli.packaging.Library.fullClassPathMaybeAsJar
import scala.cli.util.ArgHelpers.*
import scala.cli.util.ConfigDbUtils
object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers {
Expand Down Expand Up @@ -82,7 +83,9 @@ object Compile extends ScalaCommand[CompileOptions] with BuildCommandHelpers {
} yield s
if (options.printClassPath)
for (s <- successulBuildOpt) {
val cp = s.fullClassPath.map(_.toString).mkString(File.pathSeparator)
val cp = s.fullClassPathMaybeAsJar(options.shared.asJar)
.map(_.toString)
.mkString(File.pathSeparator)
println(cp)
}
successulBuildOpt.foreach(_.copyOutput(options.shared))
Expand Down
63 changes: 48 additions & 15 deletions modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import coursier.cache.FileCache
import coursier.error.{FetchError, ResolutionError}
import dependency.*

import java.util.zip.ZipFile

import scala.build.EitherCps.{either, value}
import scala.build.*
import scala.build.errors.{BuildException, CantDownloadAmmoniteError, FetchingDependenciesError}
Expand All @@ -24,8 +26,10 @@ import scala.cli.commands.run.RunMode
import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions}
import scala.cli.commands.{ScalaCommand, WatchUtil}
import scala.cli.config.{ConfigDb, Keys}
import scala.cli.packaging.Library
import scala.cli.util.ArgHelpers.*
import scala.cli.util.ConfigDbUtils
import scala.jdk.CollectionConverters._
import scala.util.Properties

object Repl extends ScalaCommand[ReplOptions] {
Expand Down Expand Up @@ -127,7 +131,7 @@ object Repl extends ScalaCommand[ReplOptions] {
def doRunRepl(
buildOptions: BuildOptions,
artifacts: Artifacts,
classDir: Option[os.Path],
mainJarOrClassDir: Option[os.Path],
allowExit: Boolean,
runMode: RunMode.HasRepl,
buildOpt: Option[Build.Successful]
Expand All @@ -136,7 +140,7 @@ object Repl extends ScalaCommand[ReplOptions] {
buildOptions,
programArgs,
artifacts,
classDir,
mainJarOrClassDir,
directories,
logger,
allowExit = allowExit,
Expand All @@ -154,12 +158,13 @@ object Repl extends ScalaCommand[ReplOptions] {
def doRunReplFromBuild(
build: Build.Successful,
allowExit: Boolean,
runMode: RunMode.HasRepl
runMode: RunMode.HasRepl,
asJar: Boolean
): Unit =
doRunRepl(
build.options,
build.artifacts,
build.outputOpt,
Some(if (asJar) Library.libraryJar(build) else build.output),
allowExit,
runMode,
Some(build)
Expand Down Expand Up @@ -210,7 +215,12 @@ object Repl extends ScalaCommand[ReplOptions] {
for (builds <- res.orReport(logger))
builds.main match {
case s: Build.Successful =>
doRunReplFromBuild(s, allowExit = false, runMode = runMode(options))
doRunReplFromBuild(
s,
allowExit = false,
runMode = runMode(options),
asJar = options.shared.asJar
)
case _: Build.Failed => buildFailed(allowExit = false)
case _: Build.Cancelled => buildCancelled(allowExit = false)
}
Expand All @@ -234,7 +244,12 @@ object Repl extends ScalaCommand[ReplOptions] {
.orExit(logger)
builds.main match {
case s: Build.Successful =>
doRunReplFromBuild(s, allowExit = true, runMode = runMode(options))
doRunReplFromBuild(
s,
allowExit = true,
runMode = runMode(options),
asJar = options.shared.asJar
)
case _: Build.Failed => buildFailed(allowExit = true)
case _: Build.Cancelled => buildCancelled(allowExit = true)
}
Expand All @@ -254,7 +269,7 @@ object Repl extends ScalaCommand[ReplOptions] {
options: BuildOptions,
programArgs: Seq[String],
artifacts: Artifacts,
classDir: Option[os.Path],
mainJarOrClassDir: Option[os.Path],
directories: scala.build.Directories,
logger: Logger,
allowExit: Boolean,
Expand Down Expand Up @@ -324,13 +339,31 @@ object Repl extends ScalaCommand[ReplOptions] {

// TODO Allow to disable printing the welcome banner and the "Loading..." message in Ammonite.

val rootClasses = classDir
.toSeq
.flatMap(os.list(_))
.filter(_.last.endsWith(".class"))
.filter(os.isFile(_)) // just in case
.map(_.last.stripSuffix(".class"))
.sorted
val rootClasses = mainJarOrClassDir match {
case None => Nil
case Some(dir) if os.isDir(dir) =>
os.list(dir)
.filter(_.last.endsWith(".class"))
.filter(os.isFile(_)) // just in case
.map(_.last.stripSuffix(".class"))
.sorted
case Some(jar) =>
var zf: ZipFile = null
try {
zf = new ZipFile(jar.toIO)
zf.entries()
.asScala
.map(_.getName)
.filter(!_.contains("/"))
.filter(_.endsWith(".class"))
.map(_.stripSuffix(".class"))
.toVector
.sorted
}
finally
if (zf != null)
zf.close()
}
val warnRootClasses = rootClasses.nonEmpty &&
options.notForBloopOptions.replOptions.useAmmoniteOpt.contains(true)
if (warnRootClasses)
Expand All @@ -354,7 +387,7 @@ object Repl extends ScalaCommand[ReplOptions] {
replArtifacts.replJavaOpts ++
options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
extraProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" },
classDir.toSeq ++ replArtifacts.replClassPath,
mainJarOrClassDir.toSeq ++ replArtifacts.replClassPath,
replArtifacts.replMainClass,
maybeAdaptForWindows(replArgs),
logger,
Expand Down
17 changes: 11 additions & 6 deletions modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import scala.cli.commands.util.{BuildCommandHelpers, RunHadoop, RunSpark}
import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil}
import scala.cli.config.{ConfigDb, Keys}
import scala.cli.internal.ProcUtil
import scala.cli.packaging.Library.fullClassPathMaybeAsJar
import scala.cli.util.ArgHelpers.*
import scala.cli.util.ConfigDbUtils
import scala.util.{Properties, Try}
Expand Down Expand Up @@ -167,7 +168,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
potentialMainClasses,
runMode,
showCommand,
scratchDirOpt
scratchDirOpt,
asJar = options.shared.asJar
)
}

Expand Down Expand Up @@ -335,7 +337,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
potentialMainClasses: Seq[String],
runMode: RunMode,
showCommand: Boolean,
scratchDirOpt: Option[os.Path]
scratchDirOpt: Option[os.Path],
asJar: Boolean
): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either {

val mainClassOpt = build.options.mainClass.filter(_.nonEmpty) // trim it too?
Expand All @@ -360,7 +363,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
allowExecve,
runMode,
showCommand,
scratchDirOpt
scratchDirOpt,
asJar
)
value(res)
}
Expand Down Expand Up @@ -394,7 +398,8 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
allowExecve: Boolean,
runMode: RunMode,
showCommand: Boolean,
scratchDirOpt: Option[os.Path]
scratchDirOpt: Option[os.Path],
asJar: Boolean
): Either[BuildException, Either[Seq[String], (Process, Option[() => Unit])]] = either {

build.options.platform.value match {
Expand Down Expand Up @@ -539,7 +544,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
val command = Runner.jvmCommand(
build.options.javaHome().value.javaCommand,
allJavaOpts,
build.fullClassPath,
build.fullClassPathMaybeAsJar(asJar),
mainClass,
args,
extraEnv = pythonExtraEnv,
Expand All @@ -552,7 +557,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
val proc = Runner.runJvm(
build.options.javaHome().value.javaCommand,
allJavaOpts,
build.fullClassPath,
build.fullClassPathMaybeAsJar(asJar),
mainClass,
args,
logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ final case class SharedOptions(
@Tag(tags.must)
resourceDirs: List[String] = Nil,

@Hidden
@Group(HelpGroup.Java.toString)
@HelpMessage("Put project in class paths as a JAR rather than as a byte code directory")
@Tag(tags.experimental)
asJar: Boolean = false,

@Group(HelpGroup.Scala.toString)
@HelpMessage("Specify platform")
@ValueDescription("scala-js|scala-native|jvm")
Expand Down
7 changes: 5 additions & 2 deletions modules/cli/src/main/scala/scala/cli/commands/test/Test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, SharedOptions}
import scala.cli.commands.update.Update
import scala.cli.commands.{CommandUtils, ScalaCommand, SpecificationLevel, WatchUtil}
import scala.cli.config.{ConfigDb, Keys}
import scala.cli.packaging.Library.fullClassPathMaybeAsJar
import scala.cli.util.ArgHelpers.*
import scala.cli.util.ConfigDbUtils

Expand Down Expand Up @@ -109,7 +110,8 @@ object Test extends ScalaCommand[TestOptions] {
options.requireTests,
args.unparsed,
logger,
allowExecve = allowExit && buildsLen <= 1
allowExecve = allowExit && buildsLen <= 1,
asJar = options.shared.asJar
)
if (printBeforeAfterMessages && idx < buildsLen - 1)
System.err.println()
Expand Down Expand Up @@ -180,6 +182,7 @@ object Test extends ScalaCommand[TestOptions] {
requireTests: Boolean,
args: Seq[String],
logger: Logger,
asJar: Boolean,
allowExecve: Boolean
): Either[BuildException, Int] = either {

Expand Down Expand Up @@ -231,7 +234,7 @@ object Test extends ScalaCommand[TestOptions] {
}.flatten
}
case Platform.JVM =>
val classPath = build.fullClassPath
val classPath = build.fullClassPathMaybeAsJar(asJar)

val testFrameworkOpt0 = testFrameworkOpt.orElse {
findTestFramework(classPath.map(_.toNIO), logger)
Expand Down
7 changes: 7 additions & 0 deletions modules/cli/src/main/scala/scala/cli/packaging/Library.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,11 @@ object Library {
finally if (zos != null) zos.close()
}

extension (build: Build.Successful) {
def fullClassPathAsJar: Seq[os.Path] =
Seq(libraryJar(build)) ++ build.dependencyClassPath
def fullClassPathMaybeAsJar(asJar: Boolean): Seq[os.Path] =
if (asJar) fullClassPathAsJar else build.fullClassPath
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package scala.cli.integration

import com.eed3si9n.expecty.Expecty.expect

import java.io.File

class CompileTestsDefault extends CompileTestDefinitions(scalaVersionOpt = None) {
test("render explain message") {
val fileName = "Hello.scala"
Expand All @@ -20,4 +23,36 @@ class CompileTestsDefault extends CompileTestDefinitions(scalaVersionOpt = None)
expect(out.contains("Explanation"))
}
}

test("as jar") {
val inputs = TestInputs(
os.rel / "Foo.scala" ->
"""object Foo {
| def n = 2
|}
|""".stripMargin
)
inputs.fromRoot { root =>
val out = os.proc(TestUtil.cli, "compile", extraOptions, ".", "--print-class-path")
.call(cwd = root)
.out.trim()
val cp = out.split(File.pathSeparator).toVector.map(os.Path(_, root))
expect(cp.headOption.exists(os.isDir(_)))
expect(cp.drop(1).forall(os.isFile(_)))

val asJarOut = os.proc(
TestUtil.cli,
"--power",
"compile",
extraOptions,
".",
"--print-class-path",
"--as-jar"
)
.call(cwd = root)
.out.trim()
val asJarCp = asJarOut.split(File.pathSeparator).toVector.map(os.Path(_, root))
expect(asJarCp.forall(os.isFile(_)))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,63 @@
package scala.cli.integration

import com.eed3si9n.expecty.Expecty.expect

import scala.util.Properties

// format: off
class ReplTestsDefault extends ReplTestDefinitions(
scalaVersionOpt = None
)
// format: on
) {
// format: on

test("as jar") {
val inputs = TestInputs(
os.rel / "CheckCp.scala" ->
"""//> using lib "com.lihaoyi::os-lib:0.9.1"
|package checkcp
|object CheckCp {
| def hasDir: Boolean =
| sys.props("java.class.path")
| .split(java.io.File.pathSeparator)
| .toVector
| .map(os.Path(_, os.pwd))
| .exists(os.isDir(_))
|}
|""".stripMargin
)

inputs.fromRoot { root =>
val ammArgs = Seq(
"-c",
"""println("hasDir=" + checkcp.CheckCp.hasDir)"""
)
.map {
if (Properties.isWin)
a => if (a.contains(" ")) "\"" + a.replace("\"", "\\\"") + "\"" else a
else
identity
}
.flatMap(arg => Seq("--ammonite-arg", arg))

val output =
os.proc(TestUtil.cli, "--power", "repl", ".", TestUtil.extraOptions, "--ammonite", ammArgs)
.call(cwd = root)
.out.trim()
expect(output == "hasDir=true")

val asJarOutput = os.proc(
TestUtil.cli,
"--power",
"repl",
".",
TestUtil.extraOptions,
"--ammonite",
ammArgs,
"--as-jar"
)
.call(cwd = root)
.out.trim()
expect(asJarOutput == "hasDir=false")
}
}
}

0 comments on commit affa002

Please sign in to comment.