diff --git a/README.md b/README.md index 9f68829..0ac2df7 100644 --- a/README.md +++ b/README.md @@ -525,9 +525,12 @@ class = { module = "keyboard:jvm", main = "keyboard.Bundle" } In addition to `BUILD_PATH`, we also set the environment variable `MODULE_PATH` when running classes. The module path is the root path of the Seed project which contains the referenced module. This environment variable is not set for commands since their current working directory will point to the module path. -For two equivalent examples of using code generation, please refer to these links: +It is possible for multiple modules to share a code generator. To access the source paths of all modules that depend on a generator, access the environment variable `MODULE_SOURCE_PATHS`. Its value uses the system path separator character (`java.io.File.pathSeparatorChar`). + +For examples of using code generation, please refer to these links: * [Class target](test/custom-class-target/) +* [Class target shared by multiple modules](test/custom-class-target-shared/) * [Command target](test/custom-command-target/) ### Compiler plug-ins diff --git a/src/main/scala/seed/cli/BuildTarget.scala b/src/main/scala/seed/cli/BuildTarget.scala index 79b5e1c..e2aae59 100644 --- a/src/main/scala/seed/cli/BuildTarget.scala +++ b/src/main/scala/seed/cli/BuildTarget.scala @@ -28,7 +28,7 @@ object BuildTarget { case _ => List() }.distinct - val inheritedTargets = modules + val flattenedDeps = modules .flatMap { case util.Target.Parsed(module, Some(Left(platform))) => BuildConfig.collectModuleDepsBase(build, module.module, platform) @@ -36,7 +36,34 @@ object BuildTarget { case util.Target.Parsed(module, None) => module.name +: BuildConfig.collectModuleDeps(build, module.module) } - .flatMap(m => build(m).module.target.keys.toList.map(m -> _)) + + // Determine direct parents of build targets + type BuildTarget = (String, String) // module, target name + type ParentModule = String + val parentModules: Map[BuildTarget, List[ParentModule]] = + flattenedDeps + .flatMap { parent => + BuildConfig + .allTargets(build, parent) + .flatMap { + case (m, t) => BuildConfig.platformModule(build(m).module, t) + } + .flatMap(_.moduleDeps) + .flatMap( + module => + build(module).module.target.keys.toList + .map(target => (module, target) -> parent) + ) + } + .groupBy(_._1) + .mapValues(_.map(_._2)) + + val inheritedTargets: List[BuildTarget] = flattenedDeps + .flatMap( + module => + build(module).module.target.keys.toList + .map(target => (module, target)) + ) .distinct if (targets.nonEmpty) @@ -58,6 +85,16 @@ object BuildTarget { val modulePath = build(m).path val target = build(m).module.target(t) + val moduleSourcePaths = parentModules((m, t)).flatMap { module => + val targets = BuildConfig.allTargets(build, module) + targets + .flatMap { + case (m, t) => BuildConfig.platformModule(build(m).module, t) + } + .flatMap(_.sources) + .map(_.toAbsolutePath.toString) + } + target.`class` match { case Some(c) => val bloopName = @@ -68,6 +105,7 @@ object BuildTarget { customLog, customLog.info(_), Some(modulePath.toAbsolutePath.toString), + moduleSourcePaths, Some(buildPath.toAbsolutePath.toString) )(args: _*) @@ -83,6 +121,7 @@ object BuildTarget { ProcessHelper.runShell( modulePath, cmd, + moduleSourcePaths, buildPath.toAbsolutePath.toString, customLog, customLog.info(_) @@ -96,6 +135,7 @@ object BuildTarget { val process = ProcessHelper.runShell( modulePath, cmd, + moduleSourcePaths, buildPath.toAbsolutePath.toString, customLog, customLog.info(_) diff --git a/src/main/scala/seed/process/ProcessHelper.scala b/src/main/scala/seed/process/ProcessHelper.scala index db599ef..ec5cdd8 100644 --- a/src/main/scala/seed/process/ProcessHelper.scala +++ b/src/main/scala/seed/process/ProcessHelper.scala @@ -1,5 +1,6 @@ package seed.process +import java.io.File import java.nio.ByteBuffer import java.nio.file.Path @@ -65,6 +66,7 @@ object ProcessHelper { cwd: Path, cmd: List[String], modulePath: Option[String] = None, + moduleSourcePaths: List[String] = List(), buildPath: Option[String] = None, log: Log, onStdOut: String => Unit, @@ -84,6 +86,16 @@ object ProcessHelper { log.debug(s"Module path: ${Ansi.italic(mp)}", detail = true) } + pb.environment() + .put( + "MODULE_SOURCE_PATHS", + moduleSourcePaths.mkString(File.pathSeparatorChar.toString) + ) + log.debug( + s"Module source paths: ${moduleSourcePaths.map(Ansi.italic).mkString(", ")}", + detail = true + ) + buildPath.foreach { bp => pb.environment().put("BUILD_PATH", bp) log.debug(s"Build path: ${Ansi.italic(bp)}", detail = true) @@ -125,6 +137,7 @@ object ProcessHelper { log: Log, onStdOut: String => Unit, modulePath: Option[String] = None, + moduleSourcePaths: List[String] = List(), buildPath: Option[String] = None, verbose: Boolean = true )(args: String*): Process = @@ -132,6 +145,7 @@ object ProcessHelper { cwd, List("bloop") ++ args, modulePath, + moduleSourcePaths, buildPath, log, output => onStdOut(output), @@ -141,6 +155,7 @@ object ProcessHelper { def runShell( cwd: Path, command: String, + moduleSourcePaths: List[String] = List(), buildPath: String, log: Log, onStdOut: String => Unit @@ -149,6 +164,7 @@ object ProcessHelper { cwd, List("/bin/sh", "-c", command), None, + moduleSourcePaths, Some(buildPath), log, onStdOut diff --git a/src/test/scala/seed/generation/BloopIntegrationSpec.scala b/src/test/scala/seed/generation/BloopIntegrationSpec.scala index bb9ab54..b2c5e20 100644 --- a/src/test/scala/seed/generation/BloopIntegrationSpec.scala +++ b/src/test/scala/seed/generation/BloopIntegrationSpec.scala @@ -281,8 +281,8 @@ object BloopIntegrationSpec extends TestSuite[Unit] { def buildCustomTarget( name: String, expectFailure: Boolean = false - ): Future[List[String]] = { - val path = Paths.get(s"test/$name") + ): Future[Either[List[String], List[String]]] = { + val path = Paths.get("test", name) val config = BuildConfig.load(path, Log.urgent).get import config._ @@ -317,7 +317,7 @@ object BloopIntegrationSpec extends TestSuite[Unit] { if (expectFailure) RTS.unsafeRunToFuture(uio).failed.map { _ => assert(lines.nonEmpty) - lines.toList + Left(lines.toList) } else { RTS.unsafeRunSync(uio) @@ -327,20 +327,38 @@ object BloopIntegrationSpec extends TestSuite[Unit] { TestProcessHelper .runBloop(buildPath)("run", "demo") .map { x => - assertEquals(x.split("\n").count(_ == "42"), 1) Files.delete(generatedFile) assertEquals(lines.toList, List()) - List() + Right(x.split("\n").toList) } } } testAsync("Build project with custom class target") { _ => - buildCustomTarget("custom-class-target").map(_ => ()) + buildCustomTarget("custom-class-target").map( + lines => assertEquals(lines.right.get.count(_ == "42"), 1) + ) + } + + testAsync( + "Build project with custom class target (shared by multiple modules)" + ) { _ => + buildCustomTarget("custom-class-target-shared").map( + lines => + assertEquals( + lines.right.get.map(_.split("test/").last), + List( + "custom-class-target-shared/template1", + "custom-class-target-shared/template2" + ) + ) + ) } testAsync("Build project with custom command target") { _ => - buildCustomTarget("custom-command-target").map { _ => + buildCustomTarget("custom-command-target").map { lines => + assertEquals(lines.right.get.count(_ == "42"), 1) + val path = tempPath .resolve("custom-command-target") .resolve(".bloop") @@ -368,7 +386,8 @@ object BloopIntegrationSpec extends TestSuite[Unit] { log => // Must indicate correct position assert( - log.exists(_.contains("[2:41]: not found: value invalidIdentifier")) + log.left.get + .exists(_.contains("[2:41]: not found: value invalidIdentifier")) ) ) } diff --git a/src/test/scala/seed/generation/util/TestProcessHelper.scala b/src/test/scala/seed/generation/util/TestProcessHelper.scala index 63e81fd..7616993 100644 --- a/src/test/scala/seed/generation/util/TestProcessHelper.scala +++ b/src/test/scala/seed/generation/util/TestProcessHelper.scala @@ -32,6 +32,7 @@ object TestProcessHelper { cwd, cmd, None, + List(), None, Log.urgent, out => sb.append(out + "\n") diff --git a/test/custom-class-target-shared/build.toml b/test/custom-class-target-shared/build.toml new file mode 100644 index 0000000..1e1d315 --- /dev/null +++ b/test/custom-class-target-shared/build.toml @@ -0,0 +1,22 @@ +[project] +scalaVersion = "2.13.0" + +[module.utils.jvm] +sources = ["utils"] + +[module.utils.target.gen-sources] +class = ["utils:jvm", "GenerateSources"] +await = true + +# `template1` and `template2` share the code generator `utils:gen-sources` +[module.template1.jvm] +moduleDeps = ["utils"] +sources = ["template1"] + +[module.template2.jvm] +moduleDeps = ["utils"] +sources = ["template2"] + +[module.demo.jvm] +moduleDeps = ["template1", "template2"] +sources = ["demo"] diff --git a/test/custom-class-target-shared/demo/Main.scala b/test/custom-class-target-shared/demo/Main.scala new file mode 100644 index 0000000..d725fc0 --- /dev/null +++ b/test/custom-class-target-shared/demo/Main.scala @@ -0,0 +1,3 @@ +object Main { + def main(args: Array[String]): Unit = Generated.modulePaths.foreach(println) +} diff --git a/test/custom-class-target-shared/utils/GenerateSources.scala b/test/custom-class-target-shared/utils/GenerateSources.scala new file mode 100644 index 0000000..eb503c6 --- /dev/null +++ b/test/custom-class-target-shared/utils/GenerateSources.scala @@ -0,0 +1,16 @@ +import java.io.File +import java.nio.file.{Files, Paths, StandardOpenOption} + +object GenerateSources { + def main(args: Array[String]): Unit = { + val modulePath = sys.env("MODULE_PATH") + val moduleSourcePaths = + sys.env("MODULE_SOURCE_PATHS").split(File.pathSeparatorChar) + val generatedPath = + Paths.get(modulePath).resolve("demo").resolve("Generated.scala") + val pathsScala = moduleSourcePaths.mkString("\"", "\", \"", "\"") + val output = s"object Generated { val modulePaths = List($pathsScala) }" + + Files.write(generatedPath, output.getBytes) + } +}