Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nativeTarget to build Scala Native static/shared libraries #2196

Merged
merged 11 commits into from
Oct 11, 2023
150 changes: 112 additions & 38 deletions modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import scala.build.interactive.InteractiveFileOps
import scala.build.internal.Util.*
import scala.build.internal.resource.NativeResourceMapper
import scala.build.internal.{Runner, ScalaJsLinkerConfig}
import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform}
import scala.build.options.PackageType.Native
import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalaNativeTarget}
import scala.cli.CurrentParams
import scala.cli.commands.OptionsHelper.*
import scala.cli.commands.doc.Doc
Expand Down Expand Up @@ -198,36 +199,54 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
// 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 @@ -363,8 +382,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 @@ -631,7 +662,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 @@ -959,7 +997,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 @@ -980,9 +1020,26 @@ 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 =>
val linkerOption =
if Properties.isLinux then s"-Wl,-soname,$filename" else s"-Wl,-install_name,$filename"
Seq("--linking-option", linkerOption)
}
}.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 Expand Up @@ -1040,7 +1097,14 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
lazy val validPackageScalaJS =
Seq(PackageType.Js, PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar)
lazy val validPackageScalaNative =
Seq(PackageType.Native, 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 @@ -1061,14 +1125,24 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
))
validatedPackageType.getOrElse(Right(PackageType.Js))
case (_, Platform.Native) =>
val specificNativePackageType =
import ScalaNativeTarget.*
build.options.scalaNativeOptions.buildTargetStr.flatMap(fromString).map {
case Application => PackageType.Native.Application
case LibraryDynamic => PackageType.Native.LibraryDynamic
case LibraryStatic => PackageType.Native.LibraryStatic
}

val validatedPackageType =
for (basePackageType <- basePackageTypeOpt)
yield
if (validPackageScalaNative.contains(basePackageType)) Right(basePackageType)
else Left(new MalformedCliInputError(
s"Unsupported package type: $basePackageType for Scala Native."
))
validatedPackageType.getOrElse(Right(PackageType.Native))
for
basePackageType <- specificNativePackageType orElse basePackageTypeOpt
yield
if (validPackageScalaNative.contains(basePackageType)) Right(basePackageType)
else Left(new MalformedCliInputError(
s"Unsupported package type: $basePackageType for Scala Native."
))

validatedPackageType.getOrElse(Right(PackageType.Native.Application))
case _ => Right(basePackageTypeOpt.getOrElse(PackageType.Bootstrap))
}
}
Expand Down
10 changes: 8 additions & 2 deletions modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import scala.build.errors.BuildException
import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand}
import scala.build.internal.util.ConsoleUtils.ScalaCliConsole
import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig}
import scala.build.options.{BuildOptions, JavaOpt, Platform, ScalacOpt}
import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalacOpt}
import scala.cli.CurrentParams
import scala.cli.commands.package0.Package
import scala.cli.commands.publish.ConfigUtil.*
Expand Down Expand Up @@ -665,7 +665,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,12 @@ 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)
keynmol marked this conversation as resolved.
Show resolved Hide resolved
@ValueDescription("app|static|dynamic")
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 @@ -272,7 +272,9 @@ final case class SharedOptions(
nativeLinking,
nativeLinkingDefaults,
nativeCompile,
nativeCompileDefaults
nativeCompileDefaults,
embedResources,
nativeTarget
keynmol marked this conversation as resolved.
Show resolved Hide resolved
)
}

Expand Down
2 changes: 1 addition & 1 deletion modules/cli/src/test/scala/cli/tests/PackageTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class PackageTests extends munit.FunSuite {
val build = maybeFirstBuild.orThrow.successfulOpt.get

val packageType = Package.resolvePackageType(build, None).orThrow
expect(packageType == PackageType.Native)
expect(packageType == PackageType.Native.Application)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import scala.cli.commands.SpecificationLevel
|
|`//> using nativeClangPP` _value_
|
|`//> using nativeEmbedResources` _true|false_""".stripMargin
|`//> using nativeEmbedResources` _true|false_
|
|`//> using nativeTarget` _application|library-dynamic|library-static_
""".stripMargin.trim
)
@DirectiveDescription("Add Scala Native options")
@DirectiveLevel(SpecificationLevel.SHOULD)
Expand All @@ -41,7 +44,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 +58,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,73 @@ abstract class PackageTestDefinitions(val scalaVersionOpt: Option[String])
}
}

if (!Properties.isWin && actualScalaVersion.startsWith("2.13"))
def libraryNativeTest(
shared: Boolean = false,
commandLineShared: Option[Boolean] = None
): Unit = {
val fileName = "simple.sc"
val directiveNativeTarget = if (shared) "dynamic" else "static"
val inputs = TestInputs(
os.rel / fileName ->
s"""
|//> using platform scala-native
|//> using nativeTarget $directiveNativeTarget
|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 && !commandLineShared.getOrElse(false))
if (Properties.isWin) ".lib" else ".a"
else if (Properties.isWin) ".dll"
else if (Properties.isMac) ".dylib"
else ".so"
fileName.stripSuffix(".sc") + ext
}

val nativeTargetOpts = commandLineShared match {
case Some(true) => Seq("--native-target", "dynamic")
case Some(false) => Seq("--native-target", "static")
case None => Seq.empty
}

inputs.fromRoot { root =>
os.proc(TestUtil.cli, "--power", "package", extraOptions, nativeTargetOpts, 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 library native") {
libraryNativeTest(shared = true)
}

test("dynamic library native override from command line") {
libraryNativeTest(shared = false, commandLineShared = Some(true))
}

// To produce a static library, `LLVM_BIN` environment variable needs to be
// present (for `llvm-ar` utility)
if (sys.env.contains("LLVM_BIN"))
keynmol marked this conversation as resolved.
Show resolved Hide resolved
test("shared library native") {
libraryNativeTest(shared = false)
}

}

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