diff --git a/.gitea/workflows/build-windows-release.yml b/.gitea/workflows/build-windows-release.yml index dd739e2..03d49b7 100644 --- a/.gitea/workflows/build-windows-release.yml +++ b/.gitea/workflows/build-windows-release.yml @@ -12,118 +12,217 @@ permissions: contents: write jobs: - build-windows-release: - name: Build Windows x64 Release - runs-on: - - windows - - env: - BUILD_TYPE: Release - QT_VERSION: '6.7.3' - QT_ARCH: win64_msvc2019_64 - QT_MODULES: qtimageformats - DIST_DIR: dist-windows\chengnan + 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 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 - uses: actions/checkout@v4 - - - name: Setup MSVC developer command prompt - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: x64 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install Qt with aqtinstall - shell: powershell run: | - $ErrorActionPreference = 'Stop' - python -m pip install --upgrade pip - python -m pip install aqtinstall - python -m aqt install-qt windows desktop $env:QT_VERSION win64_msvc2019_64 --outputdir C:\Qt --modules $env:QT_MODULES - $qtPrefix = "C:\Qt\$env:QT_VERSION\msvc2019_64" - if (-not (Test-Path "$qtPrefix\bin\windeployqt.exe")) { - throw "Qt 安装不完整,找不到 windeployqt:$qtPrefix\bin\windeployqt.exe" - } - "QT_PREFIX=$qtPrefix" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + 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" - - name: Configure and build - shell: powershell + - name: Install cross build dependencies run: | - $ErrorActionPreference = 'Stop' - powershell -ExecutionPolicy Bypass -File .\build-windows.ps1 -QtPrefix "$env:QT_PREFIX" -Config $env:BUILD_TYPE + set -euo pipefail + dnf install -y \ + cmake \ + curl \ + ninja-build \ + p7zip \ + p7zip-plugins \ + mingw64-gcc-c++ \ + mingw64-qt6-qtbase + + - 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: Build + run: | + set -euo pipefail + cmake --build build-windows-cross --parallel "$(nproc)" - name: Package - shell: powershell run: | - $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 = "chengnan-windows-x64-$version.zip" - if (-not (Test-Path $env:DIST_DIR)) { - throw "发布目录不存在:$env:DIST_DIR" - } - 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" + 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" + + 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 交叉编译发布包 + + 运行方式: + 1. 解压整个目录。 + 2. 双击 chengnan.exe 运行。 + + 说明: + - 本目录由 Linux Runner 使用 MinGW-w64 交叉编译生成。 + - 请不要只复制 chengnan.exe,需保留同目录 DLL 和 platforms 子目录。 + EOF + + (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" + + - name: Verify package + 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/' - name: Upload build artifact uses: actions/upload-artifact@v3 with: - name: chengnan-windows-x64 + name: chengnan-windows-x86_64-cross path: | - ${{ env.PACKAGE_ZIP }} - ${{ env.PACKAGE_ZIP }}.sha256 + dist-windows-cross/chengnan-windows-x86_64-cross.zip + dist-windows-cross/chengnan-windows-x86_64-cross.zip.sha256 + if-no-files-found: error - - name: Publish to Gitea Release - if: ${{ github.event_name != 'pull_request' }} - shell: powershell + - name: Upload package to repository release env: - GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | - $ErrorActionPreference = 'Stop' - if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) { - throw '缺少发布 Token。请在仓库 Secrets 中配置 RELEASE_TOKEN,或确保 Gitea 提供 GITHUB_TOKEN。' + 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()))' } - $api = $env:GITHUB_API_URL.TrimEnd('/') - $repo = $env:GITHUB_REPOSITORY - $tag = $env:RELEASE_TAG - $zip = $env:PACKAGE_ZIP - $headers = @{ - Authorization = "token $env:GITEA_TOKEN" - Accept = 'application/json' + 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")" + + 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")" + + 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 + + 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 + exit 1 + fi } - $releaseUrl = "$api/repos/$repo/releases/tags/$tag" - try { - $release = Invoke-RestMethod -Method Get -Uri $releaseUrl -Headers $headers - } catch { - $body = @{ - tag_name = $tag - target_commitish = $env:GITHUB_SHA - name = "chengnan $tag" - body = "Windows x64 Release 自动构建产物。`n`n提交:$env:GITHUB_SHA" - draft = $false - prerelease = ($env:GITHUB_REF_TYPE -ne 'tag') - } | ConvertTo-Json -Depth 5 - $release = Invoke-RestMethod -Method Post -Uri "$api/repos/$repo/releases" -Headers $headers -ContentType 'application/json' -Body $body - } - - foreach ($asset in @($zip, "$zip.sha256")) { - $existing = @($release.assets | Where-Object { $_.name -eq $asset }) - foreach ($item in $existing) { - Invoke-RestMethod -Method Delete -Uri "$api/repos/$repo/releases/assets/$($item.id)" -Headers $headers | Out-Null - } - $uploadUrl = "$api/repos/$repo/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($asset))" - Invoke-RestMethod -Method Post -Uri $uploadUrl -Headers $headers -ContentType 'multipart/form-data' -Form @{ attachment = Get-Item $asset } | Out-Null - Write-Host "Uploaded release asset: $asset" - } + 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}"