Skip to content

Commit

Permalink
Support nativeTarget to build Scala Native static/shared libraries
Browse files Browse the repository at this point in the history
  • Loading branch information
keynmol committed Jun 9, 2023
1 parent 9283e87 commit baeb22c
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 53 deletions.
154 changes: 120 additions & 34 deletions modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ import scala.cli.packaging.{Library, NativeImage}
import scala.cli.util.ArgHelpers.*
import scala.cli.util.ConfigDbUtils
import scala.util.Properties
import scala.build.options.ScalaNativeOptions
import scala.build.options.ScalaNativeTarget
import scala.deriving.Mirror
import scala.build.options.PackageType.Native

object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
override def name = "package"
Expand Down Expand Up @@ -85,6 +89,19 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
configDb.get(Keys.actions).getOrElse(None)
)

def detectNativePackageType(
platform: Platform,
sn: ScalaNativeOptions
): Option[Native & scala.deriving.Mirror.Singleton] =
Option.when(platform == Platform.Native) {
import ScalaNativeTarget.*
sn.buildTargetStr.flatMap(fromString).map {
case Application => PackageType.Native.Application
case LibraryDynamic => PackageType.Native.LibraryDynamic
case LibraryStatic => PackageType.Native.LibraryStatic
}
}.flatten

if (options.watch.watchMode) {
var expectedModifyEpochSecondOpt = Option.empty[Long]
val watcher = Build.watch(
Expand All @@ -101,12 +118,15 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
) { res =>
res.orReport(logger).map(_.main).foreach {
case s: Build.Successful =>
s.copyOutput(options.shared)
val mtimeDestPath = doPackage(
logger = logger,
outputOpt = options.output.filter(_.nonEmpty),
force = options.force,
forcedPackageTypeOpt = options.forcedPackageTypeOpt,
forcedPackageTypeOpt =
options.forcedPackageTypeOpt orElse detectNativePackageType(
s.options.platform.value,
s.options.scalaNativeOptions
),
build = s,
extraArgs = args.unparsed,
expectedModifyEpochSecondOpt = expectedModifyEpochSecondOpt,
Expand Down Expand Up @@ -146,7 +166,11 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
logger = logger,
outputOpt = options.output.filter(_.nonEmpty),
force = options.force,
forcedPackageTypeOpt = options.forcedPackageTypeOpt,
forcedPackageTypeOpt =
options.forcedPackageTypeOpt orElse detectNativePackageType(
s.options.platform.value,
s.options.scalaNativeOptions
),
build = s,
extraArgs = args.unparsed,
expectedModifyEpochSecondOpt = None,
Expand Down Expand Up @@ -199,7 +223,14 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
lazy val validPackageScalaJS =
Seq(PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar)
lazy val validPackageScalaNative =
Seq(PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar)
Seq(
PackageType.LibraryJar,
PackageType.SourceJar,
PackageType.DocJar,
PackageType.Native.Application,
PackageType.Native.LibraryDynamic,
PackageType.Native.LibraryStatic
)

forcedPackageTypeOpt -> build.options.platform.value match {
case (Some(forcedPackageType), _) => Right(forcedPackageType)
Expand All @@ -226,44 +257,63 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
else Left(new MalformedCliInputError(
s"Unsupported package type: $basePackageType for Scala Native."
))
validatedPackageType.getOrElse(Right(PackageType.Native))

validatedPackageType.getOrElse(Right(PackageType.Native.Application))
case _ => Right(basePackageTypeOpt.getOrElse(PackageType.Bootstrap))
}
}

// TODO When possible, call alreadyExistsCheck() before compiling stuff

def extension = packageType match {
case PackageType.LibraryJar => ".jar"
case PackageType.SourceJar => ".jar"
case PackageType.DocJar => ".jar"
case _: PackageType.Assembly => ".jar"
case PackageType.Spark => ".jar"
case PackageType.Js => ".js"
case PackageType.Debian => ".deb"
case PackageType.Dmg => ".dmg"
case PackageType.Pkg => ".pkg"
case PackageType.Rpm => ".rpm"
case PackageType.Msi => ".msi"
case PackageType.Native if Properties.isWin => ".exe"
case PackageType.LibraryJar => ".jar"
case PackageType.SourceJar => ".jar"
case PackageType.DocJar => ".jar"
case _: PackageType.Assembly => ".jar"
case PackageType.Spark => ".jar"
case PackageType.Js => ".js"
case PackageType.Debian => ".deb"
case PackageType.Dmg => ".dmg"
case PackageType.Pkg => ".pkg"
case PackageType.Rpm => ".rpm"
case PackageType.Msi => ".msi"

case PackageType.Native.Application =>
if Properties.isWin then ".exe" else ""
case PackageType.Native.LibraryDynamic =>
if Properties.isWin then ".dll" else if Properties.isMac then ".dylib" else ".so"
case PackageType.Native.LibraryStatic =>
if Properties.isWin then ".lib" else ".a"

case PackageType.GraalVMNativeImage if Properties.isWin => ".exe"
case _ if Properties.isWin => ".bat"
case _ => ""
}

def defaultName = packageType match {
case PackageType.LibraryJar => "library.jar"
case PackageType.SourceJar => "source.jar"
case PackageType.DocJar => "scaladoc.jar"
case _: PackageType.Assembly => "app.jar"
case PackageType.Spark => "job.jar"
case PackageType.Js => "app.js"
case PackageType.Debian => "app.deb"
case PackageType.Dmg => "app.dmg"
case PackageType.Pkg => "app.pkg"
case PackageType.Rpm => "app.rpm"
case PackageType.Msi => "app.msi"
case PackageType.Native if Properties.isWin => "app.exe"
case PackageType.LibraryJar => "library.jar"
case PackageType.SourceJar => "source.jar"
case PackageType.DocJar => "scaladoc.jar"
case _: PackageType.Assembly => "app.jar"
case PackageType.Spark => "job.jar"
case PackageType.Js => "app.js"
case PackageType.Debian => "app.deb"
case PackageType.Dmg => "app.dmg"
case PackageType.Pkg => "app.pkg"
case PackageType.Rpm => "app.rpm"
case PackageType.Msi => "app.msi"

case PackageType.Native.Application =>
if Properties.isWin then "app.exe" else "app"

case PackageType.Native.LibraryDynamic =>
if Properties.isWin then "library.dll"
else if Properties.isMac then "library.dylib"
else "library.so"

case PackageType.Native.LibraryStatic =>
if Properties.isWin then "library.lib" else "library.a"

case PackageType.GraalVMNativeImage if Properties.isWin => "app.exe"
case _ if Properties.isWin => "app.bat"
case _ => "app"
Expand Down Expand Up @@ -399,8 +449,20 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
case PackageType.Js =>
value(buildJs(build, destPath, mainClassOpt, logger))

case PackageType.Native =>
val cachedDest = value(buildNative(build, value(mainClass), logger))
case tpe: PackageType.Native =>
import PackageType.Native.*
val mainClassO =
tpe match
case Application => Some(value(mainClass))
case _ => None

val cachedDest = value(buildNative(
build = build,
mainClass = mainClassO,
targetType = tpe,
destPath = Some(destPath),
logger = logger
))
if (force) os.copy.over(cachedDest, destPath, createFolders = true)
else os.copy(cachedDest, destPath, createFolders = true)
destPath
Expand Down Expand Up @@ -667,7 +729,14 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
case Platform.JVM => value(bootstrap(build, appPath, mainClass, () => Right(()), logger))
case Platform.JS => buildJs(build, appPath, Some(mainClass), logger)
case Platform.Native =>
val dest = value(buildNative(build, mainClass, logger))
val dest =
value(buildNative(
build = build,
mainClass = Some(mainClass),
targetType = PackageType.Native.Application,
destPath = None,
logger = logger
))
os.copy(dest, appPath)
}

Expand Down Expand Up @@ -995,7 +1064,9 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {

def buildNative(
build: Build.Successful,
mainClass: String,
mainClass: Option[String], // when building a static/dynamic library, we don't need a main class
targetType: PackageType.Native,
destPath: Option[os.Path],
logger: Logger
): Either[BuildException, os.Path] = either {
val dest = build.inputs.nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
Expand All @@ -1016,9 +1087,24 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
Nil
val pythonCliOptions = pythonLdFlags.flatMap(f => Seq("--linking-option", f)).toList

val libraryLinkingOptions: Seq[String] =
Option.when(targetType != PackageType.Native.Application) {
/* If we are building a library, we make sure to change the name
that the linker will put into the loading path - otherwise
the built library will depend on some internal path within .scala-build
*/

destPath.flatMap(_.lastOpt).toSeq.flatMap { filename =>
Seq("--linking-option", s"-Wl,-install_name,$filename")
}
}.toSeq.flatten

import PackageType.Native.*

val allCliOptions = pythonCliOptions ++
cliOptions ++
Seq("--main", mainClass)
libraryLinkingOptions ++
mainClass.toSeq.flatMap(m => Seq("--main", m))

val nativeWorkDir = build.inputs.nativeWorkDir
os.makeDir.all(nativeWorkDir)
Expand Down
9 changes: 8 additions & 1 deletion modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import scala.cli.packaging.Library.fullClassPathMaybeAsJar
import scala.cli.util.ArgHelpers.*
import scala.cli.util.ConfigDbUtils
import scala.util.{Properties, Try}
import scala.build.options.PackageType

object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
override def group: String = HelpCommandGroup.Main.toString
Expand Down Expand Up @@ -663,7 +664,13 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
mainClass: String,
logger: Logger
)(f: os.Path => T): Either[BuildException, T] =
Package.buildNative(build, mainClass, logger).map(f)
Package.buildNative(
build = build,
mainClass = Some(mainClass),
targetType = PackageType.Native.Application,
destPath = None,
logger = logger
).map(f)

final class PythonDetectionError(cause: Throwable) extends BuildException(
s"Error detecting Python environment: ${cause.getMessage}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ final case class ScalaNativeOptions(
@Tag(tags.implementation)
nativeCompileDefaults: Option[Boolean] = None, //TODO does it even work when we default it to true while handling?

@Group(HelpGroup.ScalaNative.toString)
@HelpMessage("Build target type")
@Tag(tags.should)
nativeTarget: Option[String] = None,

@Group(HelpGroup.ScalaNative.toString)
@HelpMessage("Embed resources into the Scala Native binary (can be read with the Java resources API)")
@Tag(tags.should)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ final case class SharedOptions(
nativeLinking,
nativeLinkingDefaults,
nativeCompile,
nativeCompileDefaults
nativeCompileDefaults,
embedResources
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import scala.cli.commands.SpecificationLevel
|
|`//> using nativeClangPP` _value_
|
|`//> using nativeEmbedResources` _true|false_""".stripMargin
|`//> using nativeEmbedResources` _true|false_
""".stripMargin.trim
)
@DirectiveDescription("Add Scala Native options")
@DirectiveLevel(SpecificationLevel.SHOULD)
Expand All @@ -41,7 +42,8 @@ final case class ScalaNative(
nativeClang: Option[String] = None,
@DirectiveName("nativeClangPp")
nativeClangPP: Option[String] = None,
nativeEmbedResources: Option[Boolean] = None
nativeEmbedResources: Option[Boolean] = None,
nativeTarget: Option[String] = None,
) extends HasBuildOptions {
// format: on
def buildOptions: Either[BuildException, BuildOptions] = {
Expand All @@ -54,7 +56,8 @@ final case class ScalaNative(
linkingOptions = nativeLinking,
clang = nativeClang,
clangpp = nativeClangPP,
embedResources = nativeEmbedResources
embedResources = nativeEmbedResources,
buildTargetStr = nativeTarget
)
val buildOpt = BuildOptions(scalaNativeOptions = nativeOptions)
Right(buildOpt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,10 +469,52 @@ abstract class PackageTestDefinitions(val scalaVersionOpt: Option[String])
}
}

if (!Properties.isWin && actualScalaVersion.startsWith("2.13"))
def libraryNativeTest(shared: Boolean = false): Unit = {
val fileName = "simple.sc"
val nativeTarget = if (shared) "dynamic" else "static"
val inputs = TestInputs(
os.rel / fileName ->
s"""
|//> using platform scala-native
|//> using nativeTarget $nativeTarget
|import scala.scalanative.unsafe._
|object myLib{
| @exported
| def addLongs(l: Long, r: Long): Long = l + r
| @exported("mylib_addInts")
| def addInts(l: Int, r: Int): Int = l + r
|}""".stripMargin
)
val destName = {
val ext =
if (!shared)
if (Properties.isWin) ".lib" else ".a"
else if (Properties.isWin) ".dll"
else if (Properties.isMac) ".dylib"
else ".so"
fileName.stripSuffix(".sc") + ext
}

inputs.fromRoot { root =>
os.proc(TestUtil.cli, "--power", "package", extraOptions, fileName).call(
cwd = root,
stdin = os.Inherit,
stdout = os.Inherit
)

val library = root / destName
expect(os.isFile(library))
}
}

if (!Properties.isWin && actualScalaVersion.startsWith("2.13")) {
test("simple native") {
simpleNativeTest()
}
test("dynamic and static library native") {
libraryNativeTest()
}
}

test("assembly") {
val fileName = "simple.sc"
Expand Down

0 comments on commit baeb22c

Please sign in to comment.