diff --git a/backend/src/main/scala/bloop/BloopClassFileManager.scala b/backend/src/main/scala/bloop/BloopClassFileManager.scala index 53c8dcf9b..69c50fa1a 100644 --- a/backend/src/main/scala/bloop/BloopClassFileManager.scala +++ b/backend/src/main/scala/bloop/BloopClassFileManager.scala @@ -201,6 +201,34 @@ final class BloopClassFileManager( } + private def copyResources( + resources: List[AbsolutePath], + copyTo: AbsolutePath, + config: ParallelOps.CopyConfiguration + ): Task[Unit] = { + val (singleFiles, classpathEntries) = + resources.partition(path => path.exists && path.isFile) + + val singleFilesToCopy = + for (file <- singleFiles) + yield file.underlying -> copyTo.underlying.resolve(file.underlying.toFile().getName()) + + val classpathEntriesCopy = + for (entry <- classpathEntries) yield { + ParallelOps + .copyDirectories(config)( + entry.underlying, + copyTo.underlying, + inputs.ioScheduler, + enableCancellation = false, + inputs.logger, + singleFilesToCopy + ) + } + + Task.gatherUnordered(classpathEntriesCopy).map(_ => ()) + } + def complete(success: Boolean): Unit = { val deleteAfterCompilation = Task { BloopPaths.delete(AbsolutePath(backupDir)) } @@ -220,7 +248,7 @@ final class BloopClassFileManager( val config = ParallelOps.CopyConfiguration(5, CopyMode.ReplaceExisting, Set.empty, Set.empty) - ParallelOps + val copyClassFiles = ParallelOps .copyDirectories(config)( newClassesDir, clientExternalClassesDir.underlying, @@ -233,6 +261,16 @@ final class BloopClassFileManager( () } .flatMap(_ => deleteAfterCompilation) + + Task + .gatherUnordered( + List( + copyClassFiles, + copyResources(inputs.resources, clientExternalClassesDir, config) + ) + ) + .map(_ => ()) + } } ) diff --git a/backend/src/main/scala/bloop/Compiler.scala b/backend/src/main/scala/bloop/Compiler.scala index 64e4d599c..689a9f7a7 100644 --- a/backend/src/main/scala/bloop/Compiler.scala +++ b/backend/src/main/scala/bloop/Compiler.scala @@ -72,7 +72,8 @@ case class CompileInputs( ioScheduler: Scheduler, ioExecutor: Executor, invalidatedClassFilesInDependentProjects: Set[File], - generatedClassFilePathsInDependentProjects: Map[String, File] + generatedClassFilePathsInDependentProjects: Map[String, File], + resources: List[AbsolutePath] ) case class CompileOutPaths( diff --git a/backend/src/main/scala/bloop/io/ParallelOps.scala b/backend/src/main/scala/bloop/io/ParallelOps.scala index 993213133..28ff6f616 100644 --- a/backend/src/main/scala/bloop/io/ParallelOps.scala +++ b/backend/src/main/scala/bloop/io/ParallelOps.scala @@ -71,7 +71,8 @@ object ParallelOps { target: Path, scheduler: Scheduler, enableCancellation: Boolean, - logger: Logger + logger: Logger, + additionalFiles: Seq[(Path, Path)] = Nil ): Task[FileWalk] = Task.defer { val isCancelled = AtomicBoolean(false) @@ -82,6 +83,14 @@ object ParallelOps { MulticastStrategy.publish )(scheduler) + val copyAdditionalFiles = Task { + additionalFiles.foreach { + case (from, to) => + val attributes = Files.readAttributes(from, classOf[BasicFileAttributes]) + observer.onNext(((from, attributes), to)) + } + } + val discovery = new FileVisitor[Path] { var firstVisit: Boolean = true var currentTargetDirectory: Path = target @@ -274,7 +283,10 @@ object ParallelOps { } } - val orderlyDiscovery = Task.fromFuture(subscribed.future).flatMap(_ => discoverFileTree) + val orderlyDiscovery = Task + .fromFuture(subscribed.future) + .flatMap(_ => copyAdditionalFiles) + .flatMap(_ => discoverFileTree) val aggregatedCopyTask = Task { Task.mapBoth(orderlyDiscovery, copyFilesInParallel) { case (fileWalk, _) => fileWalk } }.flatten.executeOn(scheduler) diff --git a/frontend/src/main/scala/bloop/data/Project.scala b/frontend/src/main/scala/bloop/data/Project.scala index da2107c7f..7ac415cf3 100644 --- a/frontend/src/main/scala/bloop/data/Project.scala +++ b/frontend/src/main/scala/bloop/data/Project.scala @@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets import scala.collection.mutable import scala.util.Properties +import scala.util.Success import scala.util.Try import scala.util.control.NonFatal @@ -28,7 +29,6 @@ import bloop.util.JavaRuntime import scalaz.Cord import xsbti.compile.ClasspathOptions import xsbti.compile.CompileOrder -import scala.util.Success final case class Project( name: String, @@ -117,10 +117,8 @@ final case class Project( private def fullClasspath( dag: Dag[Project], client: ClientInfo, - rawClasspath: List[AbsolutePath], - pickValidResources: Project => Array[AbsolutePath] + rawClasspath: List[AbsolutePath] ): Array[AbsolutePath] = { - val addedResources = new mutable.HashSet[AbsolutePath]() val cp = (this.genericClassesDir :: rawClasspath).toBuffer // Add the resources right before the classes directory if found in the classpath @@ -128,16 +126,8 @@ final case class Project( val genericClassesDir = p.genericClassesDir val uniqueClassesDir = client.getUniqueClassesDirFor(p, forceGeneration = true) val index = cp.indexOf(genericClassesDir) - val newResources = pickValidResources(p).filterNot(r => addedResources.contains(r)) - newResources.foreach(r => addedResources.add(r)) - if (index == -1) { - // Not found? Weird. Let's add resources to end just in case - cp.appendAll(newResources) - } else { - // Replace in-place for the classes directory unique to the client + if (index != -1) { cp(index) = uniqueClassesDir - // Prepend resources to classes directories - cp.insertAll(index, newResources) } } @@ -145,7 +135,7 @@ final case class Project( } def fullClasspath(dag: Dag[Project], client: ClientInfo): Array[AbsolutePath] = { - fullClasspath(dag, client, rawClasspath, p => Project.pickValidResources(p.resources)) + fullClasspath(dag, client, rawClasspath) } def fullRuntimeClasspath(dag: Dag[Project], client: ClientInfo): Array[AbsolutePath] = { @@ -156,8 +146,7 @@ final case class Project( fullClasspath( dag, client, - rawRuntimeClasspath, - p => Project.pickValidResources(p.runtimeResources) + rawRuntimeClasspath ) } diff --git a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala index eeb7050c3..daeb365bb 100644 --- a/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala +++ b/frontend/src/main/scala/bloop/engine/tasks/CompileTask.scala @@ -162,7 +162,8 @@ object CompileTask { ExecutionContext.ioScheduler, ExecutionContext.ioExecutor, bundle.dependenciesData.allInvalidatedClassFiles, - bundle.dependenciesData.allGeneratedClassFilePaths + bundle.dependenciesData.allGeneratedClassFilePaths, + project.runtimeResources ) } diff --git a/frontend/src/test/scala/bloop/RunSpec.scala b/frontend/src/test/scala/bloop/RunSpec.scala index 967aa9ab2..24054f84e 100644 --- a/frontend/src/test/scala/bloop/RunSpec.scala +++ b/frontend/src/test/scala/bloop/RunSpec.scala @@ -3,6 +3,7 @@ package bloop import java.io.ByteArrayInputStream import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.nio.file.StandardOpenOption import java.util.concurrent.TimeUnit import scala.concurrent.Await @@ -474,6 +475,39 @@ class RunSpec extends BloopHelpers { } } + @Test + def runSeesSingleFileResources(): Unit = { + TestUtil.withinWorkspace { workspace => + object Sources { + val `a/A.scala` = + """/a/A.scala + |object A { + | def main(args: Array[String]): Unit = { + | val res = getClass.getClassLoader.getResourceAsStream("resource.txt") + | val content = scala.io.Source.fromInputStream(res).mkString + | assert("goodbye" == content) + | } + |}""".stripMargin + } + + val tmpDir = Files.createTempDirectory("runtime-single") + val resource = tmpDir.resolve("resource.txt") + Files.write(resource, "goodbye".getBytes(), StandardOpenOption.CREATE) + val logger = new RecordingLogger(ansiCodesSupported = false) + val `A` = TestProject( + workspace, + "a", + List(Sources.`a/A.scala`), + additionalResources = List(resource) + ) + + val projects = List(`A`) + val state = loadState(workspace, projects, logger) + val runState = state.run(`A`) + assertEquals(ExitStatus.Ok, runState.status) + } + } + @Test def runUsesRuntimeEnvironment(): Unit = { TestUtil.withinWorkspace { workspace => diff --git a/frontend/src/test/scala/bloop/util/TestProject.scala b/frontend/src/test/scala/bloop/util/TestProject.scala index d8638e1a1..5188dd669 100644 --- a/frontend/src/test/scala/bloop/util/TestProject.scala +++ b/frontend/src/test/scala/bloop/util/TestProject.scala @@ -113,7 +113,8 @@ abstract class BaseTestProject { order: Config.CompileOrder = Config.Mixed, jars: Array[AbsolutePath] = Array(), sourcesGlobs: List[Config.SourcesGlobs] = Nil, - sourceGenerators: List[Config.SourceGenerator] = Nil + sourceGenerators: List[Config.SourceGenerator] = Nil, + additionalResources: List[Path] = Nil ): TestProject = { val projectBaseDir = Files.createDirectories(baseDir.underlying.resolve(name)) val ProjectArchetype(sourceDir, outDir, resourceDir, classes, runtimeResourceDir) = @@ -146,7 +147,7 @@ abstract class BaseTestProject { if (strictDependencies) (directClasspath, transitiveClasspath) else (transitiveClasspath, transitiveClasspath) } - val compileResourcesList = Some(List(resourceDir.underlying)) + val compileResourcesList = Some(List(resourceDir.underlying) ::: additionalResources) val runtimeResourcesList = runtimeResources match { case None => compileResourcesList case Some(_) => Some(List(runtimeResourceDir.underlying))