feat(client/android): CI screenshot capture via Roborazzi
Play-listing/marketing screenshots of the Compose client rendered on the host JVM by Roborazzi (Robolectric Native Graphics) — no emulator, GPU, KVM, host, or JNI core. Five scenes render the REAL composables with embedded mock state under a forced brand palette (Material You has no wallpaper to seed from on the JVM): hosts grid, settings, TOFU + PIN dialogs, and the live stats HUD. Validated 5/5 locally. - New JVM unit-test source set (app/src/test) + Roborazzi/Robolectric test deps; @Config(sdk=36) is mandatory (no android-all jar for compileSdk 37) and the animation clock is paused so a text-bearing scene reaches idle. - kit: `-PskipRustBuild` skips the cargo-ndk native build so the JVM-only test job needs no Rust/NDK; normal APK/AAR builds are unchanged. - Widen BrandDark / StatsOverlay to internal so the tests can use them. - Standalone best-effort tag-gated workflow; PNGs upload as a 30-day artifact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
# Android client screenshots for the Play listing / marketing. Roborazzi renders the real Compose
|
||||||
|
# UI with mock state on the host JVM via Robolectric — NO emulator, GPU, KVM, host, or JNI core
|
||||||
|
# (`-PskipRustBuild` skips the cargo-ndk native build). The Android analogue of apple.yml's
|
||||||
|
# `screenshots` job, gated to STABLE RELEASE tags only. Standalone + best-effort: a failure here
|
||||||
|
# reds nothing else. PNGs land as a 30-day artifact; not committed or published.
|
||||||
|
name: android-screenshots
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
screenshots:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: JDK 21 (AGP 9.2 + Robolectric's SDK-36 android-all jar both want 17–21)
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "21"
|
||||||
|
|
||||||
|
- name: Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
# No NDK/CMake — the screenshot unit tests are pure JVM. compileSdk 37 auto-downloads via AGP
|
||||||
|
# if the platform channel lacks it (same note as android.yml).
|
||||||
|
- name: platform-tools + platform 36 + build-tools
|
||||||
|
run: sdkmanager "platform-tools" "platforms;android-36" "build-tools;37.0.0"
|
||||||
|
|
||||||
|
- name: Cache (gradle)
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: android-screenshots-${{ hashFiles('clients/android/**/*.gradle.kts') }}
|
||||||
|
restore-keys: android-screenshots-
|
||||||
|
|
||||||
|
# Roborazzi renders Compose on the JVM (Robolectric Native Graphics). `-PskipRustBuild` keeps
|
||||||
|
# the cargo-ndk native build out of the graph — the tests never load libpunktfunk_android.so.
|
||||||
|
- name: Capture screenshots (Roborazzi)
|
||||||
|
working-directory: clients/android
|
||||||
|
run: ./gradlew :app:testDebugUnitTest -PskipRustBuild --stacktrace
|
||||||
|
|
||||||
|
- name: Upload screenshots
|
||||||
|
if: always()
|
||||||
|
# v3: Gitea's API rejects upload-artifact@v4 (see apple.yml). Download is a zip.
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: punktfunk-android-screenshots
|
||||||
|
path: clients/android/app/build/outputs/roborazzi
|
||||||
|
retention-days: 30
|
||||||
@@ -62,6 +62,10 @@ android {
|
|||||||
|
|
||||||
buildFeatures { compose = true }
|
buildFeatures { compose = true }
|
||||||
|
|
||||||
|
// Roborazzi/Robolectric render Compose on the host JVM (the CI screenshot harness) and need the
|
||||||
|
// merged Android resources + the app's manifest/theme available to the unit tests.
|
||||||
|
testOptions { unitTests { isIncludeAndroidResources = true } }
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
@@ -99,4 +103,21 @@ dependencies {
|
|||||||
// Android TV components (we target phone + TV) land in the TV-UI milestone:
|
// Android TV components (we target phone + TV) land in the TV-UI milestone:
|
||||||
// implementation("androidx.tv:tv-material:1.1.0")
|
// implementation("androidx.tv:tv-material:1.1.0")
|
||||||
// The manifest already declares leanback so the scaffold installs on TV.
|
// The manifest already declares leanback so the scaffold installs on TV.
|
||||||
|
|
||||||
|
// --- CI screenshot harness (Roborazzi on the JVM via Robolectric — no emulator/GPU). The
|
||||||
|
// screenshot tests render the real Compose UI with mock state; never load the JNI core, so the
|
||||||
|
// job runs `:app:testDebugUnitTest -PskipRustBuild` (see kit/build.gradle.kts). ---
|
||||||
|
testImplementation(composeBom)
|
||||||
|
testImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-test-manifest") // the ComponentActivity test host
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
testImplementation("org.robolectric:robolectric:4.16.1")
|
||||||
|
testImplementation("io.github.takahirom.roborazzi:roborazzi:1.64.0")
|
||||||
|
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.64.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record (write) the screenshots when the unit tests run. These tests exist to GENERATE marketing
|
||||||
|
// images, not to diff goldens, so always capture rather than verify.
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
systemProperty("roborazzi.test.record", "true")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
|||||||
* `[fps, mbps, latP50Ms, latP95Ms, latValid, skew, w, h, hz, dropped]`.
|
* `[fps, mbps, latP50Ms, latP95Ms, latValid, skew, w, h, hz, dropped]`.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatsOverlay(s: DoubleArray, modifier: Modifier = Modifier) {
|
internal fun StatsOverlay(s: DoubleArray, modifier: Modifier = Modifier) {
|
||||||
if (s.size < 10) return
|
if (s.size < 10) return
|
||||||
val w = s[6].toInt()
|
val w = s[6].toInt()
|
||||||
val h = s[7].toInt()
|
val h = s[7].toInt()
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
|
|
||||||
// punktfunk brand violets (from the app icon: #6C5BF3 / #A79FF8 / #D2C9FB on a #16132A indigo).
|
// punktfunk brand violets (from the app icon: #6C5BF3 / #A79FF8 / #D2C9FB on a #16132A indigo).
|
||||||
// Used as the fallback dark scheme on pre-Android-12 devices; on 12+ we defer to Material You.
|
// Used as the fallback dark scheme on pre-Android-12 devices; on 12+ we defer to Material You.
|
||||||
private val BrandDark = darkColorScheme(
|
// `internal` (not private) so the CI screenshot tests can force the deterministic brand palette —
|
||||||
|
// Material You dynamic colour has no wallpaper to seed from under the Robolectric JVM renderer.
|
||||||
|
internal val BrandDark = darkColorScheme(
|
||||||
primary = Color(0xFFA79FF8),
|
primary = Color(0xFFA79FF8),
|
||||||
onPrimary = Color(0xFF1B1442),
|
onPrimary = Color(0xFF1B1442),
|
||||||
primaryContainer = Color(0xFF4C3FB3),
|
primaryContainer = Color(0xFF4C3FB3),
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package io.unom.punktfunk.screenshots
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onRoot
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import com.github.takahirom.roborazzi.captureScreenRoboImage
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-store / marketing screenshots of the native Android client, rendered on the JVM by Roborazzi
|
||||||
|
* (Robolectric Native Graphics) — no emulator, GPU, host, or JNI core. The scenes (ShotScenes.kt)
|
||||||
|
* render the REAL Compose UI with mock state.
|
||||||
|
*
|
||||||
|
* `sdk = [36]` is mandatory: Robolectric ships android-all jars only up to API 36 (Android 16), and
|
||||||
|
* the app's compileSdk is 37. PNGs land in build/outputs/roborazzi/.
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
@Config(sdk = [36], qualifiers = "w360dp-h800dp-xxhdpi")
|
||||||
|
class ScreenshotTest {
|
||||||
|
@get:Rule
|
||||||
|
val compose = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
private val out = "build/outputs/roborazzi"
|
||||||
|
|
||||||
|
// Pausing the animation clock before composing (then advancing once past the entrance animation
|
||||||
|
// and freezing) is what makes a text-field-bearing scene capturable: a focused field blinks its
|
||||||
|
// cursor via an infinite animation that otherwise keeps Compose perpetually "busy", so
|
||||||
|
// setContent's wait-for-idle never returns. Frozen, the capture is also deterministic.
|
||||||
|
|
||||||
|
/** Full-screen content scenes: the compose root fills the device, so a root capture is the shot. */
|
||||||
|
private fun shootRoot(name: String, content: @androidx.compose.runtime.Composable () -> Unit) {
|
||||||
|
compose.mainClock.autoAdvance = false
|
||||||
|
compose.setContent { ShotTheme(content) }
|
||||||
|
compose.mainClock.advanceTimeBy(800)
|
||||||
|
compose.onRoot().captureRoboImage("$out/phone-$name.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dialog scenes: the AlertDialog is a separate window, so capture the whole screen (all windows). */
|
||||||
|
private fun shootScreen(name: String, content: @androidx.compose.runtime.Composable () -> Unit) {
|
||||||
|
compose.mainClock.autoAdvance = false
|
||||||
|
compose.setContent { ShotTheme(content) }
|
||||||
|
compose.mainClock.advanceTimeBy(800)
|
||||||
|
captureScreenRoboImage("$out/phone-$name.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hosts() = shootRoot("hosts") { HostsScene() }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settings() = shootRoot("settings") { SettingsScene() }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Config(sdk = [36], qualifiers = "w800dp-h360dp-xxhdpi") // landscape — the stream is immersive
|
||||||
|
fun stream() = shootRoot("stream") { StreamScene() }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trust() = shootScreen("trust") {
|
||||||
|
HostsScene()
|
||||||
|
TrustDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pair() = shootScreen("pair") {
|
||||||
|
HostsScene()
|
||||||
|
PairDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package io.unom.punktfunk.screenshots
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.unom.punktfunk.BrandDark
|
||||||
|
import io.unom.punktfunk.Settings
|
||||||
|
import io.unom.punktfunk.SettingsScreen
|
||||||
|
import io.unom.punktfunk.StatsOverlay
|
||||||
|
import io.unom.punktfunk.components.HostCard
|
||||||
|
import io.unom.punktfunk.components.SectionLabel
|
||||||
|
import io.unom.punktfunk.models.HostStatus
|
||||||
|
|
||||||
|
// The CI screenshot scenes: the REAL app composables, fed embedded mock state, under the forced
|
||||||
|
// brand palette (Material You has no wallpaper to seed from on the JVM). The stream-video surface
|
||||||
|
// and ConnectScreen/App are intentionally absent — they require the live JNI core / a session.
|
||||||
|
|
||||||
|
/** Forces the deterministic punktfunk brand scheme (see Theme.kt) instead of dynamic colour. */
|
||||||
|
@Composable
|
||||||
|
internal fun ShotTheme(content: @Composable () -> Unit) {
|
||||||
|
MaterialTheme(colorScheme = BrandDark, content = content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class MockHost(val name: String, val address: String, val status: HostStatus)
|
||||||
|
|
||||||
|
private val SAVED = listOf(
|
||||||
|
MockHost("Living Room PC", "192.168.1.42:9777", HostStatus.PAIRED),
|
||||||
|
MockHost("Office", "192.168.1.50:9777", HostStatus.TOFU),
|
||||||
|
)
|
||||||
|
private val DISCOVERED = listOf(
|
||||||
|
MockHost("studio-deck", "192.168.1.61:9777", HostStatus.PAIRING),
|
||||||
|
MockHost("HTPC", "192.168.1.70:9777", HostStatus.TOFU),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** The connect screen's host grid, reconstructed from the real HostCard/SectionLabel components. */
|
||||||
|
@Composable
|
||||||
|
internal fun HostsScene() {
|
||||||
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(minSize = 160.dp),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text("Punktfunk", style = MaterialTheme.typography.headlineLarge)
|
||||||
|
Text(
|
||||||
|
"stream a remote desktop",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) { SectionLabel("Saved hosts") }
|
||||||
|
items(SAVED) { h ->
|
||||||
|
HostCard(h.name, h.address, h.status, enabled = true, onConnect = {}, onForget = {}, onRename = {})
|
||||||
|
}
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
SectionLabel("Discovered on the network")
|
||||||
|
}
|
||||||
|
items(DISCOVERED) { h ->
|
||||||
|
HostCard(h.name, h.address, h.status, enabled = true, onConnect = {}, onForget = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The real SettingsScreen, fed a representative non-default Settings. */
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsScene() {
|
||||||
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
SettingsScreen(
|
||||||
|
initial = Settings(
|
||||||
|
width = 1920,
|
||||||
|
height = 1080,
|
||||||
|
hz = 120,
|
||||||
|
bitrateKbps = 50_000,
|
||||||
|
compositor = 1,
|
||||||
|
gamepad = 2,
|
||||||
|
micEnabled = true,
|
||||||
|
statsHudEnabled = true,
|
||||||
|
trackpadMode = true,
|
||||||
|
),
|
||||||
|
onChange = {},
|
||||||
|
onBack = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The real TOFU AlertDialog (mirrors ConnectScreen's PendingTrust.Kind.TRUST_NEW), shown over the host grid. */
|
||||||
|
@Composable
|
||||||
|
internal fun TrustDialog() {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
title = { Text("Trust this host?") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text("First connection to 192.168.1.61:9777.")
|
||||||
|
Text("Fingerprint 9f8e7d6c5b4a3928…")
|
||||||
|
Text(
|
||||||
|
"This host allows trust-on-first-use, but that can't tell an impostor " +
|
||||||
|
"from the real host. Pairing with a PIN is stronger — it proves both sides.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = { TextButton({}) { Text("Trust (TOFU)") } },
|
||||||
|
dismissButton = { TextButton({}) { Text("Pair with PIN…") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The PIN-pairing AlertDialog (mirrors ConnectScreen's PendingTrust.Kind.PAIR). The live screen
|
||||||
|
* uses OutlinedTextFields, but a TextField inside a Dialog window never reaches idle under
|
||||||
|
* Robolectric (its focus/cursor machinery animates forever) — so the PIN is shown as a static
|
||||||
|
* display here, which also reads better in a marketing shot. */
|
||||||
|
@Composable
|
||||||
|
internal fun PairDialog() {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
title = { Text("Pair with PIN") },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text("Enter the 4-digit PIN shown on the host.")
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"4 8 2 7",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
"This device: Pixel 9 Pro",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = { TextButton({}) { Text("Pair") } },
|
||||||
|
dismissButton = { TextButton({}) { Text("Cancel") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The live stats HUD (the real StatsOverlay) over a synthetic "streamed frame" gradient. */
|
||||||
|
@Composable
|
||||||
|
internal fun StreamScene() {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(listOf(Color(0xFF2A1E5C), Color(0xFF0E1B3D), Color(0xFF06122B))),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
// [fps, mbps, latP50, latP95, latValid, skew, w, h, hz, dropped]
|
||||||
|
StatsOverlay(
|
||||||
|
doubleArrayOf(238.0, 921.4, 1.3, 2.1, 1.0, 1.0, 5120.0, 1440.0, 240.0, 0.0),
|
||||||
|
Modifier.align(Alignment.TopStart).padding(12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,6 +99,12 @@ val cargoNdkDebug = registerCargoNdk("cargoNdkDebug", release = false)
|
|||||||
val cargoNdkRelease = registerCargoNdk("cargoNdkRelease", release = true)
|
val cargoNdkRelease = registerCargoNdk("cargoNdkRelease", release = true)
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
tasks.named("preDebugBuild").configure { dependsOn(cargoNdkDebug) }
|
// `-PskipRustBuild` skips the cargo-ndk native build — for JVM-only tasks (the Roborazzi
|
||||||
tasks.named("preReleaseBuild").configure { dependsOn(cargoNdkRelease) }
|
// screenshot unit tests render Compose on the JVM and never load libpunktfunk_android.so), so
|
||||||
|
// CI/local screenshot runs don't need the Rust toolchain or NDK. The native build stays wired
|
||||||
|
// for every normal APK/AAR build.
|
||||||
|
if (!project.hasProperty("skipRustBuild")) {
|
||||||
|
tasks.named("preDebugBuild").configure { dependsOn(cargoNdkDebug) }
|
||||||
|
tasks.named("preReleaseBuild").configure { dependsOn(cargoNdkRelease) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user