From b0f8695a2553a95dca568bc9064142737a75ee47 Mon Sep 17 00:00:00 2001 From: luochen570 <1160510664@qq.com> Date: Sat, 30 May 2026 20:50:34 +0800 Subject: [PATCH] ci: build release on Windows runner --- .gitea/workflows/build-windows-release.yml | 310 +++++++++------------ 1 file changed, 136 insertions(+), 174 deletions(-) diff --git a/.gitea/workflows/build-windows-release.yml b/.gitea/workflows/build-windows-release.yml index 03d49b7..766c2ce 100644 --- a/.gitea/workflows/build-windows-release.yml +++ b/.gitea/workflows/build-windows-release.yml @@ -12,217 +12,179 @@ permissions: contents: write jobs: - cross-build-windows: - name: Cross build Windows x86_64 Release - # Gitea act_runner 标签为 9950x;该工作流在 Linux Runner 中交叉编译 Windows x86_64 包。 - runs-on: 9950x - container: - image: fedora:latest + build-windows-release: + name: Build Windows x64 Release + # 需要 Gitea act_runner 中存在并在线的 Windows runner,且带有 windows 标签。 + runs-on: windows + + env: + BUILD_TYPE: Release + QT_VERSION: '6.7.3' + QT_ARCH: win64_msvc2019_64 + QT_DIR_NAME: msvc2019_64 + DIST_DIR: dist-windows\chengnan + PACKAGE_BASENAME: chengnan-windows-x64 steps: - - name: Configure Fedora TUNA mirror - run: | - set -euo pipefail - sed -e 's|^metalink=|#metalink=|g' \ - -e 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://mirrors.tuna.tsinghua.edu.cn/fedora|g' \ - -i /etc/yum.repos.d/fedora*.repo - if [ -f /etc/yum.repos.d/fedora-cisco-openh264.repo ]; then - sed -i 's|^enabled=1|enabled=0|g' /etc/yum.repos.d/fedora-cisco-openh264.repo - fi - dnf makecache --refresh -y - - name: Checkout - run: | - set -euo pipefail - dnf install -y git rsync - WORKDIR="$(pwd)" - SRC_DIR="$(mktemp -d /tmp/chengnan-source.XXXXXX)" - REPO_URL="${GITHUB_SERVER_URL:-https://git.luochen570.cn}/${GITHUB_REPOSITORY}.git" - if [ -n "${GITHUB_TOKEN:-}" ]; then - git -c http.extraHeader="Authorization: token ${GITHUB_TOKEN}" \ - clone --depth 1 --branch "${GITHUB_REF_NAME:-main}" "$REPO_URL" "$SRC_DIR" - else - git clone --depth 1 --branch "${GITHUB_REF_NAME:-main}" "$REPO_URL" "$SRC_DIR" - fi - rsync -a --delete "$SRC_DIR/" "$WORKDIR/" - rm -rf "$SRC_DIR" + uses: actions/checkout@v4 - - name: Install cross build dependencies - run: | - set -euo pipefail - dnf install -y \ - cmake \ - curl \ - ninja-build \ - p7zip \ - p7zip-plugins \ - mingw64-gcc-c++ \ - mingw64-qt6-qtbase + - name: Setup MSVC developer command prompt + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 - - name: Configure - run: | - set -euo pipefail - cmake -S . -B build-windows-cross -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_TOOLCHAIN_FILE=/usr/share/mingw/toolchain-mingw64.cmake + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' - - name: Build + - name: Install Qt + shell: powershell run: | - set -euo pipefail - cmake --build build-windows-cross --parallel "$(nproc)" + $ErrorActionPreference = 'Stop' + python -m pip install --upgrade pip + python -m pip install aqtinstall + python -m aqt install-qt windows desktop $env:QT_VERSION $env:QT_ARCH --outputdir C:\Qt --modules qtimageformats + $qtPrefix = "C:\Qt\$env:QT_VERSION\$env:QT_DIR_NAME" + $windeployqt = Join-Path $qtPrefix 'bin\windeployqt.exe' + if (-not (Test-Path $windeployqt)) { + throw "Qt 安装不完整,找不到 windeployqt:$windeployqt" + } + "QT_PREFIX=$qtPrefix" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + + - name: Build and deploy Qt runtime + shell: powershell + run: | + $ErrorActionPreference = 'Stop' + powershell -ExecutionPolicy Bypass -File .\build-windows.ps1 -QtPrefix "$env:QT_PREFIX" -Config $env:BUILD_TYPE + if (-not (Test-Path $env:DIST_DIR)) { + throw "发布目录不存在:$env:DIST_DIR" + } + if (-not (Test-Path "$env:DIST_DIR\chengnan.exe")) { + throw "构建产物不存在:$env:DIST_DIR\chengnan.exe" + } - name: Package + shell: powershell run: | - set -euo pipefail - APP_NAME="chengnan" - PACKAGE_NAME="chengnan-windows-x86_64-cross" - PACKAGE_DIR="dist-windows-cross/${PACKAGE_NAME}" - EXE="build-windows-cross/${APP_NAME}.exe" + $ErrorActionPreference = 'Stop' + $shortSha = $env:GITHUB_SHA.Substring(0, 10) + $version = if ($env:GITHUB_REF_TYPE -eq 'tag') { $env:GITHUB_REF_NAME } else { "build-$shortSha" } + $zipName = "$env:PACKAGE_BASENAME-$version.zip" - test -f "$EXE" - rm -rf dist-windows-cross - mkdir -p "$PACKAGE_DIR" - cp "$EXE" "$PACKAGE_DIR/" - - # 递归收集 MinGW 运行时和 Qt 依赖 DLL。 - for round in 1 2 3; do - while IFS= read -r binary; do - while IFS= read -r dll; do - [ -f "$dll" ] || continue - cp -n "$dll" "$PACKAGE_DIR/" || true - done < <(x86_64-w64-mingw32-objdump -p "$binary" \ - | awk '/DLL Name:/ {print $3}' \ - | while read -r name; do \ - find /usr/x86_64-w64-mingw32/sys-root/mingw/bin -iname "$name" -print -quit; \ - done) - done < <(find "$PACKAGE_DIR" -maxdepth 1 -type f \( -name '*.exe' -o -name '*.dll' \) | sort) - done - - # Qt 平台插件是 Windows Qt GUI 程序启动所必需的。 - QT_PLUGIN_ROOT="/usr/x86_64-w64-mingw32/sys-root/mingw/lib/qt6/plugins" - if [ -d "$QT_PLUGIN_ROOT/platforms" ]; then - mkdir -p "$PACKAGE_DIR/platforms" - cp -n "$QT_PLUGIN_ROOT/platforms"/*.dll "$PACKAGE_DIR/platforms/" - fi - - cat > "$PACKAGE_DIR/README.txt" <<'EOF' - chengnan Windows x86_64 交叉编译发布包 + @" + chengnan Windows x64 发布包 运行方式: 1. 解压整个目录。 2. 双击 chengnan.exe 运行。 说明: - - 本目录由 Linux Runner 使用 MinGW-w64 交叉编译生成。 - - 请不要只复制 chengnan.exe,需保留同目录 DLL 和 platforms 子目录。 - EOF + - 本目录由 Windows Runner 使用 MSVC + Qt 构建生成。 + - 请不要只复制 chengnan.exe,需保留同目录 DLL 和 Qt 插件目录。 + "@ | Out-File -FilePath "$env:DIST_DIR\README.txt" -Encoding utf8 - (cd dist-windows-cross && 7z a -tzip "${PACKAGE_NAME}.zip" "${PACKAGE_NAME}" >/dev/null) - (cd dist-windows-cross && sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256") - du -h "dist-windows-cross/${PACKAGE_NAME}.zip" + if (Test-Path $zipName) { + Remove-Item -Force $zipName + } + Compress-Archive -Path "$env:DIST_DIR\*" -DestinationPath $zipName -Force + $hash = (Get-FileHash $zipName -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $zipName" | Out-File -FilePath "$zipName.sha256" -Encoding ascii + "PACKAGE_ZIP=$zipName" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "RELEASE_TAG=$version" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "Package: $zipName" + Write-Host "SHA256: $hash" - name: Verify package + shell: powershell run: | - set -euo pipefail - test -s dist-windows-cross/chengnan-windows-x86_64-cross.zip - test -s dist-windows-cross/chengnan-windows-x86_64-cross.zip.sha256 - (cd dist-windows-cross && sha256sum -c chengnan-windows-x86_64-cross.zip.sha256) - 7z l dist-windows-cross/chengnan-windows-x86_64-cross.zip | grep 'chengnan.exe' - 7z l dist-windows-cross/chengnan-windows-x86_64-cross.zip | grep 'platforms/' + $ErrorActionPreference = 'Stop' + if (-not (Test-Path $env:PACKAGE_ZIP)) { + throw "zip 不存在:$env:PACKAGE_ZIP" + } + if (-not (Test-Path "$env:PACKAGE_ZIP.sha256")) { + throw "sha256 不存在:$env:PACKAGE_ZIP.sha256" + } + $items = Get-ChildItem -Path $env:PACKAGE_ZIP | Select-Object -ExpandProperty FullName + if (-not $items) { + throw "zip 文件为空或不可读:$env:PACKAGE_ZIP" + } - name: Upload build artifact uses: actions/upload-artifact@v3 with: - name: chengnan-windows-x86_64-cross + name: chengnan-windows-x64 path: | - dist-windows-cross/chengnan-windows-x86_64-cross.zip - dist-windows-cross/chengnan-windows-x86_64-cross.zip.sha256 + ${{ env.PACKAGE_ZIP }} + ${{ env.PACKAGE_ZIP }}.sha256 if-no-files-found: error - name: Upload package to repository release + shell: powershell env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | - set -euo pipefail - PACKAGE_NAME="chengnan-windows-x86_64-cross" - SERVER_URL="${GITHUB_SERVER_URL:-https://git.luochen570.cn}" - REPOSITORY="${GITHUB_REPOSITORY}" - API_BASE="${SERVER_URL}/api/v1/repos/${REPOSITORY}" - TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN:-}}" - test -n "$TOKEN" - - if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then - TAG_NAME="${GITHUB_REF_NAME}" - RELEASE_NAME="${GITHUB_REF_NAME}" - PRERELEASE=false - else - TAG_NAME="build-${GITHUB_SHA:0:10}" - RELEASE_NAME="chengnan ${GITHUB_SHA:0:10}" - PRERELEASE=true - fi - - json_escape() { - python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' + $ErrorActionPreference = 'Stop' + $token = $env:RELEASE_TOKEN + if ([string]::IsNullOrWhiteSpace($token)) { + $token = $env:GITHUB_TOKEN + } + if ([string]::IsNullOrWhiteSpace($token)) { + throw '缺少发布 Token:请在仓库 Secrets 中配置 RELEASE_TOKEN,或确保 Gitea Actions 提供 GITHUB_TOKEN。' } - release_payload="$(printf '{"tag_name":%s,"target_commitish":%s,"name":%s,"body":%s,"draft":false,"prerelease":%s}' \ - "$(printf '%s' "$TAG_NAME" | json_escape)" \ - "$(printf '%s' "${GITHUB_SHA}" | json_escape)" \ - "$(printf '%s' "$RELEASE_NAME" | json_escape)" \ - "$(printf 'Automated Windows x86_64 cross build from Gitea Actions.\n' | json_escape)" \ - "$PRERELEASE")" + $serverUrl = if ($env:GITHUB_SERVER_URL) { $env:GITHUB_SERVER_URL.TrimEnd('/') } else { 'https://git.luochen570.cn' } + $repo = $env:GITHUB_REPOSITORY + $apiBase = "$serverUrl/api/v1/repos/$repo" + $tagName = $env:RELEASE_TAG + $releaseName = if ($env:GITHUB_REF_TYPE -eq 'tag') { $env:GITHUB_REF_NAME } else { "chengnan $($env:GITHUB_SHA.Substring(0, 10))" } + $isPrerelease = if ($env:GITHUB_REF_TYPE -eq 'tag') { $false } else { $true } + $headers = @{ Authorization = "token $token"; Accept = 'application/json' } - status="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - --header 'Content-Type: application/json' \ - --data "$release_payload" \ - --write-out '%{http_code}' \ - --output /tmp/gitea-release-response.json \ - "${API_BASE}/releases")" + $releasePayload = @{ + tag_name = $tagName + target_commitish = $env:GITHUB_SHA + name = $releaseName + body = "Automated Windows x64 build from Gitea Actions.`n" + draft = $false + prerelease = $isPrerelease + } | ConvertTo-Json -Depth 5 - if [ "$status" = "409" ] || [ "$status" = "422" ]; then - RELEASE_ID="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/tags/${TAG_NAME}" \ - | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" - elif [ "$status" = "201" ] || [ "$status" = "200" ]; then - RELEASE_ID="$(python3 -c 'import json; print(json.load(open("/tmp/gitea-release-response.json"))["id"])')" - else - echo "Create release failed with HTTP ${status}" >&2 - cat /tmp/gitea-release-response.json >&2 - exit 1 - fi + try { + $release = Invoke-RestMethod -Method Post -Uri "$apiBase/releases" -Headers $headers -ContentType 'application/json' -Body $releasePayload + } catch { + $release = Invoke-RestMethod -Method Get -Uri "$apiBase/releases/tags/$tagName" -Headers $headers + } - upload_asset() { - local src="$1" - local name - local existing_id - local status - name="$(basename "$src")" - existing_id="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - "${API_BASE}/releases/${RELEASE_ID}/assets" \ - | ASSET_NAME="$name" python3 -c 'import json,os,sys; assets=json.load(sys.stdin); print(next((str(a["id"]) for a in assets if a.get("name")==os.environ["ASSET_NAME"]), ""))')" - if [ -n "$existing_id" ]; then - curl --show-error --silent --location \ - --request DELETE \ - --header "Authorization: token ${TOKEN}" \ - --output /tmp/gitea-release-asset-delete-response.json \ - "${API_BASE}/releases/${RELEASE_ID}/assets/${existing_id}" - fi - status="$(curl --show-error --silent --location \ - --header "Authorization: token ${TOKEN}" \ - --form "attachment=@${src};filename=${name}" \ - --write-out '%{http_code}' \ - --output /tmp/gitea-release-asset-response.json \ - "${API_BASE}/releases/${RELEASE_ID}/assets?name=${name}")" - if [ "$status" != "201" ] && [ "$status" != "200" ]; then - echo "Upload release asset failed with HTTP ${status}: ${name}" >&2 - cat /tmp/gitea-release-asset-response.json >&2 + function Remove-ExistingAsset([string]$assetName) { + $assets = Invoke-RestMethod -Method Get -Uri "$apiBase/releases/$($release.id)/assets" -Headers $headers + foreach ($asset in @($assets)) { + if ($asset.name -eq $assetName) { + Invoke-RestMethod -Method Delete -Uri "$apiBase/releases/assets/$($asset.id)" -Headers $headers | Out-Null + } + } + } + + function Upload-Asset([string]$path) { + $name = Split-Path $path -Leaf + Remove-ExistingAsset $name + $uploadUrl = "$apiBase/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($name))" + $responseFile = Join-Path $env:RUNNER_TEMP "gitea-upload-response.json" + $status = & curl.exe --silent --show-error --location ` + --header "Authorization: token $token" ` + --form "attachment=@$path;filename=$name" ` + --write-out '%{http_code}' ` + --output $responseFile ` + $uploadUrl + if ($status -ne '200' -and $status -ne '201') { + Write-Error "上传 Release 附件失败,HTTP $status:$name" + if (Test-Path $responseFile) { Get-Content $responseFile } exit 1 - fi + } + Write-Host "Uploaded release asset: $name" } - upload_asset "dist-windows-cross/${PACKAGE_NAME}.zip" - upload_asset "dist-windows-cross/${PACKAGE_NAME}.zip.sha256" - echo "Release assets uploaded to ${SERVER_URL}/${REPOSITORY}/releases/tag/${TAG_NAME}" + Upload-Asset $env:PACKAGE_ZIP + Upload-Asset "$env:PACKAGE_ZIP.sha256" + Write-Host "Release assets uploaded to $serverUrl/$repo/releases/tag/$tagName"