Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions .github/workflows/fork-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
name: Build APK and publish release

# Builds a signed *release* APK on GitHub's runners and publishes it as a
# GitHub Release asset. The existing release.yml only builds debug APKs and
# needs a maintainer keystore, so forks had no way to ship a non-debuggable
# build from CI.
#
# Signing: set the KEYSTORE_BASE64 / KEYSTORE_PASSWORD / KEY_ALIAS /
# KEY_PASSWORD repository secrets to sign with a stable key. Without them a
# throwaway key is generated so the build still succeeds; the release notes
# record which key was used so users know whether they can install it over an
# existing copy.

on:
workflow_dispatch:
inputs:
version:
description: 'Release tag to publish, e.g. fork-v1.0.0 (default: fork-<run number>)'
required: false
default: ''
push:
tags:
- 'fork-v*'

permissions:
contents: write

concurrency:
group: fork-release-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 1
lfs: false
persist-credentials: false

- name: Pull LFS files
run: git lfs pull

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Install Rust Android target
run: rustup target add aarch64-linux-android

# The ubuntu-latest image already ships an Android SDK; android-actions/
# setup-android@v3 fails on it because it insists on installing the
# long-removed `tools` package. Use the preinstalled SDK like the other
# workflows in this repository do.
- name: Install NDK and CMake
run: |
set -euo pipefail
sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
if [ -z "$sdk" ] || [ ! -d "$sdk" ]; then
echo "::error::No Android SDK on this runner (ANDROID_SDK_ROOT/ANDROID_HOME unset)"
exit 1
fi
echo "ANDROID_SDK_ROOT=$sdk" >> "$GITHUB_ENV"
echo "ANDROID_HOME=$sdk" >> "$GITHUB_ENV"
sdkmanager_bin="$(find "$sdk/cmdline-tools" -maxdepth 3 -name sdkmanager -type f 2>/dev/null | sort -V | tail -1)"
if [ -z "$sdkmanager_bin" ]; then
echo "::warning::sdkmanager not found under $sdk; letting Gradle install NDK and CMake"
exit 0
fi
echo "Using $sdkmanager_bin"
yes | "$sdkmanager_bin" --licenses >/dev/null 2>&1 || true
"$sdkmanager_bin" "ndk;27.3.13750724" "cmake;3.22.1" "platforms;android-35" "build-tools;35.0.0"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Configure signing
id: signing
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
set -euo pipefail
if [ -n "${KEYSTORE_BASE64:-}" ]; then
printf '%s' "$KEYSTORE_BASE64" | base64 -d > release.jks
store_password="${KEYSTORE_PASSWORD:?KEYSTORE_PASSWORD is required with KEYSTORE_BASE64}"
key_password="${KEY_PASSWORD:-$store_password}"
key_alias="${KEY_ALIAS:?KEY_ALIAS is required with KEYSTORE_BASE64}"
printf 'CI signed with the repository keystore secret.\n' > signing-note.txt
echo "stable=yes" >> "$GITHUB_OUTPUT"
else
echo "::warning::No KEYSTORE_BASE64 secret; generating a throwaway signing key. Users cannot install this APK over an existing copy signed with a different key."
keytool -genkeypair -keystore release.jks -alias winnative \
-keyalg RSA -keysize 2048 -validity 10000 \
-storepass ephemeral123 -keypass ephemeral123 \
-dname "CN=WinNative CI (ephemeral), OU=CI, O=CI, L=CI, C=CI" >/dev/null 2>&1
store_password=ephemeral123
key_password=ephemeral123
key_alias=winnative
printf 'Signed with a throwaway CI key; uninstall any existing copy first.\n' > signing-note.txt
echo "stable=no" >> "$GITHUB_OUTPUT"
fi
cat > signing.properties <<EOF
storeFile=release.jks
storePassword=${store_password}
keyAlias=${key_alias}
keyPassword=${key_password}
EOF
chmod 600 signing.properties

- name: Build release APK
run: ./gradlew :app:assembleStandardRelease --build-cache --parallel -x lint

- name: Verify APK
run: |
# The runner invokes this script with `bash -e`, so any single failing
# command aborts silently. Report the failing line instead.
trap 'status=$?; echo "::error::verify APK failed at line $LINENO (status $status)"' ERR
set -uo pipefail
apk="app/build/outputs/apk/standard/release/app-standard-release.apk"
if [ ! -f "$apk" ]; then
echo "APK directory contents:"
ls -R app/build/outputs/apk || true
echo "::error::APK not found at $apk"
exit 1
fi
echo "APK: $apk ($(stat -c%s "$apk") bytes)"

build_tools="$(ls -d "$ANDROID_SDK_ROOT"/build-tools/* 2>/dev/null | sort -V | tail -1)"
echo "build-tools: ${build_tools:-<none>}"
if [ -z "${build_tools:-}" ] || [ ! -x "$build_tools/aapt2" ]; then
echo "::error::aapt2 not found under $ANDROID_SDK_ROOT/build-tools"
exit 1
fi
if ! "$build_tools/aapt2" dump badging "$apk" > badging.txt; then
echo "::error::aapt2 dump badging failed"
exit 1
fi
if grep -q "application-debuggable" badging.txt; then
echo "::error::release APK is debuggable; Android would show the 16 KB compatibility warning"
exit 1
fi
echo "APK is not debuggable"

# libwnsteam.so is produced by cargo, which drives the NDK linker
# itself; a regression there would silently fall back to 4 KB pages.
rm -rf apk-lib
mkdir -p apk-lib
if ! unzip -o -q "$apk" "lib/arm64-v8a/libwnsteam.so" -d apk-lib; then
echo "::error::could not extract libwnsteam.so from the APK"
exit 1
fi
ls -l apk-lib/lib/arm64-v8a/
readelf_bin="$(ls -d "$ANDROID_SDK_ROOT"/ndk/27.3.13750724/toolchains/llvm/prebuilt/*/bin/llvm-readelf 2>/dev/null | head -1)"
echo "llvm-readelf: ${readelf_bin:-<none>}"
if [ -z "${readelf_bin:-}" ] || [ ! -x "$readelf_bin" ]; then
echo "::error::llvm-readelf not found in NDK 27.3.13750724"
exit 1
fi
# Write to a file instead of piping into awk: an early awk exit sends
# SIGPIPE to readelf and aborts the step under `bash -e`.
"$readelf_bin" -lW apk-lib/lib/arm64-v8a/libwnsteam.so > readelf-headers.txt
align="$(awk '/LOAD/ { print $NF; exit }' readelf-headers.txt)"
echo "libwnsteam.so LOAD align: ${align:-<none>}"
if [ "$align" != "0x4000" ]; then
echo "::error::libwnsteam.so is not 16 KB aligned (align=${align:-<none>})"
exit 1
fi

- name: Work out release tag
id: tag
run: |
set -euo pipefail
tag="${{ inputs.version }}"
if [ -z "$tag" ]; then
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
tag="${GITHUB_REF_NAME}"
else
tag="fork-${GITHUB_RUN_NUMBER}"
fi
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: WinNative ${{ steps.tag.outputs.tag }}
draft: false
prerelease: true
body: |
Release APK built from `${{ github.sha }}` by the
`Build APK and publish release` workflow on GitHub's runners.

- flavour: `standard` (application id `com.winnative.cmod`)
- build type: **release** (not debuggable, so Android does not show the
16 KB page-size compatibility warning)
- Steam download fixes: CDN auth token (`GetCDNAuthToken#1`), logon
`cell_id` reuse, Asia-Pacific CDN fallback, adaptive host scheduling
with a pre-flight reachability probe

$(cat signing-note.txt)

`libcrypto.so`, `libssl.so` and `libcurl.so` still come from the
`com.android.ndk.thirdparty` prefab artefacts, which are built for
4 KB pages; devices booted with a 16 KB page-size kernel are
therefore not supported yet.
files: app/build/outputs/apk/standard/release/app-standard-release.apk
fail_on_unmatched_files: true
40 changes: 40 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,43 @@
-dontwarn androidx.window.sidecar.SidecarInterface
-dontwarn androidx.window.sidecar.SidecarProvider
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo

# snakeyaml's optional JavaBeans introspection path is unreachable on Android
# (java.beans.* is not part of the platform), but R8 still resolves its
# references and fails the release build without these suppressions.
-dontwarn java.beans.**
-dontwarn org.yaml.snakeyaml.introspector.**

# The app talks to its own native code in both directions: ~20 Java/Kotlin
# classes declare native/external methods that resolve to exported
# Java_<package>_<class>_<method> symbols, and libwinlator/libwaylandcomp/
# libwnsteam look callbacks up by name with GetMethodID (for example
# OnExtractFileListener.onExtractFileProgress, WaylandCompositor.onGameFrame,
# GPUImage.setStride, XConnectorEpoll.ConnectionHandler.handleNewConnection).
# Renaming any of those makes the lookup throw NoSuchMethodError at runtime,
# which is exactly what a release build hit while extracting imagefs.
# Keep names so the JNI surface stays addressable; shrinking and optimisation
# still run.
-dontobfuscate

# R8 cannot see calls that cross the JNI boundary, so it treats every Java method
# that only native code calls as dead and removes it. That broke imagefs
# extraction in the first release build: libwinlator looked up
# OnExtractFileListener.onExtractFileProgress with GetMethodID and got
# NoSuchMethodError because the method had been shrunk away. The same applies to
# the other direction - WaylandCompositor's static frame callbacks, the Xconnector
# connection handlers, GPUImage.setStride, and the Kotlin listeners that the Rust
# Steam client calls back (onProgress, onComplete, onAuthResult, ...).
# Keep the app's own classes intact and let R8 shrink only third-party code.
-keep class com.winlator.** { *; }
-keep class com.winnative.** { *; }
-keep class org.love2d.** { *; }


# androidx.security.crypto builds its keys through Tink, which registers
# algorithm implementations reflectively. Without these keeps, a release build
# loses them and EncryptedSharedPreferences.create() throws at runtime, taking
# every Activity down through PrefManager's initialization.
-keep class androidx.security.crypto.** { *; }
-keep class com.google.crypto.tink.** { *; }
-dontwarn com.google.crypto.tink.**
4 changes: 4 additions & 0 deletions app/src/main/cpp/proot/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ add_executable(libproot-loader.so
target_include_directories(libproot-loader.so PRIVATE src)
target_compile_definitions(libproot-loader.so PRIVATE ${PROOT_DEFINES})
target_compile_options(libproot-loader.so PRIVATE ${PROOT_CFLAGS} -fPIC -ffreestanding -fno-stack-protector)
# Release builds add -Wl,--gc-sections; without a GC root the linker discards the
# unreferenced pokedata_workaround stub that loader-info.cmake measures, which made
# assembleRelease fail. -u keeps it in the image on every build type.
target_link_options(libproot-loader.so PRIVATE -static -nostdlib -no-pie
-Wl,--undefined=pokedata_workaround
-Wl,--build-id=none,-Ttext=0x2000000000,--rosegment,-z,noexecstack)

# tracee/mem.c finds the loader's pokedata_workaround stub by its offset from _start.
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/cpp/wn-steam-client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ set(WNSTEAM_RUST_OUT "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libwnsteam.so")
set(WNSTEAM_RUST_LINKER
"${ANDROID_NDK}/toolchains/llvm/prebuilt/${WNSTEAM_NDK_HOST_TAG}/bin/${WNSTEAM_RUST_TARGET}${WNSTEAM_ANDROID_API}-clang${WNSTEAM_LINKER_SUFFIX}")

# Android 15+ ships 16 KB page-size kernels. Cargo drives the NDK linker itself
# and therefore does not inherit Gradle's CMake linker flags, so ask for 16 KB
# LOAD alignment explicitly here; otherwise libwnsteam.so would be the only ELF
# in the APK still built for 4 KB pages.
add_custom_command(
OUTPUT "${WNSTEAM_RUST_OUT}"
COMMAND "${CMAKE_COMMAND}" -E env
Expand All @@ -64,6 +68,7 @@ add_custom_command(
"AR_aarch64_linux_android=${ANDROID_NDK}/toolchains/llvm/prebuilt/${WNSTEAM_NDK_HOST_TAG}/bin/llvm-ar${WNSTEAM_AR_SUFFIX}"
"${CARGO_EXECUTABLE}" rustc --release --target "${WNSTEAM_RUST_TARGET}" --lib --
-Clink-arg=-Wl,-soname,libwnsteam.so
-Clink-arg=-Wl,-z,max-page-size=16384
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${WNSTEAM_RUST_LIB}"
"${WNSTEAM_RUST_OUT}"
Expand Down