soul-browser/scripts/build.sh
KaKi87 d1ec2a89ea
Add Cloudflare WARP (#58)
* Add unofficial Cloudflare WARP for WebView via amz proxy.

Route browsing through a local HTTP CONNECT proxy and ProxyController, mutually exclusive with the DNS VPN. Downloads and torrents stay direct.

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

* Fix soulamz DNS on Android so WARP registration can succeed.

Pure-Go resolution defaults to localhost:53 without resolv.conf; bootstrap public DNS and build both ABIs with NDK cgo.

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

* Load Android CA roots in soulamz and warm WebView before proxy override.

Fixes TLS verification on device and ProxyController startup races. Document that local VPNs like AdGuard can block WARP endpoint probes.

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

* Prefer non-443 MASQUE endpoints so WARP CONNECT succeeds.

amz auto-select often picks *:443, which fails TLS/CONNECT against current Cloudflare MASQUE; probe preferred ports and only print READY after warp=on.

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

* Prefer non-443 MASQUE endpoints so WARP CONNECT succeeds.

amz auto-selects *:443 with SNI warp.cloudflare.com, which fails against Cloudflare's masque cert and yields CONNECT 502; try known-good ports and probe warp=on before READY.

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

* Ship rebuilt soulamz with MASQUE :4443 preference and rebuild-on-change.

CI was packaging stale bundled libsoulamz.so; rebuild when main.go is newer and pin working non-443 WARP endpoints.

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

* Make WARP settings use the shared SettingActivity UI.

Match DNS and other settings pages with header chrome and list rows; add compile-only stubs so the warp dex can still build separately.

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

* Fix WARP settings crash from wrong R$string package.

Use literal public.xml string IDs so SettingWarp does not reference
com.mycompany.app.soulbrowser.R (app R is net.kaki87.soul2[.testing]).

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

* Show WARP busy spinner and refresh IP from Cloudflare trace.

Replace the settings switch with a ProgressBar while starting/stopping, and show ip/colo/loc under the toggle refreshed on connect and disconnect.

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

* Fix WARP toggle crash writing final SettingItem.t.

Mark the row busy via mutable SettingItem.s only; t is final in the app dex.

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

* Fix WARP spinner crash from wrong ViewHolder.z field type.

Locate the switch via findViewById instead of accessing ViewHolder.z (MySwitchView in app dex).

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

* Avoid off-flash when disabling WARP.

Keep the switch visually on under the spinner while stopping; the list adapter toggles eagerly before our listener runs.

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

* Route all Soul HTTP through WARP and reword VPN guidance.

Install a process-wide ProxySelector plus chokepoint openConnection patches; update warp_info to ask users to whitelist Soul in other VPNs.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 02:07:27 +02:00

322 lines
9.9 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
WARP_DEX="${ROOT_DIR}/tools/warp-runtime/classes_warp.dex"
if [[ ! -f "${WARP_DEX}" ]]; then
echo "Compiling WARP runtime..."
"${ROOT_DIR}/scripts/compile-warp-runtime.sh"
fi
if [[ -f "${WARP_DEX}" ]]; then
echo "Injecting WARP runtime dex (classes7.dex)..."
(
STAGE="$(mktemp -d)"
cp -f "${WARP_DEX}" "${STAGE}/classes7.dex"
(cd "${STAGE}" && zip -q -0 -j "${MERGED_APK}" classes7.dex)
rm -rf "${STAGE}"
)
else
echo "warning: WARP runtime 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
BUNDLED_WARP_DIR="${ROOT_DIR}/bundled-warp"
SOULAMZ_SO="${BUNDLED_WARP_DIR}/lib/arm64-v8a/libsoulamz.so"
SOULAMZ_SRC="${ROOT_DIR}/native/soulamz/main.go"
need_soulamz_build=0
if [[ ! -f "${SOULAMZ_SO}" ]]; then
need_soulamz_build=1
elif [[ -f "${SOULAMZ_SRC}" && "${SOULAMZ_SRC}" -nt "${SOULAMZ_SO}" ]]; then
need_soulamz_build=1
fi
if [[ "${need_soulamz_build}" -eq 1 ]]; then
if [[ -x "${ROOT_DIR}/scripts/build-soulamz.sh" ]] && command -v go >/dev/null 2>&1; then
echo "Building soulamz native binaries..."
"${ROOT_DIR}/scripts/build-soulamz.sh"
fi
fi
if [[ -d "${BUNDLED_WARP_DIR}/lib" ]]; then
echo "Merging soulamz native libraries..."
(cd "${BUNDLED_WARP_DIR}" && zip -q -r "${MERGED_APK}" lib/)
find "${BUNDLED_WARP_DIR}/lib" -type f -name 'libsoulamz.so' | sed 's|.*/lib/| lib/|'
else
echo "warning: bundled-warp/lib missing; WARP proxy binary will be unavailable"
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}/"