diff --git a/.github/actions/post-build-setup/action.yml b/.github/actions/post-build-setup/action.yml index ab854420f53..63ee9c9c38e 100644 --- a/.github/actions/post-build-setup/action.yml +++ b/.github/actions/post-build-setup/action.yml @@ -34,4 +34,6 @@ runs: name: ${{ inputs.os }}-selective-execution-artifact - run: mv out/mill-selective-execution/mill-selective-execution.json out/mill-selective-execution.json - shell: bash \ No newline at end of file + shell: bash + + - uses: sbt/setup-sbt@v1 diff --git a/.github/actions/pre-build-setup/action.yml b/.github/actions/pre-build-setup/action.yml index 585dc24d21b..720e58df130 100644 --- a/.github/actions/pre-build-setup/action.yml +++ b/.github/actions/pre-build-setup/action.yml @@ -65,4 +65,4 @@ runs: - uses: actions/checkout@v4 - run: echo temurin:${{ inputs.java-version }} > .mill-jvm-version - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index ce356a24ebc..fff08f9d91e 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -7,6 +7,8 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: sbt/setup-sbt@v1 + - run: | ./mill __.fix + mill.javalib.palantirformat.PalantirFormatModule/ + mill.scalalib.scalafmt.ScalafmtModule/ + mill.kotlinlib.ktlint.KtlintModule/ ./mill --meta-level 1 mill.scalalib.scalafmt.ScalafmtModule/ diff --git a/build.mill b/build.mill index 5c48e1e4e43..ee501da8c4c 100644 --- a/build.mill +++ b/build.mill @@ -53,8 +53,11 @@ object Deps { // and then add to it `bridgeScalaVersions` val scalaVersion = "3.6.2" val scala2Version = "2.13.15" + // The Scala 2.12.x version + val scalaVersion212 = "2.12.20" // The Scala 2.12.x version to use for some workers - val workerScalaVersion212 = "2.12.20" + val workerScalaVersion212 = scalaVersion212 + val sbtScalaVersion212 = scalaVersion212 val testScala213Version = "2.13.15" // Scala Native 4.2 will not get releases for new Scala version @@ -194,7 +197,8 @@ object Deps { val sourcecode = ivy"com.lihaoyi::sourcecode:0.4.3-M5" val upickle = ivy"com.lihaoyi::upickle:4.1.0" val windowsAnsi = ivy"io.github.alexarchambault.windows-ansi:windows-ansi:0.0.6" - val zinc = ivy"org.scala-sbt::zinc:1.10.7".withDottyCompat(scalaVersion) + val sbtVersion = "1.10.7" + val zinc = ivy"org.scala-sbt::zinc:$sbtVersion".withDottyCompat(scalaVersion) // keep in sync with doc/antora/antory.yml val bsp4j = ivy"ch.epfl.scala:bsp4j:2.2.0-M2" val fansi = ivy"com.lihaoyi::fansi:0.5.0" @@ -220,6 +224,7 @@ object Deps { ivy"org.apache.maven.resolver:maven-resolver-transport-wagon:$mavenResolverVersion" val coursierJvmIndexVersion = "0.0.4-84-f852c6" val gradleApi = ivy"dev.gradleplugins:gradle-api:8.11.1" + val sbt = ivy"org.scala-sbt:sbt:$sbtVersion" object RuntimeDeps { val dokkaVersion = "2.0.0" @@ -714,16 +719,18 @@ trait MillStableScalaModule extends MillPublishScalaModule with Mima { } } +trait MillPublishCrossScalaModule extends MillPublishJavaModule with CrossScalaModule + object bridge extends Cross[BridgeModule](compilerBridgeScalaVersions) -trait BridgeModule extends MillPublishJavaModule with CrossScalaModule { +trait BridgeModule extends MillPublishCrossScalaModule { def scalaVersion = crossScalaVersion def publishVersion = bridgeVersion def artifactName = "mill-scala-compiler-bridge" def pomSettings = commonPomSettings(artifactName()) def crossFullScalaVersion = true def ivyDeps = Agg( - ivy"org.scala-sbt:compiler-interface:${Deps.zinc.version}", - ivy"org.scala-sbt:util-interface:${Deps.zinc.version}" + ivy"org.scala-sbt:compiler-interface:${Deps.sbtVersion}", + ivy"org.scala-sbt:util-interface:${Deps.sbtVersion}" ) ++ Agg( if (ZincWorkerUtil.isScala3(crossScalaVersion)) ivy"org.scala-lang::scala3-compiler:${crossScalaVersion}" @@ -738,7 +745,7 @@ trait BridgeModule extends MillPublishJavaModule with CrossScalaModule { def compilerBridgeIvyDeps: T[Agg[Dep]] = Agg( (if (ZincWorkerUtil.isScala3(crossScalaVersion)) ivy"org.scala-lang:scala3-sbt-bridge:${crossScalaVersion}" - else ivy"org.scala-sbt::compiler-bridge:${Deps.zinc.version}").exclude("*" -> "*") + else ivy"org.scala-sbt::compiler-bridge:${Deps.sbtVersion}").exclude("*" -> "*") ) def compilerBridgeSourceJars: T[Agg[PathRef]] = Task { diff --git a/dist/package.mill b/dist/package.mill index fbbeacbc843..515ed31025a 100644 --- a/dist/package.mill +++ b/dist/package.mill @@ -75,6 +75,7 @@ object `package` extends RootModule with InstallModule { build.main.graphviz.testDep(), build.main.init.maven.testDep(), build.main.init.gradle.testDep(), + build.main.init.sbt.testDep(), build.scalalib.backgroundwrapper.testDep(), build.contrib.bloop.testDep(), build.contrib.buildinfo.testDep(), diff --git a/example/package.mill b/example/package.mill index 62190fd6c1e..fd3a934c8ff 100644 --- a/example/package.mill +++ b/example/package.mill @@ -64,6 +64,7 @@ object `package` extends RootModule with Module { object web extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "web")) object native extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "native")) object spark extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "spark")) + object migrating extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "migrating")) } object javascriptlib extends Module { object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic")) diff --git a/example/scalalib/migrating/1-sbt-complete/build.mill b/example/scalalib/migrating/1-sbt-complete/build.mill new file mode 100644 index 00000000000..590ce14912b --- /dev/null +++ b/example/scalalib/migrating/1-sbt-complete/build.mill @@ -0,0 +1,23 @@ +/** Usage + +> rm build.mill # remove any existing build file + +> git init . +> git remote add -f origin https://github.com/scalacenter/library-example.git +> git checkout v1.0.1 + +> ./mill init +converting module library-example +Dependency Dependency(com.lightbend.paradox,paradox-theme-generic,false,0.4.4,Some(paradox-theme)) with an unknown configuration "paradox-theme" is dropped. +Dependency Dependency(com.lightbend.paradox,paradox-theme-generic,false,0.4.4,Some(paradox-theme)) with an unknown configuration "paradox-theme" is dropped. +generated 1 Mill build file(s) +removing existing Mill build files +writing Mill build file to build.mill +converted sbt build to Mill +formatting Mill build files +Formatting 1 Scala sources +init completed, run "mill resolve _" to list available tasks + +> ./mill compile +done compiling +*/ diff --git a/example/scalalib/migrating/2-sbt-incomplete/build.mill b/example/scalalib/migrating/2-sbt-incomplete/build.mill new file mode 100644 index 00000000000..69e4a9bec2e --- /dev/null +++ b/example/scalalib/migrating/2-sbt-incomplete/build.mill @@ -0,0 +1,25 @@ +/** Usage + +> 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 + +> ./mill init +converting sbt build +Running the added `millInitExportBuild` sbt task to export the build +converting module scala-csv +generated 1 Mill build file(s) +removing existing Mill build files +writing Mill build file to build.mill +converted sbt build to Mill +formatting Mill build files +Formatting 1 Scala sources +init completed, run "mill resolve _" to list available tasks + +> ./mill compile # You will have to further configure the `CrossScalaModule` for different Scala versions +error: class CSVReader protected (private val lineReader: LineReader)(implicit format: CSVFormat) extends Closeable with CSVReaderCompat { +error: ^ +error: one error found +*/ diff --git a/integration/feature/init/src/IntegrationTesterExt.scala b/integration/feature/init/src/IntegrationTesterExt.scala new file mode 100644 index 00000000000..e0a71cc0bcb --- /dev/null +++ b/integration/feature/init/src/IntegrationTesterExt.scala @@ -0,0 +1,78 @@ +package mill.integration + +import mill.testkit.IntegrationTester +import utest.* + +val defaultInitCommand = + Seq("init", "--base-module", "BaseModule", "--deps-object", "Deps", "--merge") + +case class SplitResolvedTasks(successful: Seq[String], failed: Seq[String]) { + val all = (successful ++ failed).sorted +} + +extension (tester: IntegrationTester) + /** + * @param expectedCompileTasks [[ None ]] to denote that the `resolve __.compile` task fails + * @param expectedTestTasks [[ None ]] to denote that the `resolve __.test` task fails + * @return + */ + def testMillInit( + initCommand: Seq[String] = defaultInitCommand, + expectedInitResult: Boolean = true, + // expectedCompileResult: Boolean, + expectedCompileTasks: Option[SplitResolvedTasks], + expectedTestTasks: Option[SplitResolvedTasks] + ) = { + import tester.* + + val initResult = eval(initCommand, stdout = os.Inherit, stderr = os.Inherit) + assert(initResult.isSuccess == expectedInitResult) + + /* + val compileResult = eval("compile") + assert(compileResult.isSuccess == expectedCompileResult) + */ + + def testAllResolvedTasks(taskName: String, expected: Option[SplitResolvedTasks]) = { + val resolveAllTasksResult = eval(("resolve", s"__.$taskName")) + expected.fold( + assert(!resolveAllTasksResult.isSuccess) + )(expected => { + assert(resolveAllTasksResult.isSuccess) + val resolvedAllTasks = resolveAllTasksResult.out.linesIterator.toSeq.sorted + Predef.assert( + expected.all == resolvedAllTasks, + s""" + |expected: ${expected.all} + |resolved: $resolvedAllTasks + |""".stripMargin + ) + + for (task <- expected.successful) + Predef.assert(eval(task).isSuccess, s"task $task failed") + + for (task <- expected.failed) + Predef.assert(!eval(task).isSuccess, s"task $task succeeded") + }) + } + + testAllResolvedTasks("compile", expectedCompileTasks) + testAllResolvedTasks("test", expectedTestTasks) + } + +extension (module: String) { + def compileTask: String = + s"$module.compile" + private def testModuleOrTask: String = + s"$module.test" + def testTask: String = + testModuleOrTask + def testModule: String = + testModuleOrTask + def testCompileTask: String = + testModule.compileTask + def testTestTask: String = + testModule.testTask + def allCompileTasks: Seq[String] = + Seq(compileTask, testCompileTask) +} diff --git a/integration/feature/init/src/MillInitSbtTests.scala b/integration/feature/init/src/MillInitSbtTests.scala new file mode 100644 index 00000000000..b6ec4fe4cdb --- /dev/null +++ b/integration/feature/init/src/MillInitSbtTests.scala @@ -0,0 +1,332 @@ +package mill.integration + +import mill.constants.Util +import mill.integration.testMillInit +import utest.* + +private def bumpSbtTo1107(workspacePath: os.Path) = + // bump sbt version to resolve compatibility issues with lower sbt versions and higher JDK versions + os.write.over(workspacePath / "project" / "build.properties", "sbt.version = 1.10.7") + +// relatively small libraries + +private val scalaPlatforms = Seq("js", "jvm", "native") + +object MillInitSbtLibraryExampleTests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 21 KB + - sbt 1.5.2 + */ + val url = "https://github.com/scalacenter/library-example/archive/refs/tags/v1.0.1.zip" + + test - integrationTest(url)( + _.testMillInit( + expectedCompileTasks = Some(SplitResolvedTasks(Seq("compile"), Seq())), + expectedTestTasks = None // scalaprops not supported in `TestModule` + ) + ) + } +} + +object MillInitSbtScalaCsv200Tests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 34 KB + - originally sbt 1.10.0 + */ + val url = "https://github.com/tototoshi/scala-csv/archive/refs/tags/2.0.0.zip" + + test - integrationTest(url) { tester => + bumpSbtTo1107(tester.workspacePath) + + // Cross builds are not supported yet. + tester.testMillInit( + expectedCompileTasks = Some(SplitResolvedTasks(Seq(), Seq("compile", "test.compile"))), + expectedTestTasks = Some(SplitResolvedTasks(Seq(), Seq("test"))) + ) + } + } +} + +object MillInitSbtScalaCsv136Tests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 28 KB + - originally sbt 1.2.8 + */ + val url = "https://github.com/tototoshi/scala-csv/archive/refs/tags/1.3.6.zip" + + test - integrationTest(url) { tester => + bumpSbtTo1107(tester.workspacePath) + + tester.testMillInit( + expectedCompileTasks = Some(SplitResolvedTasks( + Seq("compile", "test.compile"), + Seq.empty + )), + expectedTestTasks = Some(SplitResolvedTasks( + Seq(), + /* + Paths relative to the workspace are used in the test sources such as `new File("src/test/resources/simple.csv")` + and they seem to cause the test to fail with Mill: + ```text + java.io.FileNotFoundException: src/test/resources/simple.csv (No such file or directory) + ``` + */ + Seq("test") + )) + ) + } + } +} + +// same as the one in the unit tests +object MillInitSbtMultiProjectExampleTests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 12 KB + - originally sbt 1.0.2 + */ + val url = + "https://github.com/pbassiner/sbt-multi-project-example/archive/152b31df9837115b183576b0080628b43c505389.zip" + + test - integrationTest(url) { tester => + bumpSbtTo1107(tester.workspacePath) + + val submodules = Seq("common", "multi1", "multi2") + if (System.getProperty("java.version").split('.').head.toInt <= 11) + tester.testMillInit( + expectedCompileTasks = Some(SplitResolvedTasks( + Seq("compile") ++ submodules.flatMap(_.allCompileTasks), + Seq.empty + )), + expectedTestTasks = Some(SplitResolvedTasks(submodules.map(_.testTask), Seq.empty)) + ) + else + tester.testMillInit( + // initCommand = defaultInitCommand ++ Seq("--jvm-id", "11"), + expectedCompileTasks = Some({ + /* + `multi1.compile` doesn't work well when Mill is run with JDK 17 and 21: + ```text + 1 tasks failed + multi1.compile java.io.IOError: java.lang.RuntimeException: /packages cannot be represented as URI + java.base/jdk.internal.jrtfs.JrtPath.toUri(JrtPath.java:175) + scala.tools.nsc.classpath.JrtClassPath.asURLs(DirectoryClassPath.scala:183) + ... + ``` + Passing a `jvmId` 11 doesn't work here. + */ + val succeededSubmoduleCompileTasks = Seq("common.compile", "multi2.compile") + SplitResolvedTasks( + Seq("compile") ++ succeededSubmoduleCompileTasks, + (submodules.flatMap(_.allCompileTasks).toSet -- succeededSubmoduleCompileTasks).toSeq + ) + }), + expectedTestTasks = Some(SplitResolvedTasks(Seq.empty, submodules.map(_.testTask))) + ) + } + } +} + +// relatively large libraries + +object MillInitSbtZioHttpTests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 1.4 MB + - originally sbt 1.10.0 + */ + val url = "https://github.com/zio/zio-http/archive/refs/tags/v3.0.1.zip" + + test - integrationTest(url) { tester => + bumpSbtTo1107(tester.workspacePath) + + object submodules { + val withTests = Seq( + "sbt-zio-http-grpc-tests", + "zio-http-cli", + "zio-http-gen", + "zio-http-htmx", + "zio-http-testkit", + "zio-http.js", + "zio-http.jvm" + ) + val withoutTests = Seq( + "sbt-zio-http-grpc", + "zio-http-benchmarks", + "zio-http-docs", + "zio-http-example", + "zio-http-tools" + ) + } + + /* + The sources shared among multiple platforms (JVM and Scala.js) in "zio-http/shared" in cross-builds + are not supported in conversion yet, + causing all dependent project's `compile` tasks to fail. + */ + tester.testMillInit( + expectedCompileTasks = + Some(SplitResolvedTasks( + Seq("compile"), + submodules.withTests.flatMap(_.allCompileTasks) ++ + submodules.withoutTests.map(_.compileTask) + )), + expectedTestTasks = + Some(SplitResolvedTasks(Seq(), submodules.withTests.map(_.testTask))) + ) + } + } +} + +// Scala.js and scala-native projects are not properly imported +object MillInitSbtScalazTests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 0.8 MB + - sbt 1.9.7 + */ + val url = "https://github.com/scalaz/scalaz/archive/refs/tags/v7.3.8.zip" + + test - integrationTest(url) { tester => + import tester.* + + if (!Util.isWindows) + os.call(("chmod", "+x", "sbt"), cwd = workspacePath) + + val crossDirs = Seq("core", "effect", "example", "iteratee", "scalacheck-binding", "tests") + val crossSubmodules = crossDirs.flatMap(dir => scalaPlatforms.map(name => s"$dir.$name")) + val rootModules = Seq("rootJS", "rootJVM", "rootNative") + + tester.testMillInit( + expectedCompileTasks = + Some(SplitResolvedTasks( + /* + These modules are converted from sbt's aggregated projects without sources or dependencies, + therefore, their no-op `compile` tasks succeed. + */ + Seq("compile") ++ rootModules.map(_.compileTask), + /* + Common sources shared among multiple platforms (JVM, Scala.js, and Scala Native) in directories such as "core/src" + are not defined as sbt projects and therefore not converted. + This leads to modules such as "core/jvm" not compiling as common definitions are not found. + */ + crossSubmodules.map(_.compileTask) + )), + // Scalaz uses ScalaCheck which is not supported in conversion yet. + expectedTestTasks = None + ) + } + } +} + +// Scala.js and scala-native projects are not properly imported +object MillInitSbtCatsTests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 1.9 MB + - sbt 1.10.7 + - MUnit + */ + val url = "https://github.com/typelevel/cats/archive/refs/tags/v2.13.0.zip" + + test - integrationTest(url) { tester => + val sbtCrossProjects = Seq( + "algebra-laws", + "alleycats-laws", + "kernel-laws", + "tests" + ) + /* + These sbt cross projects have `CrossType.Pure` set, + so there platform projects have directories named ".js", ".jvm", and ".native", + and such modules starting with "." are not properly recognized by Mill + */ + val sbtCrossProjectsWithCrossTypePure = + Seq("algebra-core", "alleycats-core", "core", "free", "kernel", "laws", "testkit") + + assert((sbtCrossProjects intersect sbtCrossProjectsWithCrossTypePure).isEmpty) + + val nonCrossModules = Seq("bench", "binCompatTest", "site", "unidocs") + val resolvedTestModules = (scalaPlatforms.map(name => s"alleycats-laws.$name") ++ + Seq( + "binCompatTest", + // "tests.jvm" and "tests.native" don't have their own test sources, so their test modules are not converted. + "tests.js" + )) + .map(module => s"$module.test") + val submoduleCompileTasks = (sbtCrossProjects.flatMap(project => + scalaPlatforms.map(platform => s"$project.$platform") + ) ++ + nonCrossModules ++ + resolvedTestModules) + .map(module => s"$module.compile") + + tester.testMillInit( + expectedCompileTasks = Some({ + val succeededSubmoduleCompileTasks = + Seq("tests.js.compile", "tests.jvm.compile", "unidocs.compile") + SplitResolvedTasks( + Seq("compile") ++ succeededSubmoduleCompileTasks, + submoduleCompileTasks diff succeededSubmoduleCompileTasks + ) + }), + expectedTestTasks = Some(SplitResolvedTasks(Seq(), resolvedTestModules)) + ) + } + } +} + +// Converting child projects nested in a parent directory which is not a project is not supported yet. +object MillInitSbtPlayFrameworkTests extends BuildGenTestSuite { + def tests: Tests = Tests { + // Commented out as it causes `java.util.concurrent.TimeoutException: Future timed out after [600000 milliseconds]` in the CI. + /* + /* + - 4.8 MB + - sbt 1.10.5 + */ + val url = "https://github.com/playframework/playframework/archive/refs/tags/3.0.6.zip" + + test - integrationTest(url) { tester => + import tester.* + + val initResult = eval(defaultInitCommand, stdout = os.Inherit, stderr = os.Inherit) + assert(initResult.isSuccess) + + val compileResult = eval("compile") + assert(compileResult.isSuccess) + } + */ + } +} + +// Scala.js and scala-native projects are not properly imported +object MillInitSbtScalaCheckTests extends BuildGenTestSuite { + def tests: Tests = Tests { + /* + - 245 KB + - originally sbt 1.10.1 + */ + val url = "https://github.com/typelevel/scalacheck/archive/refs/tags/v1.18.1.zip" + + test - integrationTest(url) { tester => + bumpSbtTo1107(tester.workspacePath) + + /* + The sources shared among multiple platforms (JVM, Scala.js, and Scala Native) in "core/shared" in cross-builds + are not supported in conversion yet, + causing all dependent project's `compile` tasks to fail. + */ + val submodules = scalaPlatforms.map(platform => s"core.$platform") :+ "bench" + tester.testMillInit( + expectedCompileTasks = + Some(SplitResolvedTasks(Seq("compile"), submodules.map(_.compileTask))), + // ScalaCheck itself as a test framework is not supported in conversion yet. + expectedTestTasks = None + ) + } + } +} diff --git a/main/init/buildgen/src/mill/main/buildgen/BuildGenBase.scala b/main/init/buildgen/src/mill/main/buildgen/BuildGenBase.scala index 3795e7920f7..f365df07f5a 100644 --- a/main/init/buildgen/src/mill/main/buildgen/BuildGenBase.scala +++ b/main/init/buildgen/src/mill/main/buildgen/BuildGenBase.scala @@ -2,68 +2,102 @@ package mill.main.buildgen import mill.main.buildgen.BuildGenUtil.{buildPackages, compactBuildTree, writeBuildObject} -import scala.collection.immutable.SortedMap +import scala.collection.immutable.{SortedMap, SortedSet} -trait BuildGenBase[M, D] { +/* +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.Config, input: Tree[Node[M]]): Unit = { + 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) } + /** + * A possibly optional module model which might be a parent directory of an actual module without its own sources. + */ + type OM + extension (om: OM) def toOption(): Option[M] + + def getModuleTree(input: I): Tree[Node[OM]] + def convert( - input: Tree[Node[M]], + input: I, cfg: C, - shared: BuildGenUtil.Config + shared: BuildGenUtil.BasicConfig ): Tree[Node[BuildObject]] = { - // for resolving moduleDeps + val moduleTree = getModuleTree(input) + val moduleOptionTree = moduleTree.map(node => node.copy(value = node.value.toOption())) - val packages = buildPackages(input)(getPackage) + // for resolving moduleDeps + val packages = buildPackages( + moduleOptionTree.nodes().flatMap(node => node.value.map(m => node.copy(value = m))) + )(getPackage) val baseInfo = shared.baseModule.fold(IrBaseInfo()) { getBaseInfo(input, cfg, _, packages.size) } - input.map { build => - val name = getArtifactId(build.value) - println(s"converting module $name") - - val inner = extractIrBuild(cfg, baseInfo, build, packages) - - val isNested = build.dirs.nonEmpty - build.copy(value = - BuildObject( - imports = BuildGenUtil.renderImports(shared.baseModule, isNested, packages.size), - companions = - shared.depsObject.fold(SortedMap.empty[String, BuildObject.Constants])(name => - SortedMap((name, SortedMap(inner.scopedDeps.namedIvyDeps.toSeq*))) - ), - supertypes = getSuperTypes(cfg, baseInfo, build), - inner = BuildGenUtil.renderIrBuild(inner), - outer = - if (isNested || baseInfo.moduleTypedef == null) "" - else BuildGenUtil.renderIrTrait(baseInfo.moduleTypedef) - ) + moduleOptionTree.map(optionalBuild => + optionalBuild.copy(value = + optionalBuild.value.fold( + BuildObject(SortedSet("mill._"), SortedMap.empty, Seq("RootModule", "Module"), "", "") + )(moduleModel => { + val name = getArtifactId(moduleModel) + println(s"converting module $name") + + val build = optionalBuild.copy(value = moduleModel) + val inner = extractIrBuild(cfg, build, packages) + + val isNested = optionalBuild.dirs.nonEmpty + BuildObject( + imports = + BuildGenUtil.renderImports(shared.baseModule, isNested, packages.size, extraImports), + companions = + shared.depsObject.fold(SortedMap.empty[String, BuildObject.Constants])(name => + SortedMap((name, SortedMap(inner.scopedDeps.namedIvyDeps.toSeq*))) + ), + supertypes = getSupertypes(cfg, baseInfo, build), + inner = BuildGenUtil.renderIrBuild(inner, baseInfo), + outer = + if (isNested || baseInfo.moduleTypedef == null) "" + else BuildGenUtil.renderIrTrait(baseInfo.moduleTypedef) + ) + }) ) - } + ) } - def getSuperTypes(cfg: C, baseInfo: IrBaseInfo, build: Node[M]): Seq[String] + def extraImports: Seq[String] + + def getSupertypes(cfg: C, baseInfo: IrBaseInfo, build: Node[M]): Seq[String] def getBaseInfo( - input: Tree[Node[M]], + input: I, cfg: C, baseModule: String, packagesSize: Int ): IrBaseInfo - def getPackage(model: M): (String, String, String) + def getPackage(moduleModel: M): (String, String, String) - def getArtifactId(model: M): String + def getArtifactId(moduleModel: M): String def extractIrBuild( cfg: C, - baseInfo: IrBaseInfo, + // baseInfo: IrBaseInfo, // `baseInfo` is no longer needed as we compare the `IrBuild` with `IrBaseInfo`/`IrTrait` in common code now. build: Node[M], packages: Map[(String, String, String), String] ): IrBuild } + +object BuildGenBase { + trait MavenAndGradle[M, D] extends BuildGenBase[M, D, Tree[Node[M]]] { + override def getModuleTree(input: Tree[Node[M]]): Tree[Node[M]] = input + override type OM = M + + override def extraImports: Seq[String] = Seq.empty + } +} diff --git a/main/init/buildgen/src/mill/main/buildgen/BuildGenUtil.scala b/main/init/buildgen/src/mill/main/buildgen/BuildGenUtil.scala index 9d5a13b11f2..7fe9490ded2 100644 --- a/main/init/buildgen/src/mill/main/buildgen/BuildGenUtil.scala +++ b/main/init/buildgen/src/mill/main/buildgen/BuildGenUtil.scala @@ -1,15 +1,17 @@ package mill.main.buildgen +import geny.Generator import mainargs.{Flag, arg} -import mill.constants.OutFiles -import mill.main.buildgen.BuildObject.Companions import mill.constants.CodeGenConstants.{ buildFileExtensions, nestedBuildFileNames, rootBuildFileNames, rootModuleAlias } +import mill.constants.OutFiles +import mill.main.buildgen.BuildObject.Companions import mill.runner.FileImportGraph.backtickWrap +import mill.scalalib.CrossVersion import scala.collection.immutable.SortedSet import scala.util.boundary @@ -33,6 +35,10 @@ object BuildGenUtil { | |${renderJavacOptions(javacOptions)} | + |${renderScalaVersion(scalaVersion)} + | + |${renderScalacOptions(scalacOptions)} + | |${renderPomSettings(renderIrPom(pomSettings))} | |${renderPublishVersion(publishVersion)} @@ -46,7 +52,7 @@ object BuildGenUtil { } - def renderIrPom(value: IrPom): String = { + def renderIrPom(value: IrPom | Null): String = { if (value == null) "" else { import value.* @@ -56,13 +62,17 @@ object BuildGenUtil { } } - def renderIrBuild(value: IrBuild): String = { - import value.* + /** + * @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, scopedDeps.testModule) + BuildGenUtil.renderTestModuleDecl(testModule, testModuleMainType, scopedDeps.testModule) s"""$declare { | @@ -82,9 +92,22 @@ object BuildGenUtil { s"""${renderArtifactName(projectName, dirs)} | - |${renderJavacOptions(javacOptions)} + |${renderJavacOptions( + javacOptions, + if (baseTrait != null) baseTrait.javacOptions else Seq.empty + )} | - |${renderRepositories(repositories)} + |${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 + )} | |${renderBomIvyDeps(scopedDeps.mainBomIvyDeps)} | @@ -100,9 +123,16 @@ object BuildGenUtil { | |${renderRunModuleDeps(scopedDeps.mainRunModuleDeps)} | - |${if (pomSettings == null) "" else renderPomSettings(renderIrPom(pomSettings))} + |${ + if (pomSettings != (if (baseTrait != null) baseTrait.pomSettings else null)) + renderPomSettings(renderIrPom(pomSettings)) + else "" + } | - |${renderPublishVersion(publishVersion)} + |${renderPublishVersion( + publishVersion, + if (baseTrait != null) baseTrait.publishVersion else null + )} | |${renderPomPackaging(packaging)} | @@ -128,9 +158,15 @@ object BuildGenUtil { def renderImports( baseModule: Option[String], isNested: Boolean, - packagesSize: Int + packagesSize: Int, + extraImports: Seq[String] ): SortedSet[String] = { - scala.collection.immutable.SortedSet("mill._", "mill.javalib._", "mill.javalib.publish._") ++ + scala.collection.immutable.SortedSet( + "mill._", + "mill.javalib._", + "mill.javalib.publish._" + ) ++ + extraImports ++ (if (isNested) baseModule.map(name => s"_root_.build_.$name") else if (packagesSize > 1) Seq("$packages._") else None) @@ -143,9 +179,9 @@ object BuildGenUtil { def buildPackage(dirs: Seq[String]): String = (rootModuleAlias +: dirs).iterator.map(backtickWrap).mkString(".") - def buildPackages[Module, Key](input: Tree[Node[Module]])(key: Module => Key) + def buildPackages[Module, Key](input: Generator[Node[Module]])(key: Module => Key) : Map[Key, String] = - input.nodes() + input .map(node => (key(node.value), buildPackage(node.dirs))) .toSeq .toMap @@ -252,11 +288,20 @@ object BuildGenUtil { def renderIvyString( group: String, artifact: String, - version: String = null, - tpe: String = null, - classifier: String = null, + 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( @@ -279,13 +324,13 @@ object BuildGenUtil { .map { case (group, artifact) => s";exclude=$group:$artifact" } .mkString - s"ivy\"$group:$artifact$sepVersion$sepTpe$sepClassifier$sepExcludes\"" + s"ivy\"$group$sepArtifact$sepVersion$sepTpe$sepClassifier$sepExcludes\"" } def isBom(groupArtifactVersion: (String, String, String)): Boolean = groupArtifactVersion._2.endsWith("-bom") - def isNullOrEmpty(value: String): Boolean = + def isNullOrEmpty(value: String | Null): Boolean = null == value || value.isEmpty val linebreak: String = @@ -329,6 +374,7 @@ object BuildGenUtil { | def jvmId = "$jvmId" |}""".stripMargin + // TODO consider renaming to `renderOptionalDef` or `renderIfArgsNonEmpty`? def optional(construct: String, args: IterableOnce[String]): String = optional(construct + "(", args, ",", ")") @@ -338,6 +384,44 @@ object BuildGenUtil { else itr.mkString(start, sep, end) } + def renderStringSeqWithSuper( + defName: String, + args: Seq[String], + superArgs: Seq[String] = Seq.empty, + 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(s"super.$defName() ++ Seq(", ",", ")")) + } else + Some( + if (args.isEmpty) "Seq.empty[String]" // The inferred type is `Seq[Nothing]` otherwise. + else args.iterator.map(transform).mkString("Seq(", ",", ")") + ) + + def renderStringSeqTargetDefWithSuper( + defName: String, + args: Seq[String], + superArgs: Seq[String] = Seq.empty, + transform: String => String + ) = + renderStringSeqWithSuper(defName, args, superArgs, transform).map(s"def $defName = " + _) + + def renderStringSeqTaskDefWithSuper( + defName: String, + args: Seq[String], + superArgs: Seq[String] = Seq.empty, + transform: String => String + ) = + renderStringSeqWithSuper(defName, args, superArgs, transform).map(s => + s"def $defName = Task.Anon { $s }" + ) + def scalafmtConfigFile: os.Path = os.temp( """version = "3.8.4" @@ -376,19 +460,26 @@ object BuildGenUtil { def renderRunModuleDeps(args: IterableOnce[String]): String = optional("def runModuleDeps = super.runModuleDeps ++ Seq", args) - def renderJavacOptions(args: IterableOnce[String]): String = - optional( - "def javacOptions = super.javacOptions() ++ Seq", - args.iterator.map(escape) - ) + def renderJavacOptions(args: Seq[String], superArgs: Seq[String] = Seq.empty): String = + renderStringSeqTargetDefWithSuper("javacOptions", args, superArgs, escape).getOrElse("") - def renderRepositories(args: IterableOnce[String]): String = - optional( - "def repositoriesTask = Task.Anon { super.repositoriesTask() ++ Seq(", - args, - ", ", - ") }" - ) + 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 = + renderStringSeqTargetDefWithSuper( + "scalacOptions", + args.getOrElse(Seq.empty), + superArgs.getOrElse(Seq.empty), + escape + ).getOrElse("") + + def renderRepositories(args: Seq[String], superArgs: Seq[String] = Seq.empty): String = + renderStringSeqTaskDefWithSuper("repositoriesTask", args, superArgs, identity).getOrElse("") def renderResources(args: IterableOnce[os.SubPath]): String = optional( @@ -409,15 +500,20 @@ object BuildGenUtil { if (isNullOrEmpty(artifact)) "" else s"def pomParentProject = Some($artifact)" - def renderPomSettings(arg: String): String = + def renderPomSettings(arg: String | Null, superArg: String | Null = null): String = if (isNullOrEmpty(arg)) "" else s"def pomSettings = $arg" - def renderPublishVersion(arg: String): String = - if (isNullOrEmpty(arg)) "" - else s"def publishVersion = ${escape(arg)}" + 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: IterableOnce[(String, String)]): String = { + def renderPublishProperties( + args: Seq[(String, String)], + superArgs: Seq[(String, String)] = Seq.empty + ): String = { val tuples = args.iterator.map { case (k, v) => s"(${escape(k)}, ${escape(v)})" } optional("def publishProperties = super.publishProperties() ++ Map", tuples) } @@ -428,7 +524,13 @@ object BuildGenUtil { val testModulesByGroup: Map[String, String] = Map( "junit" -> "TestModule.Junit4", "org.junit.jupiter" -> "TestModule.Junit5", - "org.testng" -> "TestModule.TestNg" + "org.testng" -> "TestModule.TestNg", + "org.scalatest" -> "TestModule.ScalaTest", + "org.specs2" -> "TestModule.Specs2", + "com.lihaoyi.utest" -> "TestModule.UTest", + "org.scalameta" -> "TestModule.Munit", + "com.disneystreaming" -> "Weaver", + "dev.zio" -> "TestModule.ZioTest" ) def writeBuildObject(tree: Tree[Node[BuildObject]]): Unit = { @@ -447,16 +549,20 @@ object BuildGenUtil { } } - def renderTestModuleDecl(testModule: String, testModuleType: Option[String]): String = { + def renderTestModuleDecl( + testModule: String, + testModuleMainType: String, + testModuleExtraType: Option[String] + ): String = { val name = backtickWrap(testModule) - testModuleType match { - case Some(supertype) => s"object $name extends MavenTests with $supertype" - case None => s"trait $name extends MavenTests" + testModuleExtraType match { + case Some(supertype) => s"object $name extends $testModuleMainType with $supertype" + case None => s"trait $name extends $testModuleMainType" } } @mainargs.main - case class Config( + case class BasicConfig( @arg(doc = "name of generated base module trait defining shared settings", short = 'b') baseModule: Option[String] = None, @arg( @@ -469,7 +575,15 @@ object BuildGenUtil { @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(), + 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() ) diff --git a/main/init/buildgen/src/mill/main/buildgen/OptionNodeTree.scala b/main/init/buildgen/src/mill/main/buildgen/OptionNodeTree.scala new file mode 100644 index 00000000000..c136d5b3bfb --- /dev/null +++ b/main/init/buildgen/src/mill/main/buildgen/OptionNodeTree.scala @@ -0,0 +1,33 @@ +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/main/init/buildgen/src/mill/main/buildgen/ir.scala b/main/init/buildgen/src/mill/main/buildgen/ir.scala index e43d81e0d29..bb9fa119297 100644 --- a/main/init/buildgen/src/mill/main/buildgen/ir.scala +++ b/main/init/buildgen/src/mill/main/buildgen/ir.scala @@ -41,8 +41,10 @@ case class IrTrait( baseModule: String, moduleSupertypes: Seq[String], javacOptions: Seq[String], - pomSettings: IrPom, - publishVersion: String, + scalaVersion: Option[String], + scalacOptions: Option[Seq[String]], + pomSettings: IrPom | Null, + publishVersion: String | Null, publishProperties: Seq[(String, String)], repositories: Seq[String] ) @@ -75,24 +77,53 @@ case class IrLicense( 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, - publishVersion: String, - packaging: String, - pomParentArtifact: IrArtifact, + pomSettings: IrPom | Null, + publishVersion: String | Null, + packaging: String | Null, + pomParentArtifact: IrArtifact | Null, resources: Seq[os.SubPath], testResources: Seq[os.SubPath], publishProperties: Seq[(String, 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 + ) +} + case class IrScopedDeps( + // TODO The type is `Seq` and this is deduplicated and sorted in `BuildGenUtil`. Make the type `SortedMap` here for consistency? namedIvyDeps: Seq[(String, String)] = Nil, mainBomIvyDeps: SortedSet[String] = SortedSet(), mainIvyDeps: SortedSet[String] = SortedSet(), @@ -109,11 +140,25 @@ case class IrScopedDeps( 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, - moduleTypedef: IrTrait = null + */ + // 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/main/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala b/main/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala index 7dcc2d7cbb3..123a2dcaacf 100644 --- a/main/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala +++ b/main/init/gradle/src/mill/main/gradle/GradleBuildGenMain.scala @@ -21,6 +21,7 @@ import scala.jdk.CollectionConverters.* * ===Capabilities=== * The conversion * - handles deeply nested modules + * - captures publish settings * - configures dependencies for configurations: * - implementation / api * - compileOnly / compileOnlyApi @@ -33,14 +34,14 @@ import scala.jdk.CollectionConverters.* * - TestNG * * ===Limitations=== - * The conversion does not support + * The conversion does not support: * - custom dependency configurations * - custom tasks * - non-Java sources */ @mill.api.internal -object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { - type C = GradleBuildGenMain.Config +object GradleBuildGenMain extends BuildGenBase.MavenAndGradle[ProjectModel, JavaModel.Dep] { + override type C = Config def main(args: Array[String]): Unit = { val cfg = ParserForClass[Config].constructOrExit(args.toSeq) @@ -54,7 +55,7 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { val connector = GradleConnector.newConnector() val args = - cfg.shared.jvmId.map { id => + 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" @@ -75,7 +76,7 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { (Node(dirs, project), children) } - convertWriteOut(cfg, cfg.shared, input) + convertWriteOut(cfg, cfg.shared.basicConfig, input) println("converted Gradle build to Mill") } finally connection.close() @@ -102,6 +103,11 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { file } + extension (om: ProjectModel) + override def toOption(): Option[ProjectModel] = + // TODO consider filtering out projects without the `java` plugin applied + Some(om) + override def getBaseInfo( input: Tree[Node[ProjectModel]], cfg: Config, @@ -124,28 +130,32 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { 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.jvmId, + cfg.shared.basicConfig.jvmId, baseModule, supertypes, javacOptions, + scalaVersion, + scalacOptions, pomSettings, publishVersion, publishProperties, repos ) - IrBaseInfo(javacOptions, repos, pomSettings == null, publishVersion, Seq.empty, typedef) + IrBaseInfo(typedef) } override def extractIrBuild( cfg: Config, - baseInfo: IrBaseInfo, + // baseInfo: IrBaseInfo, build: Node[ProjectModel], packages: Map[(String, String, String), String] ): IrBuild = { @@ -154,14 +164,17 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { val version = getPublishVersion(project) IrBuild( scopedDeps = scopedDeps, - testModule = cfg.shared.testModule, + testModule = cfg.shared.basicConfig.testModule, + testModuleMainType = "MavenTests", hasTest = os.exists(getMillSourcePath(project) / "src/test"), dirs = build.dirs, - repositories = getRepositories(project).diff(baseInfo.repositories), - javacOptions = getJavacOptions(project).diff(baseInfo.javacOptions), + repositories = getRepositories(project), + javacOptions = getJavacOptions(project), + scalaVersion = None, + scalacOptions = None, projectName = getArtifactId(project), - pomSettings = if (baseInfo.noPom) extractPomSettings(project) else null, - publishVersion = if (version == baseInfo.publishVersion) null else version, + pomSettings = extractPomSettings(project), + publishVersion = version, packaging = getPomPackaging(project), // not available pomParentArtifact = null, @@ -173,27 +186,29 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { } def getModuleSupertypes(cfg: Config): Seq[String] = - Seq(cfg.shared.baseModule.getOrElse("MavenModule")) + Seq(cfg.shared.basicConfig.baseModule.getOrElse("MavenModule")) override def getPackage(project: ProjectModel): (String, String, String) = { (project.group(), project.name(), project.version()) } - override def getArtifactId(model: ProjectModel): String = model.name() + override def getArtifactId(project: ProjectModel): String = project.name() - def getMillSourcePath(model: ProjectModel): Path = os.Path(model.directory()) + def getMillSourcePath(project: ProjectModel): Path = os.Path(project.directory()) - override def getSuperTypes( + override def getSupertypes( cfg: Config, baseInfo: IrBaseInfo, build: Node[ProjectModel] - ): Seq[String] = { + ): Seq[String] = Seq("RootModule") ++ - Option.when(null != build.value.maven().pom() && baseInfo.noPom) { "PublishModule" } ++ + Option.when(null != build.value.maven().pom() && { + val baseTrait = baseInfo.moduleTypedef + baseTrait == null || !baseTrait.moduleSupertypes.contains("PublishModule") + }) { "PublishModule" } ++ Option.when(build.dirs.nonEmpty || os.exists(getMillSourcePath(build.value) / "src")) { getModuleSupertypes(cfg) }.toSeq.flatten - } def groupArtifactVersion(dep: JavaModel.Dep): (String, String, String) = (dep.group(), dep.name(), dep.version()) @@ -223,17 +238,17 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { .toSeq } else Seq.empty - def getPublishVersion(project: ProjectModel): String = + def getPublishVersion(project: ProjectModel): String | Null = project.version() match { case "" | "unspecified" => null case version => version } def interpIvy(dep: JavaModel.Dep): String = { - BuildGenUtil.renderIvyString(dep.group(), dep.name(), dep.version()) + BuildGenUtil.renderIvyString(dep.group(), dep.name(), version = dep.version()) } - def extractPomSettings(project: ProjectModel): IrPom = { + def extractPomSettings(project: ProjectModel): IrPom | Null = { val pom = project.maven.pom() if (null == pom) null else { @@ -254,6 +269,7 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { } } + // TODO consider renaming to `extractConfigurationDeps` as Gradle calls them configurations instead of scopes def extractScopedDeps( project: ProjectModel, packages: PartialFunction[(String, String, String), String], @@ -264,7 +280,7 @@ object GradleBuildGenMain extends BuildGenBase[ProjectModel, JavaModel.Dep] { val _java = project._java() if (null != _java) { val ivyDep: JavaModel.Dep => String = - cfg.shared.depsObject.fold(interpIvy(_)) { objName => dep => + cfg.shared.basicConfig.depsObject.fold(interpIvy(_)) { objName => dep => val depName = s"`${dep.group()}:${dep.name()}`" sd = sd.copy(namedIvyDeps = sd.namedIvyDeps :+ (depName, interpIvy(dep))) s"$objName.$depName" diff --git a/main/init/maven/src/mill/main/maven/MavenBuildGenMain.scala b/main/init/maven/src/mill/main/maven/MavenBuildGenMain.scala index 9a77e9c8680..fcdab872d4c 100644 --- a/main/init/maven/src/mill/main/maven/MavenBuildGenMain.scala +++ b/main/init/maven/src/mill/main/maven/MavenBuildGenMain.scala @@ -36,8 +36,8 @@ import scala.jdk.CollectionConverters.* * - build profiles */ @mill.api.internal -object MavenBuildGenMain extends BuildGenBase[Model, Dependency] { - type C = MavenBuildGenMain.Config +object MavenBuildGenMain extends BuildGenBase.MavenAndGradle[Model, Dependency] { + override type C = Config def main(args: Array[String]): Unit = { val cfg = ParserForClass[Config].constructOrExit(args.toSeq) @@ -54,12 +54,14 @@ object MavenBuildGenMain extends BuildGenBase[Model, Dependency] { (Node(dirs, model), model.getModules.iterator().asScala.map(dirs :+ _)) } - convertWriteOut(cfg, cfg.shared, input) + convertWriteOut(cfg, cfg.shared.basicConfig, input) println("converted Maven build to Mill") } - def getBaseInfo( + extension (om: Model) override def toOption(): Option[Model] = Some(om) + + override def getBaseInfo( input: Tree[Node[Model]], cfg: Config, baseModule: String, @@ -67,28 +69,32 @@ object MavenBuildGenMain extends BuildGenBase[Model, Dependency] { ): IrBaseInfo = { val model = input.node.value val javacOptions = Plugins.MavenCompilerPlugin.javacOptions(model) - val repositores = getRepositories(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.jvmId, + cfg.shared.basicConfig.jvmId, baseModule, getModuleSupertypes(cfg), javacOptions, + scalaVersion, + scalacOptions, pomSettings, publishVersion, publishProperties, - getRepositories(model) + repositories ) - IrBaseInfo(javacOptions, repositores, noPom = false, publishVersion, publishProperties, typedef) + IrBaseInfo(typedef) } override def extractIrBuild( cfg: Config, - baseInfo: IrBaseInfo, + // baseInfo: IrBaseInfo, build: Node[Model], packages: Map[(String, String, String), String] ): IrBuild = { @@ -97,14 +103,17 @@ object MavenBuildGenMain extends BuildGenBase[Model, Dependency] { val version = model.getVersion IrBuild( scopedDeps = scopedDeps, - testModule = cfg.shared.testModule, + 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).diff(baseInfo.javacOptions), + javacOptions = Plugins.MavenCompilerPlugin.javacOptions(model), + scalaVersion = None, + scalacOptions = None, projectName = getArtifactId(model), - pomSettings = if (baseInfo.noPom) extractPomSettings(model) else null, - publishVersion = if (version == baseInfo.publishVersion) null else version, + pomSettings = extractPomSettings(model), + publishVersion = version, packaging = model.getPackaging, pomParentArtifact = mkPomParent(model.getParent), resources = @@ -113,24 +122,23 @@ object MavenBuildGenMain extends BuildGenBase[Model, Dependency] { testResources = processResources(model.getBuild.getTestResources, getMillSourcePath(model)) .filterNot(_ == mavenTestResourceDir), - publishProperties = getPublishProperties(model, cfg.shared).diff(baseInfo.publishProperties) + publishProperties = getPublishProperties(model, cfg.shared) ) } def getModuleSupertypes(cfg: Config): Seq[String] = Seq("PublishModule", "MavenModule") - def getPackage(model: Model): (String, String, String) = { + override def getPackage(model: Model): (String, String, String) = { (model.getGroupId, model.getArtifactId, model.getVersion) } - def getArtifactId(model: Model): String = model.getArtifactId + override def getArtifactId(model: Model): String = model.getArtifactId def getMillSourcePath(model: Model): Path = os.Path(model.getProjectDirectory) - def getSuperTypes(cfg: Config, baseInfo: IrBaseInfo, build: Node[Model]): Seq[String] = { + override def getSupertypes(cfg: Config, baseInfo: IrBaseInfo, build: Node[Model]): Seq[String] = Seq("RootModule") ++ - cfg.shared.baseModule.fold(getModuleSupertypes(cfg))(Seq(_)) - } + cfg.shared.basicConfig.baseModule.fold(getModuleSupertypes(cfg))(Seq(_)) def processResources( input: java.util.List[org.apache.maven.model.Resource], @@ -163,6 +171,7 @@ object MavenBuildGenMain extends BuildGenBase[Model, Dependency] { BuildGenUtil.renderIvyString( dep.getGroupId, dep.getArtifactId, + None, dep.getVersion, dep.getType, dep.getClassifier, @@ -205,7 +214,7 @@ object MavenBuildGenMain extends BuildGenBase[Model, Dependency] { val hasTest = os.exists(os.Path(model.getProjectDirectory) / "src/test") val ivyDep: Dependency => String = { - cfg.shared.depsObject.fold(interpIvy(_)) { objName => dep => + cfg.shared.basicConfig.depsObject.fold(interpIvy(_)) { objName => dep => { val depName = s"`${dep.getGroupId}:${dep.getArtifactId}`" sd = sd.copy(namedIvyDeps = sd.namedIvyDeps :+ (depName, interpIvy(dep))) diff --git a/main/init/maven/test/resources/expected/config/build.mill b/main/init/maven/test/resources/expected/config/build.mill index 8736bb43f98..d838312c92e 100644 --- a/main/init/maven/test/resources/expected/config/build.mill +++ b/main/init/maven/test/resources/expected/config/build.mill @@ -28,6 +28,20 @@ object `package` extends RootModule with MyModule { def javacOptions = super.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 pomPackagingType = PackagingType.Pom def publishProperties = super.publishProperties() ++ Map( @@ -40,6 +54,20 @@ object `package` extends RootModule with MyModule { def javacOptions = super.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 pomParentProject = Some(Artifact( "com.example.maven-samples", "multi-module-parent", @@ -73,6 +101,20 @@ object `package` extends RootModule with MyModule { def compileIvyDeps = super.compileIvyDeps() ++ Seq(Deps.`javax.servlet.jsp:jsp-api`, Deps.`javax.servlet:servlet-api`) + 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 pomPackagingType = "war" def pomParentProject = Some(Artifact( @@ -99,6 +141,20 @@ object `package` extends RootModule with MyModule { def ivyDeps = super.ivyDeps() ++ Seq(Deps.`javax.servlet.jsp:jsp-api`, Deps.`javax.servlet:servlet-api`) + 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 publishProperties = super.publishProperties() ++ Map( ("project.build.sourceEncoding", "utf-8"), ("project.reporting.outputEncoding", "utf-8") diff --git a/main/init/package.mill b/main/init/package.mill index e99417eaee4..c895eda7b10 100644 --- a/main/init/package.mill +++ b/main/init/package.mill @@ -1,7 +1,6 @@ package build.main.init import mill._ -import scala.util.matching.Regex object `package` extends RootModule with build.MillPublishScalaModule { @@ -63,8 +62,16 @@ object `package` extends RootModule with build.MillPublishScalaModule { } object buildgen extends build.MillPublishScalaModule { - def moduleDeps = Seq(build.runner) + def moduleDeps = Seq(build.runner /*, tree(build.Deps.scalaVersion)*/ ) def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib) + + // I tried moving `Tree` into this module, but it doesn't compile with Scala 2.12.20. + /* + object tree extends Cross[TreeModule](build.Deps.sbtScalaVersion212, build.Deps.scalaVersion) + trait TreeModule extends build.MillPublishCrossScalaModule { + def ivyDeps = Agg(build.Deps.upickle) + } + */ } object gradle extends build.MillPublishScalaModule { def moduleDeps = Seq(buildgen) @@ -86,4 +93,58 @@ object `package` extends RootModule with build.MillPublishScalaModule { ) def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib, buildgen.test) } + + object sbt extends build.MillPublishScalaModule { + def moduleDeps = Seq(buildgen, models(build.Deps.scalaVersion)) + + val sbtPluginProjectPath = millModuleBasePath.value / "sbt-mill-init-export-build" + + def sbtPluginProjectSource = Task.Source { sbtPluginProjectPath } + + // generateSbtPluginJar + def sbtPluginJarResources = Task { + models(build.Deps.sbtScalaVersion212).publishLocal()() + + sbtPluginProjectSource() + + val isWindows = System.getProperty("os.name").toLowerCase.startsWith("windows") + val version = build.millVersion() + + try { + os.call( + // The version is passed to the sbt build so it correctly resolves the "models" dependency version and sets its own version. + if (isWindows) ("sbt.bat", s"""set version := \\"$version\\"""", "assembly") + else ("sbt", s"""set version := "$version"""", "assembly"), + cwd = sbtPluginProjectPath, + stdout = os.Inherit + ) + } catch { + case e: os.SubprocessException => + throw new RuntimeException( + "Failed to run `sbt assembly` in the \"sbt-mill-init-export-build\" sbt project. " + + "Check if the project builds correctly and if you have sbt available on your system, and install it if you don't.", + e + ) + case t: Throwable => throw t + } + + os.copy( + sbtPluginProjectPath / "target" / "scala-2.12" / "sbt-1.0" / s"sbt-mill-init-export-build-assembly-$version.jar", + Task.dest / "sbt-mill-init-export-build-assembly.jar" + ) + PathRef(Task.dest) + } + def resources: T[Seq[PathRef]] = + Task.Sources(super.resources() ++ Seq(sbtPluginJarResources())) + def testModuleDeps = super.testModuleDeps ++ Seq(build.scalalib, buildgen.test) + + // 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](build.Deps.sbtScalaVersion212, build.Deps.scalaVersion) + trait ModelsModule extends build.MillPublishCrossScalaModule { + // def moduleDeps = Seq(buildgen.tree()) + def ivyDeps = Agg(build.Deps.upickle) + def compileIvyDeps = Agg(build.Deps.sbt) // for definition references only + } + } } diff --git a/main/init/sbt/models/src/mill/main/sbt/Models.scala b/main/init/sbt/models/src/mill/main/sbt/Models.scala new file mode 100644 index 00000000000..b813dd88fc7 --- /dev/null +++ b/main/init/sbt/models/src/mill/main/sbt/Models.scala @@ -0,0 +1,171 @@ +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` + name: String, // `artifactId` in Maven + // version: String, // `groupId` in Maven, moved inside `buildInfo` + // dirs: ProjectDirs, // relative + projectDirectory: String, + buildInfo: BuildInfo, + allDependencies: Seq[Dependency] +) +object Project { + implicit val rw: RW[Project] = macroRW +} + +case class Dependency( + 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)] +) +object Dependency { + implicit val rw: RW[Dependency] = 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/main/init/sbt/sbt-mill-init-export-build/.gitignore b/main/init/sbt/sbt-mill-init-export-build/.gitignore new file mode 100644 index 00000000000..f931b91f2ed --- /dev/null +++ b/main/init/sbt/sbt-mill-init-export-build/.gitignore @@ -0,0 +1,11 @@ +*.class +*.log + +target +project/project +project/target +.cache +.classpath +.project +.settings +bin diff --git a/main/init/sbt/sbt-mill-init-export-build/README.md b/main/init/sbt/sbt-mill-init-export-build/README.md new file mode 100644 index 00000000000..344d9991bac --- /dev/null +++ b/main/init/sbt/sbt-mill-init-export-build/README.md @@ -0,0 +1,10 @@ +# sbt-mill-init-generate-project-tree + +An sbt plugin to generate the project tree for `mill init` + +## Working on this project + +Make sure you are on the SNAPSHOT version of Mill, +and run `./mill "main.init.sbt.models[2.12.20].publishLocal"` +to publish the models to Ivy's local repository before working on this project. +Then open this directory as a separate IntelliJ IDEA project as this is an sbt project. diff --git a/main/init/sbt/sbt-mill-init-export-build/build.sbt b/main/init/sbt/sbt-mill-init-export-build/build.sbt new file mode 100644 index 00000000000..9cbfecbaca8 --- /dev/null +++ b/main/init/sbt/sbt-mill-init-export-build/build.sbt @@ -0,0 +1,9 @@ +name := """sbt-mill-init-export-build""" +organization := "com.lihaoyi" +version := "SNAPSHOT" + +sbtPlugin := true + +console / initialCommands := """import mill.main.sbt._""" + +libraryDependencies += "com.lihaoyi" %% "mill-main-init-sbt-models" % version.value diff --git a/main/init/sbt/sbt-mill-init-export-build/project/build.properties b/main/init/sbt/sbt-mill-init-export-build/project/build.properties new file mode 100644 index 00000000000..73df629ac1a --- /dev/null +++ b/main/init/sbt/sbt-mill-init-export-build/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.7 diff --git a/main/init/sbt/sbt-mill-init-export-build/project/plugins.sbt b/main/init/sbt/sbt-mill-init-export-build/project/plugins.sbt new file mode 100644 index 00000000000..f8ea5d0fbb0 --- /dev/null +++ b/main/init/sbt/sbt-mill-init-export-build/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1") diff --git a/main/init/sbt/sbt-mill-init-export-build/src/main/scala/mill/main/sbt/ExportBuildPlugin.scala b/main/init/sbt/sbt-mill-init-export-build/src/main/scala/mill/main/sbt/ExportBuildPlugin.scala new file mode 100644 index 00000000000..fdc4b56ec31 --- /dev/null +++ b/main/init/sbt/sbt-mill-init-export-build/src/main/scala/mill/main/sbt/ExportBuildPlugin.scala @@ -0,0 +1,150 @@ +package mill.main.sbt + +import sbt.Keys.* +import sbt.io.IO +import sbt.librarymanagement.{Constant, Disabled, For2_13Use3, For3Use2_13} +import sbt.{Def, CrossVersion as _, Developer as _, Project as _, Resolver as _, ScmInfo as _, *} +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" + ) + // not used anymore + val millInitAllDependencies = + taskKey[Seq[Dependency]]("get the all the `mill.main.sbt.Dependency`s of 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, + /*{ + // 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, + + /** See the TODO in [[sbt.Defaults]] above `allDependencies :=` for more details (v1.10.7, Lines 3210 - 3212). */ + /*allDependencies.value*/ (projectDependencies.value ++ libraryDependencies.value).flatMap( + moduleID => { + val dependency = Dependency( + 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/main/init/sbt/src/mill/main/sbt/SbtBuildGenMain.scala b/main/init/sbt/src/mill/main/sbt/SbtBuildGenMain.scala new file mode 100644 index 00000000000..409b4795410 --- /dev/null +++ b/main/init/sbt/src/mill/main/sbt/SbtBuildGenMain.scala @@ -0,0 +1,481 @@ +package mill.main.sbt + +import mainargs.{ParserForClass, arg, main} +import mill.constants.Util +import mill.main.buildgen.* +import mill.main.buildgen.BuildGenUtil.* +import mill.main.buildgen.IrDependencyType.* +import mill.scalalib.CrossVersion as MillCrossVersion +import os.Path + +import scala.collection.MapView +import scala.collection.immutable.SortedSet + +/** + * 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 + * ===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 + */ +@mill.api.internal +object SbtBuildGenMain + extends BuildGenBase[Project, String, (BuildInfo, Tree[Node[Option[Project]]])] { + override type C = Config + override type OM = Option[Project] + + 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 = 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" + ) + } + + println("Running the added `millInitExportBuild` sbt task to export the build") + + val exitCode = ( + 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) + val commandResult = os.call( + (sbtExecutable, "millInitExportBuild"), + cwd = workspace, + stdout = os.Inherit + ) + os.remove(sbtFile) + commandResult + } else + os.call( + (sbtExecutable, s"-addPluginSbtFile=${writeSbtFile().toString}", "millInitExportBuild"), + cwd = workspace, + stdout = os.Inherit + ) + ) + .exitCode + + // println("Exit code from running the `millInitExportBuild` sbt task: " + exitCode) + if (exitCode != 0) + println( + "The sbt command to run the `millInitExportBuild` sbt task has likely failed, please update the project's sbt version to the latest or our tested version v1.10.7, and try again." + ) + + 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.default.* + 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) + ) + // 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) + + convertWriteOut(cfg, cfg.shared, (buildExport.defaultBuildInfo, projectNodeTree)) + + println("converted sbt build to Mill") + } + + /** + * @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("/sbt-mill-init-export-build-assembly.jar").toExternalForm + */ + val sbtPluginJarName = "sbt-mill-init-export-build-assembly.jar" + val sbtPluginJarStream = getClass.getResourceAsStream(s"/$sbtPluginJarName") + val sbtPluginJarPath = tempDir / sbtPluginJarName + os.write(sbtPluginJarPath, sbtPluginJarStream) + val contents = + s"""addSbtPlugin("com.lihaoyi" % "mill-main-init-sbt-sbt-mill-init-export-build" % "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" + ) + + extension (om: Option[Project]) override def toOption(): Option[Project] = om + + 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) + } + + override def extractIrBuild( + cfg: Config, + // baseInfo: IrBaseInfo, + build: Node[Project], + packages: Map[(String, String, String), String] + ): IrBuild = { + val project = build.value + val buildInfo = project.buildInfo + val configurationDeps = extractConfigurationDeps(project, packages, 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 + ) + } + + override def extraImports: Seq[String] = Seq("mill.scalalib.SbtModule") + + def getModuleSupertypes(cfg: Config): Seq[String] = + cfg.shared.baseModule.fold(sbtSupertypes)(Seq(_)) + + def getPackage(project: Project): (String, String, String) = { + val buildPublicationInfo = project.buildInfo.buildPublicationInfo + (buildPublicationInfo.organization.orNull, project.name, buildPublicationInfo.version.orNull) + } + + 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] = + Seq("RootModule") ++ getModuleSupertypes(cfg) + + def groupArtifactVersion(dep: Dependency): (String, String, String) = + (dep.organization, dep.name, dep.revision) + + def getJavacOptions(buildInfo: BuildInfo): Seq[String] = + buildInfo.javacOptions.getOrElse(Seq.empty) + + def getRepositories(buildInfo: BuildInfo): Seq[String] = + buildInfo.resolvers.getOrElse(Seq.empty).map(resolver => + s"coursier.maven.MavenRepository(${escape(resolver.root)})" + ) + + def getPublishVersion(buildInfo: BuildInfo): String | Null = + buildInfo.buildPublicationInfo.version.orNull + + // originally named `ivyInterp` in the Maven and module + def renderIvy(dependency: Dependency): String = { + import dependency.* + renderIvyString( + organization, + name, + crossVersion match { + case CrossVersion.Disabled => None + case CrossVersion.Binary => Some(MillCrossVersion.Binary(false)) + case CrossVersion.Full => Some(MillCrossVersion.Full(false)) + case CrossVersion.Constant(value) => Some(MillCrossVersion.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: Dependency) = + Seq("ch.epfl.lamp", "org.scala-lang").contains(dep.organization) && + Seq("scala-library", "dotty-library", "scala3-library").contains(dep.name) + + def extractConfigurationDeps( + project: Project, + packages: PartialFunction[(String, String, String), String], + cfg: Config + ): IrScopedDeps = { + // refactored to a functional approach from the original imperative code in Maven and Gradle + + val allDepsByConfiguration = project.allDependencies + // .view // This makes the types hard to deal with here thus commented out. + .filterNot(isScalaStandardLibrary) + .flatMap(dep => + (dep.configurations match { + case None => Some(Default) + case Some(configuration) => configuration 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 unknown configuration ${escape(other)} is dropped." + ) + None + } + }) + .map(tpe => (dep, tpe)) + ) + .groupBy(_._2) + .view + .mapValues(_.map(_._1)) + // Types have to be specified explicitly here for the code to be resolved correctly in IDEA. + .asInstanceOf[MapView[IrDependencyType, Seq[Dependency]]] + + case class Deps[I, M](ivy: Seq[I], module: Seq[M]) + + // Types have to be specified explicitly here for the code to be resolved correctly in IDEA. + val ivyAndModuleDepsByConfiguration: Map[IrDependencyType, Deps[Dependency, String]] = + allDepsByConfiguration.mapValues(deps => { + val tuple2 = deps.partitionMap(dep => { + val id = groupArtifactVersion(dep) + if (packages.isDefinedAt(id)) Right(packages(id)) + else Left(dep) + }) + Deps(tuple2._1, tuple2._2) + }).toMap + + val testIvyAndModuleDeps = ivyAndModuleDepsByConfiguration.get(Test) + val testIvyDeps = testIvyAndModuleDeps.map(_.ivy) + val hasTest = os.exists(os.Path(project.projectDirectory) / "src/test") + val testModule = Option.when(hasTest)( + testIvyDeps.flatMap(_.collectFirst(Function.unlift(dep => + testModulesByGroup.get(dep.organization) + ))) + ).flatten + + cfg.shared.depsObject.fold({ + val default = ivyAndModuleDepsByConfiguration.get(Default) + val compile = ivyAndModuleDepsByConfiguration.get(Compile) + val run = ivyAndModuleDepsByConfiguration.get(Run) + val test = testIvyAndModuleDeps + IrScopedDeps( + Seq.empty, + SortedSet.empty, + // Using `fold` here causes issues with type inference. + SortedSet.from(default.map(_.ivy.iterator.map(renderIvy)).getOrElse(Iterator.empty)), + SortedSet.from(default.map(_.module).getOrElse(Seq.empty)), + SortedSet.from(compile.map(_.ivy.iterator.map(renderIvy)).getOrElse(Iterator.empty)), + SortedSet.from(compile.map(_.module).getOrElse(Seq.empty)), + SortedSet.from(run.map(_.ivy.iterator.map(renderIvy)).getOrElse(Iterator.empty)), + SortedSet.from(run.map(_.module).getOrElse(Seq.empty)), + testModule, + SortedSet.empty, + SortedSet.from(testIvyDeps.map(_.iterator.map(renderIvy)).getOrElse(Iterator.empty)), + SortedSet.from(test.map(_.module).getOrElse(Seq.empty)), + SortedSet.empty, + SortedSet.empty + ) + })(objectName => { + // Types have to be specified explicitly here for the code to be resolved correctly in IDEA. + val extractedIvyAndModuleDepsByConfiguration + : Map[IrDependencyType, Deps[((String, String), String), String]] = + ivyAndModuleDepsByConfiguration.view.mapValues({ + case Deps(ivy, module) => + Deps( + ivy.map(dep => { + val depName = s"`${dep.organization}:${dep.name}`" + ((depName, renderIvy(dep)), s"$objectName.$depName") + }), + module + ) + }).toMap + + val default = extractedIvyAndModuleDepsByConfiguration.get(Default) + val compile = extractedIvyAndModuleDepsByConfiguration.get(Compile) + val run = extractedIvyAndModuleDepsByConfiguration.get(Run) + val test = extractedIvyAndModuleDepsByConfiguration.get(Test) + IrScopedDeps( + extractedIvyAndModuleDepsByConfiguration.values.flatMap(_.ivy.iterator.map(_._1)).toSeq, + SortedSet.empty, + SortedSet.from(default.map(_.ivy.iterator.map(_._2)).getOrElse(Iterator.empty)), + SortedSet.from(default.map(_.module).getOrElse(Seq.empty)), + SortedSet.from(compile.map(_.ivy.iterator.map(_._2)).getOrElse(Iterator.empty)), + SortedSet.from(compile.map(_.module).getOrElse(Seq.empty)), + SortedSet.from(run.map(_.ivy.iterator.map(_._2)).getOrElse(Iterator.empty)), + SortedSet.from(run.map(_.module).getOrElse(Seq.empty)), + testModule, + SortedSet.empty, + SortedSet.from(test.map(_.ivy.iterator.map(_._2)).getOrElse(Iterator.empty)), + SortedSet.from(test.map(_.module).getOrElse(Seq.empty)), + SortedSet.empty, + SortedSet.empty + ) + }) + } + + @main + @mill.api.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 + ) +} diff --git a/main/init/sbt/test/resources/expected/config/all/sbt-multi-project-example/build.mill b/main/init/sbt/test/resources/expected/config/all/sbt-multi-project-example/build.mill new file mode 100644 index 00000000000..b514dff1fbd --- /dev/null +++ b/main/init/sbt/test/resources/expected/config/all/sbt-multi-project-example/build.mill @@ -0,0 +1,280 @@ +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` = + ivy"ch.qos.logback:logback-classic:1.2.3" + + val `com.github.julien-truffaut:monocle-core` = + ivy"com.github.julien-truffaut::monocle-core:1.4.0" + + val `com.github.julien-truffaut:monocle-macro` = + ivy"com.github.julien-truffaut::monocle-macro:1.4.0" + + val `com.github.pureconfig:pureconfig` = + ivy"com.github.pureconfig::pureconfig:0.8.0" + + val `com.typesafe.akka:akka-stream` = + ivy"com.typesafe.akka::akka-stream:2.5.6" + + val `com.typesafe.scala-logging:scala-logging` = + ivy"com.typesafe.scala-logging::scala-logging:3.7.2" + val `com.typesafe:config` = ivy"com.typesafe:config:1.3.1" + + val `io.netty:netty-transport-native-epoll` = + ivy"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` = + ivy"net.logstash.logback:logstash-logback-encoder:4.11" + val `org.scalacheck:scalacheck` = ivy"org.scalacheck::scalacheck:1.13.5" + val `org.scalatest:scalatest` = ivy"org.scalatest::scalatest:3.0.4" + val `org.slf4j:jcl-over-slf4j` = ivy"org.slf4j:jcl-over-slf4j:1.7.25" +} + +object `package` extends RootModule with 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 ivyDeps = super.ivyDeps() ++ 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` + ) + + object tests extends SbtTests with TestModule.ScalaTest { + + def ivyDeps = super.ivyDeps() ++ + Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + + } + } + + object multi1 extends BaseModule { + + def scalacOptions = super.scalacOptions() ++ Seq("-V") + + def ivyDeps = super.ivyDeps() ++ 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) + + 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 tests extends SbtTests with TestModule.ScalaTest { + + def ivyDeps = super.ivyDeps() ++ + Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + + } + } + + object multi2 extends BaseModule { + + def scalacOptions = Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation" + ) + + def ivyDeps = super.ivyDeps() ++ 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) + + 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 tests extends SbtTests with TestModule.ScalaTest { + + def ivyDeps = super.ivyDeps() ++ + Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + + } + } + + object nested extends Module { + + object nested extends BaseModule { + + def ivyDeps = super.ivyDeps() ++ + Seq(Deps.`io.netty:netty-transport-native-epoll`) + + 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 + )) + ) + + } + } +} + +trait BaseModule extends SbtModule with PublishModule { + + def scalaVersion = "2.12.3" + + def scalacOptions = super.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 repositoriesTask = Task.Anon { + super.repositoriesTask() ++ Seq( + coursier.maven.MavenRepository( + "https://oss.sonatype.org/service/local/repositories/releases/content/" + ), + coursier.maven.MavenRepository( + "https://oss.sonatype.org/content/repositories/snapshots" + ) + ) + } + + def zincWorker = mill.define.ModuleRef(BaseModuleZincWorker) + + object BaseModuleZincWorker extends ZincWorkerModule { + def jvmId = "11" + } +} diff --git a/main/init/sbt/test/resources/expected/config/without-base-project/sbt-multi-project-example/build.mill b/main/init/sbt/test/resources/expected/config/without-base-project/sbt-multi-project-example/build.mill new file mode 100644 index 00000000000..74948bdee64 --- /dev/null +++ b/main/init/sbt/test/resources/expected/config/without-base-project/sbt-multi-project-example/build.mill @@ -0,0 +1,209 @@ +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` = + ivy"ch.qos.logback:logback-classic:1.2.3" + + val `com.github.julien-truffaut:monocle-core` = + ivy"com.github.julien-truffaut::monocle-core:1.4.0" + + val `com.github.julien-truffaut:monocle-macro` = + ivy"com.github.julien-truffaut::monocle-macro:1.4.0" + + val `com.github.pureconfig:pureconfig` = + ivy"com.github.pureconfig::pureconfig:0.8.0" + + val `com.typesafe.akka:akka-stream` = + ivy"com.typesafe.akka::akka-stream:2.5.6" + + val `com.typesafe.scala-logging:scala-logging` = + ivy"com.typesafe.scala-logging::scala-logging:3.7.2" + val `com.typesafe:config` = ivy"com.typesafe:config:1.3.1" + + val `io.netty:netty-transport-native-epoll` = + ivy"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` = + ivy"net.logstash.logback:logstash-logback-encoder:4.11" + val `org.scalacheck:scalacheck` = ivy"org.scalacheck::scalacheck:1.13.5" + val `org.scalatest:scalatest` = ivy"org.scalatest::scalatest:3.0.4" + val `org.slf4j:jcl-over-slf4j` = ivy"org.slf4j:jcl-over-slf4j:1.7.25" +} + +object `package` extends RootModule with BaseModule { + + def artifactName = "sbt-multi-project-example" + + object common extends BaseModule { + + def ivyDeps = super.ivyDeps() ++ 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 ivyDeps = super.ivyDeps() ++ + Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + + } + } + + object multi1 extends BaseModule { + + def scalacOptions = super.scalacOptions() ++ Seq("-V") + + def ivyDeps = super.ivyDeps() ++ 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 ivyDeps = super.ivyDeps() ++ + Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + + } + } + + object multi2 extends BaseModule { + + def scalacOptions = Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation" + ) + + def ivyDeps = super.ivyDeps() ++ 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 ivyDeps = super.ivyDeps() ++ + Seq(Deps.`org.scalacheck:scalacheck`, Deps.`org.scalatest:scalatest`) + + } + } + + object nested extends Module { + + object nested extends BaseModule { + + def ivyDeps = super.ivyDeps() ++ + Seq(Deps.`io.netty:netty-transport-native-epoll`) + + } + } +} + +trait BaseModule extends SbtModule with PublishModule { + + def scalaVersion = "2.12.3" + + def scalacOptions = super.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 repositoriesTask = Task.Anon { + super.repositoriesTask() ++ Seq( + coursier.maven.MavenRepository( + "https://oss.sonatype.org/service/local/repositories/releases/content/" + ), + coursier.maven.MavenRepository( + "https://oss.sonatype.org/content/repositories/snapshots" + ) + ) + } + + def zincWorker = mill.define.ModuleRef(BaseModuleZincWorker) + + object BaseModuleZincWorker extends ZincWorkerModule { + def jvmId = "11" + } +} diff --git a/main/init/sbt/test/resources/expected/sbt-multi-project-example/build.mill b/main/init/sbt/test/resources/expected/sbt-multi-project-example/build.mill new file mode 100644 index 00000000000..f5bb1ab05f6 --- /dev/null +++ b/main/init/sbt/test/resources/expected/sbt-multi-project-example/build.mill @@ -0,0 +1,61 @@ +package build + +import $packages._ +import mill._ +import mill.javalib._ +import mill.javalib.publish._ +import mill.scalalib.SbtModule + +object `package` extends RootModule with SbtModule with PublishModule { + + def artifactName = "sbt-multi-project-example" + + def scalaVersion = "2.12.3" + + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8" + ) + + def repositoriesTask = Task.Anon { + super.repositoriesTask() ++ Seq( + coursier.maven.MavenRepository( + "https://oss.sonatype.org/service/local/repositories/releases/content/" + ), + coursier.maven.MavenRepository( + "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" + +} diff --git a/main/init/sbt/test/resources/expected/sbt-multi-project-example/common/package.mill b/main/init/sbt/test/resources/expected/sbt-multi-project-example/common/package.mill new file mode 100644 index 00000000000..72e8561ca20 --- /dev/null +++ b/main/init/sbt/test/resources/expected/sbt-multi-project-example/common/package.mill @@ -0,0 +1,75 @@ +package build.common + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ +import mill.scalalib.SbtModule + +object `package` extends RootModule with SbtModule with PublishModule { + + def scalaVersion = "2.12.3" + + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8" + ) + + def repositoriesTask = Task.Anon { + super.repositoriesTask() ++ Seq( + coursier.maven.MavenRepository( + "https://oss.sonatype.org/service/local/repositories/releases/content/" + ), + coursier.maven.MavenRepository( + "https://oss.sonatype.org/content/repositories/snapshots" + ) + ) + } + + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"ch.qos.logback:logback-classic:1.2.3", + ivy"com.typesafe.akka::akka-stream:2.5.6", + ivy"com.typesafe.scala-logging::scala-logging:3.7.2", + ivy"com.typesafe:config:1.3.1", + ivy"net.logstash.logback:logstash-logback-encoder:4.11", + ivy"org.slf4j:jcl-over-slf4j:1.7.25" + ) + + 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" + + object test extends SbtTests with TestModule.ScalaTest { + + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"org.scalacheck::scalacheck:1.13.5", + ivy"org.scalatest::scalatest:3.0.4" + ) + + } +} diff --git a/main/init/sbt/test/resources/expected/sbt-multi-project-example/multi1/package.mill b/main/init/sbt/test/resources/expected/sbt-multi-project-example/multi1/package.mill new file mode 100644 index 00000000000..a112998394a --- /dev/null +++ b/main/init/sbt/test/resources/expected/sbt-multi-project-example/multi1/package.mill @@ -0,0 +1,80 @@ +package build.multi1 + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ +import mill.scalalib.SbtModule + +object `package` extends RootModule with SbtModule with PublishModule { + + def scalaVersion = "2.12.3" + + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8", + "-V" + ) + + def repositoriesTask = Task.Anon { + super.repositoriesTask() ++ Seq( + coursier.maven.MavenRepository( + "https://oss.sonatype.org/service/local/repositories/releases/content/" + ), + coursier.maven.MavenRepository( + "https://oss.sonatype.org/content/repositories/snapshots" + ) + ) + } + + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"ch.qos.logback:logback-classic:1.2.3", + ivy"com.github.julien-truffaut::monocle-core:1.4.0", + ivy"com.github.julien-truffaut::monocle-macro:1.4.0", + ivy"com.typesafe.akka::akka-stream:2.5.6", + ivy"com.typesafe.scala-logging::scala-logging:3.7.2", + ivy"com.typesafe:config:1.3.1", + ivy"net.logstash.logback:logstash-logback-encoder:4.11", + ivy"org.slf4j:jcl-over-slf4j:1.7.25" + ) + + def moduleDeps = super.moduleDeps ++ Seq(build.common) + + 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 test extends SbtTests with TestModule.ScalaTest { + + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"org.scalacheck::scalacheck:1.13.5", + ivy"org.scalatest::scalatest:3.0.4" + ) + + } +} diff --git a/main/init/sbt/test/resources/expected/sbt-multi-project-example/multi2/package.mill b/main/init/sbt/test/resources/expected/sbt-multi-project-example/multi2/package.mill new file mode 100644 index 00000000000..2432745489e --- /dev/null +++ b/main/init/sbt/test/resources/expected/sbt-multi-project-example/multi2/package.mill @@ -0,0 +1,76 @@ +package build.multi2 + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ +import mill.scalalib.SbtModule + +object `package` extends RootModule with SbtModule with PublishModule { + + def scalaVersion = "2.12.3" + + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation" + ) + + def repositoriesTask = Task.Anon { + super.repositoriesTask() ++ Seq( + coursier.maven.MavenRepository( + "https://oss.sonatype.org/service/local/repositories/releases/content/" + ), + coursier.maven.MavenRepository( + "https://oss.sonatype.org/content/repositories/snapshots" + ) + ) + } + + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"ch.qos.logback:logback-classic:1.2.3", + ivy"com.github.pureconfig::pureconfig:0.8.0", + ivy"com.typesafe.akka::akka-stream:2.5.6", + ivy"com.typesafe.scala-logging::scala-logging:3.7.2", + ivy"com.typesafe:config:1.3.1", + ivy"net.logstash.logback:logstash-logback-encoder:4.11", + ivy"org.slf4j:jcl-over-slf4j:1.7.25" + ) + + def moduleDeps = super.moduleDeps ++ Seq(build.common) + + 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 test extends SbtTests with TestModule.ScalaTest { + + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"org.scalacheck::scalacheck:1.13.5", + ivy"org.scalatest::scalatest:3.0.4" + ) + + } +} diff --git a/main/init/sbt/test/resources/expected/sbt-multi-project-example/nested/nested/package.mill b/main/init/sbt/test/resources/expected/sbt-multi-project-example/nested/nested/package.mill new file mode 100644 index 00000000000..2459deec0e2 --- /dev/null +++ b/main/init/sbt/test/resources/expected/sbt-multi-project-example/nested/nested/package.mill @@ -0,0 +1,62 @@ +package build.nested.nested + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ +import mill.scalalib.SbtModule + +object `package` extends RootModule with SbtModule with PublishModule { + + def scalaVersion = "2.12.3" + + def scalacOptions = super.scalacOptions() ++ Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8" + ) + + def repositoriesTask = Task.Anon { + super.repositoriesTask() ++ Seq( + coursier.maven.MavenRepository( + "https://oss.sonatype.org/service/local/repositories/releases/content/" + ), + coursier.maven.MavenRepository( + "https://oss.sonatype.org/content/repositories/snapshots" + ) + ) + } + + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"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 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" + +} diff --git a/main/init/sbt/test/resources/expected/sbt-multi-project-example/nested/package.mill b/main/init/sbt/test/resources/expected/sbt-multi-project-example/nested/package.mill new file mode 100644 index 00000000000..681780798ae --- /dev/null +++ b/main/init/sbt/test/resources/expected/sbt-multi-project-example/nested/package.mill @@ -0,0 +1,5 @@ +package build.nested + +import mill._ + +object `package` extends RootModule with Module {} diff --git a/main/init/sbt/test/resources/expected/scala-seed-project/build.mill b/main/init/sbt/test/resources/expected/scala-seed-project/build.mill new file mode 100644 index 00000000000..aa7f1f841a1 --- /dev/null +++ b/main/init/sbt/test/resources/expected/scala-seed-project/build.mill @@ -0,0 +1,30 @@ +package build + +import mill._ +import mill.javalib._ +import mill.javalib.publish._ +import mill.scalalib.SbtModule + +object `package` extends RootModule with SbtModule with PublishModule { + + def artifactName = "Scala Seed Project" + + def scalaVersion = "2.13.12" + + def pomSettings = PomSettings( + "Scala Seed Project", + "com.example", + "", + Seq(), + VersionControl(None, None, None, None), + Seq() + ) + + def publishVersion = "0.1.0-SNAPSHOT" + + object test extends SbtTests with TestModule.Munit { + + def ivyDeps = super.ivyDeps() ++ Seq(ivy"org.scalameta::munit:0.7.29") + + } +} diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/LICENSE b/main/init/sbt/test/resources/sbt-multi-project-example/LICENSE new file mode 100644 index 00000000000..b54a8a16e99 --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-2018 Pol Bassiner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/README.md b/main/init/sbt/test/resources/sbt-multi-project-example/README.md new file mode 100644 index 00000000000..4534701ff33 --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/README.md @@ -0,0 +1,25 @@ +# sbt-multi-project-example + +Adapted from . + +The goal of this example is to provide a multi-project build using `sbt` providing: +* A single `build.sbt` file which allows for centralized configuration, dependency and build management +* Each sub-project contains only its source code +* Sub-projects can depend on other sub-projects +* Only *deliverable* sub-projects produce a *fat-jar* using [sbt-assembly](https://github.com/sbt/sbt-assembly) + +# Example structure +* sbt-multi-project-example/ + * common/ + * src/ + * test/ + * multi1/ + * src/ + * test/ + * multi2/ + * src/ + * test/ + * project/ + * build.properties + * plugins.sbt + * build.sbt diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/build.sbt b/main/init/sbt/test/resources/sbt-multi-project-example/build.sbt new file mode 100644 index 00000000000..b9fe3451faa --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/build.sbt @@ -0,0 +1,160 @@ +name := "sbt-multi-project-example" +organization in ThisBuild := "com.pbassiner" +scalaVersion in ThisBuild := "2.12.3" + +val urlString = "https://github.com/com-lihaoyi/mill" +ThisBuild / homepage := Some(url(urlString)) +ThisBuild / description := "This is an sbt sample project for testing Mill's init command." +ThisBuild / licenses := Seq(License.Apache2) +ThisBuild / developers := List(Developer("johnd", "John Doe", "john.doe@example.com", url("https://example.com/johnd"))) +ThisBuild / scmInfo := Some(ScmInfo(url(urlString), s"scm:git:$urlString.git")) + +// PROJECTS + +lazy val global = project + .in(file(".")) + .settings(settings) + .disablePlugins(AssemblyPlugin) + .aggregate( + common, + multi1, + multi2 + ) + +lazy val common = project + .settings( + name := "common", + settings ++ Seq(description := "This is the common module."), + libraryDependencies ++= commonDependencies + ) + .disablePlugins(AssemblyPlugin) + +lazy val multi1 = project + .settings( + name := "multi1", + settings ++ Seq(scalacOptions ++= Seq("-V")), + assemblySettings, + libraryDependencies ++= commonDependencies ++ Seq( + dependencies.monocleCore, + dependencies.monocleMacro + ) + ) + .dependsOn( + common + ) + +lazy val multi2 = project + .settings( + name := "multi2", + settings ++ Seq(scalacOptions := Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation" + )), + assemblySettings, + libraryDependencies ++= commonDependencies ++ Seq( + dependencies.pureconfig + ) + ) + .dependsOn( + common + ) + +lazy val nested = project + .in(file("nested/nested")) + .settings( + libraryDependencies += ("io.netty" % "netty-transport-native-epoll" % "4.1.118.Final") + .artifacts(Artifact("netty-transport-native-epoll").withType("pom").withClassifier(Some("linux-x86_64"))) + .exclude("io.netty", "netty-transport-native-epoll") + ) + +// DEPENDENCIES + +lazy val dependencies = + new { + val logbackV = "1.2.3" + val logstashV = "4.11" + val scalaLoggingV = "3.7.2" + val slf4jV = "1.7.25" + val typesafeConfigV = "1.3.1" + val pureconfigV = "0.8.0" + val monocleV = "1.4.0" + val akkaV = "2.5.6" + val scalatestV = "3.0.4" + val scalacheckV = "1.13.5" + + val logback = "ch.qos.logback" % "logback-classic" % logbackV + val logstash = "net.logstash.logback" % "logstash-logback-encoder" % logstashV + val scalaLogging = "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingV + val slf4j = "org.slf4j" % "jcl-over-slf4j" % slf4jV + val typesafeConfig = "com.typesafe" % "config" % typesafeConfigV + val akka = "com.typesafe.akka" %% "akka-stream" % akkaV + val monocleCore = "com.github.julien-truffaut" %% "monocle-core" % monocleV + val monocleMacro = "com.github.julien-truffaut" %% "monocle-macro" % monocleV + val pureconfig = "com.github.pureconfig" %% "pureconfig" % pureconfigV + val scalatest = "org.scalatest" %% "scalatest" % scalatestV + val scalacheck = "org.scalacheck" %% "scalacheck" % scalacheckV + } + +lazy val commonDependencies = Seq( + dependencies.logback, + dependencies.logstash, + dependencies.scalaLogging, + dependencies.slf4j, + dependencies.typesafeConfig, + dependencies.akka, + dependencies.scalatest % "test", + dependencies.scalacheck % "test" +) + +// SETTINGS + +lazy val settings = +wartremoverSettings ++ +scalafmtSettings + +lazy val compilerOptions = Seq( + "-unchecked", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-language:implicitConversions", + "-language:postfixOps", + "-deprecation", + "-encoding", + "utf8" +) + +ThisBuild / scalacOptions ++= compilerOptions +ThisBuild / resolvers ++= Seq( + // commented out as this is different on different machines + //"Local Maven Repository" at "file://" + Path.userHome.absolutePath + "/.m2/repository", + Resolver.sonatypeRepo("releases"), + Resolver.sonatypeRepo("snapshots") +) + +lazy val wartremoverSettings = Seq( + wartremoverWarnings in (Compile, compile) ++= Warts.allBut(Wart.Throw) +) + +lazy val scalafmtSettings = + Seq( + scalafmtOnCompile := true, + scalafmtTestOnCompile := true, + scalafmtVersion := "1.2.0" + ) + +lazy val assemblySettings = Seq( + assemblyJarName in assembly := name.value + ".jar", + assemblyMergeStrategy in assembly := { + case PathList("META-INF", xs @ _*) => MergeStrategy.discard + case "application.conf" => MergeStrategy.concat + case x => + val oldStrategy = (assemblyMergeStrategy in assembly).value + oldStrategy(x) + } +) diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/common/src/main/scala/Model.scala b/main/init/sbt/test/resources/sbt-multi-project-example/common/src/main/scala/Model.scala new file mode 100644 index 00000000000..4a64c26b8cc --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/common/src/main/scala/Model.scala @@ -0,0 +1,2 @@ +final case class Entity(id: String, nested: NestedEntity) +final case class NestedEntity(value: String) diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/common/src/test/scala/Test.scala b/main/init/sbt/test/resources/sbt-multi-project-example/common/src/test/scala/Test.scala new file mode 100644 index 00000000000..81dc49be006 --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/common/src/test/scala/Test.scala @@ -0,0 +1,6 @@ +import org.scalatest.FunSuite + +class Test extends FunSuite { + + test("common") {} +} diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/multi1/src/main/scala/Main.scala b/main/init/sbt/test/resources/sbt-multi-project-example/multi1/src/main/scala/Main.scala new file mode 100644 index 00000000000..0ef089fe5f1 --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/multi1/src/main/scala/Main.scala @@ -0,0 +1,11 @@ +import monocle.macros.GenLens + +object Main extends App { + println("multi1 can use common sub-project") + + val entity = Entity("id", NestedEntity("value")) + + println("multi1 can use monocle dependency") + + val idLens = GenLens[Entity](_.id) +} diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/multi1/src/test/scala/Test.scala b/main/init/sbt/test/resources/sbt-multi-project-example/multi1/src/test/scala/Test.scala new file mode 100644 index 00000000000..f8eb44cc353 --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/multi1/src/test/scala/Test.scala @@ -0,0 +1,13 @@ +import monocle.macros.GenLens +import org.scalatest.FunSuite + +class Test extends FunSuite { + + test("multi1 can use common sub-project") { + val entity = Entity("id", NestedEntity("value")) + } + + test("multi1 can use monocle dependency ") { + val idLens = GenLens[Entity](_.id) + } +} diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/multi2/src/main/scala/Main.scala b/main/init/sbt/test/resources/sbt-multi-project-example/multi2/src/main/scala/Main.scala new file mode 100644 index 00000000000..9af1d57f7fb --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/multi2/src/main/scala/Main.scala @@ -0,0 +1,11 @@ +object Main extends App { + println("multi2 can use common sub-project") + + val entity = Entity("id", NestedEntity("value")) + + println("multi2 can use pureconfig dependency") + + import pureconfig._ + + implicit def hint[T]: ProductHint[T] = ProductHint[T](ConfigFieldMapping(CamelCase, KebabCase)) +} diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/multi2/src/test/scala/Test.scala b/main/init/sbt/test/resources/sbt-multi-project-example/multi2/src/test/scala/Test.scala new file mode 100644 index 00000000000..b5781c76add --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/multi2/src/test/scala/Test.scala @@ -0,0 +1,14 @@ +import org.scalatest.FunSuite + +class Test extends FunSuite { + + test("multi2 can use common sub-project") { + val entity = Entity("id", NestedEntity("value")) + } + + test("multi2 can use pureconfig dependency") { + import pureconfig._ + + implicit def hint[T]: ProductHint[T] = ProductHint[T](ConfigFieldMapping(CamelCase, KebabCase)) + } +} diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/project/build.properties b/main/init/sbt/test/resources/sbt-multi-project-example/project/build.properties new file mode 100644 index 00000000000..fe69360b7c0 --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.10.7 diff --git a/main/init/sbt/test/resources/sbt-multi-project-example/project/plugins.sbt b/main/init/sbt/test/resources/sbt-multi-project-example/project/plugins.sbt new file mode 100644 index 00000000000..7c3cee85fd4 --- /dev/null +++ b/main/init/sbt/test/resources/sbt-multi-project-example/project/plugins.sbt @@ -0,0 +1,12 @@ +logLevel := Level.Warn + +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.2") + +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.2") + +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") +addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.2.1") + +addSbtPlugin("com.lucidchart" % "sbt-scalafmt-coursier" % "1.12") + +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5") diff --git a/main/init/sbt/test/resources/scala-seed-project/.gitignore b/main/init/sbt/test/resources/scala-seed-project/.gitignore new file mode 100644 index 00000000000..bddd1888af6 --- /dev/null +++ b/main/init/sbt/test/resources/scala-seed-project/.gitignore @@ -0,0 +1,2 @@ +/.bsp/ +target/ diff --git a/main/init/sbt/test/resources/scala-seed-project/build.sbt b/main/init/sbt/test/resources/scala-seed-project/build.sbt new file mode 100644 index 00000000000..1dd2cd9d59c --- /dev/null +++ b/main/init/sbt/test/resources/scala-seed-project/build.sbt @@ -0,0 +1,14 @@ +import Dependencies._ + +ThisBuild / scalaVersion := "2.13.12" +ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / organization := "com.example" +ThisBuild / organizationName := "example" + +lazy val root = (project in file(".")) + .settings( + name := "Scala Seed Project", + libraryDependencies += munit % Test + ) + +// See https://www.scala-sbt.org/1.x/docs/Using-Sonatype.html for instructions on how to publish to Sonatype. diff --git a/main/init/sbt/test/resources/scala-seed-project/project/Dependencies.scala b/main/init/sbt/test/resources/scala-seed-project/project/Dependencies.scala new file mode 100644 index 00000000000..1edb07a723b --- /dev/null +++ b/main/init/sbt/test/resources/scala-seed-project/project/Dependencies.scala @@ -0,0 +1,5 @@ +import sbt._ + +object Dependencies { + lazy val munit = "org.scalameta" %% "munit" % "0.7.29" +} diff --git a/main/init/sbt/test/resources/scala-seed-project/project/build.properties b/main/init/sbt/test/resources/scala-seed-project/project/build.properties new file mode 100644 index 00000000000..73df629ac1a --- /dev/null +++ b/main/init/sbt/test/resources/scala-seed-project/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.7 diff --git a/main/init/sbt/test/resources/scala-seed-project/src/main/scala/example/Hello.scala b/main/init/sbt/test/resources/scala-seed-project/src/main/scala/example/Hello.scala new file mode 100644 index 00000000000..80ea40a9b31 --- /dev/null +++ b/main/init/sbt/test/resources/scala-seed-project/src/main/scala/example/Hello.scala @@ -0,0 +1,9 @@ +package example + +object Hello extends Greeting with App { + println(greeting) +} + +trait Greeting { + lazy val greeting: String = "hello" +} diff --git a/main/init/sbt/test/resources/scala-seed-project/src/test/scala/example/HelloSpec.scala b/main/init/sbt/test/resources/scala-seed-project/src/test/scala/example/HelloSpec.scala new file mode 100644 index 00000000000..d57b5ed4534 --- /dev/null +++ b/main/init/sbt/test/resources/scala-seed-project/src/test/scala/example/HelloSpec.scala @@ -0,0 +1,7 @@ +package example + +class HelloSpec extends munit.FunSuite { + test("say hello") { + assertEquals(Hello.greeting, "hello") + } +} diff --git a/main/init/sbt/test/src/mill/main/sbt/BuildGenTests.scala b/main/init/sbt/test/src/mill/main/sbt/BuildGenTests.scala new file mode 100644 index 00000000000..2a15135d51a --- /dev/null +++ b/main/init/sbt/test/src/mill/main/sbt/BuildGenTests.scala @@ -0,0 +1,60 @@ +package mill.main.sbt + +import mill.main.buildgen.BuildGenChecker +import utest.* + +object BuildGenTests extends TestSuite { + + def tests: Tests = Tests { + val checker = BuildGenChecker() + + test("scala-seed-project") { + val sourceRoot = os.sub / "scala-seed-project" + val expectedRoot = os.sub / "expected/scala-seed-project" + assert( + checker.check(SbtBuildGenMain.main(Array.empty), sourceRoot, expectedRoot) + ) + } + + // from https://github.com/pbassiner/sbt-multi-project-example/tree/master + test("sbt-multi-project-example") { + val sourceRoot = os.sub / "sbt-multi-project-example" + val expectedRoot = os.sub / "expected/sbt-multi-project-example" + assert( + checker.check(SbtBuildGenMain.main(Array.empty), sourceRoot, expectedRoot) + ) + } + + 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") + assert( + checker.check(SbtBuildGenMain.main(args), sourceRoot, expectedRoot) + ) + } + } + } + } +} diff --git a/main/init/src/mill/init/InitSbtModule.scala b/main/init/src/mill/init/InitSbtModule.scala new file mode 100644 index 00000000000..fa55853a950 --- /dev/null +++ b/main/init/src/mill/init/InitSbtModule.scala @@ -0,0 +1,14 @@ +package mill.init + +import mill.T +import mill.api.PathRef +import mill.define.{Discover, ExternalModule} + +@mill.api.experimental +object InitSbtModule extends ExternalModule with BuildGenModule { + lazy val millDiscover = Discover[this.type] + + def buildGenClasspath: T[Seq[PathRef]] = BuildGenModule.millModule("mill-main-init-sbt") + + def buildGenMainClass: T[String] = "mill.main.sbt.SbtBuildGenMain" +} diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index 11b3b3d26f2..d08d5f20600 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -278,6 +278,11 @@ trait MainModule extends BaseModule { Seq("mill.init.InitGradleModule/init") ++ args, SelectMode.Separated ) + else if (os.exists(os.pwd / "build.sbt")) + evaluator.evaluate( + Seq("mill.init.InitSbtModule/init") ++ args, + SelectMode.Separated + ) else if (args.headOption.exists(_.toLowerCase.endsWith(".g8"))) evaluator.evaluate( Seq("mill.scalalib.giter8.Giter8Module/init") ++ args, diff --git a/website/docs/modules/ROOT/nav.adoc b/website/docs/modules/ROOT/nav.adoc index 3f0a7e3eed1..41519953ed3 100644 --- a/website/docs/modules/ROOT/nav.adoc +++ b/website/docs/modules/ROOT/nav.adoc @@ -60,6 +60,7 @@ * xref:migrating/migrating.adoc[] ** xref:migrating/maven.adoc[] ** xref:migrating/gradle.adoc[] +** xref:migrating/sbt.adoc[] // This section gives a tour of the various user-facing features of Mill: // library deps, out folder, queries, tasks, etc.. These are things that // every Mill user will likely encounter, and are touched upon in the various diff --git a/website/docs/modules/ROOT/pages/migrating/sbt.adoc b/website/docs/modules/ROOT/pages/migrating/sbt.adoc new file mode 100644 index 00000000000..bf7a60abe0e --- /dev/null +++ b/website/docs/modules/ROOT/pages/migrating/sbt.adoc @@ -0,0 +1,125 @@ += Migrating From sbt to Mill +:page-aliases: Migrating_An_sbt_Build_to_Mill.adoc +:icons: font + + + +The Mill `init` command can be used to convert an sbt build to Mill. This has +xref:#limitations[limitations] and is not intended to reliably migrate 100% of +sbt builds out there in the wild, but is instead meant to provide the basic +scaffolding of a Mill build for you to further refine and update manually. + +Each sbt project in a build tree is converted to a Mill module. +A nested `test` module is defined, if `src/test` exists, and is configured with a supported xref:scalalib/testing.adoc[test framework], if found. + +Again, note that `mill init` imports an sbt build on a best-effort basis. +This means that while simple projects can be expected to complete without issue: + +include::partial$example/scalalib/migrating/1-sbt-complete.adoc[] + +Projects with a complex build often require some manual tweaking in order to work: + +include::partial$example/scalalib/migrating/2-sbt-incomplete.adoc[] + +== Capabilities + +The conversion + +* handles deeply nested modules +* captures publish settings +* configures dependencies for configurations: +** no configuration +** Compile +** Test +** Runtime +** Provided +** Optional +* configures testing frameworks: +** Java: +*** JUnit 4 +*** JUnit 5 +*** TestNG +** Scala: +*** ScalaTest +*** Specs2 +*** µTest +*** MUnit +*** Weaver +*** ZIOTest + +[#arguments] +=== Command line arguments + +The conversion and its output (the generated Mill build files) can be customized using + +* `--base-module` (`-b`): name of generated base module trait defining shared settings ++ +[source,sh] +---- +./mill init --base-module MyModule +---- + +* `--test-module` (`-t`): name of generated nested test module (defaults to `test`) ++ +[source,sh] +---- +./mill init --test-module test +---- + +* `--deps-object` (`-d`): name of generated companion object defining dependency constants ++ +[source,sh] +---- +./mill init --deps-object Deps +---- + +* `--merge` (`-m`): merge build files generated for a multi-module build ++ +[source,sh] +---- +./mill init --merge +---- + +TIP: You can run `mill init` multiple times. It is recommended to run it first without any options. + +[#limitations] +== 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 + +sbt plugin support is limited to: + +* https://www.scala-sbt.org/1.x/api/sbt/plugins/JvmPlugin$.html[`JvmPlugin`] + +[TIP] +==== +These limitations can be overcome by: + +* configuring equivalent Mill xref:extending/contrib-plugins.adoc[contrib] +or xref:extending/thirdparty-plugins.adoc[third party] plugins +* defining custom xref:extending/writing-plugins.adoc[plugins] +* defining custom xref:fundamentals/tasks.adoc[tasks] +* defining custom xref:fundamentals/cross-builds.adoc[cross modules] +==== + +== FAQ + +How to fix errors such as +`java.lang.UnsupportedOperationException: The Security Manager is deprecated and will be removed in a future release`, +`java.io.IOError: java.lang.RuntimeException: /packages cannot be represented as URI`, +and `java.lang.RuntimeException: java.lang.reflect.InvocationTargetException` +thrown by the sbt command invoked by `mill init`? + +Update the project's sbt version to the latest or our tested version v1.10.7, and try again. + +How to fix test compilation errors? + +* The test framework configured may be for an unsupported version; try upgrading the +corresponding dependencies. +* Mill does not add `compileIvyDeps` dependencies to the transitive dependencies of the nested +test module; specify the dependencies again, in `ivyDeps` or `runIvyDeps`.