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
3 changes: 3 additions & 0 deletions .github/workflows/ci-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ jobs:
fi
done
ruby -c scripts/update-homebrew-cask.rb
ruby -c scripts/create-macos-update-manifest.rb
ruby -c scripts/test-macos-update-manifest.rb
ruby scripts/test-macos-update-manifest.rb
plutil -lint Resources/Info.plist

- name: Build release configuration
Expand Down
37 changes: 28 additions & 9 deletions .github/workflows/release-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ permissions:
contents: read

concurrency:
group: release-macos-${{ github.ref }}
group: release-macos-${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref }}
cancel-in-progress: false

jobs:
Expand Down Expand Up @@ -177,32 +177,51 @@ jobs:
(cd dist && shasum -a 256 -c "Lithe-${LITHE_VERSION}-${architecture}.dmg.sha256")
done

- name: Create GitHub Release
- name: Generate macOS update manifest
env:
LITHE_VERSION: ${{ needs.prepare.outputs.version }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
run: |
ruby scripts/create-macos-update-manifest.rb \
--version "$LITHE_VERSION" \
--repository "$GITHUB_REPOSITORY" \
--release-tag "$RELEASE_TAG"
ruby -rjson -e 'manifest = JSON.parse(File.read("dist/latest-macos.json")); abort "Invalid schema" unless manifest["schemaVersion"] == 1'

- name: Create or update GitHub Release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
LITHE_VERSION: ${{ needs.prepare.outputs.version }}
shell: bash
run: |
notes_file="docs/releases/v${LITHE_VERSION}.md"
release_args=(
create_args=(
"$RELEASE_TAG"
--repo "$GITHUB_REPOSITORY"
--target "$GITHUB_SHA"
--title "Lithe $RELEASE_TAG"
)
upload_args=(
"dist/Lithe-${LITHE_VERSION}-arm64.dmg"
"dist/Lithe-${LITHE_VERSION}-arm64.dmg.sha256"
"dist/Lithe-${LITHE_VERSION}-x86_64.dmg"
"dist/Lithe-${LITHE_VERSION}-x86_64.dmg.sha256"
--repo "$GITHUB_REPOSITORY"
--target "$GITHUB_SHA"
--title "Lithe $RELEASE_TAG"
"dist/latest-macos.json"
)

if [[ -f "$notes_file" ]]; then
release_args+=(--notes-file "$notes_file")
create_args+=(--notes-file "$notes_file")
else
release_args+=(--generate-notes)
create_args+=(--generate-notes)
fi

gh release create "${release_args[@]}"
if ! gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create "${create_args[@]}" || \
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null
fi
gh release upload "$RELEASE_TAG" "${upload_args[@]}" \
--repo "$GITHUB_REPOSITORY" --clobber

update-cask:
name: Update Homebrew Cask
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/release-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ on:
permissions:
contents: read

concurrency:
group: release-windows-${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref }}
cancel-in-progress: false

jobs:
release:
name: Build and publish Lithe for Windows
Expand Down Expand Up @@ -145,6 +149,7 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw "Could not create or find release $env:RELEASE_TAG" }
}
}

gh release upload $env:RELEASE_TAG `
"dist/Lithe-$env:LITHE_VERSION-windows-x64.exe" `
"dist/Lithe-$env:LITHE_VERSION-windows-x64.exe.sha256" `
Expand Down
12 changes: 12 additions & 0 deletions Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,18 @@
"No release is available yet" = "暂时没有可用版本";
"Could not check for updates" = "无法检查更新";
"There is no published GitHub Release to check yet." = "目前还没有可检查的 GitHub Release。";
"GitHub rejected the update request because a shared API limit was reached. Open the Release page or try again after the limit resets." = "GitHub 因共享 API 配额已耗尽而拒绝了更新请求。请打开 Release 页面,或等待配额重置后重试。";
"The update server returned HTTP %@. Open the Release page to download the update manually, or try again later." = "更新服务器返回 HTTP %@。请打开 Release 页面手动下载,或稍后重试。";
"The update request timed out. Check your proxy or VPN connection and try again." = "更新请求超时。请检查代理或 VPN 连接后重试。";
"A secure connection to GitHub could not be established. Check TLS inspection, proxy, VPN, or system certificate settings." = "无法与 GitHub 建立安全连接。请检查 TLS 检查、代理、VPN 或系统证书设置。";
"GitHub could not be reached. Check your internet, proxy, or VPN connection and try again." = "无法连接 GitHub。请检查网络、代理或 VPN 连接后重试。";
"The update server returned an unexpected response. Open the Release page and download the update manually." = "更新服务器返回了异常响应。请打开 Release 页面手动下载更新。";
"The published update manifest is invalid and cannot be trusted. Open the Release page and download the update manually." = "已发布的更新清单无效,无法信任。请打开 Release 页面手动下载更新。";
"This version of Lithe cannot read update manifest schema %@. Open the Release page and update manually." = "此版本的 Lithe 无法读取版本为 %@ 的更新清单格式。请打开 Release 页面手动更新。";
"No update package is available for this Mac architecture. Open the Release page to check available downloads." = "没有适用于此 Mac 架构的更新包。请打开 Release 页面查看可用下载。";
"The downloaded update failed its SHA-256 verification. Do not install it; retry or use the Release page." = "下载的更新未通过 SHA-256 校验,请勿安装。请重试或通过 Release 页面下载。";
"The update package could not be downloaded. Check your internet, proxy, or VPN connection and try again." = "无法下载更新包。请检查网络、代理或 VPN 连接后重试。";
"macOS could not prepare the update disk image. Open the Release page and install it manually." = "macOS 无法准备更新磁盘映像。请打开 Release 页面手动安装。";
"Check your internet connection and try again later." = "请检查网络连接,稍后再试。";
"No Java navigation results" = "没有 Java 导航结果";
"Choose Implementation" = "选择实现";
Expand Down
110 changes: 110 additions & 0 deletions Sources/Lithe/Platform/MacOS/Updates/MacUpdateNetworkTransport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Foundation

struct UpdateHTTPResponse: Sendable {
let statusCode: Int
let headers: [String: String]
let body: Data

func header(named name: String) -> String? {
headers.first { $0.key.caseInsensitiveCompare(name) == .orderedSame }?.value
}
}

protocol UpdateNetworkTransport: Sendable {
func fetch(_ request: URLRequest) async throws -> UpdateHTTPResponse
/// Returns a temporary file whose ownership transfers to the caller.
func download(
_ request: URLRequest,
progress: @escaping @Sendable (UpdateDownloadProgress) async -> Void
) async throws -> URL
}

enum UpdateTransportError: Error {
case invalidResponse
}

final class MacUpdateNetworkTransport: UpdateNetworkTransport, @unchecked Sendable {
private let session: URLSession
private let fileManager: FileManager

init(session: URLSession = .shared, fileManager: FileManager = .default) {
self.session = session
self.fileManager = fileManager
}

func fetch(_ request: URLRequest) async throws -> UpdateHTTPResponse {
let (data, response) = try await session.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw UpdateTransportError.invalidResponse
}
return UpdateHTTPResponse(
statusCode: response.statusCode,
headers: response.allHeaderFields.reduce(into: [:]) { headers, entry in
headers[String(describing: entry.key)] = String(describing: entry.value)
},
body: data
)
}

func download(
_ request: URLRequest,
progress: @escaping @Sendable (UpdateDownloadProgress) async -> Void
) async throws -> URL {
let (bytes, response) = try await session.bytes(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw UpdateTransportError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw UpdateCheckError.httpStatus(httpResponse.statusCode)
}

let totalBytes = response.expectedContentLength > 0
? response.expectedContentLength
: nil
let destination = fileManager.temporaryDirectory
.appendingPathComponent("lithe-update-\(UUID().uuidString).dmg")
guard fileManager.createFile(atPath: destination.path, contents: nil) else {
throw UpdateCheckError.downloadFailed
}

do {
let handle = try FileHandle(forWritingTo: destination)
defer { try? handle.close() }

var buffer = Data()
buffer.reserveCapacity(64 * 1024)
var downloadedBytes: Int64 = 0
var lastProgressUpdate = Date.distantPast

for try await byte in bytes {
buffer.append(byte)
downloadedBytes += 1

if buffer.count >= 64 * 1024 {
try handle.write(contentsOf: buffer)
buffer.removeAll(keepingCapacity: true)
let now = Date()
if now.timeIntervalSince(lastProgressUpdate) >= 0.05 {
lastProgressUpdate = now
await progress(UpdateDownloadProgress(
downloadedBytes: downloadedBytes,
totalBytes: totalBytes
))
}
}
}

if !buffer.isEmpty {
try handle.write(contentsOf: buffer)
}
await progress(UpdateDownloadProgress(
downloadedBytes: downloadedBytes,
totalBytes: totalBytes
))
return destination
} catch {
try? fileManager.removeItem(at: destination)
throw error
}
}
}
Loading
Loading