Skip to content

Commit

Permalink
Make main class optional in preamble-less assemblies
Browse files Browse the repository at this point in the history
This allows to generate assemblies with
'scala-cli package --assembly --preamble=false' even when no main class
is defined. This can be handy to make assemblies that are not meant to
be run as is, but are passed to other tools (such as proguard).
  • Loading branch information
alexarchambault committed Jul 25, 2022
1 parent 1da1a32 commit 82c20bf
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 9 deletions.
15 changes: 15 additions & 0 deletions modules/build/src/main/scala/scala/build/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ object Build {
else
Left(mainClasses)
}
def retainedMainClassOpt(
mainClasses: Seq[String],
logger: Logger
): Option[String] = {
val defaultMainClassOpt = sources.defaultMainClass
.filter(name => mainClasses.contains(name))
def foundMainClass =
mainClasses match {
case Seq() => None
case Seq(mainClass) => Some(mainClass)
case _ => inferredMainClass(mainClasses, logger).toOption
}

defaultMainClassOpt.orElse(foundMainClass)
}

def crossKey: CrossKey = {
val optKey = scalaParams.map { params =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ final case class PackageOptions(
preamble: Boolean = true,
@Group("Package")
@Hidden
@HelpMessage("For assembly JAR, whether to specify a main class in the JAR manifest")
mainClassInManifest: Option[Boolean] = None,
@Group("Package")
@Hidden
@HelpMessage("Generate an assembly JAR for Spark (assembly that doesn't contain Spark, nor any of its dependencies)")
spark: Boolean = false,
@Group("Package")
Expand Down
36 changes: 32 additions & 4 deletions modules/cli/src/main/scala/scala/cli/commands/Package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import scala.build.errors.{
BuildException,
CompositeBuildException,
MalformedCliInputError,
NoMainClassFoundError,
ScalaNativeBuildError
}
import scala.build.interactive.InteractiveFileOps
Expand Down Expand Up @@ -276,6 +277,11 @@ object Package extends ScalaCommand[PackageOptions] {
case None => build.retainedMainClass(build.foundMainClasses(), logger)
}

def mainClassOpt: Option[String] =
build.options.mainClass.orElse {
build.retainedMainClassOpt(build.foundMainClasses(), logger)
}

val packageOptions = build.options.notForBloopOptions.packageOptions

val outputPath = packageType match {
Expand Down Expand Up @@ -306,7 +312,24 @@ object Package extends ScalaCommand[PackageOptions] {
assembly(
build,
destPath,
value(mainClass),
a.mainClassInManifest match {
case None =>
if (a.addPreamble) {
val clsName = value {
mainClass.left.map {
case e: NoMainClassFoundError =>
// This one has a slightly better error message, suggesting --preamble=false
new NoMainClassFoundForAssemblyError(e)
case e => e
}
}
Some(clsName)
}
else
mainClassOpt
case Some(false) => None
case Some(true) => Some(value(mainClass))
},
Nil,
withPreamble = a.addPreamble,
() => alreadyExistsCheck(),
Expand All @@ -319,7 +342,7 @@ object Package extends ScalaCommand[PackageOptions] {
assembly(
build,
destPath,
value(mainClass),
mainClassOpt,
// The Spark modules are assumed to be already on the class path,
// along with all their transitive dependencies (originating from
// the Spark distribution), so we don't include any of them in the
Expand Down Expand Up @@ -727,7 +750,7 @@ object Package extends ScalaCommand[PackageOptions] {
private def assembly(
build: Build.Successful,
destPath: os.Path,
mainClass: String,
mainClassOpt: Option[String],
extraProvided: Seq[dependency.AnyModule],
withPreamble: Boolean,
alreadyExistsCheck: () => Unit,
Expand Down Expand Up @@ -766,13 +789,18 @@ object Package extends ScalaCommand[PackageOptions] {
val params = Parameters.Assembly()
.withExtraZipEntries(byteCodeZipEntries)
.withFiles(files.map(_.toIO))
.withMainClass(mainClass)
.withMainClass(mainClassOpt)
.withPreambleOpt(preambleOpt)
alreadyExistsCheck()
AssemblyGenerator.generate(params, destPath.toNIO)
ProcUtil.maybeUpdatePreamble(destPath)
}

final class NoMainClassFoundForAssemblyError(cause: NoMainClassFoundError) extends BuildException(
"No main class found for assembly. Either pass one with --main-class, or make the assembly non-runnable with --preamble=false",
cause = cause
)

def withSourceJar[T](
build: Build.Successful,
defaultLastModified: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ object PackageOptionsUtil {
forcedPackageTypeOpt.orElse {
if (v.library) Some(PackageType.LibraryJar)
else if (source) Some(PackageType.SourceJar)
else if (assembly) Some(PackageType.Assembly(addPreamble = preamble))
else if (assembly) Some(
PackageType.Assembly(
addPreamble = preamble,
mainClassInManifest = mainClassInManifest
)
)
else if (spark) Some(PackageType.Spark)
else if (deb) Some(PackageType.Debian)
else if (dmg) Some(PackageType.Dmg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import java.util.regex.Pattern
import java.util.zip.ZipFile

import scala.jdk.CollectionConverters._
import scala.util.Properties
import scala.util.{Properties, Using}

abstract class PackageTestDefinitions(val scalaVersionOpt: Option[String])
extends munit.FunSuite with TestScalaVersionArgs {
Expand Down Expand Up @@ -499,6 +499,50 @@ abstract class PackageTestDefinitions(val scalaVersionOpt: Option[String])
}
}

test("assembly no preamble nor main class") {
val inputs = TestInputs(
Seq(
os.rel / "Hello.scala" ->
s"""package hello
|
|object Hello {
| def message: String =
| "Hello from " + "assembly"
|}
|""".stripMargin
)
)
inputs.fromRoot { root =>
os.proc(
TestUtil.cli,
"package",
extraOptions,
"--assembly",
"-o",
"hello.jar",
"--preamble=false",
"."
).call(
cwd = root,
stdin = os.Inherit,
stdout = os.Inherit
)

val launcher = root / "hello.jar"
expect(os.isFile(launcher))

Using.resource(new ZipFile(launcher.toIO)) { zf =>
val entries = zf.entries()
.asScala
.iterator
.map(_.getName)
.filter(_.startsWith("hello/"))
.toVector
expect(entries.contains("hello/Hello.class"))
}
}
}

test("assembly provided") {
val inputs = TestInputs(
Seq(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ object PackageType {
override def sourceBased = true
}
case object DocJar extends PackageType
final case class Assembly(addPreamble: Boolean) extends PackageType {
final case class Assembly(
addPreamble: Boolean,
mainClassInManifest: Option[Boolean]
) extends PackageType {
override def runnable = Some(addPreamble)
}
case object Spark extends PackageType {
Expand All @@ -39,8 +42,8 @@ object PackageType {
case object Msi extends NativePackagerType

val mapping = Seq(
"assembly" -> Assembly(true),
"raw-assembly" -> Assembly(false),
"assembly" -> Assembly(true, None),
"raw-assembly" -> Assembly(false, Some(false)),
"bootstrap" -> Bootstrap,
"library" -> LibraryJar,
"source" -> SourceJar,
Expand Down
4 changes: 4 additions & 0 deletions website/docs/reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,10 @@ Generate an assembly JAR

For assembly JAR, whether to add a bash / bat preamble

#### `--main-class-in-manifest`

For assembly JAR, whether to specify a main class in the JAR manifest

#### `--spark`

Generate an assembly JAR for Spark (assembly that doesn't contain Spark, nor any of its dependencies)
Expand Down

0 comments on commit 82c20bf

Please sign in to comment.