Skip to content

Commit 5fba3ae

Browse files
committed
Merge branch 'main' of https://github.com/swiftlang/swiftly into xcode_selector
2 parents 91af227 + ab2dc50 commit 5fba3ae

File tree

11 files changed

+462
-184
lines changed

11 files changed

+462
-184
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ swiftly [--version] [--help]
2323
Install a new toolchain.
2424

2525
```
26-
swiftly install [<version>] [--use] [--verify|no-verify] [--post-install-file=<post-install-file>] [--progress-file=<progress-file>] [--assume-yes] [--verbose] [--version] [--help]
26+
swiftly install [<version>] [--use] [--verify|no-verify] [--post-install-file=<post-install-file>] [--progress-file=<progress-file>] [--format=<format>] [--assume-yes] [--verbose] [--version] [--help]
2727
```
2828

2929
**version:**
@@ -89,6 +89,11 @@ Each progress entry contains timestamp, progress percentage, and a descriptive m
8989
The file must be writable, else an error will be thrown.
9090

9191

92+
**--format=\<format\>:**
93+
94+
*Output format (text, json)*
95+
96+
9297
**--assume-yes:**
9398

9499
*Disable confirmation prompts by assuming 'yes'*
@@ -492,7 +497,7 @@ swiftly init [--no-modify-profile] [--overwrite] [--platform=<platform>] [--skip
492497
Update the version of swiftly itself.
493498

494499
```
495-
swiftly self-update [--assume-yes] [--verbose] [--version] [--help]
500+
swiftly self-update [--assume-yes] [--verbose] [--version] [--help]
496501
```
497502

498503
**--assume-yes:**

Sources/Swiftly/Install.swift

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import Foundation
44
import SwiftlyCore
55
import SystemPackage
66
@preconcurrency import TSCBasic
7-
import TSCUtility
87

98
struct Install: SwiftlyCommand {
109
public static let configuration = CommandConfiguration(
@@ -82,14 +81,17 @@ struct Install: SwiftlyCommand {
8281
))
8382
var progressFile: FilePath?
8483

84+
@Option(name: .long, help: "Output format (text, json)")
85+
var format: SwiftlyCore.OutputFormat = .text
86+
8587
@OptionGroup var root: GlobalOptions
8688

8789
private enum CodingKeys: String, CodingKey {
88-
case version, use, verify, postInstallFile, root, progressFile
90+
case version, use, verify, postInstallFile, root, progressFile, format
8991
}
9092

9193
mutating func run() async throws {
92-
try await self.run(Swiftly.createDefaultContext())
94+
try await self.run(Swiftly.createDefaultContext(format: self.format))
9395
}
9496

9597
private func swiftlyHomeDir(_ ctx: SwiftlyCoreContext) -> FilePath {
@@ -267,7 +269,10 @@ struct Install: SwiftlyCommand {
267269
progressFile: FilePath? = nil
268270
) async throws -> (postInstall: String?, pathChanged: Bool) {
269271
guard !config.installedToolchains.contains(version) else {
270-
await ctx.message("\(version) is already installed.")
272+
let installInfo = InstallInfo(
273+
version: version, alreadyInstalled: true
274+
)
275+
try await ctx.output(installInfo)
271276
return (nil, false)
272277
}
273278

@@ -315,16 +320,18 @@ struct Install: SwiftlyCommand {
315320
fatalError("unreachable: xcode toolchain cannot be installed with swiftly")
316321
}
317322

318-
let animation: ProgressAnimationProtocol =
323+
let animation: ProgressReporterProtocol? =
319324
if let progressFile
320325
{
321326
try JsonFileProgressReporter(ctx, filePath: progressFile)
327+
} else if ctx.format == .json {
328+
ConsoleProgressReporter(stream: stderrStream, header: "Downloading \(version)")
322329
} else {
323-
PercentProgressAnimation(stream: stdoutStream, header: "Downloading \(version)")
330+
ConsoleProgressReporter(stream: stdoutStream, header: "Downloading \(version)")
324331
}
325332

326333
defer {
327-
try? (animation as? JsonFileProgressReporter)?.close()
334+
try? animation?.close()
328335
}
329336

330337
var lastUpdate = Date()
@@ -353,22 +360,28 @@ struct Install: SwiftlyCommand {
353360

354361
lastUpdate = Date()
355362

356-
animation.update(
357-
step: progress.receivedBytes,
358-
total: progress.totalBytes!,
359-
text:
360-
"Downloaded \(String(format: "%.1f", downloadedMiB)) MiB of \(String(format: "%.1f", totalMiB)) MiB"
361-
)
363+
do {
364+
try await animation?.update(
365+
step: progress.receivedBytes,
366+
total: progress.totalBytes!,
367+
text:
368+
"Downloaded \(String(format: "%.1f", downloadedMiB)) MiB of \(String(format: "%.1f", totalMiB)) MiB"
369+
)
370+
} catch {
371+
await ctx.message(
372+
"Failed to update progress: \(error.localizedDescription)"
373+
)
374+
}
362375
}
363376
)
364377
} catch let notFound as DownloadNotFoundError {
365378
throw SwiftlyError(
366379
message: "\(version) does not exist at URL \(notFound.url), exiting")
367380
} catch {
368-
animation.complete(success: false)
381+
try? await animation?.complete(success: false)
369382
throw error
370383
}
371-
animation.complete(success: true)
384+
try await animation?.complete(success: true)
372385

373386
if verifySignature {
374387
try await Swiftly.currentPlatform.verifyToolchainSignature(
@@ -424,7 +437,11 @@ struct Install: SwiftlyCommand {
424437
return (pathChanged, config)
425438
}
426439
config = newConfig
427-
await ctx.message("\(version) installed successfully!")
440+
let installInfo = InstallInfo(
441+
version: version,
442+
alreadyInstalled: false
443+
)
444+
try await ctx.output(installInfo)
428445
return (postInstallScript, pathChanged)
429446
}
430447
}

Sources/Swiftly/JsonFileProgressReporter.swift

Lines changed: 0 additions & 62 deletions
This file was deleted.

Sources/Swiftly/OutputSchema.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,28 @@ struct InstalledToolchainsListInfo: OutputData {
355355
return lines.joined(separator: "\n")
356356
}
357357
}
358+
359+
struct InstallInfo: OutputData {
360+
let version: ToolchainVersion
361+
let alreadyInstalled: Bool
362+
363+
init(version: ToolchainVersion, alreadyInstalled: Bool) {
364+
self.version = version
365+
self.alreadyInstalled = alreadyInstalled
366+
}
367+
368+
var description: String {
369+
"\(self.version) is \(self.alreadyInstalled ? "already installed" : "installed successfully!")"
370+
}
371+
372+
private enum CodingKeys: String, CodingKey {
373+
case version
374+
case alreadyInstalled
375+
}
376+
377+
public func encode(to encoder: Encoder) throws {
378+
var container = encoder.container(keyedBy: CodingKeys.self)
379+
try container.encode(self.version.name, forKey: .version)
380+
try container.encode(self.alreadyInstalled, forKey: .alreadyInstalled)
381+
}
382+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Foundation
2+
import SwiftlyCore
3+
import SystemPackage
4+
import TSCBasic
5+
import TSCUtility
6+
7+
public protocol ProgressReporterProtocol {
8+
/// Updates the progress animation with the current step, total steps, and an optional text message.
9+
func update(step: Int, total: Int, text: String) async throws
10+
11+
/// Completes the progress animation, indicating success or failure.
12+
func complete(success: Bool) async throws
13+
14+
/// Closes any resources used by the reporter, if applicable.
15+
func close() throws
16+
}
17+
18+
/// Progress reporter that delegates to a `PercentProgressAnimation` for console output.
19+
struct ConsoleProgressReporter: ProgressReporterProtocol {
20+
private let reporter: PercentProgressAnimation
21+
22+
init(stream: WritableByteStream, header: String) {
23+
self.reporter = PercentProgressAnimation(stream: stream, header: header)
24+
}
25+
26+
func update(step: Int, total: Int, text: String) async throws {
27+
self.reporter.update(step: step, total: total, text: text)
28+
}
29+
30+
func complete(success: Bool) async throws {
31+
self.reporter.complete(success: success)
32+
}
33+
34+
func close() throws {
35+
// No resources to close for console reporter
36+
}
37+
}
38+
39+
enum ProgressInfo: Codable {
40+
case step(timestamp: Date, percent: Int, text: String)
41+
case complete(success: Bool)
42+
}
43+
44+
struct JsonFileProgressReporter: ProgressReporterProtocol {
45+
let filePath: FilePath
46+
private let encoder: JSONEncoder
47+
private let ctx: SwiftlyCoreContext
48+
private let fileHandle: FileHandle
49+
50+
init(_ ctx: SwiftlyCoreContext, filePath: FilePath, encoder: JSONEncoder = JSONEncoder()) throws
51+
{
52+
self.ctx = ctx
53+
self.filePath = filePath
54+
self.encoder = encoder
55+
self.fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: filePath.string))
56+
}
57+
58+
private func writeProgress(_ progress: ProgressInfo) async throws {
59+
let jsonData = try self.encoder.encode(progress)
60+
61+
self.fileHandle.write(jsonData)
62+
self.fileHandle.write("\n".data(using: .utf8) ?? Data())
63+
try self.fileHandle.synchronize()
64+
}
65+
66+
func update(step: Int, total: Int, text: String) async throws {
67+
guard total > 0 && step <= total else {
68+
return
69+
}
70+
try await self.writeProgress(
71+
ProgressInfo.step(
72+
timestamp: Date(),
73+
percent: Int(Double(step) / Double(total) * 100),
74+
text: text
75+
)
76+
)
77+
}
78+
79+
func complete(success: Bool) async throws {
80+
try await self.writeProgress(ProgressInfo.complete(success: success))
81+
}
82+
83+
func close() throws {
84+
try self.fileHandle.close()
85+
}
86+
}

0 commit comments

Comments
 (0)