Build & Release #235
Workflow file for this run
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: Build & Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Release tag to build (e.g. v1.2.8)' | |
| required: true | |
| type: string | |
| release: | |
| types: [published] | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| env: | |
| FLUTTER_VERSION: "3.44.1" | |
| CORE_VERSION: "v1.1.15" | |
| VERSION: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }} | |
| jobs: | |
| build-windows: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| runner: windows-latest | |
| flutter_channel: stable | |
| flutter_version: "3.44.1" | |
| # Flutter stable has no windows-arm64 host archive. Following the | |
| # approach used by gopeed: run on the native windows-11-arm runner | |
| # with a Flutter main-channel snapshot that ships arm64 host tools, | |
| # plus llvm-mingw for the native C/C++ toolchain. | |
| - arch: arm64 | |
| runner: windows-11-arm | |
| flutter_channel: main | |
| flutter_version: "a3222d4a" | |
| llvm_ver: "20251202" | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Enable long paths | |
| run: git config --global core.longpaths true | |
| # Install llvm-mingw (ARM64 native C/C++ toolchain) — only needed on ARM64 | |
| # where the runner's VS toolchain targets arm64 but Flutter plugins like | |
| # local_notifier pull in jni which needs a working C compiler. | |
| - name: Install llvm-mingw (ARM64 only) | |
| if: matrix.arch == 'arm64' | |
| run: | | |
| $ver = "${{ matrix.llvm_ver }}" | |
| $url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$ver/llvm-mingw-$ver-ucrt-aarch64.zip" | |
| $zip = "$env:RUNNER_TEMP\llvm.zip" | |
| $extract = "$env:RUNNER_TEMP\extract" | |
| $target = "C:\clangarm64" | |
| curl -L $url -o $zip | |
| rm -r -fo $extract,$target -ea Ignore | |
| mkdir $extract | Out-Null | |
| tar -xf $zip -C $extract | |
| mv (Get-ChildItem $extract)[0].FullName $target | |
| $b = "$target\bin" | |
| "CC=$b\clang.exe" >> $env:GITHUB_ENV | |
| "CXX=$b\clang++.exe" >> $env:GITHUB_ENV | |
| $b >> $env:GITHUB_PATH | |
| # local_notifier -> jni needs a JDK at build time for jvm.lib. | |
| # Must match the target architecture (ARM64), otherwise jvm.lib | |
| # machine type conflicts with the build target and linking fails. | |
| - name: Setup ARM64 JDK for JNI linking | |
| if: matrix.arch == 'arm64' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "21" | |
| architecture: arm64 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ matrix.flutter_version }} | |
| channel: ${{ matrix.flutter_channel }} | |
| architecture: ${{ matrix.flutter_arch || 'x64' }} | |
| cache: true | |
| - name: Sync version | |
| run: | | |
| $v = "${{ env.VERSION }}".TrimStart('v') | |
| (Get-Content pubspec.yaml) -replace '^version:.*', "version: $v" | Set-Content pubspec.yaml | |
| - name: Download core | |
| run: | | |
| curl -L "https://github.com/mapleafgo/singcast-cli/releases/download/$env:CORE_VERSION/singcast-$env:CORE_VERSION-windows-${{ matrix.arch }}.tar.gz" -o "singcast-core.tar.gz" | |
| tar -zxf singcast-core.tar.gz | |
| Move-Item -Force singcast-windows-${{ matrix.arch }}.exe windows/core/singcast-core.exe | |
| - name: Setup Chinese language for Inno Setup | |
| run: | | |
| Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl" | |
| mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\" | |
| - name: Build & package (x64) | |
| if: matrix.arch == 'amd64' | |
| run: | | |
| dart pub global activate fastforge | |
| fastforge package --platform=windows --targets=exe,zip | |
| - name: Rename artifacts (x64) | |
| if: matrix.arch == 'amd64' | |
| run: | | |
| $ver = "${{ env.VERSION }}".TrimStart('v') | |
| $distDir = "dist\$ver" | |
| $exe = (Get-ChildItem "$distDir\*.exe")[0] | |
| $zip = (Get-ChildItem "$distDir\*.zip")[0] | |
| Move-Item -Force $exe.FullName "$distDir\singcast-$ver-windows-amd64.exe" | |
| Move-Item -Force $zip.FullName "$distDir\singcast-$ver-windows-amd64-portable.zip" | |
| # ARM64: Flutter main channel builds natively to build/windows/arm64/. | |
| # fastforge does not reliably support main-channel ARM64, so build with | |
| # flutter directly and package with Inno Setup + zip manually. | |
| - name: Build (ARM64) | |
| if: matrix.arch == 'arm64' | |
| run: flutter build windows --release | |
| - name: Package ARM64 portable zip | |
| if: matrix.arch == 'arm64' | |
| run: | | |
| $ver = "${{ env.VERSION }}".TrimStart('v') | |
| $release = "build\windows\arm64\runner\Release" | |
| Compress-Archive -Path "$release\*" -DestinationPath "build\singcast-$ver-windows-arm64-portable.zip" | |
| - name: Package ARM64 installer | |
| if: matrix.arch == 'arm64' | |
| shell: pwsh | |
| run: | | |
| $ver = "${{ env.VERSION }}".TrimStart('v') | |
| $release = "build\windows\arm64\runner\Release" | |
| $content = @" | |
| [Setup] | |
| AppId={{B3039CF4-E394-42B6-971C-4BE97F322DE8} | |
| AppVersion=$ver | |
| AppName=Singcast | |
| AppPublisher=mapleafgo | |
| AppPublisherURL=https://github.com/mapleafgo/singcast | |
| DefaultDirName={autopf}\Singcast | |
| DisableProgramGroupPage=yes | |
| OutputDir=Output | |
| OutputBaseFilename=singcast-$ver-windows-arm64 | |
| Compression=lzma | |
| SolidCompression=yes | |
| SetupIconFile=..\..\windows\runner\resources\app_icon.ico | |
| WizardStyle=modern | |
| PrivilegesRequired=lowest | |
| ArchitecturesAllowed=arm64 | |
| ArchitecturesInstallIn64BitMode=arm64 | |
| [Languages] | |
| Name: "english"; MessagesFile: "compiler:Default.isl" | |
| Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" | |
| [Tasks] | |
| Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked | |
| [Files] | |
| Source: "arm64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs | |
| [Icons] | |
| Name: "{autoprograms}\Singcast"; Filename: "{app}\singcast.exe" | |
| Name: "{autodesktop}\Singcast"; Filename: "{app}\singcast.exe"; Tasks: desktopicon | |
| [Run] | |
| Filename: "{app}\singcast.exe"; Description: "{cm:LaunchProgram,Singcast}"; Flags: nowait postinstall skipifsilent | |
| [Registry] | |
| Root: HKCU; Subkey: "Software\Classes\clash"; ValueType: string; ValueName: ""; ValueData: "URL:Singcast Protocol"; Flags: uninsdeletekey | |
| Root: HKCU; Subkey: "Software\Classes\clash"; ValueType: string; ValueName: "URL Protocol"; ValueData: "" | |
| Root: HKCU; Subkey: "Software\Classes\clash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\singcast.exe,0" | |
| Root: HKCU; Subkey: "Software\Classes\clash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\singcast.exe"" ""%1""" | |
| [UninstallRun] | |
| Filename: "{app}\singcast-core.exe"; Parameters: "service uninstall"; Flags: runhidden | |
| "@ | |
| New-Item -Path "build\windows" -ItemType Directory -Force | Out-Null | |
| $content | Out-File -FilePath "build\windows\setup.iss" -Encoding UTF8 | |
| iscc.exe /Q "build\windows\setup.iss" | |
| - name: Upload release assets | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ env.VERSION }} | |
| files: | | |
| dist/*/*.exe | |
| dist/*/*.zip | |
| build/*.zip | |
| build/windows/Output/*.exe | |
| overwrite_files: true | |
| build-linux: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| architecture: ${{ matrix.flutter_arch || 'x64' }} | |
| cache: true | |
| - name: Sync version | |
| run: | | |
| v="${VERSION#v}" | |
| sed -i "s/^version:.*/version: $v/" pubspec.yaml | |
| - name: Install dependencies | |
| run: | | |
| sudo apt-get update -y | |
| sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev libnotify-dev | |
| wget -O /tmp/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" | |
| chmod +x /tmp/appimagetool | |
| sudo mv /tmp/appimagetool /usr/local/bin/appimagetool | |
| - name: Download core | |
| run: | | |
| curl -L "https://github.com/mapleafgo/singcast-cli/releases/download/$CORE_VERSION/singcast-$CORE_VERSION-linux-amd64.tar.gz" -o singcast-core.tar.gz | |
| tar -zxf singcast-core.tar.gz | |
| chmod +x singcast-linux-amd64 | |
| mv singcast-linux-amd64 linux/core/singcast-core | |
| - name: Build & package | |
| run: | | |
| dart pub global activate fastforge | |
| fastforge package --platform=linux --targets=appimage,deb,zip | |
| - name: Rename artifacts | |
| run: | | |
| v="${VERSION#v}" | |
| d="dist/$v" | |
| mv "$d/singcast-$v-linux.AppImage" "$d/singcast-$v-linux-amd64.AppImage" | |
| mv "$d/singcast-$v-linux.deb" "$d/singcast-$v-linux-amd64.deb" | |
| mv "$d/singcast-$v-linux.zip" "$d/singcast-$v-linux-amd64.zip" | |
| - name: Upload release assets | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ env.VERSION }} | |
| files: dist/*/* | |
| overwrite_files: true | |
| build-macos: | |
| runs-on: macos-latest | |
| timeout-minutes: 25 | |
| strategy: | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| core_arch: amd64 | |
| - arch: arm64 | |
| core_arch: arm64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| architecture: ${{ matrix.flutter_arch || 'x64' }} | |
| cache: true | |
| - name: Sync version | |
| run: | | |
| v="${VERSION#v}" | |
| sed -i "" "s/^version:.*/version: $v/" pubspec.yaml | |
| - name: Download core | |
| run: | | |
| mkdir -p macos/Frameworks | |
| curl -L "https://github.com/mapleafgo/singcast-cli/releases/download/$CORE_VERSION/singcast-$CORE_VERSION-darwin-${{ matrix.core_arch }}.tar.gz" -o singcast-core.tar.gz | |
| tar -zxf singcast-core.tar.gz | |
| chmod +x singcast-darwin-${{ matrix.core_arch }} | |
| mv singcast-darwin-${{ matrix.core_arch }} macos/Frameworks/singcast-core | |
| - name: Build | |
| env: | |
| ARCHS: ${{ matrix.arch == 'amd64' && 'x86_64' || '' }} | |
| run: flutter build macos --release | |
| - name: Ad-hoc sign | |
| run: codesign --force --deep --sign - build/macos/Build/Products/Release/singcast.app | |
| - name: Create DMG | |
| run: | | |
| v="${VERSION#v}" | |
| APP_PATH="build/macos/Build/Products/Release/singcast.app" | |
| hdiutil create -volname "Singcast" -srcfolder "$APP_PATH" -ov -format UDZO "build/macos/Build/Products/Release/singcast-$v-macos-${{ matrix.arch }}.dmg" | |
| - name: Upload release assets | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ env.VERSION }} | |
| files: build/macos/Build/Products/Release/*.dmg | |
| overwrite_files: true | |
| build-android: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| matrix: | |
| include: | |
| - target: android-arm64 | |
| arch: arm64 | |
| core_arch: arm64 | |
| - target: android-x64 | |
| arch: x64 | |
| core_arch: amd64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: liberica | |
| java-version: "17" | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| architecture: ${{ matrix.flutter_arch || 'x64' }} | |
| cache: true | |
| - name: Sync version | |
| run: | | |
| v="${VERSION#v}" | |
| ver="${v%%-*}" | |
| IFS='.' read -r major minor patch <<< "$ver" | |
| base=$((major * 10000000 + minor * 100000 + patch * 1000)) | |
| pre="${v#*-}" | |
| if [[ "$pre" == "$v" ]]; then | |
| vc=$((base + 999)) | |
| elif [[ "$pre" == beta* ]]; then | |
| n="${pre//[!0-9]/}" | |
| vc=$((base + 500 + ${n:-0})) | |
| elif [[ "$pre" == rc* ]]; then | |
| n="${pre//[!0-9]/}" | |
| vc=$((base + ${n:-0})) | |
| else | |
| n="${pre//[!0-9]/}" | |
| vc=$((base + ${n:-0})) | |
| fi | |
| sed -i "s/^version:.*/version: $v+$vc/" pubspec.yaml | |
| - name: Setup Android signing | |
| run: | | |
| if [ -n "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" ]; then | |
| echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/release.jks | |
| cat > android/key.properties << EOF | |
| storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }} | |
| keyAlias=${{ secrets.ANDROID_KEY_ALIAS }} | |
| storeFile=release.jks | |
| EOF | |
| fi | |
| - name: Download core | |
| run: | | |
| curl -L "https://github.com/mapleafgo/singcast-cli/releases/download/$CORE_VERSION/libsingcast-$CORE_VERSION-android-${{ matrix.core_arch }}.tar.gz" -o libsingcast.tar.gz | |
| tar -zxf libsingcast.tar.gz | |
| mv libsingcast-android-${{ matrix.core_arch }}.aar android/app/libs/libsingcast.aar | |
| - name: Build APK | |
| run: flutter build apk --release --target-platform ${{ matrix.target }} | |
| - name: Rename artifact | |
| run: | | |
| v="${VERSION#v}" | |
| mv build/app/outputs/flutter-apk/app-release.apk "build/app/outputs/flutter-apk/singcast-$v-android-${{ matrix.arch }}.apk" | |
| - name: Upload release assets | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ env.VERSION }} | |
| files: build/app/outputs/flutter-apk/*.apk | |
| overwrite_files: true | |
| # iOS build. Produces an unsigned sideload .ipa (gopeed-style). | |
| # NOTE: This is a VPN app with a Packet Tunnel Extension. The committed | |
| # Runner.xcodeproj does NOT contain the ExtensionProvider target, so xcodegen | |
| # regenerates the project from project.yml before building. The resulting ipa | |
| # is unsigned: it can be sideloaded via TrollStore or re-signed, but the | |
| # tunnel extension only runs after re-signing with a paid Apple Developer | |
| # account + NetworkExtension provisioning profiles. | |
| build-ios: | |
| runs-on: macos-latest | |
| timeout-minutes: 40 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ env.FLUTTER_VERSION }} | |
| channel: stable | |
| architecture: ${{ matrix.flutter_arch || 'x64' }} | |
| cache: true | |
| - name: Sync version | |
| run: | | |
| v="${VERSION#v}" | |
| sed -i "" "s/^version:.*/version: $v/" pubspec.yaml | |
| - name: Download core | |
| run: | | |
| rm -rf ios/Frameworks/singcast.xcframework ios/Frameworks/libsingcast-darwin.xcframework | |
| curl -L "https://github.com/mapleafgo/singcast-cli/releases/download/$CORE_VERSION/libsingcast-$CORE_VERSION-ios.tar.gz" -o ios-core.tar.gz | |
| tar -zxf ios-core.tar.gz -C ios/Frameworks | |
| mv ios/Frameworks/singcast.xcframework ios/Frameworks/libsingcast-darwin.xcframework | |
| - name: Mark core framework extension-safe | |
| # The Go-built static library inside the xcframework lacks the | |
| # MH_APP_EXTENSION_SAFE Mach-O flag (0x02000000). Xcode requires this | |
| # for any binary linked into an app-extension target. The binary is | |
| # a Mach-O fat wrapper whose slices are BSD ar archives containing | |
| # Mach-O object files. We patch the flag on every object in place. | |
| run: | | |
| python3 - <<'PY' | |
| import struct, pathlib | |
| MH_APP_EXTENSION_SAFE = 0x02000000 | |
| MH_MAGICS_LE = {0xFEEDFACE, 0xFEEDFACF} | |
| MH_MAGICS_BE = {0xCEFAEDFE, 0xCFFAEDFE} | |
| def is_macho(data, off): | |
| if off + 4 > len(data): | |
| return False | |
| be = struct.unpack(">I", data[off:off+4])[0] | |
| le = struct.unpack("<I", data[off:off+4])[0] | |
| return be in MH_MAGICS_BE or le in MH_MAGICS_LE | |
| def patch_macho(data, off): | |
| flags_off = off + 24 | |
| flags = struct.unpack("<I", data[flags_off:flags_off+4])[0] | |
| if not (flags & MH_APP_EXTENSION_SAFE): | |
| flags |= MH_APP_EXTENSION_SAFE | |
| struct.pack_into("<I", data, flags_off, flags) | |
| return True | |
| return False | |
| def patch_ar_archive(data, base, length): | |
| """Scan a BSD ar archive and patch Mach-O objects inside.""" | |
| patched = 0 | |
| end = min(base + length, len(data)) if length else len(data) | |
| pos = base + 8 # skip "!<arch>\n" | |
| while pos + 60 <= end: | |
| hdr = data[pos:pos+60] | |
| raw_name = hdr[0:16] | |
| size_str = hdr[48:58].rstrip() | |
| try: | |
| size = int(size_str) | |
| except ValueError: | |
| break | |
| member_start = pos + 60 | |
| name_len = 0 | |
| name = raw_name.rstrip() | |
| if name.startswith(b"#1/"): | |
| try: | |
| name_len = int(name[3:]) | |
| except ValueError: | |
| name_len = 0 | |
| obj_off = member_start + name_len | |
| if is_macho(data, obj_off): | |
| if patch_macho(data, obj_off): | |
| patched += 1 | |
| pos = member_start + size | |
| if pos % 2 == 1: | |
| pos += 1 | |
| return patched | |
| def patch_file(data): | |
| patched = 0 | |
| magic = struct.unpack(">I", data[:4])[0] | |
| if magic == 0xCAFEBABE: # FAT_MAGIC (big-endian header) | |
| n = struct.unpack(">I", data[4:8])[0] | |
| for i in range(n): | |
| off = 8 + i * 20 | |
| sl_off = struct.unpack(">I", data[off+8:off+12])[0] | |
| sl_size = struct.unpack(">I", data[off+12:off+16])[0] | |
| if data[sl_off:sl_off+8] == b"!<arch>\n": | |
| patched += patch_ar_archive(data, sl_off, sl_size) | |
| elif is_macho(data, sl_off): | |
| if patch_macho(data, sl_off): | |
| patched += 1 | |
| elif data[:8] == b"!<arch>\n": | |
| patched += patch_ar_archive(data, 0, 0) | |
| elif is_macho(data, 0): | |
| if patch_macho(data, 0): | |
| patched += 1 | |
| return patched | |
| root = pathlib.Path("ios/Frameworks/libsingcast-darwin.xcframework") | |
| total = 0 | |
| for bin in root.rglob("Singcast"): | |
| if not bin.is_file(): | |
| continue | |
| with open(bin, "rb") as f: | |
| data = bytearray(f.read()) | |
| n = patch_file(data) | |
| if n > 0: | |
| with open(bin, "wb") as f: | |
| f.write(data) | |
| print(f" {bin}: patched {n} Mach-O object(s)") | |
| total += n | |
| print(f"Total: {total} objects patched") | |
| if total == 0: | |
| raise SystemExit("ERROR: no Mach-O objects found to patch") | |
| PY | |
| # Re-sign ad-hoc after patching binaries. | |
| XCFW="ios/Frameworks/libsingcast-darwin.xcframework" | |
| # Sign each .framework, then the xcframework itself | |
| find "$XCFW" -name "*.framework" -type d -print0 | while IFS= read -r -d '' fw; do | |
| codesign --force --deep --all-architectures --sign - "$fw" | |
| codesign --verify --verbose "$fw" 2>&1 || true | |
| done | |
| codesign --force --deep --all-architectures --sign - "$XCFW" | |
| codesign --verify --verbose "$XCFW" 2>&1 || true | |
| - name: Regenerate Xcode project | |
| run: | | |
| flutter pub get | |
| brew install xcodegen | |
| cd ios && xcodegen generate | |
| # Disable Swift Package Manager integration: it needs a shared | |
| # Runner.xcscheme that xcodegen's output + Flutter's project patching | |
| # don't reliably provide. This project uses CocoaPods (Podfile), so | |
| # SPM integration is unnecessary. | |
| flutter config --no-enable-swift-package-manager | |
| - name: Build (no codesign) | |
| run: flutter build ios --no-codesign --release | |
| - name: Package ipa | |
| run: | | |
| v="${VERSION#v}" | |
| rm -rf Payload | |
| cp -R build/ios/iphoneos/Runner.app Payload/ | |
| zip -qry singcast-$v-ios.ipa Payload | |
| - name: Upload release assets | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ env.VERSION }} | |
| files: singcast-*-ios.ipa | |
| overwrite_files: true |