Docs
Packaging
nsh package turns an app into something you can install: a .app for the iOS simulator, a signed .ipa for iOS
devices, a signed .apk for Android, an ad hoc signed .app for macOS, a directory and .tar.gz for Linux, a directory and .zip for Windows, or a static site for the web; --target gpui makes this machine’s desktop package with the GPUI runner. 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 macos --run # build and launch on this Mac
nsh package App.tsx --target linux --run # build and launch on this machine
nsh package App.tsx --target windows --run # build and launch on this machine
nsh package App.tsx --target web --run # build, serve on this machine and open in the browser
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 | macos | linux | windows | gpui | web> | 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; macOS, Linux, Windows and GPUI apps are launched on this machine; a web site is served on this machine and opened in the browser. 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.
#macOS
nsh package App.tsx --target macos --run
Writes out/<Name>.app: the executable in Contents/MacOS/, and nsh/bundle.nshc, nsh/assets/… and nsh/app.json
(the window title and the app’s appearance) in Contents/Resources/. The app is signed ad hoc and needs macOS 14 or
later; to distribute it, sign it with a Developer ID and notarize it. See macOS.
#Linux
nsh package App.tsx --target linux --run
Writes out/<Name>/ (the executable <Name>, nsh/bundle.nshc, nsh/assets/…, nsh/app.json with the window title
and the application id, <id>.desktop and the icon <id>.png) and the same as out/<Name>.tar.gz. Package on
Linux: the runner is built for the machine it is built on. GTK 4 (4.14 or later), Pango and WebKitGTK 6.0 (for web
views) must be installed where the app runs. Installing it and the desktop entry: Linux.
#Windows
nsh package App.tsx --target windows --run
Writes out\<Name>\ (the executable <Name>.exe with the app’s icon, nsh\bundle.nshc, nsh\assets\… and
nsh\app.json with the window title, the app id, the version and the appearance) and the same as out\<Name>.zip.
No tools are needed. Package on Windows: the runner is built for the machine it is built on, and only Windows can
embed the icon. The app runs from wherever its directory is copied; nothing is installed. See
Windows.
#GPUI
nsh package App.tsx --target gpui --run
Writes what --target macos, --target windows or --target linux writes on this machine, with GPUI’s release
runner (nsh-gpui-app) as the executable instead of the native backend’s. Package on the OS the app is for: the
runner is built for the machine it is built on. See GPUI.
A Linux GPUI package links WebKitGTK 4.1 (web views) and GStreamer (videos) dynamically, so the machines it runs on need them installed, with GTK 3 and GStreamer’s base and good plug-ins (Debian / Ubuntu: libwebkit2gtk-4.1-0, libgstreamer1.0-0, libgstreamer-plugins-base1.0-0, gstreamer1.0-plugins-good), besides a Vulkan driver and libxkbcommon.
#Web
nsh package App.tsx --target web --run
Writes out/<Name>/, a static site: index.html, manifest.webmanifest, and the runtime (nsh.<digest>.js,
nsh.<digest>.wasm), the app (app.<digest>.js, its JS source), its assets and icons, each named after its content
so hosts can cache them for good. Deploy the directory to any web server or CDN; --run serves it on
http://127.0.0.1:4173/ and opens the browser. Cache headers and embedding: Web.
#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). - macOS: converted by
sipstoAppIcon.icns. - Linux: copied as
<id>.png, which the.desktopfile names as itsIcon. - Windows: resized to 16 to 64 pixels and 256 pixels and embedded in the executable as its icon, which the window, Explorer and the taskbar show.
- Web: resized to 32 pixels (the favicon), 180 (
apple-touch-icon), 192 and 512 (the web app manifest).
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.nshc, nsh/assets/… |
| Android | classes.dex, lib/<abi>/libnsh.so (stored uncompressed, 16 KB aligned) | assets/nsh/bundle.nshc, assets/nsh/assets/… |
| macOS | the nsh-macos-app executable, renamed <Name> | <Name>.app/Contents/Resources/nsh/bundle.nshc, nsh/assets/… |
| Linux | the nsh-linux-app executable, renamed <Name> | <Name>/nsh/bundle.nshc, nsh/assets/… |
| Windows | the nsh-windows-app.exe executable, renamed <Name>.exe | <Name>\nsh\bundle.nshc, nsh\assets\… |
| GPUI | the nsh-gpui-app executable, renamed as on this machine’s OS | as on this machine’s OS |
| Web | nsh.<digest>.wasm, nsh.<digest>.js | <Name>/app.<digest>.js (JS source), assets/…, index.html |
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); nsh-macos-app (on macOS), nsh-linux-app (on Linux), nsh-windows-app (on Windows) and nsh-gpui-app (on any of them) are built for this
machine in target/<profile> (cargo build -p nsh-macos-app, cargo build -p nsh-linux-app, cargo build -p nsh-windows-app, cargo build --manifest-path crates/gpui/Cargo.toml --target-dir target -p nsh-gpui-app); the web runtime is built with the web profile whatever the CLI’s, into target/wasm32-unknown-unknown/web/. The environment variable NSH_RUNTIME
points the CLI at another directory laid out like the distribution’s runtime/ (ios/iphoneos,
ios/iphonesimulator, android/release, macos, linux, windows, gpui, web, …).
#See also
- CLI: every command
- Signed packages: updates apps download
- Project structure:
nsh.jsonand the files of an app