Guide
Packaging
nsh package turns an app into something you can install: a .app for the iOS simulator, a signed .ipa for iOS
devices, or a signed .apk for Android. It combines a release build of your app with the prebuilt runtime that
ships with nsh, so it needs no Rust toolchain, no Xcode project and no Gradle.
nsh package App.tsx --target ios-simulator --run # build, install and launch in the simulator
nsh package App.tsx --target android --run # build, install and launch on a device or emulator
nsh package App.tsx --target ios --sign "Apple Development: Jane Doe (ABCDE12345)" --profile Dev.mobileprovision
The app is built in release mode (minified, compiled to QuickJS bytecode) exactly as nsh build --release would build
it. Output goes to out/ unless you pass --out.
#App metadata: nsh.json
nsh package reads nsh.json next to the entry file. nsh init writes one:
{
"name": "Gallery",
"id": "com.example.gallery",
"version": "1.0.0",
"build": 1,
"icon": "icon.png"
}
| Key | Type | Default | Used for |
|---|---|---|---|
name | string | the entry’s directory name | The display name under the icon (CFBundleDisplayName, Android android:label) and the output file name. |
id | string | com.example.<name> | The bundle identifier / Android package name. |
version | string | "1.0.0" | The user-visible version (CFBundleShortVersionString, versionName). |
build | number | 1 | The build number (CFBundleVersion, versionCode). Raise it for every release you upload. |
icon | string | none | A square PNG, ideally 1024×1024, relative to nsh.json. |
nsh.json may also hold tailwind (see Tailwind) and capabilities (see
Sandbox and capabilities); those are read by the bundler. Unknown keys are an error.
Every key can be overridden on the command line, which is handy in CI:
nsh package App.tsx --target android --app-version 1.4.0 --build 42
#Options
| Flag | Description |
|---|---|
--target <ios | ios-simulator | android> | What to build for (required). |
-o, --out <dir> | Output directory. Default out. |
--name <name> | Display name. |
--id <id> | Bundle identifier, such as com.example.app. |
--app-version <version> | Version, such as 1.2.0. |
--build <n> | Build number. |
--icon <png> | App icon: a square PNG, ideally 1024×1024. |
--sign <identity> | iOS: the code signing identity for devices. |
--profile <file> | iOS: the provisioning profile for devices (.mobileprovision). |
--keystore <file> | Android: a release keystore (default: the debug key). |
--key-alias <alias> | Android: the key alias in the keystore. |
--keystore-password <password> | Android: the keystore password; also read from NSH_KEYSTORE_PASSWORD. |
--run | Install and launch afterwards: in the iOS simulator, or on the Android device or emulator adb sees. Not for --target ios. |
#iOS simulator
nsh package App.tsx --target ios-simulator --run
Writes out/<Name>.app, signed ad hoc (the simulator needs a signature but no identity). With --run it boots a
simulator if none is running, installs the app and launches it. Requires Xcode.
#iOS devices
nsh package App.tsx --target ios \
--sign "Apple Distribution: Example Inc (ABCDE12345)" \
--profile AppStore.mobileprovision
Writes out/<Name>.ipa, ready for TestFlight, App Store upload tools or direct installation (depending on the
profile). What happens:
- The provisioning profile is copied into the app as
embedded.mobileprovision. - The entitlements come from the profile. nsh checks that the profile’s application identifier covers the app’s
id(wildcard profiles such asTEAMID.*orTEAMID.com.example.*work) and stops with an error otherwise. - The app is signed with
codesignusing the identity, then zipped into an.ipa(Payload/<Name>.app).
Find your identities with:
security find-identity -v -p codesigning
Without --sign and --profile, nsh writes an unsigned out/<Name>.app and tells you so; it cannot be installed on a
device as it is.
The app supports iPhone and iPad, portrait and landscape (portrait upside down on iPad only), and needs iOS 15.0 or newer.
#Android
nsh package App.tsx --target android --run
Writes out/<Name>.apk. The APK is assembled directly with the SDK’s build tools (aapt2, zipalign, apksigner),
without Gradle. Requirements:
- The Android SDK, found through
ANDROID_HOME(orANDROID_SDK_ROOT, or~/Library/Android/sdk), with build tools and theplatforms;android-35package. nsh tells you thesdkmanagercommand when one is missing. - A JDK, to create the debug keystore the first time.
The app targets API 35 and needs API 28 (Android 9) or newer; it contains native code for arm64-v8a and x86_64.
#Signing for release
Without --keystore, the APK is signed with the standard debug key (~/.android/debug.keystore, created if missing),
and nsh warns that it is not for store releases. For a release, create a keystore once and pass it:
keytool -genkeypair -v -keystore release.jks -alias upload -keyalg RSA -keysize 2048 -validity 10000
NSH_KEYSTORE_PASSWORD=… nsh package App.tsx --target android \
--keystore release.jks --key-alias upload
Prefer the environment variable over --keystore-password so the password does not end up in your shell history.
#Icons
Give one square PNG, ideally 1024×1024, in nsh.json ("icon": "icon.png") or with --icon:
- iOS: compiled by Xcode’s
actoolinto the app’s asset catalog (Assets.car), which derives every size iOS needs. - Android: resized to 192×192 and used as the launcher icon (
mipmap-xxxhdpi).
Without an icon, the system shows its default.
#What is inside
The package holds your app’s bytecode, its capabilities.json (when it declares capabilities) and the assets it
imports, next to the prebuilt runtime:
| Target | Runtime | Your app |
|---|---|---|
| iOS | the nsh-ios-app executable | <Name>.app/nsh/bundle.qbc, nsh/assets/… |
| Android | classes.dex, lib/<abi>/libnsh.so (stored uncompressed, 16 KB aligned) | assets/nsh/bundle.qbc, assets/nsh/assets/… |
A packaged app registers no native components; for those, embed nsh in your own app.
#Finding the runtime
nsh package looks for the prebuilt runtime next to the CLI (bin/../runtime in the distribution). In a checkout of
nsh it uses target/<triple>/<profile> and target/android/<profile>, with the CLI’s own profile (a debug CLI uses
debug runtimes): build them with cargo build -p nsh-ios-app --target aarch64-apple-ios-sim (or aarch64-apple-ios
for devices) and cargo xtask android (add --release for a release CLI). The environment variable NSH_RUNTIME
points the CLI at another directory laid out like the distribution’s runtime/ (ios/iphoneos,
ios/iphonesimulator, android/release, …).
#See also
- CLI: every command
- Signed packages: updates apps download
- Project structure:
nsh.jsonand the files of an app