feat(build): require Discord in production and keep Lite opt-in

This commit is contained in:
zarzet committed 2026-10-04 01:40:54 +07:00
1 parent 541bfc4002
commit 9c0b22e2c8
9 files changed
+333 -17

No files matched your search

+86 -7
View File
@@ -6,33 +6,112 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
OUTPUT_DIR="$PROJECT_DIR/build/app/outputs/flutter-apk"
FLUTTER_ARGS=()
PRODUCTION_BUILD=0
LITE_BUILD=0
for argument in "$@"; do
case "$argument" in
--production) PRODUCTION_BUILD=1 ;;
--lite) LITE_BUILD=1; export SPOTIFLAC_DISCORD_SDK=0 ;;
--target-platform|--target-platform=*|--split-per-abi|--no-split-per-abi)
echo "Error: set SPOTIFLAC_RUST_ANDROID_ABIS to select the release APK architectures." >&2
exit 1
;;
*) FLUTTER_ARGS+=("$argument") ;;
esac
done
if [[ "$PRODUCTION_BUILD" == "1" ]]; then
if [[ "$LITE_BUILD" == "1" ]]; then
echo "Error: production builds require Discord; --production cannot be combined with --lite." >&2
exit 1
fi
export SPOTIFLAC_DISCORD_SDK=1
fi
case "${SPOTIFLAC_DISCORD_SDK:-1}" in
0|1) ;;
*) echo "Error: SPOTIFLAC_DISCORD_SDK must be 0 (Lite) or 1 (full)." >&2; exit 1 ;;
esac
ANDROID_ABIS_RAW="${SPOTIFLAC_RUST_ANDROID_ABIS-arm64-v8a,armeabi-v7a}"
case "$ANDROID_ABIS_RAW" in
arm64-v8a|armeabi-v7a|arm64-v8a,armeabi-v7a|armeabi-v7a,arm64-v8a) ;;
*)
echo "Error: SPOTIFLAC_RUST_ANDROID_ABIS must contain arm64-v8a and/or armeabi-v7a, comma-separated without spaces or duplicates." >&2
exit 1
;;
esac
export SPOTIFLAC_RUST_ANDROID_ABIS="$ANDROID_ABIS_RAW"
IFS=',' read -r -a ANDROID_ABIS <<< "$ANDROID_ABIS_RAW"
TARGET_PLATFORMS=()
for abi in "${ANDROID_ABIS[@]}"; do
case "$abi" in
arm64-v8a) TARGET_PLATFORMS+=(android-arm64) ;;
armeabi-v7a) TARGET_PLATFORMS+=(android-arm) ;;
esac
done
TARGET_PLATFORM_LIST="$(IFS=','; echo "${TARGET_PLATFORMS[*]}")"
if [[ "$PRODUCTION_BUILD" == "1" ]]; then
SDK_ROOT="$PROJECT_DIR/third_party/spotiflac_discord/sdk"
for sdk_file in discord_partner_sdk.aar notices/Discord-License-Notices.txt android/prefab/modules/discord_partner_sdk/include/discordpp.h; do
if [[ ! -f "$SDK_ROOT/$sdk_file" ]]; then
echo "Error: production requires the staged Discord SDK ($sdk_file). Run scripts/setup_discord_sdk.sh with the official SDK first." >&2
exit 1
fi
done
for abi in "${ANDROID_ABIS[@]}"; do
if [[ ! -f "$SDK_ROOT/android/jni/$abi/libdiscord_partner_sdk.so" ]]; then
echo "Error: production requires the Discord SDK library for $abi." >&2
exit 1
fi
done
fi
AUDIT_FLAGS=()
if [[ -f "$PROJECT_DIR/third_party/spotiflac_discord/sdk/discord_partner_sdk.aar" ]]; then
if [[ "${SPOTIFLAC_DISCORD_SDK:-1}" != "0" && -f "$PROJECT_DIR/third_party/spotiflac_discord/sdk/discord_partner_sdk.aar" ]]; then
AUDIT_FLAGS+=(--discord-sdk)
fi
cd "$PROJECT_DIR"
if [[ -x "$PROJECT_DIR/.fvm/flutter_sdk/bin/flutter" ]]; then
FLUTTER_COMMAND=("$PROJECT_DIR/.fvm/flutter_sdk/bin/flutter")
elif command -v fvm >/dev/null 2>&1; then
FLUTTER_COMMAND=(fvm flutter)
elif command -v flutter >/dev/null 2>&1; then
FLUTTER_COMMAND=(flutter)
else
echo "Error: install the Flutter version pinned in .fvmrc with FVM." >&2
exit 1
fi
PINNED_FLUTTER_VERSION="$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["flutter"])' "$PROJECT_DIR/.fvmrc")"
FLUTTER_VERSION="$("${FLUTTER_COMMAND[@]}" --version --machine | python3 -c 'import json, sys; print(json.load(sys.stdin)["frameworkVersion"])')"
if [[ "$FLUTTER_VERSION" != "$PINNED_FLUTTER_VERSION" ]]; then
echo "Error: Flutter $PINNED_FLUTTER_VERSION is required; found $FLUTTER_VERSION." >&2
exit 1
fi
BUILD_GIT_COMMIT="$(git rev-parse --short=8 HEAD)"
flutter build apk \
"${FLUTTER_COMMAND[@]}" build apk \
--release \
--split-per-abi \
--target-platform android-arm,android-arm64 \
--target-platform "$TARGET_PLATFORM_LIST" \
--dart-define="GIT_COMMIT=$BUILD_GIT_COMMIT" \
"$@"
${FLUTTER_ARGS[@]+"${FLUTTER_ARGS[@]}"}
for target in armeabi-v7a arm64-v8a universal; do
for target in "${ANDROID_ABIS[@]}" universal; do
apk="app-$target-release.apk"
abis="$target"
if [[ "$target" == "universal" ]]; then
apk=app-release.apk
abis=armeabi-v7a,arm64-v8a
abis="$ANDROID_ABIS_RAW"
fi
if [[ ! -f "$OUTPUT_DIR/$apk" ]]; then
echo "Error: expected APK was not created: $OUTPUT_DIR/$apk" >&2
exit 1
fi
python3 scripts/check_backend_apk.py "$OUTPUT_DIR/$apk" \
--backend rust --abis "$abis" "${AUDIT_FLAGS[@]}"
--backend rust --abis "$abis" ${AUDIT_FLAGS[@]+"${AUDIT_FLAGS[@]}"}
done
echo "Built Android release APKs in $OUTPUT_DIR"
+192
View File
@@ -0,0 +1,192 @@
"""Check release selection and audits without building APKs or deleting artifacts."""
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
FLUTTER_STUB = """import json, os, sys
from pathlib import Path
args = sys.argv[1:]
if Path(sys.argv[0]).name == 'fvm':
assert args.pop(0) == 'flutter'
with open(os.environ['BUILD_TEST_LOG'], 'a') as log:
log.write(json.dumps({'tool': Path(sys.argv[0]).name, 'args': args,
'sdk': os.environ.get('SPOTIFLAC_DISCORD_SDK', '1')}) + '\\n')
if args == ['--version', '--machine']:
print(json.dumps({'frameworkVersion': os.environ.get('BUILD_TEST_FLUTTER_VERSION', '3.47.5')}))
else:
assert args[:2] == ['build', 'apk']
output = Path('build/app/outputs/flutter-apk')
output.mkdir(parents=True, exist_ok=True)
for abi in os.environ['SPOTIFLAC_RUST_ANDROID_ABIS'].split(','):
(output / ('app-' + abi + '-release.apk')).write_bytes(b'fixture split')
(output / 'app-release.apk').write_bytes(b'fixture universal')
"""
PYTHON_STUB = """import json, os, sys
if sys.argv[1] == 'scripts/check_backend_apk.py':
with open(os.environ['BUILD_TEST_LOG'], 'a') as log:
log.write(json.dumps({'tool': 'audit', 'args': sys.argv[2:]}) + '\\n')
else:
os.execv(sys.executable, [sys.executable, *sys.argv[1:]])
"""
class AndroidBuildScriptTest(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.project = Path(directory.name)
(self.project / "scripts").mkdir()
shutil.copyfile(Path(__file__).with_name("build_android.sh"), self.project / "scripts/build_android.sh")
(self.project / ".fvmrc").write_text('{"flutter":"3.47.5"}')
sdk = self.project / "third_party/spotiflac_discord/sdk"
sdk.mkdir(parents=True)
(sdk / "discord_partner_sdk.aar").write_bytes(b"staged fixture")
for name in (
"notices/Discord-License-Notices.txt",
"android/prefab/modules/discord_partner_sdk/include/discordpp.h",
"android/jni/arm64-v8a/libdiscord_partner_sdk.so",
"android/jni/armeabi-v7a/libdiscord_partner_sdk.so",
):
path = sdk / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"staged fixture")
self.tools = self.project / "tools"
self.tools.mkdir()
self.log = self.project / "calls.jsonl"
self.env = os.environ.copy()
for key in ("SPOTIFLAC_DISCORD_SDK", "SPOTIFLAC_RUST_ANDROID_ABIS"):
self.env.pop(key, None)
self.env.update(PATH=str(self.tools) + os.pathsep + os.defpath, BUILD_TEST_LOG=str(self.log))
self.tool(self.tools / "python3", PYTHON_STUB)
git = self.tools / "git"
git.write_text("#!/bin/sh\necho 1234abcd\n")
git.chmod(0o755)
self.pinned = self.project / ".fvm/flutter_sdk/bin/flutter"
self.tool(self.pinned, FLUTTER_STUB)
def tool(self, path, source):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("#!" + sys.executable + "\n" + source)
path.chmod(0o755)
def run_build(self, *args):
return subprocess.run(
["/bin/bash", str(self.project / "scripts/build_android.sh"), *args],
cwd=self.project, env=self.env, text=True, capture_output=True,
)
def calls(self):
return [json.loads(line) for line in self.log.read_text().splitlines()]
def test_default_full_audits_two_splits_and_universal(self):
result = self.run_build("--verbose")
self.assertEqual(result.returncode, 0, result.stderr)
calls = self.calls()
build = next(call for call in calls if call["args"][:2] == ["build", "apk"])
self.assertIn("android-arm64,android-arm", build["args"])
self.assertIn("--verbose", build["args"])
audits = [call["args"] for call in calls if call["tool"] == "audit"]
self.assertEqual([Path(args[0]).name for args in audits], [
"app-arm64-v8a-release.apk", "app-armeabi-v7a-release.apk", "app-release.apk",
])
self.assertTrue(all("--discord-sdk" in args for args in audits))
self.assertEqual(audits[-1][audits[-1].index("--abis") + 1], "arm64-v8a,armeabi-v7a")
def test_lite_single_abi_ignores_and_preserves_previous_artifacts(self):
self.env["SPOTIFLAC_RUST_ANDROID_ABIS"] = "arm64-v8a"
output = self.project / "build/app/outputs/flutter-apk"
output.mkdir(parents=True)
previous = output / "app-armeabi-v7a-release.apk"
previous.write_bytes(b"previous build")
result = self.run_build("--lite")
self.assertEqual(result.returncode, 0, result.stderr)
calls = self.calls()
build = next(call for call in calls if call["args"][:2] == ["build", "apk"])
self.assertEqual(build["sdk"], "0")
self.assertNotIn("--lite", build["args"])
self.assertIn("android-arm64", build["args"])
audits = [call["args"] for call in calls if call["tool"] == "audit"]
self.assertEqual(len(audits), 2)
self.assertTrue(all("--discord-sdk" not in args for args in audits))
self.assertTrue(all(args[args.index("--abis") + 1] == "arm64-v8a" for args in audits))
self.assertEqual(previous.read_bytes(), b"previous build")
def test_explicit_sdk_disable_is_honored_without_lite_argument(self):
self.env["SPOTIFLAC_DISCORD_SDK"] = "0"
self.env["SPOTIFLAC_RUST_ANDROID_ABIS"] = "armeabi-v7a"
result = self.run_build()
self.assertEqual(result.returncode, 0, result.stderr)
audits = [call["args"] for call in self.calls() if call["tool"] == "audit"]
self.assertEqual(len(audits), 2)
self.assertTrue(all("--discord-sdk" not in args for args in audits))
self.assertTrue(all(args[args.index("--abis") + 1] == "armeabi-v7a" for args in audits))
def test_fvm_fallback_uses_pinned_version(self):
self.pinned.unlink()
self.tool(self.tools / "fvm", FLUTTER_STUB)
result = self.run_build()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(self.calls()[0]["tool"], "fvm")
def test_production_requires_full_even_with_inherited_sdk_disable(self):
self.env["SPOTIFLAC_DISCORD_SDK"] = "0"
result = self.run_build("--production")
self.assertEqual(result.returncode, 0, result.stderr)
calls = self.calls()
build = next(call for call in calls if call["args"][:2] == ["build", "apk"])
self.assertEqual(build["sdk"], "1")
self.assertNotIn("--production", build["args"])
self.assertTrue(all("--discord-sdk" in call["args"] for call in calls if call["tool"] == "audit"))
def test_production_rejects_lite_in_either_argument_order(self):
for arguments in (("--production", "--lite"), ("--lite", "--production")):
result = self.run_build(*arguments)
self.assertNotEqual(result.returncode, 0)
self.assertIn("cannot be combined", result.stderr)
self.assertFalse(self.log.exists())
def test_production_missing_sdk_components_stops_before_flutter(self):
sdk = self.project / "third_party/spotiflac_discord/sdk"
for name in (
"discord_partner_sdk.aar", "notices/Discord-License-Notices.txt",
"android/prefab/modules/discord_partner_sdk/include/discordpp.h",
"android/jni/arm64-v8a/libdiscord_partner_sdk.so",
"android/jni/armeabi-v7a/libdiscord_partner_sdk.so",
):
with self.subTest(component=name):
path = sdk / name
data = path.read_bytes()
path.unlink()
result = self.run_build("--production")
self.assertNotEqual(result.returncode, 0)
self.assertIn("production requires", result.stderr)
path.write_bytes(data)
self.assertFalse(self.log.exists())
def test_wrong_flutter_version_stops_before_build_or_audit(self):
self.env["BUILD_TEST_FLUTTER_VERSION"] = "0.0.0"
result = self.run_build()
self.assertNotEqual(result.returncode, 0)
self.assertIn("Flutter 3.47.5 is required", result.stderr)
self.assertEqual(len(self.calls()), 1)
def test_invalid_or_overridden_abis_stop_before_flutter(self):
for abis in ("", "x86_64", "arm64-v8a,arm64-v8a", "armeabi-v7a, arm64-v8a"):
with self.subTest(abis=abis):
self.env["SPOTIFLAC_RUST_ANDROID_ABIS"] = abis
self.assertNotEqual(self.run_build().returncode, 0)
self.env.pop("SPOTIFLAC_RUST_ANDROID_ABIS")
self.assertNotEqual(self.run_build("--target-platform=android-x64").returncode, 0)
self.assertFalse(self.log.exists())
if __name__ == "__main__":
unittest.main()