iOS

By the end of this guide your Lua project runs on your own iPhone, launched from the home screen.

Read this first. iOS is the hardest platform Natiny supports, and most of the difficulty has nothing to do with the engine. Apple requires every app on a device to be signed by a registered developer, so half of this guide is about certificates, provisioning profiles and Xcode. The guide assumes you already know what code signing is and how apps reach a device. If you have never signed anything before, expect to spend an hour here, and expect to use Xcode a little even though Natiny never asks you to.

Everything below happens on a Mac. There is no way around that: the tools that sign an app are Apple's and run nowhere else.

How an iOS build is split in two

Every other platform gives you a file you can run. iOS gives you an archive, because a signature has to be added before a phone will accept the app - and a signature needs a certificate that identifies you. That certificate lives in your keychain and cannot travel inside a download.

So the work is split:

TEXT
we cross-compile        →  libnatiny_player_ios_arm64.a   (in the bundle)
you link, pack and sign →  MyGame.app                     (on your Mac)

make_app.py, which ships inside the bundle, does the second half. You will run it twice: once unsigned to check the toolchain, and once for real.

Check that your Mac can build for iOS

You need Xcode - the application, not only the command-line tools. The tools carry the macOS SDK and no other, so a Mac can have clang, ld and codesign and still be unable to compile for a phone.

Shell
xcrun --sdk iphoneos --show-sdk-path

A path means you are ready. If it says SDK "iphoneos" cannot be located, install Xcode from the App Store, open it once so it finishes installing its components, then point the tools at it:

Shell
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept

Python 3.8 or newer is the only other requirement, and macOS already has it.

Unzip the bundle

Download the Lua wrapper bundle for ios-arm64 and unzip it. Four things matter:

TEXT
natiny-ios_arm64-app/
├── libnatiny_player_ios_arm64.a   the interpreter, the engine and Lua
├── make_app.py                    turns that archive into an .app
├── Info.plist.in                  the template it fills in
└── data/
    └── main.lua                   the script that starts

data/ is the example project. Later you will point the packer at your own folder; for now the example is what we want, because if something goes wrong you will know it is not your Lua code.

Build an unsigned app to check the toolchain

Run the packer with nothing but a project and a name. It finds the archive and the template beside itself:

Shell
cd natiny-ios_arm64-app
python3 make_app.py --project ./data --name "My Game" \
    --bundle-id com.example.mygame

You should see the link command, then a line like:

TEXT
  -> /Users/you/natiny-ios_arm64-app/My Game.app  1.3 MB  com.example.mygame 1.0

My Game.app is a real app bundle: your executable, your data/ folder and an Info.plist. What it is not is installable. Without --identity the packer signs it ad-hoc, which no device accepts.

That is worth stating plainly, because the next thought is usually the Simulator: this bundle will not run there either. It is built for arm64-apple-ios, and the Simulator runs arm64-apple-ios-simulator - a different platform, compiled against a different SDK. Natiny has no simulator build. On an Apple silicon Mac the Simulator may even install it and then crash on launch, which looks like a bug in the app and is not.

So: the toolchain works, and now we make the app installable.

Register an app identifier with Apple

An app on a device is identified by its bundle identifier, and Apple keeps one global namespace of them. Your identifier has to be unused.

Warning: com.example.mygame, used above and in every example on this page, is taken. So is anything else starting with com.example. Xcode will answer "cannot be registered to your development team because it is not available". Derive your own from a domain or an account you control - ru.yourname.mygame works.

Pick yours now. It has to be the same string in three places: the Xcode project below, the provisioning profile, and --bundle-id.

Get a certificate and a profile through Xcode

Two things have to exist before a phone will run your app:

  • a certificate, which proves the app was signed by a registered developer;
  • a provisioning profile, which says this app identifier may run on this particular device.

A free Apple ID gets you both. Xcode is what issues them, and the shortest path is to let it do that once for a throwaway project.

  1. Open Xcode → Settings → Accounts and add your Apple ID. Xcode creates an Apple Development certificate in your keychain.
  2. Connect the iPhone with a cable, unlock it, and tap Trust This Computer.
  3. On the phone, turn on Settings → Privacy & Security → Developer Mode and let it restart. Without it, a development-signed app will not install at all.
  4. In Xcode: File → New → Project → iOS → App. Any product name will do, but set Bundle Identifier to the one you chose, and set Team to your personal team.
  5. Select your iPhone as the run destination and press Run (⌘R).

That last step is the one that matters. Building to a connected device is what makes Xcode register the device and issue a profile for your identifier. When the throwaway app appears on the phone, you can close the project and never open it again.

Find the two names the packer asks for

The certificate:

Shell
security find-identity -v -p codesigning
TEXT
  1) 4738DCEAD9968002836BCAC7DFD05A619052CCAD "Apple Development: you@mail.com (77UNP8U994)"

Either half works: the long hash, or the name in quotes. The hash has no spaces in it, so it is harder to mistype.

The profile is a file on disk. Xcode 16 keeps profiles in one place and earlier versions in another, so look in both, and print which identifier each one covers:

Shell
for d in ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles \
         ~/Library/MobileDevice/Provisioning\ Profiles; do
  [ -d "$d" ] || continue
  for p in "$d"/*.mobileprovision; do
    echo "$p -> $(security cms -D -i "$p" | plutil -extract Entitlements.application-identifier raw -)"
  done
done

Each line ends with TEAMID.your.bundle.id. Copy the one matching your identifier next to the bundle:

Shell
cp <that-file>.mobileprovision ~/natiny-ios_arm64-app/mygame.mobileprovision

If both folders are empty, step 5 above did not happen. Adding an Apple ID creates the certificate; only a build to a connected device creates the profile.

Build the signed app

Same command as before, plus the two you just found:

Shell
python3 make_app.py --project ./data --name "My Game" \
    --bundle-id ru.yourname.mygame \
    --identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
    --profile mygame.mobileprovision

The packer links the archive, writes Info.plist, copies data/ in, embeds the profile, reads the entitlements out of it the way Xcode does, and signs the bundle. It checks the identity against your keychain before it starts, so a mistyped name fails immediately instead of after the build.

Install it on the phone

devicectl ships with Xcode 15 and newer:

Shell
xcrun devicectl list devices
xcrun devicectl device install app --device <UDID> "My Game.app"

On older Xcode, open Window → Devices and Simulators, select the phone, and drag the .app onto Installed Apps.

The app is now on the home screen with a plain grey icon - we have not given it one yet.

Run it and read what it prints

The first launch of an app signed with a free account is refused: the phone does not trust your certificate yet. Go to Settings → General → VPN & Device Management → Developer App and trust it. Then tap the icon.

The Natiny logo appears on a dark background. That is the example data/main.lua, and it means the whole chain works: cross-compiled engine, your signature, your device.

When something goes wrong instead, launch it from the Mac so you can see why:

Shell
xcrun devicectl device process launch --console --device <UDID> ru.yourname.mygame

--console prints the program's output in your terminal. A crash with no Natiny output at all is usually a signing problem; output that stops partway is your Lua code.

Put your own project in the app

Everything under --project becomes data/ inside the bundle, and data/main.lua is what starts:

TEXT
my_game/
├── main.lua
├── player.lua
└── fonts/
    └── roboto.ttf
Shell
python3 make_app.py --project ./my_game --name "My Game" \
    --bundle-id ru.yourname.mygame \
    --identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
    --profile mygame.mobileprovision

Resource paths do not change between platforms:

Lua
local font_data = natiny.resource.load("data/fonts/roboto.ttf")

An app bundle is read-only, so files written at run time go to the app's Documents folder instead. The resource API knows that and looks there when loading, so a save and the load after it agree about where the file went:

Lua
natiny.resource.save(save_resource, "save.json")
local loaded = natiny.resource.load("save.json")

Nothing is compiled when you repack, so this step takes about a second.

Give it an icon, a version and a real name

Shell
python3 make_app.py --project ./my_game --name "My Game" \
    --bundle-id ru.yourname.mygame \
    --identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
    --profile mygame.mobileprovision \
    --icon icon.png --version 1.2.0 --build 7
  • --icon takes a square PNG, 120×120 or larger. Without it you get the grey square you saw above.
  • --version is what the App Store shows; --build is the number that must increase with every upload.
  • --ipa MyGame.ipa additionally writes the zipped bundle that App Store Connect and device installers take. You do not need it for cable installs.

python3 make_app.py --help lists the rest.

Typical problems

  • SDK "iphoneos" cannot be located - Xcode is missing, or installed and not selected. See the first section.
  • cannot be registered to your development team - the bundle identifier is taken. Choose one of your own.
  • no signing identity matches ... - the string does not match your keychain. The packer prints what you do have; copy one of those.
  • The install fails - Developer Mode is off, the profile does not cover this identifier, or the profile does not list this device.
  • The app installs but will not open - the certificate is not trusted on the phone yet.
  • The app stops launching after about a week - a free account's profile expired. Rebuild and reinstall; nothing else changes.

What you have now

Your Lua project runs on a real iPhone, and the loop is short: edit the scripts, rerun make_app.py, reinstall. The engine never has to be rebuilt, because the archive in the bundle already contains it.

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.