Files
sing-box-for-apple/Library/Update/GitHubUpdateChecker.swift
T

182 lines
5.5 KiB
Swift

import Darwin
import Foundation
import Libbox
public enum GitHubUpdateChecker {
private static let releasesURL = "https://api.github.com/repos/SagerNet/sing-box/releases"
private static let releasesPerPage = 100
private static let minimumSemver = "0.0.0-0"
public static func checkAsync(track: UpdateTrack, force: Bool = false) async throws -> UpdateInfo? {
try await BlockingIO.run {
try check(track: track, force: force)
}
}
public static func check(track: UpdateTrack, force: Bool = false) throws -> UpdateInfo? {
let client = HTTPClient()
guard let releases = try fetchReleases(client: client, track: track) else {
return nil
}
let currentVersion = Bundle.main.version
var bestRelease: GitHubRelease?
var bestVersion: String?
var bestAsset: GitHubAsset?
for release in releases {
if release.draft { continue }
if track == .stable, release.prerelease { continue }
guard let pkgAsset = findPKGAsset(in: release.assets) else { continue }
let version = release.tagName.hasPrefix("v")
? String(release.tagName.dropFirst())
: release.tagName
guard shouldIncludeRelease(
version: version,
currentVersion: currentVersion,
track: track,
force: force
) else { continue }
if let best = bestVersion {
guard LibboxCompareSemver(version, best) else { continue }
}
bestRelease = release
bestVersion = version
bestAsset = pkgAsset
}
guard let release = bestRelease,
let version = bestVersion,
let pkgAsset = bestAsset
else {
return nil
}
return UpdateInfo(
versionName: version,
releaseURL: release.htmlURL,
downloadURL: pkgAsset.browserDownloadURL,
releaseNotes: release.body,
isPrerelease: release.prerelease,
fileSize: pkgAsset.size
)
}
private static func findPKGAsset(in assets: [GitHubAsset]) -> GitHubAsset? {
let pkgAssets = assets.filter { $0.name.hasSuffix(".pkg") }
let preferred = preferredPKGVariant()
if let match = pkgAssets.first(where: { $0.name.contains(preferred) }) {
return match
}
if let universal = pkgAssets.first(where: { $0.name.contains("Universal") }) {
return universal
}
return pkgAssets.first
}
private static func preferredPKGVariant() -> String {
if let hostSupportsArm64 = hostSupportsArm64() {
return hostSupportsArm64 ? "Apple" : "Intel"
}
#if arch(arm64)
return "Apple"
#else
return "Intel"
#endif
}
private static func hostSupportsArm64() -> Bool? {
var value: Int32 = 0
var size = MemoryLayout.size(ofValue: value)
let result = withUnsafeMutablePointer(to: &value) {
sysctlbyname("hw.optional.arm64", $0, &size, nil, 0)
}
guard result == 0 else {
return nil
}
return value != 0
}
private static func shouldIncludeRelease(
version: String,
currentVersion: String,
track: UpdateTrack,
force: Bool
) -> Bool {
guard isValidSemver(version) else {
return false
}
if force || LibboxCompareSemver(version, currentVersion) {
return true
}
return track == .stable && isValidPrereleaseSemver(currentVersion)
}
private static func isValidSemver(_ version: String) -> Bool {
let trimmedVersion = version.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmedVersion == minimumSemver || LibboxCompareSemver(trimmedVersion, minimumSemver)
}
private static func isValidPrereleaseSemver(_ version: String) -> Bool {
let trimmedVersion = version.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmedVersion.contains("-") && isValidSemver(trimmedVersion)
}
private static func fetchReleases(client: HTTPClient, track: UpdateTrack) throws -> [GitHubRelease]? {
var allReleases: [GitHubRelease] = []
var page = 1
while true {
let releasesJSON = try client.getString("\(releasesURL)?per_page=\(releasesPerPage)&page=\(page)")
guard let data = releasesJSON.data(using: .utf8) else {
return nil
}
let pageReleases = try JSONDecoder().decode([GitHubRelease].self, from: data)
allReleases.append(contentsOf: pageReleases)
if track != .stable || pageReleases.count < releasesPerPage {
return allReleases
}
page += 1
}
}
}
private struct GitHubRelease: Decodable {
let tagName: String
let htmlURL: String
let body: String?
let draft: Bool
let prerelease: Bool
let assets: [GitHubAsset]
enum CodingKeys: String, CodingKey {
case tagName = "tag_name"
case htmlURL = "html_url"
case body
case draft
case prerelease
case assets
}
}
private struct GitHubAsset: Decodable {
let name: String
let browserDownloadURL: String
let size: Int64
enum CodingKeys: String, CodingKey {
case name
case browserDownloadURL = "browser_download_url"
case size
}
}