Skip to content

Commit 5f15ada

Browse files
authored
Merge pull request #812 from jchyb/native-resources
Add resources support to Scala Native and fixes to overall resources
2 parents eeae2d8 + 6f25287 commit 5f15ada

File tree

13 files changed

+389
-65
lines changed

13 files changed

+389
-65
lines changed

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

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import scala.build.Ops.*
1616
import scala.build.compiler.{ScalaCompiler, ScalaCompilerMaker}
1717
import scala.build.errors.*
1818
import scala.build.internal.{Constants, CustomCodeWrapper, MainClass, Util}
19+
import scala.build.internal.resource.ResourceMapper
1920
import scala.build.options.*
2021
import scala.build.options.validation.ValidationException
2122
import scala.build.postprocessing.*
@@ -392,9 +393,9 @@ object Build {
392393

393394
val builds = value(buildScopes())
394395

395-
copyResourceToClassesDir(builds.main)
396+
ResourceMapper.copyResourceToClassesDir(builds.main)
396397
for (testBuild <- builds.get(Scope.Test))
397-
copyResourceToClassesDir(testBuild)
398+
ResourceMapper.copyResourceToClassesDir(testBuild)
398399

399400
if (actionableDiagnostics.getOrElse(false)) {
400401
val projectOptions = builds.get(Scope.Test).getOrElse(builds.main).options
@@ -404,26 +405,6 @@ object Build {
404405
builds
405406
}
406407

407-
private def copyResourceToClassesDir(build: Build): Unit = build match {
408-
case b: Build.Successful =>
409-
for {
410-
resourceDirPath <- b.sources.resourceDirs.filter(os.exists(_))
411-
resourceFilePath <- os.walk(resourceDirPath).filter(os.isFile(_))
412-
relativeResourcePath = resourceFilePath.relativeTo(resourceDirPath)
413-
// dismiss files generated by scala-cli
414-
if !relativeResourcePath.startsWith(os.rel / Constants.workspaceDirName)
415-
} {
416-
val destPath = b.output / relativeResourcePath
417-
os.copy(
418-
resourceFilePath,
419-
destPath,
420-
replaceExisting = true,
421-
createFolders = true
422-
)
423-
}
424-
case _ =>
425-
}
426-
427408
private def build(
428409
inputs: Inputs,
429410
sources: Sources,
@@ -482,36 +463,69 @@ object Build {
482463
def classesDir(root: os.Path, projectName: String, scope: Scope, suffix: String = ""): os.Path =
483464
classesRootDir(root, projectName) / s"${scope.name}$suffix"
484465

466+
def resourcesRegistry(
467+
root: os.Path,
468+
projectName: String,
469+
scope: Scope
470+
): os.Path =
471+
root / Constants.workspaceDirName / projectName / s"resources-${scope.name}"
472+
485473
def scalaNativeSupported(
486474
options: BuildOptions,
487-
inputs: Inputs
475+
inputs: Inputs,
476+
logger: Logger
488477
): Either[BuildException, Option[ScalaNativeCompatibilityError]] =
489478
either {
490479
val scalaParamsOpt = value(options.scalaParams)
491480
scalaParamsOpt.flatMap { scalaParams =>
492-
val scalaVersion = scalaParams.scalaVersion
493-
val nativeVersion = options.scalaNativeOptions.numeralVersion
494-
val isCompatible = nativeVersion match {
495-
case Some(snNumeralVer) =>
496-
if (snNumeralVer < SNNumeralVersion(0, 4, 1) && Properties.isWin)
497-
false
498-
else if (scalaVersion.startsWith("3.0"))
499-
false
500-
else if (scalaVersion.startsWith("3") || scalaVersion.startsWith("2.12"))
501-
snNumeralVer >= SNNumeralVersion(0, 4, 3)
502-
else if (scalaVersion.startsWith("2.13"))
503-
true
504-
else false
505-
case None => false
506-
}
507-
if (isCompatible) None
508-
else
509-
Some(
481+
val scalaVersion = scalaParams.scalaVersion
482+
val nativeVersionMaybe = options.scalaNativeOptions.numeralVersion
483+
def snCompatError =
484+
Left(
510485
new ScalaNativeCompatibilityError(
511486
scalaVersion,
512487
options.scalaNativeOptions.finalVersion
513488
)
514489
)
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+
}
515529
}
516530
}
517531

@@ -993,7 +1007,7 @@ object Build {
9931007
): Either[BuildException, Build] = either {
9941008

9951009
if (options.platform.value == Platform.Native)
996-
value(scalaNativeSupported(options, inputs)) match {
1010+
value(scalaNativeSupported(options, inputs, logger)) match {
9971011
case None =>
9981012
case Some(error) => value(Left(error))
9991013
}

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

Lines changed: 8 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)
@@ -206,6 +201,10 @@ object Inputs {
206201
extends OnDisk with SourceFile with Compiled {
207202
lazy val path: os.Path = base / subPath
208203
}
204+
final case class CFile(base: os.Path, subPath: os.SubPath)
205+
extends OnDisk with SourceFile with Compiled {
206+
lazy val path = base / subPath
207+
}
209208
final case class MarkdownFile(base: os.Path, subPath: os.SubPath)
210209
extends OnDisk with SourceFile {
211210
lazy val path: os.Path = base / subPath
@@ -245,6 +244,8 @@ object Inputs {
245244
Inputs.SourceScalaFile(d.path, p.subRelativeTo(d.path))
246245
case p if p.last.endsWith(".sc") =>
247246
Inputs.Script(d.path, p.subRelativeTo(d.path))
247+
case p if p.last.endsWith(".c") || p.last.endsWith(".h") =>
248+
Inputs.CFile(d.path, p.subRelativeTo(d.path))
248249
case p if p.last.endsWith(".md") && enableMarkdown =>
249250
Inputs.MarkdownFile(d.path, p.subRelativeTo(d.path))
250251
}
@@ -274,6 +275,7 @@ object Inputs {
274275
case _: Inputs.JavaFile => "java:"
275276
case _: Inputs.SettingsScalaFile => "config:"
276277
case _: Inputs.SourceScalaFile => "scala:"
278+
case _: Inputs.CFile => "c:"
277279
case _: Inputs.Script => "sc:"
278280
case _: Inputs.MarkdownFile => "md:"
279281
}
@@ -460,6 +462,7 @@ object Inputs {
460462
else if (arg.endsWith(".sc")) Right(Seq(Script(dir, subPath)))
461463
else if (arg.endsWith(".scala")) Right(Seq(SourceScalaFile(dir, subPath)))
462464
else if (arg.endsWith(".java")) Right(Seq(JavaFile(dir, subPath)))
465+
else if (arg.endsWith(".c") || arg.endsWith(".h")) Right(Seq(CFile(dir, subPath)))
463466
else if (arg.endsWith(".md")) Right(Seq(MarkdownFile(dir, subPath)))
464467
else if (os.isDir(path)) Right(Seq(Directory(path)))
465468
else if (acceptFds && arg.startsWith("/dev/fd/")) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package scala.build.internal.resource
2+
3+
import scala.build.{Build, Inputs}
4+
5+
object NativeResourceMapper {
6+
7+
private def scalaNativeCFileMapping(build: Build.Successful): Map[os.Path, os.RelPath] =
8+
build
9+
.inputs
10+
.flattened()
11+
.collect {
12+
case cfile: Inputs.CFile =>
13+
val inputPath = cfile.path
14+
val destPath = os.rel / "scala-native" / cfile.subPath
15+
(inputPath, destPath)
16+
}
17+
.toMap
18+
19+
private def resolveProjectCFileRegistryPath(nativeWorkDir: os.Path) =
20+
nativeWorkDir / ".native_registry"
21+
22+
/** Copies and maps c file resources from their original path to the destination path in build
23+
* output, also caching output paths in a file.
24+
*
25+
* Remembering the mapping this way allows for the resource to be removed if the original file is
26+
* renamed/deleted.
27+
*/
28+
def copyCFilesToScalaNativeDir(build: Build.Successful, nativeWorkDir: os.Path): Unit = {
29+
val mappingFilePath = resolveProjectCFileRegistryPath(nativeWorkDir)
30+
ResourceMapper.copyResourcesToDirWithMapping(
31+
build.output,
32+
mappingFilePath,
33+
scalaNativeCFileMapping(build)
34+
)
35+
}
36+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package scala.build.internal.resource
2+
3+
import scala.build.Build
4+
import scala.build.internal.Constants
5+
6+
object ResourceMapper {
7+
8+
private def resourceMapping(build: Build.Successful): Map[os.Path, os.RelPath] = {
9+
val seq = for {
10+
resourceDirPath <- build.sources.resourceDirs.filter(os.exists(_))
11+
resourceFilePath <- os.walk(resourceDirPath).filter(os.isFile(_))
12+
relativeResourcePath = resourceFilePath.relativeTo(resourceDirPath)
13+
// dismiss files generated by scala-cli
14+
if !relativeResourcePath.startsWith(os.rel / Constants.workspaceDirName)
15+
} yield (resourceFilePath, relativeResourcePath)
16+
17+
seq.toMap
18+
}
19+
20+
def copyResourcesToDirWithMapping(
21+
output: os.Path,
22+
registryFilePath: os.Path,
23+
newMapping: Map[os.Path, os.RelPath]
24+
): Unit = {
25+
26+
val oldRegistry =
27+
if (os.exists(registryFilePath))
28+
os.read(registryFilePath)
29+
.linesIterator
30+
.filter(_.nonEmpty)
31+
.map(os.RelPath(_))
32+
.toSet
33+
else
34+
Set.empty
35+
val removedFiles = oldRegistry -- newMapping.values
36+
37+
for (f <- removedFiles)
38+
os.remove(output / f)
39+
40+
for ((inputPath, outputPath) <- newMapping)
41+
os.copy(
42+
inputPath,
43+
output / outputPath,
44+
replaceExisting = true,
45+
createFolders = true
46+
)
47+
48+
if (newMapping.isEmpty)
49+
os.remove(registryFilePath)
50+
else
51+
os.write.over(
52+
registryFilePath,
53+
newMapping.map(_._2.toString).toVector.sorted.mkString("\n")
54+
)
55+
}
56+
57+
/** Copies and maps resources from their original path to the destination path in build output,
58+
* also caching output paths in a file.
59+
*
60+
* Remembering the mapping this way allows for the resource to be removed if the original file is
61+
* renamed/deleted.
62+
*/
63+
def copyResourceToClassesDir(build: Build): Unit = build match {
64+
case b: Build.Successful =>
65+
val fullFilePath = Build.resourcesRegistry(b.inputs.workspace, b.inputs.projectName, b.scope)
66+
copyResourcesToDirWithMapping(b.output, fullFilePath, resourceMapping(b))
67+
case _ =>
68+
}
69+
}

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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import scala.build.errors.*
2222
import scala.build.interactive.InteractiveFileOps
2323
import scala.build.internal.Util.*
2424
import scala.build.internal.{Runner, ScalaJsLinkerConfig}
25+
import scala.build.internal.resource.NativeResourceMapper
2526
import scala.build.options.{PackageType, Platform}
2627
import scala.cli.CurrentParams
2728
import scala.cli.commands.OptionsHelper.*
@@ -912,7 +913,8 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
912913
logger: Logger
913914
): Either[BuildException, Unit] = either {
914915

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

917919
val setupPython = build.options.notForBloopOptions.doSetupPython.getOrElse(false)
918920
val pythonLdFlags =
@@ -940,10 +942,11 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
940942
nativeWorkDir
941943
)
942944

943-
if (cacheData.changed)
945+
if (cacheData.changed) {
946+
NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir)
944947
Library.withLibraryJar(build, dest.last.stripSuffix(".jar")) { mainJar =>
945948

946-
val classpath = build.fullClassPath.map(_.toString) :+ mainJar.toString
949+
val classpath = mainJar.toString +: build.artifacts.classPath.map(_.toString)
947950
val args =
948951
allCliOptions ++
949952
logger.scalaNativeCliInternalLoggerOptions ++
@@ -976,5 +979,6 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
976979
else
977980
throw new ScalaNativeBuildError
978981
}
982+
}
979983
}
980984
}

0 commit comments

Comments
 (0)