Skip to content
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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ $ nest install https://github.com/realm/SwiftLint 0.55.0
$ nest install realm/SwiftLint 0.55.0 --checksum adcc2e3b...

# When installing a direct artifact bundle URL, --checksum is required.
# Use --allow-unverified to opt out of verification explicitly.
# Use --checksum-policy to choose how missing checksums are handled explicitly.
$ nest install https://example.com/foo.artifactbundle.zip --checksum abc123...
$ nest install https://example.com/foo.artifactbundle.zip --allow-unverified
$ nest install https://example.com/foo.artifactbundle.zip --checksum-policy warn
$ nest install https://example.com/foo.artifactbundle.zip --checksum-policy skip
```

### Uninstall package
Expand Down Expand Up @@ -91,10 +92,10 @@ targets:
- reference: mtj0928/nest # or htpps://github.com/mtj0928/nest
version: 0.1.0 # (Optional) When a version is not specified, the latest release will be used.
assetName: nest-macos.artifactbundle.zip # (Optional) When a name is not specified, it will be resolved by GitHub API.
checksum: adcc2e3b4d48606cba7787153b0794f8a87e5289803466d63513f04c4d7661fb # Recommended now and required in strict mode. Run `update-nestfile` to populate.
checksum: adcc2e3b4d48606cba7787153b0794f8a87e5289803466d63513f04c4d7661fb # Recommended now and required with `--checksum-policy require`. Run `update-nestfile` to populate.
# Example 2 Specify zip URL directly
- zipURL: https://github.com/mtj0928/nest/releases/download/0.1.0/nest-macos.artifactbundle.zip
checksum: adcc2e3b4d48606cba7787153b0794f8a87e5289803466d63513f04c4d7661fb # Recommended now and required in strict mode.
checksum: adcc2e3b4d48606cba7787153b0794f8a87e5289803466d63513f04c4d7661fb # Recommended now and required with `--checksum-policy require`.
registries:
github:
- host: my-github-enterprise.example.com
Expand All @@ -107,11 +108,11 @@ $ nest update-nestfile nestfile.yaml
$ nest bootstrap nestfile.yaml

# Opt in to the future strict behavior now.
$ nest bootstrap nestfile.yaml --require-checksum
$ nest bootstrap nestfile.yaml --checksum-policy require
$ NEST_REQUIRE_CHECKSUM=1 nest bootstrap nestfile.yaml

# Pass --skip-checksum-validation to bypass verification (not recommended).
$ nest bootstrap nestfile.yaml --skip-checksum-validation
# Pass --checksum-policy skip to bypass verification (not recommended).
$ nest bootstrap nestfile.yaml --checksum-policy skip
```

### Update nestfile
Expand Down
15 changes: 11 additions & 4 deletions Sources/NestCLI/ArtifactBundleFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ public struct ArtifactBundleFetcher {
nest update-nestfile <path>

Temporary CI escape hatch:
nest bootstrap <path> --skip-checksum-validation
nest run --skip-checksum-validation ...
nest bootstrap <path> --checksum-policy skip
nest run --checksum-policy skip ...

🚨🚨🚨 CHECKSUM MISSING - UNVERIFIED ARTIFACT BUNDLE 🚨🚨🚨
""",
Expand Down Expand Up @@ -294,16 +294,23 @@ public enum ChecksumOption {
public enum ChecksumOptionError: LocalizedError, Equatable, Sendable {
case mutuallyExclusiveFlags
case missingChecksum(target: String)
case missingInstallChecksum(target: String)

public var errorDescription: String? {
switch self {
case .mutuallyExclusiveFlags:
"--checksum and --allow-unverified are mutually exclusive."
"--checksum and --checksum-policy skip are mutually exclusive."
case .missingChecksum(let target):
"""
Missing checksum for "\(target)" in the nestfile.
Run `nest update-nestfile <path>` to populate checksums, \
or pass `--skip-checksum-validation` to bypass verification.
or pass `--checksum-policy skip` to bypass verification.
"""
case .missingInstallChecksum(let target):
"""
Missing checksum for "\(target)".
Pass `--checksum <value>` to verify the downloaded file, \
or pass `--checksum-policy warn` or `--checksum-policy skip` to continue without verification.
"""
}
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/NestCLI/ChecksumValidationPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Defines how downloaded artifact bundle checksums are validated.
package enum ChecksumValidationPolicy: Equatable, Sendable {
/// Skips checksum validation.
case skip

/// Allows missing checksums with a warning.
case warn

/// Requires checksums and treats missing checksums as an error.
case require
}
7 changes: 3 additions & 4 deletions Sources/NestCLI/Nestfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,14 @@ extension Nestfile.RegistryConfigs {
}

extension Nestfile.Target {
// TODO: Remove `requireValidation` when checksum verification becomes the default behavior.
package func checksumOption(skipValidation: Bool, requireValidation: Bool = false) -> ChecksumOption {
if skipValidation {
package func checksumOption(policy: ChecksumValidationPolicy) -> ChecksumOption {
if policy == .skip {
return .skip
}
if let checksum {
return .needsCheck(expected: checksum)
}
if requireValidation {
if policy == .require {
return .unresolvable(.missingChecksum(target: identifier))
}
return .warnOnMissingChecksum(target: identifier)
Expand Down
26 changes: 26 additions & 0 deletions Sources/nest/Arguments/ChecksumValidationPolicyArgument.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ArgumentParser
import NestCLI

/// A command-line argument for selecting checksum validation behavior.
enum ChecksumValidationPolicyArgument: String, ExpressibleByArgument {
/// Skips checksum validation.
case skip

/// Allows missing checksums with a warning.
case warn

/// Requires checksums and treats missing checksums as an error.
case require

/// The checksum validation policy represented by this argument.
var policy: ChecksumValidationPolicy {
switch self {
case .skip:
.skip
case .warn:
.warn
case .require:
.require
}
}
}
17 changes: 10 additions & 7 deletions Sources/nest/Commands/BootstrapCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@ struct BootstrapCommand: AsyncParsableCommand {
@Argument(help: "A nestfile written in yaml.")
var nestfilePath: String

@Flag(name: .shortAndLong, help: "Skip checksum validation for downloaded artifactbundles.")
var skipChecksumValidation = false
@Option(help: "Checksum validation policy for downloaded artifact bundles: skip, warn, or require.")
var checksumPolicy: ChecksumValidationPolicyArgument?

// TODO: Remove this opt-in flag when checksum verification becomes the default behavior.
@Flag(help: "Require checksums for downloaded artifact bundles.")
var requireChecksum = false
@Flag(name: [.customLong("skip-checksum-validation"), .customShort("s")], help: .hidden)
var skipChecksumValidation = false

@Flag(name: .shortAndLong)
var verbose: Bool = false

mutating func run() async throws {
let nestfile = try Nestfile.load(from: nestfilePath, fileSystem: FileManager.default)
let (executableBinaryPreparer, artifactBundleManager, logger) = setUp(nestfile: nestfile)
let requireChecksum = requireChecksum || ProcessInfo.processInfo.requireChecksum
let checksumValidationPolicy = if skipChecksumValidation {
ChecksumValidationPolicy.skip
} else {
checksumPolicy?.policy ?? ProcessInfo.processInfo.checksumValidationPolicy
}

if nestfile.targets.contains(where: { $0.isDeprecatedZIP }) {
logger.warning("""
Expand All @@ -39,7 +42,7 @@ struct BootstrapCommand: AsyncParsableCommand {
for targetInfo in nestfile.targets {
let target: InstallTarget
var version: GitVersion
let checksumOption = targetInfo.checksumOption(skipValidation: skipChecksumValidation, requireValidation: requireChecksum)
let checksumOption = targetInfo.checksumOption(policy: checksumValidationPolicy)

switch (targetInfo.resolveInstallTarget(), targetInfo.resolveVersion()) {
case (.failure(let error), _):
Expand Down
60 changes: 40 additions & 20 deletions Sources/nest/Commands/InstallCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,49 +21,40 @@ struct InstallCommand: AsyncParsableCommand {
@Option(help: "Verify the downloaded artifact bundle against this checksum.")
var checksum: String?

@Flag(help: "Skip checksum verification. Required when installing a direct artifact bundle URL without --checksum.")
var allowUnverified = false

// TODO: Remove this opt-in flag when checksum verification becomes the default behavior.
@Flag(help: "Require checksum for downloaded artifact bundles.")
var requireChecksum = false
@Option(help: "Checksum validation policy for downloaded artifact bundles: skip, warn, or require.")
var checksumPolicy: ChecksumValidationPolicyArgument?

@Flag(name: .shortAndLong)
var verbose: Bool = false

mutating func run() async throws {
let (executableBinaryPreparer, nestDirectory, artifactBundleManager, logger) = setUp()
do {
let checksumValidationPolicy = checksumPolicy?.policy ?? ProcessInfo.processInfo.checksumValidationPolicy
let isChecksumPolicyExplicit = checksumPolicy != nil
// Direct artifact bundle URLs always download a ZIP, so the user must
// make a verification decision up front. The git path may build from
// source instead, so any checksum-flag inconsistency is deferred to
// `.unresolvable` and only surfaces if a ZIP is actually downloaded.
if case .artifactBundle(let url) = target {
_ = try URL.httpsURL(from: url.absoluteString)
if checksum == nil, !allowUnverified {
if requiresExplicitChecksumDecision(isChecksumPolicyExplicit: isChecksumPolicyExplicit) {
logger.error(
"""
Installing a direct artifact bundle URL requires integrity verification.
Pass --checksum <value> to verify, or --allow-unverified to skip explicitly.
Pass --checksum <value> to verify, or --checksum-policy <skip|warn|require> to choose explicitly.
""",
metadata: .color(.red)
)
Foundation.exit(1)
}
}

let checksumOption: ChecksumOption =
if checksum != nil && allowUnverified {
.unresolvable(.mutuallyExclusiveFlags)
} else if checksum == nil && !allowUnverified && (requireChecksum || ProcessInfo.processInfo.requireChecksum) {
.unresolvable(.missingChecksum(target: target.identifier))
} else {
ChecksumOption(
isSkip: allowUnverified,
expectedChecksum: checksum,
logger: logger
)
}
let checksumOption = checksumOption(
checksumValidationPolicy: checksumValidationPolicy,
isChecksumPolicyExplicit: isChecksumPolicyExplicit,
logger: logger
)

let executableBinaries: [PreparedBinary] = switch target {
case .git(let gitURL):
Expand Down Expand Up @@ -100,6 +91,35 @@ struct InstallCommand: AsyncParsableCommand {
}
}

extension InstallCommand {
func requiresExplicitChecksumDecision(isChecksumPolicyExplicit: Bool) -> Bool {
checksum == nil && !isChecksumPolicyExplicit
}

func checksumOption(
checksumValidationPolicy: ChecksumValidationPolicy,
isChecksumPolicyExplicit: Bool,
logger: Logger
) -> ChecksumOption {
if checksum != nil && checksumValidationPolicy == .skip {
return .unresolvable(.mutuallyExclusiveFlags)
}
if checksum == nil && checksumValidationPolicy == .require {
return .unresolvable(.missingInstallChecksum(target: target.identifier))
}
if checksum == nil && checksumValidationPolicy == .warn && isChecksumPolicyExplicit {
return .printActual { checksum in
logger.warning("ℹ️ The checksum is \(checksum). Please use --checksum to verify the downloaded file.")
}
}
return ChecksumOption(
isSkip: checksumValidationPolicy == .skip,
expectedChecksum: checksum,
logger: logger
)
}
}

extension InstallTarget {
var identifier: String {
switch self {
Expand Down
21 changes: 12 additions & 9 deletions Sources/nest/Commands/RunCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@ struct RunCommand: AsyncParsableCommand {

@Flag(name: .shortAndLong)
var verbose = false

@Flag(help: "Will not perform installation.")
var noInstall = false

@Flag(name: .shortAndLong, help: "Skip checksum validation for downloaded artifactbundles.")
@Option(help: "Checksum validation policy for downloaded artifact bundles: skip, warn, or require.")
var checksumPolicy: ChecksumValidationPolicyArgument?

@Flag(name: [.customLong("skip-checksum-validation"), .customShort("s")], help: .hidden)
var skipChecksumValidation = false

// TODO: Remove this opt-in flag when checksum verification becomes the default behavior.
@Flag(help: "Require checksums for downloaded artifact bundles.")
var requireChecksum = false

@Option(help: "A path to nestfile", completion: .file(extensions: ["yaml"]))
var nestfilePath = "nestfile.yaml"

Expand Down Expand Up @@ -68,7 +67,11 @@ struct RunCommand: AsyncParsableCommand {

let version = GitVersion.tag(expectedVersion)
let executables: [ExecutableBinary]
let requireChecksum = requireChecksum || ProcessInfo.processInfo.requireChecksum
let checksumValidationPolicy = if skipChecksumValidation {
ChecksumValidationPolicy.skip
} else {
checksumPolicy?.policy ?? ProcessInfo.processInfo.checksumValidationPolicy
}
let installedBinaries = executableBinaryPreparer.resolveInstalledExecutableBinariesFromNestInfo(for: subcommand.repository, version: version)
if !installedBinaries.isEmpty {
executables = installedBinaries
Expand All @@ -81,7 +84,7 @@ struct RunCommand: AsyncParsableCommand {
gitURL: subcommand.repository,
version: version,
assetName: target.assetName,
checksumOption: target.checksumOption(skipValidation: skipChecksumValidation, requireValidation: requireChecksum)
checksumOption: target.checksumOption(policy: checksumValidationPolicy)
)
executables = executableBinaryPreparer.resolveInstalledExecutableBinariesFromNestInfo(for: subcommand.repository, version: version)
}
Expand Down Expand Up @@ -116,7 +119,7 @@ extension RunCommand {
registryTokenEnvironmentVariableNames: nestfile.registries?.githubServerTokenEnvironmentVariableNames ?? [:],
logLevel: verbose ? .trace : .info
)

let controller = NestfileController(
assetRegistryClientBuilder: AssetRegistryClientBuilder(
httpClient: configuration.httpClient,
Expand Down
6 changes: 3 additions & 3 deletions Sources/nest/nest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ extension ProcessInfo {
}

// TODO: Remove this opt-in environment variable when checksum verification becomes the default behavior.
var requireChecksum: Bool {
var checksumValidationPolicy: ChecksumValidationPolicy {
switch environment["NEST_REQUIRE_CHECKSUM"]?.lowercased() {
case "1", "true", "yes", "on":
true
.require
default:
false
.warn
}
}
}
8 changes: 4 additions & 4 deletions Tests/NestCLITests/NestfileTargetChecksumOptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct NestfileTargetChecksumOptionTests {
func checksumOptionNeedsCheckWhenChecksumExists() {
let target = Nestfile.Target.repository(Nestfile.Repository(reference: "owner/repo", version: "1.0.0", assetName: nil, checksum: "abc123"))

switch target.checksumOption(skipValidation: false) {
switch target.checksumOption(policy: .warn) {
case .needsCheck(let expected):
#expect(expected == "abc123")
default:
Expand All @@ -18,7 +18,7 @@ struct NestfileTargetChecksumOptionTests {
func checksumOptionIsUnresolvableWhenChecksumIsMissing() {
let target = Nestfile.Target.repository(Nestfile.Repository(reference: "owner/repo", version: "1.0.0", assetName: nil, checksum: nil))

switch target.checksumOption(skipValidation: false) {
switch target.checksumOption(policy: .warn) {
case .warnOnMissingChecksum(let targetIdentifier):
#expect(targetIdentifier == "owner/repo")
default:
Expand All @@ -30,7 +30,7 @@ struct NestfileTargetChecksumOptionTests {
func checksumOptionIsUnresolvableWhenChecksumIsMissingInStrictMode() {
let target = Nestfile.Target.repository(Nestfile.Repository(reference: "owner/repo", version: "1.0.0", assetName: nil, checksum: nil))

switch target.checksumOption(skipValidation: false, requireValidation: true) {
switch target.checksumOption(policy: .require) {
case .unresolvable(.missingChecksum(let targetIdentifier)):
#expect(targetIdentifier == "owner/repo")
default:
Expand All @@ -42,7 +42,7 @@ struct NestfileTargetChecksumOptionTests {
func checksumOptionSkipsWhenValidationIsSkipped() {
let target = Nestfile.Target.repository(Nestfile.Repository(reference: "owner/repo", version: "1.0.0", assetName: nil, checksum: nil))

switch target.checksumOption(skipValidation: true) {
switch target.checksumOption(policy: .skip) {
case .skip:
break
default:
Expand Down
Loading
Loading