diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 7b7802af5..e8490a39e 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2782,7 +2782,7 @@ object Classpaths { Seq( publishMavenStyle :== true, sbtPluginPublishLegacyMavenStyle :== false, - useIvy :== true, + useIvy :== false, publishArtifact :== true, (Test / publishArtifact) :== false ) @@ -2912,7 +2912,7 @@ object Classpaths { }.value, publish := LibraryManagement.ivylessPublishTask.tag(Tags.Publish, Tags.Network).value, publishLocal := LibraryManagement.ivylessPublishLocalTask.value, - publishM2 := publishOrSkip(publishM2Configuration, publishM2 / skip).value, + publishM2 := LibraryManagement.ivylessPublishM2Task.tag(Tags.Publish, Tags.Network).value, credentials ++= Def.uncached { val alreadyContainsCentralCredentials: Boolean = credentials.value.exists { case d: Credentials.DirectCredentials => d.host == Sona.host @@ -3352,8 +3352,11 @@ object Classpaths { import ShowLines.* val report = updateTask.value val log = streams.value.log + val module = dependencyResolution.value.moduleDescriptor( + moduleSettings.value.asInstanceOf[ModuleDescriptorConfiguration] + ) val ew = - EvictionWarning(ivyModule.value, (evicted / evictionWarningOptions).value, report) + EvictionWarning(module, (evicted / evictionWarningOptions).value, report) ew.lines foreach { log.warn(_) } ew.infoAllTheThings foreach { log.info(_) } ew @@ -3721,6 +3724,7 @@ object Classpaths { def deliverTask(config: TaskKey[PublishConfiguration]): Initialize[Task[File]] = Def.task { Def.unit(update.value) + if !useIvy.value then sys.error("deliver/makeIvyXml requires useIvy := true") IvyActions.deliver(ivyModule.value, config.value, streams.value.log) } @@ -3752,6 +3756,10 @@ object Classpaths { val log = streams.value.log val ref = thisProjectRef.value logSkipPublish(log, ref) + } else if (!useIvy.value) { + sys.error( + "publishOrSkip requires useIvy := true. Use publish/publishLocal for ivyless publishing." + ) } else { val conf = config.value val log = streams.value.log @@ -4209,10 +4217,10 @@ object Classpaths { private[sbt] def depMap: Initialize[Task[Map[ModuleRevisionId, ModuleDescriptor]]] = import sbt.TupleSyntax.* - (buildDependencies.toTaskable, thisProjectRef.toTaskable, settingsData, streams).flatMapN { - (bd, thisProj, data, s) => + (buildDependencies.toTaskable, thisProjectRef.toTaskable, settingsData, streams) + .flatMapN { (bd, thisProj, data, s) => depMap(bd.classpathTransitiveRefs(thisProj), data, s.log) - } + } private[sbt] def depMap( projects: Seq[ProjectRef], diff --git a/main/src/main/scala/sbt/internal/GlobalPlugin.scala b/main/src/main/scala/sbt/internal/GlobalPlugin.scala index d59dccf55..e0dfe3ad3 100644 --- a/main/src/main/scala/sbt/internal/GlobalPlugin.scala +++ b/main/src/main/scala/sbt/internal/GlobalPlugin.scala @@ -79,7 +79,9 @@ object GlobalPlugin { val taskInit = Def.task { val intcp = (Runtime / internalDependencyClasspath).value val prods = (Runtime / exportedProducts).value - val depMap = projectDescriptors.value + ivyModule.value.dependencyMapping(state.log) + val depMap = + if useIvy.value then projectDescriptors.value + ivyModule.value.dependencyMapping(state.log) + else projectDescriptors.value GlobalPluginData( projectID.value, diff --git a/main/src/main/scala/sbt/internal/LibraryManagement.scala b/main/src/main/scala/sbt/internal/LibraryManagement.scala index 08648ce10..e079c0efd 100644 --- a/main/src/main/scala/sbt/internal/LibraryManagement.scala +++ b/main/src/main/scala/sbt/internal/LibraryManagement.scala @@ -622,14 +622,23 @@ private[sbt] object LibraryManagement { writeChecksums(ivyXmlFile) else log.warn(s"$ivyXmlFile already exists, skipping (overwrite=$overwrite)") + // Build a lookup from (type, classifier, ext) to cross-versioned publication name + val pubNameLookup: Map[(String, String, String), String] = + project.publications.map { (_, pub) => + (pub.`type`.value, pub.classifier.value, pub.ext.value) -> pub.name + }.toMap + // Publish each artifact artifacts.foreach: (artifact, sourceFile) => val folder = typeToFolder(artifact.`type`) val targetDir = moduleDir / folder - // Construct filename using module name (includes Scala version suffix) + classifier + extension + // Look up the cross-versioned artifact name from publications, fall back to module name + val classifierStr = artifact.classifier.getOrElse("") + val artName = pubNameLookup + .getOrElse((artifact.`type`, classifierStr, artifact.extension), moduleName) val classifier = artifact.classifier.map("-" + _).getOrElse("") - val fileName = s"$moduleName$classifier.${artifact.extension}" + val fileName = s"$artName$classifier.${artifact.extension}" val targetFile = targetDir / fileName if !targetFile.exists || overwrite then @@ -845,7 +854,13 @@ private[sbt] object LibraryManagement { ): Unit = if repoBase == null then throw new IllegalArgumentException("repoBase must not be null") val groupId = project.module.organization.value - val artifactId = project.module.name.value + // Derive artifactId: for sbt 2 plugins, module.name has cross-version (e.g. sbt-example_sbt2_3). + // For sbt 1 plugins, mavenArtifactsOfSbtPlugin cross-versions the POM artifact name (e.g. sbt-example_2.12_1.0). + val baseModuleName = project.module.name.value + val pomArtName = artifacts.collectFirst { case (a, _) if a.`type` == "pom" => a.name } + val artifactId = pomArtName match + case Some(name) if name.startsWith(baseModuleName) && name != baseModuleName => name + case _ => baseModuleName val version = project.version val groupPath = groupId.replace('.', '/') val versionDir = new File(repoBase, s"$groupPath/$artifactId/$version") @@ -878,7 +893,13 @@ private[sbt] object LibraryManagement { if baseUrl == null || baseUrl.trim.isEmpty then throw new IllegalArgumentException("baseUrl must not be null or empty") val groupId = project.module.organization.value - val artifactId = project.module.name.value + // Derive artifactId: for sbt 2 plugins, module.name has cross-version (e.g. sbt-example_sbt2_3). + // For sbt 1 plugins, mavenArtifactsOfSbtPlugin cross-versions the POM artifact name (e.g. sbt-example_2.12_1.0). + val baseModuleName = project.module.name.value + val pomArtName = artifacts.collectFirst { case (a, _) if a.`type` == "pom" => a.name } + val artifactId = pomArtName match + case Some(name) if name.startsWith(baseModuleName) && name != baseModuleName => name + case _ => baseModuleName val version = project.version val directCreds = credentials.collect: case d: Credentials.DirectCredentials => d @@ -939,8 +960,13 @@ private[sbt] object LibraryManagement { if (normalized.startsWith("file:")) new File(new java.net.URI(normalized)) else new File(normalized) val repoDir = localRepoBase.getAbsoluteFile - log.info(s"Ivyless publish to file repo: $repoDir") - ivylessPublishLocal(project, artifacts, checksumAlgorithms, repoDir, overwrite, log) + val isMavenLayout = fileRepo.patterns.isMavenCompatible + if isMavenLayout then + log.info(s"Ivyless publish (Maven layout) to file repo: $repoDir") + ivylessPublishMavenToFile(project, artifacts, checksumAlgorithms, repoDir, overwrite, log) + else + log.info(s"Ivyless publish (Ivy layout) to file repo: $repoDir") + ivylessPublishLocal(project, artifacts, checksumAlgorithms, repoDir, overwrite, log) } /** @@ -1003,15 +1029,26 @@ private[sbt] object LibraryManagement { val repoDir = (if (baseStr.startsWith("file:")) new File(new java.net.URI(baseStr)) else new File(baseStr)).getAbsoluteFile - log.info(s"Ivyless publish to file repo: $repoDir") - ivylessPublishLocal( - project, - artifacts, - config.checksums, - repoDir, - config.overwrite, - log - ) + if pbr.patterns.isMavenCompatible then + log.info(s"Ivyless publish (Maven layout) to file repo: $repoDir") + ivylessPublishMavenToFile( + project, + artifacts, + config.checksums, + repoDir, + config.overwrite, + log + ) + else + log.info(s"Ivyless publish (Ivy layout) to file repo: $repoDir") + ivylessPublishLocal( + project, + artifacts, + config.checksums, + repoDir, + config.overwrite, + log + ) case mavenCache: sbt.librarymanagement.MavenCache => ivylessPublishMavenToFile( project, @@ -1045,18 +1082,13 @@ private[sbt] object LibraryManagement { log ) else - log.warn(s"Ivyless Maven publish: unsupported root '$root'. Falling back to Ivy.") - val conf = publishConfiguration.value - val module = ivyModule.value - publisher.value.publish(module, conf, log) - case _ => - log.warn( - "Ivyless publish only supports URLRepository, FileRepository, or MavenRepository. Falling back to Ivy." + sys.error( + s"Ivyless Maven publish: unsupported root '$root'. Set useIvy := true or use a supported repository (http/https/file)." + ) + case other => + sys.error( + s"Ivyless publish does not support ${other.getClass.getName}. Set useIvy := true or use URLRepository, FileRepository, or MavenRepository." ) - val conf = publishConfiguration.value - val module = ivyModule.value - val publisherInterface = publisher.value - publisherInterface.publish(module, conf, log) } } ) @@ -1104,4 +1136,48 @@ private[sbt] object LibraryManagement { } ) ) + + /** + * Task initializer for ivyless publishM2 (publish to local Maven ~/.m2 repository). + * Uses Def.ifS for proper selective functor behavior. + */ + def ivylessPublishM2Task: Def.Initialize[Task[Unit]] = + import Keys.* + Def.ifS(Def.task { (publishM2 / skip).value })( + // skip = true + Def.task { + val log = streams.value.log + val ref = thisProjectRef.value + log.debug(s"Skipping publishM2 for ${Reference.display(ref)}") + } + )( + // skip = false + Def.ifS(Def.task { useIvy.value })( + // useIvy = true: use Ivy-based publisher + Def.task { + val log = streams.value.log + val conf = publishM2Configuration.value + val module = ivyModule.value + val publisherInterface = publisher.value + publisherInterface.publish(module, conf, log) + } + )( + // useIvy = false: use ivyless publisher to Maven local + Def.task { + val log = streams.value.log + val project = csrProject.value.withPublications(csrPublications.value) + val config = publishM2Configuration.value + val artifacts = config.artifacts.map { case (a, f) => (a, f) } + val m2Repo = Resolver.publishMavenLocal + ivylessPublishMavenToFile( + project, + artifacts, + config.checksums, + m2Repo.rootFile, + config.overwrite, + log + ) + } + ) + ) } diff --git a/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/build.sbt b/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/build.sbt index 8d6fca01b..b826fa328 100644 --- a/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/build.sbt +++ b/sbt-app/src/sbt-test/dependency-management/deliver-artifacts/build.sbt @@ -1,6 +1,7 @@ ThisBuild / csrCacheDirectory := (ThisBuild / baseDirectory).value / "coursier-cache" ThisBuild / organization := "org.example" ThisBuild / version := "1.0" +ThisBuild / useIvy := true lazy val a = project.settings(common).settings( // verifies that a can be published as an ivy.xml file and preserve the extra artifact information, diff --git a/sbt-app/src/sbt-test/dependency-management/make-ivy-xml/build.sbt b/sbt-app/src/sbt-test/dependency-management/make-ivy-xml/build.sbt index 98c0eadcb..7f3863520 100644 --- a/sbt-app/src/sbt-test/dependency-management/make-ivy-xml/build.sbt +++ b/sbt-app/src/sbt-test/dependency-management/make-ivy-xml/build.sbt @@ -4,6 +4,7 @@ val descriptionValue = "This is just a test" val homepageValue = "http://example.com" lazy val root = (project in file(".")) settings( + useIvy := true, name := "ivy-xml-test", description := descriptionValue, homepage := Some(url(homepageValue)),