diff --git a/README.md b/README.md index bf0e2fb..65cbe3a 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ promoting secure builds. ## Highlights - Uses the system command `gpg` to do all operations. *This enables - advanced features such as use of smartcards or cutting-edge + advanced features such as use of smartcards, key splitting, or cutting-edge ciphers.* -- Hooks into the `publish` and `publishLocal` tasks. *All artrifacts +- Hooks into the `publish` and `publishLocal` tasks. *All artifacts will be signed; there is no need to run a separate `publishSigned` task.* @@ -28,9 +28,9 @@ promoting secure builds. `credentials` mechanism, thus enabling global configuration without the need of adding a global plugin.* -- Works out-of-the-box. *Publishing falls back to unsigned artifacts +- Works out-of-the-box. *`publishLocal` falls back to unsigned artifacts in case key material cannot be found, after emitting an explicit - warning.* + warning. `publish` will fail the build by default if signing fails to avoid accidentally publishing unsigned artifacts, though you can override this with a setting.* ## Requirements @@ -40,13 +40,13 @@ promoting secure builds. functionality provided by this plugin) ## Getting started -```scala +```sbt addSbtPlugin("io.crashbox" % "sbt-gpg" % "") ``` Copy the above snippet to an sbt configuration file. E.g. - `project/plugins.sbt` to enable the plugin on a per-project basis -- `~/.sbt/1.0/plugins/gpg.sbt` to enable the plugin globally +- `~/.sbt/1.0/plugins/gpg.sbt` to enable the plugin globally (not recommended) That's it! The autoplugin "SbtGpg" will now be enabled for the given project(s). It will modify the `publish` and `publishLocal` tasks to @@ -62,7 +62,7 @@ By default, all signing operations will use `gpg`'s default key. A specific key can be used by setting sbt `Credentials` for the host "gpg". -```scala +```sbt credentials += Credentials( "GnuPG Key ID", "gpg", diff --git a/src/main/scala/Gpg.scala b/src/main/scala/Gpg.scala index cde4dba..edd1935 100644 --- a/src/main/scala/Gpg.scala +++ b/src/main/scala/Gpg.scala @@ -5,19 +5,22 @@ import java.io.File import scala.util.control.NonFatal import sys.process._ -class Gpg( - command: String, - options: Seq[String] = Seq.empty, - keyId: Option[String] = None)(log: String => Unit = System.err.println) { +class Gpg(command: String, + options: Seq[String] = Seq.empty, + keyId: Option[String] = None)( + info: String => Unit = System.out.println, + warn: String => Unit = System.err.println) { + + private val logger = ProcessLogger(info, info) // gpg uses stderr for everything; redirect to info def run(params: String*): Int = try { val idOption = keyId.toSeq.flatMap(id => Seq("--local-user", id)) - val process = Process(command, options ++ idOption ++ params).run() + val process = Process(command, options ++ idOption ++ params).run(logger) process.exitValue() } catch { case NonFatal(ex) => - log(ex.getMessage) + warn(ex.getMessage) 127 } diff --git a/src/main/scala/SbtGpg.scala b/src/main/scala/SbtGpg.scala index 17eff8d..d3d82b9 100644 --- a/src/main/scala/SbtGpg.scala +++ b/src/main/scala/SbtGpg.scala @@ -10,48 +10,79 @@ object SbtGpg extends AutoPlugin { override def trigger = allRequirements object autoImport { + + val gpgWarnOnFailure = settingKey[Boolean]( + "If true, only issue a warning when signing fails. If false, error " + + "and fail the build. Defaults to true in publishLocal, false in publish.") + val gpgCommand = settingKey[String]("Path to GnuPG executable.") + val gpgOptions = settingKey[Seq[String]]("Additional global options to pass to gpg.") + val gpgKey = taskKey[Option[String]]( "Key ID used to sign artifacts. Setting this to None will " + "cause sbt-gpg to fall back to using gpg's default key. When set, " + "it is equivalent to gpg's `--local-user` option.") + val gpg = taskKey[Gpg]("Utility wrapper to the underlying gpg executable.") } + + def packagedArtifactsImpl( + arts: Map[Artifact, File], + gpg: Gpg, + warnOnFailure: Boolean)(warn: String => Unit): Map[Artifact, File] = { + + val (signatures, failure) = arts.foldLeft((Map[Artifact, File](), false)) { + case ((acc, false), (art, file)) => + gpg.sign(file) match { + case Some(signed) => + (acc + (art.withExtension(art.extension + ".asc") -> signed), false) + + case None => + val report: String => Unit = + if (warnOnFailure) warn else sys.error(_) + + report("GPG reported an error. Artifacts won't be signed.") + (acc, true) + } + + case (pair @ (_, true), _) => pair + } + + // if we fail the signing part-way through, we throw out *all* the signatures + if (failure) arts else signatures ++ arts + } + import autoImport._ lazy val gpgSettings: Seq[Setting[_]] = Seq( + gpgWarnOnFailure := false, + publishLocal / gpgWarnOnFailure := true, gpgCommand := "gpg", gpgOptions := Seq("--yes"), gpgKey := Credentials.forHost(credentials.value, "gpg").map(_.userName), gpg := { val log = streams.value.log - new Gpg(gpgCommand.value, gpgOptions.value, gpgKey.value)(log.warn(_)) + new Gpg(gpgCommand.value, gpgOptions.value, gpgKey.value)(log.info(_), + log.warn(_)) } ) lazy val signingSettings: Seq[Setting[_]] = Seq( - packagedArtifacts := { - val log = streams.value.log - val arts: Map[Artifact, File] = packagedArtifacts.value - var failed = false - arts.flatMap { - case (art, file) if !failed => - gpg.value.sign(file) match { - case Some(signed) => - Map( - art -> file, - art.withExtension(art.extension + ".asc") -> signed - ) - case None => - log.warn("GPG reported an error. Artifacts won't be signed.") - failed = true - Map(art -> file) - } - case (art, file) => Map(art -> file) - } + publish / packagedArtifacts := { + packagedArtifactsImpl( + (publish / packagedArtifacts).value, + gpg.value, + (publish / gpgWarnOnFailure).value)(streams.value.log.warn(_)) + }, + publishLocal / packagedArtifacts := { + packagedArtifactsImpl( + (publishLocal / packagedArtifacts).value, + gpg.value, + (publishLocal / gpgWarnOnFailure).value)(streams.value.log.warn(_)) + } ) diff --git a/src/sbt-test/sbt-gpg/simple/build.sbt b/src/sbt-test/sbt-gpg/simple/build.sbt index 1f5dce9..8901fd5 100644 --- a/src/sbt-test/sbt-gpg/simple/build.sbt +++ b/src/sbt-test/sbt-gpg/simple/build.sbt @@ -17,7 +17,7 @@ lazy val root = project .settings( TaskKey[Unit]("check") := { val artifacts: Map[Artifact, java.io.File] = - packagedArtifacts.value + (publish / packagedArtifacts).value // check that every artifact is signed and that the actual signature file // exists