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
29 changes: 17 additions & 12 deletions Sources/Swiftly/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Foundation
import SwiftlyCore
import SystemPackage
@preconcurrency import TSCBasic
import TSCUtility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: It's great to see this TSC dependency being further isolated. The hope is to remove the dependency entirely someday, and use something that isn't deprecated, and adds to the size of the swiftly binary.


struct Install: SwiftlyCommand {
public static let configuration = CommandConfiguration(
Expand Down Expand Up @@ -313,16 +312,16 @@ struct Install: SwiftlyCommand {
}
}

let animation: ProgressAnimationProtocol =
let animation: ProgressReporterProtocol =
if let progressFile
{
try JsonFileProgressReporter(ctx, filePath: progressFile)
} else {
PercentProgressAnimation(stream: stdoutStream, header: "Downloading \(version)")
ConsoleProgressReporter(stream: stdoutStream, header: "Downloading \(version)")
}

defer {
try? (animation as? JsonFileProgressReporter)?.close()
try? animation.close()
}

var lastUpdate = Date()
Expand Down Expand Up @@ -351,22 +350,28 @@ struct Install: SwiftlyCommand {

lastUpdate = Date()

animation.update(
step: progress.receivedBytes,
total: progress.totalBytes!,
text:
"Downloaded \(String(format: "%.1f", downloadedMiB)) MiB of \(String(format: "%.1f", totalMiB)) MiB"
)
do {
try await animation.update(
step: progress.receivedBytes,
total: progress.totalBytes!,
text:
"Downloaded \(String(format: "%.1f", downloadedMiB)) MiB of \(String(format: "%.1f", totalMiB)) MiB"
)
} catch {
await ctx.message(
"Failed to update progress: \(error.localizedDescription)"
)
}
}
)
} catch let notFound as DownloadNotFoundError {
throw SwiftlyError(
message: "\(version) does not exist at URL \(notFound.url), exiting")
} catch {
animation.complete(success: false)
try? await animation.complete(success: false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: It's good to do a best effort complete here so that the original error gets reported since that's probably the more relevant one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: It's good to do a best effort complete here so that the original error gets reported, since that's probably the more relevant one.

I didn't get this @cmcgee1024.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roulpriya in this catch block, there's an error that was thrown. The animation.complete() can also throw an error. I think that the first error is probably more important and should be the one that is thrown. This code is doing the right thing in my opinion. :+1

Copy link
Member

@cmcgee1024 cmcgee1024 Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roulpriya in this catch block, there's an error that was thrown. The animation.complete() can also throw an error. I think that the first error is probably more important and should be the one that is thrown. This code is doing the right thing in my opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, got it.

throw error
}
animation.complete(success: true)
try await animation.complete(success: true)

if verifySignature {
try await Swiftly.currentPlatform.verifyToolchainSignature(
Expand Down
62 changes: 0 additions & 62 deletions Sources/Swiftly/JsonFileProgressReporter.swift

This file was deleted.

86 changes: 86 additions & 0 deletions Sources/Swiftly/ProgressReporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Foundation
import SwiftlyCore
import SystemPackage
import TSCBasic
import TSCUtility

public protocol ProgressReporterProtocol {
/// Updates the progress animation with the current step, total steps, and an optional text message.
func update(step: Int, total: Int, text: String) async throws

/// Completes the progress animation, indicating success or failure.
func complete(success: Bool) async throws

/// Closes any resources used by the reporter, if applicable.
func close() throws
}

/// Progress reporter that delegates to a `PercentProgressAnimation` for console output.
struct ConsoleProgressReporter: ProgressReporterProtocol {
private let reporter: PercentProgressAnimation

init(stream: WritableByteStream, header: String) {
self.reporter = PercentProgressAnimation(stream: stream, header: header)
}

func update(step: Int, total: Int, text: String) async throws {
self.reporter.update(step: step, total: total, text: text)
}

func complete(success: Bool) async throws {
self.reporter.complete(success: success)
}

func close() throws {
// No resources to close for console reporter
}
}

enum ProgressInfo: Codable {
case step(timestamp: Date, percent: Int, text: String)
case complete(success: Bool)
}

struct JsonFileProgressReporter: ProgressReporterProtocol {
let filePath: FilePath
private let encoder: JSONEncoder
private let ctx: SwiftlyCoreContext
private let fileHandle: FileHandle

init(_ ctx: SwiftlyCoreContext, filePath: FilePath, encoder: JSONEncoder = JSONEncoder()) throws
{
self.ctx = ctx
self.filePath = filePath
self.encoder = encoder
self.fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: filePath.string))
}

private func writeProgress(_ progress: ProgressInfo) async throws {
let jsonData = try self.encoder.encode(progress)

self.fileHandle.write(jsonData)
self.fileHandle.write("\n".data(using: .utf8) ?? Data())
try self.fileHandle.synchronize()
}

func update(step: Int, total: Int, text: String) async throws {
guard total > 0 && step <= total else {
return
}
try await self.writeProgress(
ProgressInfo.step(
timestamp: Date(),
percent: Int(Double(step) / Double(total) * 100),
text: text
)
)
}

func complete(success: Bool) async throws {
try await self.writeProgress(ProgressInfo.complete(success: success))
}

func close() throws {
try self.fileHandle.close()
}
}
Loading