Skip to content

Commit 7145b1f

Browse files
jchyblwronski
authored andcommitted
Add embedding resources support for Scala Native
Works automatically when a resourceDir option or using Directory is added. Having that working effectively required fixing duplicated classpath in Scala Native packaging. Will work only on Scala Native 0.4.4 and up. Caching was also improved, as previously only the cli option resources would be cached, and using directive resources would be ignored. A --embed-resources option was also added, allowing to disable resource embedding (which will also make the resources API not work). The use case for this is using c interop without having unnecessary libraries metadata embedded, taking up space in a binary file.
1 parent bdd782b commit 7145b1f

File tree

11 files changed

+173
-42
lines changed

11 files changed

+173
-42
lines changed

modules/build/src/main/scala/scala/build/Build.scala

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -472,34 +472,60 @@ object Build {
472472

473473
def scalaNativeSupported(
474474
options: BuildOptions,
475-
inputs: Inputs
475+
inputs: Inputs,
476+
logger: Logger
476477
): Either[BuildException, Option[ScalaNativeCompatibilityError]] =
477478
either {
478479
val scalaParamsOpt = value(options.scalaParams)
479480
scalaParamsOpt.flatMap { scalaParams =>
480-
val scalaVersion = scalaParams.scalaVersion
481-
val nativeVersion = options.scalaNativeOptions.numeralVersion
482-
val isCompatible = nativeVersion match {
483-
case Some(snNumeralVer) =>
484-
if (snNumeralVer < SNNumeralVersion(0, 4, 1) && Properties.isWin)
485-
false
486-
else if (scalaVersion.startsWith("3.0"))
487-
false
488-
else if (scalaVersion.startsWith("3") || scalaVersion.startsWith("2.12"))
489-
snNumeralVer >= SNNumeralVersion(0, 4, 3)
490-
else if (scalaVersion.startsWith("2.13"))
491-
true
492-
else false
493-
case None => false
494-
}
495-
if (isCompatible) None
496-
else
497-
Some(
481+
val scalaVersion = scalaParams.scalaVersion
482+
val nativeVersionMaybe = options.scalaNativeOptions.numeralVersion
483+
def snCompatError =
484+
Left(
498485
new ScalaNativeCompatibilityError(
499486
scalaVersion,
500487
options.scalaNativeOptions.finalVersion
501488
)
502489
)
490+
def warnIncompatibleNativeOptions(numeralVersion: SNNumeralVersion) =
491+
if (
492+
numeralVersion < SNNumeralVersion(0, 4, 4)
493+
&& options.scalaNativeOptions.embedResources.isDefined
494+
)
495+
logger.diagnostic(
496+
"This Scala Version cannot embed resources, regardless of the options used."
497+
)
498+
499+
val numeralOrError: Either[ScalaNativeCompatibilityError, SNNumeralVersion] =
500+
nativeVersionMaybe match {
501+
case Some(snNumeralVer) =>
502+
if (snNumeralVer < SNNumeralVersion(0, 4, 1) && Properties.isWin)
503+
snCompatError
504+
else if (scalaVersion.startsWith("3.0"))
505+
snCompatError
506+
else if (scalaVersion.startsWith("3"))
507+
if (snNumeralVer >= SNNumeralVersion(0, 4, 3)) Right(snNumeralVer)
508+
else snCompatError
509+
else if (scalaVersion.startsWith("2.13"))
510+
Right(snNumeralVer)
511+
else if (scalaVersion.startsWith("2.12"))
512+
if (
513+
inputs.sourceFiles().forall {
514+
case _: Inputs.AnyScript => snNumeralVer >= SNNumeralVersion(0, 4, 3)
515+
case _ => true
516+
}
517+
) Right(snNumeralVer)
518+
else snCompatError
519+
else snCompatError
520+
case None => snCompatError
521+
}
522+
523+
numeralOrError match {
524+
case Left(compatError) => Some(compatError)
525+
case Right(snNumeralVersion) =>
526+
warnIncompatibleNativeOptions(snNumeralVersion)
527+
None
528+
}
503529
}
504530
}
505531

@@ -981,7 +1007,7 @@ object Build {
9811007
): Either[BuildException, Build] = either {
9821008

9831009
if (options.platform.value == Platform.Native)
984-
value(scalaNativeSupported(options, inputs)) match {
1010+
value(scalaNativeSupported(options, inputs, logger)) match {
9851011
case None =>
9861012
case Some(error) => value(Left(error))
9871013
}

modules/build/src/main/scala/scala/build/Inputs.scala

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,6 @@ final case class Inputs(
114114
case dirInput: Inputs.Directory =>
115115
Seq("dir:") ++ Inputs.singleFilesFromDirectory(dirInput, enableMarkdown)
116116
.map(file => s"${file.path}:" + os.read(file.path))
117-
case resDirInput: Inputs.ResourceDirectory =>
118-
// Resource changes for SN require relinking, so they should also be hashed
119-
Seq("resource-dir:") ++ os.walk(resDirInput.path)
120-
.filter(os.isFile(_))
121-
.map(filePath => s"$filePath:" + os.read(filePath))
122117
case _ => Seq(os.read(elem.path))
123118
}
124119
(Iterator(elem.path.toString) ++ content.iterator ++ Iterator("\n")).map(bytes)

modules/cli-options/src/main/scala/scala/cli/commands/ScalaNativeOptions.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ final case class ScalaNativeOptions(
4343
@Group("Scala Native")
4444
@Hidden
4545
@HelpMessage("Use default compile options")
46-
nativeCompileDefaults: Option[Boolean] = None //TODO does it even work when we default it to true while handling?
46+
nativeCompileDefaults: Option[Boolean] = None, //TODO does it even work when we default it to true while handling?
47+
48+
@Group("Scala Native")
49+
@HelpMessage("Embed resources into the Scala Native binary (can be read with the Java resources API)")
50+
embedResources: Option[Boolean] = None
51+
4752
)
4853
// format: on
4954

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,8 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
912912
logger: Logger
913913
): Either[BuildException, Unit] = either {
914914

915-
val cliOptions = build.options.scalaNativeOptions.configCliOptions()
915+
val cliOptions =
916+
build.options.scalaNativeOptions.configCliOptions(!build.sources.resourceDirs.isEmpty)
916917

917918
val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
918919
val pythonLdFlags =
@@ -943,7 +944,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
943944
if (cacheData.changed)
944945
Library.withLibraryJar(build, dest.last.stripSuffix(".jar")) { mainJar =>
945946

946-
val classpath = build.fullClassPath.map(_.toString) :+ mainJar.toString
947+
val classpath = mainJar.toString +: build.artifacts.classPath.map(_.toString)
947948
val args =
948949
allCliOptions ++
949950
logger.scalaNativeCliInternalLoggerOptions ++

modules/cli/src/main/scala/scala/cli/internal/CachedBinary.scala

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import java.math.BigInteger
44
import java.nio.charset.StandardCharsets
55
import java.security.MessageDigest
66

7-
import scala.build.Build
7+
import scala.build.{Build, Inputs}
88
import scala.build.internal.Constants
99

1010
object CachedBinary {
@@ -23,10 +23,48 @@ object CachedBinary {
2323
String.format(s"%040x", calculatedSum)
2424
}
2525

26+
private def hashResources(build: Build.Successful) = {
27+
def hashResourceDir(path: os.Path) =
28+
os.walk(path)
29+
.filter(os.isFile(_))
30+
.map { filePath =>
31+
val md = MessageDigest.getInstance("SHA-1")
32+
md.update(os.read.bytes(filePath))
33+
s"$filePath:" + new BigInteger(1, md.digest()).toString()
34+
}
35+
36+
val classpathResourceDirsIt =
37+
build.options
38+
.classPathOptions
39+
.resourcesDir
40+
.flatMap(dir => hashResourceDir(dir))
41+
.iterator ++
42+
Iterator("\n")
43+
44+
val projectResourceDirsIt = build.inputs.elements.iterator.flatMap {
45+
case elem: Inputs.OnDisk =>
46+
val content = elem match {
47+
case resDirInput: Inputs.ResourceDirectory =>
48+
hashResourceDir(resDirInput.path)
49+
case _ => List.empty
50+
}
51+
Iterator(elem.path.toString) ++ content.iterator ++ Iterator("\n")
52+
case _ =>
53+
Iterator.empty
54+
}
55+
56+
(classpathResourceDirsIt ++ projectResourceDirsIt)
57+
.map(_.getBytes(StandardCharsets.UTF_8))
58+
}
59+
2660
private def projectSha(build: Build.Successful, config: List[String]) = {
2761
val md = MessageDigest.getInstance("SHA-1")
2862
val charset = StandardCharsets.UTF_8
2963
md.update(build.inputs.sourceHash().getBytes(charset))
64+
md.update("<resources>".getBytes())
65+
// Resource changes for SN require relinking, so they should also be hashed
66+
hashResources(build).foreach(md.update)
67+
md.update("</resources>".getBytes())
3068
md.update(0: Byte)
3169
md.update("<config>".getBytes(charset))
3270
for (elem <- config) {

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class CachedBinaryTests extends munit.FunSuite {
4848
(_, _, maybeBuild) =>
4949
val build = maybeBuild.successfulOpt.get
5050

51-
val config = build.options.scalaNativeOptions.configCliOptions()
51+
val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
5252
val nativeWorkDir = build.inputs.nativeWorkDir
5353
val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
5454
// generate dummy output
@@ -65,7 +65,7 @@ class CachedBinaryTests extends munit.FunSuite {
6565
(_, _, maybeBuild) =>
6666
val build = maybeBuild.successfulOpt.get
6767

68-
val config = build.options.scalaNativeOptions.configCliOptions()
68+
val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
6969
val nativeWorkDir = build.inputs.nativeWorkDir
7070
val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
7171
// generate dummy output
@@ -91,7 +91,7 @@ class CachedBinaryTests extends munit.FunSuite {
9191
(_, _, maybeBuild) =>
9292
val build = maybeBuild.successfulOpt.get
9393

94-
val config = build.options.scalaNativeOptions.configCliOptions()
94+
val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
9595
val nativeWorkDir = build.inputs.nativeWorkDir
9696
val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
9797
// generate dummy output
@@ -118,7 +118,7 @@ class CachedBinaryTests extends munit.FunSuite {
118118
(_, _, maybeBuild) =>
119119
val build = maybeBuild.successfulOpt.get
120120

121-
val config = build.options.scalaNativeOptions.configCliOptions()
121+
val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
122122
val nativeWorkDir = build.inputs.nativeWorkDir
123123
val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
124124
// generate dummy output
@@ -145,7 +145,7 @@ class CachedBinaryTests extends munit.FunSuite {
145145
(root, _, maybeBuild) =>
146146
val build = maybeBuild.successfulOpt.get
147147

148-
val config = build.options.scalaNativeOptions.configCliOptions()
148+
val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
149149
val nativeWorkDir = build.inputs.nativeWorkDir
150150
val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
151151
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
@@ -171,7 +171,7 @@ class CachedBinaryTests extends munit.FunSuite {
171171
(_, _, maybeBuild) =>
172172
val build = maybeBuild.successfulOpt.get
173173

174-
val config = build.options.scalaNativeOptions.configCliOptions()
174+
val config = build.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
175175
val nativeWorkDir = build.inputs.nativeWorkDir
176176
val destPath = nativeWorkDir / s"main${if (Properties.isWin) ".exe" else ""}"
177177
os.write(destPath, Random.alphanumeric.take(10).mkString(""), createFolders = true)
@@ -192,7 +192,8 @@ class CachedBinaryTests extends munit.FunSuite {
192192
)
193193
)
194194
)
195-
val updatedConfig = updatedBuild.options.scalaNativeOptions.configCliOptions()
195+
val updatedConfig =
196+
updatedBuild.options.scalaNativeOptions.configCliOptions(resourcesExist = false)
196197

197198
val cacheAfterConfigUpdate =
198199
CachedBinary.getCacheData(

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ case object UsingScalaNativeOptionsDirectiveHandler extends UsingDirectiveHandle
2525
|
2626
|`//> using nativeClang` _value_
2727
|
28-
|`//> using nativeClangPP` _value_""".stripMargin
28+
|`//> using nativeClangPP` _value_
29+
|
30+
|`//> using nativeEmbedResources` _true|false_""".stripMargin
2931

3032
override def examples: Seq[String] = Seq(
3133
"//> using nativeVersion \"0.4.0\""
@@ -42,13 +44,15 @@ case object UsingScalaNativeOptionsDirectiveHandler extends UsingDirectiveHandle
4244
"native-linking",
4345
"native-clang",
4446
"native-clang-pp",
47+
"native-no-embed",
4548
"nativeGc",
4649
"nativeMode",
4750
"nativeVersion",
4851
"nativeCompile",
4952
"nativeLinking",
5053
"nativeClang",
51-
"nativeClangPP"
54+
"nativeClangPP",
55+
"nativeEmbedResources"
5256
)
5357

5458
override def getValueNumberBounds(key: String): UsingDirectiveValueNumberBounds = key match {
@@ -61,9 +65,15 @@ case object UsingScalaNativeOptionsDirectiveHandler extends UsingDirectiveHandle
6165
case "native-compile" | "nativeCompile" => UsingDirectiveValueNumberBounds(1, Int.MaxValue)
6266
}
6367

68+
def getBooleanOption(groupedValues: GroupedScopedValuesContainer): Option[Boolean] =
69+
groupedValues.scopedBooleanValues.map(_.positioned.value.toBoolean).headOption.orElse(Some(
70+
true
71+
))
72+
6473
override def getSupportedTypes(key: String): Set[UsingDirectiveValueKind] = Set(
6574
UsingDirectiveValueKind.STRING,
66-
UsingDirectiveValueKind.NUMERIC
75+
UsingDirectiveValueKind.NUMERIC,
76+
UsingDirectiveValueKind.BOOLEAN
6777
)
6878

6979
def handleValues(
@@ -101,6 +111,10 @@ case object UsingScalaNativeOptionsDirectiveHandler extends UsingDirectiveHandle
101111
ScalaNativeOptions(
102112
clangpp = Some(values.head.positioned.value)
103113
)
114+
case "native-embed-resources" | "nativeEmbedResources" =>
115+
ScalaNativeOptions(
116+
embedResources = getBooleanOption(groupedValuesContainer)
117+
)
104118
}
105119
val options = BuildOptions(scalaNativeOptions = scalaNativeOptions)
106120
ProcessedDirective(Some(options), Nil)

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,40 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
258258
}
259259
}
260260

261+
test("Resource embedding in Scala Native") {
262+
val projectDir = "nativeres"
263+
val resourceContent = "resource contents"
264+
val resourceFileName = "embeddedfile.txt"
265+
val inputs = TestInputs(
266+
os.rel / projectDir / "main.scala" ->
267+
s"""|//> using platform "scala-native"
268+
|//> using resourceDir "resources"
269+
|
270+
|import java.nio.charset.StandardCharsets
271+
|import java.io.{BufferedReader, InputStreamReader}
272+
|
273+
|object Main {
274+
| def main(args: Array[String]): Unit = {
275+
| val inputStream = getClass().getResourceAsStream("/$resourceFileName")
276+
| val nativeResourceText = new BufferedReader(
277+
| new InputStreamReader(inputStream, StandardCharsets.UTF_8)
278+
| ).readLine()
279+
| println(nativeResourceText)
280+
| }
281+
|}
282+
|""".stripMargin,
283+
os.rel / projectDir / "resources" / resourceFileName -> resourceContent
284+
)
285+
inputs.fromRoot { root =>
286+
val output =
287+
os.proc(TestUtil.cli, extraOptions, projectDir, "-q")
288+
.call(cwd = root)
289+
.out.trim()
290+
println(output)
291+
expect(output == resourceContent)
292+
}
293+
}
294+
261295
if (actualScalaVersion.startsWith("3.1"))
262296
test("Scala 3 in Scala Native") {
263297
val message = "using Scala 3 Native"

modules/options/src/main/scala/scala/build/options/ScalaNativeOptions.scala

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ final case class ScalaNativeOptions(
1717
linkingOptions: List[String] = Nil,
1818
linkingDefaults: Option[Boolean] = None,
1919
compileOptions: List[String] = Nil,
20-
compileDefaults: Option[Boolean] = None
20+
compileDefaults: Option[Boolean] = None,
21+
embedResources: Option[Boolean] = None
2122
) {
2223

2324
def finalVersion = version.map(_.trim).filter(_.nonEmpty).getOrElse(Constants.scalaNativeVersion)
@@ -65,6 +66,15 @@ final case class ScalaNativeOptions(
6566
private def compileCliOptions(): List[String] =
6667
finalCompileOptions().flatMap(option => List("--compile-option", option))
6768

69+
private def resourcesCliOptions(resourcesExist: Boolean): List[String] =
70+
if (embedResources.getOrElse(true))
71+
(numeralVersion, resourcesExist) match {
72+
case (Some(numeralVersion), true) if numeralVersion >= SNNumeralVersion(0, 4, 4) =>
73+
List("--embed-resources")
74+
case _ => Nil
75+
}
76+
else Nil
77+
6878
def platformSuffix: String =
6979
"native" + ScalaVersion.nativeBinary(finalVersion).getOrElse(finalVersion)
7080

@@ -100,13 +110,14 @@ final case class ScalaNativeOptions(
100110
output = None
101111
)
102112

103-
def configCliOptions(): List[String] =
113+
def configCliOptions(resourcesExist: Boolean): List[String] =
104114
gcCliOption() ++
105115
modeCliOption() ++
106116
clangCliOption() ++
107117
clangppCliOption() ++
108118
linkingCliOptions() ++
109-
compileCliOptions()
119+
compileCliOptions() ++
120+
resourcesCliOptions(resourcesExist)
110121

111122
}
112123

0 commit comments

Comments
 (0)