diff --git a/core/api/daemon/src/mill/api/daemon/ExecResult.scala b/core/api/daemon/src/mill/api/daemon/ExecResult.scala index debc11c357cb..03bf312513ad 100644 --- a/core/api/daemon/src/mill/api/daemon/ExecResult.scala +++ b/core/api/daemon/src/mill/api/daemon/ExecResult.scala @@ -67,9 +67,10 @@ object ExecResult { def flatMap[V](f: T => ExecResult[V]): Failing[V] override def asFailing: Option[ExecResult.Failing[T]] = Some(this) - def throwException: Nothing = this match { - case f: ExecResult.Failure[?] => throw new Result.Exception(f.msg) - case f: ExecResult.Exception => throw f.throwable + def throwException: Nothing = throw exception + private[mill] def exception: Throwable = this match { + case f: ExecResult.Failure[?] => new Result.Exception(f.msg) + case f: ExecResult.Exception => f.throwable } } diff --git a/libs/javalib/src/mill/javalib/JavaModule.scala b/libs/javalib/src/mill/javalib/JavaModule.scala index 31824924cbb6..2bfa70fc6032 100644 --- a/libs/javalib/src/mill/javalib/JavaModule.scala +++ b/libs/javalib/src/mill/javalib/JavaModule.scala @@ -492,6 +492,25 @@ trait JavaModule */ def unmanagedClasspath: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } + /** + * Same class path as [[unmanagedClasspath]], but ensures every entry is a JAR file. + * + * If you'd like to add unmanaged class path entries, add them to [[unmanagedClasspath]]. + * `unmanagedClasspathAsJars` will pick them up automatically, and convert directory entries + * to JAR files if needed. + */ + private[mill] def unmanagedClasspathAsJars: T[Seq[PathRef]] = + Task { + unmanagedClasspath().zipWithIndex.flatMap { + case (ref, refIdx) if os.isDir(ref.path) => + val jar = Task.dest / s"$refIdx.jar" + Jvm.createJar(jar, Seq(ref.path)) + Seq(PathRef(jar)) + case (ref, _) if os.exists(ref.path) => Seq(ref) + case (_, _) => Nil + } + } + /** * The `coursier.Dependency` to use to refer to this module */ @@ -835,6 +854,24 @@ trait JavaModule */ def compileResources: T[Seq[PathRef]] = Task.Sources { "compile-resources" } + /** + * Same class path as [[compileResources]], but ensures every entry is a JAR file. + * + * If you'd like to add compile resources entries, add them to [[compileResources]]. + * `compileResourcesAsJars` will pick them up automatically, and convert directory entries + * to JAR files if needed. + */ + private[mill] def compileResourcesAsJars: T[Seq[PathRef]] = Task { + compileResources().zipWithIndex.flatMap { + case (ref, refIdx) if os.isDir(ref.path) => + val jar = Task.dest / s"$refIdx.jar" + Jvm.createJar(jar, Seq(ref.path)) + Seq(PathRef(jar)) + case (ref, _) if os.exists(ref.path) => Seq(ref) + case (_, _) => Nil // filtering out non-existing entries, to only keep actual JAR files + } + } + /** * Folders containing source files that are generated rather than * handwritten; these files can be generated in this task itself, @@ -1018,6 +1055,14 @@ trait JavaModule */ def compileClasspath: T[Seq[PathRef]] = Task { compileClasspathTask(CompileFor.Regular)() } + /** + * All classfiles and resources from upstream modules and dependencies + * necessary to compile this module, all packaged as JAR files. + */ + def compileClasspathAsJars: T[Seq[PathRef]] = Task { + resolvedMvnDeps() ++ transitiveJars() ++ localCompileClasspathAsJars() + } + /** * All classfiles and resources from upstream modules and dependencies * necessary to compile this module. @@ -1053,6 +1098,14 @@ trait JavaModule compileResources() ++ unmanagedClasspath() } + /** + * The *input* classfiles/resources from this module, used during compilation, + * excluding upstream modules and third-party dependencies, packaged as JAR files. + */ + def localCompileClasspathAsJars: T[Seq[PathRef]] = Task { + compileResourcesAsJars() ++ unmanagedClasspathAsJars() + } + /** * Resolved dependencies */ @@ -1127,6 +1180,13 @@ trait JavaModule localClasspath() } + override def runClasspathAsJars: T[Seq[PathRef]] = Task { + super[RunModule].runClasspathAsJars() ++ // Remove '[RunModule]' when we can break bin-compat + resolvedRunMvnDeps().toSeq ++ + transitiveJars() ++ + Seq(jar()) + } + /** * A jar containing only this module's resources and compiled classfiles, * without those from upstream modules and dependencies diff --git a/libs/javalib/src/mill/javalib/RunModule.scala b/libs/javalib/src/mill/javalib/RunModule.scala index a2773afc3836..c2c77144f65e 100644 --- a/libs/javalib/src/mill/javalib/RunModule.scala +++ b/libs/javalib/src/mill/javalib/RunModule.scala @@ -59,9 +59,20 @@ trait RunModule extends WithJvmWorkerModule with RunModuleApi { /** * All classfiles and resources including upstream modules and dependencies * necessary to run this module's code. + * + * The returned class path can include directories. See `runClasspathAsJars` + * if you'd like the class path to contain only JAR files. */ def runClasspath: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } + /** + * All classfiles and resources including upstream modules and dependencies + * necessary to run this module's code. + * + * Unlike `runClasspath`, all class path entries here are JAR files. + */ + def runClasspathAsJars: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } + /** * The elements of the run classpath which are local to this module. * This is typically the output of a compilation step and bundles runtime resources. diff --git a/libs/javalib/test/resources/classpath/app/compile-resources/empty b/libs/javalib/test/resources/classpath/app/compile-resources/empty new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/libs/javalib/test/resources/classpath/app/src/app/MyApp.java b/libs/javalib/test/resources/classpath/app/src/app/MyApp.java new file mode 100644 index 000000000000..55e595e7fa78 --- /dev/null +++ b/libs/javalib/test/resources/classpath/app/src/app/MyApp.java @@ -0,0 +1,12 @@ +package app; + +import lib.Thing; + +public class MyApp { + public static void main(String[] args) throws java.net.URISyntaxException { + String appUri = MyApp.class.getProtectionDomain().getCodeSource().getLocation().toURI().toASCIIString(); + String libUri = Thing.class.getProtectionDomain().getCodeSource().getLocation().toURI().toASCIIString(); + System.out.println("App URI: " + appUri); + System.out.println("Lib URI: " + libUri); + } +} diff --git a/libs/javalib/test/resources/classpath/appAsJars/compile-resources/empty b/libs/javalib/test/resources/classpath/appAsJars/compile-resources/empty new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/libs/javalib/test/resources/classpath/build.mill b/libs/javalib/test/resources/classpath/build.mill new file mode 100644 index 000000000000..cf6edea9f021 --- /dev/null +++ b/libs/javalib/test/resources/classpath/build.mill @@ -0,0 +1,2 @@ +import mill.* +import mill.javalib.* diff --git a/libs/javalib/test/resources/classpath/lib/src/lib/Thing.java b/libs/javalib/test/resources/classpath/lib/src/lib/Thing.java new file mode 100644 index 000000000000..8e68af82229b --- /dev/null +++ b/libs/javalib/test/resources/classpath/lib/src/lib/Thing.java @@ -0,0 +1,7 @@ +package lib; + +public class Thing { + public static String message() { + return "Foo"; + } +} diff --git a/libs/javalib/test/src/mill/javalib/ClasspathTests.scala b/libs/javalib/test/src/mill/javalib/ClasspathTests.scala new file mode 100644 index 000000000000..95edf1a2b3b5 --- /dev/null +++ b/libs/javalib/test/src/mill/javalib/ClasspathTests.scala @@ -0,0 +1,113 @@ +package mill.javalib + +import mill.api.{Discover, PathRef, Task} +import mill.testkit.{TestRootModule, UnitTester} +import mill.util.TokenReaders.* + +import utest.* + +import java.net.URI +import java.nio.file.Paths +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +object ClasspathTests extends TestSuite { + + object TestCase extends TestRootModule { + object lib extends JavaModule + + object app extends JavaModule { + def moduleDeps = Seq(lib) + def mainClass = Some("app.MyApp") + } + + object appAsJars extends JavaModule { + def moduleDeps = Seq(app) + def mainClass = app.mainClass + def compileClasspath = compileClasspathAsJars + def runClasspath = runClasspathAsJars + } + + lazy val millDiscover = Discover[this.type] + } + + val tests: Tests = Tests { + test("test") { + val sources = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "classpath" + + UnitTester(TestCase, sourceRoot = sources).scoped { eval => + def classPathTaskValue(task: Task[Seq[PathRef]]) = + eval(task) + .left.map(f => throw f.exception) + .merge + .value + .map(_.path.subRelativeTo(TestCase.moduleDir)) + + val libCompileCp = classPathTaskValue(TestCase.lib.compileClasspath) + val expectedLibCompileCp = Seq( + os.sub / "lib/compile-resources" + ) + assert(expectedLibCompileCp == libCompileCp) + + val appCompileCp = classPathTaskValue(TestCase.app.compileClasspath) + val expectedAppCompileCp = Seq( + os.sub / "lib/compile-resources", + os.sub / "out/lib/compile.dest/classes", + os.sub / "app/compile-resources" + ) + assert(expectedAppCompileCp == appCompileCp) + + val appAsJarsCompileCp = classPathTaskValue(TestCase.appAsJars.compileClasspath) + val expectedAppAsJarsCompileCp = Seq( + os.sub / "out/lib/jar.dest/out.jar", + os.sub / "out/app/jar.dest/out.jar", + os.sub / "out/appAsJars/compileResourcesAsJars.dest/0.jar" + ) + assert(expectedAppAsJarsCompileCp == appAsJarsCompileCp) + } + + def locationFromOutput(kind: String, out: String): os.SubPath = { + val uriStr = + out.linesIterator.find(_.startsWith(s"$kind URI: ")).get.stripPrefix(s"$kind URI: ") + val path = os.Path(Paths.get(new URI(uriStr))) + path.subRelativeTo(TestCase.moduleDir) + } + + val mainBaos = new ByteArrayOutputStream + val jarBaos = new ByteArrayOutputStream + + UnitTester( + TestCase, + sourceRoot = sources, + outStream = new PrintStream(mainBaos, true) + ).scoped { eval => + eval(TestCase.app.run()).left.map(f => throw f.exception) + } + UnitTester( + TestCase, + sourceRoot = sources, + outStream = new PrintStream(jarBaos, true) + ).scoped { eval => + eval(TestCase.appAsJars.run()).left.map(f => throw f.exception) + } + + val mainOut = new String(mainBaos.toByteArray) + val jarOut = new String(jarBaos.toByteArray) + + val appDir = locationFromOutput("App", mainOut) + val appJar = locationFromOutput("App", jarOut) + val libDir = locationFromOutput("Lib", mainOut) + val libJar = locationFromOutput("Lib", jarOut) + + val expectedAppDir = os.sub / "out/app/compile.dest/classes" + val expectedAppJar = os.sub / "out/app/jar.dest/out.jar" + val expectedLibDir = os.sub / "out/lib/compile.dest/classes" + val expectedLibJar = os.sub / "out/lib/jar.dest/out.jar" + + assert(expectedAppDir == appDir) + assert(expectedAppJar == appJar) + assert(expectedLibDir == libDir) + assert(expectedLibJar == libJar) + } + } +}