diff --git a/build.mill b/build.mill index 39e079106f6..81a08552203 100644 --- a/build.mill +++ b/build.mill @@ -118,7 +118,7 @@ object Deps { val coursierVersion = "2.1.19" val coursier = ivy"io.get-coursier::coursier:$coursierVersion" - val coursierInterface = ivy"io.get-coursier:interface:1.0.25" + val coursierInterface = ivy"io.get-coursier:interface:1.0.26" val coursierJvm = ivy"io.get-coursier::coursier-jvm:$coursierVersion" val cask = ivy"com.lihaoyi::cask:0.9.4" diff --git a/scalalib/src/mill/scalalib/PublishModule.scala b/scalalib/src/mill/scalalib/PublishModule.scala index cc9fc686998..7e44b3e80c8 100644 --- a/scalalib/src/mill/scalalib/PublishModule.scala +++ b/scalalib/src/mill/scalalib/PublishModule.scala @@ -70,12 +70,16 @@ trait PublishModule extends JavaModule { outer => def publishXmlDeps: Task[Agg[Dependency]] = Task.Anon { val ivyPomDeps = - processedIvyDeps().map(_.toDep).map(resolvePublishDependency.apply().apply(_)) + processedIvyDeps().map(_.toDep) + .map(resolvePublishDependency.apply().apply(_)) + + val runIvyPomDeps = runIvyDeps() + .map(resolvePublishDependency.apply().apply(_)) + .filter(!ivyPomDeps.contains(_)) val compileIvyPomDeps = compileIvyDeps() .map(resolvePublishDependency.apply().apply(_)) .filter(!ivyPomDeps.contains(_)) - .map(_.copy(scope = Scope.Provided)) val modulePomDeps = T.sequence(moduleDepsChecked.collect { case m: PublishModule => m.publishSelfDependency @@ -83,10 +87,16 @@ trait PublishModule extends JavaModule { outer => val compileModulePomDeps = T.sequence(compileModuleDepsChecked.collect { case m: PublishModule => m.publishSelfDependency })() + val runModulePomDeps = T.sequence(runModuleDepsChecked.collect { + case m: PublishModule => m.publishSelfDependency + })() - ivyPomDeps ++ compileIvyPomDeps ++ + ivyPomDeps ++ + compileIvyPomDeps.map(_.copy(scope = Scope.Provided)) ++ + runIvyPomDeps.map(_.copy(scope = Scope.Runtime)) ++ modulePomDeps.map(Dependency(_, Scope.Compile)) ++ - compileModulePomDeps.map(Dependency(_, Scope.Provided)) + compileModulePomDeps.map(Dependency(_, Scope.Provided)) ++ + runModulePomDeps.map(Dependency(_, Scope.Runtime)) } /** diff --git a/scalalib/src/mill/scalalib/publish/Ivy.scala b/scalalib/src/mill/scalalib/publish/Ivy.scala index 75ba33dbec1..29bf840694f 100644 --- a/scalalib/src/mill/scalalib/publish/Ivy.scala +++ b/scalalib/src/mill/scalalib/publish/Ivy.scala @@ -78,11 +78,11 @@ object Ivy { private def renderDependency(dep: Dependency): Elem = { if (dep.exclusions.isEmpty) ${dep.configuration.getOrElse("default(compile)")}" + depIvyConf(dep) } /> else ${dep.configuration.getOrElse("default(compile)")}" + depIvyConf(dep) }> {dep.exclusions.map(ex => )} @@ -92,12 +92,13 @@ object Ivy { private def depIvyConf(d: Dependency): String = { - if (d.optional) "optional" + def target(value: String) = d.configuration.getOrElse(value) + if (d.optional) s"optional->${target("runtime")}" else d.scope match { - case Scope.Compile => "compile" - case Scope.Provided => "provided" - case Scope.Test => "test" - case Scope.Runtime => "runtime" + case Scope.Compile => s"compile->${target("compile")};runtime->${target("runtime")}" + case Scope.Provided => s"provided->${target("compile")}" + case Scope.Test => s"test->${target("runtime")}" + case Scope.Runtime => s"runtime->${target("runtime")}" } } diff --git a/scalalib/test/src/mill/scalalib/PublishModuleTests.scala b/scalalib/test/src/mill/scalalib/PublishModuleTests.scala index e1f7d33dd8e..45f47bdb26e 100644 --- a/scalalib/test/src/mill/scalalib/PublishModuleTests.scala +++ b/scalalib/test/src/mill/scalalib/PublishModuleTests.scala @@ -17,6 +17,8 @@ import utest._ import utest.framework.TestPath import java.io.PrintStream + +import scala.jdk.CollectionConverters._ import scala.xml.NodeSeq object PublishModuleTests extends TestSuite { @@ -77,6 +79,36 @@ object PublishModuleTests extends TestSuite { } } + trait TestPublishModule extends PublishModule { + def publishVersion = "0.1.0-SNAPSHOT" + def pomSettings = PomSettings( + organization = "com.lihaoyi.pubmodtests", + description = "test thing", + url = "https://github.com/com-lihaoyi/mill", + licenses = Seq(License.Common.Apache2), + versionControl = VersionControl.github("com-lihaoyi", "mill"), + developers = Nil + ) + } + object compileAndRuntimeStuff extends TestBaseModule { + object main extends JavaModule with TestPublishModule { + def ivyDeps = Agg( + ivy"org.slf4j:slf4j-api:2.0.15" + ) + def runIvyDeps = Agg( + ivy"ch.qos.logback:logback-classic:1.5.12" + ) + } + + object transitive extends JavaModule with TestPublishModule { + def moduleDeps = Seq(main) + } + + object runtimeTransitive extends JavaModule with TestPublishModule { + def runModuleDeps = Seq(main) + } + } + val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "publish" def tests: Tests = Tests { @@ -185,7 +217,7 @@ object PublishModuleTests extends TestSuite { val ivyXml = scala.xml.XML.loadFile(result.value.path.toString) val deps: NodeSeq = (ivyXml \ "dependencies" \ "dependency") assert(deps.exists(n => - (n \ "@conf").text == "compile->default(compile)" && + (n \ "@conf").text == "compile->compile;runtime->runtime" && (n \ "@name").text == "scala-library" && (n \ "@org").text == "org.scala-lang" )) } @@ -209,6 +241,103 @@ object PublishModuleTests extends TestSuite { // ) } } + + test("scopes") - UnitTester(compileAndRuntimeStuff, null).scoped { eval => + def assertClassPathContains(cp: Seq[os.Path], fileName: String) = + assert(cp.map(_.last).contains(fileName)) + def assertClassPathDoesntContain(cp: Seq[os.Path], prefix: String) = + assert(cp.map(_.last).forall(!_.startsWith(prefix))) + + def nothingClassPathCheck(cp: Seq[os.Path]): Unit = { + assertClassPathDoesntContain(cp, "slf4j") + assertClassPathDoesntContain(cp, "logback") + } + def compileClassPathCheck(cp: Seq[os.Path]): Unit = { + assertClassPathContains(cp, "slf4j-api-2.0.15.jar") + assertClassPathDoesntContain(cp, "logback") + } + def runtimeClassPathCheck(cp: Seq[os.Path]): Unit = { + assertClassPathContains(cp, "slf4j-api-2.0.15.jar") + assertClassPathContains(cp, "logback-classic-1.5.12.jar") + } + + val compileCp = + eval(compileAndRuntimeStuff.main.compileClasspath).toTry.get.value.toSeq.map(_.path) + val runtimeCp = + eval(compileAndRuntimeStuff.main.runClasspath).toTry.get.value.toSeq.map(_.path) + + compileClassPathCheck(compileCp) + runtimeClassPathCheck(runtimeCp) + + val ivy2Repo = eval.evaluator.workspace / "ivy2Local" + val m2Repo = eval.evaluator.workspace / "m2Local" + + eval(compileAndRuntimeStuff.main.publishLocal(ivy2Repo.toString)).toTry.get + eval(compileAndRuntimeStuff.transitive.publishLocal(ivy2Repo.toString)).toTry.get + eval(compileAndRuntimeStuff.runtimeTransitive.publishLocal(ivy2Repo.toString)).toTry.get + eval(compileAndRuntimeStuff.main.publishM2Local(m2Repo.toString)).toTry.get + eval(compileAndRuntimeStuff.transitive.publishM2Local(m2Repo.toString)).toTry.get + eval(compileAndRuntimeStuff.runtimeTransitive.publishM2Local(m2Repo.toString)).toTry.get + + def localRepoCp(localRepo: coursierapi.Repository, moduleName: String, config: String) = { + val dep = coursierapi.Dependency.of("com.lihaoyi.pubmodtests", moduleName, "0.1.0-SNAPSHOT") + coursierapi.Fetch.create() + .addDependencies(dep) + .addRepositories(localRepo) + .withResolutionParams( + coursierapi.ResolutionParams.create() + .withDefaultConfiguration(if (config.isEmpty) null else config) + ) + .fetch() + .asScala + .map(os.Path(_)) + .toSeq + } + def ivy2Cp(moduleName: String, config: String) = + localRepoCp( + coursierapi.IvyRepository.of(ivy2Repo.toNIO.toUri.toASCIIString + "[defaultPattern]"), + moduleName, + config + ) + def m2Cp(moduleName: String, config: String) = + localRepoCp( + coursierapi.MavenRepository.of(m2Repo.toNIO.toUri.toASCIIString), + moduleName, + config + ) + + val ivy2CompileCp = ivy2Cp("main", "compile") + val ivy2RunCp = ivy2Cp("main", "runtime") + val m2CompileCp = m2Cp("main", "compile") + val m2RunCp = m2Cp("main", "runtime") + + compileClassPathCheck(ivy2CompileCp) + compileClassPathCheck(m2CompileCp) + runtimeClassPathCheck(ivy2RunCp) + runtimeClassPathCheck(m2RunCp) + + val ivy2TransitiveCompileCp = ivy2Cp("transitive", "compile") + val ivy2TransitiveRunCp = ivy2Cp("transitive", "runtime") + val m2TransitiveCompileCp = m2Cp("transitive", "compile") + val m2TransitiveRunCp = m2Cp("transitive", "runtime") + + compileClassPathCheck(ivy2TransitiveCompileCp) + compileClassPathCheck(m2TransitiveCompileCp) + runtimeClassPathCheck(ivy2TransitiveRunCp) + runtimeClassPathCheck(m2TransitiveRunCp) + + val ivy2RuntimeTransitiveCompileCp = ivy2Cp("runtimeTransitive", "compile") + val ivy2RuntimeTransitiveRunCp = ivy2Cp("runtimeTransitive", "runtime") + val m2RuntimeTransitiveCompileCp = m2Cp("runtimeTransitive", "compile") + val m2RuntimeTransitiveRunCp = m2Cp("runtimeTransitive", "runtime") + + // runtime dependency on the main module - doesn't pull anything from it + // at compile time, hence the nothingClassPathCheck-s + nothingClassPathCheck(ivy2RuntimeTransitiveCompileCp) + nothingClassPathCheck(m2RuntimeTransitiveCompileCp) + runtimeClassPathCheck(ivy2RuntimeTransitiveRunCp) + runtimeClassPathCheck(m2RuntimeTransitiveRunCp) + } } }