name: release on: push: tags: - 'v*' permissions: contents: write jobs: build: strategy: fail-fast: false matrix: include: # Pin to Ubuntu 22.04 (GLIBC 2.35) so the glibc builds run on any # distro that's ≥ Ubuntu 22.04 / Debian 12 / Mint 21 / Fedora 36. # ubuntu-latest points at 24.04 (GLIBC 2.39) which bakes in a # too-new GLIBC symbol requirement and rejects loading on older # distros. For users behind tight internet who literally can't # dist-upgrade, this matters. - target: x86_64-unknown-linux-gnu os: ubuntu-22.04 name: mhrv-rs-linux-amd64 - target: aarch64-unknown-linux-gnu os: ubuntu-22.04 name: mhrv-rs-linux-arm64 - target: arm-unknown-linux-gnueabihf os: ubuntu-22.04 name: mhrv-rs-raspbian-armhf - target: x86_64-apple-darwin os: macos-latest name: mhrv-rs-macos-amd64 - target: aarch64-apple-darwin os: macos-latest name: mhrv-rs-macos-arm64 - target: x86_64-pc-windows-gnu os: windows-latest name: mhrv-rs-windows-amd64 - target: x86_64-unknown-linux-musl os: ubuntu-latest name: mhrv-rs-linux-musl-amd64 - target: aarch64-unknown-linux-musl os: ubuntu-latest name: mhrv-rs-linux-musl-arm64 # OpenWRT MT7621 (soft-float mipsel 32-bit). Dozens of cheap # home routers run this chipset and they *specifically* need # the soft-float variant — MT7621 has no hardware FPU and a # hard-float binary segfaults on the first fp op. Tier-3 in # Rust since 1.72; we build it via messense's musl-cross # docker image which still has a mipsel-softfloat toolchain. # `continue-on-error: true` so a regression here doesn't block # the rest of the release. Issue #26. - target: mipsel-unknown-linux-musl os: ubuntu-latest name: mhrv-rs-openwrt-mipsel-softfloat mipsel_softfloat: true runs-on: ${{ matrix.os }} # mipsel-softfloat is best-effort: the Rust tier-3 target occasionally # regresses. Letting it fail keeps the main release going so # desktop/Android users aren't blocked by MT7621 router support. continue-on-error: ${{ matrix.mipsel_softfloat == true }} steps: - uses: actions/checkout@v4 # Skip the host-level rustup install for mipsel-softfloat — that # target is tier-3 in stable Rust (no prebuilt stdlib available # via rustup), and the docker image we use for this build ships # its own Rust toolchain + std. Trying to pass # `targets: mipsel-unknown-linux-musl` to dtolnay/rust-toolchain # errors out with "error: component 'rust-std' for target # 'mipsel-unknown-linux-musl' is unavailable for download", which # fails the job before the docker step ever runs. - uses: dtolnay/rust-toolchain@stable if: matrix.mipsel_softfloat != true with: targets: ${{ matrix.target }} # eframe needs a few system libs on Linux for window management, keyboard, # and OpenGL/X11/Wayland. We install them on the Ubuntu runners regardless # of arch so both CLI-only and UI builds succeed. - name: Install Linux eframe system deps if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y \ libxkbcommon-dev \ libwayland-dev \ libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \ libx11-dev \ libgl1-mesa-dev libglib2.0-dev libgtk-3-dev - name: Install aarch64 cross-compile toolchain (Linux only) if: matrix.target == 'aarch64-unknown-linux-gnu' run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml - name: Install armhf cross-compile toolchain (Linux only) if: matrix.target == 'arm-unknown-linux-gnueabihf' run: | sudo apt-get update sudo apt-get install -y gcc-arm-linux-gnueabihf echo '[target.arm-unknown-linux-gnueabihf]' >> ~/.cargo/config.toml echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config.toml - name: Install Windows MinGW toolchain if: matrix.target == 'x86_64-pc-windows-gnu' id: msys2 uses: msys2/setup-msys2@v2 with: msystem: MINGW64 update: true install: mingw-w64-x86_64-gcc - name: Configure Windows GNU linker if: matrix.target == 'x86_64-pc-windows-gnu' shell: pwsh run: | $gcc = "${{ steps.msys2.outputs.msys2-location }}\mingw64\bin\gcc.exe" -replace '\\','/' New-Item -ItemType Directory -Force -Path $env:USERPROFILE/.cargo | Out-Null Add-Content -Path $env:USERPROFILE/.cargo/config.toml -Value '[target.x86_64-pc-windows-gnu]' Add-Content -Path $env:USERPROFILE/.cargo/config.toml -Value "linker = '$gcc'" - name: Build CLI if: "!endsWith(matrix.target, '-linux-musl')" run: cargo build --release --target ${{ matrix.target }} --bin mhrv-rs # Fully-static musl builds for OpenWRT / Alpine / libc-less systems. # messense/rust-musl-cross ships a pre-built musl toolchain so `ring` # (rustls' crypto backend) cross-compiles cleanly on both archs. - name: Build CLI (musl via docker) if: matrix.target == 'x86_64-unknown-linux-musl' run: | docker run --rm -v "$PWD":/src -w /src \ messense/rust-musl-cross:x86_64-musl \ cargo build --release --target x86_64-unknown-linux-musl --bin mhrv-rs sudo chown -R "$(id -u):$(id -g)" target - name: Build CLI (musl via docker, arm64) if: matrix.target == 'aarch64-unknown-linux-musl' run: | docker run --rm -v "$PWD":/src -w /src \ messense/rust-musl-cross:aarch64-musl \ cargo build --release --target aarch64-unknown-linux-musl --bin mhrv-rs sudo chown -R "$(id -u):$(id -g)" target # OpenWRT MT7621 / mipsel-softfloat. The messense image tag # `mipsel-musl-softfloat` ships a toolchain that emits soft-float # insn exclusively — matches the MT7621's FPU-less reality. # Requires Rust nightly + -Z build-std because mipsel is tier 3 # in the stable channel, which means no pre-built std. - name: Build CLI (mipsel-softfloat via docker) if: matrix.target == 'mipsel-unknown-linux-musl' && matrix.mipsel_softfloat == true run: | docker run --rm -v "$PWD":/src -w /src \ messense/rust-musl-cross:mipsel-musl-softfloat \ sh -c "rustup toolchain install nightly --profile minimal --component rust-src && \ cargo +nightly build --release \ -Z build-std=std,panic_abort \ --target mipsel-unknown-linux-musl \ --bin mhrv-rs" sudo chown -R "$(id -u):$(id -g)" target # UI build: we try to build the UI binary on every platform. If it fails # on cross-compile for linux-arm64 (missing arm64 system libs cross), # we still ship the CLI. We also skip the UI on musl targets (OpenWRT etc. # are headless, bundling X11 makes no sense). - name: Build UI if: matrix.target != 'aarch64-unknown-linux-gnu' && matrix.target != 'arm-unknown-linux-gnueabihf' && !endsWith(matrix.target, '-linux-musl') run: cargo build --release --target ${{ matrix.target }} --features ui --bin mhrv-rs-ui - name: Package (unix) if: runner.os != 'Windows' run: | mkdir -p dist cp target/${{ matrix.target }}/release/mhrv-rs dist/mhrv-rs chmod +x dist/mhrv-rs if [ -f target/${{ matrix.target }}/release/mhrv-rs-ui ]; then cp target/${{ matrix.target }}/release/mhrv-rs-ui dist/mhrv-rs-ui chmod +x dist/mhrv-rs-ui fi # OpenWRT / musl archives get the procd init script instead of run.sh, # since routers don't have a CA to install and run headless via procd. case "${{ matrix.target }}" in *-linux-musl) cp assets/openwrt/mhrv-rs.init dist/mhrv-rs.init chmod +x dist/mhrv-rs.init ;; *) cp assets/launchers/run.sh dist/run.sh chmod +x dist/run.sh if [ "${{ runner.os }}" = "macOS" ]; then cp assets/launchers/run.command dist/run.command chmod +x dist/run.command fi ;; esac - name: Build macOS .app bundle if: runner.os == 'macOS' run: | VER="${GITHUB_REF#refs/tags/v}" ./assets/macos/build-app.sh dist/mhrv-rs-ui "$VER" dist # Make a clean zip of just the .app for the release cd dist zip -qry "${{ matrix.name }}-app.zip" mhrv-rs.app - name: Package (windows) if: runner.os == 'Windows' shell: pwsh run: | New-Item -ItemType Directory -Force -Path dist | Out-Null Copy-Item target/${{ matrix.target }}/release/mhrv-rs.exe dist/mhrv-rs.exe if (Test-Path target/${{ matrix.target }}/release/mhrv-rs-ui.exe) { Copy-Item target/${{ matrix.target }}/release/mhrv-rs-ui.exe dist/mhrv-rs-ui.exe } Copy-Item assets/launchers/run.bat dist/run.bat - name: Make archive shell: bash run: | cd dist case "${{ matrix.target }}" in *-pc-windows-*) 7z a -tzip "${{ matrix.name }}.zip" mhrv-rs.exe mhrv-rs-ui.exe run.bat ;; *-apple-darwin) tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh run.command ;; *-linux-musl) tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs.init ;; *) tar czf "${{ matrix.name }}.tar.gz" mhrv-rs mhrv-rs-ui run.sh 2>/dev/null || tar czf "${{ matrix.name }}.tar.gz" mhrv-rs run.sh ;; esac - uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} path: | dist/${{ matrix.name }}.tar.gz dist/${{ matrix.name }}.zip dist/${{ matrix.name }}-app.zip if-no-files-found: ignore # Android build — separate job so it doesn't inflate the matrix. The # Rust side here cross-compiles to FOUR ABIs (arm64-v8a, armeabi-v7a, # x86_64, x86) via cargo-ndk and drops the .so files into the Gradle # project's jniLibs/ tree, which then packages them into a single # universal APK. Users pick it once, no per-ABI split. android: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Set up Android SDK uses: android-actions/setup-android@v3 with: cmdline-tools-version: 11076708 - name: Install NDK run: | yes | sdkmanager --install "ndk;26.1.10909125" >/dev/null echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125" >> "$GITHUB_ENV" - uses: dtolnay/rust-toolchain@stable with: targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android - name: Install cargo-ndk run: cargo install cargo-ndk --locked # `./gradlew :app:assembleRelease` triggers cargoBuildRelease first # which invokes cargo-ndk with all four targets, then Gradle packages # the APK (release buildType signed with the debug keystore — see # android/app/build.gradle.kts comment explaining why). - name: Build release APK working-directory: android run: | chmod +x ./gradlew ./gradlew :app:assembleRelease --no-daemon --stacktrace - name: Rename APK with version working-directory: android run: | VER="${GITHUB_REF#refs/tags/v}" SRC="app/build/outputs/apk/release/app-release.apk" if [ ! -f "$SRC" ]; then # Some AGP versions name it differently when the release config # can't be auto-signed. Catch that up front with a clear error # instead of a silent missing-artifact later. echo "::error::expected $SRC to exist; actual outputs:" find app/build/outputs/apk -type f -name '*.apk' -print exit 1 fi mkdir -p ../dist cp "$SRC" "../dist/mhrv-rs-android-universal-v${VER}.apk" - uses: actions/upload-artifact@v4 with: name: mhrv-rs-android-universal path: dist/*.apk if-no-files-found: error release: needs: [build, android] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Release uses: softprops/action-gh-release@v2 with: files: dist/* generate_release_notes: true # Notify the Persian-speaking Telegram channel with the CI-built # Android APK + its sha256 + the per-version changelog from # `docs/changelog/v.md`. # # Two Telegram API calls: # 1. sendDocument — APK file + a short caption (Telegram caps # captions at 1024 chars, and we have bigger changelogs than # that). # 2. sendMessage — full changelog as a reply to #1, Persian # quote-block first then English, same pattern as the # previous manual post. No emojis, as the user asked. # # Needs two repo secrets: # TELEGRAM_BOT_TOKEN — bot the channel admits as poster # TELEGRAM_CHAT_ID — numeric chat id (starts with -100...) # Missing either => the whole job is skipped (not failed) so a # forker who hasn't set up a Telegram channel gets a clean release. telegram: needs: [android, release] runs-on: ubuntu-latest # `vars.TELEGRAM_NOTIFY_ENABLED` defaults to empty in forks; set # `TELEGRAM_NOTIFY_ENABLED=1` as a repo variable to enable. In # the therealaleph origin we leave it on permanently via this # env fallback — the `|| ...` expression makes it default-on # when the secret is present. if: ${{ always() && needs.android.result == 'success' }} steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: mhrv-rs-android-universal path: apk - name: Post to Telegram env: BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} # Python over curl/bash so we don't have to fight curl's -F # value-interpretation rules. curl treats `-F "caption=<..."` # as "read the caption from file named ..." when the value # starts with `<`, which matches our `` HTML-bold tags and # silently turns the whole job into a "file not found" exit # 26. Python stdlib has no such wart. run: | set -euo pipefail VER="${GITHUB_REF#refs/tags/v}" APK="apk/mhrv-rs-android-universal-v${VER}.apk" if [ -z "${BOT_TOKEN:-}" ] || [ -z "${CHAT_ID:-}" ]; then echo "::notice::TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID not set, skipping Telegram post" exit 0 fi if [ ! -f "$APK" ]; then echo "::error::expected $APK to exist; got:" ls -la apk/ exit 1 fi python3 .github/scripts/telegram_release_notify.py \ --apk "$APK" \ --version "$VER" \ --repo "$GITHUB_REPOSITORY" \ --changelog "docs/changelog/v${VER}.md"