Skip to content

Cross-Platform Build #140

Cross-Platform Build

Cross-Platform Build #140

Workflow file for this run

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