Skip to content

Commit 8263c39

Browse files
keynmolGedochaoMaciejG604
authored
Support nativeTarget to build Scala Native static/shared libraries (#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]>
1 parent 2fd3469 commit 8263c39

File tree

14 files changed

+300
-52
lines changed

14 files changed

+300
-52
lines changed

modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import scala.build.interactive.InteractiveFileOps
2626
import scala.build.internal.Util.*
2727
import scala.build.internal.resource.NativeResourceMapper
2828
import scala.build.internal.{Runner, ScalaJsLinkerConfig}
29-
import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform}
29+
import scala.build.options.PackageType.Native
30+
import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalaNativeTarget}
3031
import scala.cli.CurrentParams
3132
import scala.cli.commands.OptionsHelper.*
3233
import scala.cli.commands.doc.Doc
@@ -198,36 +199,54 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
198199
// TODO When possible, call alreadyExistsCheck() before compiling stuff
199200

200201
def extension = packageType match {
201-
case PackageType.LibraryJar => ".jar"
202-
case PackageType.SourceJar => ".jar"
203-
case PackageType.DocJar => ".jar"
204-
case _: PackageType.Assembly => ".jar"
205-
case PackageType.Spark => ".jar"
206-
case PackageType.Js => ".js"
207-
case PackageType.Debian => ".deb"
208-
case PackageType.Dmg => ".dmg"
209-
case PackageType.Pkg => ".pkg"
210-
case PackageType.Rpm => ".rpm"
211-
case PackageType.Msi => ".msi"
212-
case PackageType.Native if Properties.isWin => ".exe"
202+
case PackageType.LibraryJar => ".jar"
203+
case PackageType.SourceJar => ".jar"
204+
case PackageType.DocJar => ".jar"
205+
case _: PackageType.Assembly => ".jar"
206+
case PackageType.Spark => ".jar"
207+
case PackageType.Js => ".js"
208+
case PackageType.Debian => ".deb"
209+
case PackageType.Dmg => ".dmg"
210+
case PackageType.Pkg => ".pkg"
211+
case PackageType.Rpm => ".rpm"
212+
case PackageType.Msi => ".msi"
213+
214+
case PackageType.Native.Application =>
215+
if Properties.isWin then ".exe" else ""
216+
case PackageType.Native.LibraryDynamic =>
217+
if Properties.isWin then ".dll" else if Properties.isMac then ".dylib" else ".so"
218+
case PackageType.Native.LibraryStatic =>
219+
if Properties.isWin then ".lib" else ".a"
220+
213221
case PackageType.GraalVMNativeImage if Properties.isWin => ".exe"
214222
case _ if Properties.isWin => ".bat"
215223
case _ => ""
216224
}
217225

218226
def defaultName = packageType match {
219-
case PackageType.LibraryJar => "library.jar"
220-
case PackageType.SourceJar => "source.jar"
221-
case PackageType.DocJar => "scaladoc.jar"
222-
case _: PackageType.Assembly => "app.jar"
223-
case PackageType.Spark => "job.jar"
224-
case PackageType.Js => "app.js"
225-
case PackageType.Debian => "app.deb"
226-
case PackageType.Dmg => "app.dmg"
227-
case PackageType.Pkg => "app.pkg"
228-
case PackageType.Rpm => "app.rpm"
229-
case PackageType.Msi => "app.msi"
230-
case PackageType.Native if Properties.isWin => "app.exe"
227+
case PackageType.LibraryJar => "library.jar"
228+
case PackageType.SourceJar => "source.jar"
229+
case PackageType.DocJar => "scaladoc.jar"
230+
case _: PackageType.Assembly => "app.jar"
231+
case PackageType.Spark => "job.jar"
232+
case PackageType.Js => "app.js"
233+
case PackageType.Debian => "app.deb"
234+
case PackageType.Dmg => "app.dmg"
235+
case PackageType.Pkg => "app.pkg"
236+
case PackageType.Rpm => "app.rpm"
237+
case PackageType.Msi => "app.msi"
238+
239+
case PackageType.Native.Application =>
240+
if Properties.isWin then "app.exe" else "app"
241+
242+
case PackageType.Native.LibraryDynamic =>
243+
if Properties.isWin then "library.dll"
244+
else if Properties.isMac then "library.dylib"
245+
else "library.so"
246+
247+
case PackageType.Native.LibraryStatic =>
248+
if Properties.isWin then "library.lib" else "library.a"
249+
231250
case PackageType.GraalVMNativeImage if Properties.isWin => "app.exe"
232251
case _ if Properties.isWin => "app.bat"
233252
case _ => "app"
@@ -363,8 +382,20 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
363382
case PackageType.Js =>
364383
value(buildJs(build, destPath, mainClassOpt, logger))
365384

366-
case PackageType.Native =>
367-
val cachedDest = value(buildNative(build, value(mainClass), logger))
385+
case tpe: PackageType.Native =>
386+
import PackageType.Native.*
387+
val mainClassO =
388+
tpe match
389+
case Application => Some(value(mainClass))
390+
case _ => None
391+
392+
val cachedDest = value(buildNative(
393+
build = build,
394+
mainClass = mainClassO,
395+
targetType = tpe,
396+
destPath = Some(destPath),
397+
logger = logger
398+
))
368399
if (force) os.copy.over(cachedDest, destPath, createFolders = true)
369400
else os.copy(cachedDest, destPath, createFolders = true)
370401
destPath
@@ -631,7 +662,14 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
631662
case Platform.JVM => value(bootstrap(build, appPath, mainClass, () => Right(()), logger))
632663
case Platform.JS => buildJs(build, appPath, Some(mainClass), logger)
633664
case Platform.Native =>
634-
val dest = value(buildNative(build, mainClass, logger))
665+
val dest =
666+
value(buildNative(
667+
build = build,
668+
mainClass = Some(mainClass),
669+
targetType = PackageType.Native.Application,
670+
destPath = None,
671+
logger = logger
672+
))
635673
os.copy(dest, appPath)
636674
}
637675

@@ -959,7 +997,9 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
959997

960998
def buildNative(
961999
build: Build.Successful,
962-
mainClass: String,
1000+
mainClass: Option[String], // when building a static/dynamic library, we don't need a main class
1001+
targetType: PackageType.Native,
1002+
destPath: Option[os.Path],
9631003
logger: Logger
9641004
): Either[BuildException, os.Path] = either {
9651005
val dest = build.inputs.nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
@@ -980,9 +1020,26 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
9801020
Nil
9811021
val pythonCliOptions = pythonLdFlags.flatMap(f => Seq("--linking-option", f)).toList
9821022

1023+
val libraryLinkingOptions: Seq[String] =
1024+
Option.when(targetType != PackageType.Native.Application) {
1025+
/* If we are building a library, we make sure to change the name
1026+
that the linker will put into the loading path - otherwise
1027+
the built library will depend on some internal path within .scala-build
1028+
*/
1029+
1030+
destPath.flatMap(_.lastOpt).toSeq.flatMap { filename =>
1031+
val linkerOption =
1032+
if Properties.isLinux then s"-Wl,-soname,$filename" else s"-Wl,-install_name,$filename"
1033+
Seq("--linking-option", linkerOption)
1034+
}
1035+
}.toSeq.flatten
1036+
1037+
import PackageType.Native.*
1038+
9831039
val allCliOptions = pythonCliOptions ++
9841040
cliOptions ++
985-
Seq("--main", mainClass)
1041+
libraryLinkingOptions ++
1042+
mainClass.toSeq.flatMap(m => Seq("--main", m))
9861043

9871044
val nativeWorkDir = build.inputs.nativeWorkDir
9881045
os.makeDir.all(nativeWorkDir)
@@ -1040,7 +1097,14 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
10401097
lazy val validPackageScalaJS =
10411098
Seq(PackageType.Js, PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar)
10421099
lazy val validPackageScalaNative =
1043-
Seq(PackageType.Native, PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar)
1100+
Seq(
1101+
PackageType.LibraryJar,
1102+
PackageType.SourceJar,
1103+
PackageType.DocJar,
1104+
PackageType.Native.Application,
1105+
PackageType.Native.LibraryDynamic,
1106+
PackageType.Native.LibraryStatic
1107+
)
10441108

10451109
forcedPackageTypeOpt -> build.options.platform.value match {
10461110
case (Some(forcedPackageType), _) => Right(forcedPackageType)
@@ -1061,14 +1125,24 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
10611125
))
10621126
validatedPackageType.getOrElse(Right(PackageType.Js))
10631127
case (_, Platform.Native) =>
1128+
val specificNativePackageType =
1129+
import ScalaNativeTarget.*
1130+
build.options.scalaNativeOptions.buildTargetStr.flatMap(fromString).map {
1131+
case Application => PackageType.Native.Application
1132+
case LibraryDynamic => PackageType.Native.LibraryDynamic
1133+
case LibraryStatic => PackageType.Native.LibraryStatic
1134+
}
1135+
10641136
val validatedPackageType =
1065-
for (basePackageType <- basePackageTypeOpt)
1066-
yield
1067-
if (validPackageScalaNative.contains(basePackageType)) Right(basePackageType)
1068-
else Left(new MalformedCliInputError(
1069-
s"Unsupported package type: $basePackageType for Scala Native."
1070-
))
1071-
validatedPackageType.getOrElse(Right(PackageType.Native))
1137+
for
1138+
basePackageType <- specificNativePackageType orElse basePackageTypeOpt
1139+
yield
1140+
if (validPackageScalaNative.contains(basePackageType)) Right(basePackageType)
1141+
else Left(new MalformedCliInputError(
1142+
s"Unsupported package type: $basePackageType for Scala Native."
1143+
))
1144+
1145+
validatedPackageType.getOrElse(Right(PackageType.Native.Application))
10721146
case _ => Right(basePackageTypeOpt.getOrElse(PackageType.Bootstrap))
10731147
}
10741148
}

modules/cli/src/main/scala/scala/cli/commands/run/Run.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import scala.build.errors.BuildException
1515
import scala.build.input.{Inputs, ScalaCliInvokeData, SubCommand}
1616
import scala.build.internal.util.ConsoleUtils.ScalaCliConsole
1717
import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig}
18-
import scala.build.options.{BuildOptions, JavaOpt, Platform, ScalacOpt}
18+
import scala.build.options.{BuildOptions, JavaOpt, PackageType, Platform, ScalacOpt}
1919
import scala.cli.CurrentParams
2020
import scala.cli.commands.package0.Package
2121
import scala.cli.commands.publish.ConfigUtil.*
@@ -665,7 +665,13 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
665665
mainClass: String,
666666
logger: Logger
667667
)(f: os.Path => T): Either[BuildException, T] =
668-
Package.buildNative(build, mainClass, logger).map(f)
668+
Package.buildNative(
669+
build = build,
670+
mainClass = Some(mainClass),
671+
targetType = PackageType.Native.Application,
672+
destPath = None,
673+
logger = logger
674+
).map(f)
669675

670676
final class PythonDetectionError(cause: Throwable) extends BuildException(
671677
s"Error detecting Python environment: ${cause.getMessage}",

modules/cli/src/main/scala/scala/cli/commands/shared/ScalaNativeOptions.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ final case class ScalaNativeOptions(
6161
@Tag(tags.implementation)
6262
nativeCompileDefaults: Option[Boolean] = None, //TODO does it even work when we default it to true while handling?
6363

64+
@Group(HelpGroup.ScalaNative.toString)
65+
@HelpMessage("Build target type")
66+
@Tag(tags.should)
67+
@ValueDescription("app|static|dynamic")
68+
nativeTarget: Option[String] = None,
69+
6470
@Group(HelpGroup.ScalaNative.toString)
6571
@HelpMessage("Embed resources into the Scala Native binary (can be read with the Java resources API)")
6672
@Tag(tags.should)

modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,9 @@ final case class SharedOptions(
273273
nativeLinking,
274274
nativeLinkingDefaults,
275275
nativeCompile,
276-
nativeCompileDefaults
276+
nativeCompileDefaults,
277+
embedResources,
278+
nativeTarget
277279
)
278280
}
279281

modules/cli/src/test/scala/cli/tests/PackageTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class PackageTests extends munit.FunSuite {
9292
val build = maybeFirstBuild.orThrow.successfulOpt.get
9393

9494
val packageType = Package.resolvePackageType(build, None).orThrow
95-
expect(packageType == PackageType.Native)
95+
expect(packageType == PackageType.Native.Application)
9696
}
9797
}
9898

modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalaNative.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import scala.cli.commands.SpecificationLevel
2626
|
2727
|`//> using nativeClangPP` _value_
2828
|
29-
|`//> using nativeEmbedResources` _true|false_""".stripMargin
29+
|`//> using nativeEmbedResources` _true|false_
30+
|
31+
|`//> using nativeTarget` _application|library-dynamic|library-static_
32+
""".stripMargin.trim
3033
)
3134
@DirectiveDescription("Add Scala Native options")
3235
@DirectiveLevel(SpecificationLevel.SHOULD)
@@ -41,7 +44,8 @@ final case class ScalaNative(
4144
nativeClang: Option[String] = None,
4245
@DirectiveName("nativeClangPp")
4346
nativeClangPP: Option[String] = None,
44-
nativeEmbedResources: Option[Boolean] = None
47+
nativeEmbedResources: Option[Boolean] = None,
48+
nativeTarget: Option[String] = None,
4549
) extends HasBuildOptions {
4650
// format: on
4751
def buildOptions: Either[BuildException, BuildOptions] = {
@@ -54,7 +58,8 @@ final case class ScalaNative(
5458
linkingOptions = nativeLinking,
5559
clang = nativeClang,
5660
clangpp = nativeClangPP,
57-
embedResources = nativeEmbedResources
61+
embedResources = nativeEmbedResources,
62+
buildTargetStr = nativeTarget
5863
)
5964
val buildOpt = BuildOptions(scalaNativeOptions = nativeOptions)
6065
Right(buildOpt)

modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,73 @@ abstract class PackageTestDefinitions(val scalaVersionOpt: Option[String])
469469
}
470470
}
471471

472-
if (!Properties.isWin && actualScalaVersion.startsWith("2.13"))
472+
def libraryNativeTest(
473+
shared: Boolean = false,
474+
commandLineShared: Option[Boolean] = None
475+
): Unit = {
476+
val fileName = "simple.sc"
477+
val directiveNativeTarget = if (shared) "dynamic" else "static"
478+
val inputs = TestInputs(
479+
os.rel / fileName ->
480+
s"""
481+
|//> using platform scala-native
482+
|//> using nativeTarget $directiveNativeTarget
483+
|import scala.scalanative.unsafe._
484+
|object myLib{
485+
| @exported
486+
| def addLongs(l: Long, r: Long): Long = l + r
487+
| @exported("mylib_addInts")
488+
| def addInts(l: Int, r: Int): Int = l + r
489+
|}""".stripMargin
490+
)
491+
val destName = {
492+
val ext =
493+
if (!shared && !commandLineShared.getOrElse(false))
494+
if (Properties.isWin) ".lib" else ".a"
495+
else if (Properties.isWin) ".dll"
496+
else if (Properties.isMac) ".dylib"
497+
else ".so"
498+
fileName.stripSuffix(".sc") + ext
499+
}
500+
501+
val nativeTargetOpts = commandLineShared match {
502+
case Some(true) => Seq("--native-target", "dynamic")
503+
case Some(false) => Seq("--native-target", "static")
504+
case None => Seq.empty
505+
}
506+
507+
inputs.fromRoot { root =>
508+
os.proc(TestUtil.cli, "--power", "package", extraOptions, nativeTargetOpts, fileName).call(
509+
cwd = root,
510+
stdin = os.Inherit,
511+
stdout = os.Inherit
512+
)
513+
514+
val library = root / destName
515+
expect(os.isFile(library))
516+
}
517+
}
518+
519+
if (!Properties.isWin && actualScalaVersion.startsWith("2.13")) {
473520
test("simple native") {
474521
simpleNativeTest()
475522
}
523+
test("dynamic library native") {
524+
libraryNativeTest(shared = true)
525+
}
526+
527+
test("dynamic library native override from command line") {
528+
libraryNativeTest(shared = false, commandLineShared = Some(true))
529+
}
530+
531+
// To produce a static library, `LLVM_BIN` environment variable needs to be
532+
// present (for `llvm-ar` utility)
533+
if (sys.env.contains("LLVM_BIN"))
534+
test("shared library native") {
535+
libraryNativeTest(shared = false)
536+
}
537+
538+
}
476539

477540
test("assembly") {
478541
val fileName = "simple.sc"

0 commit comments

Comments
 (0)