0.1.0-0-pre.11

This commit is contained in:
2026-09-16 11:32:07 +02:00
parent fa098943bf
commit ab9403384f
14 changed files with 465 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: Android/README.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# Android
@@ -91,3 +91,11 @@ assets/<game>/ -> assets/game/
dans un répertoire généré sous `Android/<module>/build/generated/sasedevAssets/<variant>/`.
Aucune ressource source n'est copiée durablement dans un module Android ou une crate Rust.
## Version NDK du projet
`Android/gradle.properties` définit `androidNdkVersion=28.2.13676358`.
Gradle utilise cette valeur comme `ndkVersion`. Le script Rust Android résout la même version sous `${ANDROID_HOME}/ndk/` et définit `ANDROID_NDK_HOME` uniquement pour le sous-processus `cargo ndk`.
Aucun `ANDROID_NDK_HOME` global n'est requis ; plusieurs NDK peuvent rester installés côte à côte.

View File

@@ -1,15 +1,17 @@
// file: Android/common/build.gradle
// version: 3
// version: 4
plugins {
id 'com.android.library'
}
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
android {
namespace 'com.sasedev.games.common'
compileSdk 36
ndkVersion androidNdkVersion
defaultConfig {
minSdk 21

View File

@@ -1,22 +1,24 @@
// file: Android/game-reflex-poc/build.gradle
// version: 8
// version: 9
plugins {
id 'com.android.application'
}
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
android {
namespace 'com.sasedev.games.reflex'
compileSdk 36
ndkVersion androidNdkVersion
defaultConfig {
applicationId 'com.sasedev.games.reflex'
minSdk 21
targetSdk 36
versionCode 1
versionName '0.1.0-0-pre.10'
versionName '0.1.0-0-pre.11'
}
compileOptions {

View File

@@ -1,22 +1,24 @@
// file: Android/game-snake-poc/build.gradle
// version: 8
// version: 9
plugins {
id 'com.android.application'
}
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
android {
namespace 'com.sasedev.games.snake'
compileSdk 36
ndkVersion androidNdkVersion
defaultConfig {
applicationId 'com.sasedev.games.snake'
minSdk 21
targetSdk 36
versionCode 1
versionName '0.1.0-0-pre.10'
versionName '0.1.0-0-pre.11'
}
compileOptions {

View File

@@ -1,6 +1,7 @@
# file: Android/gradle.properties
# version: 1
# version: 2
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
sdl3AarName=SDL3-3.4.16.aar
androidNdkVersion=28.2.13676358

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml
# version: 20
# version: 21
[workspace]
resolver = "3"
@@ -17,7 +17,7 @@ members = [
]
[workspace.package]
version = "0.1.0-0-pre.10.fix.1"
version = "0.1.0-0-pre.11"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/games"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# Roadmap
@@ -14,7 +14,7 @@
- [x] `0-pre.7` — compilation Rust Android `cdylib`, point d'entrée SDL Android et premier lancement APK sur appareil/émulateur.
- [x] `0-pre.8` — bridge Java/JNI minimal et input tactile Android.
- [x] `0-pre.9` — assets communs + spécifiques empaquetés sans copie dans les crates.
- [ ] `0-pre.10` — POC Reflex jouable Desktop + Android.
- [x] `0-pre.10` — POC Reflex jouable Desktop + Android.
- [ ] `0-pre.11` — POC Snake jouable et validation de la réutilisation du moteur.
- [ ] `1-alpha.1` — première API moteur V1 volontairement stabilisée.
- [ ] `2-beta.1` — stabilisation, packaging, tests multi-appareils.

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-sdl/src/runtime.rs
// version: 7
// version: 8
/// Minimal SDL3 runtime used by Desktop and Android POC runners.
pub struct SdlRuntime {
@@ -47,9 +47,22 @@ impl SdlRuntime {
match event {
sdl3::event::Event::Quit { .. } => break 'running,
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Escape), .. } => break 'running,
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Left), repeat: false, .. } => {
input = input.with_action(engine_v1_common::GameAction::Left, true);
},
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Right), repeat: false, .. } => {
input = input.with_action(engine_v1_common::GameAction::Right, true);
},
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Up), repeat: false, .. } => {
input = input.with_action(engine_v1_common::GameAction::Up, true);
},
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Down), repeat: false, .. } => {
input = input.with_action(engine_v1_common::GameAction::Down, true);
},
sdl3::event::Event::MouseButtonDown { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => {
pointer = normalize_mouse_pointer(&canvas, true, x, y);
input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true);
input = apply_pointer_direction(input, pointer);
},
sdl3::event::Event::MouseButtonUp { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => {
pointer = normalize_mouse_pointer(&canvas, false, x, y);
@@ -62,6 +75,7 @@ impl SdlRuntime {
sdl3::event::Event::FingerDown { x, y, .. } => {
pointer = engine_v1_common::PointerState::normalized(true, x, y);
input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true);
input = apply_pointer_direction(input, pointer);
},
sdl3::event::Event::FingerMotion { x, y, .. } => {
pointer = engine_v1_common::PointerState::normalized(true, x, y);
@@ -119,3 +133,20 @@ fn render_scene(canvas: &mut sdl3::render::WindowCanvas, scene: engine_v1_common
canvas.present();
return std::result::Result::Ok(());
}
fn apply_pointer_direction(mut input: engine_v1_common::InputState, pointer: engine_v1_common::PointerState) -> engine_v1_common::InputState {
let horizontal = pointer.x() - 0.5;
let vertical = pointer.y() - 0.5;
if horizontal.abs() >= vertical.abs() {
if horizontal < 0.0 {
input = input.with_action(engine_v1_common::GameAction::Left, true);
} else {
input = input.with_action(engine_v1_common::GameAction::Right, true);
}
} else if vertical < 0.0 {
input = input.with_action(engine_v1_common::GameAction::Up, true);
} else {
input = input.with_action(engine_v1_common::GameAction::Down, true);
}
return input;
}

View File

@@ -1,10 +1,55 @@
// file: crates/games/game-snake-poc/src/state.rs
// version: 3
// version: 4
/// Minimal Snake state used to validate reusable engine dependencies.
const GRID_HEIGHT: i16 = 20;
const GRID_WIDTH: i16 = 12;
const MAX_SEGMENTS: usize = 48;
const MOVE_EVERY_FRAMES: u64 = 8;
/// Grid cell used by the Snake POC.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SnakeCell {
x: i16,
y: i16,
}
impl SnakeCell {
/// Creates one grid cell.
#[must_use]
pub const fn new(x: i16, y: i16) -> Self {
return Self { x, y };
}
/// Returns the horizontal grid coordinate.
#[must_use]
pub const fn x(self) -> i16 {
return self.x;
}
/// Returns the vertical grid coordinate.
#[must_use]
pub const fn y(self) -> i16 {
return self.y;
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SnakeDirection {
Left,
Right,
Up,
Down,
}
/// Playable deterministic Snake POC state.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SnakeState {
length: u32,
segments: [SnakeCell; MAX_SEGMENTS],
length: usize,
direction: SnakeDirection,
food: SnakeCell,
food_index: u64,
score: u64,
}
impl Default for SnakeState {
@@ -17,28 +62,161 @@ impl SnakeState {
/// Creates a fresh Snake state.
#[must_use]
pub const fn new() -> Self {
return Self { length: 1 };
let mut segments = [SnakeCell::new(0, 0); MAX_SEGMENTS];
segments[0] = SnakeCell::new(5, 10);
segments[1] = SnakeCell::new(4, 10);
segments[2] = SnakeCell::new(3, 10);
return Self {
segments,
length: 3,
direction: SnakeDirection::Right,
food: SnakeCell::new(8, 10),
food_index: 0,
score: 0,
};
}
/// Returns the current snake length.
#[must_use]
pub const fn length(self) -> u32 {
pub const fn length(self) -> usize {
return self.length;
}
/// Registers one consumed growth item.
pub const fn grow(&mut self) {
self.length = self.length.saturating_add(1);
/// Returns the current score.
#[must_use]
pub const fn score(self) -> u64 {
return self.score;
}
/// Returns the current food cell.
#[must_use]
pub const fn food(self) -> SnakeCell {
return self.food;
}
/// Returns the current head cell.
#[must_use]
pub const fn head(self) -> SnakeCell {
return self.segments[0];
}
fn apply_direction_input(&mut self, input: engine_v1_common::InputState) {
if input.is_active(engine_v1_common::GameAction::Left) && self.direction != SnakeDirection::Right {
self.direction = SnakeDirection::Left;
} else if input.is_active(engine_v1_common::GameAction::Right) && self.direction != SnakeDirection::Left {
self.direction = SnakeDirection::Right;
} else if input.is_active(engine_v1_common::GameAction::Up) && self.direction != SnakeDirection::Down {
self.direction = SnakeDirection::Up;
} else if input.is_active(engine_v1_common::GameAction::Down) && self.direction != SnakeDirection::Up {
self.direction = SnakeDirection::Down;
}
return;
}
fn move_once(&mut self) {
let head = self.head();
let next = match self.direction {
SnakeDirection::Left => SnakeCell::new(head.x - 1, head.y),
SnakeDirection::Right => SnakeCell::new(head.x + 1, head.y),
SnakeDirection::Up => SnakeCell::new(head.x, head.y - 1),
SnakeDirection::Down => SnakeCell::new(head.x, head.y + 1),
};
if self.hits_wall(next) || self.hits_body(next) {
*self = Self::new();
return;
}
let ate = next == self.food;
let new_length = if ate { self.length.saturating_add(1).min(MAX_SEGMENTS) } else { self.length };
let mut index = new_length.saturating_sub(1);
while index > 0 {
self.segments[index] = self.segments[index - 1];
index -= 1;
}
self.segments[0] = next;
self.length = new_length;
if ate {
self.score = self.score.saturating_add(1);
self.food_index = self.food_index.saturating_add(1);
self.food = food_for_index(self.food_index);
if self.food_overlaps_snake() {
self.food_index = self.food_index.saturating_add(1);
self.food = food_for_index(self.food_index);
}
}
return;
}
fn hits_body(self, cell: SnakeCell) -> bool {
let mut index = 0;
while index < self.length {
if self.segments[index] == cell {
return true;
}
index += 1;
}
return false;
}
fn hits_wall(self, cell: SnakeCell) -> bool {
return cell.x < 0 || cell.y < 0 || cell.x >= GRID_WIDTH || cell.y >= GRID_HEIGHT;
}
fn food_overlaps_snake(self) -> bool {
return self.hits_body(self.food);
}
}
impl engine_v1_common::EngineGame for SnakeState {
fn update(&mut self, _frame: engine_v1_common::EngineFrame, input: engine_v1_common::InputState) {
if input.is_active(engine_v1_common::GameAction::Primary) {
self.grow();
fn update(&mut self, frame: engine_v1_common::EngineFrame, input: engine_v1_common::InputState) {
self.apply_direction_input(input);
if frame.index() % MOVE_EVERY_FRAMES == 0 {
self.move_once();
}
return;
}
fn scene(&self) -> engine_v1_common::EngineScene {
let mut scene = engine_v1_common::EngineScene::empty(engine_v1_common::RenderColor::rgb(14, 20, 18));
scene = scene.with_rect(engine_v1_common::RenderRect::new(cell_rect(self.food), engine_v1_common::RenderColor::rgb(255, 96, 72)));
let mut index = 0;
while index < self.length {
let color = if index == 0 {
engine_v1_common::RenderColor::rgb(120, 255, 160)
} else {
engine_v1_common::RenderColor::rgb(64, 192, 112)
};
scene = scene.with_rect(engine_v1_common::RenderRect::new(cell_rect(self.segments[index]), color));
index += 1;
}
return scene;
}
}
fn cell_rect(cell: SnakeCell) -> engine_v1_common::NormalizedRect {
let cell_height = 1.0 / GRID_HEIGHT as f32;
let cell_width = 1.0 / GRID_WIDTH as f32;
let inset_x = cell_width * 0.08;
let inset_y = cell_height * 0.08;
return engine_v1_common::NormalizedRect::new(
cell.x as f32 * cell_width + inset_x,
cell.y as f32 * cell_height + inset_y,
cell_width - inset_x * 2.0,
cell_height - inset_y * 2.0,
);
}
fn food_for_index(index: u64) -> SnakeCell {
const FOODS: [SnakeCell; 8] = [
SnakeCell::new(8, 10),
SnakeCell::new(8, 5),
SnakeCell::new(2, 5),
SnakeCell::new(2, 15),
SnakeCell::new(9, 15),
SnakeCell::new(9, 3),
SnakeCell::new(1, 3),
SnakeCell::new(6, 17),
];
return FOODS[index as usize % FOODS.len()];
}
#[cfg(test)]

View File

@@ -1,25 +1,62 @@
// file: crates/games/game-snake-poc/unit_tests/state.rs
// version: 1
// version: 2
fn frame(index: u64) -> engine_v1_common::EngineFrame {
return engine_v1_common::EngineFrame::new(index, std::time::Duration::from_millis(16), std::time::Duration::ZERO);
}
#[test]
fn growth_increments_length() {
game_logging_lib::with_test_tracing("growth_increments_length", || {
fn eating_food_grows_and_scores() {
game_logging_lib::with_test_tracing("eating_food_grows_and_scores", || {
let mut state = crate::SnakeState::new();
state.grow();
assert_eq!(state.length(), 2);
let initial_length = state.length();
let mut step = 0;
while step < 3 {
engine_v1_common::EngineGame::update(&mut state, frame(step * 8), engine_v1_common::InputState::none());
step += 1;
}
assert_eq!(state.length(), initial_length + 1);
assert_eq!(state.score(), 1);
assert_ne!(state.food(), crate::SnakeCell::new(8, 10));
});
}
#[test]
fn engine_update_maps_primary_action_to_growth() {
game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_growth", || {
fn opposite_direction_is_rejected() {
game_logging_lib::with_test_tracing("opposite_direction_is_rejected", || {
let mut state = crate::SnakeState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.length(), 2);
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Left, true);
engine_v1_common::EngineGame::update(&mut state, frame(0), input);
assert_eq!(state.head(), crate::SnakeCell::new(6, 10));
});
}
#[test]
fn scene_contains_food_and_snake_segments() {
game_logging_lib::with_test_tracing("scene_contains_food_and_snake_segments", || {
let state = crate::SnakeState::new();
let scene = engine_v1_common::EngineGame::scene(&state);
let occupied = scene.rectangles().iter().filter(|slot| slot.is_some()).count();
assert_eq!(occupied, state.length() + 1);
});
}
#[test]
fn snake_moves_on_fixed_cadence() {
game_logging_lib::with_test_tracing("snake_moves_on_fixed_cadence", || {
let mut state = crate::SnakeState::new();
let start = state.head();
engine_v1_common::EngineGame::update(&mut state, frame(0), engine_v1_common::InputState::none());
assert_eq!(state.head(), crate::SnakeCell::new(start.x() + 1, start.y()));
});
}
#[test]
fn up_direction_changes_movement() {
game_logging_lib::with_test_tracing("up_direction_changes_movement", || {
let mut state = crate::SnakeState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Up, true);
engine_v1_common::EngineGame::update(&mut state, frame(0), input);
assert_eq!(state.head(), crate::SnakeCell::new(5, 9));
});
}

106
deltas/0.1.0/0-pre.11.md Normal file
View File

@@ -0,0 +1,106 @@
<!-- file: deltas/0.1.0/0-pre.11.md -->
<!-- version: 1 -->
# Delta 0.1.0-0-pre.11
## Base
Base validée : `0.1.0-0-pre.10.fix.1`.
## Objectifs
- rendre Snake réellement jouable ;
- valider la réutilisation de `EngineScene` et du backend input SDL3 ;
- figer le NDK au niveau projet sans variable globale ;
- permettre un smoke natif x86_64 sur l'AVD Linux.
## Snake
Le POC utilise une grille 12 × 20, un serpent initial de trois segments, un déplacement automatique, quatre directions, une nourriture déterministe, croissance/score et reset sur collision mur/corps.
Le rendu réutilise exclusivement `EngineScene` et `RenderRect`.
## Input
Desktop : flèches clavier.
Souris/tactile : le clic/tap produit aussi une direction selon l'axe dominant depuis le centre de l'écran. Reflex ignore ces directions ; Snake les consomme.
## NDK
La propriété unique :
```text
androidNdkVersion=28.2.13676358
```
est lue par Gradle et `scripts/build_android_rust.py`.
Le script définit `ANDROID_NDK_HOME` uniquement dans l'environnement du sous-processus `cargo ndk`.
## Validation
```bash
cargo fmt --all
cargo fmt --all -- --check
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android deltas history
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p engine-v1-sdl --all-targets --all-features
cargo test -p game-snake-poc --all-targets --all-features
```
Smoke Desktop :
```bash
cargo run -p game-snake-poc-desktop
```
Critères : serpent/nourriture visibles, déplacement automatique, flèches fonctionnelles, demi-tour refusé, croissance sur nourriture, reset sur collision, fermeture propre.
Android ARM64 :
```bash
python3 scripts/build_android_rust.py snake --abi arm64-v8a
```
Android x86_64 pour l'AVD :
```bash
rustup target add x86_64-linux-android
python3 scripts/build_android_rust.py snake --abi x86_64
```
Puis :
```bash
cd Android
gradle :game-snake-poc:assembleDebug
cd ..
```
Vérifier :
```bash
unzip -l Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk | grep 'lib/arm64-v8a/libgame_android_entrypoint.so'
unzip -l Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk | grep 'lib/x86_64/libgame_android_entrypoint.so'
```
Smoke AVD :
```bash
adb devices
adb shell getprop ro.product.cpu.abi
adb install -r Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk
adb shell am start -n com.sasedev.games.snake/.SnakeActivity
```
## Transition
Si ces gates sont propres, `0.1.0-0-pre.11` termine la phase `0-pre.*` prévue et permet de passer à `0.1.0-1-alpha.1`.
En cas d'échec imputable au projet, produire `0.1.0-0-pre.11.fix.1`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/004-INPUT_AND_CONTROLS.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Abstraction des entrées et contrôles
@@ -78,3 +78,9 @@ Le gameplay ne dépend d'aucun type Android, Java ou SDL3.
- `MouseButtonUp`, `FingerUp` et `FingerCanceled` désactivent le pointeur.
Cette séparation empêche un contact maintenu de produire un succès par frame.
## Directions clavier et tactile
Les flèches clavier produisent `Left`, `Right`, `Up` et `Down`.
Lors d'un clic/tap, la position normalisée produit également une direction selon l'axe dominant depuis le centre. Reflex ignore ces directions ; Snake les consomme. Le backend SDL3 reste ainsi commun aux jeux.

View File

@@ -0,0 +1,32 @@
<!-- file: history/0.1.0/0-pre.10.fix.1.md -->
<!-- version: 1 -->
# Historique 0.1.0-0-pre.10.fix.1
## Statut
Validé techniquement par l'utilisateur le 2026-09-16.
## Gates validées
- formatage Rust ;
- audits Rust/workspace ;
- audit Markdown : `1 table(s), 70 file(s)` ;
- `cargo check --workspace` ;
- Clippy global strict ;
- tests `engine-v1-common` : 6/6 ;
- test `engine-v1-sdl` : 1/1 ;
- tests `game-reflex-poc` : 4/4 ;
- smoke Desktop Reflex propre ;
- build Rust Android Reflex ;
- assemblage APK Reflex.
## Runtime Android
Un AVD API 36 est disponible sous `emulator-5554`. L'APK Reflex s'installe avec succès et l'Activity démarre via `adb shell am start`.
Le contrôle interactif tactile complet n'a pas encore été consigné comme gate observée.
## Portée durable
Ce jalon valide le modèle `EngineScene`, le rendu SDL3 par rectangles normalisés et le gameplay Reflex Desktop, avec chaîne Android exécutable sur émulateur.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# file: scripts/build_android_rust.py
# version: 2
# version: 3
"""Build one Android Rust game entrypoint and stage SDL3 and Rust jniLibs."""
@@ -22,13 +22,18 @@ ABI_TO_TRIPLE = {
}
GAME_TO_MODULE = {"reflex": "game-reflex-poc", "snake": "game-snake-poc"}
def read_sdl3_aar_name(properties_path: pathlib.Path) -> str:
"""Read the configured SDL3 AAR filename."""
def read_gradle_property(properties_path: pathlib.Path, property_name: str) -> str:
"""Read one required Android Gradle property."""
prefix = f"{property_name}="
for raw_line in properties_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if line.startswith("sdl3AarName="):
if line.startswith(prefix):
return line.split("=", 1)[1].strip()
raise RuntimeError("sdl3AarName is missing from Android/gradle.properties")
raise RuntimeError(f"{property_name} is missing from Android/gradle.properties")
def read_sdl3_aar_name(properties_path: pathlib.Path) -> str:
"""Read the configured SDL3 AAR filename."""
return read_gradle_property(properties_path, "sdl3AarName")
def find_sdl3_member(archive: zipfile.ZipFile, abi: str) -> str:
"""Return the SDL3 shared-library member for one Android ABI."""
@@ -75,7 +80,9 @@ def main() -> int:
parser.add_argument("--release", action="store_true")
arguments = parser.parse_args()
root = pathlib.Path(__file__).resolve().parent.parent
aar_name = read_sdl3_aar_name(root / "Android" / "gradle.properties")
properties_path = root / "Android" / "gradle.properties"
aar_name = read_sdl3_aar_name(properties_path)
ndk_version = read_gradle_property(properties_path, "androidNdkVersion")
aar_path = root / "Android" / "libs" / aar_name
if not aar_path.is_file():
print(f"missing SDL3 AAR: {aar_path}", file=sys.stderr)
@@ -90,6 +97,15 @@ def main() -> int:
print(str(error), file=sys.stderr)
return 2
environment = os.environ.copy()
android_home = environment.get("ANDROID_HOME")
if not android_home:
print("ANDROID_HOME is required to resolve the project NDK", file=sys.stderr)
return 2
ndk_home = pathlib.Path(android_home) / "ndk" / ndk_version
if not ndk_home.is_dir():
print(f"configured Android NDK is missing: {ndk_home}", file=sys.stderr)
return 2
environment["ANDROID_NDK_HOME"] = str(ndk_home)
rustflags = environment.get("RUSTFLAGS", "").strip()
link_flag = f"-L native={link_dir}"
environment["RUSTFLAGS"] = f"{rustflags} {link_flag}".strip()