Skip to content

Commit c7aedb0

Browse files
yim-leetomerd
andauthored
Add 'package sign' subcommand (swiftlang#6215)
* Add 'package sign' subcommand Motivation: Part of https://github.com/apple/swift-evolution/blob/main/proposals/0391-package-registry-publish.md Modifications: - Add 'package sign' command - Refactor code to share between 'package sign' and 'package-registry publish' * wip * more * fixups --------- Co-authored-by: tom doron <[email protected]>
1 parent 273c205 commit c7aedb0

File tree

6 files changed

+295
-142
lines changed

6 files changed

+295
-142
lines changed

Sources/Commands/PackageTools/SwiftPackageTool.swift

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -13,16 +13,15 @@
1313
import ArgumentParser
1414
import Basics
1515
import CoreCommands
16-
import TSCBasic
17-
import SPMBuildCore
18-
import PackageModel
19-
import PackageLoading
16+
import Foundation
2017
import PackageGraph
18+
import PackageLoading
19+
import PackageModel
2120
import SourceControl
22-
import XCBuildSupport
21+
import SPMBuildCore
22+
import TSCBasic
2323
import Workspace
24-
import Foundation
25-
import PackageModel
24+
import XCBuildSupport
2625

2726
import enum TSCUtility.Diagnostics
2827

@@ -62,12 +61,13 @@ public struct SwiftPackageTool: ParsableCommand {
6261
ArchiveSource.self,
6362
CompletionTool.self,
6463
PluginCommand.self,
65-
64+
6665
DefaultCommand.self,
6766
]
68-
+ (ProcessInfo.processInfo.environment["SWIFTPM_ENABLE_SNIPPETS"] == "1" ? [Learn.self] : []),
67+
+ (ProcessInfo.processInfo.environment["SWIFTPM_ENABLE_SNIPPETS"] == "1" ? [Learn.self] : []),
6968
defaultSubcommand: DefaultCommand.self,
70-
helpNames: [.short, .long, .customLong("help", withSingleDash: true)])
69+
helpNames: [.short, .long, .customLong("help", withSingleDash: true)]
70+
)
7171

7272
@OptionGroup()
7373
var globalOptions: GlobalOptions
@@ -78,11 +78,13 @@ public struct SwiftPackageTool: ParsableCommand {
7878
}
7979

8080
extension SwiftPackageTool {
81-
// This command is the default when no other subcommand is passed. It is not shown in the help and is never invoked directly.
81+
// This command is the default when no other subcommand is passed. It is not shown in the help and is never invoked
82+
// directly.
8283
struct DefaultCommand: SwiftCommand {
8384
static let configuration = CommandConfiguration(
8485
commandName: nil,
85-
shouldDisplay: false)
86+
shouldDisplay: false
87+
)
8688

8789
@OptionGroup(visibility: .hidden)
8890
var globalOptions: GlobalOptions
@@ -103,8 +105,7 @@ extension SwiftPackageTool {
103105
// Check for edge cases and unknown options to match the behavior in the absence of plugins.
104106
if command.isEmpty {
105107
throw ValidationError("Unknown argument '\(command)'")
106-
}
107-
else if command.starts(with: "-") {
108+
} else if command.starts(with: "-") {
108109
throw ValidationError("Unknown option '\(command)'")
109110
}
110111

@@ -122,10 +123,14 @@ extension SwiftPackageTool {
122123
extension PluginCommand.PluginOptions {
123124
func merged(with other: Self) -> Self {
124125
// validate against developer mistake
125-
assert(Mirror(reflecting: self).children.count == 3, "Property added to PluginOptions without updating merged(with:)!")
126+
assert(
127+
Mirror(reflecting: self).children.count == 3,
128+
"Property added to PluginOptions without updating merged(with:)!"
129+
)
126130
// actual merge
127131
var merged = self
128-
merged.allowWritingToPackageDirectory = merged.allowWritingToPackageDirectory || other.allowWritingToPackageDirectory
132+
merged.allowWritingToPackageDirectory = merged.allowWritingToPackageDirectory || other
133+
.allowWritingToPackageDirectory
129134
merged.additionalAllowedWritableDirectories.append(contentsOf: other.additionalAllowedWritableDirectories)
130135
if other.allowNetworkConnections != .none {
131136
merged.allowNetworkConnections = other.allowNetworkConnections

Sources/PackageRegistryTool/PackageRegistryTool+Auth.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ private func getpass(_ prompt: String) -> UnsafePointer<CChar> {
4646
defer { SetConsoleMode(hStdIn, dwMode) }
4747

4848
var dwNumberOfCharsRead: DWORD = 0
49-
_ = ReadConsoleA(hStdIn, StaticStorage.buffer.baseAddress,
50-
DWORD(StaticStorage.buffer.count), &dwNumberOfCharsRead,
51-
nil)
49+
_ = ReadConsoleA(
50+
hStdIn,
51+
StaticStorage.buffer.baseAddress,
52+
DWORD(StaticStorage.buffer.count),
53+
&dwNumberOfCharsRead,
54+
nil
55+
)
5256
return UnsafePointer<CChar>(StaticStorage.buffer.baseAddress!)
5357
}
5458
#endif

Sources/PackageRegistryTool/PackageRegistryTool+Publish.swift

Lines changed: 52 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,10 @@ extension SwiftPackageRegistryTool {
3636
@OptionGroup(visibility: .hidden)
3737
var globalOptions: GlobalOptions
3838

39-
@Option(name: [.customLong("id"), .customLong("package-id")], help: "The package identifier.")
39+
@Argument(help: .init("The package identifier.", valueName: "package-id"))
4040
var packageIdentity: PackageIdentity
4141

42-
@Option(
43-
name: [.customLong("version"), .customLong("package-version")],
44-
help: "The package release version being created."
45-
)
42+
@Argument(help: .init("The package release version being created.", valueName: "package-version"))
4643
var packageVersion: Version
4744

4845
@Option(name: [.customLong("url"), .customLong("registry-url")], help: "The registry URL.")
@@ -74,7 +71,7 @@ extension SwiftPackageRegistryTool {
7471
@Option(help: "The path to the signing certificate (DER-encoded).")
7572
var certificatePath: AbsolutePath?
7673

77-
@Option(help: "Dry run only; prepare the archive and sign it but do not publish to the registry.")
74+
@Flag(help: "Dry run only; prepare the archive and sign it but do not publish to the registry.")
7875
var dryRun: Bool = false
7976

8077
func run(_ swiftTool: SwiftTool) async throws {
@@ -133,39 +130,18 @@ extension SwiftPackageRegistryTool {
133130
)
134131

135132
// step 1: publishing configuration
136-
let publishConfiguration = PublishConfiguration(
137-
metadataLocation: self.customMetadataPath
138-
.flatMap { .external($0) } ??
139-
.sourceTree(packageDirectory.appending(component: Self.metadataFilename)),
140-
signing: .init(
141-
required: self.signingIdentity != nil || self.privateKeyPath != nil,
142-
format: self.signatureFormat,
143-
signingIdentity: self.signingIdentity,
144-
privateKeyPath: self.privateKeyPath,
145-
certificatePath: self.certificatePath
146-
)
147-
)
133+
let metadataLocation: MetadataLocation = self.customMetadataPath
134+
.flatMap { .external($0) } ??
135+
.sourceTree(packageDirectory.appending(component: Self.metadataFilename))
136+
let signingRequired = self.signingIdentity != nil || self.privateKeyPath != nil || self
137+
.certificatePath != nil
148138

149-
guard localFileSystem.exists(publishConfiguration.metadataLocation.path) else {
139+
guard localFileSystem.exists(metadataLocation.path) else {
150140
throw StringError(
151-
"Publishing to '\(registryURL)' requires metadata file but none was found at '\(publishConfiguration.metadataLocation)'."
141+
"Publishing to '\(registryURL)' requires metadata file but none was found at '\(metadataLocation)'."
152142
)
153143
}
154144

155-
if publishConfiguration.signing.privateKeyPath != nil {
156-
guard publishConfiguration.signing.certificatePath != nil else {
157-
throw StringError(
158-
"Both 'privateKeyPath' and 'certificatePath' are required when one of them is set."
159-
)
160-
}
161-
} else {
162-
guard publishConfiguration.signing.certificatePath == nil else {
163-
throw StringError(
164-
"Both 'privateKeyPath' and 'certificatePath' are required when one of them is set."
165-
)
166-
}
167-
}
168-
169145
// step 2: generate source archive for the package release
170146
swiftTool.observabilityScope.emit(info: "archiving the source at '\(packageDirectory)'")
171147
let archivePath = try self.archiveSource(
@@ -179,14 +155,41 @@ extension SwiftPackageRegistryTool {
179155

180156
// step 3: sign the source archive if needed
181157
var signature: Data? = .none
182-
if publishConfiguration.signing.required {
158+
if signingRequired {
159+
// compute signing mode
160+
let signingMode: PackageArchiveSigner.SigningMode
161+
switch (
162+
self.signingIdentity,
163+
self.certificatePath,
164+
self.privateKeyPath
165+
) {
166+
case (.none, .some, .none):
167+
throw StringError(
168+
"Both 'private-key-path' and 'certificate-path' are required when one of them is set."
169+
)
170+
case (.none, .none, .some):
171+
throw StringError(
172+
"Both 'private-key-path' and 'certificate-path' are required when one of them is set."
173+
)
174+
case (.none, .some(let certificatePath), .some(let privateKeyPath)):
175+
signingMode = .certificate(certificate: certificatePath, privateKey: privateKeyPath)
176+
case (.some(let signingStoreLabel), .none, .none):
177+
signingMode = .identityStore(signingStoreLabel)
178+
default:
179+
throw StringError(
180+
"Either 'signing-identity' or 'private-key-path' (together with 'certificate-path') must be provided."
181+
)
182+
}
183+
183184
swiftTool.observabilityScope.emit(info: "signing the archive at '\(archivePath)'")
184-
signature = try await self.sign(
185-
packageIdentity: self.packageIdentity,
186-
packageVersion: self.packageVersion,
185+
let signaturePath = workingDirectory
186+
.appending(component: "\(self.packageIdentity)-\(self.packageVersion).sig")
187+
signature = try await PackageArchiveSigner.sign(
187188
archivePath: archivePath,
188-
configuration: publishConfiguration.signing,
189-
workingDirectory: workingDirectory,
189+
signaturePath: signaturePath,
190+
mode: signingMode,
191+
signatureFormat: self.signatureFormat,
192+
fileSystem: localFileSystem,
190193
observabilityScope: swiftTool.observabilityScope
191194
)
192195
}
@@ -201,7 +204,6 @@ extension SwiftPackageRegistryTool {
201204

202205
swiftTool.observabilityScope
203206
.emit(info: "publishing '\(self.packageIdentity)' archive at '\(archivePath)' to '\(registryURL)'")
204-
// TODO: handle signature
205207
let result = try await registryClient.publish(
206208
registryURL: registryURL,
207209
packageIdentity: self.packageIdentity,
@@ -260,89 +262,21 @@ extension SwiftPackageRegistryTool {
260262

261263
return archivePath
262264
}
263-
264-
func sign(
265-
packageIdentity: PackageIdentity,
266-
packageVersion: Version,
267-
archivePath: AbsolutePath,
268-
configuration: PublishConfiguration.Signing,
269-
workingDirectory: AbsolutePath,
270-
observabilityScope: ObservabilityScope
271-
) async throws -> Data {
272-
let archiveData = try Data(localFileSystem.readFileContents(archivePath).contents)
273-
274-
var signingIdentity: SigningIdentity?
275-
if let signingIdentityLabel = configuration.signingIdentity {
276-
let signingIdentityStore = SigningIdentityStore(observabilityScope: observabilityScope)
277-
let matches = try await signingIdentityStore.find(by: signingIdentityLabel)
278-
guard !matches.isEmpty else {
279-
throw StringError("'\(signingIdentityLabel)' not found in the system identity store.")
280-
}
281-
// TODO: let user choose if there is more than one match?
282-
signingIdentity = matches.first
283-
} else if let privateKeyPath = configuration.privateKeyPath,
284-
let certificatePath = configuration.certificatePath
285-
{
286-
let certificateData = try Data(localFileSystem.readFileContents(certificatePath).contents)
287-
let privateKeyData = try Data(localFileSystem.readFileContents(privateKeyPath).contents)
288-
signingIdentity = SwiftSigningIdentity(
289-
certificate: Certificate(derEncoded: certificateData),
290-
privateKey: try configuration.format.privateKey(derRepresentation: privateKeyData)
291-
)
292-
}
293-
294-
guard let signingIdentity = signingIdentity else {
295-
throw StringError("Cannot sign archive without signing identity.")
296-
}
297-
298-
let signature = try await SignatureProvider.sign(
299-
archiveData,
300-
with: signingIdentity,
301-
in: configuration.format,
302-
observabilityScope: observabilityScope
303-
)
304-
305-
let signaturePath = workingDirectory.appending(component: "\(packageIdentity)-\(packageVersion).sig")
306-
try localFileSystem.writeFileContents(signaturePath) { stream in
307-
stream.write(signature)
308-
}
309-
310-
return signature
311-
}
312265
}
313266
}
314267

315-
struct PublishConfiguration {
316-
let metadataLocation: MetadataLocation
317-
let signing: Signing
318-
319-
enum MetadataLocation {
320-
case sourceTree(AbsolutePath)
321-
case external(AbsolutePath)
268+
enum MetadataLocation {
269+
case sourceTree(AbsolutePath)
270+
case external(AbsolutePath)
322271

323-
var path: AbsolutePath {
324-
switch self {
325-
case .sourceTree(let path):
326-
return path
327-
case .external(let path):
328-
return path
329-
}
272+
var path: AbsolutePath {
273+
switch self {
274+
case .sourceTree(let path):
275+
return path
276+
case .external(let path):
277+
return path
330278
}
331279
}
332-
333-
struct Signing {
334-
let required: Bool
335-
let format: SignatureFormat
336-
var signingIdentity: String?
337-
var privateKeyPath: AbsolutePath?
338-
var certificatePath: AbsolutePath?
339-
}
340-
}
341-
342-
extension SignatureFormat: ExpressibleByArgument {
343-
public init?(argument: String) {
344-
self.init(rawValue: argument.lowercased())
345-
}
346280
}
347281

348282
// TODO: migrate registry client to async

0 commit comments

Comments
 (0)