Skip to content

Commit 9b76b44

Browse files
Add signing support in publish command
Using both gpg and bouncycastle
1 parent 543d076 commit 9b76b44

File tree

33 files changed

+823
-46
lines changed

33 files changed

+823
-46
lines changed

build.sc

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ class Core(val crossScalaVersion: String) extends BuildLikeModule {
317317
|
318318
| def defaultGraalVMJavaVersion = ${deps.graalVmJavaVersion}
319319
| def defaultGraalVMVersion = "${deps.graalVmVersion}"
320+
|
321+
| def scalaCliSigningVersion = "${Deps.signingCli.dep.version}"
320322
|}
321323
|""".stripMargin
322324
if (!os.isFile(dest) || os.read(dest) != code)
@@ -391,7 +393,10 @@ class Options(val crossScalaVersion: String) extends BuildLikeModule {
391393
super.scalacOptions() ++ asyncScalacOptions(scalaVersion())
392394
}
393395

394-
def ivyDeps = super.ivyDeps() ++ Agg(Deps.bloopConfig)
396+
def ivyDeps = super.ivyDeps() ++ Agg(
397+
Deps.bloopConfig,
398+
Deps.signingCliShared
399+
)
395400

396401
object test extends Tests {
397402
// uncomment below to debug tests in attach mode on 5005 port
@@ -470,9 +475,15 @@ class Build(val crossScalaVersion: String) extends BuildLikeModule {
470475
}
471476

472477
trait CliOptions extends SbtModule with ScalaCliPublishModule with settings.ScalaCliCompile {
473-
def ivyDeps =
474-
super.ivyDeps() ++ Agg(Deps.caseApp, Deps.jsoniterCore, Deps.jsoniterMacros, Deps.osLib)
478+
def ivyDeps = super.ivyDeps() ++ Agg(
479+
Deps.caseApp,
480+
Deps.jsoniterCore,
481+
Deps.jsoniterMacros,
482+
Deps.osLib,
483+
Deps.signingCliShared
484+
)
475485
def scalaVersion = Scala.defaultInternal
486+
def repositories = super.repositories ++ customRepositories
476487
}
477488

478489
trait Cli extends SbtModule with CliLaunchers with ScalaCliPublishModule with FormatNativeImageConf
@@ -501,6 +512,7 @@ trait Cli extends SbtModule with CliLaunchers with ScalaCliPublishModule with Fo
501512
Deps.jniUtils,
502513
Deps.jsoniterCore,
503514
Deps.scalaPackager,
515+
Deps.signingCli,
504516
Deps.metaconfigTypesafe
505517
)
506518
def compileIvyDeps = super.compileIvyDeps() ++ Agg(
@@ -535,15 +547,22 @@ trait CliIntegrationBase extends SbtModule with ScalaCliPublishModule with HasTe
535547
super.scalacOptions() ++ Seq("-Xasync", "-Ywarn-unused", "-deprecation")
536548
}
537549

538-
def sources = T.sources {
550+
def modulesPath = T {
539551
val name = mainArtifactName().stripPrefix(prefix)
540552
val baseIntegrationPath = os.Path(millSourcePath.toString.stripSuffix(name))
541-
val modulesPath = os.Path(
553+
val p = os.Path(
542554
baseIntegrationPath.toString.stripSuffix(baseIntegrationPath.baseName)
543555
)
544-
val mainPath = PathRef(modulesPath / "integration" / "src" / "main" / "scala")
556+
PathRef(p)
557+
}
558+
def sources = T.sources {
559+
val mainPath = PathRef(modulesPath().path / "integration" / "src" / "main" / "scala")
545560
super.sources() ++ Seq(mainPath)
546561
}
562+
def resources = T.sources {
563+
val mainPath = PathRef(modulesPath().path / "integration" / "src" / "main" / "resources")
564+
super.resources() ++ Seq(mainPath)
565+
}
547566

548567
def ivyDeps = super.ivyDeps() ++ Agg(
549568
Deps.osLib
@@ -566,15 +585,23 @@ trait CliIntegrationBase extends SbtModule with ScalaCliPublishModule with HasTe
566585
"SCALA_CLI_TMP" -> tmpDirBase().path.toString,
567586
"CI" -> "1"
568587
)
588+
private def updateRef(name: String, ref: PathRef): PathRef = {
589+
val rawPath = ref.path.toString.replace(
590+
File.separator + name + File.separator,
591+
File.separator
592+
)
593+
PathRef(os.Path(rawPath))
594+
}
569595
def sources = T.sources {
570596
val name = mainArtifactName().stripPrefix(prefix)
571597
super.sources().flatMap { ref =>
572-
val rawPath = ref.path.toString.replace(
573-
File.separator + name + File.separator,
574-
File.separator
575-
)
576-
val base = PathRef(os.Path(rawPath))
577-
Seq(base, ref)
598+
Seq(updateRef(name, ref), ref)
599+
}
600+
}
601+
def resources = T.sources {
602+
val name = mainArtifactName().stripPrefix(prefix)
603+
super.resources().flatMap { ref =>
604+
Seq(updateRef(name, ref), ref)
578605
}
579606
}
580607

modules/cli-options/src/main/scala/scala/cli/commands/PublishOptions.scala

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package scala.cli.commands
22

33
import caseapp._
44

5+
import scala.cli.internal.PasswordOptionParsers._
6+
import scala.cli.signing.shared.PasswordOption
7+
58
// format: off
69
final case class PublishOptions(
710
@Recurse
@@ -65,7 +68,36 @@ final case class PublishOptions(
6568

6669
@Group("Publishing")
6770
@HelpMessage("Whether to build and publish source JARs")
68-
sources: Option[Boolean] = None
71+
sources: Option[Boolean] = None,
72+
73+
@Group("Publishing")
74+
@HelpMessage("ID of the GPG key to use to sign artifacts")
75+
@ValueDescription("key-id")
76+
@ExtraName("K")
77+
gpgKey: Option[String] = None,
78+
79+
@Group("Publishing")
80+
@HelpMessage("Secret key to use to sign artifacts with BouncyCastle")
81+
@ValueDescription("path")
82+
secretKey: Option[String] = None,
83+
84+
@Group("Publishing")
85+
@HelpMessage("Password of secret key to use to sign artifacts with BouncyCastle")
86+
@ValueDescription("value:…")
87+
@ExtraName("secretKeyPass")
88+
secretKeyPassword: Option[PasswordOption] = None,
89+
90+
@Group("Publishing")
91+
@HelpMessage("Method to use to sign artifacts")
92+
@ValueDescription("gpg|bc")
93+
signer: Option[String] = None,
94+
95+
@Group("Publishing")
96+
@HelpMessage("gpg command-line options")
97+
@ValueDescription("argument")
98+
@ExtraName("G")
99+
@ExtraName("gpgOpt")
100+
gpgOption: List[String] = Nil
69101
)
70102
// format: on
71103

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package scala.cli.internal
2+
3+
import caseapp.core.argparser.{ArgParser, SimpleArgParser}
4+
import com.github.plokhotnyuk.jsoniter_scala.core._
5+
import com.github.plokhotnyuk.jsoniter_scala.macros._
6+
7+
import scala.cli.signing.shared.{PasswordOption, Secret}
8+
9+
abstract class LowPriorityPasswordOptionParsers {
10+
11+
private lazy val commandCodec: JsonValueCodec[List[String]] =
12+
JsonCodecMaker.make
13+
14+
implicit lazy val argParser: ArgParser[PasswordOption] =
15+
SimpleArgParser.from("password") { str =>
16+
if (str.startsWith("value:"))
17+
Right(PasswordOption.Value(Secret(str.stripPrefix("value:"))))
18+
else if (str.startsWith("command:["))
19+
try {
20+
val command = readFromString(str.stripPrefix("command:"))(commandCodec)
21+
Right(PasswordOption.Command(command))
22+
}
23+
catch {
24+
case e: JsonReaderException =>
25+
Left(caseapp.core.Error.Other(s"Error decoding password command: ${e.getMessage}"))
26+
}
27+
else if (str.startsWith("command:")) {
28+
val command = str.stripPrefix("command:").split("\\s+").toSeq
29+
Right(PasswordOption.Command(command))
30+
}
31+
else
32+
Left(caseapp.core.Error.Other("Malformed password value (expected \"value:...\")"))
33+
}
34+
35+
}
36+
37+
object PasswordOptionParsers extends LowPriorityPasswordOptionParsers {
38+
39+
implicit lazy val optionArgParser: ArgParser[Option[PasswordOption]] =
40+
SimpleArgParser.from("password") { str =>
41+
if (str.trim.isEmpty) Right(None)
42+
else argParser(None, -1, -1, str).map(Some(_))
43+
}
44+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package scala.cli.commands.pgp;
2+
3+
import com.oracle.svm.core.annotate.Substitute;
4+
import com.oracle.svm.core.annotate.TargetClass;
5+
import scala.cli.commands.ScalaCommand;
6+
import scala.cli.commands.pgp.ExternalCommand;
7+
8+
@TargetClass(className = "scala.cli.commands.pgp.PgpCommands")
9+
final class PgpCommandsSubst {
10+
@Substitute
11+
ScalaCommand<?>[] allScalaCommands() {
12+
return new ScalaCommand<?>[0];
13+
}
14+
@Substitute
15+
ExternalCommand[] allExternalCommands() {
16+
return new ExternalCommand[] {
17+
new PgpCreateExternal(),
18+
new PgpSignExternal(),
19+
new PgpVerifyExternal()
20+
};
21+
}
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package scala.cli.internal;
2+
3+
import com.oracle.svm.core.annotate.Substitute;
4+
import com.oracle.svm.core.annotate.TargetClass;
5+
import coursier.publish.signing.Signer;
6+
7+
import java.nio.file.Path;
8+
import java.util.function.Supplier;
9+
10+
import scala.build.Logger;
11+
import scala.cli.publish.BouncycastleExternalSigner$;
12+
import scala.cli.signing.shared.PasswordOption;
13+
14+
@TargetClass(className = "scala.cli.publish.BouncycastleSignerMaker")
15+
final class BouncycastleSignerMakerSubst {
16+
17+
@Substitute
18+
Signer get(PasswordOption passwordOrNull, Path secretKey, Supplier<Path> launcher, Logger logger) {
19+
return BouncycastleExternalSigner$.MODULE$.apply(secretKey, passwordOrNull, launcher.get(), logger);
20+
}
21+
22+
@Substitute
23+
void maybeInit() {
24+
// do nothing
25+
}
26+
27+
}

modules/cli/src/main/scala/scala/cli/ScalaCli.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets
99
import scala.build.internal.Constants
1010
import scala.cli.internal.Argv0
1111
import scala.cli.launcher.{LauncherCli, LauncherOptions}
12+
import scala.cli.publish.BouncycastleSignerMaker
1213
import scala.util.Properties
1314

1415
object ScalaCli {
@@ -129,6 +130,8 @@ object ScalaCli {
129130
val (systemProps, scalaCliArgs) = partitionArgs(remainingArgs)
130131
setSystemProps(systemProps)
131132

133+
(new BouncycastleSignerMaker).maybeInit()
134+
132135
CacheUrl.setupProxyAuth()
133136

134137
// Getting killed by SIGPIPE quite often when on musl (in the "static" native

modules/cli/src/main/scala/scala/cli/ScalaCliCommands.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import caseapp.core.help.{Help, RuntimeCommandsHelp}
66
import java.nio.file.InvalidPathException
77

88
import scala.cli.commands._
9+
import scala.cli.commands.pgp.PgpCommands
910

1011
class ScalaCliCommands(
1112
val progName: String,
@@ -14,6 +15,8 @@ class ScalaCliCommands(
1415

1516
lazy val actualDefaultCommand = new Default(help)
1617

18+
private def pgpCommands = new PgpCommands
19+
1720
private def allCommands = Seq[ScalaCommand[_]](
1821
new About(isSipScala = isSipScala),
1922
AddPath,
@@ -39,9 +42,11 @@ class ScalaCliCommands(
3942
Test,
4043
Update,
4144
Version
42-
)
45+
) ++ pgpCommands.allScalaCommands
4346

44-
def commands = allCommands.filter(c => !isSipScala || c.inSipScala)
47+
def commands =
48+
allCommands.filter(c => !isSipScala || c.inSipScala) ++
49+
pgpCommands.allExternalCommands
4550

4651
override def description =
4752
"Scala CLI is a command-line tool to interact with the Scala language. It lets you compile, run, test, and package your Scala code."

0 commit comments

Comments
 (0)