Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'package sign' subcommand #6215

Merged
merged 4 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions Sources/Commands/PackageTools/SwiftPackageTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand All @@ -13,16 +13,15 @@
import ArgumentParser
import Basics
import CoreCommands
import TSCBasic
import SPMBuildCore
import PackageModel
import PackageLoading
import Foundation
import PackageGraph
import PackageLoading
import PackageModel
import SourceControl
import XCBuildSupport
import SPMBuildCore
import TSCBasic
import Workspace
import Foundation
import PackageModel
import XCBuildSupport

import enum TSCUtility.Diagnostics

Expand Down Expand Up @@ -62,12 +61,13 @@ public struct SwiftPackageTool: ParsableCommand {
ArchiveSource.self,
CompletionTool.self,
PluginCommand.self,

DefaultCommand.self,
]
+ (ProcessInfo.processInfo.environment["SWIFTPM_ENABLE_SNIPPETS"] == "1" ? [Learn.self] : []),
+ (ProcessInfo.processInfo.environment["SWIFTPM_ENABLE_SNIPPETS"] == "1" ? [Learn.self] : []),
defaultSubcommand: DefaultCommand.self,
helpNames: [.short, .long, .customLong("help", withSingleDash: true)])
helpNames: [.short, .long, .customLong("help", withSingleDash: true)]
)

@OptionGroup()
var globalOptions: GlobalOptions
Expand All @@ -78,11 +78,13 @@ public struct SwiftPackageTool: ParsableCommand {
}

extension SwiftPackageTool {
// This command is the default when no other subcommand is passed. It is not shown in the help and is never invoked directly.
// This command is the default when no other subcommand is passed. It is not shown in the help and is never invoked
// directly.
struct DefaultCommand: SwiftCommand {
static let configuration = CommandConfiguration(
commandName: nil,
shouldDisplay: false)
shouldDisplay: false
)

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

Expand All @@ -122,10 +123,14 @@ extension SwiftPackageTool {
extension PluginCommand.PluginOptions {
func merged(with other: Self) -> Self {
// validate against developer mistake
assert(Mirror(reflecting: self).children.count == 3, "Property added to PluginOptions without updating merged(with:)!")
assert(
Mirror(reflecting: self).children.count == 3,
"Property added to PluginOptions without updating merged(with:)!"
)
// actual merge
var merged = self
merged.allowWritingToPackageDirectory = merged.allowWritingToPackageDirectory || other.allowWritingToPackageDirectory
merged.allowWritingToPackageDirectory = merged.allowWritingToPackageDirectory || other
.allowWritingToPackageDirectory
merged.additionalAllowedWritableDirectories.append(contentsOf: other.additionalAllowedWritableDirectories)
if other.allowNetworkConnections != .none {
merged.allowNetworkConnections = other.allowNetworkConnections
Expand Down
10 changes: 7 additions & 3 deletions Sources/PackageRegistryTool/PackageRegistryTool+Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ private func getpass(_ prompt: String) -> UnsafePointer<CChar> {
defer { SetConsoleMode(hStdIn, dwMode) }

var dwNumberOfCharsRead: DWORD = 0
_ = ReadConsoleA(hStdIn, StaticStorage.buffer.baseAddress,
DWORD(StaticStorage.buffer.count), &dwNumberOfCharsRead,
nil)
_ = ReadConsoleA(
hStdIn,
StaticStorage.buffer.baseAddress,
DWORD(StaticStorage.buffer.count),
&dwNumberOfCharsRead,
nil
)
return UnsafePointer<CChar>(StaticStorage.buffer.baseAddress!)
}
#endif
Expand Down
170 changes: 52 additions & 118 deletions Sources/PackageRegistryTool/PackageRegistryTool+Publish.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,10 @@ extension SwiftPackageRegistryTool {
@OptionGroup(visibility: .hidden)
var globalOptions: GlobalOptions

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

@Option(
name: [.customLong("version"), .customLong("package-version")],
help: "The package release version being created."
)
@Argument(help: .init("The package release version being created.", valueName: "package-version"))
var packageVersion: Version

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

@Option(help: "Dry run only; prepare the archive and sign it but do not publish to the registry.")
@Flag(help: "Dry run only; prepare the archive and sign it but do not publish to the registry.")
tomerd marked this conversation as resolved.
Show resolved Hide resolved
var dryRun: Bool = false

func run(_ swiftTool: SwiftTool) async throws {
Expand Down Expand Up @@ -133,39 +130,18 @@ extension SwiftPackageRegistryTool {
)

// step 1: publishing configuration
let publishConfiguration = PublishConfiguration(
metadataLocation: self.customMetadataPath
.flatMap { .external($0) } ??
.sourceTree(packageDirectory.appending(component: Self.metadataFilename)),
signing: .init(
required: self.signingIdentity != nil || self.privateKeyPath != nil,
format: self.signatureFormat,
signingIdentity: self.signingIdentity,
privateKeyPath: self.privateKeyPath,
certificatePath: self.certificatePath
)
)
let metadataLocation: MetadataLocation = self.customMetadataPath
.flatMap { .external($0) } ??
.sourceTree(packageDirectory.appending(component: Self.metadataFilename))
let signingRequired = self.signingIdentity != nil || self.privateKeyPath != nil || self
.certificatePath != nil

guard localFileSystem.exists(publishConfiguration.metadataLocation.path) else {
guard localFileSystem.exists(metadataLocation.path) else {
throw StringError(
"Publishing to '\(registryURL)' requires metadata file but none was found at '\(publishConfiguration.metadataLocation)'."
"Publishing to '\(registryURL)' requires metadata file but none was found at '\(metadataLocation)'."
)
}

if publishConfiguration.signing.privateKeyPath != nil {
guard publishConfiguration.signing.certificatePath != nil else {
throw StringError(
"Both 'privateKeyPath' and 'certificatePath' are required when one of them is set."
)
}
} else {
guard publishConfiguration.signing.certificatePath == nil else {
throw StringError(
"Both 'privateKeyPath' and 'certificatePath' are required when one of them is set."
)
}
}

// step 2: generate source archive for the package release
swiftTool.observabilityScope.emit(info: "archiving the source at '\(packageDirectory)'")
let archivePath = try self.archiveSource(
Expand All @@ -179,14 +155,41 @@ extension SwiftPackageRegistryTool {

// step 3: sign the source archive if needed
var signature: Data? = .none
if publishConfiguration.signing.required {
if signingRequired {
// compute signing mode
let signingMode: PackageArchiveSigner.SigningMode
switch (
self.signingIdentity,
self.certificatePath,
self.privateKeyPath
) {
case (.none, .some, .none):
throw StringError(
"Both 'private-key-path' and 'certificate-path' are required when one of them is set."
)
case (.none, .none, .some):
throw StringError(
"Both 'private-key-path' and 'certificate-path' are required when one of them is set."
)
case (.none, .some(let certificatePath), .some(let privateKeyPath)):
signingMode = .certificate(certificate: certificatePath, privateKey: privateKeyPath)
case (.some(let signingStoreLabel), .none, .none):
signingMode = .identityStore(signingStoreLabel)
default:
throw StringError(
"Either 'signing-identity' or 'private-key-path' (together with 'certificate-path') must be provided."
)
}

swiftTool.observabilityScope.emit(info: "signing the archive at '\(archivePath)'")
signature = try await self.sign(
packageIdentity: self.packageIdentity,
packageVersion: self.packageVersion,
let signaturePath = workingDirectory
.appending(component: "\(self.packageIdentity)-\(self.packageVersion).sig")
signature = try await PackageArchiveSigner.sign(
archivePath: archivePath,
configuration: publishConfiguration.signing,
workingDirectory: workingDirectory,
signaturePath: signaturePath,
mode: signingMode,
signatureFormat: self.signatureFormat,
fileSystem: localFileSystem,
observabilityScope: swiftTool.observabilityScope
)
}
Expand All @@ -201,7 +204,6 @@ extension SwiftPackageRegistryTool {

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

return archivePath
}

func sign(
packageIdentity: PackageIdentity,
packageVersion: Version,
archivePath: AbsolutePath,
configuration: PublishConfiguration.Signing,
workingDirectory: AbsolutePath,
observabilityScope: ObservabilityScope
) async throws -> Data {
let archiveData = try Data(localFileSystem.readFileContents(archivePath).contents)

var signingIdentity: SigningIdentity?
if let signingIdentityLabel = configuration.signingIdentity {
let signingIdentityStore = SigningIdentityStore(observabilityScope: observabilityScope)
let matches = try await signingIdentityStore.find(by: signingIdentityLabel)
guard !matches.isEmpty else {
throw StringError("'\(signingIdentityLabel)' not found in the system identity store.")
}
// TODO: let user choose if there is more than one match?
signingIdentity = matches.first
} else if let privateKeyPath = configuration.privateKeyPath,
let certificatePath = configuration.certificatePath
{
let certificateData = try Data(localFileSystem.readFileContents(certificatePath).contents)
let privateKeyData = try Data(localFileSystem.readFileContents(privateKeyPath).contents)
signingIdentity = SwiftSigningIdentity(
certificate: Certificate(derEncoded: certificateData),
privateKey: try configuration.format.privateKey(derRepresentation: privateKeyData)
)
}

guard let signingIdentity = signingIdentity else {
throw StringError("Cannot sign archive without signing identity.")
}

let signature = try await SignatureProvider.sign(
archiveData,
with: signingIdentity,
in: configuration.format,
observabilityScope: observabilityScope
)

let signaturePath = workingDirectory.appending(component: "\(packageIdentity)-\(packageVersion).sig")
try localFileSystem.writeFileContents(signaturePath) { stream in
stream.write(signature)
}

return signature
}
}
}

struct PublishConfiguration {
let metadataLocation: MetadataLocation
let signing: Signing

enum MetadataLocation {
case sourceTree(AbsolutePath)
case external(AbsolutePath)
enum MetadataLocation {
case sourceTree(AbsolutePath)
case external(AbsolutePath)

var path: AbsolutePath {
switch self {
case .sourceTree(let path):
return path
case .external(let path):
return path
}
var path: AbsolutePath {
switch self {
case .sourceTree(let path):
return path
case .external(let path):
return path
}
}

struct Signing {
let required: Bool
let format: SignatureFormat
var signingIdentity: String?
var privateKeyPath: AbsolutePath?
var certificatePath: AbsolutePath?
}
}

extension SignatureFormat: ExpressibleByArgument {
public init?(argument: String) {
self.init(rawValue: argument.lowercased())
}
}

// TODO: migrate registry client to async
Expand Down
Loading