diff --git a/main-actions/src/main/scala/sbt/Mapper.scala b/main-actions/src/main/scala/sbt/Mapper.scala new file mode 100644 index 000000000..728a67b36 --- /dev/null +++ b/main-actions/src/main/scala/sbt/Mapper.scala @@ -0,0 +1,86 @@ +package sbt + +import sbt.io.{ AllPassFilter, FileFilter, PathFinder } +import sbt.io.Path.* +import xsbti.{ FileConverter, VirtualFile } + +import java.io.File + +object Mapper: + + /** + * Selects all descendants of `base` directory and maps them to a path relative to `base`. + * `base` itself is not included. + */ + def allSubpaths(base: File)(using conv: FileConverter): Seq[(VirtualFile, String)] = + selectSubpaths(base, AllPassFilter) + + /** + * Selects descendants of `base` directory matching `filter` and maps them to a path relative to `base`. + * `base` itself is not included. + */ + def selectSubpaths(base: File, filter: FileFilter)(using + conv: FileConverter + ): Seq[(VirtualFile, String)] = + PathFinder(base).globRecursive(filter).get().collect { + case f if f != base => + conv.toVirtualFile(f.toPath) -> base.toPath.relativize(f.toPath).toString + } + + /** + * return a Seq of mappings which effect is to add a whole directory in the generated package + * + * @example In order to create mappings for a static directory "extra" add + * {{{ + * mappings ++= directory(baseDirectory.value / "extra") + * }}} + * + * The resulting mappings sequence will look something like this + * + * {{{ + * File(baseDirectory/extras) -> "extras" + * File(baseDirectory/extras/file1) -> "extras/file1" + * File(baseDirectory/extras/file2) -> "extras/file2" + * ... + * }}} + * + * @param baseDirectory The directory that should be turned into a mappings sequence. + * @return mappings The `baseDirectory` and all of its contents + */ + def directory(baseDirectory: File)(using conv: FileConverter): Seq[(VirtualFile, String)] = + Option(baseDirectory.getParentFile) + .map(parent => PathFinder(baseDirectory).allPaths pair relativeTo(parent)) + .getOrElse(PathFinder(baseDirectory).allPaths pair basic) + .map { case (f, s) => conv.toVirtualFile(f.toPath) -> s } + + /** + * return a Seq of mappings excluding the directory itself. + * + * @example In order to create mappings for a static directory "extra" add + * {{{ + * mappings ++= contentOf(baseDirectory.value / "extra") + * }}} + * + * The resulting mappings sequence will look something like this + * + * {{{ + * File(baseDirectory/extras/file1) -> "file1" + * File(baseDirectory/extras/file2) -> "file2" + * ... + * }}} + * + * @example Add a static directory "extra" and re-map the destination to a different path + * {{{ + * mappings ++= contentOf(baseDirectory.value / "extra").map { + * case (src, destination) => src -> s"new/path/destination" + * } + * }}} + * + * @param baseDirectory The directory that should be turned into a mappings sequence. + * @return mappings - The `basicDirectory`'s contents exlcuding `basicDirectory` itself + */ + def contentOf(baseDirectory: File)(using conv: FileConverter): Seq[(VirtualFile, String)] = + (PathFinder(baseDirectory).allPaths --- PathFinder(baseDirectory)) + .pair(relativeTo(baseDirectory)) + .map { case (f, s) => conv.toVirtualFile(f.toPath) -> s } +end Mapper diff --git a/main-actions/src/test/scala/sbt/MapperTest.scala b/main-actions/src/test/scala/sbt/MapperTest.scala new file mode 100644 index 000000000..0c94f4129 --- /dev/null +++ b/main-actions/src/test/scala/sbt/MapperTest.scala @@ -0,0 +1,161 @@ +/* + * sbt + * Copyright 2023, Scala center + * Copyright 2011 - 2022, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt + +import java.nio.file.Files + +import sbt.internal.inc.MappedFileConverter +import sbt.io.IO +import sbt.io.syntax.* +import xsbti.FileConverter + +object MapperTest extends verify.BasicTestSuite: + test("directory should create mappings including the baseDirectory") { + withTempDirectory: tempDirectory => + given FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + val nestedFile1 = tempDirectory / "file1" + val nestedFile2 = tempDirectory / "file2" + val nestedDir = tempDirectory / "dir1" + val nestedDirFile = nestedDir / "dir1-file1" + + IO.touch(nestedFile1) + IO.touch(nestedFile2) + IO.createDirectory(nestedDir) + IO.touch(nestedDirFile) + + val mappings = Mapper + .directory(tempDirectory) + .map { case (h, p) => + (h.toString, p) + } + + Predef.assert( + mappings.toSet == List( + "${BASE}/" -> s"${tempDirectory.getName}", + "${BASE}/file1" -> s"${tempDirectory.getName}/file1", + "${BASE}/file2" -> s"${tempDirectory.getName}/file2", + "${BASE}/dir1" -> s"${tempDirectory.getName}/dir1", + "${BASE}/dir1/dir1-file1" -> s"${tempDirectory.getName}/dir1/dir1-file1", + ).toSet + ) + } + + test("it should create one mapping entry for an empty directory") { + withTempDirectory: tempDirectory => + given FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + val mappings = Mapper + .directory(tempDirectory) + .map { case (h, p) => + (h.toString, p) + } + Predef.assert( + mappings.toSet == List( + "${BASE}/" -> "foo" + ).toSet, + s"found $mappings" + ) + } + + test("it should create an empty mappings sequence for a non-existing directory") { + withTempDirectory: tempDirectory => + val conv0: FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + given FileConverter = conv0 + val nonExistingDirectory = tempDirectory / "imaginary" + val mappings = Mapper.directory(nonExistingDirectory) + assert(mappings.isEmpty) + } + + test("it should create one mapping entry if the directory is a file") { + withTempDirectory: tempDirectory => + val conv0: FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + given FileConverter = conv0 + val file = tempDirectory / "file" + IO.touch(file) + val mappings = Mapper.directory(file).map { case (h, p) => + (h.toString, p) + } + Predef.assert( + mappings.toSet == Set("${BASE}/file" -> s"${file.getName}"), + s"actual: $mappings" + ) + } + + test("contentOf should create mappings excluding the baseDirectory") { + withTempDirectory: tempDirectory => + val conv0: FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + given FileConverter = conv0 + val nestedFile1 = tempDirectory / "file1" + val nestedFile2 = tempDirectory / "file2" + val nestedDir = tempDirectory / "dir1" + val nestedDirFile = nestedDir / "dir1-file1" + IO.touch(nestedFile1) + IO.touch(nestedFile2) + IO.createDirectory(nestedDir) + IO.touch(nestedDirFile) + + val mappings = Mapper.contentOf(tempDirectory).map { case (h, p) => + (h.toString, p) + } + Predef.assert( + mappings.toSet == List( + "${BASE}/file1" -> "file1", + "${BASE}/file2" -> "file2", + "${BASE}/dir1" -> "dir1", + "${BASE}/dir1/dir1-file1" -> "dir1/dir1-file1", + ).toSet, + s"actual: $mappings" + ) + } + + test("it should create an empty mappings sequence for an empty directory") { + withTempDirectory: tempDirectory => + given FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + val mappings = Mapper.contentOf(tempDirectory) + assert(mappings.isEmpty) + } + + test("it should create an empty mappings sequence for a non-existing directory") { + withTempDirectory: tempDirectory => + given FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + val nonExistingDirectory = tempDirectory / "imaginary" + val mappings = Mapper.contentOf(nonExistingDirectory) + assert(mappings.isEmpty) + } + + test("it should create an empty mappings sequence if the directory is a file") { + withTempDirectory: tempDirectory => + given FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + val file = tempDirectory / "file" + val mappings = Mapper.contentOf(file) + assert(mappings.isEmpty) + } + + test("it should create an empty mappings sequence if the directory is a file") { + withTempDirectory: tempDirectory => + given FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + val file = tempDirectory / "file" + val mappings = Mapper.contentOf(file) + assert(mappings.isEmpty) + } + + test("allSubpaths should not include the base directory") { + withTempDirectory: tempDirectory => + given FileConverter = MappedFileConverter(Map("BASE" -> tempDirectory.toPath()), true) + val file = Files.createFile((tempDirectory / "file").toPath) + val paths = Mapper.allSubpaths(tempDirectory).toVector.map(_._1.toString).toSet + assert(paths.contains("${BASE}/file")) + assert(!paths.contains("${BASE}")) + } + + def withTempDirectory[A1](f: File => A1): A1 = + IO.withTemporaryDirectory: tempDirectory0 => + val tempDirectory = tempDirectory0 / "foo" + IO.createDirectory(tempDirectory) + f(tempDirectory) +end MapperTest diff --git a/sbt-app/src/main/scala/sbt/Import.scala b/sbt-app/src/main/scala/sbt/Import.scala index 51af25035..cbeadd22f 100644 --- a/sbt-app/src/main/scala/sbt/Import.scala +++ b/sbt-app/src/main/scala/sbt/Import.scala @@ -54,7 +54,6 @@ trait Import { val Hash = sbt.io.Hash val HiddenFileFilter = sbt.io.HiddenFileFilter val IO = sbt.io.IO - type Mapper = sbt.io.Mapper val NameFilter = sbt.io.NameFilter type NameFilter = sbt.io.NameFilter val NothingFilter = sbt.io.NothingFilter diff --git a/sbt-app/src/sbt-test/package/mappings-directory/build.sbt b/sbt-app/src/sbt-test/package/mappings-directory/build.sbt new file mode 100644 index 000000000..7e6a058a3 --- /dev/null +++ b/sbt-app/src/sbt-test/package/mappings-directory/build.sbt @@ -0,0 +1,16 @@ +name := "Mappings Test" + +scalaVersion := "3.3.1" +version := "0.2" + +Compile / packageBin / mappings ++= { + val converter = fileConverter.value + Mapper.directory(file("test"))(using converter) +} + +lazy val unzipPackage = taskKey[Unit]("extract jar file") +unzipPackage := { + val converter = fileConverter.value + val p = converter.toPath((Compile / packageBin).value) + IO.unzip(p.toFile(), target.value / "extracted") +} diff --git a/sbt-app/src/sbt-test/package/mappings-directory/test b/sbt-app/src/sbt-test/package/mappings-directory/test new file mode 100644 index 000000000..30589f08d --- /dev/null +++ b/sbt-app/src/sbt-test/package/mappings-directory/test @@ -0,0 +1,2 @@ +> unzipPackage +$ exists target/out/jvm/scala-3.3.1/mappings-test/extracted/test diff --git a/sbt-app/src/sbt-test/package/mappings-directory/test-directory/test b/sbt-app/src/sbt-test/package/mappings-directory/test-directory/test new file mode 100644 index 000000000..e69de29bb