diff --git a/modules/build/src/main/scala/scala/build/Inputs.scala b/modules/build/src/main/scala/scala/build/Inputs.scala index cd50cc3bec..49ba21a880 100644 --- a/modules/build/src/main/scala/scala/build/Inputs.scala +++ b/modules/build/src/main/scala/scala/build/Inputs.scala @@ -201,6 +201,10 @@ object Inputs { extends OnDisk with SourceFile with Compiled { lazy val path: os.Path = base / subPath } + final case class CFile(base: os.Path, subPath: os.SubPath) + extends OnDisk with SourceFile with Compiled { + lazy val path = base / subPath + } final case class MarkdownFile(base: os.Path, subPath: os.SubPath) extends OnDisk with SourceFile { lazy val path: os.Path = base / subPath @@ -240,6 +244,8 @@ object Inputs { Inputs.SourceScalaFile(d.path, p.subRelativeTo(d.path)) case p if p.last.endsWith(".sc") => Inputs.Script(d.path, p.subRelativeTo(d.path)) + case p if p.last.endsWith(".c") || p.last.endsWith(".h") => + Inputs.CFile(d.path, p.subRelativeTo(d.path)) case p if p.last.endsWith(".md") && enableMarkdown => Inputs.MarkdownFile(d.path, p.subRelativeTo(d.path)) } @@ -269,6 +275,7 @@ object Inputs { case _: Inputs.JavaFile => "java:" case _: Inputs.SettingsScalaFile => "config:" case _: Inputs.SourceScalaFile => "scala:" + case _: Inputs.CFile => "c:" case _: Inputs.Script => "sc:" case _: Inputs.MarkdownFile => "md:" } @@ -455,6 +462,7 @@ object Inputs { else if (arg.endsWith(".sc")) Right(Seq(Script(dir, subPath))) else if (arg.endsWith(".scala")) Right(Seq(SourceScalaFile(dir, subPath))) else if (arg.endsWith(".java")) Right(Seq(JavaFile(dir, subPath))) + else if (arg.endsWith(".c") || arg.endsWith(".h")) Right(Seq(CFile(dir, subPath))) else if (arg.endsWith(".md")) Right(Seq(MarkdownFile(dir, subPath))) else if (os.isDir(path)) Right(Seq(Directory(path))) else if (acceptFds && arg.startsWith("/dev/fd/")) { diff --git a/modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala b/modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala new file mode 100644 index 0000000000..64627d073a --- /dev/null +++ b/modules/build/src/main/scala/scala/build/internal/resource/NativeResourceMapper.scala @@ -0,0 +1,36 @@ +package scala.build.internal.resource + +import scala.build.{Build, Inputs} + +object NativeResourceMapper { + + private def scalaNativeCFileMapping(build: Build.Successful): Map[os.Path, os.RelPath] = + build + .inputs + .flattened() + .collect { + case cfile: Inputs.CFile => + val inputPath = cfile.path + val destPath = os.rel / "scala-native" / cfile.subPath + (inputPath, destPath) + } + .toMap + + private def resolveProjectCFileRegistryPath(nativeWorkDir: os.Path) = + nativeWorkDir / ".native_registry" + + /** Copies and maps c file resources from their original path to the destination path in build + * output, also caching output paths in a file. + * + * Remembering the mapping this way allows for the resource to be removed if the original file is + * renamed/deleted. + */ + def copyCFilesToScalaNativeDir(build: Build.Successful, nativeWorkDir: os.Path): Unit = { + val mappingFilePath = resolveProjectCFileRegistryPath(nativeWorkDir) + ResourceMapper.copyResourcesToDirWithMapping( + build.output, + mappingFilePath, + scalaNativeCFileMapping(build) + ) + } +} diff --git a/modules/cli/src/main/scala/scala/cli/commands/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/Package.scala index 54ebc9f988..2f85964f5c 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Package.scala @@ -28,6 +28,7 @@ import scala.build.errors.{ import scala.build.interactive.InteractiveFileOps import scala.build.internal.Util._ import scala.build.internal.{Runner, ScalaJsLinkerConfig} +import scala.build.internal.resource.NativeResourceMapper import scala.build.options.{PackageType, Platform} import scala.cli.CurrentParams import scala.cli.commands.OptionsHelper._ @@ -930,7 +931,8 @@ object Package extends ScalaCommand[PackageOptions] { nativeWorkDir ) - if (cacheData.changed) + if (cacheData.changed) { + NativeResourceMapper.copyCFilesToScalaNativeDir(build, nativeWorkDir) Library.withLibraryJar(build, dest.last.stripSuffix(".jar")) { mainJar => val classpath = mainJar.toString +: build.artifacts.classPath.map(_.toString) @@ -966,5 +968,6 @@ object Package extends ScalaCommand[PackageOptions] { else throw new ScalaNativeBuildError } + } } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index 655548aa4d..aa464aa300 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -293,6 +293,59 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) } } + test("Scala Native C Files are correctly handled as a regular Input") { + val projectDir = "native-interop" + val interopFileName = "bindings.c" + val interopMsg = "Hello C!" + val inputs = TestInputs( + os.rel / projectDir / "main.scala" -> + s"""|//> using platform "scala-native" + | + |import scala.scalanative.unsafe._ + | + |@extern + |object Bindings { + | @name("scalanative_print") + | def print(): Unit = extern + |} + | + |object Main { + | def main(args: Array[String]): Unit = { + | Bindings.print() + | } + |} + |""".stripMargin, + os.rel / projectDir / interopFileName -> + s"""|#include + | + |void scalanative_print() { + | printf("$interopMsg\\n"); + |} + |""".stripMargin + ) + inputs.fromRoot { root => + val output = + os.proc(TestUtil.cli, extraOptions, projectDir, "-q") + .call(cwd = root) + .out.text().trim + expect(output == interopMsg) + + os.move(root / projectDir / interopFileName, root / projectDir / "bindings2.c") + val output2 = + os.proc(TestUtil.cli, extraOptions, projectDir, "-q") + .call(cwd = root) + .out.text().trim + + // LLVM throws linking errors if scalanative_print is internally repeated. + // This can happen if a file containing it will be removed/renamed in src, + // but somehow those changes will not be reflected in the output directory, + // causing symbols inside linked files to be doubled. + // Because of that, the removed file should not be passed to linker, + // otherwise this test will fail. + expect(output2 == interopMsg) + } + } + if (actualScalaVersion.startsWith("3.1")) test("Scala 3 in Scala Native") { val message = "using Scala 3 Native"