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
10 changes: 5 additions & 5 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let package = Package(
),
.package(
url: "https://github.com/saagarjha/unxip",
branch: "main"
from: "3.2.0"
),
],
targets: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@ struct InstalledXcodeVersion: ParsableArguments {
help: ArgumentHelp(
"The version number of Xcode to uninstall."
),
completion: .custom { words in
completion: .custom { _, _, _ in
do {
return try _unsafeWait {
var formatter = XcodeVersionFormatter()
formatter.separator = "-"

return try await XcodeApplication
.all()
.map(formatter.string)
}
var formatter = XcodeVersionFormatter()
formatter.separator = "-"

return try await XcodeApplication
.all()
.map(formatter.string)
} catch {
return []
}
Expand Down
44 changes: 30 additions & 14 deletions Sources/XcodeVersionManager/Commands/DownloadCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ struct DownloadCommand: AsyncParsableCommand {
abstract: "Open the browser to download a version of Xcode."
)

@Flag(
name: .shortAndLong,
help: .init("Prefer downloading a Universal version, if available.")
)
var preferUniversal: Bool = false

@Flag(
name: .shortAndLong,
help: .init("Download the matched version of Xcode without prompting for confirmation.")
Expand All @@ -19,17 +25,12 @@ struct DownloadCommand: AsyncParsableCommand {
"The version number to download.",
discussion: "Matches Xcode versions that start with the entered string. In the case of multiple matches, the newest matching version of Xcode is used."
),
completion: .custom { _ in
completion: .custom { _, _, _ in
do {
struct EmptyResponse: Error {}

let versionNumbers = try _unsafeWait {
let releases = try await XcodeReleases().releases
guard !releases.isEmpty else { throw EmptyResponse() }
return releases.map { $0.version.formatted(style: .option) }
}

return ["latest"] + Set(versionNumbers).sorted(by: >)
let releases = try await XcodeReleasesAPI.releases()
let versionNumbers = releases.map { $0.version.formatted(style: .option) }
let versions = ["latest"] + versionNumbers
return versions
} catch {
return []
}
Expand All @@ -38,13 +39,28 @@ struct DownloadCommand: AsyncParsableCommand {
var version: String = "latest"

func run() async throws {
let releases = try await XcodeReleases()
let currentArchitecture = Architecture.current

let releases = try await XcodeReleasesAPI.releases()
.filter { release in
guard let currentArchitecture else { return true }

return release.download.architectures.isEmpty
|| release.download.architectures.contains(currentArchitecture)
}
.sorted { lhs, rhs in
if preferUniversal {
lhs.download.architectures.count > rhs.download.architectures.count
} else {
lhs.download.architectures.count < rhs.download.architectures.count
Comment thread
CraigSiemens marked this conversation as resolved.
}
}

let matchingRelease: XcodeRelease?
if version == "latest" {
matchingRelease = releases.releases.first
matchingRelease = releases.first
} else {
matchingRelease = releases.releases
matchingRelease = releases
.first { $0.version.formatted(style: .option).hasPrefix(version) }
}

Expand All @@ -62,7 +78,7 @@ struct DownloadCommand: AsyncParsableCommand {

var downloadComponents = URLComponents(string: "https://developer.apple.com/services-account/download")!
downloadComponents.queryItems = [
.init(name: "path", value: matchingRelease.downloadURL.path)
.init(name: "path", value: matchingRelease.download.url.path)
]

NSWorkspace.shared.open(downloadComponents.url!)
Expand Down
129 changes: 98 additions & 31 deletions Sources/XcodeVersionManager/Commands/ListCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,99 @@ import TableKit
struct ListCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "Lists the installed versions of Xcode."
abstract: "Lists versions of Xcode.",
subcommands: [
InstalledCommand.self,
KnownCommand.self
],
defaultSubcommand: InstalledCommand.self
)

func run() async throws {
let formatter = XcodeVersionFormatter()
}

extension ListCommand {
struct InstalledCommand: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
commandName: "installed",
abstract: "Lists the installed versions of Xcode."
)

let current = try XcodeApplication.current()
func run() async throws {
let formatter = XcodeVersionFormatter()

let current = try XcodeApplication.current()

let rows = try await XcodeApplication
.all()
.sorted(by: >)
.map {
Row(
current: $0 == current ? "*" : " ",
formattedVersion: formatter.string(from: $0),
buildNumber: $0.buildNumber,
name: $0.url.lastPathComponent
)
}

let style = TableStyle(header: .inside, body: .inside, paddingSize: 1)
let encoder = TableEncoder(style: style)
print(try encoder.encode(rows))
}

let formattedValues = try await XcodeApplication
.all()
.sorted(by: <)
.map {
FormattedXcodeApplication(
current: $0 == current ? "*" : " ",
formattedVersion: formatter.string(from: $0),
buildNumber: $0.buildNumber,
name: $0.url.lastPathComponent
)
private struct Row: Encodable {
let current: String
let formattedVersion: String
let buildNumber: String
let name: String

enum CodingKeys: String, CodingKey {
case current = ""
case formattedVersion = "Version"
case buildNumber = "Build"
case name = "Name"
}

let style = TableStyle(header: .inside, body: .inside, paddingSize: 1)
let encoder = TableEncoder(style: style)
print(try encoder.encode(formattedValues))
}
}
}

// MARK: - FormattedXcodeApplication
private struct FormattedXcodeApplication: Encodable {
let current: String
let formattedVersion: String
let buildNumber: String
let name: String

enum CodingKeys: String, CodingKey {
case current = ""
case formattedVersion = "Version"
case buildNumber = "Build"
case name = "Name"
extension ListCommand {
struct KnownCommand: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
commandName: "known",
abstract: "Lists the known versions of Xcode."
)

func run() async throws {
let rows = try await XcodeReleasesAPI
.releases()
.map {
Row(
formattedVersion: $0.version.formatted(style: .pretty),
buildNumber: $0.version.build,
requires: $0.requires.formatted(),
architectures: $0.download.architectures
.map(\.rawValue)
.joined(separator: ", ")
)
}

let style = TableStyle(header: .inside, body: .inside, paddingSize: 1)
let encoder = TableEncoder(style: style)
print(try encoder.encode(rows))
}

private struct Row: Encodable {
let formattedVersion: String
let buildNumber: String
let requires: String
let architectures: String

enum CodingKeys: String, CodingKey {
case formattedVersion = "Version"
case buildNumber = "Build"
case requires = "Requires"
case architectures = "Architectures"
}
}
}
}

Expand All @@ -61,3 +116,15 @@ private extension TableStyle.Body {
.init(inner: "│")
}
}

extension OperatingSystemVersion {
func formatted() -> String {
var parts: [Int] = [
majorVersion, minorVersion
]

if patchVersion != 0 { parts.append(patchVersion) }

return parts.map(String.init).joined(separator: ".")
}
}
22 changes: 22 additions & 0 deletions Sources/XcodeVersionManager/Models/Architecture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Darwin

enum Architecture: String {
case arm64
case x86_64

static var current: Self? {
var name: utsname = .init()
uname(&name)

let machine = withUnsafePointer(to: name.machine) { pointer in
pointer.withMemoryRebound(
to: UInt8.self,
capacity: MemoryLayout.size(ofValue: name.machine)
) { pointer in
String(cString: pointer)
}
}

return .init(rawValue: machine)
}
}
13 changes: 13 additions & 0 deletions Sources/XcodeVersionManager/Models/CalendarDate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
struct CalendarDate: Decodable {
let year: Int
let month: Int
let day: Int
}

extension CalendarDate: Comparable {
static func < (lhs: CalendarDate, rhs: CalendarDate) -> Bool {
guard lhs.year == rhs.year else { return lhs.year < rhs.year }
guard lhs.month == rhs.month else { return lhs.month < rhs.month }
return lhs.day < rhs.day
}
}
37 changes: 37 additions & 0 deletions Sources/XcodeVersionManager/Models/XcodeRelease.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

struct XcodeRelease {
let download: Download
let version: Version
let requires: OperatingSystemVersion
let date: CalendarDate

struct Download: Equatable {
let architectures: [Architecture]
let url: URL
}

struct Version: Equatable {
let number: String
let build: String
let release: Release

enum Release: Codable, Equatable {
case beta(Int)
case rc(Int)
case release
}
}
}

extension XcodeRelease: Equatable {
static func == (lhs: XcodeRelease, rhs: XcodeRelease) -> Bool {
lhs.version == rhs.version
}
}

extension XcodeRelease: Comparable {
static func < (lhs: XcodeRelease, rhs: XcodeRelease) -> Bool {
lhs.date < rhs.date
}
}
Loading