diff --git a/build.mill b/build.mill index 39a83e01f45..8aa64f429ff 100644 --- a/build.mill +++ b/build.mill @@ -184,6 +184,14 @@ object Deps { val sonatypeCentralClient = ivy"com.lumidion::sonatype-central-client-requests:0.3.0" val kotlinVersion = "2.0.21" val kotlinCompiler = ivy"org.jetbrains.kotlin:kotlin-compiler:$kotlinVersion" + val mavenVersion = "3.9.9" + val mavenEmbedder = ivy"org.apache.maven:maven-embedder:$mavenVersion" + val mavenResolverVersion = "1.9.22" + val mavenResolverConnectorBasic = ivy"org.apache.maven.resolver:maven-resolver-connector-basic:$mavenResolverVersion" + val mavenResolverSupplier = ivy"org.apache.maven.resolver:maven-resolver-supplier:$mavenResolverVersion" + val mavenResolverTransportFile = ivy"org.apache.maven.resolver:maven-resolver-transport-file:$mavenResolverVersion" + val mavenResolverTransportHttp = ivy"org.apache.maven.resolver:maven-resolver-transport-http:$mavenResolverVersion" + val mavenResolverTransportWagon = ivy"org.apache.maven.resolver:maven-resolver-transport-wagon:$mavenResolverVersion" object RuntimeDeps { val dokkaVersion = "1.9.20" diff --git a/dist/package.mill b/dist/package.mill index c74944f2594..b5f04424da2 100644 --- a/dist/package.mill +++ b/dist/package.mill @@ -19,6 +19,7 @@ object `package` extends RootModule with build.MillPublishJavaModule { def testTransitiveDeps = build.runner.testTransitiveDeps() ++ Seq( build.main.graphviz.testDep(), + build.main.maven.testDep(), build.runner.linenumbers.testDep(), build.scalalib.backgroundwrapper.testDep(), build.contrib.bloop.testDep(), diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index b92363e3f03..bc64ce014d4 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -40,6 +40,8 @@ ** xref:cli/flags.adoc[] ** xref:cli/alternate-installation.adoc[] ** xref:cli/builtin-commands.adoc[] +* Migrating to Mill +** xref:migrating/maven.adoc[] // This section gives a tour of the various user-facing features of Mill: // library deps, out folder, queries, tasks, etc.. These are things that // every Mill user will likely encounter, and are touched upon in the various diff --git a/docs/modules/ROOT/pages/migrating/maven.adoc b/docs/modules/ROOT/pages/migrating/maven.adoc new file mode 100644 index 00000000000..41830940c2d --- /dev/null +++ b/docs/modules/ROOT/pages/migrating/maven.adoc @@ -0,0 +1,155 @@ += Migrating From Maven to Mill +:page-aliases: Migrating_A_Maven_Build_to_Mill.adoc +:icons: font + +include::partial$gtag-config.adoc[] + +The Mill `init` command can be used to convert a Maven build to Mill. This has +xref:#limitations[limitations] and is not intended to reliably migrate 100% of +Maven builds out there in the wild, but is instead meant to provide the basic +scaffolding of a Mill build for you to further refine and update manually. + +Each Maven module with a `pom.xml` is converted to a Mill `build.mill`/`package.mill` +file containing a top-level `MavenModule`. A nested `test` module is defined if both: + +* `src/test` exists +* a supported xref:javalib/testing.adoc[test framework] is detected (for a _tests only_ +module with test sources in `src/main/java`) + + +Again, note that `mill init` importing Maven builds is best effort. +This means that while small projects can be expected to complete without issue: + +include::partial$example/javalib/migrating/1-maven-complete.adoc[] + +More larger projects often require some manual tweaking in order to work: + +include::partial$example/javalib/migrating/2-maven-incomplete.adoc[] + +Nevertheless, even for larger builds `mill init` automates most of the tedious +busy-work of writing `build.mill`/`package.mill` files, and makes it much quicker +to get a working Mill build for any existing Maven project. + + +== Capabilities + +The conversion + +* handles deeply nested modules +* captures project metadata +* configures dependencies for scopes: +** compile +** provided +** runtime +** test +* configures testing frameworks: +** JUnit 4 +** JUnit 5 +** TestNG +* configures multiple, compile and test, resource directories + +=== Command line arguments +.name of generated base module trait defining project metadata settings +[source,sh] +---- +./mill init --base-module MyModule +---- +.name of generated nested test module (defaults to `test`) +[source,sh] +---- +./mill init --test-module test +---- +.name of generated companion object defining constants for dependencies +[source,sh] +---- +./mill init --deps-object Deps +---- +.capture properties defined in `pom.xml` for publishing +[source,sh] +---- +./mill init --publish-properties +---- +.merge build files generated for a multi-module build +[source,sh] +---- +./mill init --merge +---- + +.use cache for Maven repository system +[source,sh] +---- +./mill init --cache-repository +---- +.process Maven plugin executions and configurations +[source,sh] +---- +./mill init --process-plugins +---- + +=== Verified projects + +The conversion has been tested with the following projects: + +* https://github.com/fusesource/jansi/archive/refs/tags/jansi-2.4.1.zip[jansi] +[source,sh] +---- +./mill init --base-module JansiModule --deps-object Deps --cache-repository --process-plugins +---- + +* https://github.com/davidmoten/geo/archive/refs/tags/0.8.1.zip[geo] (multi-module build) +[source,sh] +---- +./mill init --base-module GeoModule --deps-object Deps --merge --cache-repository --process-plugins +---- + +Post `init`, the following tasks were executed successfully: + +* `compile` +* `test` +* `publishLocal` + +[#limitations] +== Limitations + +The conversion does not support + +* build extensions +* build profiles +* non-Java (native) sources + +Maven plugin support is limited to + +* https://maven.apache.org/plugins/maven-compiler-plugin/[maven-compiler-plugin] + +[TIP] +==== +These limitations can be overcome by: + +* configuring equivalent Mill xref:extending/contrib-plugins.adoc[contrib] + or xref:extending/thirdparty-plugins.adoc[third party] plugins +* defining custom xref:extending/writing-plugins.adoc[plugins] +* defining custom xref:fundamentals/tasks.adoc[tasks] +* defining custom xref:fundamentals/cross-builds.adoc[cross modules] +==== + +== FAQ + +.How to fix compilation errors in generated build files? + +This could happen if a module and task name collision occurs. Either rename the module or enclose the name in backticks. + + +.How to fix JPMS `module not found` compilation errors? + +Set https://github.com/tfesenko/Java-Modules-JPMS-CheatSheet#how-to-export-or-open-a-package[additional command line options] +for dependencies. + + +.How to fix test compilation errors? + +* The test framework configured may be for an unsupported version; try upgrading the + corresponding dependencies. +* Mill does not add `provided` dependencies to the transitive dependencies of the nested + test module; specify the dependencies again, in one of `ivyDeps`, `compileIvyDeps`, `runIvyDeps`, in the test module. + + diff --git a/example/javalib/migrating/1-maven-complete/build.mill b/example/javalib/migrating/1-maven-complete/build.mill new file mode 100644 index 00000000000..d5dde1b02f1 --- /dev/null +++ b/example/javalib/migrating/1-maven-complete/build.mill @@ -0,0 +1,29 @@ +/** Usage + +> rm build.mill # remove any existing build file + +> git init . +> git remote add -f origin https://github.com/davidmoten/geo.git +> git checkout 0.8.1 # example multi-module Java project using JUnit4 + +> ./mill init +converting Maven build +writing Mill build file to geo/package.mill +writing Mill build file to geo-mem/package.mill +writing Mill build file to build.mill +init completed, run "mill resolve _" to list available tasks + +> ./mill __.compile +compiling 9 Java sources to .../out/geo/compile.dest/classes ... +compiling 2 Java sources to .../out/geo-mem/compile.dest/classes ... +compiling 10 Java sources to .../out/geo/test/compile.dest/classes ... +done compiling +compiling 1 Java source to .../out/geo-mem/test/compile.dest/classes ... +done compiling + +> ./mill __.test # all tests pass immediately +Test run com.github.davidmoten.geo.GeoHashTest finished: 0 failed, 0 ignored, 66 total, ... +Test run com.github.davidmoten.geo.DirectionTest finished: 0 failed, 0 ignored, 1 total, ... +... + +*/ diff --git a/example/javalib/migrating/2-maven-incomplete/build.mill b/example/javalib/migrating/2-maven-incomplete/build.mill new file mode 100644 index 00000000000..e43afe13db2 --- /dev/null +++ b/example/javalib/migrating/2-maven-incomplete/build.mill @@ -0,0 +1,24 @@ +/** Usage + +> rm build.mill # remove any existing build file + +> git init . +> git remote add -f origin https://github.com/dhatim/fastexcel.git +> git checkout 0.18.4 +> # pom.xml has custom profiles for JPMS that mill init does not support +> # https://github.com/dhatim/fastexcel/blob/de56e786a1fe29351e2f8dc1d81b7cdd9196de4a/pom.xml#L251 + +> ./mill init +converting Maven build +writing Mill build file to fastexcel-writer/package.mill +writing Mill build file to fastexcel-reader/package.mill +writing Mill build file to e2e/package.mill +writing Mill build file to build.mill +init completed, run "mill resolve _" to list available tasks + +> ./mill -k __.compile # Compilation needs manual tweaking to pass +error: ...fastexcel-reader/src/main/java/module-info.java:3:32: module not found: org.apache.commons.compress +error: ...fastexcel-reader/src/main/java/module-info.java:4:27: module not found: com.fasterxml.aalto +error: ...fastexcel-writer/src/main/java/module-info.java:2:14: module not found: opczip + +*/ diff --git a/example/package.mill b/example/package.mill index 1dc4e0ae50e..b1359bfb0db 100644 --- a/example/package.mill +++ b/example/package.mill @@ -34,6 +34,7 @@ object `package` extends RootModule with Module { object dependencies extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "dependencies")) object testing extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "testing")) object linting extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "linting")) + object migrating extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "migrating")) object publishing extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "publishing")) object web extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "web")) } diff --git a/integration/feature/init/src/MillInitMavenTests.scala b/integration/feature/init/src/MillInitMavenTests.scala new file mode 100644 index 00000000000..70f26f5065f --- /dev/null +++ b/integration/feature/init/src/MillInitMavenTests.scala @@ -0,0 +1,407 @@ +package mill.integration + +import mill.main.client.Util +import mill.testkit.{IntegrationTester, UtestIntegrationTestSuite} +import utest.* + +abstract class MillInitMavenTests extends UtestIntegrationTestSuite { + + // Github source zip url + def url: String + + def initMessages(buildFileCount: Int): Seq[String] = Seq( + s"generated $buildFileCount Mill build file(s)" + ) + + override def integrationTest[T](f: IntegrationTester => T): T = + super.integrationTest { tester => + val zipFile = os.temp(requests.get(url)) + val unzipDir = os.unzip(zipFile, os.temp.dir()) + val sourceDir = os.list(unzipDir).head + // move fails on Windows, so copy + for (p <- os.list(sourceDir)) + os.copy.into(p, tester.workspacePath, replaceExisting = true, createFolders = true) + f(tester) + } +} + +object MillInitMavenJansiTests extends MillInitMavenTests { + + def url: String = + // - Junit5 + // - maven-compiler-plugin release option + "https://github.com/fusesource/jansi/archive/refs/tags/jansi-2.4.1.zip" + + val compileMessages: Seq[String] = Seq( + "compiling 20 Java sources" + ) + + val testMessages: Seq[String] = Seq( + "Test run finished: 0 failed, 1 ignored, 90 total" + ) + + val publishLocalMessages: Seq[String] = Seq( + "Publishing Artifact(org.fusesource.jansi,jansi,2.4.1)" + ) + + def tests: Tests = Tests { + test - integrationTest { tester => + import tester._ + + val initRes = eval("init") + assert( + initMessages(1).forall(initRes.out.contains), + initRes.isSuccess + ) + + val compileRes = eval("compile") + assert( + compileMessages.forall(compileRes.err.contains), + compileRes.isSuccess + ) + + val testRes = eval("test") + assert( + testMessages.forall(testRes.out.contains), + testRes.isSuccess + ) + + val publishLocalRes = eval("publishLocal") + assert( + publishLocalMessages.forall(publishLocalRes.err.contains), + publishLocalRes.isSuccess + ) + } + + test("realistic") - integrationTest { tester => + import tester._ + + val initRes = eval( + ( + "init", + "--base-module", + "JansiModule", + "--deps-object", + "Deps", + "--cache-repository", + "--process-plugins" + ) + ) + assert( + initMessages(1).forall(initRes.out.contains), + initRes.isSuccess + ) + + val compileRes = eval("compile") + assert( + compileMessages.forall(compileRes.err.contains), + compileRes.isSuccess + ) + + val testRes = eval("test") + assert( + testMessages.forall(testRes.out.contains), + testRes.isSuccess + ) + + val publishLocalRes = eval("publishLocal") + assert( + publishLocalMessages.forall(publishLocalRes.err.contains), + publishLocalRes.isSuccess + ) + } + } +} + +object MillInitMavenDotEnvTests extends MillInitMavenTests { + + def url: String = + // - multi-module + // - TestNg + // - maven-compiler-plugin release option + "https://github.com/shyiko/dotenv/archive/refs/tags/0.1.1.zip" + + val resolveModules: Seq[String] = Seq( + "dotenv", + "dotenv-guice" + ) + + // JavaModule.JavaTests does not pick compileIvyDeps from outer module + val compileErrors: Seq[String] = Seq( + "DotEnvModuleTest.java:3:25: package com.google.inject does not exist" + ) + + def tests: Tests = Tests { + test - integrationTest { tester => + import tester._ + + val initRes = eval("init") + assert( + initMessages(3).forall(initRes.out.contains), + initRes.isSuccess + ) + + val resolveRes = eval(("resolve", "_")) + assert( + resolveModules.forall(resolveRes.out.contains), + resolveRes.isSuccess + ) + + val compileRes = eval("__.compile") + assert( + compileErrors.forall(compileRes.err.contains), + !compileRes.isSuccess + ) + } + } +} + +object MillInitMavenAvajeConfigTests extends MillInitMavenTests { + + def url: String = + // - multi-module + // - unsupported test framework + "https://github.com/avaje/avaje-config/archive/refs/tags/4.0.zip" + + val resolveModules: Seq[String] = Seq( + "avaje-config", + "avaje-aws-appconfig", + "avaje-dynamic-logback" + ) + // uses moditect-maven-plugin to handle JPMS + // https://github.com/moditect/moditect + val compileErrors: Seq[String] = Seq( + "avaje-config/src/main/java/module-info.java:5:31: module not found: io.avaje.lang", + "avaje-config/src/main/java/module-info.java:6:31: module not found: io.avaje.applog", + "avaje-config/src/main/java/module-info.java:7:27: module not found: org.yaml.snakeyaml", + "avaje-config/src/main/java/module-info.java:9:27: module not found: io.avaje.spi" + ) + + def tests: Tests = Tests { + test - integrationTest { tester => + import tester._ + + val initRes = eval("init") + assert( + initRes.out.contains("generated 4 Mill build file(s)"), + initRes.isSuccess + ) + + val resolveRes = eval(("resolve", "_")) + assert( + resolveModules.forall(resolveRes.out.contains), + resolveRes.isSuccess + ) + + // uses moditect-maven-plugin to handle JPMS + // https://github.com/moditect/moditect + val compileRes = eval("__.compile") + assert( + compileErrors.forall(compileRes.err.contains), + !compileRes.isSuccess + ) + } + } +} + +object MillInitMavenNettyTests extends MillInitMavenTests { + + def url: String = + // - multi-module + // - Junit5 + // - maven-compiler-plugin compilerArgs options + // - module directory and artifact names differ + // - multi line description, properties + // - property contains quotes + // - defines test dependencies in root pom.xml that get propagated to every module + "https://github.com/netty/netty/archive/refs/tags/netty-4.1.114.Final.zip" + + val initWarnings: Seq[String] = Seq( + "[codec-http2] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[transport-native-epoll] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[transport-native-kqueue] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[handler] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[example] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[testsuite] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[testsuite-shading] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[transport-blockhound-tests] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final", + "[microbench] dropping classifier ${os.detected.classifier} for dependency io.netty:netty-tcnative:2.0.66.Final" + ) + + val resolveModules: Seq[String] = Seq( + "all", + "bom", + "buffer", + "codec", + "codec-dns", + "codec-haproxy", + "codec-http", + "codec-http2", + "codec-memcache", + "codec-mqtt", + "codec-redis", + "codec-smtp", + "codec-socks", + "codec-stomp", + "codec-xml", + "common", + "dev-tools", // resources only + "example", + "handler", + "handler-proxy", + "handler-ssl-ocsp", + "microbench", + "resolver", + "resolver-dns", + "resolver-dns-classes-macos", + "resolver-dns-native-macos", + "testsuite", // tests only in src/main/java + "testsuite-autobahn", // tests only in src/main/java + "testsuite-http2", // tests only in src/main/java + "testsuite-native", // tests only in src/test/java + "testsuite-native-image", // tests only in src/main/java + "testsuite-native-image-client", // tests only in src/main/java + "testsuite-native-image-client-runtime-init", // tests only in src/main/java + "testsuite-osgi", // tests only in src/test/java + "testsuite-shading", // tests only in src/test/java + "transport", + "transport-blockhound-tests", // tests only in src/test/java + "transport-classes-epoll", + "transport-classes-kqueue", + "transport-native-epoll", // C sources + "transport-native-kqueue", // C sources + "transport-native-unix-common", // Java and C sources + "transport-native-unix-common-tests", + "transport-rxtx", + "transport-sctp", + "transport-udt" + ) + + val compileTasksThatSucceed: Seq[String] = Seq( + "common.compile", + "dev-tools.compile", + "testsuite-osgi.compile", + "buffer.compile", + "resolver.compile", + "testsuite-native-image-client-runtime-init.compile", + "buffer.test.compile", + "transport.compile", + "resolver.test.compile", + "testsuite-native-image-client-runtime-init.test.compile", + "codec.compile", + "transport-native-unix-common.compile", + "transport-rxtx.compile", + "transport-udt.compile", + "transport.test.compile", + "codec-haproxy.compile", + "codec-memcache.compile", + "codec-smtp.compile", + "codec-socks.compile", + "codec-stomp.compile", + "codec-xml.compile", + "handler.compile", + "transport-native-unix-common-tests.compile", + "transport-native-unix-common.test.compile", + "transport-rxtx.test.compile", + "transport-udt.test.compile", + "codec-http.compile", + "transport-native-unix-common-tests.test.compile", + "handler-proxy.compile", + "testsuite-autobahn.compile", + "testsuite-native-image.compile", + "testsuite-autobahn.test.compile", + "testsuite-native-image.test.compile" + ) + + val compileTasksThatFail: Seq[String] = Seq( + /* missing outer compileIvyDeps */ + "common.test.compile", + /* missing generated sources */ + "transport-sctp.compile", + "transport-classes-epoll.compile", + "transport-classes-kqueue.compile", + "codec-dns.compile", + "codec-mqtt.compile", + "codec-redis.compile", + "codec-http2.compile", + /* missing native dependency */ + "codec.test.compile", + "codec-haproxy.test.compile", + "codec-memcache.test.compile", + "codec-smtp.test.compile", + "codec-socks.test.compile", + "codec-stomp.test.compile", + "codec-xml.test.compile", + "handler.test.compile", + "codec-http.test.compile", + "handler-proxy.test.compile", + /* upstream compile fails */ + "codec-mqtt.test.compile", + "codec-redis.test.compile", + "codec-dns.test.compile", + "resolver-dns.compile", + "transport-sctp.test.compile", + "transport-classes-epoll.test.compile", + "transport-native-epoll.compile", + "transport-classes-kqueue.test.compile", + "transport-native-kqueue.compile", + "testsuite.compile", + "handler-ssl-ocsp.compile", + "resolver-dns-classes-macos.compile", + "resolver-dns.test.compile", + "testsuite-native-image-client.compile", + "transport-blockhound-tests.compile", + "testsuite-shading.compile", + "all.compile", + "codec-http2.test.compile", + "example.compile", + "microbench.compile", + "testsuite-http2.compile", + "testsuite-osgi.test.compile", + "handler-ssl-ocsp.test.compile", + "resolver-dns-classes-macos.test.compile", + "resolver-dns-native-macos.compile", + "testsuite-native-image-client.test.compile", + "transport-blockhound-tests.test.compile", + "testsuite.test.compile", + "transport-native-epoll.test.compile", + "transport-native-kqueue.test.compile", + "testsuite-shading.test.compile", + "all.test.compile", + "example.test.compile", + "microbench.test.compile", + "testsuite-http2.test.compile", + "resolver-dns-native-macos.test.compile", + "testsuite-native.compile", + "testsuite-native.test.compile" + ) + + def tests: Tests = Tests { + test - integrationTest { tester => + // Takes forever on windows and behaves differently from linux/mac + if (!Util.isWindows) { + import tester._ + + val initRes = eval(("init", "--publish-properties")) + assert( + // suppressed to avoid CI failure on Windows + JDK 17 + // initWarnings.forall(initRes.out.contains), + initMessages(47).forall(initRes.out.contains), + initRes.isSuccess + ) + + val resolveRes = eval(("resolve", "_")) + assert( + resolveModules.forall(resolveRes.out.contains), + resolveRes.isSuccess + ) + for (task <- compileTasksThatSucceed) { + assert(eval(task, stdout = os.Inherit, stderr = os.Inherit).isSuccess) + } + for (task <- compileTasksThatFail) { + assert(!eval(task, stdout = os.Inherit, stderr = os.Inherit).isSuccess) + } + } + } + } +} diff --git a/main/init/package.mill b/main/init/package.mill index 0970b00fdd6..b23012dd01a 100644 --- a/main/init/package.mill +++ b/main/init/package.mill @@ -4,7 +4,8 @@ import mill._ import scala.util.matching.Regex object `package` extends RootModule with build.MillPublishScalaModule { - def moduleDeps = Seq(build.main) + + def moduleDeps = Seq(build.main, build.scalalib) override def resources = Task { super.resources() ++ Seq(exampleList()) diff --git a/main/init/src/mill/init/InitMavenModule.scala b/main/init/src/mill/init/InitMavenModule.scala new file mode 100644 index 00000000000..6e792c1f3cf --- /dev/null +++ b/main/init/src/mill/init/InitMavenModule.scala @@ -0,0 +1,101 @@ +package mill.init + +import coursier.LocalRepositories +import coursier.maven.MavenRepository +import mill.api.{Loose, PathRef} +import mill.define.{Discover, ExternalModule, TaskModule} +import mill.scalalib.scalafmt.ScalafmtWorkerModule +import mill.util.{Jvm, Util} +import mill.{Command, T, Task} + +import scala.util.control.NoStackTrace + +@mill.api.experimental +object InitMavenModule extends ExternalModule with InitMavenModule with TaskModule { + + lazy val millDiscover: Discover = Discover[this.type] +} + +/** + * Defines a [[InitModule.init task]] to convert a Maven build to Mill. + */ +@mill.api.experimental +trait InitMavenModule extends TaskModule { + + def defaultCommandName(): String = "init" + + /** + * Classpath containing [[buildGenMainClass build file generator]]. + */ + def buildGenClasspath: T[Loose.Agg[PathRef]] = T { + val repositories = Seq( + LocalRepositories.ivy2Local, + MavenRepository("https://repo1.maven.org/maven2"), + MavenRepository("https://oss.sonatype.org/content/repositories/releases") + ) + Util.millProjectModule("mill-main-maven", repositories) + } + + /** + * Mill build file generator application entrypoint. + */ + def buildGenMainClass: T[String] = "mill.main.maven.BuildGen" + + /** + * Scalafmt configuration file for formatting generated Mill build files. + */ + def initScalafmtConfig: T[PathRef] = T { + val config = millSourcePath / ".scalafmt.conf" + if (!os.exists(config)) { + T.log.info(s"creating Scalafmt configuration file $config") + os.write( + config, + s"""version = "3.8.4-RC1" + |runner.dialect = scala213 + |newlines.source=fold + |newlines.topLevelStatementBlankLines = [ + | { + | blanks { before = 1 } + | } + |] + |""".stripMargin + ) + } + PathRef(config) + } + + /** + * Generates and formats Mill build files for an existing Maven project. + * + * @param args arguments for the [[buildGenMainClass build file generator]] + */ + def init(args: String*): Command[Unit] = Task.Command { + val root = millSourcePath + + val mainClass = buildGenMainClass() + val classPath = buildGenClasspath().map(_.path) + val exit = Jvm.callSubprocess( + mainClass = mainClass, + classPath = classPath, + mainArgs = args, + workingDir = root + ).exitCode + + if (exit == 0) { + val files = os.walk.stream(root, skip = (root / "out").equals) + .filter(_.ext == "mill") + .map(PathRef(_)) + .toSeq + val config = initScalafmtConfig() + T.log.info("formatting Mill build files") + ScalafmtWorkerModule.worker().reformat(files, config) + + T.log.info("init completed, run \"mill resolve _\" to list available tasks") + } else { + throw InitMavenException(s"$mainClass exit($exit)") + } + } +} + +@mill.api.experimental +case class InitMavenException(message: String) extends Exception(message) with NoStackTrace diff --git a/main/maven/src/mill/main/build/Tree.scala b/main/maven/src/mill/main/build/Tree.scala new file mode 100644 index 00000000000..714c4efe9e3 --- /dev/null +++ b/main/maven/src/mill/main/build/Tree.scala @@ -0,0 +1,86 @@ +package mill.main.build + +import mill.main.build.Tree.Traversal + +import scala.collection.compat.Factory + +/** + * A recursive data structure that defines parent-child relationships between nodes. + * + * @param node the root node of this tree + * @param subtrees the child subtrees of this tree + */ +@mill.api.experimental +case class Tree[+Node](node: Node, subtrees: Seq[Tree[Node]] = Seq.empty) { + + def fold[S](initial: S)(operator: (S, Node) => S)(implicit T: Traversal): S = { + var s = initial + T.foreach(this)(tree => s = operator(s, tree.node)) + s + } + + def map[Out](f: Node => Out): Tree[Out] = + transform[Out]((node, subtrees) => Tree(f(node), subtrees.iterator.toSeq)) + + def to[F](implicit F: Factory[Node, F], T: Traversal): F = + fold(F.newBuilder)(_ += _).result() + + def toSeq(implicit T: Traversal): Seq[Node] = + to + + def transform[Out](f: (Node, IterableOnce[Tree[Out]]) => Tree[Out]): Tree[Out] = { + def recurse(tree: Tree[Node]): Tree[Out] = + f(tree.node, tree.subtrees.iterator.map(recurse)) + + recurse(this) + } +} +@mill.api.experimental +object Tree { + + /** Generates a tree from `start` using the `step` function. */ + def from[Input, Node](start: Input)(step: Input => (Node, IterableOnce[Input])): Tree[Node] = { + def recurse(input: Input): Tree[Node] = { + val (node, next) = step(input) + Tree(node, next.iterator.map(recurse).toSeq) + } + + recurse(start) + } + + sealed trait Traversal { + + def foreach[Node](root: Tree[Node])(visit: Tree[Node] => Unit): Unit + } + object Traversal { + + implicit def traversal: Traversal = DepthFirst + + object BreadthFirst extends Traversal { + + def foreach[Node](root: Tree[Node])(visit: Tree[Node] => Unit): Unit = { + @annotation.tailrec + def recurse(level: Seq[Tree[Node]]): Unit = { + if (level.nonEmpty) { + level.foreach(visit) + recurse(level.flatMap(_.subtrees)) + } + } + + recurse(Seq(root)) + } + } + + object DepthFirst extends Traversal { + + def foreach[Node](root: Tree[Node])(visit: Tree[Node] => Unit): Unit = { + def recurse(tree: Tree[Node]): Unit = { + tree.subtrees.foreach(recurse) + visit(tree) + } + + recurse(root) + } + } + } +} diff --git a/main/maven/src/mill/main/build/ir.scala b/main/maven/src/mill/main/build/ir.scala new file mode 100644 index 00000000000..c078dac0d78 --- /dev/null +++ b/main/maven/src/mill/main/build/ir.scala @@ -0,0 +1,166 @@ +package mill.main.build + +import mill.main.client.CodeGenConstants.{nestedBuildFileNames, rootBuildFileNames, rootModuleAlias} +import mill.runner.FileImportGraph.backtickWrap + +import scala.collection.immutable.{SortedMap, SortedSet} + +/** + * A Mill build module defined as a Scala object. + * + * @param imports Scala import statements + * @param companions build companion objects defining constants + * @param supertypes Scala supertypes inherited by the object + * @param inner Scala object code + * @param outer additional Scala type definitions like base module traits + */ +@mill.api.experimental +case class BuildObject( + imports: SortedSet[String], + companions: BuildObject.Companions, + supertypes: Seq[String], + inner: String, + outer: String +) +@mill.api.experimental +object BuildObject { + + type Constants = SortedMap[String, String] + type Companions = SortedMap[String, Constants] + + private val linebreak = + """ + |""".stripMargin + + private val linebreak2 = + """ + | + |""".stripMargin + + private def extend(supertypes: Seq[String]): String = supertypes match { + case Seq() => "" + case Seq(head) => s"extends $head" + case head +: tail => tail.mkString(s"extends $head with ", " with ", "") + } + + implicit class NodeOps(private val self: Node[BuildObject]) extends AnyVal { + + def file: os.RelPath = { + val name = if (self.dirs.isEmpty) rootBuildFileNames.head else nestedBuildFileNames.head + os.rel / self.dirs / name + } + + def source: os.Source = { + val pkg = self.pkg + val BuildObject(imports, companions, supertypes, inner, outer) = self.module + val importStatements = imports.iterator.map("import " + _).mkString(linebreak) + val companionTypedefs = companions.iterator.map { + case (_, vals) if vals.isEmpty => "" + case (name, vals) => + val members = + vals.iterator.map { case (k, v) => s"val $k = $v" }.mkString(linebreak) + + s"""object $name { + | + |$members + |}""".stripMargin + }.mkString(linebreak2) + + s"""package $pkg + | + |$importStatements + | + |$companionTypedefs + | + |object `package` ${extend(supertypes)} { + | + |$inner + |} + | + |$outer + |""".stripMargin + } + } + + implicit class TreeOps(private val self: Tree[Node[BuildObject]]) extends AnyVal { + + def merge: Tree[Node[BuildObject]] = { + def merge(parentCompanions: Companions, childCompanions: Companions): Companions = { + var mergedParentCompanions = parentCompanions + + childCompanions.foreach { case entry @ (objectName, childConstants) => + val parentConstants = mergedParentCompanions.getOrElse(objectName, null) + if (null == parentConstants) mergedParentCompanions += entry + else { + if (childConstants.exists { case (k, v) => v != parentConstants.getOrElse(k, v) }) + return null + else mergedParentCompanions += ((objectName, parentConstants ++ childConstants)) + } + } + + mergedParentCompanions + } + + self.transform[Node[BuildObject]] { (node, subtrees) => + val isRoot = node.dirs.isEmpty + var parent = node.module + val unmerged = Seq.newBuilder[Tree[Node[BuildObject]]] + + subtrees.iterator.foreach { + case subtree @ Tree(Node(_ :+ dir, child), Seq()) if child.outer.isEmpty => + merge(parent.companions, child.companions) match { + case null => unmerged += subtree + case mergedCompanions => + val mergedImports = + parent.imports ++ ( + if (isRoot) child.imports.filterNot(_.startsWith("$file")) else child.imports + ) + val mergedInner = { + val name = backtickWrap(dir) + val supertypes = child.supertypes.diff(Seq("RootModule")) + + s"""${parent.inner} + | + |object $name ${extend(supertypes)} { + | + |${child.inner} + |}""".stripMargin + } + + parent = parent.copy( + imports = mergedImports, + companions = mergedCompanions, + inner = mergedInner + ) + } + case subtree => unmerged += subtree + } + + val unmergedSubtrees = unmerged.result() + if (isRoot && unmergedSubtrees.isEmpty) { + parent = parent.copy(imports = parent.imports.filterNot(_.startsWith("$packages"))) + } + + Tree(node.copy(module = parent), unmergedSubtrees) + } + } + } +} + +/** + * A node representing a module in a build tree. + * + * @param dirs relative location in the build tree + * @param module build module + */ +@mill.api.experimental +case class Node[Module](dirs: Seq[String], module: Module) +@mill.api.experimental +object Node { + + implicit class Ops[Module](private val self: Node[Module]) extends AnyVal { + + def pkg: String = + (rootModuleAlias +: self.dirs).iterator.map(backtickWrap).mkString(".") + } +} diff --git a/main/maven/src/mill/main/maven/BuildGen.scala b/main/maven/src/mill/main/maven/BuildGen.scala new file mode 100644 index 00000000000..06b62e719f5 --- /dev/null +++ b/main/maven/src/mill/main/maven/BuildGen.scala @@ -0,0 +1,475 @@ +package mill.main.maven + +import mainargs.{Flag, ParserForClass, arg, main} +import mill.main.build.{BuildObject, Node, Tree} +import mill.runner.FileImportGraph.backtickWrap +import org.apache.maven.model.{Dependency, Model} + +import scala.collection.immutable.{SortedMap, SortedSet} +import scala.jdk.CollectionConverters.* + +/** + * Converts a Maven build to Mill by generating Mill build file(s) from POM file(s). + * + * The generated output should be considered scaffolding and will likely require edits to complete conversion. + * + * ===Capabilities=== + * The conversion + * - handles deeply nested modules + * - captures project metadata + * - configures dependencies for scopes: + * - compile + * - provided + * - runtime + * - test + * - configures testing frameworks: + * - JUnit 4 + * - JUnit 5 + * - TestNG + * - configures multiple, compile and test, resource directories + * + * ===Limitations=== + * The conversion does not support: + * - plugins, other than maven-compiler-plugin + * - packaging, other than jar, pom + * - build extensions + * - build profiles + */ +@mill.api.internal +object BuildGen { + + def main(args: Array[String]): Unit = { + val cfg = ParserForClass[BuildGenConfig].constructOrExit(args.toSeq) + run(cfg) + } + + private type MavenNode = Node[Model] + private type MillNode = Node[BuildObject] + + private def run(cfg: BuildGenConfig): Unit = { + val workspace = os.pwd + + println("converting Maven build") + val modeler = Modeler(cfg) + val input = Tree.from(Seq.empty[String]) { dirs => + val model = modeler(workspace / dirs) + (Node(dirs, model), model.getModules.iterator().asScala.map(dirs :+ _)) + } + + var output = convert(input, cfg) + if (cfg.merge.value) { + println("compacting Mill build tree") + output = output.merge + } + + val nodes = output.toSeq + println(s"generated ${nodes.length} Mill build file(s)") + + println("removing existing Mill build files") + os.walk.stream(workspace, skip = (workspace / "out").equals) + .filter(_.ext == ".mill") + .foreach(os.remove.apply) + + nodes.foreach { node => + val file = node.file + val source = node.source + println(s"writing Mill build file to $file") + os.write(workspace / file, source) + } + + println("converted Maven build to Mill") + } + + private def convert(input: Tree[MavenNode], cfg: BuildGenConfig): Tree[MillNode] = { + val packages = // for resolving moduleDeps + input + .fold(Map.newBuilder[Id, Package])((z, build) => z += ((Id(build.module), build.pkg))) + .result() + + val baseModuleTypedef = cfg.baseModule.fold("") { baseModule => + val metadataSettings = metadata(input.node.module, cfg) + + s"""trait $baseModule extends PublishModule with MavenModule { + | + |$metadataSettings + |}""".stripMargin + } + + input.map { case build @ Node(dirs, model) => + val packaging = model.getPackaging + val millSourcePath = os.Path(model.getProjectDirectory) + + val imports = { + val b = SortedSet.newBuilder[String] + b += "mill._" + b += "mill.javalib._" + b += "mill.javalib.publish._" + if (dirs.nonEmpty) cfg.baseModule.foreach(baseModule => b += s"$$file.$baseModule") + else if (packages.size > 1) b += "$packages._" + b.result() + } + + val supertypes = { + val b = Seq.newBuilder[String] + b += "RootModule" + cfg.baseModule.fold(b += "PublishModule" += "MavenModule")(b += _) + b.result() + } + + val (companions, compileDeps, providedDeps, runtimeDeps, testDeps, testModule) = + Scoped.all(model, packages, cfg) + + val inner = { + val javacOptions = Plugins.MavenCompilerPlugin.javacOptions(model) + + val artifactNameSetting = { + val id = model.getArtifactId + val name = if (dirs.nonEmpty && dirs.last == id) null else s"\"$id\"" // skip default + optional("override def artifactName = ", name) + } + val resourcesSetting = + resources( + model.getBuild.getResources.iterator().asScala + .map(_.getDirectory) + .map(os.Path(_)) + .filterNot((millSourcePath / "src/main/resources").equals) + .map(_.relativeTo(millSourcePath)) + ) + val javacOptionsSetting = + optional("override def javacOptions = Seq(\"", javacOptions, "\",\"", "\")") + val depsSettings = compileDeps.settings("ivyDeps", "moduleDeps") + val compileDepsSettings = providedDeps.settings("compileIvyDeps", "compileModuleDeps") + val runDepsSettings = runtimeDeps.settings("runIvyDeps", "runModuleDeps") + val pomPackagingTypeSetting = { + val packagingType = packaging match { + case "jar" => null // skip default + case "pom" => "PackagingType.Pom" + case pkg => s"\"$pkg\"" + } + optional(s"override def pomPackagingType = ", packagingType) + } + val pomParentProjectSetting = { + val parent = model.getParent + if (null == parent) "" + else { + val group = parent.getGroupId + val id = parent.getArtifactId + val version = parent.getVersion + s"override def pomParentProject = Some(Artifact(\"$group\", \"$id\", \"$version\"))" + } + } + val metadataSettings = if (cfg.baseModule.isEmpty) metadata(model, cfg) else "" + val testModuleTypedef = { + val resources = model.getBuild.getTestResources.iterator().asScala + .map(_.getDirectory) + .map(os.Path(_)) + .filterNot((millSourcePath / "src/test/resources").equals) + if ( + "pom" != packaging && ( + os.exists(millSourcePath / "src/test") || resources.nonEmpty || testModule.nonEmpty + ) + ) { + val supertype = "MavenTests" + val testMillSourcePath = millSourcePath / "test" + val resourcesRel = resources.map(_.relativeTo(testMillSourcePath)) + + testDeps.testTypeDef(supertype, testModule, resourcesRel, cfg) + } else "" + } + + s"""$artifactNameSetting + | + |$resourcesSetting + | + |$javacOptionsSetting + | + |$depsSettings + | + |$compileDepsSettings + | + |$runDepsSettings + | + |$pomPackagingTypeSetting + | + |$pomParentProjectSetting + | + |$metadataSettings + | + |$testModuleTypedef""".stripMargin + } + + val outer = if (dirs.isEmpty) baseModuleTypedef else "" + + build.copy(module = BuildObject(imports, companions, supertypes, inner, outer)) + } + } + + private type Id = (String, String, String) + private object Id { + + def apply(mvn: Dependency): Id = + (mvn.getGroupId, mvn.getArtifactId, mvn.getVersion) + + def apply(mvn: Model): Id = + (mvn.getGroupId, mvn.getArtifactId, mvn.getVersion) + } + + private type Package = String + private type ModuleDeps = SortedSet[Package] + private type IvyInterp = String + private type IvyDeps = SortedSet[String] // interpolated or named + + private case class Scoped(ivyDeps: IvyDeps, moduleDeps: ModuleDeps) { + + def settings(ivyDepsName: String, moduleDepsName: String): String = { + val ivyDepsSetting = + optional(s"override def $ivyDepsName = Agg", ivyDeps) + val moduleDepsSetting = + optional(s"override def $moduleDepsName = Seq", moduleDeps) + + s"""$ivyDepsSetting + | + |$moduleDepsSetting""".stripMargin + } + + def testTypeDef( + supertype: String, + testModule: Scoped.TestModule, + resourcesRel: IterableOnce[os.RelPath], + cfg: BuildGenConfig + ): String = + if (ivyDeps.isEmpty && moduleDeps.isEmpty) "" + else { + val name = backtickWrap(cfg.testModule) + val declare = testModule match { + case Some(module) => s"object $name extends $supertype with $module" + case None => s"trait $name extends $supertype" + } + val resourcesSetting = resources(resourcesRel) + val moduleDepsSetting = + optional(s"override def moduleDeps = super.moduleDeps ++ Seq", moduleDeps) + val ivyDepsSetting = optional(s"override def ivyDeps = super.ivyDeps() ++ Agg", ivyDeps) + + s"""$declare { + | + |$resourcesSetting + | + |$moduleDepsSetting + | + |$ivyDepsSetting + |}""".stripMargin + } + } + private object Scoped { + + private type Compile = Scoped + private type Provided = Scoped + private type Runtime = Scoped + private type Test = Scoped + private type TestModule = Option[String] + + def all( + model: Model, + packages: PartialFunction[Id, Package], + cfg: BuildGenConfig + ): (BuildObject.Companions, Compile, Provided, Runtime, Test, TestModule) = { + val compileIvyDeps = SortedSet.newBuilder[String] + val providedIvyDeps = SortedSet.newBuilder[String] + val runtimeIvyDeps = SortedSet.newBuilder[String] + val testIvyDeps = SortedSet.newBuilder[String] + val compileModuleDeps = SortedSet.newBuilder[String] + val providedModuleDeps = SortedSet.newBuilder[String] + val runtimeModuleDeps = SortedSet.newBuilder[String] + val testModuleDeps = SortedSet.newBuilder[String] + var testModule = Option.empty[String] + + val notPom = "pom" != model.getPackaging + val ivyInterp: Dependency => IvyInterp = { + val module = model.getProjectDirectory.getName + dep => { + val group = dep.getGroupId + val id = dep.getArtifactId + val version = dep.getVersion + val tpe = dep.getType match { + case null | "" | "jar" => "" // skip default + case tpe => s";type=$tpe" + } + val classifier = dep.getClassifier match { + case null | "" => "" + case s"$${$v}" => // drop values like ${os.detected.classifier} + println(s"[$module] dropping classifier $${$v} for dependency $group:$id:$version") + "" + case classifier => s";classifier=$classifier" + } + val exclusions = dep.getExclusions.iterator.asScala + .map(x => s";exclude=${x.getGroupId}:${x.getArtifactId}") + .mkString + + s"ivy\"$group:$id:$version$tpe$classifier$exclusions\"" + } + } + val namedIvyDeps = Seq.newBuilder[(String, IvyInterp)] + val ivyDep: Dependency => String = { + cfg.depsObject.fold(ivyInterp) { objName => dep => + { + val depName = backtickWrap(s"${dep.getGroupId}:${dep.getArtifactId}") + namedIvyDeps += ((depName, ivyInterp(dep))) + s"$objName.$depName" + } + } + } + + model.getDependencies.asScala.foreach { dep => + val id = Id(dep) + dep.getScope match { + case "compile" if packages.isDefinedAt(id) => + compileModuleDeps += packages(id) + case "compile" => + compileIvyDeps += ivyDep(dep) + case "provided" if packages.isDefinedAt(id) => + providedModuleDeps += packages(id) + case "provided" => + providedIvyDeps += ivyDep(dep) + case "runtime" if packages.isDefinedAt(id) => + runtimeModuleDeps += packages(id) + case "runtime" => + runtimeIvyDeps += ivyDep(dep) + case "test" if packages.isDefinedAt(id) => + testModuleDeps += packages(id) + case "test" => + testIvyDeps += ivyDep(dep) + case scope => + println(s"skipping dependency $id with $scope scope") + } + // Maven module can be tests only + if (notPom && testModule.isEmpty) testModule = Option(dep.getGroupId match { + case "junit" => "TestModule.Junit4" + case "org.junit.jupiter" => "TestModule.Junit5" + case "org.testng" => "TestModule.TestNg" + case _ => null + }) + } + + val companions = cfg.depsObject.fold(SortedMap.empty[String, BuildObject.Constants])(name => + SortedMap((name, SortedMap(namedIvyDeps.result() *))) + ) + + ( + companions, + Scoped(compileIvyDeps.result(), compileModuleDeps.result()), + Scoped(providedIvyDeps.result(), providedModuleDeps.result()), + Scoped(runtimeIvyDeps.result(), runtimeModuleDeps.result()), + Scoped(testIvyDeps.result(), testModuleDeps.result()), + testModule + ) + } + } + + private def metadata(model: Model, cfg: BuildGenConfig): String = { + val description = escape(model.getDescription) + val organization = escape(model.getGroupId) + val url = escape(model.getUrl) + val licenses = model.getLicenses.iterator().asScala.map { license => + val id = escape(license.getName) + val name = id + val url = escape(license.getUrl) + val isOsiApproved = false + val isFsfLibre = false + val distribution = "\"repo\"" + + s"License($id, $name, $url, $isOsiApproved, $isFsfLibre, $distribution)" + }.mkString("Seq(", ", ", ")") + val versionControl = Option(model.getScm).fold(Seq.empty[String]) { scm => + val repo = escapeOption(scm.getUrl) + val conn = escapeOption(scm.getConnection) + val devConn = escapeOption(scm.getDeveloperConnection) + val tag = escapeOption(scm.getTag) + + Seq(repo, conn, devConn, tag) + }.mkString("VersionControl(", ", ", ")") + val developers = model.getDevelopers.iterator().asScala.map { dev => + val id = escape(dev.getId) + val name = escape(dev.getName) + val url = escape(dev.getUrl) + val org = escapeOption(dev.getOrganization) + val orgUrl = escapeOption(dev.getOrganizationUrl) + + s"Developer($id, $name, $url, $org, $orgUrl)" + }.mkString("Seq(", ", ", ")") + val publishVersion = escape(model.getVersion) + val publishProperties = + if (cfg.publishProperties.value) { + val props = model.getProperties + props.stringPropertyNames().iterator().asScala + .map(key => s"(\"$key\", ${escape(props.getProperty(key))})") + } else Seq.empty + + val pomSettings = + s"override def pomSettings = PomSettings($description, $organization, $url, $licenses, $versionControl, $developers)" + val publishVersionSetting = + s"override def publishVersion = $publishVersion" + val publishPropertiesSetting = + optional( + "override def publishProperties = super.publishProperties() ++ Map", + publishProperties + ) + + s"""$pomSettings + | + |$publishVersionSetting + | + |$publishPropertiesSetting""".stripMargin + } + + private def escape(value: String): String = + pprint.Util.literalize(if (value == null) "" else value) + + private def escapeOption(value: String): String = + if (null == value) "None" else s"Some(${escape(value)})" + + private def optional(start: String, value: String): String = + if (null == value) "" + else s"$start$value" + + private def optional(construct: String, args: IterableOnce[String]): String = + optional(construct + "(", args, ",", ")") + + private def optional( + start: String, + vals: IterableOnce[String], + sep: String, + end: String + ): String = { + val itr = vals.iterator + if (itr.isEmpty) "" + else itr.mkString(start, sep, end) + } + + private def resources(relPaths: IterableOnce[os.RelPath]): String = { + val itr = relPaths.iterator + if (itr.isEmpty) "" + else + itr + .map(rel => s"PathRef(millSourcePath / \"$rel\")") + .mkString(s"override def resources = Task.Sources { super.resources() ++ Seq(", ", ", ") }") + } +} + +@main +@mill.api.internal +case class BuildGenConfig( + @arg(doc = "name of generated base module trait defining project metadata settings") + baseModule: Option[String] = None, + @arg(doc = "name of generated nested test module") + testModule: String = "test", + @arg(doc = "name of generated companion object defining constants for dependencies") + depsObject: Option[String] = None, + @arg(doc = "capture properties defined in pom.xml for publishing") + publishProperties: Flag = Flag(), + @arg(doc = "merge build files generated for a multi-module build") + merge: Flag = Flag(), + @arg(doc = "use cache for Maven repository system") + cacheRepository: Flag = Flag(), + @arg(doc = "process Maven plugin executions and configurations") + processPlugins: Flag = Flag() +) extends ModelerConfig diff --git a/main/maven/src/mill/main/maven/Modeler.scala b/main/maven/src/mill/main/maven/Modeler.scala new file mode 100644 index 00000000000..7446b439f03 --- /dev/null +++ b/main/maven/src/mill/main/maven/Modeler.scala @@ -0,0 +1,87 @@ +package mill.main.maven + +import org.apache.maven.model.Model +import org.apache.maven.model.building.{ + DefaultModelBuilderFactory, + DefaultModelBuildingRequest, + ModelBuilder +} +import org.apache.maven.model.resolution.ModelResolver +import org.apache.maven.repository.internal.MavenRepositorySystemUtils +import org.eclipse.aether.DefaultRepositoryCache +import org.eclipse.aether.repository.{LocalRepository, RemoteRepository} +import org.eclipse.aether.supplier.RepositorySystemSupplier + +import java.io.File +import java.util.Properties + +/** + * Builds a [[Model]]. + * + * The implementation is inspired by [[https://github.com/sbt/sbt-pom-reader/ sbt-pom-reader]]. + */ +@mill.api.internal +class Modeler( + config: ModelerConfig, + builder: ModelBuilder, + resolver: ModelResolver, + systemProperties: Properties +) { + + /** Builds and returns the effective [[Model]] from `pomDir / "pom.xml"`. */ + def apply(pomDir: os.Path): Model = + build((pomDir / "pom.xml").toIO) + + /** Builds and returns the effective [[Model]] from `pomFile`. */ + def build(pomFile: File): Model = { + val request = new DefaultModelBuildingRequest() + request.setPomFile(pomFile) + request.setModelResolver(resolver.newCopy()) + request.setSystemProperties(systemProperties) + request.setProcessPlugins(config.processPlugins.value) + + val result = builder.build(request) + result.getEffectiveModel + } +} +@mill.api.internal +object Modeler { + + def apply( + config: ModelerConfig, + local: LocalRepository = defaultLocalRepository, + remotes: Seq[RemoteRepository] = defaultRemoteRepositories, + context: String = "", + systemProperties: Properties = defaultSystemProperties + ): Modeler = { + val builder = new DefaultModelBuilderFactory().newInstance() + val system = new RepositorySystemSupplier().get() + val session = MavenRepositorySystemUtils.newSession() + session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, local)) + if (config.cacheRepository.value) session.setCache(new DefaultRepositoryCache) + val resolver = new Resolver(system, session, remotes, context) + new Modeler(config, builder, resolver, systemProperties) + } + + def defaultLocalRepository: LocalRepository = + new LocalRepository((os.home / ".m2" / "repository").toIO) + + def defaultRemoteRepositories: Seq[RemoteRepository] = + Seq( + new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2") + .build() + ) + + def defaultSystemProperties: Properties = { + val props = new Properties() + sys.env.foreachEntry((k, v) => props.put(s"env.$k", v)) + sys.props.foreachEntry(props.put) + props + } +} + +@mill.api.internal +trait ModelerConfig { + def cacheRepository: mainargs.Flag + def processPlugins: mainargs.Flag +} diff --git a/main/maven/src/mill/main/maven/Plugins.scala b/main/maven/src/mill/main/maven/Plugins.scala new file mode 100644 index 00000000000..c3a54239e97 --- /dev/null +++ b/main/maven/src/mill/main/maven/Plugins.scala @@ -0,0 +1,61 @@ +package mill.main.maven + +import org.apache.maven.model.{Model, Plugin} +import org.codehaus.plexus.util.xml.Xpp3Dom + +import scala.jdk.CollectionConverters.* + +/** Utilities for handling Maven plugins. */ +@mill.api.internal +object Plugins { + + def find(model: Model, groupId: String, artifactId: String): Option[Plugin] = + model.getBuild.getPlugins.asScala + .find(p => p.getGroupId == groupId && p.getArtifactId == artifactId) + + def dom(plugin: Plugin): Option[Xpp3Dom] = + plugin.getConfiguration match { + case xpp3: Xpp3Dom => Some(xpp3) + case _ => None + } + + /** + * @see [[https://maven.apache.org/plugins/maven-compiler-plugin/index.html]] + */ + object MavenCompilerPlugin { + + def find(model: Model): Option[Plugin] = + Plugins.find(model, "org.apache.maven.plugins", "maven-compiler-plugin") + + def javacOptions(model: Model): Seq[String] = { + val options = Seq.newBuilder[String] + find(model).flatMap(dom).foreach { dom => + // javac throws exception if release is specified with source/target, and + // plugin configuration returns default values for source/target when not specified + val release = dom.child("release") + if (null == release) { + dom.child("source").foreachValue(options += "-source" += _) + dom.child("target").foreachValue(options += "-target" += _) + } else { + options += "--release" += release.getValue + } + dom.child("encoding").foreachValue(options += "-encoding" += _) + dom.child("compilerArgs").foreachChildValue(options += _) + } + + options.result() + } + } + + private implicit class NullableDomOps(val self: Xpp3Dom) extends AnyVal { + + def child(name: String): Xpp3Dom = + if (null == self) self else self.getChild(name) + + def foreachValue(f: String => Unit): Unit = + if (null != self) f(self.getValue) + + def foreachChildValue(f: String => Unit): Unit = + if (null != self) self.getChildren.iterator.foreach(dom => f(dom.getValue)) + } +} diff --git a/main/maven/src/mill/main/maven/Resolver.scala b/main/maven/src/mill/main/maven/Resolver.scala new file mode 100644 index 00000000000..0599c3c5918 --- /dev/null +++ b/main/maven/src/mill/main/maven/Resolver.scala @@ -0,0 +1,59 @@ +package mill.main.maven + +import org.apache.maven.model.building.{FileModelSource, ModelSource} +import org.apache.maven.model.resolution.{ModelResolver, UnresolvableModelException} +import org.apache.maven.model.{Dependency, Parent, Repository} +import org.apache.maven.repository.internal.ArtifactDescriptorUtils +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.resolution.{ArtifactRequest, ArtifactResolutionException} +import org.eclipse.aether.{RepositorySystem, RepositorySystemSession} + +import scala.jdk.CollectionConverters.* + +/** + * Resolves a POM from its coordinates. + * + * The implementation is inspired by [[https://github.com/sbt/sbt-pom-reader/ sbt-pom-reader]]. + */ +@mill.api.internal +class Resolver( + system: RepositorySystem, + session: RepositorySystemSession, + remotes: Seq[RemoteRepository], + context: String +) extends ModelResolver { + private[this] var repositories = remotes + + override def resolveModel(groupId: String, artifactId: String, version: String): ModelSource = { + val artifact = new DefaultArtifact(groupId, artifactId, "", "pom", version) + val request = new ArtifactRequest(artifact, repositories.asJava, context) + try { + val result = system.resolveArtifact(session, request) + new FileModelSource(result.getArtifact.getFile) + } catch { + case e: ArtifactResolutionException => + throw new UnresolvableModelException(e.getMessage, groupId, artifactId, version, e) + } + } + + override def resolveModel(parent: Parent): ModelSource = + resolveModel(parent.getGroupId, parent.getArtifactId, parent.getVersion) + + override def resolveModel(dependency: Dependency): ModelSource = + resolveModel(dependency.getGroupId, dependency.getArtifactId, dependency.getVersion) + + override def addRepository(repository: Repository): Unit = + addRepository(repository, replace = false) + + override def addRepository(repository: Repository, replace: Boolean): Unit = { + val exists = repositories.exists(_.getId == repository.getId) + if (!exists || replace) { + val remote = ArtifactDescriptorUtils.toRemoteRepository(repository) + repositories = repositories :+ remote + } + } + + override def newCopy(): ModelResolver = + new Resolver(system, session, repositories, context) +} diff --git a/main/maven/test/resources/.scalafmt.conf b/main/maven/test/resources/.scalafmt.conf new file mode 100644 index 00000000000..b3e25a743fe --- /dev/null +++ b/main/maven/test/resources/.scalafmt.conf @@ -0,0 +1,8 @@ +version = "3.8.4-RC1" +runner.dialect = scala213 +newlines.source=fold +newlines.topLevelStatementBlankLines = [ + { + blanks { before = 1 } + } +] diff --git a/main/maven/test/resources/config/deps-object/pom.xml b/main/maven/test/resources/config/deps-object/pom.xml new file mode 100644 index 00000000000..766e864e604 --- /dev/null +++ b/main/maven/test/resources/config/deps-object/pom.xml @@ -0,0 +1,226 @@ + + + 4.0.0 + + com.example.maven-samples + multi-module-parent + pom + 1.0-SNAPSHOT + Multi Module Project Parent + Sample multi module Maven project with a working, deployable site. + http://www.example.com + + + utf-8 + utf-8 + + + + server + webapp + + + + + site-server + Test Project Site + file:///tmp/multi-module-site + + + + + + + maven-compiler-plugin + + 1.6 + 1.6 + + + + + maven-release-plugin + + true + + + + + maven-site-plugin + + + + maven-checkstyle-plugin + + + + maven-jxr-plugin + + + + maven-javadoc-plugin + + + + maven-pmd-plugin + + + + maven-surefire-report-plugin + + + + org.codehaus.mojo + findbugs-maven-plugin + + + + org.codehaus.mojo + taglist-maven-plugin + + + + + + + + + + maven-checkstyle-plugin + 2.8 + + + + maven-compiler-plugin + 2.3.2 + + + + maven-javadoc-plugin + 2.8 + + + + maven-jxr-plugin + 2.3 + + + + maven-pmd-plugin + 2.6 + + + + maven-project-info-reports-plugin + 2.4 + + + + maven-release-plugin + 2.2.1 + + + + maven-resources-plugin + 2.5 + + + + maven-site-plugin + 3.0 + + + + maven-surefire-report-plugin + 2.11 + + + + maven-surefire-plugin + 2.11 + + + + org.codehaus.mojo + findbugs-maven-plugin + 2.3.3 + + + + org.codehaus.mojo + taglist-maven-plugin + 2.4 + + + + org.mortbay.jetty + jetty-maven-plugin + 8.0.0.M1 + + + + + + + + + + java.servlet + servlet-api + 2.5 + + + javax.servlet + servlet-api + 2.5 + + + + javax.servlet.jsp + jsp-api + 2.2 + + + + junit + junit-dep + 4.10 + test + + + + org.hamcrest + hamcrest-core + 1.2.1 + test + + + + org.hamcrest + hamcrest-library + 1.2.1 + test + + + + org.mockito + mockito-core + 1.8.5 + test + + + + + + scm:git:git@github.com:gabrielf/maven-samples.git + scm:git:git@github.com:gabrielf/maven-samples.git + HEAD + http://github.com/gabrielf/maven-samples + + + + 3.0.3 + + + diff --git a/main/maven/test/resources/config/deps-object/server/pom.xml b/main/maven/test/resources/config/deps-object/server/pom.xml new file mode 100644 index 00000000000..876c2af4fc8 --- /dev/null +++ b/main/maven/test/resources/config/deps-object/server/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + com.example.maven-samples + multi-module-parent + 1.0-SNAPSHOT + ../pom.xml + + + server + jar + Server + Logic. + + + ${project.artifactId} + + + + + junit + junit-dep + test + + + + org.hamcrest + hamcrest-core + test + + + + org.hamcrest + hamcrest-library + test + + + + org.mockito + mockito-core + test + + + + diff --git a/main/maven/test/resources/config/deps-object/webapp/pom.xml b/main/maven/test/resources/config/deps-object/webapp/pom.xml new file mode 100644 index 00000000000..b2c7b0044bb --- /dev/null +++ b/main/maven/test/resources/config/deps-object/webapp/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + + com.example.maven-samples + multi-module-parent + 1.0-SNAPSHOT + ../pom.xml + + + webapp + war + Webapp + Webapp. + + + ${project.artifactId} + + + + org.mortbay.jetty + jetty-maven-plugin + + + + + + + ${project.groupId} + server + ${project.version} + + + + + java.servlet + servlet-api + provided + + + javax.servlet + servlet-api + provided + + + + javax.servlet.jsp + jsp-api + provided + + + + diff --git a/main/maven/test/resources/expected/config/all/build.mill b/main/maven/test/resources/expected/config/all/build.mill new file mode 100644 index 00000000000..cde6beae10b --- /dev/null +++ b/main/maven/test/resources/expected/config/all/build.mill @@ -0,0 +1,106 @@ +package build + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object Deps { + + val `javax.servlet.jsp:jsp-api` = ivy"javax.servlet.jsp:jsp-api:2.2" + val `javax.servlet:servlet-api` = ivy"javax.servlet:servlet-api:2.5" + val `junit:junit-dep` = ivy"junit:junit-dep:4.10" + val `org.hamcrest:hamcrest-core` = ivy"org.hamcrest:hamcrest-core:1.2.1" + val `org.hamcrest:hamcrest-library` = ivy"org.hamcrest:hamcrest-library:1.2.1" + val `org.mockito:mockito-core` = ivy"org.mockito:mockito-core:1.8.5" +} + +object `package` extends RootModule with MyModule { + + override def artifactName = "parent" + + override def pomPackagingType = PackagingType.Pom + + object `multi-module` extends MyModule { + + override def artifactName = "multi-module-parent" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomPackagingType = PackagingType.Pom + + object server extends MyModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomParentProject = Some(Artifact( + "com.example.maven-samples", + "multi-module-parent", + "1.0-SNAPSHOT" + )) + + object tests extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + Deps.`junit:junit-dep`, + Deps.`org.hamcrest:hamcrest-core`, + Deps.`org.hamcrest:hamcrest-library`, + Deps.`org.mockito:mockito-core` + ) + } + } + + object webapp extends MyModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def moduleDeps = Seq(build.`multi-module`.server) + + override def compileIvyDeps = + Agg(Deps.`javax.servlet.jsp:jsp-api`, Deps.`javax.servlet:servlet-api`) + + override def pomPackagingType = "war" + + override def pomParentProject = Some(Artifact( + "com.example.maven-samples", + "multi-module-parent", + "1.0-SNAPSHOT" + )) + + } + } + + object `single-module` extends MyModule { + + override def artifactName = "single-module-project" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def ivyDeps = + Agg(Deps.`javax.servlet.jsp:jsp-api`, Deps.`javax.servlet:servlet-api`) + + object tests extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + Deps.`junit:junit-dep`, + Deps.`org.hamcrest:hamcrest-core`, + Deps.`org.hamcrest:hamcrest-library`, + Deps.`org.mockito:mockito-core` + ) + } + } +} + +trait MyModule extends PublishModule with MavenModule { + + override def pomSettings = PomSettings( + "Just a pom that makes it easy to build both projects at the same time.", + "com.example.maven-samples", + "", + Seq(), + VersionControl(), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/main/maven/test/resources/expected/config/base-module/build.mill b/main/maven/test/resources/expected/config/base-module/build.mill new file mode 100644 index 00000000000..422b9fae3f8 --- /dev/null +++ b/main/maven/test/resources/expected/config/base-module/build.mill @@ -0,0 +1,36 @@ +package build + +import $packages._ +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with MyModule { + + override def artifactName = "multi-module-parent" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomPackagingType = PackagingType.Pom + +} + +trait MyModule extends PublishModule with MavenModule { + + override def pomSettings = PomSettings( + "Sample multi module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/main/maven/test/resources/expected/config/base-module/server/package.mill b/main/maven/test/resources/expected/config/base-module/server/package.mill new file mode 100644 index 00000000000..6ec97af00e0 --- /dev/null +++ b/main/maven/test/resources/expected/config/base-module/server/package.mill @@ -0,0 +1,25 @@ +package build.server + +import $file.MyModule +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with MyModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) + + object test extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } +} diff --git a/main/maven/test/resources/expected/config/base-module/webapp/package.mill b/main/maven/test/resources/expected/config/base-module/webapp/package.mill new file mode 100644 index 00000000000..c1d990c0ed4 --- /dev/null +++ b/main/maven/test/resources/expected/config/base-module/webapp/package.mill @@ -0,0 +1,23 @@ +package build.webapp + +import $file.MyModule +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with MyModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def moduleDeps = Seq(build.server) + + override def compileIvyDeps = + Agg(ivy"javax.servlet.jsp:jsp-api:2.2", ivy"javax.servlet:servlet-api:2.5") + + override def pomPackagingType = "war" + + override def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) + +} diff --git a/main/maven/test/resources/expected/config/deps-object/build.mill b/main/maven/test/resources/expected/config/deps-object/build.mill new file mode 100644 index 00000000000..c47ff9ef4d0 --- /dev/null +++ b/main/maven/test/resources/expected/config/deps-object/build.mill @@ -0,0 +1,32 @@ +package build + +import $packages._ +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "multi-module-parent" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomPackagingType = PackagingType.Pom + + override def pomSettings = PomSettings( + "Sample multi module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/main/maven/test/resources/expected/config/deps-object/server/package.mill b/main/maven/test/resources/expected/config/deps-object/server/package.mill new file mode 100644 index 00000000000..3a6e003c149 --- /dev/null +++ b/main/maven/test/resources/expected/config/deps-object/server/package.mill @@ -0,0 +1,48 @@ +package build.server + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object Deps { + + val `junit:junit-dep` = ivy"junit:junit-dep:4.10" + val `org.hamcrest:hamcrest-core` = ivy"org.hamcrest:hamcrest-core:1.2.1" + val `org.hamcrest:hamcrest-library` = ivy"org.hamcrest:hamcrest-library:1.2.1" + val `org.mockito:mockito-core` = ivy"org.mockito:mockito-core:1.8.5" +} + +object `package` extends RootModule with PublishModule with MavenModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) + + override def pomSettings = PomSettings( + "Logic.", + "com.example.maven-samples", + "http://www.example.com/server", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples/server"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/server"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/server"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object test extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + Deps.`junit:junit-dep`, + Deps.`org.hamcrest:hamcrest-core`, + Deps.`org.hamcrest:hamcrest-library`, + Deps.`org.mockito:mockito-core` + ) + } +} diff --git a/main/maven/test/resources/expected/config/deps-object/webapp/package.mill b/main/maven/test/resources/expected/config/deps-object/webapp/package.mill new file mode 100644 index 00000000000..9f30469d148 --- /dev/null +++ b/main/maven/test/resources/expected/config/deps-object/webapp/package.mill @@ -0,0 +1,48 @@ +package build.webapp + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object Deps { + + val `java.servlet:servlet-api` = ivy"java.servlet:servlet-api:2.5" + val `javax.servlet.jsp:jsp-api` = ivy"javax.servlet.jsp:jsp-api:2.2" + val `javax.servlet:servlet-api` = ivy"javax.servlet:servlet-api:2.5" +} + +object `package` extends RootModule with PublishModule with MavenModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def moduleDeps = Seq(build.server) + + override def compileIvyDeps = Agg( + Deps.`java.servlet:servlet-api`, + Deps.`javax.servlet.jsp:jsp-api`, + Deps.`javax.servlet:servlet-api` + ) + + override def pomPackagingType = "war" + + override def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) + + override def pomSettings = PomSettings( + "Webapp.", + "com.example.maven-samples", + "http://www.example.com/webapp", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples/webapp"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/webapp"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/webapp"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/main/maven/test/resources/expected/config/merge/build.mill b/main/maven/test/resources/expected/config/merge/build.mill new file mode 100644 index 00000000000..287a175a92d --- /dev/null +++ b/main/maven/test/resources/expected/config/merge/build.mill @@ -0,0 +1,160 @@ +package build + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "parent" + + override def pomPackagingType = PackagingType.Pom + + override def pomSettings = PomSettings( + "Just a pom that makes it easy to build both projects at the same time.", + "com.example.maven-samples", + "", + Seq(), + VersionControl(), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object `multi-module` extends PublishModule with MavenModule { + + override def artifactName = "multi-module-parent" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomPackagingType = PackagingType.Pom + + override def pomSettings = PomSettings( + "Sample multi module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object server extends PublishModule with MavenModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomParentProject = Some(Artifact( + "com.example.maven-samples", + "multi-module-parent", + "1.0-SNAPSHOT" + )) + + override def pomSettings = PomSettings( + "Logic.", + "com.example.maven-samples", + "http://www.example.com/server", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples/server"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/server"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/server"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object test extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } + } + + object webapp extends PublishModule with MavenModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def moduleDeps = Seq(build.`multi-module`.server) + + override def compileIvyDeps = Agg( + ivy"javax.servlet.jsp:jsp-api:2.2", + ivy"javax.servlet:servlet-api:2.5" + ) + + override def pomPackagingType = "war" + + override def pomParentProject = Some(Artifact( + "com.example.maven-samples", + "multi-module-parent", + "1.0-SNAPSHOT" + )) + + override def pomSettings = PomSettings( + "Webapp.", + "com.example.maven-samples", + "http://www.example.com/webapp", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples/webapp"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/webapp"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/webapp"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + } + } + + object `single-module` extends PublishModule with MavenModule { + + override def artifactName = "single-module-project" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def ivyDeps = Agg( + ivy"javax.servlet.jsp:jsp-api:2.2", + ivy"javax.servlet:servlet-api:2.5" + ) + + override def pomSettings = PomSettings( + "Sample single module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object test extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } + } +} diff --git a/main/maven/test/resources/expected/config/publish-properties/build.mill b/main/maven/test/resources/expected/config/publish-properties/build.mill new file mode 100644 index 00000000000..7fe52ca2cc1 --- /dev/null +++ b/main/maven/test/resources/expected/config/publish-properties/build.mill @@ -0,0 +1,46 @@ +package build + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "single-module-project" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def ivyDeps = + Agg(ivy"javax.servlet.jsp:jsp-api:2.2", ivy"javax.servlet:servlet-api:2.5") + + override def pomSettings = PomSettings( + "Sample single module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + override def publishProperties = super.publishProperties() ++ Map( + ("project.build.sourceEncoding", "utf-8"), + ("project.reporting.outputEncoding", "utf-8") + ) + + object test extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } +} diff --git a/main/maven/test/resources/expected/config/test-module/build.mill b/main/maven/test/resources/expected/config/test-module/build.mill new file mode 100644 index 00000000000..28a3bae87d0 --- /dev/null +++ b/main/maven/test/resources/expected/config/test-module/build.mill @@ -0,0 +1,41 @@ +package build + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "single-module-project" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def ivyDeps = + Agg(ivy"javax.servlet.jsp:jsp-api:2.2", ivy"javax.servlet:servlet-api:2.5") + + override def pomSettings = PomSettings( + "Sample single module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object tests extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } +} diff --git a/main/maven/test/resources/expected/maven-samples/build.mill b/main/maven/test/resources/expected/maven-samples/build.mill new file mode 100644 index 00000000000..000a43b7ea8 --- /dev/null +++ b/main/maven/test/resources/expected/maven-samples/build.mill @@ -0,0 +1,25 @@ +package build + +import $packages._ +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "parent" + + override def pomPackagingType = PackagingType.Pom + + override def pomSettings = PomSettings( + "Just a pom that makes it easy to build both projects at the same time.", + "com.example.maven-samples", + "", + Seq(), + VersionControl(), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/main/maven/test/resources/expected/maven-samples/multi-module/package.mill b/main/maven/test/resources/expected/maven-samples/multi-module/package.mill new file mode 100644 index 00000000000..7568444b2af --- /dev/null +++ b/main/maven/test/resources/expected/maven-samples/multi-module/package.mill @@ -0,0 +1,31 @@ +package build.`multi-module` + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "multi-module-parent" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomPackagingType = PackagingType.Pom + + override def pomSettings = PomSettings( + "Sample multi module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/main/maven/test/resources/expected/maven-samples/multi-module/server/package.mill b/main/maven/test/resources/expected/maven-samples/multi-module/server/package.mill new file mode 100644 index 00000000000..c78c2759b8f --- /dev/null +++ b/main/maven/test/resources/expected/maven-samples/multi-module/server/package.mill @@ -0,0 +1,40 @@ +package build.`multi-module`.server + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) + + override def pomSettings = PomSettings( + "Logic.", + "com.example.maven-samples", + "http://www.example.com/server", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples/server"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/server"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/server"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object test extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } +} diff --git a/main/maven/test/resources/expected/maven-samples/multi-module/webapp/package.mill b/main/maven/test/resources/expected/maven-samples/multi-module/webapp/package.mill new file mode 100644 index 00000000000..465e676a906 --- /dev/null +++ b/main/maven/test/resources/expected/maven-samples/multi-module/webapp/package.mill @@ -0,0 +1,38 @@ +package build.`multi-module`.webapp + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def moduleDeps = Seq(build.`multi-module`.server) + + override def compileIvyDeps = + Agg(ivy"javax.servlet.jsp:jsp-api:2.2", ivy"javax.servlet:servlet-api:2.5") + + override def pomPackagingType = "war" + + override def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) + + override def pomSettings = PomSettings( + "Webapp.", + "com.example.maven-samples", + "http://www.example.com/webapp", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples/webapp"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/webapp"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git/webapp"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/main/maven/test/resources/expected/maven-samples/single-module/package.mill b/main/maven/test/resources/expected/maven-samples/single-module/package.mill new file mode 100644 index 00000000000..6fbfe5027cd --- /dev/null +++ b/main/maven/test/resources/expected/maven-samples/single-module/package.mill @@ -0,0 +1,41 @@ +package build.`single-module` + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "single-module-project" + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def ivyDeps = + Agg(ivy"javax.servlet.jsp:jsp-api:2.2", ivy"javax.servlet:servlet-api:2.5") + + override def pomSettings = PomSettings( + "Sample single module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object test extends MavenTests with TestModule.Junit4 { + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } +} diff --git a/main/maven/test/resources/expected/misc/custom-resources/build.mill b/main/maven/test/resources/expected/misc/custom-resources/build.mill new file mode 100644 index 00000000000..3164551b0fd --- /dev/null +++ b/main/maven/test/resources/expected/misc/custom-resources/build.mill @@ -0,0 +1,51 @@ +package build + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ + +object `package` extends RootModule with PublishModule with MavenModule { + + override def artifactName = "single-module-project" + + override def resources = Task + .Sources { super.resources() ++ Seq(PathRef(millSourcePath / "resources")) } + + override def javacOptions = Seq("-source", "1.6", "-target", "1.6") + + override def ivyDeps = + Agg(ivy"javax.servlet.jsp:jsp-api:2.2", ivy"javax.servlet:servlet-api:2.5") + + override def pomSettings = PomSettings( + "Sample single module Maven project with a working, deployable site.", + "com.example.maven-samples", + "http://www.example.com", + Seq(), + VersionControl( + Some("http://github.com/gabrielf/maven-samples"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("scm:git:git@github.com:gabrielf/maven-samples.git"), + Some("HEAD") + ), + Seq() + ) + + override def publishVersion = "1.0-SNAPSHOT" + + object test extends MavenTests with TestModule.Junit4 { + + override def resources = Task.Sources { + super.resources() ++ Seq( + PathRef(millSourcePath / "resources"), + PathRef(millSourcePath / "../src/test/secrets") + ) + } + + override def ivyDeps = super.ivyDeps() ++ Agg( + ivy"junit:junit-dep:4.10", + ivy"org.hamcrest:hamcrest-core:1.2.1", + ivy"org.hamcrest:hamcrest-library:1.2.1", + ivy"org.mockito:mockito-core:1.8.5" + ) + } +} diff --git a/main/maven/test/resources/maven-samples/multi-module/pom.xml b/main/maven/test/resources/maven-samples/multi-module/pom.xml new file mode 100644 index 00000000000..8a4a03ce8a4 --- /dev/null +++ b/main/maven/test/resources/maven-samples/multi-module/pom.xml @@ -0,0 +1,220 @@ + + + 4.0.0 + + com.example.maven-samples + multi-module-parent + pom + 1.0-SNAPSHOT + Multi Module Project Parent + Sample multi module Maven project with a working, deployable site. + http://www.example.com + + + utf-8 + utf-8 + + + + server + webapp + + + + + site-server + Test Project Site + file:///tmp/multi-module-site + + + + + + + maven-compiler-plugin + + 1.6 + 1.6 + + + + + maven-release-plugin + + true + + + + + maven-site-plugin + + + + maven-checkstyle-plugin + + + + maven-jxr-plugin + + + + maven-javadoc-plugin + + + + maven-pmd-plugin + + + + maven-surefire-report-plugin + + + + org.codehaus.mojo + findbugs-maven-plugin + + + + org.codehaus.mojo + taglist-maven-plugin + + + + + + + + + + maven-checkstyle-plugin + 2.8 + + + + maven-compiler-plugin + 2.3.2 + + + + maven-javadoc-plugin + 2.8 + + + + maven-jxr-plugin + 2.3 + + + + maven-pmd-plugin + 2.6 + + + + maven-project-info-reports-plugin + 2.4 + + + + maven-release-plugin + 2.2.1 + + + + maven-resources-plugin + 2.5 + + + + maven-site-plugin + 3.0 + + + + maven-surefire-report-plugin + 2.11 + + + + maven-surefire-plugin + 2.11 + + + + org.codehaus.mojo + findbugs-maven-plugin + 2.3.3 + + + + org.codehaus.mojo + taglist-maven-plugin + 2.4 + + + + org.mortbay.jetty + jetty-maven-plugin + 8.0.0.M1 + + + + + + + + + javax.servlet + servlet-api + 2.5 + + + + javax.servlet.jsp + jsp-api + 2.2 + + + + junit + junit-dep + 4.10 + test + + + + org.hamcrest + hamcrest-core + 1.2.1 + test + + + + org.hamcrest + hamcrest-library + 1.2.1 + test + + + + org.mockito + mockito-core + 1.8.5 + test + + + + + + scm:git:git@github.com:gabrielf/maven-samples.git + scm:git:git@github.com:gabrielf/maven-samples.git + HEAD + http://github.com/gabrielf/maven-samples + + + + 3.0.3 + + + diff --git a/main/maven/test/resources/maven-samples/multi-module/server/pom.xml b/main/maven/test/resources/maven-samples/multi-module/server/pom.xml new file mode 100644 index 00000000000..876c2af4fc8 --- /dev/null +++ b/main/maven/test/resources/maven-samples/multi-module/server/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + com.example.maven-samples + multi-module-parent + 1.0-SNAPSHOT + ../pom.xml + + + server + jar + Server + Logic. + + + ${project.artifactId} + + + + + junit + junit-dep + test + + + + org.hamcrest + hamcrest-core + test + + + + org.hamcrest + hamcrest-library + test + + + + org.mockito + mockito-core + test + + + + diff --git a/main/maven/test/resources/maven-samples/multi-module/webapp/pom.xml b/main/maven/test/resources/maven-samples/multi-module/webapp/pom.xml new file mode 100644 index 00000000000..6d3dacbf4c3 --- /dev/null +++ b/main/maven/test/resources/maven-samples/multi-module/webapp/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + com.example.maven-samples + multi-module-parent + 1.0-SNAPSHOT + ../pom.xml + + + webapp + war + Webapp + Webapp. + + + ${project.artifactId} + + + + org.mortbay.jetty + jetty-maven-plugin + + + + + + + ${project.groupId} + server + ${project.version} + + + + javax.servlet + servlet-api + provided + + + + javax.servlet.jsp + jsp-api + provided + + + + diff --git a/main/maven/test/resources/maven-samples/pom.xml b/main/maven/test/resources/maven-samples/pom.xml new file mode 100644 index 00000000000..7673b6337d2 --- /dev/null +++ b/main/maven/test/resources/maven-samples/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + com.example.maven-samples + parent + pom + 1.0-SNAPSHOT + Parent + Just a pom that makes it easy to build both projects at the same time. + + + multi-module + single-module + + + + 3.0.3 + + + diff --git a/main/maven/test/resources/maven-samples/single-module/pom.xml b/main/maven/test/resources/maven-samples/single-module/pom.xml new file mode 100644 index 00000000000..13e84d6030d --- /dev/null +++ b/main/maven/test/resources/maven-samples/single-module/pom.xml @@ -0,0 +1,173 @@ + + + 4.0.0 + + com.example.maven-samples + single-module-project + jar + 1.0-SNAPSHOT + A Single Maven Module + Sample single module Maven project with a working, deployable site. + http://www.example.com + + + utf-8 + utf-8 + + + + + site-server + Test Project Site + file:///tmp/single-module-site + + + + + ${project.artifactId} + + + + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + + + + + maven-release-plugin + 2.2.1 + + + + maven-resources-plugin + 2.5 + + + + maven-site-plugin + 3.0 + + + + maven-checkstyle-plugin + 2.8 + + + + maven-javadoc-plugin + 2.8 + + + + maven-jxr-plugin + 2.3 + + + + maven-pmd-plugin + 2.6 + + + + maven-project-info-reports-plugin + 2.4 + + + + maven-surefire-report-plugin + 2.11 + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.5.1 + + + + org.codehaus.mojo + findbugs-maven-plugin + 2.3.3 + + + + org.codehaus.mojo + taglist-maven-plugin + 2.4 + + + + + + + maven-surefire-plugin + 2.11 + + + + org.mortbay.jetty + jetty-maven-plugin + 8.0.0.M1 + + + + + + + + javax.servlet + servlet-api + 2.5 + + + + javax.servlet.jsp + jsp-api + 2.2 + + + + junit + junit-dep + 4.10 + test + + + + org.hamcrest + hamcrest-core + 1.2.1 + test + + + + org.hamcrest + hamcrest-library + 1.2.1 + test + + + + org.mockito + mockito-core + 1.8.5 + test + + + + + scm:git:git@github.com:gabrielf/maven-samples.git + scm:git:git@github.com:gabrielf/maven-samples.git + HEAD + http://github.com/gabrielf/maven-samples + + + + 3.0.3 + + + diff --git a/main/maven/test/resources/misc/custom-resources/pom.xml b/main/maven/test/resources/misc/custom-resources/pom.xml new file mode 100644 index 00000000000..517a3b136e5 --- /dev/null +++ b/main/maven/test/resources/misc/custom-resources/pom.xml @@ -0,0 +1,187 @@ + + + 4.0.0 + + com.example.maven-samples + single-module-project + jar + 1.0-SNAPSHOT + A Single Maven Module + Sample single module Maven project with a working, deployable site. + http://www.example.com + + + utf-8 + utf-8 + + + + + site-server + Test Project Site + file:///tmp/single-module-site + + + + + + + resources + + + + + test/resources + + + src/test/secrets + + + + ${project.artifactId} + + + + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + + + + + maven-release-plugin + 2.2.1 + + + + maven-resources-plugin + 2.5 + + + + maven-site-plugin + 3.0 + + + + maven-checkstyle-plugin + 2.8 + + + + maven-javadoc-plugin + 2.8 + + + + maven-jxr-plugin + 2.3 + + + + maven-pmd-plugin + 2.6 + + + + maven-project-info-reports-plugin + 2.4 + + + + maven-surefire-report-plugin + 2.11 + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.5.1 + + + + org.codehaus.mojo + findbugs-maven-plugin + 2.3.3 + + + + org.codehaus.mojo + taglist-maven-plugin + 2.4 + + + + + + + maven-surefire-plugin + 2.11 + + + + org.mortbay.jetty + jetty-maven-plugin + 8.0.0.M1 + + + + + + + + javax.servlet + servlet-api + 2.5 + + + + javax.servlet.jsp + jsp-api + 2.2 + + + + junit + junit-dep + 4.10 + test + + + + org.hamcrest + hamcrest-core + 1.2.1 + test + + + + org.hamcrest + hamcrest-library + 1.2.1 + test + + + + org.mockito + mockito-core + 1.8.5 + test + + + + + scm:git:git@github.com:gabrielf/maven-samples.git + scm:git:git@github.com:gabrielf/maven-samples.git + HEAD + http://github.com/gabrielf/maven-samples + + + + 3.0.3 + + + diff --git a/main/maven/test/src/mill/main/build/TreeTests.scala b/main/maven/test/src/mill/main/build/TreeTests.scala new file mode 100644 index 00000000000..9efd25f146e --- /dev/null +++ b/main/maven/test/src/mill/main/build/TreeTests.scala @@ -0,0 +1,43 @@ +package mill.main.build + +import utest.* + +object TreeTests extends TestSuite { + + def tests: Tests = Tests { + + val tree: Tree[Int] = + Tree( + 50, + Seq( + Tree( + 25, + Seq( + Tree(5) + ) + ), + Tree( + 10, + Seq( + Tree(5), + Tree(2) + ) + ), + Tree(5), + Tree(2) + ) + ) + + test("BreadthFirst") { + implicit val traversal: Tree.Traversal = Tree.Traversal.BreadthFirst + + assert(tree.toSeq == Seq(50, 25, 10, 5, 2, 5, 5, 2)) + } + + test("DepthFirst") { + implicit val traversal: Tree.Traversal = Tree.Traversal.DepthFirst + + assert(tree.toSeq == Seq(5, 25, 5, 2, 10, 5, 2, 50)) + } + } +} diff --git a/main/maven/test/src/mill/main/maven/BuildGenTests.scala b/main/maven/test/src/mill/main/maven/BuildGenTests.scala new file mode 100644 index 00000000000..5f0ebac94e6 --- /dev/null +++ b/main/maven/test/src/mill/main/maven/BuildGenTests.scala @@ -0,0 +1,139 @@ +package mill.main.maven + +import mill.T +import mill.api.PathRef +import mill.scalalib.scalafmt.ScalafmtModule +import mill.testkit.{TestBaseModule, UnitTester} +import utest.* +import utest.framework.TestPath + +object BuildGenTests extends TestSuite { + + def tests: Tests = Tests { + val resources = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) + val scalafmtConfigFile = PathRef(resources / ".scalafmt.conf") + + def checkBuild(sourceRel: os.SubPath, expectedRel: os.SubPath, args: String*)(implicit + tp: TestPath + ): Boolean = { + // prep + val dest = os.pwd / tp.value + os.copy.over(resources / sourceRel, dest, createFolders = true, replaceExisting = true) + + // gen + os.dynamicPwd.withValue(dest)(BuildGen.main(args.toArray)) + + // fmt + val files = buildFiles(dest) + object module extends TestBaseModule with ScalafmtModule { + override protected def filesToFormat(sources: Seq[PathRef]): Seq[PathRef] = files + override def scalafmtConfig: T[Seq[PathRef]] = Seq(scalafmtConfigFile) + } + val eval = UnitTester(module, dest) + eval(module.reformat()) + + // test + checkFiles(files, resources / expectedRel) + } + + // multi level nested modules + test("maven-samples") { + val sourceRoot = os.sub / "maven-samples" + val expectedRoot = os.sub / "expected/maven-samples" + assert( + checkBuild(sourceRoot, expectedRoot) + ) + } + + test("config") { + test("all") { + val sourceRoot = os.sub / "maven-samples" + val expectedRoot = os.sub / "expected/config/all" + assert( + checkBuild( + sourceRoot, + expectedRoot, + "--base-module", + "MyModule", + "--test-module", + "tests", + "--deps-object", + "Deps", + "--publish-properties", + "--merge", + "--cache-repository", + "--process-plugins" + ) + ) + } + + test("base-module") { + val sourceRoot = os.sub / "maven-samples/multi-module" + val expectedRoot = os.sub / "expected/config/base-module" + assert( + checkBuild(sourceRoot, expectedRoot, "--base-module", "MyModule") + ) + } + + test("deps-object") { + val sourceRoot = os.sub / "config/deps-object" + val expectedRoot = os.sub / "expected/config/deps-object" + assert( + checkBuild(sourceRoot, expectedRoot, "--deps-object", "Deps") + ) + } + + test("test-module") { + val sourceRoot = os.sub / "maven-samples/single-module" + val expectedRoot = os.sub / "expected/config/test-module" + assert( + checkBuild(sourceRoot, expectedRoot, "--test-module", "tests") + ) + } + + test("merge") { + val sourceRoot = os.sub / "maven-samples" + val expectedRoot = os.sub / "expected/config/merge" + assert( + checkBuild(sourceRoot, expectedRoot, "--merge") + ) + } + + test("publish-properties") { + val sourceRoot = os.sub / "maven-samples/single-module" + val expectedRoot = os.sub / "expected/config/publish-properties" + assert( + checkBuild(sourceRoot, expectedRoot, "--publish-properties") + ) + } + } + + test("misc") { + test("custom-resources") { + val sourceRoot = os.sub / "misc/custom-resources" + val expectedRoot = os.sub / "expected/misc/custom-resources" + assert( + checkBuild(sourceRoot, expectedRoot) + ) + } + } + } + + def buildFiles(root: os.Path): Seq[PathRef] = + os.walk.stream(root, skip = (root / "out").equals) + .filter(_.ext == "mill") + .map(PathRef(_)) + .toSeq + + def checkFiles(actualFiles: Seq[PathRef], expectedRoot: os.Path): Boolean = { + val expectedFiles = buildFiles(expectedRoot) + + actualFiles.nonEmpty && + actualFiles.length == expectedFiles.length && + actualFiles.iterator.zip(expectedFiles.iterator).forall { + case (actual, expected) => + actual.path.endsWith(expected.path.relativeTo(expectedRoot)) && + os.read.lines(actual.path) == os.read.lines(expected.path) + } + } +} diff --git a/main/package.mill b/main/package.mill index 113dafbb60d..d1db4eb4114 100644 --- a/main/package.mill +++ b/main/package.mill @@ -149,5 +149,18 @@ object `package` extends RootModule with build.MillStableScalaModule with BuildI def ivyDeps = Agg(build.Deps.jgraphtCore) ++ build.Deps.graphvizJava ++ build.Deps.javet } + object maven extends build.MillPublishScalaModule { + def moduleDeps = Seq(build.runner) + def ivyDeps = Agg( + build.Deps.mavenEmbedder, + build.Deps.mavenResolverConnectorBasic, + build.Deps.mavenResolverSupplier, + build.Deps.mavenResolverTransportFile, + build.Deps.mavenResolverTransportHttp, + build.Deps.mavenResolverTransportWagon + ) + def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib) + } + def testModuleDeps = super.testModuleDeps ++ Seq(build.testkit) } diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index 84556a3cb18..34c1ec3e15a 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -563,7 +563,13 @@ trait MainModule extends BaseModule0 { def init(evaluator: Evaluator, args: String*): Command[ujson.Value] = Task.Command(exclusive = true) { val evaluated = - if (args.headOption.exists(_.toLowerCase.endsWith(".g8"))) + if (os.exists(os.pwd / "pom.xml")) + RunScript.evaluateTasksNamed( + evaluator, + Seq("mill.init.InitMavenModule/init") ++ args, + SelectMode.Separated + ) + else if (args.headOption.exists(_.toLowerCase.endsWith(".g8"))) RunScript.evaluateTasksNamed( evaluator, Seq("mill.scalalib.giter8.Giter8Module/init") ++ args, diff --git a/scalalib/src/mill/scalalib/PublishModule.scala b/scalalib/src/mill/scalalib/PublishModule.scala index eb3a7a22f01..796c3ff10f9 100644 --- a/scalalib/src/mill/scalalib/PublishModule.scala +++ b/scalalib/src/mill/scalalib/PublishModule.scala @@ -31,6 +31,13 @@ trait PublishModule extends JavaModule { outer => */ def pomPackagingType: String = PackagingType.Jar + /** + * POM parent project. + * + * @see [[https://maven.apache.org/guides/introduction/introduction-to-the-pom.html#Project_Inheritance Project Inheritance]] + */ + def pomParentProject: T[Option[Artifact]] = None + /** * Configuration for the `pom.xml` metadata file published with this module */ @@ -89,7 +96,8 @@ trait PublishModule extends JavaModule { outer => artifactId(), pomSettings(), publishProperties(), - packagingType = pomPackagingType + packagingType = pomPackagingType, + parentProject = pomParentProject() ) val pomPath = T.dest / s"${artifactId()}-${publishVersion()}.pom" os.write.over(pomPath, pom) diff --git a/scalalib/src/mill/scalalib/publish/Pom.scala b/scalalib/src/mill/scalalib/publish/Pom.scala index ab39a88b81f..76d1e728d36 100644 --- a/scalalib/src/mill/scalalib/publish/Pom.scala +++ b/scalalib/src/mill/scalalib/publish/Pom.scala @@ -38,9 +38,11 @@ object Pom { name = name, pomSettings = pomSettings, properties = properties, - packagingType = pomSettings.packaging + packagingType = pomSettings.packaging, + parentProject = None ) + @deprecated("Use overload with parentProject parameter instead", "Mill 0.12.1") def apply( artifact: Artifact, dependencies: Agg[Dependency], @@ -48,6 +50,24 @@ object Pom { pomSettings: PomSettings, properties: Map[String, String], packagingType: String + ): String = apply( + artifact = artifact, + dependencies = dependencies, + name = name, + pomSettings = pomSettings, + properties = properties, + packagingType = packagingType, + parentProject = None + ) + + def apply( + artifact: Artifact, + dependencies: Agg[Dependency], + name: String, + pomSettings: PomSettings, + properties: Map[String, String], + packagingType: String, + parentProject: Option[Artifact] ): String = { val xml = 4.0.0 + {parentProject.fold(NodeSeq.Empty)(renderParent)} {name} {artifact.group} {artifact.id} @@ -92,6 +113,14 @@ object Pom { head + pp.format(xml) } + private def renderParent(artifact: Artifact): Elem = { + + {artifact.group} + {artifact.id} + {artifact.version} + + } + private def renderLicense(l: License): Elem = { {l.name} diff --git a/testkit/src/mill/testkit/UtestExampleTestSuite.scala b/testkit/src/mill/testkit/UtestExampleTestSuite.scala index 0a6ccb1b81a..bafe1bb5f51 100644 --- a/testkit/src/mill/testkit/UtestExampleTestSuite.scala +++ b/testkit/src/mill/testkit/UtestExampleTestSuite.scala @@ -14,7 +14,7 @@ object UtestExampleTestSuite extends TestSuite { test("exampleTest") { Retry( count = if (sys.env.contains("CI")) 1 else 0, - timeoutMillis = 10.minutes.toMillis + timeoutMillis = 15.minutes.toMillis ) { ExampleTester.run( clientServerMode,