Skip to content

Commit

Permalink
Merge pull request #129 from interstateone/xcode-releases
Browse files Browse the repository at this point in the history
Add support for and default to Xcode Releases data source
  • Loading branch information
Brandon Evans authored Feb 2, 2021
2 parents a018c5c + 1fe1b46 commit 09532d0
Show file tree
Hide file tree
Showing 16 changed files with 365 additions and 151 deletions.
13 changes: 11 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "XcodeReleases",
"repositoryURL": "https://github.com/xcodereleases/data",
"state": {
"branch": null,
"revision": "b47228c688b608e34b3b84079ab6052a24c7a981",
"version": null
}
},
{
"package": "PMKFoundation",
"repositoryURL": "https://github.com/PromiseKit/Foundation.git",
Expand Down Expand Up @@ -51,8 +60,8 @@
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
"revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc",
"version": "0.3.2"
}
},
{
Expand Down
28 changes: 22 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,53 @@ let package = Package(
.package(url: "https://github.com/scinfu/SwiftSoup.git", .upToNextMinor(from: "2.0.0")),
.package(url: "https://github.com/mxcl/LegibleError.git", .upToNextMinor(from: "1.0.1")),
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMinor(from: "3.2.0")),
.package(name: "XcodeReleases", url: "https://github.com/xcodereleases/data", .revision("b47228c688b608e34b3b84079ab6052a24c7a981")),
],
targets: [
.target(
name: "xcodes",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"), "XcodesKit"
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"XcodesKit"
]),
.testTarget(
name: "xcodesTests",
dependencies: ["xcodes"]),
dependencies: [
"xcodes"
]),
.target(
name: "XcodesKit",
dependencies: [
"AppleAPI", .product(name: "Path", package: "Path.swift"), "Version", "PromiseKit", "PMKFoundation", "SwiftSoup", "LegibleError", "KeychainAccess"
"AppleAPI",
"KeychainAccess",
"LegibleError",
.product(name: "Path", package: "Path.swift"),
"PromiseKit",
"PMKFoundation",
"SwiftSoup",
"Version",
.product(name: "XCModel", package: "XcodeReleases"),
]),
.testTarget(
name: "XcodesKitTests",
dependencies: [
"XcodesKit", "Version"
"XcodesKit",
"Version"
],
resources: [
.copy("Fixtures"),
]),
.target(
name: "AppleAPI",
dependencies: [
"PromiseKit", "PMKFoundation"
"PromiseKit",
"PMKFoundation"
]),
.testTarget(
name: "AppleAPITests",
dependencies: ["AppleAPI"],
dependencies: [
"AppleAPI"
],
resources: [
.copy("Fixtures"),
]),
Expand Down
6 changes: 6 additions & 0 deletions Sources/XcodesKit/DataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

public enum DataSource: String, CaseIterable {
case apple
case xcodeReleases
}
24 changes: 16 additions & 8 deletions Sources/XcodesKit/Models+FirstWithVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import Version

/// Returns the first XcodeType that unambiguously has the same version as `version`.
///
/// If there's an exact match that takes prerelease identifiers into account, that's returned.
/// If there's an equivalent match that takes prerelease identifiers into account, that's returned.
/// Otherwise, if a version without prerelease or build metadata identifiers is provided, and there's a single match based on only the major, minor and patch numbers, that's returned.
/// If there are multiple matches, or no matches, nil is returned.
public func findXcode<XcodeType>(version: Version, in xcodes: [XcodeType], versionKeyPath: KeyPath<XcodeType, Version>) -> XcodeType? {
// Look for the exact provided version first
if let installedXcode = xcodes.first(where: { $0[keyPath: versionKeyPath].isEqualWithoutBuildMetadataIdentifiers(to: version) }) {
return installedXcode
// Look for the equivalent provided version first
if let equivalentXcode = xcodes.first(where: { $0[keyPath: versionKeyPath].isEquivalent(to: version) }) {
return equivalentXcode
}
// If a short version is provided, look again for a match, ignore all
// identifiers this time. Ignore if there are more than one match.
// If a version without prerelease or build identifiers is provided, then ignore all identifiers this time.
// There must be exactly one match.
else if version.prereleaseIdentifiers.isEmpty && version.buildMetadataIdentifiers.isEmpty,
xcodes.filter({ $0[keyPath: versionKeyPath].isEqualWithoutAllIdentifiers(to: version) }).count == 1 {
let installedXcode = xcodes.first(where: { $0[keyPath: versionKeyPath].isEqualWithoutAllIdentifiers(to: version) })!
return installedXcode
let matchedXcode = xcodes.first(where: { $0[keyPath: versionKeyPath].isEqualWithoutAllIdentifiers(to: version) })!
return matchedXcode
} else {
return nil
}
Expand All @@ -43,3 +43,11 @@ public extension Array where Element == InstalledXcode {
findXcode(version: version, in: self, versionKeyPath: \.version)
}
}

extension Version {
func isEqualWithoutAllIdentifiers(to other: Version) -> Bool {
return major == other.major &&
minor == other.minor &&
patch == other.patch
}
}
53 changes: 13 additions & 40 deletions Sources/XcodesKit/Version+.swift
Original file line number Diff line number Diff line change
@@ -1,47 +1,20 @@
import Version

public extension Version {
func isEqualWithoutBuildMetadataIdentifiers(to other: Version) -> Bool {
return major == other.major &&
minor == other.minor &&
patch == other.patch &&
prereleaseIdentifiers == other.prereleaseIdentifiers
}

func isEqualWithoutAllIdentifiers(to other: Version) -> Bool {
return major == other.major &&
minor == other.minor &&
patch == other.patch
}

/// If release versions, don't compare build metadata because that's not provided in the /downloads/more list
/// if beta versions, compare build metadata because it's available in versions.plist
func isEquivalentForDeterminingIfInstalled(toInstalled installed: Version) -> Bool {
let isBeta = !prereleaseIdentifiers.isEmpty
let otherIsBeta = !installed.prereleaseIdentifiers.isEmpty

if isBeta && otherIsBeta {
if buildMetadataIdentifiers.isEmpty {
return major == installed.major &&
minor == installed.minor &&
patch == installed.patch &&
prereleaseIdentifiers == installed.prereleaseIdentifiers
}
else {
return major == installed.major &&
minor == installed.minor &&
patch == installed.patch &&
prereleaseIdentifiers == installed.prereleaseIdentifiers &&
buildMetadataIdentifiers.map { $0.lowercased() } == installed.buildMetadataIdentifiers.map { $0.lowercased() }
}
}
else if !isBeta && !otherIsBeta {
return major == installed.major &&
minor == installed.minor &&
patch == installed.patch
/// Determines if two Xcode versions should be treated equivalently. This is not the same as equality.
///
/// We need a way to determine if two Xcode versions are the same without always having full information, and supporting different data sources.
/// For example, the Apple data source often doesn't have build metadata identifiers.
func isEquivalent(to other: Version) -> Bool {
// If we don't have build metadata identifiers for both Versions, compare major, minor, patch and prerelease identifiers.
if buildMetadataIdentifiers.isEmpty || other.buildMetadataIdentifiers.isEmpty {
return isEqualWithoutAllIdentifiers(to: other) &&
prereleaseIdentifiers.map { $0.lowercased() } == other.prereleaseIdentifiers.map { $0.lowercased() }
// If we have build metadata identifiers for both, we can ignore the prerelease identifiers.
} else {
return isEqualWithoutAllIdentifiers(to: other) &&
buildMetadataIdentifiers.map { $0.lowercased() } == other.buildMetadataIdentifiers.map { $0.lowercased() }
}

return false
}

var descriptionWithoutBuildMetadata: String {
Expand Down
26 changes: 18 additions & 8 deletions Sources/XcodesKit/Version+Xcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public extension Version {
*/
init?(xcodeVersion: String, buildMetadataIdentifier: String? = nil) {
let nsrange = NSRange(xcodeVersion.startIndex..<xcodeVersion.endIndex, in: xcodeVersion)
// https://regex101.com/r/dLLvsz/1
let pattern = "^(Xcode )?(?<major>\\d+)\\.?(?<minor>\\d?)\\.?(?<patch>\\d?) ?(?<prereleaseType>[a-zA-Z ]+)? ?(?<prereleaseVersion>\\d?)"
// https://regex101.com/r/K7530Z/1
let pattern = "^(Xcode )?(?<major>\\d+)\\.?(?<minor>\\d*)\\.?(?<patch>\\d*) ?(?<prereleaseType>[a-zA-Z ]+)? ?(?<prereleaseVersion>\\d*)"

guard
let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive),
Expand All @@ -38,22 +38,32 @@ public extension Version {
self = Version(major: major, minor: minor, patch: patch, prereleaseIdentifiers: prereleaseIdentifiers, buildMetadataIdentifiers: [buildMetadataIdentifier].compactMap { $0 })
}

var xcodeDescription: String {
/// The intent here is to match Apple's marketing version
///
/// Only show the patch number if it's not 0
/// Format prerelease identifiers
/// Don't include build identifiers
var appleDescription: String {
var base = "\(major).\(minor)"
if patch != 0 {
base += ".\(patch)"
}
if !prereleaseIdentifiers.isEmpty {
base += " " + prereleaseIdentifiers
.map { $0.replacingOccurrences(of: "-", with: " ").capitalized.replacingOccurrences(of: "Gm", with: "GM") }
.map { identifier in
identifier
.replacingOccurrences(of: "-", with: " ")
.capitalized
.replacingOccurrences(of: "Gm", with: "GM")
.replacingOccurrences(of: "Rc", with: "RC")
}
.joined(separator: " ")

if !buildMetadataIdentifiers.isEmpty {
base += " (\(buildMetadataIdentifiers.joined(separator: " ")))"
}
}
return base
}
var appleDescriptionWithBuildIdentifier: String {
[appleDescription, buildMetadataIdentifiersDisplay].filter { !$0.isEmpty }.joined(separator: " ")
}
}

extension NSTextCheckingResult {
Expand Down
54 changes: 54 additions & 0 deletions Sources/XcodesKit/Version+XcodeReleases.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Version
import struct XCModel.Xcode

extension Version {
/// Initialize a Version from an XcodeReleases' XCModel.Xcode
///
/// This is kinda quick-and-dirty, and it would probably be better for us to adopt something closer to XCModel.Xcode under the hood and map the scraped data to it instead.
init?(xcReleasesXcode: XCModel.Xcode) {
var versionString = xcReleasesXcode.version.number ?? ""

// Append trailing ".0" in order to get a fully-specified version string
let components = versionString.components(separatedBy: ".")
versionString += Array(repeating: ".0", count: 3 - components.count).joined()

// Append prerelease identifier
switch xcReleasesXcode.version.release {
case let .beta(beta):
versionString += "-Beta"
if beta > 1 {
versionString += ".\(beta)"
}
case let .dp(dp):
versionString += "-DP"
if dp > 1 {
versionString += ".\(dp)"
}
case .gm:
versionString += "-GM"
case let .gmSeed(gmSeed):
versionString += "-GM.Seed"
if gmSeed > 1 {
versionString += ".\(gmSeed)"
}
case let .rc(rc):
versionString += "-Release.Candidate"
if rc > 1 {
versionString += ".\(rc)"
}
case .release:
break
}

// Append build identifier
if let buildNumber = xcReleasesXcode.version.build {
versionString += "+\(buildNumber)"
}

self.init(versionString)
}

var buildMetadataIdentifiersDisplay: String {
return !buildMetadataIdentifiers.isEmpty ? "(\(buildMetadataIdentifiers.joined(separator: " ")))" : ""
}
}
Loading

0 comments on commit 09532d0

Please sign in to comment.