soul-browser/scripts/build.sh
KaKi87 d8e5ebc96f
Fix i18n (#44)
* Point Language resource links at the soul-browser-i18n fork.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Ship UI translations so the language picker actually changes the app.

The official bundle kept language resources in Play splits that were never decoded here, so setApplicationLocales had only English strings to load.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Download UI translations at build time instead of vendoring them.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 16:18:31 +02:00

282 lines
8.6 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_DIR="${ROOT_DIR}/app"
DIST_DIR="${ROOT_DIR}/dist"
SPLITS_DIR="${ROOT_DIR}/splits"
TOOLS_DIR="${ROOT_DIR}/tools"
APKTOOL_VERSION="2.10.0"
APKTOOL_JAR="${TOOLS_DIR}/apktool_${APKTOOL_VERSION}.jar"
mkdir -p "${DIST_DIR}" "${TOOLS_DIR}"
if [[ ! -f "${APKTOOL_JAR}" ]]; then
echo "Downloading apktool ${APKTOOL_VERSION}..."
curl -fsSL \
-o "${APKTOOL_JAR}" \
"https://github.com/iBotPeaches/Apktool/releases/download/v${APKTOOL_VERSION}/apktool_${APKTOOL_VERSION}.jar"
fi
find_apksigner() {
if command -v apksigner >/dev/null 2>&1; then
command -v apksigner
return 0
fi
local sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
if [[ -n "${sdk}" && -d "${sdk}/build-tools" ]]; then
local signer
signer="$(find "${sdk}/build-tools" -name apksigner -type f 2>/dev/null | sort -V | tail -1)"
if [[ -n "${signer}" ]]; then
echo "${signer}"
return 0
fi
fi
return 1
}
find_zipalign() {
if command -v zipalign >/dev/null 2>&1; then
command -v zipalign
return 0
fi
local sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
if [[ -n "${sdk}" && -d "${sdk}/build-tools" ]]; then
local aligner
aligner="$(find "${sdk}/build-tools" -name zipalign -type f 2>/dev/null | sort -V | tail -1)"
if [[ -n "${aligner}" ]]; then
echo "${aligner}"
return 0
fi
fi
return 1
}
VERSION_NAME="$(grep 'versionName:' "${APP_DIR}/apktool.yml" | awk '{print $2}')"
VERSION_CODE="$(grep 'versionCode:' "${APP_DIR}/apktool.yml" | awk '{print $2}')"
OUTPUT_NAME="soul-browser-${VERSION_NAME}-${VERSION_CODE}"
detect_build_branch() {
if [[ -n "${GITHUB_REF_NAME:-}" ]]; then
echo "${GITHUB_REF_NAME}"
elif [[ -n "${GITHUB_REF:-}" && "${GITHUB_REF}" == refs/heads/* ]]; then
echo "${GITHUB_REF#refs/heads/}"
else
git -C "${ROOT_DIR}" rev-parse --abbrev-ref HEAD 2>/dev/null || true
fi
}
detect_package_id() {
if [[ -n "${SOUL_PACKAGE_ID:-}" ]]; then
echo "${SOUL_PACKAGE_ID}"
return 0
fi
if [[ "$(detect_build_branch)" == "main" ]]; then
echo "net.kaki87.soul2"
else
echo "net.kaki87.soul2.testing"
fi
}
detect_app_name() {
if [[ -n "${SOUL_APP_NAME:-}" ]]; then
echo "${SOUL_APP_NAME}"
return 0
fi
if [[ "$(detect_build_branch)" == "main" ]]; then
echo "Soul2 Browser"
else
echo "Soul2⁺ Browser"
fi
}
set_app_name() {
local name="$1"
local strings_xml="${APP_DIR}/res/values/strings.xml"
python3 - "$strings_xml" "$name" <<'PY'
import re
import sys
from pathlib import Path
path = Path(sys.argv[1])
name = sys.argv[2]
text = path.read_text(encoding="utf-8")
updated, n = re.subn(
r'(<string name="app_name">)(.*?)(</string>)',
lambda m: f"{m.group(1)}{name}{m.group(3)}",
text,
count=1,
)
if n != 1:
raise SystemExit(f"error: could not update app_name in {path}")
if updated != text:
path.write_text(updated, encoding="utf-8")
print(f"Set app_name to {name}")
else:
print(f"app_name already {name}")
PY
}
PACKAGE_ID="$(detect_package_id)"
APP_NAME="$(detect_app_name)"
echo "Setting application ID to ${PACKAGE_ID}..."
python3 "${ROOT_DIR}/scripts/set-package-id.py" --to "${PACKAGE_ID}"
echo "Setting app name to ${APP_NAME}..."
set_app_name "${APP_NAME}"
echo "Extracting Information text from README.md..."
python3 "${ROOT_DIR}/scripts/prepare-info-string.py"
echo "Preparing resources..."
python3 "${ROOT_DIR}/scripts/sync-i18n.py"
"${ROOT_DIR}/scripts/merge-split-resources.sh"
python3 "${ROOT_DIR}/scripts/sync-public-xml.py"
echo "Building ${OUTPUT_NAME}..."
java -jar "${APKTOOL_JAR}" b "${APP_DIR}" -o "${DIST_DIR}/${OUTPUT_NAME}-unsigned.apk"
MERGED_APK="${DIST_DIR}/${OUTPUT_NAME}-merged-unsigned.apk"
cp "${DIST_DIR}/${OUTPUT_NAME}-unsigned.apk" "${MERGED_APK}"
ORIGINAL_DEX="${ROOT_DIR}/original-dex/classes2.dex"
if [[ -f "${ORIGINAL_DEX}" ]]; then
echo "Replacing recompiled classes2.dex with original desugar libraries..."
zip -q -d "${MERGED_APK}" classes2.dex
(cd "${ROOT_DIR}/original-dex" && zip -q -0 -j "${MERGED_APK}" classes2.dex)
else
echo "warning: ${ORIGINAL_DEX} not found; desugared Java 8+ APIs may crash at runtime"
fi
# OCR: keep only the small ModuleDescriptor/bundled-creator dex in the APK.
# Models + native libs are downloaded at runtime from Google Maven (no Play Services).
BUNDLED_OCR_DIR="${ROOT_DIR}/bundled-ocr"
if [[ ! -f "${BUNDLED_OCR_DIR}/classes_ocr.dex" ]]; then
echo "Preparing OCR module dex (descriptors + bundled creator)..."
"${ROOT_DIR}/scripts/prepare-bundled-ocr.sh"
fi
if [[ -f "${BUNDLED_OCR_DIR}/classes_ocr.dex" ]]; then
echo "Injecting OCR module dex (classes5.dex; models/libs downloaded at runtime)..."
(
cd "${BUNDLED_OCR_DIR}"
cp -f classes_ocr.dex classes5.dex
zip -q -0 -j "${MERGED_APK}" classes5.dex
rm -f classes5.dex
)
else
echo "warning: OCR module dex missing; text recognition may fall back to Play optional modules"
fi
INSTALLER_DEX="${ROOT_DIR}/tools/ocr-runtime/classes_ocr_installer.dex"
if [[ ! -f "${INSTALLER_DEX}" ]]; then
echo "Compiling OCR runtime installer..."
"${ROOT_DIR}/scripts/compile-ocr-runtime.sh"
fi
if [[ -f "${INSTALLER_DEX}" ]]; then
echo "Injecting OCR runtime installer dex (classes6.dex)..."
(
STAGE="$(mktemp -d)"
cp -f "${INSTALLER_DEX}" "${STAGE}/classes6.dex"
(cd "${STAGE}" && zip -q -0 -j "${MERGED_APK}" classes6.dex)
rm -rf "${STAGE}"
)
else
echo "warning: OCR runtime installer dex missing"
fi
if [[ -d "${SPLITS_DIR}" ]]; then
echo "Merging native libraries from split APKs..."
TMP_DIR="$(mktemp -d)"
shopt -s nullglob
for split_apk in "${SPLITS_DIR}"/config.*.apk "${SPLITS_DIR}"/split_config.*.apk; do
[[ -f "${split_apk}" ]] || continue
# Do not use grep -q here: with pipefail, early grep exit SIGPIPEs unzip
# (status 141) and falsely skips splits that do contain native libs.
if unzip -l "${split_apk}" | grep ' lib/' >/dev/null; then
echo " extracting libs from $(basename "${split_apk}")"
unzip -q -o "${split_apk}" "lib/*" -d "${TMP_DIR}"
fi
done
shopt -u nullglob
if [[ -d "${TMP_DIR}/lib" ]]; then
(cd "${TMP_DIR}" && zip -q -r "${MERGED_APK}" lib/)
find "${TMP_DIR}/lib" -type f -name '*.so' | sed 's|.*/lib/| lib/|'
else
echo " warning: no native libraries found in splits/"
fi
rm -rf "${TMP_DIR}"
fi
# Signing: prefer SOUL_* env vars (CI/release). Local builds fall back to a
# generated debug keystore under keystore/debug.keystore.
KEYSTORE="${SOUL_KEYSTORE:-}"
KEY_ALIAS="${SOUL_KEY_ALIAS:-soulbrowser}"
if [[ -n "${KEYSTORE}" ]]; then
if [[ ! -f "${KEYSTORE}" ]]; then
echo "error: SOUL_KEYSTORE not found: ${KEYSTORE}" >&2
exit 1
fi
if [[ -z "${SOUL_KEYSTORE_PASSWORD:-}" || -z "${SOUL_KEY_PASSWORD:-}" ]]; then
echo "error: SOUL_KEYSTORE_PASSWORD and SOUL_KEY_PASSWORD must be set with SOUL_KEYSTORE" >&2
exit 1
fi
echo "Using release keystore: ${KEYSTORE} (alias=${KEY_ALIAS})"
else
KEYSTORE="${ROOT_DIR}/keystore/debug.keystore"
export SOUL_KEYSTORE_PASSWORD="${SOUL_KEYSTORE_PASSWORD:-android}"
export SOUL_KEY_PASSWORD="${SOUL_KEY_PASSWORD:-android}"
if [[ ! -f "${KEYSTORE}" ]]; then
echo "Generating debug keystore..."
mkdir -p "$(dirname "${KEYSTORE}")"
keytool -genkeypair -v \
-keystore "${KEYSTORE}" \
-alias "${KEY_ALIAS}" \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass "${SOUL_KEYSTORE_PASSWORD}" \
-keypass "${SOUL_KEY_PASSWORD}" \
-dname "CN=Soul Browser Debug, OU=Dev, O=Soul Browser, L=Unknown, ST=Unknown, C=FR"
fi
echo "Using debug keystore: ${KEYSTORE} (alias=${KEY_ALIAS})"
fi
ALIGNED_APK="${DIST_DIR}/${OUTPUT_NAME}-aligned.apk"
SIGNED_APK="${DIST_DIR}/${OUTPUT_NAME}.apk"
ZIPALIGN="$(find_zipalign || true)"
if [[ -n "${ZIPALIGN}" ]]; then
"${ZIPALIGN}" -f -p 4 "${MERGED_APK}" "${ALIGNED_APK}"
else
echo "warning: zipalign not found, skipping alignment"
cp "${MERGED_APK}" "${ALIGNED_APK}"
fi
APKSIGNER="$(find_apksigner || true)"
if [[ -z "${APKSIGNER}" ]]; then
echo "error: apksigner not found. Install Android build-tools (targetSdk 36 requires signature scheme v2+)." >&2
exit 1
fi
echo "Signing with apksigner (v1+v2+v3)..."
"${APKSIGNER}" sign \
--ks "${KEYSTORE}" \
--ks-key-alias "${KEY_ALIAS}" \
--ks-pass env:SOUL_KEYSTORE_PASSWORD \
--key-pass env:SOUL_KEY_PASSWORD \
--v1-signing-enabled true \
--v2-signing-enabled true \
--v3-signing-enabled true \
--out "${SIGNED_APK}" \
"${ALIGNED_APK}"
echo "Verifying signature..."
"${APKSIGNER}" verify --verbose "${SIGNED_APK}"
echo "Built APK: ${SIGNED_APK}"
ls -lh "${DIST_DIR}/"