Android
By the end of this guide your Lua project runs on an Android phone as an app you can install from the launcher.
Android is the easier of the two mobile platforms. Nothing has to be approved by anyone: the packer makes a signing key for you on first use, and a device in developer mode installs what you hand it. You need the Android SDK command-line tools and a phone or an emulator.
How an Android build is split in two
We cross-compile the engine and ship it inside a ready APK. Your project goes in beside it, and the result is signed and installed:
we build → natiny.apk the engine and Lua, already inside an APK
you pack → my_game.apk the same engine, running your scripts
The step in the middle is android/pack.py, which ships in the bundle. It
never compiles anything, so repacking after an edit takes a second.
Install the tools
You need the Android SDK build-tools, one platform, and a JDK 17 or newer for the signer. If you have Android Studio, you have all three. Otherwise install the command-line tools and run:
sdkmanager "build-tools;35.0.0" "platforms;android-35"
sdkmanager --licenses
export ANDROID_HOME="$HOME/Android/Sdk"
Set ANDROID_HOME to the real SDK directory - the path above is the usual one
on Linux, and macOS puts it in ~/Library/Android/sdk. The packer also takes
--sdk DIR if you would rather not export anything.
Then connect a phone with USB debugging turned on, or start an emulator, and check that it is visible:
adb devices
One device listed as device means you are ready. unauthorized means the
phone is waiting for you to accept the debugging prompt on its screen.
Unzip the bundle and run the example
Download the Lua wrapper bundle - android-arm64 for a physical phone,
android-amd64 for an x86_64 emulator:
natiny-android_arm64-apk/
├── natiny.apk the engine and Lua, ready to install
├── android/
│ ├── pack.py puts your project into an APK
│ ├── AndroidManifest.xml.in
│ └── res/ the launcher icon
└── data/
└── main.lua the script inside natiny.apk, to read and to edit
Install it before touching anything else:
adb install -r natiny.apk
Open natiny from the launcher. The Natiny logo appears on a dark
background. That is data/main.lua running, and it tells you the device, the
architecture and the driver are all fine before any of your own code is
involved.
If the install itself fails, the phone usually needs Install unknown apps allowed for whatever is handing the APK over - this one is signed with a debug key, not by Google Play.
Put your own scripts in an APK
Make a project folder with main.lua at its root:
my_game/
└── main.lua
python android/pack.py --project ./my_game --apk natiny.apk \
--package com.example.my_game --name "My Game"
adb install -r my_game.apk
--apk natiny.apk is where the engine comes from: the packer reuses the
downloaded APK, so nothing is compiled. --package is the application id, and
two apps cannot share one - give your own before you hand the APK to anybody.
Open My Game. It runs your main.lua. When it closes during startup
instead, the reason is in the log:
adb logcat -s natiny
Why not just edit natiny.apk? Signature schemes v2 and v3 sign the archive itself, not its entries. Replacing a file inside a signed APK invalidates the signature and the device refuses to install it. Repacking is the only way, and that is what
pack.pydoes.
Add assets and keep your paths
The whole --project folder becomes data/ inside the app:
my_game/
├── main.lua
├── player.lua
└── fonts/
└── roboto.ttf
local font_data = natiny.resource.load("data/fonts/roboto.ttf")
That is the same string a desktop build uses, so a project moves between platforms without touching its paths.
Assets inside an APK are read-only. Files written at run time go to the app's private directory instead, and the resource API looks there when loading, so a save and the load after it agree about where the file went:
natiny.resource.save(save_resource, "save.json")
local loaded = natiny.resource.load("save.json")
Set the name, the icon and the orientation
python android/pack.py --project ./my_game --apk natiny.apk \
--package com.example.my_game --name "My Game" \
--icon icon.png --orientation sensorLandscape \
--version-name 1.2.0 --version-code 7
--icontakes a square PNG, 192×192 or larger.--orientationis what the manifest asks for:portrait,landscape,sensorLandscape,lockedand a few more.--version-nameis what users see.--version-codeis the integer a device compares when deciding whether an APK is an update, and it has to increase with every release you publish.
python android/pack.py --help lists the rest.
Sign it with your own key before you publish
Without --keystore, the packer creates a debug key at
~/.natiny/debug.keystore on first use and reuses it afterwards. That is what
keeps adb install -r working between builds - a key made fresh each time
would make every build look like a different app.
A debug key is fine for testing and useless for distribution. For a release, sign with a key you keep:
python android/pack.py --project ./my_game --apk natiny.apk \
--package com.example.my_game --name "My Game" \
--keystore release.jks --key-alias mygame --key-pass "$KEYSTORE_PASSWORD"
Keep that keystore safe and backed up. Google Play ties an application id to the key that first signed it, and losing the key means you cannot publish an update to that app again.
Typical problems
cannot find the Android SDK-ANDROID_HOMEdoes not point at the SDK directory. Set it, or pass--sdk.- A note about no
main.luaat the root ---projectpoints at the wrong folder. The packer says it and carries on, and the app then starts with nothing to run. INSTALL_FAILED_UPDATE_INCOMPATIBLE- an app with the same application id is installed and was signed with a different key. Uninstall it, which also removes its saved data, or sign with the original key.INSTALL_FAILED_NO_MATCHING_ABIS- the bundle's architecture does not match the device. Useandroid-arm64for phones andandroid-amd64for an x86_64 emulator.- The app installs and closes immediately - read
adb logcat -s natiny. - A device older than Android 7.0 cannot run it at all. Natiny draws with Vulkan, and API 24 is the release where a device is required to support it.
What you have now
Your scripts run on Android, and the loop is short: edit, repack, adb install
-r. The engine is never rebuilt, because it comes out of the APK you
downloaded.
A touch arrives as the left mouse button at the finger's position, so a
project written for a desktop works on a phone as it stands. For more than one
finger, natiny.input reports every touch.