mirror of
https://github.com/ankitects/anki.git
synced 2026-07-28 13:49:05 -04:00
## Linked issue (required) Fixes #5110 ## Summary / motivation (required) CI builds the fcitx5 plugin against one Qt but ships it inside PyQt6's Qt — a version mismatch that crashes Anki on startup for every fcitx5 user. **Root cause:** the plugin subclasses `QPlatformInputContext`, a Qt private API with no ABI guarantee across minor versions. Built against Qt 6.2.4 headers but run inside Qt 6.11.0, it dereferences a misplaced member and SIGSEGVs on first focus. **Solution:** build the plugin against the same Qt as the bundled `pyqt6-qt6` and install it where the packaging step (`qt/tools/build_installer.py`) looks. The per-flag reasoning is in the commit message. ## Steps to reproduce (required, use N/A if not applicable) 1. On a Linux desktop configured to use fcitx5 as the input method, install the official Anki 26.05 release. 2. Launch Anki normally. 3. It dies with SIGSEGV on startup, as soon as the main window takes focus. ## How to test (required) ### Checklist (minimum) - [x] I ran `./ninja check` or an equivalent relevant check locally. - [ ] I added or updated tests when the change is non-trivial or behavior changed. ### Details `release.yml` is `workflow_dispatch`-only, so it can't run on the PR. I reproduced its build step locally: rebuilt the plugin against Qt 6.11.0 (confirmed the `Qt_6.11` ELF tag and a clean `dlopen(RTLD_NOW)`), dropped it into an Anki 26.05 bundle, and typed Chinese and Japanese without the crash. ## Risk / compatibility / migration (optional) 1. Fixes `build-linux-x86` only. `build-linux-arm-installer` installs a prebuilt `fcitx5-frontend-qt6` from apt, built against Ubuntu's Qt rather than the bundled 6.11.0 — the same class of mismatch. I have no arm hardware to confirm or fix it, so it's left for a follow-up. 2. Adds an `aqtinstall` step (~1.5 GB Qt download) to the Linux release build. 3. **Not validated on CI.** `release.yml` is `workflow_dispatch`-only, so an external contributor can't run it; only its build step is reproduced locally (see How to test). **Please confirm it in a real release build.** ## UI evidence (required for visual changes; otherwise N/A) N/A ## Scope - [x] This PR is focused on one change (no unrelated edits).
763 lines
28 KiB
YAML
763 lines
28 KiB
YAML
name: Release
|
|
run-name: "Release ${{ inputs.version }} (sign=${{ inputs.sign }}, draft=${{ inputs['draft-release'] }}, testpypi=${{ inputs['publish-testpypi'] }}, pypi=${{ inputs['publish-pypi'] }})"
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: "Version for draft release/PyPI (must match .version)"
|
|
required: true
|
|
type: string
|
|
skip-ci-check:
|
|
description: "Skip the CI status check (for hotfix releases from non-main branches)"
|
|
default: false
|
|
required: false
|
|
type: boolean
|
|
sign:
|
|
description: "Sign macOS and Windows artifacts (requires release environment)"
|
|
default: false
|
|
required: false
|
|
type: boolean
|
|
draft-release:
|
|
description: "Create a draft GitHub release (requires sign)"
|
|
default: false
|
|
required: false
|
|
type: boolean
|
|
publish-testpypi:
|
|
description: "Publish wheels to TestPyPI"
|
|
default: false
|
|
required: false
|
|
type: boolean
|
|
publish-pypi:
|
|
description: "Publish wheels to PyPI after TestPyPI"
|
|
default: false
|
|
required: false
|
|
type: boolean
|
|
|
|
permissions:
|
|
contents: write
|
|
actions: read
|
|
|
|
env:
|
|
# Build profile: 1 = Release, 2 = ReleaseWithLto (see build/ninja_gen/src/build.rs)
|
|
RELEASE: 2
|
|
N2_OUTPUT_PROGRESS: "1"
|
|
N2_OUTPUT_SUCCESS: "1"
|
|
|
|
concurrency:
|
|
group: release
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
public_release: ${{ steps.validate.outputs.public_release }}
|
|
pep440_version: ${{ steps.validate.outputs.pep440_version }}
|
|
tag_name: ${{ steps.validate.outputs.tag_name }}
|
|
release_name: ${{ steps.validate.outputs.release_name }}
|
|
is_prerelease: ${{ steps.validate.outputs.is_prerelease }}
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
fetch-depth: 0
|
|
|
|
- name: Reject incompatible inputs
|
|
run: |
|
|
if [ "$DRAFT_RELEASE" = "true" ] && [ "$SIGN" != "true" ]; then
|
|
echo "::error::Draft releases must be signed"
|
|
exit 1
|
|
fi
|
|
env:
|
|
DRAFT_RELEASE: ${{ inputs['draft-release'] }}
|
|
SIGN: ${{ inputs.sign }}
|
|
|
|
- name: Validate version and compute metadata
|
|
id: validate
|
|
run: |
|
|
pip install --break-system-packages 'packaging>=24,<26'
|
|
file_version=$(cat .version)
|
|
|
|
public_release=false
|
|
if [ "$DRAFT_RELEASE" = "true" ] || [ "$PUBLISH_PYPI" = "true" ]; then
|
|
public_release=true
|
|
fi
|
|
|
|
if [ "$public_release" = "true" ] && [ "$file_version" != "$INPUT_VERSION" ]; then
|
|
echo "::error::Input version '$INPUT_VERSION' does not match .version '$file_version'"
|
|
exit 1
|
|
fi
|
|
|
|
# Use .version for non-release runs so builds work without a prepare step
|
|
version="$INPUT_VERSION"
|
|
if [ "$public_release" != "true" ]; then
|
|
echo "::notice::Non-release run — ignoring version input '$INPUT_VERSION', using .version='$file_version'"
|
|
version="$file_version"
|
|
fi
|
|
|
|
# "0.0" skips the ordering check — non-release builds only need format validation
|
|
is_prerelease=$(python3 .github/scripts/validate_version.py "$version" "0.0")
|
|
echo "Version check passed: $version (prerelease=$is_prerelease)"
|
|
|
|
{
|
|
echo "pep440_version=$version"
|
|
echo "tag_name=$version"
|
|
echo "release_name=$version"
|
|
echo "public_release=$public_release"
|
|
echo "is_prerelease=$is_prerelease"
|
|
} >> "$GITHUB_OUTPUT"
|
|
env:
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
DRAFT_RELEASE: ${{ inputs['draft-release'] }}
|
|
PUBLISH_PYPI: ${{ inputs['publish-pypi'] }}
|
|
|
|
- name: Check CI passed
|
|
if: ${{ steps.validate.outputs.public_release == 'true' && inputs.skip-ci-check != true }}
|
|
run: |
|
|
conclusion=$(gh run list \
|
|
--workflow=ci.yml \
|
|
--commit "$COMMIT_SHA" \
|
|
--limit 5 \
|
|
--json conclusion,event \
|
|
--jq '[.[] | select(.event == "push" or .event == "workflow_dispatch")][0].conclusion')
|
|
|
|
if [[ -z "$conclusion" ]]; then
|
|
echo "::error::Could not determine CI status for commit $COMMIT_SHA"
|
|
exit 1
|
|
elif [[ "$conclusion" != "success" ]]; then
|
|
echo "::error::CI for commit $COMMIT_SHA concluded with '$conclusion'"
|
|
exit 1
|
|
fi
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
COMMIT_SHA: ${{ github.sha }}
|
|
|
|
- name: Check for duplicate tag or release
|
|
if: ${{ inputs['draft-release'] == true }}
|
|
run: |
|
|
git fetch --tags origin
|
|
if git rev-parse "refs/tags/$TAG_NAME" >/dev/null 2>&1; then
|
|
echo "::error::Tag '$TAG_NAME' already exists"
|
|
exit 1
|
|
fi
|
|
|
|
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
|
|
echo "::error::GitHub release '$TAG_NAME' already exists"
|
|
exit 1
|
|
fi
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG_NAME: ${{ steps.validate.outputs.tag_name }}
|
|
|
|
# macOS ARM and Intel are kept as separate jobs rather than a matrix because
|
|
# they are likely to diverge (signing quirks, Xcode versions, cross-compilation
|
|
# flags, Rosetta workarounds), and the conditional environment expression is
|
|
# already complex enough without adding matrix dimensions.
|
|
build-and-sign-mac:
|
|
needs: prepare
|
|
runs-on: macos-14
|
|
timeout-minutes: 90
|
|
environment: ${{ inputs.sign == true && 'release' || '' }}
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- name: Set up Apple code signing
|
|
if: inputs.sign == true
|
|
run: .github/scripts/setup_apple_signing.sh
|
|
env:
|
|
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
APPLE_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
|
|
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
|
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
|
|
|
|
- name: Build installer
|
|
run: ./tools/build-installer
|
|
env:
|
|
SIGN_IDENTITY: ${{ inputs.sign == true && secrets.APPLE_SIGNING_IDENTITY || '' }}
|
|
|
|
- name: Clean up signing credentials
|
|
if: always()
|
|
run: |
|
|
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
|
|
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
|
|
rm -f "$RUNNER_TEMP/certificate.p12" "$RUNNER_TEMP/AuthKey.p8"
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
if: always()
|
|
with:
|
|
name: logs-installer-macos
|
|
path: out/installer/logs/
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: installer-macos
|
|
path: out/installer/dist/
|
|
|
|
- name: Build wheels
|
|
run: ./ninja wheels:anki
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: wheels-macos
|
|
path: out/wheels/anki-*.whl
|
|
|
|
build-and-sign-mac-intel:
|
|
needs: prepare
|
|
runs-on: macos-15-intel
|
|
timeout-minutes: 90
|
|
environment: ${{ inputs.sign == true && 'release' || '' }}
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- name: Set up Apple code signing
|
|
if: inputs.sign == true
|
|
run: .github/scripts/setup_apple_signing.sh
|
|
env:
|
|
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
APPLE_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }}
|
|
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
|
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
|
|
|
|
- name: Build installer
|
|
run: ./tools/build-installer
|
|
env:
|
|
SIGN_IDENTITY: ${{ inputs.sign == true && secrets.APPLE_SIGNING_IDENTITY || '' }}
|
|
|
|
- name: Clean up signing credentials
|
|
if: always()
|
|
run: |
|
|
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
|
|
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
|
|
rm -f "$RUNNER_TEMP/certificate.p12" "$RUNNER_TEMP/AuthKey.p8"
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
if: always()
|
|
with:
|
|
name: logs-installer-macos-intel
|
|
path: out/installer/logs/
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: installer-macos-intel
|
|
path: out/installer/dist/
|
|
|
|
- name: Build wheels
|
|
run: ./ninja wheels:anki
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: wheels-macos-intel
|
|
path: out/wheels/anki-*.whl
|
|
|
|
build-and-sign-windows:
|
|
needs: prepare
|
|
runs-on: windows-latest
|
|
timeout-minutes: 90
|
|
environment: ${{ inputs.sign == true && 'release' || '' }}
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- name: Build app
|
|
run: tools\ninja installer:build
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
if: always()
|
|
with:
|
|
name: logs-installer-windows
|
|
path: out/installer/logs/
|
|
|
|
- name: Azure login
|
|
if: inputs.sign == true
|
|
uses: azure/login@2dd0bbf4064d5a1812889dc200bb8eed2597f82a #v3
|
|
with:
|
|
creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}'
|
|
|
|
- name: Sign app binary
|
|
if: inputs.sign == true
|
|
uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 #v1
|
|
with:
|
|
endpoint: https://eus.codesigning.azure.net/
|
|
signing-account-name: anki-signing
|
|
certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
|
|
# Path derived from Briefcase conventions + qt/installer/app/pyproject.toml.template:
|
|
# {out_dir}/build/{app_name}/{platform}/{output_format}/{packaging_root}/{formal_name}.exe
|
|
files: ${{ github.workspace }}\out\installer\build\anki\windows\app\src\Anki.exe
|
|
file-digest: SHA256
|
|
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
|
timestamp-digest: SHA256
|
|
|
|
- name: Verify app binary signature
|
|
if: inputs.sign == true
|
|
shell: pwsh
|
|
run: |
|
|
$sig = Get-AuthenticodeSignature "out\installer\build\anki\windows\app\src\Anki.exe"
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Error "Anki.exe signature status: $($sig.Status)"
|
|
exit 1
|
|
}
|
|
Write-Host "Anki.exe signed successfully: $($sig.SignerCertificate.Subject)"
|
|
|
|
- name: Package installer
|
|
# NOTE: we bypass the build system here to ensure installer:build is not run again (e.g. due to submodule clones),
|
|
# which will remove the Anki.exe signature
|
|
run: out\pyenv\scripts\python.exe qt\tools\build_installer.py --version ${{ needs.prepare.outputs.pep440_version }} package
|
|
|
|
- name: Sign MSI
|
|
if: inputs.sign == true
|
|
uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 #v1
|
|
with:
|
|
endpoint: https://eus.codesigning.azure.net/
|
|
signing-account-name: anki-signing
|
|
certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
|
|
files-folder: out/installer/dist/
|
|
files-folder-filter: msi
|
|
file-digest: SHA256
|
|
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
|
timestamp-digest: SHA256
|
|
|
|
- name: Verify MSI signature
|
|
if: inputs.sign == true
|
|
shell: pwsh
|
|
run: |
|
|
$msi = Get-ChildItem "out/installer/dist/*.msi" | Select-Object -First 1
|
|
$sig = Get-AuthenticodeSignature $msi.FullName
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Error "MSI signature status: $($sig.Status)"
|
|
exit 1
|
|
}
|
|
Write-Host "MSI signed successfully: $($sig.SignerCertificate.Subject)"
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: installer-windows
|
|
path: out/installer/dist/
|
|
|
|
- name: Build wheels
|
|
run: tools\ninja wheels:anki
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: wheels-windows
|
|
path: out/wheels/anki-*.whl
|
|
|
|
# Windows ARM uses a 4-job chain because azure/artifact-signing-action does not
|
|
# support ARM runners: build on ARM → sign EXE on x64 → package MSI on ARM → sign MSI on x64.
|
|
build-windows-arm:
|
|
needs: prepare
|
|
runs-on: windows-11-arm
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- name: Build app
|
|
run: tools\ninja installer:build
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
if: always()
|
|
with:
|
|
name: logs-installer-windows-arm-build
|
|
path: out/installer/logs/
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: build-windows-arm
|
|
path: |
|
|
out/installer/
|
|
!out/installer/logs/
|
|
|
|
- name: Build wheels
|
|
run: tools\ninja wheels:anki
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: wheels-windows-arm
|
|
path: out/wheels/anki-*.whl
|
|
|
|
sign-exe-windows-arm:
|
|
needs: [prepare, build-windows-arm]
|
|
runs-on: windows-latest
|
|
timeout-minutes: 15
|
|
environment: ${{ inputs.sign == true && 'release' || '' }}
|
|
steps:
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #v4
|
|
with:
|
|
name: build-windows-arm
|
|
path: out/installer/
|
|
|
|
- name: Azure login
|
|
if: inputs.sign == true
|
|
uses: azure/login@2dd0bbf4064d5a1812889dc200bb8eed2597f82a #v3
|
|
with:
|
|
creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}'
|
|
|
|
- name: Sign app binary
|
|
if: inputs.sign == true
|
|
uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 #v1
|
|
with:
|
|
endpoint: https://eus.codesigning.azure.net/
|
|
signing-account-name: anki-signing
|
|
certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
|
|
files: ${{ github.workspace }}\out\installer\build\anki\windows\app\src\Anki.exe
|
|
file-digest: SHA256
|
|
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
|
timestamp-digest: SHA256
|
|
|
|
- name: Verify app binary signature
|
|
if: inputs.sign == true
|
|
shell: pwsh
|
|
run: |
|
|
$sig = Get-AuthenticodeSignature "out\installer\build\anki\windows\app\src\Anki.exe"
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Error "Anki.exe signature status: $($sig.Status)"
|
|
exit 1
|
|
}
|
|
Write-Host "Anki.exe signed successfully: $($sig.SignerCertificate.Subject)"
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: signed-build-windows-arm
|
|
path: out/installer/
|
|
|
|
package-windows-arm:
|
|
needs: [prepare, build-windows-arm, sign-exe-windows-arm]
|
|
runs-on: windows-11-arm
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #v4
|
|
with:
|
|
name: signed-build-windows-arm
|
|
path: out/installer/
|
|
|
|
- name: Package installer
|
|
run: |
|
|
tools\ninja pyenv
|
|
out\pyenv\scripts\python.exe qt\tools\build_installer.py --version ${{ needs.prepare.outputs.pep440_version }} package
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
if: always()
|
|
with:
|
|
name: logs-installer-windows-arm-package
|
|
path: out/installer/logs/
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: unsigned-installer-windows-arm
|
|
path: out/installer/dist/
|
|
|
|
sign-msi-windows-arm:
|
|
needs: [prepare, package-windows-arm]
|
|
runs-on: windows-latest
|
|
timeout-minutes: 15
|
|
environment: ${{ inputs.sign == true && 'release' || '' }}
|
|
steps:
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #v4
|
|
with:
|
|
name: unsigned-installer-windows-arm
|
|
path: dist/
|
|
|
|
- name: Azure login
|
|
if: inputs.sign == true
|
|
uses: azure/login@2dd0bbf4064d5a1812889dc200bb8eed2597f82a #v3
|
|
with:
|
|
creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}'
|
|
|
|
- name: Sign MSI
|
|
if: inputs.sign == true
|
|
uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 #v1
|
|
with:
|
|
endpoint: https://eus.codesigning.azure.net/
|
|
signing-account-name: anki-signing
|
|
certificate-profile-name: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
|
|
files-folder: dist/
|
|
files-folder-filter: msi
|
|
file-digest: SHA256
|
|
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
|
timestamp-digest: SHA256
|
|
|
|
- name: Verify MSI signature
|
|
if: inputs.sign == true
|
|
shell: pwsh
|
|
run: |
|
|
$msi = Get-ChildItem "dist/*.msi" | Select-Object -First 1
|
|
$sig = Get-AuthenticodeSignature $msi.FullName
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Error "MSI signature status: $($sig.Status)"
|
|
exit 1
|
|
}
|
|
Write-Host "MSI signed successfully: $($sig.SignerCertificate.Subject)"
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: installer-windows-arm
|
|
path: dist/
|
|
|
|
# Linux x86 and ARM are kept as separate jobs rather than a matrix because the ARM
|
|
# build requires different runners for wheels vs installer: ubuntu-22.04
|
|
# for wheels (to target glibc 2.35) and ubuntu-24.04-arm for the installer (the Qt wheels require it).
|
|
build-linux-x86:
|
|
needs: prepare
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- name: Build and install fcitx5-qt for bundling
|
|
run: |
|
|
sudo apt-get install --yes --no-install-recommends \
|
|
cmake extra-cmake-modules \
|
|
libfcitx5core-dev \
|
|
libgl-dev \
|
|
libxkbcommon-dev \
|
|
libx11-dev \
|
|
gettext \
|
|
patchelf
|
|
# Build against the same Qt as the bundled pyqt6-qt6
|
|
qt_version=$(uv export --frozen --no-hashes --package aqt --extra qt \
|
|
| grep -oP '^pyqt6-qt6==\K\S+')
|
|
test -n "$qt_version"
|
|
uv tool run --from aqtinstall aqt install-qt \
|
|
linux desktop "$qt_version" linux_gcc_64 -O /tmp/qt
|
|
qt_prefix=/tmp/qt/$qt_version/gcc_64
|
|
git clone --depth 1 --branch 5.0.10 \
|
|
https://github.com/fcitx/fcitx5-qt.git /tmp/fcitx5-qt
|
|
cmake -S /tmp/fcitx5-qt -B /tmp/fcitx5-qt-build \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DENABLE_QT4=no \
|
|
-DENABLE_QT5=no \
|
|
-DENABLE_QT6=yes \
|
|
-DCMAKE_INSTALL_PREFIX=/usr \
|
|
-DCMAKE_PREFIX_PATH="$qt_prefix" \
|
|
-DQT_FIND_PRIVATE_MODULES=ON \
|
|
-DCMAKE_INSTALL_QT6PLUGINDIR=/usr/lib/qt6/plugins
|
|
cmake --build /tmp/fcitx5-qt-build -j$(nproc)
|
|
sudo cmake --install /tmp/fcitx5-qt-build
|
|
|
|
- name: Build installer
|
|
run: ./tools/build-installer
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
if: always()
|
|
with:
|
|
name: logs-installer-linux-x86
|
|
path: out/installer/logs/
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: installer-linux-x86
|
|
path: out/installer/dist/
|
|
|
|
# Upload both anki and aqt wheels; the pure-Python aqt wheel is the same on all platforms
|
|
- name: Build wheels
|
|
run: ./ninja wheels
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: wheels-linux-x86
|
|
path: out/wheels/*.whl
|
|
|
|
build-linux-arm-wheels:
|
|
needs: prepare
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- name: Build wheels
|
|
run: |
|
|
sudo apt-get install --yes --no-install-recommends libc6-dev-arm64-cross gcc-aarch64-linux-gnu
|
|
./tools/build-arm-lin
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: wheels-linux-arm
|
|
path: out/wheels/anki-*.whl
|
|
|
|
build-linux-arm-installer:
|
|
needs: prepare
|
|
# Ubuntu 24.04 (glibc 2.39) is required because the Qt wheels only
|
|
# provide ARM packages for 24.04+. Since Briefcase does not bundle
|
|
# glibc, the resulting installer requires glibc 2.39+ at runtime —
|
|
# higher than the x86 installer (built on 22.04, glibc 2.35).
|
|
runs-on: ubuntu-24.04-arm
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- name: Setup build environment
|
|
uses: ./.github/actions/setup-anki
|
|
|
|
- name: Install fcitx5 dependencies for bundling
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install --yes --no-install-recommends \
|
|
fcitx5-frontend-qt6 libfcitx5-qt6-1 patchelf
|
|
|
|
- name: Build installer
|
|
run: ./tools/build-installer
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
if: always()
|
|
with:
|
|
name: logs-installer-linux-arm
|
|
path: out/installer/logs/
|
|
|
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 #v4
|
|
with:
|
|
name: installer-linux-arm
|
|
path: out/installer/dist/
|
|
|
|
release:
|
|
needs: [prepare, build-and-sign-mac, build-and-sign-mac-intel, build-and-sign-windows, sign-msi-windows-arm, build-linux-x86, build-linux-arm-wheels, build-linux-arm-installer]
|
|
if: >-
|
|
${{ always()
|
|
&& inputs['draft-release'] == true
|
|
&& needs.prepare.result == 'success'
|
|
&& needs.build-and-sign-mac.result == 'success'
|
|
&& needs.build-and-sign-mac-intel.result == 'success'
|
|
&& needs.build-linux-x86.result == 'success'
|
|
&& needs.build-linux-arm-wheels.result == 'success'
|
|
&& needs.build-linux-arm-installer.result == 'success'
|
|
&& needs.build-and-sign-windows.result == 'success'
|
|
&& needs.sign-msi-windows-arm.result == 'success'
|
|
}}
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 #v4
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #v4
|
|
with:
|
|
pattern: installer-*
|
|
path: dist
|
|
merge-multiple: true
|
|
|
|
- name: Create draft release
|
|
run: |
|
|
prerelease_flag=""
|
|
if [ "$IS_PRERELEASE" = "true" ]; then
|
|
prerelease_flag="--prerelease"
|
|
fi
|
|
|
|
notes_start_tag=$(gh release list --exclude-drafts --limit 1 --json tagName --jq '.[0].tagName // empty')
|
|
|
|
gh release create "$TAG_NAME" \
|
|
dist/* \
|
|
--draft \
|
|
--target "$RELEASE_SHA" \
|
|
--title "$RELEASE_NAME" \
|
|
--generate-notes \
|
|
${notes_start_tag:+--notes-start-tag "$notes_start_tag"} \
|
|
${prerelease_flag:+"$prerelease_flag"}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
|
|
RELEASE_NAME: ${{ needs.prepare.outputs.release_name }}
|
|
IS_PRERELEASE: ${{ needs.prepare.outputs.is_prerelease }}
|
|
RELEASE_SHA: ${{ github.sha }}
|
|
|
|
publish-testpypi:
|
|
needs: [prepare, build-and-sign-mac, build-and-sign-mac-intel, build-and-sign-windows, build-windows-arm, build-linux-x86, build-linux-arm-wheels]
|
|
if: >-
|
|
${{ always()
|
|
&& (inputs['publish-testpypi'] == true || inputs['publish-pypi'] == true)
|
|
&& needs.prepare.result == 'success'
|
|
&& needs.build-and-sign-mac.result == 'success'
|
|
&& needs.build-and-sign-mac-intel.result == 'success'
|
|
&& needs.build-and-sign-windows.result == 'success'
|
|
&& needs.build-windows-arm.result == 'success'
|
|
&& needs.build-linux-x86.result == 'success'
|
|
&& needs.build-linux-arm-wheels.result == 'success'
|
|
}}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
environment: testpypi
|
|
permissions:
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #v4
|
|
with:
|
|
pattern: wheels-*
|
|
path: wheels
|
|
merge-multiple: true
|
|
|
|
- name: Publish to TestPyPI
|
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b #v1
|
|
with:
|
|
packages-dir: wheels/
|
|
repository-url: https://test.pypi.org/legacy/
|
|
skip-existing: true
|
|
|
|
publish-pypi:
|
|
needs: [prepare, release, publish-testpypi]
|
|
if: >-
|
|
${{ always()
|
|
&& inputs['publish-pypi'] == true
|
|
&& needs.prepare.result == 'success'
|
|
&& needs.publish-testpypi.result == 'success'
|
|
&& (inputs['draft-release'] != true || needs.release.result == 'success')
|
|
}}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
environment: pypi
|
|
permissions:
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 #v4
|
|
with:
|
|
pattern: wheels-*
|
|
path: wheels
|
|
merge-multiple: true
|
|
|
|
- name: Publish to PyPI
|
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b #v1
|
|
with:
|
|
packages-dir: wheels/
|