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
…2196)

* Support nativeTarget to build Scala Native static/shared libraries

* Address PR comments

* Update directives docs

* formatting

* Handle linux-specific linker option

* Fix old test

* Remove dirty merge

* Fix the command line option for passing the native target & add a relevant test

* Fix test

* Apply suggestions from code review

Co-authored-by: Maciej Gajek <[email protected]>

---------

Co-authored-by: Piotr Chabelski <[email protected]>
Co-authored-by: Maciej Gajek <[email protected]>
  • Loading branch information
3 people authored Oct 11, 2023
1 parent 2fd3469 commit 8263c39
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 52 deletions.
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)
@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 @@ -273,7 +273,9 @@ final case class SharedOptions(
nativeLinking,
nativeLinkingDefaults,
nativeCompile,
nativeCompileDefaults
nativeCompileDefaults,
embedResources,
nativeTarget
)
}

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"))
test("shared library native") {
libraryNativeTest(shared = false)
}

}

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

0 comments on commit 8263c39

Please sign in to comment.