182 lines
5.5 KiB
Swift
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
|
|
}
|
|
}
|