diff --git a/.github/workflows/flutter-package.yml b/.github/workflows/flutter-package.yml new file mode 100644 index 0000000..e064da7 --- /dev/null +++ b/.github/workflows/flutter-package.yml @@ -0,0 +1,82 @@ +name: publish flutter package +on: + push: + tags: + - '*.*.*' + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-22.04 + name: publish to pub.dev + + steps: + + - uses: actions/checkout@v4.2.2 + + - name: download release assets + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "VERSION=$VERSION" >> $GITHUB_ENV + + mkdir -p artifacts + cd artifacts + + # Download all platform binaries from the GitHub release + gh release download "$VERSION" --pattern "adam-*.tar.gz" + + # Extract all archives + for archive in adam-*.tar.gz; do + name=$(basename "$archive" "-$VERSION.tar.gz") + mkdir -p "$name" + tar -xzf "$archive" -C "$name" + rm "$archive" + done + + ls -la + env: + GH_TOKEN: ${{ github.token }} + + - uses: dart-lang/setup-dart@v1.7.1 + + - name: assemble and publish flutter package + run: | + FLUTTER_DIR=packages/flutter + + # Android (only arm64 and x64, no arm) + mkdir -p $FLUTTER_DIR/native_libraries/android + cp artifacts/adam-android-arm64-v8a/adam.so $FLUTTER_DIR/native_libraries/android/adam_android_arm64.so + cp artifacts/adam-android-x86_64/adam.so $FLUTTER_DIR/native_libraries/android/adam_android_x64.so + + # iOS device + mkdir -p $FLUTTER_DIR/native_libraries/ios + cp artifacts/adam-ios/adam.dylib $FLUTTER_DIR/native_libraries/ios/adam_ios_arm64.dylib + + # iOS simulator (keep universal/fat binary as-is) + mkdir -p $FLUTTER_DIR/native_libraries/ios-sim + cp artifacts/adam-ios-sim/adam.dylib $FLUTTER_DIR/native_libraries/ios-sim/adam_ios-sim.dylib + + # macOS (separate arch-specific dylibs) + mkdir -p $FLUTTER_DIR/native_libraries/mac + cp artifacts/adam-macos-arm64/adam.dylib $FLUTTER_DIR/native_libraries/mac/adam_mac_arm64.dylib + cp artifacts/adam-macos-x86_64/adam.dylib $FLUTTER_DIR/native_libraries/mac/adam_mac_x64.dylib + + # Linux + mkdir -p $FLUTTER_DIR/native_libraries/linux + cp artifacts/adam-linux-cpu-x86_64/adam.so $FLUTTER_DIR/native_libraries/linux/adam_linux_x64.so + cp artifacts/adam-linux-cpu-arm64/adam.so $FLUTTER_DIR/native_libraries/linux/adam_linux_arm64.so + + # Windows + mkdir -p $FLUTTER_DIR/native_libraries/windows + cp artifacts/adam-windows-cpu-x86_64/adam.dll $FLUTTER_DIR/native_libraries/windows/adam_windows_x64.dll + + # Update version + sed -i "s/^version: .*/version: $VERSION/" $FLUTTER_DIR/pubspec.yaml + + # Publish to pub.dev + cd $FLUTTER_DIR + dart pub get + dart pub publish --force diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..0277a71 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,672 @@ +name: Build, Test and Release +on: + push: + branches: + - '**' + tags-ignore: + - '**' + workflow_dispatch: + +permissions: + contents: write + id-token: write + +env: + GGUF_MODEL_DIR: tests/models/unsloth/gemma-3-270m-it-GGUF + GGUF_MODEL_NAME: gemma-3-270m-it-UD-IQ2_M.gguf + GGUF_MODEL_URL: https://huggingface.co/unsloth/gemma-3-270m-it-GGUF/resolve/main/gemma-3-270m-it-UD-IQ2_M.gguf + WHISPER_MODEL_DIR: tests/models/ggerganov/whisper-tiny + WHISPER_MODEL_NAME: ggml-tiny.bin + WHISPER_MODEL_URL: https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin + AUDIO_TEST_DIR: tests/audio + AUDIO_TEST_WAV: tests/audio/jfk.wav + AUDIO_TEST_WAV_URL: https://github.com/ggml-org/whisper.cpp/raw/master/samples/jfk.wav + +jobs: + download-models: + if: ${{ !contains(github.event.head_commit.message, '[auto-update]') }} + outputs: + gguf-cache-key: gguf-${{ steps.meta.outputs.gguf-hash }} + gguf-model-path: ${{ env.GGUF_MODEL_DIR }}/${{ env.GGUF_MODEL_NAME }} + whisper-cache-key: whisper-${{ steps.meta.outputs.whisper-hash }} + whisper-model-path: ${{ env.WHISPER_MODEL_DIR }}/${{ env.WHISPER_MODEL_NAME }} + audio-cache-key: audio-${{ steps.meta.outputs.audio-hash }} + audio-test-path: ${{ env.AUDIO_TEST_WAV }} + name: Download models and test assets + runs-on: ubuntu-22.04 + steps: + - name: Compute URL hashes + id: meta + run: | + if command -v sha256sum >/dev/null 2>&1; then + gguf_hash=$(echo -n "${{ env.GGUF_MODEL_URL }}" | sha256sum | cut -d' ' -f1) + whisper_hash=$(echo -n "${{ env.WHISPER_MODEL_URL }}" | sha256sum | cut -d' ' -f1) + audio_hash=$(echo -n "${{ env.AUDIO_TEST_WAV_URL }}" | sha256sum | cut -d' ' -f1) + else + gguf_hash=$(echo -n "${{ env.GGUF_MODEL_URL }}" | shasum -a 256 | cut -d' ' -f1) + whisper_hash=$(echo -n "${{ env.WHISPER_MODEL_URL }}" | shasum -a 256 | cut -d' ' -f1) + audio_hash=$(echo -n "${{ env.AUDIO_TEST_WAV_URL }}" | shasum -a 256 | cut -d' ' -f1) + fi + echo "gguf-hash=$gguf_hash" >> "$GITHUB_OUTPUT" + echo "whisper-hash=$whisper_hash" >> "$GITHUB_OUTPUT" + echo "audio-hash=$audio_hash" >> "$GITHUB_OUTPUT" + + - name: Prepare directories + run: | + mkdir -p "${{ env.GGUF_MODEL_DIR }}" + mkdir -p "${{ env.WHISPER_MODEL_DIR }}" + mkdir -p "${{ env.AUDIO_TEST_DIR }}" + + - name: Restore GGUF cache + id: cache-gguf + uses: actions/cache@v4 + with: + path: ${{ env.GGUF_MODEL_DIR }}/${{ env.GGUF_MODEL_NAME }} + key: gguf-${{ steps.meta.outputs.gguf-hash }} + + - name: Download GGUF model + if: steps.cache-gguf.outputs.cache-hit != 'true' + run: curl -L --fail --retry 3 "${{ env.GGUF_MODEL_URL }}" -o "${{ env.GGUF_MODEL_DIR }}/${{ env.GGUF_MODEL_NAME }}" + + - name: Verify GGUF model + run: test -f "${{ env.GGUF_MODEL_DIR }}/${{ env.GGUF_MODEL_NAME }}" + + - name: Restore Whisper cache + id: cache-whisper + uses: actions/cache@v4 + with: + path: ${{ env.WHISPER_MODEL_DIR }}/${{ env.WHISPER_MODEL_NAME }} + key: whisper-${{ steps.meta.outputs.whisper-hash }} + + - name: Download Whisper model + if: steps.cache-whisper.outputs.cache-hit != 'true' + run: curl -L --fail --retry 3 "${{ env.WHISPER_MODEL_URL }}" -o "${{ env.WHISPER_MODEL_DIR }}/${{ env.WHISPER_MODEL_NAME }}" + + - name: Verify Whisper model + run: test -f "${{ env.WHISPER_MODEL_DIR }}/${{ env.WHISPER_MODEL_NAME }}" + + - name: Restore audio test file cache + id: cache-audio + uses: actions/cache@v4 + with: + path: ${{ env.AUDIO_TEST_WAV }} + key: audio-${{ steps.meta.outputs.audio-hash }} + + - name: Download audio test file + if: steps.cache-audio.outputs.cache-hit != 'true' + run: curl -L --fail --retry 3 "${{ env.AUDIO_TEST_WAV_URL }}" -o "${{ env.AUDIO_TEST_WAV }}" + + - name: Verify audio test file + run: test -f "${{ env.AUDIO_TEST_WAV }}" + + build: + if: ${{ !contains(github.event.head_commit.message, '[auto-update]') }} + needs: download-models + runs-on: ${{ matrix.os }} + container: ${{ matrix.container && matrix.container || '' }} + name: ${{ matrix.name }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }} build${{ matrix.arch != 'arm64-v8a' && matrix.name != 'ios-sim' && matrix.name != 'ios' && matrix.name != 'apple-xcframework' && matrix.name != 'android-aar' && ' + test' || ''}} + timeout-minutes: 120 + env: + # SANITIZE=0 disables -fsanitize=address,undefined for the test_adam + # binary. Libasan/libubsan aren't shipped in Alpine musl or MinGW, + # and ASan + cross-arch emulators tend to fight. Local dev keeps + # sanitizers on by default; CI keeps the test fast and portable. + SANITIZE: 0 + strategy: + fail-fast: false + matrix: + include: + - os: macos-15 + name: macos + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_METAL=ON -DGGML_ACCELERATE=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=Apple" WHISPER="-DWHISPER_COREML=ON -DWHISPER_COREML_ALLOW_FALLBACK=ON" + - os: macos-15 + arch: x86_64 + name: macos + make: ARCH=x86_64 LLAMA="-DGGML_NATIVE=OFF -DGGML_METAL=ON -DGGML_ACCELERATE=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=Apple" WHISPER="-DWHISPER_COREML=ON -DWHISPER_COREML_ALLOW_FALLBACK=ON" + - os: macos-15 + arch: arm64 + name: macos + make: ARCH=arm64 LLAMA="-DGGML_NATIVE=OFF -DGGML_METAL=ON -DGGML_ACCELERATE=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=Apple" WHISPER="-DWHISPER_COREML=ON -DWHISPER_COREML_ALLOW_FALLBACK=ON" + - os: ubuntu-22.04 + arch: x86_64 + name: linux-cpu + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_AVX2=ON" + - os: ubuntu-22.04 + arch: x86_64 + name: linux-gpu + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_VULKAN=ON -DGGML_OPENCL=ON" + - os: ubuntu-22.04-arm + arch: arm64 + name: linux-cpu + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_CPU_ARM_ARCH=armv8.2-a" + - os: ubuntu-22.04-arm + arch: arm64 + name: linux-gpu + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_VULKAN=ON -DGGML_OPENCL=ON" + - os: ubuntu-22.04 + arch: x86_64 + name: linux-musl-cpu + container: alpine:latest + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_AVX2=ON" + - os: ubuntu-22.04-arm + arch: arm64 + name: linux-musl-cpu + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_CPU_ARM_ARCH=armv8.2-a" + - os: windows-2022 + arch: x86_64 + name: windows-cpu + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_AVX2=ON" + - os: windows-2022 + arch: x86_64 + name: windows-gpu + make: LLAMA="-DGGML_NATIVE=OFF -DGGML_CPU=ON -DGGML_VULKAN=ON -DGGML_OPENCL=ON" + - os: ubuntu-22.04 + arch: x86_64 + name: android + make: PLATFORM=android ARCH=x86_64 + sqlite-amalgamation-zip: https://sqlite.org/2025/sqlite-amalgamation-3490100.zip + - os: ubuntu-22.04 + arch: arm64-v8a + name: android + make: PLATFORM=android ARCH=arm64-v8a + - os: macos-15 + name: ios + make: PLATFORM=ios LLAMA="-DGGML_NATIVE=OFF -DGGML_METAL=ON -DGGML_ACCELERATE=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=Apple" WHISPER="-DWHISPER_COREML=ON -DWHISPER_COREML_ALLOW_FALLBACK=ON" + - os: macos-15 + name: ios-sim + make: PLATFORM=ios-sim LLAMA="-DGGML_NATIVE=OFF -DGGML_METAL=ON -DGGML_ACCELERATE=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=Apple" WHISPER="-DWHISPER_COREML=ON -DWHISPER_COREML_ALLOW_FALLBACK=ON" + - os: macos-15 + name: apple-xcframework + make: xcframework LLAMA="-DGGML_NATIVE=OFF -DGGML_METAL=ON -DGGML_ACCELERATE=ON -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=Apple" WHISPER="-DWHISPER_COREML=ON -DWHISPER_COREML_ALLOW_FALLBACK=ON" + - os: ubuntu-22.04 + name: android-aar + make: aar + + defaults: + run: + shell: ${{ matrix.container && 'sh' || 'bash' }} + + steps: + + - name: linux-musl x86_64 install dependencies + if: contains(matrix.name, 'linux-musl') && matrix.arch == 'x86_64' + run: apk update && apk add --no-cache git gcc g++ make cmake sqlite musl-dev linux-headers python3 curl-dev openssl-dev zlib-dev + + - uses: actions/checkout@v4.2.2 + with: + submodules: true + + - name: Prepare test asset directories + run: | + mkdir -p "${{ env.GGUF_MODEL_DIR }}" + mkdir -p "${{ env.WHISPER_MODEL_DIR }}" + mkdir -p "${{ env.AUDIO_TEST_DIR }}" + + - name: Restore GGUF cache + uses: actions/cache@v4 + with: + path: ${{ needs.download-models.outputs.gguf-model-path }} + key: ${{ needs.download-models.outputs.gguf-cache-key }} + + - name: Restore Whisper cache + uses: actions/cache@v4 + with: + path: ${{ needs.download-models.outputs.whisper-model-path }} + key: ${{ needs.download-models.outputs.whisper-cache-key }} + + - name: Restore audio test file cache + uses: actions/cache@v4 + with: + path: ${{ needs.download-models.outputs.audio-test-path }} + key: ${{ needs.download-models.outputs.audio-cache-key }} + + - name: android setup java + if: matrix.name == 'android-aar' + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: calculate cache version hashes for modules and dependencies + id: submodule-hashes + env: + MATRIX_MAKE: ${{ matrix.make && matrix.make || '' }} + run: | + LLAMA_HASH=$(git -C modules/llama.cpp rev-parse HEAD) + WHISPER_HASH=$(git -C modules/whisper.cpp rev-parse HEAD) + MINIAUDIO_HASH=$(git -C modules/miniaudio rev-parse HEAD) + MBEDTLS_HASH=$(git -C modules/mbedtls rev-parse HEAD) + CURL_HASH=$(git -C modules/curl rev-parse HEAD) + # Hash both Makefiles together — extensions/sqlite/Makefile changes + # the link step (CFLAGS / LDFLAGS) and so should invalidate dep caches. + if command -v sha256sum >/dev/null 2>&1; then + MAKE_HASH=$(echo "$MATRIX_MAKE" | sha256sum | cut -d' ' -f1) + MAKEFILE_HASH=$(cat Makefile extensions/sqlite/Makefile | sha256sum | cut -d' ' -f1) + elif command -v shasum >/dev/null 2>&1; then + MAKE_HASH=$(echo "$MATRIX_MAKE" | shasum -a 256 | cut -d' ' -f1) + MAKEFILE_HASH=$(cat Makefile extensions/sqlite/Makefile | shasum -a 256 | cut -d' ' -f1) + else + MAKE_HASH=$(echo "$MATRIX_MAKE" | openssl dgst -sha256 | cut -d' ' -f2) + MAKEFILE_HASH=$(cat Makefile extensions/sqlite/Makefile | openssl dgst -sha256 | cut -d' ' -f2) + fi + echo "llama=$LLAMA_HASH" >> $GITHUB_OUTPUT + echo "whisper=$WHISPER_HASH" >> $GITHUB_OUTPUT + echo "miniaudio=$MINIAUDIO_HASH" >> $GITHUB_OUTPUT + echo "mbedtls=$MBEDTLS_HASH" >> $GITHUB_OUTPUT + echo "curl=$CURL_HASH" >> $GITHUB_OUTPUT + echo "make=$MAKE_HASH" >> $GITHUB_OUTPUT + echo "makefile=$MAKEFILE_HASH" >> $GITHUB_OUTPUT + + - uses: msys2/setup-msys2@v2.27.0 + if: matrix.os == 'windows-2022' + with: + msystem: mingw64 + install: >- + git + make + sqlite + mingw-w64-x86_64-cc + mingw-w64-x86_64-cmake + ${{ matrix.name == 'windows-gpu' && 'mingw-w64-x86_64-vulkan-headers' || '' }} + ${{ matrix.name == 'windows-gpu' && 'mingw-w64-x86_64-vulkan-loader' || '' }} + ${{ matrix.name == 'windows-gpu' && 'mingw-w64-x86_64-shaderc' || '' }} + ${{ matrix.name == 'windows-gpu' && 'mingw-w64-x86_64-opencl-headers' || '' }} + ${{ matrix.name == 'windows-gpu' && 'mingw-w64-x86_64-opencl-icd' || '' }} + + - name: macos install dependencies + if: matrix.name == 'macos' + run: brew link sqlite --force + + - name: macos x86_64 build x86_64 sqlite3 shell + # The macos-15 runner is arm64; Homebrew's sqlite3 is arm64-only. To + # exercise `.load ./dist/adam.dylib` against an x86_64 dylib (this + # matrix entry's artifact) we need an x86_64 sqlite3 binary — + # Rosetta translates whole processes, not individual dlopen calls. + if: matrix.name == 'macos' && matrix.arch == 'x86_64' + run: | + curl -sLO https://sqlite.org/2025/sqlite-amalgamation-3490100.zip + unzip -q sqlite-amalgamation-*.zip + cd sqlite-amalgamation-* + cc -arch x86_64 -O2 -DSQLITE_ENABLE_LOAD_EXTENSION \ + shell.c sqlite3.c -o "$GITHUB_WORKSPACE/sqlite3_x86_64" + cd .. + rm -rf sqlite-amalgamation-*.zip sqlite-amalgamation-* + echo "SQLITE3=$GITHUB_WORKSPACE/sqlite3_x86_64" >> "$GITHUB_ENV" + + - name: linux install dependencies + if: startsWith(matrix.name, 'linux-cpu') || startsWith(matrix.name, 'linux-gpu') + run: sudo apt-get update && sudo apt-get install -y libssl-dev + + - name: linux-musl arm64 setup container + if: contains(matrix.name, 'linux-musl') && matrix.arch == 'arm64' + run: | + docker run -d --name alpine \ + --platform linux/arm64 \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace \ + alpine:latest \ + tail -f /dev/null + docker exec alpine sh -c "apk update && apk add --no-cache git gcc g++ make cmake sqlite musl-dev linux-headers python3 curl-dev openssl-dev zlib-dev" + # Tell git inside the container that the bind-mounted workspace is + # safe — git runs as root in the container while the files are + # owned by the runner user, which trips git's "dubious ownership" + # check. + docker exec alpine git config --global --add safe.directory '*' + + - name: linux install opencl + if: matrix.name == 'linux-gpu' + run: sudo apt-get install -y opencl-headers ocl-icd-opencl-dev + + - name: linux-x86_64 install vulkan + if: matrix.name == 'linux-gpu' && matrix.arch == 'x86_64' + run: | + # ggml-vulkan.cpp uses Vulkan symbols (PipelineRobustnessCreateInfoEXT, + # CooperativeMatrixFeaturesKHR, LayerSettingEXT, eMesaDozen, …) + # introduced in 1.4+. Jammy's libvulkan-dev ships 1.3.204 headers + # (too old), but its loader libvulkan.so is forward-compatible — + # 1.4 functions get resolved through vkGetInstanceProcAddr at + # runtime, not at link time. So: apt for the loader + drivers, + # LunarG SDK for the 1.4 headers + glslc, and a symlink so cmake's + # FindVulkan resolves Vulkan_LIBRARY against the apt-installed + # loader when VULKAN_SDK is set. + sudo apt-get update -y + sudo apt-get install -y mesa-vulkan-drivers libvulkan-dev + wget -q https://sdk.lunarg.com/sdk/download/latest/linux/vulkan-sdk.tar.xz -O /tmp/vulkan-sdk.tar.xz + mkdir /tmp/vulkan-sdk && tar -xf /tmp/vulkan-sdk.tar.xz -C /tmp/vulkan-sdk + SDK_DIR=$(ls -d /tmp/vulkan-sdk/1.* | head -n1) + sudo mkdir -p /opt/vulkan-sdk/lib + sudo cp -r "$SDK_DIR/x86_64/include" /opt/vulkan-sdk/include + sudo cp -r "$SDK_DIR/x86_64/bin" /opt/vulkan-sdk/bin + sudo ln -sf /usr/lib/x86_64-linux-gnu/libvulkan.so /opt/vulkan-sdk/lib/libvulkan.so + rm -rf /tmp/vulkan-sdk /tmp/vulkan-sdk.tar.xz + echo "VULKAN_SDK=/opt/vulkan-sdk" >> $GITHUB_ENV + echo "/opt/vulkan-sdk/bin" >> $GITHUB_PATH + + - name: linux-arm64 install vulkan + if: matrix.name == 'linux-gpu' && matrix.arch == 'arm64' + run: | + sudo dpkg --add-architecture arm64 + + # Add arch-specific repositories for non-amd64 architectures + cat << EOF | sudo tee /etc/apt/sources.list.d/arm64-ports.list + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble main universe + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble-updates main universe + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble-security main universe + deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble-backports main universe + EOF + + sudo apt-get update || true ;# Prevent failure due to missing URLs. + # On noble the binary package providing /usr/bin/glslc is `glslc` + # (the `shaderc` name is only the source package). + sudo apt-get install -y --no-install-recommends build-essential glslc crossbuild-essential-arm64 libvulkan-dev:arm64 + + - name: cache llama.cpp build + id: cache-llama + uses: actions/cache@v4 + with: + path: | + build/llama.cpp + build/llama.cpp.stamp + key: llama-${{ matrix.name }}-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.submodule-hashes.outputs.llama }}-${{ hashFiles('modules/llama.cpp/**') }}-${{ steps.submodule-hashes.outputs.make }}-${{ steps.submodule-hashes.outputs.makefile }} + + - name: cache whisper.cpp build + id: cache-whisper + uses: actions/cache@v4 + with: + path: | + build/whisper.cpp + build/whisper.cpp.stamp + key: whisper-${{ matrix.name }}-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.submodule-hashes.outputs.whisper }}-${{ hashFiles('modules/whisper.cpp/**') }}-${{ steps.submodule-hashes.outputs.make }}-${{ steps.submodule-hashes.outputs.makefile }} + + - name: cache miniaudio build + id: cache-miniaudio + uses: actions/cache@v4 + with: + path: | + build/miniaudio + build/miniaudio.stamp + key: miniaudio-${{ matrix.name }}-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.submodule-hashes.outputs.miniaudio }}-${{ hashFiles('modules/miniaudio/**') }}-${{ steps.submodule-hashes.outputs.make }}-${{ steps.submodule-hashes.outputs.makefile }} + + - name: cache mbedtls build + if: contains(matrix.name, 'linux') || matrix.name == 'android' || contains(matrix.name, 'windows') + id: cache-mbedtls + uses: actions/cache@v4 + with: + path: | + build/mbedtls + build/mbedtls.stamp + key: mbedtls-${{ matrix.name }}-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.submodule-hashes.outputs.mbedtls }}-${{ steps.submodule-hashes.outputs.make }}-${{ steps.submodule-hashes.outputs.makefile }} + + - name: cache curl build + if: contains(matrix.name, 'linux') || matrix.name == 'android' || contains(matrix.name, 'windows') + id: cache-curl + uses: actions/cache@v4 + with: + path: | + build/curl + build/curl.stamp + key: curl-${{ matrix.name }}-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.submodule-hashes.outputs.curl }}-${{ steps.submodule-hashes.outputs.mbedtls }}-${{ steps.submodule-hashes.outputs.make }}-${{ steps.submodule-hashes.outputs.makefile }} + + - name: windows build llama.cpp + if: matrix.os == 'windows-2022' && steps.cache-llama.outputs.cache-hit != 'true' + shell: msys2 {0} + run: make build/llama.cpp.stamp ${{ matrix.make && matrix.make || ''}} + env: + VULKAN_SDK: "C:/msys64/mingw64" + + - name: unix build llama.cpp + if: matrix.os != 'windows-2022' && matrix.name != 'android-aar' && matrix.name != 'apple-xcframework' && steps.cache-llama.outputs.cache-hit != 'true' + run: ${{ contains(matrix.name, 'linux-musl') && matrix.arch == 'arm64' && 'docker exec alpine' || '' }} make build/llama.cpp.stamp ${{ matrix.make && matrix.make || ''}} + + - name: windows build whisper.cpp + if: matrix.os == 'windows-2022' && steps.cache-whisper.outputs.cache-hit != 'true' + shell: msys2 {0} + run: make build/whisper.cpp.stamp ${{ matrix.make && matrix.make || ''}} + env: + VULKAN_SDK: "C:/msys64/mingw64" + + - name: unix build whisper.cpp + if: matrix.os != 'windows-2022' && matrix.name != 'android-aar' && matrix.name != 'apple-xcframework' && steps.cache-whisper.outputs.cache-hit != 'true' + run: ${{ contains(matrix.name, 'linux-musl') && matrix.arch == 'arm64' && 'docker exec alpine' || '' }} make build/whisper.cpp.stamp ${{ matrix.make && matrix.make || ''}} + + - name: windows build miniaudio + if: matrix.os == 'windows-2022' && steps.cache-miniaudio.outputs.cache-hit != 'true' + shell: msys2 {0} + run: make build/miniaudio.stamp ${{ matrix.make && matrix.make || ''}} + + - name: unix build miniaudio + if: matrix.os != 'windows-2022' && matrix.name != 'android-aar' && matrix.name != 'apple-xcframework' && steps.cache-miniaudio.outputs.cache-hit != 'true' + run: ${{ contains(matrix.name, 'linux-musl') && matrix.arch == 'arm64' && 'docker exec alpine' || '' }} make build/miniaudio.stamp ${{ matrix.make && matrix.make || ''}} + + - name: unix build mbedtls + curl + if: (contains(matrix.name, 'linux') || matrix.name == 'android') && matrix.name != 'android-aar' && (steps.cache-mbedtls.outputs.cache-hit != 'true' || steps.cache-curl.outputs.cache-hit != 'true') + run: ${{ contains(matrix.name, 'linux-musl') && matrix.arch == 'arm64' && 'docker exec alpine' || '' }} make build/mbedtls.stamp build/curl.stamp ${{ matrix.make && matrix.make || ''}} + + - name: windows build mbedtls + curl + if: contains(matrix.name, 'windows') && (steps.cache-mbedtls.outputs.cache-hit != 'true' || steps.cache-curl.outputs.cache-hit != 'true') + shell: msys2 {0} + run: make build/mbedtls.stamp build/curl.stamp ${{ matrix.make && matrix.make || ''}} + + - name: windows build adam extension + if: matrix.os == 'windows-2022' + run: make extension ${{ matrix.make && matrix.make || ''}} + shell: msys2 {0} + env: + VULKAN_SDK: "C:/msys64/mingw64" + + - name: unix build adam extension + if: matrix.os != 'windows-2022' && matrix.name != 'android-aar' && matrix.name != 'apple-xcframework' + run: ${{ contains(matrix.name, 'linux-musl') && matrix.arch == 'arm64' && 'docker exec alpine' || '' }} make extension ${{ matrix.make && matrix.make || ''}} + + - name: build aar/xcframework + if: matrix.name == 'android-aar' || matrix.name == 'apple-xcframework' + run: make ${{ matrix.make && matrix.make || ''}} + + - name: create keychain for codesign + if: matrix.os == 'macos-15' + run: | + echo "${{ secrets.APPLE_CERTIFICATE }}" | base64 --decode > certificate.p12 + security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" build.keychain + security import certificate.p12 -k build.keychain -P "${{ secrets.CERTIFICATE_PASSWORD }}" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.KEYCHAIN_PASSWORD }}" build.keychain + + - name: codesign and notarize dylib + if: matrix.os == 'macos-15' && matrix.name != 'apple-xcframework' + run: | + codesign --sign "${{ secrets.APPLE_TEAM_ID }}" --timestamp --options runtime dist/adam.dylib + ditto -c -k dist/adam.dylib dist/adam.zip + xcrun notarytool submit dist/adam.zip --apple-id "${{ secrets.APPLE_ID }}" --password "${{ secrets.APPLE_PASSWORD }}" --team-id "${{ secrets.APPLE_TEAM_ID }}" --wait + rm dist/adam.zip + + - name: codesign and notarize xcframework + if: matrix.name == 'apple-xcframework' + run: | + find dist/adam.xcframework -name "*.framework" -exec echo "Signing: {}" \; -exec codesign --sign "${{ secrets.APPLE_TEAM_ID }}" --timestamp --options runtime {} \; # Sign each individual framework FIRST + codesign --sign "${{ secrets.APPLE_TEAM_ID }}" --timestamp --options runtime dist/adam.xcframework # Then sign the xcframework wrapper + ditto -c -k --keepParent dist/adam.xcframework dist/adam.xcframework.zip + xcrun notarytool submit dist/adam.xcframework.zip --apple-id "${{ secrets.APPLE_ID }}" --password "${{ secrets.APPLE_PASSWORD }}" --team-id "${{ secrets.APPLE_TEAM_ID }}" --wait + rm -rf dist/adam.xcframework + + - name: cleanup keychain for codesign + if: matrix.os == 'macos-15' + run: | + rm certificate.p12 + security delete-keychain build.keychain + + - name: android setup test environment + if: matrix.name == 'android' && matrix.arch == 'x86_64' + run: | + + echo "::group::enable kvm group perms" + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + echo "::endgroup::" + + echo "::group::download and build sqlite3 without SQLITE_OMIT_LOAD_EXTENSION" + curl -O ${{ matrix.sqlite-amalgamation-zip }} + unzip sqlite-amalgamation-*.zip + export ${{ matrix.make }} + $ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/${{ matrix.arch }}-linux-android26-clang sqlite-amalgamation-*/shell.c sqlite-amalgamation-*/sqlite3.c -o sqlite3 -ldl + # remove unused folders to save up space + rm -rf sqlite-amalgamation-*.zip sqlite-amalgamation-* + echo "::endgroup::" + + echo "::group::prepare the test script" + make test PLATFORM=$PLATFORM ARCH=$ARCH || echo "It should fail. Running remaining commands in the emulator" + cat > commands.sh << EOF + mv -f /data/local/tmp/sqlite3 /system/xbin + cd /data/local/tmp + $(make test PLATFORM=$PLATFORM ARCH=$ARCH -n) + EOF + # remove big unused folders to avoid emulator errors + echo "::endgroup::" + + - name: android test adam extension + if: matrix.name == 'android' && matrix.arch == 'x86_64' + uses: reactivecircus/android-emulator-runner@v2.34.0 + with: + api-level: 26 + arch: ${{ matrix.arch }} + script: | + adb root + adb remount + adb push ${{ github.workspace }}/commands.sh /data/local/tmp/ + adb push ${{ github.workspace }}/sqlite3 /data/local/tmp/ + adb push ${{ github.workspace }}/dist /data/local/tmp/ + adb push ${{ github.workspace }}/Makefile /data/local/tmp/ + adb push ${{ github.workspace }}/test_adam /data/local/tmp/ + adb shell "chmod +x /data/local/tmp/test_adam" + adb shell "sh /data/local/tmp/commands.sh" + + - name: windows test adam extension + if: matrix.os == 'windows-2022' + run: make test ${{ matrix.make && matrix.make || ''}} + shell: msys2 {0} + env: + VULKAN_SDK: "C:/msys64/mingw64" + + - name: unix test adam extension + if: contains(matrix.name, 'linux') || matrix.name == 'macos' + # The macos-15 runner is arm64; the macos x86_64 matrix entry produces + # a pure x86_64 binary and runs it via Rosetta 2 (preinstalled on the + # runner image). + # + # SANITIZE=0 is the job-level default — pass it explicitly into the + # linux-musl arm64 docker container (libasan/ubsan aren't in Alpine). + run: ${{ contains(matrix.name, 'linux-musl') && matrix.arch == 'arm64' && 'docker exec -e SANITIZE alpine' || '' }} make test ${{ matrix.make && matrix.make || ''}} + + - uses: actions/upload-artifact@v4.6.2 + if: always() + with: + name: adam-${{ matrix.name }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }} + path: dist/adam.* + if-no-files-found: error + + release: + runs-on: ubuntu-22.04 + name: release + needs: build + if: github.ref == 'refs/heads/main' + + steps: + + - uses: actions/checkout@v4.2.2 + + - uses: actions/download-artifact@v4.2.1 + with: + path: artifacts + + - name: zip artifacts + run: | + VERSION=$(make version) + for folder in "artifacts"/*; do + if [ -d "$folder" ]; then + name=$(basename "$folder") + if [[ "$name" != "adam-apple-xcframework" && "$name" != "adam-android-aar" ]]; then + tar -czf "${name}-${VERSION}.tar.gz" -C "$folder" . + fi + if [[ "$name" == "adam-apple-xcframework" ]]; then + # Use the ditto-created zip that preserves macOS symlinks and extract for other steps + cp "$folder/adam.xcframework.zip" "${name}-${VERSION}.zip" + unzip -q "$folder/adam.xcframework.zip" -d "$folder/" + elif [[ "$name" != "adam-android-aar" ]]; then + (cd "$folder" && zip -rq "../../${name}-${VERSION}.zip" .) + else + cp "$folder"/*.aar "${name}-${VERSION}.aar" + fi + fi + done + + - name: release tag version from adam.h + id: tag + run: | + VERSION=$(make version) + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + LATEST_RELEASE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/${{ github.repository }}/releases/latest) + LATEST=$(echo "$LATEST_RELEASE" | jq -r '.name') + if [[ "$VERSION" != "$LATEST" || "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + echo "version=$VERSION" >> $GITHUB_OUTPUT + else + echo "::warning file=src/adam.h::To release a new version, please update the ADAM_VERSION_STRING in src/adam.h to be different than the latest $LATEST" + fi + exit 0 + fi + echo "❌ ADAM_VERSION_STRING not found in src/adam.h" + exit 1 + + - uses: actions/setup-java@v4 + if: steps.tag.outputs.version != '' + with: + distribution: 'temurin' + java-version: '17' + + - name: release android aar to maven central + if: steps.tag.outputs.version != '' + run: cd packages/android && ./gradlew publishAggregationToCentralPortal -PSIGNING_KEY="${{ secrets.SIGNING_KEY }}" -PSIGNING_PASSWORD="${{ secrets.SIGNING_PASSWORD }}" -PSONATYPE_USERNAME="${{ secrets.MAVEN_CENTRAL_USERNAME }}" -PSONATYPE_PASSWORD="${{ secrets.MAVEN_CENTRAL_TOKEN }}" -PVERSION="${{ steps.tag.outputs.version }}" -PAAR_PATH="../../artifacts/adam-android-aar/adam.aar" + + - name: update Package.swift checksum and version + id: package-swift + if: steps.tag.outputs.version != '' + run: | + VERSION=${{ steps.tag.outputs.version }} + ZIP="adam-apple-xcframework-${VERSION}.zip" + if [ ! -f "$ZIP" ]; then + echo "::warning::xcframework artifact not found; skipping Package.swift update" + exit 0 + fi + CHECKSUM=$(swift package compute-checksum "$ZIP") + URL="https://github.com/sqliteai/adam/releases/download/${VERSION}/${ZIP}" + sed -i "s|url: \".*apple-xcframework.*\"|url: \"${URL}\"|" Package.swift + sed -i "s|checksum: \".*\"|checksum: \"${CHECKSUM}\"|" Package.swift + if git diff --quiet Package.swift; then + echo "Package.swift already up to date for ${VERSION}" + exit 0 + fi + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" + git config --global user.name "$GITHUB_ACTOR" + git add Package.swift + git commit -m "Update Package.swift checksum for ${VERSION} [auto-update]" + git push origin main + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - uses: softprops/action-gh-release@v2.2.1 + if: steps.tag.outputs.version != '' + with: + token: ${{ secrets.RELEASE_PAT }} + target_commitish: ${{ steps.package-swift.outputs.sha }} + body: | + # Packages + + [**Flutter/Dart**](https://pub.dev/packages/sqlite_adam): `flutter pub add sqlite_adam:${{ steps.tag.outputs.version }}` or `dart pub add sqlite_adam:${{ steps.tag.outputs.version }}` + [**Android**](https://central.sonatype.com/artifact/ai.sqlite/adam): `ai.sqlite:adam:${{ steps.tag.outputs.version }}` + [**Swift**](https://github.com/sqliteai/adam#swift-package): [Installation Guide](https://github.com/sqliteai/adam#swift-package) + + --- + + generate_release_notes: true + tag_name: ${{ steps.tag.outputs.version }} + files: adam-*-${{ steps.tag.outputs.version }}.* + make_latest: true diff --git a/.gitignore b/.gitignore index 0cc82ad..bd03553 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ test_chat test_voice_interactive test_voice_talk adam +build/ +dist/ # Models *.gguf @@ -33,6 +35,14 @@ PLAN.md *~ .vscode/ .idea/ +*.iml + +# Gradle / Android (packages/android) +.gradle/ +local.properties + +# Dart / Flutter (packages/flutter) +.dart_tool/ # Dependency build artifacts deps/*/build/ diff --git a/Makefile b/Makefile index 2aecbd0..62e4583 100644 --- a/Makefile +++ b/Makefile @@ -6,28 +6,77 @@ UNAME_S := $(shell uname -s) CC ?= cc # ============================================================================ -# Modules (git submodules in modules/) +# Platform / arch detection (overridable from CI: PLATFORM=android ARCH=arm64-v8a) +# ============================================================================ + +ifeq ($(OS),Windows_NT) + PLATFORM ?= windows + HOST := windows + CPUS := $(shell powershell -Command "[Environment]::ProcessorCount" 2>/dev/null || echo 4) +else + HOST := $(shell uname -s | tr '[:upper:]' '[:lower:]') + ifeq ($(HOST),darwin) + PLATFORM ?= macos + CPUS := $(shell sysctl -n hw.ncpu 2>/dev/null || echo 4) + else + PLATFORM ?= $(HOST) + CPUS := $(shell nproc 2>/dev/null || echo 4) + endif +endif + +# Non-empty when building for an Apple platform (macos / ios / ios-sim). +IS_APPLE := $(filter $(PLATFORM),macos ios ios-sim) + +# Shared-extension filename per platform. Defined here (not down in the CI +# section) because the test target references EXT_FILE before that section +# is parsed, and `:=` would otherwise capture an empty value. +ifeq ($(PLATFORM),windows) + EXT_FILE := adam.dll +else ifneq ($(IS_APPLE),) + EXT_FILE := adam.dylib +else + EXT_FILE := adam.so +endif + +# CMake options pass-through from CI matrix +LLAMA ?= +WHISPER ?= +MINIAUDIO ?= + +# ============================================================================ +# Modules (git submodules in modules/) and shared build/ tree # ============================================================================ MBEDTLS_DIR := $(ADAM_ROOT)modules/mbedtls CURL_DIR := $(ADAM_ROOT)modules/curl -MBEDTLS_BUILD := $(MBEDTLS_DIR)/build -CURL_BUILD := $(CURL_DIR)/build MINIAUDIO_DIR := $(ADAM_ROOT)modules/miniaudio LLAMA_DIR := $(ADAM_ROOT)modules/llama.cpp -LLAMA_BUILD := $(LLAMA_DIR)/build WHISPER_DIR := $(ADAM_ROOT)modules/whisper.cpp -WHISPER_BUILD := $(WHISPER_DIR)/build SQLITE_MEMORY_DIR := $(ADAM_ROOT)modules/sqlite-memory SQLITE_VECTOR_DIR := $(ADAM_ROOT)modules/sqlite-vector +# All dependency builds live under build/ at the repo root. +# Kept relative (not $(ADAM_ROOT)build) so target names like +# `build/llama.cpp.stamp` match what the CI workflow invokes directly. +BUILD_DIR := build +DIST_DIR := dist +LLAMA_BUILD := $(BUILD_DIR)/llama.cpp +WHISPER_BUILD := $(BUILD_DIR)/whisper.cpp +MINIAUDIO_BUILD := $(BUILD_DIR)/miniaudio +MBEDTLS_BUILD := $(BUILD_DIR)/mbedtls +CURL_BUILD := $(BUILD_DIR)/curl + # ============================================================================ # Compiler settings # ============================================================================ SQLITE_DIR := $(ADAM_ROOT)modules/sqlite -CFLAGS := -std=c11 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Wpedantic -O2 +# Section flags are Mach-O no-ops (Apple's linker uses subsection-via-symbols +# unconditionally), but -flto and -Wl,-dead_strip still apply there. +SIZE_CFLAGS := -ffunction-sections -fdata-sections -flto + +CFLAGS := -std=gnu11 -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE -Wall -Wextra -Wpedantic -O2 -fPIC $(SIZE_CFLAGS) CFLAGS += -Isrc -I$(MINIAUDIO_DIR) -I$(SQLITE_DIR) CFLAGS += -I$(LLAMA_DIR)/include -I$(LLAMA_DIR)/ggml/include -I$(LLAMA_DIR)/tools/mtmd CFLAGS += -I$(WHISPER_DIR)/include @@ -45,37 +94,137 @@ SQLITE_FLAGS := -DSQLITE_THREADSAFE=1 \ -DSQLITE_USE_ALLOCA \ -DSQLITE_OMIT_AUTOINIT -LDFLAGS := -lpthread -lz +# Android's bionic libc has pthread built in — no separate libpthread.so to link. +# -lz lives at the END of LIBS (single-pass GNU ld + strict musl loader); +# Windows pulls in -lws2_32 / -lcrypt32 / -lbcrypt there too. See the LIBS +# blocks below. +ifeq ($(PLATFORM),android) +LDFLAGS := +else ifeq ($(PLATFORM),windows) +LDFLAGS := +else +LDFLAGS := -lpthread +endif + +ifneq ($(IS_APPLE),) +LDFLAGS += -Wl,-dead_strip -flto +else +LDFLAGS += -Wl,--gc-sections -flto +endif # ============================================================================ # Platform-specific: Apple (NSURLSession) vs Other (libcurl + mbedtls) # ============================================================================ +# MinGW Ninja sometimes ignores -DCMAKE_STATIC_LIBRARY_PREFIX=lib for ggml +# sub-targets and emits `ggml.a` / `ggml-cpu.a` / `ggml-base.a` without +# the `lib` prefix. `ggml_lib` resolves to whichever variant exists so +# the LLAMA_LIBS list works on every platform. +ggml_lib = $(firstword $(wildcard $(LLAMA_BUILD)/ggml/src/lib$(1).a $(LLAMA_BUILD)/ggml/src/$(1).a)) + LLAMA_LIBS := $(LLAMA_BUILD)/tools/mtmd/libmtmd.a \ $(LLAMA_BUILD)/src/libllama.a \ - $(LLAMA_BUILD)/ggml/src/libggml.a \ - $(LLAMA_BUILD)/ggml/src/libggml-cpu.a \ - $(LLAMA_BUILD)/ggml/src/libggml-base.a + $(call ggml_lib,ggml) \ + $(call ggml_lib,ggml-cpu) \ + $(call ggml_lib,ggml-base) + +# Optional GPU backends — wildcards resolve only when llama.cpp was built +# with -DGGML_VULKAN=ON / -DGGML_OPENCL=ON. Static archives go at the end +# of LLAMA_LIBS; matching `-lvulkan` / `-lOpenCL` runtime loaders are +# appended to LIBS by each platform block below. +GGML_VULKAN_LIB := $(firstword $(wildcard $(LLAMA_BUILD)/ggml/src/ggml-vulkan/libggml-vulkan.a $(LLAMA_BUILD)/ggml/src/ggml-vulkan/ggml-vulkan.a)) +GGML_OPENCL_LIB := $(firstword $(wildcard $(LLAMA_BUILD)/ggml/src/ggml-opencl/libggml-opencl.a $(LLAMA_BUILD)/ggml/src/ggml-opencl/ggml-opencl.a)) +ifneq ($(GGML_VULKAN_LIB),) + LLAMA_LIBS += $(GGML_VULKAN_LIB) +endif +ifneq ($(GGML_OPENCL_LIB),) + LLAMA_LIBS += $(GGML_OPENCL_LIB) +endif # Whisper uses llama's ggml (symlinked: whisper.cpp/ggml → llama.cpp/ggml). # Only libwhisper.a is needed — ggml symbols come from LLAMA_LIBS. WHISPER_LIBS := $(WHISPER_BUILD)/src/libwhisper.a -ifeq ($(UNAME_S),Darwin) - # Apple: use NSURLSession — no curl/mbedtls needed - CFLAGS += -DADAM_NO_CURL +# Apple platforms (macos, ios, ios-sim) all use NSURLSession. +# _DARWIN_C_SOURCE re-exposes BSD extensions (e.g. memmem) that the +# global _POSIX_C_SOURCE=200809L would otherwise hide. +ifneq ($(IS_APPLE),) + CFLAGS += -DADAM_NO_CURL -D_DARWIN_C_SOURCE=1 LDFLAGS += -framework Foundation LDFLAGS += -framework SystemConfiguration -framework Security - LDFLAGS += -framework CoreAudio -framework AudioToolbox + LDFLAGS += -framework CoreAudio -framework AudioToolbox -framework AVFoundation LDFLAGS += -framework Metal -framework MetalKit -framework Accelerate LDFLAGS += -lstdc++ NET_SRC := src/adam_net_apple.m TTS_SYS_SRC := src/adam_tts_system.m LLAMA_LIBS += $(LLAMA_BUILD)/ggml/src/ggml-metal/libggml-metal.a - LLAMA_LIBS += $(LLAMA_BUILD)/ggml/src/ggml-blas/libggml-blas.a - LDFLAGS += -framework AVFoundation + # Pick up ggml-blas if llama.cpp was configured with -DGGML_BLAS=ON + # (libggml.a then references blas backend symbols that need this lib). + GGML_BLAS_LIB := $(wildcard $(LLAMA_BUILD)/ggml/src/ggml-blas/libggml-blas.a) + ifneq ($(GGML_BLAS_LIB),) + LLAMA_LIBS += $(GGML_BLAS_LIB) + endif + + # PLATFORM_CFLAGS holds -arch + -isysroot bits that must apply to *every* + # compilation unit in libadam.a (otherwise sqlite3.o, sqlite-vector, + # sqlite-memory get built host-arch and the static archive has mixed + # architectures). `-x objective-c` is added only to CFLAGS — for iOS, + # miniaudio.h pulls in which is Obj-C only. + ifeq ($(PLATFORM),macos) + ifndef ARCH + PLATFORM_CFLAGS := -arch x86_64 -arch arm64 + else + PLATFORM_CFLAGS := -arch $(ARCH) + endif + CFLAGS += $(PLATFORM_CFLAGS) + LDFLAGS += $(PLATFORM_CFLAGS) + else ifeq ($(PLATFORM),ios) + APPLE_SDK := -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) -miphoneos-version-min=14.0 + PLATFORM_CFLAGS := -arch arm64 $(APPLE_SDK) + CFLAGS += $(PLATFORM_CFLAGS) -x objective-c + LDFLAGS += $(PLATFORM_CFLAGS) + else ifeq ($(PLATFORM),ios-sim) + APPLE_SDK := -isysroot $(shell xcrun --sdk iphonesimulator --show-sdk-path) -miphonesimulator-version-min=14.0 + PLATFORM_CFLAGS := -arch x86_64 -arch arm64 $(APPLE_SDK) + CFLAGS += $(PLATFORM_CFLAGS) -x objective-c + LDFLAGS += $(PLATFORM_CFLAGS) + endif + + LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) + # Pick up CoreML stub when whisper was built with -DWHISPER_COREML=ON. + WHISPER_COREML_LIB := $(wildcard $(WHISPER_BUILD)/src/libwhisper.coreml.a) + ifneq ($(WHISPER_COREML_LIB),) + LIBS += $(WHISPER_COREML_LIB) + LDFLAGS += -framework CoreML + endif +else ifeq ($(PLATFORM),android) + # Android: cross-compile via NDK, use libcurl + mbedtls. + ifndef ARCH + $(error Android ARCH must be set to ARCH=x86_64 or ARCH=arm64-v8a) + endif + ifndef ANDROID_NDK + $(error ANDROID_NDK must point to the Android NDK install) + endif + ANDROID_NDK_BIN := $(ANDROID_NDK)/toolchains/llvm/prebuilt/$(HOST)-x86_64/bin + ifneq (,$(filter $(ARCH),arm64 arm64-v8a)) + NDK_TRIPLE := aarch64-linux-android26 + else + NDK_TRIPLE := $(ARCH)-linux-android26 + endif + CC := $(ANDROID_NDK_BIN)/$(NDK_TRIPLE)-clang + # Host ar/ranlib choke on ELF objects from the NDK; use llvm-ar from the NDK. + AR := $(ANDROID_NDK_BIN)/llvm-ar + CFLAGS += -I$(CURL_DIR)/include -I$(MBEDTLS_DIR)/include + LDFLAGS += -ldl -lm + NET_SRC := src/adam_net_curl.c + TTS_SYS_SRC := src/adam_tts_system.c LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) -else ifeq ($(UNAME_S),Linux) + LIBS += $(CURL_BUILD)/lib/libcurl.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedtls.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedx509.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedcrypto.a + LIBS += -lz +else ifeq ($(PLATFORM),linux) # Linux: use libcurl + mbedtls CFLAGS += -I$(CURL_DIR)/include -I$(MBEDTLS_DIR)/include LDFLAGS += -ldl -lm -lstdc++ @@ -86,15 +235,39 @@ else ifeq ($(UNAME_S),Linux) LIBS += $(MBEDTLS_BUILD)/library/libmbedtls.a LIBS += $(MBEDTLS_BUILD)/library/libmbedx509.a LIBS += $(MBEDTLS_BUILD)/library/libmbedcrypto.a + ifneq ($(GGML_VULKAN_LIB),) + LIBS += -lvulkan + endif + ifneq ($(GGML_OPENCL_LIB),) + LIBS += -lOpenCL + endif + LIBS += -lz else - # Windows/other: use libcurl + mbedtls - CFLAGS += -I$(CURL_DIR)/include -I$(MBEDTLS_DIR)/include + # Windows/other: use libcurl + mbedtls. + # -DCURL_STATICLIB: curl.h on Windows declares functions as dllimport + # by default; without this macro, references to curl_* become __imp_* + # which a static libcurl.a can't resolve. + CFLAGS += -I$(CURL_DIR)/include -I$(MBEDTLS_DIR)/include -DCURL_STATICLIB NET_SRC := src/adam_net_curl.c TTS_SYS_SRC := src/adam_tts_system.c - LIBS := $(CURL_BUILD)/lib/libcurl.a + LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) + LIBS += $(CURL_BUILD)/lib/libcurl.a LIBS += $(MBEDTLS_BUILD)/library/libmbedtls.a LIBS += $(MBEDTLS_BUILD)/library/libmbedx509.a LIBS += $(MBEDTLS_BUILD)/library/libmbedcrypto.a + # Win32 system libs MUST come after static archives that reference them + # (single-pass GNU ld). curl needs Winsock + crypt32; mbedtls needs + # bcrypt for BCryptGenRandom; curl uses zlib for gzip. + # The Vulkan loader DLL on Windows is `vulkan-1.dll`, so the import + # library is `libvulkan-1.dll.a` and the flag is `-lvulkan-1`. + LIBS += -lws2_32 -lcrypt32 -lbcrypt + ifneq ($(GGML_VULKAN_LIB),) + LIBS += -lvulkan-1 + endif + ifneq ($(GGML_OPENCL_LIB),) + LIBS += -lOpenCL + endif + LIBS += -lz endif # ============================================================================ @@ -134,7 +307,8 @@ SQLITE_MEMORY_SRCS := $(SQLITE_MEMORY_DIR)/src/sqlite-memory.c \ SQLITE_MEMORY_OBJS := $(patsubst $(SQLITE_MEMORY_DIR)/src/%.c,$(SQLITE_MEMORY_DIR)/src/%.o,$(SQLITE_MEMORY_SRCS)) # sqlite-memory common flags -DBMEM_CFLAGS := -std=c11 -O2 -DSQLITE_CORE -DDBMEM_OMIT_REMOTE_ENGINE \ +DBMEM_CFLAGS := -std=gnu11 -O2 -DSQLITE_CORE -DDBMEM_OMIT_REMOTE_ENGINE \ + -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE -D_DARWIN_C_SOURCE=1 \ -I$(SQLITE_MEMORY_DIR)/src -I$(SQLITE_VECTOR_DIR)/src \ -I$(SQLITE_VECTOR_DIR)/libs -I$(SQLITE_DIR) \ -I$(LLAMA_DIR)/include -I$(LLAMA_DIR)/ggml/include @@ -148,36 +322,44 @@ endif # Targets # ============================================================================ -.PHONY: all clean test live voice talk chat memory vision deps mbedtls curl llama whisper +.PHONY: all clean test live voice talk chat memory vision deps \ + mbedtls curl llama whisper extension xcframework aar version all: libadam.a adam # --- Static library --- +AR ?= ar + libadam.a: $(OBJS) $(NET_OBJ) $(TTS_SYS_OBJ) $(SQLITE_OBJ) $(SQLITE_VECTOR_OBJS) $(SQLITE_MEMORY_OBJS) $(SQLITE_MEMORY_HTTP_OBJ) - ar rcs $@ $^ + $(AR) rcs $@ $^ src/%.o: src/%.c $(CC) $(CFLAGS) -c $< -o $@ # SQLite amalgamation — compiled with its own flags (no -Wpedantic, etc.) $(SQLITE_OBJ): $(SQLITE_DIR)/sqlite3.c - $(CC) -std=c11 -O2 $(SQLITE_FLAGS) -c $< -o $@ + $(CC) -std=gnu11 -O2 -fPIC $(PLATFORM_CFLAGS) $(SQLITE_FLAGS) -c $< -o $@ -# Objective-C compilation for Apple platform files +# Objective-C compilation for Apple platform files. Gated on PLATFORM — +# otherwise the explicit .m → .o rule overrides the .c pattern rule on Linux, +# making gcc try to invoke cc1obj on adam_tts_system.m even though TTS_SYS_SRC +# is set to adam_tts_system.c there. +ifneq ($(IS_APPLE),) src/adam_net_apple.o: src/adam_net_apple.m $(CC) $(CFLAGS) -c $< -o $@ src/adam_tts_system.o: src/adam_tts_system.m $(CC) $(CFLAGS) -c $< -o $@ +endif # sqlite-vector — compiled with -DSQLITE_CORE (no -Wpedantic) $(SQLITE_VECTOR_DIR)/src/%.o: $(SQLITE_VECTOR_DIR)/src/%.c - $(CC) -std=c11 -O3 -DSQLITE_CORE -I$(SQLITE_VECTOR_DIR)/src -I$(SQLITE_VECTOR_DIR)/libs -I$(SQLITE_DIR) -c $< -o $@ + $(CC) -std=gnu11 -O3 -fPIC $(PLATFORM_CFLAGS) -DSQLITE_CORE -I$(SQLITE_VECTOR_DIR)/src -I$(SQLITE_VECTOR_DIR)/libs -I$(SQLITE_DIR) -c $< -o $@ # sqlite-memory — compiled with -DSQLITE_CORE (no -Wpedantic) $(SQLITE_MEMORY_DIR)/src/%.o: $(SQLITE_MEMORY_DIR)/src/%.c - $(CC) $(DBMEM_CFLAGS) -c $< -o $@ + $(CC) -fPIC $(PLATFORM_CFLAGS) $(DBMEM_CFLAGS) -c $< -o $@ # --- CLI --- @@ -187,12 +369,67 @@ adam: libadam.a src/main.c # --- Tests --- -test: test_adam +# `test` runs an extension-load smoke test against dist/adam.{dylib,so,dll} +# plus the test_adam mock-LLM unit-test binary. +# +# SANITIZE=0 disables -fsanitize=address,undefined for environments where +# libasan/libubsan aren't shipped (Alpine musl, MinGW). Local dev keeps +# sanitizers on by default. +# +# SKIP_UNITTEST=1 falls back to smoke-test-only — kept as an escape hatch +# for any platform where the test_adam binary can't be produced at all. +SQLITE3 ?= sqlite3 +SKIP_UNITTEST ?= 0 +SANITIZE ?= 1 + +ifeq ($(SANITIZE),1) +TEST_SANITIZE := -fsanitize=address,undefined +else +TEST_SANITIZE := +endif + +# libadam.a contains miniaudio (via adam_audio.o) and libmtmd.a also embeds +# miniaudio (via mtmd-helper.cpp.o). Strict linkers (GNU ld, lld) reject +# the duplicate ma_atomic_global_lock symbol; Apple's ld merges silently. +# `--allow-multiple-definition` makes GNU ld / lld pick the first +# definition and continue — only needed for the test_adam link (the +# shared-extension link uses --gc-sections + archive semantics that avoid +# pulling mtmd-helper.o on most platforms). +ifneq ($(IS_APPLE),) +TEST_MULDEF := +else +TEST_MULDEF := -Wl,--allow-multiple-definition +endif + +# llama.cpp/whisper.cpp objects are C++. On Apple, `cc` auto-links libc++ +# when it sees C++ symbols. On Android NDK and Linux, we need the C++ +# driver explicitly so libstdc++/libc++ is brought in. +ifeq ($(PLATFORM),android) +TEST_LD := $(ANDROID_NDK_BIN)/$(NDK_TRIPLE)-clang++ +TEST_LD_EXTRA := -static-libstdc++ +else ifneq ($(IS_APPLE),) +TEST_LD := $(CC) +TEST_LD_EXTRA := +else +TEST_LD := c++ +TEST_LD_EXTRA := +endif + +TEST_DEPS := $(DIST_DIR)/$(EXT_FILE) +ifeq ($(SKIP_UNITTEST),0) +TEST_DEPS += test_adam +endif + +test: $(TEST_DEPS) + @echo "Running sqlite3 CLI smoke test (load + adam_version)..." + $(SQLITE3) ":memory:" -cmd ".bail on" ".load ./$(DIST_DIR)/adam" "SELECT adam_version();" +ifeq ($(SKIP_UNITTEST),0) ./test_adam +endif test_adam: libadam.a test/test_adam.c - $(CC) $(CFLAGS) -O0 -g -fsanitize=address,undefined \ - test/test_adam.c -L. -ladam $(LIBS) $(LDFLAGS) -o $@ + $(CC) $(CFLAGS) -O0 -g $(TEST_SANITIZE) -c test/test_adam.c -o test_adam.o + $(TEST_LD) $(PLATFORM_CFLAGS) test_adam.o -L. -ladam $(LIBS) $(LDFLAGS) $(TEST_SANITIZE) $(TEST_LD_EXTRA) $(TEST_MULDEF) -o $@ live: test_live ./test_live @@ -259,47 +496,123 @@ test_vision: libadam.a test/test_vision.c # --- Dependencies --- -deps: llama whisper +# Cross-compile cmake options derived from PLATFORM/ARCH. +# These compose with whatever the CI matrix passes via $(LLAMA)/$(WHISPER)/$(MINIAUDIO). +ifeq ($(PLATFORM),macos) + ifndef ARCH + PLATFORM_OPTS := -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 + else + PLATFORM_OPTS := -DCMAKE_OSX_ARCHITECTURES="$(ARCH)" -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 + endif +else ifeq ($(PLATFORM),ios) + PLATFORM_OPTS := -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 +else ifeq ($(PLATFORM),ios-sim) + PLATFORM_OPTS := -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=iphonesimulator -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" +else ifeq ($(PLATFORM),android) + ifndef ARCH + $(error Android ARCH must be set to ARCH=x86_64 or ARCH=arm64-v8a) + endif + ifndef ANDROID_NDK + $(error ANDROID_NDK must point to the Android NDK install) + endif + ANDROID_NDK_BIN := $(ANDROID_NDK)/toolchains/llvm/prebuilt/$(HOST)-x86_64/bin + PLATFORM_OPTS := -DCMAKE_TOOLCHAIN_FILE=$(ANDROID_NDK)/build/cmake/android.toolchain.cmake \ + -DANDROID_ABI=$(ARCH) -DANDROID_PLATFORM=android-26 \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DGGML_OPENMP=OFF -DGGML_LLAMAFILE=OFF +else + PLATFORM_OPTS := +endif + +deps: build/llama.cpp.stamp build/whisper.cpp.stamp build/miniaudio.stamp ifeq ($(UNAME_S),Linux) -deps: mbedtls curl +deps: build/mbedtls.stamp build/curl.stamp endif -llama: +# Cmake dep builds get the same size flags as adam, plus -fvisibility=hidden +# so llama/ggml/whisper/curl/mbedtls internals don't pollute the final .so's +# dynamic symbol table (only adam_* and sqlite3_adam_init need to be exported). +# Pairs with -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON for cross-archive LTO. +DEP_CFLAGS := $(SIZE_CFLAGS) -fPIC -fvisibility=hidden +DEP_CXXFLAGS := $(DEP_CFLAGS) -fvisibility-inlines-hidden + +# Shared cmake options for every dep (llama / whisper / mbedtls / curl). +# -DCMAKE_POSITION_INDEPENDENT_CODE=ON is required so the resulting .a +# archives can be linked into the shared extension — GNU ld on Linux / +# Windows rejects non-PIC TLS relocations; macOS arm64 is implicitly PIC. +CMAKE_DEP_OPTS := \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_C_FLAGS="$(DEP_CFLAGS)" \ + -DCMAKE_CXX_FLAGS="$(DEP_CXXFLAGS)" + +# Stamp targets: cacheable in CI and idempotent for local dev. +$(BUILD_DIR)/llama.cpp.stamp: + @mkdir -p $(BUILD_DIR) cmake -B $(LLAMA_BUILD) -S $(LLAMA_DIR) \ -DBUILD_SHARED_LIBS=OFF -DLLAMA_BUILD_TESTS=OFF \ -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_SERVER=OFF \ - -DCMAKE_BUILD_TYPE=Release - cmake --build $(LLAMA_BUILD) -j$$(sysctl -n hw.ncpu 2>/dev/null || nproc) --target llama --target ggml --target mtmd - -whisper: - @# whisper.cpp/ggml must be symlinked to llama.cpp/ggml + -DCMAKE_STATIC_LIBRARY_PREFIX=lib \ + -DGGML_OPENMP=OFF \ + $(CMAKE_DEP_OPTS) $(PLATFORM_OPTS) $(LLAMA) + cmake --build $(LLAMA_BUILD) --config Release -j$(CPUS) --target llama --target ggml --target mtmd + touch $@ + +$(BUILD_DIR)/whisper.cpp.stamp: $(BUILD_DIR)/llama.cpp.stamp + @mkdir -p $(BUILD_DIR) + @# whisper.cpp/ggml must be symlinked to llama.cpp/ggml so whisper compiles + @# against the same ggml that llama.cpp is built with. @test -L $(WHISPER_DIR)/ggml || (rm -rf $(WHISPER_DIR)/ggml && ln -s ../llama.cpp/ggml $(WHISPER_DIR)/ggml) cmake -B $(WHISPER_BUILD) -S $(WHISPER_DIR) \ -DBUILD_SHARED_LIBS=OFF -DWHISPER_BUILD_TESTS=OFF \ -DWHISPER_BUILD_EXAMPLES=OFF \ - -DCMAKE_BUILD_TYPE=Release - cmake --build $(WHISPER_BUILD) -j$$(sysctl -n hw.ncpu 2>/dev/null || nproc) --target whisper - -mbedtls: + -DCMAKE_STATIC_LIBRARY_PREFIX=lib \ + -DGGML_OPENMP=OFF \ + $(CMAKE_DEP_OPTS) $(PLATFORM_OPTS) $(LLAMA) $(WHISPER) + cmake --build $(WHISPER_BUILD) --config Release -j$(CPUS) --target whisper + touch $@ + +$(BUILD_DIR)/miniaudio.stamp: + @mkdir -p $(MINIAUDIO_BUILD) + @# miniaudio is header-only — the stamp exists so CI can cache the slot + @# uniformly with llama/whisper. + touch $@ + +$(BUILD_DIR)/mbedtls.stamp: + @mkdir -p $(BUILD_DIR) cd $(MBEDTLS_DIR) && git submodule update --init + @# MBEDTLS_FATAL_WARNINGS=OFF: mbedtls 3.6.5 has a `%d` printf format + @# vs `time_t` mismatch on MinGW that hits -Werror=format. Same upstream + @# code is fine on glibc/macOS where time_t is `long`, so disable globally + @# rather than per-platform. cmake -B $(MBEDTLS_BUILD) -S $(MBEDTLS_DIR) \ -DENABLE_TESTING=OFF -DENABLE_PROGRAMS=OFF \ - -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON - cmake --build $(MBEDTLS_BUILD) -j$$(sysctl -n hw.ncpu 2>/dev/null || nproc) + -DMBEDTLS_FATAL_WARNINGS=OFF \ + $(CMAKE_DEP_OPTS) $(PLATFORM_OPTS) + cmake --build $(MBEDTLS_BUILD) --config Release -j$(CPUS) + touch $@ -curl: mbedtls +$(BUILD_DIR)/curl.stamp: $(BUILD_DIR)/mbedtls.stamp cmake -B $(CURL_BUILD) -S $(CURL_DIR) \ -DCURL_USE_MBEDTLS=ON -DCURL_USE_OPENSSL=OFF \ -DCURL_DISABLE_NTLM=ON -DCURL_DISABLE_LDAP=ON -DCURL_DISABLE_LDAPS=ON \ -DCURL_BROTLI=OFF -DCURL_ZSTD=OFF -DUSE_NGHTTP2=OFF \ -DUSE_LIBIDN2=OFF -DCURL_USE_LIBPSL=OFF -DCURL_USE_LIBSSH2=OFF \ -DBUILD_SHARED_LIBS=OFF -DBUILD_CURL_EXE=OFF -DBUILD_TESTING=OFF \ - -DCMAKE_BUILD_TYPE=Release \ -DMBEDTLS_INCLUDE_DIR=$(MBEDTLS_DIR)/include \ -DMBEDTLS_LIBRARY=$(MBEDTLS_BUILD)/library/libmbedtls.a \ -DMBEDX509_LIBRARY=$(MBEDTLS_BUILD)/library/libmbedx509.a \ - -DMBEDCRYPTO_LIBRARY=$(MBEDTLS_BUILD)/library/libmbedcrypto.a - cmake --build $(CURL_BUILD) -j$$(sysctl -n hw.ncpu 2>/dev/null || nproc) + -DMBEDCRYPTO_LIBRARY=$(MBEDTLS_BUILD)/library/libmbedcrypto.a \ + $(CMAKE_DEP_OPTS) $(PLATFORM_OPTS) + cmake --build $(CURL_BUILD) --config Release -j$(CPUS) + touch $@ + +# Backwards-compatible aliases so existing developer commands still work. +llama: $(BUILD_DIR)/llama.cpp.stamp +whisper: $(BUILD_DIR)/whisper.cpp.stamp +mbedtls: $(BUILD_DIR)/mbedtls.stamp +curl: $(BUILD_DIR)/curl.stamp # --- WASM (Emscripten) --- @@ -322,10 +635,141 @@ adam.js: $(WASM_SRCS) -sMODULARIZE=1 -sEXPORT_NAME=AdamModule \ -o $@ +# ============================================================================ +# CI / packaging targets — built by .github/workflows/main.yml +# ============================================================================ + +# Print the version string from src/adam.h. Consumed by the release job. +version: + @sed -n 's/^#define ADAM_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' src/adam.h + +# Loadable SQLite extension: dist/adam.{dylib,so,dll}. +# Wraps extensions/sqlite/Makefile after libadam.a + dependencies are built. +# Non-Apple platforms (linux, windows, android) need libcurl + mbedtls. +EXT_DEPS := $(BUILD_DIR)/llama.cpp.stamp $(BUILD_DIR)/whisper.cpp.stamp $(BUILD_DIR)/miniaudio.stamp +ifeq ($(IS_APPLE),) + EXT_DEPS += $(BUILD_DIR)/mbedtls.stamp $(BUILD_DIR)/curl.stamp +endif + +# Set STRIP_DIST=0 to keep symbols when debugging the shipped artifact. +STRIP_DIST ?= 1 +ifeq ($(PLATFORM),android) + STRIP := $(ANDROID_NDK_BIN)/llvm-strip + STRIP_FLAGS := --strip-all +else ifneq ($(IS_APPLE),) + STRIP := strip + STRIP_FLAGS := -x +else + STRIP := strip + STRIP_FLAGS := --strip-unneeded +endif + +extension: $(DIST_DIR)/$(EXT_FILE) + +$(DIST_DIR)/$(EXT_FILE): $(EXT_DEPS) libadam.a + @mkdir -p $(DIST_DIR) + $(MAKE) -C extensions/sqlite all PLATFORM=$(PLATFORM) ARCH=$(ARCH) + cp extensions/sqlite/$(EXT_FILE) $(DIST_DIR)/$(EXT_FILE) +ifeq ($(STRIP_DIST),1) + $(STRIP) $(STRIP_FLAGS) $(DIST_DIR)/$(EXT_FILE) +endif + +# Apple XCFramework — builds adam.dylib three times (macos, ios, ios-sim) and +# bundles them into dist/adam.xcframework with framework metadata. +LIB_NAMES := ios.dylib ios-sim.dylib macos.dylib +FMWK_NAMES := ios-arm64 ios-arm64_x86_64-simulator macos-arm64_x86_64 + +.NOTPARALLEL: %.dylib +%.dylib: + @# Clean enough to force a per-platform rebuild WITHOUT wiping dist/ + @# (each iteration produces a renamed dylib in dist/ that the xcframework + @# bundling step needs). + rm -rf $(BUILD_DIR) libadam.a src/*.o $(SQLITE_OBJ) $(SQLITE_VECTOR_OBJS) $(SQLITE_MEMORY_OBJS) + $(MAKE) -C extensions/sqlite clean + $(MAKE) extension PLATFORM=$* LLAMA="$(LLAMA)" WHISPER="$(WHISPER)" MINIAUDIO="$(MINIAUDIO)" + mv $(DIST_DIR)/adam.dylib $(DIST_DIR)/$@ + +define ADAM_PLIST +\ +\ +\ +\ +CFBundleDevelopmentRegionen\ +CFBundleExecutableadam\ +CFBundleIdentifierai.sqlite.adam\ +CFBundleInfoDictionaryVersion6.0\ +CFBundlePackageTypeFMWK\ +CFBundleSignature????\ +CFBundleVersion$(shell make -s version)\ +CFBundleShortVersionString$(shell make -s version)\ +MinimumOSVersion11.0\ + +endef + +define ADAM_MODULEMAP +framework module adam {\ + umbrella header \"adam.h\"\ + export *\ +} +endef + +$(DIST_DIR)/%.xcframework: $(LIB_NAMES) + @$(foreach i,1 2,\ + lib=$(word $(i),$(LIB_NAMES)); \ + fmwk=$(word $(i),$(FMWK_NAMES)); \ + mkdir -p $(DIST_DIR)/$$fmwk/adam.framework/Headers; \ + mkdir -p $(DIST_DIR)/$$fmwk/adam.framework/Modules; \ + cp src/adam.h $(DIST_DIR)/$$fmwk/adam.framework/Headers; \ + printf "$(ADAM_PLIST)" > $(DIST_DIR)/$$fmwk/adam.framework/Info.plist; \ + printf "$(ADAM_MODULEMAP)" > $(DIST_DIR)/$$fmwk/adam.framework/Modules/module.modulemap; \ + mv $(DIST_DIR)/$$lib $(DIST_DIR)/$$fmwk/adam.framework/adam; \ + install_name_tool -id "@rpath/adam.framework/adam" $(DIST_DIR)/$$fmwk/adam.framework/adam; \ + ) + @lib=$(word 3,$(LIB_NAMES)); \ + fmwk=$(word 3,$(FMWK_NAMES)); \ + mkdir -p $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/Headers; \ + mkdir -p $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/Modules; \ + mkdir -p $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/Resources; \ + cp src/adam.h $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/Headers; \ + printf "$(ADAM_PLIST)" > $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/Resources/Info.plist; \ + printf "$(ADAM_MODULEMAP)" > $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/Modules/module.modulemap; \ + mv $(DIST_DIR)/$$lib $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/adam; \ + install_name_tool -id "@rpath/adam.framework/adam" $(DIST_DIR)/$$fmwk/adam.framework/Versions/A/adam; \ + ln -sf A $(DIST_DIR)/$$fmwk/adam.framework/Versions/Current; \ + ln -sf Versions/Current/adam $(DIST_DIR)/$$fmwk/adam.framework/adam; \ + ln -sf Versions/Current/Headers $(DIST_DIR)/$$fmwk/adam.framework/Headers; \ + ln -sf Versions/Current/Modules $(DIST_DIR)/$$fmwk/adam.framework/Modules; \ + ln -sf Versions/Current/Resources $(DIST_DIR)/$$fmwk/adam.framework/Resources; + xcodebuild -create-xcframework $(foreach fmwk,$(FMWK_NAMES),-framework $(DIST_DIR)/$(fmwk)/adam.framework) -output $@ + rm -rf $(foreach fmwk,$(FMWK_NAMES),$(DIST_DIR)/$(fmwk)) + +xcframework: $(DIST_DIR)/adam.xcframework + +# Android AAR — builds adam.so for arm64-v8a + x86_64, drops them into +# packages/android/src/main/jniLibs/, then runs Gradle to assemble the AAR. +AAR_ARM64 := packages/android/src/main/jniLibs/arm64-v8a/ +AAR_X86 := packages/android/src/main/jniLibs/x86_64/ +aar: + mkdir -p $(AAR_ARM64) $(AAR_X86) + $(MAKE) clean + $(MAKE) extension PLATFORM=android ARCH=arm64-v8a + mv $(DIST_DIR)/adam.so $(AAR_ARM64) + $(MAKE) clean + $(MAKE) extension PLATFORM=android ARCH=x86_64 + mv $(DIST_DIR)/adam.so $(AAR_X86) + cd packages/android && ./gradlew clean assembleRelease + @mkdir -p $(DIST_DIR) + cp packages/android/build/outputs/aar/android-release.aar $(DIST_DIR)/adam.aar + # --- Clean --- clean: - rm -f $(OBJS) $(NET_OBJ) $(TTS_SYS_OBJ) $(SQLITE_OBJ) libadam.a adam test_adam test_live test_chat test_memory test_evolve test_voice_interactive test_voice_talk test_tools test_vision + rm -f $(OBJS) $(SQLITE_OBJ) libadam.a adam test_adam test_live test_chat test_memory test_evolve test_voice_interactive test_voice_talk test_tools test_vision + @# Wipe ALL platform-specific net/tts objects, not just the current PLATFORM's, + @# so switching PLATFORM=android → PLATFORM=ios doesn't leave stale arch objects. + rm -f src/adam_net_apple.o src/adam_net_curl.o src/adam_tts_system.o rm -f $(SQLITE_VECTOR_OBJS) $(SQLITE_MEMORY_OBJS) $(SQLITE_MEMORY_HTTP_OBJ) rm -rf *.dSYM rm -f adam.js adam.wasm + rm -rf $(BUILD_DIR) $(DIST_DIR) + $(MAKE) -C extensions/sqlite clean 2>/dev/null || true diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..f03225e --- /dev/null +++ b/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "adam", + platforms: [.macOS(.v11), .iOS(.v14)], + products: [ + .library( + name: "adam", + targets: ["adam"]) + ], + targets: [ + .binaryTarget( + name: "adamBinary", + // The url + checksum below are auto-updated by .github/workflows/main.yml + // on every release (see the "update Package.swift checksum and version" + // step). The placeholder values let `swift package describe` succeed + // before the first release; consumers should pin to a tagged version. + url: "https://github.com/sqliteai/adam/releases/download/0.7.0/adam-apple-xcframework-0.7.0.zip", + checksum: "0000000000000000000000000000000000000000000000000000000000000000" + ), + .target( + name: "adam", + dependencies: ["adamBinary"], + path: "packages/swift" + ), + ] +) diff --git a/extensions/postgres/Makefile b/extensions/postgres/Makefile index f5d3d25..5e4a9a2 100644 --- a/extensions/postgres/Makefile +++ b/extensions/postgres/Makefile @@ -14,10 +14,10 @@ OBJS = pg_adam.o \ ../src/adam_ext_schema.o \ ../src/adam_ext_chat.o -# Adam library and dependencies +# Adam library and dependencies (build tree shared with extensions/sqlite via top-level build/). ADAM_LIB := $(ADAM_ROOT)/libadam.a -LLAMA_BUILD := $(ADAM_ROOT)/modules/llama.cpp/build -WHISPER_BUILD := $(ADAM_ROOT)/modules/whisper.cpp/build +LLAMA_BUILD := $(ADAM_ROOT)/build/llama.cpp +WHISPER_BUILD := $(ADAM_ROOT)/build/whisper.cpp SHLIB_LINK := $(ADAM_LIB) \ $(WHISPER_BUILD)/src/libwhisper.a \ @@ -39,10 +39,10 @@ ifeq ($(UNAME_S),Darwin) -framework AVFoundation else SHLIB_LINK := -Wl,--no-as-needed $(SHLIB_LINK) -lgomp \ - $(ADAM_ROOT)/modules/curl/build/lib/libcurl.a \ - $(ADAM_ROOT)/modules/mbedtls/build/library/libmbedtls.a \ - $(ADAM_ROOT)/modules/mbedtls/build/library/libmbedx509.a \ - $(ADAM_ROOT)/modules/mbedtls/build/library/libmbedcrypto.a \ + $(ADAM_ROOT)/build/curl/lib/libcurl.a \ + $(ADAM_ROOT)/build/mbedtls/library/libmbedtls.a \ + $(ADAM_ROOT)/build/mbedtls/library/libmbedx509.a \ + $(ADAM_ROOT)/build/mbedtls/library/libmbedcrypto.a \ -ldl -lm endif diff --git a/extensions/sqlite/Makefile b/extensions/sqlite/Makefile index ce6c81a..bd2f43a 100644 --- a/extensions/sqlite/Makefile +++ b/extensions/sqlite/Makefile @@ -1,11 +1,65 @@ # SQLite Adam Extension -# Builds adam.dylib (macOS) / adam.so (Linux) loadable extension +# Builds the loadable extension: adam.dylib (macOS/iOS) / adam.so (Linux/Android) / adam.dll (Windows) +# +# Driven by the top-level Makefile via `make extension`. Honors PLATFORM and ARCH +# the same way the parent does so cross-compile (Android, iOS) works end-to-end. ADAM_ROOT := ../.. -UNAME_S := $(shell uname -s) -CC ?= cc -CFLAGS := -std=c11 -Wall -Wextra -O2 -fPIC +# ---------------------------------------------------------------------------- +# Platform / arch detection (matches top-level Makefile) +# ---------------------------------------------------------------------------- +ifeq ($(OS),Windows_NT) + PLATFORM ?= windows + HOST := windows +else + HOST := $(shell uname -s | tr '[:upper:]' '[:lower:]') + ifeq ($(HOST),darwin) + PLATFORM ?= macos + else + PLATFORM ?= $(HOST) + endif +endif + +# Compiler — Android overrides via NDK toolchain; everything else uses cc. +CC ?= cc + +ifeq ($(PLATFORM),android) + ifndef ARCH + $(error Android ARCH must be set to ARCH=x86_64 or ARCH=arm64-v8a) + endif + ifndef ANDROID_NDK + $(error ANDROID_NDK must point to the Android NDK install) + endif + ANDROID_NDK_BIN := $(ANDROID_NDK)/toolchains/llvm/prebuilt/$(HOST)-x86_64/bin + ifneq (,$(filter $(ARCH),arm64 arm64-v8a)) + NDK_TRIPLE := aarch64-linux-android26 + else + NDK_TRIPLE := $(ARCH)-linux-android26 + endif + CC := $(ANDROID_NDK_BIN)/$(NDK_TRIPLE)-clang + LD := $(ANDROID_NDK_BIN)/$(NDK_TRIPLE)-clang++ +else + # Use the C++ driver for the final link on every platform. libllama / + # libmtmd / libwhisper are C++ archives that reference typeinfo symbols + # like _ZTVN10__cxxabiv117__class_type_infoE. With the C driver and GNU + # ld's --as-needed default, the -lstdc++ in LDFLAGS gets dropped (it + # appears before the objects), and dlopen fails at load. macOS' linker + # is permissive enough that `cc` happened to work there, but we make it + # consistent. `c++` resolves to clang++ on Apple, g++ on Linux/MinGW. + LD := c++ +endif + +# ---------------------------------------------------------------------------- +# Common flags +# ---------------------------------------------------------------------------- +# Feature-test macros: keep BSD/GNU/POSIX extensions visible on every libc. +# glibc strict-C11 hides strdup / strcasecmp / clock_gettime; Apple needs +# _DARWIN_C_SOURCE; MinGW needs _GNU_SOURCE for strndup. Defining all three +# is safe — each platform only honors the ones it knows about. +# Section flags pair with -Wl,--gc-sections / -Wl,-dead_strip below. +CFLAGS := -std=gnu11 -Wall -Wextra -O2 -fPIC -ffunction-sections -fdata-sections -flto +CFLAGS += -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE -D_DARWIN_C_SOURCE=1 CFLAGS += -I$(ADAM_ROOT)/src CFLAGS += -I$(ADAM_ROOT)/modules/sqlite CFLAGS += -I$(ADAM_ROOT)/modules/miniaudio @@ -25,47 +79,195 @@ SQLITE_SRCS := sqlite_adam.c ADAM_LIB := $(ADAM_ROOT)/libadam.a -LLAMA_BUILD := $(ADAM_ROOT)/modules/llama.cpp/build -WHISPER_BUILD := $(ADAM_ROOT)/modules/whisper.cpp/build +# Dependency builds live under the top-level build/ tree. +LLAMA_BUILD := $(ADAM_ROOT)/build/llama.cpp +WHISPER_BUILD := $(ADAM_ROOT)/build/whisper.cpp +MBEDTLS_BUILD := $(ADAM_ROOT)/build/mbedtls +CURL_BUILD := $(ADAM_ROOT)/build/curl +# ggml's CMakeLists sets VERSION/SOVERSION on its static targets, which +# on Windows MinGW + Ninja causes cmake to drop the `lib` prefix even +# with -DCMAKE_STATIC_LIBRARY_PREFIX=lib in the cache. Resolve each lib +# via $(wildcard …) so we pick up whichever name was produced. +ggml_lib = $(firstword $(wildcard $(LLAMA_BUILD)/ggml/src/lib$(1).a $(LLAMA_BUILD)/ggml/src/$(1).a)) LLAMA_LIBS := $(LLAMA_BUILD)/tools/mtmd/libmtmd.a \ $(LLAMA_BUILD)/src/libllama.a \ - $(LLAMA_BUILD)/ggml/src/libggml.a \ - $(LLAMA_BUILD)/ggml/src/libggml-cpu.a \ - $(LLAMA_BUILD)/ggml/src/libggml-base.a + $(call ggml_lib,ggml) \ + $(call ggml_lib,ggml-cpu) \ + $(call ggml_lib,ggml-base) WHISPER_LIBS := $(WHISPER_BUILD)/src/libwhisper.a -LDFLAGS := -lpthread -lz +# Pick up CoreML stub when whisper was built with -DWHISPER_COREML=ON. +# wildcard returns empty when the file isn't there, so non-CoreML builds +# are unaffected. +WHISPER_COREML_LIB := $(wildcard $(WHISPER_BUILD)/src/libwhisper.coreml.a) +ifneq ($(WHISPER_COREML_LIB),) + WHISPER_LIBS += $(WHISPER_COREML_LIB) + COREML_FRAMEWORK := -framework CoreML +endif + +# Pick up GPU backends if llama.cpp was configured with the matching flag. +# Check both naming conventions (lib prefix may be stripped on Windows MinGW). +GGML_BLAS_LIB := $(firstword $(wildcard $(LLAMA_BUILD)/ggml/src/ggml-blas/libggml-blas.a $(LLAMA_BUILD)/ggml/src/ggml-blas/ggml-blas.a)) +GGML_VULKAN_LIB := $(firstword $(wildcard $(LLAMA_BUILD)/ggml/src/ggml-vulkan/libggml-vulkan.a $(LLAMA_BUILD)/ggml/src/ggml-vulkan/ggml-vulkan.a)) +GGML_OPENCL_LIB := $(firstword $(wildcard $(LLAMA_BUILD)/ggml/src/ggml-opencl/libggml-opencl.a $(LLAMA_BUILD)/ggml/src/ggml-opencl/ggml-opencl.a)) +ifneq ($(GGML_BLAS_LIB),) + LLAMA_LIBS += $(GGML_BLAS_LIB) +endif +ifneq ($(GGML_VULKAN_LIB),) + LLAMA_LIBS += $(GGML_VULKAN_LIB) +endif +ifneq ($(GGML_OPENCL_LIB),) + LLAMA_LIBS += $(GGML_OPENCL_LIB) +endif + +# Default link libs. -lz is appended to LIBS (not LDFLAGS) for every +# platform that uses libcurl — libcurl references inflate*, and with +# single-pass GNU ld + --as-needed, -lz must sit AFTER libcurl.a or +# libz.so gets dropped from DT_NEEDED. glibc loaders happen to find +# inflateEnd through the global namespace anyway; musl is strict and +# fails dlopen with "Error relocating: inflateEnd: symbol not found". +ifeq ($(PLATFORM),android) + LDFLAGS := +else ifeq ($(PLATFORM),windows) + LDFLAGS := +else + LDFLAGS := -lpthread +endif + +# Dead-code elimination at link time + LTO across the wrapper objects and +# the input static archives (those built with -DCMAKE_INTERPROCEDURAL_- +# OPTIMIZATION=ON ship LLVM bitcode, so this can also collapse calls into +# llama / whisper / curl / mbedtls). +ifneq (,$(filter $(PLATFORM),macos ios ios-sim)) + LDFLAGS += -Wl,-dead_strip -flto +else + LDFLAGS += -Wl,--gc-sections -flto +endif -ifeq ($(UNAME_S),Darwin) +# ---------------------------------------------------------------------------- +# Per-platform link / ext settings +# ---------------------------------------------------------------------------- +ifeq ($(PLATFORM),macos) + EXT := dylib CFLAGS += -DADAM_NO_CURL - EXT := dylib - LDFLAGS += -dynamiclib + ifndef ARCH + CFLAGS += -arch x86_64 -arch arm64 + LDFLAGS += -arch x86_64 -arch arm64 + else + CFLAGS += -arch $(ARCH) + LDFLAGS += -arch $(ARCH) + endif + LDFLAGS += -dynamiclib -headerpad_max_install_names LDFLAGS += -framework Foundation -framework SystemConfiguration -framework Security + LDFLAGS += -framework CoreAudio -framework AudioToolbox -framework AVFoundation + LDFLAGS += -framework Metal -framework MetalKit -framework Accelerate $(COREML_FRAMEWORK) + LDFLAGS += -lstdc++ + LLAMA_LIBS += $(LLAMA_BUILD)/ggml/src/ggml-metal/libggml-metal.a + LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) +else ifeq ($(PLATFORM),ios) + EXT := dylib + CFLAGS += -DADAM_NO_CURL -arch arm64 -x objective-c + CFLAGS += -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) -miphoneos-version-min=14.0 + LDFLAGS += -dynamiclib -headerpad_max_install_names + LDFLAGS += -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) -miphoneos-version-min=14.0 + LDFLAGS += -framework Foundation -framework Security -framework AVFoundation LDFLAGS += -framework CoreAudio -framework AudioToolbox - LDFLAGS += -framework Metal -framework MetalKit -framework Accelerate - LDFLAGS += -lstdc++ -framework AVFoundation + LDFLAGS += -framework Metal -framework Accelerate $(COREML_FRAMEWORK) + LDFLAGS += -lstdc++ LLAMA_LIBS += $(LLAMA_BUILD)/ggml/src/ggml-metal/libggml-metal.a - LLAMA_LIBS += $(LLAMA_BUILD)/ggml/src/ggml-blas/libggml-blas.a LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) +else ifeq ($(PLATFORM),ios-sim) + EXT := dylib + CFLAGS += -DADAM_NO_CURL -arch x86_64 -arch arm64 -x objective-c + CFLAGS += -isysroot $(shell xcrun --sdk iphonesimulator --show-sdk-path) -miphonesimulator-version-min=14.0 + LDFLAGS += -arch x86_64 -arch arm64 -dynamiclib -headerpad_max_install_names + LDFLAGS += -isysroot $(shell xcrun --sdk iphonesimulator --show-sdk-path) -miphonesimulator-version-min=14.0 + LDFLAGS += -framework Foundation -framework Security -framework AVFoundation + LDFLAGS += -framework CoreAudio -framework AudioToolbox + LDFLAGS += -framework Metal -framework Accelerate $(COREML_FRAMEWORK) + LDFLAGS += -lstdc++ + LLAMA_LIBS += $(LLAMA_BUILD)/ggml/src/ggml-metal/libggml-metal.a + LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) +else ifeq ($(PLATFORM),windows) + EXT := dll + # -DCURL_STATICLIB: curl.h on Windows declares functions as dllimport + # by default; we link the static libcurl.a so undo that. + CFLAGS += -I$(ADAM_ROOT)/modules/curl/include -I$(ADAM_ROOT)/modules/mbedtls/include -DCURL_STATICLIB + LDFLAGS += -shared -static-libgcc + LDFLAGS += -Wl,--push-state,-Bstatic,-lstdc++,-lwinpthread,--pop-state + LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) + LIBS += $(CURL_BUILD)/lib/libcurl.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedtls.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedx509.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedcrypto.a + # System libs MUST come after the static archives that reference them + # (GNU ld is single-pass left-to-right). curl needs Winsock + crypt32; + # mbedtls needs bcrypt for BCryptGenRandom; curl uses zlib for gzip + # content-encoding. + LIBS += -lws2_32 -lcrypt32 -lbcrypt -lz +else ifeq ($(PLATFORM),android) + EXT := so + # -static-libstdc++ now takes effect because the final link uses clang++ + # (see PLATFORM=android block above); it bundles libc++_static into + # adam.so so it loads on devices without the NDK shared C++ runtime. + LDFLAGS += -shared -static-libstdc++ -ldl -lm -Wl,-z,max-page-size=16384 + CFLAGS += -I$(ADAM_ROOT)/modules/curl/include -I$(ADAM_ROOT)/modules/mbedtls/include + LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) + LIBS += $(CURL_BUILD)/lib/libcurl.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedtls.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedx509.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedcrypto.a + LIBS += -lz else - EXT := so + # linux (default) + EXT := so LDFLAGS += -shared -ldl -lm -lstdc++ CFLAGS += -I$(ADAM_ROOT)/modules/curl/include -I$(ADAM_ROOT)/modules/mbedtls/include LIBS := $(WHISPER_LIBS) $(LLAMA_LIBS) - LIBS += $(ADAM_ROOT)/modules/curl/build/lib/libcurl.a - LIBS += $(ADAM_ROOT)/modules/mbedtls/build/library/libmbedtls.a - LIBS += $(ADAM_ROOT)/modules/mbedtls/build/library/libmbedx509.a - LIBS += $(ADAM_ROOT)/modules/mbedtls/build/library/libmbedcrypto.a + LIBS += $(CURL_BUILD)/lib/libcurl.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedtls.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedx509.a + LIBS += $(MBEDTLS_BUILD)/library/libmbedcrypto.a + LIBS += -lz +endif + +# Vulkan / OpenCL runtime loaders. Must come AFTER the ggml-vulkan/-opencl +# static libs in LIBS (single-pass GNU ld). Only added when the matching +# backend was built (wildcard above resolved); Apple's Metal path skips +# these — neither GGML_VULKAN_LIB nor GGML_OPENCL_LIB is set on Apple. +# On Windows the Vulkan loader DLL is `vulkan-1.dll` (with the -1 suffix +# baked in) and the corresponding import library is libvulkan-1.dll.a, so +# the linker flag is `-lvulkan-1`, not `-lvulkan`. +ifneq ($(GGML_VULKAN_LIB),) + ifeq ($(PLATFORM),windows) + LIBS += -lvulkan-1 + else + LIBS += -lvulkan + endif +endif +ifneq ($(GGML_OPENCL_LIB),) + LIBS += -lOpenCL endif .PHONY: all clean test all: adam.$(EXT) -adam.$(EXT): $(SQLITE_SRCS) $(SHARED_SRCS) $(ADAM_LIB) - $(CC) $(CFLAGS) $(LDFLAGS) $(SQLITE_SRCS) $(SHARED_SRCS) \ - $(ADAM_LIB) $(LIBS) -o $@ +SQLITE_OBJS := $(SQLITE_SRCS:.c=.o) +SHARED_OBJS := $(SHARED_SRCS:.c=.o) + +# Compile rules — use $(CC) (the C driver) with -std=c11. +$(SQLITE_OBJS): %.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +$(SHARED_OBJS): %.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +# Link rule — use $(LD), which is $(CC) everywhere except Android where it +# is clang++ so that -static-libstdc++ pulls in libc++_static. +adam.$(EXT): $(SQLITE_OBJS) $(SHARED_OBJS) $(ADAM_LIB) + $(LD) $(LDFLAGS) $(SQLITE_OBJS) $(SHARED_OBJS) $(ADAM_LIB) $(LIBS) -o $@ test: adam.$(EXT) @echo "Testing adam SQLite extension..." @@ -77,4 +279,5 @@ test: adam.$(EXT) @rm -f /tmp/adam_ext_test.sql clean: - rm -f adam.$(EXT) adam.dylib adam.so + rm -f adam.dylib adam.so adam.dll adam.lib adam.def + rm -f $(SQLITE_OBJS) $(SHARED_OBJS) diff --git a/packages/android/build.gradle b/packages/android/build.gradle new file mode 100644 index 0000000..68c8c0e --- /dev/null +++ b/packages/android/build.gradle @@ -0,0 +1,121 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.5.2' + } +} + +plugins { + id 'com.gradleup.nmcp.aggregation' version '1.2.0' +} + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + namespace 'ai.sqlite.adam' + compileSdk 34 + + defaultConfig { + minSdk 26 + targetSdk 34 + } + + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + jniLibs.srcDirs = ['src/main/jniLibs'] + } + } +} + +repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + groupId = 'ai.sqlite' + artifactId = 'adam' + version = project.hasProperty('VERSION') ? project.VERSION : ['make', 'version'].execute(null, file('../..')).text.trim() + + artifact(project.hasProperty('AAR_PATH') ? project.AAR_PATH : "$buildDir/outputs/aar/android-release.aar") + + // Maven Central metadata + pom { + name = 'adam' + description = 'Adam is an embeddable AI agent library for SQLite, providing on-device chat, voice, vision, memory, and tool-calling. Works on iOS, Android, Windows, Linux, and macOS.' + url = 'https://github.com/sqliteai/adam' + + licenses { + license { + name = 'Elastic License 2.0' + url = 'https://www.elastic.co/licensing/elastic-license' + } + } + + developers { + developer { + id = 'sqliteai' + name = 'SQLite Cloud, Inc.' + email = 'info@sqlitecloud.io' + organization = 'SQLite Cloud, Inc.' + organizationUrl = 'https://sqlite.ai' + } + } + + scm { + connection = 'scm:git:git://github.com/sqliteai/adam.git' + developerConnection = 'scm:git:ssh://github.com:sqliteai/adam.git' + url = 'https://github.com/sqliteai/adam/tree/main' + } + } + } + } + } + + // Signing configuration for Maven Central + signing { + required { project.hasProperty("SIGNING_KEY") } + if (project.hasProperty("SIGNING_KEY")) { + useInMemoryPgpKeys( + project.property("SIGNING_KEY").toString(), + project.property("SIGNING_PASSWORD").toString() + ) + sign publishing.publications.release + } + } +} + +// Maven Central publishing via NMCP aggregation +nmcpAggregation { + if (project.hasProperty("SONATYPE_USERNAME") && project.hasProperty("SONATYPE_PASSWORD")) { + centralPortal { + username = project.property("SONATYPE_USERNAME") + password = project.property("SONATYPE_PASSWORD") + publishingType = "AUTOMATIC" + } + publishAllProjectsProbablyBreakingProjectIsolation() + } +} diff --git a/packages/android/gradle.properties b/packages/android/gradle.properties new file mode 100644 index 0000000..ca684b2 --- /dev/null +++ b/packages/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true \ No newline at end of file diff --git a/packages/android/gradle/wrapper/gradle-wrapper.jar b/packages/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/packages/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/android/gradle/wrapper/gradle-wrapper.properties b/packages/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df97d72 --- /dev/null +++ b/packages/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/android/gradlew b/packages/android/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/packages/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/packages/android/gradlew.bat b/packages/android/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/packages/android/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/android/src/main/AndroidManifest.xml b/packages/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/packages/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/packages/flutter/.pubignore b/packages/flutter/.pubignore new file mode 100644 index 0000000..36c5a16 --- /dev/null +++ b/packages/flutter/.pubignore @@ -0,0 +1,7 @@ +# Only ignore development files, NOT native_libraries +.dart_tool/ +pubspec.lock + +# Explicitly include native_libraries (override any parent .gitignore) +!native_libraries/ +!native_libraries/** diff --git a/packages/flutter/LICENSE b/packages/flutter/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/flutter/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/flutter/README.md b/packages/flutter/README.md new file mode 100644 index 0000000..bee37e4 --- /dev/null +++ b/packages/flutter/README.md @@ -0,0 +1,74 @@ +# sqlite_adam + +Adam is an embeddable AI agent library for SQLite, providing on-device chat, voice, vision, memory, and tool-calling. It works seamlessly on iOS, Android, Windows, Linux, and macOS, integrating directly with your embedded SQLite database. + +## Installation + +``` +dart pub add sqlite_adam +``` + +Requires Dart 3.10+ / Flutter 3.38+. + +## Usage + +### With `sqlite3` + +```dart +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite_adam/sqlite_adam.dart'; + +void main() { + // Load once at startup. + sqlite3.loadSqliteAdamExtension(); + + final db = sqlite3.openInMemory(); + + // Configure the agent (see API.md for the full list of options). + db.execute("SELECT adam_config('provider', 'anthropic')"); + db.execute("SELECT adam_config('api_key', 'sk-...')"); + + // Run the agent. + final result = db.select("SELECT adam('Say hello in 3 words')"); + print(result.first[0]); + + db.dispose(); +} +``` + +### With `drift` + +```dart +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite_adam/sqlite_adam.dart'; +import 'package:drift/native.dart'; + +Sqlite3 loadExtensions() { + sqlite3.loadSqliteAdamExtension(); + return sqlite3; +} + +// Use when creating the database: +NativeDatabase.createInBackground( + File(path), + sqlite3: loadExtensions, +); +``` + +## Supported platforms + +| Platform | Architectures | +|----------|---------------| +| Android | arm64, x64 | +| iOS | arm64 (device + simulator) | +| macOS | arm64, x64 | +| Linux | arm64, x64 | +| Windows | x64 | + +## API + +See the full [Adam API documentation](https://github.com/sqliteai/adam/blob/main/API.md). + +## License + +See [LICENSE](LICENSE). diff --git a/packages/flutter/analysis_options.yaml b/packages/flutter/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/packages/flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/packages/flutter/hook/build.dart b/packages/flutter/hook/build.dart new file mode 100644 index 0000000..f060df1 --- /dev/null +++ b/packages/flutter/hook/build.dart @@ -0,0 +1,139 @@ +// Copyright (c) 2026 SQLite Cloud, Inc. +// Licensed under the Elastic License 2.0 (see LICENSE). + +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:hooks/hooks.dart'; +import 'package:path/path.dart' as p; + +void main(List args) async { + await build(args, (input, output) async { + if (!input.config.buildCodeAssets) return; + + final codeConfig = input.config.code; + final os = codeConfig.targetOS; + final arch = codeConfig.targetArchitecture; + + final binaryPath = _resolveBinaryPath(os, arch, codeConfig); + if (binaryPath == null) { + throw UnsupportedError('sqlite_adam does not support $os $arch.'); + } + + final nativeLibDir = p.join( + input.packageRoot.toFilePath(), + 'native_libraries', + ); + final file = File(p.join(nativeLibDir, binaryPath)); + if (!file.existsSync()) { + throw StateError( + 'Pre-built binary not found: ${file.path}. ' + 'Run the CI pipeline to populate native_libraries/.', + ); + } + + output.dependencies.add(file.uri); + + final assetFile = await _prepareAssetFile( + input: input, + os: os, + arch: arch, + config: codeConfig, + file: file, + ); + + output.assets.code.add( + CodeAsset( + package: input.packageName, + name: 'src/native/sqlite_adam_extension.dart', + linkMode: DynamicLoadingBundled(), + file: assetFile.uri, + ), + ); + }); +} + +Future _prepareAssetFile({ + required BuildInput input, + required OS os, + required Architecture arch, + required CodeConfig config, + required File file, +}) async { + if (os != OS.iOS || config.iOS.targetSdk == IOSSdk.iPhoneOS) { + return file; + } + + final thinArch = switch (arch) { + Architecture.arm64 => 'arm64', + Architecture.x64 => 'x86_64', + _ => null, + }; + if (thinArch == null) { + return file; + } + + final outputName = 'adam_ios_sim_$thinArch.dylib'; + final outputFile = File.fromUri(input.outputDirectory.resolve(outputName)); + await outputFile.parent.create(recursive: true); + + final result = await Process.run('/usr/bin/lipo', [ + file.path, + '-thin', + thinArch, + '-output', + outputFile.path, + ]); + if (result.exitCode != 0) { + throw StateError( + 'Failed to thin sqlite_adam iOS simulator binary for $thinArch: ' + '${result.stderr}', + ); + } + + return outputFile; +} + +String? _resolveBinaryPath(OS os, Architecture arch, CodeConfig config) { + if (os == OS.android) { + return switch (arch) { + Architecture.arm64 => 'android/adam_android_arm64.so', + Architecture.x64 => 'android/adam_android_x64.so', + _ => null, + }; + } + + if (os == OS.iOS) { + final sdk = config.iOS.targetSdk; + if (sdk == IOSSdk.iPhoneOS) { + return 'ios/adam_ios_arm64.dylib'; + } + // Simulator: fat binary (arm64 + x64) + return 'ios-sim/adam_ios-sim.dylib'; + } + + if (os == OS.macOS) { + return switch (arch) { + Architecture.arm64 => 'mac/adam_mac_arm64.dylib', + Architecture.x64 => 'mac/adam_mac_x64.dylib', + _ => null, + }; + } + + if (os == OS.linux) { + return switch (arch) { + Architecture.x64 => 'linux/adam_linux_x64.so', + Architecture.arm64 => 'linux/adam_linux_arm64.so', + _ => null, + }; + } + + if (os == OS.windows) { + return switch (arch) { + Architecture.x64 => 'windows/adam_windows_x64.dll', + _ => null, + }; + } + + return null; +} diff --git a/packages/flutter/lib/sqlite_adam.dart b/packages/flutter/lib/sqlite_adam.dart new file mode 100644 index 0000000..73d0cb9 --- /dev/null +++ b/packages/flutter/lib/sqlite_adam.dart @@ -0,0 +1,6 @@ +// Copyright (c) 2026 SQLite Cloud, Inc. +// Licensed under the Elastic License 2.0 (see LICENSE). + +library sqlite_adam; + +export 'src/sqlite_adam.dart'; diff --git a/packages/flutter/lib/src/sqlite_adam.dart b/packages/flutter/lib/src/sqlite_adam.dart new file mode 100644 index 0000000..bac653e --- /dev/null +++ b/packages/flutter/lib/src/sqlite_adam.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2026 SQLite Cloud, Inc. +// Licensed under the Elastic License 2.0 (see LICENSE). + +import 'dart:ffi'; + +import 'package:sqlite3/sqlite3.dart'; + +// @Native resolves from the code asset declared in hook/build.dart. +// The asset ID is 'package:sqlite_adam/src/native/sqlite_adam_extension.dart'. +@Native, Pointer, Pointer)>( + assetId: 'package:sqlite_adam/src/native/sqlite_adam_extension.dart', +) +external int sqlite3_adam_init( + Pointer db, + Pointer pzErrMsg, + Pointer pApi, +); + +extension SqliteAdamExtension on Sqlite3 { + /// Loads the adam SQLite extension. + /// + /// Call once at app startup. All subsequently opened databases + /// will have the `adam_*` SQL functions available. + /// + /// Works with both `sqlite3` package and `drift` ORM. + void loadSqliteAdamExtension() { + ensureExtensionLoaded( + SqliteExtension( + Native.addressOf< + NativeFunction< + Int Function(Pointer, Pointer, Pointer)>>( + sqlite3_adam_init, + ).cast(), + ), + ); + } +} diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml new file mode 100644 index 0000000..a026fa9 --- /dev/null +++ b/packages/flutter/pubspec.yaml @@ -0,0 +1,22 @@ +name: sqlite_adam +version: 0.7.0 +description: > + Adam is an embeddable AI agent library for SQLite, providing on-device + chat, voice, vision, memory, and tool-calling. It works seamlessly on + iOS, Android, Windows, Linux, and macOS, integrating directly with + your embedded SQLite database. +homepage: https://github.com/sqliteai/adam +repository: https://github.com/sqliteai/adam + +environment: + sdk: ^3.10.0 + +dependencies: + code_assets: ^1.0.0 + hooks: ^1.0.0 + path: ^1.8.0 + sqlite3: ^3.0.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/packages/swift/adam.swift b/packages/swift/adam.swift new file mode 100644 index 0000000..62f035c --- /dev/null +++ b/packages/swift/adam.swift @@ -0,0 +1,15 @@ +// adam.swift +// Provides the path to the adam SQLite extension for use with sqlite3_load_extension. + +import Foundation + +public struct adam { + /// Returns the absolute path to the adam dylib for use with sqlite3_load_extension. + public static var path: String { + #if os(macOS) + return Bundle.main.bundlePath + "/Contents/Frameworks/adam.framework/adam" + #else + return Bundle.main.bundlePath + "/Frameworks/adam.framework/adam" + #endif + } +} diff --git a/src/adam.c b/src/adam.c index 1ec8195..da6e506 100644 --- a/src/adam.c +++ b/src/adam.c @@ -15,6 +15,7 @@ // --- Windows compatibility --- #ifdef _WIN32 #include + #include // _access #define realpath(p, r) _fullpath((r), (p), 260) static void adam_sleep_ms(int ms) { Sleep(ms); } #else @@ -338,7 +339,10 @@ adam_status_t adam_settings_allow_dir(adam_settings_t *s, const char *dir_path) // Resolve to canonical path char resolved[4096]; #ifdef _WIN32 + // _fullpath only normalizes — it does NOT check existence. To match + // POSIX realpath() semantics, follow up with _access(resolved, 0). if (!_fullpath(resolved, dir_path, sizeof(resolved))) return ADAM_ERR_INVALID_PARAM; + if (_access(resolved, 0) != 0) return ADAM_ERR_INVALID_PARAM; #elif defined(__EMSCRIPTEN__) // WASM: no realpath, just copy as-is snprintf(resolved, sizeof(resolved), "%s", dir_path); diff --git a/src/adam_audio.c b/src/adam_audio.c index 6cfa997..96d4b13 100644 --- a/src/adam_audio.c +++ b/src/adam_audio.c @@ -26,6 +26,12 @@ #include #include #include +#ifdef _WIN32 + #include + #define getpid _getpid +#else + #include +#endif // ============================================================================ // MARK: - Audio Playback diff --git a/src/adam_local.c b/src/adam_local.c index 927b694..7c9e04f 100644 --- a/src/adam_local.c +++ b/src/adam_local.c @@ -28,6 +28,28 @@ #include #endif +// Portable memmem. memmem is a GNU/BSD extension that glibc / musl / Apple +// / Android bionic / MinGW-UCRT expose, but MSVCRT-based MinGW (msys2's +// default `mingw64` toolchain) doesn't ship it at all. Fall back there. +#if defined(__MINGW32__) && !defined(_UCRT) +static void *adam_memmem(const void *haystack, size_t haystacklen, + const void *needle, size_t needlelen) { + if (needlelen == 0) return (void *)haystack; + if (haystacklen < needlelen) return NULL; + const unsigned char *h = (const unsigned char *)haystack; + const unsigned char *n = (const unsigned char *)needle; + size_t last = haystacklen - needlelen; + for (size_t i = 0; i <= last; i++) { + if (h[i] == n[0] && memcmp(h + i, n, needlelen) == 0) { + return (void *)(h + i); + } + } + return NULL; +} +#else +#define adam_memmem memmem +#endif + // ============================================================================ // MARK: - Helpers // ============================================================================ @@ -383,8 +405,8 @@ static size_t parse_local_tool_calls(arena_t *arena, const char *text, // Find "name" and "arguments" manually (avoid jsmn dependency) const char *name_key = "\"name\""; const char *args_key = "\"arguments\""; - const char *nk = memmem(json_start, json_len, name_key, 6); - const char *ak = memmem(json_start, json_len, args_key, 11); + const char *nk = adam_memmem(json_start, json_len, name_key, 6); + const char *ak = adam_memmem(json_start, json_len, args_key, 11); if (nk && tc_idx < count) { // Extract name: find the string value after "name": diff --git a/src/adam_session.c b/src/adam_session.c index 9586c2d..6b6a19e 100644 --- a/src/adam_session.c +++ b/src/adam_session.c @@ -18,6 +18,23 @@ #include #include +// Portable strndup. POSIX 2008 (glibc / musl / Apple / Android bionic / +// MinGW-UCRT) all expose strndup, so we alias straight to the system call. +// Only MSVCRT-based MinGW (msys2 `mingw64` toolchain, what GitHub Actions +// uses by default) doesn't ship strndup at all — fall back there. +#if defined(__MINGW32__) && !defined(_UCRT) +static inline char *adam_strndup(const char *s, size_t n) { + char *p = (char *)malloc(n + 1); + if (!p) return NULL; + size_t i; + for (i = 0; i < n && s[i]; i++) p[i] = s[i]; + p[i] = '\0'; + return p; +} +#else +#define adam_strndup strndup +#endif + // Internal: get the sqlite3* handle from adam_memory_t (defined in adam_memory.c) extern sqlite3 *adam_memory_db(adam_memory_t *mem); @@ -127,7 +144,7 @@ static void deserialize_tool_calls(const char *json, if (id_key && id_key < obj_end) { const char *v = strchr(id_key + 5, '"'); if (v) { v++; const char *e = strchr(v, '"'); - if (e) { calls[idx].id = strndup(v, (size_t)(e - v)); } + if (e) { calls[idx].id = adam_strndup(v, (size_t)(e - v)); } } } @@ -136,7 +153,7 @@ static void deserialize_tool_calls(const char *json, if (name_key && name_key < obj_end) { const char *v = strchr(name_key + 7, '"'); if (v) { v++; const char *e = strchr(v, '"'); - if (e) { calls[idx].name = strndup(v, (size_t)(e - v)); } + if (e) { calls[idx].name = adam_strndup(v, (size_t)(e - v)); } } } diff --git a/src/adam_tools.c b/src/adam_tools.c index c1593ce..0b7a599 100644 --- a/src/adam_tools.c +++ b/src/adam_tools.c @@ -615,8 +615,13 @@ adam_tool_result_t adam_tool_shell_exec(arena_t *arena, void *ctx, snprintf(full_cmd, cmd_size, "cd /d \"%s\" && cmd /c \"%s\" 2>&1", s->allowed_dirs[0], command); -#elif defined(__APPLE__) || defined(__ANDROID__) || defined(__ios__) - // macOS/iOS/Android: no GNU timeout command +#elif defined(__ANDROID__) + // Android has no /bin — sh lives at /system/bin/sh. + snprintf(full_cmd, cmd_size, + "cd \"%s\" && /system/bin/sh -c '%s' 2>&1", + s->allowed_dirs[0], command); +#elif defined(__APPLE__) || defined(__ios__) + // macOS/iOS: no GNU timeout command snprintf(full_cmd, cmd_size, "cd \"%s\" && /bin/sh -c '%s' 2>&1", s->allowed_dirs[0], command); diff --git a/test/test_adam.c b/test/test_adam.c index f58d74a..851c669 100644 --- a/test/test_adam.c +++ b/test/test_adam.c @@ -24,6 +24,25 @@ #include #include +#ifdef _WIN32 +#include +#define adam_mkdir(p, m) _mkdir(p) +#else +#define adam_mkdir(p, m) mkdir((p), (m)) +#endif + +// Platform-portable temp / second directory used by file-sandbox tests. +// Android's `/` is a read-only rootfs (no mkdir, no symlink); the writable +// scratch dir is `/data/local/tmp`. Other platforms keep the POSIX defaults. +// String concatenation lets us inline these into both plain paths +// (`TEST_TMP "/foo.db"`) and JSON args (`"{\"path\":\"" TEST_TMP "/foo\"}"`). +#ifdef __ANDROID__ + #define TEST_TMP "/data/local/tmp" + #define TEST_VAR "/data" +#else + #define TEST_TMP "/tmp" + #define TEST_VAR "/var" +#endif #ifndef ADAM_NO_PTHREADS #include @@ -2447,7 +2466,7 @@ TEST(local_with_stream_callback) { #include TEST(session_open_close) { - const char *path = "/tmp/adam_test_session.db"; + const char *path = TEST_TMP "/adam_test_session.db"; unlink(path); adam_memory_t *mem = adam_memory_open(path); ASSERT_NOT_NULL(mem); @@ -2456,7 +2475,7 @@ TEST(session_open_close) { } TEST(session_save_load_simple) { - const char *path = "/tmp/adam_test_session2.db"; + const char *path = TEST_TMP "/adam_test_session2.db"; unlink(path); adam_memory_t *mem = adam_memory_open(path); ASSERT_NOT_NULL(mem); @@ -2493,7 +2512,7 @@ TEST(session_save_load_simple) { } TEST(session_save_load_with_tool_calls) { - const char *path = "/tmp/adam_test_session3.db"; + const char *path = TEST_TMP "/adam_test_session3.db"; unlink(path); adam_memory_t *mem = adam_memory_open(path); @@ -2540,7 +2559,7 @@ TEST(session_save_load_with_tool_calls) { } TEST(session_list_and_delete) { - const char *path = "/tmp/adam_test_session4.db"; + const char *path = TEST_TMP "/adam_test_session4.db"; unlink(path); adam_memory_t *mem = adam_memory_open(path); @@ -2587,7 +2606,7 @@ TEST(session_list_and_delete) { TEST(session_overwrite) { // Saving the same session_id twice should replace the old data - const char *path = "/tmp/adam_test_session5.db"; + const char *path = TEST_TMP "/adam_test_session5.db"; unlink(path); adam_memory_t *mem = adam_memory_open(path); @@ -2616,7 +2635,7 @@ TEST(session_overwrite) { TEST(session_persistence_across_reopen) { // Data survives closing and reopening the database - const char *path = "/tmp/adam_test_session6.db"; + const char *path = TEST_TMP "/adam_test_session6.db"; unlink(path); // Open, save, close @@ -3546,29 +3565,29 @@ TEST(tool_file_read_sandbox) { ASSERT(strstr(r.for_llm, "denied") != NULL); // Allow /tmp - ASSERT_EQ(adam_settings_allow_dir(s, "/tmp"), ADAM_OK); + ASSERT_EQ(adam_settings_allow_dir(s, TEST_TMP), ADAM_OK); // Write a test file - FILE *f = fopen("/tmp/adam_test_read.txt", "w"); + FILE *f = fopen(TEST_TMP "/adam_test_read.txt", "w"); ASSERT_NOT_NULL(f); fprintf(f, "hello from adam test"); fclose(f); // Read it — should succeed arena_reset(a); - args = "{\"path\":\"/tmp/adam_test_read.txt\"}"; + args = "{\"path\":\"" TEST_TMP "/adam_test_read.txt\"}"; r = adam_tool_file_read(a, s, args, strlen(args)); ASSERT_EQ(r.success, 1); ASSERT_STR_EQ(r.for_llm, "hello from adam test"); // Try to escape sandbox arena_reset(a); - args = "{\"path\":\"/tmp/../etc/hosts\"}"; + args = "{\"path\":\"" TEST_TMP "/../etc/hosts\"}"; r = adam_tool_file_read(a, s, args, strlen(args)); ASSERT_EQ(r.success, 0); ASSERT(strstr(r.for_llm, "denied") != NULL); - remove("/tmp/adam_test_read.txt"); + remove(TEST_TMP "/adam_test_read.txt"); arena_destroy(a); adam_settings_destroy(s); } @@ -3576,17 +3595,17 @@ TEST(tool_file_read_sandbox) { TEST(tool_file_write_sandbox) { arena_t *a = arena_create(4096); adam_settings_t *s = adam_create_settings(); - ASSERT_EQ(adam_settings_allow_dir(s, "/tmp"), ADAM_OK); + ASSERT_EQ(adam_settings_allow_dir(s, TEST_TMP), ADAM_OK); // Write a file - const char *args = "{\"path\":\"/tmp/adam_test_write.txt\"," + const char *args = "{\"path\":\"" TEST_TMP "/adam_test_write.txt\"," "\"content\":\"test content\"}"; adam_tool_result_t r = adam_tool_file_write(a, s, args, strlen(args)); ASSERT_EQ(r.success, 1); ASSERT(strstr(r.for_llm, "Wrote") != NULL); // Verify contents - FILE *f = fopen("/tmp/adam_test_write.txt", "r"); + FILE *f = fopen(TEST_TMP "/adam_test_write.txt", "r"); ASSERT_NOT_NULL(f); char buf[64]; size_t n = fread(buf, 1, 63, f); @@ -3596,12 +3615,12 @@ TEST(tool_file_write_sandbox) { // Append arena_reset(a); - args = "{\"path\":\"/tmp/adam_test_write.txt\"," + args = "{\"path\":\"" TEST_TMP "/adam_test_write.txt\"," "\"content\":\" appended\",\"append\":true}"; r = adam_tool_file_write(a, s, args, strlen(args)); ASSERT_EQ(r.success, 1); - f = fopen("/tmp/adam_test_write.txt", "r"); + f = fopen(TEST_TMP "/adam_test_write.txt", "r"); n = fread(buf, 1, 63, f); buf[n] = '\0'; fclose(f); @@ -3614,7 +3633,7 @@ TEST(tool_file_write_sandbox) { ASSERT_EQ(r.success, 0); ASSERT(strstr(r.for_llm, "denied") != NULL); - remove("/tmp/adam_test_write.txt"); + remove(TEST_TMP "/adam_test_write.txt"); arena_destroy(a); adam_settings_destroy(s); } @@ -3622,14 +3641,14 @@ TEST(tool_file_write_sandbox) { TEST(tool_list_directory_sandbox) { arena_t *a = arena_create(64 * 1024); adam_settings_t *s = adam_create_settings(); - ASSERT_EQ(adam_settings_allow_dir(s, "/tmp"), ADAM_OK); + ASSERT_EQ(adam_settings_allow_dir(s, TEST_TMP), ADAM_OK); // Create test directory with a file - mkdir("/tmp/adam_test_dir", 0755); - FILE *f = fopen("/tmp/adam_test_dir/test.txt", "w"); + adam_mkdir(TEST_TMP "/adam_test_dir", 0755); + FILE *f = fopen(TEST_TMP "/adam_test_dir/test.txt", "w"); if (f) { fputs("x", f); fclose(f); } - const char *args = "{\"path\":\"/tmp/adam_test_dir\"}"; + const char *args = "{\"path\":\"" TEST_TMP "/adam_test_dir\"}"; adam_tool_result_t r = adam_tool_list_directory(a, s, args, strlen(args)); ASSERT_EQ(r.success, 1); ASSERT(strstr(r.for_llm, "test.txt") != NULL); @@ -3641,8 +3660,8 @@ TEST(tool_list_directory_sandbox) { r = adam_tool_list_directory(a, s, args, strlen(args)); ASSERT_EQ(r.success, 0); - remove("/tmp/adam_test_dir/test.txt"); - rmdir("/tmp/adam_test_dir"); + remove(TEST_TMP "/adam_test_dir/test.txt"); + rmdir(TEST_TMP "/adam_test_dir"); arena_destroy(a); adam_settings_destroy(s); } @@ -3658,7 +3677,7 @@ TEST(tool_shell_exec_basic) { ASSERT(strstr(r.for_llm, "denied") != NULL); // Allow /tmp - ASSERT_EQ(adam_settings_allow_dir(s, "/tmp"), ADAM_OK); + ASSERT_EQ(adam_settings_allow_dir(s, TEST_TMP), ADAM_OK); // Simple echo arena_reset(a); @@ -3667,12 +3686,15 @@ TEST(tool_shell_exec_basic) { ASSERT(strstr(r.for_llm, "hello") != NULL); ASSERT(strstr(r.for_llm, "exit code: 0") != NULL); - // Command that fails + // Command that fails. `false` doesn't exist on Windows cmd.exe — but + // any unknown command makes cmd.exe return a non-zero errorlevel + // (typically 1 or 9009), which is what we're really asserting. arena_reset(a); args = "{\"command\":\"false\"}"; r = adam_tool_shell_exec(a, s, args, strlen(args)); ASSERT_EQ(r.success, 0); // exit code != 0 - ASSERT(strstr(r.for_llm, "exit code: 1") != NULL); + ASSERT(strstr(r.for_llm, "exit code:") != NULL); + ASSERT(strstr(r.for_llm, "exit code: 0") == NULL); arena_destroy(a); adam_settings_destroy(s); @@ -3682,13 +3704,13 @@ TEST(tool_allow_dir) { adam_settings_t *s = adam_create_settings(); ASSERT_EQ(s->allowed_dir_count, 0); - ASSERT_EQ(adam_settings_allow_dir(s, "/tmp"), ADAM_OK); + ASSERT_EQ(adam_settings_allow_dir(s, TEST_TMP), ADAM_OK); ASSERT_EQ(s->allowed_dir_count, 1); - ASSERT_EQ(adam_settings_allow_dir(s, "/var"), ADAM_OK); + ASSERT_EQ(adam_settings_allow_dir(s, TEST_VAR), ADAM_OK); ASSERT_EQ(s->allowed_dir_count, 2); // NULL params - ASSERT_EQ(adam_settings_allow_dir(NULL, "/tmp"), ADAM_ERR_INVALID_PARAM); + ASSERT_EQ(adam_settings_allow_dir(NULL, TEST_TMP), ADAM_ERR_INVALID_PARAM); ASSERT_EQ(adam_settings_allow_dir(s, NULL), ADAM_ERR_INVALID_PARAM); // Nonexistent dir @@ -4883,25 +4905,25 @@ TEST(integration_json_output) { TEST(integration_file_tools_sandbox) { // Write a file, read it back, list directory — all sandboxed adam_settings_t *s = adam_create_settings(); - adam_settings_allow_dir(s, "/tmp"); + adam_settings_allow_dir(s, TEST_TMP); arena_t *a = arena_create(64 * 1024); // Write - const char *w_args = "{\"path\":\"/tmp/adam_integ_test.txt\"," + const char *w_args = "{\"path\":\"" TEST_TMP "/adam_integ_test.txt\"," "\"content\":\"Hello from integration test!\"}"; adam_tool_result_t wr = adam_tool_file_write(a, s, w_args, strlen(w_args)); ASSERT_EQ(wr.success, 1); // Read back arena_reset(a); - const char *r_args = "{\"path\":\"/tmp/adam_integ_test.txt\"}"; + const char *r_args = "{\"path\":\"" TEST_TMP "/adam_integ_test.txt\"}"; adam_tool_result_t rr = adam_tool_file_read(a, s, r_args, strlen(r_args)); ASSERT_EQ(rr.success, 1); ASSERT_STR_EQ(rr.for_llm, "Hello from integration test!"); // List directory containing the file arena_reset(a); - const char *l_args = "{\"path\":\"/tmp\"}"; + const char *l_args = "{\"path\":\"" TEST_TMP "\"}"; adam_tool_result_t lr = adam_tool_list_directory(a, s, l_args, strlen(l_args)); ASSERT_EQ(lr.success, 1); ASSERT(strstr(lr.for_llm, "adam_integ_test.txt") != NULL); @@ -4913,7 +4935,7 @@ TEST(integration_file_tools_sandbox) { ASSERT_EQ(dr.success, 0); ASSERT(strstr(dr.for_llm, "denied") != NULL); - remove("/tmp/adam_integ_test.txt"); + remove(TEST_TMP "/adam_integ_test.txt"); arena_destroy(a); adam_settings_destroy(s); } @@ -5124,6 +5146,14 @@ int main(void) { printf("Adam Test Suite v%s\n", ADAM_VERSION_STRING); printf("============================================================\n"); + // TEST_TMP / TEST_VAR map to platform-writable dirs (see top of file). + // Ensure both exist so the file-sandbox / session / allow-dir tests can + // realpath() them. EEXIST is fine; on POSIX hosts the dirs are already + // there, on Windows MinGW we create them under the current drive root, + // on Android both point at /data/* which already exist. + adam_mkdir(TEST_TMP, 0755); + adam_mkdir(TEST_VAR, 0755); + adam_init(); mem_report_start();