Skip to content

Commit ab2dc50

Browse files
authored
Add an option to self-update to provide the swiftly version to update (#370)
* Add a hidden option to self-update to provide the swiftly version to update * Update tests to pass the optional explicit version * Add a test for self-update to user-specified version * Add more swift website URL coverage for swiftly releases
1 parent 6d85742 commit ab2dc50

File tree

4 files changed

+130
-30
lines changed

4 files changed

+130
-30
lines changed

Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ swiftly init [--no-modify-profile] [--overwrite] [--platform=<platform>] [--skip
493493
Update the version of swiftly itself.
494494

495495
```
496-
swiftly self-update [--assume-yes] [--verbose] [--version] [--help]
496+
swiftly self-update [--assume-yes] [--verbose] [--version] [--help]
497497
```
498498

499499
**--assume-yes:**

Sources/Swiftly/SelfUpdate.swift

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,23 @@ import SwiftlyWebsiteAPI
55
@preconcurrency import TSCBasic
66
import TSCUtility
77

8+
extension SwiftlyVersion: ExpressibleByArgument {
9+
public init?(argument: String) {
10+
try? self.init(parsing: argument)
11+
}
12+
}
13+
814
struct SelfUpdate: SwiftlyCommand {
915
public static let configuration = CommandConfiguration(
1016
abstract: "Update the version of swiftly itself."
1117
)
1218

1319
@OptionGroup var root: GlobalOptions
1420

21+
@Option(help: .hidden) var toVersion: SwiftlyVersion
22+
1523
private enum CodingKeys: String, CodingKey {
16-
case root
24+
case root, toVersion
1725
}
1826

1927
mutating func run() async throws {
@@ -31,50 +39,80 @@ struct SelfUpdate: SwiftlyCommand {
3139
)
3240
}
3341

34-
let _ = try await Self.execute(ctx, verbose: self.root.verbose)
42+
let _ = try await Self.execute(ctx, verbose: self.root.verbose, version: self.toVersion)
3543
}
3644

37-
public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool) async throws
45+
public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool, version swiftlyVersion: SwiftlyVersion?) async throws
3846
-> SwiftlyVersion
3947
{
48+
var downloadURL: Foundation.URL?
49+
var version: SwiftlyVersion? = swiftlyVersion
50+
4051
await ctx.message("Checking for swiftly updates...")
4152

42-
let swiftlyRelease = try await ctx.httpClient.getCurrentSwiftlyRelease()
53+
if let version {
54+
#if os(macOS)
55+
downloadURL = URL(string: "https://download.swift.org/swiftly/darwin/swiftly-\(version).pkg")
56+
#elseif os(Linux)
57+
#if arch(x86_64)
58+
downloadURL = URL(string: "https://download.swift.org/swiftly/linux/swiftly-\(version)-x86_64.tar.gz")
59+
#elseif arch(arm64)
60+
downloadURL = URL(string: "https://download.swift.org/swiftly/linux/swiftly-\(version)-aarch64.tar.gz")
61+
#else
62+
fatalError("Unsupported architecture")
63+
#endif
64+
#else
65+
fatalError("Unsupported OS")
66+
#endif
4367

44-
guard try swiftlyRelease.swiftlyVersion > SwiftlyCore.version else {
45-
await ctx.message("Already up to date.")
46-
return SwiftlyCore.version
68+
guard version > SwiftlyCore.version else {
69+
await ctx.print("Self-update does not support downgrading to an older version or re-installing the current version. Current version is \(SwiftlyCore.version) and requested version is \(version).")
70+
return SwiftlyCore.version
71+
}
72+
73+
await ctx.print("Self-update requested to swiftly version \(version)")
4774
}
4875

49-
var downloadURL: Foundation.URL?
50-
for platform in swiftlyRelease.platforms {
51-
#if os(macOS)
52-
guard platform.isDarwin else {
53-
continue
76+
if downloadURL == nil {
77+
await ctx.print("Checking for swiftly updates...")
78+
79+
let swiftlyRelease = try await ctx.httpClient.getCurrentSwiftlyRelease()
80+
81+
guard try swiftlyRelease.swiftlyVersion > SwiftlyCore.version else {
82+
await ctx.print("Already up to date.")
83+
return SwiftlyCore.version
5484
}
85+
for platform in swiftlyRelease.platforms {
86+
#if os(macOS)
87+
guard platform.isDarwin else {
88+
continue
89+
}
5590
#elseif os(Linux)
56-
guard platform.isLinux else {
57-
continue
58-
}
91+
guard platform.isLinux else {
92+
continue
93+
}
5994
#endif
6095

6196
#if arch(x86_64)
62-
downloadURL = try platform.x86_64URL
97+
downloadURL = try platform.x86_64URL
6398
#elseif arch(arm64)
64-
downloadURL = try platform.arm64URL
99+
downloadURL = try platform.arm64URL
65100
#endif
66-
}
101+
}
67102

68-
guard let downloadURL else {
69-
throw SwiftlyError(
70-
message:
71-
"The newest release of swiftly is incompatible with your current OS and/or processor architecture."
72-
)
73-
}
103+
guard let downloadURL else {
104+
throw SwiftlyError(
105+
message:
106+
"The newest release of swiftly is incompatible with your current OS and/or processor architecture."
107+
)
108+
}
109+
110+
version = try swiftlyRelease.swiftlyVersion
74111

75-
let version = try swiftlyRelease.swiftlyVersion
112+
await ctx.print("A new version of swiftly is available: \(version!)")
113+
}
76114

77-
await ctx.message("A new version is available: \(version)")
115+
guard let version, let downloadURL else { fatalError() }
78116

79117
let tmpFile = fs.mktemp()
80118
try await fs.create(file: tmpFile, contents: nil)

Tests/SwiftlyTests/HTTPClientTests.swift

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ import Testing
5353
}
5454
}
5555

56-
@Test(.tags(.large)) func getSwiftlyRelease() async throws {
56+
@Test(
57+
.tags(.large),
58+
arguments: [
59+
"https://download.swift.org/swiftly/linux/swiftly-x86_64.tar.gz", // Latest
60+
"https://download.swift.org/swiftly/linux/swiftly-1.0.1-x86_64.tar.gz", // Specific version
61+
"https://download.swift.org/swiftly/linux/swiftly-1.0.1-dev-x86_64.tar.gz", // Specific dev prerelease version
62+
"https://download.swift.org/swiftly/linux/swiftly-aarch64.tar.gz", // Latest ARM
63+
"https://download.swift.org/swiftly/linux/swiftly-1.0.1-aarch64.tar.gz", // Specific ARM version
64+
"https://download.swift.org/swiftly/linux/swiftly-1.0.1-dev-aarch64.tar.gz", // Specific dev prerelease version
65+
]
66+
) func getSwiftlyLinuxReleases(url: String) async throws {
5767
let tmpFile = fs.mktemp()
5868
try await fs.create(file: tmpFile, contents: nil)
5969
let tmpFileSignature = fs.mktemp(ext: ".sig")
@@ -64,7 +74,7 @@ import Testing
6474
try await fs.withTemporary(files: tmpFile, tmpFileSignature, keysFile) {
6575
let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())
6676

67-
let swiftlyURL = try #require(URL(string: "https://download.swift.org/swiftly/linux/swiftly-x86_64.tar.gz"))
77+
let swiftlyURL = try #require(URL(string: url))
6878

6979
try await retry {
7080
try await httpClient.getSwiftlyRelease(url: swiftlyURL).download(to: tmpFile)
@@ -82,6 +92,32 @@ import Testing
8292
}
8393
}
8494

95+
@Test(
96+
.tags(.large),
97+
arguments: [
98+
"https://download.swift.org/swiftly/darwin/swiftly.pkg", // Latest
99+
"https://download.swift.org/swiftly/darwin/swiftly-1.0.1.pkg", // Specific version
100+
"https://download.swift.org/swiftly/darwin/swiftly-1.0.1-dev.pkg", // Specific dev prerelease version
101+
]
102+
) func getSwiftlyMacOSReleases(url: String) async throws {
103+
let tmpFile = fs.mktemp()
104+
try await fs.create(file: tmpFile, contents: nil)
105+
let tmpFileSignature = fs.mktemp(ext: ".sig")
106+
try await fs.create(file: tmpFileSignature, contents: nil)
107+
let keysFile = fs.mktemp(ext: ".asc")
108+
try await fs.create(file: keysFile, contents: nil)
109+
110+
try await fs.withTemporary(files: tmpFile, tmpFileSignature, keysFile) {
111+
let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())
112+
113+
let swiftlyURL = try #require(URL(string: url))
114+
115+
try await retry {
116+
try await httpClient.getSwiftlyRelease(url: swiftlyURL).download(to: tmpFile)
117+
}
118+
}
119+
}
120+
85121
@Test(.tags(.large)) func getSwiftlyReleaseMetadataFromSwiftOrg() async throws {
86122
let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())
87123
do {

Tests/SwiftlyTests/SelfUpdateTests.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ import Testing
1818
SwiftlyVersion(major: SwiftlyCore.version.major, minor: SwiftlyCore.version.minor, patch: SwiftlyCore.version.patch + 1)
1919
}
2020

21+
private static var newDevVersion: SwiftlyVersion {
22+
SwiftlyVersion(major: SwiftlyCore.version.major, minor: SwiftlyCore.version.minor, patch: SwiftlyCore.version.patch + 1, suffix: "dev")
23+
}
24+
2125
func runSelfUpdateTest(latestVersion: SwiftlyVersion) async throws {
2226
try await SwiftlyTests.withTestHome {
2327
try await SwiftlyTests.withMockedSwiftlyVersion(latestSwiftlyVersion: latestVersion) {
24-
let updatedVersion = try await SelfUpdate.execute(SwiftlyTests.ctx, verbose: true)
28+
let updatedVersion = try await SelfUpdate.execute(SwiftlyTests.ctx, verbose: true, version: nil)
2529
#expect(latestVersion == updatedVersion)
2630
}
2731
}
@@ -37,4 +41,26 @@ import Testing
3741
@Test func selfUpdateAlreadyUpToDate() async throws {
3842
try await self.runSelfUpdateTest(latestVersion: SwiftlyCore.version)
3943
}
44+
45+
@Test func selfUpdateToUserSpecifiedVersion() async throws {
46+
try await SwiftlyTests.withTestHome {
47+
// GIVEN: swiftly is installed, and at the latest published version
48+
try await SwiftlyTests.withMockedSwiftlyVersion(latestSwiftlyVersion: SwiftlyCore.version) {
49+
// WHEN: An attempt is made to self-update to an equal version
50+
var updatedVersion = try await SelfUpdate.execute(SwiftlyTests.ctx, verbose: true, version: SwiftlyCore.version)
51+
// THEN: There is no change to the swiftly version
52+
#expect(updatedVersion == SwiftlyCore.version)
53+
54+
// WHEN: An attempt is made to self-update to an older version
55+
updatedVersion = try await SelfUpdate.execute(SwiftlyTests.ctx, verbose: true, version: SwiftlyVersion(major: SwiftlyCore.version.major - 1, minor: 0, patch: 0))
56+
// THEN: There is no change to the swiftly version
57+
#expect(updatedVersion == SwiftlyCore.version)
58+
59+
// WHEN: An attempt is made to self-update to a newer development version
60+
updatedVersion = try await SelfUpdate.execute(SwiftlyTests.ctx, verbose: true, version: Self.newDevVersion)
61+
// THEN: swiftly is updated to the new version
62+
#expect(updatedVersion == Self.newDevVersion)
63+
}
64+
}
65+
}
4066
}

0 commit comments

Comments
 (0)