* 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>
282 lines
8.6 KiB
Bash
Executable file
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}/"
|