v0.1.0-U (vc=8): Phase U-1 + U-2 — Rust core + rustypipe search
NewPipeExtractor (Java) → strawcore (Rust) migration begins. Phase U:
- U-1: Rust toolchain + UniFFI smoke test
- U-2: rustypipe search via uniffi suspend fun, SearchViewModel swapped
What landed:
- rust/strawcore — UniFFI-exported Rust crate using proc-macros.
Builds for arm64-v8a + armeabi-v7a + x86 + x86_64 via cargo-ndk.
Tokio multi-thread runtime singleton drives rustypipe's async API.
- strawApp/build.gradle.kts — cargoBuildHost + cargoBuild + uniffiBindgen
Gradle Exec tasks chained into the Android build. Generated Kotlin
bindings land in src/main/java/uniffi/strawcore/ (gitignored).
- SearchViewModel.kt — calls uniffi.strawcore.search(query) directly.
NewPipeExtractor still in deps for VideoDetail/Player/Channel paths;
those move to Rust in U-3 / U-4.
- Build chain quirks beat:
* cargo absolute path in Exec tasks (PATH wasn't propagating)
* uniffi-bindgen needs UNSTRIPPED host .so — separate cargoBuildHost
builds a debug-profile host lib to read metadata from
* rustypipe rustls-tls-webpki-roots avoids the openssl-sys
cross-compile tarpit
* rquickjs-sys 'bindgen' feature opted in (no prebuilt Android
bindings ship; crafting-table has libclang 14)
- crafting-table runtime install (until Dockerfile catches up):
rustup + 4 Android targets + cargo-ndk + NDK r27c. Persists in
/caches/cargo + /caches/android-sdk via the volume mount.
APK size: 22MB (U-1) → 37MB (U-2). libstrawcore.so 3-5MB per ABI carries
rustypipe + reqwest + tokio + rustls + rquickjs. NewPipeExtractor still
in for now (still drives detail + player + channel + feed), so the
Java half is doubled up. U-5 removes it.
This commit is contained in:
parent
9550b207ab
commit
7ff5ac79e5
14 changed files with 432 additions and 29 deletions
|
|
@ -110,4 +110,98 @@ dependencies {
|
|||
implementation("androidx.media3:media3-session:1.4.1")
|
||||
// Guava ListenableFuture support for awaiting MediaController connect.
|
||||
implementation("androidx.concurrent:concurrent-futures-ktx:1.2.0")
|
||||
|
||||
// strawcore — Rust YouTube extractor via UniFFI/JNA. Built by the
|
||||
// cargoBuild + uniffiBindgen tasks below; phase U-2+ exposes search /
|
||||
// streamInfo / channelInfo to replace NewPipeExtractor.
|
||||
implementation("net.java.dev.jna:jna:5.14.0@aar")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase U-1 — Rust core build glue.
|
||||
//
|
||||
// Two tasks chain into the Android build:
|
||||
// cargoBuild — cross-compiles rust/strawcore for the four Android ABIs
|
||||
// via cargo-ndk and drops the .so files in strawApp/src/main/jniLibs/.
|
||||
// uniffiBindgen — generates the Kotlin bindings from the freshly-built lib
|
||||
// into strawApp/src/main/java/uniffi/strawcore/.
|
||||
//
|
||||
// Both depend on:
|
||||
// - cargo + rustup with the four Android targets installed
|
||||
// - cargo-ndk on PATH
|
||||
// - ANDROID_NDK_HOME pointing at an NDK with the right toolchains
|
||||
// All of that lives in the crafting-table container (see lucy-infra
|
||||
// containers/crafting-table/Dockerfile + ad-hoc install notes 2026-05-24).
|
||||
// =============================================================================
|
||||
|
||||
val rustRoot = file("../rust").absolutePath
|
||||
val jniLibsDir = file("src/main/jniLibs").absolutePath
|
||||
val bindingsDir = file("src/main/java").absolutePath
|
||||
|
||||
// Resolve cargo + the NDK by absolute path so the Gradle Exec tasks don't
|
||||
// depend on whatever PATH the user invoked gradle with. Fall back to env
|
||||
// var (CARGO_HOME) if set, else the crafting-table default.
|
||||
val cargoHome: String = System.getenv("CARGO_HOME") ?: "/caches/cargo"
|
||||
val cargoBin: String = "$cargoHome/bin/cargo"
|
||||
val ndkHome: String = System.getenv("ANDROID_NDK_HOME")
|
||||
?: System.getenv("ANDROID_NDK_ROOT")
|
||||
?: "/caches/android-sdk/ndk/27.2.12479018"
|
||||
|
||||
val cargoBuild by tasks.registering(Exec::class) {
|
||||
group = "rust"
|
||||
description = "Cross-compile strawcore for all Android ABIs via cargo-ndk."
|
||||
workingDir = file(rustRoot)
|
||||
environment("ANDROID_NDK_HOME", ndkHome)
|
||||
environment("PATH", "$cargoHome/bin:${System.getenv("PATH") ?: ""}")
|
||||
commandLine = listOf(
|
||||
cargoBin, "ndk",
|
||||
"-t", "arm64-v8a",
|
||||
"-t", "armeabi-v7a",
|
||||
"-t", "x86",
|
||||
"-t", "x86_64",
|
||||
"-o", jniLibsDir,
|
||||
"build", "--release", "-p", "strawcore",
|
||||
)
|
||||
standardOutput = System.out
|
||||
errorOutput = System.err
|
||||
}
|
||||
|
||||
// Build a host-arch debug .so for uniffi-bindgen to read metadata from.
|
||||
// Cross-compiled Android .so files have the same UniFFI metadata symbols,
|
||||
// but the release profile's strip+LTO can strip the sections in a way that
|
||||
// trips bindgen's library-mode reader. Build host debug separately.
|
||||
val cargoBuildHost by tasks.registering(Exec::class) {
|
||||
group = "rust"
|
||||
description = "Build host-arch debug strawcore so bindgen can read its UniFFI metadata."
|
||||
workingDir = file(rustRoot)
|
||||
environment("PATH", "$cargoHome/bin:${System.getenv("PATH") ?: ""}")
|
||||
commandLine = listOf(cargoBin, "build", "-p", "strawcore")
|
||||
standardOutput = System.out
|
||||
errorOutput = System.err
|
||||
}
|
||||
|
||||
val uniffiBindgen by tasks.registering(Exec::class) {
|
||||
group = "rust"
|
||||
description = "Generate Kotlin bindings for strawcore via uniffi-bindgen."
|
||||
dependsOn(cargoBuildHost)
|
||||
workingDir = file(rustRoot)
|
||||
environment("PATH", "$cargoHome/bin:${System.getenv("PATH") ?: ""}")
|
||||
commandLine = listOf(
|
||||
cargoBin, "run", "--quiet", "--bin", "uniffi-bindgen", "--",
|
||||
"generate",
|
||||
"--library", "target/debug/libstrawcore.so",
|
||||
"--crate", "strawcore",
|
||||
"--language", "kotlin",
|
||||
"--no-format",
|
||||
"--out-dir", bindingsDir,
|
||||
)
|
||||
standardOutput = System.out
|
||||
errorOutput = System.err
|
||||
}
|
||||
|
||||
// Make sure Android's JNI-libs merge picks up the freshly built .so files,
|
||||
// and Kotlin compilation can resolve the generated bindings.
|
||||
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }
|
||||
.configureEach { dependsOn(cargoBuild) }
|
||||
tasks.matching { it.name.startsWith("compile") && it.name.endsWith("Kotlin") }
|
||||
.configureEach { dependsOn(uniffiBindgen) }
|
||||
|
|
|
|||
|
|
@ -25,5 +25,14 @@ class StrawApp : Application() {
|
|||
History.init(this)
|
||||
Settings.init(this)
|
||||
Subscriptions.init(this)
|
||||
|
||||
// Phase U-1: load the strawcore native library and route its logs
|
||||
// into android logcat under the "strawcore" tag.
|
||||
runCatching {
|
||||
System.loadLibrary("strawcore")
|
||||
uniffi.strawcore.initLogging()
|
||||
}.onFailure {
|
||||
android.util.Log.w("StrawApp", "strawcore not loaded: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,10 @@ package com.sulkta.straw.feature.search
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.sulkta.straw.data.History
|
||||
import com.sulkta.straw.util.bestThumbnail
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
|
||||
data class SearchUiState(
|
||||
val query: String = "",
|
||||
|
|
@ -52,7 +45,23 @@ class SearchViewModel : ViewModel() {
|
|||
_ui.value = _ui.value.copy(loading = true, error = null, results = emptyList())
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val items = withContext(Dispatchers.IO) { search(q) }
|
||||
// Phase U-2: rustypipe via UniFFI. The bindgen-generated
|
||||
// `search()` is already a suspend fun running on the tokio
|
||||
// runtime baked into libstrawcore.so — no Dispatchers.IO
|
||||
// wrapper needed, the JNI call returns to us on the caller
|
||||
// dispatcher when the future completes.
|
||||
val rustItems = uniffi.strawcore.search(q)
|
||||
val items = rustItems.map { r ->
|
||||
StreamItem(
|
||||
url = r.url,
|
||||
title = r.title.ifBlank { "(no title)" },
|
||||
uploader = r.uploader,
|
||||
uploaderUrl = r.uploaderUrl,
|
||||
thumbnail = r.thumbnail,
|
||||
durationSeconds = r.durationSeconds,
|
||||
viewCount = r.viewCount,
|
||||
)
|
||||
}
|
||||
_ui.value = _ui.value.copy(loading = false, results = items)
|
||||
} catch (t: Throwable) {
|
||||
_ui.value = _ui.value.copy(
|
||||
|
|
@ -62,23 +71,4 @@ class SearchViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun search(query: String): List<StreamItem> {
|
||||
val service = NewPipe.getService(ServiceList.YouTube.serviceId)
|
||||
val qh = service.searchQHFactory.fromQuery(query, emptyList(), "")
|
||||
val info = SearchInfo.getInfo(service, qh)
|
||||
return info.relatedItems
|
||||
.filterIsInstance<StreamInfoItem>()
|
||||
.map {
|
||||
StreamItem(
|
||||
url = it.url,
|
||||
title = it.name ?: "(no title)",
|
||||
uploader = it.uploaderName ?: "",
|
||||
uploaderUrl = it.uploaderUrl,
|
||||
thumbnail = bestThumbnail(it.thumbnails),
|
||||
durationSeconds = it.duration,
|
||||
viewCount = it.viewCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue