c43b913dde
Release APK / build (push) Has been cancelled
react-native.config.js excludes react-native-reanimated and react-native-worklets from Android native linking — neither is used in the app and react-native-gesture-handler has a dedicated noreanimated codepath for exactly this case. build-apk.sh step 4.6 forces newArchEnabled=false in gradle.properties after expo prebuild, as a fallback for New Architecture init issues on Android 15. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
190 lines
7.9 KiB
Bash
Executable File
190 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# build-apk.sh — Build a signed release APK locally without expo.dev / EAS.
|
|
#
|
|
# Prerequisites (first time only):
|
|
# 1. Generate a keystore:
|
|
# keytool -genkey -v -keystore ~/.config/sheethappens/sheethappens.jks \
|
|
# -alias sheethappens -keyalg RSA -keysize 2048 -validity 10000
|
|
# 2. Fill in signing credentials:
|
|
# mkdir -p ~/.config/sheethappens
|
|
# cat > ~/.config/sheethappens/signing.env <<EOF
|
|
# KEYSTORE_PATH=~/.config/sheethappens/sheethappens.jks
|
|
# KEYSTORE_ALIAS=sheethappens
|
|
# KEYSTORE_STORE_PASSWORD=your_store_password
|
|
# KEYSTORE_KEY_PASSWORD=your_key_password
|
|
# EOF
|
|
# chmod 600 ~/.config/sheethappens/signing.env
|
|
#
|
|
# Usage:
|
|
# ./build-apk.sh # → dist/sheethappens-YYYYMMDD-HHMM.apk
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SIGNING_ENV="$HOME/.config/sheethappens/signing.env"
|
|
DIST_DIR="$SCRIPT_DIR/dist"
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
info() { echo -e "${GREEN}[build]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
|
abort() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; }
|
|
|
|
# ─── 1. Signing credentials ────────────────────────────────────────────────
|
|
if [ ! -f "$SIGNING_ENV" ]; then
|
|
abort "Missing signing.env — see comments at top of this script."
|
|
fi
|
|
# shellcheck source=/dev/null
|
|
source "$SIGNING_ENV"
|
|
|
|
[ -z "${KEYSTORE_PATH:-}" ] && abort "KEYSTORE_PATH not set in signing.env"
|
|
[ -z "${KEYSTORE_ALIAS:-}" ] && abort "KEYSTORE_ALIAS not set in signing.env"
|
|
[ -z "${KEYSTORE_STORE_PASSWORD:-}" ] && abort "KEYSTORE_STORE_PASSWORD not set in signing.env"
|
|
[ -z "${KEYSTORE_KEY_PASSWORD:-}" ] && abort "KEYSTORE_KEY_PASSWORD not set in signing.env"
|
|
|
|
KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH/#\$HOME/$HOME}"
|
|
KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH_EXPANDED/#~/$HOME}"
|
|
|
|
[ ! -f "$KEYSTORE_PATH_EXPANDED" ] && \
|
|
abort "Keystore not found: $KEYSTORE_PATH_EXPANDED"
|
|
|
|
# ─── 2. Java check ─────────────────────────────────────────────────────────
|
|
if [ -z "${JAVA_HOME:-}" ] || "$JAVA_HOME/bin/java" -version 2>&1 | grep -qE '"(2[5-9]|[3-9][0-9])\.'; then
|
|
if [ -d "$HOME/jdk21" ]; then
|
|
export JAVA_HOME="$HOME/jdk21"
|
|
export PATH="$JAVA_HOME/bin:$PATH"
|
|
info "Using local JDK 21: $JAVA_HOME"
|
|
else
|
|
abort "Java 25+ detected but Gradle only supports ≤ Java 24.\nInstall JDK 21 and set JAVA_HOME."
|
|
fi
|
|
fi
|
|
info "Java: $("$JAVA_HOME/bin/java" -version 2>&1 | head -1)"
|
|
|
|
# ─── 3. Android SDK ────────────────────────────────────────────────────────
|
|
if [ -z "${ANDROID_HOME:-}" ]; then
|
|
for candidate in "$HOME/android-sdk" "$HOME/Android/Sdk" "/opt/android-sdk"; do
|
|
if [ -d "$candidate/platform-tools" ]; then
|
|
export ANDROID_HOME="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
[ -z "${ANDROID_HOME:-}" ] && abort "Android SDK not found. Set ANDROID_HOME."
|
|
export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin"
|
|
info "Android SDK: $ANDROID_HOME"
|
|
|
|
# ─── 4. expo prebuild ──────────────────────────────────────────────────────
|
|
info "Running expo prebuild (Android)…"
|
|
cd "$SCRIPT_DIR"
|
|
npx expo prebuild --platform android --clean --no-install
|
|
|
|
# ─── 4.5. Patch native modules — 16KB page size (Android 15) ──────────────
|
|
info "Patching native modules for 16KB page size support (Android 15)…"
|
|
|
|
WORKLETS_CMAKE="$SCRIPT_DIR/node_modules/react-native-worklets/android/CMakeLists.txt"
|
|
REANIMATED_CMAKE="$SCRIPT_DIR/node_modules/react-native-reanimated/android/CMakeLists.txt"
|
|
|
|
if [ -f "$WORKLETS_CMAKE" ] && ! grep -q "max-page-size" "$WORKLETS_CMAKE"; then
|
|
printf '\ntarget_link_options(worklets PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$WORKLETS_CMAKE"
|
|
info "Patched react-native-worklets for 16KB pages"
|
|
fi
|
|
|
|
if [ -f "$REANIMATED_CMAKE" ] && ! grep -q "max-page-size" "$REANIMATED_CMAKE"; then
|
|
printf '\ntarget_link_options(reanimated PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$REANIMATED_CMAKE"
|
|
info "Patched react-native-reanimated for 16KB pages"
|
|
fi
|
|
|
|
# ─── 4.6. Disable New Architecture (Android 15 compat) ────────────────────
|
|
GRADLE_PROPS_NEWARCH="$SCRIPT_DIR/android/gradle.properties"
|
|
if [ -f "$GRADLE_PROPS_NEWARCH" ]; then
|
|
sed -i 's/^newArchEnabled=true/newArchEnabled=false/' "$GRADLE_PROPS_NEWARCH"
|
|
if ! grep -q "^newArchEnabled=" "$GRADLE_PROPS_NEWARCH"; then
|
|
echo "newArchEnabled=false" >> "$GRADLE_PROPS_NEWARCH"
|
|
fi
|
|
info "New Architecture disabled in gradle.properties"
|
|
fi
|
|
|
|
# ─── 5. Patch build.gradle — inject release signingConfig ──────────────────
|
|
info "Patching android/app/build.gradle for release signing…"
|
|
|
|
python3 - "$SCRIPT_DIR/android/app/build.gradle" << 'PYEOF'
|
|
import sys, re
|
|
|
|
path = sys.argv[1]
|
|
with open(path, 'r') as f:
|
|
content = f.read()
|
|
|
|
if 'MYAPP_UPLOAD_STORE_FILE' in content:
|
|
print('[patch] build.gradle already patched, skipping.')
|
|
sys.exit(0)
|
|
|
|
release_signing = """\n release {
|
|
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
|
|
storeFile file(MYAPP_UPLOAD_STORE_FILE)
|
|
storePassword MYAPP_UPLOAD_STORE_PASSWORD
|
|
keyAlias MYAPP_UPLOAD_KEY_ALIAS
|
|
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
|
|
}
|
|
}"""
|
|
|
|
content = re.sub(
|
|
r'(signingConfigs\s*\{[\s\S]*?debug\s*\{[\s\S]*?\})',
|
|
r'\1' + release_signing,
|
|
content,
|
|
count=1
|
|
)
|
|
|
|
def replace_release_signing(m):
|
|
return m.group(0).replace('signingConfig signingConfigs.debug',
|
|
'signingConfig signingConfigs.release', 1)
|
|
|
|
content = re.sub(
|
|
r'release\s*\{[^}]*signingConfig\s+signingConfigs\.debug',
|
|
replace_release_signing,
|
|
content,
|
|
count=1,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
with open(path, 'w') as f:
|
|
f.write(content)
|
|
|
|
print('[patch] build.gradle patched for release signing.')
|
|
PYEOF
|
|
|
|
# ─── 6. Inject signing props into gradle.properties ────────────────────────
|
|
info "Injecting signing credentials into gradle.properties…"
|
|
GRADLE_PROPS="$SCRIPT_DIR/android/gradle.properties"
|
|
|
|
sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS" 2>/dev/null || true
|
|
|
|
cat >> "$GRADLE_PROPS" << EOF
|
|
|
|
# --- sheethappens release signing (injected by build-apk.sh — wiped after build)
|
|
MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH_EXPANDED
|
|
MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_STORE_PASSWORD
|
|
MYAPP_UPLOAD_KEY_ALIAS=$KEYSTORE_ALIAS
|
|
MYAPP_UPLOAD_KEY_PASSWORD=$KEYSTORE_KEY_PASSWORD
|
|
# --- end signing
|
|
EOF
|
|
|
|
# ─── 7. Gradle build ───────────────────────────────────────────────────────
|
|
cd "$SCRIPT_DIR/android"
|
|
chmod +x gradlew
|
|
info "Building APK (release)…"
|
|
./gradlew assembleRelease --no-daemon
|
|
|
|
# ─── 8. Wipe credentials from gradle.properties ───────────────────────────
|
|
sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS"
|
|
info "Signing credentials wiped from gradle.properties."
|
|
|
|
# ─── 9. Copy to dist/ ─────────────────────────────────────────────────────
|
|
mkdir -p "$DIST_DIR"
|
|
TIMESTAMP="$(date +%Y%m%d-%H%M)"
|
|
OUTPUT="$DIST_DIR/sheethappens-$TIMESTAMP.apk"
|
|
cp "app/build/outputs/apk/release/app-release.apk" "$OUTPUT"
|
|
|
|
echo ""
|
|
info "Build complete!"
|
|
echo -e " ${GREEN}→ $OUTPUT${NC}"
|
|
echo ""
|