Cross-Platform Build #140
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Cross-Platform Build | |
| on: | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - alt_store.json | |
| workflow_dispatch: | |
| inputs: | |
| branch: | |
| description: "要构建的分支" | |
| required: true | |
| default: "dev" | |
| type: choice | |
| options: | |
| - dev | |
| - main | |
| target: | |
| description: "要构建的平台" | |
| required: true | |
| default: "all" | |
| type: choice | |
| options: | |
| - all | |
| - android | |
| - ios | |
| - macos | |
| - windows | |
| - linux | |
| env: | |
| BUILD_BRANCH: ${{ github.event.inputs.branch || github.ref_name }} | |
| FLUTTER_CHANNEL: stable | |
| FLUTTER_VERSION_FILE: pubspec.yaml | |
| jobs: | |
| setup: | |
| name: Setup | |
| runs-on: ubuntu-latest | |
| outputs: | |
| app-version: ${{ steps.get_version.outputs.app-version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.BUILD_BRANCH }} | |
| - id: get_version | |
| run: | | |
| version=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f1) | |
| echo "app-version=$version" >> $GITHUB_OUTPUT | |
| android-build: | |
| if: ${{ github.event_name == 'push' || github.event.inputs.target == 'android' || github.event.inputs.target == 'all' }} | |
| name: Android Build | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.BUILD_BRANCH }} | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v3 | |
| with: | |
| java-version: "17" | |
| distribution: "temurin" | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_CHANNEL }} | |
| flutter-version-file: ${{ env.FLUTTER_VERSION_FILE }} | |
| - name: Cache Gradle | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | |
| restore-keys: | | |
| ${{ runner.os }}-gradle- | |
| - name: Prepare signing files | |
| run: | | |
| echo "storePassword=${{ secrets.STORE_PASSWORD }}" > android/key.properties | |
| echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties | |
| echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties | |
| echo "storeFile=upload-keystore.jks" >> android/key.properties | |
| echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > android/app/upload-keystore.jks | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Inject version config | |
| run: dart run script/prebuild_inject_version.dart | |
| - name: Build APKs | |
| run: | | |
| flutter build apk --release | |
| flutter build apk --release --split-per-abi | |
| - name: Move And Rename APKs | |
| run: | | |
| version=${{ needs.setup.outputs.app-version }} | |
| mkdir -p artifacts | |
| for file in build/app/outputs/flutter-apk/*.apk; do | |
| filename=$(basename "$file") | |
| if [ "$filename" = "app-release.apk" ]; then | |
| filename="app-universal-release.apk" | |
| fi | |
| mv "$file" "artifacts/${filename%.apk}-v$version.apk" | |
| done | |
| - name: Upload APKs | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-apks | |
| path: artifacts/ | |
| retention-days: 5 | |
| - name: Notify Android Build | |
| uses: dawidd6/action-send-mail@v5 | |
| with: | |
| server_address: smtp.qq.com | |
| server_port: 587 | |
| username: ${{ secrets.EMAIL_USERNAME }} | |
| password: ${{ secrets.EMAIL_PASSWORD }} | |
| subject: Android Build Success | |
| to: ${{ secrets.EMAIL_TO }} | |
| from: ${{ secrets.EMAIL_FROM }} | |
| body: Android build completed. APKs attached. | |
| attachments: artifacts/app-arm64-v8a-release-v${{ needs.setup.outputs.app-version }}.apk | |
| ios-build: | |
| if: ${{ github.event_name == 'push' || github.event.inputs.target == 'ios' || github.event.inputs.target == 'all' }} | |
| name: iOS Build | |
| needs: setup | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.BUILD_BRANCH }} | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_CHANNEL }} | |
| flutter-version-file: ${{ env.FLUTTER_VERSION_FILE }} | |
| architecture: x64 | |
| - run: sudo xcode-select --switch /Applications/Xcode_26.2.app | |
| - run: flutter pub get | |
| - name: Inject version config | |
| run: dart run script/prebuild_inject_version.dart | |
| - name: Build unsigned IPA | |
| run: | | |
| flutter build ios --release --no-codesign | |
| mkdir -p build/ios/iphoneos/Payload | |
| mv build/ios/iphoneos/Runner.app build/ios/iphoneos/Payload/ | |
| cd build/ios/iphoneos/ | |
| zip -r no-codesign-ios-v${{ needs.setup.outputs.app-version }}.ipa Payload | |
| - name: Upload IPA | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-ipa | |
| path: build/ios/iphoneos/*.ipa | |
| retention-days: 5 | |
| - name: Notify iOS Build | |
| uses: dawidd6/action-send-mail@v5 | |
| with: | |
| server_address: smtp.qq.com | |
| server_port: 587 | |
| username: ${{ secrets.EMAIL_USERNAME }} | |
| password: ${{ secrets.EMAIL_PASSWORD }} | |
| subject: iOS Build Success | |
| to: ${{ secrets.EMAIL_TO }} | |
| from: ${{ secrets.EMAIL_FROM }} | |
| body: iOS build completed. IPA attached. | |
| attachments: build/ios/iphoneos/*.ipa | |
| windows-build: | |
| if: ${{ github.event_name == 'push' || github.event.inputs.target == 'windows' || github.event.inputs.target == 'all' }} | |
| name: Windows Build | |
| needs: setup | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.BUILD_BRANCH }} | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_CHANNEL }} | |
| flutter-version-file: ${{ env.FLUTTER_VERSION_FILE }} | |
| cache: false | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Inject version config | |
| run: dart run script/prebuild_inject_version.dart | |
| - name: Build Windows Release | |
| run: flutter build windows --release | |
| - name: Install Inno Setup | |
| run: choco install innosetup --no-progress -y | |
| - name: Create Inno Setup Script | |
| shell: pwsh | |
| run: | | |
| $version = "${{ needs.setup.outputs.app-version }}" | |
| $appName = "HaKa Comic" | |
| $exePath = Get-ChildItem -Path "build/windows/x64/runner/Release" -Filter "*.exe" | Select-Object -First 1 | |
| $exeName = $exePath.Name | |
| # 构造输出文件名 | |
| $outputFile = "$appName-Setup-v$version" | |
| $DefaultDir = "{autopf}/$appName" | |
| $content = @" | |
| [Setup] | |
| AppName=$appName | |
| AppVersion=$version | |
| DefaultDirName=$DefaultDir | |
| DefaultGroupName=$appName | |
| OutputBaseFilename=$outputFile | |
| Compression=lzma | |
| SolidCompression=yes | |
| [Files] | |
| Source: "assets/icons/pc/windows_icon.ico"; DestDir: "{app}"; Flags: ignoreversion | |
| Source: "build\\windows\\x64\\runner\\Release\\*"; DestDir: "{app}"; Flags: recursesubdirs | |
| [Icons] | |
| Name: `"{group}\$appName`"; Filename: `"{app}\$exeName`"; IconFilename: `"{app}\windows_icon.ico`"; WorkingDir: `"{app}`" | |
| Name: `"{userdesktop}\$appName`"; Filename: `"{app}\$exeName`"; IconFilename: `"{app}\windows_icon.ico`"; WorkingDir: `"{app}`" | |
| [Run] | |
| Filename: `"{app}\$exeName`"; WorkingDir: `"{app}`"; Flags: nowait postinstall skipifsilent | |
| [Code] | |
| // 删除 Flutter getApplicationSupportDirectory() 数据目录: | |
| // Windows 上等于: C:\Users\<User>\AppData\Roaming\HaKa Comic | |
| procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); | |
| var | |
| SupportDir: string; | |
| begin | |
| if CurUninstallStep = usUninstall then | |
| begin | |
| SupportDir := ExpandConstant('{userappdata}\com.github.raoxwup\$appName'); | |
| if DirExists(SupportDir) then | |
| begin | |
| Log('Removing ApplicationSupportDirectory: ' + SupportDir); | |
| DelTree(SupportDir, True, True, True); | |
| end; | |
| Log('Uninstall cleanup finished.'); | |
| end; | |
| end; | |
| "@ | |
| # 输出到文件 | |
| $content | Out-File -FilePath installer.iss -Encoding ascii | |
| - name: Build Installer | |
| shell: pwsh | |
| run: | | |
| & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "installer.iss" | |
| - name: Upload Windows Installer | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-installer | |
| path: Output/*.exe | |
| retention-days: 5 | |
| macos-build: | |
| if: ${{ github.event_name == 'push' || github.event.inputs.target == 'macos' || github.event.inputs.target == 'all' }} | |
| name: MacOS Build | |
| needs: setup | |
| runs-on: macos-15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.BUILD_BRANCH }} | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_CHANNEL }} | |
| flutter-version-file: ${{ env.FLUTTER_VERSION_FILE }} | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Inject version config | |
| run: dart run script/prebuild_inject_version.dart | |
| - name: Build MacOS Release | |
| run: flutter build macos --release | |
| - name: Install create-dmg | |
| run: npm install -g create-dmg | |
| - name: Package .app into DMG | |
| run: | | |
| version=${{ needs.setup.outputs.app-version }} | |
| cd build/macos/Build/Products/Release/ | |
| # 获取 .app 名称 | |
| appName=$(ls -d *.app | head -n 1) | |
| # 生成 DMG 文件名 | |
| dmgName="${appName%.app}-v$version.dmg" | |
| echo "Packaging $appName into $dmgName" | |
| create-dmg "$appName" \ | |
| --overwrite \ | |
| --no-version-in-filename \ | |
| --dmg-title "${appName%.app}" \ | |
| --no-code-sign | |
| mv *.dmg "$dmgName" | |
| - name: Upload macOS DMG | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-app-dmg | |
| path: build/macos/Build/Products/Release/*.dmg | |
| retention-days: 5 | |
| linux-build: | |
| if: ${{ github.event_name == 'push' || github.event.inputs.target == 'linux' || github.event.inputs.target == 'all' }} | |
| name: Linux Build | |
| needs: setup | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - flutter_arch: x64 | |
| runner: ubuntu-latest | |
| deb_arch: amd64 | |
| - flutter_arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| deb_arch: arm64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.BUILD_BRANCH }} | |
| - name: Setup Flutter (x64) | |
| if: ${{ matrix.flutter_arch == 'x64' }} | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: ${{ env.FLUTTER_CHANNEL }} | |
| flutter-version-file: ${{ env.FLUTTER_VERSION_FILE }} | |
| architecture: x64 | |
| - name: Setup Flutter (arm64) | |
| if: ${{ matrix.flutter_arch == 'arm64' }} | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: main | |
| flutter-version-file: ${{ env.FLUTTER_VERSION_FILE }} | |
| - name: Install Linux build deps | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y ninja-build libgtk-3-dev clang cmake pkg-config ruby ruby-dev | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Inject version config | |
| run: dart run script/prebuild_inject_version.dart | |
| - name: Inject font assets | |
| run: dart run script/prebuild_inject_font.dart | |
| - name: Build Linux Release | |
| run: | | |
| flutter build linux --release | |
| ldd build/linux/${{ matrix.flutter_arch }}/release/bundle/haka_comic | |
| - name: Install fpm | |
| run: | | |
| sudo gem install fpm --no-document | |
| echo "$(ruby -e 'print Gem.bindir')" >> $GITHUB_PATH | |
| - name: Package Linux Deb | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="${{ needs.setup.outputs.app-version }}" | |
| app_id="com.github.raoxwup.hakacomic" | |
| app_name="haka_comic" | |
| app_title="HaKa Comic" | |
| pkg_name="haka-comic" | |
| bundle_dir="build/linux/${{ matrix.flutter_arch }}/release/bundle" | |
| if [ ! -d "$bundle_dir" ]; then | |
| echo "Bundle not found at $bundle_dir" | |
| ls -la build/linux || true | |
| exit 1 | |
| fi | |
| artifacts_dir="artifacts" | |
| mkdir -p "$artifacts_dir" | |
| deb_root="build/linux/package/${{ matrix.deb_arch }}" | |
| install_dir="$deb_root/opt/$app_name" | |
| mkdir -p "$install_dir" "$deb_root/usr/bin" "$deb_root/usr/share/applications" "$deb_root/usr/share/pixmaps" | |
| cp -a "$bundle_dir/." "$install_dir/" | |
| ln -sf "/opt/$app_name/$app_name" "$deb_root/usr/bin/$app_name" | |
| cp "assets/icons/pc/linux_icon.png" "$deb_root/usr/share/pixmaps/${app_id}.png" | |
| cat > "$deb_root/usr/share/applications/${app_id}.desktop" <<EOF | |
| [Desktop Entry] | |
| Name=$app_title | |
| Comment=A third-party PICACG project. | |
| Exec=$app_name | |
| Icon=$app_id | |
| Terminal=false | |
| Type=Application | |
| Categories=Utility; | |
| StartupWMClass=$app_id | |
| EOF | |
| fpm -s dir -t deb \ | |
| -n "$pkg_name" \ | |
| -v "$version" \ | |
| -a "${{ matrix.deb_arch }}" \ | |
| --deb-compression xz \ | |
| --deb-compression-level 9 \ | |
| --maintainer "raoxwup" \ | |
| --description "A third-party PICACG project." \ | |
| --license "GPL-3.0" \ | |
| --url "https://github.com/raoxwup/haka_comic" \ | |
| --depends libstdc++6 \ | |
| --depends libgtk-3-0 \ | |
| --depends libglib2.0-0 \ | |
| --depends libblkid1 \ | |
| --depends liblzma5 \ | |
| --depends libepoxy0 \ | |
| -C "$deb_root" \ | |
| -p "$artifacts_dir/${pkg_name}-v$version-${{ matrix.deb_arch }}.deb" \ | |
| . | |
| - name: Upload Linux Artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux-packages-${{ matrix.deb_arch }} | |
| path: artifacts/* | |
| retention-days: 5 | |
| release: | |
| name: Draft Release | |
| needs: | |
| - setup | |
| - android-build | |
| - ios-build | |
| - windows-build | |
| - macos-build | |
| - linux-build | |
| if: >- | |
| ${{ always() | |
| && needs.android-build.result != 'failure' | |
| && needs.android-build.result != 'cancelled' | |
| && needs.ios-build.result != 'failure' | |
| && needs.ios-build.result != 'cancelled' | |
| && needs.windows-build.result != 'failure' | |
| && needs.windows-build.result != 'cancelled' | |
| && needs.macos-build.result != 'failure' | |
| && needs.macos-build.result != 'cancelled' | |
| && needs.linux-build.result != 'failure' | |
| && needs.linux-build.result != 'cancelled' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.BUILD_BRANCH }} | |
| fetch-depth: 0 | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: release-artifacts | |
| merge-multiple: true | |
| - name: Setup Dart | |
| if: ${{ needs.ios-build.result == 'success' }} | |
| uses: dart-lang/setup-dart@v1 | |
| - name: Update AltStore source (alt_store.json) | |
| if: ${{ needs.ios-build.result == 'success' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="${{ needs.setup.outputs.app-version }}" | |
| tag="${version}" | |
| date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" | |
| ipa_path="$(find release-artifacts -maxdepth 2 -name '*.ipa' | head -n 1)" | |
| if [ -z "${ipa_path}" ]; then | |
| echo "No IPA found under release-artifacts" | |
| find release-artifacts -maxdepth 2 -type f -print | |
| exit 1 | |
| fi | |
| ipa_name="$(basename "${ipa_path}")" | |
| dart script/update_alt_store_json.dart \ | |
| --json alt_store.json \ | |
| --ipa "${ipa_path}" \ | |
| --version "${version}" \ | |
| --buildVersion "${version}" \ | |
| --date "${date}" \ | |
| --repo "${{ github.repository }}" \ | |
| --tag "${tag}" \ | |
| --assetName "${ipa_name}" | |
| - name: Commit alt_store.json to repo (main only) | |
| if: ${{ env.BUILD_BRANCH == 'main' && needs.ios-build.result == 'success' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="${{ needs.setup.outputs.app-version }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add alt_store.json | |
| if git diff --cached --quiet; then | |
| echo "alt_store.json unchanged; skip commit" | |
| exit 0 | |
| fi | |
| git commit -m "chore: update AltStore source (${version})" | |
| git push | |
| - name: Create draft release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.setup.outputs.app-version }}+${{ github.run_number }} | |
| name: v${{ needs.setup.outputs.app-version }}+${{ github.run_number }} | |
| target_commitish: ${{ env.BUILD_BRANCH }} | |
| draft: true | |
| generate_release_notes: false | |
| files: release-artifacts/**/* | |
| fail_on_unmatched_files: false |