diff --git a/example/migrating/javalib/3-maven-complete-large/build.mill b/example/migrating/javalib/3-maven-complete-large/build.mill index a0e587772977..bcc744d871a2 100644 --- a/example/migrating/javalib/3-maven-complete-large/build.mill +++ b/example/migrating/javalib/3-maven-complete-large/build.mill @@ -6,7 +6,9 @@ > git remote add -f origin https://github.com/iluwatar/java-design-patterns > git checkout ede37bd05568b1b8b814d8e9a1d2bbd71d9d615d -> ./mill init --jvm-id 21 # Repo needs Java >=21 to build and test +> echo 21 > .mill-jvm-version # Repo needs Java >=21 to build and test + +> ./mill init > rm twin/src/test/java/com/iluwatar/twin/BallThreadTest.java # skip flaky test > rm actor-model/src/test/java/com/iluwatar/actor/ActorModelTest.java # skip flaky test diff --git a/example/migrating/javalib/4-gradle-complete/build.mill b/example/migrating/javalib/4-gradle-complete/build.mill index 274f9e08f323..ed323b640b61 100644 --- a/example/migrating/javalib/4-gradle-complete/build.mill +++ b/example/migrating/javalib/4-gradle-complete/build.mill @@ -6,7 +6,9 @@ > git remote add -f origin https://github.com/komamitsu/fluency.git > git checkout 2.7.3 # multi-module Java project that requires Java 16+ -> ./mill init --base-module FluencyModule --jvm-id 16 +> echo "16" > .mill-jvm-version + +> ./mill init converting Gradle build ... init completed, run "mill resolve _" to list available tasks diff --git a/example/migrating/javalib/5-gradle-incomplete/build.mill b/example/migrating/javalib/5-gradle-incomplete/build.mill index 764e6d440be0..0793c9651631 100644 --- a/example/migrating/javalib/5-gradle-incomplete/build.mill +++ b/example/migrating/javalib/5-gradle-incomplete/build.mill @@ -4,13 +4,14 @@ > git init . > git remote add -f origin https://github.com/mockito/mockito.git -> git checkout v5.15.2 # multi-module Java project that requires Java 17+ +> git checkout v5.19.0 # multi-module Java project that requires Java 11 -> ./mill init --base-module MockitoModule --jvm-id 17 # init ignores custom dependency configurations +> echo "17" > .mill-jvm-version # Gradle version in use requires Java 17 + +> ./mill init # imported modules are not fully functional converting Gradle build -ignoring errorprone dependency (com.google.errorprone,error_prone_core,2.23.0) init completed, run "mill resolve _" to list available tasks -> ./mill __.compile # compilation error can be by fixed by using the Mill plugin for ErrorProne -error: plug-in not found: ErrorProne +> ./mill mockito-core.compile # requires manual tweaking to support Java modules +error: ...module not found: net.bytebuddy */ diff --git a/example/migrating/scalalib/2-sbt-incomplete/build.mill b/example/migrating/scalalib/2-sbt-incomplete/build.mill index 65cb01480bdd..57f86f54fdaf 100644 --- a/example/migrating/scalalib/2-sbt-incomplete/build.mill +++ b/example/migrating/scalalib/2-sbt-incomplete/build.mill @@ -3,17 +3,15 @@ > rm build.mill # remove any existing build file > git init . -> git remote add -f origin https://github.com/tototoshi/scala-csv.git -> git checkout 2.0.0 +> git remote add -f origin https://github.com/fthomas/refined.git +> git checkout v0.11.3 > ./mill init converting sbt build ... init completed, run "mill resolve _" to list available tasks -> ./mill compile # You will have to further configure the `CrossScalaModule` for different Scala versions -compiling 7 Scala sources and 3 Java sources to ... -error: class CSVReader protected (private val lineReader: LineReader)(implicit format: CSVFormat) extends Closeable with CSVReaderCompat { -error: ^ -error: one error found +> ./mill modules.core.jvm[2.12.20].compile # custom version range source roots 3.0+/3.0- not supported +compiling 22 Scala sources to ... +error: ...object boolean is not a member of package eu.timepit.refined */ diff --git a/integration/manual/migrating/src/MillInitGradleAsmTests.scala b/integration/manual/migrating/src/MillInitGradleAsmTests.scala new file mode 100644 index 000000000000..8d0ac095c271 --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleAsmTests.scala @@ -0,0 +1,34 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleAsmTests extends GitRepoIntegrationTestSuite { + + // gradle 8.3 + def gitRepoUrl = "https://gitlab.ow2.org/asm/asm.git" + def gitRepoBranch = "ASM_9_8" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + os.write(workspacePath / ".mill-jvm-version", "11") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("asm.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("asm.publishLocal", stdout = os.Inherit, stderr = os.Inherit) + + // modules are tested with asm-test module? + eval("asm.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + + // custom sourceSets not supported + eval( + "tools.retrofitter.compile", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradleEhcache3Tests.scala b/integration/manual/migrating/src/MillInitGradleEhcache3Tests.scala new file mode 100644 index 000000000000..d55af4145949 --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleEhcache3Tests.scala @@ -0,0 +1,35 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleEhcache3Tests extends GitRepoIntegrationTestSuite { + + // gradle 7.2 + // custom dependency configurations + // dependencies with version constraints + // custom layout + // custom repository + // bom dependencies + // modules with pom packaging + // Junit4 + def gitRepoUrl = "https://github.com/ehcache/ehcache3.git" + def gitRepoBranch = "v3.10.8" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + // https://docs.gradle.org/current/userguide/compatibility.html#java_runtime + os.write(workspacePath / ".mill-jvm-version", "11") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("ehcache-api.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("ehcache-api.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // custom dependency configurations not supported + eval("ehcache-xml.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradleFastCsvTests.scala b/integration/manual/migrating/src/MillInitGradleFastCsvTests.scala new file mode 100644 index 000000000000..7cd0153bc247 --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleFastCsvTests.scala @@ -0,0 +1,33 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleFastCsvTests extends GitRepoIntegrationTestSuite { + + // gradle 9.0.0-rc-1 + // Junit5 + // uses ErrorProne + def gitRepoUrl = "https://github.com/osiegmar/FastCSV.git" + def gitRepoBranch = "v4.0.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + os.write(workspacePath / ".mill-jvm-version", "17") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("lib.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("lib.publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // requires support for Java modules + eval("example.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + + // junit-platform-launcher version is set implicitly (tasks.test.useJunitPlatform()) + // https://docs.gradle.org/8.14.3/userguide/upgrading_version_8.html#test_framework_implementation_dependencies + eval("lib.test.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradleJCommanderTests.scala b/integration/manual/migrating/src/MillInitGradleJCommanderTests.scala new file mode 100644 index 000000000000..5baea41cf0c6 --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleJCommanderTests.scala @@ -0,0 +1,29 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleJCommanderTests extends GitRepoIntegrationTestSuite { + + // gradle 8.9 + // single module + // testng 7.0.0 + def gitRepoUrl = "https://github.com/cbeust/jcommander.git" + def gitRepoBranch = "2.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + os.write(workspacePath / ".mill-jvm-version", "11") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // annotations defined in main-module cannot be used in test-module? + eval("test.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradleMicroconfigTests.scala b/integration/manual/migrating/src/MillInitGradleMicroconfigTests.scala new file mode 100644 index 000000000000..35d6126375b0 --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleMicroconfigTests.scala @@ -0,0 +1,25 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleMicroconfigTests extends GitRepoIntegrationTestSuite { + + // gradle 8.10.1 + // uses spring-boot-dependencies BOM + // Junit5 + def gitRepoUrl = "https://github.com/microconfig/microconfig.git" + def gitRepoBranch = "v4.9.5" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval(("init", "-u"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradleMockitoTests.scala b/integration/manual/migrating/src/MillInitGradleMockitoTests.scala new file mode 100644 index 000000000000..6be66939130a --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleMockitoTests.scala @@ -0,0 +1,27 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleMockitoTests extends GitRepoIntegrationTestSuite { + + // gradle 8.14.2 + // contains BOM module + def gitRepoUrl = "https://github.com/mockito/mockito.git" + def gitRepoBranch = "v5.19.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + // project requires Java 11 but Gradle version in use requires Java 17 + os.write(workspacePath / ".mill-jvm-version", "17") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // requires javacOptions for Java modules + eval("mockito-core.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradlePCollectionsTests.scala b/integration/manual/migrating/src/MillInitGradlePCollectionsTests.scala new file mode 100644 index 000000000000..41492909dee1 --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradlePCollectionsTests.scala @@ -0,0 +1,25 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradlePCollectionsTests extends GitRepoIntegrationTestSuite { + + // gradle 8.14.3 + // single module + // Junit5 + def gitRepoUrl = "https://github.com/hrldcpr/pcollections.git" + def gitRepoBranch = "v5.0.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // https://github.com/com-lihaoyi/mill/issues/5782 + eval("compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradleSpotbugsTests.scala b/integration/manual/migrating/src/MillInitGradleSpotbugsTests.scala new file mode 100644 index 000000000000..0ad6e0b0dd24 --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleSpotbugsTests.scala @@ -0,0 +1,37 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleSpotbugsTests extends GitRepoIntegrationTestSuite { + + // gradle 9.0.0 + // custom dependency configurations + // dependencies with version constraints + // custom layout + // Junit5 + def gitRepoUrl = "https://github.com/spotbugs/spotbugs.git" + def gitRepoBranch = "4.9.4" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval( + "spotbugs-annotations.compile", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> true + eval( + "spotbugs-annotations.publishLocal", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> true + + // missing sources from custom layout + eval("spotbugs-tests.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitGradleSpringFrameworkTests.scala b/integration/manual/migrating/src/MillInitGradleSpringFrameworkTests.scala new file mode 100644 index 000000000000..09c8974f392e --- /dev/null +++ b/integration/manual/migrating/src/MillInitGradleSpringFrameworkTests.scala @@ -0,0 +1,29 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitGradleSpringFrameworkTests extends GitRepoIntegrationTestSuite { + + // gradle 8.14.3 + // custom repository + // dependencies with version constraints + // BOM modules + // uses errorprone + def gitRepoUrl = "https://github.com/spring-projects/spring-framework.git" + def gitRepoBranch = "v6.2.10" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + os.write(workspacePath / ".mill-jvm-version", "17") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // requires support for dependency version constraints + eval("spring-jcl.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenAntlr4Tests.scala b/integration/manual/migrating/src/MillInitMavenAntlr4Tests.scala new file mode 100644 index 000000000000..7f7658e76021 --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenAntlr4Tests.scala @@ -0,0 +1,22 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenAntlr4Tests extends GitRepoIntegrationTestSuite { + + def gitRepoUrl = "https://github.com/antlr/antlr4.git" + def gitRepoBranch = "v4.11.1" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // custom layout not supported + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenByteBuddyTests.scala b/integration/manual/migrating/src/MillInitMavenByteBuddyTests.scala new file mode 100644 index 000000000000..6ac7279425e5 --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenByteBuddyTests.scala @@ -0,0 +1,36 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenByteBuddyTests extends GitRepoIntegrationTestSuite { + + // maven 3.9.9 + // contains Android module + // contains C sources for JNA + // Junit4 + def gitRepoUrl = "https://github.com/raphw/byte-buddy.git" + def gitRepoBranch = "byte-buddy-1.17.7" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("byte-buddy-agent.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval( + "byte-buddy-agent.publishLocal", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> true + + // requires native compilation support + eval( + "byte-buddy-agent.test.compile", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenCheckstyleTests.scala b/integration/manual/migrating/src/MillInitMavenCheckstyleTests.scala new file mode 100644 index 000000000000..a495712d638d --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenCheckstyleTests.scala @@ -0,0 +1,29 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenCheckstyleTests extends GitRepoIntegrationTestSuite { + + // maven 3.9.6 + // .mvn/jvm.config + // errorprone + // Junit5 + // single module + def gitRepoUrl = "https://github.com/checkstyle/checkstyle.git" + def gitRepoBranch = "checkstyle-11.0.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + os.write(workspacePath / ".mill-jvm-version", "17") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // missing generated sources + eval("compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenErrorProneTests.scala b/integration/manual/migrating/src/MillInitMavenErrorProneTests.scala new file mode 100644 index 000000000000..bd72e7ae662a --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenErrorProneTests.scala @@ -0,0 +1,24 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenErrorProneTests extends GitRepoIntegrationTestSuite { + + def gitRepoUrl = "https://github.com/google/error-prone.git" + def gitRepoBranch = "v2.41.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + os.write(workspacePath / ".mill-jvm-version", "17") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // requires support for javac annotation processors + eval("core.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenJansiTests.scala b/integration/manual/migrating/src/MillInitMavenJansiTests.scala new file mode 100644 index 000000000000..0411251de04c --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenJansiTests.scala @@ -0,0 +1,23 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenJansiTests extends GitRepoIntegrationTestSuite { + + // single module + def gitRepoUrl = "https://github.com/fusesource/jansi.git" + def gitRepoBranch = "jansi-2.4.2" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenJodaBeansTests.scala b/integration/manual/migrating/src/MillInitMavenJodaBeansTests.scala new file mode 100644 index 000000000000..a7a2c5c014ee --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenJodaBeansTests.scala @@ -0,0 +1,25 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenJodaBeansTests extends GitRepoIntegrationTestSuite { + + // single module + def gitRepoUrl = "https://github.com/JodaOrg/joda-beans.git" + def gitRepoBranch = "v2.11.1" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // malformed HTML + eval("publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenNettyTests.scala b/integration/manual/migrating/src/MillInitMavenNettyTests.scala new file mode 100644 index 000000000000..f512045c9dd1 --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenNettyTests.scala @@ -0,0 +1,31 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenNettyTests extends GitRepoIntegrationTestSuite { + + // maven 3.9.10 + // dynamic classifiers/properties + def gitRepoUrl = "https://github.com/netty/netty.git" + def gitRepoBranch = "netty-4.2.4.Final" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval( + ("init", "--publish-properties"), + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("common.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // The project defines dependencies for junit-jupiter-* but junit-platform-launcher is + // auto-added by Maven. In Mill, a legacy version of junit-platform-launcher gets added, as a + // transitive dependency of com.github.sbt.junit:junit-interface, resulting in a conflict. + eval("common.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitMavenSpringAiTests.scala b/integration/manual/migrating/src/MillInitMavenSpringAiTests.scala new file mode 100644 index 000000000000..7d89ed88a840 --- /dev/null +++ b/integration/manual/migrating/src/MillInitMavenSpringAiTests.scala @@ -0,0 +1,41 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitMavenSpringAiTests extends GitRepoIntegrationTestSuite { + + // maven 3.8.6 + // custom repositories + // contains BOM module + // transitive test framework dependency + def gitRepoUrl = "https://github.com/spring-projects/spring-ai.git" + def gitRepoBranch = "v1.0.1" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + os.write(workspacePath / ".mill-jvm-version", "17") + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval( + "spring-ai-model.publishLocal", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> true + + // malformed HTML + eval( + "spring-ai-commons.publishLocal", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> false + + // no test module because Junit5 dependency is transitive via spring-boot-starter-test + eval("spring-ai-model.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtAirstreamTests.scala b/integration/manual/migrating/src/MillInitSbtAirstreamTests.scala new file mode 100644 index 000000000000..2b84c674a6dc --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtAirstreamTests.scala @@ -0,0 +1,29 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtAirstreamTests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.7 + // cross Scala versions 3.3.3 2.13.16 + // single ScalaJS root module + // scalajs-dom dependency + def gitRepoUrl = "https://github.com/raquo/Airstream.git" + def gitRepoBranch = "v17.2.1" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("[_].compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("[_].publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("[2.13.16].test.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // test requires jsEnv setting + eval("[2.13.16].test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtCatsTests.scala b/integration/manual/migrating/src/MillInitSbtCatsTests.scala new file mode 100644 index 000000000000..ec33f3125322 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtCatsTests.scala @@ -0,0 +1,31 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtCatsTests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.7 + // cross Scala versions 2.12.20 2.13.16 3.3.4 + // sources for cross Scala version ranges + // sbt-crossproject 1.3.2 + // different CrossType modules + def gitRepoUrl = "https://github.com/typelevel/cats.git" + def gitRepoBranch = "v2.13.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // missing generated sources + eval( + "kernel-laws.jvm[_].compile", + stdout = os.Inherit, + stderr = os.Inherit + ).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtEnumeratumTests.scala b/integration/manual/migrating/src/MillInitSbtEnumeratumTests.scala new file mode 100644 index 000000000000..a4ae4effa679 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtEnumeratumTests.scala @@ -0,0 +1,24 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtEnumeratumTests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.7 + // cross Scala versions 2.12.20 2.13.16 3.3.5 + // sbt-crossproject 1.3.2 + def gitRepoUrl = "https://github.com/lloydmeta/enumeratum.git" + def gitRepoBranch = "enumeratum-1.9.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // publish is disabled for Scala 3 + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtFs2Tests.scala b/integration/manual/migrating/src/MillInitSbtFs2Tests.scala new file mode 100644 index 000000000000..961a3d48dc44 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtFs2Tests.scala @@ -0,0 +1,27 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtFs2Tests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.11 + // cross Scala versions 2.12.20 2.13.16 3.3.5 + // sbt-crossproject 1.3.2 + // cross partial source roots in core, io + // .sbtopts with JVM args + def gitRepoUrl = "https://github.com/typelevel/fs2.git" + def gitRepoBranch = "v3.12.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // requires cats-effect-testkit + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit) // fails with OOM + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtGatlingTests.scala b/integration/manual/migrating/src/MillInitSbtGatlingTests.scala new file mode 100644 index 000000000000..35f8850a4dbb --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtGatlingTests.scala @@ -0,0 +1,27 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtGatlingTests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.11 + // Scala version 2.13.16 + def gitRepoUrl = "https://github.com/gatling/gatling.git" + def gitRepoBranch = "v3.14.3" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + eval("__.publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + + // missing resource bundle + eval("__.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtLilaTests.scala b/integration/manual/migrating/src/MillInitSbtLilaTests.scala new file mode 100644 index 000000000000..3db7e8ba802c --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtLilaTests.scala @@ -0,0 +1,23 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtLilaTests extends GitRepoIntegrationTestSuite { + + // sbt 1.11.3 + // Scala version 3.7.2 + def gitRepoUrl = "https://github.com/lichess-org/lila.git" + def gitRepoBranch = "master" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // non-standard layout + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtNscalaTimeTests.scala b/integration/manual/migrating/src/MillInitSbtNscalaTimeTests.scala new file mode 100644 index 000000000000..cca1c9431fa9 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtNscalaTimeTests.scala @@ -0,0 +1,25 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtNscalaTimeTests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.7 + // cross Scala versions 2.11.12 2.12.20 2.13.15 3.3.4 + // single root module + def gitRepoUrl = "https://github.com/nscala-time/nscala-time.git" + def gitRepoBranch = "releases/3.0.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("[_].compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("[_].test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("[_].publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtRefinedTests.scala b/integration/manual/migrating/src/MillInitSbtRefinedTests.scala new file mode 100644 index 000000000000..be2659637df0 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtRefinedTests.scala @@ -0,0 +1,25 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtRefinedTests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.7 + // cross Scala versions 2.12.20 2.13.15 3.3.4 + // sbt-crossproject 1.3.2 + def gitRepoUrl = "https://github.com/fthomas/refined.git" + def gitRepoBranch = "v0.11.3" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // custom version range source roots 3.0+/3.0- not supported + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtScala3Tests.scala b/integration/manual/migrating/src/MillInitSbtScala3Tests.scala new file mode 100644 index 000000000000..3e6026a92bd6 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtScala3Tests.scala @@ -0,0 +1,24 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtScala3Tests extends GitRepoIntegrationTestSuite { + + // sbt 1.11.0 + // Scala version 3.7.0 + def gitRepoUrl = "https://github.com/scala/scala3.git" + def gitRepoBranch = "3.7.1" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // non-standard layout + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtScalaLoggingTests.scala b/integration/manual/migrating/src/MillInitSbtScalaLoggingTests.scala new file mode 100644 index 000000000000..44f7436f1529 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtScalaLoggingTests.scala @@ -0,0 +1,25 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtScalaLoggingTests extends GitRepoIntegrationTestSuite { + + // sbt 1.6.2 + // cross Scala versions 2.11.12 2.12.15 2.13.8 3.1.2 + // single root module + def gitRepoUrl = "https://github.com/lightbend-labs/scala-logging.git" + def gitRepoBranch = "v3.9.5" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtScalaPBTests.scala b/integration/manual/migrating/src/MillInitSbtScalaPBTests.scala new file mode 100644 index 000000000000..5a40b3ddb9c1 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtScalaPBTests.scala @@ -0,0 +1,22 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtScalaPBTests extends GitRepoIntegrationTestSuite { + + // sbt 1.11.2 + def gitRepoUrl = "https://github.com/scalapb/ScalaPB.git" + def gitRepoBranch = "v0.11.19" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // requires sbt-projectmatrix + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtScoptTests.scala b/integration/manual/migrating/src/MillInitSbtScoptTests.scala new file mode 100644 index 000000000000..3d757740a2f5 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtScoptTests.scala @@ -0,0 +1,28 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtScoptTests extends GitRepoIntegrationTestSuite { + + // sbt 1.5.2 + // cross Scala versions 2.11.12 2.12.16 2.13.8 3.1.3 + // sbt-crossproject 1.0.0 + // single cross-platform/version root module + def gitRepoUrl = "https://github.com/scopt/scopt.git" + def gitRepoBranch = "v4.1.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("__.publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // unsupported test framework + eval("__.test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/manual/migrating/src/MillInitSbtScryptoTests.scala b/integration/manual/migrating/src/MillInitSbtScryptoTests.scala new file mode 100644 index 000000000000..34012650a4d0 --- /dev/null +++ b/integration/manual/migrating/src/MillInitSbtScryptoTests.scala @@ -0,0 +1,29 @@ +package mill.integration + +import mill.testkit.GitRepoIntegrationTestSuite +import utest.* + +object MillInitSbtScryptoTests extends GitRepoIntegrationTestSuite { + + // sbt 1.10.7 + // cross Scala versions 2.11.12 2.12.20, 2.13.16 3.3.5 + // sbt-crossproject 1.3.2 + // root is a cross-platform/version module + def gitRepoUrl = "https://github.com/input-output-hk/scrypto.git" + def gitRepoBranch = "v3.1.0" + + def tests = Tests { + test - integrationTest { tester => + import tester.* + + eval("init", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval(("resolve", "_"), stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("jvm[_].compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("jvm[_].test", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + eval("jvm[_].publishLocal", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> true + + // requires support for ScalablyTypedConverterGenSourcePlugin + eval("js[_].compile", stdout = os.Inherit, stderr = os.Inherit).isSuccess ==> false + } + } +} diff --git a/integration/migrating/init/src/MillInitGradleTests.scala b/integration/migrating/init/src/MillInitGradleTests.scala index 0d7f103bd52f..e01666a60340 100644 --- a/integration/migrating/init/src/MillInitGradleTests.scala +++ b/integration/migrating/init/src/MillInitGradleTests.scala @@ -18,6 +18,7 @@ object MillInitGradleJCommanderTests extends BuildGenTestSuite { test - integrationTest(url)( testMillInit( _, + initCommand = Seq("init"), expectedAllSourceFileNums = Map("allSourceFiles" -> 65, "test.allSourceFiles" -> 107), expectedCompileTaskResults = Some(SplitTaskResults( @@ -41,10 +42,12 @@ object MillInitGradleFastCsvTests extends BuildGenTestSuite { // - Gradle 8.10.1 val url = "https://github.com/osiegmar/FastCSV/archive/refs/tags/v3.4.0.zip" - test - integrationTest(url)( + test - integrationTest(url) { tester => + writeMillJvmVersion(tester.workspacePath, "17") + testMillInit( - _, - initCommand = defaultInitCommand ++ Seq("--jvm-id", "17"), + tester, + initCommand = Seq("init"), expectedAllSourceFileNums = Map( "example.allSourceFiles" -> 24, "lib.allSourceFiles" -> 41, @@ -60,7 +63,7 @@ object MillInitGradleFastCsvTests extends BuildGenTestSuite { expectedTestTaskResults = Some(SplitTaskResults(successful = SortedSet(), failed = SortedSet("lib.test"))) ) - ) + } } } @@ -79,6 +82,7 @@ object MillInitGradleEhcache3Tests extends BuildGenTestSuite { writeMillJvmVersionTemurin11(tester.workspacePath) testMillInit( tester, + initCommand = Seq("init"), expectedAllSourceFileNums = Map( "ehcache-xml.test.allSourceFiles" -> 55, "ehcache-xml.ehcache-xml-spi.allSourceFiles" -> 7, @@ -91,7 +95,6 @@ object MillInitGradleEhcache3Tests extends BuildGenTestSuite { "ehcache-core.test.allSourceFiles" -> 61, "clustered.server.ehcache-service.allSourceFiles" -> 12, "clustered.ehcache-client.allSourceFiles" -> 82, - "demos.allSourceFiles" -> 0, "ehcache-core.allSourceFiles" -> 120, "clustered.server.ehcache-entity.allSourceFiles" -> 38, "integration-test.allSourceFiles" -> 0, @@ -113,11 +116,9 @@ object MillInitGradleEhcache3Tests extends BuildGenTestSuite { "clustered.server.ehcache-service-api.test.allSourceFiles" -> 2, "ehcache-transactions.allSourceFiles" -> 12, "ehcache-107.allSourceFiles" -> 38, - "clustered.allSourceFiles" -> 0, "clustered.ops-tool.test.allSourceFiles" -> 1, "demos.01-CacheAside.allSourceFiles" -> 4, "clustered.ops-tool.allSourceFiles" -> 9, - "clustered.server.allSourceFiles" -> 0, "docs.allSourceFiles" -> 0, "clustered.server.ehcache-entity.test.allSourceFiles" -> 16, "ehcache-impl.test.allSourceFiles" -> 182, @@ -132,18 +133,16 @@ object MillInitGradleEhcache3Tests extends BuildGenTestSuite { ), expectedCompileTaskResults = Some(SplitTaskResults( successful = SortedSet( - "clustered.compile", "clustered.ehcache-clustered.compile", "clustered.ehcache-common-api.compile", "clustered.ehcache-common-api.test.compile", "clustered.ehcache-common.compile", + "clustered.ehcache-common.test.compile", "clustered.integration-test.compile", "clustered.ops-tool.compile", "clustered.ops-tool.test.compile", "clustered.osgi-test.compile", - "clustered.server.compile", "clustered.test-utils.compile", - "demos.compile", "docs.compile", "ehcache-api.compile", "ehcache-api.test.compile", @@ -156,12 +155,9 @@ object MillInitGradleEhcache3Tests extends BuildGenTestSuite { "osgi-test.compile", "spi-tester.compile" ), - // [warn] Unexpected javac output: warning: [path] bad path element...ehcache-api/compile-resources": no such file or directory - // [warn] error: warnings found and -Werror specified failed = SortedSet( "clustered.ehcache-client.compile", "clustered.ehcache-client.test.compile", - "clustered.ehcache-common.test.compile", "clustered.integration-test.test.compile", "clustered.osgi-test.test.compile", "clustered.server.ehcache-entity.compile", @@ -190,6 +186,7 @@ object MillInitGradleEhcache3Tests extends BuildGenTestSuite { expectedTestTaskResults = Some(SplitTaskResults( successful = SortedSet( "clustered.ehcache-common-api.test", + "clustered.ehcache-common.test", "clustered.ops-tool.test", "ehcache-api.test", "ehcache-core.test", @@ -197,7 +194,6 @@ object MillInitGradleEhcache3Tests extends BuildGenTestSuite { ), failed = SortedSet( "clustered.ehcache-client.test", - "clustered.ehcache-common.test", "clustered.integration-test.test", "clustered.osgi-test.test", "clustered.server.ehcache-entity.test", diff --git a/integration/migrating/init/src/MillInitMavenTests.scala b/integration/migrating/init/src/MillInitMavenTests.scala index 14c88d136690..1446f463c0e8 100644 --- a/integration/migrating/init/src/MillInitMavenTests.scala +++ b/integration/migrating/init/src/MillInitMavenTests.scala @@ -1,12 +1,7 @@ package mill.integration import mill.constants.Util -import mill.integration.MillInitUtils.{ - SplitTaskResults, - defaultInitCommand, - defaultInitCommandWithoutMerge, - testMillInit -} +import mill.integration.MillInitUtils.{SplitTaskResults, testMillInit} import utest.* import scala.collection.immutable.SortedSet @@ -20,11 +15,11 @@ object MillInitMavenJansiTests extends BuildGenTestSuite { val url = "https://github.com/fusesource/jansi/archive/refs/tags/jansi-2.4.1.zip" test - integrationTest(url) { tester => - import tester._ + import tester.* val initRes = eval("init") assert( - initRes.out.contains(initMessage(1)), + initRes.err.contains("""init completed, run "mill resolve _" to list available tasks"""), initRes.isSuccess ) @@ -49,35 +44,6 @@ object MillInitMavenJansiTests extends BuildGenTestSuite { publishLocalRes.isSuccess ) } - - test("realistic") - integrationTest(url) { tester => - import tester._ - - // set jvmId to test the feature - val init = - ( - "init", - "--base-module", - "JansiModule", - "--jvm-id", - "11", - "--deps-object", - "Deps", - "--cache-repository", - "--process-plugins" - ) - val initRes = eval(init) - assert(initRes.isSuccess) - - val compileRes = eval("compile") - assert(compileRes.isSuccess) - - val testRes = eval("test") - assert(testRes.isSuccess) - - val publishLocalRes = eval("publishLocal") - assert(publishLocalRes.isSuccess) - } } } @@ -90,10 +56,9 @@ object MillInitMavenDotEnvTests extends BuildGenTestSuite { val url = "https://github.com/shyiko/dotenv/archive/refs/tags/0.1.1.zip" test - integrationTest(url) { tester => - import tester._ + import tester.* - val init = defaultInitCommand - val initRes = eval(init) + val initRes = eval("init") assert(initRes.isSuccess) val compileRes = eval("__.compile") @@ -111,7 +76,6 @@ object MillInitMavenNettyTests extends BuildGenTestSuite { // - 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 val url = "https://github.com/netty/netty/archive/refs/tags/netty-4.1.114.Final.zip" test - integrationTest(url) { tester => @@ -119,14 +83,7 @@ object MillInitMavenNettyTests extends BuildGenTestSuite { if (!Util.isWindows) { testMillInit( tester, - /* - `--merge` causes init to fail here: - ```text - Exception in thread "main" java.lang.NullPointerException: Cannot invoke "mill.main.buildgen.Tree.nodes$default$1()" because "tree" is null - at mill.main.buildgen.BuildGenUtil$.writeBuildObject(BuildGenUtil.scala:551) - ``` - */ - initCommand = defaultInitCommandWithoutMerge :+ "--publish-properties", + initCommand = Seq("init"), expectedAllSourceFileNums = Map( "transport-classes-kqueue.allSourceFiles" -> 29, "allSourceFiles" -> 0, diff --git a/integration/migrating/init/src/MillInitSbtTests.scala b/integration/migrating/init/src/MillInitSbtTests.scala index 348ec14ef58d..4e6cc62436a9 100644 --- a/integration/migrating/init/src/MillInitSbtTests.scala +++ b/integration/migrating/init/src/MillInitSbtTests.scala @@ -35,6 +35,7 @@ object MillInitScala3ExampleProjectWithJvmOptsTests extends BuildGenTestSuite { os.write(it.workspacePath / ".jvmopts", "-Ddummy=prop -Ddummy2=prop2") testMillInit( it, + initCommand = Seq("init"), expectedAllSourceFileNums = Map("allSourceFiles" -> 13, "test.allSourceFiles" -> 1), expectedCompileTaskResults = Some(SplitTaskResults( successful = SortedSet("compile", "test.compile"), @@ -60,7 +61,7 @@ object MillInitSbtMultiProjectExampleTests extends BuildGenTestSuite { "https://github.com/pbassiner/sbt-multi-project-example/archive/152b31df9837115b183576b0080628b43c505389.zip" test - integrationTest(url) { tester => - import tester._ + import tester.* bumpSbt(workspacePath) /* `multi1.compile` doesn't work well when Mill is run with JDK 17 and 21: @@ -78,8 +79,8 @@ object MillInitSbtMultiProjectExampleTests extends BuildGenTestSuite { val submodules = SortedSet("common", "multi1", "multi2") testMillInit( tester, + initCommand = Seq("init"), expectedAllSourceFileNums = Map( - "allSourceFiles" -> 0, "multi1.test.allSourceFiles" -> 1, "multi1.allSourceFiles" -> 1, "multi2.allSourceFiles" -> 1, @@ -88,7 +89,7 @@ object MillInitSbtMultiProjectExampleTests extends BuildGenTestSuite { "common.allSourceFiles" -> 1 ), expectedCompileTaskResults = Some(SplitTaskResults( - successful = SortedSet("compile") ++ submodules.flatMap(allCompileTasks), + successful = submodules.flatMap(allCompileTasks), failed = SortedSet.empty )), expectedTestTaskResults = @@ -146,8 +147,8 @@ object MillInitSbtGatlingTests extends BuildGenTestSuite { if (!mill.constants.Util.isWindows) { testMillInit( tester, + initCommand = Seq("init"), expectedAllSourceFileNums = Map( - "allSourceFiles" -> 0, "gatling-http.test.allSourceFiles" -> 32, "gatling-jms.allSourceFiles" -> 30, "gatling-jdbc.allSourceFiles" -> 2, @@ -192,8 +193,7 @@ object MillInitSbtGatlingTests extends BuildGenTestSuite { os.remove(tester.workspacePath / os.SubPath(skipped)) }, expectedCompileTaskResults = Some(SplitTaskResults( - successful = SortedSet("compile") - ++ submodulesWithTests.flatMap(allCompileTasks) + successful = submodulesWithTests.flatMap(allCompileTasks) ++ submodulesWithoutTests.map(compileTask), failed = SortedSet.empty )), diff --git a/integration/migrating/init/src/MillInitUtils.scala b/integration/migrating/init/src/MillInitUtils.scala index 37b8ae8583ea..c01cb10ec980 100644 --- a/integration/migrating/init/src/MillInitUtils.scala +++ b/integration/migrating/init/src/MillInitUtils.scala @@ -2,7 +2,7 @@ package mill.integration import mill.testkit.IntegrationTester import mill.testkit.IntegrationTester.EvalResult -import utest._ +import utest.* import scala.collection.immutable.SortedSet @@ -69,7 +69,7 @@ object MillInitUtils { expectedCompileTaskResults: Option[SplitTaskResults], expectedTestTaskResults: Option[SplitTaskResults] ): Unit = { - import tester._ + import tester.* val initResult = eval(initCommand) if (expectedInitResult) assertEvalSuccess(initResult) else assert(!initResult.isSuccess) diff --git a/integration/package.mill b/integration/package.mill index 016fb9f01bf1..be99c1d33bde 100644 --- a/integration/package.mill +++ b/integration/package.mill @@ -142,6 +142,7 @@ object `package` extends mill.Module { object failure extends Cross[IntegrationCrossModule](build.listCross) object feature extends Cross[IntegrationCrossModule](build.listCross) object invalidation extends Cross[IntegrationCrossModule](build.listCross) + object manual extends Cross[IntegrationCrossModule](build.listCross) object ide extends Cross[IdeIntegrationCrossModule](build.listCross) object bootstrap extends Cross[IdeIntegrationCrossModule](build.listCross) object migrating extends Cross[IdeIntegrationCrossModule](build.listCross) diff --git a/libs/init/buildgen/api/src/mill/main/buildgen/ModuleConfig.scala b/libs/init/buildgen/api/src/mill/main/buildgen/ModuleConfig.scala new file mode 100644 index 000000000000..608d56f48b14 --- /dev/null +++ b/libs/init/buildgen/api/src/mill/main/buildgen/ModuleConfig.scala @@ -0,0 +1,367 @@ +package mill.main.buildgen + +import upickle.default.{ReadWriter, macroRW} + +import scala.util.matching.Regex + +/** + * ADT that defines configuration settings for a build module. + */ +sealed trait ModuleConfig +object ModuleConfig { + implicit val rw: ReadWriter[ModuleConfig] = macroRW + + /** + * An operation that computes the base configuration for the given inputs. Settings that cannot + * be shared are set to an empty (`null` for single valued setting) value. + */ + def abstracted(self: Seq[ModuleConfig], that: Seq[ModuleConfig]) = { + def args(self: Seq[String], that: Seq[String]) = + if (that.containsSlice(self)) self else if (self.containsSlice(that)) that else Nil + def value[A](self: A, that: A, default: A = null): A = if (self == that) self else default + + self.flatMap { + case self: CoursierModuleConfig => that.collectFirst { + case that: CoursierModuleConfig => CoursierModuleConfig( + self.repositories.intersect(that.repositories) + ) + } + case self: JavaHomeModuleConfig => that.collectFirst { + case that: JavaHomeModuleConfig => JavaHomeModuleConfig( + value(self.jvmId, that.jvmId) + ) + } + case self: RunModuleConfig => that.collectFirst { + case that: RunModuleConfig => RunModuleConfig( + value(self.forkWorkingDir, that.forkWorkingDir) + ) + } + case self: JavaModuleConfig => that.collectFirst { + case that: JavaModuleConfig => JavaModuleConfig( + self.mandatoryMvnDeps.intersect(that.mandatoryMvnDeps), + self.mvnDeps.intersect(that.mvnDeps), + self.compileMvnDeps.intersect(that.compileMvnDeps), + self.runMvnDeps.intersect(that.runMvnDeps), + self.bomMvnDeps.intersect(that.bomMvnDeps), + self.moduleDeps.intersect(that.moduleDeps), + self.compileModuleDeps.intersect(that.compileModuleDeps), + self.runModuleDeps.intersect(that.runModuleDeps), + args(self.javacOptions, that.javacOptions) + ) + } + case self: PublishModuleConfig => that.collectFirst { + case that: PublishModuleConfig => PublishModuleConfig( + value(self.pomPackagingType, that.pomPackagingType), + value(self.pomParentProject, that.pomParentProject, None), + value(self.pomSettings, that.pomSettings), + value(self.publishVersion, that.publishVersion), + value(self.versionScheme, that.versionScheme, None), + value(self.artifactMetadata, that.artifactMetadata), + self.publishProperties.toSeq.intersect(that.publishProperties.toSeq).toMap + ) + } + case self: ErrorProneModuleConfig => that.collectFirst { + case that: ErrorProneModuleConfig => ErrorProneModuleConfig( + self.errorProneOptions.intersect(that.errorProneOptions), + self.errorProneJavacEnableOptions.intersect(that.errorProneJavacEnableOptions), + self.errorProneDeps.intersect(that.errorProneDeps) + ) + } + case self: ScalaModuleConfig => that.collectFirst { + case that: ScalaModuleConfig => ScalaModuleConfig( + value(self.scalaVersion, that.scalaVersion), + args(self.scalacOptions, that.scalacOptions), + self.scalacPluginMvnDeps.intersect(that.scalacPluginMvnDeps) + ) + } + case self: ScalaJSModuleConfig => that.collectFirst { + case that: ScalaJSModuleConfig => ScalaJSModuleConfig( + value(self.scalaJSVersion, that.scalaJSVersion) + ) + } + case self: ScalaNativeModuleConfig => that.collectFirst { + case that: ScalaNativeModuleConfig => ScalaNativeModuleConfig( + value(self.scalaNativeVersion, that.scalaNativeVersion) + ) + } + } + } + + /** + * An operation that computes the overriding configuration when extending a + * [[abstracted base configuration]]. Settings completely defined in `base` are set to an empty + * (`null` for single valued setting) value. + */ + def inherited(self: Seq[ModuleConfig], base: Seq[ModuleConfig]) = { + def args(self: Seq[String], base: Seq[String]) = self.indexOfSlice(base) match { + case -1 => self + case i => self.take(i) ++ self.drop(i + base.length) + } + def value[A](self: A, base: A, default: A = null): A = if (self == base) default else self + + self.map { + case self: CoursierModuleConfig => base.collectFirst { + case base: CoursierModuleConfig => CoursierModuleConfig( + self.repositories.diff(base.repositories) + ) + }.getOrElse(self) + case self: JavaHomeModuleConfig => base.collectFirst { + case base: JavaHomeModuleConfig => JavaHomeModuleConfig( + value(self.jvmId, base.jvmId) + ) + }.getOrElse(self) + case self: RunModuleConfig => base.collectFirst { + case base: RunModuleConfig => RunModuleConfig( + value(self.forkWorkingDir, base.forkWorkingDir) + ) + }.getOrElse(self) + case self: JavaModuleConfig => base.collectFirst { + case base: JavaModuleConfig => JavaModuleConfig( + self.mandatoryMvnDeps.diff(base.mandatoryMvnDeps), + self.mvnDeps.diff(base.mvnDeps), + self.compileMvnDeps.diff(base.compileMvnDeps), + self.runMvnDeps.diff(base.runMvnDeps), + self.bomMvnDeps.diff(base.bomMvnDeps), + self.moduleDeps.diff(base.moduleDeps), + self.compileModuleDeps.diff(base.compileModuleDeps), + self.runModuleDeps.diff(base.runModuleDeps), + args(self.javacOptions, base.javacOptions) + ) + }.getOrElse(self) + case self: PublishModuleConfig => base.collectFirst { + case base: PublishModuleConfig => PublishModuleConfig( + value(self.pomPackagingType, base.pomPackagingType), + value(self.pomParentProject, base.pomParentProject, None), + value(self.pomSettings, base.pomSettings), + value(self.publishVersion, base.publishVersion), + value(self.versionScheme, base.versionScheme, None), + value(self.artifactMetadata, base.artifactMetadata), + self.publishProperties.toSeq.diff(base.publishProperties.toSeq).toMap + ) + }.getOrElse(self) + case self: ErrorProneModuleConfig => base.collectFirst { + case base: ErrorProneModuleConfig => ErrorProneModuleConfig( + self.errorProneOptions.diff(base.errorProneOptions), + self.errorProneJavacEnableOptions.diff(base.errorProneJavacEnableOptions), + self.errorProneDeps.diff(base.errorProneDeps) + ) + }.getOrElse(self) + case self: ScalaModuleConfig => base.collectFirst { + case base: ScalaModuleConfig => ScalaModuleConfig( + value(self.scalaVersion, base.scalaVersion), + args(self.scalacOptions, base.scalacOptions), + self.scalacPluginMvnDeps.diff(base.scalacPluginMvnDeps) + ) + }.getOrElse(self) + case self: ScalaJSModuleConfig => base.collectFirst { + case base: ScalaJSModuleConfig => ScalaJSModuleConfig( + value(self.scalaJSVersion, base.scalaJSVersion) + ) + }.getOrElse(self) + case self: ScalaNativeModuleConfig => base.collectFirst { + case base: ScalaNativeModuleConfig => ScalaNativeModuleConfig( + value(self.scalaNativeVersion, base.scalaNativeVersion) + ) + }.getOrElse(self) + } + } +} + +case class CoursierModuleConfig(repositories: Seq[String] = Nil) extends ModuleConfig +object CoursierModuleConfig { + implicit val rw: ReadWriter[CoursierModuleConfig] = macroRW +} + +case class JavaHomeModuleConfig(jvmId: String) extends ModuleConfig +object JavaHomeModuleConfig { + + def minJvmVersion: Int = 11 // https://github.com/com-lihaoyi/mill/issues/5782 + + def jvmId(jvmVersion: Int) = { + val version = minJvmVersion.max(jvmVersion) + version match { + case 8 | 11 | 17 | 21 => version.toString // default JDK temurin supports LTS versions + case _ => s"zulu:$version" + } + } + + implicit val rw: ReadWriter[JavaHomeModuleConfig] = macroRW +} + +case class RunModuleConfig(forkWorkingDir: String = null) extends ModuleConfig +object RunModuleConfig { + implicit val rw: ReadWriter[RunModuleConfig] = macroRW +} + +case class JavaModuleConfig( + mandatoryMvnDeps: Seq[String] = Nil, + mvnDeps: Seq[String] = Nil, + compileMvnDeps: Seq[String] = Nil, + runMvnDeps: Seq[String] = Nil, + bomMvnDeps: Seq[String] = Nil, + moduleDeps: Seq[JavaModuleConfig.ModuleDep] = Nil, + compileModuleDeps: Seq[JavaModuleConfig.ModuleDep] = Nil, + runModuleDeps: Seq[JavaModuleConfig.ModuleDep] = Nil, + javacOptions: Seq[String] = Nil +) extends ModuleConfig +object JavaModuleConfig { + + val mvnDepOrgNameRegex: Regex = """^mvn"([^:]+)[:]+([^:"]+)[:"].*$""".r + + def isBomMvnDep(mvnDep: String): Boolean = { + val mvnDepOrgNameRegex(org, name) = mvnDep: @unchecked + name.endsWith("-bom") || + (org == "org.springframework.boot" && name == "spring-boot-dependencies") + } + + def mvnDep( + org: String, + name: String, + version: String = null, + classifier: Option[String] = None, + typ: Option[String] = None, + excludes: Iterable[(String, String)] = Nil, + sep1: String = ":", + sep2: String = ":" + ): String = { + var suffix = + (version match { + case null => "" + case _ => version + }) + classifier.fold("") { + case null | "" => "" + case attr => s";classifier=$attr" + } + typ.fold("") { + case null | "" | "jar" => "" + case attr => s";type=$typ" + } + excludes.iterator.map { + case (org, name) => s";exclude=$org:$name" + }.mkString + if (suffix.nonEmpty) suffix = sep2 + suffix + s"""mvn"$org$sep1$name$suffix"""" + } + + def unsupportedJavacOptions = Seq( + // TODO Supporting -Werror would require removing non-existent paths from the classpath + "-Werror" + ) + + /** + * Represents a module dependency. + * @param segments Path identifying the module. + * @param crossArgs Cross version arguments for a value in `segments` identified by its index. + * The key `-1` can be used to specify the argument for the build root module. + */ + case class ModuleDep(segments: Seq[String], crossArgs: Map[Int, String] = Map()) + object ModuleDep { + implicit val rw: ReadWriter[ModuleDep] = macroRW + } + implicit val rw: ReadWriter[JavaModuleConfig] = macroRW +} + +case class PublishModuleConfig( + pomPackagingType: String = null, + pomParentProject: Option[PublishModuleConfig.Artifact] = None, + pomSettings: PublishModuleConfig.PomSettings = null, + publishVersion: String = null, + versionScheme: Option[String] = None, + artifactMetadata: PublishModuleConfig.Artifact = null, + publishProperties: Map[String, String] = Map() +) extends ModuleConfig +object PublishModuleConfig { + case class Artifact(group: String, id: String, version: String) + object Artifact { + implicit val rw: ReadWriter[Artifact] = macroRW + } + case class License( + id: String = null, + name: String = null, + url: String = "", + isOsiApproved: Boolean = false, + isFsfLibre: Boolean = false, + distribution: String = "" + ) + object License { + implicit val rw: ReadWriter[License] = macroRW + } + case class VersionControl( + browsableRepository: Option[String] = None, + connection: Option[String] = None, + developerConnection: Option[String] = None, + tag: Option[String] = None + ) + object VersionControl { + implicit val rw: ReadWriter[VersionControl] = macroRW + } + case class Developer( + id: String = null, + name: String = null, + url: String = null, + organization: Option[String] = None, + organizationUrl: Option[String] = None + ) + object Developer { + implicit val rw: ReadWriter[Developer] = macroRW + } + case class PomSettings( + description: String = null, + organization: String = null, + url: String = null, + licenses: Seq[License] = Nil, + versionControl: VersionControl = VersionControl(), + developers: Seq[Developer] = Nil + ) + object PomSettings { + implicit val rw: ReadWriter[PomSettings] = macroRW + } + sealed trait VersionScheme + object VersionScheme { + case object Early + } + + implicit val rw: ReadWriter[PublishModuleConfig] = macroRW +} + +case class ErrorProneModuleConfig( + errorProneOptions: Seq[String] = Nil, + errorProneJavacEnableOptions: Seq[String] = Nil, + errorProneDeps: Seq[String] = Nil +) extends ModuleConfig +object ErrorProneModuleConfig { + implicit val rw: ReadWriter[ErrorProneModuleConfig] = macroRW + + def javacOptionsAndConfig(javacOptions: Seq[String], errorProneDeps: Seq[String] = Nil) = { + val (pluginOptions, javacOptions0) = + javacOptions.partition(s => s.startsWith("-Xplugin:ErrorProne") || s.startsWith("-XD")) + ( + javacOptions0, + pluginOptions.collectFirst { + case s if s.startsWith("-Xplugin:ErrorProne") => + ErrorProneModuleConfig( + errorProneOptions = s.split(" ").toSeq.tail, + errorProneJavacEnableOptions = pluginOptions.diff(Seq(s)), + errorProneDeps = errorProneDeps + ) + } + ) + } +} + +case class ScalaModuleConfig( + scalaVersion: String = null, + scalacOptions: Seq[String] = Nil, + scalacPluginMvnDeps: Seq[String] = Nil +) extends ModuleConfig +object ScalaModuleConfig { + implicit val rw: ReadWriter[ScalaModuleConfig] = macroRW +} + +case class ScalaJSModuleConfig(scalaJSVersion: String = null) extends ModuleConfig +object ScalaJSModuleConfig { + implicit val rw: ReadWriter[ScalaJSModuleConfig] = macroRW +} + +case class ScalaNativeModuleConfig(scalaNativeVersion: String = null) extends ModuleConfig +object ScalaNativeModuleConfig { + implicit val rw: ReadWriter[ScalaNativeModuleConfig] = macroRW +} diff --git a/libs/init/buildgen/api/src/mill/main/buildgen/ModuleRepr.scala b/libs/init/buildgen/api/src/mill/main/buildgen/ModuleRepr.scala new file mode 100644 index 000000000000..64d93b0b2738 --- /dev/null +++ b/libs/init/buildgen/api/src/mill/main/buildgen/ModuleRepr.scala @@ -0,0 +1,18 @@ +package mill.main.buildgen + +import upickle.default.{ReadWriter, macroRW} + +/** + * A representation for a build module that is optimized for code generation. + */ +case class ModuleRepr( + segments: Seq[String], + supertypes: Seq[String] = Seq("Module"), + mixins: Seq[String] = Nil, + configs: Seq[ModuleConfig] = Nil, + crossConfigs: Seq[(String, Seq[ModuleConfig])] = Nil, + testModule: Option[TestModuleRepr] = None +) +object ModuleRepr { + implicit val rw: ReadWriter[ModuleRepr] = macroRW +} diff --git a/libs/init/buildgen/api/src/mill/main/buildgen/TestModuleRepr.scala b/libs/init/buildgen/api/src/mill/main/buildgen/TestModuleRepr.scala new file mode 100644 index 000000000000..3a8d3968ff45 --- /dev/null +++ b/libs/init/buildgen/api/src/mill/main/buildgen/TestModuleRepr.scala @@ -0,0 +1,64 @@ +package mill.main.buildgen + +import upickle.default.{ReadWriter, macroRW} + +import scala.collection.mutable + +/** + * A representation for a test module that is optimized for code generation. + */ +case class TestModuleRepr( + name: String, + supertypes: Seq[String], + mixins: Seq[String] = Nil, + configs: Seq[ModuleConfig] = Nil, + crossConfigs: Seq[(String, Seq[ModuleConfig])] = Nil, + testParallelism: Boolean = true, + testSandboxWorkingDir: Boolean = true +) +object TestModuleRepr { + implicit val rw: ReadWriter[TestModuleRepr] = macroRW + + def mixinAndMandatoryMvnDeps(mvnDeps: Iterable[String]): Option[(String, Seq[String])] = { + val mandatoryMvnDeps = Seq.newBuilder[String] + var mixin: String = null + // frameworks like scalatest and specs2 provide integrations with other frameworks + mvnDeps.foreach { + case dep @ JavaModuleConfig.mvnDepOrgNameRegex(org, name) => (org, name) match { + case ("org.testng", _) => + if (mixin == null) mixin = "TestModule.TestNg" + mandatoryMvnDeps += dep + case ("junit", _) => + if (mixin == null) mixin = "TestModule.Junit4" + mandatoryMvnDeps += dep + case ("org.junit.jupiter" | "org.junit.platform", _) => + if (mixin == null) mixin = "TestModule.Junit5" + mandatoryMvnDeps += dep + case ("org.scalatest" | "org.scalatestplus", _) => + mixin = "TestModule.ScalaTest" + mandatoryMvnDeps += dep + case ("org.specs2", _) => + mixin = "TestModule.Specs2" + mandatoryMvnDeps += dep + case ("com.lihaoyi", "utest") => + if (mixin == null) mixin = "TestModule.Utest" + mandatoryMvnDeps += dep + case ("org.scalameta", "munit") => + if (mixin == null) mixin = "TestModule.Munit" + mandatoryMvnDeps += dep + case ("com.disneystreaming", "weaver-scalacheck") => + if (mixin == null) mixin = "TestModule.Weaver" + mandatoryMvnDeps += dep + case ("dev.zio", "zio-test" | "zio-test-sbt") => + if (mixin == null) mixin = "TestModule.ZioTest" + mandatoryMvnDeps += dep + case ("org.scalacheck", "scalacheck") => + if (mixin == null) mixin = "TestModule.ScalaCheck" + mandatoryMvnDeps += dep + case _ => + } + } + if (null == mixin) None + else Some((mixin, mandatoryMvnDeps.result())) + } +} diff --git a/libs/init/buildgen/src/mill/main/buildgen/BuildGenBase.scala b/libs/init/buildgen/src/mill/main/buildgen/BuildGenBase.scala deleted file mode 100644 index cda1c557397c..000000000000 --- a/libs/init/buildgen/src/mill/main/buildgen/BuildGenBase.scala +++ /dev/null @@ -1,112 +0,0 @@ -package mill.main.buildgen - -import mill.main.buildgen.BuildGenUtil.{compactBuildTree, writeBuildObject} - -import scala.collection.immutable.{SortedMap, SortedSet} - -/* -TODO Can we just convert all generic type parameters to abstract type members? - See https://stackoverflow.com/a/1154727/5082913. - I think abstract type members are preferred in this case. - */ -trait BuildGenBase[M, D, I] { - type C - def convertWriteOut(cfg: C, shared: BuildGenUtil.BasicConfig, input: I): Unit = { - val output = convert(input, cfg, shared) - writeBuildObject( - if (shared.merge.value) compactBuildTree(output) else output, - shared.jvmId - ) - } - - def getModuleTree(input: I): Tree[Node[Option[M]]] - - /** - * A [[Map]] mapping from a key retrieved from the original build tool - * (for example, the GAV coordinate for Maven, `ProjectRef.project` for `sbt`) - * to the module FQN reference string in code such as `parentModule.childModule`. - * - * If there is no need for such a map, override it with [[Unit]]. - */ - type ModuleFqnMap - def getModuleFqnMap(moduleNodes: Seq[Node[M]]): ModuleFqnMap - - def convert( - input: I, - cfg: C, - shared: BuildGenUtil.BasicConfig - ): Tree[Node[BuildObject]] = { - val moduleTree = getModuleTree(input) - val moduleOptionTree = moduleTree.map(node => node.copy(value = node.value)) - - // for resolving moduleDeps - val moduleNodes = - moduleOptionTree.nodes().flatMap(node => node.value.map(m => node.copy(value = m))).toSeq - val moduleRefMap = getModuleFqnMap(moduleNodes) - - val baseInfo = - shared.baseModule.fold(IrBaseInfo()) { getBaseInfo(input, cfg, _, moduleNodes.size) } - - moduleOptionTree.map(optionalBuild => - optionalBuild.copy(value = - optionalBuild.value.fold( - BuildObject(SortedSet("mill._"), SortedMap.empty, Seq("Module"), "", "") - )(moduleModel => { - val name = getArtifactId(moduleModel) - println(s"converting module $name") - - val build = optionalBuild.copy(value = moduleModel) - val inner = extractIrBuild(cfg, build, moduleRefMap) - - val isNested = optionalBuild.dirs.nonEmpty - BuildObject( - imports = - BuildGenUtil.renderImports( - shared.baseModule, - isNested, - extraImports - ), - companions = - shared.depsObject.fold(SortedMap.empty[String, BuildObject.Constants])(name => - SortedMap((name, SortedMap(inner.scopedDeps.namedMvnDeps.toSeq*))) - ), - supertypes = getSupertypes(cfg, baseInfo, build), - inner = BuildGenUtil.renderIrBuild(inner, baseInfo), - outer = - if (isNested || baseInfo.moduleTypedef == null) "" - else BuildGenUtil.renderIrTrait(baseInfo.moduleTypedef) - ) - }) - ) - ) - } - - def extraImports: Seq[String] - - def getSupertypes(cfg: C, baseInfo: IrBaseInfo, build: Node[M]): Seq[String] - - def getBaseInfo( - input: I, - cfg: C, - baseModule: String, - packagesSize: Int - ): IrBaseInfo - - def getArtifactId(moduleModel: M): String - - def extractIrBuild( - cfg: C, - // baseInfo: IrBaseInfo, // `baseInfo` is no longer needed as we compare the `IrBuild` with `IrBaseInfo`/`IrTrait` in common code now. - build: Node[M], - moduleFqnMap: ModuleFqnMap - ): IrBuild -} - -object BuildGenBase { - trait MavenAndGradle[M, D] extends BuildGenBase[M, D, Tree[Node[M]]] { - override def getModuleTree(input: Tree[Node[M]]): Tree[Node[Option[M]]] = - // TODO consider filtering out projects without the `java` plugin applied in Gradle too - input.map(node => node.copy(value = Some(node.value))) - override def extraImports: Seq[String] = Seq() - } -} diff --git a/libs/init/buildgen/src/mill/main/buildgen/BuildGenUtil.scala b/libs/init/buildgen/src/mill/main/buildgen/BuildGenUtil.scala deleted file mode 100644 index 1a00bca2937c..000000000000 --- a/libs/init/buildgen/src/mill/main/buildgen/BuildGenUtil.scala +++ /dev/null @@ -1,571 +0,0 @@ -package mill.main.buildgen - -import geny.Generator -import mainargs.{Flag, arg} -import mill.api.daemon.internal.internal -import mill.constants.CodeGenConstants.{nestedBuildFileNames, rootBuildFileNames, rootModuleAlias} -import mill.main.buildgen.BuildObject.Companions -import mill.internal.Util.backtickWrap -import mill.api.CrossVersion - -import scala.collection.immutable.SortedSet -import scala.util.boundary - -@internal -object BuildGenUtil { - - def renderIrTrait(value: IrTrait): String = { - import value.* - - s"""trait $baseModule ${renderExtends(moduleSupertypes)} { - | - |${renderJavacOptions(javacOptions)} - | - |${renderScalaVersion(scalaVersion)} - | - |${renderScalacOptions(scalacOptions)} - | - |${renderPomSettings(renderIrPom(pomSettings))} - | - |${renderPublishVersion(publishVersion)} - | - |${renderPublishProperties(publishProperties)} - | - |${renderRepositories(repositories)} - |}""".stripMargin - - } - - def renderIrPom(value: IrPom | Null): String = { - if (value == null) "" - else { - import value.* - val mkLicenses = licenses.iterator.map(renderLicense).mkString("Seq(", ", ", ")") - val mkDevelopers = developers.iterator.map(renderDeveloper).mkString("Seq(", ", ", ")") - s"PomSettings(${escape(description)}, ${escape(organization)}, ${escape(url)}, $mkLicenses, ${renderVersionControl(versionControl)}, $mkDevelopers)" - } - } - - /** - * @param baseInfo to compare with [[build]] and render the values only if they are different. - */ - def renderIrBuild(build: IrBuild, baseInfo: IrBaseInfo): String = { - val baseTrait = baseInfo.moduleTypedef - import build.* - val testModuleTypedef = - if (!hasTest) "" - else { - val declare = - BuildGenUtil.renderTestModuleDecl(testModule, testModuleMainType, scopedDeps.testModule) - - // `testSandboxWorkingDir` is disabled as other build tools such as `sbt` don't run tests in the sandbox. - s"""$declare { - | - |${renderBomMvnDeps(scopedDeps.testBomMvnDeps)} - | - |${renderMvnDeps(scopedDeps.testMvnDeps)} - | - |${renderModuleDeps(scopedDeps.testModuleDeps)} - | - |${renderCompileMvnDeps(scopedDeps.testCompileMvnDeps)} - | - |${renderCompileModuleDeps(scopedDeps.testCompileModuleDeps)} - | - |${renderResources(testResources)} - | - |def testSandboxWorkingDir = false - |def testParallelism = false - |${build.testForkDir.fold("")(v => s"def forkWorkingDir = $v")} - | - |}""".stripMargin - } - - s"""${renderArtifactName(projectName, dirs)} - | - |${renderJavacOptions( - javacOptions, - if (baseTrait != null) baseTrait.javacOptions else Seq.empty - )} - | - |${renderScalaVersion(scalaVersion, if (baseTrait != null) baseTrait.scalaVersion else None)} - | - |${renderScalacOptions( - scalacOptions, - if (baseTrait != null) baseTrait.scalacOptions else None - )} - | - |${renderRepositories( - repositories, - if (baseTrait != null) baseTrait.repositories else Seq.empty - )} - | - |${renderBomMvnDeps(scopedDeps.mainBomMvnDeps)} - | - |${renderMvnDeps(scopedDeps.mainMvnDeps)} - | - |${renderModuleDeps(scopedDeps.mainModuleDeps)} - | - |${renderCompileMvnDeps(scopedDeps.mainCompileMvnDeps)} - | - |${renderCompileModuleDeps(scopedDeps.mainCompileModuleDeps)} - | - |${renderRunMvnDeps(scopedDeps.mainRunMvnDeps)} - | - |${renderRunModuleDeps(scopedDeps.mainRunModuleDeps)} - | - |${ - if (pomSettings != (if (baseTrait != null) baseTrait.pomSettings else null)) - renderPomSettings(renderIrPom(pomSettings)) - else "" - } - | - |${renderPublishVersion( - publishVersion, - if (baseTrait != null) baseTrait.publishVersion else null - )} - | - |${renderPomPackaging(packaging)} - | - |${ - if (pomParentArtifact == null) "" - else renderPomParentProject(renderArtifact(pomParentArtifact)) - } - | - |${renderPublishProperties(Nil)} - | - |${renderResources(resources)} - | - |${renderPublishProperties(publishProperties)} - | - |$testModuleTypedef""".stripMargin - - } - def buildFile(dirs: Seq[String]): os.SubPath = { - val name = if (dirs.isEmpty) rootBuildFileNames.get(0) else nestedBuildFileNames.get(0) - os.sub / dirs / name - } - - def renderImports( - baseModule: Option[String], - isNested: Boolean, - extraImports: Seq[String] - ): SortedSet[String] = { - scala.collection.immutable.SortedSet( - "mill._", - "mill.javalib._", - "mill.javalib.publish._" - ) ++ - extraImports ++ - Option.when(isNested) { baseModule.map(name => s"_root_.build_.$name") }.flatten - } - - def buildModuleFqn(dirs: Seq[String]): String = - (rootModuleAlias +: dirs).iterator.map(backtickWrap).mkString(".") - - def buildModuleFqnMap[Module, Key](input: Generator[Node[Module]])(key: Module => Key) - : Map[Key, String] = - input - .map(node => (key(node.value), buildModuleFqn(node.dirs))) - .toSeq - .toMap - - def renderBuildSource(node: Node[BuildObject], jvmId: Option[String]): os.Source = { - val pkg = buildModuleFqn(node.dirs) - val BuildObject(imports, companions, supertypes, inner, outer) = node.value - 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) - - val millVersionPrefix = - if (node.dirs.nonEmpty) "" - else s"//| mill-version: ${mill.util.BuildInfo.millVersion}\n" - - val jvmIdPrefix = - if (node.dirs.nonEmpty) "" - else jvmId match { - case None => "" - case Some(j) => s"//| mill-jvm-version: ${j}\n" - } - - s"""${millVersionPrefix}${jvmIdPrefix}package $pkg - | - |$importStatements - | - |$companionTypedefs - | - |object `package` ${renderExtends(supertypes)} { - | - |$inner - |} - | - |$outer - |""".stripMargin - } - - def compactBuildTree(tree: Tree[Node[BuildObject]]): Tree[Node[BuildObject]] = boundary { - println("compacting Mill build tree") - - 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) }) - boundary.break(null) - else mergedParentCompanions += ((objectName, parentConstants ++ childConstants)) - } - } - - mergedParentCompanions - } - - tree.transform[Node[BuildObject]] { (node, children) => - var module = node.value - val unmerged = Seq.newBuilder[Tree[Node[BuildObject]]] - - children.iterator.foreach { - case child @ Tree(Node(_ :+ dir, nested), Seq()) if nested.outer.isEmpty => - val mergedCompanions = merge(module.companions, nested.companions) - if (null == mergedCompanions) unmerged += child - else { - val mergedImports = module.imports ++ nested.imports - val mergedInner = { - val name = backtickWrap(dir) - val supertypes = nested.supertypes - - s"""${module.inner} - | - |object $name ${renderExtends(supertypes)} { - | - |${nested.inner} - |}""".stripMargin - } - - module = module.copy( - imports = mergedImports, - companions = mergedCompanions, - inner = mergedInner - ) - } - case child => unmerged += child - } - - val unmergedChildren = unmerged.result() - - Tree(node.copy(value = module), unmergedChildren) - } - } - - def escape(value: String): String = - pprint.Util.literalize(if (value == null) "" else value) - - def escapeOption(value: String): String = - if (null == value) "None" else s"Some(\"$value\")" - - def renderMvnString( - group: String, - artifact: String, - crossVersion: Option[CrossVersion] = None, - version: String | Null = null, - tpe: String | Null = null, - classifier: String | Null = null, - excludes: IterableOnce[(String, String)] = Seq.empty - ): String = { - val sepArtifact = crossVersion match { - case None => s":$artifact" - case Some(value) => value match { - case CrossVersion.Constant(value, _) => s":${artifact}_$value" - case CrossVersion.Binary(_) => s"::$artifact" - case CrossVersion.Full(_) => s":::$artifact" - } - } - val sepVersion = - if (null == version) { - println( - s"assuming $group:$artifact is a BOM dependency; if not, please specify version in the generated build file" - ) - "" - } else s":$version" - val sepTpe = tpe match { - case null | "" | "jar" => "" // skip default - case tpe => s";type=$tpe" - } - val sepClassifier = classifier match { - case null | "" => "" - case s"$${$v}" => // drop values like ${os.detected.classifier} - println(s"dropping classifier $${$v} for dependency $group:$artifact:$version") - "" - case classifier => s";classifier=$classifier" - } - val sepExcludes = excludes.iterator - .map { case (group, artifact) => s";exclude=$group:$artifact" } - .mkString - - s"mvn\"$group$sepArtifact$sepVersion$sepTpe$sepClassifier$sepExcludes\"" - } - - def isBom(groupArtifactVersion: (String, String, String)): Boolean = - groupArtifactVersion._2.endsWith("-bom") - - def isNullOrEmpty(value: String | Null): Boolean = - null == value || value.isEmpty - - val linebreak: String = - """ - |""".stripMargin - - val linebreak2: String = - """ - | - |""".stripMargin - - val mavenMainResourceDir: os.SubPath = - os.sub / "src/main/resources" - - val mavenTestResourceDir: os.SubPath = - os.sub / "src/test/resources" - - def renderArtifact(artifact: IrArtifact): String = - s"Artifact(${escape(artifact.group)}, ${escape(artifact.id)}, ${escape(artifact.version)})" - - def renderDeveloper(dev: IrDeveloper): String = { - s"Developer(${escape(dev.id)}, ${escape(dev.name)}, ${escape(dev.url)}, ${escapeOption(dev.organization)}, ${escapeOption(dev.organizationUrl)})" - } - - def renderExtends(supertypes: Seq[String]): String = supertypes match { - case Seq() => "extends mill.Module" - case items => s"extends ${items.mkString(" with ")}" - } - - def renderLicense( - license: IrLicense - ): String = - s"License(${escape(license.id)}, ${escape(license.name)}, ${escape(license.url)}, ${license.isOsiApproved}, ${license.isFsfLibre}, ${escape(license.distribution)})" - - def renderVersionControl(vc: IrVersionControl): String = - s"VersionControl(${escapeOption(vc.url)}, ${escapeOption(vc.connection)}, ${escapeOption(vc.devConnection)}, ${escapeOption(vc.tag)})" - - // TODO consider renaming to `renderOptionalDef` or `renderIfArgsNonEmpty`? - def optional(construct: String, args: IterableOnce[String]): String = - optional(construct + "(", args, ",", ")") - - def optional(start: String, args: IterableOnce[String], sep: String, end: String): String = { - val itr = args.iterator - if (itr.isEmpty) "" - else itr.mkString(start, sep, end) - } - - def renderSeqWithSuper( - defName: String, - args: Seq[String], - superArgs: Seq[String] = Seq.empty, - elementType: String, - transform: String => String - ): Option[String] = - if (args.startsWith(superArgs)) { - val superLength = superArgs.length - if (args.length == superLength) None - else - // Note that the super def is called even when it's empty. - // Some super functions can be called without parentheses, but we just add them here for simplicity. - Some(args.iterator.drop(superLength).map(transform) - .mkString( - (if (superArgs.nonEmpty) s"super.$defName() ++ " else "") + "Seq(", - ",", - ")" - )) - } else - Some( - if (args.isEmpty) - s"Seq.empty[$elementType]" // The inferred type is `Seq[Nothing]` otherwise. - else args.iterator.map(transform).mkString("Seq(", ",", ")") - ) - - def renderSeqTaskDefWithSuper( - defName: String, - args: Seq[String], - superArgs: Seq[String] = Seq.empty, - elementType: String, - transform: String => String - ) = - renderSeqWithSuper(defName, args, superArgs, elementType, transform).map(s"def $defName = " + _) - - def renderArtifactName(name: String, dirs: Seq[String]): String = - if (dirs.nonEmpty && dirs.last == name) "" // skip default - else s"def artifactName = ${escape(name)}" - - def renderBomMvnDeps(args: IterableOnce[String]): String = - optional("def bomMvnDeps = super.bomMvnDeps() ++ Seq", args) - - def renderMvnDeps(args: IterableOnce[String]): String = - optional("def mvnDeps = Seq", args) - - def renderModuleDeps(args: IterableOnce[String]): String = - optional("def moduleDeps = super.moduleDeps ++ Seq", args) - - def renderCompileMvnDeps(args: IterableOnce[String]): String = - optional("def compileMvnDeps = Seq", args) - - def renderCompileModuleDeps(args: IterableOnce[String]): String = - optional("def compileModuleDeps = super.compileModuleDeps ++ Seq", args) - - def renderRunMvnDeps(args: IterableOnce[String]): String = - optional("def runMvnDeps = Seq", args) - - def renderRunModuleDeps(args: IterableOnce[String]): String = - optional("def runModuleDeps = super.runModuleDeps ++ Seq", args) - - def renderJavacOptions(args: Seq[String], superArgs: Seq[String] = Seq.empty): String = - renderSeqTaskDefWithSuper("javacOptions", args, superArgs, "String", escape).getOrElse("") - - def renderScalaVersion(arg: Option[String], superArg: Option[String] = None): String = - if (arg != superArg) arg.fold("")(scalaVersion => s"def scalaVersion = ${escape(scalaVersion)}") - else "" - - def renderScalacOptions( - args: Option[Seq[String]], - superArgs: Option[Seq[String]] = None - ): String = - renderSeqTaskDefWithSuper( - "scalacOptions", - args.getOrElse(Seq.empty), - superArgs.getOrElse(Seq.empty), - "String", - escape - ).getOrElse("") - - def renderRepositories(args: Seq[String], superArgs: Seq[String] = Seq.empty): String = - renderSeqTaskDefWithSuper( - "repositories", - args, - superArgs, - "String", - identity - ).getOrElse("") - - def renderResources(args: IterableOnce[os.SubPath]): String = - optional( - """def resources = Task { super.resources() ++ customResources() } - |def customResources = Task.Sources(""".stripMargin, - args.iterator.map(sub => escape(sub.toString())), - ", ", - ")" - ) - - def renderPomPackaging(packaging: String): String = - if (isNullOrEmpty(packaging) || "jar" == packaging) "" // skip default - else { - val pkg = if ("pom" == packaging) "PackagingType.Pom" else escape(packaging) - s"def pomPackagingType = $pkg" - } - - def renderPomParentProject(artifact: String): String = - if (isNullOrEmpty(artifact)) "" - else s"def pomParentProject = Some($artifact)" - - def renderPomSettings(arg: String | Null, superArg: String | Null = null): String = { - if (arg != superArg) - if (isNullOrEmpty(arg)) "" - else s"def pomSettings = $arg" - else "" - } - - def renderPublishVersion(arg: String | Null, superArg: String | Null = null): String = - if (arg != superArg) - if (isNullOrEmpty(arg)) "" - else s"def publishVersion = ${escape(arg)}" - else "" - - def renderPublishProperties( - args: Seq[(String, String)] - ): String = { - val tuples = args.iterator.map { case (k, v) => s"(${escape(k)}, ${escape(v)})" } - optional("def publishProperties = super.publishProperties() ++ Map", tuples) - } - - def renderJvmWorker(moduleName: String): String = - s"def jvmWorker = mill.api.ModuleRef($moduleName)" - - val testModulesByGroup: Map[String, String] = Map( - "junit" -> "TestModule.Junit4", - "org.junit.jupiter" -> "TestModule.Junit5", - "org.testng" -> "TestModule.TestNg", - "org.scalatest" -> "TestModule.ScalaTest", - "org.specs2" -> "TestModule.Specs2", - "com.lihaoyi" -> "TestModule.UTest", - "org.scalameta" -> "TestModule.Munit", - "com.disneystreaming" -> "TestModule.Weaver", - "dev.zio" -> "TestModule.ZioTest", - "org.scalacheck" -> "TestModule.ScalaCheck" - ) - - def writeBuildObject(tree: Tree[Node[BuildObject]], jvmId: Option[String]): Unit = { - val nodes = tree.nodes().toSeq - println(s"generated ${nodes.length} Mill build file(s)") - - println("removing existing Mill build files") - val workspace = os.pwd - mill.init.Util.buildFiles(workspace).foreach(os.remove.apply) - - nodes.foreach { node => - val file = buildFile(node.dirs) - val source = renderBuildSource(node, jvmId) - println(s"writing Mill build file to $file") - os.write(workspace / file, source) - } - } - - def renderTestModuleDecl( - testModule: String, - testModuleMainType: String, - testModuleExtraType: Option[String] - ): String = { - val name = backtickWrap(testModule) - testModuleExtraType match { - case Some(supertype) => s"object $name extends $testModuleMainType with $supertype" - case None => s"trait $name extends $testModuleMainType" - } - } - - @mainargs.main - case class BasicConfig( - @arg(doc = "name of generated base module trait defining shared settings", short = 'b') - baseModule: Option[String] = None, - @arg( - doc = "distribution and version of custom JVM to configure in --base-module", - short = 'j' - ) - jvmId: Option[String] = None, - @arg(doc = "name of generated nested test module", short = 't') - testModule: String = "test", - @arg(doc = "name of generated companion object defining dependency constants", short = 'd') - depsObject: Option[String] = None, - @arg(doc = "merge build files generated for a multi-module build", short = 'm') - merge: Flag = Flag() - ) - object BasicConfig { - implicit def parser: mainargs.ParserForClass[BasicConfig] = mainargs.ParserForClass[BasicConfig] - } - // TODO alternative names: `MavenAndGradleConfig`, `MavenAndGradleSharedConfig` - @mainargs.main - case class Config( - basicConfig: BasicConfig, - @arg(doc = "capture Maven publish properties", short = 'p') - publishProperties: Flag = Flag() - ) - - object Config { - implicit def configParser: mainargs.ParserForClass[Config] = mainargs.ParserForClass[Config] - } -} diff --git a/libs/init/buildgen/src/mill/main/buildgen/BuildRepr.scala b/libs/init/buildgen/src/mill/main/buildgen/BuildRepr.scala new file mode 100644 index 000000000000..c97cb4958643 --- /dev/null +++ b/libs/init/buildgen/src/mill/main/buildgen/BuildRepr.scala @@ -0,0 +1,42 @@ +package mill.main.buildgen + +/** + * A representation for a build defined as a tree of packages, where a package is a tree of modules. + */ +type BuildRepr = Tree[Tree[ModuleRepr]] +object BuildRepr { + + /** + * Returns a build with empty packages added for intermediate segments not in the given list. + */ + def fill(packages: Seq[Tree[ModuleRepr]]): BuildRepr = Tree.from(Seq.empty[String]): segments => + val pkg = packages.find(_.root.segments == segments).getOrElse(Tree(ModuleRepr(segments))) + val nextDepth = segments.length + 1 + val children = packages.iterator.collect: + case pkg + if pkg.root.segments.startsWith(segments) && pkg.root.segments.length >= nextDepth => + pkg.root.segments.take(nextDepth) + .distinct.toSeq + (pkg, children.sortBy(os.sub / _)) + + /** + * A build transformation that moves modules in nested packages into the root package. + */ + def merged(packages: BuildRepr) = { + def canUnify(pkg: Tree[Tree[ModuleRepr]], into: Seq[Tree[ModuleRepr]]): Boolean = + into.forall: modules => + modules.root.segments.last != pkg.root.root.segments.last && + !modules.root.testModule.exists(_.name == pkg.root.root.segments.last) && + pkg.children.forall(canUnify(_, pkg.root.children)) + val (unify, nested) = packages.children.partition(canUnify(_, packages.root.children)) + if (unify.isEmpty) packages + else Tree( + packages.root.copy(children = + packages.root.children ++ unify.map: pkg => + pkg.transform: (root, children) => + root.copy(children = root.children ++ children) + ), + nested + ) + } +} diff --git a/libs/init/buildgen/src/mill/main/buildgen/BuildWriter.scala b/libs/init/buildgen/src/mill/main/buildgen/BuildWriter.scala new file mode 100644 index 000000000000..2dbe05547ae2 --- /dev/null +++ b/libs/init/buildgen/src/mill/main/buildgen/BuildWriter.scala @@ -0,0 +1,668 @@ +package mill.main.buildgen + +import mill.constants.CodeGenConstants.{nestedBuildFileNames, rootBuildFileNames, rootModuleAlias} +import mill.constants.OutFiles.millBuild +import mill.internal.Util.backtickWrap +import mill.util.BuildInfo.millVersion +import pprint.Util.literalize + +import scala.reflect.TypeTest + +class BuildWriter( + build: BuildRepr, + metaBuild: Option[MetaBuildRepr] = None, + renderCrossValueInTask: String = "crossValue" +) { + + def writeFiles(): Unit = { + metaBuild.foreach { metaBuild => + val sub = os.sub / millBuild / rootBuildFileNames.get(0) + println(s"writing Mill meta-build file to $sub") + os.write(os.pwd / sub, renderMetaBuildRoot, createFolders = true) + import metaBuild.* + val sub1 = os.sub / millBuild / "src" / s"${depsObject.name}.scala" + println(s"writing Mill meta-build file to $sub1") + os.write(os.pwd / sub1, renderDepsObject(packageName, depsObject), createFolders = true) + baseTraits.foreach: baseTrait => + val sub = os.sub / millBuild / "src" / s"${baseTrait.name}.scala" + println(s"writing Mill meta-build file to $sub") + os.write(os.pwd / sub, renderBaseTrait(packageName, baseTrait)) + } + + val root +: nested = build.iterator.toSeq: @unchecked + val sub = os.sub / rootBuildFileNames.get(0) + println(s"writing Mill build file to $sub") + os.write( + os.pwd / rootBuildFileNames.get(0), + s"""${renderBuildHeader} + |${renderPackage(root)}""".stripMargin + ) + nested.foreach: pkg => + val sub = os.sub / pkg.root.segments / nestedBuildFileNames.get(0) + println(s"writing Mill build file to $sub") + os.write(os.pwd / sub, renderPackage(pkg)) + } + + def renderMetaBuildRoot = { + s"""package $rootModuleAlias + | + |import mill.meta.MillBuildRootModule + | + |object `package` extends MillBuildRootModule + |""".stripMargin + } + + def renderDepsObject(packageName: String, depsObject: MetaBuildRepr.DepsObject) = { + import depsObject.* + s"""package $packageName + | + |import mill.javalib._ + | + |object $name { + | + |${renderLines(refsByDep.toSeq.sortBy(_._2).iterator.map((dep, ref) => + s"val ${backtickWrap(ref)} = $dep" + ))} + |}""".stripMargin + } + + def renderBaseTrait(packageName: String, baseTrait: MetaBuildRepr.BaseTrait) = { + import baseTrait.* + s"""package $packageName + | + |${renderImports(baseTrait)} + | + |trait $name ${renderExtendsClause(supertypes ++ mixins)} { + | + | ${renderModuleConfigs(configs, crossConfigs)} + |}""".stripMargin + } + + def renderImports(baseTrait: MetaBuildRepr.BaseTrait): String = { + val b = Set.newBuilder[String] + import baseTrait.* + (supertypes ++ mixins).foreach: + case "MavenModule" => b += "mill.javalib" + case "PublishModule" => b += "mill.javalib" += "mill.javalib.publish" + case "ErrorProneModule" => b += "mill.javalib" += "mill.javalib.errorprone" + case "ScalaModule" | "SbtModule" | "CrossSbtModule" | "CrossSbtPlatformModule" => + b += "mill.scalalib" + case s if s.startsWith("SbtPlatformModule") => b += "mill.scalalib" + case "ScalaJSModule" => b += "mill.scalalib" += "mill.scalajslib" + case "ScalaNativeModule" => b += "mill.scalalib" += "mill.scalanativelib" + case _ => + + configs.foreach: + case _: CoursierModuleConfig => b += "mill.javalib" + case _: JavaHomeModuleConfig => b += "mill.javalib" + case _: RunModuleConfig => b += "mill.javalib" + case _: JavaModuleConfig => b += "mill.javalib" + case _: PublishModuleConfig => b += "mill.javalib" += "mill.javalib.publish" + case _: ErrorProneModuleConfig => b += "mill.javalib" += "mill.javalib.errorprone" + case _: ScalaModuleConfig => b += "mill.scalalib" + case _: ScalaJSModuleConfig => b += "mill.scalalib" += "mill.scalajslib" + case _: ScalaNativeModuleConfig => b += "mill.scalalib" += "mill.scalanativelib" + + renderLines(b.result().toSeq.sorted.iterator.map(s => s"import $s._")) + } + + def renderBuildHeader = { + val millJvmVersionHeader = ( + metaBuild.iterator.flatMap(_.baseTraits).flatMap(_.configs) ++ + build.iterator.flatMap(_.iterator).flatMap(_.configs) + ).collect { + case config: JavaModuleConfig => config.javacOptions + }.flatMap(javacOptions => + Option(1 + javacOptions.indexOf("--release")).filter(_ > 0) + .orElse(Option(1 + javacOptions.indexOf("-target")).filter(_ > 0)) + .map(javacOptions(_).stripPrefix("1.").toInt) + ).minOption.map(v => s"//| mill-jvm-version: ${JavaHomeModuleConfig.jvmId(v)}") + renderLines( + s"//| mill-version: $millVersion" +: millJvmVersionHeader.toSeq + ) + } + + def renderPackage(pkg: Tree[ModuleRepr]) = { + import pkg.* + s"""package ${(rootModuleAlias +: root.segments.map(backtickWrap)).mkString(".")} + | + |${renderImports(pkg)} + | + |${renderModuleDeclaration("package", root)} { + | + | ${renderModuleConfigs(root.configs, root.crossConfigs)} + | + | ${root.testModule.fold("")(renderTestModule)} + | + | ${renderLines(children.iterator.map(renderModuleSubtree))} + |}""".stripMargin + } + + def renderImports(pkg: Tree[ModuleRepr]): String = { + val b = Set.newBuilder[String] + b ++= metaBuild.map(_.packageName) + for module <- pkg.iterator do + if (module.supertypes.contains("Module") || module.crossConfigs.nonEmpty) b += "mill.api" + if (module.testModule.exists(_.mixins.exists(_.startsWith("TestModule")))) + b += "mill.javalib" + module.configs.foreach: + case _: CoursierModuleConfig => b += "mill.javalib" + case _: JavaHomeModuleConfig => b += "mill.javalib" + case _: RunModuleConfig => b += "mill.javalib" + case _: JavaModuleConfig => b += "mill.javalib" + case _: PublishModuleConfig => b += "mill.javalib" += "mill.javalib.publish" + case _: ErrorProneModuleConfig => b += "mill.javalib" += "mill.javalib.errorprone" + case _: ScalaModuleConfig => b += "mill.scalalib" + case _: ScalaJSModuleConfig => b += "mill.scalalib" += "mill.scalajslib" + case _: ScalaNativeModuleConfig => b += "mill.scalalib" += "mill.scalanativelib" + + renderLines(b.result().toSeq.sorted.iterator.map(s => s"import $s._")) + } + + def renderModuleSubtree(modules: Tree[ModuleRepr]): String = { + import modules.* + s"""${renderModuleDeclaration(root.segments.last, root)} { + | + | ${renderModuleConfigs(root.configs, root.crossConfigs)} + | + | ${root.testModule.fold("")(renderTestModule)} + | + | ${renderLines(children.iterator.map(renderModuleSubtree))} + |}""".stripMargin + } + + def renderModuleDeclaration(name: String, module: ModuleRepr) = { + import module.* + if (crossConfigs.isEmpty) + s"object ${backtickWrap(name)} ${renderExtendsClause(supertypes ++ mixins)}" + else + val crossTraitName = segments.lastOption.getOrElse(os.pwd.last).split("\\W").map(_.capitalize) + .mkString("", "", "Module") + val crossModuleExtends = crossConfigs.map((v, _) => literalize(v)) + .mkString(s"extends Cross[$crossTraitName](", ", ", ")") + s"""object ${backtickWrap(name)} $crossModuleExtends + |trait $crossTraitName ${renderExtendsClause(supertypes ++ mixins)}""".stripMargin + } + + def renderExtendsClause(supertypes: Seq[String]) = { + if (supertypes.isEmpty) "" + else + val head +: tail = supertypes: @unchecked + s" extends $head" + tail.map(" with " + _).mkString + } + + def renderTestModule(module: TestModuleRepr) = { + import module.* + s"""object $name ${renderExtendsClause(supertypes ++ mixins)} { + | + | ${renderModuleConfigs(configs, crossConfigs)} + | + | ${if (testParallelism) "" else "def testParallelism = false"} + | + | ${if (testSandboxWorkingDir) "" else "def testSandboxWorkingDir = false"} + |}""".stripMargin + } + + def renderModuleConfigs( + configs: Seq[ModuleConfig], + crossConfigs: Seq[(String, Seq[ModuleConfig])] + ) = { + def collect[T <: ModuleConfig](using T: TypeTest[ModuleConfig, T]) = crossConfigs.collect: + case (k, T(t)) => (k, t) + renderLines(configs.iterator.map: + case config: CoursierModuleConfig => renderCoursierModuleConfig(config, collect) + case config: JavaHomeModuleConfig => renderJavaHomeModuleConfig(config, collect) + case config: RunModuleConfig => renderRunModuleConfig(config, collect) + case config: JavaModuleConfig => renderJavaModuleConfig(config, collect) + case config: PublishModuleConfig => renderPublishModuleConfig(config, collect) + case config: ErrorProneModuleConfig => renderErrorProneModuleConfig(config, collect) + case config: ScalaModuleConfig => renderScalaModuleConfig(config, collect) + case config: ScalaJSModuleConfig => renderScalaJSModuleConfig(config, collect) + case config: ScalaNativeModuleConfig => renderScalaNativeModuleConfig(config, collect)) + } + + def renderCoursierModuleConfig( + config: CoursierModuleConfig, + crossConfigs: Seq[(String, CoursierModuleConfig)] + ) = { + def get[A](f: CoursierModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderRepositories(repositories, get(_.repositories))} + |""".stripMargin + } + + def renderJavaHomeModuleConfig( + config: JavaHomeModuleConfig, + crossConfigs: Seq[(String, JavaHomeModuleConfig)] + ) = { + def get[A](f: JavaHomeModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderJvmId(jvmId, get(_.jvmId))} + |""".stripMargin + } + + def renderRunModuleConfig( + config: RunModuleConfig, + crossConfigs: Seq[(String, RunModuleConfig)] + ) = { + def get[A](f: RunModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderForkWorkingDir(forkWorkingDir, get(_.forkWorkingDir))} + |""".stripMargin + } + + def renderJavaModuleConfig( + config: JavaModuleConfig, + crossConfigs: Seq[(String, JavaModuleConfig)] + ) = { + def get[A](f: JavaModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderMandatoryMvnDeps(mandatoryMvnDeps, get(_.mandatoryMvnDeps))} + | + |${renderMvnDeps(mvnDeps, get(_.mvnDeps))} + | + |${renderCompileMvnDeps(compileMvnDeps, get(_.compileMvnDeps))} + | + |${renderRunMvnDeps(runMvnDeps, get(_.runMvnDeps))} + | + |${renderBomMvnDeps(bomMvnDeps, get(_.bomMvnDeps))} + | + |${renderModuleDeps(moduleDeps, get(_.moduleDeps))} + | + |${renderCompileModuleDeps(compileModuleDeps, get(_.compileModuleDeps))} + | + |${renderRunModuleDeps(runModuleDeps, get(_.runModuleDeps))} + | + |${renderJavacOptions(javacOptions, get(_.javacOptions))} + |""".stripMargin + } + + def renderPublishModuleConfig( + config: PublishModuleConfig, + crossConfigs: Seq[(String, PublishModuleConfig)] + ) = { + def get[A](f: PublishModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderPomPackagingType(pomPackagingType, get(_.pomPackagingType))} + | + |${renderPomParentProject(pomParentProject, get(_.pomParentProject))} + | + |${renderPomSettings(pomSettings, get(_.pomSettings))} + | + |${renderPublishVersion(publishVersion, get(_.publishVersion))} + | + |${renderVersionScheme(versionScheme, get(_.versionScheme))} + | + |${renderArtifactMetadata(artifactMetadata, get(_.artifactMetadata))} + | + |${renderPublishProperties(publishProperties, get(_.publishProperties))} + |""".stripMargin + } + + def renderErrorProneModuleConfig( + config: ErrorProneModuleConfig, + crossConfigs: Seq[(String, ErrorProneModuleConfig)] + ) = { + def get[A](f: ErrorProneModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderErrorProneOptions(errorProneOptions, get(_.errorProneOptions))} + | + |${renderErrorProneJavacEnableOptions( + errorProneJavacEnableOptions, + get(_.errorProneJavacEnableOptions) + )} + | + |${renderErrorProneDeps(errorProneDeps, get(_.errorProneDeps))} + |""".stripMargin + } + + def renderScalaModuleConfig( + config: ScalaModuleConfig, + crossConfigs: Seq[(String, ScalaModuleConfig)] + ) = { + def get[A](f: ScalaModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderScalaVersion(scalaVersion, get(_.scalaVersion))} + | + |${renderScalacOptions(scalacOptions, get(_.scalacOptions))} + | + |${renderScalacPluginMvnDeps(scalacPluginMvnDeps, get(_.scalacPluginMvnDeps))} + |""".stripMargin + } + + def renderScalaJSModuleConfig( + config: ScalaJSModuleConfig, + crossConfigs: Seq[(String, ScalaJSModuleConfig)] + ) = { + def get[A](f: ScalaJSModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderScalaJSVersion(scalaJSVersion, get(_.scalaJSVersion))} + |""".stripMargin + } + + def renderScalaNativeModuleConfig( + config: ScalaNativeModuleConfig, + crossConfigs: Seq[(String, ScalaNativeModuleConfig)] + ) = { + def get[A](f: ScalaNativeModuleConfig => A) = crossConfigs.map((k, v) => (k, f(v))) + import config.* + s"""${renderScalaNativeVersion(scalaNativeVersion, get(_.scalaNativeVersion))} + |""".stripMargin + } + + def renderRepositories(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("repositories", values, crossValues, literalize(_)) + + def renderJvmId(value: String, crossValues: Seq[(String, String)]) = + renderTask("jvmId", value, crossValues, literalize(_)) + + def renderForkWorkingDir(value: String, crossValues: Seq[(String, String)]) = + renderTask("forkWorkingDir", value, crossValues, identity) + + def renderMandatoryMvnDeps(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("mandatoryMvnDeps", values, crossValues, identity) + + def renderMvnDeps(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("mvnDeps", values, crossValues, identity) + + def renderCompileMvnDeps(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("compileMvnDeps", values, crossValues, identity) + + def renderRunMvnDeps(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("runMvnDeps", values, crossValues, identity) + + def renderBomMvnDeps(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("bomMvnDeps", values, crossValues, identity) + + def renderModuleDeps( + values: Seq[JavaModuleConfig.ModuleDep], + crossValues: Seq[(String, Seq[JavaModuleConfig.ModuleDep])] + ) = renderMemberSeq("moduleDeps", values, crossValues, renderModuleDep) + + def renderCompileModuleDeps( + values: Seq[JavaModuleConfig.ModuleDep], + crossValues: Seq[(String, Seq[JavaModuleConfig.ModuleDep])] + ) = renderMemberSeq("compileModuleDeps", values, crossValues, renderModuleDep) + + def renderRunModuleDeps( + values: Seq[JavaModuleConfig.ModuleDep], + crossValues: Seq[(String, Seq[JavaModuleConfig.ModuleDep])] + ) = renderMemberSeq("runModuleDeps", values, crossValues, renderModuleDep) + + def renderJavacOptions( + values: Seq[String], + crossValues: Seq[(String, Seq[String])] + ) = renderTaskSeq("javacOptions", values, crossValues, literalize(_)) + + def renderPomPackagingType(value: String, crossValues: Seq[(String, String)]) = + renderMember("pomPackagingType", value, crossValues, literalize(_)) + + def renderPomParentProject( + value: Option[PublishModuleConfig.Artifact], + crossValues: Seq[(String, Option[PublishModuleConfig.Artifact])] + ) = renderTaskOption("pomParentProject", value, crossValues, renderArtifact) + + def renderPomSettings( + value: PublishModuleConfig.PomSettings, + crossValues: Seq[(String, PublishModuleConfig.PomSettings)] + ): String = renderTask("pomSettings", value, crossValues, renderPomSettings) + + def renderPublishVersion(value: String, crossValues: Seq[(String, String)]) = + renderTask("publishVersion", value, crossValues, literalize(_)) + + def renderVersionScheme( + value: Option[String], + crossValues: Seq[(String, Option[String])] + ): String = renderTaskOption("versionScheme", value, crossValues, renderVersionScheme) + + def renderArtifactMetadata( + value: PublishModuleConfig.Artifact, + crossValues: Seq[(String, PublishModuleConfig.Artifact)] + ) = renderTask("artifactMetadata", value, crossValues, renderArtifact) + + def renderPublishProperties( + values: Map[String, String], + crossValues: Seq[(String, Map[String, String])] + ) = renderTaskMap("publishProperties", values, crossValues, literalize(_), literalize(_)) + + def renderErrorProneOptions(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("errorProneOptions", values, crossValues, literalize(_)) + + def renderErrorProneJavacEnableOptions( + values: Seq[String], + crossValues: Seq[(String, Seq[String])] + ) = renderTaskSeq("errorProneJavacEnableOptions", values, crossValues, literalize(_)) + + def renderErrorProneDeps(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("errorProneDeps", values, crossValues, identity) + + def renderScalaVersion(value: String, crossValues: Seq[(String, String)]) = + renderTask("scalaVersion", value, crossValues, literalize(_)) + + def renderScalacOptions(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("scalacOptions", values, crossValues, literalize(_)) + + def renderScalacPluginMvnDeps(values: Seq[String], crossValues: Seq[(String, Seq[String])]) = + renderTaskSeq("scalacPluginMvnDeps", values, crossValues, identity) + + def renderScalaJSVersion(value: String, crossValues: Seq[(String, String)]) = + renderTask("scalaJSVersion", value, crossValues, literalize(_)) + + def renderScalaNativeVersion(value: String, crossValues: Seq[(String, String)]) = + renderTask("scalaNativeVersion", value, crossValues, literalize(_)) + + def renderModuleDep(dep: JavaModuleConfig.ModuleDep) = { + import dep.* + rootModuleAlias + crossArgs.getOrElse(-1, "") + segments.indices + .map(i => "." + backtickWrap(segments(i)) + crossArgs.getOrElse(i, "")) + .mkString + } + + def renderPomSettings(value: PublishModuleConfig.PomSettings): String = { + import value.* + s"PomSettings(${ + literalize(if (description == null) "" else description) + }, ${ + literalize(if (organization == null) "" else organization) + }, ${ + literalize(if (url == null) "" else url) + }, ${ + licenses.map(renderLicense).mkString("Seq(", ", ", ")") + }, ${ + renderVersionControl(versionControl) + }, ${ + developers.map(renderDeveloper).mkString("Seq(", ", ", ")") + })" + } + + def renderLicense(value: PublishModuleConfig.License) = { + import value.* + s"License(${ + literalize(if (id == null) "" else id) + }, ${ + literalize(if (name == null) "" else name) + }, ${ + literalize(if (url == null) "" else url) + }, ${ + isOsiApproved + }, ${ + isFsfLibre + }, ${ + literalize(if (distribution == null) "" else distribution) + })" + } + + def renderVersionControl(value: PublishModuleConfig.VersionControl) = { + import value.* + s"VersionControl(${ + browsableRepository.fold("None")(v => s"Some(${literalize(v)})") + }, ${ + connection.fold("None")(v => s"Some(${literalize(v)})") + }, ${ + developerConnection.fold("None")(v => s"Some(${literalize(v)})") + }, ${ + tag.fold("None")(v => s"Some(${literalize(v)})") + })" + } + + def renderDeveloper(value: PublishModuleConfig.Developer) = { + import value.* + s"Developer(${ + literalize(if (id == null) "" else id) + }, ${ + literalize(if (name == null) "" else name) + }, ${ + literalize(if (url == null) "" else url) + }, ${ + organization.fold("None")(v => s"Some(${literalize(v)})") + }, ${ + organizationUrl.fold("None")(v => s"Some(${literalize(v)})") + })" + } + + def renderArtifact(value: PublishModuleConfig.Artifact) = { + import value.* + s"Artifact(${literalize(group)}, ${literalize(id)}, ${literalize(version)})" + } + + def renderVersionScheme(value: String): String = value match { + case "early-semver" => "VersionScheme.EarlySemVer" + case "pvp" => "VersionScheme.PVP" + case "semver-spec" => "VersionScheme.SemVerSpec" + case "strict" => "VersionScheme.Strict" + } + + def renderLines(values: IterableOnce[String]) = values.iterator.mkString( + """ + |""".stripMargin + ) + + def renderMember[A]( + name: String, + value: A, + crossValues: Seq[(String, A)], + renderValue: A => String + ) = { + val crossValues0 = crossValues.collect: + case (k, v) if v != null => (k, v) + if (value == null && crossValues0.isEmpty) "" + else + val value0 = if (value == null) s"super.$name" else renderValue(value) + s"def $name = " + + (if (crossValues0.isEmpty) value0 + else + s"""crossValue match { + |${renderLines(crossValues.groupMap(_._2)(_._1).iterator.map((v, crosses) => + s" case ${crosses.map(literalize(_)).mkString(" | ")} => ${renderValue(v)}" + ))} + | case _ => $value0 + |}""".stripMargin) + } + + def renderMemberSeq[A]( + name: String, + values: Seq[A], + crossValues: Seq[(String, Seq[A])], + renderValue: A => String + ) = + if (values.isEmpty && crossValues.isEmpty) "" + else s"def $name = super.$name" + + (if (values.isEmpty) "" else s" ++ Seq(${values.iterator.map(renderValue).mkString(", ")})") + + (if (crossValues.isEmpty) "" + else + s""" ++ (crossValue match { + |${ + renderLines(crossValues.groupMap(_._2)(_._1).iterator.map((v, crosses) => + s" case ${crosses.map(literalize(_)).mkString(" | ")} => Seq(${v.map(renderValue).mkString(", ")})" + )) + } + | case _ => Nil + |}""".stripMargin) + + def renderTask[A]( + name: String, + value: A, + crossValues: Seq[(String, A)], + renderValue: A => String + ) = { + val crossValues0 = crossValues.collect: + case (k, v) if v != null => (k, renderValue(v)) + if (value == null && crossValues0.isEmpty) "" + else + val value0 = if (value == null) s"super.$name()" else renderValue(value) + s"def $name = " + + (if (crossValues0.isEmpty) value0 + else + s"""$renderCrossValueInTask match { + |${renderLines(crossValues.groupMap(_._2)(_._1).iterator.map((v, crosses) => + s" case ${crosses.map(literalize(_)).mkString(" | ")} => $v" + ))} + | case _ => $value0 + |}""".stripMargin) + } + + def renderTaskOption[A]( + name: String, + value: Option[A], + crossValues: Seq[(String, Option[A])], + renderValue: A => String + ) = { + val crossValues0 = crossValues.collect: + case (k, Some(v)) if v != null => (k, s"Some(${renderValue(v)})") + if (value.isEmpty && crossValues0.isEmpty) "" + else + val value0 = value.fold("None")(value => s"Some(${renderValue(value)})") + s"def $name = " + + (if (crossValues0.isEmpty) value0 + else + s"""$renderCrossValueInTask match { + |${renderLines(crossValues.groupMap(_._2)(_._1).iterator.map((v, crosses) => + s" case ${crosses.map(literalize(_)).mkString(" | ")} => $v}" + ))} + | case _ => $value0 + |}""".stripMargin) + } + + def renderTaskMap[K, V]( + name: String, + values: Map[K, V], + crossValues: Seq[(String, Map[K, V])], + renderKey: K => String, + renderValue: V => String + ) = + if (values.isEmpty && crossValues.isEmpty) "" + else s"def $name = super.$name()" + + (if (values.isEmpty) "" + else + s" ++ Map(${values.map((k, v) => s"(${renderKey(k)}, ${renderValue(v)})").mkString(", ")})") + + (if (crossValues.isEmpty) "" + else + s""" ++ ($renderCrossValueInTask match { + |${renderLines(crossValues.groupMap(_._2)(_._1).iterator.map((v, crosses) => + s" case ${crosses.map(literalize(_)).mkString(" | ")} => Map(${v.map((k, v) => + s"(${renderKey(k)}, ${renderValue(v)})" + ).mkString(", ")})" + ))} + | case _ => Map() + |})""".stripMargin) + + def renderTaskSeq[A]( + name: String, + values: Seq[A], + crossValues: Seq[(String, Seq[A])], + renderValue: A => String + ) = + if (values.isEmpty && crossValues.isEmpty) "" + else s"def $name = super.$name()" + + (if (values.isEmpty) "" else s" ++ Seq(${values.iterator.map(renderValue).mkString(", ")})") + + (if (crossValues.isEmpty) "" + else + s""" ++ ($renderCrossValueInTask match { + |${ + renderLines(crossValues.groupMap(_._2)(_._1).iterator.map((v, crosses) => + s" case ${crosses.map(literalize(_)).mkString(" | ")} => Seq(${v.map(renderValue).mkString(", ")})" + )) + } + | case _ => Nil + |}""".stripMargin) +} diff --git a/libs/init/buildgen/src/mill/main/buildgen/MetaBuildRepr.scala b/libs/init/buildgen/src/mill/main/buildgen/MetaBuildRepr.scala new file mode 100644 index 000000000000..923de6e32281 --- /dev/null +++ b/libs/init/buildgen/src/mill/main/buildgen/MetaBuildRepr.scala @@ -0,0 +1,146 @@ +package mill.main.buildgen + +import mill.internal.Util.backtickWrap + +import scala.collection.mutable + +case class MetaBuildRepr( + packageName: String, + baseTraits: Seq[MetaBuildRepr.BaseTrait], + depsObject: MetaBuildRepr.DepsObject +) +object MetaBuildRepr { + + def of(build: BuildRepr, packageName: String = "millbuild") = { + val deps = DepsObject("Deps") + def withDepRef(configs: Seq[ModuleConfig]) = configs.map { + case config: JavaModuleConfig => config.copy( + mandatoryMvnDeps = config.mandatoryMvnDeps.map(deps.renderRef), + mvnDeps = config.mvnDeps.map(deps.renderRef), + compileMvnDeps = config.compileMvnDeps.map(deps.renderRef), + runMvnDeps = config.runMvnDeps.map(deps.renderRef), + bomMvnDeps = config.bomMvnDeps.map(deps.renderRef) + ) + case config: ErrorProneModuleConfig => config.copy( + errorProneDeps = config.errorProneDeps.map(deps.renderRef) + ) + case config: ScalaModuleConfig => config.copy( + scalacPluginMvnDeps = config.scalacPluginMvnDeps.map(deps.renderRef) + ) + case config => config + } + var packages = build.map: pkg => + pkg.map: module => + module.copy( + configs = withDepRef(module.configs), + crossConfigs = module.crossConfigs.map((k, v) => (k, withDepRef(v))), + testModule = + module.testModule.map(test => + test.copy( + configs = withDepRef(test.configs), + crossConfigs = test.crossConfigs.map((k, v) => (k, withDepRef(v))) + ) + ) + ) + val baseTraits = if (packages.root.children.isEmpty && packages.children.isEmpty) Nil + else + def abstractedBase(suffix: String, modules: Seq[ModuleRepr]): Option[BaseTrait] = + Option.when(modules.length > 1): + modules.iterator.reduce: (m1, m2) => + m1.copy( + supertypes = m1.supertypes.intersect(m2.supertypes), + mixins = if (m1.mixins == m2.mixins) m1.mixins else Nil, + configs = ModuleConfig.abstracted(m1.configs, m2.configs), + crossConfigs = m1.crossConfigs.flatMap: (cross, configs1) => + m2.crossConfigs.collectFirst: + case (`cross`, configs2) => (cross, ModuleConfig.abstracted(configs1, configs2)) + ) + .collect: + case module + if (module.configs.nonEmpty || module.crossConfigs.nonEmpty) && (module.supertypes.nonEmpty || module.mixins.nonEmpty) => + import module.* + BaseTrait( + name = os.pwd.last.split("\\W").map(_.capitalize).mkString("", "", suffix), + supertypes, + mixins, + configs, + crossConfigs + ) + val baseTrait = abstractedBase( + "BaseModule", + packages.iterator.flatMap(_.iterator).filter(_.configs.nonEmpty).toSeq + ) + baseTrait.foreach: base => + packages = packages.map: pkg => + pkg.map: module => + if (module.configs.isEmpty) module + else base.inherited(module) + // If the first base trait has no publish settings, we attempt to generate one that does. + val publishTrait = + if (baseTrait.exists(_.configs.exists(_.isInstanceOf[PublishModuleConfig]))) None + else abstractedBase( + "PublishModule", + packages.iterator.flatMap(_.iterator) + .filter(_.configs.exists(_.isInstanceOf[PublishModuleConfig])) + .toSeq + ) + publishTrait.foreach: base => + packages = packages.map: pkg => + pkg.map: module => + if (module.configs.exists(_.isInstanceOf[PublishModuleConfig])) base.inherited(module) + else module + baseTrait.toSeq ++ publishTrait.toSeq + (packages, MetaBuildRepr(packageName, baseTraits, deps)) + } + + case class BaseTrait( + name: String, + supertypes: Seq[String] = Seq("Module"), + mixins: Seq[String] = Nil, + configs: Seq[ModuleConfig] = Nil, + crossConfigs: Seq[(String, Seq[ModuleConfig])] = Nil + ) { + + def inherited(module: ModuleRepr): ModuleRepr = module.copy( + supertypes = module.supertypes.diff(supertypes) :+ name, + mixins = if (module.mixins == mixins) Nil else module.mixins, + configs = ModuleConfig.inherited(module.configs, configs), + crossConfigs = module.crossConfigs.map: (cross, configs1) => + crossConfigs.collectFirst: + case (`cross`, configs2) => (cross, ModuleConfig.inherited(configs1, configs2)) + .getOrElse((cross, configs1)) + ) + } + + case class DepsObject(name: String, refsByDep: mutable.Map[String, String] = mutable.Map.empty) { + private val artifactRegex = """:([^:"]+)[:"]""".r + + def renderRef(dep: String) = { + /* + We use the artifact name as the seed for the reference. When a name collision occurs, a + suffix is added that starts with the '#' character. This forces backticks in the final name, + making the "duplicate" stand out visually in the output. The reference without backticks is + saved in the map so that it is grouped together with the "original", on sort, in the output. + Example output: + val catsCore = mvn"org.typelevel::cats-core:2.0.0" + val `catsCore#0` = mvn"org.typelevel::cats-core:2.6.1" + val disciplineCore = mvn"org.typelevel::discipline-core::1.7.0" + val `disciplineCore#0` = mvn"org.typelevel::discipline-core:1.7.0" + val disciplineMunit = mvn"org.typelevel::discipline-munit:2.0.0" + val `disciplineMunit#0` = mvn"org.typelevel::discipline-munit::2.0.0" + */ + var ref = refsByDep.getOrElse(dep, null) + if (ref == null) { + val artifact = artifactRegex.findFirstMatchIn(dep).get.group(1) + ref = artifact.split("\\W") match + case Array(head) => head + case parts => parts.tail.map(_.capitalize).mkString(parts.head, "", "") + if (refsByDep.valuesIterator.contains(ref)) + ref += "#" + ref += refsByDep.valuesIterator.count(_.startsWith(ref)) + refsByDep.put(dep, ref) + } + name + "." + backtickWrap(ref) + } + } +} diff --git a/libs/init/buildgen/src/mill/main/buildgen/OptionNodeTree.scala b/libs/init/buildgen/src/mill/main/buildgen/OptionNodeTree.scala deleted file mode 100644 index c136d5b3bfbd..000000000000 --- a/libs/init/buildgen/src/mill/main/buildgen/OptionNodeTree.scala +++ /dev/null @@ -1,33 +0,0 @@ -package mill.main.buildgen - -def toTree[T](prefixDirs: Seq[String], dirs: List[String], node: Node[T]): Tree[Node[Option[T]]] = - dirs match - case Nil => Tree(node.copy(value = Some(node.value))) - case dir :: nextDirs => - Tree(Node(prefixDirs, None), Seq(toTree(prefixDirs :+ dir, nextDirs, node))) - -def merge[T]( - tree: Tree[Node[Option[T]]], - prefixDirs: Seq[String], - dirs: List[String], - node: Node[T] -): Tree[Node[Option[T]]] = - dirs match - case Nil => tree.copy(node = - tree.node.value.fold(node.copy(value = Some(node.value)))(existingProject => - throw IllegalArgumentException( - s"Project at duplicate locations: $existingProject and $node" - ) - ) - ) - case dir :: nextDirs => - tree.copy(children = { - def nextPrefixDirs = prefixDirs :+ dir - tree.children.iterator.zipWithIndex.find(_._1.node.dirs.last == dir) match - case Some((childTree, index)) => - tree.children.updated(index, merge(childTree, nextPrefixDirs, nextDirs, node)) - case None => tree.children :+ toTree(nextPrefixDirs, nextDirs, node) - }) - -def merge[T](tree: Tree[Node[Option[T]]], node: Node[T]): Tree[Node[Option[T]]] = - merge(tree, Seq.empty, node.dirs.toList, node) diff --git a/libs/init/buildgen/src/mill/main/buildgen/Tree.scala b/libs/init/buildgen/src/mill/main/buildgen/Tree.scala index a3c3718f2794..9d2b4fb32518 100644 --- a/libs/init/buildgen/src/mill/main/buildgen/Tree.scala +++ b/libs/init/buildgen/src/mill/main/buildgen/Tree.scala @@ -1,85 +1,23 @@ package mill.main.buildgen -import geny.Generator - /** - * A recursive data structure that defines parent-child relationships between nodes. - * - * @param node the root node of this tree - * @param children the child subtrees of this tree + * A data structure that captures parent-child relationships between elements. */ -@mill.api.experimental -case class Tree[+Node](node: Node, children: Seq[Tree[Node]] = Seq.empty) { - - def map[Out](f: Node => Out): Tree[Out] = - transform[Out]((node, children) => Tree(f(node), children.iterator.toSeq)) - - def nodes(traversal: Tree.Traversal = Tree.Traversal.DepthFirst): Generator[Node] = - subtrees(traversal).map(_.node) +case class Tree[+A](root: A, children: Seq[Tree[A]] = Nil) { - def subtrees(traversal: Tree.Traversal): Generator[Tree[Node]] = - traversal.subtrees(this) + def iterator: Iterator[A] = Iterator(root) ++ children.iterator.flatMap(_.iterator) - def transform[Out](f: (Node, IterableOnce[Tree[Out]]) => Tree[Out]): Tree[Out] = { - def recurse(tree: Tree[Node]): Tree[Out] = - f(tree.node, tree.children.iterator.map(recurse)) + def map[B](f: A => B): Tree[B] = Tree(f(root), children.map(_.map(f))) - recurse(this) - } + def transform[B](f: (A, Seq[Tree[B]]) => Tree[B]): Tree[B] = f(root, children.map(_.transform(f))) } -@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 subtrees[Node](root: Tree[Node]): Generator[Tree[Node]] - } - object Traversal { - - object BreadthFirst extends Traversal { - - def subtrees[Node](root: Tree[Node]): Generator[Tree[Node]] = handleItem => { - @annotation.tailrec - def recurse(level: Seq[Tree[Node]]): Generator.Action = { - var last: Generator.Action = Generator.Continue - var index = 0 - while (last == Generator.Continue && index < level.length) { - last = handleItem(level(index)) - index += 1 - } - val nextLevel = level.flatMap(_.children) - if (last == Generator.Continue && nextLevel.nonEmpty) recurse(nextLevel) else last - } - - recurse(Seq(root)) - } - } - - object DepthFirst extends Traversal { - - def subtrees[Node](root: Tree[Node]): Generator[Tree[Node]] = handleItem => { - def recurse(tree: Tree[Node]): Generator.Action = { - var last: Generator.Action = Generator.Continue - var index = 0 - while (last == Generator.Continue && index < tree.children.length) { - last = recurse(tree.children(index)) - index += 1 - } - if (last == Generator.Continue) handleItem(tree) else last - } - - recurse(root) - } + def from[S, A](init: S)(step: S => (A, Seq[S])): Tree[A] = { + def recurse(state: S): Tree[A] = { + val (a, states) = step(state) + Tree(a, states.map(recurse)) } + recurse(init) } } diff --git a/libs/init/buildgen/src/mill/main/buildgen/ir.scala b/libs/init/buildgen/src/mill/main/buildgen/ir.scala deleted file mode 100644 index 339174a02e6c..000000000000 --- a/libs/init/buildgen/src/mill/main/buildgen/ir.scala +++ /dev/null @@ -1,168 +0,0 @@ -package mill.main.buildgen - -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] -} - -/** - * A node representing a module in a build tree. - * - * @param dirs relative location in the build tree - * @param value build module - */ -@mill.api.experimental -case class Node[T](dirs: Seq[String], value: T) - -case class IrTrait( - jvmId: Option[String], - baseModule: String, - moduleSupertypes: Seq[String], - javacOptions: Seq[String], - scalaVersion: Option[String], - scalacOptions: Option[Seq[String]], - pomSettings: IrPom | Null, - publishVersion: String | Null, - publishProperties: Seq[(String, String)], - repositories: Seq[String] -) - -case class IrPom( - description: String, - organization: String, - url: String, - licenses: Seq[IrLicense], - versionControl: IrVersionControl, - developers: Seq[IrDeveloper] -) - -case class IrVersionControl(url: String, connection: String, devConnection: String, tag: String) -case class IrDeveloper( - id: String, - name: String, - url: String, - organization: String, - organizationUrl: String -) - -case class IrArtifact(group: String, id: String, version: String) -case class IrLicense( - id: String, - name: String, - url: String, - isOsiApproved: Boolean = false, - isFsfLibre: Boolean = false, - distribution: String = "repo" -) - -// TODO Consider renaming to `IrModule(Build)` to disambiguate? `sbt`, for example, uses `ThisBuild` and `buildSettings` to refer to the whole build. -// TODO reuse the members in `IrTrait`? -case class IrBuild( - scopedDeps: IrScopedDeps, - testModule: String, - testModuleMainType: String, - hasTest: Boolean, - dirs: Seq[String], - repositories: Seq[String], - javacOptions: Seq[String], - scalaVersion: Option[String], - scalacOptions: Option[Seq[String]], - projectName: String, - pomSettings: IrPom | Null, - publishVersion: String | Null, - packaging: String | Null, - pomParentArtifact: IrArtifact | Null, - resources: Seq[os.SubPath], - testResources: Seq[os.SubPath], - publishProperties: Seq[(String, String)], - jvmId: Option[String], - testForkDir: Option[String] -) - -object IrBuild { - // TODO not used - def empty(dirs: Seq[String]) = IrBuild( - IrScopedDeps(), - null, - null, - false, - dirs, - Seq.empty, - Seq.empty, - None, - None, - dirs.last, - null, - null, - null, - null, - Seq.empty, - Seq.empty, - Seq.empty, - None, - None - ) -} - -case class IrScopedDeps( - // TODO The type is `Seq` and this is deduplicated and sorted in `BuildGenUtil`. Make the type `SortedMap` here for consistency? - namedMvnDeps: Seq[(String, String)] = Nil, - mainBomMvnDeps: SortedSet[String] = SortedSet(), - mainMvnDeps: SortedSet[String] = SortedSet(), - mainModuleDeps: SortedSet[String] = SortedSet(), - mainCompileMvnDeps: SortedSet[String] = SortedSet(), - mainCompileModuleDeps: SortedSet[String] = SortedSet(), - mainRunMvnDeps: SortedSet[String] = SortedSet(), - mainRunModuleDeps: SortedSet[String] = SortedSet(), - testModule: Option[String] = None, - testBomMvnDeps: SortedSet[String] = SortedSet(), - testMvnDeps: SortedSet[String] = SortedSet(), - testModuleDeps: SortedSet[String] = SortedSet(), - testCompileMvnDeps: SortedSet[String] = SortedSet(), - testCompileModuleDeps: SortedSet[String] = SortedSet() -) - -// TODO remove `IrBaseInfo` and just use `IrTrait` directly? -case class IrBaseInfo( - /* - javacOptions: Seq[String] = Nil, - scalaVersion: Option[String] = None, - scalacOptions: Option[Seq[String]] = None, - repositories: Seq[String] = Nil, - noPom: Boolean = true, - publishVersion: String = "", - publishProperties: Seq[(String, String)] = Nil, - */ - // TODO consider renaming directly to `trait` or `baseTrait`? - moduleTypedef: IrTrait | Null = null -) - -sealed class IrDependencyType -object IrDependencyType { - case object Default extends IrDependencyType - case object Test extends IrDependencyType - case object Compile extends IrDependencyType - case object Run extends IrDependencyType -} diff --git a/libs/init/buildgen/test/src/mill/main/buildgen/BuildGenChecker.scala b/libs/init/buildgen/test/src/mill/main/buildgen/BuildGenChecker.scala index c2048348f3c3..3d892b796426 100644 --- a/libs/init/buildgen/test/src/mill/main/buildgen/BuildGenChecker.scala +++ b/libs/init/buildgen/test/src/mill/main/buildgen/BuildGenChecker.scala @@ -1,12 +1,13 @@ package mill.main.buildgen -import mill.constants.{CodeGenConstants, OutFiles} import mill.api.Discover +import mill.init.Util import mill.scalalib.scalafmt.ScalafmtModule import mill.testkit.{TestRootModule, UnitTester} +import mill.util.TokenReaders.* import mill.{PathRef, T} import utest.framework.TestPath -import mill.util.TokenReaders._ + import java.nio.file.FileSystems class BuildGenChecker(sourceRoot: os.Path, scalafmtConfigFile: os.Path) { @@ -27,9 +28,9 @@ class BuildGenChecker(sourceRoot: os.Path, scalafmtConfigFile: os.Path) { os.dynamicPwd.withValue(testRoot)(generate) // fmt - val files = mill.init.Util.buildFiles(testRoot).map(PathRef(_)).toSeq + val buildFiles = mill.init.Util.buildFiles(testRoot) object module extends TestRootModule with ScalafmtModule { - override def filesToFormat(sources: Seq[PathRef]): Seq[PathRef] = files + override def filesToFormat(sources: Seq[PathRef]): Seq[PathRef] = buildFiles.map(PathRef(_)) override def scalafmtConfig: T[Seq[PathRef]] = Seq(PathRef(scalafmtConfigFile)) @@ -38,15 +39,13 @@ class BuildGenChecker(sourceRoot: os.Path, scalafmtConfigFile: os.Path) { UnitTester(module, testRoot).scoped { eval => eval(module.reformat()) - // check - val expectedRoot = sourceRoot / expectedRel - // Non *.mill files, that are not in test data, that we don't want - // to see in the diff - val toCleanUp = os.walk(testRoot, skip = (testRoot / OutFiles.out).equals) + // Remove non-build files before computing diff + val toCleanUp = os.walk.stream(testRoot, skip = buildFiles.contains) .filter(os.isFile) - .filterNot(file => CodeGenConstants.buildFileExtensions.contains(file.ext)) + .toSeq toCleanUp.foreach(os.remove) + val expectedRoot = sourceRoot / expectedRel // Try to normalize permissions while not touching those of committed test data val supportsPerms = FileSystems.getDefault.supportedFileAttributeViews().contains("posix") if (supportsPerms) @@ -78,5 +77,5 @@ class BuildGenChecker(sourceRoot: os.Path, scalafmtConfigFile: os.Path) { object BuildGenChecker { def apply(sourceRoot: os.Path = os.Path(sys.env("MILL_TEST_RESOURCE_DIR"))): BuildGenChecker = - new BuildGenChecker(sourceRoot, mill.init.Util.scalafmtConfigFile) + new BuildGenChecker(sourceRoot, os.temp(Util.scalafmtConfig)) } diff --git a/libs/init/buildgen/test/src/mill/main/buildgen/TreeTests.scala b/libs/init/buildgen/test/src/mill/main/buildgen/TreeTests.scala index 4ac76fd5e91f..a6f03d478481 100644 --- a/libs/init/buildgen/test/src/mill/main/buildgen/TreeTests.scala +++ b/libs/init/buildgen/test/src/mill/main/buildgen/TreeTests.scala @@ -28,12 +28,18 @@ object TreeTests extends TestSuite { ) ) - test("BreadthFirst") { - assert(tree.nodes(Tree.Traversal.BreadthFirst).toSeq == Seq(50, 25, 10, 5, 2, 5, 5, 2)) + test("iterator") { + assert(tree.iterator.toSeq == Seq(50, 25, 5, 10, 5, 2, 5, 2)) } - test("DepthFirst") { - assert(tree.nodes(Tree.Traversal.DepthFirst).toSeq == Seq(5, 25, 5, 2, 10, 5, 2, 50)) + test("map") { + assert(tree.map(-_).iterator.toSeq == Seq(-50, -25, -5, -10, -5, -2, -5, -2)) + } + + test("transform") { + assert(tree.transform((i, is) => Tree(-i, is.reverse)).iterator.toSeq == Seq( + -50, -2, -5, -10, -2, -5, -25, -5 + )) } } } diff --git a/libs/init/gradle/api/src/mill/main/gradle/ExportGradleBuildModel.java b/libs/init/gradle/api/src/mill/main/gradle/ExportGradleBuildModel.java new file mode 100644 index 000000000000..c88e69853a17 --- /dev/null +++ b/libs/init/gradle/api/src/mill/main/gradle/ExportGradleBuildModel.java @@ -0,0 +1,20 @@ +package mill.main.gradle; + +import java.io.Serializable; + +public interface ExportGradleBuildModel extends Serializable { + String getModulesJson(); + + class Impl implements ExportGradleBuildModel { + private final String modulesJson; + + public Impl(String modulesJson) { + this.modulesJson = modulesJson; + } + + @Override + public String getModulesJson() { + return modulesJson; + } + } +} diff --git a/libs/init/gradle/exportplugin/src/mill/main/gradle/ExportGradleBuildModelBuilder.scala b/libs/init/gradle/exportplugin/src/mill/main/gradle/ExportGradleBuildModelBuilder.scala new file mode 100644 index 000000000000..0a4c8a641e4f --- /dev/null +++ b/libs/init/gradle/exportplugin/src/mill/main/gradle/ExportGradleBuildModelBuilder.scala @@ -0,0 +1,236 @@ +package mill.main.gradle + +import mill.main.buildgen.* +import org.gradle.api.Project +import org.gradle.api.artifacts.repositories.{ArtifactRepository, UrlArtifactRepository} +import org.gradle.api.artifacts.{Dependency, ExternalDependency, ProjectDependency} +import org.gradle.api.plugins.JavaPlugin.* +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.* +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.tooling.provider.model.{ToolingModelBuilder, ToolingModelBuilderRegistry} + +import java.io.File +import javax.inject.Inject +import scala.jdk.CollectionConverters.* + +class ExportGradleBuildModelBuilder( + ctx: GradleBuildContext, + testModuleName: String, + workspace: os.Path +) extends ToolingModelBuilder { + + def canBuild(modelName: String) = classOf[ExportGradleBuildModel].getName == modelName + + def buildAll(modelName: String, project: Project) = { + val modules = Iterator.iterate(Set(project))(_.flatMap(_.getSubprojects.asScala)) + .takeWhile(_.nonEmpty) + .flatten + .map(toModule) + .toSeq + new ExportGradleBuildModel.Impl(upickle.default.write(modules)) + } + + def toModule(project0: Project) = { + import project0.* + val moduleDir = os.Path(getProjectDir) + val segments = moduleDir.subRelativeTo(workspace).segments + + def moduleDeps(configNames: String*) = getConfigurations.iterator.asScala.flatMap: config => + if (configNames.contains(config.getName)) config.getDependencies.iterator.asScala.collect: + case dep: ProjectDependency => toModuleDep(dep) + else Nil + .toSeq + def mvnDeps(configNames: String*) = getConfigurations.iterator.asScala.flatMap: config => + if (configNames.contains(config.getName)) config.getDependencies.iterator.asScala.collect: + case dep: ExternalDependency => toMvnDep(dep) + .filterNot(JavaModuleConfig.isBomMvnDep) + else Nil + .toSeq + def bomMvnDeps(configNames: String*) = getConfigurations.iterator.asScala.flatMap: config => + if (configNames.contains(config.getName)) config.getDependencies.iterator.asScala.collect: + case dep: ExternalDependency => toMvnDep(dep) + .filter(JavaModuleConfig.isBomMvnDep) + else Nil + .toSeq + + val testModule = + val testDeps = mvnDeps(TEST_IMPLEMENTATION_CONFIGURATION_NAME) + val testCompileDeps = mvnDeps(TEST_COMPILE_ONLY_CONFIGURATION_NAME) + val testRunDeps = mvnDeps(TEST_RUNTIME_ONLY_CONFIGURATION_NAME) + if (os.exists(moduleDir / "src/test")) TestModuleRepr.mixinAndMandatoryMvnDeps( + testDeps ++ testCompileDeps ++ testRunDeps + ).map: (mixin, mandatoryMvnDeps) => + TestModuleRepr( + name = testModuleName, + supertypes = Seq("MavenTests"), + mixins = Seq(mixin), + configs = Seq(JavaModuleConfig( + mandatoryMvnDeps = mandatoryMvnDeps, + mvnDeps = testDeps.diff(mandatoryMvnDeps), + compileMvnDeps = testCompileDeps.diff(mandatoryMvnDeps), + runMvnDeps = testRunDeps.diff(mandatoryMvnDeps), + bomMvnDeps = bomMvnDeps(TEST_IMPLEMENTATION_CONFIGURATION_NAME), + moduleDeps = moduleDeps(TEST_IMPLEMENTATION_CONFIGURATION_NAME), + compileModuleDeps = moduleDeps(TEST_COMPILE_ONLY_CONFIGURATION_NAME), + runModuleDeps = moduleDeps(TEST_RUNTIME_ONLY_CONFIGURATION_NAME) + )), + // Retained from https://github.com/com-lihaoyi/mill/commit/ba5960983895565f230166464e66524f0a1a5fd8 + testParallelism = false, + testSandboxWorkingDir = false + ) + else None + + val javaCompileTask = Option(getTasks.findByName(COMPILE_JAVA_TASK_NAME)).collect: + case task: JavaCompile => task + val (javacOptions, errorProneModuleConfig) = ErrorProneModuleConfig.javacOptionsAndConfig( + javaCompileTask.fold(Nil) { task => + // javac requires --release to be mutually exclusive with -source/-target + Option(task.getOptions.getRelease.getOrNull).fold( + Option(task.getSourceCompatibility).fold(Nil)(Seq("-source", _)) ++ + Option(task.getTargetCompatibility).fold(Nil)(Seq("-target", _)) + )(n => Seq("--release", n.toString)) ++ + Option(task.getOptions.getEncoding).fold(Nil)(Seq("-encoding", _)) ++ + task.getOptions.getAllCompilerArgs.asScala.toSeq + }, + mvnDeps("errorprone") + ) + val javaModuleConfig = Option.when(getPluginManager.hasPlugin("java")): + JavaModuleConfig( + mvnDeps = mvnDeps(IMPLEMENTATION_CONFIGURATION_NAME, API_CONFIGURATION_NAME), + compileMvnDeps = + mvnDeps(COMPILE_ONLY_CONFIGURATION_NAME, COMPILE_ONLY_API_CONFIGURATION_NAME), + runMvnDeps = mvnDeps(RUNTIME_ONLY_CONFIGURATION_NAME), + bomMvnDeps = bomMvnDeps(IMPLEMENTATION_CONFIGURATION_NAME, API_CONFIGURATION_NAME), + moduleDeps = moduleDeps(IMPLEMENTATION_CONFIGURATION_NAME, API_CONFIGURATION_NAME), + compileModuleDeps = + moduleDeps(COMPILE_ONLY_CONFIGURATION_NAME, COMPILE_ONLY_API_CONFIGURATION_NAME), + runModuleDeps = moduleDeps(RUNTIME_ONLY_CONFIGURATION_NAME), + javacOptions = javacOptions.diff(JavaModuleConfig.unsupportedJavacOptions) + ) + val javaHomeModuleConfig = ctx.jvmId(project0).map(JavaHomeModuleConfig(_)) + val coursierModuleConfig = { + val toUrlString: PartialFunction[ArtifactRepository, String] = { + case repo: UrlArtifactRepository => repo.getUrl.toURL.toExternalForm + } + val skip = Seq( + getRepositories.mavenCentral(), + getRepositories.mavenLocal(), + getRepositories.gradlePluginPortal() + ).collect(toUrlString) + getRepositories.iterator.asScala.collect(toUrlString).distinct.toSeq.diff(skip) match + case Nil => None + case repositories => Some(CoursierModuleConfig(repositories = repositories)) + } + val publishModuleConfig = Option(getExtensions.findByType(classOf[PublishingExtension])) + .flatMap: ext => + ext.getPublications.withType(classOf[MavenPublication]).asScala.headOption + .map: pub => + PublishModuleConfig( + pomPackagingType = Option(pub.getPom.getPackaging).filter(_ != "jar").orNull, + pomSettings = toPomSettings(pub.getPom), + artifactMetadata = toArtifactMetadata(pub), + publishVersion = getVersion.toString + ) + + val configs = Seq( + javaModuleConfig, + javaHomeModuleConfig, + coursierModuleConfig, + publishModuleConfig, + errorProneModuleConfig + ).flatten + if (configs.isEmpty && testModule.isEmpty) ModuleRepr(segments) + else ModuleRepr( + segments = segments, + supertypes = Seq("MavenModule") ++ + (if (publishModuleConfig.isEmpty) Nil else Seq("PublishModule")) ++ + (if (errorProneModuleConfig.isEmpty) Nil else Seq("ErrorProneModule")), + configs = + if (testModule.nonEmpty && javaModuleConfig.isEmpty) JavaModuleConfig() +: configs + else configs, + testModule = testModule + ) + } + + def toModuleDep(dep: ProjectDependency) = { + JavaModuleConfig.ModuleDep( + os.Path(ctx.project(dep).getProjectDir).subRelativeTo(workspace).segments + ) + } + + def toMvnDep(dep: ExternalDependency) = { + import dep.* + val artifact = getArtifacts.asScala.headOption + JavaModuleConfig.mvnDep( + org = getGroup, + name = getName, + version = getVersion, + classifier = artifact.map(_.getClassifier), + typ = artifact.map(_.getType), + excludes = getExcludeRules.asScala.map(rule => rule.getGroup -> rule.getModule) + ) + } + + def toPomSettings(pom: MavenPom) = { + import pom.* + var org: String = null + organization(pomOrg => org = Option(pomOrg).fold(null)(_.getName.getOrNull)) + val licenses = Seq.newBuilder[PublishModuleConfig.License] + pom.licenses(_.license(licenses += toLicense(_))) + var versionControl: PublishModuleConfig.VersionControl = null + pom.scm(scm => versionControl = toVersionControl(scm)) + val developers = Seq.newBuilder[PublishModuleConfig.Developer] + pom.developers(_.developer(developers += toDeveloper(_))) + PublishModuleConfig.PomSettings( + description = getDescription.getOrNull, + organization = org, + url = getUrl.getOrNull, + licenses = licenses.result(), + versionControl = versionControl, + developers = developers.result() + ) + } + + def toLicense(license: MavenPomLicense) = { + import license.* + PublishModuleConfig.License( + id = getName.getOrNull, + name = getName.getOrNull, + url = getUrl.getOrNull, + distribution = getDistribution.getOrNull + ) + } + + def toVersionControl(scm: MavenPomScm) = { + if (null == scm) PublishModuleConfig.VersionControl() + else + import scm.* + PublishModuleConfig.VersionControl( + browsableRepository = Option(getUrl.getOrNull), + connection = Option(getConnection.getOrNull), + developerConnection = Option(getDeveloperConnection.getOrNull), + tag = Option(getTag.getOrNull) + ) + } + + def toDeveloper(developer: MavenPomDeveloper) = { + import developer.* + PublishModuleConfig.Developer( + id = getId.getOrNull, + name = getName.getOrNull, + url = getUrl.getOrNull, + organization = Option(getOrganization.getOrNull), + organizationUrl = Option(getOrganizationUrl.getOrNull) + ) + } + + def toArtifactMetadata(pub: MavenPublication) = { + import pub.* + PublishModuleConfig.Artifact( + group = getGroupId, + id = getArtifactId, + version = getVersion + ) + } +} diff --git a/libs/init/gradle/exportplugin/src/mill/main/gradle/ExportGradleBuildPlugin.scala b/libs/init/gradle/exportplugin/src/mill/main/gradle/ExportGradleBuildPlugin.scala new file mode 100644 index 000000000000..0b27a1701056 --- /dev/null +++ b/libs/init/gradle/exportplugin/src/mill/main/gradle/ExportGradleBuildPlugin.scala @@ -0,0 +1,16 @@ +package mill.main.gradle + +import org.gradle.api.{Plugin, Project} +import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry + +import javax.inject.Inject + +class ExportGradleBuildPlugin @Inject (registry: ToolingModelBuilderRegistry) + extends Plugin[Project] { + + def apply(target: Project) = registry.register(ExportGradleBuildModelBuilder( + ctx = GradleBuildContext(target.getGradle), + testModuleName = System.getProperty("mill.init.test.module.name"), + workspace = os.Path(target.getRootProject.getRootDir) + )) +} diff --git a/libs/init/gradle/exportplugin/src/mill/main/gradle/GradleBuildContext.scala b/libs/init/gradle/exportplugin/src/mill/main/gradle/GradleBuildContext.scala new file mode 100644 index 000000000000..21b3e424bf73 --- /dev/null +++ b/libs/init/gradle/exportplugin/src/mill/main/gradle/GradleBuildContext.scala @@ -0,0 +1,39 @@ +package mill.main.gradle + +import mill.main.buildgen.JavaHomeModuleConfig +import org.gradle.api.Project +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.invocation.Gradle +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.util.internal.VersionNumber + +import scala.math.Ordered.orderingToOrdered + +trait GradleBuildContext { + def jvmId(project: Project): Option[String] + def project(dep: ProjectDependency): Project +} +object GradleBuildContext { + + def apply(gradle: Gradle): GradleBuildContext = { + val version = VersionNumber.parse(gradle.getGradleVersion) + val version_8_11 = VersionNumber.version(8, 11) + val version_6_7 = VersionNumber.version(6, 7) + new GradleBuildContext { + def jvmId(project: Project) = + if (version < version_6_7) None + else Option(project.getExtensions.findByType(classOf[JavaPluginExtension])) + .flatMap: ext => + Option(ext.getToolchain) + .flatMap: tc => + Option(tc.getLanguageVersion.getOrNull()) + .map: v => + JavaHomeModuleConfig.jvmId(v.asInt()) + def project(dep: ProjectDependency) = + if (version < version_8_11) + dep.getDependencyProject: @scala.annotation.nowarn("cat=deprecation") + else gradle.getRootProject.findProject(dep.getPath) + } + } +} diff --git a/libs/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala b/libs/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala index a1f2e1114b89..a1c0948b76eb 100644 --- a/libs/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala +++ b/libs/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala @@ -1,374 +1,80 @@ package mill.main.gradle -import mainargs.{ParserForClass, arg, main} -import mill.api.daemon.internal.internal +import mainargs.ParserForClass import mill.main.buildgen.* -import mill.main.buildgen.BuildGenUtil.* -import mill.main.gradle.JavaModel.{Dep, ExternalDep} -import mill.util.Jvm -import org.gradle.api.plugins.JavaPlugin import org.gradle.tooling.GradleConnector -import os.Path +import org.gradle.tooling.internal.consumer.DefaultGradleConnector +import pprint.Util.literalize -import scala.jdk.CollectionConverters.* +import java.util.concurrent.TimeUnit +import scala.util.Using /** - * Converts a Gradle build to Mill by generating Mill build file(s). - * The implementation uses the Gradle - * [[https://docs.gradle.org/current/userguide/third_party_integration.html#embedding Tooling API]] - * to extract the settings for a project using a custom model. - * - * The generated output should be considered scaffolding and will likely require edits to complete conversion. - * - * ===Capabilities=== - * The conversion - * - handles deeply nested modules - * - captures publish settings - * - configures dependencies for configurations: - * - implementation / api - * - compileOnly / compileOnlyApi - * - runtimeOnly - * - testImplementation - * - testCompileOnly - * - configures testing frameworks: - * - JUnit 4 - * - JUnit 5 - * - TestNG - * - * ===Limitations=== - * The conversion does not support: - * - custom dependency configurations - * - custom tasks - * - non-Java sources + * Converts a Gradle build to Mill by selecting module configurations using a custom plugin. + * @see [[https://docs.gradle.org/current/userguide/compatibility.html#java_runtime Gradle JDK compatibility]] + * @see [[GradleBuildGenArgs Command line arguments]] */ -@internal -object GradleBuildGenMain extends BuildGenBase.MavenAndGradle[ProjectModel, Dep] { - override type C = Config +object GradleBuildGenMain { def main(args: Array[String]): Unit = { - val cfg = ParserForClass[Config].constructOrExit(args.toSeq) - run(cfg) - } - - private def run(cfg: Config): Unit = { - val workspace = os.pwd - + val args0 = ParserForClass[GradleBuildGenArgs].constructOrExit(args.toSeq) println("converting Gradle build") - val connector = GradleConnector.newConnector() - - val args = - cfg.shared.basicConfig.jvmId.map { id => - println(s"resolving Java home for jvmId $id") - val home = Jvm.resolveJavaHome(id).get - s"-Dorg.gradle.java.home=$home" - } ++ Seq("--init-script", writeGradleInitScript.toString()) - - try { - println("connecting to Gradle daemon") - val connection = connector.forProjectDirectory(workspace.toIO).connect() - try { - val root = connection.model(classOf[ProjectTree]) - .withArguments(args.asJava) - .get - - val input = Tree.from(root) { tree => - val project = tree.project() - val dirs = os.Path(project.directory()).subRelativeTo(workspace).segments - val children = tree.children().asScala.sortBy(_.project().name()).iterator - (Node(dirs, project), children) - } - - convertWriteOut(cfg, cfg.shared.basicConfig, input) + import args0.{getClass as _, *} - println("converted Gradle build to Mill") - } finally connection.close() - } finally connector.disconnect() - } - - private def writeGradleInitScript: os.Path = { - val file = os.temp.dir() / "init.gradle" - val classpath = - os.Path(classOf[ProjectTreePlugin].getProtectionDomain.getCodeSource.getLocation.toURI) - val plugin = classOf[ProjectTreePlugin].getName - val contents = + val jar = Using.resource( + getClass.getResourceAsStream(BuildInfo.exportpluginAssemblyResource) + )(os.temp(_, suffix = ".jar")) + val script = os.temp( s"""initscript { | dependencies { - | classpath files(${escape(classpath.toString()) /* escape for Windows */}) + | classpath files(${literalize(jar.toString())}) | } |} - | - |allprojects { - | apply plugin: $plugin + |rootProject { + | apply plugin: mill.main.gradle.ExportGradleBuildPlugin |} - |""".stripMargin - os.write(file, contents) - file - } - - override def getBaseInfo( - input: Tree[Node[ProjectModel]], - cfg: Config, - baseModule: String, - packagesSize: Int - ): IrBaseInfo = { - val project = { - val projects = input.nodes(Tree.Traversal.BreadthFirst).map(_.value).toSeq - cfg.baseProject - .flatMap(name => projects.collectFirst { case m if name == m.name => m }) - .orElse(projects.collectFirst { case m if null != m.maven().pom() => m }) - .orElse(projects.collectFirst { case m if !m.maven().repositories().isEmpty => m }) - .getOrElse(input.node.value) - } - if (packagesSize > 1) { - println(s"settings from ${project.name()} will be shared in base module") - } - val supertypes = - Seq("MavenModule") ++ - Option.when(null != project.maven().pom()) { "PublishModule" } - - val javacOptions = getJavacOptions(project) - val scalaVersion = None - val scalacOptions = None - val repos = getRepositories(project) - val pomSettings = extractPomSettings(project) - val publishVersion = getPublishVersion(project) - val publishProperties = getPublishProperties(project, cfg.shared) - - val typedef = IrTrait( - cfg.shared.basicConfig.jvmId, - baseModule, - supertypes, - javacOptions, - scalaVersion, - scalacOptions, - pomSettings, - publishVersion, - publishProperties, - repos + |""".stripMargin, + suffix = ".gradle" ) - - IrBaseInfo(typedef) - } - - override type ModuleFqnMap = Map[String, String] - override def getModuleFqnMap(moduleNodes: Seq[Node[ProjectModel]]) - : ModuleFqnMap = - buildModuleFqnMap(moduleNodes)(_.path()) - - override def extractIrBuild( - cfg: Config, - // baseInfo: IrBaseInfo, - build: Node[ProjectModel], - moduleFqnMap: ModuleFqnMap - ): IrBuild = { - val project = build.value - val scopedDeps = extractScopedDeps(project, moduleFqnMap, cfg) - val version = getPublishVersion(project) - IrBuild( - scopedDeps = scopedDeps, - testModule = cfg.shared.basicConfig.testModule, - testModuleMainType = "MavenTests", - hasTest = os.exists(getMillSourcePath(project) / "src/test"), - dirs = build.dirs, - repositories = getRepositories(project), - javacOptions = getJavacOptions(project), - scalaVersion = None, - scalacOptions = None, - projectName = getArtifactId(project), - pomSettings = extractPomSettings(project), - publishVersion = version, - packaging = getPomPackaging(project), - // not available - pomParentArtifact = null, - // skipped, requires relatively new API (JavaPluginExtension.getSourceSets) - resources = Nil, - testResources = Nil, - publishProperties = getPublishProperties(project, cfg.shared), - jvmId = cfg.shared.basicConfig.jvmId, - testForkDir = None - ) - } - - def getModuleSupertypes(cfg: Config): Seq[String] = - Seq(cfg.shared.basicConfig.baseModule.getOrElse("MavenModule")) - - override def getArtifactId(project: ProjectModel): String = project.name() - - def getMillSourcePath(project: ProjectModel): Path = os.Path(project.directory()) - - override def getSupertypes( - cfg: Config, - baseInfo: IrBaseInfo, - build: Node[ProjectModel] - ): Seq[String] = - Option.when(null != build.value.maven().pom() && { - val baseTrait = baseInfo.moduleTypedef - baseTrait == null || !baseTrait.moduleSupertypes.contains("PublishModule") - }) { "PublishModule" }.toSeq ++ - Option.when(build.dirs.nonEmpty || os.exists(getMillSourcePath(build.value) / "src")) { - getModuleSupertypes(cfg) - }.toSeq.flatten - - def getDepGav(dep: ExternalDep): (String, String, String) = - (dep.group(), dep.name(), dep.version()) - - def getJavacOptions(project: ProjectModel): Seq[String] = { - val _java = project._java() - if (null == _java) Seq.empty - else _java.javacOptions().asScala.toSeq - } - - def getRepositories(project: ProjectModel): Seq[String] = - project.maven().repositories().asScala.toSeq.sorted.map(uri => escape(uri.toString)) - - def getPomPackaging(project: ProjectModel): String = { - val pom = project.maven().pom() - if (null == pom) null else pom.packaging() - } - - def getPublishProperties(project: ProjectModel, cfg: BuildGenUtil.Config): Seq[(String, String)] = - if (cfg.publishProperties.value) { - val pom = project.maven().pom() - if (null == pom) Seq.empty - else pom.properties().iterator().asScala - .map(prop => (prop.key(), prop.value())) - .toSeq - } else Seq.empty - - def getPublishVersion(project: ProjectModel): String | Null = - project.version() match { - case "" | "unspecified" => null - case version => version - } - - def interpMvn(dep: ExternalDep): String = { - BuildGenUtil.renderMvnString(dep.group(), dep.name(), version = dep.version()) - } - - def extractPomSettings(project: ProjectModel): IrPom | Null = { - val pom = project.maven.pom() - if (null == pom) null - else { - IrPom( - pom.description(), - project.group(), // Mill uses group for POM org - pom.url(), - licenses = pom.licenses().asScala - .map(lic => IrLicense(lic.name(), lic.name(), lic.url())) - .toSeq, - versionControl = Option(pom.scm()).fold(IrVersionControl(null, null, null, null))(scm => - IrVersionControl(scm.url(), scm.connection(), scm.devConnection(), scm.tag()) - ), - developers = pom.devs().asScala - .map(dev => IrDeveloper(dev.id(), dev.name(), dev.url(), dev.org(), dev.orgUrl())) - .toSeq - ) - } - } - - // TODO consider renaming to `extractConfigurationDeps` as Gradle calls them configurations instead of scopes - def extractScopedDeps( - project: ProjectModel, - getModuleFqn: PartialFunction[String, String], - cfg: Config - ): IrScopedDeps = { - var sd = IrScopedDeps() - val hasTest = os.exists(os.Path(project.directory()) / "src/test") - val _java = project._java() - if (null != _java) { - val mvnDep: ExternalDep => String = - cfg.shared.basicConfig.depsObject.fold(interpMvn(_)) { objName => dep => - val depName = s"`${dep.group()}:${dep.name()}`" - sd = sd.copy(namedMvnDeps = sd.namedMvnDeps :+ (depName, interpMvn(dep))) - s"$objName.$depName" - } - - def appendMvnDepPackage( - deps: IterableOnce[Dep], - onPackage: String => IrScopedDeps, - onMvn: (String, (String, String, String)) => IrScopedDeps - ): Unit = { - for (dep <- deps.iterator) { - if (dep.isProjectDepOrExternalDep) - sd = onPackage(getModuleFqn(dep.projectDep().path())) - else { - val externalDep = dep.externalDep() - val id = getDepGav(externalDep) - val mvn = mvnDep(externalDep) - sd = onMvn(mvn, id) - } - } - } - _java.configs().forEach { config => - import JavaPlugin.* - - val conf = config.name() - conf match { - case IMPLEMENTATION_CONFIGURATION_NAME | API_CONFIGURATION_NAME => - appendMvnDepPackage( - config.deps.asScala, - onPackage = v => sd.copy(mainModuleDeps = sd.mainModuleDeps + v), - onMvn = (v, id) => - if (isBom(id)) sd.copy(mainBomMvnDeps = sd.mainBomMvnDeps + v) - else sd.copy(mainMvnDeps = sd.mainMvnDeps + v) - ) - - case COMPILE_ONLY_CONFIGURATION_NAME | COMPILE_ONLY_API_CONFIGURATION_NAME => - - appendMvnDepPackage( - config.deps.asScala, - onPackage = v => sd.copy(mainCompileModuleDeps = sd.mainCompileModuleDeps + v), - onMvn = (v, /*id*/ _) => sd.copy(mainCompileMvnDeps = sd.mainCompileMvnDeps + v) - ) - - case RUNTIME_ONLY_CONFIGURATION_NAME => - appendMvnDepPackage( - config.deps.asScala, - onPackage = v => sd.copy(mainRunModuleDeps = sd.mainRunModuleDeps + v), - onMvn = (v, /*id*/ _) => sd.copy(mainRunMvnDeps = sd.mainRunMvnDeps + v) - ) - - case TEST_IMPLEMENTATION_CONFIGURATION_NAME => - - appendMvnDepPackage( - config.deps.asScala, - onPackage = v => sd.copy(testModuleDeps = sd.testModuleDeps + v), - onMvn = (v, id) => - if (isBom(id)) sd.copy(testBomMvnDeps = sd.testBomMvnDeps + v) - else sd.copy(testMvnDeps = sd.testMvnDeps + v) - ) - config.deps.forEach(dep => - if (hasTest && sd.testModule.isEmpty && !dep.isProjectDepOrExternalDep) - sd = sd.copy(testModule = testModulesByGroup.get(dep.externalDep().group())) - ) - - case TEST_COMPILE_ONLY_CONFIGURATION_NAME => - appendMvnDepPackage( - config.deps.asScala, - onPackage = v => sd.copy(testCompileModuleDeps = sd.testCompileModuleDeps + v), - onMvn = (v, /*id*/ _) => sd.copy(testCompileMvnDeps = sd.testCompileMvnDeps + v) - ) - - case name => - config.deps.forEach { dep => - val depString = if (dep.isProjectDepOrExternalDep) - escape(dep.projectDep().path()) - else - getDepGav(dep.externalDep()).toString() - println(s"ignoring $name dependency $depString") - } - } + val packages = { + val connector = GradleConnector.newConnector() match { + case conn: DefaultGradleConnector => + conn.daemonMaxIdleTime(1, TimeUnit.SECONDS) + conn + case conn => conn } + try + val connection = connector.forProjectDirectory(os.pwd.toIO).connect() + connection.action() + try + upickle.default.read[Seq[ModuleRepr]]( + connection.model(classOf[ExportGradleBuildModel]) + .addArguments("--init-script", script.toString()) + .addJvmArguments(s"-Dmill.init.test.module.name=$testModule") + .setStandardOutput(System.out) + .get().getModulesJson + ).map(Tree(_)) + finally connection.close() + finally connector.disconnect() } - sd - } - @main - @internal - case class Config( - shared: BuildGenUtil.Config, - @arg(doc = "name of Gradle project to extract settings for --base-module", short = 'g') - baseProject: Option[String] = None - ) + var build = BuildRepr.fill(packages) + if (merge.value) build = BuildRepr.merged(build) + val writer = if (noMetaBuild.value) BuildWriter(build) + else + val (build0, metaBuild) = MetaBuildRepr.of(build) + BuildWriter(build0, Some(metaBuild)) + writer.writeFiles() + } } + +@mainargs.arg +case class GradleBuildGenArgs( + @mainargs.arg(doc = "name of generated test module") + testModule: String = "test", + @mainargs.arg(doc = "merge generated build files") + merge: mainargs.Flag, + @mainargs.arg(doc = "disables generating meta-build") + noMetaBuild: mainargs.Flag +) diff --git a/libs/init/gradle/src/mill/main/gradle/JavaModel.java b/libs/init/gradle/src/mill/main/gradle/JavaModel.java deleted file mode 100644 index 7dd62b433dd4..000000000000 --- a/libs/init/gradle/src/mill/main/gradle/JavaModel.java +++ /dev/null @@ -1,220 +0,0 @@ -package mill.main.gradle; - -import java.io.Serializable; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.artifacts.ExternalDependency; -import org.gradle.api.artifacts.ProjectDependency; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.tasks.compile.JavaCompile; -import org.gradle.util.internal.VersionNumber; - -/** - * A model containing the Gradle Java Plugin settings for a project. - */ -public interface JavaModel extends Serializable { - - List javacOptions(); - - List configs(); - - class Impl implements JavaModel { - - private final List javacOptions; - private final List configs; - - public Impl(List javacOptions, List configs) { - this.javacOptions = javacOptions; - this.configs = configs; - } - - @Override - public List javacOptions() { - return javacOptions; - } - - @Override - public List configs() { - return configs; - } - } - - static JavaModel from(Project project) { - if (project.getPluginManager().hasPlugin("java")) { - List javacOptions = project - .getTasks() - .named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile.class) - .map(task -> task.getOptions().getAllCompilerArgs()) - .getOrElse(Collections.emptyList()); - VersionNumber gradleVersionNumber = - VersionNumber.parse(project.getGradle().getGradleVersion()); - List configs = project.getConfigurations().stream() - .map(conf -> Config.from(conf, gradleVersionNumber)) - .collect(Collectors.toList()); - return new Impl(javacOptions, configs); - } - return null; - } - - interface Config extends Serializable { - - String name(); - - List deps(); - - class Impl implements Config { - - private final String config; - private final List deps; - - public Impl(String config, List deps) { - this.config = config; - this.deps = deps; - } - - @Override - public String name() { - return config; - } - - @Override - public List deps() { - return deps; - } - } - - static Config from(Configuration conf, VersionNumber gradleVersionNumber) { - String name = conf.getName(); - List deps = conf.getDependencies().stream() - .map(dep -> Dep.from(dep, gradleVersionNumber)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - return new Impl(name, deps); - } - } - - /* - Serializing `ProjectDep`s and `ExternalDep`s as subtypes of `Dep` doesn't work well with serialization. - `instanceOf`/`isInstanceOf` checks on the deserialized objects wouldn't work as expected. - */ - interface Dep extends Serializable { - boolean isProjectDepOrExternalDep(); - - ProjectDep projectDep(); - - ExternalDep externalDep(); - - class Impl implements Dep { - private final ProjectDep projectDep; - private final ExternalDep externalDep; - - public Impl(ProjectDep projectDep) { - this.projectDep = projectDep; - this.externalDep = null; - } - - public Impl(ExternalDep externalDep) { - this.projectDep = null; - this.externalDep = externalDep; - } - - @Override - public boolean isProjectDepOrExternalDep() { - return projectDep != null; - } - - @Override - public ProjectDep projectDep() { - return projectDep; - } - - @Override - public ExternalDep externalDep() { - return externalDep; - } - } - - static @Nullable Dep from(Dependency dep, VersionNumber gradleVersionNumber) { - if (dep instanceof ProjectDependency) - return new Impl(ProjectDep.from((ProjectDependency) dep, gradleVersionNumber)); - else if (dep instanceof ExternalDependency) - return new Impl(ExternalDep.from((ExternalDependency) dep)); - else { - System.out.println( - "Gradle dependency " + dep + " with unsupported type " + dep.getClass() + " dropped"); - return null; - } - } - } - - interface ProjectDep extends Serializable /*extends Dep*/ { - String path(); - - class Impl implements ProjectDep { - private final String path; - - public Impl(String path) { - this.path = path; - } - - @Override - public String path() { - return path; - } - } - - static ProjectDep from(ProjectDependency dep, VersionNumber gradleVersionNumber) { - @SuppressWarnings("deprecation") - var path = gradleVersionNumber.compareTo(VersionNumber.parse("8.11")) >= 0 - ? dep.getPath() - : dep.getDependencyProject().getPath(); - - return new Impl(path); - } - } - - interface ExternalDep extends Serializable /*extends Dep*/ { - String group(); - - String name(); - - String version(); - - class Impl implements ExternalDep { - private final String group; - private final String name; - private final String version; - - public Impl(String group, String name, String version) { - this.group = group; - this.name = name; - this.version = version; - } - - @Override - public String group() { - return group; - } - - @Override - public String name() { - return name; - } - - @Override - public String version() { - return version; - } - } - - static ExternalDep from(ExternalDependency dep) { - return new Impl(dep.getGroup(), dep.getName(), dep.getVersion()); - } - } -} diff --git a/libs/init/gradle/src/mill/main/gradle/MavenModel.java b/libs/init/gradle/src/mill/main/gradle/MavenModel.java deleted file mode 100644 index f73beb4ae91c..000000000000 --- a/libs/init/gradle/src/mill/main/gradle/MavenModel.java +++ /dev/null @@ -1,350 +0,0 @@ -package mill.main.gradle; - -import java.io.Serializable; -import java.net.URI; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.gradle.api.Project; -import org.gradle.api.artifacts.dsl.RepositoryHandler; -import org.gradle.api.artifacts.repositories.MavenArtifactRepository; -import org.gradle.api.publish.maven.MavenPomDeveloper; -import org.gradle.api.publish.maven.MavenPomLicense; -import org.gradle.api.publish.maven.MavenPomScm; -import org.gradle.api.publish.maven.internal.publication.DefaultMavenPom; -import org.gradle.api.publish.maven.tasks.GenerateMavenPom; - -/** - * A model containing the Gradle Maven Plugin settings for a project. - */ -public interface MavenModel extends Serializable { - - Pom pom(); - - Set repositories(); - - class Impl implements MavenModel { - - private final Pom pom; - private final Set reps; - - public Impl(Pom pom, Set reps) { - this.pom = pom; - this.reps = reps; - } - - @Override - public Pom pom() { - return pom; - } - - @Override - public Set repositories() { - return reps; - } - } - - static MavenModel from(Project project) { - Pom pom = project.getTasks().withType(GenerateMavenPom.class).stream() - .map(GenerateMavenPom::getPom) - .flatMap(p -> (p instanceof DefaultMavenPom) - ? Stream.of(Pom.from((DefaultMavenPom) p)) - : Stream.empty()) - .findFirst() - .orElse(null); - RepositoryHandler rep = project.getRepositories(); - List skipReps = new LinkedList<>(); - skipReps.add(rep.mavenCentral().getUrl()); - skipReps.add(rep.mavenLocal().getUrl()); - if (rep.gradlePluginPortal() instanceof MavenArtifactRepository) { - skipReps.add(((MavenArtifactRepository) rep.gradlePluginPortal()).getUrl()); - } - - Set reps = rep.stream() - .flatMap(repo -> repo instanceof MavenArtifactRepository - ? Stream.of(((MavenArtifactRepository) repo).getUrl()) - : Stream.empty()) - .filter(uri -> !skipReps.contains(uri)) - .collect(Collectors.toSet()); - return new Impl(pom, reps); - } - - interface Dev extends Serializable { - - String id(); - - String name(); - - String url(); - - String org(); - - String orgUrl(); - - class Impl implements Dev { - - private final String id; - private final String name; - private final String url; - private final String org; - private final String orgUrl; - - public Impl(String id, String name, String url, String org, String orgUrl) { - this.id = id; - this.name = name; - this.url = url; - this.org = org; - this.orgUrl = orgUrl; - } - - @Override - public String id() { - return id; - } - - @Override - public String name() { - return name; - } - - @Override - public String url() { - return url; - } - - @Override - public String org() { - return org; - } - - @Override - public String orgUrl() { - return orgUrl; - } - } - - static Dev from(MavenPomDeveloper dev) { - return new Impl( - dev.getId().getOrNull(), - dev.getName().getOrNull(), - dev.getUrl().getOrNull(), - dev.getOrganization().getOrNull(), - dev.getOrganizationUrl().getOrNull()); - } - } - - interface License extends Serializable { - - String name(); - - String url(); - - class Impl implements License { - private final String name; - private final String url; - - public Impl(String name, String url) { - this.name = name; - this.url = url; - } - - @Override - public String name() { - return name; - } - - @Override - public String url() { - return url; - } - } - - static License from(MavenPomLicense license) { - return new Impl(license.getName().getOrNull(), license.getUrl().getOrNull()); - } - } - - interface Pom extends Serializable { - - String description(); - - String url(); - - List licenses(); - - Scm scm(); - - List devs(); - - String packaging(); - - List properties(); - - class Impl implements Pom { - - private final String description; - private final String url; - private final List licenses; - private final Scm scm; - private final List devs; - private final String packaging; - private final List properties; - - public Impl( - String description, - String url, - List licenses, - Scm scm, - List devs, - String packaging, - List properties) { - this.description = description; - this.url = url; - this.licenses = licenses; - this.scm = scm; - this.devs = devs; - this.packaging = packaging; - this.properties = properties; - } - - @Override - public String description() { - return description; - } - - @Override - public String url() { - return url; - } - - @Override - public List licenses() { - return licenses; - } - - @Override - public Scm scm() { - return scm; - } - - @Override - public List devs() { - return devs; - } - - @Override - public String packaging() { - return packaging; - } - - @Override - public List properties() { - return properties; - } - } - - static Pom from(DefaultMavenPom pom) { - List properties = Stream.ofNullable(pom.getProperties().getOrNull()) - .flatMap(map -> map.entrySet().stream()) - .map(Prop::from) - .collect(Collectors.toList()); - return new Impl( - pom.getDescription().getOrNull(), - pom.getUrl().getOrNull(), - pom.getLicenses().stream().map(License::from).collect(Collectors.toList()), - Scm.from(pom.getScm()), - pom.getDevelopers().stream().map(Dev::from).collect(Collectors.toList()), - pom.getPackaging(), - properties); - } - } - - interface Prop extends Serializable { - - String key(); - - String value(); - - class Impl implements Prop { - private final String key; - private final String value; - - public Impl(String key, String value) { - this.key = key; - this.value = value; - } - - @Override - public String key() { - return key; - } - - @Override - public String value() { - return value; - } - } - - static Prop from(Map.Entry entry) { - return new Impl(entry.getKey(), entry.getValue()); - } - } - - interface Scm extends Serializable { - - String url(); - - String connection(); - - String devConnection(); - - String tag(); - - class Impl implements Scm { - private final String url; - private final String connection; - private final String devConnection; - private final String tag; - - public Impl(String url, String connection, String devConnection, String tag) { - this.url = url; - this.connection = connection; - this.devConnection = devConnection; - this.tag = tag; - } - - @Override - public String url() { - return url; - } - - @Override - public String connection() { - return connection; - } - - @Override - public String devConnection() { - return devConnection; - } - - @Override - public String tag() { - return tag; - } - } - - static Scm from(MavenPomScm scm) { - return null == scm - ? null - : new Impl( - scm.getUrl().getOrNull(), - scm.getConnection().getOrNull(), - scm.getDeveloperConnection().getOrNull(), - scm.getTag().getOrNull()); - } - } -} diff --git a/libs/init/gradle/src/mill/main/gradle/ProjectModel.java b/libs/init/gradle/src/mill/main/gradle/ProjectModel.java deleted file mode 100644 index f3803ce0b417..000000000000 --- a/libs/init/gradle/src/mill/main/gradle/ProjectModel.java +++ /dev/null @@ -1,102 +0,0 @@ -package mill.main.gradle; - -import java.io.File; -import java.io.Serializable; -import org.gradle.api.Project; - -/** - * A model containing the settings for a Gradle project. - *
- * NOTE: The Gradle API used by the Gradle daemon is tied to the version of Gradle used in the target project (and not the API version used here). - * Consequently, relatively newer features, like {@link org.gradle.api.plugins.JavaPluginExtension#getSourceSets()}} (added in Gradle 7.1), will not be available for projects that use a legacy Gradle version. - */ -public interface ProjectModel extends Serializable { - - String group(); - - String name(); - - String version(); - - File directory(); - - String path(); - - MavenModel maven(); - - JavaModel _java(); - - class Impl implements ProjectModel { - - private final String group; - private final String name; - private final String version; - private final File directory; - private final String path; - private final MavenModel maven; - private final JavaModel _java; - - public Impl( - String group, - String name, - String version, - File directory, - String path, - MavenModel maven, - JavaModel _java) { - this.group = group; - this.name = name; - this.version = version; - this.directory = directory; - this.path = path; - this.maven = maven; - this._java = _java; - } - - @Override - public String group() { - return group; - } - - @Override - public String name() { - return name; - } - - @Override - public String version() { - return version; - } - - @Override - public File directory() { - return directory; - } - - @Override - public String path() { - return path; - } - - @Override - public MavenModel maven() { - return maven; - } - - @Override - public JavaModel _java() { - return _java; - } - } - - static ProjectModel from(Project project) { - return new Impl( - project.getGroup().toString(), - project.getName(), - project.getVersion().toString(), - project.getProjectDir(), - project.getPath(), - MavenModel.from(project), - JavaModel.from(project)); - } -} diff --git a/libs/init/gradle/src/mill/main/gradle/ProjectTree.java b/libs/init/gradle/src/mill/main/gradle/ProjectTree.java deleted file mode 100644 index eb717cc4875b..000000000000 --- a/libs/init/gradle/src/mill/main/gradle/ProjectTree.java +++ /dev/null @@ -1,56 +0,0 @@ -package mill.main.gradle; - -import java.io.Serializable; -import java.util.LinkedList; -import java.util.List; -import org.gradle.api.Project; -import org.gradle.tooling.provider.model.ToolingModelBuilder; - -/** - * A Gradle project tree. - */ -public interface ProjectTree extends Serializable { - - ProjectModel project(); - - List children(); - - class Impl implements ProjectTree { - - private final ProjectModel project; - private final List children; - - Impl(ProjectModel project, List children) { - this.project = project; - this.children = children; - } - - @Override - public ProjectModel project() { - return project; - } - - @Override - public List children() { - return children; - } - } - - class Builder implements ToolingModelBuilder { - - @Override - public boolean canBuild(String modelName) { - return ProjectTree.class.getName().equals(modelName); - } - - @Override - public Object buildAll(String modelName, Project project) { - ProjectModel model = ProjectModel.from(project); - List children = new LinkedList<>(); - for (Project child : project.getChildProjects().values()) { - children.add((ProjectTree) buildAll(modelName, child)); - } - return new ProjectTree.Impl(model, children); - } - } -} diff --git a/libs/init/gradle/src/mill/main/gradle/ProjectTreePlugin.java b/libs/init/gradle/src/mill/main/gradle/ProjectTreePlugin.java deleted file mode 100644 index 16e84b845531..000000000000 --- a/libs/init/gradle/src/mill/main/gradle/ProjectTreePlugin.java +++ /dev/null @@ -1,25 +0,0 @@ -package mill.main.gradle; - -import javax.inject.Inject; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry; - -/** - * A custom Gradle plugin that adds support for building a {@link ProjectTree}. - */ -public class ProjectTreePlugin implements Plugin { - private final ToolingModelBuilderRegistry registry; - - @Inject() - ProjectTreePlugin(ToolingModelBuilderRegistry registry) { - this.registry = registry; - } - - @Override - public void apply(Project target) { - if (target == target.getRootProject()) { - registry.register(new ProjectTree.Builder()); - } - } -} diff --git a/libs/init/gradle/test/resources/expected/application-library/app/package.mill b/libs/init/gradle/test/resources/expected/application-library/app/package.mill index 823c4a0ea728..6e4b074e7e94 100644 --- a/libs/init/gradle/test/resources/expected/application-library/app/package.mill +++ b/libs/init/gradle/test/resources/expected/application-library/app/package.mill @@ -1,21 +1,22 @@ package build.app -import mill._ import mill.javalib._ -import mill.javalib.publish._ +import millbuild._ -object `package` extends MavenModule { +object `package` extends ApplicationLibraryBaseModule { - def mvnDeps = Seq(mvn"org.apache.commons:commons-text") + def mvnDeps = super.mvnDeps() ++ Seq(Deps.commonsText) def moduleDeps = super.moduleDeps ++ Seq(build.utilities) object test extends MavenTests with TestModule.Junit5 { - def mvnDeps = Seq(mvn"org.junit.jupiter:junit-jupiter:5.10.3") + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(Deps.junitJupiter, Deps.junitPlatformLauncher) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } diff --git a/libs/init/gradle/test/resources/expected/application-library/build.mill b/libs/init/gradle/test/resources/expected/application-library/build.mill index 8017280ba200..56e564fc7763 100644 --- a/libs/init/gradle/test/resources/expected/application-library/build.mill +++ b/libs/init/gradle/test/resources/expected/application-library/build.mill @@ -1,12 +1,8 @@ //| mill-version: SNAPSHOT +//| mill-jvm-version: 11 package build -import mill._ -import mill.javalib._ -import mill.javalib.publish._ +import mill.api._ +import millbuild._ -object `package` extends mill.Module { - - def artifactName = "application-library" - -} +object `package` extends Module {} diff --git a/libs/init/gradle/test/resources/expected/application-library/list/package.mill b/libs/init/gradle/test/resources/expected/application-library/list/package.mill index 2ed2469f6fe5..f2e55b5feb45 100644 --- a/libs/init/gradle/test/resources/expected/application-library/list/package.mill +++ b/libs/init/gradle/test/resources/expected/application-library/list/package.mill @@ -1,17 +1,18 @@ package build.list -import mill._ import mill.javalib._ -import mill.javalib.publish._ +import millbuild._ -object `package` extends MavenModule { +object `package` extends ApplicationLibraryBaseModule { object test extends MavenTests with TestModule.Junit5 { - def mvnDeps = Seq(mvn"org.junit.jupiter:junit-jupiter:5.10.3") + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(Deps.junitJupiter, Deps.junitPlatformLauncher) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } diff --git a/libs/init/gradle/test/resources/expected/application-library/mill-build/build.mill b/libs/init/gradle/test/resources/expected/application-library/mill-build/build.mill new file mode 100644 index 000000000000..26686f5e0d2e --- /dev/null +++ b/libs/init/gradle/test/resources/expected/application-library/mill-build/build.mill @@ -0,0 +1,5 @@ +package build + +import mill.meta.MillBuildRootModule + +object `package` extends MillBuildRootModule diff --git a/libs/init/gradle/test/resources/expected/application-library/mill-build/src/ApplicationLibraryBaseModule.scala b/libs/init/gradle/test/resources/expected/application-library/mill-build/src/ApplicationLibraryBaseModule.scala new file mode 100644 index 000000000000..0c57e30675fd --- /dev/null +++ b/libs/init/gradle/test/resources/expected/application-library/mill-build/src/ApplicationLibraryBaseModule.scala @@ -0,0 +1,12 @@ +package millbuild + +import mill.javalib._ + +trait ApplicationLibraryBaseModule extends MavenModule { + + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.8", "-target", "1.8") + + def jvmId = "11" + +} diff --git a/libs/init/gradle/test/resources/expected/application-library/mill-build/src/Deps.scala b/libs/init/gradle/test/resources/expected/application-library/mill-build/src/Deps.scala new file mode 100644 index 000000000000..bd7eb2ef199f --- /dev/null +++ b/libs/init/gradle/test/resources/expected/application-library/mill-build/src/Deps.scala @@ -0,0 +1,10 @@ +package millbuild + +import mill.javalib._ + +object Deps { + + val commonsText = mvn"org.apache.commons:commons-text" + val junitJupiter = mvn"org.junit.jupiter:junit-jupiter:5.10.3" + val junitPlatformLauncher = mvn"org.junit.platform:junit-platform-launcher" +} diff --git a/libs/init/gradle/test/resources/expected/application-library/utilities/package.mill b/libs/init/gradle/test/resources/expected/application-library/utilities/package.mill index 663cee2c53f5..cbe8da7f6e02 100644 --- a/libs/init/gradle/test/resources/expected/application-library/utilities/package.mill +++ b/libs/init/gradle/test/resources/expected/application-library/utilities/package.mill @@ -1,10 +1,9 @@ package build.utilities -import mill._ import mill.javalib._ -import mill.javalib.publish._ +import millbuild._ -object `package` extends MavenModule { +object `package` extends ApplicationLibraryBaseModule { def moduleDeps = super.moduleDeps ++ Seq(build.list) diff --git a/libs/init/gradle/test/resources/expected/config/build.mill b/libs/init/gradle/test/resources/expected/config/build.mill index b90ae61338d9..8c9327e44048 100644 --- a/libs/init/gradle/test/resources/expected/config/build.mill +++ b/libs/init/gradle/test/resources/expected/config/build.mill @@ -2,56 +2,63 @@ //| mill-jvm-version: 11 package build -import _root_.build_.BaseModule -import mill._ +import mill.api._ import mill.javalib._ -import mill.javalib.publish._ -object Deps { +object `package` extends Module { - val `org.apache.commons:commons-text` = mvn"org.apache.commons:commons-text" + object app extends MavenModule { - val `org.junit.jupiter:junit-jupiter` = - mvn"org.junit.jupiter:junit-jupiter:5.10.3" -} - -object `package` extends mill.Module { + def mvnDeps = super.mvnDeps() ++ Seq(mvn"org.apache.commons:commons-text") - def artifactName = "application-library" - - object app extends BaseModule { + def moduleDeps = super.moduleDeps ++ Seq(build.utilities) - def mvnDeps = Seq(Deps.`org.apache.commons:commons-text`) + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.8", "-target", "1.8") - def moduleDeps = super.moduleDeps ++ Seq(build.utilities) + def jvmId = "11" object tests extends MavenTests with TestModule.Junit5 { - def mvnDeps = Seq(Deps.`org.junit.jupiter:junit-jupiter`) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq( + mvn"org.junit.jupiter:junit-jupiter:5.10.3", + mvn"org.junit.platform:junit-platform-launcher" + ) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } + object list extends MavenModule { - object list extends BaseModule { + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.8", "-target", "1.8") + + def jvmId = "11" object tests extends MavenTests with TestModule.Junit5 { - def mvnDeps = Seq(Deps.`org.junit.jupiter:junit-jupiter`) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq( + mvn"org.junit.jupiter:junit-jupiter:5.10.3", + mvn"org.junit.platform:junit-platform-launcher" + ) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } - } - object utilities extends BaseModule { + } + object utilities extends MavenModule { def moduleDeps = super.moduleDeps ++ Seq(build.list) + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.8", "-target", "1.8") + + def jvmId = "11" + } } - -trait BaseModule extends MavenModule {} diff --git a/libs/init/gradle/test/resources/expected/library/build.mill b/libs/init/gradle/test/resources/expected/library/build.mill index f8b5eed61204..56e564fc7763 100644 --- a/libs/init/gradle/test/resources/expected/library/build.mill +++ b/libs/init/gradle/test/resources/expected/library/build.mill @@ -1,12 +1,8 @@ //| mill-version: SNAPSHOT +//| mill-jvm-version: 11 package build -import mill._ -import mill.javalib._ -import mill.javalib.publish._ +import mill.api._ +import millbuild._ -object `package` extends mill.Module { - - def artifactName = "single-module" - -} +object `package` extends Module {} diff --git a/libs/init/gradle/test/resources/expected/library/lib/package.mill b/libs/init/gradle/test/resources/expected/library/lib/package.mill index a9b564ae472f..ab8cbd3b9b46 100644 --- a/libs/init/gradle/test/resources/expected/library/lib/package.mill +++ b/libs/init/gradle/test/resources/expected/library/lib/package.mill @@ -1,22 +1,25 @@ package build.lib -import mill._ import mill.javalib._ -import mill.javalib.publish._ +import millbuild._ object `package` extends MavenModule { - def mvnDeps = Seq( - mvn"com.google.guava:guava:33.2.1-jre", - mvn"org.apache.commons:commons-math3:3.6.1" - ) + def mvnDeps = super.mvnDeps() ++ Seq(Deps.commonsMath3, Deps.guava) + + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.8", "-target", "1.8") + + def jvmId = "11" object test extends MavenTests with TestModule.Junit5 { - def mvnDeps = Seq(mvn"org.junit.jupiter:junit-jupiter:5.10.3") + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(Deps.junitJupiter, Deps.junitPlatformLauncher) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } diff --git a/libs/init/gradle/test/resources/expected/library/mill-build/build.mill b/libs/init/gradle/test/resources/expected/library/mill-build/build.mill new file mode 100644 index 000000000000..26686f5e0d2e --- /dev/null +++ b/libs/init/gradle/test/resources/expected/library/mill-build/build.mill @@ -0,0 +1,5 @@ +package build + +import mill.meta.MillBuildRootModule + +object `package` extends MillBuildRootModule diff --git a/libs/init/gradle/test/resources/expected/library/mill-build/src/Deps.scala b/libs/init/gradle/test/resources/expected/library/mill-build/src/Deps.scala new file mode 100644 index 000000000000..b6cb0abafe88 --- /dev/null +++ b/libs/init/gradle/test/resources/expected/library/mill-build/src/Deps.scala @@ -0,0 +1,11 @@ +package millbuild + +import mill.javalib._ + +object Deps { + + val commonsMath3 = mvn"org.apache.commons:commons-math3:3.6.1" + val guava = mvn"com.google.guava:guava:33.2.1-jre" + val junitJupiter = mvn"org.junit.jupiter:junit-jupiter:5.10.3" + val junitPlatformLauncher = mvn"org.junit.platform:junit-platform-launcher" +} diff --git a/libs/init/gradle/test/src/mill/main/gradle/BuildGenTests.scala b/libs/init/gradle/test/src/mill/main/gradle/BuildGenTests.scala index 191cb04f0062..66d5fd76a234 100644 --- a/libs/init/gradle/test/src/mill/main/gradle/BuildGenTests.scala +++ b/libs/init/gradle/test/src/mill/main/gradle/BuildGenTests.scala @@ -27,19 +27,7 @@ object BuildGenTests extends TestSuite { test("config") { val sourceRoot = os.sub / "application-library" val expectedRoot = os.sub / "expected/config" - val args = Array( - "--base-module", - "BaseModule", - "--base-project", - "utilities", - "--jvm-id", - "11", - "--test-module", - "tests", - "--deps-object", - "Deps", - "--merge" - ) + val args = Array("--test-module", "tests", "--merge", "--no-meta-build") assert( checker.check(GradleBuildGenMain.main(args), sourceRoot, expectedRoot) ) diff --git a/libs/init/maven/src/mill/main/maven/MavenBuildGenMain.scala b/libs/init/maven/src/mill/main/maven/MavenBuildGenMain.scala index 46d1897d7db8..8971b5a9fe0e 100644 --- a/libs/init/maven/src/mill/main/maven/MavenBuildGenMain.scala +++ b/libs/init/maven/src/mill/main/maven/MavenBuildGenMain.scala @@ -1,300 +1,213 @@ package mill.main.maven import mainargs.{Flag, ParserForClass, arg, main} -import mill.api.daemon.internal.internal import mill.main.buildgen.* -import mill.main.buildgen.BuildGenUtil.* -import org.apache.maven.model.{Dependency, Model, Parent} -import os.{Path, SubPath} +import org.apache.maven.model.* 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 settings - * - 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 + * Converts a Maven build to Mill by selecting module configurations from POM files. + * @see [[https://maven.apache.org/download.cgi Maven JDK compatibility]] + * @see [[MavenBuildGenArgs Command line arguments]] */ -@internal -object MavenBuildGenMain extends BuildGenBase.MavenAndGradle[Model, Dependency] { - override type C = Config +object MavenBuildGenMain { def main(args: Array[String]): Unit = { - val cfg = ParserForClass[Config].constructOrExit(args.toSeq) - run(cfg) - } - - private def run(cfg: Config): Unit = { - val workspace = os.pwd - + val args0 = ParserForClass[MavenBuildGenArgs].constructOrExit(args.toSeq) 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 :+ _)) - } + import args0.* + + val modeler = Modeler() + val segmentsModels = Tree.from(os.sub): sub => + val model = modeler.read(os.pwd / sub) + ( + (sub.segments, model), + model.getModules.iterator.asScala.map(s => sub / os.SubPath(s)).toSeq + ) + val segmentsByGav = segmentsModels.iterator.map((segments, model) => + ((model.getGroupId, model.getArtifactId, model.getVersion), segments) + ).toMap + def gav(dep: Dependency) = (dep.getGroupId, dep.getArtifactId, dep.getVersion) + + val packages = segmentsModels.iterator.map: (segments, model) => + def mvnDeps(scopes: String*) = model.getDependencies.iterator.asScala.collect: + case dep if scopes.contains(dep.getScope) && !segmentsByGav.contains(gav(dep)) => + toMvnDep(dep) + .filterNot(JavaModuleConfig.isBomMvnDep).toSeq + def bomMvnDeps(scopes: String*) = model.getDependencies.iterator.asScala.collect: + case dep if scopes.contains(dep.getScope) && !segmentsByGav.contains(gav(dep)) => + toMvnDep(dep) + .filter(JavaModuleConfig.isBomMvnDep).toSeq + def moduleDeps(scopes: String*) = model.getDependencies.iterator.asScala + .collect: + case dep if scopes.contains(dep.getScope) => gav(dep) + .collect(segmentsByGav) + .map(JavaModuleConfig.ModuleDep(_)) + .toSeq - convertWriteOut(cfg, cfg.shared.basicConfig, input) + val moduleDir = os.pwd / segments + val testModule0 = if (os.exists(moduleDir / "src/test")) { + // "provided" scope is for both compilation and testing + val testDeps = mvnDeps("test", "provided") + TestModuleRepr.mixinAndMandatoryMvnDeps(testDeps).map: (mixin, mandatoryMvnDeps) => + TestModuleRepr( + name = testModule, + supertypes = Seq("MavenTests"), + mixins = Seq(mixin), + configs = Seq( + JavaModuleConfig( + mandatoryMvnDeps = mandatoryMvnDeps, + mvnDeps = testDeps.diff(mandatoryMvnDeps), + bomMvnDeps = bomMvnDeps("test"), + moduleDeps = moduleDeps("test", "provided") + ), + RunModuleConfig( + // Retained from https://github.com/com-lihaoyi/mill/commit/5e650f6b78d903f34e122a9f97c6b223c6251d8f + forkWorkingDir = "moduleDir" + ) + ), + // Retained from https://github.com/com-lihaoyi/mill/commit/ba5960983895565f230166464e66524f0a1a5fd8 + testParallelism = false, + testSandboxWorkingDir = false + ) + } else None + + val (javacOptions, errorProneModuleConfig) = ErrorProneModuleConfig.javacOptionsAndConfig( + Plugins.javacOptions(model), + Plugins.annotationProcessorMvnDeps(model) // TODO Filter known error-prone specific deps + ) + val javaModuleConfig = JavaModuleConfig( + mvnDeps = mvnDeps("compile"), + compileMvnDeps = mvnDeps("provided"), + runMvnDeps = mvnDeps("runtime"), + bomMvnDeps = bomMvnDeps("compile"), + moduleDeps = moduleDeps("compile"), + compileModuleDeps = moduleDeps("provided"), + runModuleDeps = moduleDeps("runtime"), + javacOptions = javacOptions.diff(JavaModuleConfig.unsupportedJavacOptions) + ) + val publishModuleConfig = Option.when(!Plugins.skipDeploy(model)): + PublishModuleConfig( + pomPackagingType = Option(model.getPackaging).filter(_ != "jar").orNull, + pomParentProject = toPomParentProject(model.getParent), + pomSettings = toPomSettings(model), + publishVersion = model.getVersion, + artifactMetadata = toArtifactMetadata(model), + publishProperties = + if (publishProperties.value) model.getProperties.asScala.toMap else Map() + ) + val javaHomeModuleConfig = Plugins.jvmId(model).map(JavaHomeModuleConfig(_)) + val coursierModuleConfig = + model.getRepositories.iterator.asScala.collect: + case repo if repo.getId != "central" => repo.getUrl + .toSeq match { + case Nil => None + case repositories => Some(CoursierModuleConfig(repositories)) + } - println("converted Maven build to Mill") + ModuleRepr( + segments = segments, + supertypes = Seq("MavenModule") ++ + (if (publishModuleConfig.isEmpty) Nil else Seq("PublishModule")) ++ + (if (errorProneModuleConfig.isEmpty) Nil else Seq("ErrorProneModule")), + configs = javaModuleConfig +: Seq( + publishModuleConfig, + javaHomeModuleConfig, + coursierModuleConfig, + errorProneModuleConfig + ).flatten, + testModule = testModule0 + ) + .map(Tree(_)).toSeq + + var build = BuildRepr.fill(packages) + if (merge.value) build = BuildRepr.merged(build) + + val writer = if (noMetaBuild.value) BuildWriter(build) + else + val (build0, metaBuild) = MetaBuildRepr.of(build) + BuildWriter(build0, Some(metaBuild)) + writer.writeFiles() } - override def getBaseInfo( - input: Tree[Node[Model]], - cfg: Config, - baseModule: String, - packagesSize: Int - ): IrBaseInfo = { - val model = input.node.value - val javacOptions = Plugins.MavenCompilerPlugin.javacOptions(model) - val scalaVersion = None - val scalacOptions = None - val repositories = getRepositories(model) - val pomSettings = extractPomSettings(model) - val publishVersion = model.getVersion - val publishProperties = getPublishProperties(model, cfg.shared) - - val typedef = IrTrait( - cfg.shared.basicConfig.jvmId, - baseModule, - getModuleSupertypes, - javacOptions, - scalaVersion, - scalacOptions, - pomSettings, - publishVersion, - publishProperties, - repositories + def toMvnDep(dep: Dependency) = { + import dep.* + JavaModuleConfig.mvnDep( + getGroupId, + getArtifactId, + getVersion, + // prevent interpolation in dynamic values such as ${os.detected.name} + Option(getClassifier).map(_.replace("$", "$$")), + Option(getType), + getExclusions.asScala.map(x => (x.getGroupId, x.getArtifactId)) ) - - IrBaseInfo(typedef) } - override type ModuleFqnMap = Map[(String, String, String), String] - override def getModuleFqnMap(moduleNodes: Seq[Node[Model]]) - : Map[(String, String, String), String] = - buildModuleFqnMap(moduleNodes)(getProjectGav) - - override def extractIrBuild( - cfg: Config, - // baseInfo: IrBaseInfo, - build: Node[Model], - moduleFqnMap: Map[(String, String, String), String] - ): IrBuild = { - val model = build.value - val scopedDeps = extractScopedDeps(model, moduleFqnMap, cfg) - val version = model.getVersion - IrBuild( - scopedDeps = scopedDeps, - testModule = cfg.shared.basicConfig.testModule, - testModuleMainType = "MavenTests", - hasTest = os.exists(getMillSourcePath(model) / "src/test"), - dirs = build.dirs, - repositories = getRepositories(model), - javacOptions = Plugins.MavenCompilerPlugin.javacOptions(model), - scalaVersion = None, - scalacOptions = None, - projectName = getArtifactId(model), - pomSettings = extractPomSettings(model), - publishVersion = version, - packaging = model.getPackaging, - pomParentArtifact = mkPomParent(model.getParent), - resources = - processResources(model.getBuild.getResources, getMillSourcePath(model)) - .filterNot(_ == mavenMainResourceDir), - testResources = - processResources(model.getBuild.getTestResources, getMillSourcePath(model)) - .filterNot(_ == mavenTestResourceDir), - publishProperties = getPublishProperties(model, cfg.shared), - jvmId = cfg.shared.basicConfig.jvmId, - // Maven subproject tests run in the subproject folder, unlike Gradle - // and SBT whose subproject tests run in the root project folder - testForkDir = Some("moduleDir") - ) + def toPomParentProject(parent: Parent) = { + Option.when(parent != null): + import parent.* + PublishModuleConfig.Artifact(getGroupId, getArtifactId, getVersion) } - def getModuleSupertypes: Seq[String] = Seq("PublishModule", "MavenModule") - - def getProjectGav(model: Model): (String, String, String) = - (model.getGroupId, model.getArtifactId, model.getVersion) - - override def getArtifactId(model: Model): String = model.getArtifactId - - def getMillSourcePath(model: Model): Path = os.Path(model.getProjectDirectory) - - override def getSupertypes(cfg: Config, baseInfo: IrBaseInfo, build: Node[Model]): Seq[String] = - cfg.shared.basicConfig.baseModule.fold(getModuleSupertypes)(Seq(_)) - - def processResources( - input: java.util.List[org.apache.maven.model.Resource], - millSourcePath: os.Path - ): Seq[SubPath] = { - input - .asScala - .map(_.getDirectory) - .map(os.Path(_).subRelativeTo(millSourcePath)) - .toSeq + def toPomSettings(model: Model) = { + import model.* + PublishModuleConfig.PomSettings( + description = getDescription, + organization = Option(getOrganization).fold(null)(_.getName), + url = getUrl, + licenses = getLicenses.iterator.asScala.map(toLicense).toSeq, + versionControl = toVersionControl(getScm), + developers = getDevelopers.iterator.asScala.map(toDeveloper).toSeq + ) } - def getRepositories(model: Model): Seq[String] = - model.getRepositories.iterator().asScala - .filterNot(_.getId == "central") - .map(repo => escape(repo.getUrl)) - .toSeq - .sorted - - def getPublishProperties(model: Model, cfg: BuildGenUtil.Config): Seq[(String, String)] = - if (cfg.publishProperties.value) { - val props = model.getProperties - props.stringPropertyNames().iterator().asScala - .map(key => (key, props.getProperty(key))) - .toSeq - .sorted - } else Seq.empty - - def interpMvn(dep: Dependency): String = - BuildGenUtil.renderMvnString( - dep.getGroupId, - dep.getArtifactId, - None, - dep.getVersion, - dep.getType, - dep.getClassifier, - dep.getExclusions.iterator().asScala.map(x => (x.getGroupId, x.getArtifactId)) + def toLicense(license: License) = { + import license.* + PublishModuleConfig.License( + name = getName, + url = getUrl, + distribution = getDistribution ) + } - def mkPomParent(parent: Parent): IrArtifact = - if (null == parent) null - else IrArtifact(parent.getGroupId, parent.getArtifactId, parent.getVersion) + def toVersionControl(scm: Scm) = { + if (scm == null) PublishModuleConfig.VersionControl() + else + import scm.* + PublishModuleConfig.VersionControl( + browsableRepository = Option(getUrl), + connection = Option(getConnection), + developerConnection = Option(getDeveloperConnection), + tag = Option(getTag) + ) + } - def extractPomSettings(model: Model): IrPom = { - IrPom( - model.getDescription, - model.getGroupId, // Mill uses group for POM org - model.getUrl, - licenses = model.getLicenses.asScala.toSeq - .map(lic => IrLicense(lic.getName, lic.getName, lic.getUrl)), - versionControl = Option(model.getScm).fold(IrVersionControl(null, null, null, null))(scm => - IrVersionControl(scm.getUrl, scm.getConnection, scm.getDeveloperConnection, scm.getTag) - ), - developers = model.getDevelopers.iterator().asScala.toSeq - .map(dev => - IrDeveloper( - dev.getId, - dev.getName, - dev.getUrl, - dev.getOrganization, - dev.getOrganizationUrl - ) - ) + def toDeveloper(developer: Developer) = { + import developer.* + PublishModuleConfig.Developer( + id = getId, + name = getName, + url = getUrl, + organization = Option(getOrganization), + organizationUrl = Option(getOrganizationUrl) ) } - def extractScopedDeps( - model: Model, - packages: PartialFunction[(String, String, String), String], - cfg: Config - ): IrScopedDeps = { - var sd = IrScopedDeps() - - val hasTest = os.exists(os.Path(model.getProjectDirectory) / "src/test") - val mvnDep: Dependency => String = { - cfg.shared.basicConfig.depsObject.fold(interpMvn(_)) { objName => dep => - { - val depName = s"`${dep.getGroupId}:${dep.getArtifactId}`" - sd = sd.copy(namedMvnDeps = sd.namedMvnDeps :+ (depName, interpMvn(dep))) - s"$objName.$depName" - } - } - } - - model.getDependencies.forEach { dep => - val id = (dep.getGroupId, dep.getArtifactId, dep.getVersion) - dep.getScope match { - - case "compile" => - if (packages.isDefinedAt(id)) - sd = sd.copy(mainModuleDeps = sd.mainModuleDeps + packages(id)) - else { - if (isBom(id)) println(s"assuming compile dependency $id is a BOM") - val mvn = mvnDep(dep) - sd = sd.copy(mainMvnDeps = sd.mainMvnDeps + mvn) - } - case "provided" => - // Provided dependencies are available at compile time in both - // `src/main/java` and `src/test/java` - if (packages.isDefinedAt(id)) - sd = sd.copy( - mainCompileModuleDeps = sd.mainCompileModuleDeps + packages(id), - testCompileModuleDeps = sd.testCompileModuleDeps + packages(id) - ) - else { - val mvn = mvnDep(dep) - sd = sd.copy( - mainCompileMvnDeps = sd.mainCompileMvnDeps + mvn, - testCompileMvnDeps = sd.testCompileMvnDeps + mvn - ) - } - case "runtime" => - if (packages.isDefinedAt(id)) - sd = sd.copy(mainRunModuleDeps = sd.mainRunModuleDeps + packages(id)) - else { - val mvn = mvnDep(dep) - sd = sd.copy(mainRunMvnDeps = sd.mainRunMvnDeps + mvn) - } - - case "test" => - if (packages.isDefinedAt(id)) - sd = sd.copy(testModuleDeps = sd.testModuleDeps + packages(id)) - else { - val mvn = mvnDep(dep) - if (isBom(id)) { - sd = sd.copy(testBomMvnDeps = sd.testBomMvnDeps + mvn) - } else { - sd = sd.copy(testMvnDeps = sd.testMvnDeps + mvn) - } - } - - case scope => - println(s"ignoring $scope dependency $id") - - } - if (hasTest && sd.testModule.isEmpty) { - sd = sd.copy(testModule = testModulesByGroup.get(dep.getGroupId)) - } - } - sd + def toArtifactMetadata(model: Model) = { + import model.* + PublishModuleConfig.Artifact(getGroupId, getArtifactId, getVersion) } - - @main - @internal - case class Config( - shared: BuildGenUtil.Config, - @arg(doc = "use cache for Maven repository system") - cacheRepository: Flag = Flag(), - @arg(doc = "process Maven plugin executions and configurations") - processPlugins: Flag = Flag() - ) extends ModelerConfig - } + +@mainargs.main +case class MavenBuildGenArgs( + @mainargs.arg(doc = "name of generated test module") + testModule: String = "test", + @mainargs.arg(doc = "merge generated build files") + merge: mainargs.Flag, + @mainargs.arg(doc = "copy properties for publish") + publishProperties: mainargs.Flag, + @mainargs.arg(doc = "disables generating meta-build") + noMetaBuild: mainargs.Flag +) diff --git a/libs/init/maven/src/mill/main/maven/Modeler.scala b/libs/init/maven/src/mill/main/maven/Modeler.scala index 0611c42c0282..25e227abdadc 100644 --- a/libs/init/maven/src/mill/main/maven/Modeler.scala +++ b/libs/init/maven/src/mill/main/maven/Modeler.scala @@ -1,6 +1,5 @@ package mill.main.maven -import mill.api.daemon.internal.internal import org.apache.maven.model.Model import org.apache.maven.model.building.{ DefaultModelBuilderFactory, @@ -9,7 +8,6 @@ import org.apache.maven.model.building.{ } 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 @@ -21,35 +19,30 @@ import java.util.Properties * * The implementation is inspired by [[https://github.com/sbt/sbt-pom-reader/ sbt-pom-reader]]. */ -@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) + def read(pomDir: os.Path): Model = + read((pomDir / "pom.xml").toIO) /** Builds and returns the effective [[Model]] from `pomFile`. */ - def build(pomFile: File): Model = { + def read(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 } } -@internal object Modeler { def apply( - config: ModelerConfig, local: LocalRepository = defaultLocalRepository, remotes: Seq[RemoteRepository] = defaultRemoteRepositories, context: String = "", @@ -59,9 +52,8 @@ object Modeler { 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) + new Modeler(builder, resolver, systemProperties) } def defaultLocalRepository: LocalRepository = @@ -80,9 +72,3 @@ object Modeler { props } } - -@internal -trait ModelerConfig { - def cacheRepository: mainargs.Flag - def processPlugins: mainargs.Flag -} diff --git a/libs/init/maven/src/mill/main/maven/Plugins.scala b/libs/init/maven/src/mill/main/maven/Plugins.scala index a40aa755801f..eb818428fe47 100644 --- a/libs/init/maven/src/mill/main/maven/Plugins.scala +++ b/libs/init/maven/src/mill/main/maven/Plugins.scala @@ -1,62 +1,114 @@ package mill.main.maven -import mill.api.daemon.internal.internal -import org.apache.maven.model.{Model, Plugin} +import mill.main.buildgen.{JavaHomeModuleConfig, JavaModuleConfig} +import org.apache.maven.artifact.versioning.VersionRange +import org.apache.maven.model.{ConfigurationContainer, Model, Plugin} import org.codehaus.plexus.util.xml.Xpp3Dom import scala.jdk.CollectionConverters.* -/** Utilities for handling Maven plugins. */ -@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 annotationProcessorMvnDeps(model: Model): Seq[String] = + mavenCompilerPlugin(model).flatMap(config).fold(Nil)( + children(_, "annotationProcessorPaths", "path").iterator.flatMap: dom => + for + groupId <- value(dom, "groupId") + artifactId <- value(dom, "artifactId") + version = value(dom, "version").orNull + exclusions = children(dom, "exclusions").iterator.flatMap: dom => + for + groupId <- value(dom, "groupId") + artifactId <- value(dom, "artifactId") + yield (groupId, artifactId) + .toSeq + yield JavaModuleConfig.mvnDep(groupId, artifactId, version, excludes = exclusions) + .toSeq + ) - def dom(plugin: Plugin): Option[Xpp3Dom] = - plugin.getConfiguration match { - case xpp3: Xpp3Dom => Some(xpp3) - case _ => None + def javacOptions(model: Model): Seq[String] = { + val b = Seq.newBuilder[String] + mavenCompilerPlugin(model).flatMap(config).foreach { dom => + // javac requires --release to be mutually exclusive with -source/-target + value(dom, "release") match { + case Some(value) => b += "--release" += value + case None => + value(dom, "source").foreach(b += "-source" += _) + value(dom, "target").foreach(b += "-target" += _) + } + value(dom, "encoding").foreach(b += "-encoding" += _) + b ++= values(dom, "compilerArgs") } + // https://maven.apache.org/configure.html + val configFile = os.pwd / ".mvn/jvm.config" + if (os.exists(configFile)) b ++= os.read.lines(configFile).iterator.flatMap(_.split(" ")) + b.result() + } + + def skipDeploy(model: Model): Boolean = + mavenDeployPlugin(model).flatMap(config).flatMap(value(_, "skip")).fold(false)(_.toBoolean) + + /** + * @see [[https://maven.apache.org/enforcer/enforcer-rules/requireJavaVersion.html]] + */ + def jvmId(model: Model): Option[String] = { + mavenEnforcerPlugin(model).flatMap( + _.getExecutions.iterator.asScala.flatMap(config) + .flatMap(value(_, "rules", "requireJavaVersion", "version")) + .map(VersionRange.createFromVersionSpec) + .flatMap: v => + Option(v.getRecommendedVersion) + .orElse(v.getRestrictions.iterator.asScala.collectFirst: + case r if r.getLowerBound != null => r.getLowerBound) + .minOption + .map(v => JavaHomeModuleConfig.jvmId(v.getMinorVersion)) + ) + } /** * @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 += _) - } + def mavenCompilerPlugin(model: Model): Option[Plugin] = + plugin(model, "org.apache.maven.plugins", "maven-compiler-plugin") - options.result() - } + /** + * @see [[https://maven.apache.org/plugins/maven-deploy-plugin/index.html]] + */ + def mavenDeployPlugin(model: Model): Option[Plugin] = + plugin(model, "org.apache.maven.plugins", "maven-deploy-plugin") + + /** + * @see [[https://maven.apache.org/enforcer/maven-enforcer-plugin/index.html]] + */ + def mavenEnforcerPlugin(model: Model): Option[Plugin] = + plugin(model, "org.apache.maven.plugins", "maven-enforcer-plugin") + + def plugin(model: Model, groupId: String, artifactId: String): Option[Plugin] = + model.getBuild.getPlugins.iterator.asScala + .find(p => p.getGroupId == groupId && p.getArtifactId == artifactId) + + def config(cc: ConfigurationContainer): Option[Xpp3Dom] = cc.getConfiguration match { + case dom: Xpp3Dom => Some(dom) + case _ => None } - private implicit class NullableDomOps(val self: Xpp3Dom) extends AnyVal { + def children(dom: Xpp3Dom, path: String*): IterableOnce[Xpp3Dom] = + if (dom == null) Nil + else if (path.isEmpty) dom.getChildren.iterator + else dom.getChildren(path.head).flatMap(children(_, path.tail*)) - def child(name: String): Xpp3Dom = - if (null == self) self else self.getChild(name) + def child(dom: Xpp3Dom, path: String*): Option[Xpp3Dom] = + if (dom == null) None + else if (path.isEmpty) Some(dom) + else child(dom.getChild(path.head), path.tail*) - def foreachValue(f: String => Unit): Unit = - if (null != self) f(self.getValue) + def value(dom: Xpp3Dom, path: String*): Option[String] = + if (null == dom) None + else if (path.isEmpty) Option(dom.getValue) + else value(dom.getChild(path.head), path.tail*) - def foreachChildValue(f: String => Unit): Unit = - if (null != self) self.getChildren.iterator.foreach(dom => f(dom.getValue)) - } + def values(dom: Xpp3Dom, path: String*): IterableOnce[String] = + if (dom == null) Nil + else if (path.isEmpty) dom.getChildren.iterator.flatMap(dom => Option(dom.getValue)) + else dom.getChildren(path.head).flatMap(values(_, path.tail*)) } diff --git a/libs/init/maven/src/mill/main/maven/Resolver.scala b/libs/init/maven/src/mill/main/maven/Resolver.scala index d101ba61ae5a..8ddf5e6e0def 100644 --- a/libs/init/maven/src/mill/main/maven/Resolver.scala +++ b/libs/init/maven/src/mill/main/maven/Resolver.scala @@ -1,6 +1,5 @@ package mill.main.maven -import mill.api.daemon.internal.internal import org.apache.maven.model.building.{FileModelSource, ModelSource} import org.apache.maven.model.resolution.{ModelResolver, UnresolvableModelException} import org.apache.maven.model.{Dependency, Parent, Repository} @@ -17,7 +16,6 @@ import scala.jdk.CollectionConverters.* * * The implementation is inspired by [[https://github.com/sbt/sbt-pom-reader/ sbt-pom-reader]]. */ -@internal class Resolver( system: RepositorySystem, session: RepositorySystemSession, diff --git a/libs/init/maven/test/resources/expected/config/build.mill b/libs/init/maven/test/resources/expected/config/build.mill index 4c4421814d06..6b6c7a00953d 100644 --- a/libs/init/maven/test/resources/expected/config/build.mill +++ b/libs/init/maven/test/resources/expected/config/build.mill @@ -2,36 +2,37 @@ //| mill-jvm-version: 11 package build -import _root_.build_.MyModule -import mill._ import mill.javalib._ import mill.javalib.publish._ -object Deps { +object `package` extends MavenModule with PublishModule { - val `javax.servlet.jsp:jsp-api` = mvn"javax.servlet.jsp:jsp-api:2.2" - val `javax.servlet:servlet-api` = mvn"javax.servlet:servlet-api:2.5" - val `junit:junit-dep` = mvn"junit:junit-dep:4.10" - val `org.hamcrest:hamcrest-core` = mvn"org.hamcrest:hamcrest-core:1.2.1" - val `org.hamcrest:hamcrest-library` = mvn"org.hamcrest:hamcrest-library:1.2.1" - val `org.mockito:mockito-core` = mvn"org.mockito:mockito-core:1.8.5" -} + def pomPackagingType = "pom" -object `package` extends MyModule { + def pomSettings = PomSettings( + "Just a pom that makes it easy to build both projects at the same time.", + "", + "", + Seq(), + VersionControl(None, None, None, None), + Seq() + ) - def artifactName = "parent" + def publishVersion = "1.0-SNAPSHOT" - def pomPackagingType = PackagingType.Pom + def artifactMetadata = + Artifact("com.example.maven-samples", "parent", "1.0-SNAPSHOT") - object `multi-module` extends MyModule { + object `multi-module` extends MavenModule with PublishModule { - def artifactName = "multi-module-parent" + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") - def javacOptions = Seq("-source", "1.6", "-target", "1.6") + def pomPackagingType = "pom" def pomSettings = PomSettings( "Sample multi module Maven project with a working, deployable site.", - "com.example.maven-samples", + "", "http://www.example.com", Seq(), VersionControl( @@ -43,20 +44,33 @@ object `package` extends MyModule { Seq() ) - def pomPackagingType = PackagingType.Pom + def publishVersion = "1.0-SNAPSHOT" + + def artifactMetadata = Artifact( + "com.example.maven-samples", + "multi-module-parent", + "1.0-SNAPSHOT" + ) def publishProperties = super.publishProperties() ++ Map( ("project.build.sourceEncoding", "utf-8"), ("project.reporting.outputEncoding", "utf-8") ) - object server extends MyModule { + object server extends MavenModule with PublishModule { + + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") - def javacOptions = Seq("-source", "1.6", "-target", "1.6") + def pomParentProject = Some(Artifact( + "com.example.maven-samples", + "multi-module-parent", + "1.0-SNAPSHOT" + )) def pomSettings = PomSettings( "Logic.", - "com.example.maven-samples", + "", "http://www.example.com/server", Seq(), VersionControl( @@ -68,11 +82,10 @@ object `package` extends MyModule { Seq() ) - def pomParentProject = Some(Artifact( - "com.example.maven-samples", - "multi-module-parent", - "1.0-SNAPSHOT" - )) + def publishVersion = "1.0-SNAPSHOT" + + def artifactMetadata = + Artifact("com.example.maven-samples", "server", "1.0-SNAPSHOT") def publishProperties = super.publishProperties() ++ Map( ("project.build.sourceEncoding", "utf-8"), @@ -81,32 +94,46 @@ object `package` extends MyModule { object tests extends MavenTests with TestModule.Junit4 { - def mvnDeps = Seq( - Deps.`junit:junit-dep`, - Deps.`org.hamcrest:hamcrest-core`, - Deps.`org.hamcrest:hamcrest-library`, - Deps.`org.mockito:mockito-core` + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(mvn"junit:junit-dep:4.10") + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.hamcrest:hamcrest-core:1.2.1", + mvn"org.hamcrest:hamcrest-library:1.2.1", + mvn"org.mockito:mockito-core:1.8.5" ) - def testSandboxWorkingDir = false - def testParallelism = false def forkWorkingDir = moduleDir + def testParallelism = false + + def testSandboxWorkingDir = false } - } - object webapp extends MyModule { + } + object webapp extends MavenModule with PublishModule { - def javacOptions = Seq("-source", "1.6", "-target", "1.6") + def compileMvnDeps = super.compileMvnDeps() ++ Seq( + mvn"javax.servlet:servlet-api:2.5", + mvn"javax.servlet.jsp:jsp-api:2.2" + ) def moduleDeps = super.moduleDeps ++ Seq(build.`multi-module`.server) - def compileMvnDeps = - Seq(Deps.`javax.servlet.jsp:jsp-api`, Deps.`javax.servlet:servlet-api`) + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") + + def pomPackagingType = "war" + + def pomParentProject = Some(Artifact( + "com.example.maven-samples", + "multi-module-parent", + "1.0-SNAPSHOT" + )) def pomSettings = PomSettings( "Webapp.", - "com.example.maven-samples", + "", "http://www.example.com/webapp", Seq(), VersionControl( @@ -118,13 +145,10 @@ object `package` extends MyModule { Seq() ) - def pomPackagingType = "war" + def publishVersion = "1.0-SNAPSHOT" - def pomParentProject = Some(Artifact( - "com.example.maven-samples", - "multi-module-parent", - "1.0-SNAPSHOT" - )) + def artifactMetadata = + Artifact("com.example.maven-samples", "webapp", "1.0-SNAPSHOT") def publishProperties = super.publishProperties() ++ Map( ("project.build.sourceEncoding", "utf-8"), @@ -133,19 +157,19 @@ object `package` extends MyModule { } } + object `single-module` extends MavenModule with PublishModule { - object `single-module` extends MyModule { - - def artifactName = "single-module-project" - - def javacOptions = Seq("-source", "1.6", "-target", "1.6") + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"javax.servlet:servlet-api:2.5", + mvn"javax.servlet.jsp:jsp-api:2.2" + ) - def mvnDeps = - Seq(Deps.`javax.servlet.jsp:jsp-api`, Deps.`javax.servlet:servlet-api`) + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") def pomSettings = PomSettings( "Sample single module Maven project with a working, deployable site.", - "com.example.maven-samples", + "", "http://www.example.com", Seq(), VersionControl( @@ -157,6 +181,14 @@ object `package` extends MyModule { Seq() ) + def publishVersion = "1.0-SNAPSHOT" + + def artifactMetadata = Artifact( + "com.example.maven-samples", + "single-module-project", + "1.0-SNAPSHOT" + ) + def publishProperties = super.publishProperties() ++ Map( ("project.build.sourceEncoding", "utf-8"), ("project.reporting.outputEncoding", "utf-8") @@ -164,32 +196,21 @@ object `package` extends MyModule { object tests extends MavenTests with TestModule.Junit4 { - def mvnDeps = Seq( - Deps.`junit:junit-dep`, - Deps.`org.hamcrest:hamcrest-core`, - Deps.`org.hamcrest:hamcrest-library`, - Deps.`org.mockito:mockito-core` + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(mvn"junit:junit-dep:4.10") + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.hamcrest:hamcrest-core:1.2.1", + mvn"org.hamcrest:hamcrest-library:1.2.1", + mvn"org.mockito:mockito-core:1.8.5" ) - def testSandboxWorkingDir = false - def testParallelism = false def forkWorkingDir = moduleDir - } - } -} - -trait MyModule extends PublishModule with MavenModule { - - def pomSettings = PomSettings( - "Just a pom that makes it easy to build both projects at the same time.", - "com.example.maven-samples", - "", - Seq(), - VersionControl(None, None, None, None), - Seq() - ) + def testParallelism = false - def publishVersion = "1.0-SNAPSHOT" + def testSandboxWorkingDir = false + } + } } diff --git a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/build.mill b/libs/init/maven/test/resources/expected/maven-samples-jvm-id/build.mill deleted file mode 100644 index eccd085fe170..000000000000 --- a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/build.mill +++ /dev/null @@ -1,26 +0,0 @@ -//| mill-version: SNAPSHOT -//| mill-jvm-version: 11 -package build - -import mill._ -import mill.javalib._ -import mill.javalib.publish._ - -object `package` extends PublishModule with MavenModule { - - def artifactName = "parent" - - def pomSettings = PomSettings( - "Just a pom that makes it easy to build both projects at the same time.", - "com.example.maven-samples", - "", - Seq(), - VersionControl(None, None, None, None), - Seq() - ) - - def publishVersion = "1.0-SNAPSHOT" - - def pomPackagingType = PackagingType.Pom - -} diff --git a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/package.mill b/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/package.mill deleted file mode 100644 index f1cb2d6fc388..000000000000 --- a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/package.mill +++ /dev/null @@ -1,31 +0,0 @@ -package build.`multi-module` - -import mill._ -import mill.javalib._ -import mill.javalib.publish._ - -object `package` extends PublishModule with MavenModule { - - def artifactName = "multi-module-parent" - - def javacOptions = Seq("-source", "1.6", "-target", "1.6") - - 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() - ) - - def publishVersion = "1.0-SNAPSHOT" - - def pomPackagingType = PackagingType.Pom - -} diff --git a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/server/package.mill b/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/server/package.mill deleted file mode 100644 index 8fd431e1c990..000000000000 --- a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/server/package.mill +++ /dev/null @@ -1,45 +0,0 @@ -package build.`multi-module`.server - -import mill._ -import mill.javalib._ -import mill.javalib.publish._ - -object `package` extends PublishModule with MavenModule { - - def javacOptions = Seq("-source", "1.6", "-target", "1.6") - - 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() - ) - - def publishVersion = "1.0-SNAPSHOT" - - def pomParentProject = Some( - Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") - ) - - object test extends MavenTests with TestModule.Junit4 { - - def mvnDeps = Seq( - mvn"junit:junit-dep:4.10", - mvn"org.hamcrest:hamcrest-core:1.2.1", - mvn"org.hamcrest:hamcrest-library:1.2.1", - mvn"org.mockito:mockito-core:1.8.5" - ) - - def testSandboxWorkingDir = false - def testParallelism = false - def forkWorkingDir = moduleDir - - } -} diff --git a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/webapp/package.mill b/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/webapp/package.mill deleted file mode 100644 index a88804fd5ef9..000000000000 --- a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/multi-module/webapp/package.mill +++ /dev/null @@ -1,38 +0,0 @@ -package build.`multi-module`.webapp - -import mill._ -import mill.javalib._ -import mill.javalib.publish._ - -object `package` extends PublishModule with MavenModule { - - def javacOptions = Seq("-source", "1.6", "-target", "1.6") - - def moduleDeps = super.moduleDeps ++ Seq(build.`multi-module`.server) - - def compileMvnDeps = - Seq(mvn"javax.servlet.jsp:jsp-api:2.2", mvn"javax.servlet:servlet-api:2.5") - - 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() - ) - - def publishVersion = "1.0-SNAPSHOT" - - def pomPackagingType = "war" - - def pomParentProject = Some( - Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") - ) - -} diff --git a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/single-module/package.mill b/libs/init/maven/test/resources/expected/maven-samples-jvm-id/single-module/package.mill deleted file mode 100644 index e11b1c158895..000000000000 --- a/libs/init/maven/test/resources/expected/maven-samples-jvm-id/single-module/package.mill +++ /dev/null @@ -1,46 +0,0 @@ -package build.`single-module` - -import mill._ -import mill.javalib._ -import mill.javalib.publish._ - -object `package` extends PublishModule with MavenModule { - - def artifactName = "single-module-project" - - def javacOptions = Seq("-source", "1.6", "-target", "1.6") - - def mvnDeps = - Seq(mvn"javax.servlet.jsp:jsp-api:2.2", mvn"javax.servlet:servlet-api:2.5") - - 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() - ) - - def publishVersion = "1.0-SNAPSHOT" - - object test extends MavenTests with TestModule.Junit4 { - - def mvnDeps = Seq( - mvn"junit:junit-dep:4.10", - mvn"org.hamcrest:hamcrest-core:1.2.1", - mvn"org.hamcrest:hamcrest-library:1.2.1", - mvn"org.mockito:mockito-core:1.8.5" - ) - - def testSandboxWorkingDir = false - def testParallelism = false - def forkWorkingDir = moduleDir - - } -} diff --git a/libs/init/maven/test/resources/expected/maven-samples/build.mill b/libs/init/maven/test/resources/expected/maven-samples/build.mill index 53f1379428ef..f6d683fd504b 100644 --- a/libs/init/maven/test/resources/expected/maven-samples/build.mill +++ b/libs/init/maven/test/resources/expected/maven-samples/build.mill @@ -1,25 +1,25 @@ //| mill-version: SNAPSHOT +//| mill-jvm-version: 11 package build -import mill._ import mill.javalib._ import mill.javalib.publish._ +import millbuild._ -object `package` extends PublishModule with MavenModule { +object `package` extends MavenSamplesBaseModule { - def artifactName = "parent" + def pomPackagingType = "pom" def pomSettings = PomSettings( "Just a pom that makes it easy to build both projects at the same time.", - "com.example.maven-samples", + "", "", Seq(), VersionControl(None, None, None, None), Seq() ) - def publishVersion = "1.0-SNAPSHOT" - - def pomPackagingType = PackagingType.Pom + def artifactMetadata = + Artifact("com.example.maven-samples", "parent", "1.0-SNAPSHOT") } diff --git a/libs/init/maven/test/resources/expected/maven-samples/mill-build/build.mill b/libs/init/maven/test/resources/expected/maven-samples/mill-build/build.mill new file mode 100644 index 000000000000..26686f5e0d2e --- /dev/null +++ b/libs/init/maven/test/resources/expected/maven-samples/mill-build/build.mill @@ -0,0 +1,5 @@ +package build + +import mill.meta.MillBuildRootModule + +object `package` extends MillBuildRootModule diff --git a/libs/init/maven/test/resources/expected/maven-samples/mill-build/src/Deps.scala b/libs/init/maven/test/resources/expected/maven-samples/mill-build/src/Deps.scala new file mode 100644 index 000000000000..0c2e27aeeadf --- /dev/null +++ b/libs/init/maven/test/resources/expected/maven-samples/mill-build/src/Deps.scala @@ -0,0 +1,13 @@ +package millbuild + +import mill.javalib._ + +object Deps { + + val hamcrestCore = mvn"org.hamcrest:hamcrest-core:1.2.1" + val hamcrestLibrary = mvn"org.hamcrest:hamcrest-library:1.2.1" + val jspApi = mvn"javax.servlet.jsp:jsp-api:2.2" + val junitDep = mvn"junit:junit-dep:4.10" + val mockitoCore = mvn"org.mockito:mockito-core:1.8.5" + val servletApi = mvn"javax.servlet:servlet-api:2.5" +} diff --git a/libs/init/maven/test/resources/expected/maven-samples/mill-build/src/MavenSamplesBaseModule.scala b/libs/init/maven/test/resources/expected/maven-samples/mill-build/src/MavenSamplesBaseModule.scala new file mode 100644 index 000000000000..3084b061ff09 --- /dev/null +++ b/libs/init/maven/test/resources/expected/maven-samples/mill-build/src/MavenSamplesBaseModule.scala @@ -0,0 +1,10 @@ +package millbuild + +import mill.javalib._ +import mill.javalib.publish._ + +trait MavenSamplesBaseModule extends MavenModule with PublishModule { + + def publishVersion = "1.0-SNAPSHOT" + +} diff --git a/libs/init/maven/test/resources/expected/maven-samples/multi-module/package.mill b/libs/init/maven/test/resources/expected/maven-samples/multi-module/package.mill index f1cb2d6fc388..fa1e3ebf3df2 100644 --- a/libs/init/maven/test/resources/expected/maven-samples/multi-module/package.mill +++ b/libs/init/maven/test/resources/expected/maven-samples/multi-module/package.mill @@ -1,18 +1,19 @@ package build.`multi-module` -import mill._ import mill.javalib._ import mill.javalib.publish._ +import millbuild._ -object `package` extends PublishModule with MavenModule { +object `package` extends MavenSamplesBaseModule { - def artifactName = "multi-module-parent" + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") - def javacOptions = Seq("-source", "1.6", "-target", "1.6") + def pomPackagingType = "pom" def pomSettings = PomSettings( "Sample multi module Maven project with a working, deployable site.", - "com.example.maven-samples", + "", "http://www.example.com", Seq(), VersionControl( @@ -24,8 +25,7 @@ object `package` extends PublishModule with MavenModule { Seq() ) - def publishVersion = "1.0-SNAPSHOT" - - def pomPackagingType = PackagingType.Pom + def artifactMetadata = + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") } diff --git a/libs/init/maven/test/resources/expected/maven-samples/multi-module/server/package.mill b/libs/init/maven/test/resources/expected/maven-samples/multi-module/server/package.mill index 8fd431e1c990..127c8eead7c8 100644 --- a/libs/init/maven/test/resources/expected/maven-samples/multi-module/server/package.mill +++ b/libs/init/maven/test/resources/expected/maven-samples/multi-module/server/package.mill @@ -1,16 +1,21 @@ package build.`multi-module`.server -import mill._ import mill.javalib._ import mill.javalib.publish._ +import millbuild._ -object `package` extends PublishModule with MavenModule { +object `package` extends MavenSamplesBaseModule { - def javacOptions = Seq("-source", "1.6", "-target", "1.6") + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") + + def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) def pomSettings = PomSettings( "Logic.", - "com.example.maven-samples", + "", "http://www.example.com/server", Seq(), VersionControl( @@ -22,24 +27,21 @@ object `package` extends PublishModule with MavenModule { Seq() ) - def publishVersion = "1.0-SNAPSHOT" - - def pomParentProject = Some( - Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") - ) + def artifactMetadata = + Artifact("com.example.maven-samples", "server", "1.0-SNAPSHOT") object test extends MavenTests with TestModule.Junit4 { - def mvnDeps = Seq( - mvn"junit:junit-dep:4.10", - mvn"org.hamcrest:hamcrest-core:1.2.1", - mvn"org.hamcrest:hamcrest-library:1.2.1", - mvn"org.mockito:mockito-core:1.8.5" - ) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq(Deps.junitDep) + + def mvnDeps = super.mvnDeps() ++ + Seq(Deps.hamcrestCore, Deps.hamcrestLibrary, Deps.mockitoCore) - def testSandboxWorkingDir = false - def testParallelism = false def forkWorkingDir = moduleDir + def testParallelism = false + + def testSandboxWorkingDir = false } + } diff --git a/libs/init/maven/test/resources/expected/maven-samples/multi-module/webapp/package.mill b/libs/init/maven/test/resources/expected/maven-samples/multi-module/webapp/package.mill index a88804fd5ef9..fbaf535539aa 100644 --- a/libs/init/maven/test/resources/expected/maven-samples/multi-module/webapp/package.mill +++ b/libs/init/maven/test/resources/expected/maven-samples/multi-module/webapp/package.mill @@ -1,21 +1,28 @@ package build.`multi-module`.webapp -import mill._ import mill.javalib._ import mill.javalib.publish._ +import millbuild._ -object `package` extends PublishModule with MavenModule { +object `package` extends MavenSamplesBaseModule { - def javacOptions = Seq("-source", "1.6", "-target", "1.6") + def compileMvnDeps = super.compileMvnDeps() ++ + Seq(Deps.servletApi, Deps.jspApi) def moduleDeps = super.moduleDeps ++ Seq(build.`multi-module`.server) - def compileMvnDeps = - Seq(mvn"javax.servlet.jsp:jsp-api:2.2", mvn"javax.servlet:servlet-api:2.5") + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") + + def pomPackagingType = "war" + + def pomParentProject = Some( + Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") + ) def pomSettings = PomSettings( "Webapp.", - "com.example.maven-samples", + "", "http://www.example.com/webapp", Seq(), VersionControl( @@ -27,12 +34,7 @@ object `package` extends PublishModule with MavenModule { Seq() ) - def publishVersion = "1.0-SNAPSHOT" - - def pomPackagingType = "war" - - def pomParentProject = Some( - Artifact("com.example.maven-samples", "multi-module-parent", "1.0-SNAPSHOT") - ) + def artifactMetadata = + Artifact("com.example.maven-samples", "webapp", "1.0-SNAPSHOT") } diff --git a/libs/init/maven/test/resources/expected/maven-samples/single-module/package.mill b/libs/init/maven/test/resources/expected/maven-samples/single-module/package.mill index e11b1c158895..6195dd85b3f5 100644 --- a/libs/init/maven/test/resources/expected/maven-samples/single-module/package.mill +++ b/libs/init/maven/test/resources/expected/maven-samples/single-module/package.mill @@ -1,21 +1,19 @@ package build.`single-module` -import mill._ import mill.javalib._ import mill.javalib.publish._ +import millbuild._ -object `package` extends PublishModule with MavenModule { +object `package` extends MavenSamplesBaseModule { - def artifactName = "single-module-project" + def mvnDeps = super.mvnDeps() ++ Seq(Deps.servletApi, Deps.jspApi) - def javacOptions = Seq("-source", "1.6", "-target", "1.6") - - def mvnDeps = - Seq(mvn"javax.servlet.jsp:jsp-api:2.2", mvn"javax.servlet:servlet-api:2.5") + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") def pomSettings = PomSettings( "Sample single module Maven project with a working, deployable site.", - "com.example.maven-samples", + "", "http://www.example.com", Seq(), VersionControl( @@ -27,20 +25,24 @@ object `package` extends PublishModule with MavenModule { Seq() ) - def publishVersion = "1.0-SNAPSHOT" + def artifactMetadata = Artifact( + "com.example.maven-samples", + "single-module-project", + "1.0-SNAPSHOT" + ) object test extends MavenTests with TestModule.Junit4 { - def mvnDeps = Seq( - mvn"junit:junit-dep:4.10", - mvn"org.hamcrest:hamcrest-core:1.2.1", - mvn"org.hamcrest:hamcrest-library:1.2.1", - mvn"org.mockito:mockito-core:1.8.5" - ) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq(Deps.junitDep) + + def mvnDeps = super.mvnDeps() ++ + Seq(Deps.hamcrestCore, Deps.hamcrestLibrary, Deps.mockitoCore) - def testSandboxWorkingDir = false - def testParallelism = false def forkWorkingDir = moduleDir + def testParallelism = false + + def testSandboxWorkingDir = false } + } diff --git a/libs/init/maven/test/resources/expected/misc/build.mill b/libs/init/maven/test/resources/expected/misc/build.mill index fbb47d129167..566c39c8ad51 100644 --- a/libs/init/maven/test/resources/expected/misc/build.mill +++ b/libs/init/maven/test/resources/expected/misc/build.mill @@ -1,27 +1,21 @@ //| mill-version: SNAPSHOT +//| mill-jvm-version: 11 package build -import mill._ import mill.javalib._ import mill.javalib.publish._ +import millbuild._ -object `package` extends PublishModule with MavenModule { +object `package` extends MavenModule with PublishModule { - def artifactName = "single-module-project" + def mvnDeps = super.mvnDeps() ++ Seq(Deps.servletApi, Deps.jspApi) - def javacOptions = Seq("-source", "1.6", "-target", "1.6") - - def repositories = Seq( - "http://repository.jboss.org/nexus/content/groups/public/", - "https://maven.java.net/content/repositories/public/" - ) - - def mvnDeps = - Seq(mvn"javax.servlet.jsp:jsp-api:2.2", mvn"javax.servlet:servlet-api:2.5") + def javacOptions = super.javacOptions() ++ + Seq("-source", "1.6", "-target", "1.6") def pomSettings = PomSettings( "Sample single module Maven project with a working, deployable site.", - "com.example.maven-samples", + "", "http://www.example.com", Seq(), VersionControl( @@ -35,24 +29,29 @@ object `package` extends PublishModule with MavenModule { def publishVersion = "1.0-SNAPSHOT" - def resources = Task { super.resources() ++ customResources() } - def customResources = Task.Sources("resources") + def artifactMetadata = Artifact( + "com.example.maven-samples", + "single-module-project", + "1.0-SNAPSHOT" + ) + + def repositories = super.repositories() ++ Seq( + "https://maven.java.net/content/repositories/public/", + "http://repository.jboss.org/nexus/content/groups/public/" + ) object test extends MavenTests with TestModule.Junit4 { - def mvnDeps = Seq( - mvn"junit:junit-dep:4.10", - mvn"org.hamcrest:hamcrest-core:1.2.1", - mvn"org.hamcrest:hamcrest-library:1.2.1", - mvn"org.mockito:mockito-core:1.8.5" - ) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq(Deps.junitDep) - def resources = Task { super.resources() ++ customResources() } - def customResources = Task.Sources("test/resources", "src/test/secrets") + def mvnDeps = super.mvnDeps() ++ + Seq(Deps.hamcrestCore, Deps.hamcrestLibrary, Deps.mockitoCore) - def testSandboxWorkingDir = false - def testParallelism = false def forkWorkingDir = moduleDir + def testParallelism = false + + def testSandboxWorkingDir = false } + } diff --git a/libs/init/maven/test/resources/expected/misc/mill-build/build.mill b/libs/init/maven/test/resources/expected/misc/mill-build/build.mill new file mode 100644 index 000000000000..26686f5e0d2e --- /dev/null +++ b/libs/init/maven/test/resources/expected/misc/mill-build/build.mill @@ -0,0 +1,5 @@ +package build + +import mill.meta.MillBuildRootModule + +object `package` extends MillBuildRootModule diff --git a/libs/init/maven/test/resources/expected/misc/mill-build/src/Deps.scala b/libs/init/maven/test/resources/expected/misc/mill-build/src/Deps.scala new file mode 100644 index 000000000000..0c2e27aeeadf --- /dev/null +++ b/libs/init/maven/test/resources/expected/misc/mill-build/src/Deps.scala @@ -0,0 +1,13 @@ +package millbuild + +import mill.javalib._ + +object Deps { + + val hamcrestCore = mvn"org.hamcrest:hamcrest-core:1.2.1" + val hamcrestLibrary = mvn"org.hamcrest:hamcrest-library:1.2.1" + val jspApi = mvn"javax.servlet.jsp:jsp-api:2.2" + val junitDep = mvn"junit:junit-dep:4.10" + val mockitoCore = mvn"org.mockito:mockito-core:1.8.5" + val servletApi = mvn"javax.servlet:servlet-api:2.5" +} diff --git a/libs/init/maven/test/src/mill/main/maven/BuildGenTests.scala b/libs/init/maven/test/src/mill/main/maven/BuildGenTests.scala index 940014cddea2..2a0db2da1b6d 100644 --- a/libs/init/maven/test/src/mill/main/maven/BuildGenTests.scala +++ b/libs/init/maven/test/src/mill/main/maven/BuildGenTests.scala @@ -16,31 +16,12 @@ object BuildGenTests extends TestSuite { checker.check(MavenBuildGenMain.main(Array.empty), sourceRoot, expectedRoot) ) } - test("maven-samples-jvm-id") { - val sourceRoot = os.sub / "maven-samples" - val expectedRoot = os.sub / "expected/maven-samples-jvm-id" - assert( - checker.check(MavenBuildGenMain.main(Array("--jvm-id", "11")), sourceRoot, expectedRoot) - ) - } test("config") { val sourceRoot = os.sub / "maven-samples" val expectedRoot = os.sub / "expected/config" - val args = Array( - "--base-module", - "MyModule", - "--jvm-id", - "11", - "--test-module", - "tests", - "--deps-object", - "Deps", - "--publish-properties", - "--merge", - "--cache-repository", - "--process-plugins" - ) + val args = + Array("--test-module", "tests", "--merge", "--publish-properties", "--no-meta-build") assert( checker.check(MavenBuildGenMain.main(args), sourceRoot, expectedRoot) ) diff --git a/libs/init/package.mill b/libs/init/package.mill index f046b92f9021..be8ead027f09 100644 --- a/libs/init/package.mill +++ b/libs/init/package.mill @@ -1,12 +1,12 @@ package build.libs.init import mill._ +import mill.api.{BuildCtx, Cross} import mill.contrib.buildinfo.BuildInfo import mill.scalalib.Assembly.Rule import mill.scalalib.ScalaModule import mill.util.Jvm import millbuild.* -import mill.api.BuildCtx object `package` extends MillPublishScalaModule { @@ -69,24 +69,37 @@ object `package` extends MillPublishScalaModule { } object buildgen extends MillPublishScalaModule { - def moduleDeps = Seq(build.libs.init, build.core.internal, build.libs.util) + def moduleDeps = + Seq(api(Deps.scalaVersion), build.libs.init, build.core.internal, build.libs.util) def testModuleDeps = super.testModuleDeps ++ Seq(build.libs.scalalib) - // I tried moving `Tree` into this module, but it doesn't compile with Scala 2.12.20. - /* - object tree extends Cross[TreeModule](Deps.sbtScalaVersion212, Deps.scalaVersion) - trait TreeModule extends MillPublishCrossScalaModule { + object api extends Cross[ApiModule](Deps.sbtScalaVersion212, Deps.scalaVersion) + trait ApiModule extends MillPublishCrossScalaModule { def mvnDeps = Seq(Deps.upickle) } - */ } - object gradle extends MillPublishScalaModule { - def moduleDeps = Seq(buildgen) - def mvnDeps = Seq( - Deps.gradleApi, - Deps.logback - ) + object gradle extends MillPublishScalaModule with BuildInfo { + def moduleDeps = Seq(api, buildgen) + def mvnDeps = Seq(Deps.gradleApi) def testModuleDeps = super.testModuleDeps ++ Seq(build.libs.scalalib, buildgen.test) + def buildInfoPackageName = "mill.main.gradle" + def buildInfoMembers = Seq( + BuildInfo.Value("exportpluginAssemblyResource", "/exportplugin-assembly.jar") + ) + def exportpluginAssemblyResource = Task { + os.copy(exportplugin.assembly().path, Task.dest / "exportplugin-assembly.jar") + PathRef(Task.dest) + } + def resources = Task { super.resources() ++ Seq(exportpluginAssemblyResource()) } + + object api extends MillPublishJavaModule + + object exportplugin extends ScalaModule { + def scalaVersion = Deps.scalaVersion + def mvnDeps = Seq(Deps.osLib) + def moduleDeps = Seq(api, buildgen.api(Deps.scalaVersion)) + def compileMvnDeps = Seq(Deps.gradleApi) + } } object maven extends MillPublishScalaModule { def moduleDeps = Seq(buildgen) @@ -102,45 +115,22 @@ object `package` extends MillPublishScalaModule { } object sbt extends MillPublishScalaModule with BuildInfo { - def moduleDeps = Seq(buildgen, models(Deps.scalaVersion)) - - def sbtPluginJarResources = Task { - val assemblyPathRef = exportplugin.assembly() - os.copy(assemblyPathRef.path, Task.dest / "exportplugin-assembly.jar") + def moduleDeps = Seq(buildgen.api(Deps.scalaVersion), buildgen) + def sbtScriptJarResource = Task { + os.copy(exportscript.assembly().path, Task.dest / "exportscript-assembly.jar") PathRef(Task.dest) } - def resources: T[Seq[PathRef]] = Task { super.resources() ++ Seq(sbtPluginJarResources()) } + def resources = Task { super.resources() ++ Seq(sbtScriptJarResource()) } def testModuleDeps = super.testModuleDeps ++ Seq(build.libs.scalalib, buildgen.test) - def buildInfoPackageName = "mill.main.sbt" - def buildInfoObjectName: String = "Versions" def buildInfoMembers = Seq( - BuildInfo.Value("sbtVersion", Deps.sbt.version, "Version of sbt.") + BuildInfo.Value("exportscriptAssemblyResource", "/exportscript-assembly.jar") ) - // An `sbt` plugin is built with Scala 2.12. See https://www.scala-sbt.org/1.x/docs/Plugins.html#Creating+an+auto+plugin. - object models extends Cross[ModelsModule](Deps.sbtScalaVersion212, Deps.scalaVersion) - trait ModelsModule extends MillPublishCrossScalaModule { - // def moduleDeps = Seq(buildgen.tree()) - def mvnDeps = Seq(Deps.upickle) - def compileMvnDeps = Seq(Deps.sbt) // for definition references only - - def docJar = - if (crossScalaVersion == Deps.scalaVersion) - // Without this, docJar currently outputs more than 50k lines of warning. Disabling it. - Task { - PathRef(Jvm.createJar(Task.dest / "empty.jar", Seq.empty[os.Path])) - } - else - super.docJar - } - - // no need to publish this - // `test` fails with `MillScalaModule`, and since it's an `sbt` plugin project it's most likely not needed. - object exportplugin extends ScalaModule { - private val scalaVersionString = Deps.sbtScalaVersion212 - def scalaVersion = scalaVersionString - def moduleDeps = Seq(models(scalaVersionString)) + object exportscript extends ScalaModule { + def scalaVersion = Deps.sbtScalaVersion212 + def mvnDeps = Seq(Deps.osLib) + def moduleDeps = Seq(buildgen.api(Deps.sbtScalaVersion212)) def compileMvnDeps = Seq(Deps.sbt) def assemblyRules = Seq(Rule.ExcludePattern("scala\\.*")) } diff --git a/libs/init/sbt/exportplugin/resources/sbt/sbt.autoplugins b/libs/init/sbt/exportplugin/resources/sbt/sbt.autoplugins deleted file mode 100644 index e94ec8dacccd..000000000000 --- a/libs/init/sbt/exportplugin/resources/sbt/sbt.autoplugins +++ /dev/null @@ -1 +0,0 @@ -mill.main.sbt.ExportBuildPlugin diff --git a/libs/init/sbt/exportplugin/src/mill/main/sbt/ExportBuildPlugin.scala b/libs/init/sbt/exportplugin/src/mill/main/sbt/ExportBuildPlugin.scala deleted file mode 100644 index ce38f03ebe00..000000000000 --- a/libs/init/sbt/exportplugin/src/mill/main/sbt/ExportBuildPlugin.scala +++ /dev/null @@ -1,150 +0,0 @@ -package mill.main.sbt - -import sbt.Keys._ -import sbt.io.IO -import sbt.librarymanagement.{Binary, Disabled, Full, MavenRepository, _} -import sbt.{Def, CrossVersion => _, Developer => _, Project => _, Resolver => _, ScmInfo => _, _} -import upickle.default._ - -import java.io.File - -object ExportBuildPlugin extends AutoPlugin { - override def trigger = allRequirements - // override def requires = ??? // defaults to `JvmPlugin` - - object autoImport { - val millInitBuildInfo = taskKey[BuildInfo]( - "get the `mill.main.sbt.BuildInfo` model of this build or this project" - ) - val millInitProject = taskKey[Project]("get the `mill.main.sbt.Project` model of this project") - val millInitExportBuild = taskKey[File]("export the build in a JSON file for `mill init`") - } - - import autoImport._ - - val buildInfoSetting = millInitBuildInfo := BuildInfo( - BuildPublicationInfo( - description.?.value, - homepage.?.value.map(_.map(_.toExternalForm)), - licenses.?.value.map(_.map { case (name, url) => - (name, url.toExternalForm) - }), - organization.?.value, - // organizationName.?.value, // not needed - // organizationHomepage.?.value.map(_.map(_.toExternalForm)), // not needed - developers.?.value.map(_.map(developer => - Developer(developer.id, developer.name, developer.email, developer.url.toExternalForm) - )), - scmInfo.?.value.map(_.map(scmInfo => - ScmInfo(scmInfo.browseUrl.toExternalForm, scmInfo.connection, scmInfo.devConnection) - )), - version.?.value - ), - javacOptions.?.value, - scalaVersion.?.value, - scalacOptions.?.value, - resolvers.?.value.map(_.flatMap { - case mavenRepository: MavenRepository => Some(Resolver(mavenRepository.root)) - case resolver => - println(s"A `Resolver` which is not a `MavenRepository` is skipped: $resolver") - None - }) - ) - - override lazy val buildSettings: Seq[Def.Setting[_]] = Seq( - buildInfoSetting - ) - - override lazy val projectSettings: Seq[Setting[_]] = Seq( - buildInfoSetting, - millInitProject := - Project( - // organization.value, - name.value, - // version.value, - // baseDirectory.value.relativeTo((ThisBuild / baseDirectory).value).get.getPath.split(File.separator), - baseDirectory.value.getPath, - thisProjectRef.value.project, - /*{ - // keep the project `BuildInfo` members only when they are different - val defaultBi = (ThisBuild / millInitBuildInfo).value - val projectBi = millInitBuildInfo.value - import projectBi.* - BuildInfo({ - val defaultBpi = defaultBi.buildPublicationInfo - import projectBi.buildPublicationInfo.* - BuildPublicationInfo( - if (description != defaultBpi.description) description else None, - if (homepage != defaultBpi.homepage) homepage else None, - if (licenses != defaultBpi.licenses) licenses else None, - if (organization != defaultBpi.organization) organization else None, - //if (organizationName != defaultBpi.organizationName) organizationName else None, // not needed - if (organizationHomepage != defaultBpi.organizationHomepage) organizationHomepage else None, - if (developers != defaultBpi.developers) developers else None, - if (scmInfo != defaultBpi.scmInfo) scmInfo else None, - if (version != defaultBpi.version) version else None - ) - }, - if (javacOptions != defaultBi.javacOptions) javacOptions else None, - if (scalacOptions != defaultBi.scalacOptions) scalacOptions else None, - if (resolvers != defaultBi.resolvers) resolvers else None - ) - }*/ - millInitBuildInfo.value, - AllDependencies( - buildDependencies.value.classpath(thisProjectRef.value).map(classpathDep => { - val depProject = classpathDep.project - InterProjectDependency(depProject.project, classpathDep.configuration) - }), - libraryDependencies.value.flatMap(moduleID => { - val dependency = LibraryDependency( - moduleID.organization, - moduleID.name, - moduleID.crossVersion match { - case Disabled => CrossVersion.Disabled - case _: Binary => CrossVersion.Binary - case _: Full => CrossVersion.Full - case _: For3Use2_13 => CrossVersion.Constant("2.13") - case _: For2_13Use3 => CrossVersion.Constant("3") - case constant: Constant => CrossVersion.Constant(constant.value) - case crossVersion => - println(s"Dependency $moduleID with unsupported `CrossVersion`: $crossVersion") - CrossVersion.Disabled - }, - moduleID.revision, - moduleID.configurations, - None, - None, - moduleID.exclusions.map(inclExclRule => - (inclExclRule.organization, inclExclRule.name) - ) - ) - val explicitArtifacts = moduleID.explicitArtifacts - if (explicitArtifacts.isEmpty) - Seq(dependency) - else - explicitArtifacts.map(artifact => - dependency.copy( - tpe = Some(artifact.`type`) /*{ - val tpe = artifact.`type` - Option.when(tpe != DefaultType)(tpe) - }*/, - classifier = artifact.classifier - ) - ) - }) - ) - ), - // `target.value` doesn't work in `globalSettings` and `buildSettings`, so this is added to `projectSettings. - millInitExportBuild := { - val defaultBuildInfo = (ThisBuild / millInitBuildInfo).value - val projects = millInitProject.all(ScopeFilter(inAnyProject)).value - val buildExport = BuildExport(defaultBuildInfo, projects) - - val outputFile = target.value / "mill-init-build-export.json" - IO.write(outputFile, write(buildExport)) - outputFile - }, - millInitExportBuild / aggregate := false - ) -} diff --git a/libs/init/sbt/exportscript/src/mill/main/sbt/ExportSbtBuildScript.scala b/libs/init/sbt/exportscript/src/mill/main/sbt/ExportSbtBuildScript.scala new file mode 100644 index 000000000000..d1829e1f0c5f --- /dev/null +++ b/libs/init/sbt/exportscript/src/mill/main/sbt/ExportSbtBuildScript.scala @@ -0,0 +1,306 @@ +package mill.main.sbt + +import _root_.sbt._ +import mill.main.buildgen._ + +import scala.util.Using + +/** + * @see [[https://github.com/JetBrains/sbt-structure/#sideloading Sideloading]] + */ +object ExportSbtBuildScript extends (State => State) { + + // ((moduleDir, isCrossPlatform), ModuleRepr) + type SbtModuleRepr = ((Seq[String], Boolean), ModuleRepr) + + // directory for exporting files and the name for the test module + val millInitExportArgs = settingKey[(File, String)]("") + val millInitExportBuild = taskKey[Unit]("") + val millInitExportModule = taskKey[Unit]("") + // https://github.com/portable-scala/sbt-crossproject/ + val crossProjectBaseDirectory = settingKey[File]("base directory of the current cross project") + + def apply(state: State) = { + val extracted = Project.extract(state) + import extracted._ + val sessionSettings = + Project.transform(_ => GlobalScope, Seq(millInitExportBuild := exportBuild.value)) ++ + structure.allProjectPairs.flatMap { + case (project, ref) => + if (project.aggregate.isEmpty) Project.transform( + Scope.resolveScope(Scope(Select(ref), Zero, Zero, Zero), ref.build, rootProject), + Seq(millInitExportModule := exportModule.value) + ) + else Nil + } + BuiltinCommands.reapply(session.appendRaw(sessionSettings), structure, state) + } + + // This task is required to ensure projects not aggregated by the root project are exported. + def exportBuild = Def.taskDyn { + val structure = Project.structure(Keys.state.value) + Def.task { + std.TaskExtra.joinTasks(structure.allProjectRefs.flatMap { ref => + (ref / millInitExportModule).get(structure.data) + }).join.value + } + } + + // Generates and writes the ADT, for the current Scala version, for a module. + def exportModule = Def.taskDyn { + val project = Keys.thisProject.value + val (outDir, testModuleName) = millInitExportArgs.value + val data = Project.structure(Keys.state.value).data + + val mvnDependencies = Keys.libraryDependencies.value.collect { + case dep + if !((dep.organization == "org.scala-lang" && + Seq("scala-library", "scala3-library").exists(dep.name.startsWith)) || + (dep.organization == "org.scala-js" && !dep.name.startsWith("scalajs-dom")) || + dep.organization == "org.scala-native" || + (dep.organization == "ch.epfl.lamp" && dep.name.startsWith("dotty"))) => + (dep, dep.configurations.toSeq.flatMap(_.split(';'))) + } + def mvnDeps(configsPredicate: Seq[String] => Boolean) = + mvnDependencies.collect { + case (dep, configs) if configsPredicate(configs) => toMvnDep(dep) + } + + val isCrossVersion = Keys.crossScalaVersions.value.length > 1 + val projectDependencies = + project.dependencies.map(dep => dep -> dep.configuration.toSeq.flatMap(_.split(";"))) + def moduleDeps(configsPredicate: Seq[String] => Boolean) = + projectDependencies.collect { + case (dep, configs) if configsPredicate(configs) => + for { + depBaseDir <- (dep.project / Keys.baseDirectory).get(data) + depSegments = os.Path(depBaseDir).subRelativeTo(os.pwd).segments + depIsCrossScalaVersion = + (dep.project / Keys.crossScalaVersions).get(data).getOrElse(Nil).length > 1 + crossArgs = if (depIsCrossScalaVersion) + Map((depSegments.length - 1, if (isCrossVersion) "()" else "(scalaVersion())")) + else Map.empty[Int, String] + } yield JavaModuleConfig.ModuleDep(depSegments, crossArgs) + }.flatten + + val baseDir = os.Path(project.base) + val crossPlatformBaseDir = crossProjectBaseDirectory.?.value.map(os.Path(_)) + .orElse( // crossProjectBaseDirectory was added in v1.3.0 + if (baseDir.last.matches("""^[.]?(js|jvm|native)$""")) Some(baseDir / os.up) else None) + val moduleDir = crossPlatformBaseDir.getOrElse(baseDir) + val isCrossPlatform = baseDir != moduleDir + val sbtPlatformSupertype = if (isCrossPlatform) Some( + if (os.isDir(baseDir / os.up / "shared")) "SbtPlatformModule.CrossTypeFull" + else if (os.isDir(baseDir / os.up / "src")) "SbtPlatformModule.CrossTypePure" + else "SbtPlatformModule.CrossTypeDummy" + ) + else None + + Def.task { + val scalaJSModuleConfig = Keys.libraryDependencies.value.collectFirst { + case dep + if dep.organization == "org.scala-js" && dep.name.startsWith("scalajs-library") => + ScalaJSModuleConfig(dep.revision) + } + val scalaNativeModuleConfig = Keys.libraryDependencies.value.collectFirst { + case dep + if dep.organization == "org.scala-native" && + dep.configurations.contains("plugin->default(compile)") => + ScalaNativeModuleConfig(dep.revision) + } + val scalaModuleConfig = ScalaModuleConfig( + scalaVersion = if (isCrossVersion) null else Keys.scalaVersion.value, + // skip options added by plugins + scalacOptions = Keys.scalacOptions.value.filterNot(option => + option.startsWith("-P") || option.startsWith("-scalajs") + ), + scalacPluginMvnDeps = mvnDeps(_.contains("plugin->default(compile)")) + ) + val javaModuleConfig = JavaModuleConfig( + mvnDeps = mvnDeps(cs => cs.isEmpty || cs.contains("compile")), + compileMvnDeps = mvnDeps(_.exists(Seq("provided", "optional").contains)), + runMvnDeps = mvnDeps(_.contains("runtime")), + moduleDeps = moduleDeps(cs => + cs.isEmpty || cs.contains("compile") || cs.exists(_.startsWith("compile->")) + ), + compileModuleDeps = moduleDeps(cs => + Seq("provided", "optional").exists(cs.contains) || + Seq("provided->", "optional->").exists(s => cs.exists(_.startsWith(s))) + ), + runModuleDeps = + moduleDeps(cs => cs.contains("runtime") || cs.exists(_.startsWith("runtime->"))), + javacOptions = Keys.javacOptions.value.diff(JavaModuleConfig.unsupportedJavacOptions) + ) + val publishModuleConfig = if ((Keys.publish / Keys.skip).value || !Keys.publishArtifact.value) + None + else Some(PublishModuleConfig( + pomSettings = toPomSettings(Keys.projectInfo.value), + publishVersion = Keys.version.value, + versionScheme = Keys.versionScheme.value, + artifactMetadata = PublishModuleConfig.Artifact( + Keys.organization.value, + Keys.moduleName.value, + Keys.version.value + ) + )) + val mainConfigs = Seq(scalaJSModuleConfig, scalaNativeModuleConfig).flatten ++ + Seq(scalaModuleConfig, javaModuleConfig) ++ publishModuleConfig + val useVersionRanges = isCrossVersion && os.walk.stream(moduleDir).exists(path => + os.isDir(path) && path.last.matches("""^scala-\d+\.\d*(-.*|\+)$""") + ) + val (mainSupertypes, mainMixins) = + mainHierarchy(sbtPlatformSupertype, isCrossVersion, useVersionRanges, mainConfigs) + + val testModule = + if ( + (Test / Keys.unmanagedSourceDirectories).value.exists(_.exists()) || + (Test / Keys.unmanagedResourceDirectories).value.exists(_.exists()) + ) { + val testMvnDeps = mvnDeps(_.contains("test")) + TestModuleRepr.mixinAndMandatoryMvnDeps(testMvnDeps).map { + case (mixin, mandatoryMvnDeps) => + val (testSupertypes, testMixins) = testHierarchy(mainSupertypes ++ mainMixins, mixin) + val testConfigs = Seq(JavaModuleConfig( + mandatoryMvnDeps = mandatoryMvnDeps, + mvnDeps = testMvnDeps.diff(mandatoryMvnDeps), + moduleDeps = moduleDeps(cs => Seq("test", "test->compile").exists(cs.contains)) ++ + moduleDeps(_.contains("test->test")) + .map(d => d.copy(d.segments :+ testModuleName)), + compileModuleDeps = + moduleDeps(cs => Seq("test->provided", "test->optional").exists(cs.contains)), + runModuleDeps = moduleDeps(_.contains("test->runtime")) + )) + TestModuleRepr( + name = testModuleName, + supertypes = testSupertypes, + mixins = testMixins, + configs = testConfigs, + crossConfigs = + if (isCrossVersion) Seq((Keys.scalaVersion.value, testConfigs)) else Nil, + testParallelism = false, + testSandboxWorkingDir = false + ) + } + } else None + + val sbtModule = ( + (moduleDir.subRelativeTo(os.pwd).segments, isCrossPlatform), + ModuleRepr( + segments = baseDir.subRelativeTo(os.pwd).segments, + supertypes = mainSupertypes, + mixins = mainMixins, + configs = mainConfigs, + crossConfigs = if (isCrossVersion) Seq((Keys.scalaVersion.value, mainConfigs)) else Nil, + testModule = testModule + ) + ) + Using.resource(os.write.outputStream(os.temp( + dir = os.Path(outDir), + deleteOnExit = false + )))(upickle.default.writeToOutputStream(sbtModule, _)) + } + } + + def mainHierarchy( + sbtPlatformSupertype: Option[String], + isCrossVersion: Boolean, + useVersionRanges: Boolean, + configs: Seq[ModuleConfig] + ) = { + val supertypes = Seq.newBuilder[String] + val mixins = Seq.newBuilder[String] + configs.foreach { + case _: ScalaJSModuleConfig => supertypes += "ScalaJSModule" + case _: ScalaNativeModuleConfig => supertypes += "ScalaNativeModule" + case _: PublishModuleConfig => supertypes += "PublishModule" + case _ => + } + supertypes ++= sbtPlatformSupertype + if (isCrossVersion) { + supertypes += "CrossScalaModule" // added for sharing cross configs in baseTrait + mixins += (if (sbtPlatformSupertype.isEmpty) "CrossSbtModule" else "CrossSbtPlatformModule") + if (useVersionRanges) mixins += "CrossScalaVersionRanges" + } else supertypes += "SbtModule" + (supertypes.result(), mixins.result()) + } + + def testHierarchy(mainHierarchy: Seq[String], testFramework: String) = { + val supertypes = Seq.newBuilder[String] + val mixins = Seq.newBuilder[String] + mainHierarchy.foreach { + case "ScalaJSModule" => supertypes += "ScalaJSTests" + case "ScalaNativeModule" => supertypes += "ScalaNativeTests" + case "SbtModule" => supertypes += "SbtTests" + case s if s.startsWith("SbtPlatformModule") => supertypes += "SbtPlatformTests" + case "CrossSbtModule" => mixins += "CrossSbtTests" + case "CrossSbtPlatformModule" => mixins += "CrossSbtPlatformTests" + case _ => + } + mixins += testFramework + (supertypes.result(), mixins.result()) + } + + def toMvnDep(dep: ModuleID) = { + import dep._ + val artifact = explicitArtifacts.find(_.name == name) + JavaModuleConfig.mvnDep( + org = organization, + name = name + (crossVersion match { + case v: librarymanagement.Constant if v.value.nonEmpty => "_" + v.value + case _: librarymanagement.For2_13Use3 => "_3" + case _: librarymanagement.For3Use2_13 => "_2.13" + case _ => "" + }), + version = revision, + classifier = artifact.flatMap(_.classifier), + typ = artifact.map(_.`type`), + excludes = exclusions.map(x => (x.organization, x.name)), + sep1 = crossVersion match { + case _: librarymanagement.Full => ":::" + case _: librarymanagement.Patch => ":::" + case _: librarymanagement.Binary => "::" + case _ => ":" + }, + sep2 = crossVersion match { + case v: librarymanagement.Full if v.prefix.nonEmpty => "::" + case v: librarymanagement.Binary if v.prefix.nonEmpty => "::" + case v: librarymanagement.For2_13Use3 if v.prefix.nonEmpty => "::" + case v: librarymanagement.For3Use2_13 if v.prefix.nonEmpty => "::" + case _ => ":" + } + ) + } + + def toPomSettings(moduleInfo: ModuleInfo) = { + import moduleInfo._ + PublishModuleConfig.PomSettings( + description = description, + organization = organizationName, + url = homepage.fold[String](null)(_.toExternalForm), + licenses = licenses.map(toLicense), + versionControl = toVersionControl(scmInfo), + developers = developers.map(toDeveloper) + ) + } + + def toLicense(license: (String, URL)) = { + val (name, url) = license + PublishModuleConfig.License(name, name, url.toExternalForm) + } + + def toVersionControl(scmInfo: Option[ScmInfo]) = scmInfo match { + case Some(scmInfo) => + import scmInfo._ + PublishModuleConfig.VersionControl( + Some(browseUrl.toExternalForm), + Some(connection), + devConnection + ) + case None => PublishModuleConfig.VersionControl() + } + + def toDeveloper(developer: Developer) = { + import developer._ + PublishModuleConfig.Developer(id, name, url.toExternalForm) + } +} diff --git a/libs/init/sbt/models/src/mill/main/sbt/Models.scala b/libs/init/sbt/models/src/mill/main/sbt/Models.scala deleted file mode 100644 index 373999634a04..000000000000 --- a/libs/init/sbt/models/src/mill/main/sbt/Models.scala +++ /dev/null @@ -1,199 +0,0 @@ -package mill.main.sbt - -import mill.main.sbt.BuildPublicationInfo.License -import mill.main.sbt.Models.URL -import sbt.Keys -import upickle.default.{macroRW, ReadWriter => RW} - -object Models { - type URL = String -} - -case class BuildExport( - /** @see [[sbt.AutoPlugin.buildSettings]] and [[sbt.AutoPlugin.globalSettings]] */ - defaultBuildInfo: BuildInfo, - projects: Seq[Project] -) -object BuildExport { - implicit val rw: RW[BuildExport] = macroRW -} - -case class BuildInfo( - buildPublicationInfo: BuildPublicationInfo, - /** @see [[Keys.javacOptions]] */ - javacOptions: Option[Seq[String]], - /** @see [[Keys.scalaVersion]] */ - scalaVersion: Option[String], - /** @see [[Keys.scalacOptions]] */ - scalacOptions: Option[Seq[String]], - /** @see [[Keys.resolvers]] */ - resolvers: Option[Seq[Resolver]] -) -object BuildInfo { - implicit val rw: RW[BuildInfo] = macroRW -} - -/** - * Members ordered by their order in [[Keys]]. - */ -case class BuildPublicationInfo( - /** @see [[Keys.description]] */ - description: Option[String], - /** - * corresponds to `url` in POM - * - * @see [[Keys.homepage]] - */ - homepage: Option[Option[String]], - /** @see [[Keys.licenses]] */ - licenses: Option[Seq[License]], - /** - * corresponds to `groupId` in POM and Mill's `PomSettings.organization` - * - * @see [[Keys.organization]] - */ - organization: Option[String], - // not needed - /* - /** - * corresponds to Maven's `organization` in POM - * - * @see [[Keys.organizationName]] - */ - organizationName: Option[String], - */ - // not needed - /* - /** - * corresponds to `organizationUrl` in POM - * - * @see [[Keys.organizationHomepage]] - */ - organizationHomepage: Option[Option[String]], - */ - /** @see [[Keys.developers]] */ - developers: Option[Seq[Developer]], - /** @see [[Keys.scmInfo]] */ - scmInfo: Option[Option[ScmInfo]], - /** @see [[Keys.version]] */ - version: Option[String] -) -object BuildPublicationInfo { - - /** @see [[sbt.librarymanagement.License]] */ - type License = (String, URL) - - implicit val rw: RW[BuildPublicationInfo] = macroRW -} - -/** - * @see [[sbt.librarymanagement.ScmInfo]] - */ -case class ScmInfo( - browseUrl: URL, - connection: String, - devConnection: Option[String] -) -object ScmInfo { - implicit val rw: RW[ScmInfo] = macroRW -} - -/** - * @see [[sbt.librarymanagement.Developer]] - */ -case class Developer(id: String, name: String, email: String, url: URL) -object Developer { - implicit val rw: RW[Developer] = macroRW -} - -/** - * Only Maven repositories are supported now. - * @see [[sbt.librarymanagement.Resolver]] - */ -case class Resolver(root: String) -object Resolver { - implicit val rw: RW[Resolver] = macroRW -} - -case class Project( - // organization: String, // `groupId` in Maven, moved inside `buildInfo` - /** @see [[sbt.Keys.name]] */ - name: String, // `artifactId` in Maven - // version: String, // `groupId` in Maven, moved inside `buildInfo` - // dirs: ProjectDirs, // relative - projectDirectory: String, - /** @see [[sbt.ProjectRef.project]] */ - projectRefProject: String, - buildInfo: BuildInfo, - allDependencies: AllDependencies -) -object Project { - implicit val rw: RW[Project] = macroRW -} - -case class AllDependencies( - projectDependencies: Seq[InterProjectDependency], - libraryDependencies: Seq[LibraryDependency] -) -object AllDependencies { - implicit val rw: RW[AllDependencies] = macroRW -} - -sealed trait Dependency { - def configurations: Option[String] -} - -case class InterProjectDependency( - /** @see [[sbt.ProjectRef.project]] */ - projectRefProject: String, - /** @see [[sbt.ClasspathDep.configuration]] */ - configurations: Option[String] -) extends Dependency -object InterProjectDependency { - implicit val rw: RW[InterProjectDependency] = macroRW -} - -/** - * @see [[sbt.librarymanagement.ModuleID]] - */ -case class LibraryDependency( - organization: String, // `groupId` in Maven - name: String, // `artifactId` in Maven - crossVersion: CrossVersion = CrossVersion.Disabled, - revision: String, - configurations: Option[String], - // BOM seems not supported by sbt. See https://stackoverflow.com/questions/42032303/how-do-i-use-a-maven-bom-bill-of-materials-to-manage-my-dependencies-in-sbt. - // isBom : Boolean = false - tpe: Option[String], - classifier: Option[String], - excludes: Seq[(String, String)] -) extends Dependency -object LibraryDependency { - implicit val rw: RW[LibraryDependency] = macroRW -} - -/** - * @see [[sbt.librarymanagement.CrossVersion]] - */ -sealed trait CrossVersion -object CrossVersion { - case object Disabled extends CrossVersion { - implicit val rw: RW[Disabled.type] = macroRW - } - case object Binary extends CrossVersion { - implicit val rw: RW[Binary.type] = macroRW - } - case object Full extends CrossVersion { - implicit val rw: RW[Full.type] = macroRW - } - - /** - * Including the cases [[sbt.librarymanagement.Constant]], [[sbt.librarymanagement.For2_13Use3]], and [[sbt.librarymanagement.For3Use2_13]]. - */ - case class Constant(value: String) extends CrossVersion - object Constant { - implicit val rw: RW[Constant] = macroRW - } - - implicit val rw: RW[CrossVersion] = macroRW -} diff --git a/libs/init/sbt/src/mill/main/sbt/SbtBuildGenMain.scala b/libs/init/sbt/src/mill/main/sbt/SbtBuildGenMain.scala index 985e18b3a86b..9951763c3fe2 100644 --- a/libs/init/sbt/src/mill/main/sbt/SbtBuildGenMain.scala +++ b/libs/init/sbt/src/mill/main/sbt/SbtBuildGenMain.scala @@ -1,166 +1,119 @@ package mill.main.sbt -import mainargs.{ParserForClass, arg, main} -import mill.api.daemon.internal.internal -import mill.constants.Util +import mill.constants.Util.isWindows import mill.main.buildgen.* -import mill.main.buildgen.BuildGenUtil.* -import mill.main.buildgen.IrDependencyType.* -import os.Path +import pprint.Util.literalize -import scala.collection.immutable.SortedSet +import scala.util.Using /** - * Converts an `sbt` build to Mill by generating Mill build file(s). - * The implementation uses the `sbt` - * [[https://www.scala-sbt.org/1.x/docs/Combined+Pages.html#addPluginSbtFile+command addPluginSbtFile command]] - * to add a plugin and a task to extract the settings for a project using a custom model. - * - * The generated output should be considered scaffolding and will likely require edits to complete conversion. - * - * ===Capabilities=== - * The conversion - * - handles deeply nested modules - * - captures publish settings - * - configures dependencies for configurations: - * - no configuration - * - Compile - * - Test - * - Runtime - * - Provided - * - Optional - * - configures testing frameworks (@see [[mill.scalalib.TestModule]]): - * - Java: - * - JUnit 4 - * - JUnit 5 - * - TestNG - * - Scala: - * - ScalaTest - * - Specs2 - * - µTest - * - MUnit - * - Weaver - * - ZIOTest - * - ScalaCheck - * ===Limitations=== - * The conversion does not support: - * - custom dependency configurations - * - custom settings including custom tasks - * - sources other than Scala on JVM and Java, such as Scala.js and Scala Native - * - cross builds + * Converts an SBT build to Mill by selecting module configurations using a custom script. + * @see [[https://docs.scala-lang.org/overviews/jdk-compatibility/overview.html#tooling-compatibility-table SBT JDK compatibility]] + * @see [[SbtBuildGenArgs Command line arguments]] */ -@internal -object SbtBuildGenMain - extends BuildGenBase[Project, String, (BuildInfo, Tree[Node[Option[Project]]])] { - override type C = Config +object SbtBuildGenMain { def main(args: Array[String]): Unit = { - val cfg = ParserForClass[Config].constructOrExit(args.toSeq) - run(cfg) - } - - private def run(cfg: Config): Unit = { - val workspace = os.pwd - println("converting sbt build") - def systemSbtExists(sbt: String) = - // The return code is somehow 1 instead of 0. - os.call((sbt, "--help"), check = false).exitCode == 1 - - val isWindows = Util.isWindows - val sbtExecutable = cfg.customSbt.getOrElse( - if (isWindows) { - val systemSbt = "sbt.bat" - if (systemSbtExists(systemSbt)) - systemSbt - else - throw new RuntimeException(s"No system-wide `$systemSbt` found") - } else { - val systemSbt = "sbt" - // resolve the sbt executable - // https://raw.githubusercontent.com/paulp/sbt-extras/master/sbt - if (os.exists(workspace / "sbt")) - "./sbt" - else if (os.exists(workspace / "sbtx")) - "./sbtx" - else if (systemSbtExists(systemSbt)) - systemSbt - else - throw new RuntimeException( - s"No sbt executable (`./sbt`, `./sbtx`, or system-wide `$systemSbt`) found" - ) - } + val args0 = mainargs.ParserForClass[SbtBuildGenArgs].constructOrExit(args.toSeq) + import args0.{getClass as _, *} + + val cmd = sbtCmd.getOrElse: + Either.cond( + !isWindows, + Seq("sbt", ".sbtx").collectFirst: + case exe if os.exists(os.pwd / exe) => s"./$exe" + .toLeft("sbt"), + "sbt.bat" + ).flatten.fold( + cmd => + if (os.call((cmd, "--help"), check = false).exitCode == 1) + sys.error(s"no system-wide $cmd found") + else cmd, + identity + ) + val jar = + Using.resource(getClass.getResourceAsStream(BuildInfo.exportscriptAssemblyResource))( + os.temp(_, suffix = ".jar") + ) + val exportDir = os.temp.dir() + // Run export with "+" to generate a file per project and cross Scala version. + // https://github.com/JetBrains/sbt-structure/#sideloading + val script = Seq( + s"set SettingKey[(File, String)](\"millInitExportArgs\") in Global := (file(${literalize(exportDir.toString())}), ${literalize(testModule)})", + s"apply -cp ${literalize(jar.toString())} mill.main.sbt.ExportSbtBuildScript", + s"+millInitExportBuild" ) - - println("Running the added `millInitExportBuild` sbt task to export the build") - - try { - if (isWindows) { - /* - `-addPluginSbtFile` somehow doesn't work on Windows, therefore, the ".sbt" file is put directly in the `sbt` "project" directory. - The error message: - ```text - [error] Expected ':' - [error] Expected '=' - [error] Expected whitespace character - [error] -addPluginSbtFile - [error] ^ - ``` - */ - val sbtFile = writeTempSbtFileInSbtProjectDirectory(workspace) - os.call( - (sbtExecutable, "millInitExportBuild"), - cwd = workspace, - stdout = os.Inherit - ) - os.remove(sbtFile) - } else - os.call( - (sbtExecutable, s"-addPluginSbtFile=${writeSbtFile().toString}", "millInitExportBuild"), - cwd = workspace, - stdout = os.Inherit - ) - - } catch { - case e: os.SubprocessException => throw RuntimeException( - "The sbt command to run the `millInitExportBuild` sbt task has failed, " + - s"please check out the following solutions and try again:\n" + - s"1. check whether your existing sbt build works properly;\n" + - s"2. make sure there are no other sbt processes running;\n" + - s"3. clear your build output and cache;\n" + - s"4. update the project's sbt version to the latest or our tested version v${Versions.sbtVersion};\n" + - "5. check whether you have the appropriate Java version.\n", - e - ) - case t: Throwable => throw t + os.proc(cmd, script).call(stdout = os.Inherit, stderr = os.Inherit) + + // ((moduleDir, isCrossPlatform), ModuleRepr) + type SbtModuleRepr = ((Seq[String], Boolean), ModuleRepr) + // SBT can generate duplicate files for modules. For example, if a project "foo" defines 3 + // cross Scala versions and another module "bar" specifies 2 of those cross Scala versions, + // a duplicate file is generated for module "bar". + val sbtModules = os.list.stream(exportDir) + .map(path => upickle.default.read[SbtModuleRepr](path.toNIO)) + .toSeq.distinct + if (sbtModules.isEmpty) { + sys.error(s"no modules found using $cmd") } - val buildExportPickled = os.read(workspace / "target" / "mill-init-build-export.json") - // TODO This is mainly for debugging purposes. Comment out or uncomment this line as needed. - // println("sbt build export retrieved: " + buildExportPickled) - import upickle.* - val buildExport = read[BuildExport](buildExportPickled) - - import scala.math.Ordering.Implicits.* - // Types have to be specified explicitly here for the code to be resolved correctly in IDEA. - val projectNodes = - buildExport.projects.view - .map(project => - Node(os.Path(project.projectDirectory).subRelativeTo(workspace).segments, project) + def toModule(crossVersionModules: Seq[ModuleRepr]): ModuleRepr = + if (crossVersionModules.tail.isEmpty) crossVersionModules.head // not cross version + else + // compute the base config for cross versions + val module = crossVersionModules.iterator.reduce: (m1, m2) => + m1.copy( + configs = ModuleConfig.abstracted(m1.configs, m2.configs), + crossConfigs = m1.crossConfigs ++ m2.crossConfigs, + testModule = (m1.testModule, m2.testModule) match + case (Some(m1), Some(m2)) => + Some(m1.copy( + configs = ModuleConfig.abstracted(m1.configs, m2.configs), + crossConfigs = m1.crossConfigs ++ m2.crossConfigs + )) + case (m1, m2) => m1.orElse(m2) + ) + // inherit the base config for cross versions + module.copy( + crossConfigs = module.crossConfigs.map: (k, v) => + (k, ModuleConfig.inherited(v, module.configs)) + .sortBy(_._1), + testModule = module.testModule.map(module => + module.copy( + crossConfigs = module.crossConfigs.map: (k, v) => + (k, ModuleConfig.inherited(v, module.configs)) + .sortBy(_._1) + ) + ) ) - // The projects are ordered differently in different `sbt millInitExportBuild` runs and on different OSs, which is strange. - .sortBy(_.dirs) - - val projectNodeTree = projectNodes.foldLeft(Tree(Node(Seq.empty, None)))(merge) + end toModule + val packages = sbtModules.groupMap(_._1)(_._2).iterator.map: + case ((_, false), crossVersionModules) => Tree(toModule(crossVersionModules)) + case ((moduleDir, _), crossPlatformModules) => Tree( + ModuleRepr(moduleDir), + crossPlatformModules.groupBy(_.segments).iterator.map: (_, crossVersionModules) => + Tree(toModule(crossVersionModules)) + .toSeq + ) + .toSeq - convertWriteOut(cfg, cfg.shared, (buildExport.defaultBuildInfo, projectNodeTree)) + var build = BuildRepr.fill(packages) + if (merge.value) build = BuildRepr.merged(build) - println("converted sbt build to Mill") + val writer = { + val renderCrossValueInTask = "scalaVersion()" + if (noMetaBuild.value) BuildWriter(build, renderCrossValueInTask = renderCrossValueInTask) + else + val (build0, metaBuild) = MetaBuildRepr.of(build) + BuildWriter(build0, Some(metaBuild), renderCrossValueInTask) + } + writer.writeFiles() - { - val jvmOptsSbt = workspace / ".jvmopts" - val jvmOptsMill = workspace / ".mill-jvm-opts" + locally { + val jvmOptsSbt = os.pwd / ".jvmopts" + val jvmOptsMill = os.pwd / ".mill-jvm-opts" if (os.exists(jvmOptsSbt)) { println(s"copying ${jvmOptsSbt.last} to ${jvmOptsMill.last}") @@ -182,361 +135,33 @@ object SbtBuildGenMain } os.copy(jvmOptsSbt, jvmOptsMill) } - } - } - - /** - * @return the temp directory the jar is in and the `sbt` file contents. - */ - private def copyExportBuildAssemblyJarOutAndGetSbtFileContents(): (os.Path, String) = { - val tempDir = os.temp.dir() - // This doesn't work in integration tests when Mill is packaged. - /* - val sbtPluginJarUrl = - getClass.getResource("/exportplugin-assembly.jar").toExternalForm - */ - val sbtPluginJarName = "exportplugin-assembly.jar" - val sbtPluginJarStream = getClass.getResourceAsStream(s"/$sbtPluginJarName") - val sbtPluginJarPath = tempDir / sbtPluginJarName - os.write(sbtPluginJarPath, sbtPluginJarStream) - val contents = - s"""addSbtPlugin("com.lihaoyi" % "mill-libs-init-sbt-exportplugin" % "dummy-version" from ${ - escape(sbtPluginJarPath.wrapped.toUri.toString) - }) - |""".stripMargin - (tempDir, contents) - } - - private def writeSbtFile(): os.Path = { - val (tempDir, contents) = copyExportBuildAssemblyJarOutAndGetSbtFileContents() - val sbtFile = tempDir / "mill-init.sbt" - os.write(sbtFile, contents) - sbtFile - } - - private def writeTempSbtFileInSbtProjectDirectory(workspace: os.Path) = - os.temp( - copyExportBuildAssemblyJarOutAndGetSbtFileContents()._2, - workspace / "project", - suffix = ".sbt" - ) - - override def getModuleTree( - input: (BuildInfo, Tree[Node[Option[Project]]]) - ): Tree[Node[Option[Project]]] = - input._2 - - private def sbtSupertypes = Seq("SbtModule", "PublishModule") // always publish - - override def getBaseInfo( - input: (BuildInfo, Tree[Node[Option[Project]]]), - cfg: Config, - baseModule: String, - packagesSize: Int - ): IrBaseInfo = { - val buildInfo = cfg.baseProject.fold(input._1)(name => - // TODO This can simplified if `buildExport.projects` is passed here. - input._2.nodes().collectFirst(Function.unlift(_.value.flatMap(project => - Option.when(project.name == name)(project) - ))).get.buildInfo - ) - - import buildInfo.* - val javacOptions = getJavacOptions(buildInfo) - val repositories = getRepositories(buildInfo) - val pomSettings = extractPomSettings(buildPublicationInfo) - val publishVersion = getPublishVersion(buildInfo) - - val typedef = IrTrait( - cfg.shared.jvmId, // There doesn't seem to be a Java version setting in `sbt` though. See https://stackoverflow.com/a/76456295/5082913. - baseModule, - sbtSupertypes, - javacOptions, - scalaVersion, - scalacOptions, - pomSettings, - publishVersion, - Seq.empty, // not available in `sbt` as it seems - repositories - ) - - IrBaseInfo(typedef) - } - - /** - * From the [[Project.projectRefProject]] to the package string built by [[BuildGenUtil.buildModuleFqn]]. - */ - override type ModuleFqnMap = Map[String, String] - - override def getModuleFqnMap(moduleNodes: Seq[Node[Project]]): Map[String, String] = - buildModuleFqnMap(moduleNodes)(_.projectRefProject) - - override def extractIrBuild( - cfg: Config, - // baseInfo: IrBaseInfo, - build: Node[Project], - moduleFqnMap: ModuleFqnMap - ): IrBuild = { - val project = build.value - val buildInfo = project.buildInfo - val configurationDeps = extractConfigurationDeps(project, moduleFqnMap, cfg) - val version = getPublishVersion(buildInfo) - IrBuild( - scopedDeps = configurationDeps, - testModule = cfg.shared.testModule, - testModuleMainType = "SbtTests", - hasTest = os.exists(getMillSourcePath(project) / "src/test"), - dirs = build.dirs, - repositories = getRepositories(buildInfo), - javacOptions = getJavacOptions(buildInfo), - scalaVersion = buildInfo.scalaVersion, - scalacOptions = buildInfo.scalacOptions, - projectName = project.name, - pomSettings = extractPomSettings(buildInfo.buildPublicationInfo), - publishVersion = version, - packaging = null, // not available in `sbt` as it seems - pomParentArtifact = null, // not available - resources = Nil, - testResources = Nil, - publishProperties = Nil, // not available in `sbt` as it seems - jvmId = cfg.shared.jvmId, - testForkDir = None - ) - } - - override def extraImports: Seq[String] = Seq("mill.scalalib.SbtModule") - - def getModuleSupertypes(cfg: Config): Seq[String] = - cfg.shared.baseModule.fold(sbtSupertypes)(Seq(_)) - - def getArtifactId(project: Project): String = project.name - - def getMillSourcePath(project: Project): Path = os.Path(project.projectDirectory) - - override def getSupertypes(cfg: Config, baseInfo: IrBaseInfo, build: Node[Project]): Seq[String] = - getModuleSupertypes(cfg) - - def getJavacOptions(buildInfo: BuildInfo): Seq[String] = - buildInfo.javacOptions.getOrElse(Seq.empty) - - def getRepositories(buildInfo: BuildInfo): Seq[String] = - buildInfo.resolvers.getOrElse(Seq.empty).map(resolver => - escape(resolver.root) - ) - - def getPublishVersion(buildInfo: BuildInfo): String | Null = - buildInfo.buildPublicationInfo.version.orNull - - // originally named `mvnInterp` in the Maven and module - def renderMvn(dependency: LibraryDependency): String = { - import dependency.* - renderMvnString( - organization, - name, - crossVersion match { - case CrossVersion.Disabled => None - // The formatter doesn't work well for the import `import mill.scalalib.CrossVersion as MillCrossVersion` in IntelliJ IDEA, so FQNs are used here. - case CrossVersion.Binary => Some(mill.api.CrossVersion.Binary(false)) - case CrossVersion.Full => Some(mill.api.CrossVersion.Full(false)) - case CrossVersion.Constant(value) => Some(mill.api.CrossVersion.Constant(value, false)) - }, - version = revision, - tpe = tpe.orNull, - classifier = classifier.orNull, - excludes = excludes - ) - } - - def extractPomSettings(buildPublicationInfo: BuildPublicationInfo): IrPom = { - import buildPublicationInfo.* - // always publish - /* - if ( - Seq( - description, - homepage, - licenses, - organizationName, - organizationHomepage, - developers, - scmInfo - ).forall(_.isEmpty) - ) - null - else - */ - IrPom( - description.getOrElse(""), - organization.getOrElse(""), - homepage.fold("")(_.getOrElse("")), - licenses.getOrElse(Seq.empty).map(license => IrLicense(license._1, license._1, license._2)), - scmInfo.flatten.fold(IrVersionControl(null, null, null, null))(scmInfo => { - import scmInfo.* - IrVersionControl(browseUrl, connection, devConnection.orNull, null) - }), - developers.getOrElse(Seq.empty).map { developer => - import developer.* - IrDeveloper(id, name, url, null, null) - } - ) - } - - private def isScalaStandardLibrary(dep: LibraryDependency) = - Seq("ch.epfl.lamp", "org.scala-lang").contains(dep.organization) && - Seq("scala-library", "dotty-library", "scala3-library").contains(dep.name) - - /** - * @param toModuleFqn see [[ModuleFqnMap]]. - */ - def extractConfigurationDeps( - project: Project, - toModuleFqn: PartialFunction[String, String], - cfg: Config - ): IrScopedDeps = { - // refactored to a functional approach from the original imperative code in Maven and Gradle - case class DepTypeAndConf(tpe: IrDependencyType, confAfterArrow: Option[String]) - def parseConfigurations(dep: Dependency): Iterator[DepTypeAndConf] = - dep.configurations match { - case None => Iterator(DepTypeAndConf(Default, None)) - case Some(configurations) => - configurations.split(';').iterator.flatMap(configuration => { - val splitConfigurations = configuration.split("->") - (splitConfigurations(0) match { - case "compile" => Some(Default) - case "test" => Some(Test) - case "runtime" => Some(Run) - case "provided" | "optional" => Some(Compile) - case other => - println( - s"Dependency $dep with an unsupported configuration before `->` ${escape(other)} is dropped." - ) - None - }) - .map(DepTypeAndConf(_, splitConfigurations.lift(1))) - }) - } - - val allDependencies = project.allDependencies - - val moduleDepsByType = allDependencies.projectDependencies.flatMap(dep => - parseConfigurations(dep).map { case DepTypeAndConf(tpe, confAfterArrow) => - tpe -> { - val dependsOnTestModule = confAfterArrow match { - case None => false - case Some(value) => - value match { - case "default" | "compile" | "default(compile)" => false - case "test" => true - case conf => - println( - s"Unsupported dependency configuration after `->` in project dependency $dep ${escape(conf)} is ignored." - ) - false - } - } - val moduleRef = toModuleFqn(dep.projectRefProject) - if (dependsOnTestModule) s"$moduleRef.${cfg.shared.testModule}" else moduleRef + val sbtOpts = os.pwd / ".sbtopts" + if (os.exists(sbtOpts)) { + var jvmArgs = os.read.lines(sbtOpts).flatMap: s => + if (s.startsWith("#")) Nil + else s.split(" ").iterator.collect: + case s if s.startsWith("-J") => s.substring(2) + if (jvmArgs.nonEmpty && os.exists(jvmOptsMill)) { + jvmArgs = jvmArgs.diff(os.read.lines(jvmOptsMill)) } - } - ).groupMap(_._1)(_._2) - - val mvnDepsByType = allDependencies.libraryDependencies - .iterator - .filterNot(isScalaStandardLibrary) - .flatMap(dep => - parseConfigurations(dep).map { case DepTypeAndConf(tpe, confAfterArrow) => - confAfterArrow match { - case None => () - case Some(value) => - value match { - case "default" | "compile" | "default(compile)" => () - case conf => - println( - s"Unsupported dependency configuration after `->` in library dependency $dep ${escape(conf)} is ignored." - ) - } - } - tpe -> dep + if (jvmArgs.nonEmpty) { + println(s"adding JVM args from ${sbtOpts.last} to ${jvmOptsMill.last}") + os.write.append(jvmOptsMill, jvmArgs.mkString(System.lineSeparator())) } - ) - .toSeq - .groupMap(_._1)(_._2) - - val defaultModuleDeps = moduleDepsByType.getOrElse(Default, Seq.empty) - val compileModuleDeps = moduleDepsByType.getOrElse(Compile, Seq.empty) - val runModuleDeps = moduleDepsByType.getOrElse(Run, Seq.empty) - val testModuleDeps = moduleDepsByType.getOrElse(Test, Seq.empty) - - val testMvnDeps = mvnDepsByType.getOrElse(Test, Seq.empty) - - val hasTest = os.exists(os.Path(project.projectDirectory) / "src/test") - val testModule = Option.when(hasTest)(testMvnDeps.collectFirst(Function.unlift(dep => - testModulesByGroup.get(dep.organization) - ))).flatten - - cfg.shared.depsObject.fold({ - val defaultMvnDeps = mvnDepsByType.getOrElse(Default, Seq.empty) - val compileMvnDeps = mvnDepsByType.getOrElse(Compile, Seq.empty) - val runMvnDeps = mvnDepsByType.getOrElse(Run, Seq.empty) - - IrScopedDeps( - Seq.empty, - SortedSet.empty, - SortedSet.from(defaultMvnDeps.iterator.map(renderMvn)), - SortedSet.from(defaultModuleDeps), - SortedSet.from(compileMvnDeps.iterator.map(renderMvn)), - SortedSet.from(compileModuleDeps), - SortedSet.from(runMvnDeps.iterator.map(renderMvn)), - SortedSet.from(runModuleDeps), - testModule, - SortedSet.empty, - SortedSet.from(testMvnDeps.iterator.map(renderMvn)), - SortedSet.from(testModuleDeps), - SortedSet.empty, - SortedSet.empty - ) - })(objectName => { - val extractedMvnDeps = mvnDepsByType.view.mapValues(_.map(dep => { - val depName = s"`${dep.organization}:${dep.name}`" - ((depName, renderMvn(dep)), s"$objectName.$depName") - })) - - val extractedDefaultMvnDeps = extractedMvnDeps.getOrElse(Default, Seq.empty) - val extractedCompileMvnDeps = extractedMvnDeps.getOrElse(Compile, Seq.empty) - val extractedRunMvnDeps = extractedMvnDeps.getOrElse(Run, Seq.empty) - val extractedTestMvnDeps = extractedMvnDeps.getOrElse(Test, Seq.empty) - - IrScopedDeps( - extractedMvnDeps.values.flatMap(_.map(_._1)).toSeq, - SortedSet.empty, - SortedSet.from(extractedDefaultMvnDeps.iterator.map(_._2)), - SortedSet.from(defaultModuleDeps), - SortedSet.from(extractedCompileMvnDeps.iterator.map(_._2)), - SortedSet.from(compileModuleDeps), - SortedSet.from(extractedRunMvnDeps.iterator.map(_._2)), - SortedSet.from(runModuleDeps), - testModule, - SortedSet.empty, - SortedSet.from(extractedTestMvnDeps.iterator.map(_._2)), - SortedSet.from(testModuleDeps), - SortedSet.empty, - SortedSet.empty - ) - }) + } + } } - - @main - @internal - case class Config( - shared: BuildGenUtil.BasicConfig, - @arg( - doc = "name of the sbt project to extract settings for --base-module, " + - "if not specified, settings are extracted from `ThisBuild`", - short = 'g' - ) - baseProject: Option[String] = None, - @arg(doc = "the custom sbt executable location") - customSbt: Option[String] = None - ) } + +@mainargs.main +case class SbtBuildGenArgs( + @mainargs.arg(doc = "name of generated test module") + testModule: String = "test", + @mainargs.arg(doc = "merge generated build files") + merge: mainargs.Flag, + @mainargs.arg(doc = "path to sbt executable") + sbtCmd: Option[String], + @mainargs.arg(doc = "disables generating meta-build") + noMetaBuild: mainargs.Flag +) diff --git a/libs/init/sbt/test/resources/expected/config/all/sbt-multi-project-example/build.mill b/libs/init/sbt/test/resources/expected/config/all/sbt-multi-project-example/build.mill index 5a40a70d2a99..d316b551d9c7 100644 --- a/libs/init/sbt/test/resources/expected/config/all/sbt-multi-project-example/build.mill +++ b/libs/init/sbt/test/resources/expected/config/all/sbt-multi-project-example/build.mill @@ -1,104 +1,110 @@ //| mill-version: SNAPSHOT -//| mill-jvm-version: 11 package build -import _root_.build_.BaseModule -import mill._ +import mill.api._ import mill.javalib._ import mill.javalib.publish._ -import mill.scalalib.SbtModule +import mill.scalalib._ -object Deps { +object `package` extends Module { - val `ch.qos.logback:logback-classic` = - mvn"ch.qos.logback:logback-classic:1.2.3" + object common extends PublishModule with SbtModule { - val `com.github.julien-truffaut:monocle-core` = - mvn"com.github.julien-truffaut::monocle-core:1.4.0" + def scalaVersion = "2.12.3" - val `com.github.julien-truffaut:monocle-macro` = - mvn"com.github.julien-truffaut::monocle-macro:1.4.0" - - val `com.github.pureconfig:pureconfig` = - mvn"com.github.pureconfig::pureconfig:0.8.0" + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8" + ) - val `com.typesafe.akka:akka-stream` = - mvn"com.typesafe.akka::akka-stream:2.5.6" + def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ + Seq(mvn"org.wartremover::wartremover:2.2.1") - val `com.typesafe.scala-logging:scala-logging` = - mvn"com.typesafe.scala-logging::scala-logging:3.7.2" - val `com.typesafe:config` = mvn"com.typesafe:config:1.3.1" + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"ch.qos.logback:logback-classic:1.2.3", + mvn"net.logstash.logback:logstash-logback-encoder:4.11", + mvn"com.typesafe.scala-logging::scala-logging:3.7.2", + mvn"org.slf4j:jcl-over-slf4j:1.7.25", + mvn"com.typesafe:config:1.3.1", + mvn"com.typesafe.akka::akka-stream:2.5.6" + ) - val `io.netty:netty-transport-native-epoll` = - mvn"io.netty:netty-transport-native-epoll:4.1.118.Final;type=pom;classifier=linux-x86_64;exclude=io.netty:netty-transport-native-epoll" + def pomSettings = PomSettings( + "This is the common module.", + "com.pbassiner", + "https://github.com/com-lihaoyi/mill", + Seq(License( + "Apache-2.0", + "Apache-2.0", + "https://www.apache.org/licenses/LICENSE-2.0.txt", + false, + false, + "" + )), + VersionControl( + Some("https://github.com/com-lihaoyi/mill"), + Some("scm:git:https://github.com/com-lihaoyi/mill.git"), + None, + None + ), + Seq( + Developer("johnd", "John Doe", "https://example.com/johnd", None, None) + ) + ) - val `net.logstash.logback:logstash-logback-encoder` = - mvn"net.logstash.logback:logstash-logback-encoder:4.11" - val `org.scalacheck:scalacheck` = mvn"org.scalacheck::scalacheck:1.13.5" - val `org.scalatest:scalatest` = mvn"org.scalatest::scalatest:3.0.4" - val `org.slf4j:jcl-over-slf4j` = mvn"org.slf4j:jcl-over-slf4j:1.7.25" -} + def publishVersion = "0.1.0-SNAPSHOT" -object `package` extends BaseModule { - - def artifactName = "sbt-multi-project-example" - - def pomSettings = PomSettings( - "This is an sbt sample project for testing Mill's init command.", - "com.pbassiner", - "https://github.com/com-lihaoyi/mill", - Seq(License( - "Apache-2.0", - "Apache-2.0", - "https://www.apache.org/licenses/LICENSE-2.0.txt", - false, - false, - "repo" - )), - VersionControl( - Some("https://github.com/com-lihaoyi/mill"), - Some("scm:git:https://github.com/com-lihaoyi/mill.git"), - None, - None - ), - Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) - ) - - object common extends BaseModule { - - def mvnDeps = Seq( - Deps.`ch.qos.logback:logback-classic`, - Deps.`com.typesafe.akka:akka-stream`, - Deps.`com.typesafe.scala-logging:scala-logging`, - Deps.`com.typesafe:config`, - Deps.`net.logstash.logback:logstash-logback-encoder`, - Deps.`org.slf4j:jcl-over-slf4j` - ) + def artifactMetadata = Artifact("com.pbassiner", "common", "0.1.0-SNAPSHOT") object tests extends SbtTests with TestModule.ScalaTest { - def mvnDeps = - Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq( + mvn"org.scalatest::scalatest:3.0.4", + mvn"org.scalacheck::scalacheck:1.13.5" + ) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } + object multi1 extends PublishModule with SbtModule { - object multi1 extends BaseModule { + def scalaVersion = "2.12.3" - def scalacOptions = super.scalacOptions() ++ Seq("-V") + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8", + "-V" + ) - def mvnDeps = Seq( - Deps.`ch.qos.logback:logback-classic`, - Deps.`com.github.julien-truffaut:monocle-core`, - Deps.`com.github.julien-truffaut:monocle-macro`, - Deps.`com.typesafe.akka:akka-stream`, - Deps.`com.typesafe.scala-logging:scala-logging`, - Deps.`com.typesafe:config`, - Deps.`net.logstash.logback:logstash-logback-encoder`, - Deps.`org.slf4j:jcl-over-slf4j` + def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ + Seq(mvn"org.wartremover::wartremover:2.2.1") + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"ch.qos.logback:logback-classic:1.2.3", + mvn"net.logstash.logback:logstash-logback-encoder:4.11", + mvn"com.typesafe.scala-logging::scala-logging:3.7.2", + mvn"org.slf4j:jcl-over-slf4j:1.7.25", + mvn"com.typesafe:config:1.3.1", + mvn"com.typesafe.akka::akka-stream:2.5.6", + mvn"com.github.julien-truffaut::monocle-core:1.4.0", + mvn"com.github.julien-truffaut::monocle-macro:1.4.0" ) def moduleDeps = super.moduleDeps ++ Seq(build.common) @@ -113,7 +119,7 @@ object `package` extends BaseModule { "https://www.apache.org/licenses/LICENSE-2.0.txt", false, false, - "repo" + "" )), VersionControl( Some("https://github.com/com-lihaoyi/mill"), @@ -126,20 +132,28 @@ object `package` extends BaseModule { ) ) + def publishVersion = "0.1.0-SNAPSHOT" + + def artifactMetadata = Artifact("com.pbassiner", "multi1", "0.1.0-SNAPSHOT") + object tests extends SbtTests with TestModule.ScalaTest { - def mvnDeps = - Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq( + mvn"org.scalatest::scalatest:3.0.4", + mvn"org.scalacheck::scalacheck:1.13.5" + ) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } + object multi2 extends PublishModule with SbtModule { - object multi2 extends BaseModule { + def scalaVersion = "2.12.3" - def scalacOptions = Seq( + def scalacOptions = super.scalacOptions() ++ Seq( "-unchecked", "-feature", "-language:existentials", @@ -149,14 +163,17 @@ object `package` extends BaseModule { "-deprecation" ) - def mvnDeps = Seq( - Deps.`ch.qos.logback:logback-classic`, - Deps.`com.github.pureconfig:pureconfig`, - Deps.`com.typesafe.akka:akka-stream`, - Deps.`com.typesafe.scala-logging:scala-logging`, - Deps.`com.typesafe:config`, - Deps.`net.logstash.logback:logstash-logback-encoder`, - Deps.`org.slf4j:jcl-over-slf4j` + def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ + Seq(mvn"org.wartremover::wartremover:2.2.1") + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"ch.qos.logback:logback-classic:1.2.3", + mvn"net.logstash.logback:logstash-logback-encoder:4.11", + mvn"com.typesafe.scala-logging::scala-logging:3.7.2", + mvn"org.slf4j:jcl-over-slf4j:1.7.25", + mvn"com.typesafe:config:1.3.1", + mvn"com.typesafe.akka::akka-stream:2.5.6", + mvn"com.github.pureconfig::pureconfig:0.8.0" ) def moduleDeps = super.moduleDeps ++ Seq(build.common) @@ -171,7 +188,7 @@ object `package` extends BaseModule { "https://www.apache.org/licenses/LICENSE-2.0.txt", false, false, - "repo" + "" )), VersionControl( Some("https://github.com/com-lihaoyi/mill"), @@ -184,22 +201,47 @@ object `package` extends BaseModule { ) ) + def publishVersion = "0.1.0-SNAPSHOT" + + def artifactMetadata = Artifact("com.pbassiner", "multi2", "0.1.0-SNAPSHOT") + object tests extends SbtTests with TestModule.ScalaTest { - def mvnDeps = - Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq( + mvn"org.scalatest::scalatest:3.0.4", + mvn"org.scalacheck::scalacheck:1.13.5" + ) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } - } + } object nested extends Module { - object nested extends BaseModule { + object nested extends PublishModule with SbtModule { + + def scalaVersion = "2.12.3" - def mvnDeps = Seq(Deps.`io.netty:netty-transport-native-epoll`) + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8" + ) + + def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ + Seq(mvn"org.wartremover::wartremover:2.2.1") + + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"io.netty:netty-transport-native-epoll:4.1.118.Final;classifier=linux-x86_64;type=Some(pom);exclude=io.netty:netty-transport-native-epoll" + ) def pomSettings = PomSettings( "This is an sbt sample project for testing Mill's init command.", @@ -211,7 +253,7 @@ object `package` extends BaseModule { "https://www.apache.org/licenses/LICENSE-2.0.txt", false, false, - "repo" + "" )), VersionControl( Some("https://github.com/com-lihaoyi/mill"), @@ -228,51 +270,11 @@ object `package` extends BaseModule { )) ) + def publishVersion = "0.1.0-SNAPSHOT" + + def artifactMetadata = + Artifact("com.pbassiner", "nested", "0.1.0-SNAPSHOT") + } } } - -trait BaseModule extends SbtModule with PublishModule { - - def scalaVersion = "2.12.3" - - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation", - "-encoding", - "utf8" - ) - - def pomSettings = PomSettings( - "This is the common module.", - "com.pbassiner", - "https://github.com/com-lihaoyi/mill", - Seq(License( - "Apache-2.0", - "Apache-2.0", - "https://www.apache.org/licenses/LICENSE-2.0.txt", - false, - false, - "repo" - )), - VersionControl( - Some("https://github.com/com-lihaoyi/mill"), - Some("scm:git:https://github.com/com-lihaoyi/mill.git"), - None, - None - ), - Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) - ) - - def publishVersion = "0.1.0-SNAPSHOT" - - def repositories = Seq( - "https://oss.sonatype.org/service/local/repositories/releases/content/", - "https://oss.sonatype.org/content/repositories/snapshots" - ) -} diff --git a/libs/init/sbt/test/resources/expected/config/without-base-project/sbt-multi-project-example/build.mill b/libs/init/sbt/test/resources/expected/config/without-base-project/sbt-multi-project-example/build.mill deleted file mode 100644 index 4851af47923e..000000000000 --- a/libs/init/sbt/test/resources/expected/config/without-base-project/sbt-multi-project-example/build.mill +++ /dev/null @@ -1,207 +0,0 @@ -//| mill-version: SNAPSHOT -//| mill-jvm-version: 11 -package build - -import _root_.build_.BaseModule -import mill._ -import mill.javalib._ -import mill.javalib.publish._ -import mill.scalalib.SbtModule - -object Deps { - - val `ch.qos.logback:logback-classic` = - mvn"ch.qos.logback:logback-classic:1.2.3" - - val `com.github.julien-truffaut:monocle-core` = - mvn"com.github.julien-truffaut::monocle-core:1.4.0" - - val `com.github.julien-truffaut:monocle-macro` = - mvn"com.github.julien-truffaut::monocle-macro:1.4.0" - - val `com.github.pureconfig:pureconfig` = - mvn"com.github.pureconfig::pureconfig:0.8.0" - - val `com.typesafe.akka:akka-stream` = - mvn"com.typesafe.akka::akka-stream:2.5.6" - - val `com.typesafe.scala-logging:scala-logging` = - mvn"com.typesafe.scala-logging::scala-logging:3.7.2" - val `com.typesafe:config` = mvn"com.typesafe:config:1.3.1" - - val `io.netty:netty-transport-native-epoll` = - mvn"io.netty:netty-transport-native-epoll:4.1.118.Final;type=pom;classifier=linux-x86_64;exclude=io.netty:netty-transport-native-epoll" - - val `net.logstash.logback:logstash-logback-encoder` = - mvn"net.logstash.logback:logstash-logback-encoder:4.11" - val `org.scalacheck:scalacheck` = mvn"org.scalacheck::scalacheck:1.13.5" - val `org.scalatest:scalatest` = mvn"org.scalatest::scalatest:3.0.4" - val `org.slf4j:jcl-over-slf4j` = mvn"org.slf4j:jcl-over-slf4j:1.7.25" -} - -object `package` extends BaseModule { - - def artifactName = "sbt-multi-project-example" - - object common extends BaseModule { - - def mvnDeps = Seq( - Deps.`ch.qos.logback:logback-classic`, - Deps.`com.typesafe.akka:akka-stream`, - Deps.`com.typesafe.scala-logging:scala-logging`, - Deps.`com.typesafe:config`, - Deps.`net.logstash.logback:logstash-logback-encoder`, - Deps.`org.slf4j:jcl-over-slf4j` - ) - - def pomSettings = PomSettings( - "This is the common module.", - "com.pbassiner", - "https://github.com/com-lihaoyi/mill", - Seq(License( - "Apache-2.0", - "Apache-2.0", - "https://www.apache.org/licenses/LICENSE-2.0.txt", - false, - false, - "repo" - )), - VersionControl( - Some("https://github.com/com-lihaoyi/mill"), - Some("scm:git:https://github.com/com-lihaoyi/mill.git"), - None, - None - ), - Seq( - Developer("johnd", "John Doe", "https://example.com/johnd", None, None) - ) - ) - - object tests extends SbtTests with TestModule.ScalaTest { - - def mvnDeps = - Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) - - def testSandboxWorkingDir = false - def testParallelism = false - - } - } - - object multi1 extends BaseModule { - - def scalacOptions = super.scalacOptions() ++ Seq("-V") - - def mvnDeps = Seq( - Deps.`ch.qos.logback:logback-classic`, - Deps.`com.github.julien-truffaut:monocle-core`, - Deps.`com.github.julien-truffaut:monocle-macro`, - Deps.`com.typesafe.akka:akka-stream`, - Deps.`com.typesafe.scala-logging:scala-logging`, - Deps.`com.typesafe:config`, - Deps.`net.logstash.logback:logstash-logback-encoder`, - Deps.`org.slf4j:jcl-over-slf4j` - ) - - def moduleDeps = super.moduleDeps ++ Seq(build.common) - - object tests extends SbtTests with TestModule.ScalaTest { - - def mvnDeps = - Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) - - def testSandboxWorkingDir = false - def testParallelism = false - - } - } - - object multi2 extends BaseModule { - - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation" - ) - - def mvnDeps = Seq( - Deps.`ch.qos.logback:logback-classic`, - Deps.`com.github.pureconfig:pureconfig`, - Deps.`com.typesafe.akka:akka-stream`, - Deps.`com.typesafe.scala-logging:scala-logging`, - Deps.`com.typesafe:config`, - Deps.`net.logstash.logback:logstash-logback-encoder`, - Deps.`org.slf4j:jcl-over-slf4j` - ) - - def moduleDeps = super.moduleDeps ++ Seq(build.common) - - object tests extends SbtTests with TestModule.ScalaTest { - - def mvnDeps = - Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) - - def testSandboxWorkingDir = false - def testParallelism = false - - } - } - - object nested extends Module { - - object nested extends BaseModule { - - def mvnDeps = Seq(Deps.`io.netty:netty-transport-native-epoll`) - - } - } -} - -trait BaseModule extends SbtModule with PublishModule { - - def scalaVersion = "2.12.3" - - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation", - "-encoding", - "utf8" - ) - - def pomSettings = PomSettings( - "This is an sbt sample project for testing Mill's init command.", - "com.pbassiner", - "https://github.com/com-lihaoyi/mill", - Seq(License( - "Apache-2.0", - "Apache-2.0", - "https://www.apache.org/licenses/LICENSE-2.0.txt", - false, - false, - "repo" - )), - VersionControl( - Some("https://github.com/com-lihaoyi/mill"), - Some("scm:git:https://github.com/com-lihaoyi/mill.git"), - None, - None - ), - Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) - ) - - def publishVersion = "0.1.0-SNAPSHOT" - - def repositories = Seq( - "https://oss.sonatype.org/service/local/repositories/releases/content/", - "https://oss.sonatype.org/content/repositories/snapshots" - ) -} diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/build.mill b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/build.mill index 11078c022b3a..96bab9a7a29f 100644 --- a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/build.mill +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/build.mill @@ -1,55 +1,7 @@ //| mill-version: SNAPSHOT package build -import mill._ -import mill.javalib._ -import mill.javalib.publish._ -import mill.scalalib.SbtModule +import mill.api._ +import millbuild._ -object `package` extends SbtModule with PublishModule { - - def artifactName = "sbt-multi-project-example" - - def scalaVersion = "2.12.3" - - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation", - "-encoding", - "utf8" - ) - - def repositories = Seq( - "https://oss.sonatype.org/service/local/repositories/releases/content/", - "https://oss.sonatype.org/content/repositories/snapshots" - ) - - def pomSettings = PomSettings( - "This is an sbt sample project for testing Mill's init command.", - "com.pbassiner", - "https://github.com/com-lihaoyi/mill", - Seq(License( - "Apache-2.0", - "Apache-2.0", - "https://www.apache.org/licenses/LICENSE-2.0.txt", - false, - false, - "repo" - )), - VersionControl( - Some("https://github.com/com-lihaoyi/mill"), - Some("scm:git:https://github.com/com-lihaoyi/mill.git"), - None, - None - ), - Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) - ) - - def publishVersion = "0.1.0-SNAPSHOT" - -} +object `package` extends Module {} diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/common/package.mill b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/common/package.mill index 72fa39c19ef6..2234cb343349 100644 --- a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/common/package.mill +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/common/package.mill @@ -1,38 +1,21 @@ package build.common -import mill._ import mill.javalib._ import mill.javalib.publish._ -import mill.scalalib.SbtModule +import mill.scalalib._ +import millbuild._ -object `package` extends SbtModule with PublishModule { +object `package` extends SbtMultiProjectExampleBaseModule { - def scalaVersion = "2.12.3" + def scalacOptions = super.scalacOptions() ++ Seq("-encoding", "utf8") - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation", - "-encoding", - "utf8" - ) - - def repositories = Seq( - "https://oss.sonatype.org/service/local/repositories/releases/content/", - "https://oss.sonatype.org/content/repositories/snapshots" - ) - - def mvnDeps = Seq( - mvn"ch.qos.logback:logback-classic:1.2.3", - mvn"com.typesafe.akka::akka-stream:2.5.6", - mvn"com.typesafe.scala-logging::scala-logging:3.7.2", - mvn"com.typesafe:config:1.3.1", - mvn"net.logstash.logback:logstash-logback-encoder:4.11", - mvn"org.slf4j:jcl-over-slf4j:1.7.25" + def mvnDeps = super.mvnDeps() ++ Seq( + Deps.logbackClassic, + Deps.logstashLogbackEncoder, + Deps.scalaLogging, + Deps.jclOverSlf4j, + Deps.config, + Deps.akkaStream ) def pomSettings = PomSettings( @@ -45,7 +28,7 @@ object `package` extends SbtModule with PublishModule { "https://www.apache.org/licenses/LICENSE-2.0.txt", false, false, - "repo" + "" )), VersionControl( Some("https://github.com/com-lihaoyi/mill"), @@ -56,17 +39,16 @@ object `package` extends SbtModule with PublishModule { Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) ) - def publishVersion = "0.1.0-SNAPSHOT" + def artifactMetadata = Artifact("com.pbassiner", "common", "0.1.0-SNAPSHOT") object test extends SbtTests with TestModule.ScalaTest { - def mvnDeps = Seq( - mvn"org.scalacheck::scalacheck:1.13.5", - mvn"org.scalatest::scalatest:3.0.4" - ) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(Deps.scalatest, Deps.scalacheck) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/build.mill b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/build.mill new file mode 100644 index 000000000000..26686f5e0d2e --- /dev/null +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/build.mill @@ -0,0 +1,5 @@ +package build + +import mill.meta.MillBuildRootModule + +object `package` extends MillBuildRootModule diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/src/Deps.scala b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/src/Deps.scala new file mode 100644 index 000000000000..6c23a3d0f71e --- /dev/null +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/src/Deps.scala @@ -0,0 +1,22 @@ +package millbuild + +import mill.javalib._ + +object Deps { + + val akkaStream = mvn"com.typesafe.akka::akka-stream:2.5.6" + val config = mvn"com.typesafe:config:1.3.1" + val jclOverSlf4j = mvn"org.slf4j:jcl-over-slf4j:1.7.25" + val logbackClassic = mvn"ch.qos.logback:logback-classic:1.2.3" + val logstashLogbackEncoder = + mvn"net.logstash.logback:logstash-logback-encoder:4.11" + val monocleCore = mvn"com.github.julien-truffaut::monocle-core:1.4.0" + val monocleMacro = mvn"com.github.julien-truffaut::monocle-macro:1.4.0" + val nettyTransportNativeEpoll = + mvn"io.netty:netty-transport-native-epoll:4.1.118.Final;classifier=linux-x86_64;type=Some(pom);exclude=io.netty:netty-transport-native-epoll" + val pureconfig = mvn"com.github.pureconfig::pureconfig:0.8.0" + val scalaLogging = mvn"com.typesafe.scala-logging::scala-logging:3.7.2" + val scalacheck = mvn"org.scalacheck::scalacheck:1.13.5" + val scalatest = mvn"org.scalatest::scalatest:3.0.4" + val wartremover = mvn"org.wartremover::wartremover:2.2.1" +} diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/src/SbtMultiProjectExampleBaseModule.scala b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/src/SbtMultiProjectExampleBaseModule.scala new file mode 100644 index 000000000000..1e02b794943c --- /dev/null +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/mill-build/src/SbtMultiProjectExampleBaseModule.scala @@ -0,0 +1,25 @@ +package millbuild + +import mill.javalib._ +import mill.javalib.publish._ +import mill.scalalib._ + +trait SbtMultiProjectExampleBaseModule extends PublishModule with SbtModule { + + def scalaVersion = "2.12.3" + + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation" + ) + + def scalacPluginMvnDeps = super.scalacPluginMvnDeps() ++ Seq(Deps.wartremover) + + def publishVersion = "0.1.0-SNAPSHOT" + +} diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi1/package.mill b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi1/package.mill index f3547c7ae1ea..ebd85956ae7f 100644 --- a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi1/package.mill +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi1/package.mill @@ -1,41 +1,23 @@ package build.multi1 -import mill._ import mill.javalib._ import mill.javalib.publish._ -import mill.scalalib.SbtModule - -object `package` extends SbtModule with PublishModule { - - def scalaVersion = "2.12.3" - - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation", - "-encoding", - "utf8", - "-V" - ) - - def repositories = Seq( - "https://oss.sonatype.org/service/local/repositories/releases/content/", - "https://oss.sonatype.org/content/repositories/snapshots" - ) - - def mvnDeps = Seq( - mvn"ch.qos.logback:logback-classic:1.2.3", - mvn"com.github.julien-truffaut::monocle-core:1.4.0", - mvn"com.github.julien-truffaut::monocle-macro:1.4.0", - mvn"com.typesafe.akka::akka-stream:2.5.6", - mvn"com.typesafe.scala-logging::scala-logging:3.7.2", - mvn"com.typesafe:config:1.3.1", - mvn"net.logstash.logback:logstash-logback-encoder:4.11", - mvn"org.slf4j:jcl-over-slf4j:1.7.25" +import mill.scalalib._ +import millbuild._ + +object `package` extends SbtMultiProjectExampleBaseModule { + + def scalacOptions = super.scalacOptions() ++ Seq("-encoding", "utf8", "-V") + + def mvnDeps = super.mvnDeps() ++ Seq( + Deps.logbackClassic, + Deps.logstashLogbackEncoder, + Deps.scalaLogging, + Deps.jclOverSlf4j, + Deps.config, + Deps.akkaStream, + Deps.monocleCore, + Deps.monocleMacro ) def moduleDeps = super.moduleDeps ++ Seq(build.common) @@ -50,7 +32,7 @@ object `package` extends SbtModule with PublishModule { "https://www.apache.org/licenses/LICENSE-2.0.txt", false, false, - "repo" + "" )), VersionControl( Some("https://github.com/com-lihaoyi/mill"), @@ -61,17 +43,16 @@ object `package` extends SbtModule with PublishModule { Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) ) - def publishVersion = "0.1.0-SNAPSHOT" + def artifactMetadata = Artifact("com.pbassiner", "multi1", "0.1.0-SNAPSHOT") object test extends SbtTests with TestModule.ScalaTest { - def mvnDeps = Seq( - mvn"org.scalacheck::scalacheck:1.13.5", - mvn"org.scalatest::scalatest:3.0.4" - ) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(Deps.scalatest, Deps.scalacheck) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi2/package.mill b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi2/package.mill index 170532f0f196..36455d6b8d94 100644 --- a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi2/package.mill +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/multi2/package.mill @@ -1,37 +1,20 @@ package build.multi2 -import mill._ import mill.javalib._ import mill.javalib.publish._ -import mill.scalalib.SbtModule - -object `package` extends SbtModule with PublishModule { - - def scalaVersion = "2.12.3" - - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation" - ) - - def repositories = Seq( - "https://oss.sonatype.org/service/local/repositories/releases/content/", - "https://oss.sonatype.org/content/repositories/snapshots" - ) - - def mvnDeps = Seq( - mvn"ch.qos.logback:logback-classic:1.2.3", - mvn"com.github.pureconfig::pureconfig:0.8.0", - mvn"com.typesafe.akka::akka-stream:2.5.6", - mvn"com.typesafe.scala-logging::scala-logging:3.7.2", - mvn"com.typesafe:config:1.3.1", - mvn"net.logstash.logback:logstash-logback-encoder:4.11", - mvn"org.slf4j:jcl-over-slf4j:1.7.25" +import mill.scalalib._ +import millbuild._ + +object `package` extends SbtMultiProjectExampleBaseModule { + + def mvnDeps = super.mvnDeps() ++ Seq( + Deps.logbackClassic, + Deps.logstashLogbackEncoder, + Deps.scalaLogging, + Deps.jclOverSlf4j, + Deps.config, + Deps.akkaStream, + Deps.pureconfig ) def moduleDeps = super.moduleDeps ++ Seq(build.common) @@ -46,7 +29,7 @@ object `package` extends SbtModule with PublishModule { "https://www.apache.org/licenses/LICENSE-2.0.txt", false, false, - "repo" + "" )), VersionControl( Some("https://github.com/com-lihaoyi/mill"), @@ -57,17 +40,16 @@ object `package` extends SbtModule with PublishModule { Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) ) - def publishVersion = "0.1.0-SNAPSHOT" + def artifactMetadata = Artifact("com.pbassiner", "multi2", "0.1.0-SNAPSHOT") object test extends SbtTests with TestModule.ScalaTest { - def mvnDeps = Seq( - mvn"org.scalacheck::scalacheck:1.13.5", - mvn"org.scalatest::scalatest:3.0.4" - ) + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ + Seq(Deps.scalatest, Deps.scalacheck) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/nested/package.mill b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/nested/package.mill index 15af027de1ff..c2159888c92c 100644 --- a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/nested/package.mill +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/nested/package.mill @@ -1,34 +1,15 @@ package build.nested.nested -import mill._ import mill.javalib._ import mill.javalib.publish._ -import mill.scalalib.SbtModule +import mill.scalalib._ +import millbuild._ -object `package` extends SbtModule with PublishModule { +object `package` extends SbtMultiProjectExampleBaseModule { - def scalaVersion = "2.12.3" + def scalacOptions = super.scalacOptions() ++ Seq("-encoding", "utf8") - def scalacOptions = Seq( - "-unchecked", - "-feature", - "-language:existentials", - "-language:higherKinds", - "-language:implicitConversions", - "-language:postfixOps", - "-deprecation", - "-encoding", - "utf8" - ) - - def repositories = Seq( - "https://oss.sonatype.org/service/local/repositories/releases/content/", - "https://oss.sonatype.org/content/repositories/snapshots" - ) - - def mvnDeps = Seq( - mvn"io.netty:netty-transport-native-epoll:4.1.118.Final;type=pom;classifier=linux-x86_64;exclude=io.netty:netty-transport-native-epoll" - ) + def mvnDeps = super.mvnDeps() ++ Seq(Deps.nettyTransportNativeEpoll) def pomSettings = PomSettings( "This is an sbt sample project for testing Mill's init command.", @@ -40,7 +21,7 @@ object `package` extends SbtModule with PublishModule { "https://www.apache.org/licenses/LICENSE-2.0.txt", false, false, - "repo" + "" )), VersionControl( Some("https://github.com/com-lihaoyi/mill"), @@ -51,6 +32,6 @@ object `package` extends SbtModule with PublishModule { Seq(Developer("johnd", "John Doe", "https://example.com/johnd", None, None)) ) - def publishVersion = "0.1.0-SNAPSHOT" + def artifactMetadata = Artifact("com.pbassiner", "nested", "0.1.0-SNAPSHOT") } diff --git a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/package.mill b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/package.mill index 29318cbd6408..41dd3f0ff923 100644 --- a/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/package.mill +++ b/libs/init/sbt/test/resources/expected/sbt-multi-project-example/nested/package.mill @@ -1,5 +1,6 @@ package build.nested -import mill._ +import mill.api._ +import millbuild._ object `package` extends Module {} diff --git a/libs/init/sbt/test/resources/expected/scala-seed-project/build.mill b/libs/init/sbt/test/resources/expected/scala-seed-project/build.mill index 94e8c492ddbd..5f898b64360d 100644 --- a/libs/init/sbt/test/resources/expected/scala-seed-project/build.mill +++ b/libs/init/sbt/test/resources/expected/scala-seed-project/build.mill @@ -1,20 +1,18 @@ //| mill-version: SNAPSHOT package build -import mill._ import mill.javalib._ import mill.javalib.publish._ -import mill.scalalib.SbtModule +import mill.scalalib._ +import millbuild._ -object `package` extends SbtModule with PublishModule { - - def artifactName = "Scala Seed Project" +object `package` extends PublishModule with SbtModule { def scalaVersion = "2.13.12" def pomSettings = PomSettings( "Scala Seed Project", - "com.example", + "example", "", Seq(), VersionControl(None, None, None, None), @@ -23,12 +21,16 @@ object `package` extends SbtModule with PublishModule { def publishVersion = "0.1.0-SNAPSHOT" + def artifactMetadata = + Artifact("com.example", "scala-seed-project", "0.1.0-SNAPSHOT") + object test extends SbtTests with TestModule.Munit { - def mvnDeps = Seq(mvn"org.scalameta::munit:0.7.29") + def mandatoryMvnDeps = super.mandatoryMvnDeps() ++ Seq(Deps.munit) - def testSandboxWorkingDir = false def testParallelism = false + def testSandboxWorkingDir = false } + } diff --git a/libs/init/sbt/test/resources/expected/scala-seed-project/mill-build/build.mill b/libs/init/sbt/test/resources/expected/scala-seed-project/mill-build/build.mill new file mode 100644 index 000000000000..26686f5e0d2e --- /dev/null +++ b/libs/init/sbt/test/resources/expected/scala-seed-project/mill-build/build.mill @@ -0,0 +1,5 @@ +package build + +import mill.meta.MillBuildRootModule + +object `package` extends MillBuildRootModule diff --git a/libs/init/sbt/test/resources/expected/scala-seed-project/mill-build/src/Deps.scala b/libs/init/sbt/test/resources/expected/scala-seed-project/mill-build/src/Deps.scala new file mode 100644 index 000000000000..c0d2d76c0999 --- /dev/null +++ b/libs/init/sbt/test/resources/expected/scala-seed-project/mill-build/src/Deps.scala @@ -0,0 +1,8 @@ +package millbuild + +import mill.javalib._ + +object Deps { + + val munit = mvn"org.scalameta::munit:0.7.29" +} diff --git a/libs/init/sbt/test/src/mill/main/sbt/BuildGenTests.scala b/libs/init/sbt/test/src/mill/main/sbt/BuildGenTests.scala index 2a15135d51a8..a059b5ee897b 100644 --- a/libs/init/sbt/test/src/mill/main/sbt/BuildGenTests.scala +++ b/libs/init/sbt/test/src/mill/main/sbt/BuildGenTests.scala @@ -26,30 +26,11 @@ object BuildGenTests extends TestSuite { } test("config") { - val commonArgs = Array( - "--base-module", - "BaseModule", - "--jvm-id", - "11", - "--test-module", - "tests", - "--deps-object", - "Deps", - "--merge" - ) test("sbt-multi-project-example") { val sourceRoot = os.sub / "sbt-multi-project-example" - test("without-base-project") { - val expectedRoot = - os.sub / "expected/config/without-base-project/sbt-multi-project-example" - val args = commonArgs - assert( - checker.check(SbtBuildGenMain.main(args), sourceRoot, expectedRoot) - ) - } test("all") { val expectedRoot = os.sub / "expected/config/all/sbt-multi-project-example" - val args = commonArgs ++ Array("--baseProject", "common") + val args = Array("--test-module", "tests", "--merge", "--no-meta-build") assert( checker.check(SbtBuildGenMain.main(args), sourceRoot, expectedRoot) ) diff --git a/libs/init/src/mill/init/BuildGenModule.scala b/libs/init/src/mill/init/BuildGenModule.scala index b36450894803..bc20872fe0cf 100644 --- a/libs/init/src/mill/init/BuildGenModule.scala +++ b/libs/init/src/mill/init/BuildGenModule.scala @@ -19,7 +19,11 @@ trait BuildGenModule extends CoursierModule with DefaultTaskModule { def buildGenMainClass: T[String] - def buildGenScalafmtConfig: T[PathRef] = PathRef(mill.init.Util.scalafmtConfigFile) + def buildGenScalafmtConfig: T[PathRef] = Task { + val out = Task.dest / ".scalafmt.conf" + os.write(out, mill.init.Util.scalafmtConfig) + PathRef(out) + } def init(args: String*): Command[Unit] = Task.Command(exclusive = true) { val root = moduleDir @@ -36,7 +40,7 @@ trait BuildGenModule extends CoursierModule with DefaultTaskModule { ).exitCode if (exitCode == 0) { - val files = mill.init.Util.buildFiles(root).map(PathRef(_)).toSeq + val files = mill.init.Util.buildFiles(root).map(PathRef(_)) val config = buildGenScalafmtConfig() Task.log.info("formatting Mill build files") ScalafmtWorkerModule.worker().reformat(files, config) diff --git a/libs/init/src/mill/init/Util.scala b/libs/init/src/mill/init/Util.scala index b4faee297a21..a9754c090e1e 100644 --- a/libs/init/src/mill/init/Util.scala +++ b/libs/init/src/mill/init/Util.scala @@ -1,27 +1,22 @@ package mill.init -import mill.constants.OutFiles -import mill.constants.CodeGenConstants.buildFileExtensions +import mill.constants.CodeGenConstants.{nestedBuildFileNames, rootBuildFileNames} +import mill.constants.OutFiles.{bspOut, millBuild, out} object Util { - def scalafmtConfigFile: os.Path = - os.temp( - """version = "3.8.4" - |runner.dialect = scala213 - |newlines.source=fold - |newlines.topLevelStatementBlankLines = [ - | { - | blanks { before = 1 } - | } - |] - |""".stripMargin - ) + def scalafmtConfig: String = + """version = "3.8.5" + |runner.dialect = scala213 + |newlines.source=fold + |""".stripMargin - def buildFiles(workspace: os.Path): geny.Generator[os.Path] = { - val outDir = workspace / os.RelPath(OutFiles.out) - val bspOutDir = workspace / os.RelPath(OutFiles.bspOut) - - os.walk.stream(workspace, skip = path => path == outDir || path == bspOutDir) - .filter(file => buildFileExtensions.contains(file.ext)) + def buildFiles(workspace: os.Path): Seq[os.Path] = { + val skip = Seq(bspOut, millBuild, out).map(s => workspace / os.RelPath(s)) + os.walk.stream(workspace, skip = skip.contains).filter(path => + os.isFile(path) && + (nestedBuildFileNames.contains(path.last) || rootBuildFileNames.contains(path.last)) + ).toSeq ++ { + val path = workspace / os.RelPath(millBuild) + if (os.exists(path)) os.walk.stream(path).filter(os.isFile).toSeq else Nil + } } - } diff --git a/libs/javalib/src/mill/javalib/MavenModule.scala b/libs/javalib/src/mill/javalib/MavenModule.scala index 664425768f5c..2c0197bb969c 100644 --- a/libs/javalib/src/mill/javalib/MavenModule.scala +++ b/libs/javalib/src/mill/javalib/MavenModule.scala @@ -1,9 +1,9 @@ package mill.javalib -import java.nio.file.Path - import mill.Task +import java.nio.file.Path + /** * A [[JavaModule]] with a Maven compatible directory layout. * `src/main/java`, `src/test/resources`, etc. @@ -12,10 +12,13 @@ import mill.Task */ trait MavenModule extends JavaModule { outer => - override def sources = Task.Sources("src/main/java") + // added for binary compatibility + private[mill] def sourcesFolders0 = Seq(os.sub / "src/main/java") + override def sourcesFolders = sourcesFolders0 + override def sources = Task.Sources(sourcesFolders*) // replicated for binary compatibility override def resources = Task.Sources("src/main/resources") - trait MavenTests extends JavaTests { + trait MavenTests extends JavaTests with MavenModule { override def moduleDir = outer.moduleDir /** @@ -27,7 +30,10 @@ trait MavenModule extends JavaModule { outer => private[mill] override def intellijModulePathJava: Path = (outer.moduleDir / "src" / testModuleName).toNIO - override def sources = Task.Sources(moduleDir / "src" / testModuleName / "java") + // added for binary compatibility + private[mill] override def sourcesFolders0 = Seq(os.sub / "src" / testModuleName / "java") + override def sourcesFolders = sourcesFolders0 + override def sources = Task.Sources(sourcesFolders*) // replicated for binary compatibility override def resources = Task.Sources(moduleDir / "src" / testModuleName / "resources") } } diff --git a/libs/scalalib/src/mill/scalalib/CrossSbtPlatformModule.scala b/libs/scalalib/src/mill/scalalib/CrossSbtPlatformModule.scala new file mode 100644 index 000000000000..1114f2f7a4f0 --- /dev/null +++ b/libs/scalalib/src/mill/scalalib/CrossSbtPlatformModule.scala @@ -0,0 +1,27 @@ +package mill.scalalib + +/** + * A [[mill.api.Cross]] that extends the [[SbtPlatformModule]] layout with additional source + * directories with the scala version patterns as suffix, e.g. + * - src/main/scala-2 + * - src/main/scala-2.12 + * - src/main/scala-2.13 + * - js/src/main/scala-2 + * - js/src/main/scala-2.13 + * - jvm/src/main/scala-2.12 + * - jvm/src/main/scala-2.13 + * - native/src/main/scala-2 + * - native/src/main/scala-3 + * @note Use with [[CrossScalaVersionRanges]] to add version range specific sources. + */ +trait CrossSbtPlatformModule extends CrossSbtModule with SbtPlatformModule { outer => + override def versionSourcesPaths = sourcesRootFolders.flatMap(root => + scalaVersionDirectoryNames.map(s => root / "src/main" / s"scala-$s") + ) + + trait CrossSbtPlatformTests extends CrossSbtTests with SbtPlatformTests { + override def versionSourcesPaths = outer.sourcesRootFolders.flatMap(root => + scalaVersionDirectoryNames.map(s => root / "src" / testModuleName / s"scala-$s") + ) + } +} diff --git a/libs/scalalib/src/mill/scalalib/SbtModule.scala b/libs/scalalib/src/mill/scalalib/SbtModule.scala index 5c299193cf2a..520e5dc62fa1 100644 --- a/libs/scalalib/src/mill/scalalib/SbtModule.scala +++ b/libs/scalalib/src/mill/scalalib/SbtModule.scala @@ -1,18 +1,17 @@ package mill.scalalib -import mill.Task +import mill.api.Task /** * A [[ScalaModule]] with sbt compatible directory layout. */ trait SbtModule extends ScalaModule with MavenModule { - override def sources = Task.Sources("src/main/scala", "src/main/java") + override def sourcesFolders = sourcesFolders0 ++ Seq("src/main/scala") + override def sources = Task.Sources(sourcesFolders*) // replicated for binary compatibility - trait SbtTests extends ScalaTests with MavenTests { - override def sources = Task.Sources( - moduleDir / "src" / testModuleName / "java", - moduleDir / "src" / testModuleName / "scala" - ) + trait SbtTests extends ScalaTests with MavenTests with SbtModule { + override def sourcesFolders = sourcesFolders0 ++ Seq(os.sub / "src" / testModuleName / "scala") + override def sources = Task.Sources(sourcesFolders*) // replicated for binary compatibility } } diff --git a/libs/scalalib/src/mill/scalalib/SbtPlatformModule.scala b/libs/scalalib/src/mill/scalalib/SbtPlatformModule.scala new file mode 100644 index 000000000000..0879135e6783 --- /dev/null +++ b/libs/scalalib/src/mill/scalalib/SbtPlatformModule.scala @@ -0,0 +1,94 @@ +package mill.scalalib + +import mill.api.PathRef + +/** + * A cross-platform [[SbtModule]] that can share sources with other modules. + * {{{ + * object foo extends Module { + * object js extends SbtPlatformModule + * object jvm extends SbtPlatformModule + * object native extends SbtPlatformModule + * } + * }}} + * The example above corresponds to the following directory structure: + * {{{ + * foo + * ├─js + * │ └─src + * │ └─main + * │ ├─java + * │ └─scala + * ├─jvm + * │ └─src + * │ └─main + * │ ├─java + * │ └─scala + * ├─native + * │ └─src + * │ └─main + * │ ├─java + * │ └─scala + * └─src + * └─main + * ├─java + * └─scala + * }}} + * Source directories `foo/src/main/java` and `foo/src/main/scala` are shared by each submodule. + * Each submodule can define platform specific sources under a similar `sbt` compatible layout. + * + * For `sbt-crossproject` plugin layout, use one of the following presets: + * - [[SbtPlatformModule.CrossTypeFull]] + * - [[SbtPlatformModule.CrossTypePure]] + * - [[SbtPlatformModule.CrossTypeDummy]] + */ +trait SbtPlatformModule extends PlatformScalaModule with SbtModule { outer => + + protected def sourcesRootFolders = Seq(os.sub, os.sub / platformCrossSuffix) + override def sourcesFolders = + sourcesRootFolders.flatMap(root => super.sourcesFolders.map(root / _)) + override def resources = + sourcesRootFolders.map(root => PathRef(moduleDir / root / "src/main/resources")) + + trait SbtPlatformTests extends SbtTests { + + override def sourcesFolders = outer.sourcesRootFolders.flatMap(root => + super.sourcesFolders.map(root / _) + ) + override def resources = outer.sourcesRootFolders.map(root => + PathRef(moduleDir / root / "src" / testModuleName / "resources") + ) + } +} +object SbtPlatformModule { + + private def crossPartialRootFolders(platformCrossSuffix: String, platforms: String*) = + platforms.diff(platformCrossSuffix).iterator + .map(platform => os.SubPath(Seq(platformCrossSuffix, platform).sorted.mkString("-"))) + .toSeq + + /** + * A [[SbtPlatformModule]] with a layout corresponding to `sbtcrossproject.CrossType.Full`. + */ + trait CrossTypeFull extends SbtPlatformModule { + override def sourcesRootFolders = Seq( + os.sub / "shared", + os.sub / platformCrossSuffix + ) ++ crossPartialRootFolders(platformCrossSuffix, "js", "jvm", "native") + } + + /** + * A [[SbtPlatformModule]] with a layout corresponding to `sbtcrossproject.CrossType.Pure`. + */ + trait CrossTypePure extends SbtPlatformModule { + override def sourcesRootFolders = + Seq(os.sub) ++ crossPartialRootFolders(platformCrossSuffix, ".js", ".jvm", ".native") + } + + /** + * A [[SbtPlatformModule]] with a layout corresponding to `sbtcrossproject.CrossType.Dummy`. + */ + trait CrossTypeDummy extends SbtPlatformModule { + override def sourcesRootFolders = Seq(os.sub / platformCrossSuffix) + } +} diff --git a/testkit/src/mill/testkit/GitRepoIntegrationTestSuite.scala b/testkit/src/mill/testkit/GitRepoIntegrationTestSuite.scala new file mode 100644 index 000000000000..ec5d6c47f23a --- /dev/null +++ b/testkit/src/mill/testkit/GitRepoIntegrationTestSuite.scala @@ -0,0 +1,32 @@ +package mill.testkit + +import os.Path + +trait GitRepoIntegrationTestSuite extends UtestIntegrationTestSuite { + + def gitRepoUrl: String + def gitRepoBranch: String + def gitRepoDepth: Int = 1 + + override def integrationTest[T](block: IntegrationTester => T): T = { + val tester = new IntegrationTester( + daemonMode, + workspaceSourcePath, + millExecutable, + debugLog, + baseWorkspacePath = os.pwd, + propagateJavaHome = propagateJavaHome + ) { + override val workspacePath: Path = { + // To preserve the repo dir name, create a directory and clone into it. + val cwd = os.temp.dir(dir = baseWorkspacePath, deleteOnExit = false) + os.proc("git", "clone", gitRepoUrl, "--depth", gitRepoDepth, "--branch", gitRepoBranch) + .call(cwd = cwd) + os.list(cwd).head + } + override def initWorkspace(): Unit = () + } + try block(tester) + finally tester.close() + } +}