Skip to content

Commit

Permalink
Allow to publish to Maven Central in publish command
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarchambault committed May 10, 2022
1 parent f36195a commit c693b5a
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,24 @@ final case class SharedPublishOptions(
@ExtraName("publishRepo")
publishRepository: Option[String] = None,

@Group("Publishing")
@HelpMessage("User to use with publishing repository")
@ValueDescription("user")
user: Option[PasswordOption] = None,

@Group("Publishing")
@HelpMessage("Password to use with publishing repository")
@ValueDescription("value:…")
password: Option[PasswordOption] = None,

@Group("Publishing")
@HelpMessage("Secret key to use to sign artifacts with BouncyCastle")
secretKey: Option[PasswordOption] = None,

@Group("Publishing")
@HelpMessage("Public key to use to verify artifacts (to be uploaded to a key server)")
publicKey: Option[PasswordOption] = None,

@Group("Publishing")
@HelpMessage("Password of secret key to use to sign artifacts with BouncyCastle")
@ValueDescription("value:…")
Expand Down
87 changes: 72 additions & 15 deletions modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,37 @@ package scala.cli.commands.publish

import caseapp.core.RemainingArgs
import coursier.cache.ArchiveCache
import coursier.core.Configuration
import coursier.core.{Authentication, Configuration}
import coursier.maven.MavenRepository
import coursier.publish.checksum.logger.InteractiveChecksumLogger
import coursier.publish.checksum.{ChecksumType, Checksums}
import coursier.publish.fileset.{FileSet, Path}
import coursier.publish.signing.logger.InteractiveSignerLogger
import coursier.publish.signing.{GpgSigner, NopSigner, Signer}
import coursier.publish.sonatype.SonatypeApi
import coursier.publish.upload.logger.InteractiveUploadLogger
import coursier.publish.upload.{FileUpload, HttpURLConnectionUpload}
import coursier.publish.{Content, Pom}
import coursier.publish.{Content, Hooks, Pom, PublishRepository}

import java.io.OutputStreamWriter
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.{Path => NioPath, Paths}
import java.time.Instant
import java.util.concurrent.Executors
import java.util.function.Supplier

import scala.build.EitherCps.{either, value}
import scala.build.Ops._
import scala.build.errors.{BuildException, CompositeBuildException, NoMainClassFoundError}
import scala.build.internal.Util
import scala.build.internal.Util.ScalaDependencyOps
import scala.build.options.publish.{ComputeVersion, Developer, License, Signer => PSigner, Vcs}
import scala.build.options.{BuildOptions, ConfigMonoid, Scope}
import scala.build.{Build, BuildThreads, Builds, Logger, Os, Positioned}
import scala.cli.CurrentParams
import scala.cli.commands.pgp.PgpExternalCommand
import scala.cli.commands.util.ScalaCliSttpBackend
import scala.cli.commands.util.SharedOptionsUtil._
import scala.cli.commands.{Package => PackageCmd, ScalaCommand, WatchUtil}
import scala.cli.errors.{FailedToSignFileError, MissingPublishOptionError, UploadError}
Expand Down Expand Up @@ -90,6 +94,8 @@ object Publish extends ScalaCommand[PublishOptions] {
gpgOptions = gpgOption,
secretKey = sharedPublish.secretKey,
secretKeyPassword = sharedPublish.secretKeyPassword,
repoUser = sharedPublish.user,
repoPassword = sharedPublish.password,
signer = value {
signer
.map(Positioned.commandLine(_))
Expand Down Expand Up @@ -218,7 +224,7 @@ object Publish extends ScalaCommand[PublishOptions] {
workingDir: os.Path,
now: Instant,
logger: Logger
): Either[BuildException, FileSet] = either {
): Either[BuildException, (FileSet, String)] = either {

logger.debug(s"Preparing project ${build.project.projectName}")

Expand Down Expand Up @@ -353,7 +359,7 @@ object Publish extends ScalaCommand[PublishOptions] {
.toSeq

// TODO version listings, …
FileSet(mainEntries ++ sourceJarEntries ++ docJarEntries)
(FileSet(mainEntries ++ sourceJarEntries ++ docJarEntries), ver)
}

private def doPublish(
Expand All @@ -371,7 +377,7 @@ object Publish extends ScalaCommand[PublishOptions] {
}

val now = Instant.now()
val fileSet0 = value {
val (fileSet0, versionOpt) = value {
it
// TODO Allow to add test JARs to the main build artifacts
.filter(_._1.scope != Scope.Test)
Expand All @@ -380,10 +386,16 @@ object Publish extends ScalaCommand[PublishOptions] {
buildFileSet(build, docBuildOpt, workingDir, now, logger)
}
.sequence0
.map(_.foldLeft(FileSet.empty)(_ ++ _))
.map { l =>
val fs = l.map(_._1).foldLeft(FileSet.empty)(_ ++ _)
val versionOpt0 = l.headOption.map(_._2)
(fs, versionOpt0)
}
}

val ec = builds.head.options.finalCache.ec
lazy val es =
Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry"))

val publishOptions = ConfigMonoid.sum(
builds.map(_.options.notForBloopOptions.publishOptions)
Expand Down Expand Up @@ -462,37 +474,82 @@ object Publish extends ScalaCommand[PublishOptions] {

val finalFileSet = fileSet2.order(ec).unsafeRun()(ec)

val repo = publishOptions.repository match {
lazy val authOpt = {
val userOpt = publishOptions.repoUser
val passwordOpt = publishOptions.repoPassword.map(_.get())
passwordOpt.map { password =>
Authentication(userOpt.fold("")(_.get().value), password.value)
}
}

def centralRepo(base: String) = {
val repo0 = {
val r = PublishRepository.Sonatype(MavenRepository(base))
authOpt.fold(r)(r.withAuthentication)
}
val backend = ScalaCliSttpBackend.httpURLConnection(logger)
val api = SonatypeApi(backend, base + "/service/local", authOpt, logger.verbosity)
val hooks0 = Hooks.sonatype(
repo0,
api,
logger.compilerOutputStream, // meh
logger.verbosity,
batch = coursier.paths.Util.useAnsiOutput(), // FIXME Get via logger
es
)
(repo0, hooks0)
}

val (repo, hooks) = publishOptions.repository match {
case None =>
value(Left(new MissingPublishOptionError(
"repository",
"--publish-repository",
"publish.repository"
)))
case Some(repo) =>
val url =
if (repo.contains("://")) repo
else os.Path(repo, Os.pwd).toNIO.toUri.toASCIIString
MavenRepository(url)
case Some("central" | "maven-central" | "mvn-central") =>
centralRepo("https://oss.sonatype.org")
case Some("central-s01" | "maven-central-s01" | "mvn-central-s01") =>
centralRepo("https://s01.oss.sonatype.org")
case Some(repoStr) =>
val repo0 = RepositoryParser.repositoryOpt(repoStr)
.collect {
case m: MavenRepository =>
m
}
.getOrElse {
val url =
if (repoStr.contains("://")) repoStr
else os.Path(repoStr, Os.pwd).toNIO.toUri.toASCIIString
MavenRepository(url)
}
(PublishRepository.Simple(repo0), Hooks.dummy)
}

val isSnapshot0 = versionOpt.exists(_.endsWith("SNAPSHOT"))
val hooksData = hooks.beforeUpload(finalFileSet, isSnapshot0).unsafeRun()(ec)

val retainedRepo = hooks.repository(hooksData, repo, isSnapshot0)
.getOrElse(repo.repo(isSnapshot0))

val upload =
if (repo.root.startsWith("http://") || repo.root.startsWith("https://"))
if (retainedRepo.root.startsWith("http://") || retainedRepo.root.startsWith("https://"))
HttpURLConnectionUpload.create()
else
FileUpload(Paths.get(new URI(repo.root)))
FileUpload(Paths.get(new URI(retainedRepo.root)))

val dummy = false
val isLocal = true
val uploadLogger = InteractiveUploadLogger.create(System.err, dummy = dummy, isLocal = isLocal)

val errors =
upload.uploadFileSet(repo, finalFileSet, uploadLogger, Some(ec)).unsafeRun()(ec)
upload.uploadFileSet(retainedRepo, finalFileSet, uploadLogger, Some(ec)).unsafeRun()(ec)

errors.toList match {
case h :: t =>
value(Left(new UploadError(::(h, t))))
case Nil =>
hooks.afterUpload(hooksData).unsafeRun()(ec)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package scala.cli.commands.publish

// from coursier.internal.SharedRepositoryParser
// delete when coursier.internal.SharedRepositoryParser.repositoryOpt is available for us

import coursier.Repositories
import coursier.core.Repository
import coursier.ivy.IvyRepository
import coursier.maven.MavenRepository

object RepositoryParser {

def repositoryOpt(s: String): Option[Repository] =
if (s == "central")
Some(Repositories.central)
else if (s.startsWith("sonatype:"))
Some(Repositories.sonatype(s.stripPrefix("sonatype:")))
else if (s.startsWith("bintray:")) {
val s0 = s.stripPrefix("bintray:")
val id =
if (s.contains("/")) s0
else s0 + "/maven"

Some(Repositories.bintray(id))
}
else if (s.startsWith("bintray-ivy:"))
Some(Repositories.bintrayIvy(s.stripPrefix("bintray-ivy:")))
else if (s.startsWith("typesafe:ivy-"))
Some(Repositories.typesafeIvy(s.stripPrefix("typesafe:ivy-")))
else if (s.startsWith("typesafe:"))
Some(Repositories.typesafe(s.stripPrefix("typesafe:")))
else if (s.startsWith("sbt-maven:"))
Some(Repositories.sbtMaven(s.stripPrefix("sbt-maven:")))
else if (s.startsWith("sbt-plugin:"))
Some(Repositories.sbtPlugin(s.stripPrefix("sbt-plugin:")))
else if (s == "scala-integration" || s == "scala-nightlies")
Some(Repositories.scalaIntegration)
else if (s == "jitpack")
Some(Repositories.jitpack)
else if (s == "clojars")
Some(Repositories.clojars)
else if (s == "jcenter")
Some(Repositories.jcenter)
else if (s == "google")
Some(Repositories.google)
else if (s == "gcs")
Some(Repositories.centralGcs)
else if (s == "gcs-eu")
Some(Repositories.centralGcsEu)
else if (s == "gcs-asia")
Some(Repositories.centralGcsAsia)
else if (s.startsWith("apache:"))
Some(Repositories.apache(s.stripPrefix("apache:")))
else
None

def repository(s: String): Either[String, Repository] =
repositoryOpt(s) match {
case Some(repo) => Right(repo)
case None =>
if (s.startsWith("ivy:")) {
val s0 = s.stripPrefix("ivy:")
val sepIdx = s0.indexOf('|')
if (sepIdx < 0)
IvyRepository.parse(s0)
else {
val mainPart = s0.substring(0, sepIdx)
val metadataPart = s0.substring(sepIdx + 1)
IvyRepository.parse(mainPart, Some(metadataPart))
}
}
else
Right(MavenRepository(s))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ case object UsingPublishDirectiveHandler extends UsingDirectiveHandler {
"gpgOptions",
"gpg-options",
"secretKey",
"secretKeyPassword"
"secretKeyPassword",
"user",
"password"
).map(prefix + _)

override def getValueNumberBounds(key: String) = key match {
Expand Down Expand Up @@ -134,6 +136,10 @@ case object UsingPublishDirectiveHandler extends UsingDirectiveHandler {
PublishOptions(secretKey = Some(value(parsePasswordOption(singleValue.value))))
case "secretKeyPassword" =>
PublishOptions(secretKeyPassword = Some(value(parsePasswordOption(singleValue.value))))
case "user" =>
PublishOptions(repoUser = Some(value(parsePasswordOption(singleValue.value))))
case "password" =>
PublishOptions(repoPassword = Some(value(parsePasswordOption(singleValue.value))))
case _ =>
value(Left(new UnexpectedDirectiveError(scopedDirective.directive.key)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ final case class PublishOptions(
signer: Option[Signer] = None,
secretKey: Option[PasswordOption] = None,
secretKeyPassword: Option[PasswordOption] = None,
repoUser: Option[PasswordOption] = None,
repoPassword: Option[PasswordOption] = None,
computeVersion: Option[ComputeVersion] = None
)

Expand Down
12 changes: 12 additions & 0 deletions website/docs/reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1003,10 +1003,22 @@ Aliases: `-R`, `--publish-repo`

Repository to publish to

#### `--user`

User to use with publishing repository

#### `--password`

Password to use with publishing repository

#### `--secret-key`

Secret key to use to sign artifacts with BouncyCastle

#### `--public-key`

Public key to use to verify artifacts (to be uploaded to a key server)

#### `--secret-key-password`

Aliases: `--secret-key-pass`
Expand Down

0 comments on commit c693b5a

Please sign in to comment.