9299176b2d
Addresses #91: the #90 posture (`deny(unsafe_code)` + three scattered `#![allow(unsafe_code)]` across solitaire_data and solitaire_engine) punched unsafe holes into otherwise-pure logic crates. Replace it by *reducing* the unsafe rather than relocating it, then forbidding it everywhere it can be forbidden. Changes: - solitaire_data: new safe `android_jni` bridge owning the cached `JavaVM` and activity `GlobalRef`; exposes `with_env` / `with_activity_env` so keystore/clipboard/safe-area never touch a raw handle. - keystore: drop the `JavaVM::from_raw` init path (now in the app) and replace the three `unsafe { JByteArray::from_raw(x.into_raw()) }` casts with the safe `JByteArray::from(JObject)` conversion jni 0.21 provides. - engine clipboard + safe_area: route through the bridge; remove their `#![allow(unsafe_code)]` and all `from_raw` calls. - solitaire_app: becomes the single owner of FFI unsafe. `android_main` reconstructs the raw `JavaVM` / activity once (it must, as the cdylib that exports `#[unsafe(no_mangle)]`) and registers the safe wrappers. It opts to its own `deny`-level lints with two scoped `#[allow(unsafe_code)]`. - workspace: `unsafe_code` is now `forbid`. Every crate except the app entry point is fully unsafe-free. Net: 7 unsafe sites across three crates collapse to 3 at the OS boundary in one crate. Verified with host `clippy --workspace -- -D warnings` and an `aarch64-linux-android` clippy build of solitaire_app (transitively engine + data); also fixed two latent android-only `collapsible_if` warnings surfaced in the keystore by the cross-target check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.2 KiB
TOML
124 lines
5.2 KiB
TOML
[package]
|
||
name = "solitaire_app"
|
||
version.workspace = true
|
||
license.workspace = true
|
||
edition.workspace = true
|
||
|
||
[[bin]]
|
||
name = "solitaire_app"
|
||
path = "src/main.rs"
|
||
|
||
# `cdylib` is what cargo-apk packages into `libsolitaire_app.so` for
|
||
# Android — the activity dlopens the shared object and calls into it.
|
||
# `rlib` lets the bin target above link the library normally on
|
||
# desktop. Both produce the same code; only the linkage form differs.
|
||
[lib]
|
||
name = "solitaire_app"
|
||
path = "src/lib.rs"
|
||
crate-type = ["cdylib", "rlib"]
|
||
|
||
[dependencies]
|
||
bevy = { workspace = true }
|
||
solitaire_engine = { workspace = true }
|
||
solitaire_data = { workspace = true }
|
||
|
||
# Android-only: the entry point reconstructs the raw `JavaVM` / activity
|
||
# handles and registers the safe `solitaire_data::android_jni` bridge. This
|
||
# is the one crate in the workspace that performs `unsafe` FFI, so it is also
|
||
# the only one that depends on `jni` directly at the app layer.
|
||
[target.'cfg(target_os = "android")'.dependencies]
|
||
jni = { workspace = true }
|
||
|
||
# Desktop-only deps. `keyring`'s default-store init only matters on
|
||
# platforms with a real keychain backend (Linux Secret Service,
|
||
# macOS Keychain, Windows Credential Store), and its transitive
|
||
# `rpassword` uses `libc::__errno_location` — a symbol Android's
|
||
# bionic doesn't expose. `winit` is promoted from a transitive
|
||
# Bevy 0.18 → bevy_winit 0.18 → winit 0.30 dep to a direct dep so
|
||
# the `Window::icon` wiring in `set_window_icon` can construct
|
||
# `winit::window::Icon` values (bevy_winit 0.18 doesn't re-export
|
||
# `Icon`). Android draws its launcher icon from the APK manifest,
|
||
# so neither dep matters there. Target-gating keeps `cargo apk
|
||
# build` viable; the desktop call sites have their own
|
||
# `cfg(not(target_os = "android"))` guards.
|
||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||
keyring = { workspace = true }
|
||
winit = { version = "0.30", default-features = false }
|
||
# `tiny-skia` is already in the workspace deps for `solitaire_engine`;
|
||
# `solitaire_app` consumes it directly only on the desktop icon path
|
||
# (PNG → raw RGBA decode for `set_window_icon`).
|
||
tiny-skia = { workspace = true }
|
||
|
||
# --- Android packaging metadata (read by `cargo-apk`) -------------------
|
||
#
|
||
# Pinning these values inside the repo means a contributor running
|
||
# `cargo apk build -p solitaire_app --target x86_64-linux-android`
|
||
# does not need to install whatever SDK version cargo-apk happens to
|
||
# default to today. The numbers track the SDK we install in the dev
|
||
# setup script: target SDK 34 (Android 14, current Play Store target),
|
||
# min SDK 26 (Android 8, the lowest Bevy 0.18 supports cleanly with
|
||
# the wgpu / GLES path).
|
||
#
|
||
# Asset path is `../assets` so the same directory the desktop build
|
||
# already uses ships into the APK without copy-tree gymnastics.
|
||
# `apk_name` keeps the output filename predictable across machines.
|
||
[package.metadata.android]
|
||
package = "com.ferrousapp.solitaire"
|
||
apk_name = "ferrous-solitaire"
|
||
build_targets = ["aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"]
|
||
assets = "../assets"
|
||
# Density-bucketed launcher icons. `aapt` processes `res/mipmap-*/` and
|
||
# packages them into the APK; the launcher selects the best-fit bucket
|
||
# for the device screen density. Sizes used:
|
||
# mdpi (1×, 48 dp) → 48 px (exact)
|
||
# hdpi (1.5×, 72 dp) → 64 px (88 %, aapt scales up slightly)
|
||
# xhdpi (2×, 96 dp) → 128 px (133 %, aapt scales down)
|
||
# xxhdpi (3×, 144 dp) → 256 px (178 %, aapt scales down)
|
||
# xxxhdpi (4×, 192 dp) → 256 px (133 %, aapt scales down)
|
||
resources = "res"
|
||
# No `runtime_libs` — we don't ship any precompiled .so files,
|
||
# the entire app is pure Rust + Bevy. cargo-apk would try to
|
||
# resolve `runtime_libs/<arch>/` if set, and fail on a non-existent
|
||
# arch directory under our package.
|
||
strip = "strip"
|
||
|
||
[package.metadata.android.sdk]
|
||
target_sdk_version = 34
|
||
min_sdk_version = 26
|
||
|
||
[[package.metadata.android.uses_feature]]
|
||
name = "android.hardware.touchscreen"
|
||
required = true
|
||
|
||
[[package.metadata.android.uses_permission]]
|
||
name = "android.permission.INTERNET"
|
||
|
||
[package.metadata.android.application]
|
||
label = "Ferrous Solitaire"
|
||
# Launcher icon — references the density-bucketed mipmap resource above.
|
||
icon = "@mipmap/ic_launcher"
|
||
# `debuggable` defaults to false on release builds; cargo-apk flips it
|
||
# automatically for debug profiles. Leaving the field unset keeps the
|
||
# default behaviour.
|
||
|
||
[package.metadata.android.application.activity]
|
||
# Lock to portrait — the current layout has only been designed and tested
|
||
# in portrait orientation. Remove (or add a landscape layout) before
|
||
# enabling auto-rotate.
|
||
orientation = "portrait"
|
||
|
||
# `solitaire_app` is the one crate that cannot inherit the workspace
|
||
# `forbid(unsafe_code)`: as the Android cdylib it must export the
|
||
# `#[unsafe(no_mangle)]` entry point and reconstruct the raw JNI handles
|
||
# there (a `no_mangle` symbol cannot live in a dependency rlib). It mirrors
|
||
# the workspace lints but at `deny`, so the two `#[allow(unsafe_code)]`
|
||
# scopes in the Android entry point are the only unsafe in the whole tree.
|
||
[lints.rust]
|
||
unsafe_code = "deny"
|
||
single_use_lifetimes = "warn"
|
||
trivial_casts = "warn"
|
||
unused_lifetimes = "warn"
|
||
unused_qualifications = "warn"
|
||
variant_size_differences = "warn"
|
||
unexpected_cfgs = "warn"
|