Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for and default to Xcode Releases data source #129

Merged
merged 6 commits into from
Feb 2, 2021
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
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