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

+16 -4
View File
@@ -50,6 +50,8 @@ jobs:
build-android:
runs-on: ubuntu-latest
needs: get-version
env:
SPOTIFLAC_DISCORD_SDK: "1"
steps:
- name: Free disk space
@@ -142,7 +144,7 @@ jobs:
- name: Build release APKs
run: |
bash scripts/build_android.sh --target lib/main.dart
bash scripts/build_android.sh --production --target lib/main.dart
ls -la build/app/outputs/flutter-apk/
- name: Sign and verify release APKs (V1/V2/V3/V4)
@@ -212,9 +214,9 @@ jobs:
run: |
apk_dir=build/app/outputs/flutter-apk
python3 scripts/check_backend_apk.py "$apk_dir/SpotiFLAC-${VERSION}-arm64.apk" \
--backend rust --abis arm64-v8a
--backend rust --abis arm64-v8a --discord-sdk
python3 scripts/check_backend_apk.py "$apk_dir/SpotiFLAC-${VERSION}-arm32.apk" \
--backend rust --abis armeabi-v7a
--backend rust --abis armeabi-v7a --discord-sdk
for abi in arm64 arm32; do
"$ANDROID_HOME/build-tools/37.0.0/zipalign" -c -P 16 4 \
"$apk_dir/SpotiFLAC-${VERSION}-${abi}.apk"
@@ -231,6 +233,8 @@ jobs:
build-ios:
runs-on: macos-15
needs: get-version # Only depends on version, NOT android build!
env:
SPOTIFLAC_DISCORD_SDK: "1"
steps:
- name: Checkout repository
@@ -283,7 +287,13 @@ jobs:
- name: Configure Rust application pods
working-directory: ios
run: pod install
run: |
test -f ../third_party/spotiflac_discord/ios/Frameworks/discord_partner_sdk.xcframework/Info.plist || {
echo "Production requires the official Discord SDK. Run scripts/setup_discord_sdk.sh in the build environment first." >&2
exit 1
}
test -f ../third_party/spotiflac_discord/ios/Frameworks/Discord-License-Notices.txt
pod install
- name: Build iOS (unsigned)
run: |
@@ -307,6 +317,8 @@ jobs:
- name: Audit Rust iOS archive before packaging
run: |
test -f ios/build/Runner.xcarchive/Products/Applications/Runner.app/Frameworks/discord_partner_sdk.framework/discord_partner_sdk
test -f ios/build/Runner.xcarchive/Products/Applications/Runner.app/Discord-License-Notices.txt
python3 -m unittest discover -s scripts -p 'test_check_backend_ios.py'
python3 scripts/check_backend_ios.py \
ios/build/Runner.xcarchive/Products/Applications/Runner.app \
+15
View File
@@ -67,6 +67,21 @@ Gradle builds the Rust native artifacts automatically. Tool versions are pinned
Start with the [Contributing Guide](CONTRIBUTING.md) for the development setup,
project boundaries, validation commands, and pull request checklist.
Build production APKs with `bash scripts/build_android.sh --production`;
this requires the staged Discord SDK and rejects Lite. CI releases use this
mode and require Discord on both Android and iOS. Stage the official SDK with
`scripts/setup_discord_sdk.sh` in the build environment before releasing;
the SDK is not committed to the repository. A fresh hosted runner must be
provided with the SDK before these release checks can pass.
For development release APKs, use `bash scripts/build_android.sh`; use `--lite` to omit
the optional Discord SDK while keeping playback and downloads. Full builds
remain the default. To build only ARM64, set
`SPOTIFLAC_RUST_ANDROID_ABIS=arm64-v8a`. The script uses the Flutter version in
`.fvmrc` and audits the selected split APKs and universal APK. For iOS Lite,
run CocoaPods and the app build with `SPOTIFLAC_DISCORD_SDK=0`; an existing Pods
installation must be regenerated with that setting.
For native iOS codec checks, see the [FFmpeg capability probe](scripts/README_ios_ffmpeg_capabilities.md).
---
+3 -2
View File
@@ -20,6 +20,7 @@ val rustAndroidAbis = providers.environmentVariable("SPOTIFLAC_RUST_ANDROID_ABIS
.get()
.split(",")
val supportedRustAndroidAbis = setOf("arm64-v8a", "armeabi-v7a")
val discordSdkEnabled = providers.environmentVariable("SPOTIFLAC_DISCORD_SDK").orElse("1").get() != "0"
require(rustAndroidAbis.size == rustAndroidAbis.toSet().size) {
"SPOTIFLAC_RUST_ANDROID_ABIS must not contain duplicate ABIs"
}
@@ -36,7 +37,7 @@ android {
buildConfig = true
}
packaging {
jniLibs.pickFirsts += "**/libdiscord_partner_sdk.so"
if (discordSdkEnabled) jniLibs.pickFirsts += "**/libdiscord_partner_sdk.so"
}
externalNativeBuild {
@@ -171,7 +172,7 @@ flutter {
dependencies {
val discordSdk = file("../../third_party/spotiflac_discord/sdk/discord_partner_sdk.aar")
if (discordSdk.exists()) {
if (discordSdkEnabled && discordSdk.exists()) {
implementation(files(discordSdk))
}
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
+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()
+11
View File
@@ -23,6 +23,17 @@ in Android assets and iOS resources. Without these files the app still builds, b
the presence setting reports that the SDK is unavailable. A clean clone alone
does not produce a Discord-enabled build.
To deliberately omit a staged SDK, set `SPOTIFLAC_DISCORD_SDK=0` during Android
builds or iOS `pod install` and app builds. `bash scripts/build_android.sh --lite`
sets this flag for Android. The bridge remains registered and reports presence
as unavailable; the staged SDK files are retained for subsequent full builds.
Production Android builds use `bash scripts/build_android.sh --production`,
which forces Discord on, checks the staged SDK and audits it in every APK ABI.
`--production --lite` is rejected. Release CI requires the SDK on both platforms
and stops before publication if the SDK is absent. Provision the official SDK
in the build environment before running that workflow.
Android uses the installed Discord app's signed-in session (Social SDK 1.10+).
The AAR and CMake use the same staged native library; the host packages one copy
with `jniLibs.pickFirsts`. Microphone/Bluetooth permissions and the SDK voice
+2 -1
View File
@@ -1,11 +1,12 @@
cmake_minimum_required(VERSION 3.22.1)
project(spotiflac_discord LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
option(SPOTIFLAC_DISCORD_SDK "Include the staged Discord Social SDK" ON)
add_library(spotiflac_discord SHARED src/main/cpp/presence.cpp)
target_compile_options(spotiflac_discord PRIVATE -Wall -Wextra -Werror -fvisibility=hidden)
target_link_options(spotiflac_discord PRIVATE "-Wl,-z,max-page-size=16384")
set(SDK "${CMAKE_CURRENT_SOURCE_DIR}/../sdk/android")
if(EXISTS "${SDK}/jni/${ANDROID_ABI}/libdiscord_partner_sdk.so")
if(SPOTIFLAC_DISCORD_SDK AND EXISTS "${SDK}/jni/${ANDROID_ABI}/libdiscord_partner_sdk.so")
add_library(discord_partner_sdk SHARED IMPORTED)
set_target_properties(discord_partner_sdk PROPERTIES IMPORTED_LOCATION
"${SDK}/jni/${ANDROID_ABI}/libdiscord_partner_sdk.so")
+7 -2
View File
@@ -4,13 +4,18 @@ version '1.0'
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
def discordSdkEnabled = System.getenv('SPOTIFLAC_DISCORD_SDK') != '0'
android {
namespace 'com.zarz.spotiflac.discord'
compileSdk 37
defaultConfig {
minSdk 24
externalNativeBuild {
cmake { arguments '-DANDROID_STL=c++_static' }
cmake {
arguments '-DANDROID_STL=c++_static',
"-DSPOTIFLAC_DISCORD_SDK=${discordSdkEnabled ? 'ON' : 'OFF'}"
}
}
}
compileOptions {
@@ -18,7 +23,7 @@ android {
targetCompatibility JavaVersion.VERSION_25
}
kotlinOptions { jvmTarget = '25' }
sourceSets.main.assets.srcDir('../sdk/notices')
if (discordSdkEnabled) sourceSets.main.assets.srcDir('../sdk/notices')
externalNativeBuild {
cmake { path 'CMakeLists.txt'; version '3.22.1' }
}
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
s.dependency 'Flutter'
s.platform = :ios, '16.0'
sdk = 'Frameworks/discord_partner_sdk.xcframework'
if File.directory?(File.join(__dir__, sdk))
if ENV['SPOTIFLAC_DISCORD_SDK'] != '0' && File.directory?(File.join(__dir__, sdk))
s.vendored_frameworks = sdk
s.resources = 'Frameworks/Discord-License-Notices.txt'
s.pod_target_xcconfig = {