# Building and testing SimplyBeautyKit Every command below runs from the repository root unless it starts with `cd`. Results quoted are from the 0.2.0 verification run (HEAD `9382714`, macOS 15.6 on an M1 Max, Xcode 26.3, CMake 4.4.3, JDK 21). ## Prerequisites | Area | Needed | |---|---| | Core | CMake ≥ 3.22, Clang or GCC with C++17 | | Android | Android SDK; **NDK 27.1.12297006** and CMake 3.22.1 from the SDK; JDK 17+; the Gradle **8.11.1 wrapper** (`android/gradlew`). Never use a system Gradle 9.x: AGP 8.7.2 needs Gradle 8 | | iOS | Xcode 15+ (SwiftPM tools 5.9; the LiveKit iOS sample needs Xcode 16.3+), `xcodegen` for the demo apps, CocoaPods only for `pod lib lint` | | Tools | Python 3.9+; Python 3.9–3.12 for anything that imports `mediapipe==0.10.14` (eval, gallery landmarks, NN reference data) | Set `export PYTHONDONTWRITEBYTECODE=1` before running the Python tools: six `__pycache__/*.pyc` files are tracked in git and would otherwise show up as modified. ## Models ```bash scripts/download_models.sh # core/models/, pinned SHA-256, skips files that match scripts/download_models.sh --check # verify only (no network); exit 1 on a mismatch scripts/sync_models.sh # copy core/models + core/assets/masks into ios/Resources/ scripts/sync_models.sh --check # CI: copies are present and identical scripts/pack_segmenter_task.sh # image_segmenter.tflite -> image_segmenter.task (reproducible STORED zip) ``` | File | Size | Used by | |---|---|---| | `face_landmarker.task` | 3,758,596 B | built-in face tracker (MediaPipe FaceLandmarker, float16) | | `image_segmenter.tflite` | 249,537 B | built-in segmenter (MediaPipe selfie segmenter, float16) | | `image_segmenter.task` | 249,681 B | the same `.tflite` in a STORED zip; loaded only from a `model_dir` without a `.tflite` (the segmenter tries `image_segmenter.tflite` first) | The Android `lib` AAR ships no models; the `models` module packs `face_landmarker.task`, `image_segmenter.tflite` and `core/assets/masks` into its assets at build time (not the `.task` copy of the segmenter). The iOS resource bundle (`scripts/sync_models.sh`) carries all three model files. `sbnn` reads only STORED zip entries, so a DEFLATE `.task` fails with `ModelMissing`. ## 1. Core (host) ```bash cmake -S core -B core/build-cmake -DCMAKE_BUILD_TYPE=Release \ -DSB_BUILD_TESTS=ON -DSB_BUILD_DESKTOP_DEMO=ON cmake --build core/build-cmake -j ctest --test-dir core/build-cmake --output-on-failure # 15/15 on macOS core/build-cmake/sb_desktop_demo # SBStudio end to end ``` CMake options: | Option | Default | Meaning | |---|---|---| | `SB_RENDERER` | `NONE` | `NONE` (CPU), `GLES` (Android), `METAL` (Apple) | | `SB_BUILD_TESTS` | OFF | registers the `ctest` suites below | | `SB_BUILD_DESKTOP_DEMO` | OFF | `examples/desktop` console demo | | `SB_BUILD_BENCH`, `SB_BUILD_EVAL`, `SB_BUILD_GALLERY` | OFF | host tools (`tools/`), never part of the packages | | `SB_FUZZ` | `OFF` | `standalone` or `libfuzzer`: fuzz targets (see below) | | `SB_FP_CONTRACT_OFF` | OFF | no FMA contraction: bit-identical output on arm64 and x86_64 (test/CI only; slower) | | `SB_INSTALL` | ON when top level | install/export rules | | `SB_SWIFT_CONSUMER_TEST` | OFF | Swift package consumer of the Metal category (about a minute) | | `SB_ANGLE_DIR`, `SB_GLES_HEADERS` | – | Metal build only: compare Metal with the production GLES renderer through ANGLE | The default build type is Release (the CPU pipeline is 5–50× slower at -O0). Test suites (`ctest -N` lists them): `sb_tests` (frame, effects, studio), `sb_graph_tests` (effect graph, reshape locality/folds, determinism, allocations), `sb_engine_tests` (API contract, formats, rotation, events, quality, injection), `sb_thread_tests` (threading contract), `sb_nn_tests` (interpreter, tracker, segmenter), `sb_hardening_tests` (NaN setters, decode bombs, zero allocations per frame), `sb_integration_tests` (real photos), `sb_gles_tests` (GLES renderer vs the CPU graph: reshape, makeup, teeth, eye-bright, GL state, allocations; renders through ANGLE on SwiftShader, found in the Android SDK automatically; override with `-DSB_ANGLE_DIR`/`-DSB_GLES_HEADERS`, turn off with `-DSB_GLES_TEST_AUTODETECT=OFF`, which leaves only the GL-free cases; `SB_GLES_TIMING_DETAIL=1` prints per-category timings), `test_build`, `sb_jni_tests` (JNI bridge on the host through a fake `JNIEnv`; needs a JDK's `jni.h`), `sb_objc_bridge_tests` and `sb_metal_bridge_tests` (macOS), `build_no_backend_defines`, `build_no_host_paths`, `build_install_consumer`. Test data (not committed; tests that need it report SKIP): ```bash tools/eval/fetch_testdata.sh build/testdata # 4 MediaPipe test images, SHA-256 checked SB_TEST_IMAGES=$PWD/build/testdata ctest --test-dir core/build-cmake --output-on-failure # sb_gles_tests' real-portrait cases need both SB_TEST_IMAGES (portrait.jpg) and # SB_TEST_DATA (e2e.portrait.jpg.face0.f32 from download_test_data.sh --e2e) scripts/download_test_data.sh --venv build/venv-ref --tensors --e2e # LiteRT/MediaPipe reference tensors SB_TEST_DATA=$PWD/core/tests/data core/build-cmake/sb_nn_tests # 0 skips with full data ``` ### Sanitizers ```bash # ASan + UBSan (CI job): 13/13 before sb_gles_tests; 15/15 on macOS with it cmake -S core -B core/build-asan -DCMAKE_BUILD_TYPE=RelWithDebInfo -DSB_BUILD_TESTS=ON \ "-DCMAKE_CXX_FLAGS=-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined" cmake --build core/build-asan -j && ctest --test-dir core/build-asan --output-on-failure # TSan: 11/11 (not yet a CI job) cmake -S core -B core/build-tsan -DCMAKE_BUILD_TYPE=RelWithDebInfo -DSB_BUILD_TESTS=ON \ -DCMAKE_CXX_FLAGS=-fsanitize=thread cmake --build core/build-tsan -j && ctest --test-dir core/build-tsan --output-on-failure ``` ### Metal (macOS host) ```bash cmake -S core -B core/build-metal -DSB_RENDERER=METAL -DSB_BUILD_TESTS=ON cmake --build core/build-metal -j && ctest --test-dir core/build-metal --output-on-failure # 16/16 # optional: Metal vs the real GLES renderer through the emulator's ANGLE cmake -S core -B core/build-metal -DSB_RENDERER=METAL -DSB_BUILD_TESTS=ON \ -DSB_ANGLE_DIR=$HOME/Library/Android/sdk/emulator/lib64/gles_angle \ -DSB_GLES_HEADERS=$HOME/Library/Android/sdk/ndk/27.1.12297006/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include # optional: a Swift package that calls the Metal category cmake -S core -B core/build-metal -DSB_RENDERER=METAL -DSB_BUILD_TESTS=ON -DSB_SWIFT_CONSUMER_TEST=ON ctest --test-dir core/build-metal -R sb_metal_swift_consumer --output-on-failure ``` `sb_metal_validation` reruns part of `sb_metal_tests` under `MTL_DEBUG_LAYER=1 MTL_DEBUG_LAYER_ERROR_MODE=assert`. Tests exit 77 (skip) without a Metal device. ### Same pixels on arm64 and x86_64 ```bash cmake -S core -B core/build-universal -DCMAKE_BUILD_TYPE=Release -DSB_BUILD_TESTS=ON \ -DSB_FP_CONTRACT_OFF=ON "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" cmake --build core/build-universal -j --target test_build ctest --test-dir core/build-universal -R build_cross_arch_identical --output-on-failure # needs Rosetta ``` ### Install and consume from CMake ```bash cmake --install core/build-cmake --prefix "$PWD/build/sb-install" ``` ```cmake find_package(simplybeauty REQUIRED) # CMAKE_PREFIX_PATH= target_link_libraries(app PRIVATE simplybeauty::simplybeauty) ``` The CMake package version comes from `sb_version.h` (0.2.0), so `find_package(simplybeauty 0.2 REQUIRED)` works (SameMinorVersion). ## 2. Fuzzing ```bash cmake -S core/fuzz -B build-fuzz -DSB_FUZZ=standalone # Apple clang: bundled driver, ASan+UBSan cmake --build build-fuzz -j ctest --test-dir build-fuzz --output-on-failure # 13/13: seeds + all 24 recorded reproducers core/fuzz/run_fuzzers.sh build-fuzz # 200k iterations per target A2=1 core/fuzz/run_fuzzers.sh build-fuzz fuzz_filter # criterion A2: >= 1M executions, strict # Linux / CI (libFuzzer): CC=clang CXX=clang++ cmake -S core/fuzz -B build-fuzz -G Ninja -DSB_FUZZ=libfuzzer # UBSan inside stb_image too: cmake -S core/fuzz -B build-fuzz-tp -DSB_FUZZ=standalone -DSB_FUZZ_IGNORE_THIRD_PARTY=OFF ``` Targets: `fuzz_filter`, `fuzz_background`, `fuzz_process`, `fuzz_inject`, `fuzz_studio_params`, `fuzz_model`, `fuzz_track`. Triage and minimisation: `core/fuzz/minimize.py`; details in `core/fuzz/README.md`. ## 3. Host tools ```bash # Benchmark (standalone project; adds core/ itself) cmake -S tools/bench -B build/bench -DCMAKE_BUILD_TYPE=Release -DSB_BENCH_TESTS=ON cmake --build build/bench -j && (cd build/bench && ctest --output-on-failure) build/bench/sb_bench --list build/bench/sb_bench --models core/models --input build/testdata/portrait.jpg --json out.json build/bench/sb_bench --soak 10 --soak-max-allocs-per-frame 0 # memory + allocation soak python3 tools/bench/perf_gate.py r1.json r2.json r3.json --baseline tools/bench/baseline_host.json \ --metric cpu --tolerance 10% --require-all # Parity harness vs MediaPipe (B2-B6) python3 -m pip install -r tools/eval/requirements.txt python3.11 -m venv build/venv-mp && build/venv-mp/bin/pip install -r tools/eval/requirements-mediapipe.txt tools/eval/run_eval.sh --testdata build/testdata --mp-python build/venv-mp/bin/python # build/eval/report/ # Visual gallery (H1/H2) PYTHON=python3.11 tools/gallery/run_gallery.sh # build/gallery/gallery/index.html ``` Methodology and results: [PERFORMANCE.md](PERFORMANCE.md). ## 4. Android ```bash cd android echo "sdk.dir=$HOME/Library/Android/sdk" > local.properties # once ./gradlew :lib:assembleRelease :models:assembleRelease :demo:assembleDebug ./gradlew :lib:testDebugUnitTest :models:testDebugUnitTest :demo:testDebugUnitTest ./gradlew :lib:apiCheck :models:apiCheck # public API vs android/{lib,models}/api/*.api ./gradlew :lib:apiDump :models:apiDump # only for an intended API change cd .. scripts/check_android_so.sh android/lib/build/outputs/aar/lib-release.aar \ android/demo/build/outputs/apk/debug/demo-debug.apk # 16 KB LOAD alignment, JNI-only exports, static libc++ scripts/build_aar.sh # lib-release.aar + the .so check ``` | Output | Size (0.2.0) | |---|---| | `android/lib/build/outputs/aar/lib-release.aar` | 1,421,940 B (arm64-v8a, armeabi-v7a, x86_64; no assets) | | `android/models/build/outputs/aar/models-release.aar` | 3,600,082 B | | `android/demo/build/outputs/apk/debug/demo-debug.apk` | 13,054,830 B | Build facts: compileSdk 35, minSdk 24, JVM target 17, AGP 8.7.2, Kotlin 2.0.21, `-DSB_RENDERER=GLES -DANDROID_STL=c++_static -DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON`; Release is `-O3`, and Debug builds of the native core are `-O2` too (`-Psimplybeauty.nativeDebugO0=true` for -O0). If the Gradle daemon dies mid-build, rerun; if the Kotlin DSL cache breaks: `./gradlew --stop && rm -rf .gradle`. ### Instrumented tests ```bash cd android ./gradlew :lib:assembleDebugAndroidTest ANDROID_SERIAL=emulator-5554 ./gradlew :lib:connectedDebugAndroidTest # 56 tests ``` Library tests run headless (no activity). `:demo:connectedDebugAndroidTest` launches the demo activity: run it on emulators only, never on a phone someone is using. With several devices attached always set `ANDROID_SERIAL`. Emulators used for 0.2.0 (arm64 images, `-gpu swiftshader_indirect`): `SB_API24_Old` (7.0), `SB_API30`, `SB_API34`, `SB_API36g` (`android-36;google_apis`). The `android-36.1;google_apis_playstore` image gets no HVF on the M1 host and does not boot. ```bash $HOME/Library/Android/sdk/emulator/emulator -avd SB_API34 -no-window -no-audio -no-snapshot -no-boot-anim \ -gpu swiftshader_indirect -port 5600 & adb -s emulator-5600 wait-for-device ``` ### Benchmark on a device (headless) ```bash scripts/bench_android.sh -s --models core/models \ --input build/testdata/portrait.jpg -- --iters 30 --warmup 5 scripts/bench_android.sh -s --detach -- --soak 30 --fps 30 \ --soak-thermal-steady --soak-max-over-budget-pct 5 # C4 ``` The script only pushes to and runs under `/data/local/tmp/sb_bench`; it never launches an app. Wireless adb serials contain spaces: quote them. ## 5. iOS ```bash xcodebuild -scheme SimplyBeauty -destination 'generic/platform=iOS Simulator' build xcodebuild -scheme SimplyBeauty -destination 'generic/platform=iOS' build scripts/build_ios_xcframework.sh pod lib lint SimplyBeauty.podspec --platforms=ios --allow-warnings pod lib lint SimplyBeauty.podspec --platforms=ios --allow-warnings --use-libraries ``` - The Swift package is the root `Package.swift` (tools 5.9): targets `SimplyBeautyCore` (C++ + Metal renderer, `SB_RENDERER_METAL=1`) and `SimplyBeauty` (ObjC++ bridge + resource bundle `SimplyBeauty_SimplyBeauty.bundle` with `models/`, `masks/` and `PrivacyInfo.xcprivacy`). No `unsafeFlags`, so it works as a versioned remote dependency. - `scripts/build_ios_xcframework.sh` writes `build/SimplyBeauty.xcframework` (ios-arm64 + ios-arm64_x86_64-simulator, `-O3`, Metal renderer; core and ObjC++ bridge in one static library), `SimplyBeauty.xcframework.zip` (9,885,141 B with `-g`; 1,752,313 B with `SB_XCF_DEBUG_INFO=0`, device slice 1,375,856 B) with its `.checksum` for `.binaryTarget(url:checksum:)`, and `SimplyBeauty_SimplyBeauty.bundle(.zip)` with `models/`, `masks/` and `PrivacyInfo.xcprivacy`. Apps that link the XCFramework must embed that bundle (or add `ios/Resources/PrivacyInfo.xcprivacy` themselves): a static library cannot ship the privacy manifest. `SB_XCF_DEBUG_INFO=0` drops `-g`; `SB_XCF_OUT` changes the output dir. - The release zip (with `LICENSE` and `NOTICE` inside the `.xcframework`): `tools/release/xcframework_checksum.sh build/SimplyBeauty.xcframework dist/SimplyBeauty-X.Y.Z.xcframework.zip` ([RELEASING.md](RELEASING.md)). - Binary target from `Package.swift`: set `SIMPLYBEAUTY_BINARY_URL` + `SIMPLYBEAUTY_BINARY_CHECKSUM`, or `SIMPLYBEAUTY_BINARY_PATH` (a local `.xcframework` inside the package), in the environment of the resolve. - CocoaPods: `pod 'SimplyBeauty', :path => ''`. Demo and LiveKit sample: ```bash cd examples/ios-demo && xcodegen generate xcodebuild -project SimplyBeautyDemo.xcodeproj -scheme SimplyBeautyDemo \ -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build TEST_RUNNER_SB_TEST_IMAGES=$PWD/../../build/testdata xcodebuild test \ -project SimplyBeautyDemo.xcodeproj -scheme SimplyBeautyDemo \ -destination 'platform=iOS Simulator,name=iPhone 17' ``` Signing for a device: `examples/ios-demo/README.md`. Release configuration matters: SwiftPM builds the core with the app's configuration. ## 6. LiveKit samples ```bash cd examples/livekit-android echo "sdk.dir=$HOME/Library/Android/sdk" > local.properties ./gradlew --no-daemon :app:assembleDebug :app:testDebugUnitTest # 10 JVM tests ANDROID_SERIAL=emulator-5600 ./gradlew --no-daemon :app:connectedDebugAndroidTest # 4 tests cd ../livekit-ios && xcodegen generate xcodebuild -project SimplyBeautyLiveKit.xcodeproj -scheme SimplyBeautyLiveKit \ -destination 'generic/platform=iOS Simulator' -derivedDataPath build/dd build ``` Server setup and end-to-end runs: [LIVEKIT.md](LIVEKIT.md). ## 7. Release and compliance checks ```bash export PYTHONDONTWRITEBYTECODE=1 python3 tools/release/preflight.py # 11 pass, 1 warn (SECURITY.md contact), 0 fail python3 tools/license/add_spdx.py --check # SPDX headers python3 tools/release/privacy_scan.py --json build/privacy-scan.json python3 -m pip install -r tools/release/requirements.txt python3 tools/release/sbom.py --strict --validate -o build/sbom.cdx.json tools/release/tests/run_tests.sh ``` Tagging, publishing and credentials: [RELEASING.md](RELEASING.md). ## 8. CI | Workflow | What it runs | |---|---| | `ci.yml` | host Release + ASan/UBSan + cross-arch + Metal configure (`test_build`); Android build, JVM tests, `apiCheck`, `.so` and asset checks; SwiftPM, iOS demo, XCFramework, `pod lib lint`; both LiveKit samples; shellcheck + model checksums | | `fuzz.yml` | seed and regression replay on PRs; nightly libFuzzer, 1M executions per target | | `bench.yml` | `sb_bench` ×3, allocation gate (blocking), +10 % timing gate (blocking only with `SB_BENCH_GATE_BLOCKING=true`), 2-minute soak | | `eval.yml` | eval unit tests + B2/B4 parity vs MediaPipe | | `gallery.yml` | gallery render, golden compare, H2 gate | | `compliance.yml` | SPDX, privacy manifest, SBOM, preflight | | `release.yml` | tag-triggered: AAR, XCFramework zip + SwiftPM checksum, SBOM and SHA256SUMS for a GitHub Release; lib and models AARs to Maven Central when signing secrets exist (the iOS resource bundle is not attached yet) | No GitHub run of these workflows is recorded yet (the repository URL in the build files is still a placeholder). The commands in sections 1–7 were run locally for 0.2.0; the nightly 1M-execution fuzz job and the release workflow were not.