diff --git a/build.sc b/build.sc index 01461d7f0a7..832ee6b18e6 100644 --- a/build.sc +++ b/build.sc @@ -191,6 +191,7 @@ object Deps { val fansi = ivy"com.lihaoyi::fansi:0.5.0" val jarjarabrams = ivy"com.eed3si9n.jarjarabrams::jarjar-abrams-core:1.14.0" val requests = ivy"com.lihaoyi::requests:0.8.2" + val sonatypeCentralClient = ivy"com.lumidion::sonatype-central-client-requests:0.2.0" /** Used to manage transitive versions. */ val transitiveDeps = Seq( @@ -1008,6 +1009,12 @@ object contrib extends Module { def ivyDeps = Agg(Deps.requests) } + + object sonatypecentral extends ContribModule { + def compileModuleDeps = Seq(scalalib) + def ivyDeps = Agg(Deps.sonatypeCentralClient) + } + object versionfile extends ContribModule { def compileModuleDeps = Seq(scalalib) } diff --git a/contrib/artifactory/src/mill/contrib/artifactory/ArtifactoryPublishModule.scala b/contrib/artifactory/src/mill/contrib/artifactory/ArtifactoryPublishModule.scala index 7990efb6d04..1c2234edc5d 100644 --- a/contrib/artifactory/src/mill/contrib/artifactory/ArtifactoryPublishModule.scala +++ b/contrib/artifactory/src/mill/contrib/artifactory/ArtifactoryPublishModule.scala @@ -3,7 +3,6 @@ package mill.contrib.artifactory import mill._ import mill.api.Result import scalalib._ -import publish.Artifact import mill.contrib.artifactory.ArtifactoryPublishModule.checkArtifactoryCreds import mill.define.{ExternalModule, Task} @@ -62,8 +61,8 @@ object ArtifactoryPublishModule extends ExternalModule { connectTimeout: Int = 5000 ) = T.command { - val x: Seq[(Seq[(os.Path, String)], Artifact)] = T.sequence(publishArtifacts.value)().map { - case PublishModule.PublishData(a, s) => (s.map { case (p, f) => (p.path, f) }, a) + val artifacts = T.sequence(publishArtifacts.value)().map { + case data @ PublishModule.PublishData(_, _) => data.withConcretePath } new ArtifactoryPublisher( artifactoryUri, @@ -73,7 +72,7 @@ object ArtifactoryPublishModule extends ExternalModule { connectTimeout, T.log ).publishAll( - x: _* + artifacts: _* ) } diff --git a/contrib/codeartifact/src/mill/contrib/codeartifact/CodeartifactPublishModule.scala b/contrib/codeartifact/src/mill/contrib/codeartifact/CodeartifactPublishModule.scala index 49f4e1bd718..bd1dbd34464 100644 --- a/contrib/codeartifact/src/mill/contrib/codeartifact/CodeartifactPublishModule.scala +++ b/contrib/codeartifact/src/mill/contrib/codeartifact/CodeartifactPublishModule.scala @@ -1,6 +1,8 @@ package mill.contrib.codeartifact -import mill._, scalalib._, define.ExternalModule, publish.Artifact +import mill._ +import scalalib._ +import define.ExternalModule trait CodeartifactPublishModule extends PublishModule { def codeartifactUri: String @@ -40,12 +42,9 @@ object CodeartifactPublishModule extends ExternalModule { connectTimeout: Int = 5000 ) = T.command { - - val x: Seq[(Seq[(os.Path, String)], Artifact)] = - T.sequence(publishArtifacts.value)().map { - case PublishModule.PublishData(a, s) => - (s.map { case (p, f) => (p.path, f) }, a) - } + val artifacts = T.sequence(publishArtifacts.value)().map { + case data @ PublishModule.PublishData(_, _) => data.withConcretePath + } new CodeartifactPublisher( codeartifactUri, codeartifactSnapshotUri, @@ -54,7 +53,7 @@ object CodeartifactPublishModule extends ExternalModule { connectTimeout, T.log ).publishAll( - x: _* + artifacts: _* ) } diff --git a/contrib/gitlab/src/mill/contrib/gitlab/GitlabPublishModule.scala b/contrib/gitlab/src/mill/contrib/gitlab/GitlabPublishModule.scala index 71fca1eeed1..2efd3035ba6 100644 --- a/contrib/gitlab/src/mill/contrib/gitlab/GitlabPublishModule.scala +++ b/contrib/gitlab/src/mill/contrib/gitlab/GitlabPublishModule.scala @@ -4,7 +4,6 @@ import mill._ import mill.api.Result.{Failure, Success} import mill.api.Result import mill.define.{Command, ExternalModule, Task} -import mill.scalalib.publish.Artifact import scalalib._ trait GitlabPublishModule extends PublishModule { outer => @@ -63,11 +62,9 @@ object GitlabPublishModule extends ExternalModule { val repo = ProjectRepository(gitlabRoot, projectId) val auth = GitlabAuthHeaders.privateToken(personalToken) - val artifacts: Seq[(Seq[(os.Path, String)], Artifact)] = - T.sequence(publishArtifacts.value)().map { - case PublishModule.PublishData(a, s) => (s.map { case (p, f) => (p.path, f) }, a) - } - + val artifacts = T.sequence(publishArtifacts.value)().map { + case data @ PublishModule.PublishData(_, _) => data.withConcretePath + } val uploader = new GitlabUploader(auth, readTimeout, connectTimeout) new GitlabPublisher( diff --git a/contrib/sonatypecentral/readme.adoc b/contrib/sonatypecentral/readme.adoc new file mode 100644 index 00000000000..85aa587746c --- /dev/null +++ b/contrib/sonatypecentral/readme.adoc @@ -0,0 +1,88 @@ += Sonatype Central +:page-aliases: Plugin_Sonatype_Central.adoc + +This plugin allows users to publish open-source packages to Maven Central via the Sonatype Central portal. + +== Quickstart +Add the following to your `build.sc`: +[source,scala] +---- +import $ivy.`com.lihaoyi::mill-contrib-sonatypecentral:` +import mill.contrib.sonatypecentral.SonatypeCentralPublishModule + +object mymodule extends SonatypeCentralPublishModule { + ... +} +---- + +Then run the following to publish the individual module: + +---- +$ mill mymodule.publishSonatypeCentral +---- + +To publish several modules at once, run the following, with arguments adjusted for your use case: + +---- +$ mill -i \ +mill.contrib.sonatypecentral.SonatypeCentralPublishModule/publishAll \ +--username myusername \ +--password mypassword \ +--gpgArgs --passphrase=$GPG_PASSPHRASE,--no-tty,--pinentry-mode,loopback,--batch,--yes,-a,-b \ +--publishArtifacts __.publishArtifacts \ +--readTimeout 36000 \ +--awaitTimeout 36000 \ +--connectTimeout 36000 \ +--shouldRelease false \ +--bundleName com.lihaoyi-requests:1.0.0 +---- + + +=== Module Settings +Below are the default publishing settings on the module level, which can be explicitly configured by users like so: + +[source,scala] +---- +object mymodule extends SonatypeCentralPublishModule { + override def gpgArgs: T[String] = "--batch, --yes, -a, -b" + + override def connectTimeout: T[Int] = 5000 + + override def readTimeout: T[Int] = 60000 + + override def awaitTimeout: T[Int] = 120 * 1000 + + override def shouldRelease: T[Boolean] = true + ... +} +---- + +=== Argument Reference + +==== publishAll + +The `mill.contrib.sonatypecentral.SonatypeCentralPublishModule/publishAll` method takes the following arguments: + +`username`: The username for calling the Sonatype Central publishing api. Defaults to the `SONATYPE_USERNAME` environment variable if unset. If neither the parameter nor the environment variable are set, an error will be thrown. + + +`password`: The password for calling the Sonatype Central publishing api. Defaults to the `SONATYPE_PASSWORD` environment variable if unset. If neither the parameter nor the environment variable are set, an error will be thrown. + + +`gpgArgs`: Arguments to pass to the gpg package for signing artifacts. _Default: `--batch, --yes, -a, -b`._ + + +`publishArtifacts`: The command for generating all publishable artifacts (ex. `__.publishArtifacts`). Required. + + +`readTimeout`: The timeout for receiving a response from Sonatype Central after the initial connection has occurred. _Default: 60000._ + + +`awaitTimeout`: The overall timeout for all retries (including exponential backoff) of the bundle upload. _Default: 120 * 1000._ + + +`connectTimeout`: The timeout for the initial connection to Sonatype Central if there is no response. _Default: 5000._ + + +`shouldRelease`: Whether the bundle should be automatically released when uploaded to Sonatype Central. If `false`, the bundle will still be uploaded, but users will need to manually log in to Sonatype Central and publish the bundle from the portal. _Default: true_ + + +`bundleName`: If set, all packages will be uploaded in a single bundle with the given name. If unset, packages will be uploaded separately. Recommended bundle name syntax: groupName-artifactId-versionNumber. As an example, if publishing the `com.lihaoyi` `requests` package, without the bundle name, four different bundles will be uploaded, one for each scala version supported. With a bundle name of `com.lihaoyi-requests-`, a single bundle will be uploaded that contains all packages across scala versions. It is recommended to set the bundle name, so that packages can be verified and deployed together. _Default: No bundle name is set and packages will be uploaded separately_ + +==== publishSonatypeCentral + +The `__.publishSonatypeCentral` command takes the `username` and `password` arguments, documented above. + +` diff --git a/contrib/sonatypecentral/src/mill/contrib/sonatypecentral/SonatypeCentralPublishModule.scala b/contrib/sonatypecentral/src/mill/contrib/sonatypecentral/SonatypeCentralPublishModule.scala new file mode 100644 index 00000000000..9463761b3ba --- /dev/null +++ b/contrib/sonatypecentral/src/mill/contrib/sonatypecentral/SonatypeCentralPublishModule.scala @@ -0,0 +1,147 @@ +package mill.contrib.sonatypecentral + +import com.lumidion.sonatype.central.client.core.{PublishingType, SonatypeCredentials} +import mill._ +import scalalib._ +import define.{ExternalModule, Task} +import mill.api.Result +import mill.contrib.sonatypecentral.SonatypeCentralPublishModule.{ + defaultAwaitTimeout, + defaultConnectTimeout, + defaultCredentials, + defaultReadTimeout, + getPublishingTypeFromReleaseFlag, + getSonatypeCredentials +} +import mill.scalalib.PublishModule.{defaultGpgArgs, getFinalGpgArgs} +import mill.scalalib.publish.Artifact +import mill.scalalib.publish.SonatypeHelpers.{ + PASSWORD_ENV_VARIABLE_NAME, + USERNAME_ENV_VARIABLE_NAME +} + +trait SonatypeCentralPublishModule extends PublishModule { + def sonatypeCentralGpgArgs: T[String] = T { defaultGpgArgs.mkString(",") } + + def sonatypeCentralConnectTimeout: T[Int] = T { defaultConnectTimeout } + + def sonatypeCentralReadTimeout: T[Int] = T { defaultReadTimeout } + + def sonatypeCentralAwaitTimeout: T[Int] = T { defaultAwaitTimeout } + + def sonatypeCentralShouldRelease: T[Boolean] = T { true } + + def publishSonatypeCentral( + username: String = defaultCredentials, + password: String = defaultCredentials + ): define.Command[Unit] = + T.command { + val publishData = publishArtifacts() + val fileMapping = publishData.withConcretePath._1 + val artifact = publishData.meta + val finalCredentials = getSonatypeCredentials(username, password)() + + val publisher = new SonatypeCentralPublisher( + credentials = finalCredentials, + gpgArgs = getFinalGpgArgs(sonatypeCentralGpgArgs()), + connectTimeout = sonatypeCentralConnectTimeout(), + readTimeout = sonatypeCentralReadTimeout(), + log = T.log, + workspace = T.workspace, + env = T.env, + awaitTimeout = sonatypeCentralAwaitTimeout() + ) + publisher.publish( + fileMapping, + artifact, + getPublishingTypeFromReleaseFlag(sonatypeCentralShouldRelease()) + ) + } +} + +object SonatypeCentralPublishModule extends ExternalModule { + + val defaultCredentials: String = "" + val defaultReadTimeout: Int = 60000 + val defaultConnectTimeout: Int = 5000 + val defaultAwaitTimeout: Int = 120 * 1000 + val defaultShouldRelease: Boolean = true + + def publishAll( + publishArtifacts: mill.main.Tasks[PublishModule.PublishData], + username: String = defaultCredentials, + password: String = defaultCredentials, + shouldRelease: Boolean = defaultShouldRelease, + gpgArgs: String = defaultGpgArgs.mkString(","), + readTimeout: Int = defaultReadTimeout, + connectTimeout: Int = defaultConnectTimeout, + awaitTimeout: Int = defaultAwaitTimeout, + bundleName: String = "" + ): Command[Unit] = T.command { + + val artifacts: Seq[(Seq[(os.Path, String)], Artifact)] = + T.sequence(publishArtifacts.value)().map { + case data @ PublishModule.PublishData(_, _) => data.withConcretePath + } + + val finalBundleName = if (bundleName.isEmpty) None else Some(bundleName) + val finalCredentials = getSonatypeCredentials(username, password)() + + val publisher = new SonatypeCentralPublisher( + credentials = finalCredentials, + gpgArgs = getFinalGpgArgs(gpgArgs), + connectTimeout = connectTimeout, + readTimeout = readTimeout, + log = T.log, + workspace = T.workspace, + env = T.env, + awaitTimeout = awaitTimeout + ) + publisher.publishAll( + getPublishingTypeFromReleaseFlag(shouldRelease), + finalBundleName, + artifacts: _* + ) + } + + private def getPublishingTypeFromReleaseFlag(shouldRelease: Boolean): PublishingType = { + if (shouldRelease) { + PublishingType.AUTOMATIC + } else { + PublishingType.USER_MANAGED + } + } + + private def getSonatypeCredential( + credentialParameterValue: String, + credentialName: String, + envVariableName: String + ): Task[String] = T.task { + if (credentialParameterValue.nonEmpty) { + Result.Success(credentialParameterValue) + } else { + (for { + credential <- T.env.get(envVariableName) + } yield { + Result.Success(credential) + }).getOrElse( + Result.Failure( + s"No $credentialName set. Consider using the $envVariableName environment variable or passing `$credentialName` argument" + ) + ) + } + } + + private def getSonatypeCredentials( + usernameParameterValue: String, + passwordParameterValue: String + ): Task[SonatypeCredentials] = T.task { + val username = + getSonatypeCredential(usernameParameterValue, "username", USERNAME_ENV_VARIABLE_NAME)() + val password = + getSonatypeCredential(passwordParameterValue, "password", PASSWORD_ENV_VARIABLE_NAME)() + Result.Success(SonatypeCredentials(username, password)) + } + + lazy val millDiscover: mill.define.Discover[this.type] = mill.define.Discover[this.type] +} diff --git a/contrib/sonatypecentral/src/mill/contrib/sonatypecentral/SonatypeCentralPublisher.scala b/contrib/sonatypecentral/src/mill/contrib/sonatypecentral/SonatypeCentralPublisher.scala new file mode 100644 index 00000000000..edb82cf00ab --- /dev/null +++ b/contrib/sonatypecentral/src/mill/contrib/sonatypecentral/SonatypeCentralPublisher.scala @@ -0,0 +1,135 @@ +package mill.contrib.sonatypecentral + +import com.lumidion.sonatype.central.client.core.{ + DeploymentName, + PublishingType, + SonatypeCredentials +} +import com.lumidion.sonatype.central.client.requests.SyncSonatypeClient +import mill.api.Logger +import mill.scalalib.publish.Artifact +import mill.scalalib.publish.SonatypeHelpers.getArtifactMappings + +import java.io.FileOutputStream +import java.util.jar.JarOutputStream +import java.util.zip.ZipEntry + +class SonatypeCentralPublisher( + credentials: SonatypeCredentials, + gpgArgs: Seq[String], + readTimeout: Int, + connectTimeout: Int, + log: Logger, + workspace: os.Path, + env: Map[String, String], + awaitTimeout: Int +) { + private val sonatypeCentralClient = + new SyncSonatypeClient(credentials, readTimeout = readTimeout, connectTimeout = connectTimeout) + + def publish( + fileMapping: Seq[(os.Path, String)], + artifact: Artifact, + publishingType: PublishingType + ): Unit = { + publishAll(publishingType, None, fileMapping -> artifact) + } + + def publishAll( + publishingType: PublishingType, + singleBundleName: Option[String], + artifacts: (Seq[(os.Path, String)], Artifact)* + ): Unit = { + val mappings = getArtifactMappings(isSigned = true, gpgArgs, workspace, env, artifacts) + + val (_, releases) = mappings.partition(_._1.isSnapshot) + + val releaseGroups = releases.groupBy(_._1.group) + val wd = os.pwd / "out" / "publish-central" + os.makeDir.all(wd) + + singleBundleName.fold { + for ((_, groupReleases) <- releaseGroups) { + groupReleases.foreach { case (artifact, data) => + val fileNameWithoutExtension = s"${artifact.group}-${artifact.id}-${artifact.version}" + val zipFile = streamToFile(fileNameWithoutExtension, wd) { outputStream => + zipFilesToJar(data, outputStream) + } + + val deploymentName = DeploymentName.fromArtifact( + artifact.group, + artifact.id, + artifact.version + ) + + publishFile(zipFile, deploymentName, publishingType) + } + } + + } { singleBundleName => + val zipFile = streamToFile(singleBundleName, wd) { outputStream => + for ((_, groupReleases) <- releaseGroups) { + groupReleases.foreach { case (_, data) => + zipFilesToJar(data, outputStream) + } + } + } + + val deploymentName = DeploymentName(singleBundleName) + + publishFile(zipFile, deploymentName, publishingType) + } + } + + private def publishFile( + zipFile: java.io.File, + deploymentName: DeploymentName, + publishingType: PublishingType + ): Unit = { + try { + sonatypeCentralClient.uploadBundleFromFile( + zipFile, + deploymentName, + Some(publishingType), + timeout = awaitTimeout + ) + } catch { + case ex: Throwable => { + throw new RuntimeException( + s"Failed to publish ${deploymentName.unapply} to Sonatype Central. Error: \n${ex.getMessage}" + ) + } + + } + + log.info(s"Successfully published ${deploymentName.unapply} to Sonatype Central") + } + + private def streamToFile( + fileNameWithoutExtension: String, + wd: os.Path + )(func: JarOutputStream => Unit): java.io.File = { + val zipFile = + (wd / s"$fileNameWithoutExtension.zip").toIO + val fileOutputStream = new FileOutputStream(zipFile) + val jarOutputStream = new JarOutputStream(fileOutputStream) + try { + func(jarOutputStream) + } finally { + jarOutputStream.close() + } + zipFile + } + + private def zipFilesToJar( + files: Seq[(String, Array[Byte])], + jarOutputStream: JarOutputStream + ): Unit = { + files.foreach { case (filename, fileAsBytes) => + val zipEntry = new ZipEntry(filename) + jarOutputStream.putNextEntry(zipEntry) + jarOutputStream.write(fileAsBytes) + jarOutputStream.closeEntry() + } + } +} diff --git a/scalalib/src/mill/scalalib/PublishModule.scala b/scalalib/src/mill/scalalib/PublishModule.scala index af133158b06..38cc609616b 100644 --- a/scalalib/src/mill/scalalib/PublishModule.scala +++ b/scalalib/src/mill/scalalib/PublishModule.scala @@ -4,6 +4,10 @@ package scalalib import mill.define.{Command, ExternalModule, Target, Task} import mill.api.{JarManifest, PathRef, Result} import mill.scalalib.PublishModule.checkSonatypeCreds +import mill.scalalib.publish.SonatypeHelpers.{ + PASSWORD_ENV_VARIABLE_NAME, + USERNAME_ENV_VARIABLE_NAME +} import mill.scalalib.publish.{Artifact, SonatypePublisher} import os.Path @@ -253,7 +257,14 @@ trait PublishModule extends JavaModule { outer => object PublishModule extends ExternalModule { val defaultGpgArgs: Seq[String] = Seq("--batch", "--yes", "-a", "-b") - case class PublishData(meta: Artifact, payload: Seq[(PathRef, String)]) + case class PublishData(meta: Artifact, payload: Seq[(PathRef, String)]) { + + /** + * Maps the path reference to an actual path so that it can be used in publishAll signatures + */ + private[mill] def withConcretePath: (Seq[(Path, String)], Artifact) = + (payload.map { case (p, f) => (p.path, f) }, meta) + } object PublishData { implicit def jsonify: upickle.default.ReadWriter[PublishData] = upickle.default.macroRW } @@ -292,7 +303,7 @@ object PublishModule extends ExternalModule { sonatypeSnapshotUri, checkSonatypeCreds(sonatypeCreds)(), signed, - gpgArgs.split(",").toIndexedSeq, + getFinalGpgArgs(gpgArgs), readTimeout, connectTimeout, T.log, @@ -306,22 +317,44 @@ object PublishModule extends ExternalModule { ) } - private[scalalib] def checkSonatypeCreds(sonatypeCreds: String): Task[String] = T.task { - if (sonatypeCreds.isEmpty) { - (for { - username <- T.env.get("SONATYPE_USERNAME") - password <- T.env.get("SONATYPE_PASSWORD") - } yield { - Result.Success(s"$username:$password") - }).getOrElse( - Result.Failure( - "Consider using SONATYPE_USERNAME/SONATYPE_PASSWORD environment variables or passing `sonatypeCreds` argument" - ) - ) + private[mill] def getFinalGpgArgs(initialGpgArgs: String): Seq[String] = { + val argsAsString = if (initialGpgArgs.isEmpty) { + defaultGpgArgs.mkString(",") } else { - Result.Success(sonatypeCreds) + initialGpgArgs } + argsAsString.split(",").toIndexedSeq } + private def getSonatypeCredsFromEnv: Task[(String, String)] = T.task { + (for { + username <- T.env.get(USERNAME_ENV_VARIABLE_NAME) + password <- T.env.get(PASSWORD_ENV_VARIABLE_NAME) + } yield { + Result.Success((username, password)) + }).getOrElse( + Result.Failure( + s"Consider using ${USERNAME_ENV_VARIABLE_NAME}/${PASSWORD_ENV_VARIABLE_NAME} environment variables or passing `sonatypeCreds` argument" + ) + ) + } + + private[scalalib] def checkSonatypeCreds(sonatypeCreds: String): Task[String] = + if (sonatypeCreds.isEmpty) { + for { + (username, password) <- getSonatypeCredsFromEnv + } yield s"$username:$password" + } else { + T.task { + if (sonatypeCreds.split(":").length >= 2) { + Result.Success(sonatypeCreds) + } else { + Result.Failure( + "Sonatype credentials must be set in the following format - username:password. Incorrect format received." + ) + } + } + } + lazy val millDiscover: mill.define.Discover[this.type] = mill.define.Discover[this.type] } diff --git a/scalalib/src/mill/scalalib/publish/SonatypeHelpers.scala b/scalalib/src/mill/scalalib/publish/SonatypeHelpers.scala new file mode 100644 index 00000000000..882e039022d --- /dev/null +++ b/scalalib/src/mill/scalalib/publish/SonatypeHelpers.scala @@ -0,0 +1,73 @@ +package mill.scalalib.publish + +import mill.util.Jvm + +import java.math.BigInteger +import java.security.MessageDigest + +object SonatypeHelpers { + // http://central.sonatype.org/pages/working-with-pgp-signatures.html#signing-a-file + + val USERNAME_ENV_VARIABLE_NAME = "SONATYPE_USERNAME" + val PASSWORD_ENV_VARIABLE_NAME = "SONATYPE_PASSWORD" + + private[mill] def getArtifactMappings( + isSigned: Boolean, + gpgArgs: Seq[String], + workspace: os.Path, + env: Map[String, String], + artifacts: Seq[(Seq[(os.Path, String)], Artifact)] + ): Seq[(Artifact, Seq[(String, Array[Byte])])] = { + for ((fileMapping0, artifact) <- artifacts) yield { + val publishPath = Seq( + artifact.group.replace(".", "/"), + artifact.id, + artifact.version + ).mkString("/") + val fileMapping = fileMapping0.map { case (file, name) => (file, publishPath + "/" + name) } + + val signedArtifacts = + if (isSigned) fileMapping.map { + case (file, name) => + gpgSigned(file = file, args = gpgArgs, workspace = workspace, env = env) -> s"$name.asc" + } + else Seq() + + artifact -> (fileMapping ++ signedArtifacts).flatMap { + case (file, name) => + val content = os.read.bytes(file) + + Seq( + name -> content, + (name + ".md5") -> md5hex(content), + (name + ".sha1") -> sha1hex(content) + ) + } + } + } + private def gpgSigned( + file: os.Path, + args: Seq[String], + workspace: os.Path, + env: Map[String, String] + ): os.Path = { + val fileName = file.toString + val command = "gpg" +: args :+ fileName + + Jvm.runSubprocess(command, env, workspace) + os.Path(fileName + ".asc") + } + + private def md5hex(bytes: Array[Byte]): Array[Byte] = + hexArray(md5.digest(bytes)).getBytes + + private def sha1hex(bytes: Array[Byte]): Array[Byte] = + hexArray(sha1.digest(bytes)).getBytes + + private def md5 = MessageDigest.getInstance("md5") + + private def sha1 = MessageDigest.getInstance("sha1") + + private def hexArray(arr: Array[Byte]) = + String.format("%0" + (arr.length << 1) + "x", new BigInteger(1, arr)) +} diff --git a/scalalib/src/mill/scalalib/publish/SonatypePublisher.scala b/scalalib/src/mill/scalalib/publish/SonatypePublisher.scala index 4d024efea71..024c357de59 100644 --- a/scalalib/src/mill/scalalib/publish/SonatypePublisher.scala +++ b/scalalib/src/mill/scalalib/publish/SonatypePublisher.scala @@ -1,10 +1,8 @@ package mill.scalalib.publish -import java.math.BigInteger -import java.security.MessageDigest - import mill.api.Logger -import mill.util.Jvm + +import mill.scalalib.publish.SonatypeHelpers.getArtifactMappings class SonatypePublisher( uri: String, @@ -59,31 +57,7 @@ class SonatypePublisher( } def publishAll(release: Boolean, artifacts: (Seq[(os.Path, String)], Artifact)*): Unit = { - val mappings = for ((fileMapping0, artifact) <- artifacts) yield { - val publishPath = Seq( - artifact.group.replace(".", "/"), - artifact.id, - artifact.version - ).mkString("/") - val fileMapping = fileMapping0.map { case (file, name) => (file, publishPath + "/" + name) } - - val signedArtifacts = - if (signed) fileMapping.map { - case (file, name) => gpgSigned(file, gpgArgs) -> s"$name.asc" - } - else Seq() - - artifact -> (fileMapping ++ signedArtifacts).flatMap { - case (file, name) => - val content = os.read.bytes(file) - - Seq( - name -> content, - (name + ".md5") -> md5hex(content), - (name + ".sha1") -> sha1hex(content) - ) - } - } + val mappings = getArtifactMappings(signed, gpgArgs, workspace, env, artifacts) val (snapshots, releases) = mappings.partition(_._1.isSnapshot) if (snapshots.nonEmpty) { @@ -196,27 +170,4 @@ class SonatypePublisher( } } } - - // http://central.sonatype.org/pages/working-with-pgp-signatures.html#signing-a-file - private def gpgSigned(file: os.Path, args: Seq[String]): os.Path = { - val fileName = file.toString - val command = "gpg" +: args :+ fileName - - Jvm.runSubprocess(command, env, workspace) - os.Path(fileName + ".asc") - } - - private def md5hex(bytes: Array[Byte]): Array[Byte] = - hexArray(md5.digest(bytes)).getBytes - - private def sha1hex(bytes: Array[Byte]): Array[Byte] = - hexArray(sha1.digest(bytes)).getBytes - - private def md5 = MessageDigest.getInstance("md5") - - private def sha1 = MessageDigest.getInstance("sha1") - - private def hexArray(arr: Array[Byte]) = - String.format("%0" + (arr.length << 1) + "x", new BigInteger(1, arr)) - }