diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 56d460b..7036dd0 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -75,6 +75,50 @@ jobs:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk
+ # ── Job 2b: Build debug APK on Linux (NDK cross-compile sanity on every PR) ──
+ # The macOS job above covers the primary dev path; this one guards the Linux
+ # toolchain (apt Boost in /usr/include + #include_next) so it can't silently rot.
+ build-linux:
+ name: Build Debug APK (Linux)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ submodules: recursive
+
+ - uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: temurin
+ cache: gradle
+
+ - uses: gradle/actions/setup-gradle@v6
+
+ - name: Install Boost via apt
+ run: sudo apt-get install -y libboost-dev
+
+ - name: Resolve Boost dirs
+ run: |
+ BOOST_DIR=$(find /usr/lib -maxdepth 4 -name "Boost-*" -type d 2>/dev/null \
+ | sort -V | tail -1)
+ echo "BOOST_CMAKE_DIR=$BOOST_DIR" >> "$GITHUB_ENV"
+ # Isolated dir with ONLY a boost/ symlink: -I
finds boost/* but does
+ # not expose glibc system headers (features-time64.h → bits/wordsize.h)
+ # which are absent in the NDK cross-compilation sysroot.
+ mkdir -p /tmp/boost-headers
+ ln -sfn /usr/include/boost /tmp/boost-headers/boost
+ echo "BOOST_INCLUDE_DIR=/tmp/boost-headers" >> "$GITHUB_ENV"
+ echo "Found Boost CMake dir: $BOOST_DIR"
+
+ - name: Build debug APK
+ run: ./gradlew assembleDebug --no-daemon
+
+ - name: Upload APK
+ uses: actions/upload-artifact@v7
+ with:
+ name: app-debug-linux
+ path: app/build/outputs/apk/debug/app-debug.apk
+
# ── Job 3: Lint ─────────────────────────────────────────────────────────────
lint:
name: Lint
diff --git a/README.md b/README.md
index 8d48524..4e6eb51 100644
--- a/README.md
+++ b/README.md
@@ -169,12 +169,35 @@ export BOOST_CMAKE_DIR=/path/to/cmake/Boost-X.Y.Z
```bash
sudo apt-get install -y libboost-dev
-BOOST_CMAKE_DIR=$(find /usr -name "BoostConfig.cmake" 2>/dev/null \
- | head -1 | xargs dirname)
+# 1. Boost's CMake config dir (for find_package)
+BOOST_CMAKE_DIR=$(dirname "$(find /usr -name BoostConfig.cmake 2>/dev/null | head -1)")
export BOOST_CMAKE_DIR
+
+# 2. An *isolated* Boost include dir — a directory containing ONLY a boost/ entry.
+# Do NOT use /usr/include directly: the Android NDK headers use #include_next,
+# which would pull the host glibc headers from /usr/include into the cross
+# build and break it (missing bits/wordsize.h, bits/libc-header-start.h).
+mkdir -p "$HOME/.boost-include"
+ln -sfn /usr/include/boost "$HOME/.boost-include/boost"
+export BOOST_INCLUDE_DIR="$HOME/.boost-include"
+
./gradlew assembleDebug
```
+> On macOS this isolation is automatic — Homebrew's prefix holds Boost but no
+> competing libc — which is why only `BOOST_CMAKE_DIR` is needed there.
+
+#### Building from Android Studio on Linux
+
+The IDE doesn't see your shell `export`s. Instead of env vars, put the same two
+paths in `local.properties` (gitignored, alongside `sdk.dir`); the build falls
+back to them automatically:
+
+```properties
+BOOST_CMAKE_DIR=/usr/lib/x86_64-linux-gnu/cmake/Boost-1.83.0
+BOOST_INCLUDE_DIR=/home//.boost-include
+```
+
## Running Tests
```bash
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b095b57..e7c93c8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,3 +1,5 @@
+import java.util.Properties
+
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
@@ -5,6 +7,16 @@ plugins {
alias(libs.plugins.screenshot)
}
+// Boost paths resolve in this order: environment variable, then local.properties
+// (gitignored, same place as sdk.dir — handy when building from the IDE where shell
+// exports aren't visible), then the macOS Homebrew default. See README "Linux".
+val localProperties = Properties().apply {
+ val file = rootProject.file("local.properties")
+ if (file.exists()) file.inputStream().use { load(it) }
+}
+fun boostProperty(name: String, default: String): String =
+ System.getenv(name) ?: localProperties.getProperty(name) ?: default
+
android {
namespace = "com.jpcottin.simpletorrent"
compileSdk = 36
@@ -19,14 +31,15 @@ android {
externalNativeBuild {
cmake {
cppFlags += "-std=c++17"
- // BOOST_CMAKE_DIR env var lets CI override the Homebrew default path
- val boostDir = System.getenv("BOOST_CMAKE_DIR")
- ?: "/opt/homebrew/lib/cmake/Boost-1.90.0"
+ // BOOST_CMAKE_DIR (env or local.properties) overrides the Homebrew default path
+ val boostDir = boostProperty("BOOST_CMAKE_DIR",
+ "/opt/homebrew/lib/cmake/Boost-1.90.0")
// BOOST_INCLUDE_DIR: passed to CMake as a cache var; used via
// target_compile_options() in CMakeLists.txt so it bypasses both
// CMake's implicit-include filtering AND configure-time feature checks.
- val boostInclude = System.getenv("BOOST_INCLUDE_DIR")
- ?: "/opt/homebrew/include"
+ // On Linux this must be a Boost-only dir, never /usr/include (see CMakeLists.txt).
+ val boostInclude = boostProperty("BOOST_INCLUDE_DIR",
+ "/opt/homebrew/include")
arguments(
// libtorrent is in libs/libtorrent submodule — no path override needed
"-DBoost_DIR=$boostDir",
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index 5356593..f42edc5 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -39,6 +39,14 @@ add_library(simpletorrent SHARED torrent_jni.cpp)
# emits raw -I flags that bypass this filtering. Using cppFlags/CMAKE_CXX_FLAGS is
# also wrong because those affect configure-time feature probes (e.g. std::atomic
# check in libtorrent) and break cross-compilation with host headers in the way.
+#
+# IMPORTANT: BOOST_INCLUDE_DIR must point at a directory that contains ONLY Boost
+# (i.e. just a `boost/` subdir), never the host's /usr/include. The NDK headers use
+# #include_next, which walks past the sysroot into any dir on the search path; if
+# host libc headers (limits.h, features.h, ...) are reachable there, the cross
+# build breaks. On macOS, Homebrew's prefix already satisfies this. On Debian/Ubuntu
+# the system Boost lives in /usr/include alongside glibc, so point BOOST_INCLUDE_DIR
+# at an isolated Boost prefix instead (see README "Linux").
if(DEFINED BOOST_INCLUDE_DIR)
target_compile_options(torrent-rasterbar PRIVATE "-I${BOOST_INCLUDE_DIR}")
target_compile_options(simpletorrent PRIVATE "-I${BOOST_INCLUDE_DIR}")